예외처리의 방법

기본 예외처리

사용자가 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)
}