[안드로이드] WorkManager

hee09·2021년 12월 7일
1
post-thumbnail

WorkManager 정의

WorkManager는 비동기 작업을 쉽게 예약할 수 있는 API로 FirebaseJobDispatcher, GcmNetworkManager 및 Job Scheduler를 포함한 이전의 모든 Android 백그라운드 스케줄링 API를 대체할 수 있습니다. WorkManager는 사용자가 화면이나 끄거나 앱을 종료하거나 디바이스를 리부팅할지라도 안정적으로 실행할 수 있는 작업을 위한 것입니다. 예를 들면 backend 서비스에 로그나 분석자료를 보내거나 서버와 계속해서 앱의 데이터를 동기화하는 등의 작업이 있습니다.

WorkManager는 앱 프로세스가 중단될 경우 안전하게 종료될 수 있는 프로세스 내 백그라운드 작업이나 즉각적인 실행이 필요한 작업을 위한 것이 아닙니다. 만약 이러한 것이 필요하다면 background processing guide를 확인하면 됩니다.
이 파트도 공부하고 글 올리기

특징

  1. 작업 제약 조건
    Work Constraints(작업 제약 조건)을 명시하여 작업을 실행할 최적의 조건을 정의합니다. 예를 들어 Wifi 상태에 있을 때만 실행하거나 저장 공간이 충분한 경우에만 실행하는 등의 조건을 정의할 수 있습니다.

  2. 강력한 스케줄링
    WorkManager를 사용하면 유연하게 스케줄링을 하여 일회성 또는 반복적으로 실행되도록 작업을 예약할 수 있습니다. 작업에 태그를 지정하거나 이름을 지정할 수 있어서 유일한 작업을 만들 수 있고 여러 작업 그룹을 함께 모니터링하거나 취소할 수 있습니다. 스케줄된 작업은 내부적으로 관리되는 SQLite database에 저장되고 WorkManager는 이러한 작업이 유지되고 디바이스가 재부팅되어도 스케줄이 변경되는지 확인합니다. 게다가 WorkManager는 Doze 모드와 같은 절전 기능을 준수하고 있습니다.
    (Doze 모드는 Android 6.0(API Level 23)부터 추가된 기능으로 기기를 오랫동안 사용하지 않는 경우 앱의 백그라운드 CPU 및 네트워크 활동을 지연시켜 배터리 소모를 줄여주는 모드입니다.)

  3. 유연한 재시도
    가끔 작업이 실패할 수도 있습니다. 그렇기에 WorkManager는 유연한 재시도 정책을 제공하고 있습니다.

  4. 작업 Chaining(연쇄)
    복잡하게 관련된 작업의 경우, 어떤 작업들이 순차적으로 실행되고 어떤 작업들이 병렬로 실행되는지 제어할 수 있는 인터페이스를 사용하여 작업을 함께 연결할 수 있습니다. 각각의 작업에 있어서 작업을 위한 input과 output 데이터를 정의할 수 있습니다. 데이터를 가지는 작업이 함께 결합이 될 때 WorkManager는 한 작업의 output 데이터를 자동으로 다음 work로 전달합니다.

  5. 스레딩 상호 운용
    WorkManager는 RxJava 및 Coroutines와 원할하게 통합되며 사용자의 비동기 API를 연결할 수 있는 유연성을 제공합니다.


WorkManager 시작

의존성 설정

WorkManager를 시작하기 위해서 build.gradle 파일에 의존성 설정이 필요합니다.

// 작성일 기준 안전한 버전
def work_version = "2.7.1"

// (Java only)
implementation "androidx.work:work-runtime:$work_version"

WorkManager 주요 클래스

WorkManager를 이용해 작업을 등록하고 실행하려면 아래의 클래스를 이용합니다.

  • Worker: 작업 내용을 가지는 추상 클래스
  • WorkRequest: 작업 의뢰 내용으로 이를 상속한 OneTimeWorkRequest, PeriodicWorkRequest 두 개의 클래스를 사용
  • Constraints: WorkRequst의 제약 조건 명시

Work 정의

