관리 메뉴

나만을 위한 블로그

[Android] Coroutine의 Job이란? 본문

Android

[Android] Coroutine의 Job이란?

참깨빵위에참깨빵_ 2023. 12. 18. 22:50
728x90
반응형

코루틴을 다룰 때 Job이란 개념은 중요하게 취급된다. 아는 사람도 있겠지만 흔히 사용하는 코루틴 빌더 중 launch의 리턴 타입은 Job이다.

 

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

 

이 Job은 대체 무엇인지 알아본다. 코틀린 공식문서에선 아래와 같이 설명한다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/

 

Job

Job A background job. Conceptually, a job is a cancellable thing with a life-cycle that culminates in its completion. Jobs can be arranged into parent-child hierarchies where cancellation of a parent leads to immediate cancellation of all its children recu

kotlinlang.org

Job은 완료될 때까지 생명주기가 있는 취소 가능한 항목이다. Job은 상위 항목을 취소하면 모든 하위 항목이 재귀적으로 즉시 취소되는 상위-하위 계층 구조로 정렬될 수 있다. CancellationException 이외의 예외로 하위 항목이 실패하면 해당 상위 항목이 즉시 취소되고 결과적으로 다른 모든 하위 항목도 취소된다. 이 동작은 SupervisorJob을 써서 커스텀할 수 있다. Job 인터페이스의 가장 기본적인 인스턴스는 아래처럼 생성된다

- Coroutine Job은 launch를 통해 생성된다. 지정된 코드 블록을 실행하고 블록이 완료되면 완료된다
- CompletableJob은 Job() 팩토리 함수를 써서 생성된다. CompletableJob.complete()를 호출하면 완료된다

Job 실행은 결과값을 생성하지 않는다. Job은 부작용 때문에만 시작된다. 결과를 생성하는 작업은 Deferred 인터페이스를 참조하라
Job은 아래와 같은 상태를 가진다

- New
- Active
- Completing
- Cancelling
- Cancelled
- Completed

Job은 일반적으로 Active(생성 및 시작) 상태에서 시작한다. 그러나 optional한 start 매개변수를 제공하는 코루틴 빌더는 start 매개변수가 CoroutineStart.LAZY로 설정되면 New 상태에서 코루틴을 생성한다. 이 작업은 start 또는 join을 호출해서 Active로 만들 수 있다. 코루틴이 작동하는 동안 CompletableJob이 완료될 때까지 또는 실패, 취소할 때까지 작업은 Active 상태다. 예외로 인해 Active Job이 실패하면 취소된다. 즉시 취소 상태로 강제 전환하는 cancel()을 쓰면 Job을 언제든 취소할 수 있다. Job이 완료되고 모든 하위 항목이 완료되면 Job이 취소된다
활성 코루틴 본문이 완료되거나 CompletableJob.complete()를 호출하면 Job이 Completing 상태로 전환된다. Completed 상태로 전환되기 전에 모든 하위 항목이 완료될 때까지 Completing 상태에서 기다린다. Completing 상태는 순수히 Job 내부에 관한 것이다. 외부 관찰자에겐 Completing Job이 여전히 Active 상태인 반면, 내부적으론 해당 하위 작업을 기다리고 있다...(중략)

 

공식문서의 내용대로, launch 코루틴 빌더는 취소 가능한 Job 인터페이스를 리턴 타입으로 갖기 때문에 launch로 만든 코루틴은 중단이 가능한 특성을 갖는다. 또한 Job은 인터페이스기 때문에 Job()은 생성자가 아니라 공식문서에서 말하는 대로 팩토리 함수임에 주의한다. 인터페이스는 생성자를 가질 수 없다!

 

이 Job은 Deferred 인터페이스의 리턴타입이기도 해서, Deferred 인터페이스를 리턴타입으로 갖는 async도 결국 Job 인터페이스를 리턴타입으로 갖는다고 볼 수 있기 때문에 중단할 수 있다. 참고로 async와 launch는 결과를 반환하냐 하지 않냐의 차이가 있을 뿐이고 그 외에는 동일하게 작동하는 코루틴 빌더다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html

 

async

