Kotlin

[Kotlin/코루틴] CoroutineBuilder와 구성 요소, 한눈에 이해하기

yujinius 2025. 1. 15. 14:09

 

Coroutine은 세 가지 주요 구성 요소로 이루어져 있다

바로 CoroutineScope, CoroutineContext, 그리고 CoroutineBuilder이다.

코루틴 구성 요소 3가지

  • CoroutineScope: 코루틴의 실행 범위를 정의.
  • CoroutineContext: 디스패처(Dispatcher)와 잡(Job) 등 코루틴의 실행 환경을 포함.
  • CoroutineBuilder: 코루틴을 생성하고 실행하는 빌더(launch, async 등).

그중에서도 이번 포스팅에서는 CoroutineBuilder를 정리해보고자 한다.

Scope Builder과 Coroutine 실행 빌더(launch, async, runBlocking), withContext, suspend, delay 등을 훑어보고자 한다.


🌠 CoroutineBuilder  

  • 비동기적인 작업을 선언하고 실행하기 위한 함수
  • 새로운 코루틴을 생성하는 역할을 한다.

✨ Scope Builder과 Coroutine 실행 빌더 

코루틴을 생성하고 실행하는데 사용되는 빌더 함수들


1. Scope Builder

Scope Builder는 새로운 Coroutine Scope를 생성하는 데 사용된다.

코루틴 실행 환경(스코프)을 정의하며, 해당 스코프에서 실행되는 모든 코루틴은 특정 컨텍스트(예: Job, Dispatcher)를 공유한다.

주요 특징:

  • 코루틴의 실행 환경을 설정.
  • 스코프 안에서 실행되는 모든 코루틴의 취소, 컨텍스트를 제어.
  • 구조적 동시성(Structured Concurrency)을 지원.

주요 Scope Builder:

  1. coroutineScope:
    • 새로운 일시적 스코프를 생성하며, 내부 코루틴이 모두 완료될 때까지 기다린다.
    • 특징
      • coroutineScope는 상위 스코프의 CoroutineContext를 상속받는다.
      • 상위 스코프의 Job이 부모로 설정되어, 상위 스코프가 취소되면 coroutineScope와 그 안의 모든 자식 코루틴도 취소된다.
      • coroutineScope 내부에서 시작된 모든 자식 코루틴은 형제 관계로 묶인다.
      • 하나의 자식 코루틴이 실패하면 다른 자식 코루틴도 모두 취소된다.
    • 사용 사례
      • 연관된 작업을 병렬로 실행하며, 하나의 실패로 전체 작업을 취소해야 할 때 유용하다
      • suspend fun exampleCoroutineScope() = coroutineScope {
            launch {
                println("작업 1 시작")
                delay(500)
                println("작업 1 완료")
            }
            launch {
                println("작업 2 시작")
                throw RuntimeException("작업 2 실패")
            }
            println("모든 작업 완료") // 실행되지 않음
        }
    • 참고 링크 : https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html
  2. supervisorScope:
    • coroutineScope와 유사하지만, 자식 코루틴의 예외가 전파되지 않는다. (자식 코루틴 간 독립성 보장)
    • 특징
      • SupervisorJob을 포함하는 CoroutineScope를 생성한 뒤, 지정된 suspend 블록을 해당 스코프에서 실행한다.
      • 자식 코루틴 중 하나가 실패해도, 다른 자식 코루틴에는 영향을 주지 않는다.
      • 상위 스코프의 CoroutineContext를 상속받는다.
      • 블록에서 발생한 예외는 모든 자식 코루틴을 취소한다.
    • 사용 사례
      • 일부 작업이 실패해도 다른 작업은 계속 진행되어야 할 때
      •  
    • 참고: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/supervisor-scope.html
suspend fun exampleSupervisorScope() = supervisorScope {
        launch {
            println("작업 1 시작")
            delay(500)
            println("작업 1 완료")
        }
        launch {
            println("작업 2 시작")
            throw RuntimeException("작업 2 실패")
        }
        println("모든 작업 완료") // 실행됨
    }

차이점 정리

특징  coroutineScope  supervisorScope
자식 간 연관성 자식 코루틴 중 하나가 실패하면 모두 취소 자식 코루틴은 독립적으로 실행됨
구조적 동시성 모든 자식 작업이 완료될 때 반환 모든 자식 작업이 완료될 때 반환
실패 전파 자식의 실패가 상위 및 다른 자식에 전파됨 자식의 실패가 상위와 다른 자식에 전파되지 않음
사용 사례 연관된 작업의 병렬 실행 독립된 작업의 병렬 실행

 


2. Coroutine 실행 빌더

Coroutine 실행 빌더는 새로운 코루틴을 실행하기 위한 도구이다.

Scope Builder가 생성한 스코프를 사용해 코루틴 실행을 정의하고 비동기 작업을 수행한다.

주요 특징:

  • 비동기 작업을 실행하며, 반환값(Job 또는 Deferred)을 제공
  • CoroutineScope와 함께 사용됨
  • 스코프 내에서 실행되어 구조적 동시성을 유지한다.

