코틀린 DSL로 선언하는 런타임 DI 컨테이너.
val appModule = module {
single { Retrofit.Builder().baseUrl(BASE).build().create(UserApi::class.java) }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
factory { GetUserFeedUseCase(get(), get()) } // 매번 새로 만든다
viewModel { HomeViewModel(get(), get()) }
}
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin { androidContext(this@MyApp); modules(appModule) }
}
}
Hilt와 무엇이 다른가
| Hilt | Koin | |
|---|---|---|
| 검증 시점 | 컴파일 타임 | 런타임 |
| 누락 시 | 빌드 실패 | 실행 중 크래시 |
| 방식 | 애노테이션 + 코드 생성 | DSL + 서비스 로케이터 |
| 빌드 시간 | 코드 생성 부담이 있다 | 없다 |
| 학습 곡선 | Dagger 개념이 필요하다 | 하루면 익힌다 |
| 멀티 모듈 | 컴포넌트 설계가 필요하다 | 모듈 리스트를 합치면 끝 |
Koin의 실패는 언제 드러나나
single<UserRepository> { UserRepositoryImpl(get(), get()) }
- get() 이 찾을 바인딩이 없으면
- NoBeanDefFoundException: No definition found for class 'UserDao'
- ...이 화면에 들어간 순간 크래시한다
대응: 검증 테스트를 CI 에서 돌린다
class KoinModuleTest : KoinTest {
@Test fun `모든 정의가 해결된다`() {
koinApplication { modules(appModule) }.checkModules()
}
}
checkModules()를 CI에 넣으면 컴파일 타임 검증의 상당 부분을 흉내 낼 수 있다. 이것 없이 Koin을 쓰면 사고가 난다.
서비스 로케이터라는 비판
by inject() 로 클래스 안에서 직접 꺼내 쓰면
- 그 클래스는 Koin 에 의존하게 된다
- 생성자 주입의 이점(순수 코틀린 테스트) 이 사라진다
Koin 을 써도 생성자 주입을 유지하는 것이 좋다
- module 안에서만 get() 을 쓰고, 클래스는 Koin 을 모르게 한다
언제 무엇을 고르나
-
Koin — 프로토타입 · 소규모 팀 · 빌드 시간이 아픈 프로젝트 · 멀티플랫폼(KMP)
-
Hilt — 규모가 크고 팀이 여럿 · 안전망이 중요한 프로덕션 · 구글 가이드 정렬
-
둘 다 '생성자로 받는다' 는 본질은 같다 — 컨테이너를 바꿔도 클래스는 안 바뀐다
면접 함정
- ❌ "Koin은 리플렉션을 쓴다" → 기본 DSL은 리플렉션 없이 람다로 생성한다.
- ❌ "런타임 DI라 느리다" → 조회는 맵 조회 수준이다. 실제 문제는 성능이 아니라 검증 시점이다.
멀티 모듈에서의 모습
// :feature:home 모듈
val homeModule = module { viewModel { HomeViewModel(get()) } }
// :app 모듈이 전부 합친다
startKoin { modules(coreModule, dataModule, homeModule, profileModule) }
Hilt 는 @InstallIn 으로 각 모듈이 알아서 붙는다 (합치는 코드가 없다) Koin 은 :app 이 목록을 알아야 한다
- 모듈이 늘 때마다 한 줄씩 추가한다. 빠뜨리면 런타임 크래시
이름으로 구분하기 — named
single(named("auth")) { OkHttpClient.Builder().addInterceptor(auth).build() }
single(named("public")) { OkHttpClient() }
single { TokenRepository(get(named("auth"))) }
문자열이라 오타가 컴파일에 안 잡힌다
- Hilt 의 @Qualifier 는 타입이라 오타가 빌드 에러다
- 이 차이가 Koin/Hilt 선택의 축소판이다
테스트에서 바꿔치기
@Before fun setUp() {
startKoin { modules(appModule, module { single<UserApi> { FakeUserApi() } }) }
}
@After fun tearDown() = stopKoin()
나중에 선언한 모듈이 앞의 정의를 덮는다 — 별도 애노테이션 없이 교체가 된다는 점은 Koin의 실질적 장점이다.