https://medium.com/androiddevelopers/coroutines-on-android-part-ii-getting-started-3bff117176dd
복습 해보자.
코루틴은 어떤 문제를 해결하는가?
1. 메인스레드 위에서 돌아가기에는 너무 긴 작업들에 대해서 처리
2. Main-safety 로 인해 suspend 함수들이 메인스레드에서 호출 가능
그래서 코루틴은 일반 함수의 연산(invoke, return) 외에 suspend, resume 연산까지 갖는다.
코루틴 관리하기 (Keeping track of coroutines)
코루틴을 왜 관리해야 할까? 코루틴 자신은 수행되는 작업을 관리하도록 도와주지 않음.
코루틴을 아주 많이 실행시키는 것은 물론 가능하지만, 비싼 연산을 하고 있고 그게 만약 leak 된다면 자원 낭비할수도.
누수된 코루틴은 메모리, CPU, Disk 를 낭비할 수 있고, 또는 필요도 없는 네트워크 요청을 할 수도 있다.
leaking coroutine 을 방지하기 위한 방안이 "structured concurrency" 이다.
structured concurrency 는 언어에서 제공해주는 기능 뿐만 아니라, 코루틴 관리를 위한 best practice 까지 포괄하는 개념이다.
Android 에서는 structured concurrency 를 통해 세 가지가 가능하다
1. 더이상 필요없는 작업을 취소 ( Cancel work)
2. 실행중인 작업을 관리 (Keep track)
3. 코루틴 실패할 경우 에러 시그널 보냄 (Signal errors)
코루틴 관리를 위한 CoroutineScope:
코루틴은 반드시 CoroutineScope 내부에서 실행되어야 한다.
CoroutineScope 는 코루틴 관리에 사용되고, suspend 된 코루틴을 관리할수도 있다.
실제 코루틴을 "실행" 시키는 Dispatcher 와 달리, 코루틴을 실제로 실행시키지는 않는다.
CoroutineScope 는 모든 코루틴들을 매니징 하며, 내부적으로 실행되는 코루틴들을 모두 cancel 할수 있다.
사용자가 어떠한 화면을 벗어났을 때, 진행중인 작업들을 취소하는 경우에 사용한다.
새로운 코루틴 시작하기
suspend 함수를 아무데서나 호출할 수 있는게 아니다.
suspend-resume 매커니즘은 일반 함수에서 코루틴으로의 전환을 필요로 한다.
코루틴을 시작하기 위한 두 가지 방법
1. launch : fire and forget - 결과를 리턴하지 않음
2. async : await 이라는 suspend function 을 통해서 결과를 리턴 받음
launch 는 일반 함수 내부에서 새롭게 코루틴을 시작할 때 사용된다.
launch 에 대해서 일종의 '다리'로 생각해보자.
launch 는 일반 함수에서 코루틴 세상으로 코드를 연결해준다.
launch 나 async 나 어차피 CoroutineScope 에서만 사용할 수 있어서,
당신이 어떤식으로 코루틴을 만들던 간에 scope 로 관리됨을 알 수 있다.
코틀린은 그냥 당신이 untracked 된 코루틴을 만들도록 허용하지 않는다. work leak 를 방지하기 위한 언어 차원의 기능.
Start in the ViewModel
launch 를 그러면 도대체 어디다가 넣어놓고, 언제 스코프 내부의 작업들을 취소하는게 좋을까?
안드로이드에서는 CoroutineScope 를 사용자 화면과 대응시켜 생각하는게 자연스럽다.
Activity, Fragment 가 사용자에게 더이상 관련이 없는 경우 (소멸되어도 되는 경우) 코루틴 스코프는 모든 작업들을 취소할 수 있다.
Structured concurrency gurantees when a scope cancels, all of its coroutines cancel.
안드로이드 아키텍처 컴포넌트들과 코루틴을 함께 고려해 보면,
launch 를 때리고 싶은 전형적인 장소는 ViewModel 이다.
화면 전환 등 config change 마다 코루틴을 죽이고 재시작할 일도 없고, 대부분의 중요한 비즈니스 로직들이 실행되기 때문이다.
뷰모델 내부에서 coroutine 을 사용하고자 할 때 viewModelScope 확장 함수를 사용할 수 있다. (뷰모델 ktx)
onCleared 콜백에서 viewModelScope 는 자동으로 내부적으로 실행된 코루틴을 모두 취소할 것이다.
Warning: Coroutines are cancelled cooperatively by throwing a CancellationException when the coroutine is suspended. Exception handlers that catch a top-level exception like Throwable will catch this exception. If you consume the exception in an exception handler, or never suspend, the coroutine will linger in a semi-canceled state.
코루틴이 하나만 있으면, 그냥 CoroutineScope 로 관리하면 되는데,
코루틴이 더 많을때는 어떻게 관리하나? - coroutineScope, supervisorScope
하나의 코루틴만 런칭하는 경우가 아니라, 여러개의 코루틴을 런칭하는 경우를 생각해보자.
Structured Concurrency 는 suspend function 이 리턴 했을 때 내부적으로 모든 자식 작업들이 끝났음을 보장한다.
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
launch 는 fire and forget 이고, async 는 결과를 리턴한다.
fetchTwoDocs 는 fetchDoc 함수 두개가 실행중일 때 리턴할까?
아니다. 함수 두개가 모두 끝난 후에야 리턴하도록 되어있다.
coroutineScope 빌더 내부로부터 누수가 일어나지 않도록 코틀린이 강제한다.
coroutineScope - 내부의 코루틴 한개라도 실패하면 모든 다른 코루틴들을 취소한다
supervisorScope - 하나가 실패하더라도 나머지를 취소하지 않는다
오류 전달)
코루틴에서 에러는 일반 함수와 동일하게 exception 을 던짐으로써 전달된다.
suspend 함수에서의 예외는 호출자쪽에 resume 에 의해 전달됨.
코루틴에서 에러가 제대로 전달되지 못하는 예시
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
unrelatedScope.async 를 호출하는걸 어떻게 분해해볼 수 있을까?
coroutineScope {...} 과 같이 내부 코루틴들을 묶어주는 녀석이 아니라
별도의 scope 를 가져와서 코루틴을 실행하는데,
resume 할 때 예외가 전달이 되지 않는 이유는?
- async 는 언젠가 어디서 await() 가 호출이 되고, 그 시점에 예외를 재전달 하리라 가정한다.
- 하지만 await() 를 호출하지 않는다면 예외는 raise 되기를 기다리면서 어딘가에 영원히 저장되고 만다.
만약 structured concurrency 를 사용한다면 에러가 올바르게 호출자에게 전달이 될 것이다.
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
coroutineScope 가 모든 자식들이 끝나기를 기다리므로, 자식이 실패한 경우에 notify 도 받는다.
요약)
structured concurrency 는 Android ViewModel 내부에서 사용하여 work leak 를 방지하는 방안이 될 수 있다.
structured concurrency 는 suspend 함수의 실행 흐름을 파악하기 용이하게 한다. return 하기 전에 작업을 마친채로 하고, 예외가 발생할 경우 예외를 던진다 - 일반 함수와 동일하게 생각할 수 있다.
unstructured concurrency 를 만들수는 있다 - 새로운 관련 없는 CoroutineScope 를 만들어서 (대문자 C이다), 혹은 GlobalScope 를 이용해서. 이것은 코루틴이 호출자의 생명주기보다 더 오래 지속되어야 하는 경우에만 고려하자.
structured concurrency 는 다음의 문제들을 해결한다
1. Cancel work - 필요없는 작업을 취소하게 함
2. Keep track - 실행중인 작업을 관리하게 함
3. Signal errors - 코루틴이 실패하면 시그널을 보내도록 함
structured concurrency 달성을 위해 다음이 보장된다
1. scope 가 취소되면 해당 코루틴도 모두 취소된다
2. suspend fun 이 반환되면 모든 작업이 완료된 것이다
3. 코루틴에서 오류가 발생하면 호출자나 scope 에 실패가 전송됨
'Android' 카테고리의 다른 글
Android - Coroutines best practices (0) | 2024.05.15 |
---|---|
3. Kotlin Coroutine - Concurrency issues (0) | 2024.05.15 |
1. Kotlin Coroutine - suspend, resume, Dispatchers. (0) | 2024.05.15 |
[Compose] Compose Phases (0) | 2024.04.29 |
멜론 UI 에 대한 나의 생각 (0) | 2022.01.26 |