[Spring] filter 를 통하여 Log 수정하기.

728x90

안녕하세요👋

오늘은 프로젝트에서 로그 메시지에 대한 내용을 수정하기 위한 작업을 해볼 예정입니다.

 

로그 메세지를 왜 수정해야 할까요?

 

이미 스프링부트 환경에서는 자동으로 로그메세지도 제공해주고,

Error 면 Error, Info 면 Info 알아서 나오지 않나요? 라는 생각을 할 수 있습니다.

 

하지만 로그도 하나의 코드라는 생각으로 스프링에서 제공해주는 일반적인 로그가 아닌

내가 알아보기 편한, 즉 가독성(=읽기 편한) 로그를 만들어주는 것 또한 중요하다고 생각합니다. 

 

실질적으로 서비스를 운영하면, 다양한 환경에서 Error 및 Info 로그를 체크해야 할 일이 많습니다.

 

그 로그들을 편리함,가독성, 팀원 과 공통적인 설정, 여러가지 보안적인 부분 

프로젝트 스펙을 클라이언트와 맞추기 위해서 로그 스펙을 설정할 필요가 있습니다.

 

물론 그냥 써도 문제되는 건 없지만, 로그 설정을 한다면 가독성이 좋아지고, 가독성이 좋아지면

나중에 유지보수 차원에서 1분 1초라도 더 효율적일 것이라고 생각합니다.

 

필터의 동작 과정은

 

위 처럼 Request -> Filter -> DispatcherServlet -> HandlerInterceptor -> Controller 까지 왔다가,

역순으로 다시 돌아가여 Response 를 주는 과정 입니다.

 

그리고 filter 는 SpringContext 가 아닌 톰캣과 같은 서블릿 컨테이너에 의해 관리가 된다.

그리고 DispatcherServlet 전/후에 처리가 되는 것입니다.

출처 : https://mangkyu.tistory.com/173

 

저희는 필터의 3가지 메소드 중에서 저희가 필요한 doFilter() 메소드만 다루어 볼 것입니다. 

코드에 들어가기전에 3가지 메소드의 역할만 간단하게 설명하고 본론으로 들어가겠습니다.

 

✅ init()

위 메소드는 필터 객체를 초기화하고 서비스에 추가하기 위한 메소드입니다. 

즉 서블릿 컨테이너(=톰캣)이 init() 을 호출하여 필터 객체를 초기화하면 이후의 요청들은 doFilter 를 통해 처리된다.

 

 

⭐️ doFilter() ⭐️

위 메소드는 url-pattern 에 맞는 모든 HTTP 요청이 DispatcherServlet 으로 전달되기 전에 서블릿 컨테이너에 의해 실행되는 메소드 입니다. doFilter() 파라미터로는 FilterChain 이 있는데 FilterChain 객체의 doFilter 메소드를 통해 다음 대상으로 요청을 전달한다.

FilterChain fc = new FilterChain();
fc.doFilter()

fc.doFilter() 전/후에 우리가 필요한 처리 과정을 넣어줌으로써 원하는 처리를 진행이 가능합니다.

 

 

✅ destroy()

destroy 메소드는 필터 객체를 서비스에서 제거히고 사용하는 자원을 반환하기 위한 메소드이다. 이는 서블릿컨테이너에 의해 1번 호출되고 이후에는 doFilter 를 통해 처리되지 않습니다.

 

이제 직접적인 코드를 보겠습니다.

 

 

LoggerFileter

package org.example.api.filter;

import java.io.IOException;
import java.util.Enumeration;

