
지난 시간에 이어 코루틴의 정석이라는 책을 읽으며 코루틴을 학습하려고 한다.
(구매처 : 교보문고 사이트 )

코틀린에서 코루틴을 이해하려면 코루틴 디스패처(Coroutine Dispatcher) 개념을 알아야 한다.
코루틴 자체는 스레드 위에서 동작하기 때문에 코루틴이 실제 어느 스레드에서 실행될지를 결정하는 주체가 필요하다. 바로 이 역할을 하는 것이 디스패처이다.
동작 예시
👉 쉽게 말해 “코루틴 작업 스케줄러” 역할을 한다.
스레드 vs 코루틴 참고
지난 블로그에도 나와있지만 가볍게 설명하자면
- 스레드 : 운영체제가 직접 관리하는 실행 단위. 생성/해제 비용 큼
- 코루틴 : 라이브러리 레벨에서 관리되는 경량 실행 단위. 하나의 스레드 위에서 수천개까지도 실행 가능. 코루틴이 아무리 가벼워도 결국 실행은 스레드가 한다.
디스패처는 크게 두 가지로 나뉜다.
제한된 디스패처
Dispatchers.IO(입출력 전용), Dispatchers.Default(CPU 연산 전용)무제한 디스패처
스레드 하나만 사용하는 디스패처를 만들 수도 있다.
이는 newSingleThreadContext 함수를 사용해 생성한다.
val dispatcher: CoroutineDispatcher = newSingleThreadContext(name = "SingleThread")
"SingleThread"가 된다여러 개 스레드를 동시에 사용하는 디스패처는 newFixedThreadPoolContext 함수로 만들 수 있다.
val multiThreadDispatcher: CoroutineDispatcher =
newFixedThreadPoolContext(nThreads = 2, name = "MultiThread")
MultiThread-1, MultiThread-2 식으로 붙는다newSingleThreadContext 역시 newFixedThreadPoolContext(1, name)의 구현체다코루틴 라이브러리 1.7.2 기준 newFixedThreadPoolContext는 아래처럼 구현되어 있다.
내부적으로 Executor 프레임워크를 활용해 스레드풀을 만들고 이를 CoroutineDispatcher로 래핑한다.
@DelicateCoroutinesApi
public actual fun newFixedThreadPoolContext(
nThreads: Int,
name: String
): ExecutorCoroutineDispatcher {
require(nThreads >= 1) { "Expected at least one thread, but $nThreads specified" }
val threadNo = AtomicInteger()
// Executor 프레임워크로 스레드풀 생성
val executor = Executors.newScheduledThreadPool(nThreads) { runnable ->
val t = Thread(
runnable,
if (nThreads == 1) name else name + '-' + threadNo.incrementAndGet()
)
t.isDaemon = true
t
}
return executor.asCoroutineDispatcher()
}
디스패처를 지정해 코루틴을 실행하려면 launch 함수의 context 인자로 넘기면 된다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val dispatcher = newSingleThreadContext("SingleThread")
launch(dispatcher) {
println("Running on thread: ${Thread.currentThread().name}")
}
}
실행 결과:
Running on thread: SingleThread
fun main() = runBlocking {
val multiThreadDispatcher = newFixedThreadPoolContext(2, "MultiThread")
launch(multiThreadDispatcher) {
println("[${Thread.currentThread().name}] coroutine#1 실행")
}
launch(multiThreadDispatcher) {
println("[${Thread.currentThread().name}] coroutine#2 실행")
}
}
실행 결과(예시):
[MultiThread-1 @coroutine#2] coroutine#1 실행
[MultiThread-2 @coroutine#3] coroutine#2 실행
코루틴은 구조화를 지원하기 때문에, 부모 코루틴 내부에서 자식 코루틴을 실행할 수 있다.
이때 자식 코루틴에 별도로 디스패처를 지정하지 않으면 부모 코루틴의 디스패처를 그대로 상속받는다.
fun main() = runBlocking {
val dispatcher = newFixedThreadPoolContext(2, "MultiThread")
launch(dispatcher) {
println("[${Thread.currentThread().name}] 부모 코루틴 실행")
launch {
println("[${Thread.currentThread().name}] 자식 코루틴 실행1")
}
launch {
println("[${Thread.currentThread().name}] 자식 코루틴 실행2")
}
}
}
실행 결과:
[MultiThread-1 @coroutine#2] 부모 코루틴 실행
[MultiThread-2 @coroutine#3] 자식 코루틴 실행1
[MultiThread-2 @coroutine#4] 자식 코루틴 실행2
👉 부모와 자식이 같은 디스패처 객체를 공유하기 때문에, 스레드풀 내에서 스레드가 적절히 배정된다.
실제로 앱 개발에서는 스레드풀을 직접 만들기보다는, 코틀린에서 기본 제공하는 미리 정의된 디스패처를 사용하는 경우가 대부분이다.
Dispatchers.IO → 네트워크, DB, 파일 읽기/쓰기 등 입출력 작업 전용Dispatchers.Default → 대규모 계산, 데이터 처리 등 CPU 연산 작업 전용Dispatchers.Main → UI 업데이트용, 안드로이드/JavaFX/Swing의 메인 스레드대규모 네트워크 요청, DB I/O 등에 최적화된 디스패처이다.
fun main() = runBlocking {
launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] IO 작업 실행")
}
}
출력:
[DefaultDispatcher-worker-1 @coroutine#2] IO 작업 실행
👉 Dispatchers.IO는 내부적으로 JVM 프로세서 개수와 64 중 큰 값을 기준으로 스레드를 할당하여, 동시에 많은 IO 작업을 처리할 수 있다.
fun main() = runBlocking {
launch(Dispatchers.Default) {
println("[${Thread.currentThread().name}] CPU 연산 실행")
}
}
출력:
[DefaultDispatcher-worker-2 @coroutine#2] CPU 연산 실행
👉 CPU 바운드 작업에 최적화된 디스패처로, 내부적으로 공유 스레드풀을 사용한다.
필요하면 limitedParallelism()을 사용해 일부 스레드만 제한적으로 사용하여 CPU 과부하를 막을 수 있다.
안드로이드나 데스크탑 UI 프로그래밍에서는 Dispatchers.Main이 기본으로 사용된다.
이는 UI 스레드(메인 스레드)에서 코루틴을 실행시켜 버튼 클릭, 화면 업데이트 같은 작업을 안전하게 수행할 수 있도록 해준다.
무거운 작업은 withContext(Dispatchers.IO)로 다른 Dispatcher에서 처리한 뒤,
UI 업데이트만 Main에서 실행하는 패턴이 안전하다.
suspend fun loadData() {
val data = withContext(Dispatchers.IO) { api.getData() }
withContext(Dispatchers.Main) { updateUI(data) }
}
실제로 안드로이드 앱이나 서버 애플리케이션을 개발할 때는 어떤 Dispatcher를 선택해야 할까?
네트워크 요청, 파일/DB IO 작업 → Dispatchers.IO
복잡한 계산, 데이터 변환, 알고리즘 처리 → Dispatchers.Default
UI 업데이트 → Dispatchers.Main
👉 정리하면:
“작업 성격에 맞는 디스패처를 골라 쓰는 것”이 핵심이다.
만약 잘못된 디스패처를 선택하면, UI가 멈추거나, 리소스가 불필요하게 낭비될 수 있다.