Dagger Hilt 내부코드 분석
Dagger Hilt가 생성하는 코드를 분석하여, Dagger Hilt가 안드로이드 앱에 의존성을 주입하는 과정을 이해해봅시다.
이 글은 드로이드나이츠 2023 행사에서 발표했었던 “Dagger Hilt로 의존성 주입부터 멀티모듈화까지” 내용의 일부입니다.
안드로이드 앱을 개발할 때, Dagger Hilt를 사용하면서 어떻게 동작하는 것인지 반드시 이해하고 쓸 필요는 없습니다. 하지만 저처럼 호기심이 많아 내부동작이 궁금한 분들도 있을 것 같은데요. 오늘은 Dagger Hilt가 생성하는 코드를 분석하여, 앱에 의존성을 주입하는 과정을 이해해보려합니다.
Dagger Hilt 동작 원리
먼저 간단한 코드를 살펴보겠습니다. 안드로이드 공식 문서에 있는 Car/Engine 예제를 Dagger Hilt로 작성한 코드입니다.
@Provides
를 이용해서 Engine 클래스를 생성 제공하고, @Inject
를 이용하여 Car 클래스 생성자에 Engine을 주입합니다. Activity처럼 안드로이드 플랫폼에서 생성을 관리하는 클래스에는 @Inject
로 필드에 Car를 주입합니다.
다양한 Annotation을 사용하여 의존성을 주입하고 있는데요. Annotation을 따라가보면 단순히 정의만 되어 있을 뿐, 의존성 주입과 관련된 코드는 찾을 수 없습니다. 🤔
Annotation 만으로는 아무런 효과가 없고, Hilt Compiler가 Annotation을 기반으로 의존성 주입에 필요한 코드들을 생성합니다. 이것이 동작 원리입니다.
본격적으로 Dagger Hilt가 생성하는 코드를 살펴볼까요?
위는 Hilt 공식 문서의 Quick Start 가이드에서 가져온 코드입니다.
그대로 앱을 빌드해보면, 앱 모듈의 build 폴더에 다양한 자바 파일들이 생성됩니다. 클래스 이름으로 구분해보면 규칙이 보이시나요?
생성된 파일 중 하나를 살펴보면, Hilt Compiler가 생성한 코드에도 여전히 Annotation이 있는 것을 볼 수 있습니다. 🙄
그 이유는 Hilt가 만들어진 목적이 Android 앱에서 Dagger를 사용하기 쉽게 만드는 것이어서, Hilt가 Dagger 코드를 생성하는 형태로 되어있기 때문입니다. 즉, Dagger가 최종 코드를 생성합니다.
기본적인 동작 원리를 소개했구요. 이제 과정을 하나씩 살펴보겠습니다.
(1) 앱 의존성 그래프 생성하기
@HiltAndroidApp
은 앱의 의존성 그래프를 생성하는 기준이 됩니다. Dagger 코드를 만든 다음, 최종적으로 구현체인 앱 의존성 그래프가 생성됩니다.
맨 처음의 Quick Start 가이드를 그림으로 그린 모습입니다.
Bar를 제공하는 FooModule이 있고, 의존성 그래프를 통해서 Application과 Activity에 주입하고 있습니다. 즉, 의존성을 제공하는 곳과 주입받는 곳이 모두 앱 의존성 그래프인 DaggerExampleApplication_HiltComponents_SingletonC
를 통합니다.
Hilt가 기본 제공하는 Component와 Scope입니다. 이런 형태의 그림을 공식 문서에서도 다들 보시고 대충 이렇구나 하고 많이 보셨을 것 같은데요. 의존성 그래프 코드는 이를 기준으로 생성됩니다. 가장 먼저 @SingletonComponent
를 간단히 살펴보겠습니다.
@Singleton
은 @Scope
을 갖고 있는 Annotation이고,@SingletonComponent
는 그것에 대한 컴포넌트 정의입니다.
Hilt Processor에 의해서 SingletonC
라는 추상 클래스가 만들어지고, 이 클래스에 @Singleton
과 @SingletonComponent
가 붙게 됩니다.
다음으로는 Dagger Processor에 의해 SingletonC
추상 클래스를 상속하는 SingletonCImpl
이라는 구현체가 만들어집니다. 아까 봤던 각각의 Component마다 이런 클래스가 생성된다고 보면 됩니다.
실제 의존성 그래프 코드를 보면 각 Component, Scope에 대응하는 클래스가 이렇게 됩니다. 직접 생성된 코드를 살펴볼 때 참고하세요.
다음으로 Component 계층에 대한 내용입니다. “상위 컴포넌트의 의존성은 하위에 주입할 수 있고, 역으로는 할 수 없다”라는 내용을 보셨을 것 같은데요. 어떻게 되어 있는지 구조를 간단히 살펴보겠습니다.
아까 봤던 앱 의존성 그래프 코드를 그림으로 그린 모습입니다.
Hilt가 각 컴포넌트의 추상 클래스들을 만들고, Dagger가 이걸 상속하는 구현체들을 의존성 그래프에 만들게 됩니다.
의존성 그래프를 보면 ActivityRetainedCImpl
은 SingletonCImpl
을 참조하고 , ActivityCImpl
은 SingletonCImpl
과 ActivityRetainedCImpl
를 직접 참조하는 형태로 되어 있습니다.
코드로는, ActivityRetainedCImpl
생성자에 SingletonCImpl
을 받구요.
ActivityCImpl
도 생성자에 SingletonCImpl
과 ActivityRetainedCImpl
을 받도록 되어 있습니다. 이런 형태를 이용하여, 하위 컴포넌트는 상위 컴포넌트의 의존성에 직접 접근할 수 있고, 반대로는 가능하지 않도록 되어 있습니다.
(2) 직접 정의한 의존성을 그래프에 추가하기
지금까지 의존성 그래프가 어떤 형태로 생성되는지 간단하게 살펴봤는데요. 이번에는 직접 정의한 의존성이 그래프에 어떤 형태로 추가되는지 살펴보겠습니다.
여기서는 Bar를 제공해주는 FooModule
을 살펴보겠습니다.
Hilt Module은 선언된 컴포넌트에 추가됩니다.
여기서 FooModule
은 @SingletonComponent
로 정의한 모듈이라서, SingletonC라는 추상 클래스에 추가됩니다.
그 다음에 @Provides
마다 의존성을 제공하는 Factory 클래스가 생성됩니다.
여기서는 FooModule_ProvideBarFactory
라는 클래스가 만들어지고, 이걸 이용해서 안드로이드 클래스에 주입하게 될 겁니다.
(3) 안드로이드 클래스에 의존성을 주입하는 과정
마지막으로 안드로이드 클래스에 주입하는 과정을 살펴보겠습니다.
맨 처음에 봤던 것처럼, Hilt_
와 Injector
클래스들이 만들어지는데요.
GeneratedInjector는 컴포넌트 추상 클래스에 선언되어, 최종 구현체에 특정 안드로이드 클래스로 의존성을 주입하는 함수를 생성합니다.
MembersInjector는 @Inject
로 주입하는 필드마다 함수가 생성되구요. 이 때, (2)번 단계에서 생성했던 FooModule_ProvideBarFactory
를 이용하여 Bar를 생성하도록 되어 있습니다.
그리고 주입받는 Application도 코드가 변경됩니다. Hilt Gradle Plugin을 이용한다면, 바이트코드를 변환하여 부모 클래스가 Hilt 클래스로 변경됩니다.
Hilt_
클래스는 GeneratedInjector를 구현하는 SingletonC
에 접근하여 injectExampleApplication
을 호출하는 방식으로 의존성을 주입합니다.
정리
전체적인 흐름을 그림으로 그린 모습입니다.
FooModule_ProvideBarFactory
를 호출해서 Bar를 생성하고, MemberInjector를 통해서 필드로 의존성을 주입하구요.- 최종적으로
Hilt_
클래스는 GeneratedInjector를 통해서,
onCreate 시점에 의존성 주입을 요청합니다.
이 과정은 다른 안드로이드 클래스도 동일하므로, 관련된 코드들을 찾아 분석하고 싶을 때는 이 형태를 참고하세요. 😉
오늘은 Dagger Hilt가 생성하는 코드를 간단히 살펴봤습니다. Hilt를 사용하는데 생성되는 코드들을 반드시 알고 사용할 필요는 없겠지만, 앱에 문제가 생긴다던지 예상치 못한 동작을 마주했을 때는 생성된 코드를 살펴보게 됩니다. 이럴 때 이 글이 Hilt가 생성한 코드를 따라가는 데에 도움이 되었으면 합니다.
아래 글에서 의존성을 그래프에 제공하는 부분과 의존성을 주입받는 부분들을 더 자세하게 살펴봅니다.