의존성 주입(DI) & Dagger2

먼저 의존성에 대해서 알아보자.

class BClass{
    var number : Int = 1
}

class AClass{
		//내부에 변수로 BClass를 사용한다.
    var internalVarialbe = BClass()
}

val a = AClass()

fun test(){
    print(a.internalVarialbe)
}

위 상황에서는 AClass가 내부에 internalVariable을 정의하면서 BClass를 사용한다.
이럴때 A클래스는 B클래스에 의존한다고 한다.
쉽게 말하면 한 객체에서 다른 객체를 쓰면 그게 의존성이다.

A클래스가 B클래스를 의존한다면 A클래스 내부에서 B클래스를 생성하는 상황인것이다.

이걸 만약 의존성 주입으로 바꾼다면 다음과 같다.

주입은 내부가 아니라 외부에서 객체를 생성해서 넣어주는 것을 의미한다.
외부에서 Class B를 생성해서 넘겨 받는 것을 의미한다.

생각보다 단순하다.
의존성이란, 한 객체가 내부에 다른 객체를 호출해서 사용하게 되면 의존성이 생기는 것이고
의존성 주입이라는 것은 다른 객체를 호출할때 내부에서 생성하지 않고 외부에서 생성해서 넘겨 받는 것이다.


Dagger

Dagger는 김태호의 커니의 코틀린의 유명한 예제를 가져왔다.

햄버거만들기!

먼저 빵종류와 패티를 정해야 한다.
그렇기 위해서는 빵 객체와 패티의 객체가 필요하다.

빵 인터페이스를 만들고 내부에 getBun()을 넣는다.

interface Bun{
    fun getBun() : String
}

동일한 방법

interface Patty {
    fun getPatty(): String
}

이제 두개를 합칠 버거를 만들자.
버거안에는 Bun과 Patty를 받아와서 내가 받아온 Bun과 Patty가 무엇인지를 알려줄 예정이다.


open class Burger(var bun: Bun, var patty: Patty) {
    fun info(){
        Log.d("test","Bun:${bun.getBun()}  patty : ${patty.getPatty()}")
    }
}

안드로이드에서 예제를 진행하고 있으므로 다음과 같은 코드가 작성된다.

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val firstbun = FirstBun()
        val firstpatty = FirstPatty()

        val mymenu = Burger(firstbun,firstpatty)

        mymenu.info()
    }
}

//번과 패티의 종류를 여기서 지정해준다.
class FirstBun : Bun {
    override fun getBun() = "호밀"
}

class FirstPatty : Patty{
    override fun getPatty() = "불고기"
}

간단하게 의존성 주입을 해보았으니 Dagger를 적용시켜보자.

implementation "com.google.dagger:dagger:$daggerVersion"
implementation "com.google.dagger:dagger-android:$daggerVersion"
implementation "com.google.dagger:dagger-android-support:$daggerVersion"

kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"

//혹신나 kapt를 플러그인하지 않으셨다면
plugins {
		id 'kotlin-kapt'	// 추가
}

Module

먼저 모듈을 만들어야 한다.
객체를 생성해서 공급을 해주는 역할을 한다.

클래스명이나 함수명은 어떤걸 사용해도 상관은 없지만 CodeConvention에서는
Module이 suffix로, 함수명은 provide가 prefix로 붙도록 권장하고 있습니다.

@Module
class BurgerModule {

    @Provides
    fun provideBun(): Bun = FirstBun()
    
    @Provides
    fun providePatty(): Patty = FirstPatty()
    
    @Provides
    fun provideFirstMenu(bun : Bun, patty: Patty) = Burger(bun,patty)
}

대거를 사용하면 왼쪽에 종속관계가 나타난다.

FirstMenu는 Bun과 Patty를 종속받고있다는 표시이다.


Component

이후에 Component를 생성한다.

component는 객체를 생성하기 위해 제공되는 interface이다.
즉, 실제로 객체를 생성해야 하는 부분에서는 module이 아닌, component를 호출하여 객체를 생성한다.

