코루틴의 exception 처리와 cancellation에 관해서는 반드시 GoogleIIO 관련 영상(https://www.youtube.com/watch?v=w0kfnydnFWI&t=30s) 이나Jetbrain의 개발자 가이드(https://kotlinlang.org/docs/exception-handling.html)를 확인하세요. 코루틴에서 이 두가지 부분이 가장 난해한 주제입니다. 왜냐하면 직관적이지 않기 때문에 자칫하면 버그를 만들 수있기 때문입니다.
코루틴의 예외 처리 원칙은 이렇습니다.
try catch를 사용해야 할 때는 가장 안쪽의 scope에서 해준다. Coroutine builder(예: launch, runBlocking, async)를 사용할 때 내부적으로 Coroutine scope이 만들어 집니다. 그리고 이 안에서 발생된 exception은 기본적으로 parent scope이 존재할 경우 parent scope으로 전달됩니다. 이걸 Job hiearachy라고 부르는데, 코루틴을 사용할 때 잘 이애해야 하는 부분 중의 하나입니다. 이 때는 아무리 try catch로 예외를 잡으려고 해도 잡히지 않습니다. Coroutine의 ExceptionHandler를 사용하거나 SupervisorScope를 사용하면 예외를 잡을 수 있습니다.
따라서, 가능하면 불필요한 builder를 호출하지 않도록 하는 것 이 좋습니다. 그리고 좀 더 골치가 덜 아픈 접근 방법은, suspend function이 예외를 던지지 않도록 하는 겁니다. 예를 들면 getUsers라는 API을 호출하는 코드가 있다고 하면, 아래처럼 예외를 내부적으로 에러와 성공을 나타내는 sealed class로 변환하여 리턴하는 겁니다.
sealed class Result<T> {
data class Error(val e: Exception): Result<Nothing>()
data class Success(val data: T): Result<T>()
}
suspend fun getUsers(): Result<List<User>> {
return try {
val users = api.getUsers()
Result.Success(users)
} catch (e: Exception) {
Result.Error(e)
}
}
끝으로, 님의 코드에서,
lifecyclescope.launch { // 첫번째 CoroutineScope
adapter.launch { // 두번째 CoroutineScope. lifecyclescope의 CoroutineScope과 다름.
lifecyclescope.launch { // 세번째 CoroutineScope. 위의 adapter와 lifecyclescope의 CorountineScope과 다름.
...
}
}
}
adapter.launch가 뭔가 이상하구요, lifecycleScope.launch는 왜 다시 한번 호출이 되고 있는지 의아하네요. 위처럼 하시면 별개의 CoroutineScope이 세개가 독립적으로 실행되게 되기 때문에 부모-자식의 관계가 성립되지 않고 adapter.launch나 제일 안쪽의 lifecyclescope.launch는 순차적으로 실행되리라는 보장이 없어요. 다른 CoroutineScope이 종료되는 걸 기다리지 않고, 먼저 실행되는 루틴이 먼저 종료될 겁니다.그리고 같은 lifecyclescope를 연달아 사용하고 있기 때문에 그로 인한 문제점이 어떤 것이 있을지 가늠하기가 직관적인지 않습니다. launch를 잘 보시면 block 람다가 CoroutineScope을 사용하는 걸 알 수 있어요.
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit // <---
): Job {
...
}
따라서, 아래처럼 같은 CoroutineScope을 사용하셔야 해요. 물론 일부로 그렇게 의도하신거면 상관없지만요.
lifecyclescope.launch {
launch { // this.launch와 동일. lifecycle.launch에서 사용하는 동일한 CoroutineScope를 사용
}
}