brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Oct 15. 2024

코루틴 취소 (Cancellation)

코틀린 코루틴 (9)

코루틴 취소 (Cancellation)


코루틴 Job에서 cancel()과 join()을 사용하여 코루틴을 취소하는 방법에 대해서 간단히 살펴봤다. 코루틴을 취소하기 위해서는 Job 객체의 cancel() 함수를 이용했다. 그렇다면 다음 코드는 어떤가? 원하는 대로 취소가 되는가? (Dispatchers.Default라는 처음 보는 것은 일단 무시하자.)

실행 결과는 실행할 때마다 조금씩 다르겠지만 아래와 같은 경우가 발생할 수 있다.

Call cancel()
Job - 1
Job - 2
...(중략)...
Job - 114
Finish Counting
Job - 115
...(중략)...
Job - 299
Job - 300

어떤 문제점이 보이는가? cancel() 이후에도 코루틴이 취소가 되지 않았다. 그러면 취소되지 않은 현재 상태에서 그럴싸한 결과를 보여주기 위해서 조작할 수 있는 방법은 무엇이 있을까? 코루틴 Job에서 다뤄보았듯이 cancel()에 이어 join()을 사용하면 결과물이 꽤 괜찮아 보인다.

Call cancel()
Job - 1
Job - 2
...(중략)...
Job - 299
Job - 300
Finish Counting

cancel()과 join()을 사용하는 경우는 흔하기 때문에 이것을 한 번에 적용할 수 있는 cancelAndJoin() 함수도 존재한다.

이렇게 사용할 수 있다.


그러나 이 경우도 결과물이 출력하는 문구랑 어울려서 그럴듯해 보이는 것이지 실제로 코루틴이 취소된 상태는 아니다. 300번의 for 문이 모두 동작했기 때문이다. 그러면 어떻게 취소할 수 있을까? 혹시 취소할 수 없는 걸까? 이런 의문보다 먼저 품어야 할 의문이 있다. 코루틴 Job에서는 cancel()을 사용해서 분명히 코루틴이 취소되는 것을 확인했다. 그런데 지금은 어째서 취소가 되지 않는 걸까?


코루틴이 정상적으로 취소되었다면 CancellationException 발생한다고 했다. 그럼 현재 정상적으로 취소가 되고 있지 않은 것이니 CancellationException이 발생하지 않는다는 말이 된다. 한번 확인해 볼까?

Call cancel()
Job - 1
...(중략)...
Job - 119
Finish Counting
Job - 120
...(중략)...
Job - 300

CancellationException 발생 시 출력되어야 하는 문구가 보이지 않는다. CancellationException이 발생하지 않았다는 것이 입증되었다.


그러면 다시 의문이 든다. 코루틴 Job에서는 cancel()을 사용했을 때는 왜 CancellationException이 발생한 것일까? 그때는 launch를 사용할 때 Dispatchers.Default와 같은 인자를 지정하지 않았다. 그러면 해당 코루틴은 runBlocking에서 사용하는 메인 스레드에서 실행된다. 메인 스레드에서 실행되는 경우 runBlocking 내 코드는 순차적으로 실행된다. 다만 launch는 비동기적으로 실행된다. 따라서 cancel()이 호출되기 전에 launch 블록이 실행 명령은 받았으나, 실제로 내부에서 한 줄의 코드도 완료되지 않은 시점인 상태에서 cancel()이 호출되었을 수 있다. 그러면 아무런 작업을 하지 않은 상태로 취소가 된다. (이 내용은 코루틴 빌더에서 언급한 바 있다.)


현재 보고 있는 예제는 Dispatchers.Default라는 디스패처를 별도로 지정하고 있다. 디스패처? 이게 무엇인지 몰라도 된다. 일단 이렇게 하면 runBlocking에서 사용하는 메인 스레드가 아닌 다른 스레드에서 코루틴이 실행된다고 알고 넘어가자. 이 경우에는 launch 코루틴이 별도의 스레드에서 실행되기 때문에 cancel()이 호출되기 전에 launch 블록이 실행될 가능성이 높다. (왜 그런 것인지는 나중에 디스패처를 공부하면 이해할 수 있다.) 따라서 취소를 감지하기 위한 별도의 조치를 취해야 한다는 결론에 이른다. 어떤 조치를 할 수 있을까? 앞서 학습한 내용에 기반하여 추론을 할 수 있다. Job을 이야기할 때 상태에 관해서 언급하며 마무리했다. Job의 상태는 다음과 같다.


