안드로이드 시스템·Compose 용어 사전
코루틴Dispatcher · Dispatchers.IO

Dispatchers.Main.immediate

이미 메인 스레드면 디스패치 없이 즉시 실행하는 디스패처. 한 프레임 깜빡임을 없앤다.

이미 메인 스레드에 있으면 큐를 거치지 않고 즉시 실행하는 디스패처.

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를 건드리면 크래시다. 전환 책임을 아래로 내리는 편이 낫다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 코루틴 on Android — lifecycleScope·repeatOnLifecycle