스프링에서 예외 처리하기 위해서 @ExceptionHandler 어노테이션을 자주 사용했습니다.
컨트롤러A에 @ExceptionHandler와 @ControllerAdvice를 이용하면 다른 컨트롤러B, C에서 일어난 예외를 잡을 수 있었습니다.
이뿐만 아니라 @ResponseStatus, TypeMismatchException, 에러 페이지 렌더링, ... 등의 기능이 있었습니다.
이게 과연 어떻게 가능한걸까요 ? 🤔
검색해봤을 때 HandlerExcpetionResovler 인터페이스를 알게 되었습니다.
하나하나 알아볼까요 ?
🟧 01. Resolvers 예외 처리기 등록
Spring configuration에서 예외 처리기인 여러 HandlerExceptionResolver을 빈 체이닝으로 선언한다.
필요하다면 order 속성을 이용해서 설정할 수 있다.
HandlerExceptionResovler는 다음과 같이 반환할 수 있다.
- 에러 view인 ModelAndView
- resolver가 예외를 해결했다면 비어있는 ModelAndView를 반환한다.
- 예외가 해결되지 않은 상태로 남았다면 후속 resovler가 처리할 수 있게 null을 반환한다.
- 예외가 마지막까지 남아 있다면 Servlet Container까지 넘겨준다
MVC config는 Spring MVC 예외( @ResponseStatus, @ExceptionHandler )를 기본으로 등록하고 있다.
🟧 02. HandlerExceptionResolver는 예외를 해결하거나 다른 대안을 제공한다.
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
요청 매핑 도중, Controller에서 예외가 발생할 경우 DispatcherServlet은 HandlerExceptionResolver 빈 체인에 넘겨준다.
HandlerExceptionResolver는 예외를 해결하거나, 에러 응답과 같은 다른 대안을 제공한다.
HandlerExceptionResolver는 다음과 같은 구현체가 있다.
- SimpleMappingExceptionResolver (브라우저 렌더링)
- DefaultHandlerExceptionResolver (기본적인 예외)
- ResponseStatusExceptionResolver (@ResponseStatus)
- ExceptionHandlerExceptionResolver (@ExceptionHandler)
02-01. SimpleMappingExceptionResolver (브라우저 렌더링)
- Exception 클래스 이름, 에러 View 이름을 매핑한다.
- 브라우저 애플리케이션에서 에러 페이지로 rendering하는데 유용하다.
public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionResolver {
// ...
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex
) {
// Expose ModelAndView for chosen error view.
String viewName = determineViewName(ex, request);
if (viewName != null) {
// Apply HTTP status code for error views, if specified.
// Only apply it if we're processing a top-level request.
Integer statusCode = determineStatusCode(request, viewName);
if (statusCode != null) {
applyStatusCodeIfPossible(request, response, statusCode);
}
return getModelAndView(viewName, ex, request);
}
else {
return null;
}
}
}
02-02. DefaultHandlerExcpetionResolver (기본적인 예외)
- Spring MVC에서 발생한 예외를 해결한다.
- HTTP Status Code를 매핑한다.
- Error Response를 응답한다.
public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionResolver {
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex
) {
try {
if (ex instanceof HttpRequestMethodNotSupportedException) {
return handleHttpRequestMethodNotSupported(
(HttpRequestMethodNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotSupportedException) {
return handleHttpMediaTypeNotSupported(
(HttpMediaTypeNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof HttpMediaTypeNotAcceptableException) {
return handleHttpMediaTypeNotAcceptable(
(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
}
else if (ex instanceof MissingPathVariableException) {
return handleMissingPathVariable(
(MissingPathVariableException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter(
(MissingServletRequestParameterException) ex, request, response, handler);
}
else if (ex instanceof ServletRequestBindingException) {
return handleServletRequestBindingException(
(ServletRequestBindingException) ex, request, response, handler);
}
else if (ex instanceof ConversionNotSupportedException) {
return handleConversionNotSupported(
(ConversionNotSupportedException) ex, request, response, handler);
}
else if (ex instanceof TypeMismatchException) {
return handleTypeMismatch(
(TypeMismatchException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotReadableException) {
return handleHttpMessageNotReadable(
(HttpMessageNotReadableException) ex, request, response, handler);
}
else if (ex instanceof HttpMessageNotWritableException) {
return handleHttpMessageNotWritable(
(HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
return handleMethodArgumentNotValidException(
(MethodArgumentNotValidException) ex, request, response, handler);
}
else if (ex instanceof MissingServletRequestPartException) {
return handleMissingServletRequestPartException(
(MissingServletRequestPartException) ex, request, response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
}
else if (ex instanceof NoHandlerFoundException) {
return handleNoHandlerFoundException(
(NoHandlerFoundException) ex, request, response, handler);
}
else if (ex instanceof AsyncRequestTimeoutException) {
return handleAsyncRequestTimeoutException(
(AsyncRequestTimeoutException) ex, request, response, handler);
}
}
catch (Exception handlerEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
}
}
return null;
}
}
02-03. ResponseStatusExceptionResolver (@ResponseStatus)
- @ResponseStatus 어노테이션이 있는 예외를 해결한다.
- 어노테이션에 있는 값을 HTTP Status Code를 매핑한다.
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
@Override
@Nullable
protected ModelAndView doResolveException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex
) {
try {
if (ex instanceof ResponseStatusException) {
return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
}
ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (status != null) {
return resolveResponseStatus(status, request, response, handler, ex);
}
if (ex.getCause() instanceof Exception) {
return doResolveException(request, response, handler, (Exception) ex.getCause());
}
}
catch (Exception resolveEx) {
if (logger.isWarnEnabled()) {
logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", resolveEx);
}
}
return null;
}
}
02-04. ExceptionHandlerExceptionResolver (@ExceptionHandler)
- @Controller 또는 @ControllerAdvice 클래스에 있는 @ExceptionHandler 메서드를 호출하여 예외를 해결한다.
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
/**
* Find an {@code @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);
if (exceptionHandlerMethod == null) {
return null;
}
// ...
try {
// ...
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
}
// ...
}
/**
* Find an {@code @ExceptionHandler} method for the given exception. The default
* implementation searches methods in the class hierarchy of the controller first
* and if not found, it continues searching for additional {@code @ExceptionHandler}
* methods assuming some {@linkplain ControllerAdvice @ControllerAdvice}
* Spring-managed beans were detected.
* @param handlerMethod the method where the exception was raised (may be {@code null})
* @param exception the raised exception
* @return a method to handle the exception, or {@code null} if none
*/
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
// ...
}
}
여기까지 4가지 ExceptionResolver를 봤다.
이런 예외 처리기는 Spring Configuration에서 빈 체이닝 방식으로 선언한다.
🟧 03. ExceptionHandlerExceptionResolver와 @ExceptionHandler
/**
* An {@link AbstractHandlerMethodExceptionResolver} that resolves exceptions
* through {@code @ExceptionHandler} methods.
*
* <p>Support for custom argument and return value types can be added via
* {@link #setCustomArgumentResolvers} and {@link #setCustomReturnValueHandlers}.
* Or alternatively to re-configure all argument and return value types use
* {@link #setArgumentResolvers} and {@link #setReturnValueHandlers(List)}.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.1
*/
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
// ...
}
- Spring MVC에서 제공하는 ExceptionHandlerExceptionResolver는 웹 애플리케이션에서 exception을 처리한다.
- ExceptionHandlerExceptionResolver는 요청에서 예외가 발생하면 @ExceptionHandler 어노테이션을 찾아서 예외를 처리하도록 호출한다.
이러한 ExceptionHandlerExceptionResolver는 중요한 두 개의 메서드가 있다.
- doResolveHandlerMethodException(...)
- getExceptionHandlerMethod(...)
자세한 내용은 바로 밑에 따로 작성했다.
03-01. doResolveHandlerMethodException() 메서드
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
/**
* Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception.
*/
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable HandlerMethod handlerMethod,
Exception exception
) {
// ...
}
- 이 메서드는 @ExceptionHandler 어노테이션이 달린 메서드를 찾아서 예외를 처리하는 책임이 있다.
- 이 메서드는 전달할 ModelAndView를 반환하거나 기본 처리의 경우 null을 반환한다.
- null을 반환할 경우 Spring MVC는 기본 예외 처리를 수행한다.
- 이 메서드에는 4개의 파라미터가 존재한다.
- request : 현재 HTTP Request
- response : 현재 HTTP Response
- handlerMethod : 실행된 handler 메서드, 예외 발생시 어떤 것도 선택하지 않았다면 null
- exception : handler exception 실행 중에 발생한 예외
03-02. getExceptionHandlerMethod() 메서드
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
/**
* Find an {@code @ExceptionHandler} method for the given exception. The default
* implementation searches methods in the class hierarchy of the controller first
* and if not found, it continues searching for additional {@code @ExceptionHandler}
* methods assuming some {@linkplain ControllerAdvice @ControllerAdvice}
* Spring-managed beans were detected.
* @param handlerMethod the method where the exception was raised (may be {@code null})
* @param exception the raised exception
* @return a method to handle the exception, or {@code null} if none
*/
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod,
Exception exception
) {
// ...
}
}
- 요청을 처리한 Controller에서 먼저 @ExceptionHandler 메서드를 찾는다.
- @ExceptionHandler를 동일한 Controller에서 찾지 못한 경우 ApplicationContext에 등록된 @ControllerAdvice 클래스에서 찾는다.
- @ExceptionHandler를 찾을 때는 AnnotaionUtils 클래스를 사용한다.
- 메타 어노테이션을 통해 간접적으로 @ExceptionHandler가 있는 메서드를 찾을 수 있다.
👊 정리하자면
Spring Configuration은 여러 예외 처리기를 체이닝 방식으로 빈 등록한다.
대표적인 예외 처리 인터페이스로 HandlerExceptionResolver가 있다.
HandlerExceptionResolver의 구현체로 4가지를 뽑자면 다음과 같다.
- SimpleMappingExceptionResolver (브라우저 렌더링)
- DefaultHandlerExceptionResolver (기본적인 예외)
- ResponseStatusExceptionResolver (@ResponseStatus)
- ExceptionHandlerExceptionResolver (@ExceptionHandler)
이중에서 ExceptionHandlerExceptionResolver는 @ExceptionHandler를 처리하기 위한 Resolver이다.
Controller에서 요청을 처리하다 예외가 발생했을 경우 해당 컨트롤러에서 먼저 @ExceptionHandler를 찾는다.
만약 없을 경우 ApplicationContext에 등록된 @ControllerAdivce 클래스에서 @ExceptionHandler를 찾는다.
🔗 이어지는 포스팅
[Spring MVC - Exception] @ExceptionHandler와 @ControllerAdvice (2/2)
'🍃 스프링' 카테고리의 다른 글
[Spring] 필터와 인터셉터의 차이 (Filter, Interceptor) (0) | 2023.05.07 |
---|---|
[Spring] 필터(Filter)란 뭘까? (0) | 2023.05.07 |
[Spring MVC] 디스패처 서블릿(DispatcherServlet) 둘러보기 (0) | 2023.04.24 |
[Spring MVC - ArgumentResolver] ArgumentResolver를 이용해서 컨트롤러 메서드의 파라미터 받기 (6) | 2023.04.13 |