안녕하세요. 오랜만에 돌아왔습니다.
오늘은 코틀린 코루틴의 정석 - CH3를 읽고 정리한 글입니다.
📌 3장에서 다루는 내용
1. CoroutineDispatcher 객체의 역할
2. 제한된 디스패처와 무제한 디스패처의 차이
3. 제한된 디스패처 생성하기
4. CoroutineDispatcher 사용해 코루틴 실행하기
5. 코루틴 라이브러리에 미리 정의된 디스패처의 종류와 사용처
3.1 CoroutineDispatcher 객체의 역할
CoroutineDispatcher는 Coroutine + Dispatcher입니다.
그럼 코루틴은 알겠고, Dispatcher은 뭘까요?
Dispatcher는 Dispatch(보내다) + -er 로 무언가를 보내는 주체를 뜻합니다.
그렇다면 CoroutineDispatcher는 코루틴을 보내는 주체겠죠?
그럼 이 코루틴을 어디로 보낼까요?
바로 스레드입니다.
코루틴은 일시 중단이 가능한 ‘작업’으로, 스레드가 있어야 실행이 가능하죠.
따라서 CoroutineDispatcher는 코루틴을 스레드로 보내 실행시키는 역할로, 가지고 있는 스레드나 스레드풀로 코루틴 실행을 요청한 스레드에 코루틴을 실행하게 해줍니다.
3.1.1 CoroutineDispatcher 동작
그럼 동작은 어떻게 할까요?
우선 2개의 스레드로 구성된 스레드풀을 사용할 수 있는 CoroutineDispatcher 객체가 있다고 가정해봅시다.

그럼 위의 이미지와 같이 그려집니다.
CoroutineDispatcher 객체는 실행돼야 하는 작업을 저장하는 작업대기열을 가지며,
2개의 스레드 중 하나에는 이미 코루틴이 실행 중이죠.
이때 이미 실행 중인 Coroutine1이라는 코루틴과는 다른 Coroutine2라는 코루틴이 들어와 실행 요청을 하면 어떻게 될까요?

우선 작업 대기열에 적재합니다.
그 후 CoroutineDispatcher 객체는 자신이 사용할 수 있는 스레드가 있는지 확인해요.
이때 있으면 바로 해당 스레드에 보내는데,
없으면(= 스레드가 모두 점유되어 사용할 수 있는 스레드가 없음) 작업 대기열에 대기하게 합니다.
그러다 자리 생기면 보내죠.
3.1.2 CoroutineDispatcher 역할
한 마디로, CoroutineDispatcher는 코루틴 실행을 관리하는 주체인 겁니다.
자신에게 실행 요청된 코루틴들을 작업 대기열에 적재하고, 스레드로 보내 실행하니까요.
3.2 제한된 디스패처와 무제한 디스패처의 차이
CoroutineDispatcher에는 두 가지 종류가 있습니다.
- 제한된 디스패처 Confined Dispatcher
- 사용 가능한 스레드, 스레드풀이 제한적인 디스패처
- 무제한 디스패처 Unconfined Dispatcher
- 사용 가능한 스레드, 스레드풀이 무제한인 디스패처
아까 앞에서 다룬 예시에서의 스레드풀에는 스레드가 2개 있었죠? 그런 걸 제한된 디스패처라고 합니다.
⭐️ 일반적으로 CoroutineDispatcher 객체별로 어떤 작업을 처리할지 미리 역할을 부여합니다.
그 역할에 맞춰 실행을 요청하는 것이 효율적이기 때문이에요.
따라서 대부분의 CoroutineDispatcher 객체가 제한된 디스패처입니다.
미리 역할을 부여한다는게 뭐지?
예를 들어볼게요.
입출력(I/O) 작업이면 입출력 작업용 CoroutineDispatcher 객체에 실행을 요청하고,
CPU 연산 작업이면 CPU 연산 작업용 객체에 실행을 요청하는 겁니다.
무제한 디스패처
제한이 없다고해서 코루틴이 아무 스레드에서나 실행되는건 아니에요.
이 코루틴이 이전 코드가 실행되던 스레드에서 계속해서 실행되도록 해요.
때문에 실행되는 스레드가 매번 달라질 수 있고, 특정 스레드로 제한되지 않아 무제한 디스패처라는 이름을 가지게 된겁니다.
이 무제한 디스패처는 이후 CH11에서 다루게 될 예정이니 이렇게 간단히만 알아볼게요.
3.3 제한된 디스패처 생성하기
코루틴 실행시킬 때 보낼 수 있는 스레드가 제한된 CoroutineDispatcher 객체입니다.
코루틴 라이브러리가 사용자가 직접 제한된 디스패처를 만들 수 있도록 함수를 제공해요.
이것들로 제한된 디스패처를 생성합니다.
3.3.1 단일 스레드 디스패처
단일 스레드 디스패처(Single-Thread Dispatcher)가 뭘까요?
사용할 수 있는 스레드가 하나인 CoroutineDispatcher 객체입니다.
코루틴 라이브러리에서 제공하는 newSingleThreadContext 함수로 만들어내죠.
val dispatcher: CoroutineDispatcher = newSingleThreadContext(name = "SingleThread")
문자열 타입 name을 인자로 받습니다. 여기서 name은 디스패처에서 관리하는 스레드명이죠.

