안드로이드 아키텍처 용어 사전
DI

Koin

코틀린 DSL 기반의 런타임 DI. 학습·빌드가 가볍지만 누락이 런타임에 드러난다.

코틀린 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와 무엇이 다른가

HiltKoin
검증 시점컴파일 타임런타임
누락 시빌드 실패실행 중 크래시
방식애노테이션 + 코드 생성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의 실질적 장점이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 의존성 주입 — Hilt·Koin