스프링에서는 웹과 관련된 공통 관심 사항을 처리할 수 있도록 지원하는 서블릿 필터와 스프링 인터셉터 기술이 존재한다고 한다. AOP를 사용해도 되지만 웹과 관련된 공통 관심 사항에서는 Http 헤더나 Url 정보 등이 필요하기 때문에 필터나 인터셉터를 사용하게 좋다고 한다. 필터나 인터셉터는 HttpServletRequest와 HttpServletResponse를 제공하기 때문!
필터는 서블릿이 지원하는 수문장 역할을 수행한다고 한다.
필터의 실행 흐름은 아래와 같다.
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() {}
}
필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다고 한다.
Http 요청이 수행된 시간을 출력하는 필터의 예시 코드는 아래와 같다.
@Slf4j
public class RunTimeCheckFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("RunTimeCheckFilter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("RunTimeCheckFilter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String runtime = LocalDateTime.now().toString();
try {
log.info("REQUEST [{}][{}]", runtime, requestURI);
chain.doFilter(request, response);
} catch (Exception e) {
throw e;
} finally {
log.info("REQUEST [{}][{}]", runtime, requestURI);
}
}
@Override
public void destroy() {
log.info("RunTimeCheckFilter destory");
}
}
Filter 인터페이스 : 필터를 사용하기 위해서는 필터 인터페이스를 구현해야함.
doFilter(ServletRequest request, ServletResponse response, FilterChain chain) :
Http 요청이 오면 doFilter가 호출됨.
chain.doFilter(request, response) : 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출함.
필터를 등록하기 위해서는 아래와 같이 FilterRegistrationBean을 등록해줘야한다.
@Configuration
public class WebConfig{
@Bean
public FilterRegistrationBean runTimeFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new RunTimeCheckFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
참고) @WebFilter 방식으로도 필터 등록이 가능하지만 필터 순서 조절이 안된다고 함.
스프링 인터셉터는 서블릿 필터와 동일하게 웹과 관련된 공통 관심 사항을 처리할 수 있는 기술이라고 한다.
필터보다 좀더 개발자가 편리하게 사용할 수 있다고 하니 필터보다는 인터셉터의 사용을 선호해야겠다.
스프링 인터셉터의 실행 흐름은 아래와 같다.
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 스프링 인터셉터 -> 컨트롤러
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 {
}
}
스프링 인터셉터는 HandlerInterceptor를 구현해야한다.
인터셉터는 위 코드에서 나오듯이 컨트롤러 호출전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)으로 단계적으로 세분화 되어 있다. 그 뿐만 아니라 인터셉터는 request, response 그리고 호출되는 컨트롤러와 반환되는 modelAndView까지 확인이 가능하다.
다만, 인터셉터는 예외가 발생했을때의 특이점이 존재한다.
스프링 인터셉터로 함수 호출 시간을 출력하는 인터셉터 코드는 아래와 같다.
@Slf4j
public class RunTimeCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String runtime = LocalDateTime.now().toString();
request.setAttribute("runtime", runtime);
log.info("REQUEST [{}][{}][{}]", runtime, 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 runtime = (String) request.getAttribute("runtime");
log.info("RESPONSE [{}][{}][{}]", runtime, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error!!", ex);
}
}
}
preHandle에서 return true면 다음 인터셉터나 컨트롤러가 호출된다.
인터셉터를 등록하기 위해서는 WebMvcConfigurer을 구현해야한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RunTimeCheckInterceptor())
.order(1)
.addPathPatterns("/**")
.excludePathPatterns("/css/**", "/*.ico", "/error");
}
WebMvcConfigurer에서 제공하는 addInterceptors()를 사용해서 인터셉터를 등록한다.
스프링은 일반적으로 @Controller와 @RequestMapping을 활용한 핸들러 맵핑을 사용하는데 이 경우 핸들러 정보로 HandlerMethod가 넘어온다. 알아만두자.
ArgumentResolver는 @ModelAttribute를 사용해서 RequestParam으로 전달된 값들이 객체로 바로 바인딩될 수 있도록 도와주는 녀석을 말한다.
물론 스프링에서 기본적으로 제공해주지만 직접 커스텀해서 사용도 가능하다.
아래는 김영한님 강의에서 예시로 들어주신 코드이다. 간단히 @Login 애노테이션으로 ArgumentResolver를 커스텀한걸 적용시켜서 ModelAttribute처럼 객체에 바인딩한다.
@GetMapping("/")
public String homeLoginV3ArgumentResolver(@Login Member loginMember, Model model) {
//세션에 회원 데이터가 없으면 home
if (loginMember == null) {
return "home";
}
//세션이 유지되면 로그인으로 이동
model.addAttribute("member", loginMember);
return "loginHome";
}
우선 @Login 애노테이션을 만들어준다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
그리고 ArugmentResolver 커스텀을 만들어준다.
@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);
}
}
HandlerMethodArgumentResolver를 구현하며, supportsParameter가 true를 리턴하면 resolveArgument가 호출되어 객체로 바로 바인딩된다.
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class):
Login 애노테이션이 존재하는지 확인한다.
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType()); :
매개변수가 Member 클래스인지 확인한다.
resolveArgument에서는 HttpSession에 저장해놓은 Member를 전달해준다.
해당 포스팅은 아래의 강의를 공부하여 정리한 내용입니다.
김영한님의 SpringMVC2-필터,인터셉터