-
[Spring Boot] request, response 로그 남기기1 - Console개발기록 2022. 9. 13. 16:15반응형728x90
※ 트러블슈팅
- 문제 발생: 파라미터가 로깅에 사용되어 사라짐
- 문제 발생 예상 파일: RequestResponseWrapperFilter.java
- 해결(미봉책): https://jeongwoo.tistory.com/59
개요
api로 들어오는 request와 response에 대해 로그를 남기기 위해 코드를 작성하던 중 기록을 남긴다.
위 이미지는 Request와 Response의 흐름도이다. Filter를 거친 후 Dispatcher Servlet을 통과하게 된다.
Filter ~ Dispatcher Servlet ~ Interceptor 순으로 실행되는 것을 알아두고 시작하도록 하자.
참고 및 출처:
https://www.baeldung.com/spring-http-logging
https://hyunsoori.tistory.com/m/12
Http Logging은 request와 response를 커스터마이징하는 방법과 SpringBoot에서 지원하는 SpringBoot Built-In request Logging이 있다.
하지만 후자의 경우 response에 대해서는 지원해주지 않기때문에 우리는 커스터마이징하는 방법을 사용하도록 한다.
개발 순서 및 코드
커스터마이징할 CachingRequestWrapper 클래스에서 HttpServletRequestWrapper 클래스를 상속받기 위해 build.gralde 파일에 spring-boot-starter-web에 관한 dependency를 추가한다.
※ ' compileOnly javax.servelt:servlet-api:2.5'로 dependency 추가할 경우, CachingRequestWrapper.java에서 ServletInputStream을 상속 받아서 사용할 때 isFinished(), isReady(), setReadListener(ReadListener listener)를 오버라이드할 수 없는 이슈가 발생한다.
※ 다른 모듈에서 쓸 경우 implementation 말고 api 'org.springframework.~~~'로 써주면 된다.
1. build.gradle
// HttpServletRequestWrapper api 'org.springframework.boot:spring-boot-starter-web'
CachingRequestWrapper.java에서
cf) 아래에 해당 파일 완성 코드가 있다.
package com.??.core.interceptor; import javax.servlet.http.HttpServletRequestWrapper; public class CachingRequestWrapper extends HttpServletRequestWrapper { }
상속만 받으면 컴파일 에러가 뜬다.
이를 해결하기 위해선 생성자를 작성해줘야한다.
/* public CachingRequestWrapper(HttpServletRequest request) { super(request); } */
이렇게만 작성해도 컴파일 에러가 해결된다.
하지만 우리는 커스터마이징할 것이기때문에 참고한 블로그대로 아래와 같이 작성한다.
이때 IOUtils.toByteArray()를 사용하기 위해 build.gradle에 dependency를 추가해줬다.
// IOUtils.toByteArray() 사용하기 위해 필요한 dependency // https://mvnrepository.com/artifact/commons-io/commons-io api 'commons-io:commons-io:2.5'
참고했던 블로그 코드를 그대로 쓸 경우 StringUtils의 isEmpty()가 deprecated 되어서 밑줄 쳐져있을 것이다.
따라서 대체할 수 있는 hasLength나 hasText 중 hasText()를 사용했다.
참고: https://creampuffy.tistory.com/120
2. CachingRequestWrapper.java
request와 response에 대해서 Wrapper Class를 만드는 이유는 HttpServletRequest의 InputStream은 오직 한 번만 읽을 수 있기 때문이라고 한다.
따라서 요청했던 데이터를 캐싱하여 여러 번 읽을 수 있도록 하기 위해 Wrapper Class를 만들어 사용한다.
출처: https://hyunsoori.tistory.com/m/12
package com.??.core.filter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.springframework.util.StringUtils; import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.*; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @Slf4j public class CachingRequestWrapper extends HttpServletRequestWrapper { // private final Charset encoding; private byte[] rawData; public CachingRequestWrapper(HttpServletRequest request) throws IOException { super(request); /* 쓰레기 코드 String characterEncoding = request.getCharacterEncoding(); if (StringUtils.hasText(characterEncoding)) { characterEncoding = StandardCharsets.UTF_8.name(); } this.encoding = Charset.forName(characterEncoding); */ try (InputStream inputStream = request.getInputStream()) { this.rawData = IOUtils.toByteArray(inputStream); } } @Override public ServletInputStream getInputStream() { return new CachingRequestWrapper.CachedServletInputStream(this.rawData); } @Override public BufferedReader getReader() { // return new BufferedReader(new InputStreamReader(this.getInputStream(), this.encoding)); return new BufferedReader(new InputStreamReader(this.getInputStream())); } private static class CachedServletInputStream extends ServletInputStream { private final ByteArrayInputStream buffer; public CachedServletInputStream(byte[] contents) { this.buffer = new ByteArrayInputStream(contents); } @Override public int read() throws IOException { return buffer.read(); } @Override public boolean isFinished() { return buffer.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException("not support"); } } }
3. CachingResponseWrapper.java
package com.??.core.filter; import org.apache.commons.io.output.TeeOutputStream; import org.springframework.util.FastByteArrayOutputStream; import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.*; public class CachingResponseWrapper extends HttpServletResponseWrapper { private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024); private ServletOutputStream outputStream; // private PrintWriter writer; // 쓰레기 코드 public CachingResponseWrapper(HttpServletResponse response) { super(response); } @Override public ServletOutputStream getOutputStream() throws IOException { if (this.outputStream == null) { this.outputStream = new CachingResponseWrapper.CachedServletOutputStream(getResponse().getOutputStream(), this.content); } return this.outputStream; } /* 쓰레기 코드 @Override public PrintWriter getWriter() throws IOException { if (writer == null) { writer = new PrintWriter(new OutputStreamWriter(content, this.getCharacterEncoding()), true); } return writer; } */ public InputStream getContentInputStream() { return this.content.getInputStream(); } private class CachedServletOutputStream extends ServletOutputStream { private final TeeOutputStream targetStream; public CachedServletOutputStream(OutputStream one, OutputStream two) { targetStream = new TeeOutputStream(one, two); } @Override public void write(int arg) throws IOException { this.targetStream.write(arg); } @Override public void write(byte[] buf, int off, int len) throws IOException { this.targetStream.write(buf, off, len); } @Override public void flush() throws IOException { super.flush(); this.targetStream.flush(); } @Override public void close() throws IOException { super.close(); this.targetStream.close(); } @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener writeListener) { throw new UnsupportedOperationException("not support"); } } }
4. RequestResponseWrapperFilter.java
상속받는 OncePerRequestFilter는 "단 한 번만 처리가 수행되도록 보장"해주는 필터라고 한다.
출처: https://hyunsoori.tistory.com/m/12
package com.??.core.filter; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Slf4j @Component public class RequestResponseWrapperFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (isAsyncDispatch(request)) { filterChain.doFilter(request, response); } else { filterChain.doFilter( new CachingRequestWrapper(request), new CachingResponseWrapper(response) ); } } }
5. HttpLogInterceptor.java
Interceptor에는 preHandle, postHandle, afterCompletion과 같이 3개의 메소드가 있다.
preHandle
- 컨트롤러의 핸들러 메서드를 실행하기전에 호출
- 핸들러 메서드가 호출되지 않게 하고 싶을 때 메서드 반환값으로 false
postHandle
- 컨트롤러의 핸들러 메서드가 정상적으로 종료된 후에 호출
- 핸들러 메서드에서 예외가 발생하면 호출 안됨
afterHandle
- 컨트롤러의 핸들러 메서드의 처리가 종료된 후에 호출
- 예외가 발생해도 호출출처: https://hyunsoori.tistory.com/m/12
참고했던 블로그 코드를 그대로 쓸 경우 HanlerInterceptorAdapter가 deprecated 되어서 밑줄 쳐져있을 것이다.
HandlerInterceptor나 AsyncHandlerInterceptor를 implement하라고 명시되어있다.
따라서 우리는 HandlerInterceptor를 implement를 해서 사용하자.
참고: https://oingdaddy.tistory.com/399
또한
String res = IOUtils.toString(((ChildCachingResponseWrapper) response).getContentInputStream(), response.getCharacterEncoding());
위 코드를 블로그처럼 그대로 쓰면 ISO-8859-1으로 인코딩 되어 있기 때문에 한글이 깨진다.
이를 해결하기 위해 StringWriter 클래스와 IOUtils 클래스의 copy 메소드와 UTF-8을 하드코딩하여 사용했다.
참고: https://tiqndjd12.tistory.com/25
package com.??.core.interceptor; import com.fasterxml.jackson.databind.ObjectMapper; import com.??.core.filter.CachingRequestWrapper; import com.??.core.filter.CachingResponseWrapper; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.InputStream; import java.io.StringWriter; @Slf4j @Component public class HttpLogInterceptor implements HandlerInterceptor { private final ObjectMapper objectMapper; public HttpLogInterceptor(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request instanceof CachingRequestWrapper) { log.info("uri: " + request.getRequestURI()); String req = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding()); // 특수문자들이 URL 인코딩되어 있기때문에 URL 디코더를 사용 log.info("request - {}", URLDecoder.decode(req, request.getCharacterEncoding())); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { if (response instanceof CachingResponseWrapper) { InputStream tmpIs = ((CachingResponseWrapper) response).getContentInputStream(); StringWriter writer = new StringWriter(); // IOUtils.copy(tmpIs, writer, response.getCharacterEncoding()); // ISO-8859-1이기때문에 UTF-8로 하드코딩 IOUtils.copy(tmpIs, writer, "UTF-8"); String res = writer.toString(); log.info("response - {}", res); } } }
6. WebMvcConfig.java
WebMvcConfig에서 HttpLogInterceptor httpLongInterceptor를 final로 선언하면 컴파일 에러가 떠서 @Autowired로 선언했다.
또한 모든 요청에 대해서 Interceptor가 request를 가로채서 로깅하도록 PathPattern을 "/**"로 설정했다.
package com.??.core.config; import com.??.core.interceptor.HttpLogInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @ComponentScan(basePackages={"com.??.core.interceptor"}) public class WebMvcConfig implements WebMvcConfigurer { @Autowired private HttpLogInterceptor httpLogInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(httpLogInterceptor) .addPathPatterns("/**"); } }
포스트맨을 통해서 request를 보내면 아래와 같이 이제 콘솔에 로그가 찍힌다.
이때 HttpLogInterceptor.java의 preHandle 메소드에서 URL 디코더를 해주지 않을 경우, request에 담긴 데이터들 중 특수문자들은 아래처럼 URL 인코딩된 채로 출력된다.
내가 수정한 코드처럼 URLDecoder를 해주면 아래와 같이 이를 해결할 수 있다.
참고: https://arabiannight.tistory.com/151
728x90'개발기록' 카테고리의 다른 글
[Spring Boot] Jasypt를 통한 암복호화 - application.properties에서 db 암호화 (0) 2022.09.21 [Spring Boot] Gradle 프로젝트에 외부 라이브러리(*.jar) 추가하는 방법 (0) 2022.09.21 [Spring Boot] 스케쥴러를 이용한 스케쥴링(Scheduler) (0) 2022.09.19 [Spring Boot] request, response 로그 남기기2 - logback(파일로 로그 남기기) (1) 2022.09.14 [Spring Boot] application.properties에서 *.java로 property 가져오기 (0) 2022.09.06