Dagger Hilt 사용방법에 대해 생각해보기: EntryPoint

Dagger Hilt의 EntryPoint를 이용하여, Hilt가 지원하지 않는 클래스에 의존성을 주입하는 방법을 살펴봅니다.

Sungyong An
11 min readApr 22, 2024

--

이 글은 I/O Extended 2023 Seoul 행사에서 발표했었던 “Dagger Hilt로 의존성 주입하기” 내용의 일부입니다.

Dagger Hilt를 사용하고 있다면 Activity, ViewModel 등에 의존성 주입할 때 사용하는 @AndroidEntryPoint, @HiltViewModel, @Inject와 같은 Annotation을 자주 사용할 것 같습니다. 그럼 혹시 @EntryPoint 라는 Annotation도 사용하고 계신가요? ✋

공식 문서에서 소개하는 것처럼, @EntryPoint는 Hilt가 지원하지 않는 클래스에 의존성을 주입할 때 사용하는 Annotation입니다. 안드로이드 플랫폼의 구성요소 일부와 ViewModel, WorkManager 같은 Jetpack 라이브러리에는 구글에서 의존성 주입 방법을 제공하고 있지만, 3rd party 라이브러리를 포함하여 이외 대부분의 클래스에는 의존성 주입 방법을 제공하지 않는데요. 오늘은 @EntryPoint를 이용한 의존성 주입 방법을 살펴보려 합니다.

앱에서 클래스 생성을 직접하는 경우에는 생성자에 바로 주입할 수 있습니다.
라이브러리의 클래스도 앱에서 직접 생성하는 경우, 이 방법으로 충분합니다.

class ExampleClass @Inject constructor(
private val bar: Bar,
) { ... }

하지만 라이브러리에서 클래스 생성을 관리하는 경우에는 의존성을 주입할 수 없습니다.

class ExampleInitializer @Inject constructor( // X
private val bar: Bar,
) : Initializer<Example> { ... }

이 때, @EntryPoint를 이용하여 Hilt의 의존성을 주입할 수 있습니다.
@EntryPoint를 사용할만한 몇 가지 예시를 들어보겠습니다.

예시: App Startup

Startup은 앱 시작 시 구성 요소를 초기화하는 라이브러리입니다.
Initializer를 만들다보면 Hilt로 의존성을 재활용하고 싶은 경우가 있는데요.
Context를 이용하여, Application 범위의 의존성을 주입할 수 있습니다.
아래는 의존성을 주입받는 곳에 EntryPoint를 정의하는 방법입니다.

class ExampleInitializer : Initializer<Example> {

@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleInitializerEntryPoint {
fun bar(): Bar
}

override fun create(context: Context): Example {
val entryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ExampleInitializerEntryPoint::class.java
)
val bar = entryPoint.bar()
...
}
}

예시: Jetpack Glance

Jetpack Glance는 Jetpack Compose 기반의 앱 위젯 프레임워크입니다.
앱 위젯을 업데이트하는데 필요한 데이터 목적으로 Repository를 주입하고 싶을 때에도 사용할 수 있습니다.
이번에는 의존성을 제공하는 측에서 EntryPoint를 정의하는 방법입니다.

class SociaLiteAppWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
val repository = WidgetModelRepository.get(context)
...
}
}

@Singleton
class WidgetModelRepository @Inject internal constructor(...) {
...

companion object {
fun get(context: Context): WidgetModelRepository {
val entrypoint = EntryPoints.get(
context.applicationContext,
WidgetModelRepositoryEntryoint::class.java,
)
return entrypoint.widgetModelRepository()
}
}
}

자세한 코드가 궁금하다면 android/socialite 코드를 참고해보세요.

예시: Glide

Glide는 이미지 로딩 라이브러리입니다.
아마도 안드로이드 앱에서 가장 많이 사용되고 있을 것 같은데요.
AppGlideModule을 이용한 configuration 기능을 제공하고 있습니다.
마찬가지로 Application 범위의 의존성을 주입할 수 있습니다.

@GlideModule
class ExampleAppGlideModule : AppGlideModule() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleAppGlideModuleEntryPoint {
fun bar(): Bar
}

override fun registerComponents(
context: Context, glide: Glide, registry: Registry
) {
val entryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ExampleAppGlideModuleEntryPoint::class.java
)
val bar = entryPoint.bar()
...
}
}

