
안드로이드 앱이 커지면 커질수록, 컴포넌트 간의 의존성과 생명주기 관리는 점점 더 복잡해집니다.
액티비티, 프래그먼트, 뷰모델, 리포지토리, 데이터소스… 이들을 하나하나 수동으로 연결하고 관리한다면 코드 유지보수는 점점 힘들어지죠.
이럴 때 필요한 개념이 바로 Dependency Injection(DI) 입니다.
DI를 활용하면 객체 생성과 주입을 프레임워크가 대신해줌으로써 코드의 결합도를 낮추고, 테스트성과 유지보수성을 높일 수 있습니다.
가장 많이 쓰이는 두 가지 DI 프레임워크는 다음과 같습니다:
그럼 두 프레임워크의 차이를 간단히 살펴볼까요?
| 항목 | koin | Hilt |
|---|---|---|
| 기반 | Kotlin DSL | Annotation + Code Generation |
| 스코프 관리 | 개발자가 수동으로 구성 | Android 컴포넌트에 자동 연결 |
| 테스트 용이성 | 쉽다?(Mock 쉽게 설정 가능) | Hilt 테스트용 컴포넌트 제공해줌 |
| 빌드 성능 | 빠름 | 컴파일 타임에 생성 → 빌드 느릴 수 있음 |
| 러닝 커브 | 낮음 | 중간~높음(dagger 이해도) |
Hilt는 기본적으로 Dagger를 바탕으로 만들어진 Annotation 기반 DI 프레임워크입니다.
@HiltAndroidApp
class MyApp : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var repository: MyRepository
}
Hilt는 안드로이드 컴포넌트의 생명주기에 따라 DI 스코프를 자동 분리합니다. 아래는 Hilt의 Component Tree입니다:
SingletonComponent (앱 전체)
└─ ActivityRetainedComponent (ViewModel 유지)
└─ ActivityComponent
└─ FragmentComponent
└─ ViewComponent (Custom View 등)
각 컴포넌트는 자동으로 관리되며, 적절한 위치에 의존성을 바인딩하면 스코프에 맞춰 주입됩니다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideApi(): MyApi = Retrofit.Builder().build().create(MyApi::class.java)
}
@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
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun myRepo(): MyRepository
}
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RealModule::class]
)
@Module
object FakeModule {
@Provides fun provideFakeRepo(): Repository = FakeRepo()
}
모든 의존성이 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")
)
@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class MyCustomScope
@MyCustomScope
class MyScopedClass @Inject constructor(...)
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 프레임워크의 가장 큰 강점입니다.
런타임 버그 대신 컴파일 타임 오류로 안전하게 관리할 수 있죠.

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