주요 Coroutine 실행 빌더:

  1. launch:
    • 비동기 작업 실행.
    • 결과값을 반환하지 않으며, Job 객체를 반환.
    • 주로 실행만 하고 결과 필요 없는 작업에 사용한다. job.cancel() 가능.
    val job = CoroutineScope(Dispatchers.Default).launch {
        println("Task executed by launch")
    }
    
    
  2. async:
    • 비동기 작업 실행.
    • 결과값을 반환하며, Deferred 객체를 통해 값을 비동기로 가져올 수 있음.
    • await()로 결과를 얻거나, 필요한 경우 작업을 취소할 수 있음.
    val deferred = CoroutineScope(Dispatchers.Default).async {
        delay(1000)
        "Result from async"
    }
    println(deferred.await())
    
    
  3. runBlocking:
    • 현재 스레드를 블로킹하여 코루틴을 실행.
    • 주로 테스트 목적으로 사용되며, 일반 코드에서는 지양.
    runBlocking {
        println("Running in runBlocking")
    }
    
    

Scope Builder와 실행 빌더 비교

  Scope Builder  Coroutine 실행 빌더
목적 코루틴의 실행 환경(스코프) 생성 코루틴을 실행하고 작업 처리
주요 역할 스코프 내부의 모든 코루틴을 관리 및 구조적 동시성 지원 비동기 작업 실행 및 제어
대표 함수 coroutineScope, supervisorScope launch, async, runBlocking
결과값 반환값 없음 Job 또는 Deferred
취소 전파 부모 스코프에서 하위 코루틴으로 전파 스코프 내에서 실행되며, 상위 스코프에서 제어
사용 예시 여러 작업을 구조적으로 관리할 때 사용 단일 작업을 비동기로 실행할 때 사용

 


정리

  1. Scope Builder는 코루틴 실행 환경을 정의하고, 구조적 동시성을 유지하는 역할을 한다.
  2. Coroutine 실행 빌더는 Scope Builder가 생성한 스코프 안에서 코루틴을 실행하는 역할을 한다.
  3. Scope Builder를 통해 생성된 스코프 안에서 실행 빌더를 사용해 비동기 작업을 실행한다.

✨ 컨텍스트 변경 코루틴 빌더 

withContext

  • withContext는 코루틴의 컨텍스트(Context)를 변경하여 특정 디스패처(스레드 풀)에서 코드를 실행할 수 있도록 하는 코루틴 빌더이다.
  • 이는 주로 스레드 전환이나 특정 컨텍스트에서 작업을 실행해야 할 때 사용된다.

특징

  1. 컨텍스트 전환:
    • 기존 코루틴의 컨텍스트를 변경하여, 다른 디스패처에서 코드를 실행.
    • 기존 코루틴은 suspend 상태가 되고, 새로운 컨텍스트에서 코드를 실행한 후 원래 컨텍스트로 복귀.
  2. suspend 함수:
    • withContext는 suspend 함수로, 호출된 코루틴이 일시 중단되며 컨텍스트 전환이 완료될 때까지 기다린다.
  3. 반환값:
    • withContext 블록 내부에서 실행된 코드의 결과값을 반환
  4. 구조적 동시성 보장:
    • withContext는 현재 코루틴 스코프 내에서 실행되므로, 구조적 동시성을 유지

 

UI 업데이트와 데이터 처리로 예를 들어보자

import kotlinx.coroutines.*

fun main() = runBlocking {
    val data = withContext(Dispatchers.IO) {
        // 백그라운드에서 데이터 처리
        fetchDataFromNetwork()
    }

    // 메인 스레드에서 UI 업데이트
    withContext(Dispatchers.Main) {
        println("Updating UI with data: $data on thread: ${Thread.currentThread().name}")
    }
}

suspend fun fetchDataFromNetwork(): String {
    delay(1000) // 네트워크 지연 시뮬레이션
    return "Fetched Data"
}

결과:

Updating UI with data: Fetched Data on thread: main
  • 데이터를 백그라운드 스레드에서 처리한 후, 메인 스레드에서 UI를 업데이트.

예외 처리

withContext는 내부에서 발생한 예외를 호출한 코루틴으로 전파

import kotlinx.coroutines.*

