HiltViewModel 의존성 주입 원리

2025. 11. 6. 15:02·Android

갑자기 새로운 버전을 배포한 앱이 크래시 되는 일이 발생했습니다. Firebase Crashlytics를 확인한 결과 다음과 같은 에러가 발생하고 있었습니다.

 

원인은 단순했습니다. ViewModel에 @HiltViewModel을 Activity에 @AndroidEntryPoint를 추가하지 않았기 때문이었죠. 

하지만 여기서 의문이 생겼습니다.

"Hilt는 컴파일 타임에 의존성 주입 코드를 생성해서 런타임 에러를 방지한다고 알고 있는데 왜 컴파일은 성공하고 런타임에 크래시가 발생한 걸까?"

이 의문을 해결하기 위해 ViewModel의 기본 작동 원리부터 Hilt가 어떻게 ViewModel을 주입하는지 차근차근 알아보겠습니다.

Hilt 의존성 주입

"Hilt는 컴파일 타임에 의존성 관계를 분석하여 의존성 주입 코드를 생성하고 이 코드를 기반으로 런타임에 실제 의존성을 주입한다"

Hilt는 @Inject, @Provides, @Binds 등의 어노테이션을 기반으로 컴파일 시점에 의존성 관계를 분석하여 의존성 주입 코드를 생성하고 각 Component 마다 독립적인 의존성 주입 코드를 생성합니다. 이 코드에는 다음 정보가 담겨있습니다.

  • 어떤 클래스가 어떤 의존성을 필요로 하는가?
  • 그 의존성을 어디서 어떻게 제공하는가?
  • 의존성들이 어떻게 연결되는가? 

ViewModel 생성 과정

Activity나 Fragment에서 ViewModelProvider를 통해 ViewModel을 생성하면 ViewModelStore에 저장된 ViewModel을 재사용합니다. 만약 저장된 ViewModel이 없다면 ViewModelFactory를 사용해 ViewModel을 생성하고 이를 ViewModelStore에 저장합니다. 이 과정을 조금 더 자세히 살펴보겠습니다.

ViewModel

기본적인 ViewModel 정의는 androidx.lifecycle.ViewModel을 상속하는 것에서 부터 시작합니다.

class MyViewModel: ViewModel()

ViewModelStore

  • 한번 생성된 ViewModel들이 저장되는 저장소입니다.
  • Map을 사용해 저장하며 ViewModelProvider는 요청된 ViewModel이 ViewModelStore에 있으면 반환하고 없다면 새로 생성해 저장합니다
public open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun put(key: String, viewModel: ViewModel) {
        val oldViewModel = map.put(key, viewModel)
        oldViewModel?.clear()
    }

    /** Returns the `ViewModel` mapped to the given `key` or null if none exists. */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public operator fun get(key: String): ViewModel? {
        return map[key]
    }

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public fun keys(): Set<String> {
        return HashSet(map.keys)
    }

    /** Clears internal storage and notifies `ViewModel`s that they are no longer used. */
    public fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

ViewModelStoreOwner

  • ViewModel이 어떤 Activity/Fragment에 대한 생명주기를 가질지 결정하는 주체입니다.
  • Activity/Fragment의 내부 구현을 보면 ViewModelStoreOwner를 구현하고 있습니다.
  • 이를 통해 ViewModelProvier에 전달되는 ViewModelStoreOwners는 Activty나 Fragment 임을 알 수 있습니다.

ViewModelProvider

  • ViewModel 인스턴스를 생성하고 저장하는 역할을 가진 객체입니다.
  • ViewModelStoreOwner를 인자로 받으며 동반 객체에 선언된 VIEW_MODEL_KEY는 ViewModelStore에 ViewModel을 저장할 때 사용되는 key입니다.
val viewModel = ViewModelProvider(
    this,  // ViewModelStoreOwner (Activity/Fragment)
    MyViewModelFactory(repository)  // ViewModelFactory
)[MyViewModel::class]

public actual open class ViewModelProvider(){
    public constructor(
        owner: ViewModelStoreOwner,
    ) : this(
        store = owner.viewModelStore,
        factory = ViewModelProviders.getDefaultFactory(owner),
        defaultCreationExtras = ViewModelProviders.getDefaultCreationExtras(owner)
    )
    
    companion object {
    	public val VIEW_MODEL_KEY: CreationExtras.Key<String>
    }
}

ViewModelFactory

  • ViewModel 인스턴스를 생성하는 책임을 가진 객체입니다.
  • Factory를 명시적으로 전달하지 않으면 defaultViewModelProviderFactory가 사용됩니다.
  • 기본 Factory는 인자가 없는 ViewModel만 생성할 수 있기 때문에 ViewModel 생성자에 인자가 있으면 반드시 Factory를 전달해야 합니다.
interface ViewModelProvider.Factory {
    fun <T : ViewModel> create(modelClass: Class<T>): T
}

class MyViewModelFactory(
    private val myRepository: MyRepository,
) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
        return MyViewModel(myRepository) as T
    }
}

Hilt를 도입하기 이전에는 by viewModels() delegate pattern을 사용할 때 직접 Factory를 전달해야 했습니다. 하지만 Hilt를 도입한 이후엔 더 이상 ViewModelFactory를 전달하지 않아도 됩니다. Hilt가 무슨 마법이라도 부린 걸까요? 

// As-is
private val viewModel: MyViewModel by viewModels {
    val repository = MyRepository()
    MyViewModelFactory(repository)
}

// To-be
private val viewModel: MyViewModel by viewModels()

SF 작가 아서 C. 클라크(Arthur C. Clarke)는 이런 작품을 남겼습니다. 

"고도로 발달한 기술은 마법과 구별할 수 없다."

