[SpringBoot] Filter & Filter를 통한 요청&응답 Logging

이서영·2024년 12월 17일

SpringBoot

목록 보기
6/7

포스팅 목적

  • Filter에 대한 내용 확인
  • Filter을 통해 Request, Response에 대한 Logging를 남기는 방법과 그 과정에서 발생한 오류들을 기록

I) Filter

  • Filter는 Spring에서 공통작업을 처리할 수 있게 제공하는 기능 중 하나로써 DispatcherServlet앞에 위치해 HTTP 요청 전달 전후로 URL패턴에 맞게 공통 작업을 수행 할 수 있도록 한다.
    -> 이를 통해서 요청과 응답에 대한 중앙집중식 처리가 가능하다.

i) Filter의 패키지

  • Filter는 인터페이스로 되어있으며, Servlet에 속해있다.
    -> 스프링과 무관하게 전역적으로 처리해야하는 작업을 처리 가능

ii) Filter의 인터페이스 및 메소드

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() : 필터 종료 메서드, 서블릿 컨테이너가 종료될 때 호출된다.

2-1) ServletRequest를 사용하는 이유

여기서 궁금한 것이 doFIlter을 구현시에 매개변수로 ServletRequest 타입의 reqeust를 가지는데 왜 HttpServletRequest가 아닌 Servlet일까

  • 프로토콜의 독립성
    • ServletRequest 인터페이스는 HTTP프로토콜 뿐만 아니라 다양한 프로토콜에 대한 요청을 처리할 수 있는 유연성을 제공한다.
      -> 이를통해 필터가 HTTP, FTP등의 프로토콜을 사용하는 요청을 동일한 방식으로 처리 가능
  • HttpServletRequest의 확장
    • HttpServletReqeust는 ServletRequest를 확장한 인터페이스이기에 HTTP프로토콜에 특화된 추가기능을 제공한다. 이를 통해서 HTTP 요청해데, 쿠키, body에 접근가능

예시

  • 필터에서 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 인터페이스를 구현하는 방식을 통해서 설정할 수 있다.

II) Filter을 사용한 logging


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를 읽지않기 때문에 ContentChachingReqeustWrapperContentChachingResponseWrapper를 사용하지 않는다.

III) logging Filter로 body데이터 읽기

  • 위의 코드는 간단한 URL만 읽는 것만 할 수 있기에 조금 더 Deep하게 본문의 Body와 header를 읽을 수 있도록 ContentChachingReqeustWrapper ,ContentChachingResponseWrapper를 사용한다.
    • 각각 이것은 body읽어올 수 있도록 해주는 캐싱 클래스이다
    • 하지만 요청의 InputStream 또는 Reader가 실제로 한 번 읽혀야만 캐싱을 시작한다. -> 즉 filterChain.doFilter()를 통해 Controller 또는 다른 필터가 요청 데이터를 읽어야 ContentCachingRequestWrapper가 캐싱을 완료
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;
        }
profile
전공자 학생

0개의 댓글