Introduce
자바와 스프링의 비동기 처리 - 1편: CompletableFuture 톺아보기
Introduce 스프링에서 비동기 처리를 위해 흔히 `@Async` 를 사용하곤 한다.나 역시 프로젝트에서 `@Async` 를 적용하여 일부 후속 로직들을 메인 로직과 분리하여 실행하고 있었다. 그런데 얼마 전 면
woojjam.tistory.com
먼저 해당 글을 읽기 전에 이전 글을 읽고 오는것을 추천한다.
`CompletableFuture` 는 비동기 작업을 간결하게 표현할 수 있게 도와주지만, 비동기 환경에서는 예외가 어디서, 어떻게 발생했는지 추적하거나 디버깅 하기도 어렵기에 이를 처리하는것이 쉽지 않다.
그렇다고 예외처리를 해놓지 않으면 다음과 같은 상황이 발생할 수 있다.
- 비동기 로직 중 하나가 실패하면 전체 로직이 중단
- 반드시 결과가 필요한 곳에서는 fallback 전략이 필요
따라서 해당 게시글에서는 비동기 환경에서 예외 처리하는 방식에 대해서 알아보고자 한다.
Handling
`CompletableFuture` 를 사용할 때 핵심적으로 2가지 방식으로 예외를 처리할 수 있다.
📌 handle

handle() 메서드는 비동기 작업이 성공하든 실패하든 무조건 호출된다. 즉, 결과와 예외를 동시에 받아 처리할 수 있는 매우 유연한 처리방식이다.
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
if (new Random().nextBoolean()) {
throw new RuntimeException("랜덤 예외 발생!");
}
return "정상 결과";
}).handle((result, ex) -> {
if (ex != null) {
System.out.println("예외 처리: " + ex.getMessage());
return "예외 발생 시 기본값";
}
return result;
}).thenAccept((result -> {
System.out.println("최종 결과: " + result);
}));
`handle` 메소드는 2개의 파라미터를 가진다. `(result, ex) -> { ... }` 형식의 `BiFunction` 이다.
- 작업이 성공
- result: 완료된 결과 값
- ex: null
- 작업이 실패
- result: null (또는 중간 처리 결과)
- ex: `Throwable` (발생한 예외 객체)

예외가 발생하지 않은 경우 result가 반환되어 최종 문자열이 출력된 것을 확인할 수 있고,

예외가 발생한 경우 result는 null이 되고, `Throwable`이 반환되어 발생한 예외가 출력되는 것을 확인할 수 있다.
즉, `handle()` 은 성공과 실패 모두에 대한 처리를 하고 싶을 때 사용하며, 궁극적으로 모든 상황에 대응할 수 있는 fallback 처리기이다.
📌 completeExceptionally()

completeExceptionally() 은 아직 완료되지 않은 `completableFuture` 에 명시적으로 실패 신호를 줄 때 사용한다.
- 외부 조건에 따라 명시적으로 실패 상태를 만들고 싶을 때
- 테스트 코드에서 실패 상황을 시뮬레이션할 때
2가지 경우에 주로 사용된다.

📌 handle() vs completeExceptionally()
궁극적으로 2가지 메소드의 차이는 역할이다. `handle` 은 예외를 처리해주고, `completeExceptionally` 는 예외를 발생시키는 쪽이다.
`handle()` 메서드는 비동기 작업이 끝난 후 성공이든 실패든 무조건 호출되는 메서드로 예외 후처리자라고 보면 된다.
반면 `completeExceptionally()` 은 말 그대로 "이 작업은 실패했어!" 라고 `Future`에게 알려주는 메서드이다. 예를 들자면 인자로 전달된 값이 null이거나, 타임 아웃 등 외부 조건에서 예외가 발생하는 경우 강제로 실패해야할 경우 해당 메소드를 통해서 예외를 발생시키는 것이다.
그러므로 둘 중에 선택하거나 대립되는 개념이 아니라 함께 쓰일 수도 있는 것이다. 외부 조건으로 반드시 예외가 발생해야 할 경우 `completeExceptionally()` 메소드로 예외를 선언하고, 후속 체이닝에서는 `handle()`로 fallback 값을 제공하는 방식으로 말이다.
Timeout
비동기 작업을 하다보면 응답 속도가 느려서 실패 처리하거나, 특정 시간이 초과되면 기본값으로 설정하고 싶을 때가 있다.
이럴 때 `CompletableFuture` 는 3가지 방식의 타임아웃 처리를 제공한다.
📌 get()

get() 메서드를 통해 timeout 설정이 가능한데 이는 java8에서 부터 가능했던 전통적인 방식이다. `Future` 처럼 결과를 지정된 시간 내에 기다리다가 시간이 초과되면 예외가 발생한다.
@Test
@DisplayName("get()으로 타임아웃 설정하기")
void get_타임아웃_설정() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000); // 3초 대기
return "응답 완료";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
try {
String result = future.get(1, TimeUnit.SECONDS); // 1초만 기다림
System.out.println("결과: " + result);
} catch (TimeoutException e) {
System.out.println("타임아웃 발생!");
}
}
java9 부터는 timeout을 위한 `orTimeout()`, `completeOnTimeout()` 메서드가 추가되었다.
📌 orTimeout()

orTimeout() 은 비동기 작업 체이닝 도중 시간이 초과되면 자동으로 `TimeoutException`이 발생한다. `get()` 과 달리 체이닝이 가능하며, 논블로킹으로 동작한다.
@Test
@DisplayName("orTimeout()으로 타임아웃 설정하기")
void orTimeout_타임아웃_설정() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
return "응답 완료";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).orTimeout(1, TimeUnit.SECONDS)
.exceptionally(ex -> "타임아웃 기본값");
assertThat("타임아웃 기본값").isEqualTo(future.get());
}
📌 completeOnTimeout()

completeOnTimeout 은 지정 시간이 지나도 완료되지 않으면 예외 대신 지정된 값으로 자동으로 완료시켜주는 메서드이다. 그러므로 실패로 만들지 않고, 성공으로 간주하는 기본값 처리 방식이다.
@Test
@DisplayName("completeOnTimeout()으로 타임아웃 설정하기")
void completeOnTimout_타임아웃_설정() throws ExecutionException, InterruptedException {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(3000);
return "응답 완료";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).completeOnTimeout("타임아웃 기본값", 1, TimeUnit.SECONDS);
assertThat("타임아웃 기본값").isEqualTo(future.get());
}
'Backend > Spring Boot' 카테고리의 다른 글
| 자바와 스프링의 비동기 처리 - 1편: CompletableFuture 톺아보기 (3) | 2025.07.11 |
|---|---|
| 스프링 이벤트를 발행하여 트랜잭션과 관심사 분리하기 (2) | 2025.04.29 |
| 동시성 문제에 대한 고찰, 점진적으로 접근하기 (0) | 2025.03.17 |