본문 바로가기
개발 이야기/안드로이드 개발

DI(의존성 주입)은 무엇인가? 그리고 Koin DI를 적용하는 과정

by 정선한 2022. 9. 14.
728x90
반응형

저번의 MVVM과 같이 많은 실무 프로젝트에서 사용되는 Dependency Injection에 대해서 알아보겠습니다. Andorid에서도 좋은 App Architecture를 구현하기 위하여 권장하고 있는 원칙이기도 합니다. 다들 DI라고 부릅니다. 좋은 App Architecture를 위해서 AAC패턴을 적용하는 것처럼 DI도 같은 목적을 위해서 적용하기를 권장하는 것입니다.

  • 코드의 재사용성
  • 리팩토링의 용이성
  • 테스트 편의성

Android에서는 아래와 같은 코드를 제시하면서 DI에 대하여 소개하고 있습니다.

class Car {
    private val engine = Engine()
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val car = Car()
    car.start()
}
class Car(private val engine: Engine) {
    fun start() {
        engine.start()
    }
}

fun main(args: Array) {
    val engine = Engine()
    val car = Car(engine)
    car.start()
}

왼쪽 코드와 오른쪽 코드의 차이점. 그것을 명확하게 아는 것이 DI에 대해서 더 잘 이해할 수 있습니다.

그냥 따로따로 코드만 바라본다면,
”뭐가 다른거야? 결국 Engine클래스 와 Car클래스를 main함수에서 사용하는 코드 아니야?”

 

이렇게 생각하실 수도 있을 것 같습니다. 근본적으로 실행하는 것에 문제가 없다면 같은 결과 확인할 수 있기 때문입니다. 하지만, DI 의존성 주입이라는 단어에 대해서 다시 한번 생각해봅시다.

제가 코드에서 밑줄 그은 부분을 다시 보시면, Car Class내부에서 Engine Class를 정의하느냐, 혹은 main함수 내에서 Engine Class와 Car Class를 정의해서 Car Class의 매개변수로 Engine Class를 넘겨주느냐의 차이점을 보실 수 있습니다.

이때 매개변수로 Car Class에 Engine 객체를 넘겨주는 것이 DI 종속성 삽입입니다. Car Class내부에서 Engine 객체를 구성하는 것이 아니라 Engine 객체 자체를 생성자의 매개변수로 받습니다. 이렇게 되면 Car는 Engine에 종속된 상태가 됩니다. 이 상태에서는 Engine Class에 변경 사항이 생기거나 Sub Class로 변경하여 적용할 때에 Car Class에 영향이 없이 코드를 재 사용할 수 있습니다. 객체만 변경해서 Car Class에 전달해 주면 되니까요.

실제 Android 프로젝트에서는 이런 방식으로 의존성을 사용하지는 않고 라이브러리를 사용하여 관련 내용들을 구현합니다. 위에서 처럼 라이브러리를 이용하지 않고 구현한 종속성 주입 코드를 수동 의존성 주입이라는 이름으로 안드로이드에서 소개하고 있습니다.

하지만 실제 프로젝트에서는 많이 들어보셨을 Dagger, KoinDI 그리고 요즘 Android에서 많이 밀고 있는 Hilt 프레임워크 등을 선택하여 DI와 관련한 구현을 진행하게 됩니다. 저의 개인적인 경험으로는 Koin DI를 통해 주로 구현을 하였었고, 프로젝트의 성향에 따라서 적절한 라이브러리를 선택하여 의존성 주입을 구현하면 될 것 같습니다. “뭐가 더 좋고 나쁘다”의 결정 사항이 아니라 각 상황에 맞는 가장 좋은 모델을 사용하는 것이 정답이 될 것 같습니다.

아래에 대략적인 큰 차이점들을 작성하여 두었습니다.

Dagger Koin DI Hilt
  • 학습곡선이 큼
  • Error 발생시점 : CompileTime
  • Koin DI와 동작방식의 차이가 있기 때문에 Error 발생시점의 차이가 발생
  • Kotlin Domain Specific Language기반으로 만들어진 프레임워크
  • Kotlin 기반 환경에서 빠르게 도입가능
  • Error 발생시점 : RunTime
  • Dagger를 기반으로 만들어진 프레임워크
  • Error 발생시점 : CompileTime

저는 저에게 가장 익숙한 Koin DI 프레임워크를 이용하여 저번에 작성했던 mvvm코드에 이어서 관련 코드를 추가해 보려고 합니다.

