runBlocking 함수를 호출하면 (부모 Job이 없는) 루트 Job 객체가 생성된다.
fun main() = runBlocking<Unit> {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
launch(CoroutineName("Coroutine2")) {
launch(CoroutineName("Coroutine5")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
}

이런 식으로 runBlocking을 루트 Job으로 지정할 수 있다.
코루틴의 구조화를 깨는 방법은 여러가지가 있다.
실무에서 코루틴의 구조화를 일부러 깨는 일은 없지만,
구조화를 깨는 행위가 무엇인지 알아둠으로써 실수로 구조화를 깨는 것을 방지하기 위한 목적으로 학습하자.
우선 CoroutineScope 함수를 사용하면 구조화를 깰 수 있다.
CoroutineScope 생성 함수는 새로운 루트 Job을 가진 CoroutineContext를 생성한다.

fun main() = runBlocking<Unit> { // 루트 Job 생성
val newScope = CoroutineScope(Dispatchers.IO) // 새로운 루트 Job 생성
newScope.launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
newScope.launch(CoroutineName("Coroutine2")) {
launch(CoroutineName("Coroutine5")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}

실행 결과를 보면, 코루틴3,4,5가 코루틴을 실행했다고 출력되었음을 알 수 있다.
이 코드는 runBlocking 코루틴을 부모 Job으로 두지 않고, CoroutineScope()를 사용하여 새로운 루트 Job을 생성한 것이다. 즉, 코루틴 구조화가 깨지게 된 것이다. (newScope 코루틴이 runBlocking 코루틴을 상속하지 않기때문)
두번째 방법은 Job 객체를 생성하는 것이다.
Job 생성 함수로 새로운 루트 Job을 생성할 수 있다.

fun main() = runBlocking<Unit> {
val newRootJob = Job() // 루트 Job 생성
launch(CoroutineName("Coroutine1") + newRootJob) {
launch(CoroutineName("Coroutine3")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine4")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
launch(CoroutineName("Coroutine2") + newRootJob) {
launch(CoroutineName("Coroutine5")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
newRootJob.cancel() // newRootJob 취소
delay(1000L)
}
launch(CoroutineName("Coroutine2") + newRootJob) -> Job() 함수를 통해 새로운 루트 Job을 만들었다.
newRootJob을 취소하면, 자식 코루틴들에게 모두 취소가 전파된다.
따라서 아무런 출력값이 없다.
이번 예제에서는 Job()을 통해 runBlocking 코루틴과 분리된 새로운 Job을 생성하였다.
그렇다면 구조화된 구조에서, 구조화를 깨서 일부 코루틴만 취소되지 않도록 만들 수도 있다.
// 변경 전
launch(CoroutineName("Coroutine5"))
// 변경 후
launch(CoroutineName("Coroutine5") + Job())

기존 코드에 Job을 추가했다. 이것은 이 코루틴이 새로운 부모 Job 객체를 갖는다는 뜻이다.
따라서 Coroutine2와의 연결이 끊어지게 되는 것이다.
Job 생성함수는 부모 Job(parent)를 인자로 받을 수 있다.
public fun Job(parent: Job? = null): CompletableJob = JobImpl(parent)
Job 생성 함수에 parent 인자가 입력되지 않으면 parent가 null이 돼 완전히 새로운 루트 Job이 생성된다.
launch(CoroutineName("Coroutine2") + newJob): 새로운 루트 Job을 만드는 val newJob을 매개변수로 하여 Coroutine2를 생성했다.

이번에는 구조화가 깨지지 않게 코드를 만들어보았다.
하지만 한가지 문제점이 있는데, 이 코드는 실행이 완료되지 않는다.
즉, 어떤 누군가(코루틴)가 메인 스레드를 계속 잡고있는 것이다.
범인은 newJob이다. Coroutine2는 종료됐지만, Job() 생성 함수로 통해 만들어진 Job은 수동으로 종료를 해주어야 한다.

newJob.complete: complete() 함수는 Job 객체를 실행 종료하는 함수다.
아마 runBlocking 함수를 사용하는 것을 지양해야 한다는 얘기를 들은적이 있을 것이다.
그 이유는 바로 호출부의 스레드(실행하고 있는 현재의 스레드)를 사용하는 방식 때문이다.
runBlocking 함수는 기본적으로 호출부의 스레드를 차단하고, 배타적으로 사용하는 코루틴을 만들어야 한다. 이때 호출부의 스레드를 사용할 수 있는 코루틴은 자기자신 코루틴과 자식 코루틴들 뿐이다.
즉, 호출부의 스레드는 runBlocking 함수가 만든 코루틴이 실행 완료될때까지 다른 작업에 사용될 수 없다.
말이 어려운데 그냥 runBlocking 함수를 실행하면 실행 중인 스레드는 자기자신 스레드와 자식 스레드 빼고 사용할 수 없게 차단한다는 뜻이다.

runBlocking 코루틴이 시작되면 5초 동안 runBlocking 코루틴 (자식 코루틴은 없으므로 예외)을 제외한 나머지 요소들은, runBlocking 코루틴을 호출한 main 스레드를 사용할 수 없다.

runBlocking 코루틴은 launch 코루틴이라는 자식 코루틴이 있다.
runBlocking을 호출했음에도 불구하고, launch 코루틴은 메인 스레드에서 실행이 차단되지 않는다.
아까 얘기했듯이 runBlocking 코루틴의 자식 코루틴은 호출부의 스레드에 차단되지 않기 때문이다.
결론적으로 강조하고자 하는 점은 크게 두 가지다.
// 안 좋은 예
fun someFunction() {
runBlocking {
// 스레드 차단됨
delay(1000)
}
}
// 권장되는 방식
fun someFunction() {
CoroutineScope(Dispatchers.IO).launch {
// 스레드 차단 없이 비동기 실행
delay(1000)
}
}
결론적으로 runBlocking이 "나쁘다"기보다는...
이런 특수한 상황 외에는 사용을 피해야하는 것이 핵심이다.
대부분의 실제 애플리케이션에서는 launch, async 등의 다른 코루틴 빌더를 사용하는 것이 더 적절하다.
• 구조화된 동시성의 원칙이란 비동기 작업을 구조화함으로써 비동기 프로그래밍을 보다 안정적이고 예측할 수 있게 만드는 원칙이다.
• 부모 코루틴은 자식 코루틴에게 실행 환경을 상속한다.
• 코루틴 빌더 함수의 인자로 CoroutineContext가 전달되면 부모 코루틴의 실행 환경을 덮어 씌울 수 있다.
• 코루틴 빌더 함수 호출시 Job은 상속되지 않고, 새롭게 생성된다. 이때 기존의 Job은 새롭게 생성되는 Job의 부모가 된다.
• 코루틴에 취소가 요청되면, 자식 코루틴에 취소가 전파된다.
• 부모 코루틴은 자식 코루틴이 완료될 때까지 완료되지 않는다.
• 부모 코루틴이 실행해야 할 코드가 모두 실행됐는데 자식 코루틴이 실행 중이라면, 부모 코루틴은 ‘실행 완료 중‘ 상태를 가진다
• CoroutineScope 인터페이스는 CoroutineContext를 가진 인터페이스이다.
• launch나 async 함수가 호출될 때 CoroutineScope으로부터 실행 환경을 상속 받아 코루틴이 실행된다.
• 실행 환경 상속의 원리이다.
• CoroutineScope 객체를 사용해 특정 범위의 코루틴을 제어할 수 있다.
• cancel 함수를 호출하면 CoroutineScope의 범위에 속한 모든 코루틴을 취소할 수 있다.
• CoroutineScope의 cancel 함수를 호출하는 것은 실제로 CoroutineScope이 가진 Job 객체에 cancel 함수를 호출하는 것이다.
• Job의 구조화가 제어에 핵심 역할을 한다.
• CoroutineScope 생성 함수나, Job 생성 함수를 사용해 코루틴의 구조화를 깰 수 있다.
• Job 생성 함수에 부모 Job을 설정할 수 있다. 이를 사용해 구조화를 깨지 않을 수도 있다.
• Job 생성 함수로 생성된 Job은 자동으로 실행 완료되지 않는다. complete 함수를 명시적으로 호출해 줘야 한다.
• runBlocking 함수를 실행하면 실행 중인 스레드는 자기자신 스레드와 자식 스레드 빼고 사용할 수 없게 차단한다.
• runBlocking 함수는 테스트 코드, 최상위 main 함수, 특별히 의도적으로 스레드를 차단해야 하는 경우 빼고 사용을 지양하는 것이 좋다 -> launch, async await 사용
강의자료 출처 및 참고한 강의:
코틀린 코루틴 완전 정복 by 조세영님
https://www.inflearn.com/course/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5/dashboard