Coroutines — 핵심 개념, Kotlin의 Job 및 Dispatchers

저자: IT Sectr 게시일: 2026-03-16 읽는 시간: 8 분

Coroutines(코루틴)은 kotlinx.coroutines 라이브러리를 통해 사용할 수 있는 Kotlin의 비동기 프로그래밍을 위한 경량 스레드입니다. JetBrains Kotlin Documentation, 2026에 따르면, Coroutines은 기존 Thread와 달리 스레드를 차단하지 않고 함수 실행을 일시 중단할 수 있습니다. 코루틴은 제한된 스레드 풀에서 실행되므로 네이티브 스레드보다 천 배 더 가볍습니다. Kotlin Coroutines은 Android Jetpack, Retrofit, Room 및 Android 생태계의 다른 인기 라이브러리와 완전히 통합됩니다.

핵심 요점

  • Coroutines — 차단 없는 비동기 코드를 위한 경량 Kotlin 스레드
  • suspend 함수 — 스레드를 차단하지 않고 일시 중단 및 재개할 수 있는 함수
  • Dispatcher는 코루틴 실행을 위한 스레드 풀을 결정합니다
  • Job — 취소 지원 및 상태 추적이 있는 코루틴 디스크립터
  • CoroutineScope는 코루틴의 수명 주기와 완료 시 취소를 관리합니다

Kotlin 코루틴이란?

Coroutines은 kotlinx.coroutines 라이브러리에 구현된 Kotlin의 비동기 프로그래밍 메커니즘입니다. 운영 체제 스레드와 달리 코루틴은 특정 스레드에 묶여 있지 않습니다. 한 스레드에서 일시 중단되고 다른 스레드에서 재개될 수 있습니다. 단일 스레드는 최소한의 오버헤드로 수천 개의 코루틴을 전환하면서 실행할 수 있습니다.

코루틴은 Kotlin 1.3(2018)에서 실험적 기능으로 등장했으며 Kotlin 1.5(2021)에서 안정화되었습니다. Coroutines은 async/await과 유사하게 콜백 지옥 문제를 해결하지만 더 풍부한 API를 제공합니다: 채널(Channel), Flow, Job 계층 구조의 예외 처리, Android Lifecycle과의 직접 통합.

JetBrains(2025)에 따르면, 각 코루틴은 약 100바이트의 메모리를 소비하는 반면 네이티브 스레드는 1MB 이상입니다. 이는 OutOfMemoryError의 위험 없이 단일 애플리케이션에서 수백만 개의 코루틴을 실행할 수 있게 합니다. 코루틴의 가벼운 특성이 Android에서 비동기성을 위한 선호 도구가 되는 이유입니다.

코루틴의 내부 동작 방식

각 Kotlin 코루틴은 Continuation Passing Style(CPS)을 통해 상태 머신으로 컴파일됩니다. 컴파일러는 각 suspend 함수에 숨겨진 Continuation 매개변수를 추가합니다. Continuation에는 재개 지점과 모든 지역 변수가 포함됩니다. 코루틴이 일시 중단되면 런타임이 Continuation을 저장하고, 재개되면 Dispatcher 풀의 사용 가능한 스레드에서 복원합니다.

suspend 함수: 일시 중단 및 재개

suspend는 함수를 일시 중단 가능으로 표시하는 Kotlin 키워드입니다. 이러한 함수는 다른 suspend 함수 또는 코루틴에서만 호출할 수 있습니다. suspend 함수 내에서는 다른 suspend 함수를任意 순서로 호출할 수 있으며, 각 호출 지점은 잠재적인 일시 중단 지점입니다.

메커니즘은 간단합니다: suspend 함수가 다른 suspend 함수를 호출하면 해당 지점에서 일시 중단되고 스레드를 해제합니다. 호출된 함수가 완료되면 런타임이 저장된 위치에서 실행을 계속합니다. 이를 협력적 취소(cooperative cancellation)라고 합니다 — 어떤 스레드도 차단되지 않습니다.

  • 일시 중단 — 코루틴이 스레드를 차단하지 않고 해제합니다
  • 재개 — 코루틴이 일시 중단된 지점에서 계속됩니다
  • 스레드 — 코루틴이 스레드 A에서 일시 중단되고 스레드 B에서 재개될 수 있습니다
  • 예외 — 동기 코드와 마찬가지로 try/catch로 처리됩니다

중요: suspend 함수는 기본적으로 비동기가 아닙니다. launch나 async를 사용하지 않으면 실행 순서가 순차적으로 유지됩니다. suspend는 단순히 함수가 스레드를 차단하지 않고 일시 중단되고 코루틴 컨텍스트의 일부가 될 수 있게 합니다. Continuation Passing Style은 각 suspend 함수가 숨겨진 Continuation 콜백을 받고 컴파일러가 일시 중단 및 재개를 관리하는 상태 머신을 생성하는 컴파일 모델입니다.

