CoroutineDispatcher을 직역하면 "코루틴을 보내는 주체"이다.
이를 좀 더 자세히 얘기하면 "코루틴을 스레드로 보내 실행시키는 객체"라고 할 수 있다.
CoroutineDispatcher는 코루틴이 어떤 스레드나 스레드 풀에서 실행될지를 결정하며,
코루틴을 동기적으로 실행할지, 비동기적으로 실행할지, 그리고 어떤 스레드에서 실행할지 결정하는 데 사용된다.

스레드1이 비어있으므로 코루틴1을 스레드1에 삽입한다.

스레드2가 비어있으므로 코루틴2를 스레드2에 삽입한다.

스레드1에 있던 코루틴1이 끝나고 대기하던 코루틴3이 스레드1에 삽입된다.
이처럼 CoroutineDispatcher의 동장방식은 Executor 객체의 ExecutorService와 매우 흡사하다.
작업단위가 코틀린인것만 다를 뿐, 나머지는 거의 비슷하다.
이렇게 멀티스레딩 프로그래밍 방식은 비슷한 것들이 많기 때문에 참고하도록 하자.

단일 스레드 디스패처는 newSingleThreadContext를 사용한다.

멀티 스레드 디스패처는 newFixedThreadContext를 사용한다.
하지만 newFixedThreadContext를 사용하려고 하면 인텔리제이에서 "이 API는 섬세하게 다뤄져야 한다.
이 API를 사용하기 전에 문서를 모두 읽고 이해하고 사용해야 한다." 라는 경고문구를 렌더링한다.
이것의 의미는 개발자가 직접 newFixedThreadContext를 사용하여 코루틴 디스패처 기능을 개발하는 것은 사용하기 어렵고 까다롭다는 뜻이다.
그렇다면 왜 코틀린 측은 newFixedThreadContext 사용을 비추천하는 걸까?
리소스 관리:
newFixedThreadContext로 생성된 스레드는 개발자가 명시적으로 해제해줘야 한다. 그렇지 않으면 스레드 리소스가 계속 점유된 상태로 남아 있을 수 있다.설정의 복잡성:
잘못된 사용으로 인한 위험성:
따라서 매우 섬세하게 코드를 작성하지 않으면 메모리 누수, 성능 저하, 리소스 낭비 등의 문제가 발생할 가능성이 크다.
개발자가 직접 newFixedThreadContext를 사용할 필요 없이 코틀린에서 제공하는 미리 만들어진 디스패처를 사용하는 것이 훨씬 안전하고 효율적이다.
기본 디스패처는 성능 최적화와 리소스 관리를 자동으로 해주기 때문에 직접적인 스레드 풀 관리가 필요하지 않고, 일반적인 비동기 작업에는 충분히 적합하다.
코틀린에서 제공하는 기본 디스패처인 Dispatchers.IO, Dispatchers.Default 등은 이러한 리소스 관리와 스레드 최적화가 자동으로 이루어진다.
스레드 풀 크기 조정이나 리소스 최적화가 이미 설정되어 있어, 성능 면에서도 안전하다.
Dispatchers.IO란, 네트워크 요청이나 DB 읽기 쓰기 같은 입출력(I/O) 작업을 실행하는 디스패처이다.
1. 코루틴 실행 환경 설정:
runBlocking 또는 GlobalScope.launch를 사용하여 코루틴을 시작한다. 이는 코루틴을 비동기적으로 실행할 수 있는 진입점이다.2. Dispatchers.IO 디스패처 사용:
Dispatchers.IO를 설정하여, I/O 관련 작업이 메인 스레드가 아닌 다른 스레드 풀에서 실행되도록 지정한다.3. 비동기 작업 처리:
launch 또는 async를 사용하여 비동기 작업을 시작한다. launch는 작업을 시작하고 결과를 기다리지 않으며, async는 결과를 반환하는 작업을 시작한다.4. 작업 완료 후 리소스 해제:
Dispatchers.IO가 사용할 수 있는 스레드의 수: 64와 JVM에서 사용할 수 있는 프로세서의 수 중 큰 값이다.
즉 Dispatchers.IO는 최소 64개의 스레드를 사용할 수 있고, CPU 성능이 좋을수록 더 많은 스레드를 활용해 입출력 작업의 성능을 극대화할 수 있다는 의미이다.

