[안드로이드 코드랩] Android Kotlin Fundamentals: WorkManager

홍석규·2022년 3월 1일
0

안드로이드 코틀린 코드랩을 학습한 내용을 정리했습니다.
https://developer.android.com/codelabs/kotlin-android-training-work-manager?hl=ko#0

WorkManager

WorkManager 개념

  • WorkManager는 안드로이드 아키텍처 컴포넌트와 젯팩에서 제공해준다. WorkManager는 지연되고 실행을 보장 해주는 백그라운드 작업을 의미한다.

지연된 작업 즉 Deferrable의 의미는 즉시 실행할 필요가 없는 작업을 의미한다. 예시로 서버에 분석 데이터를 전달 하거나, 백그라운드에서 db 동기화를 실행 하는 경우 deferred한 작업이 될 수 있다.

실행을 보장해주는 Guaranteed execution은 앱이 종료되거나 디바이스 재시작이 되더라도 작업을 실행 해주는 것을 말한다.

WorkManager가 백그라운드에서 동작 할 때 자동으로 호환성 이슈나 배터리 상태를 최적으로 관리 해준다. WorkManager는 백그라운드 태스크를 스케쥴링할 적절한 방법을 선택하게 되는데 device API level에 따라 선택하게 된다. API 23이거나 그 이상인 경우 JobScheduler를 활용하고 그렇지 않으면 AlarmManager 또는 BroadcastReceiver를 활용한다.

- WorkManager는 백그라운드에서 작업할 경우 실행 가능한 기준을 설정할 수 있다. 예시로 배터리가 적당히 있을 때, 네트워크 연결이 되어 있을 때 등 상황에 따라 실행 기준을 선택할 수 있다.

우선 이번 코드랩에서는 하루에 한번 서버에서 받아오는 DevBytes Video playlist를 WorkManger를 이용해 백그라운드 작업을 실행 시키는 내용을 공부 해보자.

백그라운드 작업 생성하기

WorkManager를 사용하기 전에 WorkManager 라이브러리가 제공해주는 클래스들을 먼저 알아보자.

  • Worker
    • 실제로 백그라운드에서 동작 해야 할 작업을 정의하는 클래스다. 실제로 구현할 때 Worker 클래스를 상속 받고 doWork()메서드를 오버라이딩 한다. doWork() 메서드 내에 서버에서 받아온 데이터 동기화 등 백그라운드 작업을 수행할 코드를 작성한다.
  • WorkRequest
    • 백그라운드에서 실행하기 위해 요청하는 클래스다. WorkRequest 클래스를 이용해서 어떻게 언제 worker task를 실행 시킬지 정의 할 수 있다. 예시로 디바이스가 충전중이거나, wifi mode인 경우에서만 백그라운드 작업을 수행 한다던지
  • WorkManager
    • WorkRequest를 스케쥴링 하고 실행 시키는 관리 클래스다. WorkManager는 사용자가 정의한 제약조건을 준수하면서 시스템 리소스에 대한 부하를 분산 시킬 수 있는 방법으로 작업을 스케쥴링 한다.

즉 실제로 background에서 작업 되어야 할 내용을 Worker클래스의 doWork()에 작성한 다음, WorkRequest로 설정, WorkManager로 관리하게 된다.

실제로 코드로 생성해보자.

class RefreshDataWorker(appContext: Context, params: WorkerParameters) :
       CoroutineWorker(appContext, params) {
}
  • Context와 WorkerParameters를 생성자로 받고 CoroutineWorker()를 상속하는 클래스다. CoroutineWorker 클래스 구현을 확인 해보자.
  • CoroutineWorker는 내부적으로 ListenableWorker()를 상속하고 있으며 코틀린 코루틴으로 작업할 수 있게 구현해준 추상 클래스다 내부에는 suspend doWork() 메서드가 존재하며 추상메서드로 구현 되어있다. 실제로 Worker객체를 구현할 때 내부에 doWork()를 오버라이딩 해야한다고 언급 했었는데 CoroutineWorker()는 doWork()를 코루틴 내에서 사용할 수 있게 지원 해주는것이다.
  • 내부에는 오버라이딩 된 startWork()메서드를 이용해서 doWork()메서드를 코루틴 스코프 내에서 실행 시켜준다.
    • coroutineContext는 Dispatchers.Default 로 설정 되어있다.

그런데 코드랩 언급에서는 Worker를 상속 해야한다고 되어있는데 실제로 CoroutineWorker의 경우 ListenableWorker를 상속 하고 있다. ListenableWorker는 어떤 역할을 할까.

  • ListenableWorker의 경우 WorkManager에서 비동기적으로 작업을 수행할 수 있게 하는 클래스라고 되어있다. 대부분의 경우 synchronous API인 Worker를 상속받아 사용하라고 추천한다.
  • ListenableWorker 클래스는 WorkerFactory를 통해서 런타임 시점에 객체화 되고**startWork() 메서드는 메인 쓰레드에서 호출 된다.**
    • 단순 Worker를 상속하는 경우 백그라운드 쓰레드에서 호출 될것인데. CoroutinWorker의 startWork()는 메인 쓰레드에서 호출되고 코루틴 스코프를 이용해서 백그라운드 쓰레드에서 비동기로 동작하게 되는것이다.
  • 또한 만약에 Work가 재 시작되는 경우 ListenableWorker의 새로운 객체가 다시 생성된다. 이 뜻은 ListenableWorker 객체의 startWork메서드는 정확하게 각 인스턴스마다 한번만 호출 된다는 것이다.

다시 본론으로 돌아와서 CoroutineWorker를 상속받는 RefreshDataWorker에서 실제로 백그라운드에서 수행 해야 할 doWork()를 정의 해준다.

doWork() 구현하기

  • doWork()메서드는 작업을 동기적으로 수행하고 ListenableWorker.Result 객체를 반드시 반환 해야한다. 안드로이드 os는 Worker에게 doWork()수행 시간을 최대 10분으로 한정하는데 만약 시간이 끝나버리면 os는 강제로 Worker 객체를 종료시킨다.

ListenableWorker.Result 객체를 만들기 위해서 background work의 완료상태를 나타내는 다음 static method를 사용하면 된다.

  • Result.success() : 작업이 성공적으로 끝난 경우
  • Result.failure() : 작업이 끝났지만 실패 한 경우
  • Result.retry() : 일시적 오류로 인해 재 시도 해야하는 경우

실제로 구현 해뒀던 respository객체를 이용해서 서버에 데이터를 받아오고 db를 갱신하는 작업을 수행하는 코드는 다음과 같다.

override suspend fun doWork(): Result {
       val database = getDatabase(applicationContext)
       val repository = VideosRepository(database)
       try {
           repository.refreshVideos()
       } catch (e: HttpException) {
           return Result.retry()
       }
       return Result.success()
   }
  • repostitory.refreshVideos()로직을 수행 하고 만약 HttpException이 발생하는 경우 catch구문에 걸려서 retry 해야함을 알려준다. 그렇지 않은 경우 success하게 끝났다는 것을 알려준다.

WorkRequest 생성하기

Worker객체를 이용해서 백그라운드에서 수행해야할 작업을 정의 했다면 WorkRequest를 이용해서 어떻게 언제 작업을 수행 할것 인지 정의해줘야한다. WorkRequest 클래스는 2개의 구현체를 가지고 있다.

  • OneTimeWorkRequest : 오직 한번만 수행될 작업
  • PeriodicWorkRequest : 주기적으로 반복 되어야 할 작업

안드로이드 앱에서 Application 클래스는 액티비티나 서비스같은 컴포넌트를 포함하는 가장 기본 클래스가 된다. 앱 프로세스가 생성되면 **Application 또는 Application 클래스의 서브클래스가 다른 어떤 클래스들 보다 가장 먼저 객체화 된다. 코드랩 앱 기준 WorkManager를 스케쥴링 하기 가장 좋은 위치는 Application을 상속하는 DevByteApplication이다.**

private fun setupRecurringWork() {
val repeatingRequest = PeriodicWorkRequestBuilder<RefreshDataWorker>(15, TimeUnit.DAYS)
       .build()
}
  • PeriodicWorkRequestBuilder를 이용해서 PeriodicWork 객체를 생성하자. 제너릭 타입에 어떤 Worker객체를 사용할 것인지 명시하고 repeatInterval과 repeatIntervalTimeUnit을 설정 해준다.
    • 위와같이 설정 해주면 하루기준 15분마다 반복해서 작업을 수행해달라는 의미가 된다.

WorkManager는 WorkRequest를 실행 시키는 여러가지 메서드를 제공 해주는데 그중 enqueueUniquePeriodicWork() 메서드를 사용할 것이다. 이 메서드는 PeriodicWorkRequest객체마다 고유한 이름을 붙여서 큐에 넣는데 한번에 고유한 이름을 가진 객체가 1개만 존재할 수 있다.

WorkRequest를 어떻게 WorkManager가 스케쥴링 하는지 자세하게 알고 싶다면 WorkManager docs를 참고하자..

다시 돌아와서 work객체에 이름을 붙여줄 수 있게 설정하자.

companion object {
   const val WORK_NAME = "com.example.android.devbyteviewer.work.RefreshDataWorker"
}

이제 WorkManager의 enqueueUniquePeriodicWork()메서드를 이용해서 백그라운드 작업을 설정하자.

WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
            RefreshDataWorker.WORK_NAME,
            ExistingPeriodicWorkPolicy.KEEP,
            repeatingRequest
        ) 
  • getInstance()메서드는 deprecated되어서 getInstance(Context) 메서드를 이용해야한다.
  • 첫번째 파라미터로 Work객체마다 고유한 name을 가질 수 있는 작업 이름을 넘겨주고, 두번째 파라미터로 이미 작업이 존재하면 어떤 정책을 사용할것인지 결정해준다.
    • 예시로 같은 이름을 가진 아직 끝나지 않은 작업이 존재할 경우 KEEP 파라미터는 WorkManager가 기존의 작업을 유지하고 새로운 작업을 무시할 수 있게 정책을 설정한다.
  • 그리고 PeriodicRequestBuilder를 이용해서 만든 WorkRequest를 마지막 파라미터로 전달한다.

이제 앱을 실행 해보면 다음과 같은 로그가 찍히면서 workManager가 동작하는것을 볼 수 있다.

2022-03-01 21:21:56.221 12684-12761/com.example.android.devbyteviewer D/RefreshDataWorker: Work request for sync is run
2022-03-01 21:21:56.223 12684-12778/com.example.android.devbyteviewer I/WM-WorkerWrapper: Worker result SUCCESS for Work
  • periodicRequest의 경우 첫번째 요청은 바로 실행 되지만 그 이후의 요청은 15분보다 빨리 수행 될 수도 있고 더 늦게 수행 될 수도 있다. os가 배터리상황, 여러 정책들을 고려해서 판단하기 때문
profile
학습한 내용을 공유하고 기록합니다.

0개의 댓글