이 마법 같은 일은 사실 Hilt라는 고도로 발달한 기술이 부린 마법이었던 것이죠. 이제 Hilt의 마법을 풀어 보겠습니다.

HiltViewModel

비밀을 파헤치기에 앞서 앞서 살펴본 내용들을 되짚어 보겠습니다.

 

인자가 필요한 ViewModel에 의존성을 주입하기 위해서는 반드시 ViewModelFactory를 직접 전달해야 했습니다. 하지만 Hilt를 사용한 이후로는 더 이상 Factory를 전달하지 않아도 되죠. 그렇다면 자연스럽게 이런 의문이 생깁니다.

 

그럼 ViewModelFactory는 누가 생성해 주는 걸까?

우리가 생성하지 않았다면 Hilt가 내부에서 ViewModelFactory를 대신 만들어주는 게 아닐까?

 

바로 이것이 우리가 파헤칠 Hilt의 첫 번째 마법입니다.

@HiltViewModel
class BookViewModel @Inject constructor(
    private val bookRepository: BookRepository,
) : ViewModel()

Hilt가 컴파일 타임에 생성한 클래스들입니다. 이를 통해 첫 번째 마법의 비밀이 드러나게 되었습니다. Hilt는 컴파일 타임에 ViewModel을 생성하기 위한 ViewModelFactory를 생성합니다. 필요한 의존성을 Provider를 통해 주입받고 newInstance()를 호출해 VIewModel을 생성하는 형태입니다.

Provider

Hilt는 필요한 의존성을 Provider 형태로 관리합니다. Provider란 실제 인스턴스를 직접 들고있는게 아니라 필요할 때 꺼내쓸 수 있도록 지연 생성을 지원하는 래퍼입니다. 즉, 의존성 주입 위해 인스턴스들을 모두 미리 초기화해 들고 있는게 아니라 get()이 호출되는 시점에 실제 인스턴스를 가져옵니다. 

이를 조금 더 쉽게 설명하기 위해 우아한테크코스에서 수동 DI를 구현하는 미션을 예시로 들어보겠습니다. 저는 의존성 주입을 위한 Repository 객체들을 미리 초기화 해놓고 필요할 때마다 꺼내서 주입해 주는 방식으로 구현해 다음과 같은 리뷰를 받았습니다.

샤라웃 투 디랙

이러한 방식의 문제점은 바로 메모리 누수입니다. 실제로 사용되지 않은 객체임에도 모두 객체가 생성되어 DI Container에 담아놓고 있었기 때문입니다. 이를 개선하고자 디랙이 제안해 준 것처럼 인스턴스 대신 인스턴스를 초기화하기 위한 게터를 들고 있게 하였습니다.

 

이후 의존성을 주입할 때 인스턴스가 이미 있다면 그대로 반환하고 만약 없다면 생성해서 저장하도록 구현했습니다. 해당 코드는 여기서 확인하실 수 있습니다.

정리

이제 처음의 크래시 원인을 이해할 수 있습니다.

  • Hilt는 @HiltViewModel과 @AndroidEntryPoint 어노테이션을 보고 코드를 생성합니다
  •  어노테이션이 없으면 Hilt는 "아, 이건 내가 관리할 필요 없구나"라고 판단합니다.
  • 따라서 컴파일은 성공하지만 런타임에 Factory를 찾지 못해 크래시가 발생합니다

Hilt의 마법이 작동하려면 올바른 주문(@HiltViewModel, @AndroidEntryPoint)이 필요합니다. 어노테이션 하나를 빼먹는 실수로 런타임 크래시를 경험했지만 덕분에 ViewModel의 생성 과정과 Hilt의 내부 동작 원리를 깊이 이해할 수 있었습니다.

'Android' 카테고리의 다른 글

Compose 디자인 시스템 설계하기  (0) 2026.01.22
아주 쉽게 알아보는 뷰가 그려지기까지의 여정  (0) 2025.10.29
운영체제 메모리  (0) 2025.10.14
안드로이드에서 네트워크 상태에 따라 API를 재호출해보자  (0) 2025.09.27
Retrofit Internals - Retrofit In Coroutine  (0) 2025.06.20
'Android' 카테고리의 다른 글
  • Compose 디자인 시스템 설계하기
  • 아주 쉽게 알아보는 뷰가 그려지기까지의 여정
  • 운영체제 메모리
  • 안드로이드에서 네트워크 상태에 따라 API를 재호출해보자
빨주노초잠만보
빨주노초잠만보
  • 빨주노초잠만보
    과거의 나를 통해 미래의 나를 성장시키자
    빨주노초잠만보
  • 전체
    오늘
    어제
    • 분류 전체보기 (108)
      • 우아한테크코스 (6)
      • TEKHIT ANDROID SCHOOL (4)
      • Android Architecture (8)
      • Android (38)
      • PROJECT (11)
      • KOTLIN (10)
        • 코루틴의 정석 (3)
      • BACK END (12)
      • CS (4)
      • 컨퍼런스 (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    STATEFLOW
    코틀린 코루틴의 정석
    process Context Switching
    callbackflow
    Repository Pattern
    컴포즈 디자인 시스템
    android clean architecture
    DataSource
    android Room
    MVI
    view 생명주기
    의존성 주입
    2025 우아콘 후기
    DI
    sealed class
    coroutine Context Switching
    android view lifecylce
    thread Context Switching
    retrofit call
    orbit
    repository
    Throttle
    Two pass process
    안드로이드 디자인 시스템
    ThrottleFirst
    flow
    value class
    Room
    Compose Typography
    Clean Architecture
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
빨주노초잠만보
HiltViewModel 의존성 주입 원리
상단으로

티스토리툴바