2024. 1. 15. 21:07ㆍSpring
이번 포스팅에서는 @ExceptionHandler의 동작 원리를 살펴보도록 하겠습니다.
ExceptionHandler라는 이름에서 알 수 있듯이 예외를 핸들링하는 데 사용하는 애노테이션이다.
예외를 핸들링하려면 예외가 발생해야합니다.
그렇다면, Spring에서는 예외가 발생하였을 때 어떤 흐름일까요?
예외 발생 FLOW
HTTP 요청 : WAS -> Filter -> DispatcherServlet -> Interceptor -> Controller -> Service -> 등등...
만약 파란색부분에서 에러가 발생하였을 경우, 아래와 같은 FLOW를 타게됩니다.
Controller -> Interceptor -> DispatcherServlet -> Filter -> WAS
만약 에러 페이지를 요청한다면, 다시 컨트롤러까지 요청이 전달되어 에러 페이지를 반환합니다.
WAS -> Filter -> DispatcherServlet -> Interceptor -> Controller -> 에러 페이지
Spring에서 기본적으로 제공하는 에러 컨트롤러는 BasicErrorController이다.
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
*/
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.emptyList());
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
HttpStatus status = getStatus(request);
return ResponseEntity.status(status).build();
}
...
}
코드에서 볼 수 있듯이 ModelAndView와 ResponseEntity 타입으로 반환하고 있다.
하지만, BasicErrorController에서 에러 핸들링을 한다는 것은 에러가 WAS까지 전달이 됬다는 것이다.
만약 WAS까지 에러가 전달이 되지 않는다면, 상당한 리소스 절약이 될 수 있을 것이다.
HandlerExceptionResolver
HandlerExceptionResolver란 WAS에 에러가 전파되기 전에 Exception을 해결해주는 녀석이다.
ExceptionResolver는 Handler → DispatecherServlet -> Filter -> WAS 이렇게 예외가 전달되기 전에 DispatcherServlet에서 다음으로 넘어갈 때 해당 예외를 잡아서 해결을 시도해준다.
즉, WAS로 전달되기 전에 예외를 잡아서 해결해준다는 것!
사실 우리는 ExceptionResolver를 알게모르게 사용하고 있다. 바로, @ExceptionHandler이다.
ExceptionResolver 인터페이스는 여러 구현체를 가지고 있는 데, 그 중ExceptionHandlerExceptionResolver에서 @ExceptionHandler를 처리한다.
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
implements ApplicationContextAware, InitializingBean {
...
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
// Local exception handler methods on the controller class itself.
// To be invoked through the proxy, even in case of an interface-based proxy.
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
if (resolver == null) {
resolver = new ExceptionHandlerMethodResolver(handlerType);
this.exceptionHandlerCache.put(handlerType, resolver);
}
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
}
// For advice applicability check below (involving base packages, assignable types
// and annotation presence), use target class instead of interface-based proxy.
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
return null;
}
...
}
해당 메서드는 예외에 대한 @ExceptionHandler 메서드를 찾습니다.
먼저, 컨트롤러의 클래스 계층 구조에서 메서드를 검색하고 찾지 못할 경우에는 @ControllerAdvice가 선언된 클래스에서 @ExceptionHanlder 메서드를 찾는다.
따라서, 아래와 같이 @ExceptionHanlder를 사용하면, 해당 예외(예외를 구현한 예외)를 핸들링할 수 있다.
@RestControllerAdvice
public class GlobalExceptionHandler {
...
@ExceptionHandler(NoHandlerFoundException.class)
protected ApiResponse<?> handleNoHandlerFoundException(NoHandlerFoundException e) {
return ApiResponseGenerator.fail(e.getMessage(), HttpStatus.NOT_FOUND);
}
...
}
'Spring' 카테고리의 다른 글
private 메서드는 트랜잭션 처리를 할 수 없는 이유 (0) | 2023.07.26 |
---|---|
DTO에 대한 고민 (0) | 2023.07.17 |
Mocktio - Spring 단위 테스트 (0) | 2023.05.10 |
Mockito when-thenReturn 사용 시 WrongTypeOfReturnValue 오류 (0) | 2023.05.10 |
필터와 인터셉터 (0) | 2023.04.07 |