Context 주입방법 @BindsInstance, Builder / Factory
Dagger 관련 글
- DI (Dependency Injection) - 의존성 주입
- Dagger2 #1 - 기본 개념
- Dagger2 #2 - Scope / Binds / MultiBinding
- Dagger2 #3 - @Named @Qualifier
- Dagger2 #4 - context 주입방법 @BindsInstance @Component.Builder @Component.Factory (현재 글)
- Dagger2 #5 - Android Dagger 사용방법 3가지
: HasAndroidInjector, DaggerApplication, DispatchingAndroidInjector - Dagger2 #6 - Retrofit + DaggerApplication Factory 예시
context는 SharedPreferences 사용 또는 특정 퍼미션의 기능 호출에 사용 등 여러 방면에 많이 사용되는데
context 객체는 직접 new생성자로 만들 수 있는게 아니라, 안드로이드 시스템에서 만들어주는 객체이기 때문에
클래스에서 context를 사용하기 위해서는 이 값을 파라메터를 통해 전달받아야 합니다
context를 dagger2 그래프에 주입하는 방법을 찾아본 결과 3가지로 나눠서 보겠습니다
Context 주입방법 3가지
- 생성자 인수가있는 Module
- @Component.Builder
- @Component.Factory
생성자 인수가 있는 Module
이 방법은 단순히 생성자 인수를 Module에 전달하는 방법입니다
@Module
class AppModule(val context: Context) {
@Provides
fun providecontext() : Context = context
}
AppModule 생성자를 보면 context 파라메터가 존재합니다.
그리고 의존성 주입 메서드인 provideContext()는 AppModule의 프로퍼티인 context를 반환하는 함수입니다
@Component(modules = [AppModule::class])
interface AppComponent {
...
@Component.Builder
interface Builder {
fun appModule(appModule: AppModule) : Builder
fun build() : AppComponent
}
}
AppComponent.Builder를 통해 appModule을 파라메터로 받아 초기화 할 수 있습니다
DaggerAppComponent.builder()
.appModule(AppModule(this))
.build()
appModule을 초기화하고 build() 메서드로 Component 인스턴스를 반환받습니다
문제점 :
Module 생성자를 통한 방법은 간단하지만, Module을 추상화할 수 없다는 접근방식의 문제점이 있습니다
(Dagger는 모듈이 상태를 가지면 안되고, 더 나은 성능을 위해 Module 메서드를 정적으로 사용간으 하도록 권장)
@Component.Builder
Dagger2.12에는 생성자에 인수를 전달하여 1번의 방법과 동일한 작업을 수행하기 위해 @Component.Builder 와 @BindsInstance 두 가지의 주석이 추가되었고, 이제 Module에 초기화를 하지 않습니다
@Component(modules = [AppModule::class])
interface AppComponent {
...
@Component.Builder
interface Builder {
@BindsInstance
fun application(context: Context): Builder
fun build(): AppComponent
}
}
@BindsInstance는 인스턴스를 구성요소에 바인딩합니다. 이 작업을 통해 Dagger 그래프에 context를 추가되고 이제 어디에서나 dagger를 통해 context를 얻을 수 있게됩니다
@BindsInstance 주석으로 AppComponent가 context를 요소로 갖게되었고, 이 Component에 설정된 module에서는 context를 자유롭게 사용할 수 있게됩니다
@Module
class AppModule {
@Provides
fun provideTestClass(context: Context): TestClass = TestClass(context)
}
TestClass에 context 파라메타가 필요한 경우 AppComponent에 설정된 AppModule에서는 context를 위 코드처럼 자유롭게 사용이 가능합니다
( AppComponent가 context 인스턴스를 갖고있기 때문에 연결된 Module에서 자유롭게 사용이 가능 )
Builder.interface 사용 시 규칙
- Builder에는 Component 또는 Component의 super 유형을 리턴하는 메소드가 하나 이상 있어야합니다
(여기서는 AppComponent 인스턴스를 반환하는 build() 함수) - 인스턴스를 바인드하는데 사용되는 build() 메서드 이외의 메서드는 Builder를 리턴해야 합니다
(여기서는 application() 메서드로 return Type이 Builder) - 종속성 바인딩에 사용되는 메서드에는 둘 이상의 매개변수가 없어야 합니다.
더 많은 종속성을 바인드하려면 각 종속성마다 다른 메서드를 추가로 생성해줘야 합니다
(즉, Builder를 반환하는 종속성 바인딩 메서드는 매개변수가 꼭 하나로만 존재)
아래는 DaggerComponent를 생성하는 방법입니다
DaggerAppComponent
.builder()
.application(this)
.build()
Builder 메서드인 application()의 파라메터로 @BindsInstance 주석이 붙은 context에 전달해주면, 이제 dagger를 통해 이 context를 어디서나 참조할 수 있게됩니다
생성자 Module보다 나은 점
더 이상 생성자 인수를 전달할 필요가 없어지므로, Module은 상태를 저장하지 않는 구현이 되고, 모든 정적 메서드를 포함할 수 있게됩니다
하지만 이 @Component.Builder를 사용하는 방법도 문제점이 있습니다
문제점
성능 면에서 문제는 없지만, 많은 Instance를 Component 요소에 바인딩하려면 긴 메서드 체인을 만들고, 만약 체인에서 메서드 호출을 빠트린다면 런타임 예외가 발생합니다
3번 조건을 예시로 context, age, name의 종속성을 바인딩하려고 한다면 아래와 같습니다
@Component(modules = [AppModule::class])
interface AppComponent {
...
@Component.Builder
interface Builder {
// context 종속성 바인딩 함수
@BindsInstance
fun application(context: Context): Builder
// age 종속성 바인딩 함수
@BindsInstance
fun age(age: Int): Builder
// name 종속성 바인딩 함수
@BindsInstance
fun name(name: String): Builder
fun build(): AppComponent
}
}
이렇게 종속성 바인딩 할 갯수만큼 메서드가 늘어나고, 반환Type을 Builder로 설정해서 메서드 체이닝을 구현해야합니다
사용방법은 아래와 같습니다
DaggerAppComponent
.builder()
.application(this)
.age(20)
.name("홍길동")
.build()
application() 메서드로 context만 Component 요소에 바인딩할 때 보다 2개의 메서드가 추가되었습니다(age() name())
이렇게 Builder를 반환함으로써 메서드체이닝 구현으로 Component 인스턴스 생성 시 코드가 길어지게 됩니다
@Component.Factory
Dagger 2.22에서는 @Component.Builder의 문제점을 해결하기 위해 @Component.Factory가 추가되었습니다
Builder와 다른 점은 Factory는 각 매개변수에 @BindsInstance annotation을 붙이면서 Component 요소에 바인딩을 create() 메서드 하나로 구현이 가능하게 되었습니다
(@BindsInstance를 메서드에 사용하지 않고, 각 매개변수에 사용한다는 점)
@Component(modules = [AppModule::class])
interface AppComponent {
...
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context) : AppComponent
}
}
Builder 대신 Factory를 사용하면 create() 메서드 하나로 context를 Component 요소에 바인딩까지 하고 Component 인스턴스를 반환하는 것이 가능합니다
DaggerAppComponent
.factory()
.create(this)
Builder와 다르게 메서드 체이닝을 구현하지 않아도 Component에 @BindsInstance가 달린 요소를 등록할 수 있습니다
Builder처럼 Factory도 규칙이 존재
- Factory는 둘 이상의 create() 메서드를 선언할 수 없습니다
많은 종속성을 요소에 바인딩하려면 Builder처럼 각 종속성에 대한 메서드를 추가로 만들지 않고,
create() 메서드에 각 종속성에 대한 새 매개변수를 추가하면 됩니다 - create() 메서드는 Component 또는 super 인스턴스를 리턴해야합니다
Builder에서 처럼 Component에 3개의 요소(context, age, name)를 바인딩하려면 아래와 같습니다
Component(modules = [AppModule::class])
interface AppComponent {
...
@Component.Factory
interface Factory {
fun create(
@BindsInstance context: Context,
@BindsInstance age: Int,
@BindsInstance name: String
): AppComponent
}
}
Factory 내부 create() 메서드 하나에 바인딩할 요소를 매개변수로 추가해주면 됩니다
DaggerAppComponent
.factory()
.create(this, 22, "홍길동")
Builder에는 각 요소들을 바인딩하기 위해 메서드 체이닝으로 긴 체인이 발생하였는데, Factory에서는 create() 메서드 하나로 모두 바인딩이 가능하게 됩니다
Builder보다 나은 점
- 각 의존성에 대해 메소드를 새로 추가하지 않기 때문에, Component 생성 시 긴 체인이 없으며 메서드 수는 모든 경우에 동일하게 유지됩니다 (create() 하나로 구성)
- 각 종속성은 함수에 매개변수를 추가하는 방식으로, 종속성을 전달하지 않은 경우에는 런타임 예외가 아닌 컴파일 시간에 오류가 발생합니다 (빠른 오류처리)