관리 메뉴

오늘도 더 나은 코드를 작성하였습니까?

9장 취소 본문

Coroutine/코틀린 코루틴

9장 취소

hik14 2026. 2. 1. 19:52

취소는 매우 중요하여 중단함수를 사용하는 몇몇 클래스와 라이브러리는 취소를 반드시 지원한다.

기본적인 취소

job 인터페이스는 취소를 가능하게 하는 cancel 메서드를 가지고 있다.

 

cancel

- 첫 번째 중단점에서 job을 종료한다.

- 자식 코루틴을 가지고 있다면 종료한다. 하지만 부모에게는 영향을 주이 않는다.

- 취소되면, 새로운 코루틴의 부모가 될수 없다.

import kotlinx.coroutines.*


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 함수에 예외를 인자로 넣어 취소 이유를 명확히 할수 있습니다.

canlcel이 호출된 이후 취소 과정이 완료되는걸 기다리기 위해 join을 사용하는것이 일반적이다.

import kotlinx.coroutines.*


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

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

/*
    printing 0
    printing 1
    printing 2
    printing 3
    Cancelled successfully
    printing 4
 */

 

join의 호출이 없다면, race condition이 발생하여, Cancelled successfully뒤에 printing 4가 출력된다.

 

job.cancel()은 코루틴에게 "이제 그만해"라고 신호를 보내는 것입니다. 하지만 코루틴이 그 신호를 받고 실제로 종료(Cleanup)되는 데는 시간이 걸립니다.

  • 코드에는 **Thread.sleep(100)**이 포함되어 있습니다.
  • 만약 cancel()이 호출된 시점에 코루틴이 Thread.sleep을 실행 중이라면, 그 100ms가 다 지나고 다음 delay를 만날 때까지 코루틴은 죽지 않고 살아있습니다.

* join 자식 코루틴의 작업이 완료를 기다리는 함수.

import kotlinx.coroutines.*


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

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

/*
    printing 0
    printing 1
    printing 2
    printing 3
    printing 4
    Cancelled successfully
 */

 

cancel() join()을 합친 cancelAndJoin() 확장함수를 제공한다

 

Job팩토리 함수로 생성된 job도 같은 방법을 통해 취소 가능하며, job에 딸리 수많은 코루틴을 한번에 취소할때 자주 사용된다.

import kotlinx.coroutines.*


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
 */

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

job이 취소되면 Cancelling 상태가 된다.

첫 번째 중단점에서 CancellationException을 던진다.

try-catch로 잡을수 있지만, 다 던지는것이 좋다.

import kotlinx.coroutines.*


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

    delay(1100)
    job.cancelAndJoin()
    println("Cancelled successfully")
    delay(1000)
}

//Printing 0
//Printing 1
//Printing 2
//Printing 3
//Printing 4
//kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@692f3e3
//Cancelled successfully

 

코루틴의 취소는 내부적으로 CancellationException이라는 예외를 던지는 방식을 사용한다.

finally 블록에서 자원의 정리가 가능합니다.

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        try {
            delay(Random.nextLong(2_000))
            println("Done!")
        } finally {
            println("Will always be printed")
        }
    }

    delay(1000)
    job.cancelAndJoin()
}

취소 중 코루틴을 한 번 더 호출하기.

CancellationException 예외를 잡고, 후처리 과정에서 자원을 정리할 필요가 있다면, 계속해서 실행할 수 있다.

하지만, 중단되거나 다른 코루틴을 시작하는 것은 허용되지 않는다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        try {
            delay(2_000)
            println("Job is Done!")
        } finally {
            println("Finally")
            launch { // 새로운 코루틴 무시.
                println("Will not be printed")
            }
            delay(1_000) // 예외 발생지점.
            println("Will always be printed")
        }
    }

    delay(1000)
    job.cancelAndJoin()
    println("Cancel done")
}

//Finally
//Cancel done

 

코루틴이 이미 취소 되었음에도 중단 한수를 반드시 호출해야 하는 경우.

 

withContext(NonCancellable)으로 포장하는 방법을 많이 사용한다.

취소할 수 없는 Job인 NonCancellable을 사용한다. 

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        try {
            delay(2_000)
            println("Job is Done!")
        } finally {
            println("Finally")
            withContext(NonCancellable) {
                delay(1_000)
                println("CleanUp done")
            }
        }
    }

    delay(100)
    job.cancelAndJoin()
    println("Done")
}