작업은 Worker 클래스를 사용하여 정의합니다. doWork() 메서드는 WorkManager에 의해 제공되는 백그라운드 스레드에서 비동기적으로 실행됩니다.

WorkManager에서 실행시킬 작업을 정의한다면 Worker 클래스를 상속받고 doWork() 메서드를 오버라이드합니다. 이 doWork() 메서드안에 실제 수행되어야 하는 작업을 명시하면 됩니다. 예를 들어, 이미지를 업로드 하는 작업을 만들려면 아래와 같이 사용합니다.

class LogWorker(appContext: Context, val workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // 작업을 정의합니다
        Log.d("LogWorker", "Worker : ${workerParams.id}, ${workerParams.tags}")

        // 결과와 함께 work가 성공적으로 끝났는지 나타냄
        return Result.success()
    }
}

생성자의 두 번째 매개변수인 WorkParameters를 이용해 Worker의 id 값이나 tag 값을 획득할 수 있습니다. id 값은 Worker가 등록되면 자동으로 발급되는 Worker의 식별자이고 tag 값은 Worker를 등록한 곳에서 지정한 식별자 값입니다.

doWork()의 반환값은 WorkManager에 작업의 성공 여부와 실패 시 작업의 재시도 여부를 알려줍니다. 반환 값은 아래와 같습니다.

  • Result.success(): 작업이 성공적으로 끝났음을 뜻합니다.
  • Result.failure(): 작업이 실패했음을 뜻합니다.
  • Result.retry(): 작업이 실패했고 재시도 정책에 따라서 다시 시도할것을 뜻합니다.

WorkRequest 정의

작업이 정의되었다면 WorkManager를 통해 스케줄되어야 작업이 실행됩니다. WorkManager는 어떻게 작업을 스케줄할 지 다양한 유용성을 제공합니다. 일정한 기간 동안 주기적으로 실행되도록 예약하거나 한 번만 실행되도록 예약할 수 있습니다.

이렇게 작업을 스케줄할 때 WorkRequest를 사용하여야 합니다. Worker가 하나의 작업을 정의한다면 WorkRequest와 WorkRequest의 SubClass들은 작업이 어떻게, 언제 작동해야할지를 결정합니다. 이러한 내용을 담는 클래스는 OneTimeWorkRequest와 PeriodicWorkRequest가 제공됩니다. 이 둘은 WorkRequest를 상속받아 작성한 클래스로 OneTimeWorkRequest는 단일 작업을 위한 의뢰 내용이고 PeriodicWorkRequest는 반복되는 작업을 위한 의뢰 내용입니다.

// OneTimeWorkRequest 객체 생성
// setInitialDelay를 설정하여 1분 후에 실행되게 지정
val logWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .setInitialDelay(1, TimeUnit.MINUTES)
    .build()

// PeriodicWorkRequest 객체 생성
// Builder의 생성자로 15분 후에 실행되게 설정
// 취소하기 전까지 15분마다 반복해서 실행
val logPeriodicWorkRequest: WorkRequest = PeriodicWorkRequestBuilder<LogWorker>(15, TimeUnit.MINUTES)
    .build()

OneTimeWorkRequest와 PeriodicWorkRequest 객체를 생성하는 코드입니다. 제네릭 정보로 Worker 클래스를 지정하여 실행시킬 작업을 지정하였습니다. 그리고 Builder를 사용해 필요한 값을 설정하고 build()를 통해 생성하는 구조입니다. PeriodicWorkRequest는 Builder 생성자의 매개변수로 15라는 값과 분이라는 값을 주어 15분마다 실행되게 설정하였습니다. 이렇게 등록된 작업은 취소되기 전까지 15분마다 실행됩니다.


WorkManager에게 WorkRequest 제출 및 상태 파악

WorkManager에게 WorkRequest 제출

마지막으로 enqueue() 메서드를 사용하여 WorkManager에게 WorkRequest를 제출합니다.

// WorkManager 획득 및 enqueue 메서드를 사용하여 작업 실행
// WorkManager는 싱글턴으로 존재
val workManager = WorkManager.getInstance(this)
    .enqueue(logWorkRequest)

