Spring Boot에서 WebFlux나 WebClient를 사용하다 보면 에러 처리와 관련된 메서드를 자주 만나게 됩니다.
대표적으로 다음과 같은 메서드들이 있습니다.
onErrordoOnErroronErrorResumeonStatus
이름이 비슷해서 처음에는 헷갈릴 수 있습니다.
하지만 각각의 역할을 나누어 보면 생각보다 어렵지 않습니다.
간단히 말하면 다음과 같습니다.
- onError → 에러가 발생했음을 알리는 신호
- doOnError → 에러가 발생했을 때 로그를 남기는 용도
- onErrorResume → 에러가 발생했을 때 대체 흐름으로 복구하는 용도
- onStatus → WebClient에서 HTTP 상태 코드를 예외로 바꾸는 용도
onError
onError는 Reactive Stream에서 에러가 발생했을 때 전달되는 에러 종료 신호입니다.
Mono나 Flux는 데이터를 처리하다가 에러가 발생하면 onError 신호를 보내고 스트림을 종료합니다.
예를 들어 다음 코드를 보겠습니다.
Mono.just("hello")
.map(value -> {
throw new RuntimeException("문제 발생");
})
.subscribe(
value -> System.out.println("value = " + value),
error -> System.out.println("error = " + error.getMessage())
);
위 코드에서 subscribe()의 두 번째 인자가 에러를 받는 부분입니다.
error -> System.out.println("error = " + error.getMessage())
이 부분이 바로 onError 콜백이라고 볼 수 있습니다.
즉, onError는 에러가 최종적으로 도착했을 때 실행되는 콜백입니다.
자바의 try-catch로 비유하면 catch 블록과 비슷합니다.
try {
// 정상 처리
} catch (Exception e) {
// 에러 처리
}
다만 중요한 점이 있습니다.
onError는 보통 스트림 중간에서 사용하는 에러 처리 연산자라기보다는,
최종 구독 시점에 에러를 받는 신호 또는 콜백에 가깝습니다.
그래서 중간에서 로그를 남기고 싶다면 doOnError를 사용하고,
에러를 복구하고 싶다면 onErrorResume을 사용합니다.
doOnError
doOnError는 에러가 발생했을 때 로그를 남기거나, 알림을 보내거나, 메트릭을 기록할 때 사용합니다.
여기서 가장 중요한 점은 다음입니다.
doOnError는 에러를 처리하지 않습니다.
에러를 관찰하고, 부가 작업만 수행합니다.
예제를 보겠습니다.
return userService.findUser(id)
.doOnError(error -> log.error("사용자 조회 실패: id={}", id, error));
위 코드는 findUser()에서 에러가 발생했을 때 로그를 남깁니다.
하지만 로그를 남겼다고 해서 에러가 사라지는 것은 아닙니다.
에러는 그대로 다음 단계로 전파됩니다.
자바 코드로 비유하면 다음과 비슷합니다.
try {
return findUser(id);
} catch (Exception e) {
log.error("사용자 조회 실패", e);
throw e;
}
즉, doOnError는 에러를 잡아서 해결하는 것이 아니라,
에러가 발생했다는 사실을 보고 필요한 부가 작업만 하는 용도입니다.
그래서 보통 다음과 같은 작업에 사용합니다.
- 에러 로그 남기기
- 장애 알림 보내기
- 에러 카운트 증가시키기
- 모니터링용 메트릭 기록하기
예를 들어 다음처럼 사용할 수 있습니다.
return externalApiService.call()
.doOnError(error -> log.error("외부 API 호출 실패", error));
이 코드는 외부 API 호출이 실패했을 때 로그만 남깁니다.
실패한 요청을 복구하지는 않습니다.
onErrorResume
onErrorResume은 에러가 발생했을 때 다른 Mono나 Flux로 대체하는 연산자입니다.
쉽게 말하면, 에러가 발생했을 때 fallback 처리를 할 수 있습니다.
예제를 보겠습니다.
return userService.findUser(id)
.onErrorResume(error -> {
log.warn("사용자 조회 실패. 기본 사용자 정보를 반환합니다.", error);
return Mono.just(new UserDto("anonymous"));
});
위 코드는 findUser()에서 에러가 발생하면 기본 사용자 정보를 반환합니다.
자바 코드로 비유하면 다음과 비슷합니다.
try {
return findUser(id);
} catch (Exception e) {
return new UserDto("anonymous");
}
즉, onErrorResume은 에러가 발생했을 때 그냥 실패로 끝내지 않고,
다른 흐름으로 이어가고 싶을 때 사용합니다.
조건에 따라 다르게 처리하기
onErrorResume은 에러의 종류에 따라 다르게 처리할 수도 있습니다.
return userService.findUser(id)
.onErrorResume(error -> {
if (error instanceof UserNotFoundException) {
return Mono.empty();
}
if (error instanceof TimeoutException) {
return cacheService.findUser(id);
}
return Mono.error(error);
});
위 코드는 에러 종류에 따라 다음처럼 동작합니다.
- UserNotFoundException 발생
→ 값 없이 정상 종료 - TimeoutException 발생
→ 캐시에서 사용자 정보 조회 - 그 외 에러 발생
→ 다시 에러 전파
여기서 Mono.empty()는 값 없이 정상 종료하겠다는 의미입니다.
- return Mono.empty();
반대로 Mono.error(error)는 에러를 다시 전파하겠다는 의미입니다.
- return Mono.error(error);
즉, 내가 처리할 수 있는 에러는 처리하고,
처리할 수 없는 에러는 다시 넘길 수 있습니다.
onErrorResume 사용 시 주의할 점
onErrorResume을 사용할 때는 반환 타입에 주의해야 합니다.
다음 코드는 잘못된 예입니다.
.onErrorResume(error -> defaultValue)
onErrorResume은 단순 값을 반환하는 것이 아니라,Mono나 Flux를 반환해야 합니다.
따라서 다음처럼 작성해야 합니다.
.onErrorResume(error -> Mono.just(defaultValue))
값 없이 정상 종료하고 싶다면 다음처럼 작성할 수 있습니다.
.onErrorResume(error -> Mono.empty())
에러를 다시 던지고 싶다면 다음처럼 작성할 수 있습니다.
.onErrorResume(error -> Mono.error(error))
onStatus
onStatus는 앞에서 살펴본 doOnError, onErrorResume과는 조금 다릅니다.
onStatus는 Reactor의 일반적인 에러 처리 연산자가 아니라,
Spring WebClient에서 HTTP 응답 상태 코드를 보고 예외로 바꿀지 결정하는 메서드입니다.
보통 retrieve() 뒤에 사용합니다.
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(
status -> status.is4xxClientError(),
response -> Mono.error(new RuntimeException("클라이언트 에러입니다."))
)
.onStatus(
status -> status.is5xxServerError(),
response -> Mono.error(new RuntimeException("서버 에러입니다."))
)
.bodyToMono(UserDto.class);
위 코드는 HTTP 응답 상태 코드에 따라 예외를 발생시킵니다.
예를 들어 4xx 응답이 오면 클라이언트 에러로 처리하고,5xx 응답이 오면 서버 에러로 처리합니다.
조금 더 구체적으로 404만 처리하고 싶다면 다음처럼 작성할 수 있습니다.
.onStatus(
status -> status.value() == 404,
response -> Mono.error(new UserNotFoundException("사용자를 찾을 수 없습니다."))
)
이 코드는 HTTP 응답 상태 코드가 404일 때 UserNotFoundException을 발생시킵니다.
onStatus는 언제 사용할까요?
onStatus는 WebClient로 외부 API를 호출할 때 자주 사용합니다.
예를 들어 외부 API에서 다음과 같은 응답이 온다고 가정해 보겠습니다.
- 200 OK → 정상 응답
- 404 Not Found → 사용자를 찾을 수 없음
- 500 Server Error → 외부 서버 장애
이때 상태 코드별로 의미 있는 예외를 만들고 싶다면 onStatus를 사용하면 됩니다.
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(
status -> status.value() == 404,
response -> Mono.error(new UserNotFoundException("사용자를 찾을 수 없습니다."))
)
.onStatus(
status -> status.is5xxServerError(),
response -> Mono.error(new ExternalApiException("외부 API 서버 오류입니다."))
)
.bodyToMono(UserDto.class);
이렇게 작성하면 HTTP 상태 코드를 서비스에서 이해하기 쉬운 예외로 바꿀 수 있습니다.
onStatus와 onErrorResume 함께 사용하기
실무에서는 onStatus와 onErrorResume을 함께 사용하는 경우가 많습니다.
onStatus는 HTTP 상태 코드를 예외로 바꾸고,onErrorResume은 그 예외를 어떻게 처리할지 결정합니다.
예제를 보겠습니다.
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(
status -> status.value() == 404,
response -> Mono.error(new UserNotFoundException("사용자를 찾을 수 없습니다."))
)
.onStatus(
status -> status.is5xxServerError(),
response -> Mono.error(new ExternalApiException("외부 API 서버 오류입니다."))
)
.bodyToMono(UserDto.class)
.doOnError(error -> log.error("외부 사용자 API 호출 실패", error))
.onErrorResume(UserNotFoundException.class, error -> Mono.empty())
.onErrorResume(ExternalApiException.class, error -> cacheService.findUser(id));
위 코드의 흐름은 다음과 같습니다.
- WebClient 요청
↓ - HTTP 응답 도착
↓ - onStatus에서 상태 코드 확인
↓ - 404이면 UserNotFoundException 발생
5xx이면 ExternalApiException 발생
↓ - bodyToMono 이후 에러 전파
↓ - doOnError로 로그 기록
↓ - onErrorResume으로 fallback 처리
조금 더 풀어서 보면 다음과 같습니다.
404 Not Found가 오면 UserNotFoundException이 발생합니다.
이후 onErrorResume에서 Mono.empty()로 처리합니다.
.onErrorResume(UserNotFoundException.class, error -> Mono.empty())
외부 API 서버에 문제가 있어서 5xx 응답이 오면 ExternalApiException이 발생합니다.
이 경우에는 캐시에서 사용자 정보를 조회하도록 처리합니다.
.onErrorResume(ExternalApiException.class, error -> cacheService.findUser(id))
이렇게 작성하면 HTTP 상태 코드 처리와 fallback 처리를 역할별로 나눌 수 있습니다.
차이 정리
| 구분 | 역할 | 에러를 처리하나요? | 주 사용 위치 |
|---|---|---|---|
onError |
에러 종료 신호 또는 subscribe의 에러 콜백 | 직접 복구하지 않습니다 | subscribe() |
doOnError |
에러 발생 시 로그, 알림, 메트릭 처리 | 처리하지 않습니다 | Mono, Flux 중간 |
onErrorResume |
에러 발생 시 대체 흐름으로 전환 | 처리합니다 | Mono, Flux 중간 |
onStatus |
HTTP 상태 코드를 예외로 변환 | 예외를 생성합니다 | WebClient.retrieve() 뒤 |
자주 헷갈리는 부분
doOnError는 에러를 해결하지 않습니다
다음 코드를 보겠습니다.
return service.call()
.doOnError(error -> log.error("호출 실패", error));
이 코드는 로그만 남깁니다.
에러는 그대로 전파되기 때문에,
이후에 별도의 처리가 없다면 최종적으로 에러 응답이 발생할 수 있습니다.
에러를 복구하려면 onErrorResume을 함께 사용해야 합니다.
return service.call()
.doOnError(error -> log.error("호출 실패", error))
.onErrorResume(error -> Mono.just(defaultValue));
onErrorResume은 값을 바로 반환하지 않습니다
다음 코드는 잘못된 방식입니다.
.onErrorResume(error -> defaultValue)
onErrorResume은 Mono나 Flux를 반환해야 합니다.
따라서 다음처럼 작성해야 합니다.
.onErrorResume(error -> Mono.just(defaultValue))
값이 없다는 것을 정상적으로 표현하고 싶다면 Mono.empty()를 사용할 수 있습니다.
.onErrorResume(error -> Mono.empty())
onStatus는 HTTP 상태 코드만 처리합니다
onStatus는 404, 500 같은 HTTP 응답 상태 코드를 처리합니다.
하지만 다음과 같은 문제는 HTTP 상태 코드가 아닙니다.
- 네트워크 연결 실패
- DNS 오류
- 타임아웃
- JSON 파싱 실패
이런 경우에는 onStatus가 아니라 onErrorResume, retry, doOnError 등을 사용해야 합니다.
return webClient.get()
.uri("/users/{id}", id)
.retrieve()
.onStatus(
status -> status.isError(),
response -> Mono.error(new RuntimeException("HTTP 에러입니다."))
)
.bodyToMono(UserDto.class)
.onErrorResume(WebClientRequestException.class, error -> {
// 연결 실패, DNS 문제, timeout 등은 여기서 처리할 수 있습니다.
return Mono.empty();
});
즉, onStatus는 HTTP 응답이 도착한 뒤의 상태 코드를 처리합니다.
반면 네트워크 연결 실패처럼 응답 자체를 받지 못한 경우에는 onErrorResume 같은 방식으로 처리해야 합니다.
실무에서 많이 사용하는 흐름
실무에서는 보통 다음과 같은 흐름으로 작성합니다.
return webClient.get()
.uri("/api")
.retrieve()
.onStatus(...)
.bodyToMono(Response.class)
.doOnError(...)
.onErrorResume(...);
각 단계의 역할은 다음과 같습니다.
- retrieve()
→ HTTP 응답을 가져옵니다. - onStatus(...)
→ HTTP 상태 코드를 보고 예외로 바꿉니다. - bodyToMono(...)
→ 응답 body를 원하는 객체로 변환합니다. - doOnError(...)
→ 에러가 발생했다면 로그를 남깁니다. - onErrorResume(...)
→ 처리 가능한 에러라면 fallback 처리를 합니다.
이 흐름을 기억해 두면 WebClient 에러 처리를 훨씬 이해하기 쉬워집니다.
마무리
정리하면 다음과 같습니다.
onError는 에러가 발생했을 때 전달되는 신호 또는 콜백입니다.
doOnError는 에러가 발생했을 때 로그를 남기거나 부가 작업을 수행하는 용도입니다.
다만 에러를 직접 해결하지는 않습니다.
onErrorResume은 에러가 발생했을 때 대체 흐름으로 복구하는 용도입니다.
onStatus는 WebClient에서 HTTP 상태 코드를 예외로 바꾸는 용도입니다.
처음에는 이름이 비슷해서 헷갈릴 수 있지만, 역할을 나누어 생각하면 이해하기 쉽습니다.
- HTTP 상태 코드 처리 → onStatus
- 에러 로그 기록 → doOnError
- 에러 복구 처리 → onErrorResume
- 최종 에러 콜백 → onError
각 메서드의 역할을 명확히 구분해서 사용하면 WebFlux와 WebClient의 에러 처리를 더 깔끔하게 작성할 수 있습니다.
'프로그래밍 > SpringBoot' 카테고리의 다른 글
| Apache HttpClient maxConnTotal 과 maxConnPerRoute 이해하기 (0) | 2026.05.08 |
|---|---|
| WebClient - java.util.concurrent.TimeoutException (0) | 2026.03.28 |
| WebClient - io.netty.channel.ConnectTimeoutException (0) | 2026.03.28 |
댓글