``` Coroutine = Co(협력) + Routine(루틴) └── 협력적으로 실행되는 루틴 └── “일시 중단 가능한 함수” └── 스레드보다 훨씬 가벼움
Kotlin Coroutine과 @Async 비교이(가) 등장한 배경과 기존 한계를 정리한다.
이 주제를 이해하고 적용해야 하는 이유를 정리한다.
작성일: 2026-01-29 카테고리: Backend / Kotlin / Concurrency 포함 내용: Coroutine, async, launch, suspend, Continuation, @Async, 동시성
Coroutine = Co(협력) + Routine(루틴)
└── 협력적으로 실행되는 루틴
└── "일시 중단 가능한 함수"
└── 스레드보다 훨씬 가벼움
┌─────────────────────────────────────────────────────────┐
│ │
│ 핵심 특징: │
│ ├── Suspend (일시 중단): 실행 중 멈출 수 있음 │
│ ├── Resume (재개): 멈춘 곳에서 다시 시작 │
│ └── 스레드 차단 없이 대기 │
│ │
│ 스레드 vs 코루틴: │
│ ┌──────────────────┬──────────────────────────────┐ │
│ │ 스레드 │ 코루틴 │ │
│ ├──────────────────┼──────────────────────────────┤ │
│ │ OS가 관리 │ 라이브러리/언어가 관리 │ │
│ │ 무거움 (~1MB) │ 가벼움 (~수백 바이트) │ │
│ │ 선점형 스케줄링 │ 협력적 스케줄링 │ │
│ │ 컨텍스트 스위칭 │ 상태 객체 교체 │ │
│ │ 비용 높음 │ 비용 낮음 │ │
│ └──────────────────┴──────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 콜백 지옥 (Callback Hell) │
│ │
│ fetchUser(userId) { user -> │
│ fetchOrders(user.id) { orders -> │
│ fetchProducts(orders[0].id) { products -> │
│ fetchReviews(products[0].id) { reviews -> │
│ // 계속 들여쓰기... │
│ } │
│ } │
│ } │
│ } │
│ │
│ 문제점: │
│ ├── 가독성 최악 (피라미드 코드) │
│ ├── 에러 처리 복잡 │
│ ├── 취소 처리 어려움 │
│ └── 디버깅 지옥 │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ │
│ 1세대: Callback │
│ └── 콜백 지옥 문제 │
│ │
│ 2세대: Future/Promise (Java CompletableFuture) │
│ └── 나아졌지만 여전히 체이닝이 복잡 │
│ │
│ 3세대: RxJava/Reactor (리액티브) │
│ └── 강력하지만 학습 곡선 높음 │
│ │
│ 4세대: Coroutine (2018, Kotlin 1.3) │
│ val user = fetchUser(userId) // 동기처럼 보이지만│
│ val orders = fetchOrders(user.id) // 실제로는 비동기! │
│ └── 동기 코드처럼 작성, 비동기로 실행 │
│ │
└─────────────────────────────────────────────────────────┘
fun main() = runBlocking {
val job: Job = launch {
delay(1000)
println("Hello") // 결과 반환 없음
}
job.join() // 완료 대기 (결과는 없음)
}
┌─────────────────────────────────────────────────────────┐
│ │
│ launch: │
│ ├── 반환 타입: Job │
│ ├── 결과값: 없음 │
│ ├── 용도: 결과가 필요 없는 비동기 작업 │
│ └── 비유: "이거 해두고, 나 안 기다려도 돼" │
│ │
└─────────────────────────────────────────────────────────┘
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
delay(1000)
42 // 결과 반환!
}
val result: Int = deferred.await() // 결과 받기
println(result) // 42
}
┌─────────────────────────────────────────────────────────┐
│ │
│ async: │
│ ├── 반환 타입: Deferred<T> (결과를 담는 그릇) │
│ ├── 결과값: await()로 받음 │
│ ├── 용도: 결과가 필요한 비동기 작업 │
│ └── 비유: "이거 해두고, 나중에 결과 알려줘" │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ │
│ ┌────────────┬─────────────────┬─────────────────┐ │
│ │ │ launch │ async │ │
│ ├────────────┼─────────────────┼─────────────────┤ │
│ │ 반환 타입 │ Job │ Deferred<T> │ │
│ │ 결과값 │ 없음 │ T (await으로) │ │
│ │ 예외 처리 │ 즉시 전파 │ await시 전파 │ │
│ │ 용도 │ 실행만 │ 결과 필요 │ │
│ │ 비유 │ 심부름 시키기 │ 배달 주문하기 │ │
│ └────────────┴─────────────────┴─────────────────┘ │
│ │
│ 선택 기준: │
│ "결과가 필요해?" → Yes → async │
│ → No → launch │
│ │
└─────────────────────────────────────────────────────────┘
fun main() = runBlocking { // main 스레드 1개만 사용
println("메인 시작")
launch {
println("☕ 커피 주문 시작")
delay(3000)
println("☕ 커피 완성!")
}
launch {
println("🥪 샌드위치 주문 시작")
delay(2000)
println("🥪 샌드위치 완성!")
}
launch {
println("🧃 주스 주문 시작")
delay(1000)
println("🧃 주스 완성!")
}
println("메인: 모든 주문 접수 완료!")
}
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ launch의 2단계: │
│ │
│ 1️⃣ 스케줄링 단계 (매우 빠름, 수 나노초) │
│ └── 코루틴 객체 생성 │
│ └── 실행 큐에 추가 │
│ └── 즉시 리턴 (블로킹 없음!) │
│ │
│ 2️⃣ 실행 단계 (스케줄러가 처리) │
│ └── 큐에서 코루틴 꺼내서 실행 │
│ └── suspend 만나면 다음 코루틴으로 │
│ └── resume 되면 다시 실행 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ 시간 │ main 스레드가 하는 일 │
│ ───────┼──────────────────────────────────────────────────────────────│
│ │ │
│ 0ms │ "메인 시작" 출력 │
│ │ launch{A} → Queue에 추가 (즉시 리턴) │
│ │ launch{B} → Queue에 추가 (즉시 리턴) │
│ │ launch{C} → Queue에 추가 (즉시 리턴) │
│ │ "메인: 모든 주문 접수 완료!" 출력 │
│ │ │
│ 1ms │ A 실행: "커피 시작" → delay(3000) → SUSPEND │
│ 2ms │ B 실행: "샌드위치 시작" → delay(2000) → SUSPEND │
│ 3ms │ C 실행: "주스 시작" → delay(1000) → SUSPEND │
│ │ │
│ │ ═══ 3개 모두 delay 중, 스레드는 idle ═══ │
│ │ │
│ 1003ms │ C resume → "주스 완성!" → C 종료 │
│ 2002ms │ B resume → "샌드위치 완성!" → B 종료 │
│ 3001ms │ A resume → "커피 완성!" → A 종료 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ │
│ 단일 스레드에서: │
│ ├── 스케줄링은 "순차적"이지만 매우 빠름 (나노초) │
│ ├── 실행은 "한 번에 하나", suspend 시 교대 │
│ └── 결과적으로 "동시에 대기" 가능 (I/O 효율적) │
│ │
│ Thread Pool (Dispatchers.Default 등) 사용 시: │
│ ├── 여러 스레드에서 진짜 병렬 실행 가능 │
│ └── CPU 작업에 유리 │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Thread의 메모리 구조: │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Thread 1 (1MB Stack) │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ Stack Frame: main() │ │ │
│ │ ├─────────────────────────────────────────────────────────┤ │ │
│ │ │ Stack Frame: functionA() │ │ │
│ │ ├─────────────────────────────────────────────────────────┤ │ │
│ │ │ Stack Frame: functionB() │ │ │
│ │ ├─────────────────────────────────────────────────────────┤ │ │
│ │ │ (미사용 공간도 할당됨) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 스레드 10,000개 = Stack만 10GB 메모리 필요! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Coroutine의 메모리 구조: │
│ │
│ 스레드 Stack을 사용하지 않음! │
│ 대신 Heap에 작은 객체(Continuation)만 저장 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Heap │ │
│ │ ┌──────────────────┐ ┌──────────────────┐ │ │
│ │ │ Continuation 1 │ │ Continuation 2 │ ... │ │
│ │ │ - state: 2 │ │ - state: 0 │ │ │
│ │ │ - label: 3 │ │ - label: 1 │ │ │
│ │ │ - 지역변수들 │ │ - 지역변수들 │ │ │
│ │ │ (수백 바이트) │ │ (수백 바이트) │ │ │
│ │ └──────────────────┘ └──────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ 코루틴 10,000개 = 수 MB만 필요! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
// 원본 suspend 함수
suspend fun fetchData(): String {
val a = api1() // suspend point 1
val b = api2() // suspend point 2
return a + b
}
// 컴파일러가 변환한 상태 머신 (개념적)
class FetchData$Continuation : Continuation<String> {
var label = 0 // 현재 어디까지 실행했는지
var a: String? = null // 지역 변수 저장
var b: String? = null
fun invokeSuspend(result: Any?): Any {
when (label) {
0 -> { label = 1; /* api1 호출 */ }
1 -> { a = result; label = 2; /* api2 호출 */ }
2 -> { b = result; return a + b }
}
}
}
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────────────┬─────────────────────────────────────────┐ │
│ │ Thread │ Coroutine │ │
│ ├─────────────────────────┼─────────────────────────────────────────┤ │
│ │ OS 커널 개입 필요 │ 유저 스페이스에서 처리 │ │
│ │ 모든 레지스터 저장 │ label + 지역변수만 저장 │ │
│ │ TLB 플러시 가능성 │ TLB 영향 없음 (같은 스레드) │ │
│ │ 캐시 미스 발생 │ 캐시 유지 │ │
│ │ 비용: 1~10 μs │ 비용: 수십~수백 ns │ │
│ │ 메모리: ~1MB/스레드 │ 메모리: 수백 바이트/코루틴 │ │
│ └─────────────────────────┴─────────────────────────────────────────┘ │
│ │
│ 💡 코루틴 전환이 스레드 전환보다 약 100배 빠름! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
@Service
class MyService {
@Async // 별도 스레드에서 실행!
fun heavyTask(): CompletableFuture<String> {
println("실행 스레드: ${Thread.currentThread().name}")
Thread.sleep(1000) // 블로킹! 이 스레드는 점유됨
return CompletableFuture.completedFuture("결과")
}
}
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ @Async 호출 시: │
│ │
│ Main Thread Thread Pool (TaskExecutor) │
│ │ │
│ │── heavyTask() 호출 ────►│ │
│ │◄─ Future 즉시 리턴 ─────│ │
│ │ (다른 작업 계속) │── 새 스레드에서 실행 ──┐ │
│ │ │ Thread-1 │
│ │ │ [heavyTask] │
│ │ │ Thread.sleep() │
│ │ │ (블로킹!) │
│ │── future.get() ─────────┼───────────────────────┘ │
│ │
│ 💡 별도 스레드가 생성됨! (스레드 풀에서 가져옴) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌──────────────────┬─────────────────────┬───────────────────────────┐│
│ │ │ @Async │ Coroutine ││
│ ├──────────────────┼─────────────────────┼───────────────────────────┤│
│ │ 스레드 │ 별도 스레드 필요 │ 기존 스레드 재사용 ││
│ │ 메모리 │ ~1MB/스레드 │ ~수백 바이트/코루틴 ││
│ │ 전환 비용 │ OS Context Switch │ 상태 객체 교체 ││
│ │ 동시 실행 수 │ 스레드 풀 크기 제한 │ 수십만 개 가능 ││
│ │ I/O 대기 시 │ 스레드 블로킹 │ 스레드 양보 (suspend) ││
│ │ 사용법 │ @Async 어노테이션 │ suspend fun, launch ││
│ │ 반환 타입 │ CompletableFuture │ Deferred, Flow ││
│ └──────────────────┴─────────────────────┴───────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ 10개의 API 호출 (각 1초 대기): │
│ │
│ @Async 방식: │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Thread-1│ │Thread-2│ │Thread-3│ ... │Thread-10│ ← 10개 스레드 필요! │
│ │[1초대기]│ │[1초대기]│ │[1초대기]│ │[1초대기]│ (각 스레드 블로킹) │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ 메모리: 10 × 1MB = 10MB │
│ │
│ Coroutine 방식: │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Thread-1 (하나!) │ │
│ │ [코루틴1 suspend][코루틴2 suspend]...[코루틴10 suspend] │ │
│ │ ↓ 모두 대기 중, 스레드는 비어있음! │ │
│ │ [코루틴1 완료][코루틴2 완료]...[코루틴10 완료] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ 메모리: 10 × 수백 바이트 = 수 KB │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ │
│ @Async 적합한 경우: │
│ ├── CPU 집약적 작업 (계산, 인코딩, 압축) │
│ ├── 레거시 블로킹 코드 호출 │
│ ├── 진짜 병렬 실행 필요 (멀티코어 활용) │
│ └── 기존 Spring 프로젝트에서 간단히 비동기 추가 │
│ │
│ Coroutine 적합한 경우: │
│ ├── I/O 집약적 작업 (네트워크, DB) │
│ ├── 대량의 동시 요청 처리 │
│ ├── 자원 효율성이 중요한 경우 │
│ └── 새 Kotlin 프로젝트 │
│ │
│ 💡 판단 기준: │
│ "작업이 CPU를 계속 쓰나?" → @Async (별도 스레드 필요) │
│ "작업이 주로 대기하나?" → Coroutine (스레드 낭비 없음)│
│ │
└─────────────────────────────────────────────────────────┘
launch(Dispatchers.Main) {
// UI 스레드에서 실행
}
launch(Dispatchers.IO) {
// I/O 작업용 스레드 풀 (DB, 네트워크, 파일)
}
launch(Dispatchers.Default) {
// CPU 집약적 작업용 (코어 수만큼 스레드)
}
withContext(Dispatchers.IO) {
// 컨텍스트 전환
val data = networkCall()
}
┌─────────────────────────────────────────────────────────┐
│ │
│ Dispatcher 선택: │
│ │
│ ├── Dispatchers.Main │
│ │ └── Android UI 업데이트, 짧은 작업 │
│ │ │
│ ├── Dispatchers.IO │
│ │ └── 네트워크, DB, 파일 I/O │
│ │ └── 스레드 수 많음 (64개 또는 코어 수) │
│ │ │
│ ├── Dispatchers.Default │
│ │ └── CPU 집약적 계산 │
│ │ └── 스레드 수 = CPU 코어 수 │
│ │ │
│ └── Dispatchers.Unconfined │
│ └── 특수 용도 (거의 안 씀) │
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ │
│ 코루틴이 스레드를 양보하는 순간: │
│ │
│ ├── delay(ms) : 시간 대기 │
│ ├── yield() : 명시적 양보 │
│ ├── channel.send() : 채널 전송 │
│ ├── channel.receive() : 채널 수신 │
│ ├── mutex.lock() : 뮤텍스 획득 │
│ ├── deferred.await() : 결과 대기 │
│ ├── withContext() : 컨텍스트 전환 │
│ └── 모든 suspend 함수 : 네트워크, DB, 파일 등 │
│ │
│ ⚠️ suspend 포인트가 없으면 양보하지 않음! │
│ │
│ // 스레드 양보 안 함 (CPU 독점)
│ launch {
│ while(true) { /* 계산 */ }
│ }
│
│ // 스레드 양보함
│ launch {
│ while(true) {
│ yield() // 매번 양보
│ }
│ }
│ │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ Coroutine = 경량 스레드, 일시 중단 가능한 함수 │
│ │
│ 가벼운 이유: │
│ ├── Stack 없음 (Stackless) - Heap에 Continuation만 │
│ ├── Context Switching 없음 - 상태 객체 교체만 │
│ ├── 캐시 친화적 - 같은 스레드 사용 │
│ └── 컴파일러가 상태 머신으로 변환 │
│ │
│ launch vs async: │
│ ├── launch: 결과 없음, fire-and-forget │
│ └── async: 결과 있음, await()로 받기 │
│ │
│ @Async vs Coroutine: │
│ ├── @Async: 별도 스레드, 무거움, CPU 작업용 │
│ └── Coroutine: 같은 스레드 재사용, 가벼움, I/O 작업용 │
│ │
│ 비유: │
│ ├── 스레드 = 각자 책상(Stack)이 있는 직원, 교대 시 책상 전체 정리 │
│ ├── 코루틴 = 책상 하나 공유, 교대 시 메모(Continuation)만 남김 │
│ ├── @Async = 일꾼을 더 고용 (월급 비쌈) │
│ └── Coroutine = 한 일꾼이 효율적으로 여러 일 처리 │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Coroutine, launch, async, suspend, Continuation, @Async, Dispatcher, Stackless, 동시성, 비동기