New 상태는 어떤 상태일까? 아래와 같이 CoroutineStart.LAZY 옵션을 사용하면 New 상태가 된다.

val job = launch(start = CoroutineStart.LAZY) {
    println("Coroutine started")
}

그렇다면 취소를 감지하기 위해서 Job의 상태를 이용하면 되지 않을까? 해당 상태에 대한 변경 흐름은 다음과 같다.

우리가 확인하고자 하는 부분은 cancel 했을 때이다. 흐름을 참고하면 Activie 상태에서 cancel을 하면 Cancelling 상태가 된다. 이 경우의 상태 변경을 감지하려면 isActive가 true에서 false로 전환되는 것 또는 isCancelled가 false에서 true로 전환되는 것을 확인하면 된다. 하지만 isCancelled는 내부 API이기 때문에 일반 코드에서 사용하지 않아야 하며 deprecated 되기도 했다. 따라서 isActive를 활용하면 된다.

Call cancel()
Job - 1
...(중략)...
Job - 76
Job - 77
Job is canceled
Finish Counting

위 예제 코드처럼 cancel 후 isActivie가 false일 때 CancellationException을 발생시키면 된다. 그러면 코루틴은 취소가 되어 for 문도 멈춘다.

if(!isActive) {
    throw CancellationException()
}

이 부분은 아래와 같은 함수로 대체할 수 있다.

ensureActive()

해당 함수에 대해서 추적해 보면 결국 다음과 같은 함수를 만날 수 있다.

public fun Job.ensureActive(): Unit {
    if (!isActive) throw getCancellationException()
}

이렇게 취소를 할 때 CancellationException이 발생하면 어떤 장점이 있을까? 다르게 표현하면 그냥 단순히 취소가 되고 아무런 피드백이 없는 경우엔 어떤 문제가 있을까?


만약 취소를 했을 때 코루틴에서 할당한 자원 중 반드시 해제를 해야 하는 작업이 있다면 예외가 발생하는 것이 도움이 된다. catch 문 내에서도 처리할 수 있겠지만 CancellationException이 발생하는 것은 코루틴의 정상적인 취소를 의미하기 때문에 굳이 CancellationException에 대한 catch 문을 작성할 필요가 없다. 대신에 finally 문을 사용하면 된다. finally는 예외가 발생하든 안 하든 항상 실행되니 자원을 해제하는 작업을 하기에 유용하다. 또한 모든 예외 상황에서 자원 정리가 가능하다는 장점도 있다.


만약 취소가 불가능한 코루틴을 만들고 싶으면 어떻게 해야 할까? 상황에 따라 그런 경우가 필요할 수 있다. 그럴 때는 withContext 코루틴 빌더에 NonCancellable 콘텍스트를 전달하면 된다.

실행 결과는 아래와 같다.

Call cancel()
Finish Counting
Job - 1
...(중략)...
Job - 300

앞선 예제에서 ensureActive()으로 대체하긴 했지만 기존에 취소가 정상적으로 되던 것을 확인한 코드에 withContext(NonCancellable)를 추가했을 뿐이다. 이 상태에서는 코루틴이 취소가 되지 않았음을 실행 결과를 통해서 확인할 수 있다.


네트워크 통신은 일반적으로 비동기적으로 실행하지만 무한정 대기하도록 방치하지 않는다. 그래서 지정된 시간을 초과할 경우 SocketTimeoutException을 발생하는 등의 장치가 마련되어 있다.


코루틴에서도 이와 비슷한 장치가 있다. 코루틴의 수행 시간이 예상 밖으로 길어질 경우 설정된 제한 시간을 넘기면 취소할 수 있다. 물론 직접 구현할 수도 있겠지만 withTimeout()라는 함수를 제공한다. withTimeout()은 TimeoutCancellationException을 발생시킨다. TimeoutCancellationException은 CancellationException의 하위 클래스다.

예제 코드를 실행하면 300ms 후에 코루틴이 취소되는 것을 확인할 수 있다.

Job - 1
Job - 2
Job - 3
Job is canceled
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms


요약하자.

- 단순히 cancel() 호출하는 것으로 모든 코루틴을 취소할 수 있는 것은 아니다.
- 코루틴의 상태를 추적하는 Job을 이용하여 상태에 따른 처리를 통해 코루틴을 취소할 수 있다.
- 취소할 수 없는 코루틴을 만들 수 있다.
- 일정 시간 후에 코루틴을 취소할 수 있다.
매거진의 이전글 코루틴 async
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari