이미 메인 스레드에 있으면 큐를 거치지 않고 즉시 실행하는 디스패처.
Dispatchers.Main 항상 메인 큐에 넣는다 (이미 메인이어도 다음 루프로 미뤄진다)
Dispatchers.Main.immediate 이미 메인이면 그 자리에서 실행한다
왜 차이가 보이나
onCreate 에서 lifecycleScope.launch { render(state) } 를 부른다
- Main — → 다음 프레임에 render 가 돈다
- 그 사이 한 프레임 동안 빈 화면이 그려진다 (깜빡임)
- Main.immediate — → onCreate 안에서 바로 render 가 돈다
lifecycleScope · viewModelScope 는 기본이 Main.immediate 다
디스패처 선택
-
Main — UI 갱신. 메인 스레드
-
IO — 네트워크 · 디스크 · DB. 스레드 64개까지(기본), 블로킹 대기용
-
Default — CPU 연산. 코어 수만큼
-
Unconfined 거의 쓰지 않는다
-
IO 와 Default 는 스레드 풀을 공유한다
- withContext(IO) → withContext(Default) 는 스레드 전환 비용이 거의 없다
어디서 전환하나
// ❌ 호출부가 스레드를 알아야 한다
viewModelScope.launch(Dispatchers.IO) { repository.getUsers() }
// ✅ suspend 함수가 자기 스레드를 책임진다 (main-safety)
suspend fun getUsers(): List<User> = withContext(ioDispatcher) { api.getUsers() }
// 호출부는 그냥 viewModelScope.launch { repository.getUsers() }
"suspend 함수는 메인 스레드에서 불러도 안전해야 한다" 가 공식 가이드의 원칙이다
- 스레드 결정을 라이브러리/저장소가 내부에서 한다
- 호출부가 실수할 여지가 사라진다
테스트를 위해 주입한다
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) {
suspend fun getUsers() = withContext(ioDispatcher) { api.getUsers() }
}
// 테스트
UserRepositoryImpl(FakeApi(), StandardTestDispatcher(testScheduler))
Dispatchers.IO 를 하드코딩하면 테스트가 실제 스레드 풀을 쓴다
- 느리고, 가상 시간(advanceTimeBy) 을 못 쓰고, 결과가 비결정적이다
Retrofit·Room은 이미 안전하다
Retrofit 의 suspend 함수와 Room 의 suspend DAO 는 내부에서 자체 스레드로 옮긴다 → withContext(IO) 로 감쌀 필요가 없다 (감싸도 해롭진 않지만 불필요한 전환이다)
면접 함정
- ❌ "IO는 무제한 스레드를 만든다" → 기본 64개(또는 코어 수 중 큰 값)로 제한된다.
- ❌ "launch(Dispatchers.IO)면 안전하다" → 그 안에서 UI를 건드리면 크래시다. 전환 책임을 아래로 내리는 편이 낫다.