[Android] DI 를 하는 이유와 Hilt 와 Koin 의 차이에 대해 알아보자

오규성·4일 전
0

기존에 작성한 티스토리 글을 옮겨온 것입니다.


# DI 란?

의존성 주입 (Dependency Injection) 의 줄임말.
A 객체가 B 객체의 기능을 사용해야 한다면 A는 B에 의존하는 관계인데, 이러한 객체 간의 의존 관계를 외부에서 설정하고 주입하는 디자인 패턴이다.

위의 이미지로 예시를 들어보자

Car class 의 내부에서 Engine class 를 사용해야 하므로 이 둘은 의존 관계이다.

이를 코드로 나타내보면 다음과 같다.

class Engine {
    fun start() = println("the car moves forward")
}

class Car {
    private val engine = Engine()

    fun start() {
        engine.start()
    }
}

fun main() {
    val car = Car()
    car.start()
}

하지만 이 코드는 좋지 못한 코드이다.

위처럼 내부에서 인스턴스를 생성하여 사용하게 되면, 종속성이 매우 높아져 테스트하기가 어려워진다.

또한 "차가 앞으로 나아간다" 가 아닌, "뒤로 나아간다" 로 설정해주고 싶다면 Engine 클래스의 start() 를 직접 수정해줘야 하는데, 이것은 SOLID 원칙OCP (기존 코드를 변경하지 않아도 수정이 가능해야함) 에 위배된다.

우리는 이러한 원리를 바탕으로 의존 역전 원칙 (DIP)DI 를 활용하여 Car 내부에서가 아닌 외부에서 추상화된 인터페이스를 주입받고 이를 활용하는 것이 바람직하다.

그럼 이제 위 이미지와 같이 코드를 변경해보도록 하자.

interface Engine {
    fun start()
}

class ForwardEngine: Engine {
    override fun start() = println("the car moves forward")
}

class BackwardEngine: Engine {
    override fun start() = println("the car moves backward")
}

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

위의 코드대로 작성한다면 사용하는 함수마다 이를 서로 다르게 호출하여 사용할 수 있다.

fun forwardEngineTest() {
    val car = Car(ForwardEngine())
    car.start() // the car moves forward
}

fun backwardEngineTest(){
    val car = Car(BackwardEngine())
    car.start() // the car moves backward
}

이렇게 설정하는 경우 OCP, DIP 에도 위배되지 않을 수 있고 자신이 필요한 기능이 존재하다면 기존 클래스의 수정없이 새로운 클래스를 구현하여 인스턴스를 생성하면 되므로 테스트와 유지 보수에 많은 도움이 된다.

참고로 종속 삽입 방법은 위처럼 생성자를 통한 전달 방법이 있고, 다른 방법으로는 내부 필드에 전달하는 방법이 있는데 주로 추천하는 것은 생성자를 통한 전달 방법이다.
코드가 명시적이고 lateinit var 과 달리 내부에서 수정할 수 없게 Immutable 한 상태이기 때문이다.

이제 DI 에 대해서 어느정도 설명을 하였으니, 의존성 주입 라이브러리로 유명한 Dagger Hilt 와 Koin 에 대해서 간단한 설명을 하고 차이점에 대해 알아보도록 하자

# 의존성 주입 라이브러리를 사용하는 이유?

객체를 매번 생성하며 의존 관계를 직접 수동으로 연결해야 하는 직접적인 의존성 주입과는 달리, 라이브러리를 활용하면 이 모든 것을 자동으로 처리해주기 때문에 구현이 변경되어야 하는 경우 코드 수정의 양이 대폭 줄어든다.

이전에 예시로 들었던 Car(engine: Engine)클래스를 예로 들어보자

현재 Car 클래스의 생성자는 Engine 하나 뿐이다. 즉, 의존 관계가 1개이다.

그러나 의존 관계가 4개로 늘어나고 Car class 를 사용하는 곳이 10개가 넘어간다면 일일이 모든 코드를 구현해줘야할 것이다.

