들어가며
해당 글은 도서 <코틀린 코루틴의 정석>을 기반으로 코루틴에 대해 학습하는 시리즈의 글입니다.
코틀린 코루틴 시리즈의 이전 글
[Kotlin] 멀티 스레드와 코루틴
들어가며최근 코루틴을 오랜만에 다시, 그러나 제대로 공부해보았습니다.사실 공부를 제대로 시작하기 전까지는 코루틴 사용 방법과 주요 특징만 제대로 알고 있었고, 정확한 내부 동작은 잘
walnut-dev.tistory.com
2. [Kotlin] CoroutineDispatcher란?
[Kotlin] CoroutineDispatcher란?
들어가며해당 글은 도서 을 기반으로 코루틴에 대해 학습하는 시리즈의 글입니다. 코틀린 코루틴 시리즈의 이전 글[Kotlin] 멀티 스레드와 코루틴 [Kotlin] 멀티 스레드와 코루틴들어가며최근 코루
walnut-dev.tistory.com
코루틴 빌더와 Job
코루틴 빌더란?
코루틴 빌더(Coroutine Builder)는 코루틴을 생성하는 함수입니다.
저희가 코루틴을 만드는데 사용했던 launch 와 runBlocking 함수가 바로 코루틴 빌더입니다.
대표적인 코루틴 빌더는 다음과 같습니다.
- launch
- runBlocking
- async
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
위 코드는 launch 함수의 실제 구현 코드입니다. 내부적으로 코루틴 객체를 생성하고 반환하는 것을 알 수 있습니다.
이처럼 모든 코루틴 빌더는 코루틴을 생성하고 Job 객체를 반환합니다.
(단, async는 Job을 구현한 Deferred 객체를 반환합니다. async와 Deferred에 대해서는 다음 시리즈에서 자세히 다뤄보겠습니다.)
Job이란?
코루틴 빌더로 생성되는 Job 객체는 코루틴을 추상화한 객체입니다.
우리는 이 Job을 통해 코루틴을 제어하거나 코루틴의 상태를 확인할 수 있습니다.
Job 객체는 코루틴을 제어할 수 있는 함수와 코루틴 자신의 상태 값을 외부에 노출하고, 개발자는 이러한 메서드와 프로퍼티를 사용해 코루틴을 제어하고 상태를 체크할 수 있습니다.
Job 분석하기
Job 객체에 대해 좀 더 살펴보겠습니다.
public interface Job : CoroutineContext.Element {
// 코루틴을 제어하는 메서드
public fun start(): Boolean
public fun cancel(cause: CancellationException? = null)
public suspend fun join()
public fun invokeOnCompletion(handler: CompletionHandler): DisposableHandle
// 코루틴의 상태를 나타내는 프로퍼티
public val isActive: Boolean
public val isCompleted: Boolean
public val isCancelled: Boolean
/*
이 외에도 코루틴을 제어하는 더 다양한 함수와 상태 값이 정의되어 있습니다.
*/
}
보시다시피 Job은 인터페이스로 정의되어있고, CoroutineContext의 Element로 구현되어 있습니다. 코루틴을 구성하는 구성 요소라고 이해하셔도 좋습니다.
(CoroutineContext는 코루틴을 구성하는 요소를 나타냅니다. 이 역시 추후에 자세히 다뤄볼 예정이며, 우선 이 정도만 이해하고 넘어가겠습니다.)
Job에는 start, join, cancel 같은 코루틴 제어 메서드와, isActive, isCompleted, isCancelled와 같은 상태 프로퍼티 값이 외부에 노출되어 있습니다.
Job 객체의 상태
Job 객체는 코루틴의 실행 흐름에 따라 6가지의 상태를 가질 수 있습니다.