//Finally
//CleanUp done
//Done

invokeOnCompletion

invokeOnCompletion 

- Completed, Cancelled 등 마지막 상태에 도달했을 때, 호출될 핸들러를 지정

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = launch {
        delay(1000L)
    }

    job.invokeOnCompletion { exception: Throwable? ->
        println("Finished")
    }

    delay(400L)
    job.cancelAndJoin()
}

//Finished

 

- job이 예외 없이 끝나면 null

- 코루틴이 취소되었으면 CancellationException이 

- 코루틴을 종료시킨 예외일 수 있습니다.

 

job이  invokeOnCompletion이 호출되기 전에 완료되었으면, 핸들러는 즉시 호출됩니다.

import kotlinx.coroutines.*
import kotlin.random.Random


suspend fun main(): Unit = coroutineScope {
    val job = launch {
        delay(Random.nextLong(2400))
        println("Finished")
    }

    delay(800)
    job.invokeOnCompletion { exception: Throwable? ->
        println("Will always be printed")
        println("The exception was: $exception")
    }

    delay(800)
    job.cancelAndJoin()
}

//Will always be printed
//The exception was: kotlinx.coroutines.JobCancellationException
//또는
//Finished
//Will always be printed
//The exception was: null

 

중단될 수 없는 걸 중단하기

취소는 중단점에서 발생하기 때문에 중단점이 없다면, 취소할 수 없습니다.

Thread.sleep을 사용한 구현은 정말 나쁜 방식이기에 현업에선 절대로 사용하면 안됩니다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        repeat(1000) { i ->
            Thread.sleep(200)
            //파일을 읽는 등의 작업이 있다고 가정합니다.
            println("Printing $i")
        }
    }
    delay(1000)

    job.cancelAndJoin()
    println("Canceled successfully")
    delay(1000)
}

//Printing 0
//Printing 1
//Printing 2
//Printing 3
//Printing 4
//Printing 5
//1000까지 찍힘

 

이에 대한 해결방법으로는

 

1. yield(코루틴을 중단후 즉시, 재실행) 주기적으로 호출해준다.

중단 가능하지 않으면서 cpu 집약적이거나 시간이 오래걸리는 연산이 중단함수에 있다면, 각 연산들 사이에 yield를 사용하는것이 좋습니다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        repeat(1000) { i ->
            Thread.sleep(200)
            //파일을 읽는 등의 작업이 있다고 가정합니다.
            yield()
            println("Printing $i")
        }
    }
    delay(1100)
    job.cancelAndJoin()
    println("Canceled successfully")
    delay(1000)
}

 

2. job의 상태를 추적하는 것.

isActive를 이용하여 잡이 액티브한지 확인할 수 있고, 그렇지 않다면 연산을 중단할수 있다.

* cancelAndJoin() 호출후 즉시 멈추지 않음.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        do {
            Thread.sleep(200)
            println("Printing")
        } while (isActive)
    }

    delay(1100)
    job.cancelAndJoin()
    println("Canceled successfully")
}

 

3. ensureActive() Job이 엑티브 상태가 아니면, CancellationException을 던지는 함수.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        repeat(1000) { num ->
            Thread.sleep(200)
            ensureActive()
            println("Printing $num")
        }
    }

    delay(1100)
    job.cancelAndJoin()
    println("Canceled successfully")
}

 

* ensureActive()가 좀 더 가볍워 더 선호 되고있다. yield() 전형적인 최상위 중단 함수이다. 스코프가 필요하지 않기 때문에 일반 중단함수에서도 사용가능하다.

suspendCancellableCoroutine

결과가 나올 때까지 이 코루틴을 잠깐 멈춰줘, 그런데 만약 취소되면, 중단된 작업도 같이 취소해줘!"**라고 명령하는 도구입니다.

public suspend inline fun <T> suspendCancellableCoroutine(
    crossinline block: (CancellableContinuation<T>) -> Unit
): T

 

가장 중요한 메서드는 코루틴이 취소되었을때 행동을 정의하는 데 사용되는 invokeOnCancellation으로 라이브러리의 실행을 취소하거나 자원을 해제할때 주로 사용한다.

 

이미 정의된 콜백 방식의 API(예: 네트워크 라이브러리, 위치 정보 등)를 코루틴으로 래핑할 때 사용

 

취소는 적절하게 사용하면 자원낭비와 메모리 누수를 줄일 수 있다. 애플리케이션의 성능을 개선할 수 있습니다.