코틀린 언어 용어 사전
코루틴CoroutineScope · Job · 취소 · viewModelScope

구조적 동시성

코루틴이 부모 스코프에 묶여 함께 취소되는 규율. 안드로이드의 생명주기 누수를 구조적으로 막는다.

코루틴이 부모 스코프에 묶여 함께 살고 함께 죽는 규율.

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
// 스택 트레이스에 코루틴 이름이 찍힌다

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 코루틴 — suspend·구조적 동시성·Dispatchers·취소