CoroutineScope — 정의, 생명주기 및 코루틴에서의 동작

저자: IT Sectr 게시일: 2026-06-22 읽는 시간: 10 분

CoroutineScope는 코루틴의 생명주기를 정의하고 새 코루틴을 시작하기 위한 컨텍스트를 제공하는 Kotlin 인터페이스입니다. Kotlin 문서(2025)에 따르면, 각 CoroutineScope 인스턴스는 CoroutineContext를 포함하며 그 안에서 시작된 모든 코루틴을 관리합니다. Scope가 취소(cancel)되면 모든 하위 코루틴이 자동으로 취소되어 메모리 누수를 방지합니다.

핵심 요점

  • CoroutineScope — 단일 CoroutineContext 필드를 가진 인터페이스로, 코루틴의 생명주기를 정의합니다
  • Job — 취소를 담당하는 컨텍스트 요소: scope 취소 시 모든 하위 코루틴이 취소됩니다
  • 구조적 동시성 — 하위 코루틴이 상위 scope에 바인딩되는 원칙
  • GlobalScope — 애플리케이션 전체 scope로, 메모리 누수 위험으로 인해 권장되지 않습니다
  • supervisorScope — 하나의 하위 코루틴을 취소해도 나머지가 취소되지 않는 특수 scope

Kotlin에서 CoroutineScope란?

CoroutineScope는 kotlinx.coroutines 라이브러리의 기본 인터페이스로, 코루틴을 위한 컨테이너 역할을 합니다. 코루틴 생명주기의 경계를 정의합니다: scope가 완료되면 그 안의 모든 코루틴이 자동으로 취소됩니다.

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

인터페이스에는 단 하나의 필드만 있습니다 — coroutineContext입니다. 이를 통해 scope는 그 안에서 시작된 모든 코루틴에 디스패처(Dispatcher), 작업(Job), 예외 처리기 및 기타 컨텍스트 요소를 제공합니다.

kotlinx.coroutines 라이브러리에서의 역할

모든 코루틴 시작 함수 — launch, async, runBlocking — 는 CoroutineScope의 확장 함수입니다. 즉, scope 객체가 있을 때만 호출할 수 있습니다. 이 설계는 각 코루틴에 명확하게 정의된 상위 요소와 생명주기가 있음을 보장합니다.

CoroutineScope 사용처

Android에서는 각 아키텍처 구성 요소에 고유한 scope가 있습니다: ViewModel용 viewModelScope, Activity/Fragment용 lifecycleScope. 서버 애플리케이션에서는 scope를 HTTP 요청 또는 데이터베이스 연결 풀에 바인딩할 수 있습니다.

CoroutineScope 작동 방식: Job과 구조적 동시성

CoroutineScope의 내부 작동 방식을 이해하려면 Job의 개념과 구조적 동시성의 원칙을 숙지해야 합니다.

Job — 코루틴의 작업

각 코루틴은 시작 시 Job 객체(async의 경우 Deferred)를 반환합니다. Job은 유한한 생명주기를 가진 작업을 나타냅니다: New, Active, Completing, Completed, Cancelling, Cancelled. Job 객체는 트리 구조를 형성합니다:

  • 상위 Job — 코루틴이 시작된 scope
  • 하위 Job — launch/async를 통해 시작된 각 코루틴
  • 상위 취소 → 모든 하위 취소
  • 하위에서 예외 발생 → 상위 취소(supervisorScope 제외)

구조적 동시성의 원칙

구조적 동시성은 Kotlin Coroutines의 핵심 아키텍처 원칙으로, 코루틴의 생명주기가 해당 scope의 생명주기에 바인딩됩니다. 이는 scope 완료 후에도 코루틴이 계속 존재하는 “발사 후 잊기” 모델과 대조됩니다. 구조적 동시성의 장점:

  • 예측 가능한 생명주기 — scope가 완료되면 모든 코루틴이 중단됩니다
  • 자동 오류 처리 — 하위 코루틴의 예외가 scope로 전파됩니다
  • 메모리 누수 없음 — scope 완료 후 실행 중인 코루틴이 없습니다
  • 명확한 계층 구조 — 코드가 병렬 작업의 논리적 구조를 반영합니다

CoroutineScope 생명주기

scope.cancel()이 호출되면 scope의 Job이 Cancelled 상태로 전환되어 모든 하위 Job을 재귀적으로 취소합니다. 취소 후에는 새 CoroutineScope 인스턴스를 생성해야만 scope를 재사용할 수 있습니다.

