코루틴 디스패처: IO vs Main
코루틴 디스패처: IO vs Main
코틀린 코루틴은 코루틴이 무엇을 하는지와 어디에서 실행되는지를 분리합니다. 실행 위치를 결정하는 것이 바로 코루틴 디스패처(coroutine dispatcher)이며, 디스패처는 코루틴의 Continuation을 적절한 스레드에 할당하는 역할을 합니다. 안드로이드 개발에서 가장 빈번하게 사용되는 디스패처는 두 가지입니다. Dispatchers.Main은 단일 UI 스레드에서 코루틴을 실행하며, Dispatchers.IO는 블로킹 작업에 최적화된 공유 백그라운드 스레드 풀에 작업을 분배합니다. 적절하지 않은 디스패처를 선택하면 눈에 띄지 않는 프레임 드롭부터 시스템 수준의 ANR 다이얼로그까지 다양한 문제가 발생할 수 있습니다. 각 디스패처의 스레딩 모델, 디스패처 간 전환 메커니즘, 그리고 스레드 풀 구성의 설계 철학을 이해하는 것은 정확하고 성능이 우수한 동시성 코드를 작성하기 위한 필수 조건입니다. 면접에서도 두 디스패처의 차이와 올바른 활용법에 대한 질문이 자주 등장하므로, 이 부분을 깊이 학습해 두시면 큰 도움이 됩니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
Dispatchers.Main의 스레딩 모델과 단일 스레드로 제한되는 이유Dispatchers.IO가 스레드 풀을 관리하는 방식과Dispatchers.Default와 스레드를 공유하는 원리withContext를 사용하여 디스패처 간 전환할 때 코루틴의 실행 흐름- 메인 스레드에서 블로킹 I/O를 실행했을 때 발생하는 문제점(프레임 드롭, ANR 등)
limitedParallelism을 활용하여 특정 리소스에 대한 동시성을 제한하는 디스패처 뷰를 생성하는 방법
Dispatchers.Main: 단일 UI 스레드
Dispatchers.Main은 코루틴 실행을 애플리케이션의 메인 스레드에 한정합니다. 안드로이드에서 메인 스레드는 프로세스가 시작될 때 시스템이 생성하는 스레드이며, Activity 생명주기 콜백, 입력 이벤트, measure-layout-draw 과정, 그리고 Choreographer 프레임 콜백을 모두 처리하는 스레드이기도 합니다. 프로세스당 메인 스레드는 정확히 하나만 존재하며, 안드로이드 프레임워크는 메인 스레드가 아닌 스레드에서 뷰 계층 구조를 수정하려고 하면 CalledFromWrongThreadException을 던져 이 제약을 강제합니다.
안드로이드의 메인 디스패처는 kotlinx-coroutines-android 아티팩트가 제공하며, 내부적으로 안드로이드 Looper와 Handler를 기반으로 한 MainCoroutineDispatcher를 등록합니다. 코루틴이 Dispatchers.Main으로 디스패치되면, 디스패처는 Continuation을 Runnable로 감싸 메인 스레드의 Handler에 포스팅합니다.
// 내부 동작을 단순화한 코드
internal class HandlerDispatcher(
private val handler: Handler
) : MainCoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block) // Continuation을 메인 스레드 메시지 큐에 추가
}
}
안드로이드에서는 Dispatchers.Main.immediate도 제공되며, viewModelScope와 lifecycleScope가 기본적으로 이를 사용합니다. 둘의 차이는 다음과 같습니다. Dispatchers.Main은 이미 메인 스레드에서 실행 중이더라도 항상 핸들러 큐에 포스팅하여 다음 루프 반복에서 실행합니다. 반면 Main.immediate는 현재 스레드가 메인 스레드인지 먼저 확인하고, 이미 메인 스레드라면 인라인으로 즉시 실행하여 한 프레임의 지연을 제거합니다.
// viewModelScope는 기본적으로 Main.immediate를 사용
val viewModelScope = CoroutineScope(
SupervisorJob() + Dispatchers.Main.immediate
)
메인 디스패처의 핵심 제약은 단일 스레드로 구성되어 있다는 점입니다. 디스패치된 모든 작업은 순차적으로 실행됩니다. Dispatchers.Main에서 실행 중인 코루틴이 Thread.sleep, 동기 네트워크 호출, 또는 오래 걸리는 연산 등으로 스레드를 블로킹하면, 메인 스레드 큐에 대기 중인 다른 모든 작업이 지연됩니다. 프레임 렌더링, 터치 이벤트 처리, 애니메이션 콜백 등이 모두 블로킹 호출이 반환될 때까지 멈추게 됩니다. 이러한 원리를 정확히 이해해 두시면 면접에서 "왜 메인 스레드에서 네트워크 호출을 하면 안 되는가"라는 질문에 근본적인 수준에서 답변하실 수 있습니다.
Dispatchers.IO: 블로킹 작업에 최적화된 스레드 풀
Dispatchers.IO는 호출 스레드를 블로킹하면서 외부 리소스를 기다리는 작업, 즉 네트워크 응답, 파일 시스템 읽기, 데이터베이스 쿼리, 프로세스 간 통신(IPC) 등을 위해 설계되었습니다. 이러한 작업은 대부분의 시간을 연산이 아닌 대기에 소비하므로, 스레드가 점유되어 있지만 실제로는 유휴 상태입니다. IO 디스패처는 CPU 바운드 작업에 적합한 것보다 더 큰 스레드 풀을 유지하여 이 특성에 대응합니다.
내부적으로 Dispatchers.IO는 Dispatchers.Default와 동일한 CoroutineScheduler 스레드 풀을 공유합니다. 두 디스패처의 차이는 병렬성 한도에 있습니다. Dispatchers.Default는 동시 실행 코루틴 수를 CPU 코어 수(최소 2개)로 제한하여 CPU 바운드 작업이 프로세서를 과점유하는 것을 방지합니다. Dispatchers.IO는 기본적으로 최대 64개(또는 CPU 코어 수 중 더 큰 값)의 동시 코루틴을 허용하는데, 대부분의 코루틴이 I/O 대기 중 블로킹 상태에 있어 실제 CPU 시간을 소비하지 않기 때문입니다.