목차
이전 글에서는 Filter를 통해 요청이 DispatcherServlet에 도달하기 전에 어떻게 가로채지고 처리되는지 살펴보았다.
Spring을 공부하다 보면 Filter와 함께 자주 등장하는 개념이 하나 더 있는데, 바로 Interceptor 이다.
Interceptor 역시 요청을 가로채 처리한다는 점에서 Filter와 비슷해 보이지만,
실제로는 동작 위치와 역할에서 분명한 차이를 가진다.
이번 글에서는 Interceptor가 무엇인지, 어떻게 동작하는지, 그리고 Filter와는 어떤 차이가 있는지 중심으로 살펴보려 한다.
Interceptor 란 ?
웹 애플리케이션 내에서 특정한 URI 호출을 가로채는 역할을 한다.
Interceptor를 활용하면 기존 컨트롤러의 로직을 수정하지 않고도, 사전이나 사후에 제어가 가능하다.
쉽게 말해, 요청과 으답을 가로채서 원하는 동작을 추가하는 역할이다.
웹 컨테이너에서 동작하는 필터와 달리 인터셉터는 Spring이 제공해주는 기술로, Spring 영역에서 동작한다.
주요 메서드
01 ``preHandle()``
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
`preHandle()`은 Controller(Handler)가 실행되기 전에 호출되는 메서드로, interceptor의 가장 핵심적인 진입 지점이다.
이 시점은 HandlerMapping을 통해 어떤 컨트롤러가 실행될지 결정된 이후이며, HandlerAdapter가 실제로 해당 핸들러를 호출하기 직전에 위치한다.
따라서 이 메서드는 요청을 본격적으로 처리하기 전에 공통적인 전처리를 수행하기에 적합하다.
해당 메서드는 ``boolean`` 타입으로 true와 false를 반환하며, 각각의 반환값에 따라 실행 흐름이 다르다.
- ``true`` : 다음 interceptor 또는 controller로 요청이 정상적으로 전달된다.
- ``false`` : 실행 체인이 중단되며, controller는 호출되지 않는다. 이 경우, interceptor에서 직접 응답을 생성해야 한다.
Execution Chain 이란 ?
요청이 Controller에 도달하기까지 거치는 전체 흐름을 의미한다.
특히 Spring에서는 하나의 요청이 여러 Interceptor를 순서대로 통과하면서 처리되는데, 이 흐름 전체를 실행 체인이라고 한다.
02 ``postHandle()``
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
Controller가 정상적으로 실행된 이후, View가 렌더링되기 전에 호출되는 메서드이다.
즉, 비즈니스 로직이 수행된 결과를 기반으로 후처리를 할 수 있는 시점이다.
이 메서드는 Controller가 반환한 ModelAndView 객체를 인자로 받아, View에 전달된 데이터를 수정하거나 공통 데이터를 추가하는 작업을 수행할 수 있다.
다만, 예외가 발생하여 정상적인 흐름이 아닌 경우에는 호출되지 않는다. 또한 여러 개의 interceptor가 체인으로 구성되어 있을 경우, `postHandle()` 은 요청이 들어온 순서와 반대로, 즉 역순으로 실행되는 특징을 가진다.
03 ``afterCompletion()``
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
요청 처리의 모든 과정이 완료된 이후, 즉 View 렌더링까지 끝난 시점에 호출되는 메서드이다.
이 메서드는 요청 처리 결과와 관계없이 항상 호출되며, 예외가 발생한 경우에도 호출된다는 특징이 있다.
댠, `preHandle()`이 ``true``를 반환하여 정상적으로 실행 체인에 포함된 경우에만 호출된다.
이 시점에서는 더 이상 응답을 수정하기보다는, 리소스 정리나 예외 로깅과 같은 마무리 작업을 수행한다.
또한 여러 interceptor가 존재할 경우, `afterCompletion()` 역시 `postHandle()`과 동일하게 역순으로 실행되어 먼저 진입한 interceptor가 가장 마지막에 종료 작업을 수행하게 된다.
04 ``addInterceptors()``
default void addInterceptors(InterceptorRegistry registry) {
}
Controller 실행 전/후에 개입하는 interceptor를 등록할 수 있는 메서드이다.
전체 요청에 적용하거나 특정 URL에만 적용하도록 구현할 수 있다.
실제 코드를 실행하며 알아보기
위 실습 강의에서 작성된 코드를 기반으로 실습해보았다.
01 ``preHandle()``
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
LOGGER.info("[preHandle] preHandle is performed");
LOGGER.info("[preHandle] request : {}", request);
LOGGER.info("[preHandle] request path info : {}", request.getPathInfo());
LOGGER.info("[preHandle] request header names : {}", request.getHeaderNames());
LOGGER.info("[preHandle] request request URL : {}", request.getRequestURL());
LOGGER.info("[preHandle] request request URI: {}", request.getRequestURI());
LOGGER.info("[preHandle] request Requested Session Id : {}", request.getRequestedSessionId());
// TODO HttpServletRequestWrapper 구현하여 Body 값 확인할 수 있게 코드 추가
return true;
}
기존 `preHandle()` 메서드를 오버라이딩한 후, request 정보들을 로그로 출려해보았다.
모든 과정을 성공하면, ``true``를 반환한다.
02 ``postHandle()``
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView)
throws Exception {
LOGGER.info("[postHandle] postHandle is performed");
LOGGER.info("[postHandle] response : {}", response);
LOGGER.info("[postHandle] response : {}", response.getHeaderNames());
}
`postHandle()`도 `preHandle()`과 마찬가지로 기존 메서드를 오버라이딩한 후, response와 헤더 정보들을 출력해보았다.
03 ``afterCompletion()``
@Override
public void afterCompletion(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
LOGGER.info("[afterCompletion] afterCompletion is performed");
}
`afterCompletion()` 또한 기존 메서드를 오버라딩한 후, 해당 메서드가 동작했는지 여부만 판단하기 위한 로그를 출력해보았다.
04 ``addInterceptors()``
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(new HttpInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/hello");
}
`addInterceptor()`를 오버라이딩한 후, 구현해둔 ``HttpInterceptor`` 를 추가해주고,
`/hello` 를 제외한 모든 엔드포인트에 대하여 해당 interceptor를 실행하도록 구현해주었다.
실행 결과
01 ``/hello``를 호출했을 경우

