Deep Dive /Java Web /12-3

12-3. DispatcherServlet → ExceptionHandler: MethodArgumentNotValidException 처리

검증 오류가 발생하면 MethodArgumentNotValidException이 던져지고, DispatcherServlet은 일반 컨트롤러 흐름 대신 예외 처리 체인으로 분기합니다.


시퀀스 다이어그램

sequenceDiagram participant DS as DispatcherServlet participant ResolverChain as HandlerExceptionResolver[] participant EHER as ExceptionHandlerExceptionResolver participant EH as @ExceptionHandler / @ControllerAdvice participant Resp as Response DS->>ResolverChain: processHandlerException(request, response, handler, ex) ResolverChain->>EHER: resolveException(...) EHER->>EHER: getExceptionHandlerMethod(...) alt 매칭되는 @ExceptionHandler 존재 EHER->>EH: invokeAndHandle(...) EH-->>EHER: ModelAndView / ResponseEntity EHER-->>DS: 예외 처리 결과 반환 else 매칭 없음 EHER-->>ResolverChain: null ResolverChain->>ResolverChain: 다음 Resolver 시도 end DS-->>Resp: 400 응답 또는 에러 응답 렌더링

DispatcherServlet의 예외 처리 분기

// DispatcherServlet.java
protected @Nullable ModelAndView processHandlerException(
        HttpServletRequest request,
        HttpServletResponse response,
        @Nullable Object handler,
        Exception ex) throws Exception {

    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break; // 첫 매칭 resolver 채택
            }
        }
    }

    if (exMv != null) {
        return exMv;
    }

    throw ex;
}

즉, Step 9의 HandlerMapping과 유사하게 예외 처리도 “리졸버 체인”에서 first-match-wins 방식으로 동작합니다.


ExceptionHandlerExceptionResolver의 핵심

// ExceptionHandlerExceptionResolver.java
protected @Nullable ModelAndView doResolveHandlerMethodException(..., Exception exception) {
    ServletInvocableHandlerMethod exceptionHandlerMethod =
        getExceptionHandlerMethod(handlerMethod, exception, webRequest);

    if (exceptionHandlerMethod == null) {
        return null;
    }

    exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
    ...
}

getExceptionHandlerMethod는 다음 순서로 탐색합니다.

  1. 해당 컨트롤러 내부의 @ExceptionHandler
  2. @ControllerAdvice에 등록된 전역 @ExceptionHandler

MethodArgumentNotValidException의 일반적인 결과

  1. 기본 리졸버 또는 @ExceptionHandler가 예외를 처리
  2. 보통 HTTP 400(Bad Request) 응답
  3. 응답 본문에는 필드 오류 목록을 담도록 커스터마이징 가능

탐구 질문

  1. Validation 예외를 컨트롤러별 @ExceptionHandler로 나누는 방식과 전역 @ControllerAdvice 방식 중, 지금 프로젝트에 더 맞는 방식은 무엇일까요?
  2. 오류 응답 포맷을 고정하려면 어떤 계층에서 표준화하는 것이 가장 안정적일까요?
  3. 예외가 처리된 경우 인터셉터의 postHandle, afterCompletion 호출 순서는 어떻게 될까요?