티스토리 뷰

Backend

Handling-request-binding-exception in webflux

지마켓 양승권 2023. 2. 15. 12:08

이번 글에서는 Spring Webflux + Kotlin 기술 스택에서 Request Binding Exeption에 대한 처리를 예제코드를 통해서 구현해 보도록 하겠습니다.


Controller 처리

Controller에서는 @Valid annotation을 사용해서 request Object 대한 validation을 체크합니다.

    @ApiOperation("xxx")
    @GetMapping("/main")
    suspend fun getMainDeals(
        @Valid retrieveMainDealRequest: RetrieveMainDealRequest
    ) =
        superDealGoodsService.readSuperDealMain(retrieveMainDealRequest)

validation 조건

validation 조건은 @Min 조건을 사용할 예정인데, 해당값이 만족하지 않으면 message를 return 하도록 message를 작성합니다.

    @ApiModel(description = "슈퍼딜 메인딜 요청모델")
    data class RetrieveMainDealRequest(

      ...
      @ApiModelProperty("from range", example = "1")
      @Min(value = 1, message = "fromRange has to be larger than 0")
      var fromRange: Int = 0,

      @ApiModelProperty("to range", example = "10")
      @Min(value = 1, message = "toRange has to be larger than 0")
      var toRange: Int = 0,
      ...

    )

CommonResponse의 처리

CommonResponse는 아래처럼 구현해보겠습니다.

    data class CommonResponse(

            var code: Int = 0,
            var message: String = "",
            var details: String = ""

    )

Global Exception Error 처리

자 이제 binding 처리를 위한 준비는 끝났습니다. 이제 중요한 작업이 ExceptionHandler를 구현해 보겠습니다.

 

ControllerAdvice

과연 ControllerAdvice에서 Error를 어떻게 잡을 것인가?

Binding exeption이 발생시 spring webflux에서는 WebExchangeBindException을 발생
spring mvc 에서는 MethodArgumentNotValidException을 발생

Spring Weblflux에서는 ControllerAdvice에서 WebExchangeBindException를 등록해 주면 됩니다.

 

ServerResponse or ResponseEntity

그리고 reactive package의 ServerResponse로 구현할 것이냐 spring-mvc package의 ResponseEntity로 구현을 할 것 이냐를 고민할 수도 있습니다. (저는 고민했습니다.)

 

둘의 차이는 ServerResponse는 Filter 단에서 AbstractErrorWebExceptionHandler을 상속받은 구현체가 동작하는 것이고, ResponseEntity는 Contoller Datatype의 하나로 ControllerAdvice에서 구현시 지원하는 data type으로 DispatcherServlet 내부에서 동작합니다.

 

Controller Advice는 Controller에 구현된 데이터 타입은 아래와 같습니다.

Controller Data Type

 

Web on Reactive Stack

The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports

docs.spring.io

 

WebExchangeBindException : binding Exception in Webflux

위에서 말했듯이 Spring Webflux에서는 @Valid 에 대한 binding Exception이 발생하시에 WebExchangeBindException을 발생합니다. 그래서 WebExchangeBindException를 Catch 해줘야 합니다.

 

이 둘을 합하면 아래와 같습니다.

    @ExceptionHandler(WebExchangeBindException::class)
    fun handlerWebClientException(ex: WebExchangeBindException): Mono<ResponseEntity<CommonResponse>> {
        log.error(ex.message, ex)
        moaLogger.error(ex.message, ex)

        return Mono.just(ResponseEntity.badRequest().body(CommonResponse(400, ex.message)))
    }

결과는 아래와 같습니다.

Error: Internal Server Error
Response body
Download
{
  "code": 500,
  "message": "Exeption!",
  "details": "n >= 0 required but it was -1"
}

하지만 Webflux 를 Functional Endpoint 로 구현할 경우 ServerResponse 구현은 아래와 같이 구현할 수 있습니다.
Functional Endpoint

 

Web on Reactive Stack

The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports

docs.spring.io

 

ServerResponse 로 구현하기

class GlobalErrorWebExceptionHandler(
        errorAttributes: ErrorAttributes,
        resources: ResourceProperties,
        applicationContext: ApplicationContext,
        serverCodecConfigurer: ServerCodecConfigurer
) : AbstractErrorWebExceptionHandler(
        errorAttributes, resources, applicationContext
) {
    init {
        super.setMessageWriters(serverCodecConfigurer.writers)
        super.setMessageReaders(serverCodecConfigurer.readers)
    }

    override fun getRoutingFunction(
            errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse?>? {
        return RouterFunctions.route(RequestPredicates.all(), getBindingErrorResult)
    }

    val getBindingErrorResult = HandlerFunction<ServerResponse> { request ->
        when (val throwable = super.getError(request)) {

            is WebExchangeBindException -> {

                log.error(throwable.message, throwable)
                moaLogger.error(throwable.message, throwable)


                ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .bodyValue(
                                CommonResponse(101, "Binding Exeption!", throwable.bindingResult.allErrors.map { m -> m.defaultMessage }
                                        .toList().toString())
                        )
            }
            else -> {

                log.error(throwable.message, throwable)
                moaLogger.error(throwable.message, throwable)

                ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
                        .bodyValue(
                                CommonResponse(500, "Exeption!", throwable.message ?: "")
                        )
            }
        }
    }

    companion object {
        private val log = LoggerFactory.getLogger(this::class.java)
        private val moaLogger = LoggerFactory.getLogger("MoALogger")
    }
}

이상 Webflux에서의 bindingException 처리를 구현해 보았습니다.

 

저는 GlobalErrorWebExceptionHandler를 구현해서 적용해 봤지만, Application에서 활용하는 패턴등에 따라 선택하시면 될 것 같습니다.

감사합니다.

댓글