이전에 작성했던 글인 BatterySaveMode 관련된 작업을 추가할 때도 그랬지만, 실무를 진행하면서 CoroutineWorker를 사용하여 기능을 개발하는 일이 종종 있다.
그리고 대부분 생각보다 무겁지 않은 작업을 수행하기 때문에 디테일하에 CoroutineWorker를 살펴볼 일은 많이 없다.
필자도 마찬가지였고, 그래서 CoroutineWorker에서 작업을 할당하고 수행하는 부분에서 문제를 겪어 이유를 찾고 수정하는데 생각보다 많은 시간이 들었다.
따라서, 이번 기회에 CoroutineWorker에 대하여 조금 더 디테일하게 사용하는 방법을 알아보고자 한다.
우선,
CoroutineWorker를 사용하는 이유는 무엇인가?
다양한 이유가 있어서 사용하겠지만 필자가 생각하는 CoroutineWorker를 사용하는 이유는 다음과 같다.
- 간단하게 비동기 작업을 처리할 수 있다.
- 백그라운드에서 작업을 수행하여 메인 스레드의 부하를 줄인다.
- 앱이 종료되어도 예약된 작업의 실행을 보장한다.
이것들 외에도 리소스 최적화라던지 다양한 이유가 있겠지만, 구태여 CoroutineWorker를 사용한다고 하면 위의 3가지 이유 때문이 아닐까 싶다.
백그라운드에서 비동기 작업을 수행한다는 특징 때문에 대량의 데이터 작업을 수행할 때 주로 사용하며,
필자도 이와 같은 이유로 CoroutineWorker를 사용하게 되었다.
사용하는 이유에 대해서 간단하게 생각해봤으니, 사용하는 방법에 대해서 알아보자.
coroutineWorker를 사용하기 위해서는 CoroutineWorker를 상속받는 클래스를 만들어야 한다.
class BackGroundWorker(
val context: Context,
workerParams: WorkerParameters,
) : CoroutineWorker(context, workerParams), KoinComponent {
override suspend fun doWork(): Result {
TODO("Not yet implemented")
}
}
해당 클래스를 실행했을 때, doWork에 작성된 항목들이 실행되게 되는 아주 간단한 구조이다.
앞서 CoroutineWorker는 백그라운드에서 비동기로 작업이 수행된다고 했는데, doWork에서 작업이 수행될 때는 백그라운드에서 작업이 수행되므로 Dispathcers.Default 스레드풀에서 실행된다.
suspend로 함수가 선언되어있기 때문에 이와 같은 내용을 알지 못해도 비동기적으로 수행되는 함수이구나.라는 것은 알 수 있을 것이다.
우선 이렇게 CoroutineWorker를 상속받는 클래스를 만들었다면 CoroutineWorker를 사용할 준비는 됐다는 것이다.
doWork에 작성할 작업은 차치하고, CoroutineWorker를 실행시킬 수 있도록 함수를 구현해보도록 하자.
처음에는 CoroutineWorker 클래스를 사용하여 WorkRequest를 선언해야 한다.
val uniqueWorkRequest =
OneTimeWorkRequest.Builder(BackGroundWorker::class.java).build()
RequestBuilder.build()를 통해 WorkReqeust를 선언할 수 있는데, 필자는 OneTimeWorkRequest를 사용하였다.
이름 그대로, 한번의 실행을 보장하는 WorkRequest라고 생각하면 되는데,
다음과 같이 선언하여 주기적으로 반복되는 WorkRequest를 선언할 수 있다.
val periodicWorkRequest =
PeriodicWorkRequestBuilder<BackGroundWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS
).build()
간단하게 한 시간에 한 번씩 반복되는 Worker가 되는 것이다.
이와 같이 workRequest를 선언하였으면,
WorkManager.getInstance(context).enqueue(uniqueWorkRequest)
enqueue를 통해 간단하게 CoroutineWorker를 실행시킬 수 있다.
아주 기본적으로, 최소한의 선언을 통해 CoroutineWorker를 실행시키는 방법에 대해서 알아보았으니 살짝 고도화를 해보도록 하자.
OneTimeWorkRequest.Builder(BackGroundWorker::class.java)
.setInputData(data)
.setInitialDelay(2, TimeUnit.SECONDS)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.addTag(uniqueReplaceWorkTag)
.build()
WorkRequest를 선언하는 부분에 다양한 데이터를 넣어보았다.
사용하는 함수들이 모두 직관적인 이름을 갖고 있기 때문에 어떠한 역할을 하는지는 쉽게 알 수 있을 것이다.
하나씩 확인해보도록 하자.
.setInputData(data)
CoroutineWorker를 실행할 때 전달할 데이터를 넣어주는 부분이다.
public final @NonNull B setInputData(@NonNull Data inputData)
해당 함수를 확인해보면 이와 같이 되어있다. Data Type의 객체에 원하는 데이터를 넣어주면 된다.
Data 타입 객체를 확인해 보면 이와 같이 다양한 타입을 넣어서 사용할 수 있는데,
전달할 데이터에 따라서 데이터를 추가하여 coroutineWorker에 전달하면 된다.
다음으로는,
.setInitialDelay(2, TimeUnit.SECONDS)
딜레이를 설정하는 부분이다.
public @NonNull B setInitialDelay(long duration, @NonNull TimeUnit timeUnit)
duration과 TimeUnit 객체를 매개변수로 받고 있는데, 기간과 시간을 지정해 주면 그만큼 기다린 후에 작업이 수행된다.
아주 간단하게 사용이 가능한 부분이기 때문에 바로 넘어가도록 하겠다.
BackoffCriteria 부분인데 백오프에 관련한 정책으로 작업을 재시도 할 때 사용되는 옵션이다.
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
예제에는 다음과 같이 선언해서 사용하였는데, 우선 함수 선언부 부분부터 확인해보자.
public final @NonNull B setBackoffCriteria(
@NonNull BackoffPolicy backoffPolicy,
long backoffDelay,
@NonNull TimeUnit timeUnit)
backOffPolicy 부분은 재시도할 때 수행 될 정책의 종류를 설정해 주는 부분이며, delay와 TimeUnit 부분은 말 그대로 재시도를 수행할 때 딜레이를 설정해주는 부분이다.
TimeUnit는 많이 사용했던 부분이기 때문에 넘어가도록 하고, backoffDelay를 확인해 보자.
long type으로 선언된 변수이기 때문에 그냥 임의로 작성해도 상관없지만, 주로 사용하는 WorkReqeust에 지정된 상수를 사용한다.
WorkRequest class에서 선언된 상수이기 때문에, OneTimeWorkRequest와 PeriodicWorkRequest 둘 다 동일하게 선언하여 사용이 가능하다.
마지막으로 BackoffPolicy는 해당 재시도 작업을 얼마만큼의 주기로 수행할 것인지 설정하는 부분인데, BackoffPolicy의 종류는 2가지 밖에 존재하지 않는다.
지수 형태와 선형 형태가 존재한다.
이 두 가지 케이스는 재시도 작업을 수행하는 주기의 증가를 나타내는데, 말 그대로 재시도 작업을 수행하는 주기가 지수 형태로 증가하도록 할 것인지, 선형 형태로 증가하도록 할 것인지 선택하여 작업하면 된다.
또한, backoffCriteria를 통해 설정한 재시도 작업에 대한 정책은 doWork 작업이 실패했을 때 수행되는 부분이 아니라, retry를 요청했을 때 수행되는 부분이다.
즉, doWork에서 Result.retry()를 호출했을 때 영향을 받는 부분이고, Result.success(), Result.failure()가 호출되는 경우에는 재시도를 하지 않음을 알아두고 넘어가야 한다.
마지막으로는 tag에 관련한 부분이다.
.addTag(uniqueReplaceWorkTag)
String 타입의 값을 해당 WorkRequest에 지정함으로써 WorkRequest를 식별할 수 있는 ID로 사용할 수 있는 것이다.
이것은 실행시킨 Workmanager를 취소할 때 사용한다.
물론 작업을 취소시키는 것은 여러 가지 방법을 사용할 수 있지만, 아주 간단하게 특정 작업을 취소하기 위해서는 해당 tag를 설정해 주고 그 값을 사용해서 특정 작업을 취소할 수 있게 되는 것이다.
val uniqueWorkTag = "unique_work_tag"
...
WorkManager.getInstance(context).cancelAllWorkByTag(uniqueWorkTag)
workRequest를 선언할 때 사용한 tag를 사용하여 cancelAllWorkByTag 함수의 매개변수로 넣어주기만 하면 끝이다.
하지만 함수 이름을 보면 알 수 있다시피, Tag값은 유니크한 아이디 값이 될 수 있지만 그렇게 사용하지 않고 중복으로 사용해도 상관이 없는 부분이다.
그렇기 때문에 같은 tag로 설정된 모든 work를 취소하므로 cancelAllWorkByTag라는 이름이 붙어있는 것이다.
물론,
WorkManager.getInstance(context).cancelAllWork()
이와 같이 간단하게 모든 작업을 취소할 수 있다는 것을 잊지 말자.
tag와는 상관없지만 작업을 취소하는 것에서 이어나가자면, 단건으로 작업을 취소하기 위해서는 어떻게 해야 하는가?
방법은 2가지가 존재한다.
- workRequest의 id 값을 사용한다.
- uniqueWorkName을 사용한다.
해당 방법을 간단하게 설명하자면,
첫 번째 workReqeust의 id값을 사용하기 위해서는 선언한 workRequest 객체의 id 값을 가져와서 사용하는 방법이 있다.
val workerRequestId = remember { mutableStateOf<UUID?>(null) }
val uniqueWorkRequest = OneTimeWorkRequest.Builder(BackGroundWorker::class.java)
...
.build()
workerRequestId.value = uniqueWorkRequest.id
WorkManager.getInstance(context).cancelWorkById(workerRequestId.value)
간단하게 이와 같이 생성한 workRequest 객체의 id값을 저장해서 사용할 수 있는데, 해당 데이터는 UUID 데이터 타입을 가지고 있다.
cancel 함수 이름을 보면 알 수 있듯이, 단건의 작업을 취소하는 작업이기 때문에 All 키워드가 없는 것을 볼 수 있다.
두 번째의 uniqueWorkName을 사용하는 방법은, Tag와 비슷하며 tag로 사용해도 괜찮다.
하지만, tag는 동일하게 만들고 WorkName을 유니크한 값으로 지정하여 사용할 수 있는데 이 경우, 작업을 실행시킬 때 이름을 지정해 주고 이 데이터를 사용하여 취소하는 방법이다.
WorkManager.getInstance(context).enqueueUniqueWork(
"uniqueWorkNameSample",
ExistingWorkPolicy.REPLACE,
uniqueReplaceWorkRequest
)
...
WorkManager.getInstance(context).cancelUniqueWork("uniqueWorkNameSample")
작업을 실행시킬 때, enqueue를 사용하는 것이 아닌 enqueueUniqueWork를 사용하면서, 첫 번째 매개변수로 uniqueWorkName를 넣어주면 된다.
cancel 함수를 수행시킬 때는 첫 번째 매개변수로 넣은 String 값을 그대로 cancelUniqueWork의 값으로 넣어주면 된다.
enqueueUniqueWork으로 work를 실행시키는 것을 보면 지금까지 보지 못했던 것을 볼 수 있는데,
ExistingWorkPolicy.REPLACE
해당 값이 무엇인가 궁금할 수 있다.
우선, 단어의 뜻 그대로 기존 작업에 대한 정책을 말하는 것인데 해당 enum 타입의 객체를 확인해 보면 다음과 같은 설명이 나와있다.
An enumeration of the conflict resolution policies available to unique OneTimeWorkRequests in case of a collision.
작업에 충돌이 발생했을 때 해결할 수 있는 정책들이라는 것을 알 수 있다.
이것은 고유한 이름을 가진 작업이 이미 존재할 때 이 작업을 어떻게 처리할 것인가? 에 대한 정책을 의미하며,
이 정책의 열거형 타입에는 Replace, Keep, Append, Append_or_replace의 4가지 정책이 존재한다.
이 4가지 정책을 간단하게 설명하면 다음과 같은 차이점이 존재한다.
- Replace : 기존 작업을 새 작업으로 대체한다. 완료가 되지 않은 작업이 있다면 즉시 취소하고 마지막에 요청한 작업이 실행된다.
- keep : 기존 작업을 유지하고 새 작업을 무시한다. 완료가 되지 않은 작업이 있다면 해당 작업을 수행하고 요청이 들어온 새로운 작업은 수행되지 않는다.
- Append : 기존 작업을 유지하고 새 작업을 추가한다. 완료가 되지 않은 작업이 있다면 해당 작업을 수행하고, 순차적으로 새롭게 들어온 작업을 수행한다.
- Append_or_replace : 실행 중인 작업이 있다면 Append와 같이 동작하고 실행 중인 작업이 없다면 Replace와 같이 동작한다. 즉, 이미 등록된 작업이 있더라도 현재 실행 중이지 않다면 Replace, 현재 실행 중인 같은 이름을 가진 작업이 있다면 Append가 된다.
이 4가지 정책을 통해서 작업의 수행을 원하는 대로 보장할 수 있게 된다.
필자는 예제로 Replace를 사용했지만 사용하는 목적과 요구사항에 따라서 적절한 정책을 사용하면 될 것이다.
얼추 CoroutineWorker를 사용하는데 자주 사용하는 함수들에 대한 설명이 끝났는데,
위의 정책과 같이 작업을 연속해서 수행할 수 있는 방법도 있어 간단하게 소개하고 넘어가고자 한다.
WorkManager.getInstance(context)
.beginWith(uniqueReplaceWorkRequest1)
.then(uniqueReplaceWorkRequest2)
.then(uniqueReplaceWorkRequest3)
.enqueue()
workRequest를 실행시킬 때, beginWith과 then 절을 사용하여 체이닝 된 작업을 요청할 수 있다.
이러한 경우, periodicWorkRequest는 사용할 수 없고 OneTimeWorkRequest 만 사용이 가능하며, 쉽게 이해할 수 있도록 workRequest에 넘버링을 해두었는데, 1, 2, 3 순서대로 작업이 실행되게 된다.
순차적으로 작업의 순서가 보장되어야 할 때 위와 같은 체이닝 방법을 사용하여 작업을 수행하면 보다 편리하게 원하는 결과 값을 가져올 수 있을 것이다.
이것으로 대중적으로 많이 사용이 되는 CoroutineWorker의 함수에 대해 알아보았다.
필자는 작업의 순차적인 접근 방법과, 중복된 작업 호출 시 충돌 정책에 대해서 명확하게 알고 있지 않은 상태에서 작업을 하는 바람에 그곳에서 생긴 사이드 이팩트들을 쉽게 해결하지 못하였다.
이런 것들을 설정하지 않으면 여러 가지 케이스의 오류를 처리하지 못하는 것은 당연한 것이기 때문에, 높은 확률로 미리 알고 이런 것들을 처리할 수 있는 장치를 마련해 두기 마련이다.
간단한 플로우로 작업이 수행되는 것뿐 아니라, 다양한 케이스에서 조금만 더 생각을 해보았더라면 진즉에 이러한 사용 방법들이 있는 것을 알 수 있었을 텐데 그러지 못해서 아쉽다는 생각이 들었다.
그래도 덕분에,
보다 더 시간을 투자하고, 자세히 workmanger에 대해서 확인하고 공부할 수 있어서 좋은 시간이 된 것 같다.
추가적으로,
여러 가지 테스트를 해보다가 doWork() 함수 내부에서 coroutineScope를 중첩하여 사용하는 경우 순차적인 호출을 보장하지 않는 케이스를 발견하였는데,
당연하다시피 여러가지 스레드 풀을 중첩해서 사용하면 순서를 보장할 수 없다는 것을 생각하고 예제를 한번 확인해 보길 바란다.
해당 케이스 또한 예제 코드에 넣어두었으니 한번 확인해보길 바란다.
해당 게시글에 사용한 예제는 Github에 올려두었다.
https://github.com/HeeGyeong/ComposeSample
'Android > Utility' 카테고리의 다른 글
[Android] 개발자 계정 인증하기 (4) | 2024.10.01 |
---|---|
[Android] Lottie Animation을 적용해보자. (2) | 2024.09.17 |
[Android] BatterySaveMode (PowerSaveMode) 옵션과 유의해야할 점 (0) | 2024.06.12 |
[Android] Network 연결 여부를 화면과 API에서 체크하는 방법. (0) | 2024.06.03 |
[Android] 정책 변경 후 구글 플레이스토어 개발자 계정 생성부터 신규 앱 배포까지 과정 정리 - 2주간 테스터 20명 유지하기 (27) | 2024.03.16 |