CoroutineScope
- Coroutine들에 대한 scope를 정의하며 scope에는 여러 Coroutine들 포함
- 자식 Coroutine들에 대한 생명주기를 관리하며 자식 Coroutine들이 모두 완료되어야만 scope도 완료되었다고 처리
- CoroutineScope의 coroutineContext에는 꼭 Job을 포함해야 함

1. CoroutineScope 함수
- CoroutineScope 함수를 이용해 CoroutineScope 생성

부연 설명
- 인자로 주어진 context에 Job이 포함되었는지 확인
- 포함되어 있지 않다면 Job을 생성하여 추가하고 ContextScope에 전달
- 여기서 ContextScope는 CoroutineScope의 단순한 구현체
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
val cs = CoroutineScope(EmptyCoroutineContext) | |
log.info("context: {}", cs.coroutineContext) | |
log.info("class name: {}", cs.javaClass.simpleName) | |
cs.launch { | |
log.info("context: {}", this.coroutineContext) | |
log.info("class name: {}", this.javaClass.simpleName) | |
} | |
} |

부연 설명
- EmptyCoroutineContext를 통해 빈 CoroutineScope를 생성
- 다만, Job을 생성하여 ContextScope를 생성하기 때문에 JobImpl을 포함
2. Coroutine Builder
- 코틀린에서 코루틴을 생성하고 실행하기 위한 기본적인 도구
- Coroutine Builder는 CoroutineScope의 CoroutineContext와 인자로 전달받은 context를 merge 하여 newContext 생성
- Coroutine을 생성하여 start하고 반환
- CoroutineScope의 Job을 부모로 가짐
- Coroutine Builder를 통해 생성된 Coroutine은 비동기 하게 동작
2.1 launch 코루틴 빌더
- StandaloneCoroutine을 생성하고 start 하는데 isLazy가 참일 경우 LazyStandaloneCoroutine 생성
- StandaloneCoroutine을 Job으로 반환하는데 외부에서 launch를 실행한 후 Job을 획득하여 cancel, join 등을 실행 가능

- newCoroutineContext를 통해 CoroutineScope의 coroutineContext와 인자로 주어진 context를 merge

launch 코루틴 빌더 예제
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
@OptIn(ExperimentalCoroutinesApi::class) | |
fun main() { | |
runBlocking { | |
val cs = CoroutineScope(EmptyCoroutineContext) | |
log.info("job: {}", cs.coroutineContext[Job]) | |
val job = cs.launch { | |
// coroutine created | |
delay(100) | |
log.info("context: {}", this.coroutineContext) | |
log.info("class name: {}", this.javaClass.simpleName) | |
log.info("parent: {}", this.coroutineContext[Job]?.parent) | |
} | |
log.info("step1") | |
job.join() | |
log.info("step2") | |
} | |
} |

부연 설명
- CoroutineScope에 Coroutine Builder인 launch를 통해 코루틴을 생성하고 this로 접근
- 비동기로 동작하기 때문에 join을 통해 완료될 때까지 지연
2.2 async 코루틴 빌더
- DeferredCoroutine을 생성하고 시작하는데 isLazy가 참이라면 LazyDeferredCoroutine을 생성
- DeferredCoroutine을 Deferred로 반환
- Deferred는 Kotlin 코루틴에서 비동기 작업의 결과를 나타내는 객체
- Deferred는 미래에 완료될 값을 나타내며, 이는 자바의 Future나 자바스크립트의 Promise와 비슷한 역할
- Deferred는 Job을 상속하고 있기 때문에 join, cancel 뿐만 아니라 await을 통해 block이 반환하는 값에 접근 가능
async 코루틴 빌더 예제
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
@OptIn(ExperimentalCoroutinesApi::class) | |
fun main() { | |
runBlocking { | |
val cs = CoroutineScope(EmptyCoroutineContext) | |
log.info("job: {}", cs.coroutineContext[Job]) | |
val deferred = cs.async { | |
// coroutine created | |
delay(100) | |
log.info("context: {}", this.coroutineContext) | |
log.info("class name: {}", this.javaClass.simpleName) | |
log.info("parent: {}", this.coroutineContext[Job]?.parent) | |
100 | |
} | |
log.info("step1") | |
log.info("result: {}", deferred.await()) | |
log.info("step2") | |
} | |
} |

