안드로이드에서의 DI: Hilt, Koin (feat. Hilt Deep Dive)

rivermoon·2025년 3월 22일
post-thumbnail

📘 왜 안드로이드에서 DI가 필요할까?

안드로이드 앱이 커지면 커질수록, 컴포넌트 간의 의존성과 생명주기 관리는 점점 더 복잡해집니다.
액티비티, 프래그먼트, 뷰모델, 리포지토리, 데이터소스… 이들을 하나하나 수동으로 연결하고 관리한다면 코드 유지보수는 점점 힘들어지죠.

이럴 때 필요한 개념이 바로 Dependency Injection(DI) 입니다.
DI를 활용하면 객체 생성과 주입을 프레임워크가 대신해줌으로써 코드의 결합도를 낮추고, 테스트성과 유지보수성을 높일 수 있습니다.


🔄 Android에서 사용되는 DI 프레임워크

가장 많이 쓰이는 두 가지 DI 프레임워크는 다음과 같습니다:

  • Koin: Kotlin DSL 기반, 러닝 커브가 낮고 가볍습니다.
  • Hilt: Google이 공식 지원, Dagger 기반, 컴파일 타임 검증을 제공 해줍니다.

그럼 두 프레임워크의 차이를 간단히 살펴볼까요?

항목koinHilt
기반Kotlin DSLAnnotation + Code Generation
스코프 관리개발자가 수동으로 구성Android 컴포넌트에 자동 연결
테스트 용이성쉽다?(Mock 쉽게 설정 가능)Hilt 테스트용 컴포넌트 제공해줌
빌드 성능빠름컴파일 타임에 생성 → 빌드 느릴 수 있음
러닝 커브낮음중간~높음(dagger 이해도)

✅ Hilt 기본 구조 이해

Hilt는 기본적으로 Dagger를 바탕으로 만들어진 Annotation 기반 DI 프레임워크입니다.

  • @HiltAndroidApp: Application 클래스에 붙여 Hilt를 초기화
  • @AndroidEntryPoint: 의존성을 주입 받을 컴포넌트 (Activity, Fragment, ViewModel 등)에 선언
  • @Inject: 생성자 또는 필드에 사용해 의존성 주입
  • @Module, @Provides: 수동 바인딩이 필요한 경우 사용

👀 예시

@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  @Inject lateinit var repository: MyRepository
}

🧬 Component 구조

Hilt는 안드로이드 컴포넌트의 생명주기에 따라 DI 스코프를 자동 분리합니다. 아래는 Hilt의 Component Tree입니다:

SingletonComponent (앱 전체)
└─ ActivityRetainedComponent (ViewModel 유지)
        └─ ActivityComponent
                └─ FragmentComponent
                        └─ ViewComponent (Custom View 등)

각 컴포넌트는 자동으로 관리되며, 적절한 위치에 의존성을 바인딩하면 스코프에 맞춰 주입됩니다.

🧪 Module, Provides, Qualifier 사용하기

▶ Module & Provides 예시

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides
    fun provideApi(): MyApi = Retrofit.Builder().build().create(MyApi::class.java)
}

▶ Qualifier 예시

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Local

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Remote

@Module
@InstallIn(SingletonComponent::class)
object RepositoryModule {
    @Provides
    @Local
    fun provideLocalRepo(): Repository = LocalRepository()

    @Provides
    @Remote
    fun provideRemoteRepo(): Repository = RemoteRepository()
}

🔐 EntryPoint, 테스트와 커스텀 사용법

  • @EntryPoint로 Hilt 외부에서 의존성 접근 가능
  • 테스트에서는 @TestInstallIn으로 테스트 모듈을 지정 가능
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    fun myRepo(): MyRepository
}

@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [RealModule::class]
)
@Module
object FakeModule {
    @Provides fun provideFakeRepo(): Repository = FakeRepo()
}

🔹 Hilt의 고급 기능++