Creates a coroutine and returns its future result as an implementation of Deferred. The running coroutine is cancelled when the resulting deferred is cancelled. The resulting coroutine has a key difference compared with similar primitives in other language

kotlinlang.org

 

Job의 생명주기를 도식화하면 아래와 같다.

 

 

문서에서 설명하다시피 일반적으로 Job은 처음 생성될 때부터 Active 상태를 갖지만, start 매개변수를 CoroutineStart.LAZY로 설정한 경우 Job은 New 상태로 만들어진다. 아래는 Job의 생명주기를 확인하는 예시다.

 

import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main() = coroutineScope {
    val job = Job()
    println(job)    // JobImpl{Active}@58651fd0
    job.complete()
    println(job)    // JobImpl{Completed}@58651fd0

    // launch로 만들어진 Job은 기본적으로 Active 상태를 갖는다
    val activeJob = launch {
        delay(1000)
    }
    println(activeJob)  // StandaloneCoroutine{Active}@49fc609f

    activeJob.join()    // 1초 대기
    println(activeJob)  // StandaloneCoroutine{Completed}@49fc609f

    // start 매개변수를 CoroutineStart.LAZY로 설정하면 New 상태의 Job이 만들어진다
    val lazyJob = launch(start = CoroutineStart.LAZY) {
        delay(1000)
    }
    println(lazyJob)    // LazyStandaloneCoroutine{New}@51b7e6e0
}

 

join()이 호출되면서 delay(1000)의 영향으로 1초동안 일시 중단된다. 이 메서드의 설명은 아래 문서를 참고한다.

 

https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html

 

join

Suspends the coroutine until this job is complete. This invocation resumes normally (without exception) when the job is complete for any reason and the Job of the invoking coroutine is still active. This function also starts the corresponding coroutine if

kotlinlang.org

이 Job이 완료될 때까지 코루틴을 일시 중단한다. 어떤 이유로든 Job이 완료되고, 호출하는 코루틴의 Job이 아직 Active 상태일 때 join() 호출은 정상적으로 재개(resume)된다. 이 메서드는 Job이 아직 New 상태인 경우 해당 코루틴도 시작한다. 모든 하위 항목이 완료된 경우에만 Job이 완료된다. 이 정지 함수는 취소 가능하며 항상 호출하는 코루틴 작업의 취소를 확인한다. 이 정지 함수가 호출되거나 정지된 동안 호출하는 코루틴의 Job이 취소 or 완료되면 이 함수는 CancellationException을 발생시킨다. 이것은 하위 코루틴이 실패한 경우 상위 코루틴이 CancellationException을 발생시킨다는 걸 의미한다...(중략)

 

위에서 Job의 상태를 확인했는데 마지막 상태라 볼 수 있는 2가지 상태는 Completed, Cancelled의 2가지다. join()은 해당 Job의 상태가 이 2가지 중 하나에 도달할 때까지 기다리는 중단 함수다.

 

만약 launch 빌더를 여러 번 사용했다면 Job도 여러 개 생성될텐데, 이 경우에는 어떻게 해야 생성된 Job들이 마지막 상태가 될 때까지 기다릴 수 있을까? 한 가지 방법으로 Job 인터페이스가 가진 children 프로퍼티를 통해 forEach를 쓰고, 이 안에서 join()을 호출하는 방법이 있다.

 

import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main(): Unit = coroutineScope {
    val job = Job()
    launch(job) {
        delay(1000)
        println("test1")
    }
    launch(job) {
        delay(2000)
        println("test2")
    }

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

 

Job()을 써서 Job을 만든 다음, 이것을 다른 코루틴의 부모로 지정한 뒤 join()을 호출하는 경우가 있을 수 있다.

이 경우엔 자식 코루틴이 모든 작업을 끝내도 Job은 여전히 Active 상태기 때문에 프로그램이 종료되지 않는다. Job() 팩토리 함수로 만들어진 Job은 다른 코루틴에 의해서 계속 사용될 수 있기 때문이다. 때문에 위와 같이 Job의 모든 자식 코루틴에서 join()을 호출하는 게 바람직한 방법이다.

반응형
Comments