본문으로 건너뛰기
김신건의 로그

[Java] Servlet Filter

· 수정 · 📖 약 2분 · 797자/단어 #java #servlet #filter #http
Servlet Filter, Filter, FilterChain, @WebFilter, OncePerRequestFilter, FilterRegistrationBean, 서블릿 필터

정의

Filter 는 servlet 호출 전/후 에 끼어들어 요청과 응답을 가로채는 컴포넌트. 인증, 로깅, 압축, CORS, 인코딩 변환 같은 횡단 관심사 (cross-cutting concern) 를 servlet 코드 밖에서 처리할 때 쓴다.

여러 filter 가 체인 (chain) 으로 연결돼 순차 실행된다. 마지막 filter 가 chain.doFilter(req, resp) 를 호출하면 비로소 매핑된 servlet 의 service() 가 실행된다.

HandlerInterceptor 와 다른 점: 컨테이너 레벨 (servlet 보다 바깥) 에서 동작. Spring 의 DispatcherServlet 진입 전후 모두를 감쌀 수 있다.

Filter 인터페이스

import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.*;
import java.io.IOException;

@WebFilter("/api/*")
public class RequestLoggingFilter implements Filter {

    @Override
    public void init(FilterConfig config) {
        // 컨테이너 시작 시 1회
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
            throws IOException, ServletException {
        long start = System.nanoTime();
        HttpServletRequest http = (HttpServletRequest) req;

        try {
            chain.doFilter(req, resp);   // ← 다음 filter / servlet 으로 진행
        } finally {
            long ms = (System.nanoTime() - start) / 1_000_000;
            log.info("{} {} {}ms", http.getMethod(), http.getRequestURI(), ms);
        }
    }

    @Override
    public void destroy() { /* 정리 */ }
}

JDK 8+ 에서 init / destroy 는 default 구현이 있어 생략 가능. 핵심은 doFilter 하나.

체인 진입/종료가 어떤 순서로 일어나는지:

java
@WebFilter(urlPatterns = "/*", filterName = "A")
public class FilterA implements Filter {
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
          throws IOException, ServletException {
      System.out.println("A pre");
      chain.doFilter(req, resp);
      System.out.println("A post");
  }
}

@WebFilter(urlPatterns = "/*", filterName = "B")
public class FilterB implements Filter {
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
          throws IOException, ServletException {
      System.out.println("B pre");
      chain.doFilter(req, resp);
      System.out.println("B post");
  }
}

// Servlet
protected void doGet(...) { System.out.println("servlet"); }
log
A pre
B pre
servlet
B post
A post

chain.doFilter 호출 전후가 명확하게 양파처럼 감싸진다.

시각화

양파 (onion) 구조. Filter A 의 chain.doFilter 호출이 Filter B 진입, Servlet 처리 후 역순으로 복귀.

chain.doFilter 의 의미

Request 흐름:

[Filter1.doFilter pre]
  ↓ chain.doFilter
  [Filter2.doFilter pre]
    ↓ chain.doFilter
    [Servlet.service]

