코틀린에서의 스레드와 코루틴 비교
코틀린에서의 스레드와 코루틴 비교
스레드와 코루틴은 모두 동시 실행을 가능하게 하지만, 관리 방식, 블로킹 처리 방식, 리소스 소비량에서 근본적인 차이가 있습니다. 스레드는 운영 체제 수준의 구조물로서 자체 호출 스택(call stack)을 가지며, OS 스케줄러가 관리합니다. 반면 코루틴은 언어 수준의 구조물로서 코틀린 런타임이 관리하며, 기반 스레드를 블로킹하지 않고 일시 중단(suspend)할 수 있습니다. 이러한 리소스 오버헤드 차이 때문에 코루틴을 흔히 경량 스레드(lightweight threads)라고 부릅니다. 면접에서 스레드와 코루틴의 차이를 정확히 설명할 수 있다면, 안드로이드 동시성(concurrency)에 대한 깊은 이해를 효과적으로 보여줄 수 있습니다.
이번 면접 질문을 통하여 아래 내용들을 학습하실 수 있습니다.
- 스레드가 운영 체제에 의해 어떻게 관리되며, 각 스레드가 어떤 리소스를 소비하는지 설명할 수 있습니다.
- 코루틴이 기반 스레드를 블로킹하지 않고 어떻게 일시 중단하고 재개하는지 설명할 수 있습니다.
CoroutineDispatcher가 코루틴을 스레드에 매핑하는 역할을 설명할 수 있습니다.- 소수의 스레드 풀에서 다수의 코루틴이 동시에 실행될 수 있는 이유를 설명할 수 있습니다.
- 동시 작업의 성격에 따라 스레드와 코루틴 중 적절한 방식을 선택할 수 있습니다.
스레드(Threads)
스레드는 운영 체제가 관리하는 독립적인 실행 경로입니다. 각 스레드는 자체 호출 스택을 가지며, 일반적으로 약 1MB의 메모리를 소비합니다. OS 스케줄러가 각 스레드의 실행 시점을 결정하고, 스레드 간 전환(컨텍스트 스위칭)에는 레지스터 상태를 저장하고 복원하는 과정이 수반되므로 오버헤드가 발생합니다.
스레드가 네트워크 I/O나 디스크 접근 같은 블로킹 작업을 수행하면, 작업이 완료될 때까지 해당 스레드 전체가 일시 정지됩니다. 대기 중에는 그 스레드에서 다른 작업을 실행할 수 없습니다. 많은 블로킹 작업을 동시에 처리해야 한다면 그만큼 많은 스레드가 필요하고, 각 스레드는 메모리를 소비하며 스케줄링 오버헤드를 가중시킵니다.
수천 개의 동시 연결을 처리하기 위해 수천 개의 스레드를 생성하는 것은 기술적으로 가능하지만, 비용이 매우 높습니다. 메모리 사용량은 스레드 수에 비례하여 선형적으로 증가하고, 스레드 수가 늘어날수록 OS 스케줄러가 컨텍스트 스위치를 관리하는 데 소비하는 시간도 함께 증가합니다. 안드로이드처럼 리소스가 제한된 모바일 환경에서는 이러한 문제가 더욱 심각하게 작용할 수 있습니다.
코루틴(Coroutines)
코루틴은 특정 지점에서 일시 중단(suspend)한 뒤 나중에 재개(resume)할 수 있는 연산 단위입니다. 코루틴이 delay(), withContext() 또는 suspend API를 사용하는 네트워크 호출 같은 suspend 함수에 도달하면, 실행 중이던 스레드를 해제합니다. 해제된 스레드는 즉시 다른 코루틴을 실행하는 데 사용할 수 있게 됩니다.
코틀린 런타임이 코루틴 스케줄링을 관리합니다. 코루틴을 일시 중단할 때는 전체 호출 스택이 아닌 작은 크기의 Continuation 객체만 저장합니다. 코루틴을 재개할 때는 디스패처가 지정하는 스레드에서 이 Continuation을 복원합니다. 덕분에 단일 스레드가 일시 중단 지점에서 코루틴 간 전환을 수행하며, 수천 개의 코루틴을 구동할 수 있습니다.
suspend fun fetchData(): String {
delay(1000) // 일시 중단하며 스레드를 해제
return "result"
}
1초의 지연 시간 동안 스레드는 다른 코루틴을 실행할 수 있는 상태가 됩니다. 대기하는 동안 어떤 스레드도 블로킹되거나 점유되지 않습니다.
suspend 키워드는 해당 함수가 일시 중단 지점임을 표시합니다. 컴파일러는 이 함수를 Continuation을 활용한 상태 머신(state machine)으로 변환합니다. 각 Continuation은 일시 중단 시점의 로컬 상태를 캡처하여, 코루틴이 정확히 중단된 지점부터 재개할 수 있도록 합니다. 이 변환 과정은 개발자에게 투명하게 이루어지며, 개발자는 순차적인 코드를 작성하면 컴파일러가 이를 논블로킹 실행 방식으로 자동 변환해 줍니다. 이러한 내부 동작 원리를 깊이 이해하고 있으면 면접에서 큰 차별점이 됩니다.
구조화된 동시성(Structured Concurrency)
코루틴은 구조화된 동시성(structured concurrency)이라는 개념을 도입합니다. 모든 코루틴은 생명주기를 정의하는 스코프(scope) 내에서 실행됩니다. 스코프가 취소되면 해당 스코프 내의 모든 코루틴도 함께 취소됩니다. 코루틴이 실패하면 부모 스코프가 실패를 처리하는 방법을 결정할 수 있습니다. 이러한 계층적 구조 덕분에 직접 스레드를 관리할 때 빈번하게 발생하는 리소스 누수나 고아 태스크(orphaned task) 문제를 효과적으로 방지할 수 있습니다.