euqueue 메서드는 백그라운드 처리를 위해 인자로 넘어오는 WorkRequest 하나를 대기열에 넣습니다.

Worker 상태 파악 및 데이터 전달

정리하자면 WorkerManager를 이용해 WorkRequest를 등록하면 Worker가 실행되는 구조입니다. 그런데 때로 Worker를 등록 / 실행 후 Worker가 어떻게 실행된 것인지 상태 파악이 필요할 수 있습니다. 즉 Worker의 작업이 성공한 것인지, 실패한 것인지, 취소된 것인지 상태 파악이 필요한 것입니다.

이와 같은 Worker의 상태 파악은 WorkManager의 getWorkInfoByIdLiveData() 함수에 파악되고자 하는 Worker의 ID 값을 등록하면 됩니다. 그러면 등록한 ID의 Worker가 성공, 실패, 취소 등의 상태가 발생하면 observe() 메서드에 등록한 Observer가 이를 관찰하여 상태를 알려줍니다.

// 상태를 파악하고자 하는 Worker의 id 를 등록
// 성공, 실패, 취소 상태가 되면 람다가 자동으로 호출되며 상태 정보 등이 WorkInfo 타입의 매개변수로 전달
workManager.getWorkInfoByIdLiveData(logWorkRequest.id)
    .observe(this, Observer {
        it?.let {
            if(it.state.isFinished) {
                // 코드
            }
        }
    })

작업 연쇄

WorkManager는 여러 개의 종속 작업을 지정한 후 실행 순서를 정의하는 일련의 작업을 생성하고 대기열에 넣을 수 있습니다. 이러한 기능은 특히 몇몇의 업무가 특별한 순서로 진행되어야 할 때 유용합니다.

작업의 순서는 우선 WorkManager의 beginWith() 메서드를 호출합니다. 이 메서드는 인자로 OneTimeRequest를 받고 반환값은 WorkContinuation 클래스입니다. WorkContinuation 클래스는 작업의 연쇄를 할 수 있게 허용하는 클래스로 이제 이 클래스의 메서드들을 사용해 작업의 순서를 정하는 것입니다. 예제를 보며 확인하겠습니다.

예제 1

workManager.beginWith(request1)
    .then(request2)
    .enqueue()

위의 코드는 두 개의 Request를 만들고 순서를 등록한 예입니다. beginWith에 하나의 Request를 전달하여 등록하고 then() 메서드를 사용하여 새로운 Request를 등록합니다. 이렇게 작업의 순서를 정하면 request1 작업이 모두 실행된 후 자동으로 request2의 작업이 실행됩니다. 그리고 마지막으로 enqueue 메서드를 실행해 대기열에 넣습니다.

예제 2


위의 그림은 work1, work2, work3가 동시에 실행되고 모두 완료되면 work4가 실행되며, 그 이후 work5와 work6을 동시에 진행해야 한다고 가정하겠습니다. 아래와 같이 코드를 작성하면 됩니다

// work1, work2, work3를 묶는 Collection
val beginRequest = ArrayList<OneTimeWorkRequest>()
beginRequest.add(request1)
beginRequest.add(request2)
beginRequest.add(request3)

// work5, work6를 묶는 Collection
val lastRequest = ArrayList<OneTimeWorkRequest>()
lastRequest.add(request5)
lastRequest.add(request6)

workManager.beginWith(beginRequest)
    .then(request4)
    .then(lastRequest)
    .enqueue()

여러 작업을 동시에 실행시키려면 Request를 List로 묶어서 표현하면 됩니다. 위의 코드는 beginRequest(request1, request2, request3)가 먼저 실행되고 그 후 request4가 실행되고 마지막으로 lastRequest(request5, request6)가 실행되는 구조입니다.

예시 3

위 그림은 work1을 싱핸한 후 work2를 실행합니다. 그런데 동시에 work3를 실행한 후 work4를 실행해야합니다. 그리고 마지막으로 work2와 work4를 모두 실행한 후 work5를 실행하는 구조입니다.

이러한 흐름을 만들려면 작업 흐름을 두개로 만들어야 합니다. WorkContinuation으로 각각의 작업 흐름을 명시하면 되는 것입니다. 아래와 같이 코드를 작성하면 됩니다.

