[Android/Jetpack] AAC - WorkManager (Persistent Background)
이전 포스팅에서는 간단하게 안드로이드의 백그라운드 작업에 대해 알아보았다.
이번에는 Jetpack의 WorkManager의 사용법에 대해 알아보려고 한다.
WorkManager
WorkManager는 지속적인 작업에 권장되는 솔루션입니다. 앱이 다시 시작되거나 시스템이 재부팅될 때 작업이 예약된 채로 남아 있으면 그 작업은 유지됩니다. 대부분의 백그라운드 처리는 지속적인 작업을 통해 가장 잘 처리되므로 WorkManager는 백그라운드 처리에 권장하는 기본 API입니다.
Persistent(지속적인) 작업의 유형
👀 위의 정리해둔 이전 포스팅과 함께 보면 더욱 이해를 잘 할 수 있을 것!
다시 간단하게 설명하자면, 백그라운드 작업에는 3가지 카테고리가 있고 각 카테고리 별로 persistent/impersistent 작업이 나뉜다.
- Immediate (즉시) : 사용자가 App과 상호작용하는 동안 작업을 완료해야 하는 경우
- Deferrable (정시) : 바로 처리할 필요가 없거나, 특정 작업을 정확한 시간에 실행해야 하는 경우
- Long Running (지연) : 작업을 끝내는데 시간이 좀 걸리거나, 정확한 시간에 실행할 필요가 없는 경우
WorkManager를 사용해야 하는 이유 (장점)
- Work constraints (작업 제약 조건)
- Robust scheduling (강력한 예약 관리)
- Expedited work (신속 처리 작업)
- Flexible retry policy (유연한 재시도 정책)
- Work chaining (작업 체이닝)
- Built-In threading interoperability (내장 스레딩 상호 운용성)
자세한 내용은 아래에서 더 자세하게 설명할 예정이다.
WorkManager 사용법
dependencies {
val work_version = "2.7.1"
implementation("androidx.work:work-runtime-ktx:$work_version")
}
WorkManager의 구성
Worker 클래스 - task(작업)의 단위를 정의
- doWork() 메서드 : WorkManager에서 실행할 작업을 만들기 위해선 Worker 클래스를 상속받고, 오버라이딩 (비동기)
- 작업의 성공 여부는 doWork의 반환 값, Result를 통해 알 수 있음
- Result.success() : 작업이 성공적으로 완료된 경우
- Result.failure() : 작업에 실패한 경우
- Result.retry() : 작업에 실패했으며, 재시도 정책에 따라 다른 시점에서 시도되어야 하는 경우
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()
}
}
WorkRequest 클래스 - 언제, 어떻게 task(작업)가 실행되어야 하는지를 정의
- Worker에 정의된 작업을 실행하기 위해서는 WorkManager 서비스로 예약해야 함
- WorkRequest를 생성할 때, 반복 여부 & 제약 사항 등의 정보를 담음
- OneTimeWorkRequest : 한 번만 실행할 작업을 요청하는 WorkRequest (impersistent)
- PeriodicWorkRequest : 일정한 주기로 여러 번 작업을 요청하는 WorkRequest (persistent)
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<UploadWorker>()
.build()
WorkManager 클래스 - WorkRequest를 스케줄링하고 실행하고 관리
- 인스턴스를 받아와 WorkRequest를 큐에 추가하여 실행하며, WorkManager는 제한사항에 따라 최상의 상태로 작동
- enqueue() 메서드 : WorkRequest를 WorkManager에 제출
WorkManager
.getInstance(myContext)
.enqueue(uploadWorkRequest)
WorkRequest에 포함될 수 있는 추가 정보
위에서 설명한 WorkManager의 장점이자 특징이 WorkRequest에 추가 정보로 포함될 수 있다.
Work constraints (작업 제약 조건) ⭐
- 작업을 실행하는 데 최적인 조건을 선언적으로 정의함 → 최적의 조건이 충족될 때까지 작업이 지연되도록 함
- 디바이스가 무제한 네트워크 / 유휴 상태 / 충분한 배터리인 경우에만 실행함
- 작업 실행 중에 제약조건이 충족되지 않다면, WorkManager에서 작업을 중지하고 다시 제약조건들이 충족할 때 재시도
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // Wi-Fi에 연결되어 있고,
.setRequiresCharging(true) // 디바이스가 충전중일 때
.build()
val myWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<MyWork>()
.setConstraints(constraints)
.build()
- 제약조건과 task를 연결하기 위해서는 Constraint.Builder()를 사용하여 Constraints 인스턴스를 만들어야 함
- 만든 Constraints 인스턴스는 WorkRequest.Builder()에 할당함
Expedited work (신속 처리 작업) ⭐
- 사용자에게 중요하고 몇 분 내에 완료되는 작업은 Expedited work를 사용해야 함
- 백그라운드에서 immediate(즉시) 실행할 작업을 예약할 수 있음
- 신속 처리 작업의 특성
- 중요도 : 사용자에게 중요하거나 사용자가 시작한 작업에 적합
- 속도 : 즉시 시작되어 몇 분 안에 끝나는 짧은 작업
- 할당량 : 포그라운드 실행 시간을 제한하는 시스템 수준 할당량에 따라 신속 처리 작업의 시작 가능 여부가 결정
- 전원 관리 : 절전 모드, 잠자기와 같은 전력 관리 제한사항은 신속 처리 작업에 영향을 미칠 가능성이 적음
- 지연 시간 : 시스템의 현재 워크로드로 처리 가능한 경우 시스템은 이 작업을 즉시 실행 (나중에 실행되도록 예약 불가능)
ex. 채팅 앱에서 사용자가 메세지/첨부 이미지를 전송하려는 경우, 결제/구독 흐름을 처리하는 앱
val request = OneTimeWorkRequestBuilder()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
.enqueue(request)
- OneTimeWorkRequest의 인스턴스를 초기화하고, 해당 인스턴스에서 setExpedited() 호출
- enqueue()를 이용하여 해당 WorkRequest를 WorkManager에 제출
아래 내용은 잘 이해하지 못해서 더 자세한 내용은 추후 Codelab 실습과 함께 정리할 예정이다.
❇️ Android 12 보다 낮은 버전의 플랫폼에서는?
→ 신속 처리 작업의 하위 호환성을 위해 포그라운드 서비스(foreground service)를 실행할 수 있음
Worker의 getForegroundInfoAsync() / getForegroundInfo() 메서드를 사용하면 setExpeidated() 호출할 때
WorkManager가 알림을 표시할 수 있음
Robust scheduling (강력한 예약 관리) ⭐
- flexible scheduling을 통해 one-time / repeatedly 하게 실행할 작업을 예약할 수 있음
- 작업에 태그 / 이름을 지정하여 고유 작업 및 대체 가능한 작업을 예약하고 작업 그룹을 함께 모니터링 / 취소할 수 있음
- 예약된 작업은 SQLite DB에 저장되고 WorkManager에서 디바이스를 재부팅해도 작업이 유지되고 예약되도록 보장
- 절전 기능을 사용하고 권장사항(ex. Doze 모드)을 준수하기 때문에 배터리 소모를 걱정하지 않아도 됨
일회성 작업 예약
// 1번 : 간단한 작업
val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)
// 2번 : 복잡한 작업
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<MyWork>()
.build()
- from() : 추가 구성이 필요없는 간단한 task
- builder : 추가 구성이 필요한 복잡한 빌더
주기적 작업 예약
ex. 주기적으로 데이터 백업, 최신 콘텐츠 다운로드
// 1시간마다 작업 예약
val saveRequest =
PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
// Additional configuration
.build()
- PeriodicWorkRequest를 이용한 주기적으로 실행되는 WorkRequest 객체
- 간격 주기 == 반복 사이의 최소 시간 (정의할 수 있는 최소 반복 시간은 15분)
- Worker가 실행될 정확한 시간은 WorkRequest의 제약 조건과 시스템의 최적화에 따라 달라짐
// 1시간마다 마지막 15분 동안 실행할 수 있는 작업
val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
1, TimeUnit.HOURS, // repeatInterval (the period cycle)
15, TimeUnit.MINUTES) // flexInterval
.build()
- 작업이 실행 타이밍에 민감한 경우, 각 간격 주기 안에서 가변 기간 내에 실행되도록 구성할 수 있음
- PeriodicWorkReques를 생성할 때 flexInterval(가변 기간)을 repeatInterval(반복 간격)과 함게 전달
- 작업을 실행할 수 있는 기간 == (repeatInterval - flexInterval) ~ 기간의 끝
- 이때 반복 간격(repeatInterval)은 PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS (15분) 이상이어야 함
- 이때 가변 기간(flexInterval)은 PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS (5분) 이상이어야 함
❇️ PeriodicWorkRequest + 제약 조건?
→ 정의된 반복 기간보다 제약 조건이 더 우선순위!
따라서 작업 실행이 지연될 수 있고, 실행간격 내에 제약 조건이 충족되지 못하면 건너뛸 수도 있음
Flexible retry policy (유연한 재시도 정책) ⭐
- 경우에 따라 실패하기도 함 → exponential backoff policy를 포함한 유연한 재시도 정책을 제공함
- 다시 시도해야 하는 경우에는 Worker에서 Result.retry() 반환하면 retry policy, backoff policy에 의해 다시 예약됨
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
- backoff delay : 첫 번째 시도 후 작업을 다시 시도하기 전에 대기해야 하는 최소 시간 (MIN_BACKOFF_MILLIS 미만일 수 없음)
- backoff policy : 다음 재시도의 backoff delay가 시간 경과에 따라 증가하는 방식 정의 (LINEAR, EXPONENTIAL 지원)
- LINEAR - 10초, 20초, 30초... 로 재시도 간격이 늘어남
- EXPONENTIAL - 20초, 40초, 80초... 로 시퀀스로 재시도 간격이 늘어남 (기본 정책)
태그
모든 WorkRequest에는 고유 식별자(unique identifier)가 있음
이 고유 식별자에 태그를 지정하여 유용하게 할 수 있음
- 특정 태그가 있는 작업 취소 : WorkManager.cancelAllWorkByTag(String)
- 현재 작업 진행 사항 관찰 : WorkManager.getWorkInfosByTag(String) → WorkInfo 객체 리스트 반환
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.addTag("cleanup")
.build()
- 하나의 WorkRequest에 여러 태그를 추가할 수 있음 (내부적으로 String 집합으로 저장됨)
- WorkRequest.getTags()를 통해 태그 집합을 가져올 수 있음
입력 데이터 할당
작업을 실행하고자 할 때, 입력 데이터를 넣어야 해당 작업을 실행하도록 하고 싶은 경우에 사용
입력값은 Data 객체에 key-value 쌍으로 저장되며, WorkRequest에서 설정할 수 있음
// Define the Worker requiring input
class UploadWork(appContext: Context, workerParams: WorkerParameters)
: Worker(appContext, workerParams) {
override fun doWork(): Result {
val imageUriInput =
inputData.getString("IMAGE_URI") ?: return Result.failure()
uploadFile(imageUriInput)
return Result.success()
}
...
}
// Create a WorkRequest for your Worker and sending it input
val myUploadWork = OneTimeWorkRequestBuilder<UploadWork>()
.setInputData(workDataOf(
"IMAGE_URI" to "http://..."
))
.build()
- Worker 클래스에서는 getInputData()를 통해 입력값에 접근할 수 있음
- 해당 코드에서는 "IMAGE_URI" key를 가진 Data 인스턴스가 없으면 Result.failure()를 반환함
- WorkRequest에서는 setInputData()를 통해 입력값을 넣어 Data 인스턴스를 해당 Worker의 입력값으로 설정함
👀 더 자세한 내용은 Codelab 실습과 함께 다음 포스팅에서 정리할 예정!
reference >