HTTP 요청 → 내장 톰켓 서버(WAS) → 필터 → 디스패처 서블릿 → 인터셉터 → 컨트롤러
- 필터를 적용하면 필터가 호출된 다음 서블릿이 호출되기 때문에 서블릿에 도달하기 전에 부가적인 작업을 먼저 처리할 수 있다.
- 스프링 하위 프레임워크인 스프링 시큐리티 역시 필터 체인으로 구성이 되어 있다.
public interface Filter {
/**
* Called by the web container to indicate to a filter that it is being placed into service. The servlet container
* calls the init method exactly once after instantiating the filter. The init method must complete successfully
* before the filter is asked to do any filtering work.
* <p>
* The web container cannot place the filter into service if the init method either:
* <ul>
* <li>Throws a ServletException</li>
* <li>Does not return within a time period defined by the web container</li>
* </ul>
* The default implementation is a NO-OP.
*
* @param filterConfig The configuration information associated with the filter instance being initialised
*
* @throws ServletException if the initialisation fails
*/
default void init(FilterConfig filterConfig) throws ServletException {
// 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출
}
/**
* The <code>doFilter</code> method of the Filter is called by the container each time a request/response pair is
* passed through the chain due to a client request for a resource at the end of the chain. The FilterChain passed
* in to this method allows the Filter to pass on the request and response to the next entity in the chain.
* <p>
* A typical implementation of this method would follow the following pattern:- <br>
* 1. Examine the request<br>
* 2. Optionally wrap the request object with a custom implementation to filter content or headers for input
* filtering <br>
* 3. Optionally wrap the response object with a custom implementation to filter content or headers for output
* filtering <br>
* 4. a) <strong>Either</strong> invoke the next entity in the chain using the FilterChain object
* (<code>chain.doFilter()</code>), <br>
* 4. b) <strong>or</strong> not pass on the request/response pair to the next entity in the filter chain to block
* the request processing<br>
* 5. Directly set headers on the response after invocation of the next entity in the filter chain.
*
* @param request The request to process
* @param response The response associated with the request
* @param chain Provides access to the next filter in the chain for this filter to pass the request and response
* to for further processing
*
* @throws IOException if an I/O error occurs during this filter's processing of the request
* @throws ServletException if the processing fails for any other reason
*/
// 요청이 들어올 때마다 호출, 필터 로직을 구현하는 부분
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
/**
* Called by the web container to indicate to a filter that it is being taken out of service. This method is only
* called once all threads within the filter's doFilter method have exited or after a timeout period has passed.
* After the web container calls this method, it will not call the doFilter method again on this instance of the
* filter. <br>
* <br>
* This method gives the filter an opportunity to clean up any resources that are being held (for example, memory,
* file handles, threads) and make sure that any persistent state is synchronized with the filter's current state in
* memory. The default implementation is a NO-OP.
*/
default void destroy() {
// 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출
}
}
@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; // 예외 로깅 지점은 필요 시 추가 가능, 여기서는 WAS까지 예외를 던짐
} finally {
log.info("인증 체크 필터 종료 {} ", requestURI);
}
}
/**
* 화이트 리스트의 경우 인증 체크X
*/
private boolean isLoginCheckPath(String requestURI) {
return !PatternMatchUtils.simpleMatch(whitelist, requestURI);
}
}
-
chain.doFilter(request, response)
- 다음 필터가 있으면 다음 필터를 호출하고 다음 필터가 없다면 서블릿을 호출한다.
- 해당 코드를 기준으로 앞쪽을 서블릿 호출 전, 뒤쪽을 서블릿 호출 후라고 볼 수 있다.
- 해당 코드가 없으면 필터를 호출하지 않고, 서블릿도 호출하지 않게 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
// 필터 등록: 앞서 만든 LoginCheckFilter를 등록
filterRegistrationBean.setFilter(new LoginCheckFilter());
// 필터 순서: 숫자가 낮을수록 먼저 실행됨
filterRegistrationBean.setOrder(1);
// 적용 URL: 모든 요청(/*)에 대해 필터 적용
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
- Servlet이 자바 기반의 서버 사이드 컴포넌트라면 Interceptor는 스프링 프레임워크 기반의 기술이다.
- 필터와 마찬가지로 공통 관심 사항 및 부가 기능을 담당하지만 컨트롤러 앞에서 동작하고 필터보다 더 편리하고 정교하며 더 많은 기능을 제공한다.
- 필터보다는 인터셉터를 사용하는 것을 권장한다.
- 인터셉터 역시 필터와 마찬가지로 컨트롤러 호출 전에 동작해서 적절하지 않은 요청이라면 컨트롤러를 호출하지 않고 끝낼 수 있다.
public interface HandlerInterceptor {
/**
* 컨트롤러(핸들러) 호출 전에 실행됩니다.
* HandlerMapping이 적절한 핸들러 객체를 결정한 후, HandlerAdapter가 핸들러를 호출하기 직전에 호출됩니다.
* * @return true이면 다음 인터셉터나 핸들러로 진행하고, false이면 진행을 중단합니다.
*/
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
// 컨트롤러 호출 전에 호출
// 응답 값이 true이면 그대로 진행되고, false이면 더 이상 진행되지 않는다.
return true;
}
/**
* 컨트롤러(핸들러)가 정상적으로 실행된 후에 실행됩니다.
* HandlerAdapter가 핸들러를 실제로 호출한 후, DispatcherServlet이 뷰를 렌더링하기 전에 호출됩니다.
* 핸들러가 반환한 ModelAndView를 통해 추가적인 모델 작업을 할 수 있습니다.
*/
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
// 컨트롤러 호출 후에 호출
}
/**
* 모든 요청 처리가 완료된 후(뷰 렌더링 이후)에 실행됩니다.
* 핸들러 실행 결과와 상관없이 항상 호출되므로, 리소스 정리 등의 공통 처리에 적합합니다.
* 단, preHandle이 true를 반환했을 때만 호출됩니다.
*/
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
// 뷰가 렌더링 된 이후에 호출
// afterCompletion()은 예외가 발생하더라도 호출이 된다.
// 공통 처리를 하려면 이 메서드를 활용해야 한다.
}
}
@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);
// 세션이 없으면 새로 생성하지 않도록 false가 아닌 기본 getSession()을 사용하거나
// 용도에 따라 request.getSession(false)를 검토할 수 있습니다.
HttpSession session = request.getSession();
if (session == null || session.getAttribute(SessionConst.LOGIN_MEMBER) == null) {
log.info("미인증 사용자 요청");
// 로그인으로 redirect, 현재 페이지를 파라미터로 전달하여 로그인 후 복귀 가능하게 함
response.sendRedirect("/login?redirectURL=" + requestURI);
// false를 반환하면 다음 인터셉터나 컨트롤러가 호출되지 않고 여기서 끝남
return false;
}
// true를 반환하면 다음 인터셉터나 컨트롤러 호출
return true;
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 인터셉터 등록 및 체인 설정
registry.addInterceptor(new LoginCheckInterceptor())
.order(1) // 인터셉터 실행 순서 (낮을수록 우선)
.addPathPatterns("/**") // 모든 경로에 인터셉터 적용
.excludePathPatterns(
"/", "/members/add", "/login", "/logout",
"/css/**", "/*.ico", "/error"
); // 위 경로는 인터셉터 호출을 제외 (화이트리스트)
}
}
@Target(ElementType.PARAMETER) // 파라미터에만 적용
@Retention(RetentionPolicy.RUNTIME) // 리플렉션 등을 활용할 수 있도록 런타임까지 정보가 남음
public @interface Login {
}
- 이전에도 공부를 했었지만 우리가 편리하게 사용하는 어노테이션인
@RequestBody, @RequestMapping 등등은 모두 ArgumentResolver에 의해 동작이 된다.
- 우리가 개발한 커스텀 어노테이션을 사용하려면 ArgumentResolver를 만들어서 추가해주어야 한다.
@Slf4j
public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
log.info("supportsParameter 실행");
// @Login 애노테이션이 붙어 있는지 확인
boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class);
// 파라미터 타입이 Member 클래스이거나 그 자식 타입인지 확인
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);
// 세션이 없으면 null 반환
if (session == null) {
return null;
}
// 세션에 보관된 회원 객체를 찾아서 반환
return session.getAttribute(SessionConst.LOGIN_MEMBER);
}
}
-
supportsParameter()
- LoginMemberArgumentResolver가 어떤 파라미터를 지원하는가?
-
parameter.hasParameterAnnotation : 파라미터에 해당 어노테이션이 붙어있는가?
-
isAssignableFrom : 파라미터 타입을 체크
-
resolveArgument()
- 구현하고자하는 실질적인 로직을 구현하는 곳으로 여기선 서버 측 세션 저장소에 있는 로그인 멤버를 가져와서 반환한다.
-
supportsParameter가 true가 나온 경우 resolveArgument가 동작하게 된다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
// ... 기존 필터나 인터셉터 설정들
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// 직접 구현한 LoginMemberArgumentResolver를 리스트에 추가
// 이제 스프링 MVC는 컨트롤러 파라미터를 결정할 때 이 리졸버를 거치게 됩니다.
resolvers.add(new LoginMemberArgumentResolver());
}
}