
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:
- 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
- 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 실행 빌더:
- launch:
- 비동기 작업 실행.
- 결과값을 반환하지 않으며, Job 객체를 반환.
- 주로 실행만 하고 결과 필요 없는 작업에 사용한다. job.cancel() 가능.
val job = CoroutineScope(Dispatchers.Default).launch { println("Task executed by launch") } - async:
- 비동기 작업 실행.
- 결과값을 반환하며, Deferred 객체를 통해 값을 비동기로 가져올 수 있음.
- await()로 결과를 얻거나, 필요한 경우 작업을 취소할 수 있음.
val deferred = CoroutineScope(Dispatchers.Default).async { delay(1000) "Result from async" } println(deferred.await()) - runBlocking:
- 현재 스레드를 블로킹하여 코루틴을 실행.
- 주로 테스트 목적으로 사용되며, 일반 코드에서는 지양.
runBlocking { println("Running in runBlocking") }
Scope Builder와 실행 빌더 비교
| Scope Builder | Coroutine 실행 빌더 | |
| 목적 | 코루틴의 실행 환경(스코프) 생성 | 코루틴을 실행하고 작업 처리 |
| 주요 역할 | 스코프 내부의 모든 코루틴을 관리 및 구조적 동시성 지원 | 비동기 작업 실행 및 제어 |
| 대표 함수 | coroutineScope, supervisorScope | launch, async, runBlocking |
| 결과값 | 반환값 없음 | Job 또는 Deferred |
| 취소 전파 | 부모 스코프에서 하위 코루틴으로 전파 | 스코프 내에서 실행되며, 상위 스코프에서 제어 |
| 사용 예시 | 여러 작업을 구조적으로 관리할 때 사용 | 단일 작업을 비동기로 실행할 때 사용 |
정리
- Scope Builder는 코루틴 실행 환경을 정의하고, 구조적 동시성을 유지하는 역할을 한다.
- Coroutine 실행 빌더는 Scope Builder가 생성한 스코프 안에서 코루틴을 실행하는 역할을 한다.
- Scope Builder를 통해 생성된 스코프 안에서 실행 빌더를 사용해 비동기 작업을 실행한다.
✨ 컨텍스트 변경 코루틴 빌더
withContext
- withContext는 코루틴의 컨텍스트(Context)를 변경하여 특정 디스패처(스레드 풀)에서 코드를 실행할 수 있도록 하는 코루틴 빌더이다.
- 이는 주로 스레드 전환이나 특정 컨텍스트에서 작업을 실행해야 할 때 사용된다.
특징
- 컨텍스트 전환:
- 기존 코루틴의 컨텍스트를 변경하여, 다른 디스패처에서 코드를 실행.
- 기존 코루틴은 suspend 상태가 되고, 새로운 컨텍스트에서 코드를 실행한 후 원래 컨텍스트로 복귀.
- suspend 함수:
- withContext는 suspend 함수로, 호출된 코루틴이 일시 중단되며 컨텍스트 전환이 완료될 때까지 기다린다.
- 반환값:
- withContext 블록 내부에서 실행된 코드의 결과값을 반환
- 구조적 동시성 보장:
- 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를 사용해야 하는 경우
- 스레드 전환:
- I/O 작업(Dispatchers.IO)과 CPU 작업(Dispatchers.Default)을 명확히 구분.
- 예: 백그라운드 작업 후 UI 업데이트.
- 단일 작업:
- 단일 작업의 컨텍스트를 전환하고, 결과값을 받아야 할 때.
- 구조적 동시성 유지:
- 호출한 스코프 내에서 작업이 실행되므로, 구조적 동시성을 유지하며 코드의 안정성을 높임.
✨ suspend와 delay
1. suspend란?
- suspend는 코루틴을 일시 중단할 수 있는 함수를 정의하기 위한 키워드
- 코루틴의 가장 중요한 특징 중 하나는 작업을 중단(suspend) 했다가, 필요할 때 재개(resume) 할 수 있다는 점
- suspend 키워드는 이러한 중단 가능성을 표시하는 역할
suspend의 특징
- 코루틴 내부에서만 호출 가능:
- suspend 함수는 반드시 코루틴 내부 또는 다른 suspend 함수에서 호출되어야 한다.
- 일반적인 함수에서는 suspend 함수를 직접 호출할 수 없다.
- 스레드를 블로킹하지 않음:
- 작업을 중단할 때 해당 스레드는 반환되며, 다른 작업을 처리할 수 있도록 한다.
- 작업이 재개될 때 다른 스레드에서 이어서 실행될 수도 있다.
- 비동기 작업을 쉽게 처리:
- 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의 차이
- delay:
- 스레드를 블로킹하지 않음.
- 코루틴을 일시 중단하고, 스레드를 반환.
- 코루틴의 동시성을 유지하며, 다른 코루틴이 실행될 수 있도록 자원을 효율적으로 활용.
- 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 함수의 내부 동작
- 중단 지점 생성:
- suspend 함수가 호출되면, 현재 실행 상태(위치, 변수 등)가 Continuation 객체에 저장된다.
- Continuation 객체란?
- Continuation은 코루틴의 실행 상태를 캡슐화한 객체로, 다음과 같은 정보를 포함
- 현재 실행 위치: 중단된 작업의 위치(코드 흐름).
- 컨텍스트(Context): 작업을 실행하기 위한 실행 환경(Dispatcher, Job 등).
- 필요한 변수: 중단된 상태에서 사용 중이던 지역 변수 등.
- 재개 메서드: resume 또는 resumeWithException을 호출해 작업을 재개.
- Continuation은 코루틴의 실행 상태를 캡슐화한 객체로, 다음과 같은 정보를 포함
- 컨텍스트 반환:
- suspend 함수는 실행 중단 시 스레드를 반환하며, 스레드는 다른 작업을 처리할 수 있다.
- Continuation은 나중에 작업을 이어가기 위한 정보를 유지한다.
- 재개(resume):
- 작업이 준비되면 저장된 상태에서 다시 실행을 시작한다.
- Continuation은 재개(resume)될 때, 디스패처가 작업을 적절한 스레드에서 실행한다.
- 이때 다른 스레드에서 실행될 수도 있다.
- 작업이 준비되면 저장된 상태에서 다시 실행을 시작한다.
5. suspend 함수와 동기/비동기 함수 비교
| 특징 | 동기 함수 | 비동기 함수 | suspend 함수 |
| 실행 방식 | 호출되면 결과가 반환될 때까지 블로킹 | 백그라운드에서 실행 | 실행 중단 가능 (중단 후 재개 가능) |
| 스레드 반환 여부 | 반환하지 않음 | 반환하지 않음 | 반환하여 다른 작업 실행 가능 |
| 호출 위치 | 어디서나 호출 가능 | 어디서나 호출 가능 | 코루틴 내부 또는 다른 suspend 함수 |
'Kotlin' 카테고리의 다른 글
| [Kotlin] Kotlin Flow 정리: Flow, StateFlow, SharedFlow, CallbackFlow 차이와 활용 (1) | 2025.01.15 |
|---|---|
| [Kotlin/코루틴] CoroutineContext 중 Job과 Dispatcher 정리하기 (1) | 2025.01.15 |
| [Kotlin/코루틴] Coroutine 구성 요소 소개 및 CoroutineScope 설명 (0) | 2025.01.15 |
| [Kotlin/코루틴] Thread vs Coroutine: 차이를 비교 정리해보자! (2) | 2025.01.15 |
| [Kotlin] 입출력 방식 비교와 코딩 테스트 빠른 입출력 최적화 방법 (2) | 2024.12.26 |