코틀린 코루틴에서 아주 중요한 기능 중 하나는 취소(cancellation)다. 중단 함수를 사용하는 몇몇 클래스와 라이브러리는 취소를 반드시 지원하고 있다. 단순히 스레드를 죽이면 연결을 닫고 자원을 해제하는 기회가 없기 때문에 최악의 취소 방식으로 볼 수 있다. 개발자들이 상태가 여전히 액티브한지 자주 확인하는 방법 또한 불편하다. 이 책에서 제시하는 취소 방법이 가장 좋다고 볼 수 있다.

기본적인 취소

Job 인터페이스는 취소하게 하는 cancel 메서드를 가지고 있다. cancel 메서드를 호출하면 다음과 같은 효과를 가져올 수 있다.

import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main(): Unit = coroutineScope {
    val job = launch {
        repeat(1_000) { i ->
            delay(200)
            println("Printing $i")
        }
    }

    delay(1100)
    job.cancel()
    job.join()
    println("Cancelled successfully")
}

Printing 0
Printing 1
Printing 2
Printing 3
Printing 4
Cancelled successfully

cancel 함수에 각기 다른 예외를 인자로 넣는 방법을 사용하면 취소된 원인을 명확하게 할 수 있다. 코루틴을 취소하기 위해서 사용되는 예외는 CancellationException이어야 하기 때문에 인자로 사용되는 예외는 반드시 CancellationException의 서브타입이어야 한다.

cancel이 호출된 뒤 다음 작업을 진행하기 전에 취소 과정이 완료되는 걸 기다리기 위해 join을 사용하는 것이 일반적이다. join을 호출하지 않으면 경쟁 상태(race condition)가 될 수도 있다. 다음 코드에서 join 호출이 없기 때문에 ‘Cancelled successfully’ 뒤에 ‘Printing 4’가 출력되는 걸 확인할 수 있다.(책 설명과 달리 여러 번 실행해도 Cancel 문구가 나중에 호출됨)

suspend fun main(): Unit = coroutineScope {
    val job = launch {
        repeat(1_000) { i ->
            delay(100)
            println("Printing $i")
        }
    }

    delay(1000)
    job.cancel()
//    job.join()
    println("Cancelled successfully")
}

Printing 0
Printing 1
Printing 2
Printing 3
Printing 4
Printing 5
Printing 6
Printing 7
Printing 8
Cancelled successfully

kotlinx.coroutines 라이브러리는 call과 join을 함께 호출할 수 있는 간단한 방법으로, 이름에서 기능을 유추할 수 있는 cancelAndJoin이라는 편리한 확장함수를 제공한다.

public suspend fun Job.cancelAndJoin() {
    cancel()
    return join()
}

Untitled

Job() 팩토리 함수로 생성된 잡은 같은 방법으로 취소될 수 있다. 이 방법은 잡에 딸린 수많은 코루틴을 한번에 취소할 때 자주 사용된다.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        repeat(1_000) { i ->
            delay(200)
            println("Printing $i")
        }
    }
    delay(1100)
    job.cancelAndJoin()
    println("Cancelled successfully")
}

Printing 0
Printing 1
Printing 2
Printing 3
Printing 4
Cancelled successfully

한꺼번에 취소하는 기능은 아주 유용하다. 안드로이드를 예로 들면 사용자가 뷰 창을 나갔을 때 뷰에서 시작된 모든 코루틴을 취소하는 경우에 다음과 같이 사용할 수 있다.

class ProfileViewModel : ViewModel() {
    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

    fun onCreate() {
        scope.launch { loadUserData() }
    }

    override fun onCleared() {
        scope.coroutineContext.cancelChildren()
    }
}

취소는 어떻게 작동하는가?