[Android] WorkManager 공부해보기

혜령·2021년 11월 18일
0

Android 공부하기

목록 보기
1/8

WorkManager란❓

WorkManager는 개발자를 대신하여 비동기로 백그라운드 작업을 처리합니다. 앱이 종료되거나 기기가 다시 시작되어도 안정적으로 실행이 되는 지연 가능한 비동기 작업을 처리하는데 적합합니다.

WorkManager에서 제공하는 백그라운드 스레드에서 작업 수행합니다. 참고로, WorkManager의 작업은 내부적으로 관리되는 SQLite 데이터베이스에 저장이 됩니다. WorkManager는 이 작업이 지속되고 기기 재부팅시 일정이 재조정되도록 합니다.

작업의 정확한 실행시간은 하나의 코드로 API Level 마다 비슷한 동작을 보장합니다. WorkManager는 API의 버전에 맞게 AlarmManager 또는 JobScheduler를 사용하고, FirebaseDispathcer의 의존성이 추가되었다면 이를 적극 이용합니다.

특징

  • 앱이나 기기가 다시 시작되는 경우에도 실행이 보장됩니다. 그리고 제약 조건을 가지고 실행을 할 수 있습니다.
  • 장치의 상태를 존중합니다. 일의 처리를 위해서 기기를 깨우거나 하지 않으므로, 기기에 무리를 주지 않습니다.
  • 작업의 모니터링 및 관리가 가능합니다. 작업의 현재 상태 조회가 가능합니다.
  • 작업 체이닝이 가능하여 작업의 순차적 처리가 가능해졌습니다.
  • 기회주의적인 성격을 가져서, 어떠한 제한 조건이 충족되면 즉시 실행이 됩니다.
  • API 14 이상 단말을 지원한다
  • 일회성 혹은 주기적인 비동기 작업을 예약할 수 있습니다.
  • 잠자기 모드와 같은 절전 기능을 지원합니다.

언제 사용할까?

앞서 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 구성

  • WorkManager
    • 처리해야 하는 작업을 자신의 큐에 넣고 관리합니다.
    • 싱클톤으로 구현이 되어있고, getInstance()로 WorkManager의 인스턴스를 받아 사용하게 됩니다.
  • Worker
    • 백그라운드에서 처리하고 싶은 작업을 정의하기 위해서 사용하는 추상 클래스입니다.
      • doWork()
        • 메소드를 재정의하여 WorkManager에서 처리할 작업을 정의합니다. WorkManager에서 제공하는 백그라운드 스레드에서 실행됩니다.
        • 작업을 완료하고 결과에 따라 Worker클래스 내에 정의된 enum인 Result의 값중 하나를 리턴해야 합니다.
        • Result.success() : 작업이 성공적으로 완료
        • Result.failure(): 작업이 실패로 완료
        • Result.retry(): 작업에 오류가 발생하여 재시도가 필요함
  • WorkRequest
    • 작업이 언제, 어떻게 처리될지를 정의합니다.
    • WorkManager를 통해 실제 요청하게 될 개별 작업입니다.
    • 처리 할 작업인 Work와 작업 반복 여부, 작업 실행 조건, 제약 사항 등 작업에 대한 정보가 담겨 있습니다.
    • onTimeWorkRequest : 반복하지 않고, 한번만 실행될 작업의 요청을 나타내는 클래스
    • PeriodicWorkRequest : 여러번 실행할 작업의 요청을 나타내는 클래스
  • WorkState
    • WorkRequst의 id와 해당 WorkRequest의 현재 상태를 담는 클래스
    • 이 WorkState의 상태정보를 이용해서 자신이 요청한 작업의 현재 상태를 파악할 수 있습니다.
    • ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCLLED의 6개 상태를 가집니다.

State

일회성 작업의 상태 다이어그램입니다.

해당 그림을 보면 작업의 생명 주기를 알 수 있습니다.

작업이 요청되면 ENQUEUED 상태로 들어오게 됩니다. 작업의 요구사항이 만족이 되면 RUNNING 상태로 변경됩니다. SUCCEEDED, FAILED, CANCELLED 모두 작업의 최종 상태가 됩니다.

작업이 retry 된 경우에는 다시 ENQUEUED 상태로 되어 재실행되게 됩니다.

체이닝된 작업에는 BLOCKED 상태가 추가됩니다. 앞선 작업이 최종 상태가 되기 전까지 후속 작업들은 BLOCKED 상태입니다. 만일 SUCCEEDED가 된다면 다음 작업은 ENQUEUED 상태가 되어 실행을 기다리게 됩니다. 하지만 앞선 작업이 FAILED나 CANCELLED 상태로 종료된다면 후속 작업도 모두 이를 따라가게 됩니다.

주기적 실행이 되는 작업의 상태 다이어그램입니다.

주기적 실행이 되는 작업은 종료되지 않기 때문에 성공, 실패 상태가 존재하지 않습니다. 성공과 실패에 상관없이 다시 예약이 됩니다. 따라서 작업의 최종 상태는 CANCELLED만 있습니다.

중복 작업 관리하기

WorkManager는 자신의 Queue를 가지고 있습니다. 따라서 enqueue() 메서드를 통해서 작업을 예약합니다. 하지만 주의할 점이 있습니다. Queue는 동일 작업에 관해 중복처리를 하지 않으므로 동일한 작업이 여러번 예약되지 않도록 조심해야 합니다.

이런 중복 작업은 차단하는 가장 좋은 방법은 작업을 고유작업으로 등록하는 것입니다. 고유작업은 특정 작업의 인스턴스가 하나만 있도록 도와줍니다. 이는 고유작업에 개발자가 직접 고유 이름을 정하여 관리됩니다. 다음 메서드를 호출합니다.

  • WorkManager.enqueueUniqueWork() : 일회성 작업
  • WorkManager.enqueueUniquePeriodicWork() : 주기적 작업

이 메서드를 통해서 작업의 고유한 이름을 설정하고, 고유 이름이 있는 작업 체인이 완료되지 않은 경우에 할 작업을 지정하고, 예약할 workRequest를 지정합니다.

자세한 사용법은 작업관리페이지를 참고하면 됩니다.

사용 방법

단순 작업

  1. Worker 클래스를 상속받아서 클래스를 만들고, doWork() 메서드를 오바라이딩합니다.
    class FirstWorker(context: Context, workerParams:WorkerParameters): Worker(context, workerParams) {
        override fun doWork(): Result {
            // 처리할 코드 추가
            return Result.success()
        }
    }
  1. OneTimeWorkRequestBuilder 를 이용해서 OneTimeWorkRequest 객체를 생성합니다.
    fun createOneTimeWorkRequest() = OneTimeWorkRequestBuilder<FirstWorker>().build()
이렇게 하면 작업을 한번 수행하는 예시입니다.

- 반복되는 작업을 위해서는 PeriodicWorkRequestBuilder 를 이용하여 PeriodicWorkRequest 객체를 생성하여 WorkManager 의 큐에 추가 하면 됩니다. 이때 첫번째 인자로는 반복될 인터벌 값, 두번째 인자로는 이 인터벌의 시간타입입니다.
    
        fun createWorkRequest(data: Data) 
        	= PeriodicWorkRequestBuilder<FirstWorker>(12, TimeUnit.HOURS)

반복 시간에 사용할수 있는 가장 짧은 최소값은 PeiodicWorkRequest 클래스 내의 MIN_PERIODIC_INTERVAL_MILLIS 상수로 정의되어 있으며 15분 보다 짧은 시간은 사용할수 없습니다.

  1. WorkManager 클래스의 getInstance() 메서드로 싱글톤 객체를 받아서 WorkManager 작업 큐에 OneTimeWorkRequest 객체를 추가하면 됩니다.
    val work = createWorkRequest(Data.EMPTY)
    WorkManager.getInstance(this).enqueue(work)

제약 조건을 가지는 작업

WorkManager은 다양한 제약조건을 추가하여 작업을 실행할 수 있습니다. 제약조건이 만족이 되면 작업을 수행하고, 그렇지 않으면 작업을 취소합니다. 처리가 완료되지 않았다면 다시 제약조건이 만족되는 타이밍에 다시 처리를 시도합니다.

  1. Constraints 클래스의 Builder 를 이용하여 생성합니다. 이 제약조건을 WorkRequest에setConstraints()메서드를 통해서 작업을 생성합니다.
    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이내에서만 가능합니다.

  1. 첫번째 작업을 수행할 때 이미지 파일의 이름을 보냅니다.
    • Data.Builder 를 사용하거나 코틀린의 경우 Map 의 확장함수로 toWorkData() 메서드를 제공하므로 쉽게 Map 객체에서 Data 객체를 생성할수 있습니다.
    val input = mapOf("data" to "1")
    val inputData = Data.Builder().putAll(input).build()
    
    val work1 = createOneTimeWorkRequest(inputData)
  1. 첫번째 작업에서 Data객체에 담겨 있는 이미지 파일의 이름을 받습니다.
    • WorkRequest.Builder 클래스에는 setInputData() 메서드가 존재(inputData)하고 인자로 Data 객체를 받습니다. 이 메서드를 이용해서 Worker 에 이미지의 이름을 전달할수 있습니다.
        override fun doWork(): Result {
                
                val data = inputData.getString("data")
                Log.d("Sample", "input data: ${data}")
                
                return Result.success()
            }
  1. 첫번째 작업에서 받은 데이터를 두번째 작업으로 보냅니다.
    • data를 생성해서 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)
            }

References

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

profile
안녕하세요

0개의 댓글