본문으로 바로가기
반응형

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

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

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


이번에는 SubComponent와 Singleton의 개념에 대해서 설명합니다.

선행되는 지식이 필요한 관계로 앞에 글을 보지 않으셨다면 앞선 포스팅을 먼저 보고 오시기 바랍니다.

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

2020/06/01 - [개발이야기/Kotlin] - [Dagger 2] Qualifier, instance binding 사용, @Named, @BinsInstance #2

2020/06/03 - [개발이야기/Kotlin] - [Dagger 2] Provider injection, Lazy Injection #3



단일 객체의 생성 - @Singleton

앞선 component에서는 inject 시점에 매번 새로운 객체를 생성해서 반환합니다.

또한 직접 객체를 반환하는 함수를 사용는 경에도 항상 새로운 객체를 반환합니다.


하지만 singleton 처럼 객체를 한번만 생성하고 그 이후엔 동일한 객체를 반환하도록 하는 경우도 종종 발생합니다.

예를 들어 안드로이드에서 Room을 사용해 DB를 생성하면 room DB에 접근할때 Database객체를 받아와야 합니다.

이 Database 객체는 한번만 생성하고,  그 이후에는 생성된 객체를 이용해서 사용하도록 권고 됩니다.

(그래서 이런경우 대부분 singleton으로 구현합니다.)

이번에는 생성시 한개의 객체만 생성하도록 하는 방법에 대해서 알아봅니다.


계속 Hero관련 예제를 들었으니 이번에는 어벤져스 객체를 만들어 보겠습니다.

class Avengers {
     lateinit var ironMan: Hero
     lateinit var captainAmerica: Hero
     lateinit var hulk: Hero


    fun info() {
        println("Avengers created!!")
    }
}


그리고 이전 예제부터 계속 사용해 왔던 Hero관련 class를 그대로 사용합니다.

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 CaptainAmerica: Person {
    override fun name() = "스티브 로저스"
    override fun skill() = "방패 던지기"
}

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

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

class HulkBuster: 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()}")
    }
}


어벤져스는 세명의 Hero로 구성됩니다.

세상에 어벤져스는 하나 입니다.

따라서 세상 어디서 호출 하든지 동일한 어벤져스가 나와야 합니다.


먼저 Avengers를 호출하는 module을 만듭니다.

import dagger.Module
import dagger.Provides
import javax.inject.Singleton

@Module
class AvengersModule {
    @Singleton
    @Provides
    fun provideAvengers(): Avengers {
        return Avengers()
    }
}


어벤져스를 성성해주는 Provider를 만들었습니다.

이전과 다른점은 해당 함수에 @Singleton annotation을 붙였습니다.

이 annotation의 역할은 Component의 구현까지 보고 자세히 알아 봅니다.


@Singleton
@Component(modules = [AvengersModule::class])
interface AvengersComponent {
    fun getAvengers(): Avengers
}


Component는 간단하게 Avengers 객체를 반환하는 객체를 하나 만듭니다.

단 Component level에서도 @Singleton annotation을 붙입니다.


그리고 아래와 같이 호출해 보겠습니다.

물론 호출하기 위해서는 project rebuild를 한번 해야 IDE에서 컴파일 에러 없이 코드를 작성할 수 있습니다.

 val avengers = DaggerAvengersComponent.create()
 println("avengers1:${avengers.getAvengers()} avengers2:${avengers.getAvengers()}")


이때 출력되는 avengers의 hash 정보는 아래와 같이 동일합니다.

avengers1:~~~~Avengers@7d7c802 avengers2:~~~~Avengers@7d7c802


만약 호출 시점에 아래와 같이 코드를 작성했다면 어떨까요?

val avengers1 = DaggerAvengersComponent.create()
val avengers2 = DaggerAvengersComponent.create()
println("avengers1:${avengers1.getAvengers()} avengers2:${avengers2.getAvengers()}")

이때는 생성된 두개의 객체값이 다르게 나옵니다.


우리가 흔히 자바에서 사용하는 singleton의 개념은 process와 동일한 생명주기를 같습니다.

따라서 Process가 살아있는한 동일한 객체를 반환합니다.