CoroutineScope 생성 및 구성

CoroutineScope는 팩토리 함수를 통해 또는 클래스에 인터페이스를 구현하여 생성할 수 있습니다. 두 가지 방법을 살펴보겠습니다.

팩토리 함수 CoroutineScope()

kotlin
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

scope.launch {
    println("Running on ${Thread.currentThread().name}")
}

팩토리 함수는 CoroutineContext를 받아 지정된 컨텍스트로 scope를 생성합니다. 예제는 CPU 집약적 작업에 Dispatchers.Default를 사용하고 하위 코루틴 간 예외를 격리하는 SupervisorJob을 사용합니다.

컴포지션을 통한 인터페이스 구현

kotlin
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 구현을 위임할 수 있습니다:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // coroutine runs in DataLoader scope
        }
    }
}

이 방법은 클래스 자체가 scope이고 코루틴 시작 메서드를 제공하려는 경우 편리합니다. 그러나 주의하세요: 클래스는 CoroutineScope의 모든 메서드(cancel 포함)를 상속받아 캡슐화를 깨뜨릴 수 있습니다.

GlobalScope vs 커스텀 CoroutineScope

GlobalScope는 전체 애플리케이션의 싱글톤 CoroutineScope입니다. 프로덕션 코드에서의 사용은 공식적으로 권장되지 않습니다.

GlobalScope의 문제점

  • 구조적 동시성 부족 — GlobalScope의 코루틴은 구성 요소 생명주기에 바인딩되지 않습니다
  • 메모리 누수 — Activity/Fragment가 닫힌 후에도 코루틴이 계속 실행될 수 있습니다
  • 테스트 어려움 — GlobalScope는 테스트에서 대체할 수 없습니다
  • 통제되지 않은 리소스 소비 — 많은 코루틴이 예상보다 오래 실행될 수 있습니다

GlobalScope가 정당화되는 경우

JetBrains는 드문 시나리오에서만 GlobalScope를 허용합니다: 모든 Activity가 닫힌 후에도 계속 실행되어야 하는 애플리케이션 수준의 백그라운드 프로세스(예: 데이터 동기화, 분석). 그러나 이러한 경우에도 CoroutineScope(SupervisorJob())으로 자체 scope를 만드는 것이 좋습니다.

권장 사항

명시적 생명주기 관리가 있는 커스텀 CoroutineScope를 항상 사용하세요. Android에서는 viewModelScope와 lifecycleScope입니다. 서버 애플리케이션에서는 각 요청 또는 연결 풀에 대해 scope를 만듭니다.

coroutineScope vs supervisorScope: 차이점

두 함수 모두 병렬 작업을 위한 임시 scope를 만드는 일시 중단 함수이지만, 예외 처리 동작이 근본적으로 다릅니다.

특성coroutineScopesupervisorScope
오류 시 동작하위 코루틴의 예외가 다른 모든 코루틴을 취소하위 코루틴의 예외가 다른 코루틴을 취소하지 않음
오류 전파예, 첫 번째 예외가 외부로 전파됨예, 첫 번째 예외가 외부로 전파됨
기본 JobJob() — 하위가 상위에 바인딩됨SupervisorJob() — 하위가 서로 독립적
일반적인 사용 사례여러 단계의 원자적 작업독립적인 병렬 작업(UI 로드)

coroutineScope를 선택해야 하는 경우

여러 병렬 작업이 단일 원자적 작업을 형성하는 경우 coroutineScope를 사용하세요. 예를 들어, 세 대의 서버에서 데이터를 로드하는 경우: 하나의 요청이 실패하면 나머지는 의미가 없습니다.

kotlin
suspend fun loadProductPage(): ProductPage = coroutineScope {
    val product = async { api.getProduct() }
    val reviews = async { api.getReviews() }
    ProductPage(product.await(), reviews.await())
}

getProduct 또는 getReviews가 예외를 throw하면 두 코루틴 모두 취소되고 예외가 호출 코드로 전파됩니다.

supervisorScope를 선택해야 하는 경우

병렬 작업이 서로 독립적인 경우 supervisorScope를 사용하세요. 예를 들어, 여러 독립 섹션에서 프로필 데이터를 로드하는 경우: 추천 섹션이 실패해도 프로필 헤더와 친구 목록은 표시되어야 합니다.

CoroutineScope 사용 시 일반적인 실수

Kotlin에서 CoroutineScope 사용 시 개발자들이 가장 흔히 저지르는 실수를 살펴보겠습니다.

