Dagger2 사용법

6 minute read

이글은 보통 지능을 가진 사람이 dagger을 공부하면서 가장 쉽게 이해할 수 있도록 설명한 글입니다.(단 제가 이해한 수준이 되겠습니다.)

실전용이니 자세한 설명을 원하시는 분은 안드로이드 문서나 여러 다른 블로그 글들을 보시면 좋습니다.

시작!!

가장 먼저 dagger의 구조를 알아야 한다. (더 복잡하거나 다른 구조가 있을지 모르겠다.)

  1. @SubComponent가 있는 경우
  2. @SubComponent가 없는 경우
                  +-------------+
                  |(Application)|
                  |  Component  |
        +-------> |             |  +------+
        |         +-------------+         |  
        |                 ^               |
+---------------+  +--------------+       +
|               |  |              |
|    Module1    |  |    Module2   |   +-+ +-+ +-+
|               |  |              |   +-+ +-+ +-+
+---------------+  +--------------+

그림1 - @SubComponent가 없는 경우 구조


                                       +-------------+
                                       |             |
                                       |  Component  |
                             +-------> |             |
                             |         +-------+-----+  +------+
                             |                 ^               |
                     +-------+-------+  +------+-------+       +
                     |               |  |              |   +-+ +-+ +-+
                     |    Module1    |  |    Module2   |   +-+ +-+ +-+
                +---->               |  |              |
                |    +---------------+  +----^---------+<----+
                |                            |               |
         +------+---------+             +----+-----------+   |
         |                |             |                |      
    +---->  SubComponent1 +--------+    |  SubComponent2 |   +--+ +-+ +-+
    |    |                |        |    |                |   +--+ +-+ +-+
    |    +-------------^--+        |    +----------------+
+---+--------+  +------------+     |            |    
|            |  |            |     |            |    
|  Module1-1 |  | Module1-2  | ++ ++ ++     +-+  +--+ +--+
|            |  |            | ++ ++ ++     +-+  +--+ +--+
+------------+  +------------+

그림2 - @SubComponent가 있는 경우 구조

  • Component(interface)나 SubComponent(interface)는
    의존성 주입이 필요한 (내)클래스와 모듈을 연결해준다.
  • Module은 dagger와 컴포넌트를 연결해준다고 생각해도 되겠다.
    • @Inject (필요한 의존성 요청) -> (SubComponen -> module ;Subcomponent있을 경우) -> Component -> module -> dagger(generated,~injector) -> injection!!
  • SubComponent는 계층이 밑에 있는것 외에
    구조와 구성은 Component와 동일하다.
  • Module(object or abstract class)은
    @Binds,@Provides가 붙은 메서드로 의존성들을 구현해서
    dagger에게 알려주는 역할을 한다.
    (어떤 의존성을 어떻게 구현하는지를 dagger가 알아야 하기에)
  • 위의 그래프의 계층관계가 성립한다.
    그리고 dagger 내부에서는 밑에서 부터 의존성을 찾아 없으면 위로 올라가는 구조라고 한다.
  • 프로젝트가 작다면 1)경우나 2)경우나 크게 불편하지 않을 듯 하다.
  • 프로젝트가 크다면 당연히 2)가 유지보수성이나 스코프관리를 이용한 훨씬 효울적인 구조겠다.

1번 경우(SubComponent없는 경우) 시나리오(소설:배다른민족)
식당(Restaurant)을 창업한다.
그런데 요리사가 없는 식당을 만들고 싶다.
배민에서 요즘 요리사 로봇을 시범 운영중이라는데 한번 문의를 해봐야겠다.

-“여보세요? 배민로보틱스 인가요?”
-“원하시는 번호를 입력해주세요. 1. 로봇 주문 문의, 2. 로봇 고장 문의 3. 로봇 배송 문의 4. … “
-‘1’ 삐이익~
-뚜루루루 뚜루루루”1.음식 로봇, 2.배달 로봇, 3.청소 로봇 … 원하시는 로봇을 선택해주세요.”
-‘1’ 삐익~~ “UI 깔끔하군..비빔밥으로 한번 시작해보자~!!”

