Skip to content

⚙ [기술 분석] DI

Yongsu Lim edited this page Nov 28, 2024 · 1 revision

Hilt를 왜 사용해야 하는지, @Provides, @Binds를 어떻게 사용해야 하는지 분석

🔍 DI

Service Locator를 사용하거나 수동으로 의존성 관리를 하게 되면 의존성이 어떻게 주입되고 있는지 관계를 파악하기 어려웠다. DI 라이브러리를 사용하여 의존성 주입을 자동화하면 의존성 관계를 파악하기 쉬워지고 코드를 관리하는 데 도움이 된다고 판단하였다.


1️⃣ Hilt

Hilt는 의존성 주입을 쉽게 구현할 수 있도록 Google에서 제공하는 DI 라이브러리이다. Hilt는 Dagger2를 기반으로 하며, Dagger의 복잡성을 간소화하여 Android에 최적화된 형태로 제공한다.

장점

  • Dagger에 비해 사용법이 직관적이고, DI 설정 과정이 크게 단축된다.

  • Activity, Fragment, ViewModel 등 Android Component와 연동하여 DI Container를 자동으로 관리한다.

단점

  • 컴파일 타임에 DI 그래프를 생성하여 빌드 시간이 증가한다.

  • 러닝 커브가 높다.


2️⃣ Dagger

Dagger는 Java & Kotlin 기반의 의존성 주입 라이브러리이다. 컴파일 타임에 의존성 그래프를 생서앟여 런타임 성능이 뛰어나고, 안드로이드 뿐만이 아니라 다양한 플랫폼에서 사용 가능하다.

장점

  • 컴파일 타임에 DI 그래프 생성 => 런타임 오버헤드가 거의 없음.

  • 다양한 요구사항을 지원하고 Android 뿐만 아니라 다른 플랫폼에서도 사용 가능

단점

  • DI 그래프를 컴파일 타임에 생성하여 빌드 시간이 증가

  • Android 생명 주기와 직접 통합되어 있지 않음.


3️⃣ Koin

Kotlin 개발을 위해 설계된 의존성 주입 라이브러리이다. 컴파일 타임에 DI그래프를 생성하지 안혹, 런타임에 의존성을 주입한다.

장점

  • Kotlin DSL을 사용하여 직관적이고 간결한 설정이 가능

  • 별도의 Annotation이나 코드 생성 없이 간단하게 사용 가능

단점

  • 런타임에 의존성을 처리하여 대규모 어플리케이션에서 성능 이슈 발생 가능성이 있다.

  • DI 그래프 오류를 컴파일 타임에 잡아내지 못하여 런타임에서 에러가 발생할 수 있다.


✅ Hilt

DI 라이브러리로 Hilt를 사용하기로 결정

선정 이유

  1. Koin은 런타임에 의존성을 처리하기 때문에 Hilt가 런타임 성능이 우수함.

  2. Hilt는 컴파일 타임에 에러를 검출하여 런타임에서 에러를 확인해야 하는 Koin보다 안정성이 높음.

  3. Hilt는 Dagger의 복잡한 설정을 자동화함.

  4. Android Copmonent와 연동하여 자동으로 DI Container를 관리함.

따라서, 우리 팀은 Android에 특화되어 있으며, 안정성이 높은 Hilt를 사용하기로 결정하였다.

@Provides vs @Binds

Interface, abstract class에서는 Hilt가 인스턴스화 할 방법을 모르기 때문에 @Inject를 사용할 수 없고 구체적인 구현체에서만 적용할 수 있다. 따라서 @Binds, @Provides를 사용하여 의존성을 주입하여야 한다.

그렇다면 어떤 경우에 @Binds, @Provides를 사용하는 것이 좋을지에 대해서 논의하였다.

@Provides

@Module
@InstallIn(SingletonComponent::class)
object AppModule {

    @Provides
    fun provideApiService(): ApiService {
        return ApiServiceImpl()
    }
}
  • 의존성을 주입하기 위해 직접 객체를 생성하고 반환할 수 있도록 해주는 Annotation

  • 개발자가 명시적으로 객체 생성 방식을 정의할 수 있다.

  • 팩토리 패턴을 사용하여 생성된 객체는 의존성 그래프에 저장되고, 이후 같은 타입 요청 시, 저장된 객체를 반환

장점

  • 의존성을 생성하기 위한 복잡한 로직을 작성할 수 있다.

  • 팩토리 패턴, 조건문 등을 통해 생성 방식에 유연성을 부여할 수 있다.

  • @Inject를 추가하지 않아도 의존성을 제공할 수 있다.

단점

  • 구현체가 단순한 경우에도 팩토리 메서드를 작성해야 하는 불필요한 코드가 생길 수 있다.

@Binds

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {

    @Binds
    abstract fun bindApiService(
        impl: ApiServiceImpl
    ): ApiService
}
  • @Provides와 달리 @Inject를 사용해야 한다.

  • 추상 타입을 통해 의존성을 주입한다.

  • 이미 생성된 구현체를 사용하기 때문에 추가적인 객체 생성 로직이 필요하지 않다.

장점

  • 인터페이스와 구현체의 관계를 간단하게 정의할 수 있다.

  • 구현체가 명시적으로 바인딩되지 않으면 컴파일 에러가 발생하여 안전성이 높다.

  • 런타임에 객체를 생성하지 않기에 컴파일 타임에 더 최적화된다.

단점

  • 추상 클래스나 인터페이스 내부에서만 사용 가능하여 클래스가 추상적이어야 한다.

  • 복잡한 객체 생성 로직을 구현할 수 없다.

결론

Provides는 @Inject를 사용하지 않아도 되고, 객체 생성에 추가 처리가 가능하기 때문에 외부 라이브러리ApiService같은 인터페이스를 주입해야 할 때, 추가적인 객체 생성 처리가 필요할 때 사용하기로 결정하였다.

Binds는 코드가 단순하고 @Inject를 사용해야 하며, 컴파일 타임에 바인딩을 처리하므로 단순히 인터페이스와 구현체를 연결하는 경우 사용하기로 결정하였다.

Clone this wiki locally