위의 웹 사이트에서 간단한 Koin 적용 법을 확인하실 수 있습니다.

일단 라이브러리를 사용하기 위한 과정은 아래와 같습니다.
저는 라이브러리의 버전을 따로 관리하기 위하여 koin_version을 통해 따로 지정해 주었습니다.

현재 최신 버전은 3.2.0 version입니다.

buildscript {
    repositories {
        mavenCentral()
    }
}
ext {
    koin_version = '3.2.0'
}
dependencies {
	...
	// Koin for Android
    implementation "io.insert-koin:koin-android:$koin_version"
}

Application() 클래스를 상속받아서 그 내부에 Koin DI를 실행시키는 코드를 작성합니다.
startKoin 키워드를 사용하여 Koin을 실행시키고 modules()를 통하여 컨테이너에 로드할 module 목록을 지정하여 주는 코드입니다.

package com.dev_seonhan.mvvm

import android.app.Application
import com.dev_seonhan.mvvm.di.appModule
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class BaseApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
			androidLogger()
            androidContext(this@BaseApplication)
            modules(appModule)
        }
    }
}

아래와 같이 module에 대한 목록을 작성해주면 되는데 이때 두 가지의 작성 방법을 사용하여 선언할 수 있습니다.

Classical DSL Way Constructor DSL Way
val appModule = module {

    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // Simple Presenter Factory
    factory { MySimplePresenter(get()) }
}
val appModule = module {

    // single instance of HelloRepository
    singleOf(::HelloRepositoryImpl) { bind<HelloRepository>() }

    // Simple Presenter Factory
    factoryOf(::MyPresenter)
}

약간의 표현의 차이를 볼 수 있습니다.

저는 TextUtil Interface를 생성하고 TextUtilImpl class를 생성하여 위와 같이 종속성 주입을 해주었습니다.

package com.dev_seonhan.mvvm.di

import com.dev_seonhan.mvvm.utils.TextUtil
import com.dev_seonhan.mvvm.utils.TextUtilImpl
import org.koin.dsl.module

val appModule = module {
    single<TextUtil> { TextUtilImpl() }
}

single, factory, scoped의 컴포넌트를 이용하여 객체 인스턴스의 제공 방식을 지정할 수 있습니다.
아래에 해당 컴포넌트들의 간단한 내용들을 정리하여 두었습니다

single factory scoped
  • 전체 컨테이너에 영속적인 객체를 생성
  • 해당 객체를 싱글톤으로 제공 (단일 인스턴스)
  • 클래스의 인스턴스를 by inject() , get() 으로 요청할 시 싱글톤의 인스턴스를 제공
  • 요청할 때 마다 새로운 인스턴스를 생성하여 제공
  • Dagger의 Provider와 비슷한 개념
  • factory 컴포넌트로 제공되는 객체는 컨테이너에 저장하지 않기 때문에 재참조가 불가능
  • 명시된 scope 생명주기에 영속적인 객체를 생성
  • Dagger의 Scope와 비슷한 개념
  • 해당 컴포넌트를 이용하기 위하여 먼저 scope() 함수를 통해 범위를 지정
  • scope의 이름 지정을 위하여 Qulifiernamed가 필요
    문자열 한정인지, 타입 한정인지 구별
    • StringQulifier
    • TypeQulifier

해당 컴포넌트들의 간단한 내용들을 정리하여 두었습니다.

위에서 간단하게 컨테이너에 담길 모듈을 single 컴포넌트를 이용하여 lazy 하게 생성하도록 하였는데, 이것을 필요한 곳에서 by inject(), get()하여 객체를 받아오면 됩니다.

val mtestTextUtilFirst: TextUtil by inject()

위와 같이 by inject()를 이용하여 변수의 선언 시에 lazy하게 호출 시 생성하는 방법이 있고, 아래에 get()을 이용하여 바로 주입받는 방법으로 이용이 가능합니다.

override fun onCreate(savedInstanceState: Bundle?) {
	super.onCreate(savedInstanceState)
	val mTestTextUtilSecond: TextUtil = get()
}

이렇게 하면 간단하게 의존성 주입이 무엇인지, 그리고 Koin DI 프레임워크를 이용한 DI적용 방법에 대해서 간단하게 요약해보았습니다.
이 글이 모든 내용을 담고 있지는 않기 때문에 실제의 프로젝트에서는 더 상세한 내용들이 필요하겠지만, 그것들 또한 차차 다뤄보도록 하겠습니다.

 

 

728x90
반응형