본문 바로가기

[Android][Kotlin] Dagger2 #1 - 기본 개념

Dagger2 #1 - 기본 개념

 

Dagger 관련 글


 

Dagger

 

Dagger 5가지 필수 개념

  • Inject
  • Component
  • SubComponent
  • Module
  • Scope

 

Inject

@Inject는 필드, 생성자, 메서드에 붙여 Component로 부터 의존성 객체를 주입 요청하는 annotation입니다.

@Inject로 의존성 주입을 요청하면 연결된 ComponentModule로부터 객체를 생성하여 넘겨줍니다.
Component는 @Inject 어노테이션을 의존성 주입할 멤버변수와 생성자에 달아줌으로 DI 대상을 확인할 수 있습니다

객체(인스턴스)의 생성이 클래스에서 이루어지지 않고, Component가 생성해주기 때문에 BoilerPlateCode(보일러 플레이트코드)를 작성할 필요없이 클래스를 테스트하기 수월해 집니다 

@Inject - 생성자에 의존성 주입 방식
class AA()

class BB(val aa: AA)

// 생성자에 @Inject를 사용해서 CC Type의 인스턴스를 주입하는 방식
class CC @Inject constructor(val aa: AA, val bb: BB)

@Inject - 필드에 의존성 주입 방식
@Inject
lateinit var aa: AA	// @Inject로 필드에 의존성 주입을 하는 방식

@Inject
lateinit var bb: BB

@Inject
lateinit var cc: CC

기본적으로 Dagger는 요청된 Type(자료형)에 맞게 Component가 Module로부터 객체를 생성하여 주입 합니다

단, @Inject가 어디에서나 사용이 가능한 것은 아니며 @Inject를 붙일 수 없는 경우는 다음과 같습니다

  1.  Interface는 생성자 개념이 없으므로 불가능
  2. 써드파티 라이브러리 등의 클래스는 참조가 불가능하여 annotation프로세싱이 불가능

Module

Component에 연결되어 의존성 객체를 생성하는 역할입니다. 생성 후 Scope에 따라 객체를 관리도 합니다

@Module 클래스에만 붙이고, @Provides는 반드시 @Module 클래스에 선언된 메서드에만 사용합니다

Module 클래스는 의존성 주입에 필요한 객체들을 @Provide @Binds 메서드를 통해 관리합니다.

@Provides @Binds메서드의 파라미터 또한 Component에게 객체를 전달받고 Component에게 제공합니다

Module_A.kt
@Module
class Module_A {
    @Provides
    fun provideAA() : AA = AA()	// AA 객체(인스턴스)를 Component에게 제공

    @Provides
    fun provideBB(aa: AA) : BB = BB(aa) // 필요한 인자(AA)를 Component로 부터 전달받아 BB 객체를 생성해서 Component에게 제공
}
Dagger는 기본적으로 Null을 인젝션하는 것을 금지합니다. Null을 인젝션하고 싶다면 @Nullable 애노테이션을 @Provide 메서드와 객체 주입받을 타입에 모두 @Nullable 애노테이션을 사용할 경우에만 Null 주입을 허용하고, 그 이외의 경우에는 컴파일 에러를 발생합니다

Component

@Component는 Interface 또는 abstract class에만 사용이 가능합니다

컴파일 타임에 접두어 'Dagger'와 Component 클래스 이름이 합쳐진 Dagger클래스 자동생성
(ex : @Component interface MyComponent -> DaggerMyComponent 클래스 생성)

연결된 Module을 이용하여 의존성 객체를 생성하고, Inject로 요청받은 인스턴스에 생성한 객체를 전달(주입)합니다

의존성을 요청받고 전달(주입)하는 Dagger의 주된 역할을 수행하는 부분입니다

Component Methods

@Component 애노테이션이 달린 Interface 또는 abstract class는 반드시 1개 이상의 abstract-method가 있어야 합니다 
Component Method 유형에는 Provision 메서드 + Member-Injection 메서드로 구분됩니다

Component Methods 유형
@Component(modules = [Module_A::class, Module_B::class])
interface MyComponent {

    // provision 메서드 유형
    fun makeAA() : AA

    // Member-Injection 메서드 유형 - 인자로 받은 MainActivity 내부 멤버필드에 의존성 주입
    fun inject(mainActivity : MainActivity)
}

1. Provision Method

