본문으로 바로가기
반응형

이글은 Dagger 2 v2.25.2 를 기반으로 설명하며, Kotlin으로 예제 코드를 설명합니다.

또한 공식 Dagger site를 참고하였습니다.

https://dagger.dev/dev-guide/

앞서 포스팅에서 기본적인 Dagger의 사용에 대해서 알아봤습니다.

여기서는 이전 포스팅을 바탕으로 설명하므로 앞선 포스팅을 꼭 먼저 확인후 보시기 바랍니다.


2020/06/01 - [개발이야기/Kotlin] - [Dagger 2] Dependency injection, Dagger2 사용 - 기초 #1


여기서는 해당 포스트의 예제를 바탕으로 변형하여 사용합니다.

Sample code

interface Person {
    fun name(): String
    fun skill(): String
}

interface Weapon {
    fun type(): String
}

class IronMan: Person {
    override fun name() = "토니 스타크"
    override fun skill() = "수트 변형"
}

class Suit : Weapon {
    override fun type() = "수트"
}

class Hero @Inject constructor(private val person: Person, private val weapon: Weapon) {
    fun info() {
        Log.d("doo", "name: ${person.name()} skill: ${person.skill()} | weapon:${weapon.type()}")
    }
}


생성에 필요한 class들은 위와 같고 이를 생성하기 위해서는 아래와 같이 Dagger를 위한 코드를 생성합니다.

@Module
class HeroModule {
    @Provides
    fun providePerson(): Person = IronMan()

    @Provides
    fun provideWeapon(): Weapon = Suit()

    @Provides
    fun provideHero(person: Person, weapon: Weapon) =  Hero(person, weapon)
}

@Component(modules = [HeroModule::class])
interface HeroComponent {
    fun callHero(): Hero
}

// Hero 객체 생성
private fun main () {
    val hero = DaggerHeroComponent.create().callHero()
    val hero2 = DaggerHeroComponent.builder().build().callHero()
}


혹시 위 예제 코드가 이해가 가지 않는다면 이전 포스팅을 참고 하시기 바랍니다.

Qualifiers

생성하고 싶은 Hero가 한명 더 있기에 CaptainAmerica 클래스와 캡틴아메리카의 무기인 방패 클래스를 하나 더 추가합니다.
class CaptainAmerica: Person {
    override fun name() = "스티브 로저스"
    override fun skill() = "방패 던지기"
}

class Shield : Weapon {
    override fun type() = "방패"
}

따라서 해당 클래스를 생성하기 위해서는 Module이 provide가 추가 되어야 합니다.
@Module
class HeroModule {
    @Provides
    fun provideIronMan(): Person = IronMan()

    @Provides
    fun provideSuit(): Weapon = Suit()

    @Provides
    fun provideCaptinAmerica(): Person = CaptainAmerica()

    @Provides
    fun provideShield(): Weapon = Shield()

    @Provides
    fun provideHero(person: Person, weapon: Weapon) =  Hero(person, weapon)
}


모듈에 기존 아이언맨과 수트, 캡틴과 방배를 생성할수 있는 Provide 함수를 추가합니다.

하지만 main() 함수에서 실제 Hero 객체를 생성하기 위해 아래와 같은 코드를 수행하면, proivider의 return type만 보고는 Dagger 입장에서 Hero 객체의 인자로 어떤 클래스를 넣어야 할지가 모호해 집니다.


"IronMang", "CaptainAmerica" 전부 Person interface를 retrun하도록 되어있고, "Suit", "Shield" 모두 Weapon interface를 반환하기 때문에 Dagger는 어떤 클래스를 선택해서 넣어야 할지 애매해 지는거죠.

Dagger를 사용하는 Caller 역시 Hero의 인자로 들어가길 바라는 객체가 따로 존재할것이기 때문에 유저 입장에서도 모호한 객체 생성 호출이 됩니다.


따라서 이런 경우 @Named Annotation을 통해서 provider에 이름을 줄 수 있습니다.

@Named은 javax.inject에 구현되어 있으면 아래와 같습니다.

@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}


이제 @Named를 이용해서 Module을 수정합니다.

import dagger.Module
import dagger.Provides
import javax.inject.Named

@Module
class HeroModule {
    @Provides
    @Named("ironMan")
    fun provideIronMan(): Person = IronMan()

    @Provides
    @Named("suit")
    fun provideSuit(): Weapon = Suit()

    @Provides
    @Named("captainAmerica")
    fun provideCaptainAmerica(): Person = CaptainAmerica()

    @Provides
    @Named("shield")
    fun provideShield(): Weapon = Shield()

    @Provides
    @Named("heroIronMan")
    fun provideHeroIronMan(@Named("ironMan") person: Person,
                    @Named("suit") weapon: Weapon) =  Hero(person, weapon)

    @Provides
    @Named("heroCaptainAmerica")
    fun provideHeroCaptainAmerica(@Named("captainAmerica") person: Person,
                    @Named("shield") weapon: Weapon) =  Hero(person, weapon)
}


모델에서 각각의 객체를 생성해주는 provider에 이름을 넣었습니다.

또한 Hero 객체를 생성할때 IronManCaptainAmerica를 각각 생설할수 있도록 분리해서 provider를 만들었습니다.


이제 캡틴과 아이언맨을 각각 생성하는 함수를 제공하기 위해 Component도 변경 합니다.

@Component(modules = [HeroModule::class])
interface HeroComponent {
    @Named("heroIronMan")
    fun callIronMan(): Hero

    @Named("heroCaptainAmerica")
    fun callCaptainAmerica(): Hero
}


해당 함수가 불릴때 어떤 provider를 사용할지를 역시 @Named 를 이용해서 지정해 줍니다.

이제 준비가 끝났으니 Project를 rebuild합니다.


그리고 나서 호출부분을 아래와 같이 작성하면, 원하는 Hero를 호출 할 수 있습니다.

private fun main () {
    val heroIronMan = DaggerHeroComponent.create().callIronMan()

    val heroCaptain = DaggerHeroComponent.create().callCaptainAmerica()
}


예제에서 Hero 객체의 생성자를 이용한 생성이기 때문에 @Named를 Module과 Component에 추가해 주었습니다.

만약 Field injection을 사용한다면 아래와 같이 @Inject과 함께 사용 할 수도 있습니다.

class Hero {

    @Inject
    @Named("ironMan")
    lateinit var person: Person

    @Inject
    @Named("suit")
    lateinit var weapon: Weapon

    fun info() {
        Log.d("doo", "name: ${person.name()} skill: ${person.skill()} | weapon:${weapon.type()}")
    }
}


Binding Instances

Module을 작성하다 보면 객체 생성시 param으로 입력되는 인자를 외부에서 전달받아야 하는 경우가 있습니다.
지금까지의 예제에서는 객체 생성에 필요한 param역시 Module에 정의했으나 생성 시점에 따라 가변적으로 바뀌어야 하거나, 생성을 못하는 class인 경우가 있기 때문입니다.

예를들어 main 함수를 실행시키기 위해서 command 라인에서 수행하면서 넘겨받은 args 가 객체 생성 인자값으로 들어가는 경우라던가,
Android에서 Activity나 Application의 context를 넘겨받아야 하는 경우 입니다.

Android에서 Activity나 Service같은 기본 component 들은 Android Framework이 생성하기 때문에 이런 경우 Dagger에서 생성하는게 아니라 이미 생성된 객체를 Dagger 넘겨주도록 합니다.

이번엔 Hero로 Hulk 클래스를 추가합니다.

class Hulk : Person {
    override fun name() = "브루스 배너"
    override fun skill() = "육탄전"
}


Hreo 클래스는 생성자로 Person과 Weapon 객체를 받아야 하나 Hulk는 무기가 없습니다.

다만 헐크용 수트를 넘겨줄수 있다고 가정합니다.

즉  Dagger가 직접 weapon을 생성하지는 못하지만 외부에서 생성된 무기 객체를 넘겨받도록 합니다.

class HulkBuster: Weapon {
    override fun type() = "헐크버스터"
}

헐크의 무기를 나타내는 HulkBuster 클래스를 정의합니다.

다만 이 클래스는 Module에서 생성하지 않고, 외부에서 주입받기에 아래와 같이 Module을 구성합니다.

@Module
class HeroModule {

    ...

    @Provides
    @Named("hulk")
    fun provideHulk(): Person = Hulk()

    ...
    @Provides
    @Named("heroHulkWihWeapon")
    fun provideHeroHulkWithWeapon(@Named("hulk") person: Person, weapon: Weapon) = Hero(person, weapon)
}


Hulk 객체를 생성하는 provider와 Hulk를 가진 Hero 객체를 생성하는 Provider를 추가합니다.

여기서 중요한 부분은 provideHeroHulkWithWeapon()의 wepon 인자에 아무런 annotation도 넣지 않았다는점 입니다.


@Component(modules = [HeroModule::class])
interface HeroComponent {
    @Named("heroIronMan")
    fun callIronMan(): Hero

    @Named("heroCaptainAmerica")
    fun callCaptainAmerica(): Hero

   // 헐크용 Hero 객체 생성 함수 추가 
   @Named("heroHulkWihWeapon")
    fun callHulkWithWeapon(): Hero

    // weapon을 넘겨받기 위한 builder 추가
    @Component.Builder
    interface Builder {
        fun setHulkWeapon(@BindsInstance hulkWeaponProvider: Weapon): Builder
        fun build(): HeroComponent
    }
}


