CompletableFuture가 Java에서 비동기 프로그래밍을 어떻게 단순화하나요?
I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you already provided) here? Once I have the article text, I’ll translate it into Korean while preserving the original formatting, markdown, and any code blocks or URLs.
Overview
앱을 사용해 음식을 주문한다고 상상해 보세요.
음식이 준비되는 동안 화면을 바라보며 아무것도 하지 않는 대신, 소셜 미디어를 스크롤하고, 메시지에 답장하고, 동영상을 시청합니다.
바로 비동기 프로그래밍이 이렇게 작동해야 합니다.
전통적인 Java에서 비동기 코드를 작성하면 보통 다음과 같은 작업을 해야 했습니다:
- 스레드를 직접 관리
- 콜백 처리
- 복잡하고 읽기 어려운 로직 작성
여기에 **CompletableFuture**가 등장합니다.
CompletableFuture는 Java에서 비동기 프로그래밍을 더 깔끔하고, 가독성이 좋으며, 조합 가능하게 만들어 줍니다—동시성 전문가가 될 필요 없이 말이죠.
이 블로그에서는 CompletableFuture가 Java에서 비동기 프로그래밍을 어떻게 단순화하는지를 단계별로 설명하고, 완전한 Spring Boot 엔드‑투‑엔드 예제와 cURL 요청 및 실제 응답을 포함하여 보여드립니다.
핵심 개념
CompletableFuture란?
CompletableFuture는 미래의 어느 시점에 완료될 연산을 나타내는 Java 클래스입니다.
블로킹하고 기다리는 대신 다음을 할 수 있습니다:
- 작업을 비동기적으로 시작하기
- 작업을 체인으로 연결하기
- 결과와 오류를 깔끔하게 처리하기
CompletableFuture가 기존 스레드보다 나은 이유
Before CompletableFuture:
new Thread(() -> doWork()).start();
문제점
- ❌ 관리가 어려움
- ❌ 오류 처리가 용이하지 않음
- ❌ 체이닝이 없음
With CompletableFuture:
CompletableFuture.supplyAsync(() -> doWork())
.thenApply(result -> transform(result))
.thenAccept(finalResult -> use(finalResult));
코드 예시
CompletableFuture.supplyAsync(() -> doWork())
.thenApply(result -> transform(result))
.thenAccept(finalResult -> use(finalResult));
큰 그림 (한 문장)
이 코드는 작업을 비동기적으로 실행하고, 결과를 변환한 뒤 최종 출력을 소비합니다 — 메인 스레드를 차단하지 않습니다.
실제 비유 (음식 주문)
1️⃣ 주문하기 → 주방에서 요리 시작
2️⃣ 음식이 준비됨 → 양념 추가
3️⃣ 음식이 제공됨 → 당신이 먹음
당신은 주방에 서서 기다리지 않습니다 — 모든 일이 순차적으로, 비동기적으로 일어납니다.
Step‑by‑Step Breakdown
CompletableFuture.supplyAsync(() -> doWork())
CompletableFuture.supplyAsync(() -> doWork())
- What it does:
doWork()를 백그라운드 스레드에서 실행하고 즉시CompletableFuture를 반환합니다. - Main thread: 차단되지 않음.
- Analogy: “이 작업을 시작하고, 끝나면 알려줘.”
Example
String doWork() {
return "raw data";
}
Output (future result): "raw data"
.thenApply(result -> transform(result))
.thenApply(result -> transform(result))
-
What it does:
doWork()가 끝날 때까지 기다립니다.- 그 결과(
result)를 가져옵니다. - 변환하여 다른 형태로 만듭니다.
- 새로운
CompletableFuture를 반환합니다.
-
Key rule:
thenApply는 데이터 변환을 위해 사용합니다.
Example
String transform(String input) {
return input.toUpperCase();
}
Input: "raw data"
Output: "RAW DATA"
.thenAccept(finalResult -> use(finalResult))
.thenAccept(finalResult -> use(finalResult));
- What it does: 최종 변환된 결과를 받아 소비하고, 반환값은 없습니다.
- Key rule:
thenAccept는 부수 효과를 위해 사용하며, 변환을 수행하지 않습니다.
Example
void use(String data) {
System.out.println(data);
}
Output
RAW DATA
전체 흐름 (시각적)
doWork()
↓
thenApply (transform)
↓
thenAccept (consume)
또는 파이프라인으로:
Supplier → Transformer → Consumer
구체적인 예시 (완전 작동)
CompletableFuture.supplyAsync(() -> "hello world")
.thenApply(result -> result.toUpperCase())
.thenAccept(finalResult -> System.out.println(finalResult));
출력
HELLO WORLD
왜 이것이 강력한가
CompletableFuture 없이 | CompletableFuture와 함께 |
|---|---|
| 블로킹 호출 | 논블로킹 |
| 수동 스레드 처리 | 자동 |
| 콜백 지옥 | 깨끗한 체이닝 |
| 어려운 오류 처리 | 구조화된 |
일반적인 실수
- ❌ 부수 효과를 위해
thenApply사용 - ❌ 불필요하게
.get()또는.join()호출 - ❌ 비동기 단계에서 차단
- ❌ 예외 무시
요령 (이것을 기억하세요)
| 메서드 | 사용 시기 |
|---|---|
supplyAsync | 비동기 작업을 시작하고 싶을 때 |
thenApply | 데이터를 변환하고 싶을 때 |
thenAccept | 데이터를 사용하고 싶을 때 (부수 효과) |
thenRun | 데이터가 필요 없을 때 |
최종 요약
이 코드는:
- 비동기 작업을 시작합니다
- 결과를 변환합니다
- 최종 출력을 사용합니다
- 메인 스레드를 차단하지 않습니다
이 코드는 깨끗하고, 가독성이 높으며, 확장 가능합니다 — 현대 Java 비동기 코드가 보여야 할 바로 그 모습입니다.
- ✔ 가독성 좋음
- ✔ 구성 가능
- ✔ 논블로킹
일반적인 사용 사례
- ✅ 외부 API 호출
- ✅ 병렬 서비스 호출
- ✅ 백그라운드 처리
- ✅ API 응답 시간 개선
- ✅ 논블로킹 작업
워크플로우
코드 예제 (엔드‑투‑엔드)
예제 1: 기본 CompletableFuture (Pure Java)
사용 사례 – 데이터를 비동기적으로 가져와 처리합니다.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
// Simulate long‑running task
sleep(1000);
return "Async result";
});
future.thenApply(result -> result.toUpperCase())
.thenAccept(finalResult ->
System.out.println("Received: " + finalResult));
// Prevent JVM from exiting early
sleep(2000);
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {
}
}
}
What We’ll Cover
- 프로그램이 전체적으로 하는 일
- 각 섹션의 상세 내용
- 왜
sleep(2000)이 필요한가 - 단계별 실행 동작
큰 그림 (한 줄 요약)
프로그램은 작업을 비동기적으로 실행하고, 그 결과를 변환한 뒤 출력하며, 비동기 작업이 끝날 때까지 JVM이 살아 있도록 유지합니다.
Source: …
전체 코드 (참조)
(위 코드 블록과 동일 – 빠른 참고를 위해 여기에도 남겨 둡니다.)
CompletableFuture.supplyAsync(...)
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Async result";
});
동작 설명
- 백그라운드 스레드(ForkJoinPool 공용 풀)에서 작업을 시작합니다.
- 작업은 1초 동안
sleep하여(느린 작업을 흉내)"Async result"를 반환합니다. CompletableFuture를 즉시 반환하고,main스레드는 대기하지 않습니다.
중요:
main스레드는 비동기 작업이 백그라운드에서 실행되는 동안 계속 진행됩니다.
thenApply(...) – 결과 변환
future.thenApply(result -> result.toUpperCase())
동작 설명
- 비동기 작업이 끝날 때까지 기다립니다.
- 결과(
"Async result")를 받아 대문자로 변환하고 다음 단계에 전달합니다.
| 입력 | 출력 |
|---|---|
"Async result" | "ASYNC RESULT" |
규칙: 데이터를 변환해야 할 때
thenApply를 사용합니다.
thenAccept(...) – 결과 소비
.thenAccept(finalResult ->
System.out.println("Received: " + finalResult));
동작 설명
- 변환된 값을 받아옵니다.
- 값을 출력합니다.
- 반환값이 없습니다(
void).
출력 결과
Received: ASYNC RESULT
규칙: 부수 효과(로그 기록, 출력, 저장 등)를 수행할 때
thenAccept를 사용합니다.
main에서 sleep(2000)이 필요한 이유
sleep(2000);
- 이 코드는 JVM을 충분히 오래 유지시켜 비동기 작업이 끝날 때까지 기다리게 합니다.
- 이 라인이 없으면
main()이 바로 종료되고, JVM이 종료되어 비동기 작업이 완료되지 않을 수 있습니다.
실제 애플리케이션(예: Spring Boot, 서버 등)에서는 JVM이 자동으로 계속 실행되므로 이러한 인위적인
sleep은 필요하지 않습니다.
실행 타임라인 (단계별)
| 시간 (ms) | 동작 |
|---|---|
| 0 | main() 시작; supplyAsync()가 작업을 백그라운드 스레드에 제출합니다. |
| 1 | main()이 thenApply와 thenAccept를 등록합니다. |
| 1000 | 백그라운드 스레드가 sleep(1000)을 마치고 "Async result"를 반환합니다. |
| ~1000 | thenApply 실행 → "ASYNC RESULT"; thenAccept 실행 → 결과를 출력합니다. |
| 2000 | main()이 sleep(2000)을 마치고 JVM이 안전하게 종료됩니다. |
시각적 흐름
Main Thread
|
|-- submit async task -------------------->
|
|-- register thenApply
|-- register thenAccept
|
|-- sleep(2000) ← keeps JVM alive
|
Background Thread
|
|-- sleep(1000)
|-- return "Async result"
|-- transform to "ASYNC RESULT"
|-- print result
흔히 하는 초보자 실수
- ❌ 독립 실행 프로그램에서
sleep(2000)을 제거하기. - ❌ 비동기 코드 블록이 자동으로 실행된다고 가정하기.
- ❌ 출력에
thenApply를 사용하기 (thenAccept를 대신 사용). - ❌ 불필요하게
.get()을 호출하여 논블로킹 코드의 목적을 무력화하기.
핵심 요점 (외우세요)
| Operation | Purpose |
|---|---|
supplyAsync | 별도의 스레드에서 비동기 작업을 시작합니다. |
thenApply | 이전 단계의 결과를 변환합니다. |
thenAccept | 결과를 소비합니다(부수 효과). |
sleep in main | 데모 프로그램을 위해 JVM을 계속 실행시킵니다. |
수면 시간, 변환 로직을 바꾸거나 추가 단계를 연결해 보면서 자유롭게 실험해 보세요!
Source: …
예제 2: Spring Boot + CompletableFuture (100 % 엔드‑투‑엔드)
사용 사례
비동기적으로 작업을 수행하고 결과를 반환하는 REST API를 노출합니다.
1단계: 서비스 레이어 (비동기 로직)
package com.example.demo.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class AsyncService {
public CompletableFuture<String> processAsync() {
return CompletableFuture.supplyAsync(() -> {
simulateDelay();
return "Async processing completed";
});
}
private void simulateDelay() {
try {
Thread.sleep(1500);
} catch (InterruptedException ignored) {
}
}
}
2단계: REST 컨트롤러
package com.example.demo.controller;
import com.example.demo.service.AsyncService;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
@RestController
@RequestMapping("/async")
public class AsyncController {
private final AsyncService asyncService;
public AsyncController(AsyncService asyncService) {
this.asyncService = asyncService;
}
/**
* Example:
* GET /async/process
*/
@GetMapping("/process")
public CompletableFuture<String> processAsync() {
return asyncService.processAsync();
}
}
📌 Spring Boot 자동 처리
CompletableFuture감지- 요청 스레드 해제
- 계산이 완료되면 응답 전송
3단계: cURL 로 테스트
요청
curl --location 'http://localhost:8080/async/process'
응답 (약 1.5초 후)
Async processing completed
장점
- ✔ 논블로킹 요청
- ✔ 스레드 효율성
- ✔ 깔끔한 비동기 API
내부에서 무슨 일이 일어났나요?
- HTTP 요청이 컨트롤러에 도달합니다.
- 컨트롤러가
CompletableFuture를 반환합니다. - 서블릿 스레드가 해제됩니다.
- 비동기 작업이 백그라운드에서 실행됩니다.
- 미래가 완료될 때 응답이 작성됩니다.
이것이 고성능 API가 구축되는 정확한 방식입니다.
모범 사례
- Avoid blocking calls inside
CompletableFuture– don’t mix async code with heavy blocking logic. - Use chaining instead of nested callbacks – prefer
thenApply,thenCompose,thenAccept.