Http RequestBody 로깅처리하기
자바 진영에서 HttpServletRequest
에서 body
는 InputStream
에 저장되어 스프링 어플리케이션이 한번 바디를 읽고 나면 버퍼가 비워져 읽어 올 수가 없기 때문에 이를 읽기 위해서는 다음과 같은 방법을 사용할 수 있다.
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 parameter
와 pointcut argument
의 class type
을 비교하므로 요청 모델 외에 동일한 클래스 타입이 메소드 시그니처에 있는 경우 의도하지 않은대로 동작 할 여지가 있음
- 하지만 해당 케이스가 현 HTTP 스펙상 지원하지 않고 매우 드물기 때문에 문제가 없다고 판단됨
ref
결론
필요와 상황에 따라 각기 다른 방법을 사용해 RequestBody
를 로깅할 수 있다.
개발한 소스코드는 여기에서 확인할 수 있습니다.