Filter , Interceptor
Servlet의 Filter와 Spring의 인터셉터 둘 모두 웹과 관련된 요청을 처리할 때 사용한다.
웹과 관련된 공통 관심사를 처리할 때는 HTTP의 헤더, URL의 정보들이 필요할 수도 있다. 서블릿 필터 or 스프링 인터셉터는 HttpServletRequest를 제공하기 때문에 Http와 관련된 요청을 처리할 때는 다른 기능 보다는 둘의 기능을 이용하는것이 효과적이다.
이를 바탕으로 Filet와 Interceptor는 위에서 말했듯이 웹과 관련된 요청을 처리, 요청에 관한 로그 남기기, 인증과 인가에 대한 공통 로직 적용, 응답에 대한 공통적인 변경(모든 응답에 대한 특정 HTTP 헤더를 추가 or 변경), 이미지나 다른 파일의 압축 등등 이러한 공통된 작업들을 Filter or Interceptor를 통해 분리하여 관찰할 수 있기 때문에, 코드의 중복을 최소화 하고 유지보수성을 높이는 것이 가능하다.
둘의 눈에 띄는 차이점은 로직이 적용되는 위치이다.
Filter와 Interceptor의 기능에는 눈에띄는 차이점은 존재하지 않는다. 둘 모두 웹과 관련된 공통관심사를 처리하는 기술이지만, 적용 되는 순서와 범위, 그리고 사용방법이 다르다.
- 필터의 특성
- 필터 흐름
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터를 적용하면 필터가 호출 된 다음에 디스패처 서블릿이 호출된다. - 만약 모든 고객의 요청 로그를 남기는 요구사항이 있다면, 필터를 사용하면 된다.
필터는 특정 URL 패턴에 적용할 수 있다. /*이라고 하면 모든 요청에 대해 필터가 적용된다.
- 필터 제한
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러 //로그인 사용자필터에서 적절하지 않은 요청이라고 판단된다면 거기에서 끝을 낼 수도 있다. 그래서 로그인 여부를 체크하기에 딱 좋다. - HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X) //비 로그인 사용자
- 필터 체인
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() {}
}
필터 인터페이스를 구현하고 빈으로등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다.
init():
필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.doFilter():
고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.destroy():
필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
필터는 SpringContext전의 위치인 WebContext에서 요청을 처리한다.
필터의 인터페이스를 구현하여, 간단하게 요청 로그를 남기는 코드를 작성해보자.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("LogFilter init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// HTTP 요청이 오면 doFilter 메서드가 호출된다.
// ServletRequest는 HTTP 요청이 아닌 경우까지 고려했기 때문에 HttpServlet이 아닌 Servlet이다.
// HTTP를 사용한다면 아래와 같이 다운캐스팅해주면 된다.
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 이미 로그인한 사용자의 요청 정보를 가져올것이기 때문에
HttpSession session = request.getSession(false);
if(session != null) {
Member member = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
// 로그인한 사용자의 세션이 유효하다면
if (member != null) {
MDC.put("USERID", member.getLoginId());
}
}
String requestURI = request.getRequestURI();
try {
log.info("REQUEST: [{}], USERID [{}]", requestURI, MDC.get("USERID"));
// 이 부분이 가장 중요하다. 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다 음 단계로 진행되지 않는다.
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception e) {
throw e;
} finally {
// 로그인이 성공했지만, 맨 위의 if문에서 null로 통과 되었기 때문에 null이다.
log.info("RESPONSE: [{}], USERID [{}]", requestURI, MDC.get("USERID"));
MDC.remove("REQUEST USERID");
}
}
@Override
public void destroy() {
log.info("LogFilter destroy");
}
}
MDC는 로그 메시지에 문맥을 추가하는데 유용하다.
예를 들어, 특정 사용자의 요청을 처리하는 동안 발생하는 모든 로그 이벤트에 사용자 ID를 더하면, 나중에 이 로그를 보면 어떤 사용자의 요청이 어떤 로그를 초래했는지 쉽게 파악할 수 있다. 쉽게 말해 어떤 사용자가 어떤 요청을 했는지 쉽게 식별할 수 있다는 것.
Filter를 구현하였으니 Bean으로 등록해보자
WebConfig - 필터 설정
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
필터를 등록하는 방법은 여러가지가 있지만, 스프링 부트를 사용한다면 FilterRegistrationBean 을 사용해서 등록하면 된다.
setFilter(new LogFilter()) : 등록할 필터를 지정한다.
setOrder(1) : 필터는 체인으로 동작한다. 따라서 순서가 필요하다. 낮을 수록 먼저 동작한다.
addUrlPatterns("/*") : 필터를 적용할 URL 패턴을 지정한다. 한번에 여러 패턴을 지정할 수 있다.
참고
@ServletComponentScan @WebFilter(filterName = "logFilter", urlPatterns = "/*") 로 필터 등록이 가능하지만 필터 순서 조절이 안된다. 따라서 FilterRegistrationBean 을 사용하자.
Filter 적용 실행 결과
2024-04-25 22:50:51.881 INFO 5766 --- [nio-8080-exec-1] hello.login.web.filter.LogFilter : REQUEST: [/login], USERID [null]
2024-04-25 22:50:51.948 INFO 5766 --- [nio-8080-exec-1] hello.login.web.login.LoginController : Login Success
2024-04-25 22:50:51.949 INFO 5766 --- [nio-8080-exec-1] hello.login.web.filter.LogFilter : RESPONSE: [/login], USERID [null]
2024-04-25 22:50:51.953 INFO 5766 --- [nio-8080-exec-2] hello.login.web.filter.LogFilter : REQUEST: [/], USERID [memberA]
2024-04-25 22:50:51.975 INFO 5766 --- [nio-8080-exec-2] hello.login.web.filter.LogFilter : RESPONSE: [/], USERID [memberA]
2024-04-25 22:50:52.007 INFO 5766 --- [nio-8080-exec-3] hello.login.web.filter.LogFilter : REQUEST: [/css/bootstrap.min.css], USERID [memberA]
2024-04-25 22:50:52.013 INFO 5766 --- [nio-8080-exec-3] hello.login.web.filter.LogFilter : RESPONSE: [/css/bootstrap.min.css], USERID [memberA]
필터를 등록할 때 urlPattern 을 /* 로 등록했기 때문에 모든 요청에 해당 필터가 적용된다.
- 인터셉터의 특성
- **스프링 인터셉터의 흐름**
`HTTP -> WAS -> Filter -> DispatchServlet -> Spring intercepter -> Controller`
- 스프링 인터셉터는 디스패처 서블릿과 컨트롤러 사이에서 컨트롤러 호출 직전에 호출 된다.
- 스프링 인터셉터는 스프링 MVC가 제공하는 기능이기 때문에, 결국 디스패처 서블릿 이후에 등장하게 된다.
- 스프링 인터셉터에도 URL 패턴을 적용할 수 있는데, 서블릿 URL 패턴과는 다르고, 매우 정밀하게 설정할 수 있다.
**스프링 인터셉터 제한**
`HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러 //로그인 사용자`
`HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터(적절하지 않은 요청이라 판단, 컨트롤러 호출X) // 비 로그인 사용자`
인터셉터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다. 그래서 로그인 여부를 체크하기에 좋다.
**스프링 인터셉터 체인**
`HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터1 -> 인터셉터2 -> 컨트롤러`
스프링 인터셉터는 체인으로 구성되는데, 중간에 인터셉터를 자유롭게 추가할 수 있다. 예를 들어서 로그를 남기는 인터 셉터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 인터셉터를 만들 수 있다.
지금까지 내용을 보면 서블릿 필터와 호출 되는 순서만 다르고, 제공하는 기능은 비슷해 보인다. 앞으로 설명하겠지만, 스프링 인터셉터는 서블릿 필터보다 편리하고, 더 정교하고 다양한 기능을 지원한다.
스프링 인터셉터는 요청이 오면 WAS -> Filter를 거쳐 SpringContext위치에서 실행된다. 요청 URL에 맞는 HandlerMethod
가 호출되기 직전에 불리운다.
스프링 인터셉터 인터페이스
스프링의 인터셉터를 사용하려면 HandlerIntercepter 인터페이스를 구현하면 된다.
인터셉터는 컨트롤러 호출 전(preHandle), 호출 후 (postHandle), 요청 완료 이후 (afterCompletion)와 같이 단계적으로 잘 세분화 되어있다.
서블릿 필터의 경우 단순히 request,response만 제공했지만, 인터셉터는 어떤 컨트롤러가 호출 되는지에 대한 호출 정보도 받을 수 있다. 그리고 어떤 modelAndView가 반환되는지 응답 정보도 받을 수 있다.
@Slf4j
public class LogIntercepter implements HandlerInterceptor {
public static final String USERID = "USERID";
public static final String HANDLER = "handler";
public static final String REQUEST_URI = "requestURI";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("Interceptor Prehandle 호출");
String requestURI = request.getRequestURI();
MDC.put(REQUEST_URI, requestURI);
HttpSession session = request.getSession(false);
if (session != null) {
Member member = (Member) session.getAttribute(SessionConst.LOGIN_MEMBER);
if (member != null) {
MDC.put(USERID, member.getLoginId());
}
}
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
MDC.put(HANDLER, String.valueOf(handler));
}
log.info("REQUEST : [{}] [{}] [{}] ", MDC.get(REQUEST_URI), MDC.get(USERID), MDC.get(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 {
log.info("Interceptor afterCompletion 호출");
log.info("RESPONSE : [{}] [{}] [{}] ", MDC.get(REQUEST_URI), MDC.get(USERID), MDC.get(HANDLER));
if (ex != null) {
log.error("afterCompletion error!!!", ex) ;
}
MDC.remove(USERID);
MDC.remove(HANDLER);
MDC.remove(REQUEST_URI);
}
}
HandlerMethod
핸들러 정보는 어떤 핸들러 매핑을 사용하는가에 따라 달라진다. 스프링을 사용하면 일반적으로@Controller, @RequestMapping 을 활용한 핸들러 매핑을 사용하는데, 이 경우 핸들러 정보로 HandlerMethod가 넘어온다.
postHandle, afterCompletion
종료 로그를 postHandle이 아니라 afterCompletion에서 실행한 이유는, 예외가 발생한 경우 postHandle이 호출되지 않기 때문이다. afterCompletion은 예외가 발생해도 호출되는 것을 보장하기 때문에.
Interceptor 빈으로 등록하기
WebConfig - Interceptor 설정
@Configuration
public class WebConfig implements WebMvcConfigurer {
//로그 필터는 addIntercerptors를 사용
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogIntercepter())
.addPathPatterns("/**")
.order(1)
.excludePathPatterns("/css/**", "/js/**", "/images/**", "/fonts/**", "/favicon.ico","/error");
}
}
인터셉터를 빈으로 등록하기 위해서는 WebMvcConfigurer를 구현하여, addInterceptors 메서드를 구현하면된다.
- registry.addInterceptor(new LogInterceptor()) : 인터셉터를 등록한다.
- order(1) : 인터셉터의 호출 순서를 지정한다. 낮을 수록 먼저 호출된다.
- addPathPatterns("/**") : 인터셉터를 적용할 URL 패턴을 지정한다.
- excludePathPatterns("/css/*", "/.ico", "/error") : 인터셉터에서 제외할 패턴을 지정한다.
스프링의 URL 경로
스프링이 제공하는 URL 경로는 서블릿 기술이 제공하는 URL 경로와 완전히 다르다. 더욱 자세하고, 세밀하게 설정할수 있다.
인터셉터 실행 결과
20:01:39.580 [http-nio-8080-exec-1] INFO h.login.web.filter.LoginCheckFilter - 인증 체크 필터 시작: /login
20:01:39.585 [http-nio-8080-exec-1] INFO h.l.web.intercepter.LogIntercepter - Interceptor Prehandle 호출
20:01:39.588 [http-nio-8080-exec-1] INFO h.l.web.intercepter.LogIntercepter - REQUEST : [/login] [null] [hello.login.web.login.LoginController#loginForm(Model)]
20:01:39.602 [http-nio-8080-exec-1] INFO h.l.web.intercepter.LogIntercepter - PostHandle: [ModelAndView [view="login/loginForm"; model={loginForm=LoginForm(loginId=null, password=null), org.springframework.validation.BindingResult.loginForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}]]
20:01:39.810 [http-nio-8080-exec-1] INFO h.l.web.intercepter.LogIntercepter - Interceptor afterCompletion 호출
20:01:39.811 [http-nio-8080-exec-1] INFO h.l.web.intercepter.LogIntercepter - RESPONSE : [/login] [null] [hello.login.web.login.LoginController#loginForm(Model)]
20:01:39.813 [http-nio-8080-exec-1] INFO h.login.web.filter.LoginCheckFilter - 인증 체크 종료: /login
인터셉터의 정상 흐름과 예외 발생 시의 흐름
정상 흐름
- preHandle : 컨트롤러 호출 전에 호출 된다(더 정확히는 핸들러 어댑터 호출 전에 호출)
- preHandle의 응답값이 true이면 다음으로 진행, false이면 더는 진행되지 않는다.
- postHandle : 컨트롤러가 호출 된 후 호출된다.(핸들러 어댑 터 호출 후에 호출된다.)
- afterCompletion: 뷰가 렌더링 된 이후에 호출된다.
예외 발생 시
- preHandle : 컨트롤러 호출 전에 호출된다.
- postHandle : 컨트롤러에서 예외가 발생하면 postHandle 은 호출되지 않는다.
- afterCompletion : afterCompletion 은 항상 호출된다.
(예외 발생과 무관하게 호출된다.)
, 이 경우 예외( ex )를 파라미터로 받아서 어떤 예외가 발생했는지 로그로 출력할 수 있다.
정리
인터셉터는 스프링 MVC 구조에 특화된 필터 기능을 제공한다고 이해하면된다. 스프링 MVC를 사용하고, 특별히 필터를 꼭 사용해야 하는 상황이 아니라면 인터셉터를 사용하는 것이 더 편리하다.
Filter vs Inteceptor
위의 예제 코드 및 설명들로 보았을 때 둘의 차이를 정리해보겠다.
- 서로 관리되는 컨테이너가 다르다
-> Filter는 WebContext, Interceptor는 SpringContext에서 관리된다. - 예외처리(
필터의 예외처리와 인터셉터의 예외처리에 대해서는 다음에 포스팅 하도록 하겠습니다.
)
-> Spring을 활용하여 웹 개발중 Servlet 필터에서 Exception이 발생했다면 에러는 디스패처 서블릿 까지 전달된다. 디스패처 서블릿은 예외가 핸들링 되지 않는 예외를 받았기 때문에 예외가 그대로 올라와서 예상치 못한 Exception을 만난 상황이다. 따라서 내부에 문제가 있다고 판단하여 500 Status로 응답을 반환한다. 이럴 경우에는 ExceptionResolver를 사용하여 예외를 적절히 처리하고 클라이언트에게 유용한 응답을 제공할 수 있게 해야한다. - Reqeust,Response 지원
-> 필터는 Request와 Response를 조작할 수 있지만 인터셉터는 조작할 수 없다.
'스프링' 카테고리의 다른 글
[Spring] RuntimeException(UncheckExcpetion) 활용하기 (0) | 2024.07.12 |
---|---|
[Spring] 서블릿의 예외 처리와 ExceptionHandler, ControllerAdvice 알아보기 (0) | 2024.05.17 |
[Spring] 로그인 기능 구현으로 알아보는 쿠키 및 세션 (2) | 2024.05.13 |
[Spring] 애노테이션 파라미터를 처리하는 ArgumentResolver (1) | 2024.05.13 |
[Spring] MultipartFile을 이용한 파일 업로드 및 수정, 삭제 (1) | 2024.04.29 |