코루틴이란?
코루틴(Coroutine)은 Kotlin의 비동기 프로그래밍 솔루션입니다. 스레드보다 가볍고, 콜백 지옥 없이 순차적인 코드 스타일로 비동기 로직을 작성할 수 있습니다. suspend 함수를 통해 실행을 일시 중단하고 재개할 수 있으며, 구조화된 동시성(Structured Concurrency)으로 생명주기를 안전하게 관리합니다.
이 글에서는 코루틴의 기본 개념부터 Flow까지 실전 패턴을 정리합니다.
launch와 async — 코루틴 빌더
launch는 결과를 반환하지 않는 코루틴, async는 결과를 반환하는 코루틴을 생성합니다.
import kotlinx.coroutines.*
// suspend 함수 — 코루틴 내부에서만 호출 가능
suspend fun fetchUser(id: Int): String {
delay(1000) // 1초 대기 (스레드를 차단하지 않음)
return "사용자_$id"
}
suspend fun fetchOrders(userId: String): List<String> {
delay(800) // 0.8초 대기
return listOf("주문A", "주문B", "주문C")
}
fun main() = runBlocking {
println("시작: ${Thread.currentThread().name}")
// launch — 결과가 필요 없는 비동기 작업
val job = launch {
println("백그라운드 작업 시작")
delay(500)
println("백그라운드 작업 완료")
}
// async — 결과를 반환하는 비동기 작업
val deferred = async {
fetchUser(1)
}
val user = deferred.await() // 결과 대기
println("사용자: $user")
// 병렬 실행 — async로 동시에 여러 작업 수행
val startTime = System.currentTimeMillis()
val user1 = async { fetchUser(1) }
val user2 = async { fetchUser(2) }
val user3 = async { fetchUser(3) }
// 모든 결과를 동시에 대기
println("결과: ${user1.await()}, ${user2.await()}, ${user3.await()}")
val elapsed = System.currentTimeMillis() - startTime
println("소요 시간: ${elapsed}ms (병렬이므로 약 1000ms)")
job.join() // launch 작업 완료 대기
// 실행 결과:
// 시작: main
// 백그라운드 작업 시작
// 사용자: 사용자_1
// 백그라운드 작업 완료
// 결과: 사용자_1, 사용자_2, 사용자_3
// 소요 시간: 1012ms (병렬이므로 약 1000ms)
}
async로 여러 작업을 동시에 시작하고 await()로 결과를 모으면, 순차 실행 대비 실행 시간을 크게 줄일 수 있습니다.
구조화된 동시성과 코루틴 스코프
코루틴은 반드시 CoroutineScope 내에서 실행됩니다. 부모 코루틴이 취소되면 자식 코루틴도 모두 취소됩니다. 이를 구조화된 동시성(Structured Concurrency)이라 합니다.
import kotlinx.coroutines.*
// 사용자 정의 스코프에서 코루틴 관리
class DataProcessor {
// 전용 스코프 — 취소 가능
private val scope = CoroutineScope(
Dispatchers.Default + SupervisorJob()
)
fun processAsync(items: List<String>) {
scope.launch {
println("처리 시작: ${items.size}개 항목")
// coroutineScope — 모든 자식이 완료될 때까지 대기
coroutineScope {
items.forEach { item ->
launch {
processItem(item)
}
}
}
println("모든 항목 처리 완료")
}
}
private suspend fun processItem(item: String) {
delay(500) // 처리 시뮬레이션
println("처리 완료: $item [${Thread.currentThread().name}]")
}
fun cancel() {
scope.cancel() // 모든 코루틴 취소
println("프로세서 취소됨")
}
}
fun main() = runBlocking {
// withContext — 디스패처 전환
val result = withContext(Dispatchers.IO) {
// IO 스레드에서 실행 (파일, 네트워크 등)
"IO 스레드에서 가져온 데이터"
}
println(result)
// 실행 결과: IO 스레드에서 가져온 데이터
// 타임아웃 처리
val data = withTimeoutOrNull(1500) {
delay(1000)
"성공 데이터"
}
println("타임아웃 결과: $data")
// 실행 결과: 타임아웃 결과: 성공 데이터
val expired = withTimeoutOrNull(500) {
delay(1000)
"이 값은 반환되지 않음"
}
println("만료 결과: $expired")
// 실행 결과: 만료 결과: null
// 예외 처리
val handler = CoroutineExceptionHandler { _, exception ->
println("예외 발생: ${exception.message}")
}
val job = launch(handler) {
throw RuntimeException("코루틴 내부 에러")
}
job.join()
// 실행 결과: 예외 발생: 코루틴 내부 에러
}
Flow — 비동기 데이터 스트림
Flow는 여러 값을 순차적으로 내보내는 비동기 스트림입니다. Sequence의 비동기 버전이며, 리액티브 스트림(RxJava)을 대체할 수 있습니다.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// Flow 생성 — 여러 값을 비동기로 방출
fun numbersFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(300) // 비동기 대기
emit(i) // 값 방출
println("방출: $i")
}
}
// 실시간 데이터 스트림 시뮬레이션
fun sensorDataFlow(): Flow<Double> = flow {
repeat(5) {
delay(500)
val value = 20.0 + (Math.random() * 10)
emit(value)
}
}
fun main() = runBlocking {
// 기본 Flow 수집
println("=== 기본 Flow ===")
numbersFlow().collect { value ->
println("수집: $value")
}
// 실행 결과:
// 방출: 1
// 수집: 1
// 방출: 2
// 수집: 2
// ... (5까지 반복)
// Flow 연산자 — map, filter, take
println("\n=== Flow 연산자 ===")
numbersFlow()
.filter { it % 2 != 0 } // 홀수만
.map { it * it } // 제곱
.take(2) // 처음 2개만
.collect { println("결과: $it") }
// 실행 결과:
// 결과: 1
// 결과: 9
// 센서 데이터 스트림
println("\n=== 센서 데이터 ===")
sensorDataFlow()
.map { String.format("%.1f°C", it) }
.collect { println("온도: $it") }
// 실행 결과 (랜덤 값):
// 온도: 25.3°C
// 온도: 22.8°C
// ... (5회)
// flowOf, asFlow — 간단한 Flow 생성
println("\n=== 간단한 Flow ===")
flowOf("A", "B", "C")
.onEach { delay(100) }
.collect { print("$it ") }
println()
// 실행 결과: A B C
listOf(1, 2, 3, 4, 5)
.asFlow()
.reduce { acc, value -> acc + value }
.let { println("합계: $it") }
// 실행 결과: 합계: 15
}
디스패처 정리
코루틴 디스패처는 코루틴이 실행될 스레드를 결정합니다.
Dispatchers.Main— UI 스레드 (Android, JavaFX)Dispatchers.IO— I/O 작업 (파일, 네트워크, DB)Dispatchers.Default— CPU 집약 작업 (정렬, JSON 파싱)Dispatchers.Unconfined— 호출 스레드에서 시작 (테스트용)
실전 팁
- runBlocking은 테스트와 main에서만: 프로덕션 코드에서는
CoroutineScope를 사용합니다. - SupervisorJob 사용: 하나의 자식 코루틴 실패가 다른 자식에 영향을 주지 않아야 하면
SupervisorJob을 사용합니다. - withContext로 디스패처 전환:
launch(Dispatchers.IO)대신withContext(Dispatchers.IO)로 전환하면 코루틴 계층이 단순해집니다. - Flow는 Cold 스트림: 수집자가
collect를 호출할 때만 실행됩니다. 여러 수집자에게 동시에 방출하려면SharedFlow나StateFlow를 사용합니다. - 취소 협조: 장시간 실행되는 코루틴에서는
isActive를 확인하거나ensureActive()를 호출하여 취소에 협조합니다.