서블릿 필터
필터(Filter)는 J2EE 표준 스펙 기능으로 디스패처 서블릿(Dispatcher Servlet)에 요청이 전달되기 전/후에 url 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공한다.
필터 흐름
디스패처 서블릿은 스프링의 가장 앞단에 존재하는 프론트 컨트롤러이므로, 필터는 스프링 범위 밖에서 처리가 되는 것이다. 즉, 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너(서블릿 컨테이너)에 의해 관리가 되는 것이고, 디스패처 서블릿 전/후에 처리하는 것이다. 흐름을 간단하게 정리하면 아래와 같다.
HTTP 요청 → WAS → 필터1 → …. → 필터5 → 서블릿 → 컨트롤러
필터는 체인으로 구성되는데, 중간에 자유롭게 추가할 수 있다.
필터 인터페이스와 메서드
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() {}
}
위의 필터 인터페이스를 구현하고 등록하면, 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고 관리한다. 즉, 필터를 추가하기 위해서는 javax.servlet의 Filter 인터페이스를 구현해야 하며 이는 다음의 3가지 메소드를 가지고 있다. init() 메서드는 필터 초기화 메서드로 서블릿 컨테이너가 생성될 때 호출된다. 해당 메서드를 호출하여 필터 객체를 초기화 하면, 이후의 로직들은doFilter() 를 통해 처리된다.
doFilter() 는 고객의 요청이 오면, 해당 요청이 디스패처 서블릿으로 전달되기 전에 웹 컨테이너(서블릿 컨테이너)에 의해 실행되는 메서드이다. doFilter()의 파라미터로는 FilterChain이 있는데, FilterChain의 doFilter() 통해 다음 대상으로 요청을 전달하게 된다. 따라서 chain.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행할 수 있다.
destory() 메서드는 필터의 종료 메서드로 서블릿 컨테이너가 종료될 때 호출된다.
스프링 인터셉터
인터셉터(Interceptor)는 필터(Filter)와 달리 Spring MVC가 제공하는 기술로, 디스패처 서블릿(Dispatcher Servlet)이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 기능을 제공한다.
위의 사진에서 확인할 수 있듯, 서블릿 컨테이너에서 동작하는 필터와 달리 인터셉터는 스프링 컨텍스트에서 동작을 하는 것이다. 즉, 인터셉터는 스프링 컨테이너 내에서 동작하므로 필터를 거쳐 디스패처 서블릿이 요청을 받은 이후에 동작하게 된다.
HTTP 요청 → WAS → 필터 → 서블릿 → 인터셉터1 → … → 인터셉터 4 → 컨트롤러
인터셉터 또한 필터와 마찬가지로 체인으로 구성된다. 디스패처 서블릿은 핸들러 매핑을 통해 적절한 컨트롤러를 찾도록 요청하고 그 결과로 실행 체인(HandlerExecutionChain)을 돌려주는데, 이때 이 실행 체인은 1개 이상의 인터셉터가 등록되어 있다면 순차적으로 인터셉터들을 거쳐 컨트롤러가 실행되도록 하고, 인터셉터가 없다면 바로 컨트롤러를 실행한다.
인터셉터 인터페이스와 메서드
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(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {}
}
인터셉터를 추가하기 위해서는 org.springframework.web.servlet의 HandlerInterceptor인터페이스를 구현해야 하며, 이는 3가지 메소드를 가지고 있다.
preHandle() 메서드는 컨트롤러가 호출되기 전(정확히는 핸들러 어댑터가 호출되기 전)에 실행된다. 때문에 컨트롤러 이전에 처리해야 하는 전처리 작업이나 요청 정보를 가공하거나 추가하는 경우에 사용한다. preHandle의 반환 타입은 boolean인데 반환값이 true이면 다음 단계로 진행이 되지만, false라면 작업을 중단하여 이후의 작업은 진행되지 않는다.
3번째 파라미터인 handler 파라미터는 핸들러 매핑이 찾아준 컨트롤러 빈에 매핑되는 HandlerMethod라는 새로운 타입의 객체로써, @RequestMapping이 붙은 메소드의 정보를 추상화한 객체이다.
postHandle() 메서드는 컨트롤러 호출 후에 실행된다. 때문에 컨트롤러 이후에 처리해야 하는 후처리 작업이 있을 때 사용한다. 만약, 컨트롤러 하위 계층에서 작업을 진행하다가 예외가 발생하면 postHandle은 호출되지 않는다. postHandle()에는 컨트롤러가 반환하는 ModelAndView 타입의 정보가 제공되는데, 최근에는 Json 형태로 데이터를 제공하는 RestAPI 기반의 컨트롤러를 만들면서 자주 사용되지는 않는다고 한다.
afterCompletion() 메소드는 이름 그대로 모든 뷰에서 최종 결과를 생성하는 일을 포함해 모든 작업이 완료된 후에 실행된다. 요청 처리 중에 사용한 리소스를 반환할 때 사용하기에 적합하다. postHandle()과 달리 컨트롤러 하위 계층에서 작업을 진행하다가 중간에 예외가 발생하더라도 반드시 호출된다.
필터 VS 인터셉터
지금까지 적은 내용을 전체적으로 정리하면 아래와 같다.
ArgumentResolver 활용
HandlerMethodArgumentResolver
HandlerMethodArgumentResolver를 줄여서 ArgumentResolver라고 부른다.
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
supportsRarameter()는 주어진 메서드 파라미터를 확인하여 해당 ArgumentResolver가 처리할 수 있는지 여부를 반환하는 메서드이다. 만약 supportsRarameter()를 호출했을 때, true를 반환한다면 resloveArgument()를 호출하여 실제 파라미터의 값을 생성하거나 변환하여 컨트롤러 메서드로 넘어가게 된다. 해당 인터페이스를 구현하여 특정 애너테이션이 붙은 파라미터를 처리하는 등, 우리가 원하는 커스텀 로직으로 ArgumentResolver를 만들 수 있다.
커스텀 ArgumentResolver 생성하기
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login {
}
위의 코드는 사용자가 @Login 이라는 애노테이션을 만드는 방법이다. @Target(ElementType.PARAMETER) 는 파라미터에만 사용한다는 뜻이며, @Retention(RetentionPolicy.RUNTIME) 은 리플렉션 등을 활용할 수 있도록 런타임까지 애노테이션 정보가 남아있도록 하는 것이다.
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); // @Login 애노테이션이 존재하는가?
boolean hasMemberType = Member.class.isAssignableFrom(parameter.getParameterType()); // Member 타입인가?
return hasLoginAnnotation && hasMemberType;
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest)
webRequest.getNativeRequest();
HttpSession session = request.getSession(false);
if (session == null) {
return null;
}
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
위 코드는 supportsParameter() 메서드를 @Login 애노테이션이 존재하면서 Member 타입인지를 체크하도록 재정의한 것이다. 만약 지원한다면 아래의 resloveArgument() 를 호출하는 방식으로 동작한다. resloveArgument() 메서드는 컨트롤러 호출 직전에 호출되어 필요한 파라미터 정보를 생성해준다. 위의 코드에서는 세션에 있는 로그인 회원 정보인 member 객체를 찾아서 반환해준다. 이후, 스프링 MVC는 컨트롤러의 메서드를 호출하면서 반환된 member 객체를 파라미터에 전달해준다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new LoginMemberArgumentResolver());
}
}
이렇게 우리가 만든 ArgumentResolver를 설정 정보에 등록하면 완성이다.
'Spring' 카테고리의 다른 글
빈으로 등록된 필터가 WebSecurity의 ignoring에 의해 무시되지 않는 이유 (0) | 2025.03.17 |
---|---|
스프링 MVC 2편 - 섹션 10~11 (0) | 2025.03.17 |
스프링 MVC 2편 - 섹션 4~5 (0) | 2025.03.17 |
스프링 MVC 1편 - 섹션 4~7 (0) | 2025.03.17 |
스프링 MVC 1편 - 섹션 1~3 (0) | 2025.03.17 |