[한줄스프링] ResponseEntityException에서 제공하는 ExceptionResolver를 커스텀하기 위한 방법

hoyong.eom·2023년 11월 26일
0

스프링

목록 보기
63/64
post-thumbnail

오늘의 한줄 궁금증

스프링에서는 전역 예외 처리기를 위해서 ControllerAdvice를 제공한다. 이뿐만 아니라 sprig MVC에서 제공하는 예외를 처리하기 위해서 ResponseEntityException이라는 클래스를 상속받아 처리할 수 있도록 지원한다.

public abstract class ResponseEntityExceptionHandler implements MessageSourceAware {

	/**
	 * Log category to use when no mapped handler is found for a request.
	 * @see #pageNotFoundLogger
	 */
	public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";

	/**
	 * Specific logger to use when no mapped handler is found for a request.
	 * @see #PAGE_NOT_FOUND_LOG_CATEGORY
	 */
	protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);

	/**
	 * Common logger for use in subclasses.
	 */
	protected final Log logger = LogFactory.getLog(getClass());


	@Nullable
	private MessageSource messageSource;


	@Override
	public void setMessageSource(MessageSource messageSource) {
		this.messageSource = messageSource;
	}

	/**
	 * Get the {@link MessageSource} that this exception handler uses.
	 * @since 6.0.3
	 */
	@Nullable
	protected MessageSource getMessageSource() {
		return this.messageSource;
	}


	/**
	 * Handle all exceptions raised within Spring MVC handling of the request.
	 * @param ex the exception to handle
	 * @param request the current request
	 */
	@ExceptionHandler({
			HttpRequestMethodNotSupportedException.class,
			HttpMediaTypeNotSupportedException.class,
			HttpMediaTypeNotAcceptableException.class,
			MissingPathVariableException.class,
			MissingServletRequestParameterException.class,
			MissingServletRequestPartException.class,
			ServletRequestBindingException.class,
			MethodArgumentNotValidException.class,
			NoHandlerFoundException.class,
			AsyncRequestTimeoutException.class,
			ErrorResponseException.class,
			ConversionNotSupportedException.class,
			TypeMismatchException.class,
			HttpMessageNotReadableException.class,
			HttpMessageNotWritableException.class,
			BindException.class
		})
	@Nullable
	public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception {

따라서 RestiControllerAdvice 또는 ControllerAdvice에서는 SpringMVC 예외를 전역적으로 처리하기 위해서 ResponseEntityException 추상 클래스를 상속받아야함이 필수적이라고 할 수 있다. (그렇지 않으면 스프링 예외와 사용자 정의 예외가 서로 다른 모양새가 된다. 왜냐하면 서로 다른곳에서 예외를 처리하기 되기 떄문이다.)

아래의 로그와 Response는 DefaultExceptionResolver가 동작했을떄의 동작이다.

DefaultExceptionResolver

2023-11-26T15:16:21.542+09:00 WARN 21348 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.board.api.post.PostApiController$CreatePostResponseDto com.example.board.api.post.PostApiController.savePost(com.example.board.dto.post.CreatePostRequestDto): [Field error in object 'createPostRequestDto' on field 'title': rejected value []; codes [NotBlank.createPostRequestDto.title,NotBlank.title,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [createPostRequestDto.title,title]; arguments []; default message [title]]; default message [공백일 수 없습니다]] ]

{
"timestamp": "2023-11-26T06:16:21.558+00:00",
"status": 400,
"error": "Bad Request",
"trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.board.api.post.PostApiController$CreatePostResponseDto com.
...
...

사용자가 ExceptionResolver를 등록한 경우

사용자가 직접 ExceptionResolver를 등록했으며, ResponseEntityException을 상속했다.

2023-11-26T15:26:07.146+09:00 WARN 18280 --- [nio-8080-exec-2] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.board.api.post.PostApiController$CreatePostResponseDto com.example.board.api.post.PostApiController.savePost(com.example.board.dto.post.CreatePostRequestDto): [Field error in object 'createPostRequestDto' on field 'title': rejected value []; codes [NotBlank.createPostRequestDto.title,NotBlank.title,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [createPostRequestDto.title,title]; arguments []; default message [title]]; default message [공백일 수 없습니다]] ]