- DispatcherServlet 이 생성된다.
DispatcherServlet는 첫 요청을 받을 때만 생성된다. 이후, ``/hello``를 한 번 더 요청했을 경우에는 생성되지 않는다. - 내부 구성 초기화가 완료된다.
이 단계에서는 HandlerMapping이 생성되고, HandlerAdapter가 생성되며, Interceptor 목록을 로딩하여 Controller 매핑을 준비하는 단계이다.
위 실행에서는 interceptor 에 관련된 로그가 하나도 찍히지 않는 것을 확인할 수 있다.
이는 WebMvcConfig 파일을 보면 알 수 있는데, `addInterceptors()` 메서드에서 `/hello` 경로를 제외했기 때문에 해당 요청에서는 interceptor가 실행되지 않는다.
따라서, interceptor 체인을 거치지 않기 때문에 `preHandle`, `postHandle`, `afterCompletion`에 정의한 로그가 전혀 출력되지 않는 것이다.
02 ``/hello1``를 호출했을 경우

반면 `/hello1`를 호출하면, 이는 excludePathPatterns에 포함되지 않은 경로이기 때문에 interceptor가 정상적으로 동작하게 된다.
- `preHandle()` 이 호출되며, Controller 실행 전에 요청에 대한 정보를 확인하거나 전처리를 수행할 수 있다.
- Controller 로직이 실행된다.
- Controller 로직 실행이 완료되면, `postHandle()` 이 호출된다.
이 단계에서는 응답이 생성되었지만 아직 클라이언트로 전달되기 전이기 때문에 추가적인 가공이 가능하다. - 응답이 완료된 이후 `afterCompletion()`이 호출되며, 전체 요청 처리 과정이 마무리된다.
실제 로그를 보면
``preHandle -> postHandle -> afterCompletion`` 순서로 출력되는 것을 확인할 수 있으며,
이를 통해 interceptor가 요청의 전/후 처리 과정에 개입하고 있음을 알 수 잇다.
결과적으로 `/hello` 요청은 interceptor가 제외된 흐름을 따르고,
`/hello1` 요청은 interceptor 체인을 거치는 흐름을 따르게 되며,
이 차이를 통해 interceptor의 동작 위치와 역할을 명확하게 이해할 수 있다.
interceptor의 전체 흐름

