서블릿 필터(Servlet Filter)와 스프링 인터셉터(Spring Interceptor)는 왜 사용하는걸까?
만약 우리가 개발을 할 때 로그인 여부를 체크하는 로직을 모든 컨트롤러 쪽에 작성을 한다면 작업이 많이 번거로울 것이다.
이렇게 모든 컨트롤러 로직에서 공통으로 관심이 있는 것을 공통 관심사(cross-cutting concern)라고 한다.
이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만,
웹과 관련된 공통 관심사는 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다.
웹과 관련된 공통 관심사를 처리할 때는 HTTP Header나 URL의 정보들이 필요한데, 서블릿 필터와 스프링 인터셉터는 HttpServletRequest를 제공하고 있어 편리하게 공통 관심사를 처리할 수 있다.
결론적으로 서블릿 필터와 스프링 인터셉터는 웹과 관련된 공통 관심사를 처리할 때 사용한다.
서블릿 필터(Servlet Filter)란?
필터는 서블릿이 지원하는 수문장과 같은 것이다.
필터의 흐름은 아래와 같다.
HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 컨트롤러
필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출이 된다.
필터는 특정 URL 패턴에 적용할 수 있는데 만약 /* 이렇게 설정을 하면 모든 요청에 필터가 적용된다.
만약 필터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다.
HTTP 요청 -> WAS -> 필터
필터는 체인으로 구성되어 중간에 필터를 자유롭게 추가할 수 있다.
예를 들어 로그를 남기는 필터 적용 후 그 다음에 로그인 여부를 체크하는 필터를 만들어서 적용할 수 있다.
HTTP 요청 -> WAS -> 필터A -> 필터B -> 필터C -> 디스패처 서블릿 -> 컨트롤러
필터 인터페이스(Filter) 살펴보기
서블릿 필터를 사용하려면 Filter 인터페이스를 구현하면 되는데,
필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
Filter 인터페이스 코드를 살펴보면 아래와 같다.
package javax.servlet;
import java.io.IOException;
public interface Filter {
public default void init(FilterConfig filterConfig) throws ServletException {}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException;
public default void destroy() {}
}
각각의 메서드를 정리하자면
- init() : 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출된다.
- doFilter() : 실제 필터로 동작하는 부분으로 필터의 로직을 구현하면 된다. 요청이 올 때 마다 해당 메서드가 호출된다.
- destroy() : 필터 종료 메서드로 서블릿 컨테이너가 종료될 때 호출된다.
서블릿 필터 사용해보기
요청 로그를 남기고 로그인 여부를 체크하는 서블릿 필터를 만들어서 등록해보자
필터를 구현할 때 주의해야할 점은 javax.servlet.Filter 인터페이스를 구현해야 하며,
doFilter()를 꼭 호출해야한다. 해당 메서드를 호출하지 않으면 다음 단계로 진행이 되지 않는다.
스프링 부트를 사용하고 있어 FilterRegistrationBean을 사용해서 등록한다.
- setFilter(new xxxFilter()): 등록할 필터 지정
- serOrder(1): 필터는 체인으로 동작하기 때문에 순서가 필요하여 순서를 지정, 숫자가 낮을 수록 먼저 동작한다.
- addUrlPatterns("/*"): 필터를 적요할 URL 패턴 지정, 한번에 여러 패턴을 지정할 수 있다.
LogFilter(로그 필터)
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("REQUEST [{}][{}]", uuid, requestURI);
chain.doFilter(request, response); // 다음 필터가 있으면 다음 필터를 호출, 없으면 서블릿 호출
} catch (Exception e) {
throw e;
} finally {
log.info("RESPONSE [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
LoginCheckFilter(로그인 인증 체크 필터)
미인증 사용자가 로그인 페이지가 아닌 로그인을 한 다음에 진입이 가능한 페이지에 직접 접근했을 때
해당 필터에서 진입했던 URL을 포함하여 로그인 페이지로 redirect 시킨다.
해당 URL을 가지고 가서 컨트롤러에서 로그인 처리 후에 진입하려고 했던 페이지로 이동시키기 위함이다.
import hello.login.web.session.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.PatternMatchUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
@Slf4j
public class LoginCheckFilter implements Filter {
private static final String[] whitelist = {"/", "/members/add", "/login", "/logout", "/css/*"};
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
HttpServletResponse httpResponse = (HttpServletResponse) response;
try {
log.info("인증 체크 필터 시작 {}", requestURI);
if (isLoginCheckPath(requestURI)) {
log.info("인증 체크 로직 실행 {}", requestURI);
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청 {}", requestURI);
// 현재 페이지 정보도 같이 넘겨서 로그인 한 다음에 해당 페이지로 이동시킬 수 있도록 requestURI도 포함하여 리다이렉트 시킴
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return; // 미인증 사용자는 다음으로 진행하지 않고 여기서 끝.
}
}
filterChain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* 화이트 리스트의 경우 인증 체크 안함
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
WebConfig에 필터 등록하기
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import hello.login.web.interceptor.LogInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1); // 필터가 체인으로 동작하기 때문에 순서를 지정, 낮을수록 먼저 동작
filterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴 지정
return filterRegistrationBean;
}
/**
* 여기서 모든 URL에 대해서 호출을 하고, LoginCheckFilter에서 화이트 리스트를 제외시키는 방법
* 해당 필터에서 적용할 URL을 추가하면 새로운 기능을 추가할 때 지속적으로 추가를 해줘야함
*/
@Bean
public FilterRegistrationBean loginCheckFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LoginCheckFilter());
filterRegistrationBean.setOrder(2); // 필터가 체인으로 동작하기 때문에 순서를 지정, 낮을수록 먼저 동작
filterRegistrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴 지정
return filterRegistrationBean;
}
}
LoginController
컨트롤러에서 @RequestParam(defaultValue = "/") String redirectURL 매개변수를 추가하여 요청 받은 파라미터를 가지고 로그인 성공시 해당 경로로 사용자를 redirect 한다.
@Slf4j
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
@PostMapping("/login")
public String loginV4(@Validated @ModelAttribute("loginForm") LoginForm loginForm,
BindingResult bindingResult,
@RequestParam(defaultValue = "/") String redirectURL,
HttpServletRequest request) {
if (bindingResult.hasErrors()) {
return "login/loginForm";
}
Member loginMember = loginService.login(loginForm.getLoginId(), loginForm.getPassword());
log.info("login? {}", loginMember);
if (loginMember == null) {
bindingResult.reject("loginFail", "아이디 또는 비밀번호가 맞지 않습니다.");
return "login/loginForm";
}
// 세션이 있으면 존재하는 세션을 반환, 없으면 세션을 새롭게 생성
HttpSession session = request.getSession();
// 세션에 로그인 회원 정보 보관
session.setAttribute(SessionConst.LOGIN_MEMBER, loginMember);
return "redirect:" + redirectURL;
}
}
스프링 인터셉터(Spring Interceptor)란?
서블릿 필터가 서블릿이 제공하는 기술이라면, 스프링 인터셉터는 스프링 MVC가 제공하는 기술이다.
스프링 인터셉터 흐름은 아래와 같다.
HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 스프링 인터셉터 -> 컨트롤러
스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러가 호출되기 직전에 호출이 된다.
스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에 디스패처 서블릿 이후에 등장하게 된다.
스프링 인터셉터에도 URL 패턴을 적용할 수 있는데, 서블릿 필터에 적용되는 URL 패턴과는 다르고 매우 정밀하게 설정할 수 있다.
만약, 적절하지 않은 요청이라 판단하면 아래와 같이 인터셉터에서 끝을 낼 수 있다.
HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 스프링 인터셉터
스프링 인터셉터는 체인으로 구성된다.
HTTP 요청 -> WAS -> 필터 -> 디스패처 서블릿 -> 스프링 인터셉터A -> 스프링 인터셉터B -> 컨트롤러
스프링 인터셉터 인터페이스(HandlerInterceptor) 살펴보기
스프링 인터셉터를 사용하려면 HandlerInterceptor 인터페이스를 구현하면된다.
HandlerInterceptor 인터페이스 코드를 살펴보면 아래와 같다.
package org.springframework.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;
import org.springframework.web.method.HandlerMethod;
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
서블릿 필터의 경우에는 단순하게 doFilter() 하나만 제공해주고, request와 response만 제공했지만
인터셉터는 컨트롤러 호출 전, 호출 후, 요청 완료 이후와 같이 단계적으로 잘 세분화 되어 있고, 어떤 컨트롤러(handler)가 호출되는지 호출 정보와 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있다.
스프링 인터셉터 호출 흐름을 그림으로 살펴보면 아래와 같다.

