관리 메뉴

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

8장 Job과 자식 코루틴 기다리기. 본문

Coroutine/코틀린 코루틴

8장 Job과 자식 코루틴 기다리기.

hik14 2026. 1. 26. 01:14

구조화된 동시성 (부모-자식)

- 자식은 부모로부터 CoroutineContext를 상속받는다.

- 부모는 모든 자식이 작업을 마무리 지을때까지 기다립니다.

- 부모 Coroutine이 취소되면 자식 Coroutine도 취소된다. 

- 자식 Coroutine에서 에러가 발생하면 부모 자식 Coroutine도 에러로 소멸한다.

 

Job

- Coroutine을 취소하고, 상태를 파악하는 등 다양하게 활용된다.

Job이란 무엇인가?

수명을 가지고 있으며 취소 가능하다

Active

- job이 실행되고, 코루틴은 잡을 수행한다. job이 코루틴 빌더에 의해 생성되었을 때, 코루틴의 본체가 실행되는 상태 

- 자식 코루틴을 시작 시킬수 있다.

 

New

- 지연 시작되는 코루틴의 시작 상태.

- 실행하면, Active 상태가 됨

 

Completing

- 실행을 완료 

- 자식 코루틴들을 기다리는 상태.

 

Completed

- 최종상태 

 

*Active, Completing 상태에서 취소 및 실패하면, Cancelling 상태가 된다.

 

Cancelling

- 연결해제 및 자원반납등 후처리 작업을 수행.

 

Canceled

- 최종상태

 

모든 job은 생성과 즉시 Active상태가 된다.

 

import kotlinx.coroutines.*

suspend fun main() = coroutineScope {
    // 빌더로 생성된 Job은
    val job = Job()
    println(job)    // JobImpl{Active}@58651fd0
    // 메서드를 완료시킬 때까지 Completed 상태다
    job.complete()
    println(job)    // JobImpl{Completed}@58651fd0

    // launch는 기본적으로 활성화되어 있습니다.
    val activeJob = launch {
        delay(1000)
    }
    println(activeJob)  // StandaloneCoroutine{Active}@49fc609f

    // 여기서 Job의 완료를 기다린다
    activeJob.join()    // (1초 후)
    println(activeJob)  // StandaloneCoroutine{Completed}@49fc609f

    // launch는 New 상태로 지연 시작된다
    val lazyJob = launch(start = CoroutineStart.LAZY) {
        delay(1000)
    }
    println(lazyJob)    // LazyStandaloneCoroutine{New}@51b7e6e0

    // Active 상태가 되려면 시작하는 함수를 호출해야 한다
    lazyJob.start()
    println(lazyJob)    // LazyStandaloneCoroutine{Active}@51b7e6e0

    lazyJob.join()  // (1초 후)
    println(lazyJob)    // LazyStandaloneCoroutine{Completed}@51b7e6e0
}

코루틴 빌더는 부모의 잡을 기초로 자신의 잡을 생성한다.

코틀린 코루틴 라이브러리는 모든 코루틴 빌더는 자신만의 잡을 생성한다.

 

launch 명시적 반환 타입이 job이다.

async Deferred<T> 지만, Deferred는 job 인터페이스를 구현한다.

import kotlinx.coroutines.*

suspend fun main() = coroutineScope {
    val job1: Job = launch {
        delay(1000L)
        println("test")
    }

    val deferred: Deferred<String> = async {
        delay(1000L)
        "test"
    }
    val job2: Job = deferred
}

 

 

job은 CoroutineContext이기 때문에 coroutineContext[Job]으로도 접근 가능하다. 접근의 편의성을 위해 확장 프로퍼티를 사용한다.

// 확장 프로퍼티
val CoroutineContext.job: Job
    get() = get(Job) ?: error("current context doesn't have job context")

fun main(): Unit = runBlocking {
    println(coroutineContext.job.isActive)
}

 

Job은 코루틴이 상속하지 않는 유일한 coroutinecontext이며, 매우 중요한 법칙이다.

모든 코루틴은 자신만의 Job을 생성하며, 부모 코루틴으로 부터온 job은 새로운 잡의 부모로 사용이된다

import kotlinx.coroutines.*


fun main(): Unit = runBlocking {
    val name = CoroutineName("some name")
    val job = Job()

    launch(name + job) {
        val childName = coroutineContext[CoroutineName]
        println(childName == name) // true

        val childJob = coroutineContext[Job]
        println(childJob == job) // false
        println(childJob == job.children.firstOrNull()) // true
    }
}

 

부모 잡은 자식잡 모두를 참조할 수 있고, 자식도 부모를 참수할 수 있다.  job을 참조할 수 있는 부모-자식 관계가 있기에 코루틴 스코프내에서, 취소와 예외처리 구현이 가능하다.

