스프링 예외 처리 방법(2)

이상민·2023년 10월 1일
2

스프링 예외처리

목록 보기
2/2

스프링 예외 처리 방법(1)에서 개발자가 일일이 HandlerExceptionResolver를 구현하는 것은 매우 번거로운 일이라고 말했다. 스프링에서 직접 구현한 구현체에 대해서 알아보자!

ExceptionResolver 구현체 종류

스프링에서 빈으로 등록한 ExceptionHandler는 3가지 존재한다. 우선 순위가 높은 순서대로

  1. ExceptionHandlerExceptionResolver
  2. ResponseStatusExceptionResolver
  3. DefaultHandlerExceptionResolver

가 존재하고 우선 순위가 높은 ExceptionResolver가 먼저 실행되고 예외 처리를 할 수 없으면 다음 ExceptionResolver로 넘어간다.

ResponseStatusExceptionResolver

ResponseStatusExceptionResolver는 다음과 같은 2가지 경우에 예외를 처리한다.

  1. @ResponseStatus가 존재하는 예외 클래스
  2. ResponseStatusException

@ResponseStatus

해당 어노테이션은 HttpStatus를 변경하는 어노테이션이다. 예시는 아래와 같다.

@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "custom exception")
public class CustomException extends RuntimeException{
}

Controller에서 CustomException을 발생시키면 ResponseStatusExceptionResolver는 해당 예외를 처리하고 DispatcherServlet으로 ModelAndView를 반환한다.

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test")
    public void test(HttpServletResponse response) throws IOException {
        throw new CustomException();
    }
}

Postman으로 해당 api를 호출하고 얻은 응답 값을 확인해 보자

형태가 BasicErrorController에서 응답해준 형식이랑 동일하다. 실재로 Was가 BasicErrorController를 호출하고 에러 메세지를 반환해준 것인데 이유는 ResponseStatusExceptionResolver 내부 구현 코드를 살펴보면 확인이 가능하다.

ResponseStatusExceptionResolver는 에러를 처리하기 위해 내부적으로 applyStatusAndReason() 함수를 호출한다. 해당 함수에는 response.sendError(statusCode, resolvedReason);이 존재한다. 이로 인해 예외는 Was에게 전달되고 Was는 다시 Controller를 호출하여 예외 응답 메세지를 반환하는 것이다.

디버깅해보면 실재 BasicErrorController의 error()함수가 실행되는 것을 확인할 수 있다.

ResponseStatusException

@ResponseStatus는 이미 라이브러리에 등록된 예외에는 적용할 수 없고 세밀한 설정은 불가능하다는 단점이 존재한다. 이에 대한 대안책으로 Spring 5.0 이후 등장한 ResponseStatusException이 존재한다. ResponseStatusException은 HttpsStatus, reason, cause를 지정할 수 있다. 예외를 발생시키는 예시 코드는 아래와 같다.

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test")
    public void test(HttpServletResponse response) throws IOException {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"custom exception");
    }
}

DefaultHandlerExceptionResolver

DefaultHandlerExceptionResolver는 스프링 내부에서 발생한 예외를 처리해준다. 처리해주는 예외는 아래와 같다.

HttpRequestMethodNotSupportedException 405 (SC_METHOD_NOT_ALLOWED)
HttpMediaTypeNotSupportedException 415 (SC_UNSUPPORTED_MEDIA_TYPE)
HttpMediaTypeNotAcceptableException 406 (SC_NOT_ACCEPTABLE)
MissingPathVariableException 500 (SC_INTERNAL_SERVER_ERROR)
MissingServletRequestParameterException 400 (SC_BAD_REQUEST)
MissingServletRequestPartException 400 (SC_BAD_REQUEST)
ServletRequestBindingException 400 (SC_BAD_REQUEST)
ConversionNotSupportedException 500 (SC_INTERNAL_SERVER_ERROR)
TypeMismatchException 400 (SC_BAD_REQUEST)
HttpMessageNotReadableException 400 (SC_BAD_REQUEST)
HttpMessageNotWritableException 500 (SC_INTERNAL_SERVER_ERROR)
MethodArgumentNotValidException 400 (SC_BAD_REQUEST)
BindException 400 (SC_BAD_REQUEST)
NoHandlerFoundException 404 (SC_NOT_FOUND)
AsyncRequestTimeoutException 503 (SC_SERVICE_UNAVAILABLE)

