코루틴이 부모 스코프에 묶여 함께 살고 함께 죽는 규율.
viewModelScope.launch { // ViewModel 이 사라지면 이 코루틴도 취소된다
val data = repository.load()
_state.value = data
}
왜 중요한가 — 안드로이드의 고질병
예전 방식
- 화면을 나갔는데 네트워크 응답이 뒤늦게 온다
- 이미 파괴된 View 를 건드린다 → 크래시
- 또는 Activity 참조가 남아 메모리 누수
구조적 동시성
- 화면이 사라지면 스코프가 취소되고 그 안의 코루틴이 전부 멈춘다
- 잊어버릴 수가 없다. 구조가 강제한다
주요 스코프
viewModelScope ViewModel 이 clear 될 때 취소 (가장 많이 쓴다)
lifecycleScope Activity/Fragment 가 파괴될 때 취소
rememberCoroutineScope() Compose 에서 컴포저블이 떠날 때 취소
GlobalScope ❌ 취소되지 않는다. 사실상 쓰지 않는다
부모-자식 관계
- 부모가 취소되면 — → 모든 자식이 취소된다
- 자식 하나가 실패하면 — → 부모와 나머지 형제도 취소된다 (기본 동작)
- 부모는 자식이 다 끝날 때까지 완료되지 않는다
// 형제가 함께 죽는 게 싫으면 SupervisorJob
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
// 또는 블록 단위로
supervisorScope {
launch { mayFail() } // 실패해도
launch { alsoRun() } // 이건 계속 돈다
}
취소는 협력적이다
// ❌ 취소 신호를 확인하지 않으면 계속 돈다
launch {
while (true) { heavyCompute() } // 취소해도 안 멈춘다
}
// ✅ suspend 함수는 취소 지점을 자동으로 확인한다
launch {
while (true) { heavyCompute(); delay(1) }
}
// ✅ 또는 직접 확인한다
launch {
while (isActive) { heavyCompute() }
// 또는 ensureActive() · yield()
}
// 취소는 CancellationException 으로 전파된다 — 삼키면 안 된다
try { ... }
catch (e: CancellationException) { throw e } // 반드시 다시 던진다
catch (e: Exception) { handle(e) }
// catch (e: Exception) 만 쓰면 취소까지 잡아 버린다 → 코루틴이 안 멈춘다
try { } catch (e: Exception)이 취소를 삼키는 것이 가장 흔한 버그다.
정리 작업은 NonCancellable로
try { work() }
finally {
withContext(NonCancellable) { cleanup() } // 취소된 상태에서도 실행된다
}
면접 함정
- ❌ "GlobalScope로 실행하면 편하다" → 취소되지 않아 누수와 크래시의 원인이 된다.
- ❌ "cancel()을 부르면 즉시 멈춘다" → 협력적이다. 코루틴이 확인 지점을 지나야 멈춘다.
스코프를 직접 만들 때
class MyManager {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun start() = scope.launch { ... }
fun clear() { scope.cancel() } // 반드시 취소한다 — 안 하면 누수
}
직접 만든 스코프는 직접 취소해야 한다 — viewModelScope처럼 자동으로 정리되지 않는다.
타임아웃
withTimeout(5_000) { api.load() } // 초과하면 TimeoutCancellationException
withTimeoutOrNull(5_000) { api.load() } // 초과하면 null
여러 작업을 병렬로 하고 전부 기다리기
coroutineScope { // 자식이 다 끝나야 이 블록이 끝난다
val a = async { loadA() }
val b = async { loadB() }
combine(a.await(), b.await())
}
// 하나가 실패하면 나머지도 취소되고 예외가 전파된다
디버깅
// 코루틴에 이름을 붙이면 로그에서 추적하기 쉽다
launch(CoroutineName("user-load")) { ... }
// JVM 옵션: -Dkotlinx.coroutines.debug
// 스택 트레이스에 코루틴 이름이 찍힌다