
코루틴은 Structured Concurrency 원칙을 따른다. Structured Concurrency이란 새로운 코루틴은 반드시 CoroutineScope 안에서만 생성 (런치)될 수 있다는 것으로 코루틴의 범위를 제한하는 메커니즘이다. 코루틴의 범위를 제한하고 코루틴의 라이프사이클을 관리하는 인터페이스는 kotlinx.coroutines.CoroutineScope이다.
Scope builder
Scope builder란 코루틴의 범위를 나타내는 인터페이스 CorotuineScope을 생성하고, 그 안에서 생성된 모든 코루틴 (자식 코루틴)이 완료될 때까지 기다리는 빌더 함수 (람다)를 말한다. 코루틴 스코프의 최상위 코루틴 (부모 코루틴)은 블로킹된다. 스코프 빌더는 크게 2가지 runBlocking {}과 coroutineScope {}이 있다.
runBlocking {} vs coroutineScope {}
두 빌더는 공통적으로 해당 스코프의 모든 코루틴이 종료될 때까지 최상위 코루틴이 블로킹된다는 점이 있다. 하지만 runBlocking {}은 현재 스레드를 블로킹하지만 coroutineScope {}은 susped funciton으로서 현재 스레드를 릴리즈한다. 아래 두 코드가 어떻게 실행되는지 확인해 보자.
// runBlocking
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
// coroutineScope
fun main() = runBlocking {
coroutineScope {
launch {
delay(1000L)
println("World!")
}
}
println("Hello,")
}
runBlocking {}을 활용한 것은 Hello, World! 로 콘솔에 찍히지만 coroutineScope {}을 사용한 밑에 main()은 World! Hello, 로 콘솔에 찍힌다. 아래 main() 함수는 아래 순서로 실행되기 때문이다.
- main() 실행, runBlocking 빌더로 코루틴 A 생성 (main 스레드 blocking)
- coroutineScope 빌더로 코루틴 스코프 생성 (여기서 코루틴 A는 중지되어 해당 스코프가 종료될 때까지 기다림)
- 해당 스코프에서 코루틴 B 생성
- 코루틴 B 1초 동안 중지 후 World! 출력
- 코루틴 B 종료, 코루틴 스코프 종료
- 코루틴 A Hello, 출력 후 종료, main() 종료
따라서 해당 main()함수가 Hello, World!로 콘솔에 나오려면 아래처럼 수정해야 한다.
fun main() = runBlocking {
coroutineScope {
launch {
delay(1000L)
println("World!")
}
println("Hello,")
}
}
차이점 1. 빌더와 동시에 코루틴 생성 여부



IntelliJ Debug 탭을 사용하면 실시간 코루틴 정보를 얻을 수 있다. 위 캡처화면은 코드의 break point 부분에서 확인한 현재 코루틴의 상황이다. 보시다시피 runBlocking은 body 실행과 동시에 새로운 해당 스코프의 자식 코루틴을 만들어 실행한다. 이는 해당 빌더 docs에서도 확인이 가능하다. 그러나 coroutineScope {} 빌더는 main 스레드를 실행하던 코루틴에서 이어서 계속 실행되는 것을 볼 수 있다.


차이점 2. 스레드 블로킹 여부
runBlocking {}을 실행한 스레드는 blocking 된다. 즉 생성된 코루틴이 스레드를 릴리스해주지 않는다. 난 이걸 바인딩된다고 표현한다. 반면 coroutineScope {} 은 단지 suspend funciton으로 정의된 body block을 실행할 뿐이다. 아래 두 메서드의 실행을 비교해 보자


- demoWithCoroutineScope을 스레드 A, 코루틴 a로 실행 blocking. A-a binding
- 스레드 A, 코루틴 a에서 자식 코루틴 a-1 생성 후 coroutineScope 진입
- coroutineScope에서 a가 delay (suspend), 스레드 A를 release
- 스레드 B, 코루틴 a의 자식 코루틴 a-2 생성 후 coroutineScope 진입
- coroutineScope에서 a-2가 delay (suspend), 스레드 B를 release
- 스레드 B, 코루틴 a의 자식 코루틴 a-3 생성 후 coroutineScope 진입
- coroutineScope에서 a-3이 delay (suspend), 스레드 B를 release.... (반복)
- 2에서 중지된 코루틴 a-1, 스레드 B에서 재개... (반복)

- demoWithCoroutineScope을 스레드 A, 코루틴 a로 실행 blocking. A-a binding
- 스레드 A, 코루틴 a에서 자식 코루틴 a-1 생성 후 runBlocking에 진입 (이때, 코루틴 a-1이 스레드 a에 바인딩되어 blocking) => 즉 자식 코루틴 a-1이 완료될 때까지 스레드 A를 release 하지 않는다.
- 스레드 B, 코루틴 a의 자식 코루틴 a-2 생성 후 runBlocking에 진입 (이때, 코루틴 a-2가 스레드 B에 바인딩되어 blocking) => 즉 자식 코루틴 a-2가 완료될 때까지 스레드 B를 release 하지 않는다.
- 스레드가 총 2개라 다음 코루틴을 론칭할 수 없음
- 2에서 중지된 코루틴 a-1, 스레드 A에서 재개, 완료 후 스레드 A release
- 3에서 중지된 코루틴 a-2, 스레드 B에서 재개, 완료 후 스레드 B release
- ... (반복)
runBlocking {}은 언제 쓰는가

그럼 runBlocking {}은 언제 쓰는가? 공식 문서에도 나와있든 runBlocking {}은 application 내부에서 극히 드물게 쓰이지만, 스레드 세상과 코루틴 세상을 최초 연결할 때는 반드시 필요한 빌더이다. 따라서 application 최초 시작 지점과 같은 곳에서 드물지만 피수적으로 쓰이게 된다.
참고
https://kotlinlang.org/docs/coroutines-basics.html
Coroutines basics | Kotlin
kotlinlang.org
https://www.baeldung.com/kotlin/coroutines-runblocking-coroutinescope
'Programming > Languages (Java, etc)' 카테고리의 다른 글
코루틴 Thread confinement 기법 (0) | 2024.10.28 |
---|---|
[Kotlin, Jackson] @JsonCreator 로 enum 값 유연하게 역직렬화 (0) | 2024.08.09 |
[Java] JDK 21 Virtual Threads 톺아보기 (1) | 2024.01.04 |
[JPA] JPA와 @Transactional (2) | 2023.12.03 |
[Spring] AOP 2편 - Spring의 AOP과 @Transactional (1) | 2023.12.03 |
댓글