개발/Spring

[Spring 프로젝트] AOP Logging (Post 메서드의 Json Body 값 로깅하기)

nova_dev 2020. 12. 4. 22:42
반응형

Logging AOP - Json으로 들어온 Request Body값 로깅하기

테스트 하다보니 기존에 추가했었던 로깅 Aspect 에서는 Parameter값을 로깅하는 것만 추가했어서 Post 메서드에서 들어오는 json body값을 로깅하고 있지 않았다.
그러다보니 Post 메서드에서 body값에 들어온 것을 확인하려고 팀원이 직접 컨트롤러나 서비스단에서 로그를 찍고 있었던 것을 발견하게 되었고, 이 부분을 개선하기 위해 Logging Aspect에 추가해두어야 겠다는 생각을 하게되었다.
기존 Logging AOP

기존 로깅 자료

기존의 로그는 위 포스트에서 정리했었는데, LogAspect 파일에서 실제 로깅 하는 부분은 아래와 같았다.

@Aspect
@Component
public class LoggingAspect {
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Pointcut("within(com.bootproj.pmcweb.Controller..*)")
    public void onRequest() {
    }

    @Around("com.bootproj.pmcweb.Network.Aspect.LoggingAspect.onRequest()")
    public Object requestLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        long start = System.currentTimeMillis();
        try {
            return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        } finally {
            long end = System.currentTimeMillis();
            logger.info("Request: {} {}: {} ({}ms)", request.getMethod(), request.getRequestURL(), paramMapToString(request.getParameterMap()), end - start);
        }
    }

    private String paramMapToString(Map<String, String[]> paraStringMap) {
        return paraStringMap.entrySet().stream()
                .map(entry -> String.format("%s : %s",
                        entry.getKey(), Arrays.toString(entry.getValue())))
                .collect(Collectors.joining(", "));
    }
}

Post 메서드 AOP 로깅 추가하기

처음에는 logger.info에 있는 부분만 body값을 request.getReader로 읽어와서 뿌려주면 끝날 거라고 생각하고 단순하게 접근을 했는데, 아래와 같은 에러가 나면서 로그를 찍지 못했었다.

if ("POST".equalsIgnoreCase(request.getMethod())){
    logger.info("Request: {} {}: {} ({}ms)", request.getMethod(), request.getRequestURL(), IOUtils.toString(request.getReader()), end - start);
} else {
    logger.info("Request: {} {}: {} ({}ms)", request.getMethod(), request.getRequestURL(), paramMapToString(request.getParameterMap()), end - start);
java.lang.IllegalStateException: getInputStream() has already been called for this request

이 부분은 검색해보니 request.getReader()에서 InputStream을 생성하는데, 이걸 tomcat에서 한번만 사용할 수 있도록 막아두어서, 한번 read한 body값은 다시 읽을 수 없게 되어 있었다.
이런 부분을 찾아보니 새로운 HttpServletRequestWrapper를 만들어서, getReader() 메서드를 오버라이드하고 새로운 InputStreamReader를 만들어서 반환하도록 한 뒤에 Filter를 통해 들어오는 request들을 새로 만든 HttpServletRequestWrapper로 변경하여 받아오는 해결할 수 있었다.
자세한 설명은 아래 블로그에서 더 자세하게 설명하고 있으니 읽어보도록 하자.
Spring-POST방식으로-전달된-JSON-데이터-처리하기

Post 메서드에서 들어온 body값을 logging에 추가해주려면 해야 하는 일은 아래와 같다.

1. HttpServletRequestWrapper 클래스를 상속받은 클래스 만들기

우선, 다시 읽을 수 있는 RereadableRequestWrapper를 만들어 준다. 여기서는 getReader()에서 getInputStream 메서드를 통해 새로운 InputStreamReader를 반환해 주는 로직이 핵심이다.

나중에 찾아보다가 알았는데, HttpServletRequest와 HttpServletResponse를 spring-web에서 이미 구현해둔 ContentCachingRequestWrapper와 ContentCachingResponseWrapper 클래스가 있기 때문에 직접 Wrapper를 만들지 않고 이미 만들어진 클래스를 사용해도 된다. 이미 있는 것을 사용하는 방법은 나중에 다른 포스트를 작성해 볼 것이다.

package com.bootproj.pmcweb.Common.Utils;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class RereadableRequestWrapper extends HttpServletRequestWrapper {

    private final Charset encoding;
    private byte[] rawData;

    public RereadableRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);

        String characterEncoding = request.getCharacterEncoding();
        if (StringUtils.isBlank(characterEncoding)) {
            characterEncoding = StandardCharsets.UTF_8.name();
        }
        this.encoding = Charset.forName(characterEncoding);

        // Convert InputStream data to byte array and store it to this wrapper instance.
        try {
            InputStream inputStream = request.getInputStream();
            this.rawData = IOUtils.toByteArray(inputStream);
        } catch (IOException e) {
            throw e;
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.rawData);
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding));
    }

    @Override
    public ServletRequest getRequest() {
        return super.getRequest();
    }
}

