시작하기전에
Kotlin은 Android 개발에서 널리 사용되며, 비동기 및 논블로킹 프로그래밍을 지원하는 코루틴 기능을 제공합니다. 코루틴은 launch, async, runBlocking 같은 빌더로 시작할 수 있습니다.
이 중 runBlocking은 실행되는 스레드를 차단하기 때문에 Android의 메인 스레드에서 사용하면 UI 멈춤이나 ANR(Application Not Responding) 오류를 유발할 수 있습니다.
이 글에서는 runBlocking의 내부 메커니즘과 Android 개발 시 주의해야 할 사례들을 살펴보겠습니다.
runBlocking을 살펴보자
runBlocking으로 작성하고, 10초의 delay를 걸어주었습니다. 이 코드는 Android UI에서 호출된 코드입니다.
class MainViewModel : ViewModel() {
fun load() = runBlocking {
android.util.Log.w("TEMP", "Thread ${Thread.currentThread()}")
delay(10000)
}
}
무슨 일이 생길까요?
- 저 코드를 UI에서 호출했다면 UI가 멈추게 됩니다..
- UI에서 오랜 시간 응답이 없다면 ANRs(Application Not Responding)이 발생할 수 있습니다.
따라서 runBlockingAndroid UI 코드에서 데이터베이스 쿼리나 네트워크에서 데이터 불러오기와 같은 I/O 작업을 실행하는 것은 Android 개발에 상당한 위험을 초래하므로 피해야 합니다.
그렇다면 runBlocking은 내부적으로 어떻게 작동할까요?
runBlocking의 내부 구현을 살펴보면, 현재 스레드에서 새로운 코루틴을 실행하면서 GlobalScope를 활용해 코루틴 컨텍스트를 파생하는 것을 알 수 있습니다.
runBlocking은 새로운 BlockingCoroutine 인스턴스를 초기화하며, BlockingCoroutine의 내부 구현, 특히 joinBlocking 메서드를 살펴보면 이 메서드가 모든 작업이 완료될 때까지 현재 스레드를 완전히 차단(blocking)하고 점유한다는 것이 분명해집니다.
![](https://blog.kakaocdn.net/dn/QjS17/btsL5ZN0qdA/04ELZdMjXX9f6bNHjHGTVK/img.png)
![](https://blog.kakaocdn.net/dn/nnxfU/btsL6pepYVe/PTVWieFhK2j3Y1c91KlVkk/img.png)
위의 코드에서 볼 수 있듯이, BlockingCoroutine은 while (true) 무한 루프를 실행하여 현재 스레드의 이벤트 루프에서 이벤트를 지속적으로 처리(차단)합니다.
이 무한 루프는 코루틴 작업이 완료될 때 루프를 종료(차단 해제)하면서 중단됩니다. 궁극적으로 이는 현재 스레드를 차단한 상태로 유지하다가 실행된 코루틴 작업이 종료될 때까지 기다리는 동기화 작업을 의미합니다.
이러한 이유로 runBlocking을 사용할 때는 Android 메인 스레드에서의 차단을 피하기 위해 신중하게 사용해야 합니다. 메인 스레드를 차단하면 ANR(Application Not Responding)이 발생할 수 있으며, 이는 애플리케이션의 성능과 사용자 경험에 심각한 영향을 줄 수 있습니다.
Android에서 runBlocking을 사용하는 것이 문제가 되는 이유
runBlocking제공된 샘플 코드 예제를 살펴보면서 왜 안드로이드 개발에서 문제가 될 수 있는지 알아보겠습니다 .
The first example
fun sample1() = runBlocking(Dispatchers.IO) {
val currentThread = Thread.currentThread()
Log.d("tag_main", "currentThread: $currentThread")
delay(3000) // a task that takes 3 seconds
Log.d("tag_main", "job completed")
}
백그라운드 스레드에서 코루틴을 시작하기 위해 를 Dispatchers.IO. 사용으로 전환했기 때문에 모든 것이 예상대로 작동할 것 같습니다. 그러나 함수를 실행하면 아래에 표시된 출력과 유사한 로그가 생성됩니다.
![](https://blog.kakaocdn.net/dn/vFlRx/btsL53pbnSA/iucrTt5krx9x7eb3lh7mA1/img.png)
위의 로그 출력에서 알 수 있듰이 "작업 완료" 로그 메시지가 인쇄되기 까지 3초가 걸립니다.
delay(3000) 함수는 I/O 디스패처로 사용으로 워커 스레드에서 실행되지만, 메인 스레드는 차단 된 상태이기 때문에 코루틴 작업이 완료될 때까지 기다립니다.
결과적으로 전체 UI가 3초 동안 차단되어 이 시간 동안 애플리케이션이 응답하지 않게 됩니다.
runblocking에서 Dispatcher.IO를 사용해 코루틴을 다른 스레드에서 실행하더라도, 이 경우 진정한 비동기(asynchronous)동작을 달성할 수 없습니다.
The second example
runBlocking에서 Dispaters.IO대신 Dispatchers.Main을 사용하면 어떻게 될까요?
기본적으로 메인 스레드에서 작동하므로 아래 샘플코드에서는 이론적으로 예상대로 동작해야 할 것입니다.
fun sample2() = runBlocking(Dispatchers.Main) {
val currentThread = Thread.currentThread()
Log.d("tag_main", "currentThread: $currentThread")
delay(3000)
Log.d("tag_main", "job completed")
}
하지만 위의 함수를 실행하면 아래와 같은 로그 결과가 표시됩니다.
현재 스레드에 대한 로그 메시지조차 출력되지 않고 있습니다.
왜 이런 현상이 발생할까요?
우리는 runBlocking을 실행할 때 Dispatcher.Main으로 컨텍스트 전환을 시도하는 과정에서 데드락(Deadlock)이 발생했기 때문입니다.
데드락 원인 파악을 위해, 위에서 언급한 runBlocking 내부 소스 코드를 다시 살펴 보겠습니다.
runBlocking은 본질적으로 현재 스레드를 점유(차단)한 채 새로운 코루틴 스코프를 실행합니다.
이 과정에서 runBlocking은 내부적으로 이벤트 루프를 추가적으로 생성해서 관리합니다.
하지만 디스패처 유무에 따라 이벤트 루프를 관리하는 방식이 달라지게 되는데요.
만약 디스패처가 없다면 runBlocking은 새로운 코루틴 Context를 만들고, 새로운 이벤트 루프를 독립적으로 생성하여 모든 코루틴 작업을 자체적으로 처리합니다.
반면, runBloking 생성시 디스패처를 파라미터로 전달하면, 독립적인 이벤트 루프를 생성하는 대신, 해당 디스패처가 관리하는 이벤트 루프를 활용하려고 합니다.
만약 위 예제와 같이 runBlocking(Dsipatcher.Main)과 같이 호출하면, 코루틴의 컨텍스트가 메인 스레드의 이벤트 루프(Looper)로 전환될 것입니다.
이 경우 앞서 말한 결과와 같이, 데드락 현상이 발생합니다.
runBlocking은 코루틴이 완료될 때 까지 기다리게 되고, 반대로 코루틴은 자신의 작업(Task)를 실행하기 위해 메인 스레드가 이벤트 루프를 처리해주기를 기다리기 때문입니다.
하지만, 이 시점에서 메인 스레드는 이미 runBlocking에 의해 차단된 상태이기 때문에, 이벤트 루프가 더이상 동작할 수 없다는 것입니다.
결과적으로 서로가 서로의 작업 완료를 기다리는 상황(Deadlock)이 발생하면서 UI가 무한히 멈춰버리고 화면에 어떤 레이아웃도 렌더링 되지 않게 됩니다.
(만약 runBlocking(Dispatcher.Main)을 단순히 runBlocking()으로 했다면 독립적인 이벤트 루프를 가져 데드락이 발생하지는 않았을 겁니다.)
The third example
이제 다른 시나리오를 살펴보겠습니다.
runBlocking 블록이 현재 스레드를 내부적으로 차단한 이후, IO 스레드에서 실행하면 어떻게 될까요?
fun sample3() = CoroutineScope(Dispatchers.IO).launch {
// current thread is the I/O thread, so the runBlocking will block the I/O thread.
Log.d("tag_main", "currentThread: ${Thread.currentThread()}")
val result = runBlocking {
Log.d("tag_main", "currentThread: ${Thread.currentThread()}") // current thread is the I/O thread
delay(3000)
Log.d("tag_main", "job completed")
}
Log.d("tag_main", "Result: $result")
}
위 함수를 실행하면 아래와 같은 로그 메시지 결과가 표시됩니다.
runBlocking을 CoroutineScope(Dispaters.IO)를 사용하여 코루틴 스코프를 생성하면, 현재 스레드는 워커 스레드로 전환됩니다.
이 경우 runBlocking은 워커 스레드만 차단(blocking)하게 됩니다.
즉, 실행이 완료되기까지 3초의 지연 시간(delay)가 발생하더라도 이 작업은 전적으로 워커 스레드에서 처리되므로 메인 스레드는 차단되지 않고 정상적으로 동작합니다.
결과적으로, 이 방법을 사용하면 UI 멈춤 현상 없이 비동기 작업을 처리할 수 있는 방법이 됩니다.
언제 runBlocking을 사용하면 좋을까요?
Unit Test
가장 일반적인 사용 사례 중 하나는 Unit Test 코드를 실행하는 것입니다.
테스트 시나리오에서 runBlocking은 중단(suspend) 함수 또는 코루틴 기반 코드를 차단 방식으로 테스트하는데 자주 사용합니다.
private fun awaitUntil(timeoutSeconds: Long, predicate: () -> Boolean) {
runBlocking {
val timeoutMs = timeoutSeconds * 1_000
var waited = 0L
while (waited < timeoutMs) {
if (predicate()) {
return@runBlocking
}
delay(100)
waited += 100
}
throw AssertionError("Predicate was not fulfilled within ${timeoutMs}ms")
}
}
이러한 접근 방식은 코루틴 컨텍스트가 제어되어 Assertion에 대한 예층 가능한 동작을 동기식으로 보장할 수 있습니다.
동기화 및 launch()
두 번째 사용 사례는 runBlocking 작업이 I/O 스레드에서 실행될 것이라고 확신할 수 있을 때 사용합니다.
runBlocking은 코루틴 작업이 완료될 때까지 현재 스레드를 차단하기 때문에, 차단 동작이 허용되는 I/O 스레드에서 동기화된 작업을 실행하는 데 적합할 수 있습니다.
RunBlocking vs CoroutineScope
스레드 차단 여부에 따라 차이가 있습니다.
runBlocking
현재 스레드를 차단(Blocking)합니다. 주로 테스트 코드나 main() 함수에서 사용됩니다.
Android에서는 메인 스레드 차단으로 ANR을 유발할 수 있어 권장되지 않습니다.
또한, 외부의 컨텍스트를 상속받지 않고 독립적으로 동작하는 특징이 있습니다.
부모-자식 관계란 일반적으로 launch나 async처럼 부모 코루틴의 컨텍스트를 상속받아 실행되는 경우를 말합니다.하지만 runBlocking은 항상 새로운 루트 코루틴 스코프를 생성하며,외부의 컨텍스트를 상속받지 않고 독립적으로 동작합니다.
즉, runBlocking은 항상 자신만의 독립적인 컨텍스트를 관리하기 때문에
coroutineScope 내 runBlocking을 선언해도 부모-자식 관계가 성립하지 않습니다.
CoroutineScope
현재 스레드를 차단하지 않고(Non-Blocking), 일시 중단(suspending)합니다.
코루틴 내에서 안전하게 사용할 수 있으며, 비동기 코드 작성에 적합합니다.
CoroutineScope는 runBlocking과 달리, 자식 코루틴은 부모로부터 Context를 상속받으며, 부모 - 자식 관계가 성립하는 구조적 동시성(Structure Concurrency)을 보장합니다.
결론
이 글에서는 특히 안드로이드에서 runBlocking을 주의해서 사용해야 하는 이유를 살펴보았습니다. 코루틴은 최근 몇 년 동안 언어 수준에서 비동기 작업을 처리하기 위해 상당한 인기를 얻었으며, 가장 널리 채택된 도구 중 하나가 되었습니다. 그러나 프로젝트에서 효과적으로 사용하려면 정확한 역할, 내부 메커니즘 및 적절한 적용을 이해하는 것이 필수적입니다.
참고자료
https://getstream.io/blog/caution-runblocking-android/
Exercise Caution When Using runBlocking on Android
Explore the internal mechanism of runBlocking and the reasons why you exercise caution when using runBlocking.
getstream.io
https://thdev.tech/kotlin/2020/12/15/kotlin_effective_15/
Kotlin Coroutines의 runBlocking은 언제 써야 할까? 잘 알고 활용하자! |
I’m an Android Developer.
thdev.tech
[Kotlin] 코루틴이 Deadlock을 유발하는 경우
runBlocking 메소드는 사용에 있어서 크게 주의해야할 점이 하나 있는데, 이는 코틀린 공식 문서에도 언급되어있다. runBlocking 은 코루틴 내부에서 사용하지 말 것.
jaeyeong951.medium.com
'Android > Coroutine' 카테고리의 다른 글
[Android] 중단(suspend)함수를 비동기적으로 실행하는 방법도 있을까? (0) | 2025.02.05 |
---|---|
[Android] 중단(suspend)함수란 무엇이고 어떻게 동작할까? (feat. delay()) (1) | 2025.02.05 |
[Android] Coroutine 왜 스레드보다 가볍다고 할까? (0) | 2025.01.31 |
[Android] Coroutine Dispatcher에 대한 고찰 - Deep Dive (0) | 2025.01.27 |
[Android] 코루틴(Coroutine) 내부적으로 어떻게 동작할까? (0) | 2025.01.24 |