예시 코드를 확인해보자. Controller에서 @PathVariable 인자 타입을 Integer 형으로 받고 있다.

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test/{id}")
    public void test(@PathVariable("id") Integer id) throws IOException {
        throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"custom exception");
    }
}

이때, 아래와 같이 정수 타입이 아닌 문자열을 요청으로 보내주면

아래와 같이 MethodArgumentTypeMismatchException 에러가 발생하는 것을 확인할 수 있다.

MethodArgumentTypeMismatchException는 TypeMismatchException의 자식 클래스임으로 DefaultHandlerExceptionResolver는 아래의 코드를 실행함으로서 예외를 처리한다.

else if (ex instanceof TypeMismatchException theEx) {
				return handleTypeMismatch(theEx, request, response, handler);
			}

이때, handleTypeMismatch() 함수 코드를 살펴보면,

protected ModelAndView handleTypeMismatch(TypeMismatchException ex,
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {

		response.sendError(HttpServletResponse.SC_BAD_REQUEST);
		return new ModelAndView();
	}

response.sendError()가 존재함으로 Was는 /error 경로에 있는 BasicErrorController를 호출한다.

ExceptionHandlerExceptionResolver

배경

앞서 살펴본 예외 처리 과정들은 Http 상태 코드를 변경하거나 스프링 내부 예외를 처리하였다. 하지만 API 환경에서는 Json 형태로 응답을 직접 response에 넣어줘야 한다. 또한 API마다, 다른 Controller에서 발생한 동일한 예외도 다르게 처리하고 응답해 주는 데이터도 다르는 등 세밀한 예외 처리가 필요하다.
이러한 환경에서 BasicErrorController를 이용하거나 HandlerExceptionResolver를 직접 구현하는 것은 적절하지 않다.

@ExceptionHandler

ExceptionHandlerExceptionResolver는 앞서 소개한 2개의 ExceptionResolver보다 우선순위가 높다. 해당 ExceptionResolver는 예외가 발생한 Controller에 @ExceptionHandler가 존재하는지 확인하고 @ExceptionHandler()를 사용하여 지정한 예외 클래스와 그것의 자식 클래스를 처리할 수 있도록 해준다. @ExceptionHandler()는 메소드 위에 작성하고 지정된 예외 클래스가 발생했을 경우, 해당 메소드가 예외를 처리하여 응답한다.

예시 코드를 살펴보면,

@RestController
@RequestMapping("/api")
public class TestController {

    @GetMapping("/test")
    public void test() throws IOException {
        throw new CustomException();
    }

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<Map<String,String>> testException(){

        Map<String, String> result = new HashMap<>();
        result.put("code","400");
        result.put("message","custom exception");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(result);
    }
}

TestController에 testException()이라는 메소드가 존재하고 해당 메소드에 @ExceptionHandler(CustomException.class) 어노테이션이 존재한다. 이것은 TestController에서 발생된 CustomException은 testException()에서 resolve한다는 의미이다.
이처럼 @ExceptionHandler는 사용하면 클래스마다 동일한 예외가 발생하여도 다르게 처리하고 데이터를 응답해줄 수 있다. 뿐만 아니라 예외가 Was까지 전달되지 않아 BasicErrorController를 호출할 필요도 없다. 나는 이러한 이유로 스프링에서 예외 처리를 할 때 주로 ErrorCode를 enum으로 만들고 @ExceptionHandler를 사용한다.

@ControllerAdvice, @RestControllerAdvice

해당 어노테이션을 이용하여 정상 응답과 예외 응답 로직을 서로 분리할 수 있다. @ControllerAdvice, @RestControllerAdvice를 사용하여 지정한 Controller에서 발생된 예외를 @ExceptionHandler를 사용하여 해당 클래스에서 전역적으로 처리가 가능하다.

내부 구현 코드

ExceptionHandlerExceptionResolver 클래스에 doResolveHandlerMethodException()가 존재하는데

Find an @ExceptionHandler method and invoke it to handle the raised exception.

@Override
	@Nullable
	protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
			HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {

		ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
        
        / ...

getExceptionHandlerMethod(handlerMethod, exception);를 호출하여 @ExceptionHandler 메소드를 찾는다.

Reference: https://www.baeldung.com/exception-handling-for-rest-with-spring

profile
하나씩 쌓아가는 백엔드 개발자

0개의 댓글