본문 바로가기

[Android][Kotlin] Dagger2 #2 - Scope / Binds / MultiBinding

Dagger2 #2 - Scope / Binds / MultiBinding

 

 

Dagger 관련 글


 

지난 포스트에 이어서 Dagger2의 중요한 기능들을 정리한 포스트입니다 

 

추가 기능

  • SubComponent
  • Scope
  • @Binds
  • MultiBinding

 

SubComponent

dagger2에서는 SubComponent를 생성할 수 있습니다.

SubComponent는 말 그대로 부모 Component가 있는 자식 Component라고 보시면 됩니다.

Component는 SubComponent로 계층관계를 만들 수 있습니다. inner Class 방식의 하위계층 Component를 의미하고
연속된 Sub의 Sub도 구현이 가능합니다. Subcomponent는 Dagger의 중요한 컨셉인 그래프를 형성하는 역할입니다

Inject로 의존성 주입을 요청받으면 SubComponent에서 먼저 의존성을 검색하고, 없으면 부모로 올라가면서 검색합니다

SubComponent는 Component와 달리 코드 생성은 부모 Component에서 이루어 집니다.

SubComponent는 Component와 마찬가지로 interface 또는 abstract class에 @SubComponent 에노테이션으로 생성

 

Socpe

dagger2는 Scope를 지원합니다. Component별로 @Scope annotation으로 주입되는 객체들을 관리합니다

생성된 객체의 Lifecycle 범위입니다. 안드로이드는 주로 PerActivity, PerFragment 등으로 하면의 생명주기와 맞춰서 사용합니다.

Module에서 Scope를 보고 객체를 관리하는 방식입니다

* @Singleton과 비슷하게 개체의 단일 인스턴스를 유지하지만, @Singleton과 다른점은 전체 application이 아닌 구성요소의 lifecycle(수명주기)와 관련이 있다는 차이점

Scoping rules

  1. type에 scope 어노테이션을 붙일 때는 오직 같은 scope가 붙은 Components에 의해서만 사용이 가능합니다
  2. Components에 scope 어노테이션을 붙이면 scope 어노테이션이 없는 type이나 동일한 scope 어노테이션을 붙인 타입만 제공할 수 있습니다
  3. Subcomponent는 parentComponent 들에서 사용중인 어노테이션을 사용할 수 없습니다 

 

@Binds

Module에서 제공하고자 하는 클래스의 구현체(interface 구현체, 즉 객체)를 바인딩하고자 할 때 사용합니다

 

MultiBinding

dagger2에서는 set이나 map을 이용한 MultiBinding을 지원

set - @IntoSet / map - @IntoMap / Key - @StringKey("key")

 

지난 포스트에서 CoffeeApp을 만들었습니다.

이번 포스트에서는 추가로 CafeApp을 구현하겠습니다. CafeApp은 CafeInfo CoffeeMaker로 구성되어 있습니다

CafeApp

CafeApp은 CafeInfo, CoffeeMaker로 구성되어 있습니다

CafeInfo - 단순히 카페이름을 가지고 있는 클래스

CoffeMaker - Heater 객체로 구성되어있고, CoffeeBeen을 받아 커피를 내릴 수 있는 기능

CafeInfo.class

CafeInfo.class
class CafeInfo(private val name: String = "") {
    fun welcome() { Log.d("Dagger", "Welcome ${name}")}
}
CoffeeMaker.class
class CoffeeMaker @Inject constructor(private var heater: Heater) {

    fun brew(coffeeBean: CoffeeBean) {
        Log.d("Dagger", "CoffeeBean(${coffeeBean}) [_}P coffee ! [_}P ")
    }
}

 

CoffeeBean.class
class CoffeeBean {
    var name = Log.d("Dagger", "CoffeeBean")
}
CafeComponent.interface
@Component(modules = [CafeModule::class])
interface CafeComponent {

    fun cafeInfo() : CafeInfo

    fun coffeeMaker() : CoffeeMaker
}
CafeModule.class
@Module
class CafeModule {
    @Provides
    fun provideCafeInfo(): CafeInfo {
        return CafeInfo()
    }

    @Provides
    fun provideCoffeeMaker(heater: Heater): CoffeeMaker {
        return CoffeeMaker(heater)
    }

