[2026] Kotlin 코루틴 vs 스레드 완벽 비교 | 비동기 처리 선택 가이드
이 글의 핵심
Kotlin 코루틴과 스레드의 차이점을 성능, 메모리, 사용성 관점에서 비교. 실전에서 어떤 비동기 처리 방식을 써야 하는지 선택 기준과 예제를 설명합니다.
들어가며
“코루틴과 스레드 중 무엇을 써야 할까요?” Kotlin으로 비동기 처리를 할 때 자주 나오는 질문입니다. 이 글에서는 코루틴과 스레드의 차이를 명확히 이해하고, 실전에서 어떤 것을 써야 하는지 선택 기준을 제시합니다. 비유로 말씀드리면, 스레드는 직원을 한 명 더 고용하는 것이고, 코루틴은 한 직원이 작업을 번갈아 처리하되, 기다리는 동안 다른 일을 보게 하는 것에 가깝습니다. I/O 대기가 많으면 코루틴이 메모리·컨텍스트 비용에서 유리한 경우가 많습니다.
언제 코루틴을, 언제 스레드를 쓰나요?
| 관점 | 코루틴 | 스레드 |
|---|---|---|
| 성능 | 대량 생성 시 가벼운 스케줄 단위 | OS 스레드마다 스택·전환 비용 |
| 사용성 | suspend, 구조적 동시성으로 취소·에러 전파 | 블로킹·CPU 바운드·레거시 API와 궁합 |
| 적용 시나리오 | 네트워크·DB 대기 | 병렬 CPU 작업·JNI 등 |
이 글을 읽으면
- 코루틴과 스레드의 동작 원리를 이해합니다
- 성능과 메모리 사용량 차이를 배웁니다
- 구조적 동시성의 이점을 익힙니다
- 실전에서 어떤 것을 써야 하는지 판단할 수 있습니다
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
1. 빠른 비교표
| 특성 | 코루틴 | 스레드 |
|---|---|---|
| 무게 | 경량 (수천~수만 개 가능) | 무거움 (수십~수백 개) |
| 메모리 | ~KB | ~MB (스택 크기) |
| 생성 비용 | 매우 낮음 | 높음 (OS 호출) |
| 컨텍스트 스위칭 | 빠름 (사용자 공간) | 느림 (커널 공간) |
| 취소 | 구조적 취소 지원 | 수동 구현 필요 |
| 예외 처리 | 자동 전파 | 수동 처리 |
| 디버깅 | 어려움 | 상대적으로 쉬움 |
| 권장 사용 | ✅ 기본 선택 | 특수한 경우만 |
2. 동작 원리
스레드: OS 레벨
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 스레드 생성
val thread = Thread {
println("Running in thread: ${Thread.currentThread().name}")
Thread.sleep(1000)
}
thread.start()
thread.join()
// 메모리 구조
// 각 스레드마다:
// - OS 스레드 생성 (커널 리소스)
// - 스택 메모리 할당 (보통 1-2MB)
// - 컨텍스트 스위칭 (커널 개입)
코루틴: 사용자 레벨
아래 코드는 kotlin를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import kotlinx.coroutines.*
// 코루틴 생성
runBlocking {
launch {
println("Running in coroutine")
delay(1000) // 중단 (스레드는 블록 안 됨)
}
}
// 메모리 구조
// - 코루틴은 스레드 위에서 실행
// - 중단 시 상태만 저장 (수십 bytes)
// - 재개 시 다른 스레드에서도 실행 가능
3. 성능 비교
생성 비용
다음은 kotlin를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import kotlin.system.measureTimeMillis
// 스레드 10,000개 생성
val threadTime = measureTimeMillis {
val threads = List(10000) {
Thread { Thread.sleep(100) }
}
threads.forEach { it.start() }
threads.forEach { it.join() }
}
println("Threads: ${threadTime}ms") // 약 5000ms (5초)
// 코루틴 10,000개 생성
val coroutineTime = measureTimeMillis {
runBlocking {
val jobs = List(10000) {
launch { delay(100) }
}
jobs.forEach { it.join() }
}
}
println("Coroutines: ${coroutineTime}ms") // 약 150ms (30배 빠름)
컨텍스트 스위칭
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 스레드: 커널 개입 (느림)
// - 레지스터 저장/복원
// - 스택 포인터 변경
// - TLB 플러시
// 약 1-10 마이크로초
// 코루틴: 사용자 공간 (빠름)
// - 상태 객체만 교체
// - 커널 호출 없음
// 약 0.1 마이크로초 (10-100배 빠름)
4. 메모리 사용량
스레드 메모리
아래 코드는 kotlin를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 스레드 1개: 약 1-2MB
val threads = List(1000) { Thread { Thread.sleep(1000) } }
threads.forEach { it.start() }
// 총 메모리: 1000 × 1MB = 1GB
// → 메모리 부족 가능성
코루틴 메모리
아래 코드는 kotlin를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 코루틴 1개: 약 수십 bytes
runBlocking {
val jobs = List(100000) { launch { delay(1000) } }
jobs.forEach { it.join() }
}
// 총 메모리: 100,000 × 50 bytes = 5MB
// → 10만 개도 문제없음
일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.
5. 구조적 동시성
일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. {#structured-concurrency}
스레드: 수동 관리
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 나쁜 패턴: 스레드 누수
fun fetchData() {
Thread {
val data = api.fetch()
// 예외 발생 시 스레드가 죽지만 호출자는 모름
}.start()
// 스레드가 끝날 때까지 기다리지 않음
}
// 취소도 수동
val thread = Thread { /* ....*/ }
thread.start()
// 취소 방법이 없음! (interrupt는 협력적)
코루틴: 구조적 동시성
아래 코드는 kotlin를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ✅ 좋은 패턴: 자동 관리
suspend fun fetchData(): Data = coroutineScope {
val data = async { api.fetch() }
data.await()
// 예외 발생 시 자동으로 상위로 전파
// 함수 종료 시 모든 자식 코루틴 자동 취소
}
// 취소도 자동
val job = launch {
fetchData()
}
job.cancel() // 모든 자식 코루틴도 취소됨
6. 실전 선택 가이드
코루틴을 써야 하는 경우 (대부분)
- 네트워크 요청
suspend fun fetchUsers(): List<User> = withContext(Dispatchers.IO) { api.getUsers() } - 데이터베이스 쿼리
suspend fun saveUser(user: User) = withContext(Dispatchers.IO) { database.insert(user) } - 동시 작업
아래 코드는 kotlin를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
suspend fun fetchAll() = coroutineScope {
val users = async { fetchUsers() }
val posts = async { fetchPosts() }
Pair(users.await(), posts.await())
}
스레드를 써야 하는 경우 (드물음)
- CPU 집약적 작업 (코루틴도 가능)
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 코루틴으로도 가능
withContext(Dispatchers.Default) {
heavyComputation()
}
// 스레드로도 가능 (레거시)
Thread {
heavyComputation()
}.start()
- Java 라이브러리 통합
// ExecutorService 등 기존 Java 코드 val executor = Executors.newFixedThreadPool(4) executor.submit { /* ....*/ }
7. 코드 비교
예제: 동시에 10개 API 호출
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 스레드 방식
fun fetchAllThreads(): List<User> {
val results = mutableListOf<User>()
val threads = (1..10).map { id ->
Thread {
try {
val user = api.getUser(id)
synchronized(results) {
results.add(user)
}
} catch (e: Exception) {
// 예외 처리 복잡
}
}
}
threads.forEach { it.start() }
threads.forEach { it.join() }
return results
}
// 코루틴 방식
suspend fun fetchAllCoroutines(): List<User> = coroutineScope {
(1..10).map { id ->
async { api.getUser(id) }
}.awaitAll()
// 예외 자동 전파, 취소 자동 처리
}
마무리
Kotlin 비동기 처리의 핵심:
- 기본은 코루틴 (경량, 구조적 동시성)
- 스레드는 특수한 경우만 (레거시, Java 통합)
- Dispatchers로 스레드 풀 관리
- 구조적 동시성으로 안전성 확보 핵심: 코루틴은 스레드의 상위 추상화입니다. 특별한 이유가 없다면 코루틴을 사용하세요.
관련 글
키워드
Kotlin, Coroutine, 코루틴, Thread, 스레드, 비동기, 동시성, 성능, 메모리, 구조적 동시성, 비교