with ChatGPT 시리즈는 ChatGPT의 내용과 개인의 생각을 토대로 학습해 보는 컨텐츠입니다.
Java 개발자에게 Kotlin의 코루틴은 익숙한 Thread, ExecutorService, CompletableFuture 등과는 전혀 다른 개념처럼 느껴질 수 있습니다.
하지만 실제로는 동시성과 비동기 처리를 더 안전하고 선언적으로 표현하기 위해 만들어진 경량 스레드 모델입니다.
이번 글에서는 코루틴의 개념을 이해하고, Java의 기존 방식들과 비교하여 어떤 점이 다른지, 왜 더 나은 선택지가 될 수 있는지 살펴보겠습니다.
🔹 1. Java에서의 비동기 처리 방식
✅ 1) Thread 또는 Runnable
new Thread(() -> {
System.out.println("비동기 작업 수행 중...");
}).start();
- 단순하지만 스레드 비용이 크고, 제어가 어렵다
- 결과를 반환하려면 복잡한 코드 필요
✅ 2) ExecutorService + Future
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> "작업 결과");
String result = future.get(); // 블로킹
- 비동기적으로 작업을 실행하고 결과를 받을 수 있음
- 하지만 get()은 여전히 블로킹 (blocking)
✅ 3) CompletableFuture (Java 8+)
CompletableFuture.supplyAsync(() -> "비동기 결과")
.thenAccept(result -> System.out.println("결과: " + result));
- 체이닝, 콜백 등 다양한 비동기 로직을 표현할 수 있지만
- 콜백 지옥, 예외 흐름 복잡, 병렬 제어 어려움 등 유지보수 측면에서 어려움
🔹 2. Kotlin의 코루틴 기본 개념
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000)
println("비동기 처리 완료")
}
println("코루틴 시작")
}
- launch는 코루틴을 시작하는 빌더
- delay()는 일시 중단 (suspend) 함수 - 실제 스레드를 블로킹하지 않음
- runBlocking은 메인 함수에서 코루틴을 실행할 수 있게 하는 진입점
✅ 핵심 철학: "비동기 코드를 동기처럼 읽기 쉽게 만들자"
val result = fetchData() // suspend 함수라면 마치 동기처럼 보임
println(result)
- 내부는 비동기지만, 코드는 동기적인 흐름처럼 작성 가능
- async/await, launch, suspend, flow 등을 통해 코드의 선언적 표현력 극대화
🔍 Java vs Kotlin 코루틴 비교
항목 | Java (Future, CompletableFuture) | Kotlin Coroutine |
실행 단위 | Thread / Future / Runnable | Coroutine (경량 스레드) |
비동기 표현 방식 | 콜백 기반 | 순차적 코드 스타일 |
블로킹 | 많음 (get(), join()) | 없음 (delay(), suspend) |
예외 처리 | try-catch, exceptionally 등 복잡 | try-catch 그대로 사용 가능 |
취소 / 타임아웃 | 복잡, 수동 처리 필요 | 간단한 API 제공 (withTimeout, cancel()) |
병렬 처리 | thenCombine, allOf, 직접 구현 | async, awaitAll 등 간단하고 직관적 |
실무 적용 | 유지보수 어려움, 버그 발생 위험 | 구조적 동시성 제공 → 안정성 높음 |
🧠 구조적 동시성 (Structured Concurrency)
fun CoroutineScope.fetch() = launch {
val user = async { fetchUser() }
val posts = async { fetchPosts() }
println(user.await())
println(posts.await())
}
- launch, async 모두 상위 Scope가 끝날 때 하위 코루틴도 함께 종료됨
- 메모리 누수나 고아 코루틴 방지
- Java에서는 직접 명시적으로 관리해야 하는 부분을 코틀린이 구조적으로 묶어줌
🧪 간단 비교 예제
✅ Java (CompletableFuture)
CompletableFuture.supplyAsync(() -> {
Thread.sleep(1000);
return "Data";
}).thenAccept(System.out::println);
✅ Kotlin (Coroutine)
GlobalScope.launch {
val data = fetchData() // suspend 함수
println(data)
}
- 논리 흐름이 깔끔하고, 예외 처리도 자연스럽게 try-catch로 가능
🚀 결론
핵심 비교 | Java 방식 | Kotlin 코루틴 |
개념 | Thread/Future 기반 | 경량 스레드 (Coroutine) |
비동기 표현 | 콜백, thenApply | suspend, launch, async |
코드 가독성 | 낮음 (콜백 지옥) | 높음 (동기처럼 작성) |
예외 처리 | 복잡 | 직관적 |
자원 관리 | 수동적 | 구조적 동시성으로 자동 |
학습 난이도 | 익숙하지만 지저분 | 처음 어렵지만 강력 |
Kotlin 코루틴은 단순한 비동기 도구가 아니라 비동기 프로그래밍의 패러다임을 바꾸는 핵심 도구입니다.
Java 개발자라면 코루틴을 도입함으로써 코드의 가독성, 안정성, 유지보수성 모두를 개선할 수 있습니다.
🤔 추가로 학습할 질문들
- launch와 async의 차이점은 정확히 무엇인가요?
- suspend 함수는 언제 선언해야 하나요?
- runBlocking은 언제 사용하고, 실무에서는 어떻게 대체하나요?
- Java에서 Kotlin 코루틴을 연동하는 방법은?
- Spring WebFlux나 Reactor와 코루틴을 비교하면 어떤 차이가 있을까요?