3.3.2 멀티 스레드 디스패처
그럼 멀티 스레드 디스패처란?
사용할 수 있는 스레드가 2개 이상인 CoroutineDispatcher겠죠.
이 디스패처를 만들기 위해서는 newFixedThreadPoolContext 함수를 사용합니다.
val multiThreadDispatcher: CoroutineDispatcher = newFixedThreadPoolContext(
nThreads = 2,
name = "MultiThread"
)
여기서 nThreads는 스레드의 개수를, name은 스레드명을 의미합니다. 이름 뒤에 각각 -1부터 숫자 하나씩 증가해요.

newSingleThreadPoolContext로 만들어진거랑 비슷하죠?
당연합니다.
newSingleThreadPoolContext가 내부적으로 newFixedThreadPoolContext를 사용하고 있기 때문이에요.
public fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher =
newFixedThreadPoolContext (1, name)
코드를 보시면 지금 newFixedThreadPoolContext의 개수에 1을 넣어서 사용하고 있어요.
사실상 같은 함수라고 볼 수 있습니다.
그러니 비슷할 수 밖에 !
3.4 CoroutineDispatcher 사용해 코루틴 실행하기
그럼 이제 CoroutineDispatcher를 사용해서 코루틴을 실행해봅시다.
3.4.1 launch 파라미터로 CoroutineDispatcher 사용하기
3.4.1.1 단일 스레드 디스패처 사용해 코루틴 실행하기
launch 함수로 코루틴을 만듭니다 → 그 다음 context 인자로 CoroutineDispatcher 객체를 넘겨요.
fun main() = runBlocking<Unit> {
val dispatcher = newSingleThreadContext(name = "SingleThread")
launch (context = dispatcher) {
println("[${Thread. currentThread() .name}] ₫ " )
}
}
/*
/ / 결 과 :
[SingleThread@coroutine#2] 실행
*/
SingleThread라는 이름을 가진 코루틴 실행이 찍혔죠? 잘 생성되어 실행되었다는 걸 보여줍니다.
+) 여기서 launch 함수의 첫 인자가 context라 context를 명시적으로 사용하지 않아도 됩니다.
3.4.1.2 멀티 스레드 디스패처 사용해 코루틴 실행하기
단일 스레드 디스패처 사용 방식과 동일해요.
fun main() = runBlocking<Unit>{
val multiThreadDispatcher = newFixedThreadPoolContext(
Threads = 2,
name = "MultiThread"
)
launch(context = multiThreadDispatcher) {
println("[${Thread. currentThread().name}] 실행")
}
launch(context = multiThreadDispatcher) {
println("[${Thread. currentThread().name}] 실행")
}
}