하지만 Dagger에서 제공하는 @Singleton은 java에서의 singleton 개념과는 다릅니다.

첫번째 예제에서 단일 Component에서 생성한 두개의 객체는 동일한 객체이며, 두번째 예제에서 각각 다른 Component에서 생성한 객체는 서로 다른 객체 입니다.

즉 단일객체를 제공하기는 하지만 그 생명주기는 해당 Component에 의존합니다.

즉 Component와 해당 객체는 생명주기를 같이 합니다.


Dagger에서는 Scope이라는 개념을 제공합니다.

따라서 Scope정의하여 생명주기를 같이 할 객체들을 묶을수 있습니다.


예를 들어 Android의 Activity, Service등의 기본 구성 Component들은 각각의 생명주기를 갖습니다.

만약 Activity 내부에서 inject되는 멤버변수가 있고 이 멤버 변수의 생성을 Activity의 생명주기와 동일하게 유지하려고 할때 (Activity가 살아있는 동안은 내부에서 Dagger를 이용해 객체 생성시 동일한 객체가 반환되도록 사용) Scope을 이용할 수 있습니다.


@Singleton역시 javax에서 제공하는 기본 scope 입니다.

또한 @Singleton을 붙인 경우에 dagger가 자동생성한 Module / Component의 내부를 열어보면, 우리가 java에서 말하는 singleton 기법으로 구현되지 않습니다.

그저 생명주기를 묶기 위한 이미 만들어 놓은 scope중 하나 입니다.


실제 @Singleton은 아래와 같은 코드로 정의되어 있습니다.

package javax.inject;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * Identifies a type that the injector only instantiates once. Not inherited.
 *
 * @see javax.inject.Scope @Scope
 */
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}


@Singleton은 보통 Component에 붙여서 Component와 생명주기를 같이하도록 할때 사용합니다.


SubComponent

Dagger는 SubComponent를 지원하며, Component와 부모-자식 관계를 맺을수 있습니다.

일단 간단하게 subComponent를 어떻게 사용하는지 에제로 보도록 하겠습니다.


예제가 길어 보이지만 위에 함수들은 각 어벤저스 주인공을 생성, 무기를 생성하는 함수이고 마지막 세개는 사람과 무기를 합쳐서 각각의 Hero를 생성하는 함수 입니다.

(이름만 바꾼 동일한 코드의 반복입니다.)


@Module
class HeroModule2 {

    @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("hulk") fun provideHulk(): Person = Hulk() @Provides @Named("hulkBuster") fun provideHulkBuster(): Weapon = HulkBuster() @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) @Provides @Named("heroHulk") fun provideHeroHulk( @Named("hulk") person: Person, @Named("hulkBuster") weapon: Weapon ) = Hero(person, weapon) }


먼저 Avengers 클래스에  Hero 멤버변수를 채워주기 위한 모듈을 위와같이 만듭니다

아이언맨, 헐크, 캡틴아메리카를 생성해주는 함수와 각각의 무기를 생성해 주는 수트, 방배, 헐크 버스터 함수를 추가 합니다.

또한 각각의 Hro( Person + Weapon) 객체를 만들기 위해 provideHeroIron,Man() ProvideHeroCaptainAmerica(), ProvideHeroHulk() 역시 만듭니다.

사람과 무기는 동일한 Person, Weapon 객체를 return하기 때문엥 각각의 Hero 생성 함수에서 인자에 어떤 값을 넣어줄지를 명시하기 위해 @Named annotation을 사용합니다.


마찬가지로 Hero를 생성해주는 함수들 역시 @Named를 사용하여 caller에서 호출할때 헷깔리지 않도록 만들겠습니다.

위 모듈이 이해가 가지 않는다면 상위에 링크되어 있는 #2번 글을 읽고오시기 바랍니다.

  

@Subcomponent(modules = [HeroModule2::class])
interface HeroSubComponent {
    @Named("heroIronMan")
    fun callIronMan(): Hero

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

    @Named("heroHulk")
    fun callHulk(): Hero