Provision 메서드 유형은 인자(매개변수)가 없고, Module이 제공하는 객체의 타입을 반환형으로 갖습니다.
생성된 Component 클래스에서 Provision 메서드를 통해 객체를 얻을 수 있습니다 

2. Member-Injection Method

의존성을 주입시킬 객체를 메서드의 파라미터로 넘기는 방식으로, Member-Injection 메서드를 호출하면 인자(매개변수)로 받은 타겟 클래스 내부 @Inject가 붙은 필드에 객체를 주입합니다 

+ @Component.Builder

Component 인스턴스를 생성하기 위한 Builder용 annotation입니다. Module초기 설정을 한다거나 할때 사용합니다
Component 내의 interface 또는 abstract class에 붙여서 사용하며, 다음과 같이 Builder를 정의합니다

@Component.Builder
@Component(modules = [Moduel_A::class, Module_B::class])
interface MyComponent {

    @Component.Builder
    interface Builder {

        fun moduleA(moduleA: Module_A) : Builder

        fun moduleB(moduleB: Module_B) : Builder

        fun build() : MyComponent
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 사용방법

DaggerMyComponent.builder()
    .moduleA(Module_A())
    .moduleB(Module_B())
    .build()

Builder는 반드시 Component반환하는 메서드(여기선 build())와 Builder반환하면서 Component가 필요로하는 Module을 파라미터로 받는 메서드(여기선 moduleA())를 가지고 있어야합니다

Builder를 반환하며 Module을 파라미터로 받는 메서드는 무조건 1개의 파라미터만 받는 추상메서드로 구성해야합니다.
파라미터(인자)를 2개 이상 선언할 수 없기 때문에, 여러 Module을 초기화 하려면 각 Module마다 메서드를 추가해서 메서드 체이닝 방식으로 Component 인스턴스를 생성해야 합니다

 

+ @Component.Factory

dagger-android 2.22버전 부터 추가된 Factory annotation 입니다
Builder와 의미는 동일하지만 사용방법이 조금 다른 형태입니다. Builder는 파라미터를 1개만 받으며 여러 Module을 설정해야 한다면 각각 메서드를 따로 선언해서 메서드 체이닝이 길어지는 점을 보완한 Factory입니다.

Factory는 단 하나의 메서드(create())만 선언되어야 하고 반환타입은 Component 인스턴스여야 합니다

@Component.Factory
@Component(modules = [Moduel_A::class, Module_B::class])
interface MyComponent {

    @Component.Factory
    interface Factory {

        fun create(moduleA: Module_A, moduleB: Module_B) : MyComponent
}
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 사용방법

DaggerMyComponent.factory()
    .create(Module_A(), Module_B())

Builder를 사용할 때는 각 메서드마다 호출해서 메서드 체이닝으로 Component 인스턴스를 생성하기 때문에 코드가 길어지고, 빼먹을 실수가 발생하는데, Factory는 create() 메서드 하나로 설정이 가능하기 때문에 코드의 간결화가 구현됩니다


 Dagger Graph 구조

 

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 에노테이션으로 생성

 

Scope

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

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

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

 

5가지 개념의 의존성 주입 Flow

Dagger 의존성 주입 Flow
Dagger 의존성 주입 Flow

  1. @Inject annotation이 붙은 Type 체크 (인스턴스 종류)
  2. SubComponent에서 먼저 해당 Type 반환 의존성이 있는지 검사
    (Component에 등록된 Module의 @Provides @Binds 메서드의 반환Type 동일여부)
  3. SubComponent에 없다면 상위 Component에서 의존성 검사
Module에서 동일 Type 반환하는 의존성 메서드 발견 시 Scope 등록 여부에 따라 인스턴스를 새로 생성하거나 기존의 것을 그대로 Retrun할지 결정 

 

 

 

간단 사용예시

[DI (Dependency Injection) -의존성 주입] 글에서 사용했던 DI 사용예시를 이어서 설명 하겠습니다

아래 예시는 Dagger에서 사용하는 Coffee Maker 예제입니다

구조 

 1. Heater : Coffee를 내리는데 필요한 열을 가하는 Interface "on() / off() / ishot() 추상메서드 포함"
     + A_Heater (Heater 구현체)

 2. Pump : Coffee를 내리는데 필요한 압력을 가하는 Interface, "pump() 추상메서드 포함"
     + A_Pump (Pump 구현체) 

