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

의존성 주입 프레임워크인 Dagger Hilt를 이용하여, 안드로이드 플랫폼에 대한 직접적인 참조를 줄이는 방법을 소개해봅니다.

Sungyong An
8 min readApr 15, 2024

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

요즘에는 안드로이드 앱을 개발하면 기본으로 설치할 정도로 Dagger Hilt가 많이 쓰이고 있습니다. 물론 의존성 주입을 지원하는 다른 도구도 많지만, 공식 문서에서 다루는 만큼 가장 많이 사용할 것 같은데요. 오늘은 Dagger Hilt를 이용하여 WorkManager를 사용하는 방법을 살펴보려고 합니다.

먼저 Hilt로 WorkManager에 의존성을 주입하는 방법을 간단히 살펴보면:

  • 1️⃣ 프로젝트에 androidx.hilt 라이브러리를 설치하고,
  • 2️⃣ 의존성을 주입하려는 Worker에 Annotation을 추가하면 됩니다.
@HiltWorker
class ExampleWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters,
workerDependency: WorkerDependency
) : Worker(appContext, workerParams) { ... }

물론 실제로는 Custom Application 유무에 따라 코드를 좀 더 작성해야 합니다만, 오늘 다루려고 하는 주요 내용은 Worker에 의존성을 주입하는 부분이 아니라서 생략하겠습니다.

기본: WorkManager 사용하기

이렇게 정의한 ExampleWorker를 사용할 때는:

  • 1️⃣ 원하는 제약조건을 설정한 WorkRequest를 만들고,
  • 2️⃣ WorkManager에 요청을 전달합니다.
val exampleWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<ExampleWorker>()
.build()

WorkManager
.getInstance(context)
.enqueue(exampleWorkRequest)

여기서 주의깊게 봐야할 부분은 WorkManager에 접근하려면 Context가 반드시 필요하다는 것입니다. 그래서 Context에 접근할 수 있는 Activity, View와 같은 UI Layer의 컴포넌트에서 WorkManager에 접근하는 경우를 많이 봤습니다. 심지어는 ViewModel에서 호출하려고 AndroidViewModel로 변경하는 경우도 있었습니다.

@HiltViewModel
class ExampleViewModel @Inject constructor(
private val application: Application,
) : AndroidViewModel(application) {
...
WorkManager
.getInstance(application)
.enqueue(...)
...
}

이렇게 WorkManager를 사용할 수 있는 위치가 제한되는 문제를 Dagger Hilt로 개선해 보겠습니다.

1단계: Context 없이, WorkManager 사용하기

방법은 간단합니다.

  • 1️⃣ WorkManager를 제공하는 Dagger Module을 추가합니다.
@Module
@InstallIn(SingletonComponent::class)
object TasksModule {

@Singleton
@Provides
fun provideWorkManager(
@ApplicationContext context: Context,
): WorkManager {
return WorkManager.getInstance(context)
}
}
  • 2️⃣ WorkManager가 필요한 곳에서는 @Inject 등으로 주입하면 됩니다.
  @HiltViewModel
class ExampleViewModel @Inject constructor(
+ private val workManager: WorkManager,
) : ViewModel() { ... }

이렇게 Context 의존성 없이, 어느 곳에서나 WorkManager를 사용할 수 있게 개선할 수 있습니다.

하지만 여기서도 아쉬운 점은 있습니다. 호출하는 곳에서는 Worker, WorkManager 등 플랫폼 클래스에 대한 의존성을 갖게 됩니다. 이러면 단위 테스트를 작성하기 어려워지고, 모듈을 분리할 때에도 WorkManager 라이브러리 의존성을 가져가야 합니다.

이 부분은 어떻게 개선할 수 있을까요?

2단계: 플랫폼 클래스에 대한 의존성 숨기기

이것도 간단합니다.

  • 1️⃣ WorkManager에 요청하는 코드를 별도의 클래스로 분리하면 됩니다.
class ExampleTasks @Inject constructor(
private val workManager: WorkManager,
) {
fun execute() {
val request = OneTimeWorkRequestBuilder<ExampleWorker>()
.addTag(ExampleWorker.TAG)
.build()
workManager.enqueue(request)
}
}

Worker, WorkManager 등 플랫폼 클래스에 대한 직접 참조를 없앨 수 있고, 여러 곳에서 반복해서 사용할 때에도 유용합니다.

@HiltViewModel
class ExampleViewModel @Inject constructor(
private val exampleTasks: ExampleTasks,
) : ViewModel() {
...
exampleTasks.execute()
...
}

지금까지 WorkManager를 호출하는 부분에 대해서 사용방법을 개선했는데요. 반대로 호출당하는 Worker 코드도 살펴보겠습니다.

안드로이드 공식 문서에서 가이드하는 Worker에 의존성 주입하는 방법을 따르면, 여러 의존성을 주입받아 복잡한 코드를 작성하게 되기 쉽습니다.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val primary: PrimaryDependency,
private val secondary: SecondaryDependency,
private val tertiary: TertiaryDependency,
) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
val success = /* Complex codes to test */
return if (success) Result.success() else Result.failure()
}
}

물론 WorkManager가 테스트를 지원하고 있지만, 보통은 Worker 동작을 테스트 하기보다 내가 작성한 코드에 대해서만 테스트하고 싶습니다.

보너스: Worker를 가볍게 만들기

Hilt를 이용하여 비즈니스 로직을 UseCase로 분리하면, Worker의 역할을 최소화할 수 있고 테스트하기도 편해집니다.

@HiltWorker
class ExampleWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val example: ExampleUseCase,
) : CoroutineWorker(context, params) {

override suspend fun doWork(): Result {
val success = example()
return if (success) Result.success() else Result.failure()
}
}

class ExampleUseCase @Inject constructor(
private val primary: PrimaryDependency,
private val secondary: SecondaryDependency,
private val tertiary: TertiaryDependency,
) {

operator suspend fun invoke(): Boolean {
return /* Complex codes to test */
}
}

이렇게 기능을 이용하기 위한 목적으로만 WorkManager를 사용하고, 실제 코드는 별도의 클래스로 관심사를 분리하는 것을 권장하고 싶습니다. 이러면 플랫폼 클래스가 Deprecated 되고 새로운 플랫폼 클래스로 마이그레이션해야 할 때도 비즈니스 로직을 보존할 수 있습니다.

오늘은 의존성 주입 프레임워크인 Dagger Hilt를 이용하여, WorkManager에 대한 직접적인 참조를 줄이는 방법을 소개해 봤습니다. 이 내용이 앱 코드를 조금이나마 덜 복잡하게 관리하는데 도움이 되었으면 하구요. 다른 안드로이드 플랫폼 클래스를 사용할 때도 한번쯤 고민해보시길 추천합니다.

--

--