[Spring 프로젝트] Interceptor로 request, response body json 값 로깅하기

2022. 1. 24. 15:21 Spring Framework/Spring Core

Spring Logging (Interceptor로 Request, Response body json 값 로깅하기)

스프링 프로젝트를 하면서 기존에는 LoggingAspect를 만들어서 Aspect파일에서 parameter값과 body값을 찍어주고 있었다.

response 값도 찍어주기 위해 여러가지 찾아보면서 공부하던 와중에 Filter와 Interceptor, AOP의 구조를 다시 공부하는데 Interceptor의 인자로 HttpServletRequest, HttpServletResponse가 들어오는 것을 보고 response, request 로그를 Interceptor로 찍어야 하는 것이 맞는 구조라는 것을 그제야 깨달았다.

1. Filter, Interceptor, AOP 구조

아래 갓대희님 블로그에서 Filter, Interceptor, AOP 구조를 잘 설명해두어서 혹시 기억나지 않는다면 아래 블로그에서 다시 한번 공부해보자.


[이미지 출처] Filter, Interceptor, AOP 차이 - 갓대희님 블로그

Filter와 Interceptor는 그림과 같이 Servlet단위로 실행되고 있고, AOP는 더 내부에서 실행된다.

즉, response와 request는 servlet단위로 실행되기 때문에 그저 들어온 값과 나가는 값을 로그만 찍어주면 될 경우 aop보다는 interceptor에서 로그를 찍는게 더 올바른 구조로 보인다.

2. Interceptor에서 Request, Response Json Body 로그 찍기

(1) 필터 등록하기

우선 필터를 통해 HttpServletResponse, HttpServletRequest 클래스로 들어온 request와 response를 ContentCachingRequestWrapper와 ContentCachingResponseWrapper로 래핑해주어야 한다.

왜냐하면 HttpServletRequest 그대로 request.getReader 함수를 호출하거나 안에 있는 데이터를 읽으려고 하면, 단 한번만 읽을 수 있도록 톰캣에서 만들어두었기 때문에 이걸 다시 읽을 수 있는 클래스로 래핑해주어야 하기 때문이다.

Response도 동일하게, 안에 있는 Body값을 한번만 읽을 수 있게 해두었기 때문에 필터로 다시 읽을 수 있는 클래스로 래핑하지 않으면 사용자가 response값을 받지 못하는 참사가 일어날 수 있다.

wrappingResponse.copyBodyToResponse(); 이 부분이 핵심이다. 이걸 통해 body값을 copy해서 캐시로 저장해두기 때문에 다시 읽을 수 있다.

package com.bootproj.pmcweb.Common.Filter;

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

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class CustomServletWrappingFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappingResponse = new ContentCachingResponseWrapper(response);
        filterChain.doFilter(wrappingRequest, wrappingResponse);
        wrappingResponse.copyBodyToResponse();
    }
}

(2) LoggingInterceptor 파일 만들기

그 다음은 Logging Interceptor파일을 만든다.
이 파일에서는 request, response값이 application/json 타입 형식일 경우 로그를 찍어주는 로직을 넣어두었다.

interceptor를 만들 때에는 HandlerInterceptorAdapter를 상속받아 interceptor에서 제공하는 메서드를 오버라이드 해서 사용해야 한다.

아래에서는 request와 response를 한꺼번에 찍어주기 위해 afterCompletion 이 메서드를 오버라이드 하여 작성했는데 만약 따로 찍어주고 싶다면 preHandler, postHandler 메서드를 이용하여 각각 로그를 찍어주거나 별도 작업을 하여도 무방하다.

이때 request가 SecurityContextHolderAwareRequestWrapper인지 확인하는 것은, 시큐리티에서도 RequestWrapper를 통하여 @AuthenticationPrincipal를 통해 들어오는 user정보를 포함하는 등의 작업을 해주게 되는데 이 부분과 충돌이 나서 만약 이미 시큐리티에서 래핑한 클래스일 경우 다시 래핑하지 않도록 로직을 추가해두었다.
만약 시큐리티를 적용하지 않았다면, 이 부분은 제외해도 무방하다.

package com.bootproj.pmcweb.Common.Intercepter;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Log4j2
@RequiredArgsConstructor
@Component
public class LoggingInterceptor extends HandlerInterceptorAdapter {
    private final ObjectMapper objectMapper;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (request.getClass().getName().contains("SecurityContextHolderAwareRequestWrapper")) return;
        final ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request;
        final ContentCachingResponseWrapper cachingResponse = (ContentCachingResponseWrapper) response;

        if (cachingRequest.getContentType() != null && cachingRequest.getContentType().contains("application/json")) {
            if (cachingRequest.getContentAsByteArray() != null && cachingRequest.getContentAsByteArray().length != 0){
                log.info("Request Body : {}", objectMapper.readTree(cachingRequest.getContentAsByteArray()));
            }
        }
        if (cachingResponse.getContentType() != null && cachingResponse.getContentType().contains("application/json")) {
            if (cachingResponse.getContentAsByteArray() != null && cachingResponse.getContentAsByteArray().length != 0) {
                log.info("Response Body : {}", objectMapper.readTree(cachingResponse.getContentAsByteArray()));
            }
        }
    }
}

(3) Interceptor 등록하기

2번에서 만든 interceptor를 등록해준다. 이 때, 이미지 파일을 불러오는 것은 interceptor로 잡을 필요가 없기 때문에 제외해준다.

package com.bootproj.pmcweb.Config;

import com.bootproj.pmcweb.Common.Intercepter.LoggingInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    final LoggingInterceptor loggingInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loggingInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/vendor/**", "/css/*", "/img/*");
    }
}

(4) 결과

결과는 아래와 같다.
Logging Aspect로 찍힌건 이전 블로그 포스트에서 찍힌 것이므로 이전 포스트를 참고하자.

interceptor_logging

출처: https://hirlawldo.tistory.com/44?category=874134 [도비의 블로그]