Dagger Hilt 내부코드 분석: Advanced
Dagger Hilt가 생성하는 코드를 분석하여, Dagger Hilt가 안드로이드 앱에 의존성을 주입하는 과정을 이해해봅시다.
이 글은 드로이드나이츠 2023 행사에서 발표했었던 “Dagger Hilt로 의존성 주입부터 멀티모듈화까지” 내용의 일부입니다.
“Dagger Hilt 내부코드 분석” 글에 이어지는 후속 내용입니다. 이 글의 내용을 이해하려면, 이전 글을 먼저 읽고 오는 것을 권장합니다. 🙇
이 글에서는 의존성을 제공하는 부분과 주입받는 부분에서 사용할 수 있는 API마다 생성되는 코드에 어떤 차이가 생기는지 세부적으로 살펴봅니다.
의존성을 제공하는 부분
먼저 의존성을 제공하는 부분에 생성되는 코드를 살펴보겠습니다.
@Provides
이전 글에서 사용했던 방법으로,@Provides
는 Factory 클래스를 통해 의존성을 제공합니다.
@Inject
@Inject
는 @Provides
와 다르게, 별도의 클래스 생성 없이 의존성을 제공합니다. 그래서 당연히 @Inject
를 사용하는 편이 더 효율적입니다.
@Binds
그 다음으로 @Binds
라는 것도 있습니다. 구현체에 대한 직접적인 참조를 줄이고 싶을 때, 인터페이스를 분리하는데요. 이럴 때, @Inject
와 @Binds
를 결합하여 구현체를 인터페이스로 제공하는데 사용할 수 있습니다.
이 경우에도 별도의 클래스 생성 없이 구현체를 바로 제공합니다.
즉, @Inject
와 @Binds
를 함께 사용하는 것도 효율적입니다.
Scope
이번에는 Scope Annotation입니다.
가장 흔히 사용되는 @Singleton
하나만 살펴보겠습니다.
이 경우에는 DoubleCheck
와 SwitchingProvider
라는 것을 사용합니다.
Scope를 선언하면 해당 컴포넌트에 Provider를 생성하는데요. 여기서는 @Singleton
이라 SingletonComponent에 생성됩니다.
그리고 Provider는 SwitchingProvider
에서 의존성을 생성하는데요. 생성한 Bar는 DoubleCheck
의 synchronized를 통해서 객체가 하나만 생성되도록 제한됩니다.
Scope에 대해서 조금 더 알아보겠습니다.
Scope를 선언하는 위치에 DoubleCheck
가 붙습니다.
예를 들어, FooModule에다가 @Singleton
을 붙이면 Provider<BarImpl>이 아닌 Provider<Bar>에 DoubleCheck
가 붙게 됩니다.
즉, Scope를 사용하더라도 의존성이 여러 벌 생성될 수 있다는 의미입니다.
예를 들어, BarImpl 클래스를 Bar/Bar2 인터페이스로 묶어주는 @Binds
함수가 여러 개고 각 함수에 @Singleton
을 사용한다면, 2개의 BarImpl 객체가 생성될 수 있습니다.
그래서 BarImpl이 하나만 생성되는 것을 의도했다면, @Binds
부분이 아닌 @Inject
하는 BarImpl 클래스의 생성자에 @Singleton
을 선언해줘야 합니다.
즉, 이렇게 Scope Annotation을 선언하는 위치를 주의해야 합니다.
의존성을 주입받는 부분
다음으로 의존성을 주입받는 부분에 생성되는 코드를 살펴보겠습니다.
@Inject
안드로이드 클래스의 필드에 @Inject
를 붙인 경우에는 의존성을 생성하여 주입하는 코드가 생성됩니다.
여기서는 Bar를 생성합니다.
그런데 만약 상속하는 클래스에서도 필드에 @Inject
한다면, 각 클래스의 필드마다 서로 다른 객체를 생성할 수 있어서 주의해야 합니다.
더군다나 부모 클래스가 Java라면, package-private으로 가시성을 제한할 수 있어서 동일한 이름으로도 필드를 선언할 수 있습니다. 그러면 불필요하게 여러 번 생성될 수 있으니, 특히 주의해야 합니다. Base 클래스에 @Inject
하는 필드는 접근제한자를 protected로 둬서, 하위 클래스에 중복 선언되지 않도록 방지하는 것을 권장합니다.
Provider
다음으로 필드에 @Inject
할 때, Provider
를 이용할 수도 있습니다.
Provider
는 get()을 호출하는 시점에 의존성을 생성하는, 지연 생성 방법이라고 할 수 있을 것 같습니다. 다만 get()을 호출할 때마다 새로 생성하기 때문에, 자주 호출되는 경우라면 사용하지 않는 편이 좋을 것 같습니다.
Lazy
이런 경우에는 Lazy
를 이용하여, 최초 한 번만 생성하도록 할 수 있습니다.
코드를 보면 SwitchingProvider
와 DoubleCheck
가 붙는 것을 볼 수 있습니다. 즉, Scope Annotation을 사용한 것과 유사한 형태가 됩니다.
참고로 DoubleCheck
의 provider()와 lazy()는 동일한 클래스를 만들도록 되어 있어서, 동작이 거의 동일하다고 보면 됩니다.
하지만 Lazy
를 사용할 때에도 주의할 점이 있습니다.
단일 초기화는 DoubleCheck
에서 구현하고, SwitchingProvider
는 매번 생성합니다. 즉, @Inject
마다 서로 다른 객체를 생성하게 되서, 여러 번 생성될 수 있습니다.
예를 들어, Bar 클래스가 상태를 갖고 있어서 하나의 화면에 하나의 객체만 유지해야 한다면, 위처럼 Lazy
를 사용할 때 문제가 됩니다.
Scope + Provider
이런 경우에는 Lazy
보다, Scope Annotation과 Provider
를 함께 사용하는 것도 방법입니다.
코드로는, 의존성을 지연초기화 할 수 있는 DoubleCheck
가 주입됩니다.
Scope
Scope만 지정하더라도 단일 초기화됩니다. 항상 사용되거나 초기화 비용이 크지 않다면, Provider
는 불필요합니다.
코드로는, 의존성을 주입하기 전에 Provider
의 get()을 먼저 호출합니다.
지난번에 이어, 이번 글에서도 Dagger Hilt가 생성하는 코드를 살펴봤는데요. 아래에 주의해야 할 내용을 요약해뒀으니 참고해보시고, Hilt를 사용하는데 직접적인 도움이 되었으면 합니다. 🙇
- 의존성을 제공할 때,
@Provides
보다는@Inject
/@Binds
를 사용하자. - 객체가 여러 개 생성되지 않도록, Scope를 선언하는 위치를 주의하자.
- 객체를 지연 초기화하되 단 하나만 생성되어야 한다면,
Lazy
보다는 Scope +Provider
를 사용하자.