스레드 2개에 각각 코루틴이 할당되어 실행됩니다.
+) 실행 환경에 따라 코루틴이 다른 속도로 처리 가능합니다.
→ 따라서 사용되는 스레드가 다른 순서로 나오거나 일부 스레드만 사용될 수 있는거죠.
3.4.2 부모 코루틴의 CoroutineDispatcher 사용해 자식 코루틴 실행하기
코루틴은 구조화를 제공하기 때문에 코루틴 내부에 새로운 코루틴 실행이 가능해요.
이때 바깥쪽 코루틴을 부모 코루틴, 내부 코루틴을 자식 코루틴이라고 합니다.
코루틴을 계층 관계로 만들 뿐 아니라 부모 코루틴의 실행 환경을 자식 코루틴으로 전달해요.
fun main() = runBlocking<Unit> {
val multiThreadDispatcher = newFixedThreadPoolContext(
Threads = 2,
name = "MultiThread"
)
launch(multiThreadDispatcher) { // 부모 코루틴
println("[${Thread.currentThread().name}] 부모 코루틴 실행")
launch { // 자식 코루틴 실행
println("[${Thread.currnetThread().name}] 자식 코루틴 실행")
}
launch { // 자식 코루틴 실행
println("[${Thread.currnetThread().name}] 자식 코루틴 실행")
}
}
}
위 코드를 보시면 자식 코루틴을 실행할 때 별도의 CoroutineDispatcher 객체를 설정하지 않을 것을 볼 수 있습니다.
설정하지 않아도 자동으로 부모 객체(multiThreadDispathcer)를 사용하니까요.
따라서 특정 CoroutineDispatcher에서 여러 작업을 실행해야 한다면 부모 코루틴에서 설정해주면 됩니다. 그 아래 자식 코루틴으로 여러 개 생성하면 되니까요.
3.5 미리 정의된 CoroutineDispatcher
newFixedThreadPoolContext 함수를 사용해 CoroutineDispatcher 객체를 생성하면 아래와 같은 경고가 출력됩니다.