import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class LoggerFilter implements Filter {

	@Override
	public void init (FilterConfig filterConfig) throws ServletException {
		Filter.super.init(filterConfig);
	}

	// 우리가 수정할 부분은 이 부분 이다.
	@Override
	public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws
		IOException,
		ServletException {

		// request 내용을 한번 읽어버리면, 뒷단에서 다시 읽어버릴 수 없도록 설정이 되어있다.
		// 그것을 캐싱해주는 래퍼클래스가 있기 떄문에 그것을 설정해준다.
		ContentCachingRequestWrapper req = new ContentCachingRequestWrapper( (HttpServletRequest) request);
		ContentCachingResponseWrapper res = new ContentCachingResponseWrapper( (HttpServletResponse) response);

		// 파라미터로 넘어온 request 와 response 를 넣어주는게 아닌, 우리가 직접 만들어 둔 req,res 객체를 필터에 넘겨준다.
		// 객체가 넘어 갔기 때문에, 뒷단에 컨트롤러, 인터셉터 등 뒷단에서 받는 request 는 랩핑된 객체들이 넘아가게 된다.
		// 필터 같은 경우는 request 가 들어오면 doFilter 를 기준으로 위에 로직이 실행전, 아래 로직이 실행 후 response 가 나가는 로직이다.
		chain.doFilter(req,res);

		// 가장 좋은 방법은 요청이 들어왔을 때, 헤더정보와 바디 정보를 찍어준다 (실행 전)
		// 그러기 위해서는 ContentCachingRequestWrapper 말고 별도의 다른 객체를 만들어서 사용하는게 좋다.

		// 우리는 ContentCachingRequestWrapper 가 제공하는 이후의 단에서 로그를 남기겠다.(필터 실행 후)


		// request 정보
		Enumeration<String> headerNames = req.getHeaderNames(); // header Name
		StringBuilder headerValues = new StringBuilder();

		// key value 구조 이므로 headerName 에 대한 값을 찍어본다.
		headerNames.asIterator().forEachRemaining(headerKey -> {
			String headerValue = req.getHeader(headerKey);

			// authorization-token : ???, user-agent : ??? 이런 형식으로 찍히게 작성함
			headerValues.append("[")
						.append(headerKey)
						.append(" : ")
						.append(headerValue)
						.append(" , ")
						.append("] ");
		});

		// RequestBody 정보 -> 요청이 들어올 때 로그
		// Client --------------------------------------------------> Server
		String requestBody = new String(req.getContentAsByteArray());
		String requestURI = req.getRequestURI();
		String method = req.getMethod();
		log.info("Request Info : URI : {}, Method : {}, header :  {} , body : {}", requestURI, method, headerValues, requestBody); // toString 을 안하는 이유는 Logger 에서 자동으로 toString() 을 호출 해준다.


		// Response 정보
		StringBuilder responseHeaderValues = new StringBuilder();

		res.getHeaderNames().forEach(headerKey -> {
			String headerValue = res.getHeader(headerKey);

			// authorization-token : ???, user-agent : ??? 이런 형식으로 찍히게 작성함
			responseHeaderValues.append("[")
				.append(headerKey)
				.append(" : ")
				.append(headerValue)
				.append(" , ")
				.append("] ");
		});

		// ResponseBody 정보 -> 응답이 나갈 때 로그
		// Server --------------------------------------------------> Client
		String responseBody = new String(res.getContentAsByteArray());
		log.info("Response Info : URI : {}, Method : {}, header :  {} , body : {}", requestURI, method, responseHeaderValues, responseBody); // toString 을 안하는 이유는 Logger 에서 자동으로 toString() 을 호출 해준다.


		// ⭐️ 중요한게 이미 req,res 내용을 읽어버렸기 때문에 다시 초기화를 시켜 호출을 해줘야 한다.
		// 아래 로직을 쓰지 않으면 responseBody 가 비어져서 나옵니다.
		res.copyBodyToResponse();

	}

	@Override
	public void destroy () {
		Filter.super.destroy();
	}

}

 

 위 코드에 주석과 함께 모든 설명이 나와 있습니다.

 

일단 기본적으로 filter 를 수정시키기 위해선 Filter 인터페이스를 구현받아서 직접 수정을 했습니다.

 

Filter 인터페이스 안에는 3개의 구현메소드가 있는데, 그 중에서 저희가 필요한 건 doFilter 입니다. 

 

doFilter 는 클라이언트 요청이 오면 체인을 통해 컨테이너를 호출하여 실행 됩니다. 

위 메소드의 FilterChain 객체에 의해 요청, 응답이 필터링이 됩니다.

 

FilterChain 인터페이스에는 doFilter 라는 구현 메소드가 하나 있고, 그 안에 request 와 response 를 파라미터로 받고 있습니다. 

public interface FilterChain {

    /**
     * Causes the next filter in the chain to be invoked, or if the calling filter is the last filter in the chain,
     * causes the resource at the end of the chain to be invoked.
     *
     * @param request  the request to pass along the chain.
     * @param response the response to pass along the chain.
     *
     * @throws IOException      if an I/O error occurs during the processing of the request
     * @throws ServletException if the processing fails for any other reason
     */
    void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;

}

 

즉 chain 에 request 와 response 가 아 알맞게 있으면 리소스가 호출이 된다라는 의미 정도로 알고있으면 될 것같습니다.

 

( 위 코드에 주석으로 자세한 설명들이 나와 있습니다)

 

위 코드를 작성후 서버를 실행시키고 보면 API 호출을 해보면 

 

 

이런식으로 저희가 Custom 한 로그가 나오게 됩니다. 

 

현재는 그냥 API 호출시 요청 스펙 및 응답 스펙이 INFO 로 찍히게 됩니다.

나중에 필요에 의해서 INFO 말고 debug,error 등등 다양한 로그를 변형 시킬 수 있습니다.

 

 

참고 : https://mangkyu.tistory.com/173

728x90