-로봇 스펙(BibimbabRobot)-
재료

  • 야채

기능

  • 요리하기

-“결제하기” 꾸욱~~

이 로봇을 제작하는 배민의 로직이라 본다면….

//@Qualifier를 지정해 만든 어노테이션은
//모둘,컴포넌트,@Inject시 
//여러게의 동일 타입이 있을 경우
//dagger가 정확히 1:1 대응을 알 수 있도록 
//싱크를 맞추게 해준다.
@Qualifier
annotation class Vegetable
@Qualifier
annotation class Rice

interface FoodRobot {
    fun cooked()
}

//* 여기서 쓰인 @Inject 가 constructor-injection이다. 
//* 런타임시(유저 인풋) 값을 넣기 위한 방법을 
//보이기 위해 생성자 파라미터에 Int값 두개를 넣음.
//(맨 위의 주석 참고)
//* dagger는 똑같은 타입(Int)이 2개 이상 있을시 뭐가 뭔지 구분 못함.
// @Vegetable,@Rice 이런 구분 어노터에션이 없으면 컴파일 에러 발생!!
// 그래서 @Vegetable,@Rice 이런 방법을 사용(@Named를 이용해도 됨)
class BibimbabRobot @Inject constructor( //<< ---1-1/3  constructor @Inject 
    @Vegetable val vegetableAmount: Int,
    @Rice val riceAmount: Int
) : FoodRobot {
    fun cooking(){
        println("야채:${vegetableAmount}g,밥:${riceAmount}g 비빔밥 요리중입니다.")
    }
    override fun cooked() {
        println("비빔밥을 완성했습니다.")
    }
}

//* object-@Provides and abstract class - @Binds 
//이렇게 짝을 이룬다.
//* @Provides는 모듈 내에서 기타 구현이 필요할 때 사용한다.
//* @Binds는 모듈내에서 인터페이스(추상클래스)가 리턴타입일 때
//  구현체 한개를 파라미터로 넣어서 abstract fun bindPizza(pizza:Pizza):Food
//  이런 식으로 구현한다.
@Module
object BibimbabModule{

    @Provides
    fun provideBibimbamRobot(
        @Vegetable vegetableAmount:Int,
        @Rice riceAmount:Int
    ):FoodRobot = BibimbabRobot(
        vegetableAmount,riceAmount
        ) //<< ---1-2/3 위의 @Inject가 
          //여기서 필요하다.
}

//컴포넌트를 지정한다.
//* 보통의 경우 Application-level로 지정한다.
//여기선 생략했지만 그러기 위해 @Singleton을 사용한다. 
@Component(modules = [BibimbabModule::class])
interface FoodComponent {
    //이 메서드로 컴포넌트와 내 클래스를 연결시켜준다.
    //메서드 이름은 아무렇게 해도 된다.
    //그러나 많은 문서엔 거의 다 이렇게 돼있다.'inject()'
    fun inject(restaurant: Restaurant) // <--- 2 inject 


    @Component.Factory
    interface Factory{
        fun create(
            //@BindsInstance는 Component.Builder나 .Factory의
            //파라미터들에 모두 포함시켜야 한다.
            @BindsInstance @Vegetable vegetableAmount: Int,
            @BindsInstance @Rice riceAmount: Int
        ):FoodComponent
    }
    //아래는 위의 Factory와 똑같은 기능을 하는 Builder패턴의 예이다.
    // @Component.Builder
    // interface Builder{
    //     fun setVegetableAmount(
    //         @BindsInstance @Vegetable vegetableAmount: Int
    //         ):Builder
    //     fun setRiceAmount(
    //         @BindsInstance @Rice riceAmount: Int
    //         ):Builder
    //     fun build():FoodComponent
    // }
}

//내 식당  
class Restaurant{
    //* << ---1-3/3 여기의 @Inject는 이미 dagger가 알고 있는 BibimbabRobot을
    // 사용하기 위한 field-injection이다. 1-1/3과 직접적으로 연관이 있진 않다.
    // 단지 `dagger의 BibimbabRobot을 사용하겠다`라는 의미다.
    //* private를 하면 dagger가 접근못한다.
    @Inject 
    lateinit var bibimbabRobot: BibimbabRobot
}

fun main() {
    val bibimbabStore = Restaurant()
//    DaggerFoodComponent.factory()
//        .create(200,100)
//        .inject(bibimbabStore) // <--- 2 inject
    DaggerFoodComponent.builder()
        .setRiceAmount(100)
        .setVegetableAmount(200)
        .build()
        .inject(bibimbabStore) // <--- 2 inject

    bibimbabStore.bibimbabRobot.apply {
        cooking()
        cooked()
    }
}

println »

야채:200g,밥:100g 비빔밥 요리중입니다.
비빔밥을 완성했습니다.

Process finished with exit code 0


위의 경우는 딱 한 계층에 한 컴포넌트, 한 모듈을 사용한 예이다.
모듈이나 field-injection(내 클래스)을 더 사용하고 싶은 경우
그냥 추가해 사용하면 된다.


@Component(modules = [
    BibimbabModule::class,
    PizzaModule::class,
    ChickenModule::class])
interface FoodComponent {
    fun inject(pizzaHouse:PizzaHouse)
    fun inject(chickenZip:ChickenZip)
    ...
}
@Inject
lateinit var chickenRobot:ChickenRobot
@Inject
lateinit var pizzaRobot:PizzaRobot
    ...


2번의 경우는 (@SubComponent가 있는 경우) 이런 구조(1번)를 이어 붙이기만 하면 된다.

-이어서 계속-
…1년 후…
-“월 매출 5천!! 이제 확장을 해봐야겠어!!”
-“피자를 추가해서 전세계를 사로잡아야지!!”



참고 - 아래의 코드 예는 좋은 구조가 아니다.
단지 dagger의 주의점을 보이기 위한 구조라고 보면 되겠다.

@Qualifier
annotation class Pizza // < --- 추가

class Restaurant{

    @Named("bibimbab") // < --- 추가 - 0;
    @Inject
    lateinit var bibimbabRobot: FoodRobot // < --- BibimbabRobot -> FoodRobot

    //추가 
    //pizzaRobot과 bibimbabRobot의 
    //리턴 타입이 같아 dagger가 구분할 수 있도록
    //이런식으로 알려줘야한다.
    // @Qualifier나 @Named나 기능은 같음을 보이려고 사용  
    @Pizza
    @Inject
    lateinit var pizzaRobot: FoodRobot
}


//Motor는 @Inject를 안하고 런타임시에 사용자가 넣어줌을 보여주기 위해 사용
class Motor
class PizzaRobot @Inject constructor(val motor:Motor):FoodRobot{
    fun run(){
        println("$motor 가 작동했습니다.")
    }
    override fun cooked() {
        println("피자를 완성했습니다.")
    }
}

@Component(modules = [BibimbabModule::class,PizzaConnectModule::class])// < --- 1-1/2
interface FoodComponent {
    fun inject(restaurant: Restaurant) //< --- 2-1/2중요 포인트!!
    fun foodSubComponent():FoodSubComponent.Factory
        //...이하 동일
}

@Module
object BibimbabModule{
    @Named("bibimbab") // < --- 추가 - 0;
    @Provides          
    fun provideBibimbamRobot(
        //...이하 동일
}

//pizzaRobot을 위한 subcomponent와 모듈들
//이 모듈(PizzaConnectModule)은 단지 
//FoodSubComponent와 FoodComponent의 `연결자`라고 생각하면 됨
@Module(subcomponents = [FoodSubComponent::class])
abstract class PizzaConnectModule // < --- 1-2/2 
//실질적인 피자 구현 모듈
@Module
abstract class PizzaModule{
    @Pizza
    @Binds
    abstract fun bindPizzaRobot(pizzaRobot: PizzaRobot) :FoodRobot
}
//위의 컴포넌트와 구조가 같음을 볼 수 있다.
@Subcomponent(modules =[PizzaModule::class] )
interface FoodSubComponent{
    
