안드로이드/Coroutine

[Android Coroutine] 구조적 동시성을 완성하는 마지막 퍼즐: 코루틴 취소(Cancellation)

Moimeme Futur 2025. 5. 3. 10:52
반응형

"ViewModel에서 viewModelScope를 쓰면 따로 cancel() 안 해도 된다고들 하죠. 그 이유, 정확히 알고 계신가요?"

혹시 아직도 정확한 원리를 설명 못하겠다면, 이 글을 반드시 끝까지 읽어보세요. 안 읽고 지나가면 분명 손해입니다.

 

Android에서 Kotlin Coroutines를 제대로 쓰기 위해 꼭 알아야 하는 개념 중 하나가 바로 "코루틴 취소"입니다. 특히 ViewModel이나 Activity 생명주기에 따라 적절한 시점에 코루틴을 종료하지 않으면, 앱은 곧 메모리 누수나 예외의 늪에 빠지게 됩니다.

 

이번 글에서는 Kotlin Coroutines: Deep Dive의 9장 "취소(Cancellation)" 내용을 Android 관점에서 재해석하고, 실전 코드에 어떻게 적용할 수 있을지를 살펴보겠습니다.

 

 

 

왜 코루틴 취소가 중요한가?

코루틴은 자동으로 종료되지 않습니다. 무한히 실행될 수 있고, 블로킹 코드나 외부 API 호출 중일 수도 있습니다. Android 앱에서는 다음과 같은 상황에서 코루틴 취소가 필수입니다.

  • Activity/ViewModel이 종료된 이후에도 계속 네트워크 요청 중
  • 사용자가 화면을 벗어났는데도 Flow가 계속 데이터 방출
  • 앱 종료 직전에도 IO 작업이 계속 진행 중

이런 상황에서 코루틴을 정리하지 않으면 다음 문제가 생깁니다.

  • 메모리 누수
  • 취소되지 않은 API 요청 → 중복 요청
  • Lifecycle 충돌 및 예외

 

 

코루틴은 어떻게 취소되는가?

  1. 코루틴은 ‘협조적으로’ 취소된다.
    • delay(), withContext(), yield()  취소 가능한 suspend 지점을 지나야만 취소가 반응됩니다.
  2. CancellationException을 던진다.
    • 내부적으로는 throw CancellationException()이 발생하며 코루틴이 종료됩니다.
  3. finally 블록은 항상 실행된다.
    • 리소스 정리(소켓 종료, DB 트랜잭션 정리 등)는 여기서 해야 합니다.
  4. 자식 코루틴은 부모가 취소되면 같이 취소된다.
    • 구조적 동시성(structured concurrency)의 핵심 원칙입니다.

 

 

Android 실전 예제: ViewModel에서 취소 처리

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            try {
                val data = repository.load() // suspend 함수
                _state.value = data
            } catch (e: CancellationException) {
                Log.d("MyViewModel", "코루틴 취소됨")
                throw e // 꼭 다시 던져줘야 취소 전파됨
            } finally {
                Log.d("MyViewModel", "리소스 정리")
            }
        }
    }
}

핵심 포인트

  • viewModelScope는 ViewModel의 onCleared() 시점에 자동으로 cancel() 호출됨 → 별도 호출 필요 없음
  • CancellationException은 반드시 throw로 다시 던져줘야 제대로 취소됨
  • 리소스 정리는 finally에서!

 

 

실전 팁: 취소 안 되는 코드들!

  • while(true) { doSomething() }  절대 안 멈춤!  isActive로 감싸야 함
  • CPU 연산, 블로킹 IO → suspend 지점 없음 → 취소되지 않음
while (isActive) {
    heavyComputation()
}

또는

withContext(Dispatchers.Default) {
    for (item in list) {
        if (!isActive) return@withContext
        process(item)
    }
}

 

 

viewModelScope, lifecycleScope는 왜 cancel() 안 해도 되나요?

이건 Android 개발자라면 꼭 알아야 할 중요한 개념입니다.

✔ viewModelScope

  • 내부에서 CloseableCoroutineScope를 사용해 ViewModel에 자동 등록됨
  • onCleared() 시점에 scope.cancel() 자동 호출됨 (직접 호출 X)

✔ lifecycleScope

  • 내부적으로 LifecycleEventObserver로 등록됨
  • Lifecycle이 ON_DESTROY 되면 자동으로 cancel() 호출됨

Android에서 코루틴을 구조적으로 쓰고 싶다면 이 두 스코프만 써도 된다고 봐도 무방합니다.

 

 

마무리하며: 취소는 책임이다

코루틴 취소는 단순히 작업을 멈추는 게 아니라, 앱의 안정성과 메모리 효율성을 지키기 위한 개발자의 책임입니다.

  • 취소 가능한 지점이 있는가?
  • 누가 코루틴을 관리하고 있는가?
  • 언제 취소되는가?
  • 리소스는 어떻게 정리되는가?

이 질문에 자신 있게 답할 수 있어야 Android에서 안전하게 코루틴을 다룰 수 있습니다.

 

 

 

[Android Coroutine] 구조적 동시성 쉽게 이해하기 – Job과 launch의 관계

launch를 launch 안에 또 썼는데… 이게 무슨 순서로 실행되는 거지? viewModelScope 안에서 launch를 썼는데 화면이 닫혀도 계속 돌아가는 이유가 뭘까? Android 개발자라면 한 번쯤은 겪어봤을 이 찜찜한

devgeek.tistory.com

 

 

Android 개발자를 위한 CoroutineContext 깊이 이해하기

코틀린 코루틴을 쓰면서 CoroutineContext는 반드시 마주치는 개념이다. 하지만 공식 문서를 읽거나 튜토리얼을 보면, "Context에 여러 요소를 담는다"는 식의 설명만 있어서 막연하게 느껴지기 쉽다.

devgeek.tistory.com

 

반응형