🔧 Assisted Injection

모든 의존성이 DI 보호 형식으로 제공될 수는 없습니다.
UI에서 전달되는 객체를 구성자에서 제안되거나, Navigation arguments같은 경우에 사용할 수 있습니다.

@AssistedInject
class MyViewModel @AssistedInject constructor(
    private val repository: MyRepository,
    @Assisted private val userId: String
)

val factory = HiltViewModelFactory.create(
    owner = this,
    defaultArgs = bundleOf("userId" to "1234")
)

🔹 Custom Scope

  • @Singleton 하나로는 모든 상황을 커버할 수 없어요.
  • Activity, Fragment, Feature-specific Scope를 지정할 수 있고, 이를 통해 가변한 사이트 구성이 가능합니다.
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class MyCustomScope

@MyCustomScope
class MyScopedClass @Inject constructor(...)

🔬 Hilt의 내부 동작 메커니즘

1. Annotation Processor (KAPT or KSP)
Hilt는 @HiltAndroidApp, @Inject, @Module, @Provides 등의 애노테이션을 컴파일 타임에 분석합니다.

이때 Dagger 기반의 코드 생성기가 작동하여, 각각의 의존성들을 적절히 연결하는 컴포넌트 클래스와 팩토리 클래스들을 자동 생성합니다.

이 과정은 KAPT나 KSP를 통해 수행됩니다

2. Component Tree 자동 생성
Hilt는 안드로이드의 생명주기를 기준으로 다음과 같은 컴포넌트 트리 구조를 자동으로 생성합니다.(위 컴포넌트 트리 구조 확인)

각 컴포넌트는 스코프별로 객체의 생존 기간을 구분하고, 여기에 등록된 모듈이나 주입 객체가 해당 범위 내에서만 사용되도록 보장합니다.

3. 객체 그래프 생성 및 의존성 연결
컴파일 타임에 생성된 각종 팩토리/컴포넌트 클래스들이 모여 전체 의존성 그래프를 구성합니다.

@Inject가 붙은 생성자나 필드는 이 그래프를 기반으로 자동 주입됩니다.

중첩된 의존성 (A가 B를 필요로 하고, B가 C를 필요로 하는 경우 등) 도 그래프 안에서 자동으로 해결됩니다.

4. 컴파일 타임 검증 (정적 오류 방지)
Hilt는 모든 DI 구성 요소를 컴파일 타임에 미리 검증하기 때문에,
잘못된 스코프 혼용, 중복 바인딩, 미정의된 주입 대상 등과 같은 문제를 앱이 실행되기 전에 미리 탐지할 수 있습니다.

이게 바로 Dagger 기반 DI 프레임워크의 가장 큰 강점입니다.
런타임 버그 대신 컴파일 타임 오류로 안전하게 관리할 수 있죠.

🐆 다이어그램


⚖️ Koin vs Hilt 실전 예제 비교

✅ ViewModel 주입 예시 (Hilt)

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: MyRepository
): ViewModel()

✅ ViewModel 주입 예시 (Koin)

val appModule = module {
    viewModel { MyViewModel(get()) }
    single { MyRepository() }
}

class MyActivity : AppCompatActivity() {
    private val viewModel: MyViewModel by viewModel()
}

🧐 어떤 프레임워크를 선택해야 할까?

  • 빠른 학습, 간단한 프로젝트 → Koin
  • 안드로이드 생명주기 연동, 공식 지원, 규모가 큰 프로젝트 → Hilt

✅ 마치며

안드로이드에서의 DI는 단순히 의존성 주입을 넘어서, 생명주기 관리 + 테스트 유연성 + 모듈화 설계까지 확장되는 중요한 개념입니다.
처음엔 복잡해보일 수 있지만, Hilt의 구조를 이해하면 앱 설계의 레벨이 한층 올라갈 수 있습니다.

📚 함께 보면 좋은 자료:

profile
Android Developer

0개의 댓글