자바와 스프링의 비동기 처리 - 2편: CompletableFuture의 예외 처리와 타임 아웃

2025. 8. 1. 14:29·Backend/Spring Boot

Introduce

 

자바와 스프링의 비동기 처리 - 1편: CompletableFuture 톺아보기

Introduce 스프링에서 비동기 처리를 위해 흔히 `@Async` 를 사용하곤 한다.나 역시 프로젝트에서 `@Async` 를 적용하여 일부 후속 로직들을 메인 로직과 분리하여 실행하고 있었다. 그런데 얼마 전 면

woojjam.tistory.com

먼저 해당 글을 읽기 전에 이전 글을 읽고 오는것을 추천한다.

 

`CompletableFuture` 는 비동기 작업을 간결하게 표현할 수 있게 도와주지만, 비동기 환경에서는 예외가 어디서, 어떻게 발생했는지 추적하거나 디버깅 하기도 어렵기에 이를 처리하는것이 쉽지 않다.

 

그렇다고 예외처리를 해놓지 않으면 다음과 같은 상황이 발생할 수 있다.

  • 비동기 로직 중 하나가 실패하면 전체 로직이 중단
  • 반드시 결과가 필요한 곳에서는 fallback 전략이 필요

따라서 해당 게시글에서는 비동기 환경에서 예외 처리하는 방식에 대해서 알아보고자 한다.


Handling

 

`CompletableFuture` 를 사용할 때 핵심적으로 2가지 방식으로 예외를 처리할 수 있다.


📌 handle

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` 이다.

  1. 작업이 성공
    • result: 완료된 결과 값
    • ex: null
  2. 작업이 실패
    • result: null (또는 중간 처리 결과)
    • ex: `Throwable` (발생한 예외 객체)

정상 처리

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

예외 처리

예외가 발생한 경우 result는 null이 되고, `Throwable`이 반환되어 발생한 예외가 출력되는 것을 확인할 수 있다.

 

즉, `handle()` 은 성공과 실패 모두에 대한 처리를 하고 싶을 때 사용하며, 궁극적으로 모든 상황에 대응할 수 있는 fallback 처리기이다.


📌 completeExceptionally()

completeExceptionally() 은 아직 완료되지 않은 `completableFuture` 에 명시적으로 실패 신호를 줄 때 사용한다.

  1. 외부 조건에 따라 명시적으로 실패 상태를 만들고 싶을 때
  2. 테스트 코드에서 실패 상황을 시뮬레이션할 때

2가지 경우에 주로 사용된다.

completeExceptionally 테스트 코드


📌 handle() vs completeExceptionally()

궁극적으로 2가지 메소드의 차이는 역할이다. `handle` 은 예외를 처리해주고, `completeExceptionally` 는 예외를 발생시키는 쪽이다.

 

`handle()` 메서드는 비동기 작업이 끝난 후 성공이든 실패든 무조건 호출되는 메서드로 예외 후처리자라고 보면 된다.

반면 `completeExceptionally()` 은 말 그대로 "이 작업은 실패했어!" 라고 `Future`에게 알려주는 메서드이다. 예를 들자면 인자로 전달된 값이 null이거나, 타임 아웃 등 외부 조건에서 예외가 발생하는 경우 강제로 실패해야할 경우 해당 메소드를 통해서 예외를 발생시키는 것이다.

 

그러므로 둘 중에 선택하거나 대립되는 개념이 아니라 함께 쓰일 수도 있는 것이다. 외부 조건으로 반드시 예외가 발생해야 할 경우 `completeExceptionally()` 메소드로 예외를 선언하고, 후속 체이닝에서는 `handle()`로 fallback 값을 제공하는 방식으로 말이다.


Timeout

비동기 작업을 하다보면 응답 속도가 느려서 실패 처리하거나, 특정 시간이 초과되면 기본값으로 설정하고 싶을 때가 있다.

이럴 때 `CompletableFuture` 는 3가지 방식의 타임아웃 처리를 제공한다.


📌 get()

get(long timeout, TimeUnit unit)

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

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()

completeOnTimout

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
'Backend/Spring Boot' 카테고리의 다른 글
  • 자바와 스프링의 비동기 처리 - 1편: CompletableFuture 톺아보기
  • 스프링 이벤트를 발행하여 트랜잭션과 관심사 분리하기
  • 동시성 문제에 대한 고찰, 점진적으로 접근하기
WooJJam
WooJJam
  • WooJJam
    우쨈의 개발 블로그
    WooJJam
  • 전체
    오늘
    어제
    • 분류 전체보기 (16)
      • 끄적끄적 (1)
      • Backend (6)
        • Spring Boot (4)
        • MySQL (1)
        • Java (1)
      • DevOps (6)
        • Monitoring (3)
        • Deployment (1)
        • Github Actions (2)
      • Computer Science (3)
        • Network (1)
        • Operating System (0)
        • Database (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 공지사항

  • 인기 글

  • 태그

    로깅 시스템
    GitHub Actions
    non repeatable
    스프링 이벤트
    self-hosted runner
    CompletableFuture
    동시성
    비관적 락
    낙관적 락
    트랜잭션 분리
    GitHub hosted runner
    비동기
    devops
    promtail
    모니터링
    TransactionalEventListener
    공간인덱스
    plg stack
    List.of
    격리 수준
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
WooJJam
자바와 스프링의 비동기 처리 - 2편: CompletableFuture의 예외 처리와 타임 아웃
상단으로

티스토리툴바