들어가며
해당 글은 도서 <코틀린 코루틴의 정석>을 기반으로 코루틴에 대해 학습하는 시리즈의 글입니다.
코틀린 코루틴 시리즈의 이전 글
[Kotlin] 멀티 스레드와 코루틴
들어가며최근 코루틴을 오랜만에 다시, 그러나 제대로 공부해보았습니다.사실 공부를 제대로 시작하기 전까지는 코루틴 사용 방법과 주요 특징만 제대로 알고 있었고, 정확한 내부 동작은 잘
walnut-dev.tistory.com
CoroutineDispatcher란?
Dispatch : 보내다, 전달하다
Dispatcher는 무언가를 전달하는 주체입니다. 즉, CoroutineDispatcher란 Coroutine을 전달하는 주체를 의미합니다.
CoroutineDispatcher는 코루틴을 스레드로 전달해줍니다. 이전 글에서 설명하였듯, 코루틴은 스레드 위에서 실행되는 중단 가능한 작업입니다. 이 코루틴을 실행할 스레드를 요청할 수 있고, 해당 요청을 받아 코루틴 작업을 스레드로 전달하는 역할을 합니다.
CoroutineDispatcher는 아래와 같은 특징을 갖습니다.
- 스레드 또는 스레드 풀을 가지고 있으며, 코루틴을 어떤 스레드에서 실행시킬지 결정합니다.
- CoroutineContext의 구성 요소입니다.
- 제한된 디스패처, 무제한 디스패처로 나뉘며, 제한된 디스패처에는 Default, IO, Main 가 있습니다.
CoroutineDispatcher 동작 방식
CoroutineDispatcher 객체는 실행되어야 하는 코루틴 작업을 저장하는 작업 대기열을 가지고 있으며, 해당 디스패처 객체가 사용할 수 있는 스레드 또는 스레드풀이 있습니다.
CoroutineDispatcher 내부 구조는 구현체에 따라 다를 수 있지만, 일반적인 CoroutineDispatcher의 구조에 따라 동작 방식을 그림으로 이해해봅니다.
아래 그림에서 CoroutineDispatcher 객체가 작업 대기열을 가지고 있고, 이 객체가 사용하는 스레드 풀에는 총 2개의 스레드가 있다고 가정해봅니다.

사용자가 네트워크 요청 등의 동작으로 작업이 요청되면, CoroutineDispatcher의 작업 대기열에 코루틴 작업이 들어옵니다.

CoroutineDispatcher는 스레드 풀 안의 사용 가능한 스레드에 작업 대기열의 코루틴 작업을 전달해 실행시킵니다.

또 다른 작업 요청이 들어오면 위와 같은 과정을 반복합니다.
만약 스레드 풀에 사용 가능한 스레드가 존재하지 않아 코루틴을 실행시킬 수 없다면, 해당 코루틴은 사용 가능한 스레드가 나타날 때까지 작업 대기열에서 대기합니다.

