HTTP 요청이 DispatcherServlet 전과 후 실행되는 기능
요청 전에만 실행되지 않고, 후에도 실행될 수 있다

OncePerRequestFilter같은 요청에서 필터가 중복 실행되지 않게 관리하는 클래스
OncePerRequestFilter를 상속받아 필터를 구현할 수 있다.Filter라는 인터페이스를 구현하는 것이었다.300번대 응답(Redirect)에서 발생할 수 있는 현상이다.OncePerRequestFilter를 사용하는 것을 권장하고 있다.Filter를 구현한 추상 클래스이다.필터 정의
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {
long start = System.nanoTime();
log.info("BEFORE {} {}", request.getMethod(), request.getRequestURI());
try {
chain.doFilter(request, response);
} finally {
long tookMs = (System.nanoTime() - start) / 1_000_000;
log.info("AFTER {} {} -> {} ({}ms)", request.getMethod(),
request.getRequestURI(), response.getStatus(), tookMs);
}
}
}
필터 등록
앞 강의 프로퍼티 바인딩과 비슷하게, 별도 설정 클래스에서 등록할 수도 있고, 필터 클래스 자체를 빈 등록 할 수도 있다.
1. @Configuration 클래스 사용
- addUrlPatterns을 통해 특정 경로의 API에 대해 필터가 동작하도록 할 수 있다.
@Configuration
public class FilterConfig {
@Bean // 메서드 레벨 빈 등록
public FilterRegistrationBean<RequestLoggingFilter> requestLoggingFilter() {
FilterRegistrationBean<RequestLoggingFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new RequestLoggingFilter());
registration.addUrlPatterns("/posts", "/posts/*"); // 필터 적용할 요청 종류
registration.setOrder(1); // 필터 적용 순서 (이 경우 첫 번째)
return registration;
}
}
@Component로 자체 등록, @Order(n)로 순서 지정addUrlPatterns 같은 효과를 보기가 어렵다. (가능은 하지만 조금 복잡하다.)@Slf4j
@Component
@Order(1)
public class RequestLoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain
) throws ServletException, IOException {
long start = System.nanoTime();
log.info("BEFORE {} {}", request.getMethod(), request.getRequestURI());
try {
chain.doFilter(request, response);
} finally {
long tookMs = (System.nanoTime() - start) / 1_000_000;
log.info("AFTER {} {} -> {} ({}ms)", request.getMethod(),
request.getRequestURI(), response.getStatus(), tookMs);
}
}
}
HandlerInterceptor 인터페이스를 구현하여 사용할 수 있다.
(`preHandle` = 컨트롤러 전 / `postHandle` = 컨트롤러 후 / `afterCompletion` = 요청 처리 완전 종료 후)
preHandle, postHandle, afterCompletion 중 필요한 메서드만 구현한다.preHandle은 리턴값이 boolean이다. (나머지는 void)true라면 컨트롤러 혹은 다음 인터셉터로 계속 진행한다.false라면 중단한다. (컨트롤러 메서드가 실행되지 않는다.)postHandle은 컨트롤러 메서드 실행 직후 호출된다. 컨트롤러에서 예외 발생 시 건너뛴다.afterCompletion은 클라이언트에게 응답 전송을 마치고 요청처리가 완전히 끝난 시점에 호출된다. (DispatcherServlet 기준) 정상 종료든 예외 상황이든 무조건 실행된다. (preHandle이 true를 반환했다면 afterCompletion 역시 반드시 호출된다.)@Slf4j
@Component
public class RequestLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler
) {
log.info("preHandle: {}", request.getRequestURI());
return true;
}
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView
) {
log.info("postHandle: {} -> {}", request.getRequestURI(), response.getStatus());
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex
) {
log.info("afterCompletion: {} -> {}", request.getRequestURI(), response.getStatus());
}
}
필터와 마찬가지로, 인터셉터도 빈 등록을 해야 한다.
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final RequestLoggingInterceptor requestLoggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestLoggingInterceptor)
.addPathPatterns("/posts/**") // 인터셉터를 적용할 경로
.excludePathPatterns("/posts/public/**"); // 인터셉터를 제외할 경로
}
}
.
addPathPatterns와 excludePathPatterns가 충돌하는 경우 excludePathPatterns가 항상 우선시된다. (먼저 통합 경로를 지정하고, 그 중에서 세부 경로를 쳐내는 방식)
addPathPatterns 안에 여러 경로를 함께 넣을 수 있다. (가변인자도 되고, 리스트로 넘겨도 됨) 혹은 반복적으로 체이닝도 가능하다. (excludePathPatterns도 마찬가지)**도 *와 같은 와일드카드이지만, 조금 더 포함 범위가 넓다.
*(단일 와일드카드): 하나의 경로 세그먼트(/ 사이 문자열)/posts/*라면 /posts/1, /posts/2는 되지만, /posts/1/comments와는 매칭 안됨**(다중 와일드카드): 0개 이상의 경로 세그먼트 전체/posts/**라면 경로 깊이에 무관하게 /posts로 시작하는 모든 URL과 매칭됨 (하위 세그멘트가 없는 /posts도 가능)전체 실행 순서 흐름
chain.doFilter() 호출 전DispatcherServlet 진입preHandle)postHandle): 정상 수행 시 뷰 렌더링 직전 호출DispatcherServlet 내부 작업 마무리)afterCompletion): DispatcherServlet이 HTTP 요청 처리를 완전히 끝내기 직전 호출DispatcherServlet 실행 종료 후 호출자(서블릿 필터 체인)로 반환chain.doFilter() 다음 코드 및 finally 블록 실행