Java에서 비동기 처리를 구현하는 방식은 여러 가지가 있지만,
Java 8 이후 가장 널리 사용되는 방식은 CompletableFuture이다.
과거에는 Future + ExecutorService 조합으로 비동기 처리를 구현했지만,
이 방식은 결과 조합, 예외 처리, 체이닝이 불편했다.
CompletableFuture는 이러한 한계를 해결하기 위해 등장했다.
1. 비동기 처리?
비동기 처리는 작업을 요청한 후, 결과를 기다리지 않고 다음 코드를 실행하는 방식이다.
즉, 흐름을 멈추지 않는다.
동기 처리: 작업 요청 → 결과 대기 → 다음 코드 실행
비동기 처리: 작업 요청 → 다음 코드 실행 → 결과는 나중에 처리
네트워크 호출, DB 조회, 파일 I/O 같은 작업은 대기 시간이 길기 때문에
비동기로 처리하지 않으면 시스템 자원이 낭비된다.
2. CompletableFuture란?
CompletableFuture는 Java의 비동기 연산 결과를 표현하는 클래스이다.
- 비동기 작업을 실행할 수 있다
- 작업이 끝난 뒤 이어서 실행할 로직을 정의할 수 있다
- 여러 비동기 작업을 조합할 수 있다
- 예외 처리까지 포함한 비동기 파이프라인을 구성할 수 있다
CompletableFuture는 “비동기 작업 + 그 이후 동작”을 하나의 흐름으로 표현한다.
3. 기본 사용법
3-1. 비동기 작업 실행: supplyAsync
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
return callExternalApi();
});
- supplyAsync는 반환값이 있는 비동기 작업에 사용한다
- 내부적으로 ForkJoinPool.commonPool을 사용한다
3-2. 결과 처리: thenApply, thenAccept
CompletableFuture
.supplyAsync(() -> callApi())
.thenApply(result -> result.toUpperCase())
.thenAccept(result -> System.out.println(result));
- thenApply → 값을 변환
- thenAccept → 소비만 하고 반환 없음
이 방식은 콜백 지옥 없이 비동기 흐름을 자연스럽게 표현한다.
4. ExecutorService와 함께 사용하기
기본 commonPool 대신 직접 만든 스레드 풀을 사용하는 것이 실무에서 권장된다.
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> callApi(), executor);
이렇게 하면:
- 스레드 개수 제어 가능
- 시스템 전체에 영향을 주지 않음
- 장애 격리가 쉬워진다
5. 여러 비동기 작업 조합하기
5-1. 두 작업 결과를 조합: thenCombine
CompletableFuture<String> f1 =
CompletableFuture.supplyAsync(() -> callApi1());
CompletableFuture<String> f2 =
CompletableFuture.supplyAsync(() -> callApi2());
CompletableFuture<String> combined =
f1.thenCombine(f2, (r1, r2) -> r1 + r2);
두 작업이 모두 끝났을 때만 실행된다.
5-2. 여러 작업 완료 대기: allOf
CompletableFuture<Void> all =
CompletableFuture.allOf(f1, f2, f3);
all.join(); // 모든 작업 완료 대기
여러 API 호출, 병렬 계산 결과를 모을 때 자주 사용된다.
6. 예외 처리 방식
비동기에서 예외 처리는 매우 중요하다.
CompletableFuture는 이를 위한 전용 메서드를 제공한다.
6-1. exceptionally
CompletableFuture
.supplyAsync(() -> callApi())
.exceptionally(ex -> {
log.error("에러 발생", ex);
return "fallback";
});
예외 발생 시 대체 값을 반환한다.
6-2. handle
CompletableFuture
.supplyAsync(() -> callApi())
.handle((result, ex) -> {
if (ex != null) {
return "error";
}
return result;
});
성공/실패를 모두 처리할 수 있다.
7. 동기처럼 결과를 기다려야 할 때
비동기 처리라도 경계 지점에서는 결과를 기다려야 한다.
String result = future.join();
- join()은 unchecked 예외(RuntimeException) 형태로 던진다
- get()은 checked 예외를 강제한다
실무에서는 join()을 더 많이 사용한다.
8. CompletableFuture가 적합한 실무 사례
✔ 외부 API 병렬 호출
- 결제 정보 조회
- 사용자 정보 + 권한 정보 병렬 로딩
✔ I/O 중심 작업
- 이메일 발송
- 알림 전송
- 로그 적재
✔ 마이크로서비스 간 통신
- 여러 서비스 호출 결과를 하나의 응답으로 합칠 때
9. CompletableFuture vs Thread 직접 사용
| 항목 | Thread | CompletableFuture |
| 비동기 흐름 표현 | 어렵다 | 매우 쉽다 |
| 결과 조합 | 불편 | 체이닝으로 가능 |
| 예외 처리 | 번거롭다 | 전용 API 제공 |
| 가독성 | 낮다 | 높다 |
결론적으로, Thread를 직접 다루는 경우는 거의 없다.
10. 주의사항
⚠ 블로킹 코드 섞지 말 것
CompletableFuture.supplyAsync(() -> {
Thread.sleep(1000); // 좋지 않다
});
비동기 안에서 다시 블로킹을 하면 장점이 사라진다.
⚠ commonPool 남용 주의
- CPU 바운드 작업과 섞이면 성능 저하 발생
- 반드시 커스텀 Executor 사용을 고려해야 한다
'IT > JAVA' 카테고리의 다른 글
| CompletableFuture vs WebFlux (0) | 2026.01.02 |
|---|---|
| Java 멀티스레드와 병렬 실행 (0) | 2025.12.07 |
| Java 함수형 인터페이스 Supplier, Runnable (0) | 2025.12.07 |
| getOrDefault() vs computeIfAbsent() (0) | 2025.12.06 |
| ReentrantLock 락 제어 (0) | 2025.12.06 |