    fun provideHeater() : Heater {
        return A_Heater()
    }
}

 

기본구조는 생성하였습니다. 이제 CafeApp의 조건에 맞춰 구성을 변경하겠습니다

CafeApp 조건

// CafeInfo, @SingleTon
1. 카페에 CafeInfo는 "하나뿐이며" 누가 보던지 "같은 결과"를 보여줘야합니다.
  그리고, 카페가 망하기 전까지 "CafeInfo는 동일합니다"

2. Cafe가 망하면 CafeMaker도 없어집니다
  하지만, Cafe가 망하지 않아도 CafeMaker를 새로 만들거나 교체할 수 있습니다.

// Heater, @SingleTon + @CoffeeScope
3. CafeMaker 생성 시 들어가는 Heater는 같은 CoffeeMaker에는 "항상 같은 Heater"가 들어갑니다.	

SubComponent와 Scope를 이용해서 Dagger에서 객체들을 관리할 수 있습니다

 

Java에서 기본제공하는 @SingleTon@CoffeeScope 커스텀 annotation을 활용해서 조건을 구현하겠습니다

 

Scope

먼저 조건 1번에서 'CafeInfo는 하나뿐이다' 라고 하였습니다. 즉 CafeInfo는 SingleTon으로 구현해야함을 의미합니다.

ComponentScope annotation을 사용하면 해당 Component에 Binding되는 객체들은 해당 Component와 같은 Lifecycle을 갖게 됩니다

CafeComponent.interface
@Singleton	// CafeComponent 에 @SingleTon Scope 설정
@Component(modules = [
    CafeModule::class		// CafeModule에 Component와 동일 Scope 사용 시 같은 Lifecycle 적용
])
interface CafeComponent {

    fun cafeInfo() : CafeInfo	// Module의 동일 리턴Type의 메서드와 바인딩

    fun coffeeMaker() : CoffeeMaker	// Module의 동일 리턴Type의 메서드와 바인딩
}
CafeModoule.class
@Module
class CafeModule {
    // 1번 조건, 싱글톤(SingleTon) 설정 - 연결된 Component와 동일한 LifeCycle을 같게 됩니다
    @Singleton
    @Provides
    fun provideCafeInfo(): CafeInfo {
        return CafeInfo()
    }

    @Provides
    fun provideCoffeeMaker(heater: Heater): CoffeeMaker {
        return CoffeeMaker(heater)
    }

    @Provides
    fun provideHeater() : Heater {
        return A_Heater()
    }
}


위와 같이 Component에 @SingleTon annotation를 붙이고 Module의 CafeInfo를 객체를 제공하는 메서드에 @SingleTon annotation을 붙이면 같은 CafeComponent로 CafeInfo 객체를 호출 시 싱글톤(SingleTon)으로 구현된 CafeInfo 객체가 주입됩니다.

하지만 CafeComponent로 @SingleTon이 없는 CafeMaker를 호출할 경우에는 매번 다른 CafeMaker 객체가 주입됩니다

var cafeComponent = DaggerCafeComponent.create()

var cafeInfo1 :CafeInfo = cafeComponent.cafeInfo()
var cafeInfo2 :CafeInfo = cafeComponent.cafeInfo()

var coffeeMaker1 : CoffeeMaker = cafeComponent.coffeeMaker()
var coffeeMaker2 : CoffeeMaker = cafeComponent.coffeeMaker()


cafeInfo1 과 cafeInfo2는 싱글톤(SingleTon)으로 동일한 객체가 주입되지만,

coffeeMaker1 과 coffeeMaker2는 서로 다른 객체가 주입됩니다

Cafe(CafeComponent)와 CafeInfo의 Scope를 맞췄습니다. 다음으로 CoffeeMaker와 Header의 Scope를 맞추겠습니다

먼저 CoffeeMaker의 Scope를 정의하기 위해 CoffeeMaker를 관리하는 CoffeeComponent를 정의하고 CoffeeComponent는 @CoffeeScope라는 커스텀 Scope를 정의하겠습니다

커스텀 Scope 선언 방법

Dagger는 개발자가 Custom Scope를 만들 수 있습니다

Kotlin : annotation class로 설정 (@Retention - RUNTIME, 런타임 시 적용)

