Dagger Hilt 내부코드 분석: Advanced

Dagger Hilt가 생성하는 코드를 분석하여, Dagger Hilt가 안드로이드 앱에 의존성을 주입하는 과정을 이해해봅시다.

Sungyong An
8 min readMay 2, 2024

이 글은 드로이드나이츠 2023 행사에서 발표했었던 “Dagger Hilt로 의존성 주입부터 멀티모듈화까지” 내용의 일부입니다.

Dagger Hilt 내부코드 분석” 글에 이어지는 후속 내용입니다. 이 글의 내용을 이해하려면, 이전 글을 먼저 읽고 오는 것을 권장합니다. 🙇

이 글에서는 의존성을 제공하는 부분과 주입받는 부분에서 사용할 수 있는 API마다 생성되는 코드에 어떤 차이가 생기는지 세부적으로 살펴봅니다.

의존성을 제공하는 부분

먼저 의존성을 제공하는 부분에 생성되는 코드를 살펴보겠습니다.

@Provides

이전 글에서 사용했던 방법으로,
@ProvidesFactory 클래스를 통해 의존성을 제공합니다.

@Provides

@Inject

@Inject@Provides와 다르게, 별도의 클래스 생성 없이 의존성을 제공합니다. 그래서 당연히 @Inject를 사용하는 편이 더 효율적입니다.

@Inject

@Binds

그 다음으로 @Binds라는 것도 있습니다. 구현체에 대한 직접적인 참조를 줄이고 싶을 때, 인터페이스를 분리하는데요. 이럴 때, @Inject@Binds를 결합하여 구현체를 인터페이스로 제공하는데 사용할 수 있습니다.

이 경우에도 별도의 클래스 생성 없이 구현체를 바로 제공합니다.
즉, @Inject@Binds를 함께 사용하는 것도 효율적입니다.

@Binds

Scope

이번에는 Scope Annotation입니다.
가장 흔히 사용되는 @Singleton 하나만 살펴보겠습니다.
이 경우에는 DoubleCheckSwitchingProvider라는 것을 사용합니다.

Scope = 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를 붙인 경우에는 의존성을 생성하여 주입하는 코드가 생성됩니다.

@Inject

여기서는 Bar를 생성합니다.

@Inject: Inheritance

그런데 만약 상속하는 클래스에서도 필드에 @Inject 한다면, 각 클래스의 필드마다 서로 다른 객체를 생성할 수 있어서 주의해야 합니다.

더군다나 부모 클래스가 Java라면, package-private으로 가시성을 제한할 수 있어서 동일한 이름으로도 필드를 선언할 수 있습니다. 그러면 불필요하게 여러 번 생성될 수 있으니, 특히 주의해야 합니다. Base 클래스에 @Inject하는 필드는 접근제한자를 protected로 둬서, 하위 클래스에 중복 선언되지 않도록 방지하는 것을 권장합니다.

Provider

다음으로 필드에 @Inject 할 때, Provider를 이용할 수도 있습니다.

Provider는 get()을 호출하는 시점에 의존성을 생성하는, 지연 생성 방법이라고 할 수 있을 것 같습니다. 다만 get()을 호출할 때마다 새로 생성하기 때문에, 자주 호출되는 경우라면 사용하지 않는 편이 좋을 것 같습니다.

Lazy

이런 경우에는 Lazy를 이용하여, 최초 한 번만 생성하도록 할 수 있습니다.

코드를 보면 SwitchingProviderDoubleCheck가 붙는 것을 볼 수 있습니다. 즉, Scope Annotation을 사용한 것과 유사한 형태가 됩니다.

DoubleCheck.provider() ≈ DoubleCheck.lazy()

참고로 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를 사용하자.

--

--