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(", "));
}
}
참고자료
'개발 > Spring' 카테고리의 다른 글
[Spring 프로젝트] Interceptor로 request, response body json 값 로깅하기 (1) | 2020.12.09 |
---|---|
[Spring 프로젝트] Annotation 동작 원리와 사용법 (0) | 2020.12.07 |
[Spring 프로젝트] Mybatis에서 Insert, Update Batch 처리하기 (0) | 2020.12.02 |
[Spring 프로젝트] Junit5 테스트 코드 작성하기 (Junit4와 차이점 정리) (0) | 2020.11.20 |
[Spring] 컴포넌트 스캔 (0) | 2020.11.17 |