scope.kt
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class CoffeeScope {}

Java : 주의할 점은 @interface Type 설정 필요

@Scope
@Retention(AnnotationRetention.RUNTIME)
public @interface [커스텀 Scope이름] {}

여기선 CoffeeScope 라는 Custom scope를 생성해서 사용합니다. 하지만 이 방법은 Dagger에서 추천하지 않는 방법입니다. scope 어노테이션의 이름은 목적을 나타내는 이름보다는 siblings Component들도 재사용할 수 있는 lifeTime에 의존적으로 이름을 짓는걸 권장합니다 @ActivityScope @FragmentScope 등 다른 Component들도 재사용할 수 있도록 

 

Subcomponent

SubComponent는 부모 Component를 갖고 있는 Component입니다

@SubComponent에서 Builder Interface를 정의 해주어야 부모 Component에서 코드가 생성됩니다

Dagger는 Component 생성 시 builder를 사용합니다.

@Subcomponent는 @Component 클래스 안에서 코드가 생성될 때 @SubComponent.Builder annotation이 붙은 interface가 없으면 builder가 자동으로 생성되지 않습니다.

즉, 부모Component에 SubComponent 구현을 위해선 @Subcomponent.Builder 인터페이스를 꼭 정의해야 합니다
Coffeecomponent.interface
@CoffeeScope    // CoffeeMaker와 Heater의 범위를 맞추기위해 Scope 설정
@Subcomponent(modules = [
    CoffeeModule::class
])
interface Coffeecomponent {

    fun coffeeMaker() : CoffeeMaker

    fun coffeeBean() : CoffeeBean

    @Subcomponent.Builder
    interface Builder {
        fun build() : Coffeecomponent
    }
}

CoffeeComponent를 통해 CoffeeMaker를 얻을 수 있고, Scope 비교를 위해서 CoffeeBean도 얻을 수 있도록 추가합니다

CoffeeMaker가 Cafe안에 속해있기 때문에 Component 관계 역시 부모-자식 관계를 설정해줘야 합니다

(Cafe 없이는 CoffeeMaker에 접근할 수 없도록 Encapsulate 하고 Scope 역시 넓은 범위에서 좁은 범위로 진행되게 하기 위해서도 부모-자식 개념의 설정은 필요)

 

그럼 Component하고 SubComponent를 연결하는 방법은 부모Component의 Module을 통해서 연결합니다 

부모 Component에 설정된 모듈인 CafeModule의 @Module의 속성으로 subcomponents=[SubComponentName::class]

방식으로 부모-자식 Component 연결을 합니다

CafeModule.class
@Module(subcomponents = [Coffeecomponent::class])	// 부모-자식 Component 관계 설정
class CafeModule {

    @Singleton      // CafeInfo, 싱글톤(SingleTon) 설정 : 1번 조건
    @Provides
    fun provideCafeInfo(): CafeInfo {
        return CafeInfo()
    }

}

부모 Component에 설정된 모듈인 CafeModule에 SubComponent 관계를 설정하였습니다

그리고 기존에 CafeModule에 있던 CoffeeMaker Heater 객체를 주입하는 Provider메서드는 삭제합니다

(SubComponent에서 CoffeeMaker / Heater를 제공하기 때문에)

이제 이 CafeModule을 가지는 Component가 부모가 되고 CoffeeComponent는 자식 컴포넌트가 성립됩니다

 

CoffeeModule.class
@Module
class CoffeeModule {

    @CoffeeScope    // 조건 3 - CoffeeMaker와 Heater의 범위를 같게 하기위해 Scope 설정
    @Provides
    fun provideCoffeeMaker(heater: Heater) : CoffeeMaker {
        return CoffeeMaker(heater)
    }

    @CoffeeScope    // 조건 3 - CoffeeMaker와 Heater의 범위를 같게 하기위해 Scope 설정
    @Provides
    fun provideHeater() : Heater {
        return A_Heater()
    }

    @Provides       // CoffeeBean은 커피를 만들 때마다 소모되기 때문에 Scope 설정 X
    fun provideCoffeeBean() : CoffeeBean {
        return CoffeeBean()
    }
}

SubComponent에 설정된 CoffeeModule 클래스입니다

