본문 바로가기

[Android][Kotlin] Dagger2 #4 - context 주입방법 @BindsInstance @Component.Builder @Component.Factory

Context 주입방법 @BindsInstance, Builder / Factory

 

 

Dagger 관련 글


 

context는 SharedPreferences 사용 또는 특정 퍼미션의 기능 호출에 사용 등 여러 방면에 많이 사용되는데

context 객체는 직접 new생성자로 만들 수 있는게 아니라, 안드로이드 시스템에서 만들어주는 객체이기 때문에 

클래스에서 context를 사용하기 위해서는 이 값을 파라메터를 통해 전달받아야 합니다

context를 dagger2 그래프에 주입하는 방법을 찾아본 결과 3가지로 나눠서 보겠습니다

 

Context 주입방법 3가지

  1. 생성자 인수가있는 Module
  2. @Component.Builder
  3. @Component.Factory

 

생성자 인수가 있는 Module 

이 방법은 단순히 생성자 인수Module에 전달하는 방법입니다 

AppModule.kt
@Module
class AppModule(val context: Context) {

    @Provides
    fun providecontext() : Context = context
}

AppModule 생성자를 보면 context 파라메터가 존재합니다.

그리고 의존성 주입 메서드인 provideContext()는 AppModule의 프로퍼티인 context를 반환하는 함수입니다

AppComponent.interface
@Component(modules = [AppModule::class])
interface AppComponent {
    ...

    @Component.Builder
    interface Builder {
        fun appModule(appModule: AppModule) : Builder
        
        fun build() : AppComponent
    }
}

AppComponent.Builder를 통해 appModule을 파라메터로 받아 초기화 할 수 있습니다

MainActivity.kt
DaggerAppComponent.builder()
    .appModule(AppModule(this))
    .build()

appModule을 초기화하고 build() 메서드로 Component 인스턴스를 반환받습니다

문제점 :

Module 생성자를 통한 방법은 간단하지만, Module을 추상화할 수 없다는 접근방식의 문제점이 있습니다
(Dagger는 모듈이 상태를 가지면 안되고, 더 나은 성능을 위해 Module 메서드를 정적으로 사용간으 하도록 권장) 

 

@Component.Builder

Dagger2.12에는 생성자에 인수를 전달하여 1번의 방법과 동일한 작업을 수행하기 위해 @Component.Builder@BindsInstance 두 가지의 주석이 추가되었고, 이제 Module에 초기화를 하지 않습니다 

AppComponent.interface
@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를 자유롭게 사용할 수 있게됩니다

AppModule.kt
@Module
class AppModule {

    @Provides
    fun provideTestClass(context: Context): TestClass = TestClass(context)
}

TestClass에 context 파라메타가 필요한 경우 AppComponent에 설정된 AppModule에서는 context를 위 코드처럼 자유롭게 사용이 가능합니다
( AppComponent가 context 인스턴스를 갖고있기 때문에 연결된 Module에서 자유롭게 사용이 가능 ) 

 

Builder.interface 사용 시 규칙

  1. Builder에는 Component 또는 Component의 super 유형을 리턴하는 메소드가 하나 이상 있어야합니다
    (여기서는 AppComponent 인스턴스를 반환하는 build() 함수)
  2. 인스턴스를 바인드하는데 사용되는 build() 메서드 이외의 메서드는 Builder를 리턴해야 합니다  
    (여기서는 application() 메서드로 return Type이 Builder)
  3. 종속성 바인딩에 사용되는 메서드에는 둘 이상의 매개변수가 없어야 합니다.
    더 많은 종속성을 바인드하려면 각 종속성마다 다른 메서드를 추가로 생성해줘야 합니다
    (즉, Builder를 반환하는 종속성 바인딩 메서드는 매개변수가 꼭 하나로만 존재)

 

아래는 DaggerComponent를 생성하는 방법입니다 

MainActivity.kt
DaggerAppComponent
    .builder()
    .application(this)
    .build()

 

Builder 메서드인 application()의 파라메터로 @BindsInstance 주석이 붙은 context에 전달해주면, 이제 dagger를 통해 이 context를 어디서나 참조할 수 있게됩니다

생성자 Module보다 나은 점

더 이상 생성자 인수를 전달할 필요가 없어지므로, Module은 상태를 저장하지 않는 구현이 되고, 모든 정적 메서드를 포함할 수 있게됩니다

하지만 이 @Component.Builder를 사용하는 방법도 문제점이 있습니다

문제점

성능 면에서 문제는 없지만, 많은 Instance를 Component 요소에 바인딩하려면 긴 메서드 체인을 만들고, 만약 체인에서 메서드 호출을 빠트린다면 런타임 예외가 발생합니다  

3번 조건을 예시로 context, age, name의 종속성을 바인딩하려고 한다면 아래와 같습니다

AppComponent.interface
@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로 설정해서 메서드 체이닝을 구현해야합니다

사용방법은 아래와 같습니다

MainActivity.kt
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를 메서드에 사용하지 않고, 각 매개변수에 사용한다는 점)

 

AppComponent.interface
@Component(modules = [AppModule::class])
interface AppComponent {
    ...

    @Component.Factory
    interface Factory {
    
        fun create(@BindsInstance context: Context) : AppComponent
    }
}

Builder 대신 Factory를 사용하면 create() 메서드 하나로 context를 Component 요소에 바인딩까지 하고 Component 인스턴스를 반환하는 것이 가능합니다 

MainActivity.kt
DaggerAppComponent
    .factory()
    .create(this)

Builder와 다르게 메서드 체이닝을 구현하지 않아도 Component에 @BindsInstance가 달린 요소를 등록할 수 있습니다

 

Builder처럼 Factory도 규칙이 존재

  1. Factory는 둘 이상의 create() 메서드를 선언할 수 없습니다
    많은 종속성을 요소에 바인딩하려면 Builder처럼 각 종속성에 대한 메서드를 추가로 만들지 않고,
    create() 메서드에 각 종속성에 대한 새 매개변수를 추가하면 됩니다
  2. create() 메서드는 Component 또는 super 인스턴스를 리턴해야합니다

Builder에서 처럼 Component에 3개의 요소(context, age, name)를 바인딩하려면 아래와 같습니다

AppComponent.interface
Component(modules = [AppModule::class])
interface AppComponent {
    ...

    @Component.Factory
    interface Factory {
    
        fun create(
            @BindsInstance context: Context,
            @BindsInstance age: Int,
            @BindsInstance name: String
        ): AppComponent
    }
}

Factory 내부 create() 메서드 하나에 바인딩할 요소를 매개변수로 추가해주면 됩니다

MainActivity.kt
DaggerAppComponent
    .factory()
    .create(this, 22, "홍길동")

Builder에는 각 요소들을 바인딩하기 위해 메서드 체이닝으로 긴 체인이 발생하였는데, Factory에서는 create() 메서드 하나로 모두 바인딩이 가능하게 됩니다

Builder보다 나은 점

  • 의존성에 대해 메소드를 새로 추가하지 않기 때문에, Component 생성 시 긴 체인이 없으며 메서드 수는 모든 경우에 동일하게 유지됩니다 (create() 하나로 구성)
  • 각 종속성은 함수에 매개변수를 추가하는 방식으로, 종속성을 전달하지 않은 경우에는 런타임 예외가 아닌 컴파일 시간에 오류가 발생합니다 (빠른 오류처리)