이런 경고를 하는 이유는 뭘까요?
사용자가 newFixedThreadPoolContext 함수로 객체 만드는게 비효율적일 가능성이 높기 때문입니다.
newFixedThreadPoolContext 함수로 객체를 만들면
특정 CoroutineDispatcher 객체에서만 사용되는 스레드풀을 생성합니다.
→ 스레들 풀에 속한 스레드의 수가 너무 적거나 많이 생성돼
→ 비효율적으로 동작하게 됩니다.
+) 여러 개발자가 함께 개발할 때 특정 용도를 위한 CoroutineDispatcher가 이미 메모리상에 있음에도 존재를 모르고 또 만들 수 있겠죠?
그럼 불필요한 리소스 낭비가 생깁니다. 첫 챕터부터 강조했던 스레드 생성 비용이 비싸다는 점 ..
그래서 이런 개발자가 직접 객체를 생성하는 문제를 방지하기 위해
코루틴 라이브러리가 미리 정의된 CoroutineDispatcher 목록을 제공합니다.
- Dispatchers.IO
네트워크 요청/파일 입출력 등 입출력 작업을 위한 CoroutineDispatcher - Dispatchers.Default
CPU를 많이 사용하는 연산 작업을 위한 CoroutineDispatcher - Dispatchers.Main
메인 스레드를 사용하는 작업을 위한 CoroutineDispatcher
위 객체들은 멀티 스레드 프로그래밍에 맞춰서 만들어졌습니다.
따라서 사용자들은 새 객체를 만들 필요없이 위 객체를 사용하면 됩니다.
3.5.1 Dispatchers.IO
멀티 스레드가 가장 많이 사용되는 작업은 바로 입출력(I/O) 작업입니다.
네트워크 통신(HTTP 요청)/ DB 작업 등 모두 동시에 여러 개가 수행되므로 많은 스레드가 필요하죠.
이때 Dispatchers.IO를 제공합니다.
Dispatchers.IO가 최대로 사용할 수 있는 스레드의 수는 JVM에서 사용 가능한 프로세스 수와 64 중 큰 값으로 설정 돼 있어요.
즉, Dispatchers.IO를 사용하면 여러 입출력 작업을 동시에 수행하는데 무리가 없다는 뜻이죠.
Dispatchers.IO는 싱글톤 인스턴스라 launch 함수의 인자로 곧바로 넘겨서 사용이 가능합니다.
fun main() = runBlocking<Unit>{
launch(Dispatchers.IO){
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
// 결과
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
위 코드의 실행 결과를 보면 실행된 스레드의 이름이 DefaultDispatcher-worker인 것을 볼 수 있어요.
바로 코루틴 라이브러리에서 제공하는 공유 스레드풀에 속하는 스레드의 이름입니다.
Dispatchers.IO는 공유 스레드풀의 스레드를 사용해요.
3.5.2 Dispatchers.Default
대용량 데이터를 처리해야하는 작업처럼 CPU 연산이 필요한 작업을 우리는 CPU 바운드 작업이라고 합니다.
이때 사용하는게 Dispathcers.Default입니다.
얘도 싱글톤 인스턴스라 launch 함수의 인자로 곧바로 넘겨주면 돼요.
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default){
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
// 결과
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
+) 입출력 vs CPU 바운드
둘의 차이는 스레드를 지속적으로 사용하는지의 여부입니다.
입출력 작업은 결과를 반환받을 때까지 스레드를 사용하지 않아요.
반면에 CPU 바운드 작업은 작업하는 동안 지속적으로 스레드를 사용하죠.
위 작업들을 스레드 기반 작업으로 했을 때와 코루틴을 사용했을 때로 비교하면 효율성에서 차이가 납니다.
입출력 작업은 코루틴을 쓰면 대기하는 동안 해당 스레드에서 다른 작업이 가능합니다. 효율이 올라가죠.
반면에 CPU 바운드는 별 차이가 없습니다. 계속 스레드를 차지하니까요.
3.5.3 limitedParallelism 사용해 Dispatchers.Default 스레드 사용 제한하기
무겁고 오래 걸리는 연산을 Dispatchers.Default로 처리합니다.
그랬을 때 특정 연산을 위해 Dispatchers.Default의 모든 스레드가 사용될 수 있죠.
그럼 이 연산을 하는 동안 다른 연산을 할 수 없게 된다는 문제가 발생합니다.
따라서 이때 일부 스레드만 사용해 연산 실행하게 하는 limitedParallelism 함수를 사용합니다.
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default.limitedParallelism(2)){
repeat(10){
launch{
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
}
}
// 결과
[DefaultDispatcher-worker-2 @coroutine#2] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#2] 코루틴 실행
....
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#2] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#2] 코루틴 실행
위 결과를 보면 limitedParallelism(2)를 통해 Dispatchers.Default의 여러 스레드 중 2개만 사용해 10개의 코루틴을 실행시켰다는 것을 알 수 있습니다.
실행 결과에 DefaultDispatcher-worker-1, 2만 있죠.
3.5.4 공유 스레드풀을 사용하는 Dispatchers.IO와 Dispatchers.Default
위 두 코드의 결과를 보면 두 결과 모두 코루틴을 실행시킨 스레드의 이름이 DefaultDispatcher-worker인 것을 볼 수 있어요.
그 뜻은 Dispatchers.IO와 Dispatchers.Default 모두 같은 공유 스레드풀을 사용하고 있다는 거죠.
코루틴 라이브러리는 공유 스레드풀에 스레드를 생성하고 사용할 수 있도록 API를 제공해요.
Dispatchers.IO와 Dispatchers.Default 모두 이 API를 사용해 구현됐기 때문에 같은 스레드풀을 사용하는 겁니다.
그렇다고 완전히 같은 걸 사용하는 건 아니에요.

이런 식으로 스레드풀 내에서 구분해서 사용합니다.
위 이미지를 보면 지금 limitedParallelism(2)를 통해 Dispatchers.Default의 스레드 중에서 2개를 제한하는 것을 볼 수 있죠.
이런 식으로 둘 다 공유 스레드풀을 사용하되, 그 안에서 각자 구분해서 사용하고 있다는 것만 알면 됩니다.
+) newFixedThreadPoolContext 함수는 자기만 사용하는 전용 스레드풀을 생성하는 것도요.
3.5.5 Dispatchers.Main
IO랑 Default와는 다르게 Main은 UI가 있어야해요.
메인 스레드의 사용을 위해 사용되는 특별한 객체입니다.
따라서 코루틴 라이브러리뿐만 아니라 별도 라이브러리가 필요합니다.
→ 그래서 위에서 같이 알아본 IO와 Default 코드와 같이 실행하면 오류가 납니다.
CH3 요약
- CoroutineDispatcher 객체는 코루틴을 스레드로 보내 실행하는 객체입니다.
코루틴을 작업 대기열에 적재 → 사용 가능 스레드에 보내고 → 실행해요. - 제한된 디스패처란 사용 가능한 스레드가 특정 스레드-스레드풀로 제한된 객체를 말해요.
무제한 디스패처란 사용 가능한 스레드가 제한되지 않은 객체를 말합니다. - newSingleThreadContext 및 newFixedThreadPoolContext 함수로 제한된 디스패처 객체 생성이 가능해요.
- launch 함수를 사용해 코루틴을 실행할 때 context 인자로 CoroutineDispatcher 객체를 넘겨주면 해당 객체로 코루틴이 실행됩니다.
- 자식 코루틴은 기본적으로 부모 코루틴 객체를 상속받아 사용합니다.
- 코루틴 라이브러리는 미리 정의된 Dispatchers.IO, Default, Main을 제공해요.
- Dispatchers.IO는 입출력 작업을 위한 CoroutineDispatcher 객체입니다. - 네트워크 요청, 파일 I/O 등
- Dispatchers.Default는 CPU 바운드 작업을 위한 CoroutineDispatcher 객체입니다. - 대용량 데이터 처리 등
- limitedParallelism 함수 사용해 Dispatchers.Default의 스레드 수를 제한할 수 있어요.
- Dispatchers.IO와 Dispatchers.Default는 코루틴 라이브러리에서 제공하는 공유 스레드풀을 사용해요.
- Dispatchers.Main은 메인 스레드에서 실행돼야 하는 작업을 위한 CoroutineDispatcher 객체입니다. - 별도 라이브러리 추가가 필요해요.
- Dispatchers.Main는 일반적으로 UI가 있는 앱에서 업데이트에 사용합니다.
이렇게 오늘은 CoroutineDispatcher에 대해서 정리해 보았어요.
이번 챕터에서는 Dispatcherer에 대해 알아볼 수 있어서 좋았습니다. 역할, 동작, 종류에 대해서 하나하나 알아보니 제대로 알지도 못하고 코루틴을 막 사용하던 코드를 돌아볼 수 있는 시간이었어요.
다음에는 CH4- 코루틴 빌더와 Job에 대해서 알아보겠습니다.

'🤖안드로이드🤖' 카테고리의 다른 글
| [안드로이드] Custom Convention Plugin (1) | 2025.10.12 |
|---|---|
| [안드로이드] 코루틴 빌더와 Job (1) | 2025.10.03 |
| [안드로이드] 코루틴 개발 환경 설정 (0) | 2025.09.17 |
| [안드로이드] 스레드 기반 작업의 한계와 코루틴의 등장 (4) | 2025.09.08 |
| [안드로이드] 테스트 코드에 대하여 (2) | 2025.08.26 |