정리하면 Context에 접근할 수 있는 곳이라면 어디든 @EntryPoint를 이용하여 손쉽게 의존성을 주입할 수 있습니다.

Jetpack Compose

Compose를 모르는 분은 없겠죠?
아쉽게도 Compose 역시 Hilt가 의존성 주입 방법을 기본 지원하지 않는데요.
Composable 함수에 의존성을 전달하는 방법이 몇 가지 있습니다.

State Hoisting

첫번째로 모든 Composable 함수를 거쳐 의존성을 전달하는 방법입니다.

예시로는 GrandChildren에서 Bar를 사용하려면, Activity에 주입받은 Bar를 GrandParents에 전달하고 각각의 Composable을 순차적으로 거쳐 GrandChildren까지 전달할 수 있습니다.

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var bar: Bar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GrandParents(bar = bar)
}
}
}
@Composable fun GrandParents(bar: Bar) {
Parents(bar = bar)
}
@Composable fun Parents(bar: Bar) {
Children(bar = bar)
}
@Composable fun Children(bar: Bar) {
GrandChildren(bar = bar)
}
@Composable fun GrandChildren(bar: Bar) {
// Use bar
}

그런데 GrandChildren에서만 사용하는 Bar가 모든 Composable을 거쳐가는 것은 조금 과해보이지 않나요?

CompositionLocal

CompositionLocal을 이용하여 개선할 수 있습니다.

예시로는, Activity에 주입 받은 Bar를 LocalBar를 통해 GrandChildren에 전달할 수 있습니다. 전달하는 관점에서는 코드가 한결 간결해 보입니다.

@AndroidEntryPoint
class ExampleActivity : Activity() {
@Inject lateinit var bar: Bar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
CompositionLocalProvider(LocalBar provides bar) {
GrandParents()
}
}
}
}

val LocalBar = staticCompositionLocalOf<Bar> { error("...") }

@Composable fun GrandParents() {
Parents()
}
@Composable fun Parents() {
Children()
}
@Composable fun Children() {
GrandChildren()
}
@Composable fun GrandChildren() {
val bar: Bar = LocalBar.current
// Use bar
}

그런데 CompositionLocal를 의도한대로 사용한 것이 맞을까요? 특정 Composable에 의존성을 주입하는 목적으로 사용한다면, 주입해야 하는 항목이 점점 늘어나는 문제가 있습니다. 자연스럽게 CompositionLocalProvider를 구성해야 하는 Activity 등에도 필드 주입 항목이 점점 늘어날 거구요.

✅ EntryPoint

EntryPoint가 하나의 대안이 될 수 있을 것 같습니다.

코드로는 더 이상 Activity에 주입받을 필요 없이, CompositionLocal처럼 GrandChildren에서 바로 참조할 수 있습니다. 방법은 LocalContext를 이용하여, Application 범위의 의존성을 주입하는 것입니다. 간단하죠? 😄

@AndroidEntryPoint
class ExampleActivity : Activity() {
// @Inject lateinit var bar: Bar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
GrandParents()
}
}
}

...

@Composable fun GrandChildren() {
val bar: Bar = rememberBar()
// Use bar
}

@Composable private fun rememberBar(): Bar {
val context = LocalContext.current
return remember {
val entryPoint = EntryPointAccessors.fromApplication(
context.applicationContext,
ExampleEntryPoint::class.java,
)
entryPoint.bar()
}
}

@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleEntryPoint {
fun bar(): Bar
}

여기서는 간단히 Application 범위의 의존성 주입을 소개했는데요. Compose에서의 의존성 주입에 대해 잘 작성되어 있는 공식 블로그 글이 있으니, 한번쯤 읽어보시는 것을 추천합니다.

오늘은 Dagger Hilt가 기본으로 지원하지 않는 클래스에 의존성을 주입하는 방법과 몇 가지 예시를 살펴봤습니다. 혹시라도 @EntryPoint를 몰랐거나, 활용하지 않는 분들에게 이 내용이 조금이나마 도움이 되었으면 하구요. 다음번에는 Dagger Hilt의 내부동작에 대한 내용으로 돌아오겠습니다.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Sungyong An
Sungyong An

Written by Sungyong An

Android Developer in South Korea, Android GDE

No responses yet

Write a response