 3. CoffeeMaker : Heater와 Pump를 사용해 커피를 내리는 CoffeeMaker 클래스

 

Build.greadle 세팅

가장 먼저 Dagger 사용을 위해 Dagger 라이브러리 의존성을 추가합니다

2020.05.05 기준 최신버전 '2.27'[ Dagger2 최신 버전 보러가기 ]

apply plugin: 'kotlin-kapt'


...

dependencies {
    // 2020.05.05 기준 최신버전
    def dagger_version = '2.27'

    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    // if you use the support libraries
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:$dagger_version"
}

 

Module

먼저 의존성 관계를 설정하는 클래스를 정의하겠습니다. Dagger는 이런 역할의 클래스를 Module이라 부르고 @Module이라는 annotation을 포함해야 합니다

Module 클래스에서는 객체를 주입하기 위한 메서드를 정의합니다. 이 메서드들은 @Provides annotation을 포함합니다
여기에 Heater와 Pump 인터페이스 구현체를 주입하기 위한 메서드를 구현하겠습니다

@Module
class CoffeeMakerModule {
    @Provides
    fun provideHeater() : Heater = A_Heater()

    @Provides
    fun providePump(heater: Heater) : Pump = A_Pump(heater)
}

위에 적용된 @Module, @Provides 어노테이션은 @Component 클래스에서 실제 주입 코드가 생성될 때 영향을 줍니다

실제 코드가 Generate되는 Component 인터페이스를 만들어 보겠습니다

 

Component

Component 생성 조건

     1. Component는 interface 또는 abstract class로 선언

     2. 최소 1개 이상의 abstract method(추상 메서드)가 포함 - abstract 메서드를 통해 의존성 주입이 가능

abstract method는 두 가지 유형으로 구분됩니다

     1. "Provision Return Method" - Injection 시킬 객체를 넘기는 유형     
     2. "Members-injection Methods" - 멤버 파라미터에 의존성 주입 시킬 객체를 넘기는 유형

 

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

    // provision method 유형
    fun make() : CoffeeMaker

    // member-injection method 유형
    fun inject(coffeeMaker: CoffeeMaker)
}


CoffeeComponent 인터페이스를 구현하고 abstract 메서드의 두가지 유형을 모두 구현했습니다

@Component 어노테이션을 달아서 Component임을 Dagger에게 알려줍니다. @Component 어노테이션에 속성으로 modules를 설정했는데 이건 해당 Component에 의존성 주입을 구현할 모듈들을 알려주는 의미입니다

그러면 Dagger는 CoffeeMakerModule에 @Provides로 구현된 메서드들을 이용해 의존성을 주입시킬 코드를 자동으로 생성해줍니다

사용자는 Dagger에게 @Inject 어노테이션으로 의존성 주입을 어디에 해줘야하는지 알려주면 의존성 주입이 됩니다

 

Inject

@Inject을 통해서 의존성 주입을 실행합니다. 생성자(Constructor) 또는 멤버변수(필드)에 @Inject annotation을 선언하여 사용합니다

먼저 Provision method인 make()를 사용하기 위해 CoffeeMaker클래스 구조를 수정합니다

// 주 생성자에 @Inject 설정으로 의존성 주입 대상을 알림
class CoffeeMaker @Inject constructor(
    private val heater: Heater,
    private val pump: Pump
) {
    fun brew() {
        heater.on()
        pump.pump()
        Log.d("coffeMaker", "[_]P coffee! [_]P")
        heater.off()
    }
}


CoffeeMaker 클래스의 주 생성자에 @Inject를 달아 의존성 주입 대상임을 Dagger에게 알려줍니다

주 생성자는 반환Type이 CoffeeMaker이기 때문에 Component에서 반환타입이 같은 메서드를 찾아 주입해줍니다 

Dagger는 Build시 @Component이 달린 인터페이스를 Dagger클래스로 자동으로 구현해줍니다

구현되는 Dagger클래스명은 선언된 Component인터페이스 이름 앞에 Dagger 접두어를 붙여 생성합니다

여기선 인터페이스명이 "CoffeeComponent" 이기에 생성되는 Dagger 클래스는 "DaggerCoffeeComponent"가 생성됩니다

Component 객체는 create() 또는 build() 함수로 구현이 가능합니다

// 방법 1 - build() 사용
DaggerCoffeeComponent.builder().build().make().brew()

// 방법 2 - create() 사용
DaggerCoffeeComponent.create().make().brew()

CoffeeComponent에서 선언한 provision method 형태의 make() 메서드로 커피를 내렸습니다

 

다음으로 Member-injection method 형태인 inject()를 사용하기 위해 CoffeeMaker 클래스를 변경합니다

class CoffeeMaker() {
    // 멤버변수에 @Inject 추가
    @Inject
    lateinit var heater: Heater		// inject() 메서드로 의존성 주입 대상 설정
    @Inject
    lateinit var pump: Pump			// inject() 메서드로 으존성 주입 대상 설정

