Filter를 공부하다 보면 자연스럽게 이런 의문이 생긴다. "Filter랑 비슷한 역할 같은데, Interceptor는 왜 따로 있는 거지?"
Interceptor(인터셉터)는 DispatcherServlet이 Controller를 실행하기 전후에 요청을 가로채서 공통 처리를 할 수 있는 컴포넌트다.
org.springframework.web.servlet.HandlerInterceptor 인터페이스를 구현해서 만든다.
Filter와 가장 큰 차이는 Interceptor는 Spring Context 안에서 동작한다는 점이다. Filter가 Tomcat(Servlet Container) 레벨이라면, Interceptor는 Spring MVC 레벨이다. 그래서 Spring Bean을 자유롭게 주입받아 사용할 수 있다.
Filter로도 공통 처리를 할 수 있는데, 왜 Interceptor가 별도로 필요할까?
Filter는 Spring Context 밖에서 동작하기 때문에 몇 가지 한계가 있다.
@Component 방식이면 가능하지만 제약이 있다)ModelAndView)에 접근할 수 없다Interceptor는 이 한계를 채워준다.
실무에서 Interceptor가 담당하는 대표적인 역할들이다.
request에 미리 담아두기
GET /api/my-page 요청이 들어왔을 때 Interceptor가 어느 위치에서 개입하는지 보자.
[Client]
|
| HTTP Request
↓
[FilterChain] ← Filter들 (Spring Context 밖)
|
↓
[DispatcherServlet]
|
| HandlerMapping으로 Handler 결정
| → HandlerExecutionChain 구성 (Handler + Interceptor 목록)
↓
[Interceptor1.preHandle()] ← 등록 순서대로 실행
| → false 반환 시 요청 차단, 이후 흐름 전부 중단
↓
[Interceptor2.preHandle()]
|
↓
[Controller 메서드 실행]
|
| Service → Repository → DB 처리
↓
[Interceptor2.postHandle()] ← 등록 역순으로 실행
|
↓
[Interceptor1.postHandle()]
|
↓
[View 렌더링 or MessageConverter로 JSON 직렬화]
|
↓
[Interceptor2.afterCompletion()] ← 등록 역순으로 실행 (예외 발생해도 항상 실행)
|
↓
[Interceptor1.afterCompletion()]
|
↓
[FilterChain] ← 응답 방향으로 Filter 역순 통과
|
↓
[Client]
HTTP Response
preHandle()은 등록 순서대로, postHandle()과 afterCompletion()은 등록 역순으로 실행된다.
스택(Stack) 구조처럼 들어온 순서의 반대로 나간다고 생각하면 된다.
Controller 실행 전에 호출된다.
반환값이 boolean이라는 게 핵심이다.
true 반환 → 다음 Interceptor 또는 Controller로 진행false 반환 → 요청 처리 중단. 이후 모든 흐름이 멈춤
로그인 여부 확인, 토큰 검증 같은 요청 차단 로직을 여기에 넣는다.
파라미터로 handler(실행될 Controller 메서드 정보)를 받을 수 있다.
Controller 실행 후, View 렌더링(또는 응답 직렬화) 전에 호출된다.
Controller가 반환한 ModelAndView에 접근할 수 있다.
REST API에서는 @ResponseBody를 쓰기 때문에 ModelAndView가 null인 경우가 많다.
Controller에서 예외가 발생하면 postHandle()은 호출되지 않는다.
이 점이 afterCompletion()과의 결정적 차이다.
요청 처리가 완전히 끝난 후 호출된다. 예외가 발생해도 반드시 실행된다.
파라미터로 Exception ex를 받기 때문에 어떤 예외가 발생했는지도 확인할 수 있다.
리소스 정리, 소요 시간 측정 마무리, 예외 로깅 같은 처리를 여기에 넣는다.
Interceptor는 WebMvcConfigurer를 구현한 설정 클래스에서 addInterceptors()로 등록한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginCheckInterceptor loginCheckInterceptor;
private final LoggingInterceptor loggingInterceptor;
public WebConfig(LoginCheckInterceptor loginCheckInterceptor,
LoggingInterceptor loggingInterceptor) {
this.loginCheckInterceptor = loginCheckInterceptor;
this.loggingInterceptor = loggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 로깅 Interceptor — 모든 경로에 적용, 가장 먼저 실행
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/**")
.order(1);
// 로그인 체크 Interceptor — /api/** 적용, 일부 경로 제외
registry.addInterceptor(loginCheckInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/api/login",
"/api/signup",
"/api/health"
)
.order(2);
}
}
addPathPatterns()로 적용 경로, excludePathPatterns()로 제외 경로를 지정한다.
order() 값이 낮을수록 preHandle()이 먼저 실행된다.
@Component
@RequiredArgsConstructor
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 실행될 Handler가 실제 Controller 메서드인지 확인
if (!(handler instanceof HandlerMethod)) {
return true; // 정적 리소스 등은 그냥 통과
}
// 세션에서 로그인 정보 확인
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("loginUser") == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\": \"로그인이 필요합니다.\"}");
return false; // 요청 차단
}
return true; // 통과
}
}
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final String START_TIME_KEY = "startTime";
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
request.setAttribute(START_TIME_KEY, System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
long start = (Long) request.getAttribute(START_TIME_KEY);
long elapsed = System.currentTimeMillis() - start;
String handlerName = "";
if (handler instanceof HandlerMethod method) {
// 실행된 Controller 클래스명.메서드명 추출
handlerName = method.getBeanType().getSimpleName()
+ "." + method.getMethod().getName();
}
System.out.printf("[%s %s] handler=%s | %dms | status=%d%n",
request.getMethod(),
request.getRequestURI(),
handlerName,
elapsed,
response.getStatus());
}
}
Interceptor의 강점 중 하나다.
handler를 HandlerMethod로 캐스팅하면 실행될 Controller 메서드의 상세 정보를 얻을 수 있다.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod handlerMethod) {
// 커스텀 애노테이션 존재 여부 확인
LoginRequired loginRequired =
handlerMethod.getMethodAnnotation(LoginRequired.class);
if (loginRequired != null) {
// @LoginRequired 붙은 메서드만 로그인 체크
// ... 인증 로직
}
}
return true;
}
Controller에서 예외가 발생하면 postHandle()은 건너뛴다.
예외 여부와 무관하게 항상 실행되어야 하는 코드는 반드시 afterCompletion()에 넣어야 한다.
소요 시간 측정, 리소스 정리, 로깅 마무리가 여기에 해당한다.
preHandle()에서 예외를 던지면 DispatcherServlet이 이를 잡아서
HandlerExceptionResolver에 위임한다.
즉, @ControllerAdvice로 Interceptor 예외를 잡을 수 있다.
이 점이 Filter 예외와 다르다. Filter 예외는 @ControllerAdvice가 못 잡지만,
Interceptor 예외는 Spring MVC 안에 있기 때문에 잡힌다.
HttpServletRequest 접근 필요할 때로그인 체크처럼 HTTP 요청 자체를 제어해야 한다면 Interceptor, 트랜잭션 로깅이나 실행 시간 측정처럼 메서드 레벨 공통 처리라면 AOP가 적합하다.
addPathPatterns("/**")로 전체 경로에 Interceptor를 적용하면
정적 리소스 요청(.js, .css, .html 등)에도 Interceptor가 실행된다.
handler가 HandlerMethod가 아닌 ResourceHttpRequestHandler인 경우를 체크해서
정적 리소스는 그냥 통과시키는 코드를 넣는 게 일반적이다.
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true; // 정적 리소스는 그냥 통과
}
// 실제 로직
return true;
}
Spring Security를 쓰는 프로젝트라면 인증/인가는 Security Filter에서 처리하고, Interceptor는 비즈니스 레벨의 접근 제어나 로깅에 집중하는 게 깔끔하다. 예를 들어 "로그인 여부"는 Security가 처리하고, "이 사용자가 이 리소스의 소유자인가"는 Interceptor 또는 Service에서 처리하는 식이다.
preHandle()에서 false를 반환하면 이후 Interceptor의 preHandle()도 실행되지 않는다.
단, 이미 preHandle()이 true를 반환한 Interceptor들의 afterCompletion()은 실행된다.
리소스 정리가 필요한 Interceptor가 있다면 이 동작을 염두에 두어야 한다.
HandlerInterceptor 인터페이스의 세 메서드: preHandle() → Controller 실행 전, postHandle() → 실행 후, afterCompletion() → 완전히 끝난 후preHandle()이 false를 반환하면 요청이 차단된다postHandle()은 예외 발생 시 호출되지 않는다. 항상 실행되어야 하는 코드는 afterCompletion()에 넣는다@ControllerAdvice로 잡을 수 있다 — Filter 예외와 다른 점WebMvcConfigurer의 addInterceptors()에서 하고, 경로와 순서를 제어할 수 있다Filter 공부하고 나서 Interceptor를 보니까 "아, 이 둘이 서로 보완 관계구나"가 바로 느껴졌다. Filter는 Spring 밖에서 넓게 잡아주고, Interceptor는 Spring 안에서 세밀하게 제어한다.
특히 postHandle()이 예외 발생 시 호출되지 않는다는 부분은 실수하기 쉬운 포인트다.
소요 시간 측정을 postHandle()에 넣었다가 예외 상황에서 로그가 안 찍히는 걸 경험하고 나서야 afterCompletion()으로 옮겼다.
이런 건 직접 겪어봐야 확실히 머리에 남는 것 같다.