CoroutineScope와 구조적 동시성

CoroutineScope는 코루틴의 수명 주기를 정의하는 컨텍스트입니다. 모든 코루틴은 스코프 내에서 시작되어야 합니다. 스코프가 취소되면(예: Activity 종료 시) 모든 하위 코루틴이 자동으로 취소됩니다. 이는 백그라운드 작업 누수를 방지합니다. Android Jetpack은 각 구성 요소에 대해 미리 정의된 스코프를 제공합니다: ViewModel용 viewModelScope와 Activity/Fragment용 lifecycleScope는 해당 구성 요소가 소멸될 때 자동으로 취소됩니다.

구조적 동시성(Structured Concurrency)은 모든 하위 코루틴이 완료될 때까지 코루틴이 완료되지 않음을 보장하는 원칙입니다. Job 계층 구조는 트리를 형성합니다: 루트 코루틴이 부모 Job을 생성하고, 하위가 자식 Job을 생성합니다. 부모 Job의 취소는 모든 자식에게 전파됩니다. Structured Concurrency는 코루틴과 스레드의 근본적인 차이점입니다.

Scope사용 위치취소
GlobalScope데몬 작업 전용자동 취소되지 않음
viewModelScopeAndroid ViewModelViewModel 정리 시
lifecycleScopeAndroid Activity/Fragmentlifecycle 소멸 시
coroutineScopesuspend 함수 내부부모 Job 취소 시

오류 처리를 위한 SupervisorJob

일반 Job은 하나의 하위 코루틴이 실패하면 모든 형제를 취소합니다. SupervisorJob은 예외입니다: 하나의 하위 코루틴 실패가 다른 코루틴에 영향을 미치지 않습니다. 이는 여러 독립적인 작업이 병렬로 실행되고 그중 하나가 다른 작업을 취소할 필요 없이 실패할 수 있는 경우에 중요합니다.

Dispatchers와 코루틴 빌더

Dispatchers는 코루틴이 실행될 스레드를 결정합니다. Dispatchers.Main — 기본 Android UI 스레드. Dispatchers.IO — 차단 작업(네트워크, 디스크)용 풀. Dispatchers.Default — CPU 집약적 작업용. Dispatchers.Unconfined — 현재 스레드에서 시작하지만 그 스레드에 유지됨을 보장하지 않습니다. 올바른 Dispatcher를 선택하는 것은 성능에 중요합니다: Default에서 IO 작업을 실행하면 계산 풀이 차단되고, IO에서 CPU 작업을 실행하면 불필요한 스레드가 생성됩니다.

withContext — 코루틴 내에서 Dispatcher를 전환하는 함수입니다. 예를 들어, JSON을 파싱하는 suspend 함수는 계산을 위해 Dispatchers.Default로 전환하고 UI 업데이트를 위해 Dispatchers.Main으로 돌아올 수 있습니다. withContext는 Android 개발에서 가장 많이 사용되는 빌더입니다.

세 가지 주요 코루틴 빌더

launch — 코루틴을 시작하고 Job을 반환하며 결과를 반환하지 않습니다(fire-and-forget). async — 코루틴을 시작하고 await를 통해 결과를 얻을 수 있는 Deferred를 반환합니다. runBlocking — 코루틴을 실행하기 위해 현재 스레드를 차단합니다(테스트 및 main 함수 전용). 빌더 선택은 시나리오에 따라 다릅니다: launch는 이벤트 및 업데이트에 적합하고, async는 결과가 필요한 작업에, runBlocking은 테스트 또는 진입점에만 사용합니다.

Kotlin에서 코루틴을 사용한 코드 예제

세 가지 실용적인 시나리오를 살펴보겠습니다: launch를 사용한 기본 코루틴, async를 사용한 병렬 호출, SupervisorJob을 사용한 오류 처리.

launch로 코루틴 시작하기

viewModelScope.launch는 ViewModel 컨텍스트에서 코루틴을 시작합니다. ViewModel이 정리되면 코루틴이 자동으로 취소됩니다.

kotlin
class ProfileViewModel : ViewModel() {
    fun loadUser() {
        viewModelScope.launch(Dispatchers.IO) {
            val user = api.fetchUser()
            withContext(Dispatchers.Main) {
                showUser(user)
            }
        }
    }
}

async를 사용한 병렬 요청

coroutineScope와 async는 세 개의 요청을 병렬로 시작합니다. 결과는 .await()를 통해 수집됩니다. 요청 중 하나라도 실패하면 모두 취소됩니다.

kotlin
suspend fun loadDashboard(): Dashboard = coroutineScope {
    val user = async { api.fetchUser() }
    val posts = async { api.fetchPosts() }
    val stats = async { api.fetchStats() }
    Dashboard(user.await(), posts.await(), stats.await())
}