// 첫 번째 WorkContinuation
val chain1: WorkContinuation = WorkManager.getInstance(this)
    .beginWith(request1)
    .then(request2)

// 두 번째 WorkContinuation
val chain2: WorkContinuation = WorkManager.getInstance(this)
    .beginWith(request3)
    .then(request4)

// WorkContinuation 타입의 컬렉션 생성
val chainList = ArrayList<WorkContinuation>()
chainList.add(chain1)
chainList.add(chain2)

val chain3 = WorkContinuation
    .combine(chainList)
    .then(request5)

chain3.enqueue()

Combine은 여러 WorkContinuations를 새로운 WorkContinuation의 전제 조건으로 결합하여 복잡한 체인을 허용하는 메서드입니다. 이 메서드를 통해 WorkContinuation을 묶어서 구현하였습니다.


WorkRequest의 여러 조건

WorkRequest 객체는 아래와 같은 방식으로 정의하여 사용할 수 있습니다.

  • 한 번만 실행되게 하거나 작업을 반복 실행되게 스케줄
  • Wi-Fi 또는 충전과 같은 작업 제약 조건 걸기
  • 작업 실행에 있어서 최소의 딜레이 보장
  • 재시도 및 백오프 전략
  • 작업에 필요한 input 데이터 전달
  • 태그를 사용하여 관계된 작업 그룹화

WorkRequest는 위에서 설명하였듯이 WorkManager가 작업을 스케줄하고 실행하는데 필요한 모든 정보를 포함하는 객체입니다. WorkRequest는 작업을 실행하기 위해 충족해야 하는 제약 조건, 지연 또는 반복 간격과 같은 정보 스케줄링, 구성의 재시도, 작업에 필요한 input 데이터가 포함될 수 있습니다.


Constraints를 이용한 제약 조건

Constraints는 최적의 조건이 충족될 때까지 작업이 지연되도록 보장합니다. 그 조건이 충족되는 순간 한 번이나 반복해서 작업이 실행되게 할 수 있습니다. Constraints 클래스를 이용해 충전 상태, 배터리가 부족하지 않을 때, 기기의 Idle 상태일 때, 기기의 저장공간이 부족하지 않을 때, 네트워크 타입 등의 조건을 명시할 수 있습니다.

제약조건을 만들 때 Constraints.Builder()를 사용하여 Constraints 객체를 만들고 WorkRequest.Builder()를 사용하여 할당합니다.


여러가지 조건 명시 예제

여러 조건을 명시하면 조건을 모두 만족해야만 작업이 실행됩니다. 아래 코드는 네트워크 상태를 지정하고 배터리의 조건을 명시하여 이 두 조건이 만족해야만 실행되도록 작성한 코드입니다.

// Constraints 객체 생성 후 제약조건 명시
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresCharging(true)
    .build()

// setConstraints 메서드를 사용해 WorkRequest에 제약조건 걸기
val workRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .setConstraints(constraints)
    .build()

serRequiredNetworkType의 매개변수로 NetworkType.CONNECTED를 지정하여 네트워크 불가능 상태에서 가능 상태로 될 때 등록한 작업을 실행합니다. CONNECTED 이외에 UNMETERED는 와이파이 네트워크가 가능 상태에 작업이 실행되고 METERED로 등록하면 모바일 네트워크가 가능한 상태에 작업이 실행됩니다.

그리고 setRequiresCharging() 메서드를 이용해 배터리 충전 상태 변경에 따라 작업이 실행되게 제약조건을 주었습니다.


Retry and Backoff 정책

만약 WorkManager가 작업을 다시 시작할 필요가 있다면 Worker에서 Result.retry()를 반환하면 됩니다. 그러면 작업은 backoff delaybackoff policy에 따라 다시 스케줄됩니다.

  • Backoff delay는 첫 번째 시도 후 작업을 재시도하기 전에 대기할 최소 시간을 지정합니다. 이 값은 10초(또는 MIN_BACKOFF_MILLIS) 이상이어야 합니다.

  • Backoff policy는 backoff delay가 시간이 지남에 따라 증가하는 방식을 정의합니다. WorkManager는 LINEAR와 EXPONENTIAL 두 개의 backoff policy를 가지고 있습니다.

