Jetpack Compose에서 Dispatcher와 스레드 관리

오리·2025년 2월 20일

스레드를 바꿔야 하는 경우

  • Jetpack Compose의 Composable 함수는 메인 스레드(UI 스레드)에서 실행됨
  • 하지만 다음과 같은 작업을 메인 스레드에서 실행하면 UI가 멈추거나, 앱이 느려질 수 있음

⇒ 따라서 필요한 경우 백그라운드 스레드로 이동

상황필요한 Dispatcher
UI 업데이트Dispatchers.Main
네트워크 요청 / DB 작업Dispatchers.IO
무거운 연산 (JSON 파싱, 데이터 변환)Dispatchers.Default
UI 상태 변경을 즉시 실행Dispatchers.Main.immediate

컨텍스트 스위칭(Context Switching)

  • 컨텍스트 스위칭이란 CPU가 실행 중인 작업을 멈추고 다른 스레드의 작업을 실행하는 과정
  • 이 과정에서 현재 실행 중인 작업의 상태(Context)를 저장, 새로운 작업의 상태를 불러와야 함

컨텍스트 스위칭의 문제점

  • CPU가 실행 중인 정보를 저장하고 불러오는 과정에서 오버헤드 발생
  • 캐시가 무효화될 가능성이 있어 성능 저하 가능
  • 스레드가 많아질수록 경쟁이 발생해서 성능이 낮아질 수 있음

⇒ 불필요한 스레드 변경은 성능을 저하시킬 수 있기 때문에 최소화하는 게 중요

코루틴의 컨텍스트 스위칭 최적화

  • 스레드 풀 재사용
    Dispatcher스레드 개수설명
    Dispatchers.Main1개 (메인 스레드)UI 작업 전용 (새로운 스레드 X)
    Dispatchers.IO여러 개 (CPU 코어수 * 64개)I/O 작업 전용 스레드 풀 사용
    Dispatchers.DefaultCPU 코어 개수만큼CPU 연산 최적화
    Dispatchers.Main.immediate1개 (메인 스레드)메인 스레드에서 바로 실행 가능
  • 컨텍스트 전환 최소화
    • 같은 Dispatcher에서 실행중이라면 불필요한 스레드 변경 없이 같은 스레드에서 실행
    • 예를 들어, Dispatchers.IO 내에서 withContext(Dispatchers.IO) {}를 호출해도 추가적인 스위칭 없이 실행
  • Lazy 스레드 생성
    • Dispatchers.IO와 Dispatchers.Default는 기존 스레드를 재사용하는 방식, 오랜 시간 사용하지 않으면 자동으로 줄어드는 방식
  • 필요할 때만 스레드 변경

스레드 변경이 불필요한 경우

  • 메인 스레드에서 실행해도 되는 작업을 굳이 Dispatchers.IO에서 실행
  • 불필요하게 여러 번 withContext()로 바꾸는 경우
    • 이 경우 같은 Dispatcher 내에서 중첩 호출해도 성능에 영향은 거의 없다고 하나, 필요 없는 스레드 전환을 최소화하는 것이 좋음
  • 이미 메인 스레드인데 또 Dispatchers.Main으로 변경

⇒ 이런 경우 컨텍스트 스위칭 오버헤드만 증가하고 성능이 나빠질 수 있음

Jetpack Compose에서 Dispatcher 최적화 방법

Dispatchers.Main.immediate 사용

UI 상태를 변경할 때, 이미 메인 스레드라면 불필요한 컨텍스트 스위칭 없이 바로 실행

launch(Dispatchers.Main.immediate) { updateUi() }

Dispatchers.Main.immediate 란?

  • 현재 코루틴이 이미 메인 스레드에서 실행 중이면 즉시 실행
  • 그렇지 않으면 메인 디스패처를 통해 실행을 예약

⇒ 메인 스레드에서 실행 중일 때 추가적인 스케줄링을 피하고 즉시 실행하는 최적화된 버전의 디스패처

네트워크 요청 후 바로 UI 업데이트

LaunchedEffect(Unit) {
    val data = withContext(Dispatchers.IO) { fetchData() }
    textState = data // 자동으로 메인 스레드에서 실행됨
}

무거운 연산은 Dispatchers.Default에서 실행

LaunchedEffect(Unit) {
    val result = withContext(Dispatchers.Default) { processHeavyData() }
    textState = result
}

rememberCoroutineScope() 사용

LaunchedEffect는 Composable이 재구성될 때마다 실행될 수 있음

즉, 화면이 리컴포지션될 때 의도치 않게 fetchData()가 여러 번 호출될 수 있음

var textState by remember { mutableStateOf("Initial") }

LaunchedEffect(Unit) {
    textState = fetchData() // 네트워크 요청
}

UI 이벤트에서 코루틴을 실행할 때는 rememberCoroutineScope()을 사용하는 것이 좋음

@Composable
fun MyScreen() {
    val scope = rememberCoroutineScope()
    var textState by remember { mutableStateOf("Initial") }

    Button(onClick = {
        scope.launch(Dispatchers.IO) { 
            val result = fetchData()  
            textState = result // UI 업데이트 (자동으로 Main 스레드에서 실행됨)
        }
    }) {
        Text("Load Data")
    }
}

rememberCoroutineScope()를 사용하면 Composable의 생명주기에 맞춰 CoroutineScope가 관리됨

onClick 이벤트가 발생할 때만 코루틴이 실행되므로 불필요한 호출 방지

코루틴 Job을 활용한 취소 처리

네트워크 요청이나 무거운 연산을 수행할 때 이전 작업이 완료되지 않았는데 다시 실행되면 불필요한 리소스 낭비

불필요한 작업을 최소화하고, 새로운 요청이 들어오면 이전 요청을 취소하는 방식

var job: Job? = null
Button(onClick = {
    job?.cancel() // 이전 작업 취소
    job = scope.launch(Dispatchers.IO) { fetchData() }
}) {
    Text("Reload Data")
}
  • 사용자가 버튼을 여러 번 눌러도 이전 요청을 취소한 후 새로운 요청을 실행
  • API 호출 시 특히 중요 (ex: 검색 기능에서 이전 검색 요청 취소)

결론

스레드를 바꿀 때는 신중하게!

필요한 경우에만 적절한 Dispatcher를 사용해야 함

0개의 댓글