지금까지 두 편에 걸쳐 스프링 필터와 인터셉터를 각각 살펴봤습니다. 둘 다 "요청이 컨트롤러에 도착하기 전에 공통 작업을 처리한다"는 점은 같은데, 그렇다면 실무에서는 어떤 상황에 무엇을 써야 할까요? 이번 포스팅에서는 두 도구를 나란히 놓고 비교하면서 선택 기준을 정리해보도록 하겠습니다.
※ 스프링 필터와 인터셉터의 정의가 궁금하시다면 아래 글을 참고해 주세요.
[Spring] 스프링 필터(Filter)란? 개념과 사용법, 등록 방법 총정리
[Spring] 스프링 인터셉터(Interceptor)란? 개념과 사용법, 등록 방법 총정리
스프링 필터, 인터셉터 요약 정리

앞의 글을 읽지 않으신 분들을 위해 핵심만 짚고 가자면 먼저 필터는 자바 서블릿 표준 스펙입니다. 톰캣 같은 서블릿 컨테이너가 관리하고, 스프링 바깥에서 동작합니다. 그래서 정적 리소스 요청까지 포함한 모든 요청을 거치죠. doFilter() 안에서 chain.doFilter()를 직접 호출해야 다음 단계로 넘어가고, 스프링 빈을 쓰려면 약간의 우회가 필요합니다.
인터셉터는 스프링 MVC가 제공하는 기능입니다. DispatcherServlet 안에서 동작하며, 어떤 컨트롤러로 갈지 정해진 요청만 거칩니다. preHandle()의 반환값으로 통과 여부를 결정하고, 스프링 빈이기 때문에 다른 빈을 자유롭게 주입받을 수 있습니다. 컨트롤러 전후로 preHandle, postHandle, afterCompletion 세 지점에 끼어들 수 있습니다.
스프링 필터, 인터셉터 비교표