  [Filter2.doFilter post]

[Filter1.doFilter post]

Response

chain.doFilter 호출 전 = 요청 전처리, 호출 후 = 응답 후처리. 호출 안 하면 체인 중단 = servlet 까지 도달하지 않음. 인증 실패 처리에 사용:

if (!isAuthenticated(http)) {
    ((HttpServletResponse) resp).sendError(401);
    return;   // chain.doFilter 호출 안 함
}
chain.doFilter(req, resp);

등록 방법

1. @WebFilter annotation

@WebFilter(urlPatterns = "/api/*", initParams = @WebInitParam(name = "level", value = "DEBUG"))
public class LogFilter implements Filter { ... }

2. web.xml

<filter>
    <filter-name>logFilter</filter-name>
    <filter-class>com.example.LogFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>logFilter</filter-name>
    <url-pattern>/api/*</url-pattern>
</filter-mapping>

3. Spring Boot FilterRegistrationBean

@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean<LogFilter> logFilter() {
        FilterRegistrationBean<LogFilter> reg = new FilterRegistrationBean<>();
        reg.setFilter(new LogFilter());
        reg.addUrlPatterns("/api/*");
        reg.setOrder(1);    // 낮을수록 먼저
        return reg;
    }
}

순서 제어가 필요하면 FilterRegistrationBean 권장. @Component 로 등록하면 Spring Boot 가 자동 매핑하지만 순서를 제어하기 어렵다.

흔한 패턴

인증

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
    HttpServletRequest http = (HttpServletRequest) req;
    String token = http.getHeader("Authorization");
    if (token == null || !jwtVerifier.verify(token)) {
        ((HttpServletResponse) resp).sendError(401);
        return;
    }
    chain.doFilter(req, resp);
}

CORS

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
    HttpServletResponse http = (HttpServletResponse) resp;
    http.setHeader("Access-Control-Allow-Origin", "https://app.example.com");
    http.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
    http.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");

    if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
        http.setStatus(204);
        return;
    }
    chain.doFilter(req, resp);
}

문자 인코딩

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
    req.setCharacterEncoding("UTF-8");
    chain.doFilter(req, resp);
}

Spring Boot 는 CharacterEncodingFilter 가 기본 등록돼 있음.

응답 압축 / 캐싱 (wrapper)

응답을 가로채려면 HttpServletResponseWrapper 로 감싸기:

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
    ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper((HttpServletResponse) resp);
    chain.doFilter(req, wrapper);
    byte[] body = wrapper.getContentAsByteArray();
    // 로깅, 압축, 변환 ...
    wrapper.copyBodyToResponse();    // 실제 응답으로 복사
}

요청도 마찬가지: ContentCachingRequestWrapper 로 본문을 캐싱해야 logging filter + controller 가 모두 읽을 수 있다.

OncePerRequestFilter (Spring)

Servlet spec 에선 forward / include / async dispatch 시 filter chain 이 다시 실행 될 수 있다. 인증 filter 가 2 번 돌면 안 되는데.

Spring 의 OncePerRequestFilter 가 이 문제를 해결:

public class JwtFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain)
            throws ServletException, IOException {
        // 요청당 정확히 1회만 실행 보장
        ...
        chain.doFilter(req, resp);
    }
}

내부적으로 request attribute 에 alreadyFilteredAttributeName 플래그를 두고 중복 실행을 차단. Spring 환경에선 거의 항상 이걸 상속하는 게 안전.

Filter vs HandlerInterceptor vs AOP

항목FilterHandlerInterceptorAOP @Around
실행 위치servlet 컨테이너DispatcherServlet 내부Bean 메서드 호출
Spring context 접근등록 시 의존성 주입 가능Spring Bean 으로 자연 통합Spring Bean 만 적용
handler 정보 접근불가가능 (Object handler)proxy target 메서드
응답 변환 가능가능 (wrapper 로)postHandle 에서 ModelAndView메서드 반환값 변형
예외 처리직접 try/catchafterCompletion 의 exthrowing advice
적용 범위URL 패턴URL 패턴 + handlerpointcut (메서드/클래스)

규칙: HTTP 레이어 (인증, CORS, 압축, 로깅) = Filter, MVC handler 정보 필요 = Interceptor, 서비스 메서드 횡단 관심사 = AOP.

함정과 베스트 프랙티스

  • 항상 OncePerRequestFilter (Spring 환경): forward/include 중복 방지
  • 순서 제어 명시적으로: FilterRegistrationBean.setOrder() 또는 @Order
  • chain.doFilter 누락 방지: 중단 의도가 아니면 반드시 호출
  • try/finally 패턴: ThreadLocal set/remove, 시간 측정 등은 finally 에서
  • 응답 commit 후 헤더 변경 불가: filter 후처리에서 헤더 추가하려면 응답이 아직 commit 전이어야
  • 본문 wrapper 는 메모리 비용: 큰 body 전체 캐싱은 OOM 위험

관련 위키

이 글의 용어 (6개)
[Java] Servlet 생명주기java
정의 Servlet 은 컨테이너 (Tomcat / Jetty / Undertow) 가 생성, 호출, 소멸시키는 객체. 개발자는 인터페이스 메서드만 구현하고 인스턴스 관리는 일체 …
[Java] Servlet API: HttpServletRequest, HttpServletResponsejava
정의 Servlet 은 Java 진영의 표준 HTTP 처리 API. 컨테이너 (Tomcat / Jetty / Undertow) 가 HTTP 요청을 객체 ( ) 로 만들어 serv…
[Spring] AOP: @Aspect, Pointcut, Advicespring
정의 AOP (Aspect-Oriented Programming)는 cross-cutting concerns(로깅, 트랜잭션, 보안 등)를 비즈니스 코드에서 분리하는 기법. Sp…
[Spring] DispatcherServlet: Front Controller 내부spring
정의 가 / 사용법 을 다룬다면, 이 페이지는 그 모든 요청을 받아 처리하는 의 내부 메커니즘 을 다룬다. 은 본질적으로 하나의 Servlet (). Front Controlle…
[Spring] HandlerInterceptor: preHandle, postHandle, afterCompletionspring
정의 HandlerInterceptor 는 내부에서 handler (Controller 메서드) 실행 전후 에 끼어드는 Spring MVC 전용 메커니즘. Filter 와 비슷하…
[Spring] Security: Filter Chain, JWT, OAuth2spring
정의 Spring Security는 인증·인가의 사실상 표준. 서블릿 필터 체인 위에 다양한 인증/권한 처리를 모듈식으로. 6.0+ 부터 Lambda DSL 표준, Bean 방식…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기