Spring Boot 애플리케이션에서 AOP를 활용하여 API 요청과 응답을 로깅하는 기능을 추가하려고 했다.
기존 AOP 로깅 코드
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
private final HttpServletRequest request;
private final HttpServletResponse response;
@Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..))")
public Object loggingApiRequests(ProceedingJoinPoint joinPoint) throws Throwable {
// 요청 바디 로깅
String requestBody = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
System.out.println("요청 바디: " + requestBody);
// 컨트롤러 실행
Object result = joinPoint.proceed();
// 응답 바디 로깅
String responseBody = response.getWriter().toString();
System.out.println("응답 바디: " + responseBody);
return result;
}
}
request.getInputStream().readAllBytes()
로 요청 바디를 읽은 후,response.getWriter().toString()
으로 응답 바디를 읽은 후,request.getInputStream().readAllBytes()
를 사용했다.getInputStream()
은 한 번만 읽을 수 있으며, 이후 컨트롤러에서 다시 읽으려 하면 바디가 비어 있음.response.getWriter().toString()
으로 응답을 읽었다.getWriter()
역시 한 번만 사용할 수 있으며, AOP에서 사용한 후에는 클라이언트에게 응답이 전달되지 않음.getInputStream()
은 한 번만 읽을 수 있음HttpServletRequest.getInputStream()
은 한 번만 읽을 수 있도록 설계되어 있음.getWriter()
는 한 번만 사용할 수 있음HttpServletResponse.getWriter()
를 호출하면 스트림이 닫히며, 이후 다시 응답을 작성할 수 없음.getWriter().toString()
으로 응답을 읽어버리면, 클라이언트까지 전달되지 않음.CachedBodyHttpServletRequest
)if (request instanceof CachedBodyHttpServletRequest cachedRequest) {
requestBody = new String(cachedRequest.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
} else {
requestBody = "[요청 바디 캐싱 실패]";
}
CachedBodyHttpServletRequest
는 요청 바디를 한 번 읽어서 저장해두고, 이후에도 다시 읽을 수 있도록 함.ContentCachingResponseWrapper
)ContentCachingResponseWrapper
를 사용하여 응답을 캐싱하고, AOP에서 읽은 후에도 클라이언트에게 정상적으로 전달되도록 함.Object response = joinPoint.proceed(); // 컨트롤러 실행 이후 응답 바디 읽기
if (response instanceof ContentCachingResponseWrapper cachedResponse) {
byte[] responseBodyBytes = cachedResponse.getContentAsByteArray();
String responseBodyJson = responseBodyBytes.length > 0 ? new String(responseBodyBytes, StandardCharsets.UTF_8) : "없음";
}
ContentCachingResponseWrapper
를 사용하면 응답 바디를 저장한 후, 다시 읽을 수 있음.copyBodyToResponse()
를 호출해야 클라이언트에게 정상적으로 전달됨.BodyCachingFilter
) 추가@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest httpServletRequest && response instanceof HttpServletResponse httpServletResponse) {
CachedBodyHttpServletRequest cachedBodyHttpServletRequest = new CachedBodyHttpServletRequest(httpServletRequest);
ContentCachingResponseWrapper cachingResponse = new ContentCachingResponseWrapper(httpServletResponse);
chain.doFilter(cachedBodyHttpServletRequest, cachingResponse);
cachingResponse.copyBodyToResponse(); // 클라이언트로 응답 전달
} else {
chain.doFilter(request, response);
}
}
참고 자료
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/util/ContentCachingResponseWrapper.html
- https://leeeeeyeon-dev.tistory.com/51
- http://velog.io/@dhk22/Spring-AOP-%EA%B0%84%EB%8B%A8%ED%95%9C-AOP-%EC%A0%81%EC%9A%A9-%EC%98%88%EC%A0%9C-Logging
- https://passionfruit200.tistory.com/981
- https://f-lab.kr/insight/spring-aop-logging-validation-20240519