2022. 3. 17. 15:16ㆍAndroid/Library
- Dagger2란? Dependency Injection을 도와주는 Framework
- 구성요소 : annotation으로 구분
- 핵심요소
- @Module : 실제 객체를 생성해서 공급(@Provides)해주는 역할
- @Component : 객체를 생성하기 위해 제공되는 interface(객체 생성을 위한 통로)
- @Inject : 객체 생성 주입 대상을 알림
- 부수요소
- @Named : 객체 생성을 위해 리턴 타입만으로 대상을 판단할 수 없을 때 구분자로 사용
- @BindsInstance : Dagger내에서 객체 생성이 불가능한 경우, 외부로부터 입력받아 사용(예로, android의 context)
- @Singleton : 싱글톤(한 컴포넌트 내에서만 유효한 생명주기, 즉, 동일 컴포넌트를 재생성한 경우 서로 다름)
- @Subcomponent : 부모-자식 컴포넌트 관계를 형성하게 해주는 역할, Builder를 반드시 기술해야 접근 가능
- @Binds : 추상화(인터페이스 혹은 부모 타입으로 묶일 수 있는)가 가능한 @Provides 형태, 인자로 넘어오는 값은 생성자에 @Inject가 붙은 주입 대상이던지 @Provides로 생성이 명시되어 있어야 함
- 핵심요소
- 기본구조
import dagger.Module
import dagger.Provides
@Module
class HeroModule {
@Provides
fun providePerson(): Person = IronMan()
@Provides
fun provideWeapon(): Weapon = Suit()
@Provides
fun provideHero(person: Person, weapon: Weapon) = Hero(person, weapon)
}
import dagger.Component
@Component(modules = [HeroModule::class])
interface HeroComponent {
fun callHero(): Hero
}
import javax.inject.Inject
class Hero @Inject constructor(val person: Person, val weapon: Weapon) {
fun info() {
Log.d("doo", "name: ${person.name()} skill: ${person.skill()} | weapon:${weapon.type()}")
}
}
class Hero {
// val person = IronMan()
// val weapon = Suit()
// val hero = Hero(person, weapon)
val hero = DaggerHeroComponent.create().callHero()
// val hero2 = DaggerHeroComponent.builder().build().callHero()
@Inject
lateinit var weapon : Weapon
}
@Inject가 선언된 생성자와 @Module을 통해 주입 객체 생성 준비를 마치고, @Component에서 module 선언을 통해 주입 객체에 대한 접근을 위한 통로를 만든다.
사용을 위한 Class에서 @Component의 상속을 받아 자동으로 생성된 Dagger 컴포넌트를 통해 DI를 실행해주면 실제 객체 생성에 대한 코드는 사라지고 @Component에 선언된 메서드 혹은 @Inject로 주입대상으로 표시한 변수에 자동으로 주입이 된다.
사용예제#1. @BindsInstance
@Module
class HeroModule {
...
@Provides
@Named("hulk")
fun provideHulk(): Person = Hulk() ...
@Provides
@Named("heroHulkWihWeapon")
fun provideHeroHulkWithWeapon(@Named("hulk") person: Person, weapon: Weapon) =
Hero(person, weapon)
}
여기서 중요한 부분은 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
}
}
@BinsInstance annotation을 사용하여 setHulkWeapon() 이란 함수로 Weapon 객체를 넘겨 줄것이라는걸 Dagger에 알려주며, Builder가 재생성되어야 함을 알려주기 위해서 @Component.Builder 를 사용하도록 한다.
val hulkBuster = HulkBuster()
val hulkWithWeapon = DaggerHeroComponent.builder().setHulkWeapon(hulkBuster).build().callHulkWithWeapon()
사용예제#2. Provider<T> & Lazy<T>
@Module
class RichHeroModule {
@Provides
fun provideRichHero(person: Person) = RichHero(person)
@Provides
fun provideSuit(): Weapon = Suit()
}
@Component(modules = [RichHeroModule::class])
interface RichHeroComponent {
fun callRichHero(): RichHero
fun getWeapon(): Weapon
fun inject(main: Main)
@Component.Factory
interface Factory {
fun create(@BindsInstance person: Person): RichHeroComponent
}
}
import javax.inject.Inject
import javax.inject.Provider
class Main {
@Inject
lateinit var weaponProvider: Provider<Weapon>
@Inject
lateinit var lazyWeapon: Lazy<Weapon>
fun getRichHero() {
val person = IronMan()
val richHeroComponent =
DaggerRichHeroComponent.factory().create(person) richHeroComponent . inject (this)
val richHero = richHeroComponent.callRichHero()(1..10).forEach { _ ->
val weapon = weaponProvider.get()
val weapon2 = lazyWeapon.get() // 이 때 lazyWeapon의 객체가 생성되어 주입됨
richHero.weapons.add (weapon)
}
}
}
@Component는 Builder 또는 Factory 형태 두가지로 생성이 가능하다.
@Inject 대상인 weaponProvider의 타입을 보면, Provider<Weapon>인 것을 알 수 있는데 이는 Dagger에서 weapon의 실체를 만드는 Wrapper 자체를 넘겨받는 것으로, get() 메서드를 통해 계속해서 새로 생성된 weapon 객체를 전달받을 수 있다.
두번째 @Inject 대상인 lazyWeapon의 타입을 보면, Lazy<Weapon>인 것을 알 수 있는데 이는 지연 생성을 요청하는 타입으로, get()이 호출되었을 때 비로소 객체가 생성되어 주입된다.
사용예제#3. @Singleton
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class AvengersModule {
@Singleton
@Provides
fun provideAvengers(): Avengers {
return Avengers()
}
}
@Singleton
@Component(modules = [AvengersModule::class])
interface AvengersComponent {
fun getAvengers(): Avengers
}
// 동일
val avengers = DaggerAvengersComponent.create()
println ("avengers1:${avengers.getAvengers()} avengers2:${avengers.getAvengers()}")
// 다름
val avengers1 = DaggerAvengersComponent.create()
val avengers2 = DaggerAvengersComponent.create()
println ("avengers1:${avengers1.getAvengers()} avengers2:${avengers2.getAvengers()}")
우리가 흔히 자바에서 사용하는 singleton의 개념은 process와 동일한 생명주기를 같다.
따라서 Process가 살아있는한 동일한 객체를 반환한다.
하지만 Dagger에서 제공하는 @Singleton은 java에서의 singleton 개념과는 다르다.
즉 Component와 해당 객체는 생명주기를 같이 한다.
Dagger에서는 Scope이라는 개념을 제공한다.
따라서 Scope정의하여 생명주기를 같이 할 객체들을 묶을수 있다.
예를 들어 Android의 Activity, Service등의 기본 구성 Component들은 각각의 생명주기를 갖는다.
만약 Activity 내부에서 inject되는 멤버변수가 있고 이 멤버 변수의 생성을 Activity의 생명주기와 동일하게 유지하려고 할때 (Activity가 살아있는 동안은 내부에서 Dagger를 이용해 객체 생성시 동일한 객체가 반환되도록 사용) Scope을 이용할 수 있다.
사용예제#4. @Subcomponent
@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
}
@Module
class HeroSubModule {
...
@Provides
@Named("ironMan")
fun provideIronMan(): Person = IronMan()
@Provides
@Named("suit")
fun provideSuit(): Weapon = Suit()
@Provides
@Named("heroIronMan")
fun provideHeroIronMan(
@Named("ironMan") person: Person,
@Named("suit") weapon: Weapon
) = Hero(person, weapon)
}
@Subcomponent(modules = [HeroSubModule::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
}
}
AvengersComponent의 자식으로 종속시키기 위해 @SubComponent를 사용한다.
단 SubComponent의 경우 Builder를 정의해 놓지 않는다면, 외부에서 접근 할 수가 없다.
따라서 SubComponent의 경우 반드시 Builder를 생성해 놔야 한다. (정의해 놓지 않는다면 빌드자체가 실패)
SubComponent에는 각각의 Hero를 생성할 함수를 정의하고, 외부에서 Avengers 객체를 받았을때, 내부를 채워줄 inject 함수를 추가로 정의한다.
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()}")
부모 컴포넌트에서 Avengers 객체 생성 주입 -> Avengers 객체 생성 -> 자식 컴포넌트에서 넘겨받아 생성된 Avengers 객체 내의 멤버 변수에 객체 생성 주입
사용예제#5. @Scope
@Scope
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
annotation class Heros
@Module
class HeroSubModule {
...
@Provides
@Heros
@Named("ironMan")
fun provideIronMan(): Person = IronMan()
@Provides
@Heros
@Named("suit")
fun provideSuit(): Weapon = Suit()
@Provides
@Heros
@Named("heroIronMan")
fun provideHeroIronMan(
@Named("ironMan") person: Person,
@Named("suit") weapon: Weapon
) = Hero(person, weapon)
}
@Heros
@Subcomponent(modules = [HeroSubModule::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
}
}
사용예제#6. MultiBinding
- Map
@Module
class HerosMapModule {
@Provides
@IntoMap
@StringKey("ironMan")
fun provideWeaponsForIronMan(): Weapon {
return Suit()
}
@Provides
@IntoMap
@StringKey("captainAmerica")
fun provideWeaponsForCaptain(): Weapon {
return Shield()
}
@Provides
@IntoMap
@ClassKey(Shield::class)
fun providePersonForShield(): Person {
return CaptainAmerica()
}
@Provides
@IntoMap
@ClassKey(Suit::class)
fun providePersonForSuit(): Person {
return IronMan()
}
}
@IntoMap을 사용하여 Map에 넣을 정보라는걸 표기해 준 후에 @StringKey를 이용하여 키값을 정해 준다.
만약 키를 Class 형태로 사용하려면 @ClassKey를 사용한다.
@Component(modules = [HerosMapModule::class])
interface HerosMapComponent {
fun personMap(): Map<String, Weapon>
fun weaponMap(): Map<Class<*>, Person>
}
val mapComponent = DaggerHerosMapComponent.create()
println(mapComponent.personMap()["ironMan"]?.type())
println(mapComponent.personMap()["captainAmerica"]?.type())
println(mapComponent.weaponMap()[Suit::class.java]?.name())
println(mapComponent.weaponMap()[Shield::class.java]?.name())
- Set
@Module
class HerosSetModule {
@Provides
@IntoSet
fun provideIronMan(): Person = IronMan()
@Provides
@IntoSet
fun provideCaptainAmerica(): Person = CaptainAmerica()
@Provides
@IntoSet
fun provideHulk(): Person = Hulk()
@Provides
@ElementsIntoSet
fun provideWeapons() = setOf(Suit(), Shield(), HulkBuster())
}
@Component(modules = [HerosSetModule::class])
interface HerosSetComponent {
fun heroFriends(): Set<Person>
fun heroWeapons(): Set<Weapon>
fun inject(heroFriends: HeroFriends)
}
class HeroFriends {
@Inject
lateinit var heroFriends: Set<@JvmSuppressWildcards Person>
@Inject
lateinit var heroWeapons: Set<@JvmSuppressWildcards Weapon>
}
참고로 Inject을 받는 HeroModel에서 @JvmSupressWildcards 를 사용했다.
이는 kotlin에서 사용시 명확한 제너릭 타입을 요구하기 때문에 필요한 annotation이며, 이를 붙이지 않으면 rebuilt시 dagger에서 에러가 발생하여 컴파일되지 않는다.
val setComponent = DaggerHerosSetComponent.create()
val heroFriends = HeroFriends()
setComponent.inject(heroFriends)
heroFriends.heroFriends.forEach {
println("name: ${it.name}")
}
heroFriends.heroWeapons.forEach {
println("type: ${it.type}") }
println ("${setComponent.heroFriends().size}, ${setComponent.heroWeapons().size}")
'Android > Library' 카테고리의 다른 글
Lint - Android Studio 내장 Lint vs. Ktlint (0) | 2022.04.23 |
---|---|
Dagger2 사용법 (0) | 2022.03.18 |