표를 보면 차이가 꽤 많아 보이지만, 사실 대부분은 "스프링 바깥이냐 안이냐"라는 한 가지 차이에서 파생된 것입니다. 아래에서 스프링 필터와 인터셉터의 차이점을 하나씩 나열해 보겠습니다.
스프링 필터와 인터셉터 차이점
누가 만들었고 누가 관리하는가
필터는 자바 EE(지금은 Jakarta EE) 서블릿 스펙에 정의된 표준입니다. 스프링이 없어도, 심지어 다른 프레임워크를 써도 필터는 그대로 동작합니다. 반면 인터셉터는 스프링 MVC가 자체적으로 만든 기능이라 스프링 밖에서는 존재하지 않습니다. 관리 주체도 다릅니다. 필터는 서블릿 컨테이너가 생성하고 관리하기 때문에 스프링 컨테이너 입장에서는 "바깥 세상"입니다. 인터셉터는 스프링 빈이라 스프링이 직접 생성하고 의존성을 주입해 줍니다.
적용 대상 범위
필터는 서블릿 컨테이너로 들어오는 모든 요청을 거칩니다. /css/style.css 같은 정적 파일 요청도, 존재하지 않는 URL로 들어온 404 요청도 필터는 통과합니다. 인터셉터는 DispatcherServlet이 "이 요청은 이 컨트롤러로 보내야겠다"고 핸들러 매핑에 성공한 요청만 거칩니다. 그래서 필터는 전체 집합, 인터셉터는 그 안의 부분 집합이라고 보시면 됩니다. 요청 수를 세는 전역 카운터나 모든 응답에 공통 헤더를 붙이는 작업처럼 "빠짐없이 전부"가 중요하다면 필터가 맞고, "API 요청만" 다루면 된다면 인터셉터로 충분합니다.
다음 단계로 넘기는 방식
필터는 chain.doFilter(request, response)를 개발자가 직접 호출해야 다음 필터나 서블릿으로 요청이 넘어갑니다. 이 호출을 빠뜨리면 요청이 그 자리에서 멈춥니다. 반면 인터셉터는 preHandle()에서 true만 반환하면 스프링이 알아서 다음 인터셉터와 컨트롤러를 호출해 줍니다. 필터 방식은 실수 여지가 있지만 그만큼 자유도가 높습니다. chain.doFilter()를 호출하기 전에 요청을 가공하고, 호출한 뒤에 응답을 가공하는 것을 한 메서드 안에서 할 수 있기 때문입니다. 인터셉터 방식은 안전하지만 전처리와 후처리가 각각 다른 메서드로 나뉘어 있어서, 둘 사이에 값을 공유하려면 request.setAttribute() 같은 수단을 써야 합니다.
스프링 빈 주입
인터셉터는 스프링 빈이라 생성자 주입이든 필드 주입이든 평소처럼 쓰면 됩니다. 필터는 서블릿 컨테이너가 관리하기 때문에 원칙적으로는 스프링 빈을 모릅니다. 그래서 스프링은 DelegatingFilterProxy라는 다리를 제공합니다. 서블릿 컨테이너에는 이 프록시만 등록해 두고, 실제 요청이 오면 프록시가 스프링 컨테이너에서 진짜 필터 빈을 찾아 일을 넘기는 구조입니다. 스프링 시큐리티의 필터 체인이 바로 이 방식으로 동작합니다.
다만 스프링 부트에서는 이 부분이 많이 편해졌습니다. FilterRegistrationBean으로 등록하는 필터는 사실상 스프링 빈처럼 다룰 수 있어서, 생성자에서 다른 빈을 받아도 문제없습니다. 그래서 "필터는 빈 주입이 안 된다"는 말은 요즘 기준으로는 "조금 번거롭다" 정도로 이해하시면 되겠습니다.
핸들러 정보 접근
이것은 인터셉터만의 강점입니다. preHandle()의 세 번째 파라미터 handler에는 실제로 호출될 컨트롤러 메서드 정보가 들어 있습니다. 덕분에 "이 메서드에 @AdminOnly 어노테이션이 붙어 있는가?" 같은 질문에 답할 수 있습니다.
if (handler instanceof HandlerMethod handlerMethod) {
if (handlerMethod.hasMethodAnnotation(AdminOnly.class)) {
// 관리자 권한 확인
}
}
필터는 DispatcherServlet 앞에서 동작하기 때문에 이 요청이 어떤 컨트롤러로 갈지 아직 결정되지 않은 상태입니다. 그래서 핸들러 기반의 세밀한 제어는 필터에서는 불가능합니다.
Request/Response 객체 교체
이것은 반대로 필터만의 강점입니다. 필터는 chain.doFilter(request, response)를 호출할 때 다른 객체를 넘길 수 있습니다. 원래 요청 객체를 래퍼로 감싸서 넘기면, 그 뒤의 모든 단계는 래퍼를 원본인 줄 알고 사용합니다.
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 원본 대신 래퍼를 넘김
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(wrappedRequest, wrappedResponse);
// 이제 요청 본문과 응답 본문을 읽을 수 있음
String requestBody = new String(wrappedRequest.getContentAsByteArray());
String responseBody = new String(wrappedResponse.getContentAsByteArray());
wrappedResponse.copyBodyToResponse(); // 응답을 실제로 클라이언트에 전송
}
왜 이게 중요할까요? HTTP 요청 본문은 스트림이라서 한 번 읽으면 끝입니다. 로깅 목적으로 필터에서 본문을 읽어버리면 컨트롤러는 빈 본문을 받게 됩니다. 그래서 본문을 캐싱해 두는 래퍼로 감싸서 여러 번 읽을 수 있게 만드는 것인데, 이 작업은 객체를 교체할 수 있는 필터에서만 가능합니다.
인터셉터는 파라미터로 받은 request, response를 그대로 써야 하고 다음 단계로 다른 객체를 넘길 방법이 없습니다. 2편의 "postHandle에서 응답 본문을 수정하면 안 된다"는 이야기가 바로 여기서 나옵니다.
그래서 언제 뭘 써야 할까요?
차이를 전부 외울 필요는 없습니다. 한 문장으로 요약하면 이렇습니다. 스프링 바깥 영역까지 건드려야 하면 필터, 스프링이나 컨트롤러 정보가 필요하면 인터셉터입니다. 구체적인 용도로 정리하면 다음과 같습니다.

보통은 둘을 같이 씁니다
필터냐 인터셉터냐를 고르는 문제가 아니라, 역할을 나눠서 둘 다 쓰는 것이 일반적입니다. 실제 프로젝트에서 자주 보이는 조합을 예시로 말씀드리자면 아래와 같습니다.

