본문 바로가기
Mobile Development/Android

[Android] Dependency Injection (a.k.a DI) - 1. 뭔데? 왜 하는데? Dagger? Koin? Hilt?

by 두잇뚜 2020. 10. 24.
반응형

최근 개발자 채용공고를 보면 자격요건 혹은 우대사항에 DI가 심심치 않게 나타난다.

이말은 곧 DI라는 것이 개발에 있어 꽤 중요한 부분으로 인식되고 있다는 증거다.

나 또한 회사에서의 개발, 사이드 프로젝트에서 DI를 활용하고 있다.

이번 포스팅은 DI가 무엇이고 왜 쓰는지를 예시와 함께 알아 볼 것이다.


- DI가 뭐야?

DI는 "Dependency Injecton" 의 줄임말이며 "의존성 주입" 이라는 의미로 해석이 된다.

의존성이란 A라는 객체(클래스)가 B라는 객체(클래스)를 사용한다는 의미로 이해하자.

이해를 돕기 위해 아래의 예시를 보자.

class SomeDataRepository {
    private val source: SomeDataSource = SomeDataSource()
}

class SomeDataSource {

}

 SomeDataRepository 내부에 SomeDateSource 객체를 생성하여 갖고 있다. 즉, SomeDataRepository 객체는 SomeDataSource객체에 의존성을 갖고 있다.

자, 의존성은 이제 알았고 "의존성 주입" 은 무엇일까? "주입" 은 무엇을 무엇에 넣는다는 느낌이다.

(주입식 교육을 생각해보자)

이제 위 의존성에 대한 코드를 "의존성 주입" 코드로 변경해보자. 아래 예시를 보자.


class SomeDataRepository(private val source: SomeDataSource) {

}

class SomeDataSource {

}

 

 

변경된 부분은 클래스 내부에서 생성하는 SomeDataSource 객체의 위치를 생성자로 이동시켰다. 이 경우, SomeDataRepository를 생성하기 위해선 외부에서 생성자에 SomeDataSource를 생성하여 넣어 주어야 한다.

val someDataSource = SomeDataSource()
val someDataRepository = SomeDataRepository(someDataSource)

위와 같이 말이다.


이 같은 것을 "의존성 주입" 이라고 한다.


- 왜 하는데?

나도 처음엔 왜 하는지 이해 못했다. 어차피 쓸거 안에서 생성하도록 두면 매번 생성할 때 마다 생성자에 넣는 귀찮은 짓을 굳이 왜!? 라고 생각했다.

하지만 이번 포스팅에서, 그리고 이후에 진행할 포스팅에서 왜 하는지가 납득이 되길 바란다.

자 우선 코드를 아래와 같이 변경해보겠다.

class SomeDataRepository(private val source: SomeDataSource) {
    fun loadData(): SomeData? = source.loadData()

    fun saveData() { source.saveData(null) }
}

data class SomeData(
    val data: String
)

interface SomeDataSource {
    fun loadData(): SomeData?
    fun saveData(data: SomeData?)
}

class SomeRemoteDataSourceImpl: SomeDataSource {
    override fun loadData(): SomeData? = null

    override fun saveData(data: SomeData?) {}
}

class SomeLocalDataSourceImpl: SomeDataSource {
    override fun loadData(): SomeData? = null

    override fun saveData(data: SomeData?) {}
}

 

안드로이드를 개발하다보면 API 통신을 통해 네트워크에서 데이터를 받아올 수도, 혹은 SQLite를 사용하는 등의 로컬에서 데이터를 받아올 수도 있다.

두 가지 모두 행위 자체는 같을 수 있다. 이럴 때 생성자로부터 주입받는 것이 아닌 내부 객체에 있으면 어떻게 할 것인가? 두 DataSource 객체를 모두 생성하거나 플래그를 넣거나 등등 방법은 있겠지만 제어가 쉽지는 않을 것이다.

그리고 만약 테스트 코드를 작성한다면? 직접 API를 호출하거나 데이터베이스에서 읽을 것인가? 쉽지 않을 것이다.

그럼 위 코드는 어떠한가?

데이터를 획득하는 행위를 인터페이스로 추상화하고 Remote인지, Local인지에 따라 구현을 다르게 하고, 인터페이스 구현체를 생성자에 넣는다.

즉 상황에 따라 다른 구현체를 주입한다. 상황에 따라 코드를 수정할 필요도 없고 SomeDataRepository도 영향을 받지 않는다.

어느정도 느낌이 오는가? 없어도 괜찮다. 나도 그랬다.


- Dagger? Koin? Hilt?

Android의 DI에 대해 검색해본 경험이 있거나 개발 한 경험이 있다면 위 세 가지에 대해 들어보았을 것이다.

위는 DI를 위한 프레임워크이다. (라이브러리 라고 봐야할까?)

직접 DI를 구현했는데 매번 객체를 생성자에 주입하기 위해 똑같은 코드를 작성해야 한다. 만약 Retorift을 사용한다면 okHttpClinet를 생성하면서 길고 긴 코드를 작성하게 될 것이다. 즉, 보일러 플레이트 코드가 생긴다.

그리고 어떤 객체는 한번 생성하면 새로 생성할 필요가 없는 Singletone 객체일 필요도 있다.

다 좋은데, 이런 부분을 어떻게 해결할 것인가에 대한 답이 DI Framework다.

Dagger는 developer.android.com/training/dependency-injection/dagger-android?hl=ko를 참고하면 좋다.

Dagger는 학습곡선이 높고 처음 환경을 갖추기가 굉장히 어렵고, Android에서 사용하기에 적합하지 않다는 평가도 있다. 하지만 구글에서 밀고 있다. (안드로이드 스튜디오가 업데이트 되면서 Dagger 그래프 시각화를 제공할 만큼)

또한 앞으로 포스팅할 Hilt는 Dagger를 기반으로 Android에서 사용하기 편리한 라이브러리로 변경되었다.

Dagger를 사용한 예제는 패스하고 Koin과 Hilt를 사용한 예제만 작성 할 것이다. (귀찮다.)

이제 다음에서 MVVM으로 기반 코드를 작성하고 Koin, Hilt를 사용하여 DI를 구현하는 예제를 포스팅하겠다.

댓글0