본문으로 바로가기
반응형

이글은 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

먼저 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

Gradle을 이용하여 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을  경우 아래 링크를 참고하시기 바랍니다.

https://github.com/google/dagger


Module 생성

Dagger 는 annotation으로 각각의 역할을 구분합니다.
따라서 여러가지의 annotation이 존재하는데, 하나씩 예를 들어가면 확장해가는 형태로 설명을 진행합니다.


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 생성

component는 객체를 생성하기 위해 제공되는 interface 입니다.
즉, 실제로 객체를 생성 해야하는 부분에서는 module이 아닌, 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 한 클래스를 자동 생성 합니다.


생성자를 이용한 객체 생성

Component에서 Hero 객체 자체를 생성합니다.
따라서 Hero 객체는 Dagger에 의해 생성되는 대상이며, Dagger가 Hero를 생성할때 필요한 생성자의 인자들을 채워 넣어야 합니다.
이를 Dagger에게 알려주기 위해서 아래와 같이 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()}")
    }
}

Hero 생성자에 @Inject annotation을 붙여줬습니다.


이로써 Dagger가 객체를 주입해야하는 위치를 선정해 줬고, 주입해야 하는 객체를 생성하는 방법을 Module에 기술했으며, 자동 생성하기 위해 caller에게 노출할 함수까지 component에 정의하여 전부 준비가 완료 되었습니다.


Dagger의 자동구성

Dagger를 이용하여 객체를 생성하는 부분을 작업하기에 앞서 Dagger작업한 Module과 Component를 연결하고 concrete 한 클래스를 생성해야 합니다.
따라서 프로젝트를 한번 rebuild해 줍니다.

IntelliJ의 경우 Build -> Rebuild Project를 클릭합니다.

Dagger library가 정상적으로 잘 import되었고, 코드에 오타가 없었다면 아래와 같은 클래스들이 자동 생성 됩니다.


이 클래스들에 대한 내용은 아래에서 따로 확인해 보겠습니다.


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()를 호출해도 상관은 없습니다.


자동생성 코드

Dagger에서는 annotation을 통해 자동으로 필요한 클래스를 생성하고 연결합니다.

먼저 HeroModule에 세개의 provider를 정의하여 새개의 클래스에 대한 객체 생성을 정의 했습니다.
이에 대해 Dagger는 각 객체를 생성하기 위한 Factory를 만듭니다.

세개에 대한 Factory가 자동생성되나, Person 생성에 대한 코드만 보겠습니다.

@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 합니다.


  1. 먼저 static 함수인 create()로 Factory 객체를 생성합니다. 이때 우리가 정의한 HeroModule을 인자로 전달 받습니다.
  2. 그리고 override된 get() 함수를 호출하면, 우리가 정의해 놓은 HeorModule에서 person 객체를 반환하는 함수를 호출합니다.
  3. 또한 static 함수로 Person 객체를 바로 반환하는 providePerson() 함수도 제공합니다.

이제 Component의 자동생성 코드를 확인해 봅니다.

@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

위에서는 한번에 Hero를 생성해서 반환했습니다.
Dagger는 객체의 주입을 세가지 형태로 제공합니다.
  • 생성자에 주입
  • 멤버 변수에 주입
  • 함수의 param에 주입
하지만 주로 사용되는건 constructor injection, 또는 member variable inject이며 method inject는 여러가지 이유에서 잘 사용하지 않습니다.

위 예제에서 생성자 주입에 대한 코드를 간단하게 멤버변수 inject으로 수정해 보겠습니다.


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)
}

모듈에 더이상 Hero 객체를 생성하는 provider는 필요하지 않으므로 주석처리합니다.

@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이 존재하며, 다음 포스팅에서 다루도록 하겠습니다. 


반응형