여기서 CoffeeMaker / Heater / CoffeeBeen에 대한 Provider 메서드를 작성하고, 조건 3번을 위한 설정을 합니다

조건 3 : "CoffeeMaker"에 들어가는 Heater는 "항상 같은 Heater"

Heater와 CoffeeMaker는 CoffeeComponent와 같은 범위의 커스텀 Scope인 @CoffeeScope로 정의하였습니다

그러면 이제 CoffeeComponent와 Heater+CoffeeMaker는 같은 범위를 갖게됩니다

CoffeeBean은 커피를 만들때마다 소비되는 것이기 때문에 @Scope를 정의하지 않습니다 ( 일회용, 매번 다른 Bean)

 

이제 부모인 CafeComponent에서 자식 CoffeeComponent를 부를 수 있도록 메서드를 정의하고 커스텀 Scope의 적용을 확인하겠습니다

CafeComponent.interface
@Singleton
@Component(modules = [CafeModule::class])
interface CafeComponent {

    fun cafeInfo() : CafeInfo

    // 자식 Component의 Builder를 반환하는 메서드
    fun coffeecomponent() : Coffeecomponent.Builder
}

이제 부모인 CafeComponent에서 CoffeeMaker 대신 자식인 CoffeeComponentBuilder를 받을 수 있습니다

이제 조건들에 맞게 설정된 Scope들의 결과를 테스트해보겠습니다

// 조건 1 - CafeInfo의 SignleTon 테스트
var cafeComponent: CafeComponent = DaggerCafeComponent.create()
var cafeInfo1: CafeInfo = cafeComponent.cafeInfo()
var cafeInfo2: CafeInfo = cafeComponent.cafeInfo()
Log.d(TAG, "SingleTon scope CafeInfo is euqal : ${cafeInfo1.equals(cafeInfo2)}")

// 조건 2 - CoffeeMaker의 CoffeeScope 테스트
var coffeeComponent1: Coffeecomponent = cafeComponent.coffeecomponent().build()
var coffeeComponent2: Coffeecomponent = cafeComponent.coffeecomponent().build()
// 동일한 CoffeComponent로 CoffeeMaker 의존성 주입
var coffeeMaker1: CoffeeMaker = coffeeComponent1.coffeeMaker()
var coffeeMaker2: CoffeeMaker = coffeeComponent1.coffeeMaker()
Log.d(TAG, "CoffeeScope / same component coffeeMaker is equal : ${coffeeMaker1.equals(coffeeMaker2)}")

// 서로 다른 CoffeComponent로 CoffeeMaker 의존성 주입
var coffeeMaker3: CoffeeMaker = coffeeComponent2.coffeeMaker()
Log.d(TAG,"CoffeeScope / different component coffeeMaker is equal : ${coffeeMaker1.equals(coffeeMaker3)}")

// Non-Scope, CoffeeScope 설정안한 CoffeeBean 테스트
var coffeeBean1: CoffeeBean = coffeeComponent1.coffeeBean()
var coffeeBean2: CoffeeBean = coffeeComponent1.coffeeBean()
Log.d(TAG,"Non-Scoped coffeeBean is equal : ${coffeeBean1.equals(coffeeBean2)}")

출력 결과

// CafeInfo는 @Singleton Scope 설정을 하였기 때문에 모두 같은 객체
SingleTon scope CafeInfo is euqal : true

// @CoffeeScope로 인해 동일한 CoffeeComponent로 생성 시 같은 객체를 리턴하므로 True
CoffeeScope / same component coffeeMaker is equal : true

// @CoffeeScope로 인해 CoffeeComponent와 1:1매칭, 서로다른 CoffeeComponent이기 때문에 False
CoffeeScope / different component coffeeMaker is equal : false

// Scope를 설정하지 않았기 때문에 매번 새로 생성하므로 False
Non-Scoped coffeeBean is equal : false

CoffeeComponent와 provider메서드에 같은 @CoffeeScope Scope를 설정하므로 CoffeeComponent 당 한개CoffeeMaker만 존재하게 됩니다

 

@Component.Builder / @SubComponent.Builder

Dagger는 Component 생성 시 Builder Pattern을 사용합니다. @Component의 경우 코드가 generate 되기 때문에 builder역시 generate됩니다

