[수미수의 개발 브로구]

[WebFlux] WebFlux Exception 처리 본문

Language & Framework/WebFlux

[WebFlux] WebFlux Exception 처리

수미수 2023. 9. 21. 19:22
반응형

들어가기 전

  Spring  WebFlux 로 개발을 진행 하면서, 비즈니스 로직에 대한 예외처리 및 회사 내규에 따라 서버의 오류를 그데로 클라이언트에게 응답 하는 것이 아닌 한번 서버에서 가공처리 후 응답해야 했다. WebFlux 에서도 Spring MVC 와 같이 각 기능별 그리고 전역으로 오류 및 예외 처리를 핸들링 할 수 있는 클래스를 제공 하고 있다. 해당 글에서는 WebFlux 프로젝트에서 오류 및 커스텀 예외를 전역으로 어떻게 핸들링 했는지에 대해서 설명 하고자 한다.

글로벌 오류 및 예외 핸들링

  기존 Spring MVC 로 개발 할 경우 ContrllerAdvice 어노테이션을 통해서, 모든 에러 및 예외처리를 전역으로 핸들링 할 수 있다. WebFlux 에서도, 전역으로 핸들링 할 수 있는데 이를 위해서, 두 개의 Spring 콤포넌트를 클래스를 생성 해야 한다.

GlobalErrorAttributes 클래스 생성

  기본적으로 예외 발생시 자동으로 Http 상태와 Json 오류 본문으로 변환되는 이를 커스터마이징 하기 위해서 DefaultErrorAttributes 를 확장한 클래스와 getErrorAttributes 메서드를 재정의 해야 한다. 아래는 해당 클래스의 예제 코드 이다.

@Component
class GlobalErrorAttributes : DefaultErrorAttributes() {
    override fun getErrorAttributes(request: ServerRequest?, options: ErrorAttributeOptions?): Map<String, Any> {
        val map: MutableMap<String, Any> = super.getErrorAttributes(request, options)
        map["status"] = HttpStatus.BAD_REQUEST
        map["message"] = "please provide a name"
        return map
    }
}

  GloabalErrorExceptionHandler 클래스 생성

  WebFlux 에서는 전역 오류 및 예외처리를 확장할 수 잇는 AbstractErrorWebExceptionHandler 를 제공 하는데 이를 확장한 클래스를 생성한다. 생성한 클래스 코드는 아래 소스와 같다.

  모든 오류가 발생했을 getRoutingFunction 메서드를 타게 되고, renderErrorResponse 메서드가 실행 된다. 실제로 오류 및 예외에 맞게 renderErrorResponse 를 확장 하면 된다.

