컨트롤러마다 로그인 확인 코드를 붙이던 시절
Spring Filter와 Interceptor를 이해하면 컨트롤러에서 반복되는 인증 코드를 제거할 수 있다. 요청이 컨트롤러에 닿기 전에 가로채서 처리하는 두 가지 메커니즘의 차이와 실전 활용법을 정리한다.
로그인이 필요한 페이지가 늘어날수록 아래 코드가 컨트롤러마다 반복된다.
@GetMapping("/mypage")
public String mypage(HttpSession session) {
if (session.getAttribute("loginUser") == null) {
return "redirect:/login";
}
return "mypage";
}
@GetMapping("/orders")
public String orders(HttpSession session) {
if (session.getAttribute("loginUser") == null) { // 또 반복
return "redirect:/login";
}
return "orders";
}
페이지가 10개면 이 코드가 10번 복사된다. 로그인 확인 로직이 바뀌면 10곳을 전부 수정해야 한다. Filter와 Interceptor는 이 반복을 한 곳에서 처리하기 위해 존재한다.
Insight: 중복 인증 코드는 AOP가 해결하려는 문제와 같다. Filter와 Interceptor는 웹 요청 레이어에서 이 문제를 해결하는 전용 메커니즘이다.
요청 흐름 전체 그림
Filter와 Interceptor가 어디에 위치하는지 파악하는 것이 먼저다.
요청
↓
[Filter] ← Tomcat(서블릿 컨테이너) 레벨
↓
[DispatcherServlet]
↓
[Interceptor - preHandle] ← Spring MVC 레벨
↓
[Controller]
↓
[Interceptor - postHandle]
↓
[View 렌더링]
↓
[Interceptor - afterCompletion]
↓
[Filter]
↓
응답
Filter는 DispatcherServlet 앞에 있다. Spring이 뜨기 전, Tomcat 레벨에서 동작한다. Interceptor는 DispatcherServlet이 요청을 컨트롤러로 넘기기 직전에 끼어든다.
| Filter | Interceptor | |
| 동작 위치 | Tomcat (서블릿 컨테이너) | Spring (DispatcherServlet 내부) |
| 적용 대상 | 모든 요청 (정적 파일 포함) | Spring MVC가 처리하는 요청만 |
| Spring 빈 주입 | 불가 (직접 생성) | 가능 (@Autowired 등) |
| 주요 용도 | 인코딩, 보안 헤더, CORS | 로그인 체크, 로깅, 권한 검사 |
Insight: Spring Security가 Filter 레벨에서 동작하는 이유가 여기 있다. DispatcherServlet보다 앞에서 막아야 Spring MVC 예외 처리보다 먼저 작동하고, 정적 리소스 요청까지 통제할 수 있다.
Filter — chain.doFilter()가 핵심이다
Filter의 구조는 `doFilter()` 메서드 하나로 설명된다. `chain.doFilter()` 호출 전후로 코드를 나눠 넣으면 요청 전처리와 응답 후처리가 된다.
public class RequestLogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("요청 처리 전"); // 전처리
chain.doFilter(request, response); // 다음 필터 또는 서블릿으로 전달
System.out.println("응답 처리 후"); // 후처리
}
}
필터가 여러 개일 때는 체인 구조로 연결된다. MyFilter2 → MyFilter 순서로 등록하면 아래처럼 실행된다.
// 실행 순서
Filter2: 요청 전
Filter1: 요청 전
[서블릿/컨트롤러]
Filter1: 응답 후
Filter2: 응답 후
Spring Boot에서 필터를 등록하는 방법은 두 가지다. web.xml이 없으므로 Java 코드로 대신한다.
// 방법 1: @Component + @Order (전체 URL에 적용)
@Component
@Order(1)
public class RequestLogFilter implements Filter { ... }
// 방법 2: FilterRegistrationBean (URL 패턴, 순서 세밀하게 제어)
@Configuration
public class FilterRegistrar {
@Bean
public FilterRegistrationBean<RequestLogFilter> logFilter() {
FilterRegistrationBean<RequestLogFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new RequestLogFilter());
bean.addUrlPatterns("/api/*"); // 특정 경로만
bean.setOrder(1);
return bean;
}
}
URL 패턴을 지정해야 하거나 순서를 정밀하게 제어해야 할 때는 `FilterRegistrationBean`을 쓴다. 전체 요청에 적용되는 간단한 필터는 `@Component`가 편하다.
Insight: Filter는 Spring 빈을 주입받을 수 없다. `FilterRegistrationBean`에서 `new RequestLogFilter()`로 직접 생성하는 이유다. Spring Security는 이 제약을 우회하기 위해 별도의 `DelegatingFilterProxy` 구조를 사용한다.
Interceptor — 세 가지 실행 시점
Interceptor는 `HandlerInterceptor` 인터페이스를 구현한다. 세 메서드가 `default`로 선언되어 있어서 필요한 것만 골라 오버라이드하면 된다.
@Component
public class TraceInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 컨트롤러 실행 전
// false 반환 시 이후 진행 중단
System.out.println("Pre: " + request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
// 컨트롤러 실행 후, 뷰 렌더링 전
// 예외 발생 시 실행되지 않음
System.out.println("Post");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 뷰 렌더링 완료 후 (finally처럼 항상 실행)
// 예외 발생 시 ex 파라미터에 담겨 옴
System.out.println("After");
}
}
| 메서드 | 실행 시점 | 특이사항 |
preHandle |
컨트롤러 실행 전 | false 반환 시 이후 진행 중단 |
postHandle |
컨트롤러 실행 후, 뷰 렌더링 전 | 예외 발생 시 실행 안 됨 |
afterCompletion |
뷰 렌더링 완료 후 | 예외 발생해도 항상 실행 |
인터셉터가 여러 개 등록되면 Pre는 등록 순서대로, Post와 After는 역순으로 실행된다.
// 등록 순서: Alpha → Beta → Gamma
// 실행 결과:
Alpha:Pre → Beta:Pre → Gamma:Pre
→ [컨트롤러]
→ Gamma:Post → Beta:Post → Alpha:Post
→ Gamma:After → Beta:After → Alpha:After
Insight: Post와 After가 역순인 이유는 스택 구조다. 가장 나중에 들어간 인터셉터가 가장 먼저 나온다. 리소스 정리 같은 작업에서 중요하다 — 열린 순서 반대로 닫아야 안전하다.
실전: LoginInterceptor로 인증 처리 한 곳에서
컨트롤러마다 반복되던 세션 체크를 인터셉터 하나로 해결한다.
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
HttpSession session = request.getSession();
if (session.getAttribute("loginUser") == null) {
response.sendRedirect("/login");
return false; // 컨트롤러로 진행하지 않음
}
return true;
}
}
인터셉터를 등록할 때 URL 패턴을 지정한다. `WebMvcConfigurer`를 구현한 설정 클래스에서 처리한다.
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/**") // 모든 경로에 적용
.excludePathPatterns("/login"); // 로그인 페이지는 제외
}
}
`/**`로 전체 경로에 적용하고 `/login`만 제외했다. 로그인 페이지 자체를 막으면 무한 리다이렉트가 발생하기 때문이다. 인터셉터는 Spring 빈이라 `@Autowired`로 주입받을 수 있다.
Insight: 필터는 `new`로 직접 생성하고, 인터셉터는 `@Autowired`로 주입받는다. 이 차이가 동작 레이어의 차이를 그대로 보여준다. 인터셉터는 Spring 컨텍스트 안에 있으니 서비스 빈도 주입받아 DB 조회나 복잡한 권한 검사도 할 수 있다.
Q. Filter와 Interceptor 중 로그인 체크는 어디서 해야 하나?
둘 다 가능하지만 일반적으로 Interceptor가 적합하다. Spring 빈 주입이 가능해서 UserService 같은 서비스 계층과 연동하기 쉽고, URL 패턴 제외 설정도 간단하다. Filter 레벨의 인증이 필요하다면 Spring Security를 쓰는 것이 낫다.
Q. Listener는 Filter, Interceptor와 무엇이 다른가?
Listener는 요청마다 실행되지 않는다. 서버 시작 시 한 번, 서버 종료 시 한 번 실행된다. DB 커넥션 풀 초기화, 공통 설정값 로딩처럼 애플리케이션 생명주기에 맞춰 실행해야 하는 작업에 쓴다. `@WebListener` 또는 web.xml에 등록한다.
Q. excludePathPatterns에 정적 리소스 경로도 넣어야 하나?
Interceptor는 Spring MVC가 처리하는 요청에만 적용된다. `resources/static/` 폴더의 정적 파일은 Spring MVC를 거치지 않아서 자동으로 제외된다. 별도로 `excludePathPatterns("/css/**", "/js/**")`를 쓸 필요는 없다.
Filter는 Tomcat 레벨에서 모든 요청을 가로채고, Interceptor는 Spring MVC 레벨에서 컨트롤러 전후를 제어한다. 컨트롤러마다 반복되던 인증 코드는 `AuthInterceptor` 하나로 대체하고, 로그인 페이지만 `excludePathPatterns`로 제외하면 된다. 이 흐름을 이해하면 Spring Security가 왜 Filter 레벨에서 동작하는지도 자연스럽게 연결된다.
Environment: Windows 11, JDK 17, Spring Boot 3.4.4, VS Code