이번 영상에서는 안드로이드의 백그라운드 작업을 처리하기 위해 만들어진 WorkManager에 대해 알아보도록 하겠습니다.
안드로이드의 백그라운드 작업은 다음과 같이 네 종류의 작업 형태로 분류할 수 있습니다.
실행 시점에 따른 분류
실행 완료 여부에 따른 분류
안드로이드에는 이미 백그라운드 작업을 수행하기 위해 만들어진 수많은 라이브러리(Thread, Executors, Services, AsyncTasks, Handlers and Loopers, Jobs, GCMNetworkManager, SyncAdapters, Loaders, AlarmManager..)들이 존재하는데 위의 기준에 따라 분류해보면 다음과 같을 것입니다.
| Exact Timing | Deferrable | |
|---|---|---|
| Best-Effort | ThreadPool | ThreadPool |
| Guaranteed Execution | ForegroundService | JobScheduler JobDispatcher AlarmManager BroadcastReceivers |
WorkManager는 이중 Deferrable-Guaranteed Execution 에 사용되는 API를 대체하기 위해 만들어졌습니다. 라이브러리가 이미 있는데도 구글은 왜 굳이 또 새로운 라이브러리를 만든걸까요? 그 이유를 알기 위해서는 안드로이드 백그라운드 정책의 변천 과정을 살펴볼 필요가 있습니다.
API 18(Jelly Bean) 까지는 시스템에 Broadcast receiver를 등록하고 앱에서 AlarmManager 로 알람을 전달하면 호출되는 Service로 백그라운드 작업을 수행했습니다. 하지만 API 19(KitKat)부터는 기기의 wakeup 알람과 배터리 소모를 최소화하기 위해 시스템이 알람 타이밍을 변경할 수 있게 되어서 이 AlarmManager가 더 이상 정확한 시간에 동작하지 않게 됩니다.
그러던 와중에 구글은 2014년 Google IO에서 Project Volta를 소개합니다. Project Volta는 전력 소모를 줄일 수 있는 다섯가지 기술(Lazies first, JobScheduler, The ART runtime, Battery Historian, Battery Saver mode)을 제안하였고, 이 내용은 API 21(Lollypop)에 적용됩니다. 이 중 JobScheduler는 백그라운드 작업의 실행 타이밍을 앱이 아닌 시스템이 판단하게 함으로써 배터리 시간을 갉아먹는 무분별한 백그라운드 작업을 제한하는 것이었는데요, 발표에 따르면 JobScheduler는 AlarmManager에 비해 25%의 배터리 개선 효과를 냈다고 합니다.
그 이후로 구글은 API 백그라운드 작업을 제한하는 정책을 끊임없이 추가합니다.
버전에 따라 서로 다른 백그라운드 관리 정책을 적용하는 것은 복잡하고 까다로울 뿐만 아니라, 작업을 잘못 설계할 경우 유저에 따라서는 백그라운드 작업을 수행할 수 없는 경우가 생길 수도 있습니다. 이런 문제에 대응하기 위해 구글은 2016년 Google IO에서 Firebase JobDispatcher를 발표합니다. JobDispatcher는 API 9+에서 사용할 수 있고, 시스템 버전에 따라 AlarmManager와 JobScheduler를 알아서 선택했으므로 버전 간 구현을 구별할 필요가 없었습니다...만 Google Play Service가 설치되어 있어야 한다는 제한이 있어 중국 시장을 대상으로 하는 앱들에는 적용할 수가 없었습니다.
그러다가 결국 2018년 Google IO에서 다음과 같은 특징을 가진 WorkManager가 발표됩니다.
WorkManager의 동작 구조는 다음 그림과 같습니다. API 14부터 사용할 수 있는데 Firebase JobDispatcher처럼 시스템에 따라 적절한 백그라운드 API를 알아서 선택하도록 설계되어 있습니다.

앞에서도 설명했지만, WorkManager는 만능 라이브러리가 아니라 Deferrable-Guaranteed Execution의 특성을 가진 백그라운드 작업에 사용하도록 설계되었습니다. 구글에서는 사용 예시로 다음과 같은 Use Case를 들고 있습니다.
| Use Case | Examples | Solution |
|---|---|---|
| Guaranteed execution of deferrable work | Upload logs to your server Encrypt/Decrypt content to upload/download |
WorkManager |
| A task initiated in response to an external event | Syncing new online content like email | FCM + WorkManager |
| Continue user-initiated work that needs to run immediately even if the user leaves the app | Music player Tracking activity Transit navigation |
Foreground Service |
| Trigger actions that involve user interactions, like notifications at an exact time. | Alarm clock Medicine reminder Notification about a TV show that is about to start |
AlarmManager |
그러면 WorkManager의 실행구조에 대해 알아보도록 하겠습니다.
백그라운드에서 수행할 작업은 Worker 클래스를 상속한 뒤 doWork() 안에서 구현합니다. 이 때 Result를 써서 작업의 성공여부를 반환해야 합니다.
class UploadWorker(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
override fun doWork(): Result {
// Do the work here--in this case, upload the images.
uploadImages()
// Indicate whether the work finished successfully with the Result
return Result.success()
}
}
Constraints를 써서 작업에 다음과 같은 제약조건을 걸 수 있습니다.
| Constraints | Description |
|---|---|
| NetworkType | Constrains the type of network required for your work to run. For example, Wi-Fi (UNMETERED). |
| BatteryNotLow | When set to true, your work will not run if the device is in low battery mode. |
| RequiresCharging | When set to true, your work will only run when the device is charging. |
| DeviceIdle | When set to true, this requires the user’s device to be idle before the work will run. This can be useful for running batched operations that might otherwise have a negative performance impact on other apps running actively on the user’s device. |
| StorageNotLow | When set to true, your work will not run if the user’s storage space on the device is too low. |
WorkRequest를 통해 doWork()에서 구현한 작업을 어떤 형태로 실행할지 정의합니다. PeriodicWorkRequest를 써서 주기적으로 실행되게 하거나, OneTimeWorkRequest로 단 한 번만 실행되게 예약할 수 있습니다. 다음 코드는 네트워크 타입과 충전상태에 관한 제약조건을 설정한 WorkRequest를 만드는 작업입니다.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(constraints)
.build()
WorkRequest가 작성되었으면 enqueue 명령으로 WorkManager에 제출하면 됩니다.
WorkManager
.getInstance(myContext)
.enqueue(uploadWorkRequest)
제출된 WorkRequest는 WorkManager 내부의 데이터베이스에서 관리되는데, 데이터베이스에는 작업의 현재 상태, 작업에 대한 입력 및 출력 그리고 작업에 대한 제약 조건도 포함됩니다. 그리고 Constraint가 만족되면 내부의 TaskExecutor에 의해 작업이 실행되는 구조를 가지고 있습니다.

WorkRequest을 큐에 추가하고 나면 name, id 또는 tag를 WorkManager에 쿼리해서 언제든지 Work States를 확인할 수 있습니다.
// by id
workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>
// by name
workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>
// by tag
workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>
Work State는 1회성 작업의 경우 다음과 같이 ENQUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED 상태를 가지며

주기적 작업의 경우 ENQUEUED, RUNNING, CANCELLED 상태만을 가집니다.

이렇게 해서 WorkManager에 대해 알아보았습니다.