2. RequestFilter 만들기

여기서는 1번에서 생성했었던 들어오는 ServletRequest들에 대해 새로 생성한 RereadableRequestWrapper로 필터를 통해 바꾸어 주는 일을 한다.

package com.bootproj.pmcweb.Common.Utils;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class RequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        RereadableRequestWrapper rereadableRequestWrapper = new RereadableRequestWrapper((HttpServletRequest)request);
        chain.doFilter(rereadableRequestWrapper, response);
    }
}

3. HttpRequestConfig 만들기 (web.xml에서 필터를 설정해주는 것과 동일한 작업)

여기서는 2번에서 생성한 Filter를 등록해 주는 작업을 한다. 현재 프로젝트에서는 web.xml을 사용하는 방식이 아닌, Configuration을 사용하고 있기 때문에 아래와 같이 HttpRequestConfig를 만들어서 FilterRegisterationBean을 등록해 주었다.

import com.bootproj.pmcweb.Common.Utils.RequestFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

@Configuration
public class HttpRequestConfig {

    @Bean
    public FilterRegistrationBean reReadableRequestFilter(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new RequestFilter());
        filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
        return filterRegistrationBean;
    }
}

4. JsonUtil 만들기

여기서는 json 형식으로 유입된 HttpServletRequest를 string 형태로 return 해주는 일을 한다.

package com.bootproj.pmcweb.Common.Utils;

import lombok.extern.log4j.Log4j;
import lombok.extern.log4j.Log4j2;
import org.json.JSONObject;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;

@Log4j2
public class JsonUtils {

    public JsonUtils() {
    }
    // json 형식으로 유입된 HttpServletRequest를 string 형태로 return
    public  JSONObject readJSONStringFromRequestBody(HttpServletRequest request){
        StringBuffer json = new StringBuffer();
        String line = null;

        try {
            BufferedReader reader = request.getReader();
            while((line = reader.readLine()) != null) {
                json.append(line);
            }

        }catch(Exception e) {
            log.info("Error reading JSON string: " + e.toString());
        }

        JSONObject jObj = new JSONObject(json.toString());
        return jObj;
    }
}

5. Loggin Aspect 변경하기

기존 Logging Aspect에서 Post메서드인지 확인하고, post 메서드일 경우에 body값도 같이 찍어주는 로직을 추가해두었다.

package com.bootproj.pmcweb.Common.Aspect;

import org.apache.commons.io.IOUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

@Aspect
@Component
public class LoggingAspect {
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Pointcut("within(com.bootproj.pmcweb.Controller..*)")
    public void onRequest() {
    }

    @Around("com.bootproj.pmcweb.Common.Aspect.LoggingAspect.onRequest()")
    public Object requestLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        long start = System.currentTimeMillis();
        try {
            return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        } finally {
            long end = System.currentTimeMillis();
            logger.info("Request: {} {}: {} ({}ms)", request.getMethod(), request.getRequestURL(), paramMapToString(request.getParameterMap()), end - start);
            if ("POST".equalsIgnoreCase(request.getMethod())){
                logger.info("Body: {} ", IOUtils.toString(request.getReader()));
            }
        }
    }

    private String paramMapToString(Map<String, String[]> paraStringMap) {
        return paraStringMap.entrySet().stream()
                .map(entry -> String.format("%s : %s",
                        entry.getKey(), Arrays.toString(entry.getValue())))
                .collect(Collectors.joining(", "));
    }

}

참고자료

반응형