코루틴(Coroutine) vs 쓰레드(Thread)
코루틴과 쓰레드의 차이를 공부해 봅시다
코루틴(Coroutine) vs 쓰레드(Thread)
1. 기존 방식: 쓰레드(Thread)와 그 한계
1) 쓰레드란?
우리가 작성한 코드가 실제로 실행되는 흐름의 단위입니다.
JVM에서 main 함수를 실행하면 메인 스레드가 하나 생성되고, 이 스레드가 우리가 작성한 코드를 순차적으로 처리합니다.
문제는 **단일 스레드(Single Thread)**만 사용하는 경우입니다.
- DB 조회, 네트워크 요청 같이 오래 걸리는 작업이 하나 실행되고 있으면
- 그 작업이 끝날 때까지 다음 작업은 전혀 실행되지 못합니다.
즉, 스레드는 한 시점에 한 가지 작업만 처리할 수 있고, 그 작업이 오래 걸리면 전체 프로그램이 같이 느려지는 구조입니다.
2) 멀티 스레드와 스레드 풀 (Executor Framework)
이 문제를 완화하기 위해 등장한 것이 **멀티 스레드(Multi Thread)**입니다.
- 여러 개의 스레드를 만들어서
- 각 스레드가 서로 다른 작업을 동시에 처리하도록 하는 방식입니다.
하지만 new Thread { ... }로 스레드를 직접 생성하는 방식에는 큰 문제가 있습니다.
- 스레드 하나를 만드는 데도 메모리·생성 비용이 크고
- 생성·종료·예외 처리까지 수명 주기를 직접 관리해야 하며
- 스레드 수가 많아질수록 컨텍스트 스위칭 비용도 커집니다.
그래서 등장한 것이 스레드 풀(Thread Pool) + Executor 프레임워크입니다.
- 미리 일정 개수의 스레드를 만들어 두고(풀)
- 작업이 들어올 때마다 노는 스레드가 그 작업을 가져가 처리합니다.
- 스레드를 재사용하기 때문에, 매번 새로 만들고 버리는 것보다 훨씬 효율적입니다.
3) 여전히 남는 문제: Blocking(블로킹)
스레드 풀을 써도 해결되지 않는 치명적인 문제가 하나 있습니다. 바로 **스레드 블로킹(Thread Blocking)**입니다.
스레드 블로킹: 스레드가 어떤 작업의 결과를 기다리느라 그동안 아무 일도 하지 못하고 멈춰 있는 상태
대표적인 상황은 이렇습니다.
- 작업 1을 수행하다가 작업 2의 결과가 필요해짐
- 작업 2를 다른 스레드에서 실행시키고, 작업 1은 결과를 기다림
- 이때 작업 1을 수행하던 스레드는 결과가 올 때까지 멍하니 기다립니다.
문제는, 스레드는 굉장히 비싼 자원이라는 점입니다.
- 스레드가 블로킹되어 있는 동안에도
- 스레드 스택 메모리, 문맥 정보, OS 리소스는 그대로 잡혀 있고
- 다른 작업을 위해 재활용될 수도 없습니다.
그래서:
- 스레드가 많이 필요해지는 시점(동시 요청/동시 작업 증가)에는
- 스레드 풀의 스레드가 모두 블로킹 상태에 빠져
- “스레드는 잔뜩 떠 있는데, 할 수 있는 일은 없는” 황당한 상황이 발생하기도 합니다.
이 지점에서, “스레드 자체를 더 잘 쓰는 게 아니라, 아예 다른 단위를 쓸 수 없을까?” 하는 고민이 나오고, 그 해답 중 하나가 바로 **코루틴(Coroutine)**입니다.
2. 코루틴(Coroutine)이란?
코루틴은 이런 스레드 기반 모델의 한계를 보완하기 위해 등장한 개념입니다.
1) 경량 스레드 (Light-weight Thread)
코루틴은 흔히 **“경량 스레드(Light-weight Thread)”**라고 불립니다.
핵심 특징은 딱 하나로 정리할 수 있습니다.
코루틴은 실행 도중 “기다려야 할 순간”이 오면, 쓰레드의 사용 권한을 잠시 양보할 수 있다.
즉, 코루틴은 스레드에 고정된 존재가 아니라, 필요할 때 스레드에 붙었다가, 필요 없을 때는 스레드를 반납하고 잠시 중단될 수 있는 실행 단위입니다.
2) Blocking vs Suspension (블로킹 vs 중단)
쓰레드와 코루틴의 가장 중요한 차이는 바로 **“기다리는 방식”**입니다.
- 쓰레드(Blocking)
- I/O 작업(DB, 네트워크 등)을 호출하면
- 그 작업이 끝날 때까지 스레드가 점유된 채 그대로 멈춰 있습니다.
- 다른 코드를 실행할 수 없습니다.
- 코루틴(Suspension)
- 기다려야 할 일이 생기면
- **스레드 사용 권한을 반납(yield)**하고, 자신은 중단(Suspend) 상태로 들어갑니다.
- 이때 그 스레드는 즉시 다른 코루틴을 실행하는 데 사용될 수 있습니다.
- 기다리던 작업이 끝나면, 코루틴은 다시 스케줄링되어 스레드를 배정받고 중단했던 지점부터 이어서 실행됩니다.
정리하면:
하나의 스레드 위에서 여러 코루틴이 번갈아 가며 실행되기 때문에, 매우 적은 수의 스레드로도 많은 동시 작업을 처리할 수 있다.
3. 코드로 보는 차이: Blocking vs Suspension
1) 기본 동작 비교 – “기다리는 방식이 다르다”
❌ [Thread] 방식 – Thread.sleep
스레드가 sleep을 호출하는 순간,
해당 스레드는 자원을 점유한 채 1초 동안 아무 것도 하지 않습니다.
import kotlin.concurrent.thread
fun main() {
println("작업 시작")
// 스레드 생성
thread {
println("[${Thread.currentThread().name}] 스레드: 1초간 잠듭니다...")
// 1. Blocking: 스레드가 1초 동안 멈춥니다. 다른 일을 할 수 없습니다.
Thread.sleep(1000L)
println("[${Thread.currentThread().name}] 스레드: 기상!")
}
println("메인 종료")
}
✅ [Coroutine] 방식 – delay
코루틴이 delay를 만나면,
스레드 사용 권한을 잠시 내려놓고(Suspend) 비켜줍니다.
그 사이 같은 스레드에서는 다른 코루틴이 실행될 수 있습니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
println("작업 시작")
// 코루틴 생성 (launch)
launch {
println("[${Thread.currentThread().name}] 코루틴: 1초간 양보합니다...")
// 1. Suspension: 잠시 중단하고 스레드 권한을 양보합니다 (Non-blocking)
delay(1000L)
println("[${Thread.currentThread().name}] 코루틴: 다시 돌아왔습니다!")
}
println("메인 종료")
}
2) 성능/효율 비교 – “10만 개를 동시에 띄워본다면?”
코루틴이 왜 **“경량 스레드”**인지 가장 직관적으로 보여주는 예입니다.
❌ [Thread] – 10만 개 생성 도전
스레드는 생성 비용도 크고, 스택 메모리도 많이 차지합니다. 아래처럼 10만 개를 만들어 보면, 대부분의 환경에서:
- 엄청 느려지거나
OutOfMemoryError가 발생하며 프로그램이 뻗어버릴 수 있습니다.
import kotlin.concurrent.thread
fun main() {
// 10만 개의 스레드를 생성 시도 -> OOM 또는 심각한 성능 저하 가능
repeat(100_000) {
thread {
Thread.sleep(1000L)
print(".")
}
}
}
✅ [Coroutine] – 10만 개 생성 도전
코루틴은 객체 하나를 생성하는 수준으로 가볍습니다. 10만 개를 만들어도 충분히 감당 가능합니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
// 10만 개의 코루틴 생성 -> 가볍게 소화 가능
repeat(100_000) {
launch {
delay(1000L)
print(".")
}
}
val endTime = System.currentTimeMillis()
println("\n소요 시간: ${endTime - startTime}ms")
}
실제로 돌려보면,
10만 개의 코루틴이 1초 delay를 거치고도 1~2초 수준 안에서 전체가 끝나는 걸 볼 수 있습니다.
(쓰레드로는 상상하기 어려운 규모죠.)
4. 그래서, 언제 코루틴을 써야 할까?
정리하면, 코루틴이 특히 강력한 영역은 “기다림이 많은 작업”, 즉 I/O 바운드 작업입니다.
| 작업 종류 | 쓰레드 기반 | 코루틴 기반 |
|---|---|---|
| I/O 작업 (네트워크, DB 등) | 스레드 블로킹 → 비효율 | 스레드 재사용 → 유리 |
| CPU 작업 (계산, 인코딩 등) | 코어 수에 따라 비슷 | 대체로 비슷 |
1) I/O 바운드 작업
- 네트워크, DB, 파일 IO처럼 “대기 시간”이 긴 작업들
- 쓰레드 기반:
- 응답을 기다리는 동안 스레드가 그냥 놀고 있음
- 코루틴 기반:
- 기다리는 동안 스레드 사용 권한을 반납하고
- 같은 스레드로 다른 코루틴 작업을 계속 처리할 수 있음
- 결과:
- 같은 스레드 수로도 훨씬 많은 동시작업을 소화 가능
2) CPU 바운드 작업
- 이미지/영상 인코딩, 대용량 데이터 변환처럼
CPU를 꽉 채우는 작업은
- 코루틴이든, 쓰레드든
- 어차피 CPU 코어 수에 의해 성능이 결정되기 때문에
- “코루틴이라서 무조건 더 빠르다”라고 보긴 어렵고, 주로 코드 구조·취소/에러 처리·구조적 동시성 측면에서 장점이 있습니다.