위의 상태 중 '실행 완료 중' 상태는 코루틴의 구조화된 동시성에 대해 이야기할 때 다뤄보겠습니다.
생성(New)
- 코루틴 빌더를 통해 코루틴을 생성하면 코루틴은 기본적으로 “생성” 상태에 놓임
- 자동으로 실행 중 상태로 넘어감
- 만약 생성 상태의 코루틴이 실행 중 상태로 자동으로 변경되지 않도록 만들고 싶다면, 코루틴 빌더의 start 인자로 CoroutineStart.LAZY 를 넘겨 지연 코루틴을 만들면 된다.
실행 중(Active)
- 지연 코루틴이 아닌 코루틴을 만들면 자동으로 실행 중 상태로 변경됨
- 실제로 실행 중일 때 뿐 아니라 실행된 후 일시 중단된 때도 실행 중 상태
실행 완료(Completed)
- 코루틴의 모든 코드가 실행 완료된 경우 실행 완료 상태로 넘어감
취소 중(Cancelling)
- Job.cancel() 등을 통해 코루틴에 취소가 요청됐을 경우 취소 중 상태로 넘어감
- 아직 취소된 상태가 아니어서 코루틴은 계속해서 실행됨
취소 완료(Cancelled)
- 코루틴의 취소 확인 시점(일시 중단 등)에 취소가 확인된 경우 취소 완료 상태가 된다.
- 코루틴은 더 이상 실행되지 않는다.
그리고 위의 상태는 위에서 보았던 3가지 Boolean 프로퍼티(isActive, isCancelled, isCompleted)로 확인할 수 있습니다.
| 상태 (State) | 설명 | isActive | isCancelled | isCompleted |
| 생성(New) | 코루틴이 생성되었지만 아직 실행 요청이 없는 상태 (e.g. CoroutineStart.LAZY 사용 시) |
false | false | false |
| 실행 중(Active) | 코루틴이 실행 중이거나 실행 대기 중, 혹은 일시 중단된 상태 | true | false | false |
| 실행 완료 중 (Completing) |
구조적 동시성 하에서 모든 자식 코루틴이 완료되기를 기다리는 상태 | true | false | false |
| 실행 완료 (Completed) |
코루틴의 모든 코드가 성공적으로 실행을 마친 상태 | false | false | true |
| 취소 중(Cancelling) | cancel()이 호출되어 취소 요청 플래그가 설정되었지만, 아직 취소가 확인되지 않아 코드가 실행되고 있는 상태 | false | true | false |
| 취소 완료 (Cancelled) |
코루틴이 취소 요청을 확인(일시 중단 지점 등)하고 최종 종료된 상태 | false | true | true |
표의 설명만을 보아서는 어떤 상황에서 각 상태와 상태 프로퍼티가 변화되는지 이해하기 어렵습니다. 아래에서 코루틴을 제어하는 방법을 살펴보며 좀 더 자세히 다뤄보겠습니다.
코루틴 제어하기
어떤 상황에서 코루틴을 제어해야하고, 어떻게 제어할 수 있는지 알아보겠습니다.
코루틴 순차 실행
개발을 하다보면 코루틴을 순서대로 처리해야하는 경우가 종종 발생합니다.
- DB 작업을 순차적으로 처리
- 캐싱된 토큰 값이 업데이트된 이후에 네트워크 요청 필요
만약 순서대로 실행되지 않고 인증 토큰을 업데이트하기 전에 네트워크 요청이 실행되면 문제가 발생할 수 있겠죠.
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
delay(100)
println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
}
val networkRequestJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 네트워크 요청")
}
}
/* 실행 결과(다를 수 있음)
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
*/
Job 객체는 순차 처리를 할 수 있는 함수를 제공해줍니다.
join() 함수로 순차 처리하기
join() 함수는 먼저 처리되어야 하는 코루틴의 실행이 완료될 때까지 호출부의 코루틴을 일시 중단하도록 만들 수 있습니다.
먼저 실행이 완료되어야 하는 코루틴이 존재한다면, 해당 Job 객체의 join() 함수를 호출하면 됩니다.
예를 들어 토큰을 업데이트하는 코루틴이 완료된 후 네트워크 요청 코루틴이 실행되어야 할 때, 네트워크 요청 코루틴을 실행하기 전 토큰 업데이트 코루틴의 join()을 호출하여 순차 처리할 수 있습니다.
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
delay(100)
println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
}
updateTokenJob.join() // updateTokenJob이 완료될 때까지 runBlocking 코루틴을 일시 중단
val networkRequestJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 네트워크 요청")
}
}
/* 실행 결과
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-1 @coroutine#3] 네트워크 요청
*/
- updateTokenJob(coroutine#2) 실행 : DefaultDispatcher-worker-1 스레드에서 실행
- updateTokenJob.join() 실행 : updateTokenJob이 완료될 때까지 DefaultDispatcher-worker-1 스레드가 대기
- updateTokenJob 완료
- networkRequestJob(coroutine#3) 실행 : DefaultDispatcher-worker-1 스레드에서 실행
- networkRequestJob 완료
참고로 join() 함수는 join()을 호출한 코루틴만 일시 중단시킵니다. join()을 호출한 코루틴을 제외하고 이미 실행 중인 다른 코루틴은 중단시키지 않습니다.
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
delay(100)
println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
}
val independentJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 독립적인 작업 실행")
}
updateTokenJob.join()
val networkRequestJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 네트워크 요청")
}
}
/* 실행 결과(달라질 수 있음)
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-1 @coroutine#3] 독립적인 작업 실행
[DefaultDispatcher-worker-3 @coroutine#4] 네트워크 요청
또는
[DefaultDispatcher-worker-3 @coroutine#3] 독립적인 작업 실행
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-1 @coroutine#4] 네트워크 요청
*/
joinAll()을 사용해 순차 처리하기
실제 개발 시에는 서로 독립적인 여러 코루틴을 병렬로 실행한 후, 실행한 요청들이 모두 끝날 때까지 기다렸다가 다음 작업을 진행하는 것이 효율적일 것입니다.
이런 작업을 위해 코루틴 라이브러리는 복수의 코루틴 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단시키는 joinAll() 함수를 제공합니다.
joinAll()의 구현 코드를 살펴보면, 가변 인자로 Job 타입의 객체를 받은 후, Job 객체에 대해 모두 join() 함수를 호출합니다.
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }
joinAll()의 대상이 된 코루틴들의 실행이 모두 끝날 때까지 호출부 코루틴을 일시 중단합니다.
아래와 같이 사용할 수 있습니다.
joinAll(job1, job2, …)
또한 Job 객체를 담은 Collection의 확장 함수로 구현된 joinAll() 함수도 있습니다.
public suspend fun Collection<Job>.joinAll(): Unit = forEach { it.join() }
join()을 호출한 이후의 코루틴의 상태
위에서 Job의 상태와 이를 나타내는 상태 프로퍼티를 살펴봤습니다. join()을 사용하지 않았을 때와 join()을 사용했을 때의 코루틴의 상태가 어떻게 다를 수 있는지 확인해보겠습니다.
코루틴의 상태를 확인하기 위해 상태 프로퍼티를 출력하는 코드를 작성해 활용합니다.
fun printJobState(job: Job) {
println(
"Job State\n" +
"isActive >> ${job.isActive}\n" +
"isCancelled >> ${job.isCancelled}\n" +
"isCompleted >> ${job.isCompleted}" +
)
}
join()을 사용하지 않았을 때 아래와 같이 코루틴의 상태를 출력하면 어떻게 될까요?
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(1000L)
}
printJobState(job)
}
/* 실행 결과:
Job State
isActive >> true
isCancelled >> false
isCompleted >> false
*/
코루틴은 isActive가 true, 나머지 프로퍼티는 false인 "실행 중" 상태에 있습니다.
launch는 job을 생성하자마자 코루틴을 실행시키고, 코루틴이 delay를 만나 1초간 대기하는 도중 job의 상태가 출력됩니다. 이 때의 코루틴은 "실행 중" 상태입니다.
그러나 생성한 Job 객체에서 join()을 호출한 뒤 코루틴의 상태를 출력하면,
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(1000L)
}
job.join()
printJobState(job)
}
/* 실행 결과:
Job State
isActive >> false
isCancelled >> false
isCompleted >> true
*/
코루틴은 isCompleted가 true, 나머지 프로퍼티가 false인 "실행 완료" 상태에 있습니다.
이는 join()의 호출로 현재 스레드가 코루틴 Job의 실행이 완료될 때까지 대기한 뒤, 코루틴 실행이 끝난 후 코루틴의 상태가 출력되었기 때문입니다.
CoroutineStart.LAZY를 사용해 코루틴 지연 시작하기
위에서 본 것처럼, launch 함수를 사용해 코루틴을 생성하면 해당 코루틴이 곧바로 실행됩니다. (사용할 수 있는 스레드가 존재할 경우)
하지만 생성할 코루틴을 나중에 실행시키고 싶을 수 있습니다. 코루틴 라이브러리는 생성된 코루틴을 지연 시작할 수 있는 옵션을 제공합니다.
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
위의 launch 함수 코드를 살펴보면 알 수 있듯, 함수의 start 인자에는 CoroutineStart라는 enum 객체를 넘길 수 있습니다. CoroutineStart에는 4가지 상수가 있습니다.
- DEFAULT
- LAZY
- ATOMIC
- UNDISPATCHED
이 중 CoroutinStart.LAZY를 start 파라미터의 인자로 넘기면 코루틴에 지연 시작 옵션을 적용할 수 있습니다.
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
println("[${getElapsedTime(startTime)}] 지연 실행")
}
}
/* 실행 결과:
*/
위 코드를 실행하면 아무것도 출력되지 않는데요. 이는 지연 시작 옵션을 적용한 코루틴의 경우, 명시적으로 실행을 요청하지 않으면 코루틴이 실행되지 않기 때문입니다.
아래처럼 지연 시작 코루틴에 start() 함수를 명시적으로 호출하여 코루틴을 실행 요청할 수 있습니다.
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
println("[${getElapsedTime(startTime)}] 지연 실행")
}
delay(1000L) // 1초간 대기
lazyJob.start() // 지연 코루틴 실행 요청
}
/* 실행 결과:
[1014ms] 지연 실행
*/
그렇다면 지연 시작 옵션이 적용된 코루틴의 상태는 어떨까요? 아래처럼 코루틴의 상태를 출력시켜보면,
fun main() = runBlocking<Unit> {
val job: Job = launch(start = CoroutineStart.LAZY) {
// 생성 상태의 Job 생성
delay(1000L)
}
printJobState(job)
}
/* 실행 결과:
Job State
isActive >> false
isCancelled >> false
isCompleted >> false
*/
지연 시작 코루틴은 모든 상태 프로퍼티 값이 false인 "생성" 상태에 있는 것을 알 수 있습니다.
코루틴 취소하기
코루틴이 실행되는 도중 코루틴을 더 이상 실행할 필요가 없어지면 즉시 취소하는 것이 좋습니다.
실행할 필요가 없어졌음에도 취소하지 않고 계속 실행되도록 두면, 해당 코루틴이 스레드를 계속 사용하여 애플리케이션의 성능 저하로 이어질 수 있기 때문입니다. 그렇다면 어떻게 코루틴을 취소할 수 있을까요?
cancel()을 사용해 Job 취소하기
Job은 코루틴을 취소할 수 있는 cancel() 메서드를 제공합니다. 취소를 원하는 시점에 cancel()을 호출하면 됩니다.
fun main() = runBlocking<Unit> {
val startTime = System.currentTimeMillis()
val longJob: Job = launch(Dispatchers.Default) {
repeat(10) { repeatTime ->
delay(1000L) // 1000ms 대기
println("[${getElapsedTime(startTime)}] 반복횟수 ${repeatTime}")
}
}
delay(3500L) // 3500밀리초(3.5초)간 대기
longJob.cancel() // 코루틴 취소
}
/*
[지난 시간: 1016ms] 반복횟수 0
[지난 시간: 2021ms] 반복횟수 1
[지난 시간: 3027ms] 반복횟수 2
*/
cancel()을 호출해도 곧바로 취소되는게 아니다
cancel() 함수를 호출한 이후 곧바로 코루틴 취소 이후에 실행되어야 하는 작업을 실행하면, 해당 작업은 코루틴이 취소되기 전에 실행될 수 있습니다.
fun main() = runBlocking<Unit> {
val longJob: Job = launch(Dispatchers.Default) {
// 작업 실행
}
longJob.cancel() // longJob 취소 요청
executeAfterJobCancelled() // 코루틴 취소 후 실행돼야 하는 동작
}
우리가 원하는 동작은 longJob이 완전히 취소된 이후 executeAfterJobCancelled()가 호출되는 것이지만, longJob의 취소 이전에 executeAfterJobCancelled()가 실행될 수 있습니다.
이는 Job 객체에 cancel()을 호출하면 해당 코루틴은 즉시 취소되는 것이 아니기 때문입니다.
코루틴의 취소 과정
- Job 객체 내부의 취소 확인용 플래그를 ‘취소 요청됨’으로 변경함으로써 코루틴이 취소돼야 한다는 것만 알립니다.
- 이후 미래 어느 시점에 코루틴 취소가 요청됐는지 확인하고 비로소 취소됩니다.
즉, cancel 함수를 사용하면 해당 Job은 바로 취소되는 것이 아닌 미래의 어느 시점에 취소됩니다. 때문에 코루틴 취소 이후 실행되어야 하는 작업의 순서를 보장할 수 없는 것입니다.
cancelAndJoin()을 사용한 순차 처리
취소에 대한 순차성을 보장하기 위해 cancelAndJoin() 함수를 사용할 수 있습니다. cancelAndJoin()의 대상이 된 코루틴의 취소가 완료될 때까지 호출부의 코루틴이 일시 중단됩니다. 그러면 취소 요청된 코루틴이 취소된 이후 다음 작업이 실행됩니다.
fun main() = runBlocking<Unit> {
val longJob: Job = launch(Dispatchers.Default) {
// 작업 실행
}
longJob.cancelAndJoin() // longJob 취소될 때까지 runBlocking 코루틴 일시 중단
executeAfterJobCancelled() // 코루틴 취소 후 실행돼야 하는 동작
}
코루틴의 취소 확인
앞에서 설명했듯, cancel()과 cancelAndJoin()을 사용했다고 해서 코루틴이 즉시 취소되는 것이 아닙니다.
이들은 Job 객체 내부에 있는 취소 확인용 플래그를 바꾸기만 하고, 코루틴이 이 플래그를 확인하는 시점에 비로소 취소된다.
즉, 코루틴이 취소를 확인할 수 있는 시점이 없다면 취소는 일어나지 않습니다.
코루틴 취소를 확인하는 시점
그렇다면 코루틴의 취소를 확인할 수 있는 시점은 언제일까요?
일반적으로 일시 중단 지점이나 코루틴이 실행을 대기하는 시점입니다. 이 시점들이 없다면 코루틴은 취소되지 않습니다.
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(true) println("작업 중")
}
delay(100L) // 100ms 대기
whileJob.cancel() // 코루틴 취소
}
/* 실행 결과:
작업 중
작업 중
작업 중
...(취소나 종료 없이 무제한 실행)
*/
위 코드의 whileJob 코루틴은 cancel()을 호출했음에도 취소되지 않고 무한히 실행됩니다. 이유는 launch 블록 내부에 코루틴의 취소를 확인할 수 있는 시점이 없기 때문입니다.
코루틴은 일반적으로 실행 대기 시점 또는 일시 중단 지점에서 취소를 확인한 후 취소됩니다. whileJob 코루틴은 while문에서 코드가 무한히 반복해 실행되고 있어 벗어날 수 없고, while문 내부에 일시 중단 지점이 없기 때문에 일시 중단이 일어날 수 없습니다.
즉, 취소를 확인할 수 있는 시점이 없기 때문에 취소가 요청됐음에도 계속 실행됩니다.
코루틴이 취소되도록 만드는 방법
코루틴이 취소되도록 만드는 방법, 즉 코루틴의 취소 요청을 확인할 수 있는 방법으로는 크게 3가지가 있습니다.
- delay를 사용한 취소 확인
- yield를 사용한 취소 확인
- CoroutineScope.isActive를 사용한 취소 확인
1. delay를 사용한 취소 확인
delay 함수는 일시 중단 함수(suspend fun)로 선언되어 있습니다. 인자로 넘겨준 특정 시간만큼 호출부의 코루틴을 일시 중단하게 만듭니다.
코루틴은 일시 중단 시점에 코루틴 취소를 확인하기 때문에, delay 함수를 코루틴 내부에 넣으면 코루틴 실행 중 코루틴의 취소를 확인할 수 있습니다.
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(true) {
println("작업 중")
delay(1L)
}
}
delay(100L) // 100ms 대기
whileJob.cancel() // 코루틴 취소
}
/* 실행 결과:
작업 중
작업 중
...
작업 중
작업 중
Process finished with exit code 0
*/
whileJob 내부에 10ms만큼 일시 중단하여 취소를 확인할 수 있도록 delay(1L)을 넣었습니다. 이렇게 하면 실행후 약 100ms 정도 뒤에 프로세스가 종료됩니다.
하지만 이 방법은 while문이 반복될 때마다 작업을 강제로 1ms 동안 일시 중단시킵니다. 이런 방식은 오히려 작업을 불필요하게 지연시켜 성능 저하가 일어나게 됩니다.
2. yield를 사용한 취소 확인
yield : 양보하다
yield() 함수는 단어의 뜻에서 알 수 있듯, 양보를 하는 동작을 합니다. yield()를 호출한 코루틴은 자신이 사용하던 스레드를 양보합니다.
스레드를 양보한다는 것은 스레드 사용을 중단하는 것입니다.
즉, yield()를 호출한 코루틴이 일시 중단되며, 이 시점에 코루틴은 취소가 요청됐는지 확인할 수 있습니다.
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(true) {
println("작업 중")
yield()
}
}
delay(100L) // 100밀리초 대기
whileJob.cancel() // 코루틴 취소
}
/* 실행 결과:
작업 중
작업 중
...
작업 중
작업 중
Process finished with exit code 0
*/
그러나 이러한 방식도 “작업 중”이 출력될 때마다 yield()로 인해 코루틴의 일시 중단이 발생합니다. yield() 또한 delay()처럼 while문을 한 번 돌 때마다 스레드의 사용이 양보되며 일시 중단되는 문제가 있습니다. 아무리 코루틴이 경량 스레드라고 하더라도 매번 일시 중단되는 것은 작업을 비효율적으로 만들 수 있습니다.
3. CoroutineScope.isActive를 사용한 취소 확인
CoroutineScope.isActive는 현재 코루틴이 활성화된 상태인지 확인할 수 있는 Boolean 타입의 프로퍼티입니다.
코루틴의 취소 요청 시, 해당 코루틴의 isActive 프로퍼티의 값은 false로 바뀝니다. 이 프로퍼티를 활용하면 코루틴의 취소 요청이 들어왔는지 곧바로 확인할 수 있습니다.
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(this.isActive) {
println("작업 중")
}
}
delay(100L) // 100밀리초 대기
whileJob.cancel() // 코루틴 취소
}
/* 실행 결과:
작업 중
작업 중
...
작업 중
작업 중
Process finished with exit code 0
*/
이 방법을 사용하면 코루틴이 일시 중단되지 않고 스레드 사용을 양보하지도 않으면서 작업을 계속 이어갈 수 있어 효율적입니다.
💡 만약 코루틴 내부 작업이 일시 중단 지점 없이 계속된다면, 명시적으로 코루틴이 취소됐는지 확인하는 코드를 넣어주어 코루틴을 취소할 수 있도록 만들어야 합니다. 그렇지 않으면 코루틴의 취소가 올바르게 동작하지 않음을 명시합시다.
코루틴 취소 시 코루틴의 상태
코루틴에 취소 요청이 들어왔을 때와 코루틴이 취소되었을 때 코루틴의 상태를 확인해봅시다.
취소 중인 코루틴은 아래의 상태 프로퍼티 값을 가집니다.
fun main() = runBlocking<Unit> {
val job: Job = launch {
Thread.sleep(1000)
}
job.cancel()
printJobState(job)
}
/* 실행 결과:
Job State
isActive >> false
isCancelled >> true
isCompleted >> false
*/
isCancelled의 값은 true, 나머지 프로퍼티의 값은 false인 "취소 중" 상태입니다.
cancelAndJoin()을 호출해 취소가 완료될 때까지 대기했을 때, 취소가 완료된 코루틴은 아래와 같은 상태 프로퍼티가 출력됩니다.
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(1000L)
}
job.cancelAndJoin()
printJobState(job)
}
/* 실행 결과:
Job State
isActive >> false
isCancelled >> true
isCompleted >> true
*/
isCancelled와 isCompleted의 값은 true, isActive의 값은 false인 "취소 완료" 상태에 있습니다.
사실 코루틴의 상태를 확인하고 이를 바탕으로 코드를 동작시키는 경우는 잘 없을 것입니다. 그러나 코루틴을 깊게 이해하기 위해서는 코루틴 내부에서 어떤 상태 전이가 일어나는지 제대로 아는 것이 중요합니다.
마치며
지금까지 코루틴 빌더와 Job, 그리고 Job을 제어하는 여러 가지 방법에 대해 알아보았습니다.
다음 시리즈에서는 async와 Deferred에 대해 알아보겠습니다.