이렇듯 CoroutineDispatcher는 마치 이전 글에서 소개된 ExecutorService와 유사하게 동작하며, 코루틴 작업을 스레드에 전달시켜줍니다.
CoroutineDispatcher의 역할
정리하면, CoroutineDispatcher는 코루틴의 실행을 관리하는 주체입니다.
자신에게 실행 요청된 코루틴들을 작업 대기열에 적재하고, 자신이 사용할 수 있는 스레드가 새 작업을 실행할 수 있는 상태라면 스레드로 코루틴을 보내 실행될 수 있게 만드는 역할을 합니다.
단, 코루틴의 실행 옵션에 따라 작업 대기열에 적재되지 않고 즉시 실행될 수도 있고, 작업 대기열이 없는 구현체도 있는 등 예외적인 경우가 존재합니다. 이에 관한 자세한 내용은 추후에 다뤄보겠습니다.
제한된 디스패처와 무제한 디스패처
CoroutineDispatcher는 두 가지 종류가 있습니다. 하나는 제한된 디스패처(Confined Dispatcher)이고, 다른 하나는 무제한 디스패처(Unconfined Dispatcher)입니다.
제한된 디스패처(Confined Dispatcher)
사용할 수 있는 스레드나 스레드풀이 제한된 디스패처입니다.
쉽게 말하면, 작업을 실행하려는 스레드풀이 정해져있다는 의미입니다. 이를 제한되었다(Confined)고 표현한 것입니다.
위에서 동작 방식을 설명할 때 예시로 든 디스패처는 제한된 디스패처이며, 앞으로 글에서 다루거나 실제로 사용할 디스패처 대부분이 제한된 디스패처입니다.
이는 일반적으로 CoroutineDispatcher 별로 어떤 작업을 처리할지 미리 역할을 부여하고, 역할에 맞춰 실행을 요청하는 것이 효율적이기 때문입니다. 자세한 이유는 잠시 뒤에 설명드리겠습니다.
무제한 디스패처(Unconfined Dispatcher)
사용할 수 있는 스레드나 스레드풀이 제한되어있지 않은 디스패처입니다.
스레드가 제한되어있지 않다, 즉 정해져있지 않기 때문에 코루틴 작업이 어느 스레드, 스레드풀에서나 실행될 수 있습니다.
물론 그렇다고 해서 실행 요청된 코루틴이 아무 스레드에서나 무작위로 실행되는건 아닙니다. 무제한 디스패처는 실행 요청된 코루틴이 이전 코드가 실행되던 스레드에서 계속 실행되도록 합니다. 만약 이전 코드가 실행되던 스레드가 Thread-3 이라면, 실행 요청된 코루틴 작업은 Thread-3 에서 실행됩니다.
그러나 이전 코드가 특정 스레드에서만 실행된다는 보장이 없습니다. 이전 코드 또한 여러 스레드에서 실행될 가능성이 있기 때문에, 실행 요청된 코루틴이 실행되는 스레드가 매번 달라질 수 있으며, 그래서 특정 스레드로 제한되어있지 않다고 설명하는 것입니다.
이런 이유로 실행 요청한 코루틴이 실행되는 스레드를 예측하기 어렵고, 이는 코루틴 작업을 통제하기 어렵게 만들기 때문에 실제 프로덕트 환경에서는 잘 사용하지 않는다고 합니다.
이런 특성을 가졌음에도 무제한 디스패처를 사용할 수 있는 상황이 존재하는데, 이에 대한 이야기는 추후에 깊게 다뤄보겠습니다.
제한된 디스패처 사용하기
제한된 디스패처를 사용하는 방법을 살펴봅시다. 제한된 디스패처를 직접 만들어 사용하는 방법도 있고, 미리 정의된 디스패처를 사용하는 방법도 있습니다.
직접 디스패처 정의하기
코루틴 라이브러리는 개발자가 직접 제한된 디스패처를 생성할 수 있는 API를 제공해줍니다.
단일 스레드 디스패처 정의하기
사용할 수 있는 스레드가 하나인 디스패처를 의미합니다. newSingleThreadContext 함수를 이용해 만들 수 있습니다.
name 파라미터에 문자열을 넣어 디스패처가 사용하는 스레드의 이름을 지정할 수 있습니다.
fun main() = runBlocking<Unit> {
val singleThreadDispatcher: CoroutineDispatcher = newSingleThreadContext(name = "SingleThread")
launch(context = dispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
}
/*
// 결과
[SingleThread @coroutine#2] 실행
*/
멀티 스레드 디스패처 정의하기
사용할 수 있는 스레드가 2개 이상인 디스패처를 의미합니다. newFixedThreadPoolContext 함수로 멀티 스레드 디스패처를 만들 수 있습니다.
스레드의 개수(nThreads)와 스레드의 이름(name)을 매개변수로 받으며, 만들어지는 스레드는 이름 뒤에 ‘-1’, ‘-2’와 같이 숫자가 붙습니다.
fun main() = runBlocking<Unit> {
val multiThreadDispatcher: CoroutineDispatcher =
newFixedThreadPoolContext(
nThreads = 2,
name = "SingleThread",
)
launch(context = multiThreadDispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
launch(context = multiThreadDispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
}
/*
// 결과 (실행 순서는 다를 수 있습니다)
[MultiThread-1 @coroutine#2] 실행
[MultiThread-2 0coroutine#3] 실행
*/
그러나 위의 두 함수를 사용하면, IDE에서 아래와 같이 경고를 보냅니다.

이는 직접 디스패처를 생성하는 방식이 비효율적일 수 있기 때문입니다.
- 디스패처의 스레드풀에 속한 스레드의 수가 너무 적거나 많이 생성돼 비효율적으로 동작할 수 있습니다.
- 여러 개발자가 함께 개발할 경우, CoroutineDispatcher 객체가 이미 메모리상에 있음에도 해당 객체의 존재를 몰라 다시 객체를 만들어 리소스를 낭비할 수 있습니다.
스레드의 생성 비용은 비싸고 스레드를 낭비하는 것은 프로그램을 무겁게 만들 수 있기 때문에 주의해야 합니다.
미리 정의된 디스패처 사용하기
이러한 비효율을 없애기 위해, 코루틴 라이브러리는 미리 정의되어 있어 개발자가 사용할 수 있는 CoroutineDispatcher 목록을 제공해줍니다. 이를 미리 정의된 디스패처라 하며, 그 목록은 아래와 같습니다.
- Dispatchers.IO : 네트워크 요청이나 파일 입출력 등의 입출력(I/O) 작업을 위한CoroutineDispatcher
- Dispatchers.Default : CPU를 많이 사용하는 연산 작업을 위한 CoroutineDispatcher
- Dispatchers.Main : 메인 스레드(UI 스레드)를 사용하기 위한 CoroutineDispatcher
Dispatchers.IO
Dispatchers.IO는 입출력(I/O) 작업을 위해 사용되는 CoroutineDispatcher 객체입니다.
멀티 스레드 프로그래밍이 가장 많이 사용되는 작업은 입출력 작업입니다. 애플리케이션에서는 네트워크 통신을 위한 HTTP 요청이나 DB 작업 등 다수의 입출력 작업을 동시에 수행하는 경우가 많기 때문에 여러 개의 스레드가 필요합니다. 코루틴 라이브러리는 입출력 작업에 사용할 수 있도록 미리 정의된 Dispatchers.IO를 제공해줍니다.
Dispatcher.IO의 특징
- 네트워크, DB 작업 등의 입출력(IO) 작업을 할 때 사용합니다.
- Dispatchers.IO는 싱글톤 인스턴스입니다.
- 코루틴 라이브러리 1.7.2를 기준으로, Dispatchers.IO가 최대 사용 가능한 스레드의 개수는 JVM에서 사용 가능한 프로세서의 수와 64 중 더 큰 값으로 설정됩니다.
- I/O 작업으로 인한 블로킹 현상을 줄이고자, 필요 시 스레드를 새로 생성해 요청된 작업을 진행합니다.
- 블로킹 I/O 대기를 감안하여 스레드 개수의 한도를 크게 늘렸기 때문에, I/O 대기 동안 스레드가 묶여도 전체가 멈추지 않도록 설계되었습니다.
입출력 작업은 기본적으로 블로킹으로 인해 대기가 발생하는 작업입니다. 작업의 결과가 필요하기에 작업이 완료될 때까지 대기해야 합니다. 블로킹으로 인해 입출력 작업을 진행하는 스레드가 묶이게 되는데, 만약 입출력 작업이 너무 많아서 모든 스레드가 블로킹되어 사용할 수 있는 스레드가 사라지면 안될 것입니다.
IO 디스패처는 이런 멈춤 현상을 방지하도록 내부적으로 설계되어있습니다. 필요한 경우(사용 가능한 스레드의 수가 부족해질 경우) 스레드를 새로 생성하여 작업을 진행합니다. 그래서 새로 생성한 스레드가 입출력 작업을 수행해 블로킹되더라도 기존의 스레드는 사용 가능한 상태로 남겨놓아 다른 작업을 수행할 수 있도록 합니다.
아래는 Dispatchers.IO를 사용하는 예제입니다.
fun main() = runBlocking<Unit> {
launch(Dispatchers.I0) { // 싱글톤 객체이므로 바로 호출할 수 있습니다.
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
/*
// 결과
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
*/
Dispatchers.Default
Dispatchers.Default는 CPU 작업을 위해 사용되는 CoroutineDispatcher 객체입니다.
대용량 데이터를 처리하는 등의 연산 작업은 CPU를 사용하는 작업이며, 이를 CPU Bound 작업이라고 부릅니다. Dispatchers.Default는 이러한 작업들을 수행할 때 사용하는 CoroutineDispatcher입니다.
Dispatcher.Default의 특징
- 대용량 데이터 처리, 정렬, 해싱, 파싱 등 무거운 연산 작업(CPU Bound 작업)을 할 때 사용합니다.
- Dispatcher.Default도 싱글톤 인스턴스입니다.
- CPU 코어 개수만큼 스레드를 생성합니다. (병렬성 최적화)
- limitedParallelism(n)으로 동시에 처리할 스레드의 개수를 제어할 수 있습니다. (풀을 새로 만들지 않고 공유 스레드풀 내에 스레드를 새로 만듦)
아래는 Dispatchers.Default를 사용하는 예제입니다.
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default) { // 싱글톤 객체이므로 바로 호출할 수 있습니다.
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
/*
// 결과
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
*/
그런데 실행 결과를 잘 보시면, 코루틴이 실행된 스레드의 이름이 “DefaultDispatcher-worker-1”로 Dispatchers.IO를 사용했을 때의 스레드 이름과 동일한 것을 알 수 있습니다.
이는 두 디스패처가 코루틴 라이브러리가 제공하는 공용 스레드풀을 함께 사용하고 있기 때문입니다.
공용 스레드풀을 사용하는 IO와 Defaults
코루틴 라이브러리는 여러 개의 작업을 동시에 실행할 수 있는 공유 스레드풀을 제공해줍니다. 이 공유 스레드풀은 Dispatchers.Default와 Dispatchers.IO가 함께 사용합니다. 공유 스레드풀에서 생성된 스레드 개수는 정해져있으나, 언제든 개수를 제한 없이 늘일 수 있습니다.
두 디스패처가 공유 스레드풀을 함께 사용하고 관리하기 때문에, 각각의 디스패처를 사용 시 주의 사항이 있습니다.
Dispatchers.IO에서 CPU 작업을 올리지 않기
- Dispatchers.IO는 앞서 설명했듯 스레드 블로킹 현상으로 인해 전체가 멈추지 않도록, 필요 시 스레드를 새로 생성해 작업을 수행합니다.
- 만약 Dispatchers.IO에서 CPU 작업을 수행하면, 새로운 스레드를 생성해 CPU 작업을 진행할 수 있습니다.
- CPU 작업이 많이 늘어나면 각각의 스레드가 CPU를 점유하기 위해 Context Switching이 자주 발생하게 되며, 이는 성능에 악영향을 끼칠 수 있습니다.
- 따라서 CPU 작업은 반드시 Default 디스패처에서 수행해야합니다.
Dispatchers.Default에서 I/O 작업을 올리지 않기
- Dispatchers.Default에서 블로킹이 발생할 수 있는 입출력(I/O) 작업을 수행하면, 공유 스레드풀에서 사용 가능한 스레드가 없어질 수 있습니다.
- 결국 모든 스레드가 블로킹되어, 다른 작업을 수행하지 못해 지연 및 빈곤 현상이 발생할 위험이 생깁니다.
- 따라서 I/O 작업은 반드시 IO 디스패처에서 수행해야합니다.
Dispatchers.Main
해당 디스패처는 메인 스레드, 즉 UI 스레드가 있는 UI 애플리케이션에서 사용하는 특별한 디스패처입니다. 그래서 'kotlinx-coroutines-android' 와 같이 별도의 라이브러리가 있어야만 해당 디스패처를 사용할 수 있습니다.
Dispatchers.Main은 메인 스레드에서 UI 관련 작업을 수행할 때 사용하는 디스패처입니다.
Dispatchers.Main의 특징
- UI를 업데이트하는 작업을 할 때 사용합니다.
- UI 환경이 없는 곳에서는 해당 디스패처 사용 시 예외 발생할 수 있습니다.
사용 시 주의 사항으로는, 해당 디스패처를 사용해 무거운 연산 작업 및 입출력과 같은 블로킹 작업을 호출하지 않아야 합니다. 해당 작업으로 인해 메인 스레드가 지연/중단되어 UI가 멈출 수 있기 때문입니다.
마치며
지금까지 CoroutineDispatcher에 대해 살펴보았습니다. 실제 CoroutineDispatcher 구현체의 동작 방식이나 무제한 디스패처의 종류 및 활용법, 미리 정의된 디스패처에 대한 더 자세한 설명 등 더 학습해볼 내용이 많으니, 더 깊이있는 내용은 추후에 다뤄보겠습니다.
코루틴 시리즈의 다음 글에서는 코루틴 빌더와 Job에 대해 알아보겠습니다.
'개발 > Kotlin' 카테고리의 다른 글
| [Kotlin] 멀티 스레드와 코루틴 (0) | 2025.09.15 |
|---|