CoroutineScope는 코루틴의 생명주기를 정의하고 새 코루틴을 시작하기 위한 컨텍스트를 제공하는 Kotlin 인터페이스입니다. Kotlin 문서(2025)에 따르면, 각 CoroutineScope 인스턴스는 CoroutineContext를 포함하며 그 안에서 시작된 모든 코루틴을 관리합니다. Scope가 취소(cancel)되면 모든 하위 코루틴이 자동으로 취소되어 메모리 누수를 방지합니다.
핵심 요점
CoroutineScope는 kotlinx.coroutines 라이브러리의 기본 인터페이스로, 코루틴을 위한 컨테이너 역할을 합니다. 코루틴 생명주기의 경계를 정의합니다: scope가 완료되면 그 안의 모든 코루틴이 자동으로 취소됩니다.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
인터페이스에는 단 하나의 필드만 있습니다 — coroutineContext입니다. 이를 통해 scope는 그 안에서 시작된 모든 코루틴에 디스패처(Dispatcher), 작업(Job), 예외 처리기 및 기타 컨텍스트 요소를 제공합니다.
모든 코루틴 시작 함수 — launch, async, runBlocking — 는 CoroutineScope의 확장 함수입니다. 즉, scope 객체가 있을 때만 호출할 수 있습니다. 이 설계는 각 코루틴에 명확하게 정의된 상위 요소와 생명주기가 있음을 보장합니다.
Android에서는 각 아키텍처 구성 요소에 고유한 scope가 있습니다: ViewModel용 viewModelScope, Activity/Fragment용 lifecycleScope. 서버 애플리케이션에서는 scope를 HTTP 요청 또는 데이터베이스 연결 풀에 바인딩할 수 있습니다.
CoroutineScope의 내부 작동 방식을 이해하려면 Job의 개념과 구조적 동시성의 원칙을 숙지해야 합니다.
각 코루틴은 시작 시 Job 객체(async의 경우 Deferred)를 반환합니다. Job은 유한한 생명주기를 가진 작업을 나타냅니다: New, Active, Completing, Completed, Cancelling, Cancelled. Job 객체는 트리 구조를 형성합니다:
구조적 동시성은 Kotlin Coroutines의 핵심 아키텍처 원칙으로, 코루틴의 생명주기가 해당 scope의 생명주기에 바인딩됩니다. 이는 scope 완료 후에도 코루틴이 계속 존재하는 “발사 후 잊기” 모델과 대조됩니다. 구조적 동시성의 장점:
scope.cancel()이 호출되면 scope의 Job이 Cancelled 상태로 전환되어 모든 하위 Job을 재귀적으로 취소합니다. 취소 후에는 새 CoroutineScope 인스턴스를 생성해야만 scope를 재사용할 수 있습니다.
CoroutineScope는 팩토리 함수를 통해 또는 클래스에 인터페이스를 구현하여 생성할 수 있습니다. 두 가지 방법을 살펴보겠습니다.
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("Running on ${Thread.currentThread().name}")
}
팩토리 함수는 CoroutineContext를 받아 지정된 컨텍스트로 scope를 생성합니다. 예제는 CPU 집약적 작업에 Dispatchers.Default를 사용하고 하위 코루틴 간 예외를 격리하는 SupervisorJob을 사용합니다.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
scope를 클래스 필드로 저장하고 수동으로 cleanup을 호출하여 취소합니다. 이 방법은 생명주기가 관리되는 구성 요소(예: 리포지토리 또는 관리자)에 적합합니다.
Kotlin은 by 키워드를 통해 CoroutineScope 구현을 위임할 수 있습니다:
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// coroutine runs in DataLoader scope
}
}
}
이 방법은 클래스 자체가 scope이고 코루틴 시작 메서드를 제공하려는 경우 편리합니다. 그러나 주의하세요: 클래스는 CoroutineScope의 모든 메서드(cancel 포함)를 상속받아 캡슐화를 깨뜨릴 수 있습니다.
GlobalScope는 전체 애플리케이션의 싱글톤 CoroutineScope입니다. 프로덕션 코드에서의 사용은 공식적으로 권장되지 않습니다.
JetBrains는 드문 시나리오에서만 GlobalScope를 허용합니다: 모든 Activity가 닫힌 후에도 계속 실행되어야 하는 애플리케이션 수준의 백그라운드 프로세스(예: 데이터 동기화, 분석). 그러나 이러한 경우에도 CoroutineScope(SupervisorJob())으로 자체 scope를 만드는 것이 좋습니다.
명시적 생명주기 관리가 있는 커스텀 CoroutineScope를 항상 사용하세요. Android에서는 viewModelScope와 lifecycleScope입니다. 서버 애플리케이션에서는 각 요청 또는 연결 풀에 대해 scope를 만듭니다.
두 함수 모두 병렬 작업을 위한 임시 scope를 만드는 일시 중단 함수이지만, 예외 처리 동작이 근본적으로 다릅니다.
| 특성 | coroutineScope | supervisorScope |
|---|---|---|
| 오류 시 동작 | 하위 코루틴의 예외가 다른 모든 코루틴을 취소 | 하위 코루틴의 예외가 다른 코루틴을 취소하지 않음 |
| 오류 전파 | 예, 첫 번째 예외가 외부로 전파됨 | 예, 첫 번째 예외가 외부로 전파됨 |
| 기본 Job | Job() — 하위가 상위에 바인딩됨 | SupervisorJob() — 하위가 서로 독립적 |
| 일반적인 사용 사례 | 여러 단계의 원자적 작업 | 독립적인 병렬 작업(UI 로드) |
여러 병렬 작업이 단일 원자적 작업을 형성하는 경우 coroutineScope를 사용하세요. 예를 들어, 세 대의 서버에서 데이터를 로드하는 경우: 하나의 요청이 실패하면 나머지는 의미가 없습니다.
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
getProduct 또는 getReviews가 예외를 throw하면 두 코루틴 모두 취소되고 예외가 호출 코드로 전파됩니다.
병렬 작업이 서로 독립적인 경우 supervisorScope를 사용하세요. 예를 들어, 여러 독립 섹션에서 프로필 데이터를 로드하는 경우: 추천 섹션이 실패해도 프로필 헤더와 친구 목록은 표시되어야 합니다.
Kotlin에서 CoroutineScope 사용 시 개발자들이 가장 흔히 저지르는 실수를 살펴보겠습니다.
가장 흔한 코루틴 누수 시나리오는 구성 요소 종료 시 cancel을 호출하지 않고 scope를 생성하는 것입니다. Scope가 취소되지 않으면 코루틴이 계속 실행되어 객체에 대한 참조를 유지합니다. Android에서는 자동으로 취소되는 viewModelScope 또는 lifecycleScope를 사용하세요.
GlobalScope는 Android 구성 요소 생명주기를 무시합니다. Activity가 닫힌 후 GlobalScope에서 시작된 코루틴은 계속 실행되며 UI를 업데이트하려고 시도하여 충돌을 일으킵니다. UI 구성 요소에는 항상 lifecycleScope를 사용하세요.
cancel()을 호출한 후에는 scope를 재사용할 수 없습니다 — 그 안의 모든 코루틴이 이미 완료되었습니다. 팩토리 함수를 통해 새 CoroutineScope 인스턴스를 만드세요. Job()은 재활성화를 지원하지 않습니다.
by로 위임하면 클래스가 공개 cancel() 메서드를 얻어 어디서든 호출될 수 있으며 캡슐화가 깨집니다. 인터페이스를 위임하는 대신 scope를 비공개 필드로 저장하세요.
자주 묻는 질문
CoroutineScope는 CoroutineContext를 소유하고 코루틴 생명주기를 담당하는 인터페이스입니다. CoroutineContext는 코루틴이 “어떻게” 실행되는지 정의하는 요소(디스패처, job, 오류 처리기)의 집합입니다. 한 가지 차이점: scope는 코루틴을 생성하고, 컨텍스트는 그 동작을 제어합니다.
예, 표준 패턴입니다: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob은 하나의 하위 코루틴에서 예외가 발생해도 다른 하위 코루틴이 연쇄적으로 취소되는 것을 방지합니다. 이는 한 작업의 오류가 다른 작업을 중단해서는 안 되는 독립적인 병렬 작업에 유용합니다.
scope의 코루틴 수에는 제한이 없습니다 — 사용 가능한 메모리와 디스패처 설정에 의해서만 제한됩니다. 실질적인 한계는 일반적으로 단일 scope에서 수천 개의 활성 코루틴입니다. 그러나 많은 수의 코루틴은 아키텍처 문제를 나타낼 수 있습니다.
올바른 방법은 생성자를 통해 클래스에 scope를 전달하거나 kotlinx-coroutines-test의 runBlockingTest / runTest를 사용하는 것입니다. 테스트에서는 scope를 TestCoroutineDispatcher로 대체하고 코루틴 실행을 수동으로 제어할 수 있습니다.
아니요, scope는 코루틴의 외부 컨테이너입니다. 코루틴 자체는 scope가 아닙니다. 그러나 코루틴 내에서 coroutineScope 또는 supervisorScope를 통해 새 scope를 만들어 하위 코루틴을 병렬로 시작할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.