✔️ 필터란, J2EE 표준 스펙 기능으로 디스패처 서블릿(Dispatcher Servlet)에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공한다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 (적절한 요청)
HTTP 요청 -> WAS -> 필터(적절하지 않은 요청, 서블릿 호출하지 않음)
HTTP 요청 -> WAS -> 필터1 -> 필터2 -> 필터3 -> 서블릿 -> 컨트롤러

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() {}
}
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
FilterRegistrationBean을 사용해서 등록하면 된다.SetFilter(new LogFilter()) : 등록할 필터를 지정한다.SetOrder(1) : 필터를 동작할 순서를 등록한다. 낮을 수록 먼저 동작한다.addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한 번에 여러 패턴 지정도 가능하다.💡 참고 1 : URL 패턴은 필터와 서블릿이 동일하다. 서플릿 URL 패턴을 참고하자.
💡 참고 2 : 실무에서 HTTP 요청 시 해당 요청에 대한 로그에 모두 같은 식별자를 남기고 싶은 경우, logback mdc 를 사용하면 된다. 사용 방법은 검색해서 알아보자.
@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 chain) 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);
//로그인으로 redirect
httpResponse.sendRedirect("/login?redirectURL=" + requestURI);
return; // 미인증 사용자는 다음으로 진행하지 않고 종료
}
}
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("인증 체크 필터 종료 {}", requestURI);
}
}
/**
* 화이트 리스트의 경우 인증 체크 패스
* @param requestURI {@link String}
* @return boolean {@link Boolean}
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
httpResponse.sendRedirect("/login?redirectURL=" + requestURI); : 미인증 사용자가 로그인 후 자신이 보고 있던 화면을 그대로 보여주기 위해 현재 요청 경로인 reuqestURI를 쿼리 파라미터로 함께 전달한다.💡 참고
✔️ 인터셉터란, Spring이 제공하는 기술로써 디스패처 서블릿(Dispatcher Servlet)이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.
✔️ 웹 컨테이너(서블릿 컨테이너)에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작한다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> Interceptor -> 컨트롤러
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> Interceptor -> 컨트롤러 (적절한 요청)
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> Interceptor(적절하지 않은 요청, 컨트롤러 호출하지 않음)
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러

public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request
, HttpServletResponse response
, Object handler ) throws Exception {}
default void postHandle(HttpServletRequest request
, HttpServletResponse response
, Object handler
, @Nullable ModelAndView modelAndView) throws Exception {}
default void afterCompletion(HttpServeltReqeust request
, HttpServletResponse response
, Object handler
, @Nullable Exception ex) throws Exception {}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
}
WebMvcConfigurer가 제공하는 addInterceptors()를 사용해서 인터셉터를 등록할 수 있다.
order(1) : 호출할 인터셉터의 순서를 지정한다. 낮을 수록 먼저 호출된다.
addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.
excludePathPatterns("/css/**", "/*.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.
💡 참고 : 스프링이 제공하는 URL 경로는 서블릿 URL 경로와 완전히 다르다. 스프링에서 제공하는 URL이 더욱 자세하고 세밀하게 설정할 수 있다.
@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();
request.setAttribute(LOG_ID, uuid);
//@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);
}
}
}
String uuid = UUID.randomUUID().toString() : 요청 로그를 구분하기 위한 uuidrequest.setAttribute(LOG_ID, uuid); : 인터셉터는 각 메소드의 호출 시점이 분리되어 있기 때문에 preHandle()에서 사용했던 값을 다른 메소드에서 사용하기 위해 request.Settribute()를 통해 request에 담아서 사용할 수 있다.HandlerMethod가 넘어온다.ResourceHttpRequestHandler가 넘어온다.@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("미인증 사용자 요청");
//로그인으로 redirect
response.sendRedirect("/login?redirectURL=" + requestURI);
return false;
}
return true;
}


preHandle: 컨트롤러 호출 전에 호출 됨postHandle: 컨트롤러에서 예외가 발생하면 postHandler은 호출되지 않음afterCompletion: afterCompletion은 항상 호출 됨. 예외 발생 시 ex를 포함해서 호출되며, 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력 가능
Interceptor는 스프링 MVC 구조에 특화된 Filter 기능을 제공한다고 보면 됨. 특별히 Filter를 사용해야 하는 상황이 아니면, Interceptor를 사용하는 것이 편리함
✔️ 이 전에 포스팅했던 로그인 처리 - 쿠키 세션에서 로그인 회원 정보를 찾을 때 아래와 같이 사용했었다.
//HomeController
public String homeLoginV3Spring(@SessionAttribute(name = SessionConst.LOGIN_MEMBER, required = false) Member loginmember,
Model model) {
if (loginmember == null) {
return "home";
}
model.addAttribute("member", loginmember);
return "loginHome";
}
✔️ 위 방법보다 좀 더 편리하게 회원 정보를 찾을 수 있도록 ArgumentResolver를 사용하여 작성해본다.
//HomeController
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
//@Login Annotation
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
LoginMemberArgumentResolver 가 동작해서 자동으로 세션에 있는 로그인 회원을 찾아주고 없다면 null을 반환해주도록 ArgumentResolver를 만들어본다.✔️ 커스텀 ArgumentResolver
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType());
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer
, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
log.info("resolveArgument 실행");
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
supportsParameter() : @Login 어노테이션이 있고 Member 타입이면 해당 LoginMemberArgumentResolver가 실행된다.
resolveArgument() : 컨트롤러 호출 직전에 호출되어서 필요한 파라미터 정보를 생성해준다. 위 예제에서는 세션에 있는 member 객체를 찾아서 반환해준다. 이후 스프링 MVC에서 컨트롤러 메소드를 호출하면서 해당 member 객체를 파라미터에 전달해준다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
//LoginMemberArgumentResolver 등록
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
...
}
참고 Reference