WorkManager는 개발자를 대신하여 비동기로 백그라운드 작업을 처리합니다. 앱이 종료되거나 기기가 다시 시작되어도 안정적으로 실행이 되는 지연 가능한 비동기 작업을 처리하는데 적합합니다.
WorkManager에서 제공하는 백그라운드 스레드에서 작업 수행합니다. 참고로, WorkManager의 작업은 내부적으로 관리되는 SQLite 데이터베이스에 저장이 됩니다. WorkManager는 이 작업이 지속되고 기기 재부팅시 일정이 재조정되도록 합니다.
작업의 정확한 실행시간은 하나의 코드로 API Level 마다 비슷한 동작을 보장합니다. WorkManager는 API의 버전에 맞게 AlarmManager 또는 JobScheduler를 사용하고, FirebaseDispathcer의 의존성이 추가되었다면 이를 적극 이용합니다.
앞서 WorkManager의 특성을 살펴보았습니다. 이러한 특성을 보면 언제 사용을 해야할지 알 수 있습니다.
앱의 프로세스 수명과 별도로 수행되어야 하는 작업에 알맞습니다. 하지만 기억해야 할 점은 반드시 실행되지만, 상황에 따라 지연될 수 있고, 다시 실행될 수 있다는 점입니다.
그리고 작업 체이닝이 가능하기 때문에, 아래 그림과 같이 이미지를 압축하는 앞선 작업들이 끝나면 이미지를 업로드하는 후속 작업을 처리해야하는 경우에 사용하기에 알맞습니다.
이외의 Background Task
만일 사용자가 보고 있는 빠르게 변경하는 작업이나 즉시 처리해야 하는 작업이라면 WorkManager의 사용은 적절치 않습니다. 이런 작업은 Foreground Service를 사용하는 것이 좋습니다.
또한 WorkManager은 지연이 될 수 있기 때문에, 정확한 시간에 동작해야하는 작업은 AlarmManager을 사용하는 것이 좋습니다.
버전 별 이슈
Android 12의 Foreground Service 실행 제한이 생겼습니다. 따라서 Foreground Service의 권장 대안으로 WorkManager을 제안했습니다.
기존에도 Doze모드 도입에 의해서 백그라운드 작업을 위해 Service를 점점 사용하지 않게 되었지만, 신속한 처리를 위해서 Foreground Service를 사용했습니다. 그렇다면 이제는 지연이 가능한 WorkManager로 이를 어떻게 대체 할까요?
WorkManager 2.7.0 부터 앱은 setEcpedited() 메서드로 Worker가 신속 처리 작업을 사용해야 한다고 선언할 수 있습니다. 이 새로운 API는 Android 12에서 실행되면 신속 처리 작업을 사용하고, 이전 버전에서는 Foreground Service를 이용하여 이를 처리하기 때문에 이전 버전과의 호환성을 제공해줍니다.
WorkManager의 신속 처리 작업이란, 시스템이 최대한 빨리 작업을 실행 시키도록 하는 것입니다. Foreground Service와 일반적인 JobScheduler 작업 사이의 특성을 가집니다. 이를 통해서 앱이 짧고 중요한 작업을 실행하는 동시에 시스템에서는 리소스 액세스를 효과적으로 제어할 수 있습니다.
Worker클래스를 이용한다면 WorkManager는 자동으로 백그라운드 스레드에서 doWork()를 호출합니다. 백그라운드 스레드의 출처는 WorkManager의 Configuration에 명시된 Executor이므로, 맞춤 설정을 원하면 직접 WorkManager를 초기화해야 합니다.
주의할 점은 Work.doWork() 메서드는 동기 호출입니다. 따라서 메서드 내에서 비동기 호출을 사용한다면 제대로 동작하지 않을 수 있습니다.
그렇다면 doWork()에서 비동기 작업을 처리하려면 어떻게 해야 할까요?
ListenableWorker나 CoroutineWorker을 사용하면 됩니다.
Kotlin 사용자는 CoroutineWorker를 사용합니다. CoroutineWorker.doWork()는 suspend함수입니다. 따라서 해당 메서드안에서 비동기 작업을 처리할 수 있습니다. 또한 CoroutineWorker는 Configuration에 명시된 Executor에서 실행되지 않고, 기본으로 Dispatcher.Default에서 실행됩니다. 변경을 원하면 CoroutineContext로 설정합니다.
WorkManager
Worker
doWork()
WorkRequest
onTimeWorkRequest
: 반복하지 않고, 한번만 실행될 작업의 요청을 나타내는 클래스PeriodicWorkRequest
: 여러번 실행할 작업의 요청을 나타내는 클래스WorkState
일회성 작업의 상태 다이어그램입니다.
해당 그림을 보면 작업의 생명 주기를 알 수 있습니다.
작업이 요청되면 ENQUEUED 상태로 들어오게 됩니다. 작업의 요구사항이 만족이 되면 RUNNING 상태로 변경됩니다. SUCCEEDED, FAILED, CANCELLED 모두 작업의 최종 상태가 됩니다.
작업이 retry 된 경우에는 다시 ENQUEUED 상태로 되어 재실행되게 됩니다.
체이닝된 작업에는 BLOCKED 상태가 추가됩니다. 앞선 작업이 최종 상태가 되기 전까지 후속 작업들은 BLOCKED 상태입니다. 만일 SUCCEEDED가 된다면 다음 작업은 ENQUEUED 상태가 되어 실행을 기다리게 됩니다. 하지만 앞선 작업이 FAILED나 CANCELLED 상태로 종료된다면 후속 작업도 모두 이를 따라가게 됩니다.
주기적 실행이 되는 작업의 상태 다이어그램입니다.
주기적 실행이 되는 작업은 종료되지 않기 때문에 성공, 실패 상태가 존재하지 않습니다. 성공과 실패에 상관없이 다시 예약이 됩니다. 따라서 작업의 최종 상태는 CANCELLED만 있습니다.
WorkManager는 자신의 Queue를 가지고 있습니다. 따라서 enqueue() 메서드를 통해서 작업을 예약합니다. 하지만 주의할 점이 있습니다. Queue는 동일 작업에 관해 중복처리를 하지 않으므로 동일한 작업이 여러번 예약되지 않도록 조심해야 합니다.
이런 중복 작업은 차단하는 가장 좋은 방법은 작업을 고유작업으로 등록하는 것입니다. 고유작업은 특정 작업의 인스턴스가 하나만 있도록 도와줍니다. 이는 고유작업에 개발자가 직접 고유 이름을 정하여 관리됩니다. 다음 메서드를 호출합니다.
이 메서드를 통해서 작업의 고유한 이름을 설정하고, 고유 이름이 있는 작업 체인이 완료되지 않은 경우에 할 작업을 지정하고, 예약할 workRequest를 지정합니다.
자세한 사용법은 작업관리페이지를 참고하면 됩니다.
단순 작업
class FirstWorker(context: Context, workerParams:WorkerParameters): Worker(context, workerParams) {
override fun doWork(): Result {
// 처리할 코드 추가
return Result.success()
}
}
fun createOneTimeWorkRequest() = OneTimeWorkRequestBuilder<FirstWorker>().build()
이렇게 하면 작업을 한번 수행하는 예시입니다.
- 반복되는 작업을 위해서는 PeriodicWorkRequestBuilder 를 이용하여 PeriodicWorkRequest 객체를 생성하여 WorkManager 의 큐에 추가 하면 됩니다. 이때 첫번째 인자로는 반복될 인터벌 값, 두번째 인자로는 이 인터벌의 시간타입입니다.
fun createWorkRequest(data: Data)
= PeriodicWorkRequestBuilder<FirstWorker>(12, TimeUnit.HOURS)
반복 시간에 사용할수 있는 가장 짧은 최소값은 PeiodicWorkRequest 클래스 내의 MIN_PERIODIC_INTERVAL_MILLIS 상수로 정의되어 있으며 15분 보다 짧은 시간은 사용할수 없습니다.
val work = createWorkRequest(Data.EMPTY)
WorkManager.getInstance(this).enqueue(work)
제약 조건을 가지는 작업
WorkManager은 다양한 제약조건을 추가하여 작업을 실행할 수 있습니다. 제약조건이 만족이 되면 작업을 수행하고, 그렇지 않으면 작업을 취소합니다. 처리가 완료되지 않았다면 다시 제약조건이 만족되는 타이밍에 다시 처리를 시도합니다.
fun createConstraints() = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) //와이파이 연결 요구
// 다른값(NOT_REQUIRED, CONNECTED, NOT_ROAMING, METERED)
.setRequiresBatteryNotLow(true) // 배터리 요구
.setRequiresStorageNotLow(true) // 저장소 요구
.build()
작업 상태 확인
WorkManager 의 getStatusById() 메서드에 인자값으로 주어진 ID 에 해당하는 작업을 추적할수 있도록 LiveData 객체를 반환합니다.
WorkManager.getInstance(this).getWorkInfoByIdLiveData(work.id)
.observe(this, { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
Toast.makeText(this, "WorkInfo.State.SUCCEEDED", Toast.LENGTH_SHORT).show()
}
})
연결된 작업
2개의 작업을 연결하여 처리할 수 있습니다.
각 작업을 WorkRequest 로 만들어서 처음 실행될 작업을 WorkManager 의 beginWith() 메서드의 인자로 추가 하고, then() 메서드에 이어할 작업을 추가합니다.
이제 WorkManager 는 앞선 작업을 적절한 타이밍에 수행하고, 앞선 작업이 완료 된 이후 두 번째 작업 수행합니다.
fun chainWorks(work1: OneTimeWorkRequest, work2: OneTimeWorkRequest, work3: OneTimeWorkRequest, work4: OneTimeWorkRequest) {
WorkManager.getInstance(this)
// Worker를 동시에 병렬로 실행
.beginWith(listOf(work1, work2))
// 이전 beginWith의 모든 작업이 끝난 경우 실행
.then(work3)
.then(work4)
//enqueue()를 호출해야 이 모든 작업이 실행
.enqueue()
}
작업 간의 정보 전달
연결된 작업 간에 정보를 전달해야 하는 경우가 있습니다.
예를 들면, 첫번째 작업에서 이미지의 압축을 수행했다면 그 이미지의 이름을 전달하여 두번째 작업에서 이미지 업로드를 수행하는 경우가 있습니다.
작업 간의 정보 전달은 Data 클래스를 사용합니다. Data의 내부는 Hash Map으로 이루어져 있습니다. key값은 문자열이고, value는 원시값 또는 문자열, 배열이 사용 가능합니다. Serializable을 사용하려면 10KB이내에서만 가능합니다.
val input = mapOf("data" to "1")
val inputData = Data.Builder().putAll(input).build()
val work1 = createOneTimeWorkRequest(inputData)
inputData
)하고 인자로 Data 객체를 받습니다. 이 메서드를 이용해서 Worker 에 이미지의 이름을 전달할수 있습니다. override fun doWork(): Result {
val data = inputData.getString("data")
Log.d("Sample", "input data: ${data}")
return Result.success()
}
override fun doWork(): Result {
val data = inputData.getString("data")
Log.d("Sample", "input data: ${data}")
val output = mapOf("data" to data)
val outputData = Data.Builder().putAll(output).build()
return Result.success(outputData)
}
https://developer.android.com/topic/libraries/architecture/workmanager/how-to/states-and-observation
https://medium.com/androiddevelopers/workmanager-kotlin-apis-a0fb9dfbfeb6
https://medium.com/@limgyumin/workmanager-잘-써보기-1643a999776b