사용자가 subscribe 실행을 프레임워크에 위임했을 경우, 에외처리도 프레임워크가 담당해준다.
Mono에서 에러 발생
↓
WebFlux 내부 onError 처리
↓
DefaultErrorWebExceptionHandler (기본 에러 핸들러)
↓
HTTP 응답으로 변환
↓
클라이언트에 에러 응답 반환
이때 동기식 서버랑 다르게 이해해야 하는건 리액티브 파이프라인에서 예외는, throw되는게 아니라 스트림을 타고 흐르는 하나의 onError 시그널로 취급해서 이해하는게 좋음.
이때 이 시그널을 처리하는 도구가 onErrorResume , onErrorReturn , onErrorMap , doOnError , switchIfEmpty , timeout , retry 같은 것들임. 처음 mono 파이프라인 구성시 예외 처리하도록 구성하는 방식.
fun getUser(req: ServerRequest): Mono<ServerResponse> =
userService.findById(req.pathVariable("id").toLong())
.switchIfEmpty(Mono.error(UserNotFoundException())) // empty를 에러 신호로
.flatMap { ServerResponse.ok().bodyValue(it) }
.onErrorResume(UserNotFoundException::class.java) { // 여기서 흡수
ServerResponse.notFound().build()
}
기본핸들러(DefaultErrorWebExceptionHandler)의 반환값은 다음과 같음.
// DefaultErrorWebExceptionHandler 기본 응답
{
"timestamp": "2026-05-18T00:00:00",
"path": "/users/999",
"status": 500,
"error": "Internal Server Error",
"message": ""
}
물론 개발자가 중간에 개입할 수도 있다.
@Controller, @RestController등을 이용해 구현했다면 @ControllerAdvice등이 유효하다. MVC와 거의 동일하게 애너테이션으로도 처리할 수 있음. Mono나 Flux가 onError로 끝나면 프레임워크가 알아서 글로벌 예외 핸들러에서 특정 예외를 처리해준다. - @ExceptionHandler 를 이용하는건 동일함.
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException::class)
fun handleUserNotFound(e: UserNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(ErrorResponse(e.message))
}
}
// WebFlux도 동일하게 동작
이때는 핸들러 하나하나에서 onErrorResume(...) 로 잡거나, 라우터 빌더에서 filter 를 이용해 해당 라우터의 예외처리 필터를 둔다.
핸들러에서 하나하나 처리하기
fun getUser(id: Long): Mono<User> {
return userService.find(id)
.onErrorResume(UserNotFoundException::class.java) {
Mono.error(CustomException("유저 없음"))
}
.onErrorReturn(defaultUser)
}