모든 WorkRequest는 backoff policy와 backoff delay를 가지고 있습니다. 기본 정책은 EXPONENTIAL로 10초의 지연 시간을 갖는 정책이지만 WorkRequest를 정의할 때 오버라이드할 수 있습니다.


Tag

모든 Worker Request는 유일한 식별자를 가지고 있고 나중에 작업을 취소하거나 진행 상황을 관찰하기 위해 해당 작업을 식별하는데 사용할 수 있습니다.

만약 논리적으로 관계가 있는 작업 그룹을 가지고 있다면, 그러한 작업 아이템들을 찾는데 태그는 도움이 됩니다. 태그를 지정하면 Work Request 그룹을 함께 실행할 수 있습니다.

태그 추가

// OneTimeWorkRequest 객체 생성
val logWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .addTag("Log")
    .build()

태그는 addTag를 사용하여 추가합니다. 이러한 태그로 설정된 WorkRequest를 WorkManager.cancelAllWorkByTag(String tag)를 이용하면 태그와 관련된 모든 WorkRequest를 취소할 수 있습니다. 그리고 WorkManager.getWorkInfosByTag(String tag)를 이용하면 현재 작업 상태를 확인하는 데 사용할 수 있는 WorkInfo 객체 목록을 반환합니다.

위와 같이 태그말고 id로도 작업을 취소할 수 있습니다. id를 통해 취소하는 방법은 WorkManager.cancelWorkById() 메서드를 사용하여 취소하면 됩니다. ID 값은 위에서 나왔듯이 자동으로 부여되는 값으로 WorkRequest.getId() 메서드를 사용하면 획득할 수 있고 이 id를 cancelWorkById()의 인자로 넣으면 됩니다.


Worker에 데이터 전달 및 획득

Worker에 데이터 전달

Worker를 등록할 시점에 실행될 Worker에게 데이터를 넘겨야 할 때가 있는데 이때 setInputData() 메서드를 사용합니다. 전달하는 데이터는 Data 타입이며 Data.Builder에 의해 만들어집니다. Builder의 putString, putInt의 함수로 전달할 데이터를 명시하면 됩니다.

// Data 객체
// Builder 패턴
val data = Data.Builder()
    .putString("item1", "아이템 1")
    .putInt("item2", 100)
    .putBoolean("item3", true)
    .build()

// OneTimeWorkRequest 객체 생성
// setInitialDelay를 설정하여 1분 후에 실행되게 지정
val logWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<LogWorker>()
    .setInputData(data)
    .addTag("Log")
    .setInitialDelay(1, TimeUnit.MINUTES)
    .build()

Worker의 결과 데이터 획득

Worker의 실행 결과 데이터를 받아야 할 때가 있습니다. Worker에서 Data 타입의 객체를 매개변수로 가지는 success(Data data), failure(Data data) 메서드를 이용하여 데이터를 담아 반환하면 됩니다.

class LogWorker(appContext: Context, val workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // 작업을 정의합니다
        Log.d("LogWorker", "Worker : ${workerParams.id}, ${workerParams.tags}")
		
        // Data 객체 생성
        val data = Data.Builder()
            .putString("item1", "결과값")
            .build()
        
        // Data 객체를 매개변수로 넣어서 전달
        return Result.success(data)
    }
}

Worker에서 반환하는 데이터를 획득하는 방법은 앞에서 살펴본 Observer를 통해 관찰하면 됩니다.

workManager.getWorkInfoByIdLiveData(logWorkRequest.id)
    .observe(this, Observer {
        it?.let {
            if(it.state.isFinished) {
                // 데이터 획득
                // it은 WorkInfo에 해당
                val resultData = it.outputData
            }
        }
    })

참조
깡쌤의 안드로이드 프로그래밍
안드로이드 developer - Schedule tasks with WorkManager
안드로이드 블로그 - introducing workmanager

틀린 부분 댓글로 남겨주시면 수정하겠습니다..!!

profile
되새기기 위해 기록

0개의 댓글