코틀린 코루틴에서 아주 중요한 기능 중 하나는 취소(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()
}
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()
}
}