일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 그리디 알고리즘
- 알고리즘
- 접근제어자
- XLF
- 제태크
- javascript
- 기업분석
- 금리인상
- 프로그래머스
- FCF
- 무디스
- 백준
- 오버라이딩
- 주식
- 주린이
- 객체지향
- 다형성
- S&P500
- 금리인하
- 잉여현금흐름
- 자바
- object
- etf
- 현금흐름표
- 배당성장
- 미국주식
- mco
- Java
- StringBuffer
- 인플레이션
- Today
- Total
오늘의하루
Spring Controller Advice: Global vs. Custom Advice 본문
Spring의 @ControllerAdvice는 애플리케이션 전역에서 발생하는 예외를 처리하는 강력한 도구이며 특정 컨트롤러나 패키지에 국한되지 않고 다양한 방식으로 활용할 수 있습니다.
자사 API 코드를 분석하던 중 Custom @ControllerAdvice에 MissingServletRequestParameterException 예외를 처리하도록 정의되어 있는 것을 확인했습니다. 하지만 이 예외는 Custom @ControllerAdvice에서 절대 처리되지 않는다는 사실을 알고 있었기 때문에 이를 Global @ControllerAdvice에서 정의해야 한다는 의견을 제안했습니다. 이 기회에 Custom @ControllerAdvice가 왜 MissingServletRequestParameterException과 같은 예외를 처리할 수 없는지에 대해 간단히 정리해보려 합니다.
Global Adivce vs Custom Advice
- Global Advice : base packages를 지정하지 않은 경우, 모든 컨트롤러에서 발생하는 예외를 처리합니다.
- Custom Advice : base packages를 지정한 경우 해당 패키지 내 컨트롤러에서 발생하는 예외를 처리합니다.
Why? Gloabl Advice 작동할까?
Spring의 예외 처리 메커니즘은 ExceptionHandlerExceptionResolver 클래스에서 구현됩니다.
이 클래스의 내부 동작을 살펴보면 Global @ControllerAdvice가 모든 예외를 처리할 수 있는 이유를 알 수 있습니다.
아래는 핵심 메서드와 그 역할을 간략히 정리한 내용입니다.
1. doResolveHandlerMethodException
// ExceptionHandlerExceptionResolver.class
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
ServletInvocableHandlerMethod exceptionHandlerMethod = this.getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null;
} else {
if (this.argumentResolvers != null) {
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
ServletWebRequest webRequest = new ServletWebRequest(request, response);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
ArrayList<Throwable> exceptions = new ArrayList<>();
try {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
}
Throwable cause;
for (Throwable exToExpose = exception; exToExpose != null; exToExpose = cause != exToExpose ? cause : null) {
exceptions.add(exToExpose);
cause = exToExpose.getCause();
}
Object[] arguments = new Object[exceptions.size() + 1];
exceptions.toArray(arguments);
arguments[arguments.length - 1] = handlerMethod;
exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
} catch (Throwable invocationEx) {
if (!exceptions.contains(invocationEx) && this.logger.isWarnEnabled()) {
this.logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
}
return null;
}
if (mavContainer.isRequestHandled()) {
return new ModelAndView();
} else {
ModelMap model = mavContainer.getModel();
HttpStatus status = mavContainer.getStatus();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
mav.setViewName(mavContainer.getViewName());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
return mav;
}
}
}
- 해당 메서드는 발생한 예외를 처리할 수 있는 @ExceptionHandler 메서드를 찾아 호출합니다.
- getExceptionHandlerMethod를 통해 적절한 Handler를 찾고 이를 실행한 뒤 결과를 반환합니다.
2. getExceptionHandlerMethod
// ExceptionHandlerExceptionResolver.class
@Nullable
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(@Nullable HandlerMethod handlerMethod, Exception exception) {
Class<?> handlerType = null;
if (handlerMethod != null) {
handlerType = handlerMethod.getBeanType();
ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver) 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);
}
if (Proxy.isProxyClass(handlerType)) {
handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
}
}
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
ControllerAdviceBean advice = (ControllerAdviceBean) entry.getKey();
if (advice.isApplicableToBeanType(handlerType)) {
ExceptionHandlerMethodResolver resolver = (ExceptionHandlerMethodResolver) entry.getValue();
Method method = resolver.resolveMethod(exception);
if (method != null) {
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
}
}
}
return null;
}
- 해당 메서드는 @ControllerAdvice에 정의된 예외 처리 메서드 중 적합한 것을 찾습니다.
- isApplicableToBeanType(handlerType)에서 basePackages 조건을 확인합니다.
- Global Advice는 이 조건이 항상 true로 평가됩니다.
3. test
// HandlerTypePredicate.class
public boolean test(@Nullable Class<?> controllerType) {
if (!this.hasSelectors()) {
return true; // basePackages가 없으면 모든 컨트롤러에 적용
} else {
if (controllerType != null) {
for (String basePackage : this.basePackages) {
if (controllerType.getName().startsWith(basePackage)) {
return true;
}
}
for (Class<?> clazz : this.assignableTypes) {
if (ClassUtils.isAssignable(clazz, controllerType)) {
return true;
}
}
for (Class<? extends Annotation> annotationClass : this.annotations) {
if (AnnotationUtils.findAnnotation(controllerType, annotationClass) != null) {
return true;
}
}
}
return false;
}
}
- basePackages가 지정되지 않은 경우(Global Advice) 모든 컨트롤러에 대해 true를 반환합니다.
- Custom Advice는 basePackages에 해당하는 컨트롤러만 필터링합니다.
4. resolveMethodByThrowable
@Nullable
public Method resolveMethodByThrowable(Throwable exception) {
Method method = this.resolveMethodByExceptionType(exception.getClass());
if (method == null) {
Throwable cause = exception.getCause();
if (cause != null) {
method = this.resolveMethodByThrowable(cause);
}
}
return method;
}
- 해당 메서드는 Handler가 발생한 예외를 처리하도록 정의되어 있는지 확인합니다.
5. 결론
MissingServletRequestParameterException은 Spring MVC의 요청 파라미터 검증 중 DispatcherServlet에서 발생하며 컨트롤러 메서드 호출 전에 처리됩니다.
Custom @ControllerAdvice는 basePackages로 제한된 컨트롤러의 예외만 다룰 수 있기 때문에 이 예외가 범위 밖이거나 컨트롤러 실행 전 발생 시 처리할 수 없습니다.
하지만 Global @ControllerAdvice는 범위 제한 없이 모든 예외를 포괄하므로 이를 효과적으로 처리할 수 있습니다.
'Spring' 카테고리의 다른 글
[Rest Docs]Test 없는 API 문서는 시한폭탄 (0) | 2025.02.26 |
---|---|
Spring Cloud Eureka와 Go의 만남 (0) | 2025.01.06 |
[레거시 리팩토링] 상속을 넘어 컴포지션으로 가보자 (0) | 2024.12.17 |
DTO(Data Transfer Object)를 쓰는 이유? (1) | 2024.11.11 |
BeanPostProcessor 사용법: 빈 초기화 과정에서의 조작과 시점 이해하기 (0) | 2024.11.07 |