@Component
//@Order(-2)
class GlobalErrorWebExceptionHandler(
    val gson: Gson,
    g: GlobalErrorAttributes?, applicationContext: ApplicationContext?,
    serverCodecConfigurer: ServerCodecConfigurer
) : AbstractErrorWebExceptionHandler(g, WebProperties.Resources(), applicationContext) {

    val log = logger<GlobalErrorWebExceptionHandler>()

    override fun getRoutingFunction(errorAttributes: ErrorAttributes?): RouterFunction<ServerResponse> {
        return router { (RequestPredicates.all()) { renderErrorResponse(it) } }
    }

    private fun renderErrorResponse(request: ServerRequest): Mono<ServerResponse> {
        val throwable = super.getError(request)
        return when (throwable) {
            is WebClientRequestException -> {
                log.info(throwable.message)
                ServerResponse.status(HttpStatus.OK)
                    .bodyValue(
                        createRestResponse(
                            ApiResponseCode.ERROR.code,
                            ApiResponseCode.ERROR.message,
                            "0",
                            ExceptionMessage.WEB_CLIENT_READ_TIMEOUT_EXCEPTION.code,
                            ExceptionMessage.WEB_CLIENT_READ_TIMEOUT_EXCEPTION.message,
                            emptyMap<Any, Any>()
                        )
                    )
            }
            is ServerWebInputException -> {
                ServerResponse.status(HttpStatus.OK)
                    .bodyValue(
                        createRestResponse(
                            ApiResponseCode.ERROR.code,
                            ApiResponseCode.ERROR.message,
                            "0",
                            ExceptionMessage.SERVER_ERROR.code,
                            ExceptionMessage.SERVER_ERROR.message,
                            emptyMap<Any, Any>()
                        )
                    )
            }
            is RuntimeException -> {
                ServerResponse.status(HttpStatus.OK)
                    .bodyValue(
                        createRestResponse(
                            ApiResponseCode.ERROR.code,
                            ApiResponseCode.ERROR.message,
                            "0",
                            ExceptionMessage.SERVER_ERROR.code,
                            ExceptionMessage.SERVER_ERROR.message,
                            emptyMap<Any, Any>()
                        )
                    )
            }
            is ResponseStatusException -> {
                ServerResponse.status(HttpStatus.OK)
                    .bodyValue(
                        createRestResponse(
                            ApiResponseCode.ERROR.code,
                            ApiResponseCode.ERROR.message,
                            "0",
                            ExceptionMessage.SERVER_ERROR_NOT_FOUND.code,
                            ExceptionMessage.SERVER_ERROR_NOT_FOUND.message,
                            emptyMap<Any, Any>()
                        )
                    )
            }
            else -> {
                ServerResponse.status(HttpStatus.OK)
                    .bodyValue(
                        createRestResponse(
                            ApiResponseCode.ERROR.code,
                            ApiResponseCode.ERROR.message,
                            "0",
                            ExceptionMessage.SERVER_ERROR.message,
                            ExceptionMessage.SERVER_ERROR.code,
                            ExceptionMessage.SERVER_ERROR.message
                        )
                    )
            }
        }
            .flatMap {
                val restResponseEntity = (it as EntityResponse<ServerResponse>).entity() as RestResponse<Any>
                val responseCode = restResponseEntity.returnCode
                val errorCode = restResponseEntity.errorCode
                val errorMessage = restResponseEntity.errorMsg
                Mono.just(it)
            }

    }

    init {
        super.setMessageWriters(serverCodecConfigurer.writers)
        super.setMessageReaders(serverCodecConfigurer.readers)
    }

    private fun createRestResponse(
        returnCode: String,
        returnMessage: String,
        elapsedTime: String,
        errorCode: String,
        errorMessage: String,
        body: Any
    ): Any {

        val restResponse = RestResponse(returnCode, returnMessage, elapsedTime, errorCode, errorMessage, body)

        log.info("[RestResult Exception] : ${gson.toJson(restResponse)}")
        return restResponse

    }
}

  위 두개의 클래스를 생성하고 빈으로 등록하게 되면, WebFlux에서 오류 및 예외가 발생 했을 때 자동으로 신규 생성한 전역 핸들링 클래스의 소스를 타게 된다. 위 소스는 renderErrorResponse 메서드에서 오류 또는 예외 발생된 클래스가 WebClientRequestException 부터 순차적으로 체크 하여, RestResponse 라는 Custom 응답 DTO 로 응답 하는 구조이다.

  해당 DTO 는 응답 코드, 응답 메시지, 시간, 에러 코드, 에러 메시지 및 데이터 정보를 담고 있는데, 각 오류 및 예외 상황에 맞게 미리 정의 한 것으로 설정 한다. 해당 소스의 경우 모든 오류 및 예외의 Http 상태 코드는 Http.OK 로 설정하여 내려주고 있는 예제 소스이며, Http 상태 값은 주어진 환경에 맞게 설정 하면 된다.

커스텀 에러 응답 DTO

  아래 RestResponse data 클래스는 현재 사용중인 응답 구조의 표준이며, 해당 표준에 맞게 오류 값을 셋팅하여 응답 하도록 한다. 

data class RestResponse<T> (

		val returnCode:String,
        val returnMsg:String,
        val elapsedTime: String,
        val errorCode: String,
        val errorMsg: String,
        val returnData: T
)

결론

  해당 글에서는 기본적으로 발생되는 서버의 오류 및 예외가 발생 했을 때 전역으로 처리 하는 방법에 대해서 예제 코드로 설명 하였다. 다음 장에서는 실제 비지니스 로직에 따라 커스텀 예외를 생성 해야 하는데, 이러한 경우 어떤 식으로 처리 했는지에 대해서 설명 하고자 한다.

참고 사이트

반응형