이글은 Dagger 2 v2.25.2 를 기반으로 설명하며, Kotlin으로 예제 코드를 설명합니다.
Dagger는 Dependency Injection을 도와주는 Framework 입니다.
하나의 application은 각각의 역할을 분담하는 class들로 이루어 지고, 이 class들이 서로 상호관계를 이루며 동작합니다.
따라서 class간 서로 의존성을 가질수 밖에 없으나, 이런 의존성들이 tight할 경우 변경에 취약하다는 단점이 있습니다.
이런 단점들을 극복하기 위해서 객체간 loose 한 coupling을 갖도록 서로에 대한 의존성을 낮게 하는것이 좋으며, 객체 생성부분을 위임함으로써 이런 원론적인 개념을 도와주는게 Dagger 입니다.
특히나 DI를 이용하면 Test를 진행하는데 수월해 집니다.
개인적으로는 테스트 코드를 작성하지 않는 프로젝트라면 Dagger를 이용하여 의존성을 주입하는 방식이 큰 장점이 있다고는 생각되지 않습니다만, 원론적인 Class 설계방식에서 지향하는 부분과 동일하다는 관점에서는 적용해 볼만 합니다.
다만 여기서는 Dagger의 장점, 써야하는 이유를 다루기 보다는 사용하는 방법에 대하여만 다루려고 합니다.
(사실 Dagger를 이용하는것 자체만으로도 쉽지 않은 작업이기 때문입니다.)
Dependency Injection Sample
interface Person {
fun name(): String
fun skill(): String
}
interface Weapon {
fun type(): String
}
class Hero(private val person: Person, private val weapon: Weapon) {
fun info() {
Log.d("doo", "name: ${person.name()} skill: ${person.skill()} | weapon:${weapon.type()}")
}
}
Hero라는 클래스는 생성자로 Person과 Weapon를 인자로 받습니다.
만약 우리가 아이언맨을 생성하고 싶다면 아래와 같이 생성해야 합니다.
class IronMan: Person {
override fun name() = "토니 스타크"
override fun skill() = "수트 변형"
}
class Suit : Weapon {
override fun type() = "수트"
}
fun main(args: Array) {
val person = IronMan()
val weapon = Suit()
val hero = Hero(person, weapon)
}
main() 에서 Hero라는 클래스를 만들기 위해서 직접 아이언맨과 수트를 생성하여 hero에 넘겨줬습니다.
이 동작중에 아이언맨과 수트를 직접 생성하는 부분을 Dagger에게 위임하여 Hero 클래스를 생성하거나, 아예 Hero 클래스의 생성까지도 Dagger에 위임할 수 있습니다.
전자의 경우 hero는 person과 weapon이 어떻게 생성되는지 알 필요가 없으며, hero까지 dagger가 생성해 준다면, hero에 어떤 인지가 들어가야하는지 조차 알 필요가 없어집니다.
그럼 Dagger를 이용하여 가장 심플하게 작업해 보겠습니다.
Library import
implementation "com.google.dagger:dagger:$dagger2_version"
kapt "com.google.dagger:dagger-compiler:$dagger2_version"
// java project인 경우
//annotationProcessor "com.google.dagger:dagger-compiler:$dagger2_version"
$daager2_version 변수에는 실제 버전명을 넣으면 됩니다. (현재 최신버전은 2.25.2 입니다.)
저는 kotlin을 사용하기 때문에 dagger-compiler에 kapt를 사용합니다.
java를 사용한다면 주석처리된 문구를 사용하면 됩니다.
maven을 경우 아래 링크를 참고하시기 바랍니다.
Module 생성
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)
}
Hero객체를 생성하기 위해서는 아이언맨과 수트가 필요 합니다.
따라서 위와 같이 해당 객체를 반환해주는 함수를 생성하고 @Provides 를 붙입니다.
또한 hero 객체 역시 생성이 가능하도록 @Provides 를 제공해 줍니다.
"Hero에 인자로 들어가는 객체는 어떻게 지정하지?" 란 의문점이 있을수 있으나, @Provides로 제공하는 함수들의 반환타입을 보고 Dagger 알아서 해당 함수를 찾아 호출하여 객체를 생성해서 넣어줍니다.
Hero 객체를 생성해주는 Module이라는 의미로 @Module을 class 단위에 붙여주고, 각각의 객체생성 함수에는 @Provides를 붙여 줍니다.
클래스명이나 함수명은 어떤걸 사용해도 상관은 없습니다.
다만 클래스명에는 Module이 suffix로, 함수명은 provide가 prefix로 붙도록 권장하고 있습니다.
Component 생성
import dagger.Component
@Component(modules = [HeroModule::class])
interface HeroComponent {
fun callHero(): Hero
}
component 역시 @Component annotation을 class 레벨에서 달아 줍니다.
이때 annotation의 인자값으로 어떤 모듈을 이용할지 모듈 정보를 "modules="에 넣어 줍니다.
여기서는 한개만 넣었으나, 다수의 Module인 경우 "," 로 구분하여 넣어줍니다.
interface의 구성요소로는 외부에서 객체를 생성하기 위해 호출해야 할 함수를 정의 합니다.
함수명은 상관없으며, 생성할 객체가 Hero이기 때문에 return값이 Hero인 abstract 함수를 하나 만들어 둡니다.
component는 interface로 구성됩니다.
따라서 Dagger가 해당 구성요소를 보고 실제 concrete 한 클래스를 자동 생성 합니다.
생성자를 이용한 객체 생성
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()}")
}
}
Hero 생성자에 @Inject annotation을 붙여줬습니다.
이로써 Dagger가 객체를 주입해야하는 위치를 선정해 줬고, 주입해야 하는 객체를 생성하는 방법을 Module에 기술했으며, 자동 생성하기 위해 caller에게 노출할 함수까지 component에 정의하여 전부 준비가 완료 되었습니다.
Dagger의 자동구성
이 클래스들에 대한 내용은 아래에서 따로 확인해 보겠습니다.
Dagger를 이용한 객체 생성
호출하는 부분에서는 Component를 이용해서 객체를 생성합니다.
이미 Rebuild를 이용해서 Dagger가 Component를 구체화 시켰으며, concrete한 클래는 DaggerXXX란 이름으로 생성됩니다.
예제에서는 HeroComponent interface를 만들었으니 Dagger는 위의 그림과 같이 DaggerHeroComponent란 class를 자동 생성합니다.
// val person = IronMan()
// val weapon = Suit()
//
// val hero = Hero(person, weapon)
val hero = DaggerHeroComponent.create().callHero()
// val hero2 = DaggerHeroComponent.builder().build().callHero()
실제로 기존 생성 코드가 아래 한줄로 변경됩니다.
val hero = DaggerHeroComponent.create().callHero()
Component는 builder pattern을 이용하기 때문에 create() 대신에 마지막에 주석으로 처리해 놓은 builder().build()를 호출해도 상관은 없습니다.
자동생성 코드
@SuppressWarnings({
"unchecked",
"rawtypes"
})
public final class HeroModule_ProvidePersonFactory implements Factory<Person> {
private final HeroModule module;
public HeroModule_ProvidePersonFactory(HeroModule module) {
this.module = module;
}
@Override
public Person get() {
return providePerson(module);
}
public static HeroModule_ProvidePersonFactory create(HeroModule module) {
return new HeroModule_ProvidePersonFactory(module);
}
public static Person providePerson(HeroModule instance) {
return Preconditions.checkNotNull(instance.providePerson(), "Cannot return null from a non-@Nullable @Provides method");
}
}
먼저 "HeroModule_ProvidePersonFactory" 이름의 factory가 생겼습니다.
이는 Factory interface를 상속받았고, get() 함수를 override 합니다.
- 먼저 static 함수인 create()로 Factory 객체를 생성합니다. 이때 우리가 정의한 HeroModule을 인자로 전달 받습니다.
- 그리고 override된 get() 함수를 호출하면, 우리가 정의해 놓은 HeorModule에서 person 객체를 반환하는 함수를 호출합니다.
- 또한 static 함수로 Person 객체를 바로 반환하는 providePerson() 함수도 제공합니다.
@SuppressWarnings({
"unchecked",
"rawtypes"
})
public final class DaggerHeroComponent implements HeroComponent {
private final HeroModule heroModule;
private DaggerHeroComponent(HeroModule heroModuleParam) {
this.heroModule = heroModuleParam;
}
public static Builder builder() {
return new Builder();
}
public static HeroComponent create() {
return new Builder().build();
}
@Override
public Hero callHero() {
return HeroModule_ProvideHeroFactory.provideHero(heroModule,
HeroModule_ProvidePersonFactory.providePerson(heroModule),
HeroModule_ProvideWeaponFactory.provideWeapon(heroModule));}
public static final class Builder {
private HeroModule heroModule;
private Builder() {
}
public Builder heroModule(HeroModule heroModule) {
this.heroModule = Preconditions.checkNotNull(heroModule);
return this;
}
public HeroComponent build() {
if (heroModule == null) {
this.heroModule = new HeroModule();
}
return new DaggerHeroComponent(heroModule);
}
}
}
먼저 Builder pattern이 적용되어 있습니다.
Builder는 HeroModule을 공급받거나 아니면 직접 생성하여 DaggerHeroComponent 객체를 생성합니다.
DaggerHeroComponent 클래스는 예제에서 정의한 HeroComponent를 구현했기 때문에 callHero() 함수가 Override 되어 있습니다.
callHero() 함수는 Hero 함수를 생성하여 return하며, 이때 필요한 인자들은 각 Class factory에서 static 함수를 호출하여 생성합니다.
멤버변수 injection
- 생성자에 주입
- 멤버 변수에 주입
- 함수의 param에 주입
class Hero {
@Inject
lateinit var person: Person
@Inject
lateinit var weapon: Weapon
fun info() {
Log.d("doo", "name: ${person.name()} skill: ${person.skill()} | weapon:${weapon.type()}")
}
}
먼저 Hero 클래스의 생성자를 제거하고, 주입되어야 할 멤버변수에 @Inject 을 달아 놓습니다.
@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
fun inject(hero: Hero)
}
Component 역시 Hero 객체 자체를 생성하는 function은 주석으로 막습니다.
다만 외부에서 Hero 객체를 전달받았을때, 그 내부의 멤버변수를 체워주기 위해 호출할 inject 함수를 하나 만듭니다.
(함수명은 상관없습니다.)
실제 호출 부분에서는 아래와 같이 호출 합니다.
val hero = Hero()
DaggerHeroComponent.create().inject(hero)
사실 위에서 언급한 기본적인 내용만으로는 Dagger를 충분히 사용하기에는 부족합니다.
기본적으로 @Component, @Module, @Provider, @Inject에 대한 가장 기본적인 annotation에 대해 다루었으나, 그외에도 다양한 기능을 제공하는 annotation이 존재하며, 다음 포스팅에서 다루도록 하겠습니다.
'개발이야기 > Kotlin' 카테고리의 다른 글
[Dagger 2] Provider injection, Lazy Injection #3 (0) | 2020.06.03 |
---|---|
[Dagger 2] Qualifier, instance binding 사용, @Named, @BinsInstance #2 (1) | 2020.06.01 |
[RxKotlin] Reactive 코틀린 #12 - Custom operators (2) | 2019.12.23 |
[RxKotlin] Reactive 코틀린 #11 - using을 이용한 자원 해제 (0) | 2019.12.23 |
[RxKotlin] Reactive 코틀린 #10 - 병렬처리를 위한 Scheduler (2) | 2019.12.20 |