CoroutineBuilder
CoroutineBuilder는 CoroutineScope의 확장함수로, 다양한 요구사항에 맞게 개별적인 Coroutine(코루틴)을 만드는 방법입니다.
Coroutine Builder에는 여러 종류가 존재합니다. ( launch / async / withContext / runBlocking )
자세히 살펴보기 전에 각 Builder에 대해 간단히 알아보겠습니다.
1. launch() -> Job 반환
결과가 없는 코루틴을 생성하는 빌더입니다.
여기서 결과는 반환인스턴스가 아닌 결과값(Value)을 뜻하며, 반환하는 Job인스턴스는 생성된 해당 코루틴을 제어하는데 사용됩니다.
2. async() -> Deferred<T> 반환
launch와 다르게 결과를 가지는 코루틴을 생성하는 빌더입니다.
반환하는 Deferred<T>로 코루틴을 제어 / 결과를 받을 수 있는데, Deferred의 await() 함수를 통해 코루틴 영역의 마지막 라인(결과)를 받을 수 있습니다.
3. withContext() - T 반환
async처럼 결과값을 반환하는 빌더로 async와 유사하지만 다른점이 존재합니다.
async는 반환하는 Deferred<T> 객체로 결과값을 원하는 시점에 await()함수를 통해 결과값을 얻지만, withContext()는 Deferred<T>객체로 반환하지 않고, 결과(T)를 그 자리에서 반납하는 특징을 갖습니다.
( async의 Deferred<T>는 지연객체라고 생각하면 좋습니다. 내가 원하는 시점에 await()으로 결과를 받기때문입니다. - 지연 )
4. runBlocking() - T 반환
runBlocking은 Scope내의 코루틴(루틴)들이 모두 완료할 때 까지 스레드를 점유합니다.
만약 runBlocking을 사용하지 않고 Main()에서 CoroutineScope로 코루틴을 생성/실행 할 경우 스레드를 점유하지 않기 때문에, Main()은 코루틴이 실행 중임에도 함수가 끝나게 됩니다.
launch
launch는 결과가 없는 코루틴을 생성하는 빌더(함수)로, 새로운 코루틴을 실행하고, 해당 코루틴을 참조할 수 있는 Job 객체를 반환합니다.
Job을 통해 코루틴의 상태를 제어할 수 있습니다:
- job.cancel() → 코루틴 취소
- job.join() → 완료될 때까지 대기
- job.isActive → 활성화 여부 확인
// 사용 예시
// context, start인수 생략 - context인수 생략시 부모의 Dispatchers값 사용
launch { // block은 람다식으로 선언
delay(1000L) // 1초 대기
println("Sample Main Thread Running") // Sample Coroutin 출력
}
// context인수 값을 Dispatchers.IO로 설정 (백그라운드 작업)
launch(Dispatchers.IO) {
delay(1000L)
println("Sample Background Thread Running")
}
Job은 비동기 작업을 수행하지만 결과 값을 return하지 않는다는 점에서 async의 Deferred<T>와 차이가 있습니다.
그래서 Job은 뭐에요?
Job은 코루틴 Context의 Element 중 하나로, 취소가 가능한 작업 단위입니다.
생명 주기(lifecycle)를 가지고 있으며, 최종적으로 생성(New) -> 활성(Active) -> 취소(Cancel) -> 완료(completion) 상태에 도달합니다.
Job의 생명주기는 아래와 같습니다.
Job 이 생성(NEW) 되면 기본적으로 활성(ACTIVE) 상태가 됩니다. 이를 비활성 상태로 시작하고 싶다면 launch { }, async { }등의 코루틴(Job) 빌더 사용 시 CoroutineStart.LAZY 파라미터를 전달하면 해당 코루틴이 명시적으로 start() 요청을 받거나 다른 코루틴으로부터 join() 요청을 받기 전까지 시작이 지연 됩니다.
이렇게 활성 상태로 실행 중이던 Job 이 작업을 성공적으로 완료하면 완료중(COMPLETING) 상태로 진입합니다. 이 상태에서는 모든 자식 Job 들의 완료를 대기하게 됩니다. 이후 모든 자식 Job 이 정상적으로 완료되면 최종적으로 완료(COMPLETED) 상태가 됩니다.
한편, 활성 상태에서 Job 이 취소 되거나 어떤 오류로 인해 실패하게 되면 취소중(CANCELLING) 상태로 진입합니다. 또한 현재 Job 이 정상적으로 완료되어 완료중(COMPLETING) 상태로 모든 자식들의 완료를 대기하던 중 어떤 자식 Job 에서 오류가 발생하면 현재 Job(부모) 역시 취소중(CANCELLING) 상태로 진입합니다. 이후 모든 자식 Job 들이 취소 될 때까지 대기한 후 취소(CANCELLED) 상태가 됩니다.
위와 같은 생명주기를 갖기 때문에, 아래와 같이 각 상태를 조회, 제어할 수 있는 함수를 갖습니다.
그 외 자주 사용되는 Suspend함수인 Join() 등이 있습니다.
join() 함수는 Job이 완료될 때까지 코루틴을 일시 중단(suspend) 시키는 역할을 합니다.
또한, 취소 가능(cancellable)한 일시 중단 함수이기 때문에, 코루틴이 취소되었거나 이미 완료된 경우 즉시 CancellationException을 발생시킬 수 있습니다.
launch 내부코드
매개변수는 아래와 같이 구성되어 있습니다.
- context: CoroutineContext = EmptyCoroutineContext
→ 코루틴의 컨텍스트를 설정합니다. (예: Dispatcher, Job 등) - start: CoroutineStart = CoroutineStart.DEFAULT
→ 코루틴의 시작 방식을 설정합니다. (즉시 실행/지연 실행 등) - block: suspend CoroutineScope.() -> Unit
→ 실제로 실행할 suspend 함수 블록입니다.
기본 동작, Coroutinecontext, CoroutineStart 등의 역할에 대해 자세히 알아보겠습니다.
🚀 기본 동작
- 코루틴 실행: launch를 호출하면 새로운 코루틴이 즉시 실행됩니다.
- 취소: 반환된 Job 객체를 사용해 코루틴을 취소(cancel) 할 수 있습니다.
- 부모-자식 관계: 코루틴은 부모 CoroutineScope로부터 컨텍스트를 상속받으며, 부모 코루틴이 취소되면 자식 코루틴도 함께 취소됩니다.
⚡ 코루틴 컨텍스트(Context) 및 디스패처(Dispatcher)
- 컨텍스트 상속: CoroutineScope의 컨텍스트를 기본으로 상속합니다.
- 추가 컨텍스트 설정: context 파라미터를 통해 추가적인 컨텍스트 요소를 지정할 수 있습니다.
- 디스패처 기본값: 컨텍스트에 Dispatcher 또는 ContinuationInterceptor가 없다면 Dispatchers.Default가 기본으로 사용됩니다.
- 부모 작업(Parent Job): 부모 Job을 상속하지만, 필요 시 컨텍스트 요소로 직접 지정할 수 있습니다.
⏱️ 코루틴 시작 방식 (CoroutineStart)
CoroutineStart에 따라 실행 방식이 달라집니다:
- DEFAULT: 즉시 실행
- LAZY: 호출 시까지 대기
- ATOMIC: 취소 불가능한 상태로 시작
- ATOMIC 모드에서는 취소 요청이 무시됩니다
- UNDISPATCHED: 현재 스레드에서 즉시 실행
코루틴은 기본적으로 CoroutineStart.DEFAULT로 즉시 실행됩니다.
LAZY를 사용하면 어떻게 동작해요?
지연 실행 (Lazy Start): CoroutineStart.LAZY를 사용하면 코루틴이 지연 실행됩니다.
이 경우 코루틴은 대기 상태로 생성되며, start()를 호출하거나 join()으로 대기할 때 자동으로 시작됩니다.
아래 예시 코드를 통해 LAZY를 이해 해보겠습니다.
fun main() = runBlocking {
val job = launch(start = CoroutineStart.LAZY) {
println("🚀 코루틴 시작: ${Thread.currentThread().name}")
delay(1000)
println("✅ 코루틴 작업 완료")
}
println("😴 코루틴은 아직 시작되지 않음")
delay(500)
println("▶️ `start()` 호출로 코루틴 시작")
job.start() // 명시적으로 시작
job.join() // 코루틴이 끝날 때까지 대기
println("🔚 메인 종료")
}
//😴 아직 시작 안 함
//🚀 코루틴 자동 시작
//✅ 완료
//🔚 메인 종료
launch(start = CoroutineStart.LAZY)로 코루틴을 생성하지만, 즉시 실행되지 않는 것을 확인할 수 있습니다.
LAZY 모드는 필요할 때만 실행하므로 리소스 절약에 효과적인 장점이 있습니다.
만약 CoroutineStart.UNDISPATCHED + Dispatchers.IO으로 실행하면 코루틴이 어떻게 동작하나요?
CoroutineStart.UNDISPATCHED와 Dispatchers.IO를 함께 사용하면 코루틴의 초기 실행과 디스패처 전환이 독특한 방식으로 동작합니다.
초기 suspend 지점(첫 번째 suspend 함수 호출)까지는 디스패처를 무시하고 현재 스레드에서 계속 실행됩니다.
이후 suspend 지점에서 설정된 디스패처(여기서는 Dispatchers.IO)로 전환됩니다.
async
async는 코루틴을 생성하고 그 결과를 Deferred 형태로 반환하는 코루틴 빌더(함수)입니다.
이 Deferred 객체를 통해 비동기 작업의 결과를 나중에 받을 수 있습니다.
launch는 결과를 반환하지 않고 단순히 작업을 수행하기 위한 코루틴을 생성하는 반면,
async는 비동기 작업의 결과를 반환하기 위해 사용되는 차이가 있습니다.
실제로 async는 await()를 통해 결과를 비동기적으로 받아올 수 있습니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferred = async {
delay(1000)
"✅ 결과 반환 완료"
}
println("⌛ 결과를 기다리는 중...")
val result = deferred.await() // 결과를 기다림
println(result)
}
//⌛ 결과를 기다리는 중...
//✅ 결과 반환 완료
async 내부코드
async의 내부 코드는 위와 같으며, launch와 동작 방식은 동일합니다. (위에서 설명했으므로 생략)
async는 코루틴을 생성한 뒤, 결과를 반환하는 Deferred<T> 객체를 반환하여 await()을 통해 비동기 작업의 결과를 얻을 수 있다는 점이 launch와의 가장 큰 차이점입니다.
Deffered<T>는 뭐에요?
Deferred는 결과를 반환하는 비동기 작업(Future)으로, Job의 기능을 확장한 인터페이스입니다.
즉, 결과(result)를 가질 수 있는 취소 가능(cancellable)한 비동기 작업이라고 볼 수 있습니다.
Deferred는 Job을 상속하기 때문에, 동일한 상태 머신(state machine)을 따르며, 결과 조회 기능이 추가된 형태입니다.
New | 아직 시작되지 않은 상태 (CoroutineStart.LAZY 사용 시) |
Active | 결과를 계산 중인 상태 |
Completing | 완료 중이며 자식 Job의 완료를 대기 중 |
Cancelled | 취소된 상태 (취소된 Deferred도 완료된 것으로 간주됨) |
Completed | 작업이 성공적으로 완료된 상태 |
launch vs async
구분 | launch | async |
용도 | 결과가 필요 없는 작업 (부수 효과 수행) | 결과가 필요한 비동기 작업 (값 반환) |
반환값 | Job (작업 상태 관리용) | Deferred<T> (비동기 결과 관리용) |
완료 대기 방법 | join()을 호출하여 코루틴 완료 대기 | await()을 호출하여 결과를 기다림 |
결과 반환 | 결과를 반환하지 않음 (Unit 반환) | 비동기 작업의 결과를 반환 (T 타입) |
예외 처리 | 예외 발생 시 즉시 부모 코루틴으로 전파 | await() 호출 시 예외 발생 (지연된 예외 처리) |
launch와 async는 각각 언제 사용하면 좋을까?
launch
- UI 업데이트, 로그 기록, 알림 전송 등 결과가 필요 없는 작업
- 단순한 작업 완료 여부만 확인하면 충분할 때
async
- API 호출, 데이터베이스 쿼리, 계산 결과 처리 등 결과가 필요한 작업
- 병렬 처리 후 결과를 효율적으로 결합해야 할 때
launch와 async의 예외 처리에서의 차이
(1) launch의 예외 처리
fun main() = runBlocking {
val job = launch {
throw IllegalArgumentException("❌ launch 오류 발생!")
}
job.invokeOnCompletion { exception ->
if (exception != null) {
println("🚨 launch 예외 처리됨: ${exception.message}")
}
}
}
launch에서 발생한 예외는 부모 코루틴으로 즉시 전파됩니다.
invokeOnCompletion으로 예외를 감지하거나, 부모 코루틴에서 try-catch로 처리할 수 있습니다.
(2) async의 예외 처리
fun main() = runBlocking {
val deferred = async {
throw IllegalArgumentException("❌ async 오류 발생!")
}
try {
deferred.await() // 예외가 이 시점에서 발생
} catch (e: Exception) {
println("🚨 async 예외 처리됨: ${e.message}")
}
}
async의 예외는 즉시 전파되지 않고, await()를 호출할 때 발생합니다.
예외 처리를 위해 반드시 await()을 try-catch로 감싸야 합니다.
launch와 async의 병렬 처리에서의 차이
(1) launch를 사용한 병렬 처리
fun main() = runBlocking {
val time = measureTimeMillis {
val job1 = launch { delay(1000); println("🚀 작업 1 완료") }
val job2 = launch { delay(1000); println("🚀 작업 2 완료") }
job1.join()
job2.join()
}
println("⏱️ 실행 시간: $time ms")
}
결과: 약 1초 소요 (병렬 실행)
launch를 병렬로 실행했지만, 결과를 반환하지 않고 완료만 확인합니다.
(2) async를 사용한 병렬 처리
fun main() = runBlocking {
val time = measureTimeMillis {
val deferred1 = async { delay(1000); 10 }
val deferred2 = async { delay(1000); 20 }
val result = deferred1.await() + deferred2.await()
println("✅ 병렬 처리 결과: $result")
}
println("⏱️ 실행 시간: $time ms")
}
결과: 약 1초 소요 (병렬 실행)
async는 비동기적으로 실행된 두 작업의 결과를 반환하고, await()으로 결과를 받아와 합산합니다.
병렬 작업에서 결과 병합이 쉽고 직관적입니다.
withContext
withContext는 지정된 코루틴 컨텍스트로 코드를 실행하고, 해당 작업이 완료될 때까지 일시 중단(suspend)된 후 결과를 반환하는 일시 중단 함수(suspending function)입니다.
async와 차이점은 async는 결과값을 얻으려면 await()을 호출해야 하지만
withContext는 처음부터 결과 리턴까지 대기하는 형태입니다.
보통 아래와 같이 Context를 전환하고 싶을 경우 사용합니다.
- 컨텍스트 전환 (Context Switching)
- 현재 coroutineContext와 지정한 context를 병합(merge)하여 새로운 컨텍스트를 생성합니다.
- 이 과정은 coroutineContext + context를 통해 이루어집니다.
- 디스패처 전환 (Dispatcher Switching)
- 새롭게 지정된 디스패처가 있을 경우, 해당 디스패처의 스레드로 작업이 전환됩니다.
- 작업 완료 후에는 원래의 디스패처로 다시 전환됩니다.
WithContext 내부코드
suspendCoroutineUninterceptedOrReturn() 함수는 전달된 코드 블럭에서 호출 코루틴(Continuation)정보에 접근할 수 있도록 해줍니다.
코드 동작의 흐름은 다음과 같습니다.
1. 컨텍스트 병합: 기존 컨텍스트와 새로운 컨텍스트를 병합합니다.
2. 취소 상태 확인: 새로운 컨텍스트가 활성 상태인지 확인합니다.
3. 빠른 경로(FAST PATH):
- 컨텍스트 또는 디스패처가 동일하면 즉시 실행(최적화).
4. 느린 경로(SLOW PATH):
- 디스패처가 다를 경우, 새로운 스레드에서 작업을 수행합니다.
5. 작업 완료 및 결과 반환: 작업 완료 후 호출한 코루틴으로 결과를 반환합니다.
컨텍스트 병합 및 취소 상태 확인
val oldContext = uCont.context
val newContext = oldContext + context
newContext.ensureActive()
WithContext는 기존 컨텍스트(oldContext)와 새로운 컨텍스트(newContext)를 병합하고 ensureActive()를 통해 새로운 컨텍스트가 취소여부를 체크합니다.
→ 만약 코루틴이 이미 취소 상태라면 CancellationException을 즉시 발생시키고, 더 이상 작업을 진행하지 않습니다.
FAST PATH #1 — 컨텍스트가 동일한 경우
if (newContext === oldContext) {
val coroutine = ScopeCoroutine(newContext, uCont)
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
컨텍스트가 완전히 동일한 경우, 새로운 디스패처로 전환할 필요가 없으므로 추가 오버헤드 없이 바로 실행합니다.
- suspendCoroutineUninterceptedOrReturn: 현재 코루틴을 일시 중단하고, 특정 로직이 완료된 후 다시 재개할 수 있게 합니다.
FAST PATH #2 — 디스패처가 동일한 경우
if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
val coroutine = UndispatchedCoroutine(newContext, uCont)
withCoroutineContext(newContext, null) {
return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
디스패처는 동일하지만, 컨텍스트의 다른 부분이 변경된 경우 (이름, 설정 등 디스패처 외 다른 컨텍스트 요소 변경)
이 경우에도 스레드 전환 없이 실행하되, 컨텍스트만 업데이트합니다.
느린 경로 (Slow Path) — 디스패처 전환이 필요한 경우
val coroutine = DispatchedCoroutine(newContext, uCont)
block.startCoroutineCancellable(coroutine, coroutine)
coroutine.getResult()
디스패처가 변경된 경우, 새로운 디스패처로 전환해야 하므로 DispatchedCoroutine을 생성합니다.
디스패처의 전환으로 Context 스위칭이 발생하게 됩니다.
WithContext 예제
1️⃣ 디스패처 전환 예제
fun main() = runBlocking {
println("🌍 실행 스레드: ${Thread.currentThread().name}")
withContext(Dispatchers.IO) {
println("⚡ IO 컨텍스트에서 실행 중: ${Thread.currentThread().name}")
delay(1000) // 비동기 작업
}
println("🔙 다시 메인 컨텍스트로 전환: ${Thread.currentThread().name}")
}
withContext(Dispatchers.IO)를 통해 IO 스레드로 전환한 후 작업을 수행합니다.
작업이 끝나면 원래의 메인 스레드로 복귀합니다.
2️⃣ 취소 가능성 예제
fun main() = runBlocking {
val job = launch {
withContext(Dispatchers.Default) {
println("🚀 작업 시작")
delay(2000)
println("✅ 작업 완료") // 이 부분은 취소되면 실행되지 않음
}
}
delay(1000)
println("❌ 코루틴 취소 시도")
job.cancel() // 코루틴 취소
job.join()
println("🔚 메인 종료")
}
withContext로 전환된 작업이 진행 중일 때 cancel()을 호출하면, 즉시 작업이 취소됩니다.
CancellationException이 발생하고, 이후 코드는 실행되지 않습니다.
3️⃣ NonCancellable 사용 예제
fun main() = runBlocking {
val job = launch {
withContext(NonCancellable) {
println("🚀 취소 불가능한 작업 시작")
delay(2000)
println("✅ 취소되지 않고 작업 완료")
}
}
delay(1000)
println("❌ 코루틴 취소 시도")
job.cancel() // 취소 요청이 무시됨
job.join()
println("🔚 메인 종료")
}
withContext(NonCancellable)을 사용하면, 작업이 취소되지 않습니다.
cancel() 호출에도 불구하고 끝까지 작업이 수행됩니다.
NonCancellable은 갑자기 뭔가요?
NonCancellable은 CoroutineContext의 일종으로, 취소 로직을 무시하는 역할을 합니다.
withContext(NonCancellable)은 Kotlin 코루틴에서 취소가 불가능한 블록을 실행할 때 사용됩니다. 이는 주로 리소스 정리(clean-up) 또는 트랜잭션 처리 및 네트워크 연결해제 등의 작업을 처리할 때 활용됩니다.
이 경우 withContext는 일반적인 디스패처 전환과는 다르게 동작합니다.
즉, 스레드 전환이 발생하지 않으며, 취소 검사 로직이 무시됩니다.
fun main() = runBlocking {
val job = launch {
try {
delay(1000)
} finally {
withContext(NonCancellable) { // 취소 불가능한 블록
println("🗄️ 트랜잭션 롤백 중...")
database.rollback() // suspend 함수
println("✅ 롤백 완료")
}
}
}
delay(500)
println("❌ 코루틴 취소 시도")
job.cancel() // 취소 요청
job.join()
println("🔚 메인 종료")
}
//❌ 코루틴 취소 시도
//🗄️ 트랜잭션 롤백 중...
//✅ 롤백 완료
//🔚 메인 종료
job.cancel()로 취소를 요청해도 NonCancellable 블록은 끝까지 실행됩니다.
이는 내부적으로 취소 검사 로직이 무시되기 때문입니다.
그렇다면 왜 취소 검사 로직이 무시될까요?
분명 위에서는 WithContext에서 ensureActive()를 통해 취소 상태를 확인하고, 취소 상태이면 CancellationException예외를 발생시킨다고 했는데 말이죠.
이는 NonCancellable의 내부구조를 살펴보면 명확히 이해하실 수 있습니다.
Noncancellable의 isActive를 살펴보면 항상 true를 return하는 것을 확인할 수 있습니다.
그래서 withContext 내부에서 ensureActive를 호출해도 CancellationException을 발생하지 않고 항상 통과되었던 것입니다.
왜 finally{}에서 Noncancellable을 WithContext와 같이 써야하는 걸까요?
코루틴이 취소(Cancellation)될 경우, 내부적으로 CancellationException 예외가 발생합니다.
일반적으로 자원 정리(resource cleanup)나 예외 처리는 try-finally 구문을 통해 수행됩니다. 코루틴이 취소된 상황에서도 finally 블록은 반드시 실행됩니다.
그러나 이 시점에는 코루틴이 이미 취소된 상태이기 때문에, finally 블록 내부에서 suspend 함수(예: delay(), 네트워크 요청, 파일 I/O 등)를 호출하면, 해당 suspend 함수가 즉시 취소 신호를 감지하고 다시 CancellationException을 발생시킵니다. 이로 인해 정리 작업이 정상적으로 완료되지 않을 위험이 있습니다.
또한, finally 블록 내에서 새로운 코루틴을 launch로 생성하는 경우에도 문제가 발생할 수 있습니다. 새로운 코루틴은 부모의 컨텍스트를 상속하기 때문에, 부모 코루틴이 이미 취소된 상태라면 자식 코루틴 역시 즉시 취소되어 정상적으로 실행되지 않습니다.
따라서 WithContext+NonCancellable을 사용하는 것입니다.
또한, 아래 사진과 같이 내부 코드에서도 launch, async과 함께 사용 되도록 설계되지 않았다고 명시되어있다.
launch와 async에 사용하게 되면 결국 구조적 동시성을 위반하는 행위이며, 부모 자식 관계가 깨진다고 얘기하고 있습니다.
부모 코루틴이 취소 되더라도 자식 코루틴은 취소되지 않는 등의 많은 문제가 나타나며, 이는 지양해야합니다.
runBlocking
runBlocking은 Scope내의 코루틴(루틴)들이 모두 완료할 때 까지 현재 스레드를 점유하는 함수입니다.
runBlocking은 launch(), async()와 달리 CoroutineScope의 확장함수가 아니며, 항상 새로운 루트 코루틴 스코프를 생성하며, 외부의 컨텍스트를 상속받지 않고 독립적으로 동작합니다.
fun main() = runBlocking {
launch {
println("Coroutine Running")
delay(5000)
}
println("test")
}
// test
// Coroutine Running
// 5초 뒤 main() 종료
runBlocking은 delay가 끝날 때까지 메인 스레드를 차단합니다.
따라서, 자식 코루틴인 launch가 완료될 때까지 메인 스레드는 5초 동안 대기합니다.
(단, launch 대신 GlobalScope와 같이 구조적 동시성(Structured Concurrency)을 따르지 않는 경우에는 대기하지 않고 종료됩니다.)
만약 위 코드에서 runBlocking을 사용하지 않으면 어떻게 될까요?
fun main() {
GlobalScope.launch {
println("Coroutine Running")
delay(5000)
}
println("test")
}
// test
// main() 종료
대기 하지 않고 "test"만 출력되는 것을 확인할 수 있습니다.
명령어 실행순서는 분명 순차적으로 실행되는데, 왜 test가 먼저 출력되고 끝나는 걸까요?
GlobalScope로 생성된 코루틴 내부의 println("Coroutine Running")이 먼저 실행되지만 스레드 풀에서 인식하고 처리까지 시간이 살짝 걸리기 때문입니다.
즉, 코루틴은 즉시 실행되는 것이 아니라, 스케줄러에 등록된 후 스레드 풀 또는 디스패처에 의해 적절한 시점에 실행됩니다.
그것 때문에 println("test")가 먼저 출력되고 코루틴이 진행 중 임에도 main()함수는 끝나버리는 것입니다.
runBlocking 내부 코드
위 코드에서 runblocking()은 currentThread()를 통해 실행중인 스레드를 가져오고 있습니다.
그리고, context에서 디스패처(ContinuationInterceptor)를 가져와 디스 패처 유무에 따라 이벤트 루프를 생성 및 재사용 처리를 합니다.
디스패처가 없는 경우
if (contextInterceptor == null) {
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
}
ContinuationInterceptor가 없다면, 새로운 이벤트 루프를 생성합니다.
GlobalScope를 사용해 새로운 코루틴 컨텍스트를 만듭니다.
디스패처가 있는 경우
else {
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
기존의 이벤트 루프를 재사용합니다.
shouldBeProcessedFromContext()는 현재 컨텍스트에서 처리해야 하는지 여부를 판단합니다.
코루틴 객체 생성 & 시작
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
BlockingCoroutine 객체를 생성하여 코루틴의 실행을 관리합니다.
CoroutineStart.DEFAULT를 사용해 코루틴을 즉시 실행합니다. block은 실제 수행할 코루틴 코드 블록입니다.
코루틴 완료까지 스레드 스레드 차단
return coroutine.joinBlocking()
마지막으로, joinBlocking을 호출하여 코루틴이 완료될 때까지 현재 스레드를 차단합니다.
코루틴이 끝나면 결과를 반환하며, 이는 runBlocking 함수의 반환 값이 됩니다.
runBlocking은 지양해야한다고 하던데 왜 그런가요?
runblocking은 사용을 권장하지 않는 코루틴 빌더입니다.
runBlocking은 호출한 스레드를 차단(blocking)하기 때문에 이미 동작 중인 코루틴 내부에서는 이를 사용하면 데드락 위험이 있어 사용을 지양해야 한다고 코루틴 내부코드에서 명시하고 있습니다.
데드락이 어떻게 발생하는지에 대해 알고 싶다면 아래 링크를 참고하시면 좋을 것 같습니다.
https://jtm0609.tistory.com/267
[Android] runBlocking을 왜 주의해서 써야할까? (feat. Deadlock)
시작하기전에 Kotlin은 Android 개발에서 널리 사용되며, 비동기 및 논블로킹 프로그래밍을 지원하는 코루틴 기능을 제공합니다. 코루틴은 launch, async, runBlocking 같은 빌더로 시작할 수 있습니다. 이
jtm0609.tistory.com
또한, runBlocking은 사용하는 스레드는 호출된 지점의 스레드를 사용하는데 Android에서 MainThread 내부에 선언시 runBlocking은 메인스레드를 차단(점유)하기 때문에 5초이상 작업이 발생할 경우 시스템에 의해 ANR 발생이 발생하는 문제가 있습니다.
'Android > Coroutine' 카테고리의 다른 글
[Android] 코루틴(Coroutine) 예외 처리 방법 (1) | 2024.11.09 |
---|---|
[Android] 코루틴(Coroutine)의 구조적 동시성(Structured Concurrency) (0) | 2024.11.09 |
[Android] 코루틴(Coroutine) Scope에 대한 고찰 - Deep Dive (0) | 2024.11.09 |
[Android] 코루틴(Coroutine)의 Context란 ? (0) | 2024.11.09 |
[Android] 코루틴(Coroutine)의 개념 (1) | 2024.11.08 |