[2026] Kotlin 코루틴 vs 스레드 완벽 비교 | 비동기 처리 선택 가이드

[2026] Kotlin 코루틴 vs 스레드 완벽 비교 | 비동기 처리 선택 가이드

이 글의 핵심

Kotlin 코루틴과 스레드의 차이점을 성능, 메모리, 사용성 관점에서 비교. 실전에서 어떤 비동기 처리 방식을 써야 하는지 선택 기준과 예제를 설명합니다.

들어가며

“코루틴과 스레드 중 무엇을 써야 할까요?” Kotlin으로 비동기 처리를 할 때 자주 나오는 질문입니다. 이 글에서는 코루틴과 스레드의 차이를 명확히 이해하고, 실전에서 어떤 것을 써야 하는지 선택 기준을 제시합니다. 비유로 말씀드리면, 스레드직원을 한 명 더 고용하는 것이고, 코루틴한 직원이 작업을 번갈아 처리하되, 기다리는 동안 다른 일을 보게 하는 것에 가깝습니다. I/O 대기가 많으면 코루틴이 메모리·컨텍스트 비용에서 유리한 경우가 많습니다.

언제 코루틴을, 언제 스레드를 쓰나요?

관점코루틴스레드
성능대량 생성 시 가벼운 스케줄 단위OS 스레드마다 스택·전환 비용
사용성suspend, 구조적 동시성으로 취소·에러 전파블로킹·CPU 바운드·레거시 API와 궁합
적용 시나리오네트워크·DB 대기병렬 CPU 작업·JNI 등

이 글을 읽으면

  • 코루틴과 스레드의 동작 원리를 이해합니다
  • 성능과 메모리 사용량 차이를 배웁니다
  • 구조적 동시성의 이점을 익힙니다
  • 실전에서 어떤 것을 써야 하는지 판단할 수 있습니다

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

목차

  1. 빠른 비교표
  2. 동작 원리
  3. 성능 비교
  4. 메모리 사용량
  5. 구조적 동시성
  6. 실전 선택 가이드
  7. 마무리

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. 실전 선택 가이드

코루틴을 써야 하는 경우 (대부분)

  1. 네트워크 요청
    suspend fun fetchUsers(): List<User> = withContext(Dispatchers.IO) {
        api.getUsers()
    }
  2. 데이터베이스 쿼리
    suspend fun saveUser(user: User) = withContext(Dispatchers.IO) {
        database.insert(user)
    }
  3. 동시 작업

아래 코드는 kotlin를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

   suspend fun fetchAll() = coroutineScope {
       val users = async { fetchUsers() }
       val posts = async { fetchPosts() }
       Pair(users.await(), posts.await())
   }

스레드를 써야 하는 경우 (드물음)

  1. CPU 집약적 작업 (코루틴도 가능)

아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

   // 코루틴으로도 가능
   withContext(Dispatchers.Default) {
       heavyComputation()
   }
   
   // 스레드로도 가능 (레거시)
   Thread {
       heavyComputation()
   }.start()
  1. 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 비동기 처리의 핵심:

  1. 기본은 코루틴 (경량, 구조적 동시성)
  2. 스레드는 특수한 경우만 (레거시, Java 통합)
  3. Dispatchers로 스레드 풀 관리
  4. 구조적 동시성으로 안전성 확보 핵심: 코루틴은 스레드의 상위 추상화입니다. 특별한 이유가 없다면 코루틴을 사용하세요.

관련 글


키워드

Kotlin, Coroutine, 코루틴, Thread, 스레드, 비동기, 동시성, 성능, 메모리, 구조적 동시성, 비교

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3