≣ 목차
Spring 프레임워크를 사용하면서 필터와 인터셉터는 자주 접하게 되는 개념입니다. 이 두 가지는 모두 요청과 응답을 처리하는 데 중요한 역할을 하지만, 그 기능과 사용 목적에는 차이가 있습니다. 이번 글에서는 왜 필터와 인터셉터를 사용하는지, 각각의 개념과 역할, 간단한 구현, 그리고 필터와 인터셉터의 차이점을 정리해보겠습니다.
체인 관련 내용은 아래 포스팅을 읽어주세요!!
https://jun-itworld.tistory.com/28
[JSP&Javascript] FilterChain 이란?
Filter는 요청(Request)과 응답(Response)에 대한 정보들을 변경할 수 있게 개발자들에게 제공하는 서블린 컨테이너 입니다. FilterChain은 이런 Filter가 여러개 모여서 하나의 체인을 형성하는 것 인데요,
jun-itworld.tistory.com
1. 왜 필터와 인터셉터를 사용하는가?
Filters와 Interceptor는 웹 애플리케이션의 요청과 응답을 가로채고 처리하여 보안, 로깅, 인증, 권한 부여 등 다양한 기능을 추가함으로써, 코드의 재사용성과 유지보수성을 향상시키는 중요한 역할을 합니다.
2. 필터의 개념과 역할
필터는 서블릿 컨테이너 레벨에서 동작하며, URL 패턴에 따라 요청과 응답의 사전 및 사후 처리를 담당하여 디스패처 서블릿(Dispatcher Servlet) 으로 전달합니다.예를 들어, 요청이 서블릿에 도달하기 전에 특정 조건을 검사하거나, 응답이 클라이언트에게 전달되기 전에 데이터를 변형할 수 있습니다.
디스패처 서블릿은 Spring의 가장 앞에 위치한 프론트 컨트롤러이기 때문에, 필터는 Spring의 범위를 벗어난 곳에서 처리됩니다. 즉, 필터는 Spring 컨테이너에 등록된 빈이 아닌 Tomcat과 같은 웹 컨테이너(서블릿 컨테이너)에 의해 관리되며, 디스패처 서블릿의 전후로 처리됩니다. 이 과정을 그림으로 표현하면 다음과 같습니다.
2-1 필터 인터페이스 특징 및 주요 메소드
필터 인터페이스를 구현체를 만들고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리합니다
init() | 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출됩니다. 즉 필터가 사용할 때 필요한 초기 설정을 수행합니다. |
doFilter() | HTTP/S 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현 |
destroy() | 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출 |
2-2 간단한 필터 구현
간단하게 HTTP 요청을 로그로 남기는 필터를 만들어보겠습니다.
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("request [{}][{}]", uuid, requestURI);
chain.doFilter(request, response);
}catch (Exception e){
throw e;
}finally {
log.info("response [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
ServletRequest는 서블릿 컨테이너가 처리하는 모든 유형의 요청을 나타내는 인터페이스입니다. 그러나 실제 웹 애플리케이션에서 대부분의 요청은 HTTP 요청입니다. 따라서, HttpServletRequest httpRequest = (HttpServletRequest) request로 다운캐스팅하면 HTTP 요청에 대한 로그를 남길 수 있습니다.
코드에서 chain.doFilter(request, response)는 필터 체인에서 다음 필터를 호출하거나, 다음 필터가 없으면 서블릿을 호출하는 역할을 합니다. 이 메서드가 호출되지 않으면 요청이 다음 단계로 진행되지 않으므로, 필터 체인에서 다음 필터나 서블릿이 호출되지 않게 됩니다.
이제 이렇게 구현한 필터를 어떻게 애플리케이션에 등록하지는 알려드리겠습니다.
2-3 필터 등록 방법
1. @WebFilter 사용
@WebFilter는 특정 URL 사용이 가능하지만 필터가 실행되는 순서를 제어하는 기능을 제공하지 않습니다.
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("===init===");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("===doFilter===");
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("===destroy===");
}
}
@SpringBootApplication
@ServletComponentScan // 해당 애노테이션 등록
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
2. @Component 사용
@Component는 @Order를 사용해 우선 순위 제어가 가능하지만 특정 URL 패턴에 추가하는 기능이 매우 어렵습니다.
@Slf4j
@Component
@Order(2)
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("111init111");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("111doFilter111");
chain.doFilter(request, response);
}
@Override
public void destroy() {
log.info("111destroy111");
}
}
3. FilterRegistrationBean 사용
@Slf4j
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("log filter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("log filter doFilter");
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
String uuid = UUID.randomUUID().toString();
try {
log.info("request [{}][{}]", uuid, requestURI);
chain.doFilter(request, response);
}catch (Exception e){
throw e;
}finally {
log.info("response [{}][{}]", uuid, requestURI);
}
}
@Override
public void destroy() {
log.info("log filter destroy");
}
}
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<Filter> logFilter() {
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter());
filterRegistrationBean.setOrder(1);
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
FilterRegistrationBean은 Spring 애플리케이션에서 필터를 등록하는 방법 중 하나로 가장 보편적인 방법입니다. 실행 순서, URL 패턴 등등 필터가 적용되는 방법을 세부적으로 제어할 수 있습니다.
3. 인터셉터의 개념과 역할
Dispatcher Servlet에 도달하기 전에 요청과 응답을 처리하는 필터와 달리 인터셉터는 Spring Framework의 일부이며 Spring 컨텍스트 내에서 작동하여 컨트롤러에 도달하기 전과 후에 요청을 처리합니다. 기본적으로 인터셉터는 필터 및 Dispatcher Servlet 이후에 작동하여 컨트롤러 실행 중에 인증 확인이나 로깅과 같은 작업이 수행되도록 합니다.
예를 들어, 요청이 컨트롤러 메서드에 도달하기 전에 인증을 확인하거나, 응답 데이터를 로깅할 수 있습니다.
3-1 주요 메소드 (preHandle, postHandle, afterCompletion)
서블릿 필터 호출 시 단순하게 doFilter() 하나만 제공되지만, 인터셉터는 호출 전(preHandle), 호출 후(postHandle), 요청 완료 이후(afterCompletion)와 같이 세부적으로 제공합니다.
- preHandle: 요청이 컨트롤러에 도달하기 전에 실행되며(정확히는 핸들러 어댑터 호출 전), 응답값이 true이면 컨트롤러가 호출되고, false면 더 진행하지 않습니다.
- postHandle: 컨트롤러가 요청을 처리한 후 뷰가 렌더링되기 전에 실행
- afterCompletion: 요청이 완전히 처리되고 뷰가 렌더링된 후 호출
그럼 만약 컨트롤러에서 예외가 발생하면 어떻게 될까??
만약 컨트롤러가 어떤 예외를 발생했다면 앞에 설명했지만 preHandle()는 컨트롤러 호출 전에 실행 되니까 정상적으로 실행 될 것습니다. 그러면 postHandle(), afterCompletion은 호출이 안 될 것이다. 라고 생각하겠지만 afterCompletion은 예외와 무관하게 항상 실행됩니다 또한 예외 발생 시 afterCompletion은 예외 정보를 포함해서 호출됩니다.
3-2 간단한 인터셉터 구현
요청과 응답의 각 단계에서 로그를 남기고 코드입니다.
@Slf4j
public class LogInterceptor implements HandlerInterceptor {
public static final String LOG_ID = "logId";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
String uuid = UUID.randomUUID().toString();
request.setAttribute(LOG_ID, uuid);
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler; // 호출할 컨트롤러 메소드의 모든 정보를 포함
}
log.info("request [{}][{}][{}]", uuid, requestURI, handler);
return true; // 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 uuid = (String) request.getAttribute(LOG_ID);
log.info("response [{}][{}][{}]", uuid, requestURI, handler);
if (ex != null) {
log.error("afterCompletion error", ex);
}
}
}
인터셉터를 구현하면서 주의사항
인터셉터는 호출 시점이 완전히 분리되어 있습니다. 따라서 preHandle에서 지정한 값을 postHandle , afterCompletion에서 함께 사용하려면 어딘가에 담아두어야 합니다. LogInterceptor도 싱글톤 처럼 사용되기 때문에 맴버변수로 사용하면 위험합니다. 따라서 request에 담아두었다가 afterCompletion에서 request.getAttribute(LOG_ID) 로 찾아서 사용해야 합니다.
3-3 인터셉터의 등록 방법
WebMvcConfigurer를 통한 인터셉터 추가
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor())
.order(1)
.addPathPatterns("/**") // : 인터셉터를 적용할 URL 패턴을 지정
.excludePathPatterns("/css/**", "/*.ico", "/error"); // 인터셉터에서 제외할 패턴을 지정
}
}
서블릿 필터와 비교해보면 스프링 인터셉터는 addPathPatterns , excludePathPatterns 로 매우 정밀하게 URL 패턴을 지정할 수 있습니다.
패턴을 지정할 때 에는 아래에 있는 공식 문서를 참고 하는게 좋습니다.
PathPattern (Spring Framework 6.1.11 API)
Compare this pattern with a supplied pattern: return -1,0,+1 if this pattern is more specific, the same or less specific than the supplied pattern.
docs.spring.io
4. 필터와 인터셉터의 차이점 정리
필터 | 인터셉터 | |
동작 시기 | 서블릿 컨테이너 도달하기 전에 동작 | Spring Context에서 컨트롤러 도달하기 전에 동작 |
적용 범위 | 모든(특정) 요청과 응답에 대해 동작 | Spring 컨트롤러가 처리하는 요청과 응답 대상 |
사용 목적 | 주로 보안, 로깅, 데이터 변형 등에 사용 | 인증, 인가, 로깅 등에 사용 |
참고
'Spring' 카테고리의 다른 글
[Spring+DB] 트랜잭션 추상화 및 동기화- TransactionManager (0) | 2024.08.10 |
---|---|
[Spring]PathPattern 공식 문서 내용에 대한 예시 (0) | 2024.08.01 |
[Spring + Servlet] 파일 업로드 (1) | 2024.07.31 |
[Spring+Thymeleaf] Spring Converter 및 Formatter 정리 (0) | 2024.07.11 |
[Spring] 스프링이 제공하는 ExceptionResolver에 대하여 (0) | 2024.07.08 |