Hilt 를 이용하여 @AndroidEntryPoint @Provide @Inject 등 어노테이션을 붙여주는 것으로 수동으로 의존성 주입을 했을 때의 보일러 플레이트 코드를 모두 눈에서 치워준다. 그래서 처음 학습할 때에는 오히려 마법같은 일이 벌어져서 어리둥절하며 적용했던 기억이 난다. 조금 익숙해진 지금도 Annotation의 의미를 파악하지 못한 채, 쓰던 방식만 계속 쓰고 있었던 것 같아 이번 기회에 외면하고 있던 내부 동작에 대해 살펴보기로 했다.
다소 내 맘대로 그렸는데, 보통 이런 흐름으로 이런 어노테이션을 붙여 의존성 주입을 하고 있었다.
앞으로 코드를 뜯어볼 프로젝트는 이 레포지토리에서 볼 수 있다. (별거 없어요)
@HiltAndroidApp
Hilt를 사용하려면 Application class를 만들어 @HiltAndroidApp 어노테이션을 붙여주어야 한다. @HiltAndroidApp은 Hilt Component를 생성하고, 생성된 Hilt Component를 사용하는 Base 클래스도 생성한다.
@HiltAndroidApp
class SnapKarloApplication : Application()
빌드하면 build-hilt 경로에 이런 녀석이 생긴다. 복잡하게 생겼지만 알아볼 수 있는 것을 하나하나 짚어보자.
/**
* A generated base class to be extended by the @dagger.hilt.android.HiltAndroidApp annotated class. If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
*/
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ApplicationGenerator")
public abstract class Hilt_SnapKarloApplication extends Application implements GeneratedComponentManagerHolder {
private boolean injected = false;
private final ApplicationComponentManager componentManager = new ApplicationComponentManager(new ComponentSupplier() {
@Override
public Object get() {
return DaggerSnapKarloApplication_HiltComponents_SingletonC.builder()
.applicationContextModule(new ApplicationContextModule(Hilt_SnapKarloApplication.this))
.build();
}
});
@Override
public final ApplicationComponentManager componentManager() {
return componentManager;
}
@Override
public final Object generatedComponent() {
return this.componentManager().generatedComponent();
}
@CallSuper
@Override
public void onCreate() {
hiltInternalInject();
super.onCreate();
}
protected void hiltInternalInject() {
if (!injected) {
injected = true;
// This is a known unsafe cast, but is safe in the only correct use case:
// SnapKarloApplication extends Hilt_SnapKarloApplication
((SnapKarloApplication_GeneratedInjector) generatedComponent()).injectSnapKarloApplication(UnsafeCasts.<SnapKarloApplication>unsafeCast(this));
}
}
}
- Application을 상속받고 있으며, GeneratedComponentManagerHolder를 구현하고 있다.
- ApplicationComponentManager를 가지고 있다.
- ApplicationContextModule 이 ApplicationContext를 제공한다.
- onCreate()에서 뭔가 생성해서 Application 클래스를 Inject하고 있다.
- 1번만 주입되게끔 injected라는 flag를 사용하고 있다.
Inject 하고 있는 GeneratedInjecter들은 build-source-kapt-debug 경로에 인터페이스가 생성되어 있는 것을 볼 수 있다.
@GeneratedEntryPoint
@InstallIn(SingletonComponent.class)
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.InjectorEntryPointGenerator")
public interface SnapKarloApplication_GeneratedInjector {
void injectSnapKarloApplication(SnapKarloApplication snapKarloApplication);
}
ApplicationComponentManager 클래스와 generatedComponent() 를 살펴보지 않을 수 없다.
public final class ApplicationComponentManager implements GeneratedComponentManager<Object> {
private volatile Object component;
private final Object componentLock = new Object();
private final ComponentSupplier componentCreator;
public ApplicationComponentManager(ComponentSupplier componentCreator) {
this.componentCreator = componentCreator;
}
@Override
public Object generatedComponent() {
if (component == null) {
synchronized (componentLock) {
if (component == null) {
component = componentCreator.get();
}
}
}
return component;
}
}
- GeneratedComponentManager를 구현하고 있다.
- generatedComponent()는 ComponentSupplier를 통해 component를 생성하고 있다.
- 여기서 생성되는건 대체 뭐지?? component라는게 대체 머야.. 여기에 injectApplication()을 한다는거지?? 👀
- A manager for the creation of components that live in the Application. 라는 주석이 달려는 있는데..
➡ 일단 OnCreate()에서 무언가 Component를 생성하고, Application()를 주입하고 있는 것은 명백하다!
🙋♀️ 근데.. Hilt_Application에서 inject하고 있지 Application에서 하고 있는건 아니지 않나요? 이게 어떻게 실행되죠?
// If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
Gradle이 Application Bytecode를 슬쩍 Hilt_Application Bytecode로 바꿔치기 한다고 한다.
@AndroidEntryPoint
어노테이션이 붙은 클래스에 관한 개별 Hilt Component를 생성한다. EntryPoint를 통해 특정 Component에 접근하고, 해당 Component로부터 의존성을 주입 받을 수 있다.
우선 AndroidEntryPoint가 가장 먼저 시작되는 MainActivity를 살펴보자. 역시나 Hilt_MainAcitivy가 생성되어 있었다. 전체적인 구성은 Hilt_Appllication이랑 비슷하지만 다른 곳이 몇군데 있다.
* A generated base class to be extended by the @dagger.hilt.android.AndroidEntryPoint annotated class. If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
*/
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ActivityGenerator")
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManagerHolder {
private volatile ActivityComponentManager componentManager;
private final Object componentManagerLock = new Object();
private boolean injected = false;
Hilt_MainActivity() {
super();
_initHiltInternal();
}
Hilt_MainActivity(int contentLayoutId) {
super(contentLayoutId);
_initHiltInternal();
}
private void _initHiltInternal() {
addOnContextAvailableListener(new OnContextAvailableListener() {
@Override
public void onContextAvailable(Context context) {
inject();
}
});
}
@Override
public final Object generatedComponent() {
return this.componentManager().generatedComponent();
}
protected ActivityComponentManager createComponentManager() {
return new ActivityComponentManager(this);
}
@Override
public final ActivityComponentManager componentManager() {
if (componentManager == null) {
synchronized (componentManagerLock) {
if (componentManager == null) {
componentManager = createComponentManager();
}
}
}
return componentManager;
}
protected void inject() {
if (!injected) {
injected = true;
((MainActivity_GeneratedInjector) this.generatedComponent()).injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
}
}
@Override
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
return DefaultViewModelFactories.getActivityFactory(this, super.getDefaultViewModelProviderFactory());
}
}
- 생성자에서 OnContextAvailableListener를 달아주었고, 오버라이드하여 MainActivity를 주입하는 inject() 함수를 onContextAvailable에서 호출하도록 하고 있다.
- 누가 OnContextAvailableListener를 구현하고 있을까?
- 어느 시점에 onContextAvailable이 호출될까?? (= 어느 시점에 MainActivity가 주입되는걸까??)
바로 ContextAwareHelper라는 클래스이다. 그리고 이 클래스는 ComponentActivity (를 비롯해 상속받는 FragmentActivity, AppcompatActivity) 안에서 사용되고 있다. Lifecycle에 따라 context를 관리하고 있다.
class ContextAwareHelper {
private val listeners: MutableSet<OnContextAvailableListener> = CopyOnWriteArraySet()
@Volatile
private var context: Context? = null
// 생략
fun addOnContextAvailableListener(listener: OnContextAvailableListener) {
context?.let {
listener.onContextAvailable(it)
}
listeners.add(listener)
}
// 생략
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
... {
final ContextAwareHelper mContextAwareHelper = new ContextAwareHelper();
public void onStateChanged(...) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
...
}
}
protected void onCreate(@Nullable Bundle savedInstanceState) {
...
mContextAwareHelper.dispatchOnContextAvailable(this);
super.onCreate(savedInstanceState);
...
}
@Override
public final void addOnContextAvailableListener(
@NonNull OnContextAvailableListener listener) {
mContextAwareHelper.addOnContextAvailableListener(listener);
}
@Override
public final void removeOnContextAvailableListener(
@NonNull OnContextAvailableListener listener) {
mContextAwareHelper.removeOnContextAvailableListener(listener);
}
복잡하기도 하고, 더 깊어지면 주제에서 벗어날 것 같아 Pluu님이 ContextAware과 ContextAwareHelper에 대해 분석한 글을 참고하여 이해했다.
https://pluu.github.io/blog/android/2020/09/30/contextaware/
어느 시점에 onContextAvailable이 호출될까??
➡ Context가 유효해지는 시점에 호출된다.
➡ Context가 유효해지는 시점에 생성된 Component에 MainActivity를 주입한다.
- 그래서 Component가 뭔데??? 거기 주입해서 뭘하는건데???? 언제 거기 접근하는데?? 💥
그래서 Component가 뭔데??
Dagger에 대한 지식이 없어서 그런지, Hilt 내부 구조에 대해 잘 몰라서 그런지... 혼자서 뒤적거려봐도 실마리를 잡을 수 없었다. 그래서 자료들을 찾아보는 도중 2020 드로이드나이츠 발표 영상을 보게 되었다.
- @HiltAndroidApp으로 ApplicationComponent가 먼저 생성된다.
- @AndroidEntryPoint를 Activity에 추가하여 ActivityComponent가 생성된다.
- ActivityComponent는 ApplicationComponent의 하위 Component 이다.
! 하위 컴포넌트는 직계관계의 상위 컴포넌트가 가지고 있는 의존성에 접근할 수 있음
>> MemoRepository를 ActivityComponent를 통해 주입받는다면, 귀속된 Activity Scope 내에서 주입하기 때문에 서로 다른 MemoRepository 객체가 주입된다.
>> 같은 객체를 주입받고 싶다면 ApplicationComponent를 통해 주입받아야 한다.
Component 정리
- 주입할 객체들을 Component 속에 담아두었다가 필요한 곳에 주입한다.
- Component의 Scope 내에서 유효한 Container다.
아하! 공식문서에는 항상 '구성요소'라고 번역되어서 더 헷갈렸던 것 같다.
수동 의존성 주입을 하며 알았던 Container와 '구성요소'로 번역된 Component를 연결짓지 못해서 혼란스러웠던 것이었다.
Component를 이해하고 나니, @InstallIn 어노테이션은 자연스럽게 이해가 되기 시작했다.
@InstallIn
Hilt Module이 어떤 모듈에 설치될 것인지 명시한다.
Hilt가 이걸 보고 컴파일 타임에 관련 코드를 생성한다.
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.addInterceptor(KaKaoInterceptor())
.addInterceptor(
HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.build()
}
...
➡ NetworkModule을 ApplicationComponent에 설치하겠다는거다. 앱 어디서 불러도 같은 NetworkModule을 주입한다.
NetworkModule을 @InstallIn(ViewModelComponent::class)로 변경하고, 똑같은 기능을 하는 ViewModel 클래스를 하나 더 만들어서 실행해보면... 각기 다른 Retroift 객체가 주입된 것을 확인할 수 있었따.
Component 의 생명주기?
사실 우린 이미 봤다.
public abstract class Hilt_SnapKarloApplication extends Application implements GeneratedComponentManagerHolder {
@CallSuper
@Override
public void onCreate() {
hiltInternalInject();
super.onCreate();
}
}
public class ComponentActivity extends androidx.core.app.ComponentActivity {
protected void onCreate(...) {
...
mContextAwareHelper.dispatchOnContextAvailable(this);
super.onCreate(savedInstanceState);
...
}
}
public void onStateChanged(..) {
if (event == Lifecycle.Event.ON_DESTROY) {
mContextAwareHelper.clearAvailableContext();
...
}
}
...
- ApplicationComponent
- Application의 onCreate()에서 생성
- ActivityComponent
- Activity의 onCreate()에서 생성
- onDestroy()에서 context 지우면서 삭제
생성된 Component | 생성 위치 | 소멸 위치 |
SingletonComponent | Application#onCreate() | Application 소멸됨 |
ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() |
ViewModelComponent | ViewModel 생성됨 | ViewModel 소멸됨 |
ActivityComponent | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View#super() | View 소멸됨 |
ViewWithFragmentComponent | View#super() | View 소멸됨 |
ServiceComponent | Service#onCreate() | Service#onDestroy() |
@Inject
생성자, 필드에 @Inject를 붙여서 객체를 주입하여 사용한다.
아래의 두 코드를 각각 디컴파일하면 어떤 차이가 있을까?
class GalleryRepository @Inject constructor(
private val galleryLocalDataSource: GalleryLocalDataSource
)
class GalleryRepository (
private val galleryLocalDataSource: GalleryLocalDataSource
)
public final class GalleryRepository {
private final GalleryLocalDataSource galleryLocalDataSource;
@Inject
public GalleryRepository(@NotNull GalleryLocalDataSource galleryLocalDataSource) {
Intrinsics.checkNotNullParameter(galleryLocalDataSource, "galleryLocalDataSource");
super();
this.galleryLocalDataSource = galleryLocalDataSource;
}
public final class GalleryRepository {
private final GalleryLocalDataSource galleryLocalDataSource;
public GalleryRepository(@NotNull GalleryLocalDataSource galleryLocalDataSource) {
Intrinsics.checkNotNullParameter(galleryLocalDataSource, "galleryLocalDataSource");
super();
this.galleryLocalDataSource = galleryLocalDataSource;
}
}
생성자에 @Inject가 붙은것 외에는 차이가 없다. 그렇다면 의존성 주입이 일어나는 곳은 따로 있다는 것이다. 생성자에 친히 GalleryLocalDataSource 인스턴스를 넣어주는 곳은 build 하여 생성된 GalleryRepository_Factory 였다.
public final class GalleryRepository_Factory implements Factory<GalleryRepository> {
private final Provider<GalleryLocalDataSource> galleryLocalDataSourceProvider;
public GalleryRepository_Factory(
Provider<GalleryLocalDataSource> galleryLocalDataSourceProvider) {
this.galleryLocalDataSourceProvider = galleryLocalDataSourceProvider;
}
@Override
public GalleryRepository get() {
return newInstance(galleryLocalDataSourceProvider.get());
}
public static GalleryRepository_Factory create(
Provider<GalleryLocalDataSource> galleryLocalDataSourceProvider) {
return new GalleryRepository_Factory(galleryLocalDataSourceProvider);
}
public static GalleryRepository newInstance(GalleryLocalDataSource galleryLocalDataSource) {
return new GalleryRepository(galleryLocalDataSource);
}
}
Factory<T> 에서 인스턴스를 만들 때 Provider<T>를 통해 인스턴스를 가져와서, 생성자에 넣어서 만들어 주는 모양이다.
Provider<T>
Provider<T>에 대한 자세한 설명은 주석에 달려있었다. 내용을 번역하여 정리하자면,
- Provider<T>는 일반적으로 Injector에 의해 T의 인스턴스를 제공하는 인터페이스
- Provider<T>를 통해 T 타입의 인스턴스를 요청할 수 있으며, Injector가 이 요청에 따라 T의 인스턴스를 생성하거나 캐싱된 인스턴스를 제공
갑자기 튀어나온 Injector?
우리는 Hilt를 사용하면서 Injector라는 것을 만든 적이 없다. 그런데 Injector가 인스턴스를 관리한다고?
당연한 것이다. Injector는 Hilt가 내부적으로 생성하는 클래스이기 때문이다. @AndroidEntryPoint 로 지정된 Activity, Fragment, Service 의 Injector를 생성하고 관리한다.
@Module
생성자가 없는 Interface, 혹은 Room과 Retrofit 등 외부 라이브러리에 존재하는 클래스들을 주입하기 위해서는 Hilt 모듈을 사용해야만 한다. ??: 모르는데 어떻게 주입해요!!
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
...
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
...
.build()
}
@Provides
@Singleton
fun provideKarloService(retrofit: Retrofit): KarloService {
return retrofit.create(KarloService::class.java)
}
}
이렇게 @Provide로 지정한 클래스 각각의 Factory가 생성되어 있다. 하나만 살펴보자면... 앞서 살펴본 Factory의 구조와 동일하게 Provider를 통해 필요한 인스턴스(okhttpclient)를 가져와서 생성자에 넣어 인스턴스(retrofit)를 만들고 있다. 다른 부분은 생성자에서 provideRetrofit으로 인스턴스를 생성하는 과정에서 내가 만든 NetworkModule에 접근하여 Retrofit의 인스턴스를 생성하고 있다. 내가 Retrofit의 인스턴스를 만드는 방법을 NetworkModule에 적어놨기 때문이다..
public final class NetworkModule_ProvideRetrofitFactory implements Factory<Retrofit> {
private final Provider<OkHttpClient> clientProvider;
public NetworkModule_ProvideRetrofitFactory(Provider<OkHttpClient> clientProvider) {
this.clientProvider = clientProvider;
}
@Override
public Retrofit get() {
return provideRetrofit(clientProvider.get());
}
public static NetworkModule_ProvideRetrofitFactory create(Provider<OkHttpClient> clientProvider) {
return new NetworkModule_ProvideRetrofitFactory(clientProvider);
}
public static Retrofit provideRetrofit(OkHttpClient client) {
return Preconditions.checkNotNullFromProvides(NetworkModule.INSTANCE.provideRetrofit(client));
}
}
그러면 NetworkModule의 Injector는? @InstallIn(SingletonComponent::class) 이라고 적었으니 Application의 Injector 일 것이다. 지금까지 알아본 것으로 미루어 보면 다른 컴포넌트를 적으면 그 컴포넌트와 관련된 Injector 가 Module을 관리할 것이다.
지금까지 Hilt를 사용하면서 각각의 Annotation이 하는 일을 제대로 알지 못했고, @InstallIn과 @Singleton과 같은 Scope와 관련된 옵션을 파악하지 못한 채 모두 Singleton으로 사용했었다. 이제는 찝찝하지 않게 Hilt의 도움을 받아 의존성을 주입할 수 있겠다!