- preHandle : 컨트롤러 호출 전, 더 정확히는 핸들러 어댑터 호출전에 호출이 된다.
- preHandle의 응답값이 true이면 다음으로 진행하고, false이면 더 이상 진행하지 않는다.
- false가 되어버리면 더 이상 진행하지 않기 때문에 나머지 이후의 인터셉터는 물론, 핸들러 어댑터, 컨트롤러도 호출되지 않는다.
- postHandle : 컨트롤러 호출 후, 더 정확히는 핸들러 어댑터 호출 후에 호출이 된다.
- afterCompletion : 뷰가 렌더링 된 이후에 호출된다.
위와 같은 흐름으로 이루어져 있기 때문에 예외가 발생한다면
preHandle은 컨트롤러 호출 전에 호출이 되기 때문에 호출을 하고, 컨트롤러에서 예외가 발생하면 postHandle은 호출되지 않는다.
afterCompletion은 항상 호출이 되어 이 경우 예외를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
스프링 인터셉터 사용해보기
서블릿 필터로 구현했던 로그 남기기와 로그인 인증하는 부분을 스프링 인터셉터로 구현해보자
LogInterceptor(로그 인터셉터)
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
// afterCompletion 호출할 때도 uuid를 넘기기 위해서
request.setAttribute(LOG_ID, uuid);
/**
* 핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다.
* @Controller, @RequestMapping에서 사용하는 핸들러는 HandlerMethod 가 사용이 되서 핸들러 정보로 넘어오고,
* 정적 리소스를 사용하는 경우에는 ResourceHttpRequestHandler 가 사용이 되서 핸들러 정보로 넘어온다.
*/
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메서드의 모든 정보 포함되어있음
}
log.info("REQUEST [{}][{}][{}]", uuid, requestURI, handler);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle [{}]", modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String requestURI = request.getRequestURI();
String logId = (String) request.getAttribute(LOG_ID);
log.info("RESPONSE [{}][{}]", logId, requestURI);
if (ex != null) { // 예외가 터진 경우
log.error("afterCompletion error!!", ex);
}
}
}
LoginCheckInterceptor(로그인 인증 체크 인터셉터)
import hello.login.web.session.SessionConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("인증 체크 인터셉터 실행 {}", requestURI);
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
response.sendRedirect("/login?redirect=" + requestURI);
return false;
}
return true;
}
}
WebConfig에 인터셉터 등록하기
import hello.login.web.argumentresolver.LoginMemberArgumentResolver;
import hello.login.web.filter.LogFilter;
import hello.login.web.filter.LoginCheckFilter;
import hello.login.web.interceptor.LogInterceptor;
import hello.login.web.interceptor.LoginCheckInterceptor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.Filter;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor()) // 인터셉터 등록
.order(1) // 인터셉터 호출 순서 지정
.addPathPatterns("/**") // 인터셉터를 적용할 패턴
.excludePathPatterns("/css/**", "/*.ico", "/error"); //인터셉터에서 제외할 패턴
registry.addInterceptor(new LoginCheckInterceptor())
.order(2)
.addPathPatterns("/**")
.excludePathPatterns("/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error");
}
}
기존에 필터 등록하는 것과 비교해보면 인터셉터는 addPathPatterns, excludePathPatterns로 정밀하게 URL 패턴을 지정할 수 있다.
더욱 상세한 부분 공식 문서 링크를 참고하자!
'TIL > Spring' 카테고리의 다른 글
[Spring] ArgumentResolver (HandlerMethodArgumentResolver)란 무엇일까? (0) | 2025.03.29 |
---|---|
[Spring] Session으로 로그인 기능 구현하기(HttpSession, TrackingModes, Session Timeout) (0) | 2025.03.27 |
[Spring] Spring - Bean Validation (0) | 2025.03.20 |
[Spring] Spring Validation-BindingResult(with.thymeleaf) (0) | 2025.03.16 |
[Spring] 빈 스코프 란? (0) | 2025.03.08 |