서론
Spring MVC 기반의 애플리케이션에서는 @ExceptionHandler 혹은 @ControllerAdvice 등의 애너테이션을 이용하여 예외 처리를 진행했습니다.
Spring WebFlux 기반의 애플리케이션에서도 두 애너테이션을 사용 가능하지만 이번 게시글에서는 Spring WebFlux 전용 예외 처리 기법에 대해 간단하게 정리해 보겠습니다.
Reactor에서 제공하는 에러 처리 Operator를 이용한 예외 처리
- 자주 사용하는 Operator로 error, onErrorReturn, onErrorResume, onErrorContinue, retry가 있으며 해당 Operator들을 통해 inline 예외 처리 수행
- 사용하기 쉽고 간편하지만
- 클래스 내 여러 개의 Sequence가 존재할 때 각 Sequence마다 Operator를 일일이 추가해야 되고 중복 코드가 발생할 수 있음
위에서 언급한 다섯 가지 Operator 중 onErrorResume() Operator를 기준으로 예시를 들면 다음과 같습니다.
- onErrorResume() Operator는 에러 이벤트 발생 시 에러 이벤트를 Downstream으로 전파하지 않고 대체 Publisher를 통해 여러 이벤트에 대한 대체 값을 emit 하거나 발생한 에러 이벤트를 wrapping 한 후에 다시 에러 이벤트를 발생시키는 역할
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public Mono<ServerResponse> createBook(ServerRequest request) { | |
return request.bodyToMono(BookDto.Post.class) | |
.doOnNext(post -> validator.validate(post)) | |
.flatMap(post -> bookService.createBook(mapper.bookPostToBook(post))) | |
.flatMap(book -> ServerResponse | |
.created(URI.create("/v9/books/" + book.getBookId())) | |
.build()) | |
.onErrorResume(BusinessLogicException.class, error -> ServerResponse | |
.badRequest() | |
.bodyValue(new ErrorResponse(HttpStatus.BAD_REQUEST, | |
error.getMessage()))) | |
.onErrorResume(Exception.class, error -> | |
ServerResponse | |
.unprocessableEntity() | |
.bodyValue( | |
new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, | |
error.getMessage()))); | |
} |
부연 설명
- onErrorResume() Operator의 첫 번째 파라미터는 처리할 Exception 타입
- 자바의 try-catch 문에서 catch 문에 Exception 클래스를 지정하는 것과 유사
- onErrorResume()의 두 번째 파라미터는 대체할 Publisher의 Sequence
- 첫 번째 onErrorResume() Operator는 BusinessLogicException 타입의 예외 발생 시 ErrorResponse 클래스에 HttpStatus 정보와 에러 메시지를 추가해서 응답으로 전송
- 두 번째 onErrorResume() Operator는 BusinessLogicException 타입 이외의 Exception이 발생할 경우 역시 ErrorResponse 클래스에 HttpStatus 정보와 에러 메시지를 추가해서 응답으로 전송
ErrorWebExceptionHandler를 이용한 글로벌 예외 처리
- Reactor에서 제공하는 에러 처리 Operator들의 단점인 중복 코드를 보완하기 위해 Global Exception Handler를 별도로 작성할 수 있음
- Spring MVC의 @ControllerAdvice와 유사
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Order(-2) | |
@Configuration | |
public class GlobalWebExceptionHandler implements ErrorWebExceptionHandler { | |
private final ObjectMapper objectMapper; | |
public GlobalWebExceptionHandler(ObjectMapper objectMapper) { | |
this.objectMapper = objectMapper; | |
} | |
@Override | |
public Mono<Void> handle(ServerWebExchange serverWebExchange, | |
Throwable throwable) { | |
return handleException(serverWebExchange, throwable); | |
} | |
private Mono<Void> handleException(ServerWebExchange serverWebExchange, | |
Throwable throwable) { | |
ErrorResponse errorResponse = null; | |
DataBuffer dataBuffer = null; | |
DataBufferFactory bufferFactory = | |
serverWebExchange.getResponse().bufferFactory(); | |
serverWebExchange.getResponse().getHeaders() | |
.setContentType(MediaType.APPLICATION_JSON); | |
if (throwable instanceof BusinessLogicException) { | |
BusinessLogicException ex = (BusinessLogicException) throwable; | |
ExceptionCode exceptionCode = ex.getExceptionCode(); | |
errorResponse = ErrorResponse.of(exceptionCode.getStatus(), | |
exceptionCode.getMessage()); | |
serverWebExchange.getResponse() | |
.setStatusCode(HttpStatus.valueOf(exceptionCode.getStatus())); | |
} else if (throwable instanceof ResponseStatusException) { | |
ResponseStatusException ex = (ResponseStatusException) throwable; | |
errorResponse = ErrorResponse.of(ex.getStatus().value(), ex.getMessage()); | |
serverWebExchange.getResponse().setStatusCode(ex.getStatus()); | |
} else { | |
errorResponse = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR.value(), | |
throwable.getMessage()); | |
serverWebExchange.getResponse() | |
.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); | |
} | |
try { | |
dataBuffer = | |
bufferFactory.wrap(objectMapper.writeValueAsBytes(errorResponse)); | |
} catch (JsonProcessingException e) { | |
bufferFactory.wrap("".getBytes()); | |
} | |
return serverWebExchange.getResponse().writeWith(Mono.just(dataBuffer)); | |
} | |
} |
부연 설명
- Reactor에서 제공하는 예외 처리 Operator로 처리되지 않는 Exception을 글로벌 수준에서 처리하기 위한 Global Exception Handler
- ErrorWebExceptionHandler 구현체
- ErrorWebExceptionHandler 인터페이스의 추상 메서드인 handle 추상 메서드를 구현하고
- handle 메서드의 첫 번째 파라미터를 통해 ServerWebExchange 객체로 반환하도록 설정 가능
- handle 메서드의 두 번째 파라미터로 발생한 예외 처리 가능
- SpringBoot의 ErrorWebFluxAutoConfiguration을 통해 등록된 DefaultErrorWebExceptionHandler Bean의 우선순위보다 높은 우선순위인 -2 지정
- bufferFactory() 메서드를 통해 BufferFactory 인터페이스의 구현 객체를 생성
- BufferFactory는 DataBuffer를 위한 팩토리로서 response body를 작성하는 데 사용
- response body의 content-type이 JSON 포맷임을 response header에 추가
- 비즈니스 로직 처리 중 발생하는 예외와 Valdiation 처리 중 발생하는 Exception을 구분해서 ErrorResponse 구성
- ErrorResponse 객체를 DataBuffer로 wrapping 하여 response body 구성 가능
- writeWith() 메서드를 통해 파라미터로 지정한 Mono로 response body를 작성
참고
- 스프링으로 시작하는 리액티브 프로그래밍 (황정식 저자)
반응형
'Spring > 스프링으로 시작하는 리액티브 프로그래밍' 카테고리의 다른 글
[Spring WebFlux] Reactive Streaming 데이터 처리 (0) | 2024.10.08 |
---|---|
[Spring WebFlux] WebClient (0) | 2024.10.07 |
Spring Data R2DBC 간단 정리 (0) | 2024.09.11 |
[Spring WebFlux] 함수형 엔드포인트(Functional Endpoint) (0) | 2024.09.06 |
[Spring WebFlux] 애너테이션 기반 컨트롤러 (0) | 2024.08.20 |