import kotlinx.coroutines.*


fun main(): Unit = runBlocking {
    val job: Job = launch {
        delay(1000L)
    }

    val parentJob: Job = coroutineContext.job
    println(job == parentJob) // false
    
    val parentChildren: Sequence<Job> = parentJob.children
    println(parentChildren.first() == job) //true
}

 

새로운 Job Context가 부모 잡을 대체하면, 구조화된 동시성의 작동방식은 더 이상 유효하지 않다.

fun main(): Unit = runBlocking {
    launch(Job()) {
        delay(1000)
        println("Will not be printed")
    }
}

 

자식은 인자로 들어온 Job을 부모로 사용하기 때문에 runBlocking의 job과 관계가 없어 기다리지 않고 종료된다. 

자식들 기다리기

join 지정한 job이 최종상태(completed, canceled) 마지막 상태에 도달할 때까지 기다리는 중단한수이다. 

import kotlinx.coroutines.*


fun main(): Unit = runBlocking {
    launch {
        delay(1000)
        println("test1")
    }
    launch {
        delay(2000)
        println("test2")
    }

    val children = coroutineContext[Job]?.children
    val childrenNum = children?.count()
    println("Number of children: $childrenNum")

    children?.forEach { it.join() }
    println("All tests are done")
}

/*
    Number of children: 2
    test1
    test2
    All tests are done
 */

 

 

Job 팩토리 함수

팩토리 함수로 생성한 job

- 어떤 코루틴과도 연관되어 있지 않다.

- 컨텍스트로 사용할수 있다.(즉, 한 개 이상의 자식을 가진 부모 잡으로 사용될 수 있다.)

 

 * 흔한 실수중 하나는 Job() 팩토리 함수로 잡을 생성하고 다른 코루틴의 부모로 지정한뒤에 join을 호출하는 것

모든 작업이 종료되어도 job은 acitve 상태라서 프로그램이 종료되지 않습니다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Text 1")
    }

    launch(job) {
        delay(2000)
        println("Text 2")
    }

    job.join() // active 상태로 영원히 대기한다.
    println("Will not be printed")
}

 

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Text 1")
    }

    launch(job) {
        delay(2000)
        println("Text 2")
    }

    job.children.forEach { it.join() }
    // 자식들만 완료가 되면 팩토리로 생성한 job은 active 상태라도 무시하고 바로 종료됨(coroutineScope 사이에는 부모-자식 관계가 형성되지 않기 때문).
}

 

잡팩토리 함수

@Suppress("FunctionName")
public fun Job(parent: Job? = null): CompletableJob

 

CompletableJob

 

 complete(): Boolean

- 잡을 완료하는데 사용됨.

- 모든 자식 코루틴은 작업이 완료될 때까지 실행된 상태를 유지한다.

- complete 호출한 잡은 새로운 코루틴을 시작할순 없다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        repeat(5) { num ->
            delay(200)
            println("repeat: $num")
        }
    }

    launch {
        delay(500)
        job.complete()
    }

    job.join()

    // job 이미 완료되서 시작되지 않음. 
    launch(job) {
        println("Will not be printed")
    }

    println("Done!")
}

/*
repeat: 0
repeat: 1
repeat: 2
repeat: 3
repeat: 4
Done!
*/

 

completeExceptionally(exception: Throwable): Boolean

- 인자로 받은 예외로 잡을 완료시킨다.

- 모든 자식은 주어진 예외를 랩핑한 CancellationException으로 즉시취소 된다.

- 잡이 메서드의 실행으로 종료되었습니까?를 반환한다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        repeat(5) { num ->
            delay(200)
            println("repeat: $num")
        }
    }

    launch {
        delay(500)
        job.completeExceptionally(Error("some Error"))
    }

    job.join()

    // job 이미 완료되서 시작되지 않음.
    launch(job) {
        println("Will not be printed")
    }

    println("Done!")
}

 

complete 함수는 잡의 마지막 코루틴을 시작한 후, 자주 사용되며, join을 통해 완료되는걸 기다리는 형태로 자주 쓰인다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("Text 1")
    }

    launch(job) {
        delay(2000)
        println("Text 2")
    }

    job.complete()
    job.join()
}

 

Job 팩토리 함수의 인자로 부모 잡의 참조값을 전달할 수 있다.

부모 잡이 취소되면, 해당 잡 또한 취소된다.

import kotlinx.coroutines.*


suspend fun main(): Unit = coroutineScope {
    val parentJob = Job()
    val job = Job(parentJob)

    launch(job) {
        delay(1000)
        println("Text 1")
    }

    launch(job) {
        delay(2000)
        println("Text 2")
    }

    delay(1100)
    parentJob.cancel() // 부모 잡 취소

    job.children.forEach { it.join() }
}