    fun inject(avengers: Avengers)

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


이번에는 SubComponent를 정의 합니다.

AvengersComponent의 자식으로 종속시키기 위해 @SubComponent를 사용합니다.

단 SubComponent의 경우 Builder를 정의해 놓지 않는다면, 외부에서 접근 할 수가 없습니다.

따라서 SubComponent의 경우 반드시 Builder를 생성해 놔야 합니다. (정의해 놓지 않는다면 빌드자체가 실패합니다.)


SubComponent에는 각각의 Hero를 생성할 함수를 정의하고, 외부에서 Avengers 객체를 받았을때, 내부를 채워줄 inject 함수를 추가로 정의합니다.

@Module(subcomponents = [HeroSubComponent::class])
class AvengersModule {
    @Singleton
    @Provides
    fun provideAvengers(): Avengers {
        return Avengers()
    }
}


부모와 자식의 연결을 표시하기 위해서는 Module에 subComponents 속성을 이용합니다.

SubComponent와 Component의 종속 관계는 Component에 표기해야 할것 같지만 Module에 이를 정의하도록 되어 있습니다.


Dagger에서 Module은 객체를 생성하는 역할을 하고, Component는 외부에서 Dagger를 사용하기 위한 함수를 노출해 주는 interface 역할을 합니다. 

따라서 객체의 생성은 Module이 담당하기에 상위 Module에서 하위 Module의 객체 생성에 대한 역할까지 가지며, 하위모듈에서 객체를 생성하기 위해서는 하위 Component를 이용해야 하기 때문에 상위 Module과 하위 Component가 연결되도록 설계 했을거라는 예상을 해 봅니다.


@Singleton
@Component(modules = [AvengersModule::class])
interface AvengersComponent {
    fun getAvengers(): Avengers
    fun heroSubComponent(): HeroSubComponent.Builder
}


 Avengers의 Component에는 하위 모듈에서 제공하는 생성기능을 사용하기 위해 Builder를 return해 주는 함수를 하나 추가해야 합니다.

마지막으로 하위 모듈의 inject을 이용하기 위해 Avengers class를 아래와 같이 변경합니다.


class Avengers {

    @Inject
    @Named("heroIronMan")
    lateinit var ironMan: Hero

    @Inject
    @Named("heroCaptainAmerica")
    lateinit var captainAmerica: Hero

    @Inject
    @Named("heroHulk")
    lateinit var hulk: Hero


    fun info() {
        println("Avengers created!!")
    }
}

이제 상위/하위 역할을 모두 연결 하였으므로 이를 호출해 보겠습니다,


val avengersComponent = DaggerAvengersComponent.create()
val avengers = avengersComponent.getAvengers()

avengersComponent.heroSubComponent().build().inject(avengers)

println("info ${avengers.ironMan.info()} | ${avengers.captainAmerica.info()} | ${avengers.hulk.info()}")


실행하면 정상적으로 각 hero의 정보가 출력됩니다.


상위 / 하위 모듈의 scope 분할

상위 / 하위 모듈을 정의 했으니 각각의 scope을 나누어 각 객체들의 생명주기를 확인해 보겠습니다.

Avengers는 상위에 위치하므로 이미 정의된 @Singleton을 유지합니다.

Hero의 구성요소중 사람은 유일하고, 무기는 변경이 가능합니다.

따라서 "아이언맨", "캡틴아메리카", "헐크"같은 Person 객체는 한번만 생성되고 그 이후엔 공유되어야 합니다.

반면 "아이언맨"의 수트와 "아이너맨"이 만들어준 헐크 버스터는 호출 할 때마다 새로 생성되도록 합니다.

물론 캡틴의 방배는 세상에 한개뿐이니 이것도 한번만 생성되고 다음 호출시에는 동일한 객체가 반환되어야 합니다.

@Scope
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class Heros


이런 생명주기를 한정하기 위해 위와 같이 "Heros"라는 scope을 하나 만듭니다.

위 코드는 kotlin이기 때문에 자바 코드의 경우 위 singleton과 동일한 형태로 하나를 생성하면 됩니다.

그리고 생성을 한정 할 Module에 @Heros annotation을 붙여줍니다


@Module
class HeroModule2 {

