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을 이해했다면:
- preHandle에서 false를 반환하는 경우의 사용 사례는?
- 예를 들어, 인증 실패 시?
- CORS 사전 요청(preflight) 처리?
- 요청 크기 초과?
- interceptorIndex = 1일 때 afterCompletion은 정확히 어떻게 실행될까?
- 0번 인터셉터의 afterCompletion만 실행되나요?
- 그런데 2번 인터셉터는? (preHandle 실행 안 됐으니까?)
- HandlerInterceptor를 구현해서 preHandle에서 false를 반환하면, 응답은 누가 작성하나요?
- 인터셉터가 이미 response를 작성했다고 가정?
다음 단계에서는 이런 실제 사례들을 학습할 거예요! 🚀