Primary/Kotlin

[Kotlin] Coroutines Job

해스끼 2022. 11. 10. 15:09

Job

코루틴에서 백그라운드 작업을 나타내는 인터페이스이다. 인터페이스이긴 하지만 동명의 함수로 Job 객체를 만들 수 있다.

interface Job : CoroutineContext.Element

public fun Job(parent: Job? = null): CompletableJob = ...

Job은 부모-자식 관계로 정렬될 수 있다. 부모 Job이 종료되면 자식 Job도 재귀적으로 모두 종료되며, 자식 Job에서 exception이 발생하면 그 Job의 부모도 즉시 종료되고, 부모의 모든 자식이 재귀적으로 종료된다. 부모의 cancel과 다르게 자식의 failure는 부모 쪽으로 전파되지는 않으며, 자신의 부모에만 영향을 미친다. 

 

Job 객체를 얻는 방법은 크게 두 가지이다. 위에서처럼 ``Job()`` 함수를 실행하여 얻을 수도 있고, ``launch`` 등의 코루틴 빌더가 반환하는 Job을 받을 수도 있다. 보통 첫 번째 방법은 ``CoroutineContext``를 선언할 때 사용하고, 두 번째 방법은 Job을 실제로 컨트롤할 때 사용한다.

 

Job은 일반적으로 결과값을 반환하지 않는 형태로 설계되었다. ``Deferred``를 통해 값을 비동기적으로 반환할 수 있다.

 

``Job.complete()``를 호출하면 Job을 종료할 수 있다. Job은 작업이 모두 끝나거나 ``complete()``로 종료될 때 자신의 자식이 모두 종료될 때까지 기다린다. 이때 부모가 자식을 강제로 종료하지는 않으며, 자식의 작업이 모두 끝날 때까지 예의 바르게 기다린다.

 

``Job.completeExceptionally()``를 호출하면 Job 실행 도중 exception이 발생하여 종료되는 효과를 낼 수 있다. 하지만 이 경우에도 자식 작업이 모두 complete할 때까지 예의 바르게 기다린다.

 

``CancellationException`` 때문에 종료된 Job은 정상적으로 취소된 것으로 간주된다. 그러나 ``CancellationException``이 아닌 다른 exception이 발생했다면 Job이 실패했다(fail)고 간주된다. Job이 실패하면 그 Job의 모든 부모 역시 같은 이유로 fail한다. 한 부분이라도 실패하면 전체가 실패한 것으로 처리되는 것이다.

 

그런데 ``Job.cancel()``은 ``CancellationException``만을 매개변수로 받는다. 따라서 ``cancel()``을 호출하면 항상 정상적으로 취소된 것으로 간주되어 부모가 취소되지 않는다. 코드로 시험해 보자.

@OptIn(ExperimentalTime::class)
fun main(): Unit = runBlocking {
    val time = measureTimedValue {
        launch {
            launch {
                delay(500)
            }
            launch {
                delay(450)
            }
            launch {
                delay(100)
                cancel()
            }
        }.join()
    }
    println("Time: ${time.duration}")
}
Time: 525.640600ms

 

자식 중 하나가 취소되었지만 나머지 자식은 취소되지 않았다.

SupervisorJob

Job에서는 자식이 fail하면 부모도 fail한다고 했다. 하지만 부모가 SupervisorJob이라면 자식 중 하나가 fail해도 부모는 전혀 영향받지 않는다. 부모에서 자식의 fail을 직접 처리하고 싶을 때 사용할 수 있다.

 

반면 부모 SupervisorJob이 cancel 또는 fail하면 모든 자식이 종료된다. 또, SupervisorJob을 ``CancellationException`` 이외의 다른 exception으로 cancel했을 때 부모 Job 역시 취소된다.

 

일반적으로 자식의 실패가 부모의 실패를 유발하면 안 되는 상황일 때 ``CoroutineContext``에 정의하여 사용한다.

class SomeClass : CoroutineScope {
    override val coroutineContext: CoroutineContext
        get() = SupervisorJob()
    
    // ...
}

NonCancellable

항상 active 상태를 유지하는 Job이다. 개념적으로 취소되지 않는 작업을 실행할 때 ``withContext``와 함께 사용하도록 디자인되었다.

withContext(NonCancellable) {
    // 블럭 내부는 취소되지 않을 것이다(고 가정해도 좋다)
}

``launch``나 ``async`` 같은 다른 코루틴 빌더 함수에서 사용하지 말자. 항상 active를 유지하는 특성 때문에 부모-자식 간 cancel 관계가 깨질 수 있다.