그런데 @Subcomponent는 부모Component 클래스 안에서 코드가 생성될 때 @Subcomponent.Builder 가 붙은 interface가 없으면 Subcomponent의 builder를 generate(자동생성) 하지 않습니다.

그러므로 꼭 @SubComponent.Builder 가 붙은 interface를 Subcomponent에 정의해주어야 합니다

Builder는 build 하기 전에 모듈을 파라미터로 넣을 수 있습니다.

이 방법은 멤버변수를 갖고있는 Module이 있을 경우 유용하게 사용할 수 있습니다

CafeInfo를 갖고있는 CafeMoudle을 멤버변수를 포함하게 수정해보겠습니다

>

CafeModule.class
@Module(subcomponents = [Coffeecomponent::class])
class CafeModule(private var name: String? = null) {	// 멤버변수 name 추가

    @Singleton
    @Provides
    fun provideCafeInfo(): CafeInfo {
        if (name == null || name!!.isEmpty()) {
            return CafeInfo()
        }
        return CafeInfo(name!!)
    }
}

이제 CafeModule은 생성자에서 name 멤버변수를 파라미터로 받고 CafeInfo 객체 생성에 사용하게 됩니다

CafeComponent에서 Component.Builder를 정의하겠습니다

CafeComponent.interface
@Singleton
@Component(modules = [CafeModule::class])
interface CafeComponent {

    fun cafeInfo() : CafeInfo

    fun coffeecomponent() : Coffeecomponent.Builder

    @Component.Builder
    interface Builder {
        fun cafeModoule(cafeModule: CafeModule) : Builder
        fun build() : CafeComponent
    }
}

이제 Component를 build하기 전에 CafeModule을 먼저 적용할 수 있습니다

var cafeComponent: CafeComponent = DaggerCafeComponent.builder()
    .cafeModoule(CafeModule("개발자 카페"))
    .build()
Log.d(TAG, cafeComponent.cafeInfo().welcome())

위 코드처럼 CafeComponent를 build 하기전에 먼저 name을 적용한 CafeModule을 미리 bind할 수 있고

이를 통해 카페이름이 설정된 CafeInfo 객체를 제공받을 수 있습니다 

 

다음으로 @Bind, MultiBinding에 대해서 알아보겠습니다

 

@Binds

Module에서 추상메서드 앞에 붙여 Binding을 위임하는 annotation입니다

CoffeeBean을 상속받은 EthiopiaBean 이 있을 경우 @Binds를 통해 abstract(추상)메서드로 정의하는 것으로 CoffeeBean 객체를 EthiopiaBean 객체에 바인딩할 수 있습니다

먼저 CoffeeBean을 상속한 EthiopiaBean을 정의하겠습니다

EthiopiaBean.class

: CoffeeBean 클래스를 상속한 EthiopiaBean 클래스를 정의합니다

EthiopiaBean.class
class EthiopiaBean : CoffeeBean(){

    fun name() { println("EthiopiaBean") }
}

 

그리고 CoffeeBean 객체를 EthiopiaBean 객체에 바인딩하기 위해서는 아래와 같이 선언합니다

CoffeeBeanModule.class : 추상메서드(abstract)로 선언해야합니다

@Module
abstract class CoffeeBeanModule {
    @Binds
    abstract fun provideCoffeeBean(ethiopiaBean: EthiopiaBean) : CoffeeBean
}

이렇게 하면 CoffeeBean 객체를 EthiopiaBean 객체에 바인딩 하는 것을 의미합니다

@Binds 메서드는 객체를 생성하는 대신 Component 내에 있는 객체를 파라미터로 받아 바인딩함으로 좀 더 효율적으로 동작하게 만들어줍니다

단, EthiopiaBean이 Provider 메서드를 제공하고 있거나

CoffeeModule.class
@Module
class CoffeeModule {
    @CoffeeScope
    @Provides
    fun provideCoffeeMaker(heater: Heater): CoffeeMaker = CoffeeMaker(heater)

    @CoffeeScope
    @Provides
    fun provideHeater(): Heater = A_Heater()

    // EthiopiaBean을 @Provides로 제공하는 메서드 추가하거나
    @Provides
    fun provideEthiopiaBean(): EthiopiaBean = EthiopiaBean()
}

@Inject로 선언이 되어있어야 합니다

EthiopiaBean.class
class EthiopiaBean @Inject constructor() : CoffeeBean() {

    override fun name() { println("EthiopiaBean") }
}

 

 @Binds 메서드를 Component가 참조할 수 있도록 Component에 @Binds 메서드를 포함한 Module을 추가합니다

CoffeeComponent.interface
@CoffeeScope
@Subcomponent(modules = [
    CoffeeModule::class,
    CoffeeBeanModule::class		// @Binds 메서드가 포함된 CoffeeBeanModule 추가
])
interface Coffeecomponent {

    fun coffeeMaker(): CoffeeMaker

    fun coffeeBean(): CoffeeBean

    @Subcomponent.Builder
    interface Builder {
        fun cafeModule(coffeeModule: CoffeeModule): Builder
        fun build(): Coffeecomponent
    }
}

이제 CoffeeBean을 반환하는 coffeeBean() 메서드 실행 결과를 보겠습니다

var coffeeComponent = DaggerCafeComponent.create().coffeecomponent().build()
coffeeComponent.coffeeBean().name()

출력 결과

EthiopiaBean

CoffeeBean 객체를 생성해서 반환하는 coffeeBean() 메서드 호출할 경우 반환타입이 CoffeeBean인 @Binds 메서드에 EthiopiaBean이 파라미터로 전달되면서 CoffeeBean에 EthiopiaBean이 바인딩 되서 주입됩니다

 

정리하면 @Provides@Binds 는 모두 Module에서 객체를 제공해주는 어노테이션이지만 차이점은 

@Binds는 abstract(추상) 메서드로 구현부분이 없습니다

그리고 @Provides를 사용하면 @Binds 보다 약 40%이상의 코드가 더 생성되어 비효율적입니다.

그러면 효율적인 @Binds만 사용하면 좋겠지만, @Binds 어노테이션은 오직 1개의 파라미터를 가져야하고 abstract 함수로 호출되어야 합니다

이부분에 자세한 설명은  [Dagger2 @Binds vs @Provides] 글을 참고하시기 바랍니다  

 

MultiBinding

Dagger는 한 객체가 여러가지 형태로 Binding 기능할 때 Set이나 Map을 이용해서 MultiBinding이 가능합니다

CoffeeBean을 상속받은 GuatemalaBean을 추가로 만들어 보겠습니다

GuatemalaBean.class

GuatemalaBean.class
class GuatemalaBean @Inject constructor() : CoffeeBean() {

    override fun name(){ println("GuatemalaBean") }
}

반환 Type이 CoffeeBean으로 Binding 시도 시 EthiopiaBeanGuatemalaBean 중 어디에서 CoffeeBean Binding을 시도할 지 혼란을 가져오게 됩니다

이럴때 MultiBinding을 사용합니다

CoffeeBeanModule.class

CoffeeBeanModule.class
@Module
abstract class CoffeeBeanModule {
	// @Binds 사용하려면 먼저 파라미터 Type의 @Provide 또는 @Inject 설정이 필요합니다

    @Binds
    @IntoMap
    @StringKey("ethiopia")
    abstract fun provideEthiopiaBean(ethiopiaBean: EthiopiaBean) : CoffeeBean

    @Binds
    @IntoMap
    @StringKey("guatemala")
    abstract fun provideGuatemalaBean(guatemalaBean: GuatemalaBean) : CoffeeBean
}

@IntoMap@StringKey를 사용하여 CoffeeBean을 String Key를 가진 Map 형태로 감싸서 제공됩니다

그러면 CoffeeComponent의 coffeeBean() 메서드의 반환타입을 수정합니다

Coffeecomponent.interface
@CoffeeScope    // CoffeeMaker와 Heater의 범위를 맞추기위해 Scope 설정
@Subcomponent(modules = [
    CoffeeModule::class,
    CoffeeBeanModule::class
])
interface Coffeecomponent {

    fun coffeeMaker(): CoffeeMaker

    fun coffeeBean(): Map<String, CoffeeBean>	// Map형태로 변경

    @Subcomponent.Builder
    interface Builder {
        fun cafeModule(coffeeModule: CoffeeModule): Builder
        fun build(): Coffeecomponent
    }
}

 

var coffeeComponent = DaggerCafeComponent.create().coffeecomponent().build()
coffeeComponent.coffeeBean().get("guatemala")?.name()
coffeeComponent.coffeeBean().get("ethiopia")?.name()

위와 같이 다양한 Type의 MultiBinding을 구현할 수 있습니다

 

+ MultiBinding 바인딩 방법 (Provision / Member-Injection)

CoffeeModule.kt
@Module
abstract class CoffeeModule {

    @Binds
    @IntoMap
    @ClassKey(Espresso::class)
    abstract fun bindEspresso(espresso: Espresso): Coffee

    @Binds
    @IntoMap
    @ClassKey(Americano::class)
    abstract fun bindAmericano(americano: Americano): Coffee
}

@IntoMap으로 Espresso와 Americano 객체를 Coffee 타입으로 MultiBinding을 구현합니다

반환되는 Type인 Map 컬렉션의 Key로는 @ClassKey로 설정하였습니다

 

CoffeeComponent.interface
@Component(modules = [
        CoffeeModule::class
])
interface CoffeeComponent {

        fun coffees() : Map<Class<*>, Coffee>	// Provision 메서드
        
        fun inject(mainActivity: MainActivity)	// Member-Injection 메서드
}

MultiBinding 사용방법으로 Provision 유형의 메서드 선언과 Member-Injection을 위한 Inject 함수를 선언합니다

Provision 메서드의 Type은 CoffeeComponent에서 @IntoMap과 @ClassKey 그리고 반환Tpye인 Coffee로 Map<Class<*>, Coffee> 타입의 Map컬렉션이 됩니다.

Member-Injection 방법은 주입 Target인 MainActivity의 멤버필드를 추가로 선언해줘야하는데 Provision 메서드의 Type과는 조금 다르게 설정해야합니다

MainActivity.kt
class MainActivity: AppCompatActivity() {

	// Member-Injection으로 의존성 주입
    @Inject
    lateinit var coffees: Map<Class<*>, @JvmSuppressWildcards Coffee>
    
    override fun onCreate(...) {
    	...
        
        // Member-Injection, lateinit var coffees 의존성 주입 요청
        DaggerCoffeeComponent.create().inject(this@MainActivity)	
        	
        // Provision 메서드를 통해 Coffees2 의존성 주입
        var coffeeComponent = DaggerCoffeeComponent.create()	
        var coffees2 = coffeeComponent.coffees()
        
        coffee[Espresso::class.java].name()	// Map<Class<*>, Coffee> 사용방법
        coffee.get[Espresso::class.java].name()	// Map<Class<*>, Coffee> 사용방법
    }

실제 MainActivity에서 2가지 방법 (Provision, Member-injection)으로 의존성 주입하고 사용하는 방법입니다

여기서 Member-injection으로 의존성 주입할 @Inject coffees 멤버변수 타입을 보면 Provision 메서드에 설정했던 Map컬렉션의 Type과 다르게 @JvmSuppressWildcards를 추가해야 합니다

Kotlin은 제네릭을 컴파일 시점에 자동변환합니다.

// 컴파일 전
@Inject
lateinit var coffees : Map<Class<*>, Coffee>

// 컴파일 후 (제네릭 Type 자동변환)
@Inject
lateinit var coffees : Map<Class<*>, <? extends Coffee>>	// MultiBinding 타입과 불일치

Map<Class<*>, Coffee>Map<Class<*>, <? extends Coffee>> 타입으로 자동변환됩니다.

그래서 Dagger는 MultiBinding 타입인 Map<Class<*>, Coffee>와 타입이 일치하지 않기때문에 에러를 발생시킵니다.

Kotlin annotation인 @JvmSuppressWildcards는 이런 컴파일 시점에 제네릭의 자동변환을 하지않도록 하는 주석을 사용해서 자동변환이 이뤄지지 않도록 설정하면 MultiBinding 타입의 의존성 주입이 정상적으로 가능합니다

// 컴파일 전
@Inject
lateinit var coffees : Map<Class<*>, @JvmSuppressWildcards Coffee>

// 컴파일 후 (제네릭 Type 자동변환)
@Inject
lateinit var coffees : Map<Class<*>, Coffee>		// MultiBinding 타입과 일치

 

참고