    fun brew() {
        heater.on()
        pump.pump()
        Log.d("coffeMaker", "[_]P coffee! [_]P")
        heater.off()
    }
}

 

이제 CoffeeMaker 객체를 생성해서 inject() 함수로 넘겨 의존성 주입을 하겠습니다

// 기본 생성자로 CoffeeMaker 인스턴스 생성
var coffeeMaker: CoffeeMaker = CoffeeMaker()
// inject(), Member-injection Method 실행 (coffeeMaker객체의 멤버변수/필드에 의존성 주입)
DaggerCoffeeComponent.create().inject(coffeeMaker)
coffeeMaker.brew()

inject() 함수는 Member-injection Method 형태의 메서드입니다.

Member-injection Method가 무슨 의미인지 처음에 쉽게 이해하지 못했는데, 이해하고 나선 간단했습니다

inject() 함수에 파라미터로 전달받은 coffeeMaker 객체의 내부에 @Inject anntation이 붙은 멤버변수(필드)가 있다면

Module을 통해 의존성 주입을 실행하는 메서드입니다.

쉽게 이해하기 위해 다른 예시를 보겠습니다

먼저 Component의 inject() 함수를 수정하겠습니다

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

    // member-injection method 유형
    fun inject(mainActivity: MainActivity)	// 인자로 MainActivity를 받는 inject()
}


inject() 함수의 인자를 CoffeeMaker -> MainActivity 타입으로 변경하였습니다

// CoffeeMaker 클래스 생성자 수정
class CoffeeMaker(val heater: Heater, val pump: Pump) {

    fun brew() {
        heater.on()
        pump.pump()
        Log.d("coffeMaker", "[_]P coffee! [_]P")
        heater.off()
    }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// MainActivity에서 의존성 주입 - 사용
class MainActivity : AppCompatActivity() {

    // ActivityMain의 멤버변수(필드)
    @Inject
    lateinit var heater: Heater

    // ActivityMain의 멤버변수(필드)
    @Inject
    lateinit var pump: Pump

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // inject() 함수로 MainActivity의 멤버변수에 의존성을 주입
        DaggerCoffeeComponent.create().inject(this@MainActivity)

        var coffeeMaker = CoffeeMaker(heater, pump)	// 의존성 주입된 heater와 pump 사용
        coffeeMaker.brew()
    }
}


위 코드를 보면 MainActivity의 멤버변수(필드)로 heater와 pump가 존재합니다. 이 멤버변수는 @Inject로 의존성 주입 대상임을 Dagger에게 알립니다

inject(this@MainActivity) 함수로 MainActivity 인자를 넘겨 MainActivity 멤버변수의 의존성 주입을 요청하게 됩니다

 

Component 객체 구현 - Create() / Build()

Dagger는 Component 인터페이스를 Dagger[YourComponent이름] 형태의 Component클래스를 자동 구현합니다

위 예제에서는 CoffeeComponent로 Interface를 @Component 설정했으니 -> DaggerCoffeeComponent로 구현됩니다 

만약 Dagger[Component이름] 클래스가 인식이 되지 않을 경우

 1. Rebuild Project 실행
 2. 1번 수행에도 인식되지 않을 경우 dagger-compiler 라이브러리 포함하는지 확인

 

Component 인스턴스를 구현하는 방법은 2가지 - Create() / Build()

 1. create() 함수로 Component 구현

// Dagger[YourComponent이름]
DaggerCoffeeComponent.create().make().brew()


 2. build() 함수로 Component 구현

// Dagger[YourComponent이름]
DaggerCoffeeComponent.builder().build().make().brew()

make() 함수는 Component에 선언된 추상메서드입니다. make().brew()는 제외하시고 보시면 됩니다