부연 설명
- CoroutineScope에 코루틴 빌더인 async를 통해 코루틴을 생성하고 this로 접근
- 비동기로 동작하기 때문에 await을 통해 완료될 때까지 suspend 하고 값에 접근
3. AbstractCoroutine
- 모든 코루틴은 AbstractCoroutine을 상속
- 코루틴은 Job이며 Continuation이고 CoroutineScope

부연 설명
- Job: 작업의 단위가 되고 start, cancel로 상태를 변경할 수 있으며 join으로 완료 시점 명시
- CoroutineScope: 코루틴 빌더를 통해 자식 코루틴을 생성하고 자식 코루틴들의 생명주기를 관리
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
@OptIn(ExperimentalCoroutinesApi::class) | |
fun main() { | |
runBlocking { | |
val cs = CoroutineScope(EmptyCoroutineContext) | |
val job = cs.launch { | |
// coroutine1 created | |
delay(100) | |
log.info("job: {}", this.coroutineContext[Job]) | |
val job2 = this.launch { | |
// coroutine2 created | |
delay(500) | |
log.info("parent job: {}", | |
this.coroutineContext[Job]?.parent) | |
log.info("coroutine2 finished") | |
} | |
job2.join() | |
log.info("coroutine1 finished") | |
} | |
log.info("step1") | |
job.join() | |
log.info("step2") | |
} | |
} |

부연 설명
- CoroutineScope에 코루틴 빌더인 launch를 통해 coroutine1 생성
- coroutine1에서 자식 코루틴인 coroutine2 생성하며 이는 coroutine1이 CoroutineScope이기 때문에 가능한 것
- job2의 join을 통해 완료될 때까지 지연
4. Coroutine Builder의 문제점과 해결 방법
4.1 launch 코루틴 빌더 문제점
- 여러 job들이 완료되어야 하는 시점이 중요할 때 job을 받아서 직접 join을 실행해야 하지만 매번 여러 job들을 join 하는 것은 쉽지 않음
- ex) job1, job2, job3은 정확히 step1 출력과 step2 출력 사이에 완료되어야 하는 케이스
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val job = CoroutineScope(EmptyCoroutineContext).launch { | |
val job1 = launch { | |
delay(100) | |
log.info("complete job1") | |
} | |
val job2 = launch { | |
delay(100) | |
log.info("complete job2") | |
} | |
val job3 = launch { | |
delay(100) | |
log.info("complete job3") | |
} | |
log.info("step1") | |
job1.join() | |
job2.join() | |
job3.join() | |
log.info("step2") | |
} | |
job.join() | |
} | |
} |
4.1.1 launch 코루틴 빌더 문제점 - 해결 방법 1
- 코루틴들을 묶는 코루틴을 생성하여 join
- job1은 3개의 코루틴을 포함하고 join을 호출하여 지연시킨 후 3개의 코루틴이 모두 완료된 후 재개
- 여전히 job1에 대한 join은 존재하지만 기존처럼 N번 join을 호출할 필요는 없어짐
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val job = CoroutineScope(EmptyCoroutineContext).launch { | |
val job1 = launch { | |
launch { | |
delay(100) | |
log.info("complete job1") | |
} | |
launch { | |
delay(100) | |
log.info("complete job2") | |
} | |
launch { | |
delay(100) | |
log.info("complete job3") | |
} | |
} | |
log.info("step1") | |
job1.join() | |
log.info("step2") | |
} | |
job.join() | |
} | |
} |

4.1.2 launch 코루틴 빌더 문제점 - 해결 방법 2
- CoroutineScope를 생성하여 step1과 step2 사이에 추가
- CoroutineScope는 동기적으로 동작
- CoroutineScope의 ScopeCoroutine이 launch의 StandaloneCoroutine을 자식으로 두기 때문에 launch들이 모두 완료되고 coroutineScope가 종료됨을 보장
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val job = CoroutineScope(EmptyCoroutineContext).launch { | |
log.info("step1") | |
coroutineScope { | |
launch { | |
delay(100) | |
log.info("complete job1") | |
} | |
launch { | |
delay(100) | |
log.info("complete job2") | |
} | |
launch { | |
delay(100) | |
log.info("complete job3") | |
} | |
} | |
log.info("step2") | |
} | |
job.join() | |
} | |
} |