IO Dispatcher를 사용할 때는 한번만 IO Dispatcher를 넘겨서 사용하는 것이 효율적이다.
코루틴은 구조화를 제공하기 때문에 다음과 같이 코루틴 내부에서 새로운 코루틴을 실행할 수있다.
이때 바깥에서 실행되는 코루틴을 부모 코루틴, 안쪽에서 실행되는 코루틴을 자식 코루틴이라고 부른다.
자식 코루틴은 별도로 CoroutineDispatcher를 설정하지 않으면 부모 코루틴에 설정된 코루틴 디스패처를 그대로 사용한다.
따라서 이렇게 바깥에 launch 함수로 기존 3개의 launch 함수를 감싸고 바깥쪽 launch 함수에만 IO Dispatcher를 설정해주면 하위에 있는 코루틴들에 IO Dispathcer가 자동으로 설정된다.
단, 부모-자식 코루틴은 구조화된 동시성에 연관된 내용이기 때문에 매우 깊이 다뤄야 하는 내용이므로 이에 대한 내용은 나중에 다루도록 하겠다.
구조화된 동시성의 핵심은 부모-자식 관계를 통해 코루틴의 수명을 명확하게 관리할 수 있다는 것이다. 자식 코루틴들은 부모 코루틴이 완료되기 전까지 항상 함께 관리되며, 부모가 취소되면 자식도 함께 취소된다.
Default Dispatcher는 CPU 바운드 작업(이미지, 동영상 처리나 대용량 데이터 변환 같은 끊이지 않고 연산이 필요한 작업)을 위한 디스패처이다.
Default Dispatcher가 사용할 수 있는 스레드의 수는 IO Dispatcher와 동일하다.

Code 3-4와 동일하지만 Default Dispatcher를 사용한 것만 다르다.
즉, 사용방법은 동일하다.

두 코드의 실행 결과를 보면 사용하는 스레드의 이름이 DefaultDispatcher-worker-1,3,4로 동일하다. 하지만 실제 공유 스레드풀을 보면 두 디스패처가 사용하는 스레드풀은 완전히 달라서 겹칠 수 없다.
그런데 도대체 왜 이름이 같은것일까?
Dispatchers.IO와 Dispatchers.Default가 사용하는 스레드의 이름이 유사한 것은 사실이다. 둘 다 "DefaultDispatcher-worker-X" 형식의 이름을 사용한다.
하지만 스레드 이름이 같다고 해서 반드시 같은 스레드 풀을 사용한다는 의미는 아니다.
LA 다저스에 클레이튼 커쇼라는 투수가 있고 LA 에인절스에 클레이튼 커쇼라는 동명이인 타자가 있더라도, 둘은 완전히 다른 선수인것 처럼 이해하면 될 것이다.

이것은 이 디스패쳐들이 스레드를 만들 때 사용하는 공유 스레드풀 때문이다.
코루틴 라이브러리는 스레드의 생성과 관리를 효율적으 로 할 수 있도록 애플리케이션 레벨의 공유 스레드풀을 제공한다.
이 공유 스레드풀에서는 스레드를 무제한으로 생성할 수 있고, 이 공유 스레드풀을 사용해 각각의 디스패처에서 사용할 스레드를 생성한다.
그림을 보면 DispatchersDefault의 스레드와 Dispatchers IO의 스레드가 나누어져 있는 것을 볼 수 있다.
이것이 바로 스레드의 이름의 앞부분이 동일했던 이유다.

우선 Limited Parallelism에 대해 알아보자.
만약 Default Dispatcher의 모든 스레드를 특정 작업만을 위해 모두 사용한다면 문제가 된다.
왜냐하면 다른 작업을 수행할 수 없게 되기 때문이다.
이런 문제를 방지하기 위해 코루틴 라이브러리는 Limited Parallelism이라는 함수를 제공한다.
이는 특정 작업 위해 사용 가능한 스레드의 개수를 제한한다.
예를 들어 이미지 처리를 위해 2개의 스레드만 사용하고 싶다면 다음과 같이 코드를 작성하면
CPU 바운드 작업을 위한 스레드 중 2개만 사용하도록 할 수 있다.
그러면 이미지 처리 작업을 아무리 많이 요청해도 2개의 스레드만을 사용하게 된다.
fun main() = runBlocking<Unit> {
val imageProcessingDispatcher = Dispatchers.Default.limitedParallelism(2)
repeat(100) {
launch(imageProcessingDispatcher) {
Thread.sleep(1000L) // 이미지 처리 작업
println("[${Thread.currentThread().name}] 이미지 처리 완료")
}
}
}


Dispatchers.IO에서 limitedParallelism()은 작동 방식이 다르다.
Dispatchers.IO의 limitedParallelism()은 실제로 새로운 스레드 풀을 생성한다.
이 스레드 풀은 Dispatchers.Default나 일반적인 Dispatchers.IO가 사용하는 스레드 풀과는 완전히 별개다.
다른 작업에 방해받지 않아야 하는 높은 우선순위의 작업이 있을 경우(ex. 메시지 앱의 메시지 싱크 작업)에는 다른 작업에 의해 방해받으면 안되기 때문에 전용 스레드 풀에서 높은 우선순위로 작업되도록 하는 것이다.
fun main() = runBlocking<Unit> {
val dedicatedDispatcher = Dispatchers.IO.limitedParallelism(2)
repeat(100) {
launch(dedicatedDispatcher) {
println("[${Thread.currentThread().name}] 중요 작업 실행")
}
}
}