@Component(modules = [BurgerModule::class])
interface BugerComponent {
    fun callBurger():Burgers
}

annotation을 달아주고 modules에 어떤 모듈을 부착할지 설정해준다.
다수의 module인 경우 ","를 넣어 구분해준다.


다음은 생성자를 이용하여 객체를 생성해준다.

bun과 patty를 생성자로 받아서
실행할 함수를 구체화시켜준다.

생성자에 @Inject를 붙여준다.

class Burgers @Inject constructor(val bun : Bun,  val patty : Patty){
    fun info(){
        Log.d("test","Bun:${bun.getBun()}  patty : ${patty.getPatty()}")
    }
}

지금까지 한 것을 정리해보자.
Dagger가 객체를 주입해야 하는 위치를 선정해주었고, 주입해야 하는 객체를 생성하는 방법을 Module에 기술했으며 자동 생성하기 위해 caller에게 노출할 함수까지 component에 정의하여 준비를 하였다.


Dagger를 이용한 객체 생성

최종적으로 mymenu처럼 DaggerBugerComponet를 통해 호출할 수 있다.


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

//        val firstbun = FirstBun()
//        val firstpatty = FirstPatty()
//
//        val mymenu = Burger(firstbun,firstpatty)
//
//        mymenu.info()
        val mymenu = DaggerBugerComponent.create().callBurger()
    }
}

class FirstBun : Bun {
    override fun getBun() = "호밀"
}

class FirstPatty : Patty{
    override fun getPatty() = "불고기"
}

막상 실습을 하곤나니 그렇게 복잡하고 접근하지 못할만큼 어려운 개념은 아니였다.
하지만 이걸 실무에 사용하기에는 아직 갈 길이멀고
결론적으로는 Hilt를 위한 발판이기에 조금 더 알아보고 넘어가자.


Dagger는 객체의 주입을 세가지 형태로 제공한다.

  • 생성자에 주입
  • 멤버 변수에 주입
  • 함수의 params에 주입

하지만 주로 사용되는건 1,2번째이다.

우리가 한 것은 1번째이므로 2번째를 진행해 보자.

class Burgers{
    @Inject
    lateinit var bun : Bun
    @Inject
    lateinit var patty:Patty
    fun info(){
        Log.d("test","Bun:${bun.getBun()}  patty : ${patty.getPatty()}")
    }
}

모듈

이전과 달라진 것은 Burgers객체를 생성하는 provider는 필요없다.

@Module
class BurgerModule {

    @Provides
    fun provideBun(): Bun = FirstBun()

    @Provides
    fun providePatty(): Patty = FirstPatty()

//    @Provides
//    fun provideFirstMenu(bun : Bun, patty: Patty) = Burgers(bun,patty)
}

Component에서는 provider가 더이상 필요없다


@Component(modules = [BurgerModule::class])
interface BugerComponent {
//    fun callBurger():Burgers
    fun inject(burger : Burgers)
}

실제 호출

				val burger = Burgers()
        val mymenu = DaggerBugerComponent.create().inject(burger)

작성하다보니 레고와 비슷하다는 생각이 들었다.
팔을 만들고 잡는다는기능이 있다.
다리를 만들고 걷는다는 기능이 있다.
몸통이 있고 중심을 잡는다는 기능이 있다.

모듈에서 팔과 다리 몸통을 호출하고
팔,다리,몸통은 어떤 팔 다리 몸통인지 지정한 후에
맨 아랫줄에서 전부 합친다.

모듈을 들고 직접 돌아다니기에는 위험하기 때문에 Component를 통해 간접적으로 호출한다.
그리고 호출하는 객체가 하는 역할은 따로 관리 해준다.
다리로 걸어다니면서 팔로 물건을 잡고 몸통은 안쓰러지게한다 ← 식으로?

장점

  • Unit Test가 용이해진다.
  • 코드의 재활용성이 높아진다.
  • 객체간의 의존성을 줄일 수 있다.
  • 객체간의 결합도가 낮아진다.

좋은 웹페이지 즐겨찾기