부연 설명
- CoroutineScope는 외부 scope의 context를 상속하고 job을 오버라이드하기 때문에 분리된 환경에서 자식 코루틴들의 생명주기를 관리하고 자식 코루틴이 모두 완료되고 coroutineScope 함수가 완료됨을 보장
- ScopeCoroutine을 생성하여 block을 실행하기 때문에 ScopeCoroutine은 동기적으로 동작


4.2 async 코루틴 빌더 문제점
- 여러 deferred들이 완료되어야 하는 시점이 중요할 경우 모든 deferred에 await()을 호출해줘야 함
- ex) deferred 1, 2, 3은 정확히 step 1 출력과 step2 출력 사이에 반환되어야 함
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val job = CoroutineScope(EmptyCoroutineContext).launch { | |
val deferred1 = async { | |
delay(100) | |
100 | |
} | |
val deferred2 = async { | |
delay(100) | |
200 | |
} | |
val deferred3 = async { | |
delay(100) | |
300 | |
} | |
log.info("step1") | |
val result = deferred1.await() + | |
deferred2.await() + | |
deferred3.await() | |
log.info("result: {}", result) | |
log.info("step2") | |
} | |
job.join() | |
} | |
} |
4.2.1 aysnc 코루틴 빌더 문제점 - 해결 방법
- coroutineScope로 async를 감싸고 마지막에 await를 호출하여 결과를 합산할 경우 async 함수를 호출하는 순간부터 각각의 코루틴들을 시작
- coroutineScope로 감싸고 async를 실행하기 때문에 포함하고 있는 async들만 종료되고 나면 coroutineScope도 종료
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val job = CoroutineScope(EmptyCoroutineContext).launch { | |
log.info("step1") | |
val result = coroutineScope { | |
val deferred1 = async { | |
delay(100) | |
100 | |
} | |
val deferred2 = async { | |
delay(100) | |
200 | |
} | |
val deferred3 = async { | |
delay(100) | |
300 | |
} | |
deferred1.await() + | |
deferred2.await() + | |
deferred3.await() | |
} | |
log.info("result: {}", result) | |
log.info("step2") | |
} | |
job.join() | |
} | |
} |

5. Job Cancellation
- 코루틴은 기본적으로 협력적 취소(cooperative cancellation) 방식을 사용하며, 이는 코루틴이 취소 신호를 받아들여 스스로 취소되는 방식
- 코루틴에서 Job은 코루틴의 생명 주기를 관리하며 launch나 async 같은 코루틴 빌더는 Job 객체를 반환하는데, 해당 객체들을 통해 코루틴을 취소할 수 있음
- 코루틴 취소를 할 때는 Job.cancel() 메서드를 호출하면 됨
- 취소 신호는 코루틴이 실행되는 동안 협력적으로 전달되며, 코루틴이 중단 가능한 상태에서 해당 신호를 감지했을 때 취소가 완료됨
- 중단 가능한 지점은 suspend 함수 중에서 취소를 인식하고 처리할 수 있는 함수들이며 대표적인 예는 delay(), yield(), 그리고 채널 또는 플로우와 같은 비동기 스트림의 중단 함수
5.1 Root Coroutine에서 cancel을 호출하는 예제
- Root Coroutine에서 cancel을 진행하고 join을 통해 모든 코루틴이 종료될 때까지 대기
- job2, job3의 delay에서 모두 JobCancellationException 발생
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val cs = CoroutineScope(Dispatchers.Default) | |
// launch1 | |
val job1 = cs.launch { | |
// launch2 | |
launch { | |
try { | |
delay(1000) | |
log.info("job2: I'm done") | |
} catch (e: Exception) { | |
log.info("job2: I'm cancelled") | |
log.info("e2: {}", e.message) | |
} | |
} | |
// launch3 | |
launch { | |
try { | |
delay(1000) | |
log.info("job3: I'm done") | |
} catch (e: Exception) { | |
log.info("job3: I'm cancelled") | |
log.info("e3: {}", e.message) | |
} | |
} | |
delay(1000) | |
log.info("job1: I'm done") | |
} | |
delay(100) | |
job1.cancelAndJoin() | |
} | |
} |