이 코드는 100개의 코루틴이 이 dedicatedDispatcher를 사용해 실행되도록 했다.
그러면 코루틴들이 다음과 같이 2개의 스레드만을 사용하는 것을 볼 수 있다.
이 두개의 스레드의 위치는 새로 만들어진스레드 풀에 위치할 것이다.
limitedParallelism 함수를 IO 디스패처에 적용했을 때는 기존 IO 스레드의 개수에 영향을 받지 않는 별도의 스레드 집합을 사용하는 디스패쳐가 만들어진다. 이런 디스패처는 다른 작업에 영향을 받지 않아야 하는 작업을 할 때 사용돼야 한다.
limitedParallelism 함수를 Default 디스패처에 적용했을 때는 기존 Default 디스패처의 스레드들 중 일부를 제한적으로 사용하는 디스패처가 만들어진다. 이런 디스패처는 특정 작업이 모든 스레드를 사용하는 것을 방지해야 할 때 사용된다.
즉 limitedParallism 함수는 위와 같이 매우 특별한 상황에만 사용되며, 대부분의 경우에는 Dispatcher.IO나 Dispatchers.Default만 사용해도 문제가 없다.
Dispatchers.Main은 메인 스레드에서의 작업을 위한 디스패처이다.
이 디스패처는 코루틴을 호출한 스레드에서 작업을 즉시 실행하지 않고, UI 업데이트를 위해 대기한다. 다른 코루틴이 UI 스레드를 점유하고 있다면, 이 디스패처는 그 코루틴이 완료될 때까지 대기한다.
기본 코루틴 라이브러리에는 Dispatchers.Main 구현체가 없으므로 사용을 위해서는 안드로이드 코루틴 라이브러리를 추가해야한다.
이 디스패처는 주로 사용자 인터페이스(UI)를 업데이트해야 할 때, 예를 들어 버튼 클릭 이벤트 후 데이터를 가져와 UI를 갱신하는 경우에 주로 사용한다.