이제 코드 관점에서 Interceptor가 포함된 전체 요청 처리 흐름을 살펴보자.
- 애플리케이션이 시작되면 사전 준비 단계를 거치게 된다.
서버가 실행되면 Spring은 설정 파일을 먼저 읽어 필요한 객체들을 생성한다.
`@Configuration` 클래스를 로딩하고, `WebConfigurer` 구현체를 확인한 후, `addInterceptors()` 메서드를 실행한다. 이 과정에서 interceptor가 어떤 경로에 적용될지 미리 등록된다. - Client가 요청한다.
사용자가 브라우저 또는 클라이언트를 통해 특정 URL로 HTTP 요청을 보낸다. - Tomcat (WAS)이 실행된다.
Apache Tomcat이 요청을 수신하고, HttpServletRequest와 HttpServletResponse 객체를 생성한다. - DispatcherServlet이 실행된다.
DispatcherServlet이 요청을 받아 전체 흐름을 제어하는 역할을 수행한다. 이는 프론트 컨트롤러의 역할이다. - HandlerMapping을 통해 Controller를 찾는다.
요청 URL에 매핑된 Controller를 탐색하며, 이 과정에서 Controller와 Interceptor가 함께 묶인 실행 체인 (HandlerExecutionChain)이 생성된다. - Interceptor의 `preHandle()`이 호출된다.
Controller 실행 전에 호출되며, 요청 로그 처리, 인증/인가, 요청 차단 등의 전처리를 수행할 수 있다. - Controller가 실행된다.
실제 비즈니스 로직이 수행되는 단계로, 요청을 처리하고 View 이름 또는 데이터를 반환한다. - Interceptor의 `postHandle()`이 호출된다.
Controller가 실행된 이후 호출되며, 응답이 생성되었지만 아직 클라이언트로 전달되기 전 단계이다.
이 시점에서 Model 데이터 추가 및 View 변경 등의 후처리가 가능하다. - View or JSON으로 변환한다.
반환 값에 따라 ViewResolver를 통해 HTML이 생성되거나, HttpMessageConverter를 통해 JSON으로 변환된다. - Interceptor의 `afterCompletion()`이 호출된다.
응답까지 완료된 이후 실행되며, 리소스 정리, 예외처리, 최종로그 기록 등에 사용된다 - 응답이 반환된다.
최종적으로 생성된 응답이 Tomcat을 통해 클라이언트에게 전달된다.
이렇게 intercptor의 개념, 주요 메서드, 그리고 실제 코드 실행 흐름까지 단계별로 살펴보았다.
이처럼 Interceptor는 요청 처리 흐름의 전반에 개입하여 공통 로직을 효율적으로 분리할 수 있는 핵심 요소이다.
특히 Controller를 수정하지 않고도 전처리와 후처리를 수행할 수 있다는 점에서 실무에서 활용도가 매우 높다.
Spring MVC의 전체 흐름 속에서 Interceptor의 위치를 이해해두면, 보다 구조적인 설계가 가능해진다.
다음 글에서는 Servlet과 Handler에 대해 좀 더 자세히 살펴보도록 하겠다.