Develop

Android Hilt 의존성 주입 완벽 가이드 — Dagger 없이 DI 구현하기

Android Hilt로 ViewModel, Repository, 네트워크 클라이언트에 의존성 주입을 구현하는 방법을 모듈 설계부터 테스트까지 단계별로 정리하였다.

★★★★★적극 추천
AndroidHiltDIDaggerKotlin
Android Hilt 의존성 주입 완벽 가이드 — Dagger 없이 DI 구현하기
  • ·Hilt는 Dagger 위에서 동작하며 Android 컴포넌트 생명주기에 맞는 스코프를 자동으로 관리한다
  • ·@HiltViewModel 어노테이션만 붙이면 ViewModel에 의존성 주입이 가능해진다
  • ·Hilt 모듈은 @Module + @InstallIn 조합으로 어느 컴포넌트에 설치할지 지정한다
  • ·테스트에서는 @UninstallModules + @BindValue로 실제 의존성을 페이크로 교체할 수 있다
이전에는 직접 Dagger 컴포넌트와 서브컴포넌트를 설계했는데, 팀원이 합류할 때마다 Dagger 구조 설명에 많은 시간이 걸렸다. Hilt로 전환한 뒤 보일러플레이트가 대폭 줄었고 신규 입사자가 DI 코드를 이해하는 데 걸리는 시간이 확연히 단축됐다. 특히 @HiltViewModel 하나로 ViewModel 주입이 처리되는 것이 가장 큰 생산성 향상이었다.

Hilt 기본 설정과 첫 번째 의존성 주입

Android Hilt 프로젝트 초기 설정과 @HiltAndroidApp 적용 방법

Hilt를 시작하려면 build.gradle에 hilt-android 플러그인과 kapt 또는 ksp 의존성을 추가하고 Application 클래스에 @HiltAndroidApp 어노테이션을 붙인다. 이 어노테이션이 Dagger 컴포넌트 생성의 시작점이며 없으면 앱이 초기화되지 않는다. Activity와 Fragment에는 @AndroidEntryPoint를 붙여야 해당 클래스에서 @Inject를 사용할 수 있다. 어노테이션 하나로 Dagger 컴포넌트 계층 전체가 자동 생성되는 구조라 처음 Hilt를 접하면 설정이 너무 단순해서 오히려 당황스럽다는 반응이 많다. 실제로 처음 설정할 때 gradle 동기화 후 빌드 오류가 나면 kapt 또는 ksp 버전과 Hilt 버전의 호환성을 먼저 확인하는 것이 좋다.

@HiltAndroidApp
class MyApp : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var analytics: AnalyticsService
}

Android Hilt @Module로 인터페이스와 외부 라이브러리 객체 주입하기

생성자 주입이 불가능한 경우에 Hilt 모듈을 사용한다. Retrofit, OkHttpClient처럼 외부 라이브러리 클래스나 인터페이스 구현체는 @Module 클래스 안에서 @Provides 또는 @Binds로 등록한다. @Provides는 객체 생성 코드를 직접 작성하고, @Binds는 인터페이스와 구현체를 연결할 때 사용하며 추상 함수 형태로 선언한다. @InstallIn(SingletonComponent::class)이면 앱 전체 생명주기 동안 유지되는 싱글턴, @InstallIn(ViewModelComponent::class)이면 ViewModel 생명주기와 동기화된다. 스코프를 잘못 지정하면 의존성이 예상보다 오래 살아남아 메모리가 낭비되거나 반대로 너무 일찍 소멸해 NullPointerException이 발생하기도 한다.

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
    @Provides @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit =
        Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .client(okHttpClient)
            .build()
}

Hilt ViewModel과 Repository 패턴 연결

Android Hilt @HiltViewModel로 ViewModel에 Repository 주입하기

Hilt가 가장 빛을 발하는 지점이 ViewModel 주입이다. @HiltViewModel을 붙이고 생성자에 @Inject를 추가하면 ViewModel에 원하는 의존성을 선언적으로 주입받을 수 있다. Fragment나 Activity에서는 by viewModels()로 ViewModel을 생성하면 Hilt가 자동으로 의존성 그래프를 해석해 필요한 객체를 주입한다. 별도 ViewModelFactory를 만들 필요가 없어 코드가 간결해진다. Repository도 @Inject constructor로 선언하면 별도 모듈 없이 자동으로 주입된다. 인터페이스를 구현하는 Repository라면 @Binds 모듈이 필요하지만, 단순히 클래스에 @Inject constructor만 붙이는 경우 대부분의 의존성은 모듈 작성 없이 처리된다. 팀 내에서 새 기능을 추가할 때 모듈을 건드리지 않아도 되는 경우가 많아져 충돌이 줄었다.

@HiltViewModel
class PostViewModel @Inject constructor(
    private val postRepository: PostRepository
) : ViewModel() {
    val posts = postRepository.getPosts().stateIn(
        viewModelScope, SharingStarted.Lazily, emptyList()
    )
}

Hilt 테스트 환경 구성

Android Hilt 테스트에서 실제 의존성을 페이크로 교체하는 방법

Hilt의 테스트 지원은 @HiltAndroidTest와 HiltAndroidRule 조합으로 시작한다. 계측 테스트에서 특정 모듈을 페이크로 교체하려면 @UninstallModules(NetworkModule::class)로 실제 모듈을 제거하고 테스트 클래스 안에 @Module @InstallIn로 대체 모듈을 선언한다. 단위 테스트에서는 Hilt 없이 생성자에 직접 페이크 의존성을 전달하는 것이 더 빠르고 간단하다. ViewModel 단위 테스트 시 @HiltViewModel을 쓴다고 해서 Hilt 컨테이너가 필요하지는 않으며, TestCoroutineDispatcher와 FakeRepository를 생성자에 직접 전달하면 된다. 이 구분을 명확히 하지 않으면 간단한 단위 테스트에 불필요하게 Hilt 계측 환경을 세팅하다가 실행 속도가 느려진다.

자주 묻는 질문

Hilt와 Koin 중 어떤 것을 선택해야 하나요?+

Hilt는 컴파일 타임에 의존성 그래프를 검증해 런타임 오류가 없는 반면, Koin은 설정이 더 단순하고 순수 Kotlin으로 동작합니다. 대형 프로젝트나 팀 규모가 크다면 Hilt가 안전하고, 소규모 프로젝트에서는 Koin이 빠르게 시작할 수 있습니다.

Hilt에서 동일한 타입의 의존성을 여러 개 등록하려면 어떻게 하나요?+

@Qualifier 어노테이션을 사용합니다. @Named("debug") @Named("release")처럼 이름을 붙이거나 커스텀 Qualifier 어노테이션을 만들어 구분합니다.

Hilt를 기존 Dagger 프로젝트에 점진적으로 도입할 수 있나요?+

가능합니다. Hilt와 Dagger 컴포넌트를 @EntryPoint로 연결하면 기존 Dagger 코드를 유지하면서 새로운 클래스에만 Hilt를 적용할 수 있습니다.

관련 글