    @Provides
    @Heros
    @Named("ironMan")
    fun provideIronMan(): Person = IronMan()

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

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

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

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

    @Provides
    @Named("hulkBuster")
    fun provideHulkBuster(): Weapon = HulkBuster()

    ...
}


provideIronMan(), ProvideCaptainAmerica(), ProvideHulk() 에는 @Heros를 추가합니다.

또한 캡틴의 방패는 세상에 딱 하나만 존재하므로 @Heros를 붙여주었습니다.

@Heros
@Subcomponent(modules = [HeroModule2::class])
interface HeroSubComponent {
    @Named("heroIronMan")
    fun callIronMan(): Hero

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

    @Named("heroHulk")
    fun callHulk(): Hero

    fun inject(avengers: Avengers)

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


그리고 SubComponenet인 HeroSubComponent의 Class 레벨에서 @Heros를 붙여 줍니다.

따라서 HeroSubComponent가 생성되면 해당 Component에서 생성되는 IronMan, CaptainAmerica, Hulk, 방패는 딱 하나만 생성되어 생성 요청시 동일한 객체가 반환됩니다.


테스트를 위해 아래와 같이 호출합니다.


val avengersComponent = DaggerAvengersComponent.create()
val heroComponent = avengersComponent.heroSubComponent().build()


val avengers1 = avengersComponent.getAvengers()
heroComponent.inject(avengers1)

println("avengers1: ${avengers1} |  ${avengers1.ironMan} | ${avengers1.ironMan.person} | ${avengers1.ironMan.weapon}")

val avengers2 = avengersComponent.getAvengers()
heroComponent.inject(avengers2)

println("avengers2: ${avengers2} |  ${avengers2.ironMan} | ${avengers2.ironMan.person} | ${avengers2.ironMan.weapon}")


결과는 아래와 같습니다.


avengers1: com.dbjt.mylittleworld.sample.Avengers@c715b9com.dbjt.mylittleworld.sample.Hero@378cdfe | com.dbjt.mylittleworld.sample.IronMan@9a7495f | com.dbjt.mylittleworld.sample.Suit@30f5fac

avengers2: com.dbjt.mylittleworld.sample.Avengers@c715b9com.dbjt.mylittleworld.sample.Hero@7ba5475 | com.dbjt.mylittleworld.sample.IronMan@9a7495f | com.dbjt.mylittleworld.sample.Suit@78fcc0a


AvengersComponent와 Herocomponent는 각각 하나씩만 생성하여 avengers1, avengers2만듭니다.

avengers1, avengers2 객체의 생성
  • AvengersComponentgetAvengers()AvengersModuleProvideAvenger() 함수를 통해서 생성됩니다.
  • avengers1과 avengers2는 @Singleton으로  AvengersComponent와 생명주기를 같이 합니다.
  • 따라서 위 코드처럼 avengersComponent를 생성하고, 해당 component로 만드는 Avengers 객체는 전부 동일한 객체가 됩니다.
  • 결과에서 보듯이 avengers1avengers2는 동일한 객체입니다.
Avenger의 멤버 변수인 Hero 객체 생성
  • AvengerComponent를 이용하여 subComponentheroComponent를 하나 만들었습니다.
  • 이를 가지고 Avengers 객체에 hero를 inject 시킵니다.
  • 각각의 Hero를 생성하는 모듈의 함수들에는 (provideHeroIronMan()...) scope annotation이 없으니 매번 새로운 객체가 생성됩니다.
Hero를 구성하는 Person, Weapon 객체들의 생성
  • 위 예제 에서는 IronMan만 로그를 찍었습니다.
  • HerosModule2provideIronMan()@Heros annotation으로 지정되어 있고, provideSuit()는 annotation이 없습니다.
  • 따라서 IronMan 객체는 한번만 생성되며, 이후 생성요청시 동일한 객체가 반환됩니다.
  • 반면, suit()는 생성요청때 마다 새로운 객체가 생성됩니다.

이처럼 Component와 Module의 생명주기는 scope annotation을 이용하여 묶을수 있습니다.

반응형