@ExceptionHanlder의 동작 원리(Feat.HandlerExceptionResolver)

2024. 1. 15. 21:07Spring

728x90

 

이번 포스팅에서는 @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);
    }
	
	...

}
728x90