ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 예외 처리
    Spring Reactive Web Application/Spring WebFlux 2023. 9. 16. 09:00
    반응형

      Spring MVC 기반의 애플리케이션에서 @ExceptionHandler@ControllerAdvice 등의 애너테이션을 이용하는 예외 처리 방식은 Spring WebFlux 기반의 애플리케이션에서도 사용할 수 있는 방식입니다. 이번 포스팅에서는 @ExceptionHandler나 @ControllerAdvice를 사용하는 방법 이외에 Spring WebFlux 전용 예외 처리 기법을 정리하겠습니다.

    Note. @ExceptionHandler, @ControllerAdvice 등의 애너테이션을 사용한 예외 처리 기법은 아래 Spring 공식 문서를 참고하시면 됩니다.

    https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html

     

    onErrorResume() Operator를 이용한 예외 처리

      에러 처리를 위한 Operator는 https://hanseom.tistory.com/374 참고하시면 됩니다. onErrorResum() Operator는 에러 이벤트가 발생했을 때 에러 이벤트를 Downstream으로 전파하지 않고, 대체 Publisher를 통해 에러 이벤트에 대한 대체 값을 emit하거나 발생한 에러 이벤트를 래핑한 후에 다시 에러 이벤트를 발생시키는 역할을 합니다.

     

      다음은 에러 처리를 위해 BookHandler 클래스의 핸들러 메서드에 onErrorResume() Operator를 적용한 코드입니다. onErrorResume() Operator첫 번째 파라미터는 처리할 Exception 타입입니다. Java의 try ~ catch 문에서 catch 문에 Exception 클래스를 지정해 주는 것과 유사하다고 생각하면 됩니다. 두 번째 파라미터는 대체할 Publisher의 Sequence입니다.

    import com.itvillage.exception.BusinessLogicException;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.web.reactive.function.server.ServerRequest;
    import org.springframework.web.reactive.function.server.ServerResponse;
    import reactor.core.publisher.Mono;
    import reactor.util.function.Tuple2;
    import reactor.util.function.Tuples;
    
    import java.net.URI;
    
    @Slf4j
    @Component("BookHandlerV9")
    public class BookHandler {
        private final BookMapper mapper;
        private final BookValidator validator;
        private final BookService bookService;
    
        public BookHandler(BookMapper mapper, BookValidator validator, BookService bookService) {
            this.mapper = mapper;
            this.validator = validator;
            this.bookService = bookService;
        }
    
        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())));
        }
    
        public Mono<ServerResponse> updateBook(ServerRequest request) {
            final long bookId = Long.valueOf(request.pathVariable("book-id"));
            return request
                    .bodyToMono(BookDto.Patch.class)
                    .doOnNext(patch -> validator.validate(patch))
                    .flatMap(patch -> {
                        patch.setBookId(bookId);
                        return bookService.updateBook(mapper.bookPatchToBook(patch));
                    })
                    .flatMap(book -> ServerResponse.ok()
                                            .bodyValue(mapper.bookToResponse(book)))
                    .onErrorResume(error -> ServerResponse
                            .badRequest()
                            .bodyValue(new ErrorResponse(HttpStatus.BAD_REQUEST,
                                    error.getMessage())));
        }
    
        public Mono<ServerResponse> getBook(ServerRequest request) {
            long bookId = Long.valueOf(request.pathVariable("book-id"));
    
            return bookService.findBook(bookId)
                            .flatMap(book -> ServerResponse
                                    .ok()
                                    .bodyValue(mapper.bookToResponse(book)))
                            .onErrorResume(error -> ServerResponse
                                    .badRequest()
                                    .bodyValue(new ErrorResponse(HttpStatus.BAD_REQUEST,
                                            error.getMessage())));
        }
    
        public Mono<ServerResponse> getBooks(ServerRequest request) {
            Tuple2<Long, Long> pageAndSize = getPageAndSize(request);
            return bookService.findBooks(pageAndSize.getT1(), pageAndSize.getT2())
                    .flatMap(books -> ServerResponse
                            .ok()
                            .bodyValue(mapper.booksToResponse(books)));
        }
    
        private Tuple2<Long, Long> getPageAndSize(ServerRequest request) {
            long page = request.queryParam("page").map(Long::parseLong).orElse(0L);
            long size = request.queryParam("size").map(Long::parseLong).orElse(0L);
            return Tuples.of(page, size);
        }
    }

     

    ErrorWebExceptionHandler를 이용한 글로벌 예외 처리

      onErrorResume() 등의 Operator를 이용한 인라인(inline) 예외 처리는 사용하기 간편하지만, 클래스 내 여러 개의 Sequence가 존재한다면 각 Sequence마다 onErrorResume() Operator를 일일이 추가해야 되고 중복 코드가 발생할 수 있다는 단점이 존재합니다. 이러한 단점을 보완하기 위해 Global Exception Handler를 추가로 작성할 수 있습니다.

     

      다음은 Exception을 글로벌 수준에서 처리하기 위한 Global Exception Handler 코드입니다.

    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.itvillage.book.v10.ErrorResponse;
    import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.core.io.buffer.DataBufferFactory;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.web.server.ResponseStatusException;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    @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));
        }
    }
    • Spring Boot의 ErrorWebFluxAutoConfiguration을 통해 등록된 DefaultErrorWebExceptionHandler Bean의 우선 순위보다 높은 우선 순위인 -2를 지정합니다.
    • BufferFactoryDataBuffer를 위한 팩토리로서 response body를 write하는 데 사용합니다.

     

    Note. DataBuffer는 Netty의 ByteBuf와 유사하지만 Netty가 아닌 플랫폼(서블릿 컨테이너 등)에서도 사용할 수 있도록 추상화한 바이트 컨테이너입니다.

    https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/buffer/DataBuffer.html

     

     

     

    [참고 정보]

    반응형

    'Spring Reactive Web Application > Spring WebFlux' 카테고리의 다른 글

    Reactive Streaming  (0) 2023.09.21
    WebClient  (0) 2023.09.16
    R2dbcEntityTemplate  (0) 2023.09.10
    Spring Data R2DBC  (0) 2023.09.09
    함수형 엔드포인트(Functional Endpoint)  (0) 2023.08.26

    댓글

Designed by Tistory.