코루틴 vs 스레드
코루틴 vs 스레드
코틀린 코루틴과 JVM 스레드는 모두 동시성(concurrency) 실행을 지원하지만, 근본적으로 서로 다른 추상화 수준에서 동작합니다. 스레드는 운영 체제가 관리하는 구조로 실제 커널 리소스를 소비하는 반면, 코루틴은 컴파일러가 생성한 상태 머신(state machine)으로서 디스패처(dispatcher)가 관리하는 소규모 스레드 풀 위에서 다중화(multiplex)됩니다. 이 두 모델의 내부 동작 원리를 정확히 이해하면 안드로이드 애플리케이션에서 올바른 동시성 도구를 선택하는 데 큰 도움이 됩니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 코루틴의 일시 중단(suspension)과 스레드 블로킹(blocking)이 JVM 수준에서 어떻게 구조적으로 다른지 설명할 수 있습니다.
- 코루틴이 전용 스레드를 점유하지 않으면서도 어떻게 동시성을 달성하는지 이해할 수 있습니다.
- 코틀린 컴파일러가 suspend 함수를 상태 머신으로 변환하는 과정을 파악할 수 있습니다.
- 코루틴과 스레드의 메모리 및 컨텍스트 스위칭 비용을 비교할 수 있습니다.
- 코루틴이 유리한 워크로드와 스레드 수준 병렬성이 필요한 워크로드를 구분할 수 있습니다.
- 디스패처를 활용하여 코루틴과 스레드 풀을 연결하고 CPU 바운드 작업을 처리하는 방법을 익힐 수 있습니다.
일시 중단(Suspension) vs 블로킹(Blocking)
스레드가 Thread.sleep(1000)을 호출하거나 블로킹 I/O 작업을 대기하면, 해당 OS 스레드는 작업이 끝날 때까지 점유된 상태를 유지합니다. 블로킹 호출이 반환될 때까지 그 스레드에서는 다른 작업을 실행할 수 없으며, 다른 작업을 진행해야 할 경우 OS 스케줄러가 다른 스레드로 컨텍스트 스위치를 수행해야 합니다. 이 과정에서 CPU 레지스터, 스택 포인터, 커널 구조체 등을 저장하고 복원하는 비용이 발생합니다.
반면에 코루틴이 delay(1000)을 호출하면 스레드를 점유하지 않습니다. 코틀린 컴파일러는 모든 suspend 함수를 상태 머신으로 변환하는데, delay 지점에 도달하면 코루틴은 현재 상태를 기록하고, 스레드를 디스패처에 반환한 뒤 타이머가 만료된 후 재개(resumption)를 예약합니다. 이 대기 시간 동안 동일한 스레드가 다른 코루틴을 실행할 수 있으므로 리소스를 훨씬 효율적으로 활용할 수 있습니다.
// 스레드를 1초 동안 블로킹합니다
fun blockingWork() {
Thread.sleep(1000)
}
// 코루틴을 일시 중단하여 스레드를 해제합니다
suspend fun suspendingWork() {
delay(1000)
}
이 차이는 대규모 작업에서 극명하게 드러납니다. 10만 개의 스레드를 생성하면 메모리가 금방 고갈되는데, JVM에서 각 스레드는 기본적으로 512KB에서 1MB 크기의 스택을 할당하기 때문입니다. OS가 가상 메모리를 지연 할당하더라도 10만 개의 실행 가능한 스레드를 관리하는 스케줄러 오버헤드는 감당하기 어렵습니다. 반면에 10만 개의 코루틴을 실행하는 것은 전혀 문제가 되지 않습니다. 코루틴은 스레드 풀의 소수 스레드를 공유하며, 일시 중단된 코루틴은 힙(heap)에 작은 객체만 소비하기 때문입니다.
면접에서 이 질문에 답변할 때는 "코루틴은 경량 스레드"라는 표면적인 설명에 그치지 말고, 스레드의 커널 리소스 소비와 코루틴의 힙 객체 기반 상태 관리라는 근본적인 메커니즘 차이를 설명하시는 것이 좋습니다.
동시성 모델과 디스패처
코루틴은 기반 스레드 풀을 관리하는 디스패처 위에서 실행됩니다. Dispatchers.Default는 CPU 코어 수에 맞춰진 스레드 풀을 사용하며, CPU 바운드 연산에 최적화되어 있습니다. Dispatchers.IO는 블로킹 I/O 작업을 위해 더 크고 유연한(elastic) 풀을 사용합니다. Dispatchers.Main은 안드로이드 메인 스레드에서만 실행을 보장합니다.
suspend fun loadData(): Data = withContext(Dispatchers.IO) {
// IO 풀의 스레드에서 실행
database.query("SELECT * FROM users")
}
suspend fun processData(data: Data) = withContext(Dispatchers.Default) {
// Default 풀의 스레드에서 실행
data.items.map { transform(it) }
}