| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- Vue+Typescript
- frontend
- TCP/IP
- 추천
- 인생꿀팁
- DI
- 이벤트드리븐
- KafkaFollower
- 카프카실무
- Kafka순서보장
- enum
- 백엔드개발
- kafka
- ECMAScript
- GC
- 타입스크립트
- 의존성주입
- TypeScript
- 자바스크립트
- 카프카설계
- springboot
- vue store
- 멱등성Producer
- 실천방법
- Javascript
- MSA
- KafkaOrdering
- 자기계발
- HTTP란
- webpack
- Today
- Total
끄적끄적
Spring Boot Filter vs Interceptor — 실무에서 겪은 차이점 완전 정리 (코드 예제 포함) 본문
Spring Boot Filter vs Interceptor — 실무에서 겪은 차이점 완전 정리 (코드 예제 포함)
mashko 2026. 2. 25. 21:54Filter vs Interceptor
직접 구현해보며 깨달은 차이점 · 동작 원리 · 언제 뭘 써야 하는지 완전 정리
📋 목차
- 들어가며 — 실무에서 겪은 혼란
- 요청 처리 흐름으로 보는 전체 그림
- Filter — 서블릿 레벨에서 동작하는 문지기
- Interceptor — 스프링 레벨에서 동작하는 관리자
- 핵심 차이 비교표
- 실제 코드로 구현해보기
- 언제 Filter를, 언제 Interceptor를 쓸까?
- 삽질했던 실수들 (주의사항)
들어가며 — 실무에서 겪은 혼란
처음 스프링 시큐리티 없이 직접 인증 처리를 구현해야 했을 때의 일입니다. JWT 토큰 검증 로직을 어디에 넣어야 할지 고민이 됐어요. 필터에 넣어야 하나, 인터셉터에 넣어야 하나.
구글링을 해봤지만 "둘 다 요청을 가로챈다"는 이야기만 있고, 실제로 왜 다르게 동작하는지를 명확히 설명하는 글이 없었습니다. 그냥 "인증은 필터, 로깅은 인터셉터" 라고만 하는데 — 왜? 라는 질문에 답을 못 찾았죠.
필터와 인터셉터가 각각 어느 레이어에서 동작하는지를 이해하면, 언제 뭘 써야 하는지는 자연스럽게 따라옵니다.
요청 처리 흐름으로 보는 전체 그림
가장 먼저 알아야 할 건 요청이 컨트롤러에 도달하기까지의 경로입니다. 필터와 인터셉터는 이 경로 위의 서로 다른 지점에 위치합니다.
(javax/jakarta)
(Spring MVC 진입점)
(Spring Context)
Filter는 DispatcherServlet 앞에서 동작합니다. 즉, 스프링 MVC가 요청을 받기 전이에요.
Interceptor는 DispatcherServlet 뒤, 컨트롤러 앞에서 동작합니다. 이미 스프링 컨텍스트 안으로 들어온 상태죠.
이 차이 하나가 모든 걸 설명합니다.
Filter — 서블릿 레벨의 문지기
Filter는 javax.servlet.Filter (Spring Boot 3부터는 jakarta.servlet.Filter) 인터페이스를 구현합니다. 스프링과 완전히 독립된 서블릿 컨테이너 레벨에서 동작하기 때문에, 스프링 빈을 사용할 수도 있지만 본질적으로는 서블릿 스펙입니다.
@Component
public class JwtAuthFilter implements Filter {
@Autowired
private JwtTokenProvider tokenProvider;
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Authorization");
if (token != null && tokenProvider.validateToken(token)) {
// SecurityContext에 인증 정보 세팅
Authentication auth = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
// 다음 필터로 넘김 (이걸 안 하면 요청이 멈춤!)
chain.doFilter(request, response);
}
}
Filter의 가장 중요한 특징은 HttpServletRequest와 HttpServletResponse를 직접 조작할 수 있다는 점입니다. 요청/응답 본문을 래핑하거나, 헤더를 추가하거나, 심지어 완전히 다른 응답을 써버릴 수도 있어요.
Interceptor — 스프링 레벨의 관리자
Interceptor는 HandlerInterceptor 인터페이스를 구현합니다. DispatcherServlet이 요청을 받은 이후, 스프링 컨텍스트 안에서 동작하기 때문에 스프링의 모든 기능을 자유롭게 사용할 수 있습니다.
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private static final Logger log =
LoggerFactory.getLogger(LoggingInterceptor.class);
// 컨트롤러 실행 전
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler // ← 어떤 컨트롤러 메서드인지 알 수 있음!
) throws Exception {
log.info("[요청] {} {}",
request.getMethod(),
request.getRequestURI());
request.setAttribute("startTime", System.currentTimeMillis());
return true; // false 반환 시 컨트롤러 실행 안 됨
}
// 컨트롤러 실행 후, View 렌더링 전
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView
) throws Exception {
log.info("[컨트롤러 완료] handler={}", handler);
}
// 요청 완전 완료 후 (예외 발생 시에도 호출)
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex
) throws Exception {
long duration =
System.currentTimeMillis()
- (long) request.getAttribute("startTime");
log.info("[응답완료] {}ms | status={}",
duration, response.getStatus());
}
}
인터셉터의 가장 강력한 특징은 preHandle의 handler 파라미터입니다. 이걸 통해 어떤 컨트롤러의 어떤 메서드가 실행될 예정인지 알 수 있어요. 어노테이션 기반 권한 체크를 구현할 때 매우 유용합니다.
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private LoggingInterceptor loggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor)
.addPathPatterns("/api/**") // 이 경로만 인터셉터 적용
.excludePathPatterns("/api/auth/**"); // 인증 경로는 제외
}
}
핵심 차이 비교표
| 항목 | Filter | Interceptor |
|---|---|---|
| 동작 레벨 | 서블릿 컨테이너 (Tomcat) | 스프링 MVC (DispatcherServlet 이후) |
| 인터페이스 | jakarta.servlet.Filter |
HandlerInterceptor |
| 스프링 빈 접근 | 가능 (DelegatingFilterProxy 통해) | 자유롭게 가능 |
| 실행 시점 | DispatcherServlet 진입 전/후 | Controller 실행 전/후/완료 |
| 요청 정보 조작 | 직접 가능 (Wrapper 사용) | 제한적 (헤더 조작 등 일부 불가) |
| Handler 정보 접근 | 불가 | 가능 (어떤 컨트롤러 메서드인지 알 수 있음) |
| ModelAndView 접근 | 불가 | 가능 (postHandle) |
| 예외 처리 | 직접 처리 필요 | @ExceptionHandler와 연동 가능 |
| URL 패턴 설정 | @WebFilter or 빈 등록 |
addPathPatterns()로 유연하게 |
언제 Filter를, 언제 Interceptor를?
🔒 Filter 를 써야 할 때
🎯 Interceptor 를 써야 할 때
인증/보안 → Filter (Spring Security 체인과 동일 레벨에서 처리)
비즈니스 로직 관련 공통 처리 → Interceptor (스프링 빈 자유롭게 사용 가능)
Request Body를 읽어야 하면 → 반드시 Filter에서 ContentCachingRequestWrapper 사용
직접 삽질한 실수들 & 주의사항
request.getInputStream()은 한 번만 읽을 수 있습니다. 필터에서 이미 읽었다면 인터셉터에서는 빈 값이 나와요. 해결책은 Filter에서 ContentCachingRequestWrapper로 래핑해서 여러 번 읽을 수 있게 만드는 것입니다.
Filter는 서블릿 레벨이라 스프링의 트랜잭션 AOP가 정상 동작하지 않는 경우가 있습니다. DB 작업이 필요하다면 Interceptor로 옮기거나, 서비스 레이어에서 처리하세요.
preHandle()에서 false를 반환하면 컨트롤러가 실행되지 않습니다. 이때 반드시 response에 적절한 응답을 써줘야 합니다. 그냥 false만 반환하면 클라이언트는 빈 응답을 받게 돼요.
// Filter에서 RequestBody를 여러 번 읽을 수 있게 래핑
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 래핑하면 이후 어디서든 getContentAsByteArray()로 body 읽기 가능
ContentCachingRequestWrapper wrappedRequest =
new ContentCachingRequestWrapper((HttpServletRequest) req);
ContentCachingResponseWrapper wrappedResponse =
new ContentCachingResponseWrapper((HttpServletResponse) res);
chain.doFilter(wrappedRequest, wrappedResponse);
// 응답 완료 후 body 내용 로깅
byte[] requestBody = wrappedRequest.getContentAsByteArray();
log.info("Request body: {}", new String(requestBody));
// 반드시 response body를 복사해줘야 실제 클라이언트에게 전달됨!
wrappedResponse.copyBodyToResponse();
}
☕ "필터는 건물 입구 보안요원, 인터셉터는 내부 안내 데스크"
필터는 건물(서블릿 컨테이너)에 들어오기 전 보안 검사를 담당하고,
인터셉터는 건물 안에서 목적지(컨트롤러)에 가기 전 안내와 통제를 담당합니다.
둘의 위치를 이해하면 어디에 뭘 넣어야 할지 자연스럽게 결정됩니다 🎯
질문이나 다른 경험이 있으시면 댓글로 공유해 주세요!
'Back-end > Java' 카테고리의 다른 글
| 서버가 새벽마다 OOM으로 죽었다 — Java 메모리 누수 패턴 7가지와 Heap Dump로 범인 잡는 법 (1) | 2026.02.26 |
|---|---|
| Kafka 실무 완전 정복 — Consumer Lag 폭증·메시지 중복·파티션 설계 실수 직접 겪고 해결한 이야기 (0) | 2026.02.25 |
| [SPOCK] Spock의 Mock, Stub, Spy에 대해서 알아보자 (0) | 2022.09.13 |
| [JAVA] @Transaction 전파 및 격리에 대한 정리 (0) | 2022.04.28 |
| gradle 환경에서 apache log4j 2.15.0 버전 업데이트하기 (0) | 2021.12.13 |
