Servlet Filter는 언제 필요할까?
여러 로직에서 공통으로 관심이 있는 있는 것을 공통 관심사(cross-cutting concern)라고 한다.애플리케이션을 개발 하다 보면 이러한 공통 관심사를 사용할 때가 많다.
- 쇼핑몰을 예를 들면 등록,수정,삭제 라는 기능을 로그인 한 사람만 사용할 수 있어야한다. 일일히 컨트롤러에서 조건문을 걸어주면 나중에 로그인 관련 로직이 변경되면 수정해야하는 부분이 많으므로 유지보수 측면에서도 좋지않다.
- 해결법은 공통 관심사로 나눈 후 공통으로 처리하면된다, 이러한 공통 관심사는 스프링의 AOP로도 해결할 수 있지만, 웹과 관련된 공통 관심사는 지금부터 설명할 서블릿 필터 또는 스프링 인터셉터를 사용하는 것이 좋다. 웹과 관련(특정 관련 url처리)된 공통 관심사를 처리할 때는 HTTP의 헤더나 URL의 정보들이 필요한데, 서블릿 필터나 스프링 인터셉터는 HttpServletRequest 를 제공한다.
필터 흐름
- HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
필터를 적용하면 필터가 호출 된 다음에 서블릿이 호출된다. 그래서 모든 고객의 요청 로그를 남기는 요구사항이 있다면 필터를 사용하면 된다. 참고로 필터는 특정 URL 패턴에 적용할 수 있다. /* 이라고 하면 모든 요청에 필터가 적용된다. 참고로 스프링을 사용하는 경우 여기서 말하는 서블릿은 스프링의 디스패처 서블릿으로 생각하면 된다.
필터 사용시
- [로그인 사용자]: HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 컨트롤러
- [비 로그인 사용자]: HTTP 요청 -> WAS -> 필터(적절하지 않은 요청이라 판단, 서블릿 호출X)
필터에서 적절하지 않은 요청이라고 판단하면 거기에서 끝을 낼 수도 있다. 그래서 로그인 여부를 체크하기에 딱 좋다.
필터 체인
- 위 사진을 보면 클라이언트측에서 전달받은 정보를 컨트롤러로 전달시켜주는 Dispatcher Servlet에 들어오기전에 필터에 거쳐서 들어온다. 또한 필터는 하나가 아닌 여러개의 필터를 적용 시킬수 있는데 이런 여러개의 필터를 필터 체인이라한다.
필터는 체인으로 구성되는데, 중간에 필터를 자유롭게 추가할 수 있다. 예를 들어서 로그를 남기는 필터를 먼저 적용하고, 그 다음에 로그인 여부를 체크하는 필터를 만들 수 있다.
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() {}
}
필터 인터페이스를 구현하고 등록하면 서블릿 컨테이너가 필터를 싱글톤 객체로 생성하고, 관리한다.
- init(): 필터 초기화 메서드, 서블릿 컨테이너가 생성될 때 호출된다.
- doFilter(): 고객의 요청이 올 때 마다 해당 메서드가 호출된다. 필터의 로직을 구현하면 된다.
- destroy(): 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.
서블릿 필터 - 요청 로그
package hello.login.web.filter;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@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 {
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");
}
}
- 필터를 사용하려면 필터 인터페이스를 구현(LogFilter)해야 한다.
- ServletRequest는 부모 인터페이스이다. 기능이 없어서 여러 HTTP기능을 사용하려면 HttpServletRequest로 사용해야한다. 그럴려면 위처럼 전달받은 ServletRequest 를 HttpServletRequest 를 생성 후 ServletRequest를 HttpServletRequest로 형변환 시켜줘야한다.
- 가장 주의해야할 점은 chain.doFilter(request,response) 이 부분이 말그대로 필터 체인을 실행한다. 다음 필터가 있으면 다음 필터로 넘겨주고 없을시에는 servlet 에 넘겨주며 로직을 실행시킨다. 이 함수가 없으면 servlet으로 넘어가지 않는다.
package inflearn.springmvc2;
import inflearn.springmvc2.web.LogFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean logFilter(){
FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(new LogFilter()); // 등록할 필터 지정
filterRegistrationBean.setOrder(1); // 필터 순서 지정
filterRegistrationBean.addUrlPatterns("/*"); // 필터 적용할 url 지정
return filterRegistrationBean;
}
}
- 위 방법은 스프링 부트를 사용해서 FilterRegistrationBean을 사용해서 등록했다.
- 위처럼 필터를 빈으로 등록해줘야 작동한다. 없을시에는 필터가 작동하지않는다.
결론
- 위처럼 LogFilter 클래스를 작성하고 필터를 설정한 후 /v1/items로 request를 날리면 LogFilter 클래스에 작성한것과 같이 밑처럼 reqeust/response 로그가 잘찍히는걸 확인할 수 있다.
- REQUEST [cfcb37cf-43b1-466e-a68f-68b079f61f0a][/v1/items]
- RESPONSE [cfcb37cf-43b1-466e-a68f-68b079f61f0a][/v1/items]
- 웹과 관련된 공통 처리가 필요할때 AOP 보단 filter 를 사용하면 좋다.filter는 클라이언트가 요청을 보내면 필터를 거친다 그 후에 servlet을 통해 controller에 사용자의 요청을 전달 후 처리한다.로그 필터를 구현해준후 설정파일을 통해 로그를 빈 등록을 해줘야 필터가 작동한다. 작동을 하면 설정한 url을 작동할때마다 위 결과처럼 설정한 포맷대로 출력된다.
reference
- 김영한님의 인프런강의[스프링 MVC 패턴 2]
'Java' 카테고리의 다른 글
@Converter를 사용하여 Enum값 활용하기 (0) | 2023.08.23 |
---|---|
[Spring] 싱글톤 컨테이너란? (3) | 2023.03.13 |
[Spring] 예외처리(ExceptionHandling)하는 방법 (0) | 2023.03.12 |
[JAVA] JUnit 5 사용해보자! (0) | 2023.02.27 |
[Spring] @Autowired를 사용하지말고 다양한 의존성 주입을 알아보자 (0) | 2023.01.25 |