runBlocking을 신중하게 사용해야 하는 이유
runBlocking을 신중하게 사용해야 하는 이유
runBlocking은 코틀린에서 일반적인 블로킹 코드와 suspend 함수를 연결해 주는 코루틴 빌더입니다. 코루틴 실행이 완료될 때까지 현재 스레드를 블로킹하는 방식으로 동작하며, suspend 함수를 비(非) suspend 컨텍스트에서 호출할 때 편리해 보일 수 있지만, 블로킹 특성 때문에 안드로이드 UI 코드에서는 반드시 신중하게 접근해야 합니다. runBlocking은 비동기(asynchronous) 또는 논블로킹(non-blocking) 솔루션이 아니라, 본질적으로 동기화 메커니즘에 해당합니다. 특히 면접에서 runBlocking의 내부 동작 원리와 사용 시 주의점에 대해 자주 질문이 나오므로, 이 개념을 확실히 이해해 두시는 것이 중요합니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
runBlocking의 내부 동작 메커니즘과 호출 스레드를 블로킹하는 이유를 설명할 수 있습니다.- 안드로이드 메인 스레드에서
runBlocking을 호출하면 ANR이 발생하는 원인을 파악할 수 있습니다. - 테스트 코드 및 백그라운드 스레드 실행에서
runBlocking의 올바른 사용 사례를 설명할 수 있습니다. runBlocking중첩 호출이나 suspend 함수 내부에서의 사용과 같은 대표적인 실수 패턴을 인식할 수 있습니다.runBlocking과runTest를 코루틴 테스트 관점에서 비교할 수 있습니다.- 코루틴 컨텍스트에서
job.join()이 왜 더 나은 대안인지 설명할 수 있습니다.
runBlocking의 내부 동작 원리
runBlocking은 launch나 async 같은 다른 코루틴 빌더와 근본적으로 다릅니다. launch와 async는 코루틴을 비동기로 실행한 뒤 즉시 반환하는 반면, runBlocking은 코루틴이 완료될 때까지 호출 스레드를 점유한 채 대기합니다. 즉, 해당 스레드가 다른 작업을 전혀 수행할 수 없게 됩니다.
내부 구현을 살펴보면, Thread.currentThread()로 현재 스레드를 캡처한 뒤 BlockingCoroutine을 생성하고, 코루틴이 완료될 때까지 스레드를 루프 안에서 파킹(parking)하는 방식으로 동작합니다.
val currentThread = Thread.currentThread()
val coroutine = BlockingCoroutine<T>(
newContext, currentThread, eventLoop
)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine.joinBlocking()
joinBlocking() 내부에서는 while(true) 루프가 내부 이벤트 루프의 이벤트를 처리하면서, 각 반복 사이에 스레드를 파킹합니다. isCompleted가 true를 반환할 때까지 스레드는 블로킹 상태를 유지합니다.
fun joinBlocking(): T {
while (true) {
val parkNanos =
eventLoop?.processNextEvent() ?: Long.MAX_VALUE
if (isCompleted) break
parkNanos(this, parkNanos)
}
val state = this.state.unboxState()
(state as? CompletedExceptionally)?.let { throw it.cause }
return state as T
}
이러한 설계 때문에 runBlocking은 본질적으로 동기적이며 스레드를 블로킹합니다. 호출된 스레드는 코루틴이 완료될 때까지 다른 작업을 수행할 수 없습니다. joinBlocking() 내부의 이벤트 루프가 블로킹된 스레드로 디스패치된 코루틴 연속(continuation)을 처리하기 때문에, 같은 runBlocking 스코프 내에서의 단순한 데드락은 방지됩니다. 하지만 안드로이드 메시지 큐(message queue)의 이벤트까지 처리하지는 않는다는 점에 유의해야 합니다.
메인 스레드에서 위험한 이유
안드로이드에서 메인 스레드는 모든 UI 렌더링, 입력 이벤트, 그리고 생명주기 콜백을 처리합니다. runBlocking으로 메인 스레드를 블로킹하면 시스템이 이 모든 작업을 처리하지 못하게 되어, UI가 멈추고 블록이 수 초 이상 지속될 경우 ANR(Application Not Responding) 다이얼로그가 표시됩니다. 실제 프로덕션 환경에서 이런 문제가 발생하면 사용자 경험에 치명적인 영향을 미치므로, 메인 스레드에서의 runBlocking 사용은 반드시 피해야 합니다.