이렇게 바깥쪽 필터에서는 "모든 요청에 공통으로, 스프링과 무관하게" 처리해야 하는 일을 하고, 안쪽 인터셉터에서는 "스프링 빈과 컨트롤러 정보가 필요한" 일을 합니다. 이렇게 역할을 나누면 각 컴포넌트가 작고 명확해지고, 나중에 하나를 바꿔도 다른 쪽에 영향을 주지 않습니다.
참고로 스프링 시큐리티를 쓴다면 인증과 인가는 대부분 시큐리티의 필터 체인이 담당합니다. 그 경우 인터셉터에는 비즈니스 수준의 권한 체크나 로깅 정도만 남게 됩니다. 시큐리티를 쓰지 않는 가벼운 프로젝트라면 위 조합처럼 인터셉터로 직접 인증을 구현하는 경우가 많습니다.
자주 하는 실수와 해결법
인터셉터에서 요청 본문을 로깅하려다 컨트롤러가 빈 본문을 받는 경우
인터셉터의 preHandle()에서 request.getInputStream()으로 본문을 읽으면 스트림이 소비되어 컨트롤러의 @RequestBody가 아무것도 받지 못합니다. 본문을 읽어야 한다면 필터에서 ContentCachingRequestWrapper로 감싸서 넘긴 뒤, 인터셉터나 컨트롤러에서 getContentAsByteArray()로 읽어야 합니다. 즉 "래핑은 필터, 읽기는 그 이후 어디서든"입니다.
postHandle에서 응답 본문을 수정하려는 경우
REST API에서는 postHandle()이 호출될 때 이미 응답 본문이 쓰여 있습니다. 응답을 가공하려면 두 가지 방법이 있습니다. 응답 객체 수준에서 가공하려면 필터에서 ContentCachingResponseWrapper를 쓰고, 컨트롤러가 반환한 객체 수준에서 가공하려면 ResponseBodyAdvice를 구현합니다. 인터셉터는 어느 쪽에도 맞지 않습니다.
필터에서 핸들러 메서드의 어노테이션을 읽으려는 경우
필터는 어떤 컨트롤러로 갈지 모르는 시점에 동작하므로 불가능합니다. 어노테이션 기반 제어가 필요하면 인터셉터로 옮겨야 합니다.
인증 로직을 필터에 두고 빈 주입이 안 돼서 고생하는 경우
스프링 부트라면 FilterRegistrationBean으로 등록하고 생성자 주입을 쓰면 됩니다. 하지만 애초에 인증은 인터셉터가 더 자연스러운 자리입니다. 로그인 제외 경로를 excludePathPatterns()로 관리하기도 쉽고, request.setAttribute()로 사용자 정보를 컨트롤러에 넘기기도 편합니다.
정리하면
- 필터는 서블릿 표준이고 스프링 바깥에서, 인터셉터는 스프링 MVC 기능이고 스프링 안에서 동작합니다. 대부분의 차이는 이 위치 차이에서 나옵니다.
- 필터는 모든 요청을, 인터셉터는 컨트롤러로 가는 요청만 거칩니다.
- 필터는
chain.doFilter()를 직접 호출하고, 인터셉터는true를 반환하면 스프링이 알아서 넘깁니다. - 필터만 할 수 있는 것: Request/Response 객체 교체 (본문 캐싱, 인코딩, 래핑).
- 인터셉터만 할 수 있는 것: 핸들러 정보 접근 (어노테이션 기반 권한 제어).
- 인코딩, CORS, Trace ID, 본문 래핑은 필터로. 인증, 인가, 컨트롤러 단위 로깅은 인터셉터로.
- 실무에서는 둘 중 하나를 고르는 것이 아니라 역할을 나눠 함께 씁니다.
이것으로 스프링 필터와 인터셉터 시리즈를 마칩니다. 비슷해 보이는 두 도구지만 서 있는 위치가 다르고, 그 위치가 할 수 있는 일을 결정한다는 점만 기억하시면 어떤 상황에서도 어렵지 않게 선택하실 수 있을 것입니다.
'ETC. > Spring' 카테고리의 다른 글
| [Spring] 스프링 인터셉터(Interceptor)란? 개념과 사용법, 등록 방법 총정리 (1) | 2026.10.03 |
|---|---|
| [Spring] 스프링 필터(Filter)란? 개념과 사용법, 등록 방법 총정리 (2) | 2026.10.01 |
| [Spring] 스프링 JPA란 무엇인가? - 동작 원리와 처리 흐름 정리 (1) | 2025.11.19 |
| [Spring] 스프링 MyBatis란 무엇인가? - 동작 원리와 처리 흐름 정리 (1) | 2025.11.18 |