Dagger를 안드로이드에 맞게 감싼 컴파일 타임 DI 라이브러리.
DI가 푸는 문제
// ❌ 직접 만들면 결합이 생긴다
class HomeViewModel {
private val repo = UserRepositoryImpl(RetrofitApi(), RoomDao()) // 구현에 묶인다
}
// → 테스트에서 가짜로 바꿀 수 없다
// → 의존이 늘면 생성 코드가 폭발한다
// ✅ 받아 쓰면 교체 가능하다
class HomeViewModel @Inject constructor(private val repo: UserRepository)
Hilt의 구성
@HiltAndroidApp
class MyApp : Application()
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideApi(): UserApi = Retrofit.Builder()...create()
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds // 인터페이스 → 구현 연결
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}
@AndroidEntryPoint
class HomeFragment : Fragment() {
private val viewModel: HomeViewModel by viewModels()
}
@HiltViewModel
class HomeViewModel @Inject constructor(private val repo: UserRepository) : ViewModel()
@Provides는 생성 방법을 적을 때, @Binds는 인터페이스를 구현에 연결할 때 쓴다. 후자가 더 효율적이다(코드 생성이 적다).
스코프 — 객체의 수명
-
@Singleton — 앱 전체 (SingletonComponent)
-
@ActivityRetainedScoped 구성 변경을 넘어 유지 (ViewModel 과 같은 수명)
-
@ActivityScoped — Activity 수명
-
@FragmentScoped — Fragment 수명
-
@ViewModelScoped — ViewModel 수명
-
스코프를 안 붙이면 요청할 때마다 새 인스턴스가 만들어진다
스코프를 잘못 잡으면
- 너무 길게 → 메모리 누수 (Activity 를 Singleton 이 참조)
- 너무 짧게 → 불필요한 재생성 (매번 Retrofit 인스턴스 생성)
Koin과의 트레이드오프
-
Hilt — 컴파일 타임 검증 — 의존성이 빠지면 빌드가 실패한다
- 코드 생성이라 빌드 시간이 는다 (KSP 로 많이 개선됐다)
- 안드로이드 컴포넌트 통합이 매끄럽다
-
Koin — 런타임 DI — DSL 로 선언한다. 학습이 쉽고 빌드가 빠르다
- 의존성 누락이 런타임에 드러난다 (앱이 실행 중 크래시)
- 검증 테스트를 따로 돌려야 한다
-
규모가 크고 팀이 여럿이면 Hilt, 작고 빠른 반복이면 Koin
면접 함정
- ❌ "DI는 프레임워크가 있어야 한다" → 생성자로 넘기는 것이 이미 DI다. 컨테이너는 그걸 자동화할 뿐이다.
- ❌ "@Singleton을 붙이면 안전하다" → 상태를 가지면 여전히 위험하고, Activity를 참조하면 누수다.
의존성 그래프가 실제로 어떻게 검증되나
빌드 시 Hilt 가 그래프를 만든다 UserApi 가 필요한데 @Provides 가 없다
- [Dagger/MissingBinding] UserApi cannot be provided...
- 빌드 실패. 앱이 실행되기 전에 잡힌다
순환 의존도 빌드에서 잡힌다 A → B → A → [Dagger/DependencyCycle]
같은 타입을 두 개 제공해야 할 때 — 한정자
@Qualifier @Retention(AnnotationRetention.BINARY)
annotation class AuthClient
@Provides @AuthClient
fun provideAuthClient(): OkHttpClient = OkHttpClient.Builder().addInterceptor(auth).build()
@Provides
fun providePublicClient(): OkHttpClient = OkHttpClient()
class TokenRepository @Inject constructor(@AuthClient private val client: OkHttpClient)
한정자가 없으면 DuplicateBindings 빌드 에러가 난다 — 런타임에 엉뚱한 걸 받는 일은 없다.
테스트에서 바꿔치기
@HiltAndroidTest
@UninstallModules(NetworkModule::class)
class HomeScreenTest {
@BindValue @JvmField val api: UserApi = FakeUserApi()
}
@UninstallModules 로 실제 모듈을 떼고 @BindValue 로 가짜를 꽂는다 = DI 를 쓰는 가장 실질적인 이유