끄적끄적

Spring Boot Filter vs Interceptor — 실무에서 겪은 차이점 완전 정리 (코드 예제 포함) 본문

Back-end/Java

Spring Boot Filter vs Interceptor — 실무에서 겪은 차이점 완전 정리 (코드 예제 포함)

mashko 2026. 2. 25. 21:54
반응형
☕ Spring Boot Deep Dive

Filter vs Interceptor

직접 구현해보며 깨달은 차이점 · 동작 원리 · 언제 뭘 써야 하는지 완전 정리

☕ Spring Boot 3.x 기준 💻 Java 17+ ⏱ 읽기 약 15분 🔬 실무 경험 기반

📋 목차

  1. 들어가며 — 실무에서 겪은 혼란
  2. 요청 처리 흐름으로 보는 전체 그림
  3. Filter — 서블릿 레벨에서 동작하는 문지기
  4. Interceptor — 스프링 레벨에서 동작하는 관리자
  5. 핵심 차이 비교표
  6. 실제 코드로 구현해보기
  7. 언제 Filter를, 언제 Interceptor를 쓸까?
  8. 삽질했던 실수들 (주의사항)
📖 Section 01

들어가며 — 실무에서 겪은 혼란

처음 스프링 시큐리티 없이 직접 인증 처리를 구현해야 했을 때의 일입니다. JWT 토큰 검증 로직을 어디에 넣어야 할지 고민이 됐어요. 필터에 넣어야 하나, 인터셉터에 넣어야 하나.

구글링을 해봤지만 "둘 다 요청을 가로챈다"는 이야기만 있고, 실제로 왜 다르게 동작하는지를 명확히 설명하는 글이 없었습니다. 그냥 "인증은 필터, 로깅은 인터셉터" 라고만 하는데 — 왜? 라는 질문에 답을 못 찾았죠.

💡
이 글은 그 "왜?"에 대한 답입니다.
필터와 인터셉터가 각각 어느 레이어에서 동작하는지를 이해하면, 언제 뭘 써야 하는지는 자연스럽게 따라옵니다.
· · ·
🔄 Section 02

요청 처리 흐름으로 보는 전체 그림

가장 먼저 알아야 할 건 요청이 컨트롤러에 도달하기까지의 경로입니다. 필터와 인터셉터는 이 경로 위의 서로 다른 지점에 위치합니다.

HTTP Request
Filter Chain
(javax/jakarta)
DispatcherServlet
(Spring MVC 진입점)
HandlerInterceptor
(Spring Context)
Controller
← 서블릿 컨테이너 영역 ──────────────────── Spring 애플리케이션 컨텍스트 영역 →
🔑
핵심 포인트:
Filter는 DispatcherServlet 앞에서 동작합니다. 즉, 스프링 MVC가 요청을 받기 전이에요.
Interceptor는 DispatcherServlet 뒤, 컨트롤러 앞에서 동작합니다. 이미 스프링 컨텍스트 안으로 들어온 상태죠.
이 차이 하나가 모든 걸 설명합니다.
· · ·
🔒 Section 03

Filter — 서블릿 레벨의 문지기

Filter는 javax.servlet.Filter (Spring Boot 3부터는 jakarta.servlet.Filter) 인터페이스를 구현합니다. 스프링과 완전히 독립된 서블릿 컨테이너 레벨에서 동작하기 때문에, 스프링 빈을 사용할 수도 있지만 본질적으로는 서블릿 스펙입니다.

 
 
 
JwtAuthFilter.java
Java
@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);
    }
}
⚠️
실무 삽질 포인트: chain.doFilter()를 반드시 호출해야 합니다. 이걸 빠뜨리면 요청이 거기서 멈춰버려요. 처음에 이걸 빠뜨려서 모든 API가 응답이 없는 상황을 겪었습니다 😅

Filter의 가장 중요한 특징은 HttpServletRequestHttpServletResponse직접 조작할 수 있다는 점입니다. 요청/응답 본문을 래핑하거나, 헤더를 추가하거나, 심지어 완전히 다른 응답을 써버릴 수도 있어요.

· · ·
🎯 Section 04

Interceptor — 스프링 레벨의 관리자

Interceptor는 HandlerInterceptor 인터페이스를 구현합니다. DispatcherServlet이 요청을 받은 이후, 스프링 컨텍스트 안에서 동작하기 때문에 스프링의 모든 기능을 자유롭게 사용할 수 있습니다.

 
 
 
LoggingInterceptor.java
Java
@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());
    }
}