실수 1: Scope 취소를 잊는 경우

가장 흔한 코루틴 누수 시나리오는 구성 요소 종료 시 cancel을 호출하지 않고 scope를 생성하는 것입니다. Scope가 취소되지 않으면 코루틴이 계속 실행되어 객체에 대한 참조를 유지합니다. Android에서는 자동으로 취소되는 viewModelScope 또는 lifecycleScope를 사용하세요.

실수 2: Activity 또는 Fragment에서 GlobalScope 사용

GlobalScope는 Android 구성 요소 생명주기를 무시합니다. Activity가 닫힌 후 GlobalScope에서 시작된 코루틴은 계속 실행되며 UI를 업데이트하려고 시도하여 충돌을 일으킵니다. UI 구성 요소에는 항상 lifecycleScope를 사용하세요.

실수 3: 취소된 scope 재사용

cancel()을 호출한 후에는 scope를 재사용할 수 없습니다 — 그 안의 모든 코루틴이 이미 완료되었습니다. 팩토리 함수를 통해 새 CoroutineScope 인스턴스를 만드세요. Job()은 재활성화를 지원하지 않습니다.

실수 4: CoroutineScope 인터페이스의 잘못된 위임

by로 위임하면 클래스가 공개 cancel() 메서드를 얻어 어디서든 호출될 수 있으며 캡슐화가 깨집니다. 인터페이스를 위임하는 대신 scope를 비공개 필드로 저장하세요.

자주 묻는 질문

CoroutineScope와 CoroutineContext의 차이는 무엇인가요?

CoroutineScope는 CoroutineContext를 소유하고 코루틴 생명주기를 담당하는 인터페이스입니다. CoroutineContext는 코루틴이 “어떻게” 실행되는지 정의하는 요소(디스패처, job, 오류 처리기)의 집합입니다. 한 가지 차이점: scope는 코루틴을 생성하고, 컨텍스트는 그 동작을 제어합니다.

SupervisorJob으로 CoroutineScope를 만들 수 있나요?

예, 표준 패턴입니다: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob은 하나의 하위 코루틴에서 예외가 발생해도 다른 하위 코루틴이 연쇄적으로 취소되는 것을 방지합니다. 이는 한 작업의 오류가 다른 작업을 중단해서는 안 되는 독립적인 병렬 작업에 유용합니다.

CoroutineScope는 몇 개의 코루틴을 포함할 수 있나요?

scope의 코루틴 수에는 제한이 없습니다 — 사용 가능한 메모리와 디스패처 설정에 의해서만 제한됩니다. 실질적인 한계는 일반적으로 단일 scope에서 수천 개의 활성 코루틴입니다. 그러나 많은 수의 코루틴은 아키텍처 문제를 나타낼 수 있습니다.

CoroutineScope가 있는 코드를 테스트하는 방법은?

올바른 방법은 생성자를 통해 클래스에 scope를 전달하거나 kotlinx-coroutines-test의 runBlockingTest / runTest를 사용하는 것입니다. 테스트에서는 scope를 TestCoroutineDispatcher로 대체하고 코루틴 실행을 수동으로 제어할 수 있습니다.

코루틴이 자체 scope를 가질 수 있나요?

아니요, scope는 코루틴의 외부 컨테이너입니다. 코루틴 자체는 scope가 아닙니다. 그러나 코루틴 내에서 coroutineScope 또는 supervisorScope를 통해 새 scope를 만들어 하위 코루틴을 병렬로 시작할 수 있습니다.

요약

  • CoroutineScope — coroutineContext 필드를 가진 인터페이스로, 그 안에서 시작된 코루틴의 생명주기를 정의합니다
  • 구조적 동시성 — scope 취소 시 모든 하위 코루틴이 자동으로 취소되어 메모리 누수를 방지합니다
  • Job과 SupervisorJob — 두 가지 오류 처리 모드: 연쇄 취소(Job)와 격리된 오류(SupervisorJob)
  • GlobalScope — 생명주기 바인딩 부족으로 프로덕션에 비권장
  • coroutineScope vs supervisorScope — 원자적 병렬 작업 vs 독립적인 병렬 작업
  • viewModelScope와 lifecycleScope — Android용 준비된 scope, 구성 요소 종료 시 자동 취소
  • 팩토리 함수 — CoroutineContext + 명시적 cancel 호출을 통한 scope 생성의 선호 방식

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

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

프로젝트 논의

더 읽어보기