의존성이란, 클래스가 필요로 하는 다른 객체 인스턴스를 의미한다. 의존성 주입은 말 그대로 클래스 내에서 객체 인스턴스의 생성을 직접 하지않고, 외부에서 생성하여 필요한 곳에 주입되도록 코드를 조직화 하는 방법이다.
의존성 주입의 이점
코드를 재사용하게 해준다.
- 객체 생성 방법이 변경되더라도 클라이언트의 코드에 영향을 주지 않는다. (클라이언트는 의존성 주입을 받는 클래스)
코드 테스트를 더 용이하게 한다.
- 테스트를 위해서, Fake 저장소를 주입하는 방식과 같이 쉽게 테스트가 가능하다.
클라이언트는 인터페이스로 객체를 알고 있으면 되므로, 좋은 설계를 촉진한다.
- 클래스의 추상화를 더 잘 할 수 있도록 하며, 멀티 모듈에서 모듈간의 의존성을 소거하는데 유용하다.
DI(의존성 주입) 프레임 워크의 이점
- 프레임워크를 사용하지 않는 의존성 주입 과정에서 많은 보일러 플레이트 코드가 발생하며, 프로젝트가 확장될 수록 의존성 주입 코드의 유지보수가 어려워 오히려 의존성 주입이 확장을 어렵게 만들 수 있다.
- DI 프레임 워크들은 멀티 모듈을 구성을 쉽게 하도록 도와준다. (주입을 통해 모듈간 의존성 제거)
- DI 프레임워크는 보통 의존성 정의를 위한 여러 단계의 계층을 제공한다 -> 언제 파괴되고 언제까지 살아있는지 (라이프 사이클 처리) / 의존성 정의를 쉽게 유지보수하고 더 작은 단위로 쪼갤 수 있음.
DI 프레임워크 : Dagger
코드 생성 기반의 의존성 주입 프레임워크로, 안드로이드 표준 프레임워크이다. Annotation processor 기반 코드 생성을 이용하여 구현한다. Annotation을 이용하며, 모든 구현이 코드 생성 기반이므로, 빌드 시간에 오버헤드가 발생하나 코드 생성 기반이므로 실행 속도가 빠르다는 장점이 있다.
Dagger 구현
1. gradle 설정
plugins {
id 'kotlin-kapt'
}
dependencies {
implementation "com.google.dagger:dagger:$dagger_version"
kapt "com.google.dagger:dagger-compiler:$dagger_version"
}
현재 dagger version을 확인한 후 build.gradle에 추가한다.
2. 컴포넌트 구현
@Component
interface ApplicationComponent{
fun inject(activity : MainActivity)
}
컴포넌트를 구현하면, 컴포넌트를 토대로 Injector 가 자동 생성된다. Inject 메소드를 생성하고, 어디에 어떻게 주입되는 지를 작성한다. (Injector 클래스는 주입되는 모든 타입들을 위한 생성 방법을 정의한 클래스를 의미한다.)
3. Application, MaiinActivity 초기화
class MyApplication : Application(){
val appComponent = DaggerApplicationComponet.create()
}
class MainActivity : ComponentActivity(){
override fun onCreate(savedInstanceState: Bundle?){
(applicationContext as MyApplication).appComponent.inject(this)
super.onCreate(savedInstanceState)
}
}
ApplicationComponent를 구현하면, Dagger가 붙은 클래스(DaggerApplicationComponent)가 자동으로 생성된다. Activity의 경우 onCreate() 메소드 안에서 주입을 선언한다. Activity에서 주입은 ApplicationComponent를 통해 자동으로 주입된다.
4. 바인딩 구현
class Foo @Inejct constructor(){
init{
}
}
@Inject 생성자 활용하여, 주입할 클래스를 선언한다.
5. 주입 구현
class MainActivity : ComponentAcitivty(){
@Inject lateinit var foo : Foo
}
@Inject가 붙은 필드에 자동으로 ApplicationComponent를 통해 주입된다. @Inject는 클래스의 생성자에 추가하여 주입할 인스턴스를 정의할 수도 있으나, 어디에 주입이 되는지 설정할 수도 있다.
Dagger의 주요 개념 정리
Component
Dagger는 컴포넌트 인터페이스를 상속받아, 주입되는 클래스의 생성방법을 정의한 Inject 클래스를 생성한다.
컴포넌트는 interface와 abstract class로 정의될 수 있다.
컴포넌트의 메소드의 형태로 객체 인스턴스를 얻을 수 있도록 한다.
Bindings
각 객체의 생성이 실제로 정의되는 곳이다.
원하는 클래스가 key이고, 클래스를 생성하는 방법을 value로 설정한 해쉬 테이블과 유사한 개념으로 key를 통해 호출하면 value가 따라오는 개념이다.
@Provides
fun provideNewsRemoteDataSource(newsApi: NewsApi): NewsRemoteDataSource =
NewsRemoteDataSource(newsApi)
// 리턴 타입이 뭔지가 중요함.
@Provides
fun provideNewsApi(newsApi: ProdNewsApi): NewsApi = newsApi
NewsRemoteDataSource를 얻고자하는 @inject이 붙어 있으면, 해당 생성자가 내부적으로 호출된다.
- 함수의 매개변수가 있는 경우 주의 : 매개 변수도 또 다른 바인딩이 있다면 해당 바인딩을 검색하여 주입하고, 아니면 컴파일 에러가 발생
@Inject
// 1
@Provides
fun provideNewsRemoteDataSource(newsApi: NewsApi): NewsRemoteDataSource {
return NewsRemoteDataSource(newsApi)
}
// 2
class NewsRemoteDataSource @Inject constructor(newsApi: NewsApi) {
}
1번과 2번의 경우 같은 역할을 하는 코드, 그러나 2번의 경우는 인터페이스에서는 사용이 불가능하다.
Module
주입되는 바인딩을 모은 것으로, 모듈은 컴포넌트에 설치된다.
하나의 모듈이 여러 개의 컴포넌트에 설치될 수 없다.
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
...
}
Multi Component
특정 범위 (Activity, Fragment와 같은) 안에서만 이뤄지는 바인딩이 필요한 경우, SubComponent를 사용한다.
서브 컴포넌트는 다른 컴포넌트의 자식이며, 부모의 모든 바인딩을 상속받는다.
@Subcomponent(modules = [ClickActivityModule::class])
interface ActivityComponent {
fun clickManager(): ClickManager
}
안드로이드에서의 의존성 주입
Dagger의 경우 안드로이드 전용 DI 프레임워크는 아니다. 안드로이드에서 자주 사용되는 프레임 워크로 해당 Dagger를 경량화한 Hilt를 사용하기 위해서는 Dagger의 이해가 필수적이다. 필자의 경우 Dagger보다 Hilt를 먼저 접했었고, Hilt를 적용하는 과정은 알고 있었으나, 이가 내부적으로 어떻게 수행이 되는지 각 어노테이션이 무엇을 의미하는지 이해를 하지 못했었다.