Part 2 에서 더이상 실행될 필요가 없는 코루틴을 취소하는 것의 중요성과 방법을 알아봤다. 안드로이드 개발에서는 Jetpack 에서 제공하는 viewMdoelScope
나 lifecycleScope
를 사용하여 이를 처리해줄 수 있었다. 만약 직접 CoroutineScope 를 생성한다면 특정한 하나의 Job 에 스코프를 묶어주고, cancel 까지 직접 호출해줘야 한다.
그런데 만약 사용자가 앱 화면을 벗어난 경우에도 코루틴의 실행을 취소하지 말아야 하는 경우에는 어떻게 해야할까? 이 아티클에 대해서는 그러한 패턴을 구현하는 방법에 대해서 알아본다.
Coroutines or WorkManager?
코루틴은 앱의 프로세스가 살아있는 동안 계속해서 동작할 수 있다. 앱이 죽으면 코루틴도 종료된다. 만약 앱의 프로세스 라이프사이클보다 더 길게 유지되어야 하는 작업을 해야한다면 Android WorkManager 를 사용해야 한다.
만약 앱의 프로세스가 살아있는 동안에만 동작을 수행해야할 필요가 있다면 코루틴을 사용할 수 있다. 이러한 동작을 구현할 수 있는 패턴은 무엇이 있을까?
Coroutines best practices
앞서 말한 요구사항을 구현할 패턴은 Coroutines best practices 들을 기반으로 만들어지기 때문에 Coroutines best practices 를 다시 한번 복습해보자.
1. Inject Dispatchers into classes
클래스에서 코루틴을 생성하거나 withContext
와 같은 함수로 컨텍스트를 변환할때 Dispatchers 를 하드코딩 하지 말고 클래스에 Inject 하는 방식으로 구현해야 한다. 이런 식으로 구현하면 테스트시 Dispatchers 를 테스트 용도에 맞게 바꿔서 전달할 수 있어서 테스트 코드 작성을 쉽게 할 수 있다.
2. The ViewModel/Presenter layer should create coroutines
UI 레이어가 직접 비즈니스 로직을 처리하지 않아야 한다. 비즈니스 로직은 ViewModel/Presenter 에서 처리하도록 한다. UI 레이어는 테스트를 하기 위해 안드로이드 에뮬레이터가 필요해서, 애뮬레이터가 필요없는 ViewModel/Presenter 를 테스트하는 것이 더 쉽다.
3. The layers below the ViewModel/Presenter layer should expose suspend functions and Flows
Data 레이어와 같은 영역은 suspend functions 나 Flows 를 반환하도록 메소드를 작성해야 한다. 이렇게 함으로써 ViewModel Layer 에서 코루틴을 생성하고 해당 코루틴의 lifecycle 을 관리해줄 수 있다. 만약 코루틴을 생성할 필요가 있다면 coroutineScope 나 supervisorScope 를 이용해서 sub scope 를 만든 다음 생성해야 한다(?)
만약 다른 lifecycle 의 scope 를 사용하고 싶다면 이 글을 계속 읽어라
Operations that shouldn’t be cancelled in Coroutines
다음 예제 코드를 보자
class MyViewModel(private val repo: Repository) : ViewModel() {
fun callRepo() {
viewModelScope.launch {
repo.doWork()
}
}
}
class Repository(private val ioDispatcher: CoroutineDispatcher) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
veryImportantOperation() // This shouldn’t be cancelled
}
}
}
만약 doWork()
을 실행하는 코루틴의 lifecycle 과는 상관없이 veryImportantOperation()
가 반드시 실행되게 하고 싶으면 어떻게 해야할까?
이런 경우에는 앱의 커스텀 Application 클래스에 직접 CoroutineScope 를 만들고 CoroutineScope를 이용해서 새로운 코루틴을 생성한 뒤 그 코루틴 내부에서 veryImportantOperation()
실행되게 함으로써 요구사항을 구현할 수 있다. 이 scope 는 클래스에 inject 되어 사용되어야 한다.
이 스코프를 applicationScope 라고 이름 지을 수 있고, 자식 코루틴의 실패가 다른 코루틴에 영향을 주어서는 안되기 때문에 SupervisorJob
을 사용해야 한다.
class MyApplication : Application() {
// No need to cancel this scope as it'll be torn down with the process
val applicationScope = CoroutineScope(SupervisorJob() + otherConfig)
}
앱이 종료될때 앱 내부의 모든 코루틴이 종료되기 때문에 따로 이 applicationScope 를 취소시켜줄 필요는 없다.
Which coroutine builder to use?
veryImportantOperation
의 유형에 따라 사용해야할 코루틴 빌더의 유형이 달라질 수 있다. launch 나 async
- 만약 값을 반환해야 하는 경우에는
async
코루틴 빌더를 사용하고await
를 통해 값을 반환받는다. - 만약 반환 값이 없는 경우에는
launch
코루틴 빌더를 사용하고join
을 통해서 코루틴이 종료될때까지 기다릴 수 있다.
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
externalScope.launch {
// if this can throw an exception, wrap inside try/catch
// or rely on a CoroutineExceptionHandler installed
// in the externalScope's CoroutineScope
veryImportantOperation()
}.join()
}
}
}
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork(): Any { // Use a specific type in Result
withContext(ioDispatcher) {
doSomeOtherWork()
return externalScope.async {
// Exceptions are exposed when calling await, they will be
// propagated in the coroutine that called doWork. Watch
// out! They will be ignored if the calling context cancels.
veryImportantOperation()
}.await()
}
}
}
이 doWork
를 호출한 viewModelScope 가 종료된다고 하더라도 veryImportantOperation() 의 실행은 종료되지 않을 것이다. 또한 veryImportantOperation()
가 종료되기 전까지는 doWork
함수 호출이 종료되지 않을 것이다.
What about something simpler?
veryImportantOperation
를 처리할 또다른 시도를 해볼 수 있는데 withContext
를 이용해서 extenralScope
의 context 로 변환해주는 것이다.
class Repository(
private val externalScope: CoroutineScope,
private val ioDispatcher: CoroutineDispatcher
) {
suspend fun doWork() {
withContext(ioDispatcher) {
doSomeOtherWork()
withContext(externalScope.coroutineContext) {
veryImportantOperation()
}
}
}
}
그러나 이러한 방법에는 주의해야할 함점이 있다.
doWork
를 실행하는 코루틴이 취소되는 경우veryImportantOperation()
가 완료된 시점이 아니라 다음 suspend 포인트에서doWork()
취소가 마무리된다.extenralScope
에 설정한CoroutineExceptionHandlers
가 예외를 잡지 못하는데,withContext
를 사용하는 경우 예외를 재발생 시키기 때문에다.