인터셉터의 가장 강력한 특징은 preHandlehandler 파라미터입니다. 이걸 통해 어떤 컨트롤러의 어떤 메서드가 실행될 예정인지 알 수 있어요. 어노테이션 기반 권한 체크를 구현할 때 매우 유용합니다.

 
 
 
WebMvcConfig.java — 인터셉터 등록
Java
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoggingInterceptor loggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor)
            .addPathPatterns("/api/**")       // 이 경로만 인터셉터 적용
            .excludePathPatterns("/api/auth/**"); // 인증 경로는 제외
    }
}
· · ·
⚖️ Section 05

핵심 차이 비교표

항목 Filter Interceptor
동작 레벨 서블릿 컨테이너 (Tomcat) 스프링 MVC (DispatcherServlet 이후)
인터페이스 jakarta.servlet.Filter HandlerInterceptor
스프링 빈 접근 가능 (DelegatingFilterProxy 통해) 자유롭게 가능
실행 시점 DispatcherServlet 진입 전/후 Controller 실행 전/후/완료
요청 정보 조작 직접 가능 (Wrapper 사용) 제한적 (헤더 조작 등 일부 불가)
Handler 정보 접근 불가 가능 (어떤 컨트롤러 메서드인지 알 수 있음)
ModelAndView 접근 불가 가능 (postHandle)
예외 처리 직접 처리 필요 @ExceptionHandler와 연동 가능
URL 패턴 설정 @WebFilter or 빈 등록 addPathPatterns()로 유연하게
· · ·
🎯 Section 06

언제 Filter를, 언제 Interceptor를?

🔒 Filter 를 써야 할 때

서블릿 레벨 처리가 필요한 경우
JWT/OAuth 토큰 인증 — Spring Security도 내부적으로 필터 체인 사용
CORS 처리 — 실제 요청 전 Preflight 포함 전체 처리 필요
Request/Response Body 로깅 — InputStream을 래핑해야 하므로 필터에서만 가능
멀티파트 요청 처리 — 서블릿 레벨 파싱 필요
XSS 방어, SQL 인젝션 필터링 — 입력값 전처리
압축/인코딩 처리 — 응답 바디 gzip 압축 등

🎯 Interceptor 를 써야 할 때

스프링 컨텍스트 활용이 필요한 경우
API 실행 시간 측정 — preHandle/afterCompletion 조합
어노테이션 기반 권한 체크 — handler에서 메서드 어노테이션 읽기
로그인 여부 확인 — 세션 기반 로그인 체크 (JWT는 필터 추천)
API 호출 감사 로그 — 어떤 컨트롤러가 실행됐는지 기록
공통 모델 데이터 주입 — postHandle에서 ModelAndView 조작
Rate Limiting — 스프링 빈(Redis 등)과 연동 시
🚀
실무 경험에서 나온 결론:
인증/보안 → Filter (Spring Security 체인과 동일 레벨에서 처리)
비즈니스 로직 관련 공통 처리 → Interceptor (스프링 빈 자유롭게 사용 가능)
Request Body를 읽어야 하면 → 반드시 Filter에서 ContentCachingRequestWrapper 사용
· · ·
⚠️ Section 07

직접 삽질한 실수들 & 주의사항

🔴
실수 1 — 인터셉터에서 Request Body 읽으려다 빈 값
request.getInputStream()은 한 번만 읽을 수 있습니다. 필터에서 이미 읽었다면 인터셉터에서는 빈 값이 나와요. 해결책은 Filter에서 ContentCachingRequestWrapper로 래핑해서 여러 번 읽을 수 있게 만드는 것입니다.
🔴
실수 2 — Filter에 @Transactional 걸기
Filter는 서블릿 레벨이라 스프링의 트랜잭션 AOP가 정상 동작하지 않는 경우가 있습니다. DB 작업이 필요하다면 Interceptor로 옮기거나, 서비스 레이어에서 처리하세요.
🟡
주의 — 인터셉터 preHandle에서 false 반환
preHandle()에서 false를 반환하면 컨트롤러가 실행되지 않습니다. 이때 반드시 response에 적절한 응답을 써줘야 합니다. 그냥 false만 반환하면 클라이언트는 빈 응답을 받게 돼요.
 
 
 
ContentCachingRequestWrapper 패턴
Java
// 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();
}

☕ "필터는 건물 입구 보안요원, 인터셉터는 내부 안내 데스크"

필터는 건물(서블릿 컨테이너)에 들어오기 전 보안 검사를 담당하고,
인터셉터는 건물 안에서 목적지(컨트롤러)에 가기 전 안내와 통제를 담당합니다.
둘의 위치를 이해하면 어디에 뭘 넣어야 할지 자연스럽게 결정됩니다 🎯

질문이나 다른 경험이 있으시면 댓글로 공유해 주세요!

#스프링부트 #SpringBoot #Filter #Interceptor #필터인터셉터차이 #백엔드개발 #자바개발 #JWT인증 #스프링MVC #개발블로그
반응형
Comments