{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Invalid request content.",
"instance": "/api/post"
}

훨씬 더 간결해진 내용을 확인할 수 있다.

아무튼 서론이 길었는데, 여기에서 궁금한점은 만약 springMVC에서 정의한 예외의 Response를 커스텀하기 위해서는 어떻게 해야할까?

내 생각엔 단순히 ResponseEntityException에 정의된 ExceptionHandler를 다시 정의해주면 될거라고 생각했다. 즉, ExceptionHandler를 새롭게 추가한다!
그런데 이 경우 에러를 내뱉는다. 어떤 에러일까?

Ambiguous @ExceptionHandler method mapped for [class org.springframework.web.bind.MethodArgumentNotValidException]: {public org.springframework.http.ResponseEntity com.example.board.exhandler.advice.GlobalExceptionControllerAdvice.handleMehtodArgumentNotValidException(org.springframework.web.bind.MethodArgumentNotValidException), public final org.springframework.http.ResponseEntity org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler.handleException(java.lang.Exception,org.springframework.web.context.request.WebRequest) throws java.lang.Exception}
at org.springframework.web.method.annotation.ExceptionHandlerMethodResolver.addExceptionMapping(ExceptionHandlerMethodResolver.java:114) ~[spring-web-6.0.13.jar:6.0.13]
at org.springframework.web.method.annotation.ExceptionHandlerMethodResolver.(ExceptionHandlerMethodResolver.java:78) ~[spring-web-6.0.13.jar:6.0.13]
at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.initExceptionHandlerAdviceCache(ExceptionHandlerExceptionResolver.java:276) ~[spring-webmvc-6.0.13.jar:6.0.13]
at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.afterPropertiesSet(ExceptionHandlerExceptionResolver.java:243) ~[spring-webmvc-6.0.13.jar:6.0.13]
at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.addDefaultHandlerExceptionResolvers(WebMvcConfigurationSupport.java:1063) ~[spring-webmvc-6.0.13.jar:6.0.13]
at org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport.handlerExceptionResolver(WebMvcConfigurationSupport.java:1005) ~[spring-webmvc-6.0.13.jar:6.0.13]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]

에러 내용은 이미 동일한 ExceptionHandler가 등록되어 있다는 의미다.
내가 작성한 코드는 아래와 같았다.

      @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Object> handleMehtodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error("[handleMehtodArgumentNotValidException]", e);

        ErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
        return handleExceptionInternal(errorCode, e);
    }

ResponseEntityException에 ExceptionHandler가 정의되어있으니 중복된다는 에러다.
그렇다면 커스텀을 위해서는 아래의 함수를 재정의해야했다.

	@Nullable
	protected ResponseEntity<Object> handleMethodArgumentNotValid(
			MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {

		return handleExceptionInternal(ex, null, headers, status, request);
	}

근데 여기서 궁금한점이 또 있다.
그렇다면 springMVC예외와 사용자 정의 예외의 Response Format을 통일시킬수는 없는걸까? 그렇게 하지 않아도 될까? 였다. 그래서 찾아보았다.

찾은 결과는...

내 맘대로 포맷을 지정하기 위해서는 재정의 뿐이었다...

  
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        CommonErrorCode errorCode = CommonErrorCode.INVALID_PARAMETER;
        ErrorResponse errorResponse = new ErrorResponse(errorCode.getHttpStatus().value(), errorCode.getHttpStatus(), errorCode.getMessage(), null);
        return handleExceptionInternal(ex, errorResponse, headers, status, request);
    }

결과가 아쉽다.

0개의 댓글