Spring Boot에서 Filter는 Web Application에서 관리되는 영역으로 커스텀 코드로 작성되어 들어오는 HTTP 요청을 처리할 수 있는 Spring Boot 프레임워크의 기능입니다. Client로 부터 오는 요청/응답에 대해 최초/최종 단계의 위치에 존재합니다. 이를 통해 요청/응답의 정보를 변경하거나, Spring에 의해 데이터가 변환되기 전의 순수한 클라이언트의 요청/응답 값을 확인할 수 있습니다. 유일하게 ServletRequest, ServletResponse의 객체 변환 가능
Spring Boot의 Filter는 쉽게 정의하고 사용가능하며 로깅 및 보안과 같은 일반적인 작업에 사용할 수 있는 Filter를 제공합니다.
이미지 출처 : https://nesoy.github.io/articles/2019-02/Spring-request-lifecycle-part-1
Request가 처음 들어왔을 때 처음으로 Filter를 거치고 DispathcerServlet -> Interceptor -> AOP동작이 되는 것을 확인할 수 있습니다. 즉, Filter, Interceptor, AOP를 다 실행하게 된다면 위의 그림과 같은 순서로 실행됩니다.
1.Logging
필터는 들어오는 request, response를 기록하는데 사용할 수 있어 디버깅 및 모니터링에 사용할 수 있습니다.
2.보안
필터는 request 진위 여부, 사용자에게 리소스 엑세스에 필요한 권한이 있는지 등과 같은 보안 검사를 수행하는데 사용될 수 있습니다. 또한 세션 생성, 유지, 무효화와 같은 사용자 세션을 관리하는데 사용할 수도 있습니다.
3.유효성 검사
request에서 데이터가 올바른 형식인지 확인하는 유효성 검사하는데 필터를 사용할 수 도 있습니다.
4.캐싱
자주 엑세스하는 리소스에 대한 응답을 캐시하여 서버의 부하를 줄여 이용자의 응답 시간을 단축할 수도 있습니다.
5. request, response 조작
헤더 추가, 응답 데이터 변환 및 조작하는데 필터를 사용할 수 있습니다.
dto, controller
@Data //getter + setter + toString
@NoArgsConstructor //default constructor
@AllArgsConstructor // all constructor
class User {
private String name;
private String email;
private int age;
}
@Slf4j // log 사용 가능
@RestController
@RequestMapping("/api")
public class ApiController {
@PostMapping("/user")
public User user(@RequestBody User user) {
log.info("user : {} ", user); //{}와 매칭이 가능하며 {}안에 객체가 들어간다.
return user;
}
}
GlobalFilter
@Slf4j
@Component
public class GlobalFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("전처리");
chain.doFilter(request, response);
log.info("후처리");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
log.info("init filter {}", filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
log.info("destroy filter");
}
}
Filter에 주요 메서드는 init, doFilter, destroy로 구성되어있습니다.
말 그대로 Global하게 사용하기 위해 @Component를 사용하였습니다. doFilter실행하기 전에 코드는 전처리 구간, 이후는 후처리 구간으로 나뉩니다. 실제로 해당 메서드를 실행하면 다음과 같은 결과가 나옵니다.
com.example.filter.filter.GlobalFilter : 전처리
c.e.filter.controller.ApiController : user : User(name=a1w1, email=aaaaa, age=10)
com.example.filter.filter.GlobalFilter : 후처리
doFilter를 기준으로 나뉘는 것을 확인할 수 있습니다. Filter를 사용하면서 여러가지로 기능을 구현할 있지만 간단하게 데이터를 조작해보겠습니다.
RefactFilter
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//길이만 초기화 , 전처리과정 내용을 담고 있지는 않다.
ContentCachingRequestWrapper httpServletRequest = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper httpServletResponse = new ContentCachingResponseWrapper((HttpServletResponse) response);
//doFilter 를 통해 실제 Spring 내부로 들어가야 content 담긴다.
//후처리는 항상 doFilter 이후에 처리 해야한다.
String url = httpServletRequest.getRequestURI();
String reqContent = new String(httpServletRequest.getContentAsByteArray());
log.info("request url : {}, requestBody : {}", url, reqContent);
chain.doFilter(httpServletRequest, httpServletResponse);
User user = new User("After", "user@naver.com", 123123);
String stringUser = mapper.writeValueAsString(user);
httpServletResponse.reset();
httpServletResponse.setContentType("application/json");
httpServletResponse.getWriter().write(stringUser);
String resContent = new String(httpServletResponse.getContentAsByteArray());
int httpStatusCode = httpServletResponse.getStatus();
log.info("response status : {}, responseBody : {}", httpStatusCode, resContent);
//ContentAsByte를 써서 리턴할 값을 다 뺀 상태이기 때문에 복사하여 반환해주어야한다.
httpServletResponse.copyBodyToResponse();
}
실행결과
com.example.filter.filter.GlobalFilter : request url : /api/user, requestBody :
c.e.filter.controller.ApiController : user : User(name=a1w1, email=aaaaa, age=10)
com.example.filter.filter.GlobalFilter : response status : 200, responseBody : {"name":"After","email":"user@naver.com","age":123123}
이전과 같은 request를 보낸것을 메서드에서 확인이 가능합니다. 후처리로 response값을 바꿔 보냈기 때문에 다른 값이 출력되는 것을 확인 할 수 있습니다.
만약 특정한 경우에만 지정하고 싶은 경우는 어떻게 해야 할까요?
@WebFilter(urlPatterns = "/api/") @Compoent를 제외하고 적용시키면 됩니다. 의미는 해당 URI로 호출되는 것들에 대해서만 Filter를 적용하겠다는 의미입니다. 특정한 URI에 해당되는 만큼 Application에 @ServletComponentScan 어노테이션을 사용합니다.
그렇다면 같은 조건의 필터가 만약 2개 이상이라면 작동이 어떻게 될까요?
TempFilter
@Slf4j
@WebFilter(urlPatterns = "/api/*")
public class TempFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
...
}
해당 메서드는 값을 이전과 다르게 변경하지 않고 그대로 리턴하게 됩니다.
실행 결과
com.example.filter.filter.TempFilter : request url : /api/user, requestBody :
com.example.filter.filter.GlobalFilter : request url : /api/user, requestBody :
c.e.filter.controller.ApiController : user : User(name=a1w1, email=aaaaa, age=10)
com.example.filter.filter.GlobalFilter : response status : 200, responseBody : {"name":"After","email":"user@naver.com","age":123123}
com.example.filter.filter.TempFilter : response status : 200, responseBody : {"name":"After","email":"user@naver.com","age":123123}
범위를 지정한 TempFilter가 먼저 실행되고 끝나는 것을 알 수 있다. 전역적으로 지정한 것보다는 구체적인 범위의 것을 먼저 실행하는 것을 알 수 있습니다.