|

Hilt assisted injection은 언제 필요할까: 런타임 값을 ViewModel에 넣는 안전한 방법

Hilt assisted injection은 언제 필요할까: 런타임 값을 ViewModel에 넣는 안전한 방법
Hilt assisted injection이 필요한 상황을 SavedStateHandle, ViewModel 런타임 파라미터, factory 기준으로 나눠 설명합니다. 화면 인자와 DI 의존성을 구분해 불필요한 복잡도를 줄이는 방법을 정리합니다.

Hilt assisted injection은 Hilt가 아는 의존성과 실행 중에만 알 수 있는 값을 함께 넣어 객체를 만들 때 쓰는 방식입니다. 특히 ViewModel에 런타임 값을 넣고 싶을 때 자주 검색하게 됩니다.

하지만 먼저 볼 기준은 그 값이 navigation argument인지, DI 의존성인지, 생성 시점 런타임 객체인지 구분하는 것입니다. 이 구분 없이 assisted injection부터 쓰면 구조가 더 복잡해질 수 있습니다.

Hilt assisted injection 판단 기준 카드
ViewModel 인자는 SavedStateHandle과 Hilt assisted injection의 역할을 나눠 판단한다.

Hilt가 기본으로 잘 처리하는 것

Repository, use case, DAO, API client처럼 앱의 DI 그래프 안에서 만들 수 있는 객체는 Hilt가 생성자 주입으로 넣어주는 것이 자연스럽습니다.

@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val repository: ProfileRepository
) : ViewModel()

이런 객체는 화면이 어떤 userId를 받았는지와 무관하게 Hilt가 제공할 수 있습니다. 그래서 assisted injection이 필요하지 않습니다.

SavedStateHandle로 충분한 경우

화면 route나 navigation argument로 넘어온 id, tab, mode 같은 값은 SavedStateHandle로 받는 경우가 많습니다. 이 값은 프로세스 재생성 상황에서도 ViewModel 상태 복원 흐름과 잘 맞습니다.

@HiltViewModel
class ProfileViewModel @Inject constructor(
    private val repository: ProfileRepository,
    savedStateHandle: SavedStateHandle
) : ViewModel() {
    private val userId: String = checkNotNull(savedStateHandle["userId"])
}

단순히 화면 인자를 읽는 것이 목적이라면 assisted injection보다 SavedStateHandle이 더 단순하고 Android 아키텍처 흐름에도 잘 맞습니다.

Hilt assisted injection이 필요한 신호

assisted injection은 DI 그래프가 만들 수 없는 값을 생성 시점에 호출자가 직접 넘겨야 할 때 의미가 있습니다. 예를 들어 런타임에 만들어진 세션 객체, 복잡한 초기 파라미터, DI 그래프에 넣기 애매한 값이 여기에 해당할 수 있습니다.

  • 값이 navigation argument가 아니다
  • DI container에 전역처럼 등록하면 오히려 위험하다
  • 객체 생성 시점에 반드시 호출자가 넘겨야 한다
  • 테스트에서 해당 값을 명시적으로 바꿔 넣어야 한다

Dagger assisted injection의 기본 모양

assisted injection은 의존성과 assisted parameter를 구분합니다. 의존성은 DI가 제공하고, assisted parameter는 factory 호출자가 넘깁니다.

class DetailPresenter @AssistedInject constructor(
    private val repository: ItemRepository,
    @Assisted private val itemId: String
) {
    @AssistedFactory
    interface Factory {
        fun create(itemId: String): DetailPresenter
    }
}

ViewModel에 적용할 때는 AndroidX/Hilt 버전별 지원 방식과 factory 연결 방식이 달라질 수 있으므로 공식 문서 기준을 확인해야 합니다.

ViewModel에서 무리하게 쓰면 생기는 문제

ViewModel은 생성 주체가 Activity나 Fragment가 아니라 ViewModelProvider입니다. 그래서 일반 객체처럼 factory.create(…)를 직접 호출하는 흐름과 다릅니다. 이 지점에서 assisted injection을 억지로 붙이면 생성 흐름이 불필요하게 복잡해질 수 있습니다.

  • navigation argument로 충분한 값을 factory 인자로 다시 넘긴다
  • 프로세스 재생성 시 초기값 복원 흐름을 놓친다
  • ViewModelProvider와 별도 factory 흐름이 섞인다
  • 테스트에서 Hilt 설정과 factory 설정을 모두 맞춰야 한다

판단 순서

  1. 이 값이 Repository 같은 의존성인지 확인한다
  2. 화면 route argument라면 SavedStateHandle로 받을 수 있는지 본다
  3. DI 그래프에 넣으면 lifecycle이나 scope가 어색한지 확인한다
  4. 생성 시점 호출자가 반드시 넘겨야 한다면 assisted injection을 검토한다
  5. 테스트와 프로세스 재생성 시나리오를 같이 확인한다

테스트 관점

테스트에서는 런타임 값을 명시적으로 바꾸기 쉽다는 장점이 assisted injection의 매력입니다. 다만 ViewModel의 경우 SavedStateHandle도 테스트에서 직접 구성할 수 있으므로, 테스트 편의성만으로 assisted injection을 선택할 필요는 없습니다.

val handle = SavedStateHandle(mapOf("userId" to "42"))
val viewModel = ProfileViewModel(fakeRepository, handle)

정리

Hilt assisted injection은 런타임 값을 넣는 강력한 도구지만, ViewModel 인자를 처리하는 기본 답은 아닙니다. 화면 인자는 SavedStateHandle, 앱 의존성은 Hilt 생성자 주입, 그 둘로 설명되지 않는 생성 시점 값은 assisted injection 후보로 보는 것이 안전합니다.

Hilt ViewModel 기본 흐름은 Android Developers Hilt 통합 문서를 기준으로 확인할 수 있습니다. 상태 보존 기준은 SavedStateHandle과 rememberSaveable 차이와 함께 보면 좋습니다.

함께보면 좋은 글