포스팅 목적
- Filter에 대한 내용 확인
- Filter을 통해 Request, Response에 대한 Logging를 남기는 방법과 그 과정에서 발생한 오류들을 기록
- Filter는 Spring에서 공통작업을 처리할 수 있게 제공하는 기능 중 하나로써 DispatcherServlet앞에 위치해 HTTP 요청 전달 전후로 URL패턴에 맞게 공통 작업을 수행 할 수 있도록 한다.
-> 이를 통해서 요청과 응답에 대한 중앙집중식 처리가 가능하다.
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() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.여기서 궁금한 것이 doFIlter을 구현시에 매개변수로 ServletRequest 타입의 reqeust를 가지는데 왜 HttpServletRequest가 아닌 Servlet일까
예시
- 필터에서 HTTP에 특화된 처리가 필요할 때는 다음과 같이 ServletRequest를 HttpServletRequest로 캐스팅하여 사용할 수 있다.
- 이렇게 ServletRequest와 HttpServletRequest를 적절히 사용함으로써, 필터는 HTTP 요청뿐만 아니라 다른 프로토콜을 사용하는 요청에 대해서도 유연하게 대응할 수 있다.
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // HTTP 프로토콜에 특화된 처리가 필요한 경우 캐스팅을 사용한다. if (request instanceof HttpServletRequest) { HttpServletRequest httpRequest = (HttpServletRequest) request; // 여기서 HTTP 요청에 특화된 작업을 수행한다. } chain.doFilter(request, response); }
iii) Spring Framework에서의 FilterChain(필터체인)
- FilterChain은 서블릿 컨테이너가 관리하는 필터들의 연결고리로, 클라이언트 요청이 서블릿에 도달하기 전에 여러 필터를 순차적으로 거치게 하는 메커니즘
- YAML을 사용하여서 설정할 수 있으며 web.xml이나 @WebFilter 어노테이션이나 WebApplicationInitializer 인터페이스를 구현하는 방식을 통해서 설정할 수 있다.
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 {
log.info("log filter doFilter");
// ServletRequest에는 기능이 얼마 없어서 다운캐스팅 해줘야 함
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
// 요청을 구분하기 위해 uuid 생성
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");
}
}
chain.doFilter(request, response); (가장 중요❗️) 다음 필터가 있으면 필터를 호출하고, 필터가 없으면 서블릿을 호출한다. 만약 이 로직을 호출하지 않으면 다음 단계로 진행되지 않는다.
이 코드는 예제코드이며, 본문의 body를 읽지않기 때문에 ContentChachingReqeustWrapper 나 ContentChachingResponseWrapper를 사용하지 않는다.
ContentChachingReqeustWrapper ,ContentChachingResponseWrapper를 사용한다.package org.ITBridge.filter;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.annotations.Comment;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
@Component
@Slf4j
public class LoggerFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
var request = new ContentCachingRequestWrapper((HttpServletRequest) servletRequest);
var response = new ContentCachingResponseWrapper((HttpServletResponse) servletResponse);
// 이것 전에 body정보와 header정보를 찍어주어야 한다 -> 하지만 그렇게 하기 위해서는 contentclasshasing외에 캐싱해 줄 수 있는 클래스를 하나 생성해야한다
filterChain.doFilter(request,response);
var headernames = request.getHeaderNames();
var headervalues = new StringBuilder();
headernames.asIterator().forEachRemaining(headerKey-> {
var headervalue = request.getHeader(headerKey);
headervalues.append("[").append(headerKey).append(":").append(headervalue).append(",").append("]");
});
/// 요청 정보를 확인
var reqeustBody = new String(request.getContentAsByteArray());
var url = request.getRequestURL();
var method = request.getMethod();
log.info(">>>>>url : {}, method : {}, header : {} , body : {}", url, method,headervalues,reqeustBody);
// 응답 데이터 확인
var responseHeaderValues = new StringBuilder();
response.getHeaderNames().forEach(headerKey->{
var headerValue = response.getHeader(headerKey);
responseHeaderValues.append(headerKey).append(" :").append(headerValue).append(",");
});
var responseBody = new String(response.getContentAsByteArray());
log.info("<<<<<< url :{} , method : {} , header: {} , body: {}",url , method,responseHeaderValues,responseBody);
// 해당 코드 없으면 responser를 사용했기때문에 안내려감
response.copyBodyToResponse();;
}
}
response.copyBodyToResponse();; (중요❗️)
원래는 webconfig에 추가를 해주어야 하지만 @component어노테이션을 통해서 bean에 등록을 해놓았기 때문에 Spring Boot는 Servlet Filter 인터페이스를 구현한 클래스를 자동으로 감지하고 필터 체인에 등록
다만 필터 순서를 지정할떄는 추가해서 지정해야됨
//에제는
@Configuration
public class WebConfig {
@Bean
public FilterRegistrationBean<LoggerFilter> loggingFilter() {
FilterRegistrationBean<LoggerFilter> registrationBean = new FilterRegistrationBean<>();
// 필터 등록
registrationBean.setFilter(new LoggerFilter());
// 필터 순서 지정
registrationBean.setOrder(1);
// 특정 URL 패턴에만 적용
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}