시배's Android
Kotlin Coroutine | Kotlin Coroutines patterns & anti-patterns 본문
Kotlin Coroutine | Kotlin Coroutines patterns & anti-patterns
si8ae 2023. 9. 23. 14:23제 생각에 Kotlin 코루틴을 사용할 때 해야 할 일과 하지 말아야 할 일(또는 최소한 피해야 할 일) 몇 가지에 대해 글을 쓰기로 결정했습니다.
Wrap async calls with coroutineScope or use SupervisorJob to handle exceptions
❌ async 블록이 예외를 발생시킬 수 있는 경우 try/catch 블록으로 감싸는 것에 의존하지 마세요.
val job: Job = Job()
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... } // (1)
fun loadData() = scope.launch {
try {
doWork().await() // (2)
} catch (e: Exception) { ... }
}
위의 예에서 doWork 함수는 처리되지 않은 예외를 던질 수 있는 새 코루틴(1)을 시작합니다. doWork를 try/catch 블록(2)으로 감싸려고 하면 여전히 충돌이 발생합니다.
이는 작업의 자식 중 하나가 실패하면 부모가 즉시 실패하기 때문에 발생합니다.
✅ (1) 충돌을 피할 수 있는 한 가지 방법은 SupervisorJob을 사용하는 것입니다.
한 자식의 실패 또는 취소로 인해 SupervisorJob 역할이 실패하는 것은 아니며 다른 자식에게도 영향을 미치지 않습니다.
val job = SupervisorJob() // (1)
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw Exception
fun doWork(): Deferred<String> = scope.async { ... }
fun loadData() = scope.launch {
try {
doWork().await()
} catch (e: Exception) { ... }
참고: 이 방법은 SupervisorJob을 사용하여 코루틴 범위에서 async를 명시적으로 실행하는 경우에만 작동합니다. 따라서 아래 코드는 부모 코루틴(1)의 범위에서 async가 실행되기 때문에 애플리케이션이 여전히 충돌합니다.
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
fun loadData() = scope.launch {
try {
async { // (1)
// may throw Exception
}.await()
} catch (e: Exception) { ... }
}
✅ (2) 더 바람직한 방법은 async를 코루틴 스코프로 감싸는 것입니다.
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
// may throw Exception
suspend fun doWork(): String = coroutineScope { // (1)
async { ... }.await()
}
fun loadData() = scope.launch { // (2)
try {
doWork()
} catch (e: Exception) { ... }
}
Prefer the Main dispatcher for root coroutine
❌ Root 코루틴 내에서 백그라운드 작업을 수행하고 UI를 업데이트해야 하는 경우, Main 디스패처가 아닌 디스패처로 실행하지 마세요.
val scope = CoroutineScope(Dispatchers.Default) // (1)
fun login() = scope.launch {
withContext(Dispatcher.Main) { view.showLoading() } // (2)
networkClient.login(...)
withContext(Dispatcher.Main) { view.hideLoading() } // (2)
}
위의 예에서는 기본 디스패처(1)가 있는 스코프를 사용하여 루트 코루틴을 시작합니다. 이 접근 방식을 사용하면 사용자 인터페이스를 터치해야 할 때마다 컨텍스트(2)를 전환해야 합니다.
✅ 대부분의 경우 메인 디스패처를 사용하여 스코프를 생성하는 것이 코드가 더 간단하고 컨텍스트 전환이 덜 명시적인 결과를 낳습니다.
val scope = CoroutineScope(Dispatchers.Main)
fun login() = scope.launch {
view.showLoading()
withContext(Dispatcher.IO) { networkClient.login(...) }
view.hideLoading()
}
Avoid usage of unnecessary async/await
❌ async 함수를 사용하다가 즉시 await()를 사용하는 경우 이 작업을 중단해야 합니다.
launch {
val data = async(Dispatchers.Default) { /* code */ }.await()
}
✅ 코루틴 컨텍스트를 전환하고 부모 코루틴을 즉시 일시 중단하려면 컨텍스트를 사용하는 것이 바람직합니다.
launch {
val data = withContext(Dispatchers.Default) { /* code */ }
}
성능 측면에서는 큰 문제가 되지 않지만(비동기화는 작업을 수행하기 위해 새로운 코루틴을 생성한다고 생각하더라도), 의미상 비동기화는 백그라운드에서 여러 코루틴을 시작한 다음 대기만 한다는 것을 의미합니다.
Avoid cancelling scope job
❌ 코루틴을 취소해야 하는 경우 애초에 스코프 작업을 취소하지 마세요.
class WorkManager {
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
fun doWork1() {
scope.launch { /* do work */ }
}
fun doWork2() {
scope.launch { /* do work */ }
}
fun cancelAllWork() {
job.cancel()
}
}
fun main() {
val workManager = WorkManager()
workManager.doWork1()
workManager.doWork2()
workManager.cancelAllWork()
workManager.doWork1() // (1)
}
위 코드의 문제점은 작업을 취소하면 완료 상태로 전환된다는 것입니다. 완료된 작업의 범위에서 실행된 코루틴은 실행되지 않습니다(1).
✅ 특정 범위의 모든 코루틴을 취소하려면 cancelChildren 함수를 사용할 수 있습니다. 또한 개별 작업을 취소할 수 있는 기능을 제공하는 것도 좋은 방법입니다(2).
class WorkManager {
val job = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + job)
fun doWork1(): Job = scope.launch { /* do work */ } // (2)
fun doWork2(): Job = scope.launch { /* do work */ } // (2)
fun cancelAllWork() {
scope.coroutineContext.cancelChildren() // (1)
}
}
fun main() {
val workManager = WorkManager()
workManager.doWork1()
workManager.doWork2()
workManager.cancelAllWork()
workManager.doWork1()
}
Avoid writing suspend function with an implicit dispatcher
❌ 특정 코루틴 디스패처의 실행에 의존하는 일시 중단 함수를 작성하지 마세요.
suspend fun login(): Result {
view.showLoading()
val result = withContext(Dispatcher.IO) {
someBlockingCall()
}
view.hideLoading()
return result
}
위의 예시에서 로그인 함수는 메인 디스패처가 아닌 코루틴에서 실행하면 충돌이 발생하는 일시 중단 함수입니다.
launch(Dispatcher.Main) { // (1) no crash
val loginResult = login()
...
}
launch(Dispatcher.Default) { // (2) cause crash
val loginResult = login()
...
}
CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
✅ 모든 코루틴 디스패처에서 실행할 수 있는 방식으로 일시 중단 함수를 설계하세요.
suspend fun login(): Result = withContext(Dispatcher.Main) {
view.showLoading()
val result = withContext(Dispatcher.IO) {
someBlockingCall()
}
view.hideLoading()
return result
}
이제 모든 디스패처에서 로그인 기능을 호출할 수 있습니다.
launch(Dispatcher.Main) { // (1) no crash
val loginResult = login()
...
}
launch(Dispatcher.Default) { // (2) no crash ether
val loginResult = login()
...
}
Avoid usage of global scope
❌ Android 애플리케이션의 모든 곳에서 GlobalScope를 사용하고 있다면 이 작업을 중단해야 합니다.
GlobalScope.launch {
// code
}
GlobalScope는 전체 애플리케이션 수명 동안 작동하며 조기에 취소되지 않는 최상위 코루틴을 실행하는 데 사용됩니다.
애플리케이션 코드는 일반적으로 애플리케이션 정의 코루틴 스코프를 사용해야 하며, GlobalScope의 인스턴스에서 async 또는 launch를 사용하는 것은 매우 권장하지 않습니다.
✅ 안드로이드에서 코루틴은 activity, fragment, view 또는 viewmodel 수명 주기로 쉽게 범위를 지정할 수 있습니다.
class MainActivity : AppCompatActivity(), CoroutineScope {
private val job = SupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun onDestroy() {
super.onDestroy()
coroutineContext.cancelChildren()
}
fun loadData() = launch {
// code
}
}
'Kotlin > Kotlin Coroutine' 카테고리의 다른 글
Kotlin Corotuine | 만개의 Coroutine을 실행하면 몇개의 스레드가 활성화 될까? (0) | 2023.07.16 |
---|