fun main() = runBlocking {
    try {
        withContext(Dispatchers.IO) {
            throw Exception("Something went wrong")
        }
    } catch (e: Exception) {
        println("Caught exception: ${e.message}")
    }

결과:

Caught exception: Something went wrong

 

withContext와 launch, async 비교

  withContext   launch  async
반환값 블록 내부 실행 결과 없음 (Job 반환) 결과값 (Deferred) 반환
컨텍스트 전환 특정 컨텍스트에서 작업 실행 스코프 내에서 실행 스코프 내에서 실행
구조적 동시성 유지 유지 유지
사용 목적 스레드 전환 및 컨텍스트 변경 Fire-and-Forget 작업 비동기 작업의 결과를 반환

 

withContext를 사용해야 하는 경우

  1. 스레드 전환:
    • I/O 작업(Dispatchers.IO)과 CPU 작업(Dispatchers.Default)을 명확히 구분.
    • 예: 백그라운드 작업 후 UI 업데이트.
  2. 단일 작업:
    • 단일 작업의 컨텍스트를 전환하고, 결과값을 받아야 할 때.
  3. 구조적 동시성 유지:
    • 호출한 스코프 내에서 작업이 실행되므로, 구조적 동시성을 유지하며 코드의 안정성을 높임.

suspend와 delay 

1. suspend란?

  • suspend는 코루틴을 일시 중단할 수 있는 함수를 정의하기 위한 키워드
  • 코루틴의 가장 중요한 특징 중 하나는 작업을 중단(suspend) 했다가, 필요할 때 재개(resume) 할 수 있다는 점
  • suspend 키워드는 이러한 중단 가능성을 표시하는 역할

suspend의 특징

  1. 코루틴 내부에서만 호출 가능:
    • suspend 함수는 반드시 코루틴 내부 또는 다른 suspend 함수에서 호출되어야 한다.
    • 일반적인 함수에서는 suspend 함수를 직접 호출할 수 없다.
  2. 스레드를 블로킹하지 않음:
    • 작업을 중단할 때 해당 스레드는 반환되며, 다른 작업을 처리할 수 있도록 한다.
    • 작업이 재개될 때 다른 스레드에서 이어서 실행될 수도 있다.
  3. 비동기 작업을 쉽게 처리:
    • suspend 함수를 통해 비동기 작업을 동기 코드처럼 간단하게 작성할 수 있다.

suspend 함수 예제

import kotlinx.coroutines.*

suspend fun mySuspendFunction() {
    println("Start suspend function")
    delay(1000) // 1초 동안 중단
    println("End suspend function")
}

fun main() = runBlocking {
    println("Before suspend function")
    mySuspendFunction() // 코루틴 내부에서 호출
    println("After suspend function")
}


2. delay란?

  • delay는 suspend 함수의 한 예로, 지정된 시간 동안 코루틴을 중단(suspend) 한다.
  • 코루틴이 중단되면 해당 스레드가 반환되어 다른 코루틴이나 작업이 실행될 수 있다.

delay와 Thread.sleep의 차이

  1. delay:
    • 스레드를 블로킹하지 않음.
    • 코루틴을 일시 중단하고, 스레드를 반환.
    • 코루틴의 동시성을 유지하며, 다른 코루틴이 실행될 수 있도록 자원을 효율적으로 활용.
  2. Thread.sleep:
    • 스레드를 블로킹.
    • 해당 스레드가 반환되지 않아 다른 작업을 실행할 수 없음.

delay 예제

import kotlinx.coroutines.*

fun main() = runBlocking {
    println("Task starts")
    delay(1000) // 1초 동안 중단
    println("Task ends after 1 second")
}

실행 결과:

Task starts
(1초 후)
Task ends after 1 second


3. suspend와 delay의 관계

  • delay는 suspend 함수
  • 따라서 코루틴 내부 또는 다른 suspend 함수에서만 호출할 수 있다.
  • delay는 코루틴을 중단하고, 지정된 시간이 지난 후 다시 작업을 재개한다.

4. suspend 함수의 내부 동작

  1. 중단 지점 생성:
    • suspend 함수가 호출되면, 현재 실행 상태(위치, 변수 등)가 Continuation 객체에 저장된다.
    • Continuation 객체란?
      • Continuation은 코루틴의 실행 상태를 캡슐화한 객체로, 다음과 같은 정보를 포함
        1. 현재 실행 위치: 중단된 작업의 위치(코드 흐름).
        2. 컨텍스트(Context): 작업을 실행하기 위한 실행 환경(Dispatcher, Job 등).
        3. 필요한 변수: 중단된 상태에서 사용 중이던 지역 변수 등.
        4. 재개 메서드: resume 또는 resumeWithException을 호출해 작업을 재개.
  2. 컨텍스트 반환:
    • suspend 함수는 실행 중단 시 스레드를 반환하며, 스레드는 다른 작업을 처리할 수 있다.
    • Continuation은 나중에 작업을 이어가기 위한 정보를 유지한다.
  3. 재개(resume):
    • 작업이 준비되면 저장된 상태에서 다시 실행을 시작한다.
      • Continuation은 재개(resume)될 때, 디스패처가 작업을 적절한 스레드에서 실행한다.
    • 이때 다른 스레드에서 실행될 수도 있다.

5. suspend 함수와 동기/비동기 함수 비교

특징  동기 함수 비동기 함수 suspend 함수
실행 방식 호출되면 결과가 반환될 때까지 블로킹 백그라운드에서 실행 실행 중단 가능 (중단 후 재개 가능)
스레드 반환 여부 반환하지 않음 반환하지 않음 반환하여 다른 작업 실행 가능
호출 위치 어디서나 호출 가능 어디서나 호출 가능 코루틴 내부 또는 다른 suspend 함수