5.2 Leaf Coroutine에서 cancel을 호출하는 예제
- 종단에 해당하는 coroutine2와 coroutine3 중에 coroutine2를 job으로 받아 cancel
- job2는 cancel 된 후 JobCancellationException 발생하지만 job3는 제대로 실행됨
- Root Coroutine도 cancelled 상태가 아닌 것을 확인 가능
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val cs = CoroutineScope(Dispatchers.Default) | |
// launch1 | |
val job = cs.launch { | |
// launch2 | |
val job2 = launch { | |
try { | |
delay(1000) | |
log.info("job2: I'm done") | |
} catch (e: Exception) { | |
log.info("job2: I'm cancelled") | |
log.info("e2: {}", e.message) | |
} | |
} | |
// launch3 | |
launch { | |
try { | |
delay(1000) | |
log.info("job3: I'm done") | |
} catch (e: Exception) { | |
log.info("job3: I'm cancelled") | |
log.info("e3: {}", e.message) | |
} | |
} | |
delay(100) | |
job2.cancel() | |
} | |
job.join() | |
log.info("job is cancelled: {}", job.isCancelled) | |
} | |
} |

5.3 Leaf Coroutine에서 예외가 발생한다면?
- coroutine3의 delay가 JobCancellationException을 던지면 Root Coroutine, CoroutineScope의 job 모두 cancelled 상태로 변경하며 이유는 Coroutine 예외 전파 규칙 내용을 참고
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
val cs = CoroutineScope(Dispatchers.Default) | |
val csJob = cs.coroutineContext[Job] | |
// launch1 | |
val job = cs.launch { | |
// launch2 | |
launch { | |
delay(100) | |
throw IllegalStateException("unexpected") | |
} | |
// launch3 | |
launch { | |
try { | |
delay(1000) | |
log.info("job3: I'm done") | |
} catch (e: Exception) { | |
log.info("job3: I'm cancelled") | |
log.info("e3: {}", e.message) | |
} | |
} | |
} | |
job.join() | |
log.info("job is cancelled: {}", job.isCancelled) | |
log.info("csJob is cancelled: {}", csJob?.isCancelled) | |
} | |
} |

비고
1. launch 코루틴 빌더 vs CoroutineScope
- launch는 비동기적으로 동작하는데 반해 coroutineScope는 동기적으로 동작
- coroutineScope는 ScopeCoroutine을 생성하지만 ContinuatinoId를 오버라이드하지 않고 launch는 새로운 ContinuationId를 생성
- 또한 coroutineScope는 결과를 반환하지만 launch는 Job을 반환
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
private val log = kLogger() | |
fun main() { | |
runBlocking { | |
suspend fun getResult(): Int { | |
delay(100) | |
return 100 | |
} | |
val job = CoroutineScope(EmptyCoroutineContext).launch { | |
log.info("context in root: {}", coroutineContext) | |
val result = coroutineScope { | |
log.info("context in coroutineScope: {}", coroutineContext) | |
getResult() | |
} | |
log.info("result: {}", result) | |
launch { | |
log.info("context in launch: {}", coroutineContext) | |
} | |
} | |
job.join() | |
} | |
} |

2. Coroutine 예외 전파 규칙
- 코루틴에서 예외는 CoroutineScope에 따라 다르게 전파
- 코루틴의 예외는 부모-자식 관계에 따라 전파되며, 자식 코루틴에서 발생한 예외는 부모 코루틴에 영향을 미침
- launch 코루틴 빌더는 결과를 반환하지 않기 때문에, 예외가 발생하면 바로 부모 코루틴으로 전파되며 만약 예외를 잡아내지 않을 경우 부모 코루틴이 예외에 의해 취소됨
- async 코루틴 빌더는 결과를 반환하는 Deferred 객체를 생성하기 때문에 예외가 발생해도 즉시 부모 코루틴으로 전파되지 않으며, await()을 호출할 때 예외가 발생
- SupervisorJob을 사용하면, 자식 코루틴의 예외가 부모에게 전파되지 않고 독립적으로 처리될 수 있음
- SupervisorJob을 사용하면 부모 코루틴이 자식 코루틴의 예외로 인해 영향을 받지 않으며, 다른 자식 코루틴들도 계속해서 실행

참고
패스트 캠퍼스 - Spring Webflux 완전 정복 : 코루틴부터 리액티브 MSA 프로젝트까지
반응형
'Kotlin' 카테고리의 다른 글
[Kotlin] CoroutineContext (0) | 2024.10.03 |
---|---|
[Kotlin] 코루틴 (Coroutine) 기초 (0) | 2024.10.02 |
[Kotlin] 코루틴 (Coroutine) 개요 (0) | 2024.10.02 |
[Kotlin] lateinit과 by lazy의 차이점 (2) | 2022.07.13 |