[Spring] Http Request Body Logging Concepts

Http RequestBody 로깅처리하기

자바 진영에서 HttpServletRequest에서 bodyInputStream에 저장되어 스프링 어플리케이션이 한번 바디를 읽고 나면 버퍼가 비워져 읽어 올 수가 없기 때문에 이를 읽기 위해서는 다음과 같은 방법을 사용할 수 있다.

HttpServletRequest Wrapping

가장 간단한 방법으로는 HttpServletRequest를 랩핑하여 버퍼를 복제하는 방법이다. 특정 프레임워크에 종속되진 않지만 바디를 읽어오는데 번거로워 뭔가 코드가 깔끔한 느낌이 나지는 않는다.

다음과 같이 Wrapper를 통해 InputStream을 카피해두면 된다.

1
2
3
4
5
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
}

버퍼가 스프링 프레임워크에 읽히기 전에 먼저 읽어 버퍼를 복제해 한 후에 복제한 버퍼에서 바디를 읽으면 된다. 읽어올땐 다음과 같이 사용한다.

1
2
3
4
5
6
7
8
@ExceptionHandler(value = NullPointerException.class)
public ResponseEntity<String> handleNullPointerException(NullPointerException ex, HttpServletRequest req) throws IOException {
CustomHttpServletRequestWrapper customWrapper = new CustomHttpServletRequestWrapper(req);
String body = req.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
log.error("Request Body from custom wrapper [{}]", body);

return ResponseEntity.internalServerError().body(NPE);
}
  • 장점
    • 특정 프레임워크에 종속되지 않는 자바진영 코드로만 개발
    • 모든 요청에 대한 버퍼를 복사해야 하는 추가 IO 발생
  • 단점

    • 바디를 불러오기 위한 코드가 깔끔하지 못함
  • ref

RequestBodyAdvice

RequestBody를 읽고 난 후에 Advice를 통해서 읽은 요청 본문을 RequestScope빈에 저장해둔 후에 꺼내다 쓰는 방식이다.

1
2
3
4
5
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
contextHelper.setRequestBodyJsonString(gson.toJson(body));
return body;
}

RequestBodyAdviceContextHelper 빈에 바디를 저장했으므로 필요한 곳에서 꺼내 사용하면 된다.

1
2
3
4
5
6
@ExceptionHandler(value = IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex, HttpServletRequest req) {
log.error("Request Body from advice context [{}]", requestBodyAdviceContextHelper.getRequestBodyJsonString());

return ResponseEntity.internalServerError().body(IAE);
}
  • 장점
    • 스프링진영에서 공식적으로 지원하는 기능들을 사용한 RequestBody 추적
  • 단점

    • 모든 RequestBody에 대해서 Advice가 동작하는 추가 비용 발생
  • ref

RequestBodyHijacking

AOP를 통해 RequestBody의 로깅이 필요한 곳에만 선별적으로 요청 본문을 가져와 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Before("@annotation(com.example.requestbodylogging.app.annotation.RequestBodyHijacking)")
public void getRequestBody(JoinPoint pointCut) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
contextHelper.setRequest(request);

MethodSignature signature = (MethodSignature) pointCut.getSignature();
Method method = signature.getMethod();

Arrays.stream(method.getParameters())
.filter(p -> {
try {
return p.getAnnotation(RequestBody.class) != null;
} catch (Exception e) {
return false;
}
})
.findFirst()
.flatMap(p ->
Arrays.stream(pointCut.getArgs())
.filter(arg -> p.getType().equals(arg.getClass()))
.findFirst()
).ifPresent(arg -> contextHelper.setRequestBodyJsonString(gson.toJson(arg)));
}

이 또한 RequestScope 빈에 저장하므로 다음과 같이 필요에 꺼내 따라 사용할 수 있다.

1
2
3
4
5
6
@ExceptionHandler(value = ClassCastException.class)
public ResponseEntity<String> handleClassCastException(ClassCastException ex, HttpServletRequest req) {
log.error("Request Body from aop context [{}]", requestBodyAOPContextHelper.getRequestBodyJsonString());

return ResponseEntity.internalServerError().body(CCE);
}
  • 장점
    • 개발자가 의도한 영역에서만 RequestBody에 대한 추적이 가능
  • 단점

    • reflection을 통한 method signature parameterpointcut argumentclass type을 비교하므로 요청 모델 외에 동일한 클래스 타입이 메소드 시그니처에 있는 경우 의도하지 않은대로 동작 할 여지가 있음
      • 하지만 해당 케이스가 현 HTTP 스펙상 지원하지 않고 매우 드물기 때문에 문제가 없다고 판단됨
  • ref

결론

필요와 상황에 따라 각기 다른 방법을 사용해 RequestBody를 로깅할 수 있다.

개발한 소스코드는 여기에서 확인할 수 있습니다.

Author: Song Hayoung
Link: https://songhayoung.github.io/2022/01/01/Spring/request-body-logging/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.