SupervisorJob을 사용한 오류 처리

SupervisorJob은 각 코루틴이 독립적으로 완료될 수 있게 합니다. 하나의 요청에서 오류가 발생해도 다른 요청이 취소되지 않습니다.

kotlin
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
    try { api.fetchUsers() } catch (e: Exception) { log(e) }
}
scope.launch {
    try { api.fetchPosts() } catch (e: Exception) { log(e) }
}

코루틴 vs 스레드: 비교 및 시나리오

스레드(Thread)는 운영 체제 프리미티브입니다. 각 스레드는 자체 스택(~1MB)을 가지며 생성 및 전환에 시스템 호출이 필요합니다. 코루틴은 언어 프리미티브로, OS에 묶여 있지 않습니다. 상태 저장을 위해 Continuation을 사용하고 시스템 호출 없이 런타임 수준에서 전환합니다.

  • 메모리 — 스레드 ~1MB, 코루틴 ~100바이트. 10,000배 차이
  • 생성 — 스레드 ~1µs syscall, 코루틴 ~0.01µs(JVM 수준)
  • 전환 — 스레드 ~0.1µs(syscall), 코루틴 ~0.001µs(continuation)
  • 최대 — 수천 개의 스레드 vs 수백만 개의 코루틴(장치당)
  • 취소 — 스레드는 외부에서 취소 불가(사용 중단된 Thread.stop), 코루틴은 Job.cancel()로 가능

Google(2025)에 따르면, 스레드 대신 코루틴을 사용하면 Android 애플리케이션의 백그라운드 작업 메모리 소비가 90~95% 감소합니다. 모든 최신 Android 라이브러리(Retrofit, Room, WorkManager)는 suspend 함수를 통한 코루틴 지원을 내장하고 있습니다. Ktor(JetBrains의 HTTP 클라이언트 프레임워크)도 완전히 코루틴 위에 구축되어 콜백 API 없이 각 요청에 suspend 함수를 제공합니다. Room은 DAO의 suspend 함수를 통해 코루틴을 지원하여 메인 스레드를 차단하지 않고 데이터베이스 쿼리를 실행할 수 있습니다.

코루틴 대신 스레드를 사용해야 하는 경우

스레드는 JNI를 통한 네이티브 코드, 장기 실행 CPU 집약적 차단 호출(비디오 렌더링, 시뮬레이션) 및 C 라이브러리와의 통합에 필요합니다. 그 외의 모든 경우에는 코루틴을 사용합니다.

자주 묻는 질문

코루틴과 스레드의 차이점은 무엇인가요?

코루틴은 기존 스레드에서 실행되는 일시 중단 가능한 작업 단위입니다. 스레드는 자체 스택을 가진 시스템 리소스입니다. 코루틴은 스레드보다 수천 배 가볍고 일시 중단 시 리소스를 차단하지 않습니다.

Dispatchers.IO란 무엇이며 Default와 어떻게 다른가요?

Dispatchers.IO는 차단 I/O 작업(네트워크, 파일)용으로 설계되었으며 필요 시 새 스레드를 생성할 수 있습니다. Dispatchers.Default는 CPU 집약적 계산을 위해 고정 크기 풀(CPU 코어 수)을 가집니다.

실행 중인 코루틴을 어떻게 취소하나요?

Job.cancel()로 코루틴과 모든 하위 코루틴을 취소합니다. 코루틴 내에서 취소를 확인하려면 ensureActive()를 사용하세요 — 코루틴이 취소된 경우 CancellationException을 던집니다.

코루틴을 RxJava와 함께 사용할 수 있나요?

네 — kotlinx-coroutines-rx3 라이브러리를 통해 가능합니다. Observable/Single을 suspend 함수로 변환하고 flowable을 통해 되돌리기 위한 awaitSingle, awaitFirst 등의 함수를 제공합니다.

코루틴에서 Flow란 무엇인가요?

Flow는 콜드 비동기 데이터 스트림으로, 코루틴에서 RxJava Observable에 해당합니다. Flow는 값을 순차적으로 방출하고 예외 또는 성공으로 완료됩니다. map, filter, catch 및 기타 연산자를 지원합니다.

요약

  • Coroutines — Continuation Passing Style을 통한 비차단 일시 중단이 있는 경량 Kotlin 스레드
  • suspend — 일시 중단 가능 함수를 표시하는 키워드
  • Dispatchers가 스레드 풀 관리: Main, IO, Default
  • CoroutineScope가 코루틴 수명 주기를 구성 요소(Activity, ViewModel)에 바인딩
  • launch는 결과 없이 코루틴 시작, async/await는 결과 있음
  • Structured Concurrency가 하위 코루틴의 계층적 취소 보장
  • 코루틴 vs 스레드 — 코루틴이 10,000배 가볍고 Android 표준

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기