    fun inject(restaurant: Restaurant)//< --- 2-2/2중요 포인트!!
    @Subcomponent.Factory
    interface Factory{
        fun create(
            @BindsInstance motor: Motor
        ):FoodSubComponent
    }
}

위의 코드를 실행하면 에러가 난다. 이 로그만 보면 뭐가 문제인지 좀 알기 어렵다.

[Dagger/MissingBinding] @dagger.Pizza dagger.FoodRobot cannot be provided without an @Provides-annotated method.
public abstract interface FoodComponent {
                ^
  A binding with matching key exists in component: dagger.FoodSubComponent
      @dagger.Pizza dagger.FoodRobot is injected at
          dagger.Restaurant.pizzaRobot
      dagger.Restaurant is injected at
          dagger.FoodComponent.inject(dagger.Restaurant)

"< --- 2 중요"라고 한 fun inject() 이 부분에서 발생한건데
1/2와 2/2 중 뭐때문일까?? 한번 로그를 자세히 보면…

A binding with matching key exists in component: dagger.FoodSubComponent
... 이하 ...

이 부분이 키포인트다.
이미 바인딩 된 키가 있단다…FoodSubComponent에.

  • dagger에서 바인딩 되는 의존성(모든 타입을 갖는 값,클래스)은 하나만 존재할 수 있다.
    • 위의 FoodRobot, Int 타입은 여러개지만 @Qualifier나 @Named로 구분해 주었다. 그래서 각각의 구분된 의존성들이 된다.
  • subcomponent가 있으면 dagger는 subcomponent부터 의존성을 찾는다.
    그리고 거기에 원하는 의존성이 없으면 계속 위쪽 계층을 타고 component까지 가게 된다.(중요 포인트)

    여기를 읽어보세요.

Subcomponents are components that inherit and extend the object graph of a parent component. Thus, all objects provided in the parent component are provided in the subcomponent too. In this way, an object from a subcomponent can depend on an object provided by the parent component.

To create instances of subcomponents, you need an instance of the parent component. Therefore, the objects provided by the parent component to the subcomponent are still scoped to the parent component.


FoodSubComponent에서 이미 inject(restaurant)가 있는데
FoodComponent에서 또 inject(restaurant)가 있어서 중복됨을 알려준다.
…이하… 로그는 그 중복된 클래스들임을 알 수 있다.

그럼 1/2, 2/2중 무엇을 지워야 할까?
정답은 1/2(FoodComponent에 있는)이다. 이유는 단순하다.
FoodComponent(부모),FoodSubComponent(자식) 에서 모두 restaurant를 연결 시켜야하는데
dagger는 아래(자식)서 위(부모)로 의존성을 찾기에
1/2(위)를 남기고 2/2(아래)를 지우면
아래서 subcomponent와 restaurant를 연결시키지 못한다.(이미 지났기 때문에)
자식(subcomponents)에 한번 연결된 관계는 부모도 알 수가 있다.

fun main() {
    val restaurant = Restaurant()
    DaggerFoodComponent.builder()
        .setRiceAmount(100)
        .setVegetableAmount(200)
        .build().apply {
            foodSubComponent()
                .create(Motor()).inject(restaurant)
        }
    with(restaurant.bibimbabRobot as BibimbabRobot)  {
        cooking()
        cooked()
    }
    with(restaurant.pizzaRobot as PizzaRobot) {
        run()
        cooked()
    }
}

이상의 코드를 제대로 실행시키면 아래와 같이 잘 나오게 된다.

야채:200g,밥:100g 비빔밥 요리중입니다.
비빔밥을 완성했습니다.
dagger.Motor@7a81197d 가 작동했습니다.
피자를 완성했습니다.

Process finished with exit code 0


글이 좀 장황하다. 글을 쉽게 쓰기가 쉽지 않다.
나중에 까먹으면 이것만 봐도 좀 수월하겠다.


Leave a comment