Deep Dive /Java Web /11

11. DispatcherServlet → HandlerInterceptor: preHandle()

DispatcherServlet이 받은 HandlerExecutionChain을 사용해서 인터셉터들의 preHandle()을 실행합니다. 이것이 “샌드위치”의 위쪽 빵 입니다.


시퀀스 다이어그램

sequenceDiagram participant DispatcherServlet participant Chain as HandlerExecutionChain participant I1 as Interceptor 1 participant I2 as Interceptor 2 participant HandlerAdapter participant Controller DispatcherServlet->>Chain: applyPreHandle(request, response) Chain->>I1: preHandle(...) I1-->>Chain: true Chain->>I2: preHandle(...) alt I2 == false I2-->>Chain: false Chain->>Chain: triggerAfterCompletion() Chain-->>DispatcherServlet: false Note over DispatcherServlet: 컨트롤러 호출 중단 else I2 == true I2-->>Chain: true Chain-->>DispatcherServlet: true DispatcherServlet->>HandlerAdapter: handle(handler) HandlerAdapter->>Controller: invoke() Controller-->>HandlerAdapter: ModelAndView / Object HandlerAdapter-->>DispatcherServlet: 결과 반환 end

DispatcherServlet의 doDispatch() 코드

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HandlerExecutionChain mappedHandler = null;
    
    try {
        // 1️⃣ 체인 획득
        mappedHandler = getHandler(processedRequest);
        if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
        }

        // 2️⃣ ← 여기! preHandle() 호출
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;  // ← false면 여기서 즉시 중단!
        }

        // 3️⃣ 컨트롤러 실행
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
        ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

        // 4️⃣ postHandle() 호출 (Step 17-V2)
        mappedHandler.applyPostHandle(processedRequest, response, mv);
    }
}

HandlerExecutionChain.applyPreHandle() 내부 동작

// HandlerExecutionChain.java
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    // 정방향 순회: interceptor[0] → interceptor[1] → interceptor[2] → ...
    for (int i = 0; i < this.interceptorList.size(); i++) {
        HandlerInterceptor interceptor = this.interceptorList.get(i);
        
        // 각 인터셉터의 preHandle() 호출
        if (!interceptor.preHandle(request, response, this.handler)) {
            
            // ❌ false 반환 시: 
            // - 이 지점 이후의 인터셉터는 실행 안 됨
            // - 컨트롤러도 실행 안 됨
            // - 하지만 afterCompletion은 여전히 실행됨 (예외 처리)
            triggerAfterCompletion(request, response, null);
            return false;
        }
        
        // ✅ true 반환 시: 다음 인터셉터로 계속 진행
        this.interceptorIndex = i;  // ← 마지막으로 실행된 인덱스 기록!
    }
    
    // 모든 인터셉터가 true 반환 시에만 여기 도달
    return true;
}

핵심: interceptorIndex는 왜 기록할까?

interceptorIndex를 기록하는 이유:
┌─────────────────────────────────────┐
│ Interceptor 1: preHandle() → true   │ interceptorIndex = 0
│ Interceptor 2: preHandle() → true   │ interceptorIndex = 1
│ Interceptor 3: preHandle() → FALSE  │ ← 여기서 중단!
│                                     │
│ 컨트롤러는 실행 안 됨               │
│ 따라서 afterCompletion도 실행 안 됨 │
└─────────────────────────────────────┘

그런데 잠깐! afterCompletion은 여전히 실행되어야 합니다.
왜냐하면 이미 실행된 1, 2번 인터셉터의 자원을 정리해야 하기 때문!

그래서 interceptorIndex = 1 이기 때문에:
afterCompletion은 역순으로 1번, 0번만 실행합니다.
(3번은 preHandle이 실행 안 됐으니 afterCompletion도 실행 안 함)

실제 흐름: 3개 인터셉터의 경우

✅ 모두 성공 케이스

preHandle 호출 (정방향):
┌─────────────────────────────────┐
│ Interceptor 1: preHandle() ✅   │ → interceptorIndex = 0
│ Interceptor 2: preHandle() ✅   │ → interceptorIndex = 1
│ Interceptor 3: preHandle() ✅   │ → interceptorIndex = 2
└─────────────────────────────────┘
              ↓
         컨트롤러 실행
              ↓
postHandle 호출 (역방향):
┌─────────────────────────────────┐
│ Interceptor 3: postHandle()     │
│ Interceptor 2: postHandle()     │
│ Interceptor 1: postHandle()     │
└─────────────────────────────────┘

❌ Interceptor 2에서 false 반환

preHandle 호출 (정방향):
┌─────────────────────────────────┐
│ Interceptor 1: preHandle() ✅   │ → interceptorIndex = 0
│ Interceptor 2: preHandle() ❌   │ ← 여기서 중단!
│ Interceptor 3: preHandle() (실행 안 됨)
└─────────────────────────────────┘
              ↓
    컨트롤러 실행 안 됨
              ↓
afterCompletion 호출 (역방향, 0부터 시작):
┌─────────────────────────────────┐
│ Interceptor 1: afterCompletion()│ ← 0번만 정리
└─────────────────────────────────┘

왜 역순이고 0부터? interceptorIndex = 0이기 때문!

HandlerInterceptor 인터페이스

public interface HandlerInterceptor {
    
    /**
     * preHandle() - 컨트롤러 실행 전
     * @return true → 다음 인터셉터/컨트롤러 실행
     *         false → 여기서 중단, afterCompletion만 실행
     */
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, 
                             Object handler) throws Exception {
        return true;  // 기본값: 항상 통과
    }
    
    /**
     * postHandle() - 컨트롤러 실행 후
     * 역순으로 실행됨
     */
    default void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
    
    /**
     * afterCompletion() - 뷰 렌더링 후
     * 항상 실행됨 (예외 발생해도)
     */
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                                Object handler, @Nullable Exception ex) throws Exception {
    }
}

흐름 다이어그램

                    ┌─────────────────────┐
                    │  DispatcherServlet  │
                    │    doDispatch()     │
                    └──────────┬──────────┘
                              │
                    ┌─────────▼──────────┐
                    │ getHandler()       │  ← Step 9
                    │ ↓                  │
                    │ HandlerMapping     │
                    │ ↓                  │
                    │ HandlerExecution  │
                    │    Chain 반환     │  ← Step 10
                    └─────────┬──────────┘
                              │
                    ┌─────────▼──────────┐
                    │ applyPreHandle()   │
                    │ (Step 11)          │
                    │                    │
                    │ 인터셉터 1 ✅      │
                    │ 인터셉터 2 ✅  ??? │ ← 여기서 false?
                    │ 인터셉터 3 ✅      │
                    └─────────┬──────────┘
                              │
                    ┌─────────▼──────────┐
                    │  컨트롤러 실행     │
                    │  (Step 13+)        │
                    └────────────────────┘

🤔 탐구 질문

Step 11을 이해했다면:

  1. preHandle에서 false를 반환하는 경우의 사용 사례는?
    • 예를 들어, 인증 실패 시?
    • CORS 사전 요청(preflight) 처리?
    • 요청 크기 초과?
  2. interceptorIndex = 1일 때 afterCompletion은 정확히 어떻게 실행될까?
    • 0번 인터셉터의 afterCompletion만 실행되나요?
    • 그런데 2번 인터셉터는? (preHandle 실행 안 됐으니까?)
  3. HandlerInterceptor를 구현해서 preHandle에서 false를 반환하면, 응답은 누가 작성하나요?
    • 인터셉터가 이미 response를 작성했다고 가정?

다음 단계에서는 이런 실제 사례들을 학습할 거예요! 🚀