또한 어느 순간 Car 클래스에서 Engine 에 대한 의존이 필요가 없어진다면?
Car 클래스를 인스턴스화 한 모든 곳에서 다시 수정을 진행해줘야 하는 것이다.

이러한 단점을 보완하기 위해 나온 것이 Hilt, Koin 등의 의존성 주입 라이브러리이고 많은 이들이 이러한 이점 때문에 이것들을 활용하여 개발을 진행하고 있는 것이다.

# Hilt 란?

Google 이 Dagger 를 기반으로 개발한 DI 프레임워크로 앱 개발시 DI 를 보다 더 쉽게 만들어준다.

Jetpack Compose 의 Navigation 와 연계된 hilt-navigation-compose 라이브러리도 존재하며 이를 통해 Compose 환경에서 각 NavGraph 별 ViewModel 을 쉽게 생성할 수 있게 해줄수도 있다.

# Koin 이란?

Kotlin 및 Android 애플리케이션을 위한 경량화된 의존성 주입(DI) 프레임워크로 Hilt 와는 달리 컴파일 단계의 코드 생성을 사용하지 않고 런타임에 의존성을 주입하기 때문에 빌드 속도가 Hilt 에 비해 빠르고 설정이 간편하다.

Hilt 와 Koin 의 차이

Hilt 의 경우 의존성 주입을 @Inject Annotation 이 붙어있는 경우 실행하게 된다.

그리고 의존성 주입을 위한 코드 생성은 Annotation Processor, Kotlin Annotation Processing Tools, Kotlin Symbol Processor 중 하나의 기능을 통하여 생성되는데 이들은 모두 컴파일 단계에서 이루어진다.

KSP 기준으로 앱 빌드가 이루어지는 경우 SymbolProvider 를 통해 SymbolProcessor 에 접근하고 이곳에서 @Inject Annotation 이 붙은 클래스를 탐색하고 개발자 설정에 맞춰 코드 파일을 생성한다.

이러한 이유로 컴파일 단계에서 코드 오류가 존재하는 경우 빌드 에러가 발생할 수 있어 안정성이 높아지지만 파일 생성으로 인한 빌드 속도가 느려진다는 단점이 존재한다.

Koin 의 경우 Hilt 와는 달리 Reflection 을 활용하여 의존성 주입을 진행하기 때문에 Runtime 에 실행된다.

이로 인해 빌드 속도는 Hilt 에 비해서 빠를 수 있으나 컴파일 단계에서 코드 오류에 대한 검증이 이뤄지지 않아 안정성이 떨어지고 런타임 성능이 좋지 않을 수 있다.

Hilt, Koin 차이점 정리

Hilt

컴파일 단계에서 코드 파일 생성하여 의존성 주입
컴파일에서 의존성 관련 오류를 찾아낼 수 있으므로 빌드 안정성이 증가
컴파일 단계에서 파일 생성으로 인해 빌드 속도 단축
러닝커브 비교적 높음

Koin

런타임 단계에서 Reflection 을 통한 의존성 주입
컴파일 단계에서 의존성 주입이 이뤄지지 않으므로, 의존성 관련 오류를 찾지 못하여 빌드 안정성이 떨어짐
Hilt 에 비해 빠른 빌드가 가능하지만 런타임 성능이 떨어짐
러닝커브 비교적 낮음


DI 관련하여 안드로이드 면접에서 자주 나오는 Hilt 와 Koin 의 차이이다 !

이전에 KSP 를 통한 NavGraphBuilder.composable 함수들을 추가하는 기능을 개발하면서 관련 부분들을 많이 알게 되었는데, 이번에서야 정리를 할 수 있었다.

DI 라이브러리에 대한 더욱 깊은 곳은 알지 못하여 자세히는 적지 못하였으나, 나중에 더욱 많은 것을 알게 된다면 새로운 글을 올려보려고 한다.

profile
안드로이드 개발자 Gyu 의 개발 블로그 !

0개의 댓글