Main Dispatcher를 통해 Test Text1,2,3을 렌더링 하고 그 사이에 1초간 delay한다.
class WeatherActivity : AppCompatActivity() {
private lateinit var binding: ActivityWeatherBinding
private val weatherRepository = WeatherRepository()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWeatherBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.refreshButton.setOnClickListener {
loadWeatherData()
}
}
private fun loadWeatherData() {
lifecycleScope.launch(Dispatchers.Main) {
try {
binding.progressBar.visibility = View.VISIBLE
binding.weatherInfo.visibility = View.GONE
binding.errorMessage.visibility = View.GONE
val weatherData = withContext(Dispatchers.IO) {
weatherRepository.fetchWeatherData()
}
updateUI(weatherData)
} catch (e: Exception) {
binding.errorMessage.text = "Error: ${e.message}"
binding.errorMessage.visibility = View.VISIBLE
binding.weatherInfo.visibility = View.GONE
} finally {
binding.progressBar.visibility = View.GONE
}
}
}
private fun updateUI(weatherData: WeatherData) {
binding.weatherInfo.text = "Temperature: ${weatherData.temperature}°C\n" +
"Condition: ${weatherData.condition}"
binding.weatherInfo.visibility = View.VISIBLE
}
}
이 접근 방식은 Main Dispatcher의 이점을 최대한 활용하여 UI 업데이트를 효율적으로 처리한다.
코루틴 사용:
lifecycleScope.launch(Dispatchers.Main)을 사용하여 코루틴을 시작한다. 이는 액티비티의 생명주기와 연동되어 메모리 누수를 방지한다.Main Dispatcher의 효과적 활용:
백그라운드 작업 분리:
withContext(Dispatchers.IO)를 사용하여 네트워크 요청을 수행한다. 이는 메인 스레드를 차단하지 않고 I/O 작업을 수행할 수 있게 한다.자연스러운 순차 실행:
효율적인 스레드 전환:
예외 처리의 단순화:
리소스 관리:
lifecycleScope를 사용함으로써 액티비티가 종료될 때 자동으로 코루틴이 취소되어 리소스 누수를 방지한다.반응성 향상:
Q. Dispatchers.Main은 메인 스레드에서 작동합니다. 그런데 "멀티스레딩"은 다른 스레드와 비동기적으로 작동하는 것이 기본적인 개념인데 Dispatchers.Main은 어차피 메인 스레드에서 동작하는데 이게 왜 멀티 스레딩의 일부분입니까?
A.
우선 코루틴 != 멀티스레딩인 것을 알아야 한다.
우선 코루틴은 '협력적 멀티태스킹'을 구현한다. 이는 전통적인 멀티스레딩과는 다른 접근 방식이다. 코루틴은 스레드 위에서 실행되지만, 하나의 스레드가 1000개가 넘는 여러 개의 코루틴을 실행할 수 있고, 중단(suspension)과 재개(resumption)가 가능하다.
Dispatchers.Main은 주로 UI 작업을 위해 사용되며, 메인 스레드에서 동작한다.
그러나 코루틴이 중단 포인트를 만나면, 메인 스레드를 차단하지 않고 다른 작업을 수행할 수 있게 한다. 다른 디스패처에서 실행되던 코루틴이 UI 업데이트를 위해 Dispatchers.Main으로 전환될 수 있다.
또한 Dispatchers.Main은 직접적으로 다른 스레드를 사용하지 않지만, 멀티스레딩 환경의 일부로 작동하기도 한다. 다른 디스패처(예: Dispatchers.IO, Dispatchers.Default)와 협력하여 작업을 분배할수도 있고, 백그라운드 스레드에서 수행된 작업의 결과를 UI에 반영할 때 사용되기도 한다.
따라서 Dispatchers.Main은 비동기 작업의 결과를 동기화하고 조정하는 역할을 한다.
Dispatchers.Main은 직접적으로 다중 스레드를 생성하지 않지만, 코루틴 기반의 비동기 프로그래밍 모델에서 중재자 역할을 한다. 백그라운드 작업(IO, Default)과 UI 작업 사이의 다리 역할을 하기도 하며, 다양한 비동기 작업의 결과를 UI에 안전하게 반영한다.
다른 디스패처와 비교하자면, Dispatchers.IO, Dispatchers.Default는 주로 '작업 실행'에 초점을 두지만 Dispatchers.Main: '작업 결과의 조정 및 반영'에 초점을 둔다고 볼 수 있다.
이 방식에서는 웨이터가 홀과 주방 사이를 여러 번 오가야 한다 (여러 번의 컨텍스트 전환).
이 방식에서는 웨이터가 홀과 주방 사이를 한 번씩만 오간다 (최소한의 컨텍스트 전환).
Main Dispatcher를 효과적으로 사용하는 것은 시나리오 2와 같이 작업을 최적화하는 것과 유사하다.
이 비유를 Android 앱 개발에 적용해보자.
비효율적인 방식:
효율적인 방식 (Main Dispatcher 활용):
"추가적인 컨텍스트 전환이 필요 없다"는 것은 이처럼 불필요한 스레드 간 전환을 최소화하여 앱의 성능을 향상시키는 것을 의미한다.
따라서 Main Dispatcher를 효과적으로 사용하면, 마치 효율적인 웨이터처럼 불필요한 "이동"(컨텍스트 전환)을 줄일 수 있다.
lifecycleScope과 viewModelScope은 코루틴의 실행 환경을 가진 객체다.
두 scope의 내부를 보면 Dispatchers.Main.immediate를 사용한다.
그럼 이것이 무엇인지 알아보자.


우선 Dispatchers.Main은 어느 스레드에서 코루틴을 실행 요청하든 코루틴을 작업 대기열에 먼저 적재한 후 Main Thread가 비었을 때 코루틴을 보낸다. 그러니까 메인 스레드가 비어있어도 일단 코루틴을 적재는 무조건 한다.

이와 다르게 Dispatchers.Main.immediate는 코루틴을 실행하는 코드가 메인 스레드에서 실행되고 있다면, 작업 대기열에 적재 없이, 그대로 메인 스레드에서 실행될 수 있게 한다.
즉시 실행이 필요한 UI 관련 작업을 처리할 때 사용한다.
당연히 적재 과정을 생략하기 때문에 즉각적인 UI를 만들 때 주로 사용한다.
ex. 빠른 피드백이 중요한 상황(예: 폼 제출 후 즉각적인 UI 업데이트, 실시간 데이터 표시 등)
하지만 이것은 실행요청을 하는 스레드가 메인 스레드일 때만 해당된다.

Dispatchers.Main.immediate를 호출했더라도, 실행 요청한 스레드가 백그라운드 스레드라면, Main 디스패처처럼 작업 대기열에 적재를 거쳐야한다.
강의자료 출처 및 참고한 강의:
코틀린 코루틴 완전 정복 by 조세영님
https://www.inflearn.com/course/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%BD%94%EB%A3%A8%ED%8B%B4-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5/dashboard