Component에는 CallHulkWithWeapon() 이라는 헐크용 Hero 생성 함수를 추가합니다.


이전 포스팅에서 언급했듯인 Component는 Builder pattern을 사용합니다.

빌더에 대해서 아무것도 언급하지 않는 다면 Dagger가 기본 builder를 자동 생성합니다.


여기서는 필요한 객체를 넘겨야 하기 때문에 Dagger 호출시 Builder에 객체를 넘겨줄 수 있도록 Builder를 재 구성합니다.

@BinsInstance annotation을 사용하여 setHulkWeapon() 이란 함수로 Weapon 객체를 넘겨 줄것이라는걸 Dagger에 알려주며, Builder가 재생성되어야 함을 알려주기 위해서 @Component.Builder 를 사용하도록 합니다.


실제로 객체를 호출하는 부분은 아래와 같습니다.
val hulkBuster = HulkBuster()
val hulkWithWeapon = DaggerHeroComponent.builder().setHulkWeapon(hulkBuster).build().callHulkWithWeapon()


그럼 이번에는 component의 자동구성된 파일이 어떻게 되어있는지를 한번 살펴 봅니다.

public final class DaggerHeroComponent implements HeroComponent {
  private final HeroModule heroModule;

  private final Weapon setHulkWeapon;

  private DaggerHeroComponent(HeroModule heroModuleParam, Weapon setHulkWeaponParam) {
    this.heroModule = heroModuleParam;
    this.setHulkWeapon = setHulkWeaponParam;
  }

  public static HeroComponent.Builder builder() {
    return new Builder();
  }

  @Override
  public Hero callIronMan() {
    return HeroModule_ProvideHeroIronManFactory.provideHeroIronMan(heroModule, 
             HeroModule_ProvideIronManFactory.provideIronMan(heroModule),
             HeroModule_ProvideSuitFactory.provideSuit(heroModule));}

  @Override
  public Hero callCaptainAmerica() {
    return HeroModule_ProvideHeroCaptainAmericaFactory.provideHeroCaptainAmerica(heroModule,
                HeroModule_ProvideCaptainAmericaFactory.provideCaptainAmerica(heroModule),
                HeroModule_ProvideShieldFactory.provideShield(heroModule));}

  @Override
  public Hero callHulkWithWeapon() {
    return HeroModule_ProvideHeroHulkWithWeaponFactory.provideHeroHulkWithWeapon(heroModule,
             HeroModule_ProvideHulkFactory.provideHulk(heroModule),
             setHulkWeapon);}

  private static final class Builder implements HeroComponent.Builder {
    private Weapon setHulkWeapon;

    @Override
    public Builder setHulkWeapon(Weapon hulkWeaponProvider) {
      this.setHulkWeapon = Preconditions.checkNotNull(hulkWeaponProvider);
      return this;
    }

    @Override
    public HeroComponent build() {
      Preconditions.checkBuilderRequirement(setHulkWeapon, Weapon.class);
      return new DaggerHeroComponent(new HeroModule(), setHulkWeapon);
    }
  }
}


먼저 builder를 통해서 넘어올 Weapon 객체를 담는 멤버변수가 추가되었습니다.

변수명이 @Component.Builder에 추가한 함수명과 동일합니다.

private final Weapon setHulkWeapon;


@Override
  public Hero callHulkWithWeapon() {
    return HeroModule_ProvideHeroHulkWithWeaponFactory.provideHeroHulkWithWeapon(heroModule,
             HeroModule_ProvideHulkFactory.provideHulk(heroModule),
             setHulkWeapon);}

그리고 callHulkWithWeapon()이란 provider는 내부적으로 Builder에서 넘겨받은 객체를 Hero생성자로 넘겨 줍니다.

하지만 다른 IronMan이나 CaptainAmerica 생성 함수는 Module에 정의된 @Named에 따라 각각의 Provider를 써서 넘겨주는걸 알수 있습니다.


  private static final class Builder implements HeroComponent.Builder {
    private Weapon setHulkWeapon;

    @Override
    public Builder setHulkWeapon(Weapon hulkWeaponProvider) {
      this.setHulkWeapon = Preconditions.checkNotNull(hulkWeaponProvider);
      return this;
    }

    @Override
    public HeroComponent build() {
      Preconditions.checkBuilderRequirement(setHulkWeapon, Weapon.class);
      return new DaggerHeroComponent(new HeroModule(), setHulkWeapon);
    }
  }
}

마지막으로 inner class로 정의돈 Builder class에 위에서 정의한 함수들이 구현되어 있음을 알수 있습니다.


즉 Component 구성시에 Hero의 생성자 객체로 Weapon의 대상을 정확하게 지정(@Named 를 사용하여)해주지 않는다면 Dagger는 자동 구성시 넘겨받은 객체 타입을 생성에 사용합니다.

반응형