발단 : 요청 본문, 응답 본문에 대해서 로그를 남기고 싶었다. 하지만 내 생각만큼 그렇게 간단한 문제가 아니었다.
우선 AOP에서 그냥 요청 본문과 응답 본문을 읽을 경우, 후에 실제로 컨트롤러나 클라이언트가 데이터를 읽지 못하는 문제가 발생한다는 것이다.
HTTP 스트림은 한 번 읽으면 다시 읽을 수 없음!!
전개 : 이를 해결하기 위해 우리는 요청과 응답을 캐싱이 가능한 래퍼 객체로 감싸줘야 한다.
캐싱(Caching) : 복사본을 임시 저장소 (캐시)에 저장하여 원본 데이터에 접근하지 않고 빠르게 응답하는 기술
필터에서 In Filter
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 캐싱 가능한 래퍼로 감싸기
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
chain.doFilter(requestWrapper, responseWrapper);
// 중요: 응답 본문을 다시 복사해줘야 클라이언트가 응답을 받을 수 있음, 응답 본문을 복사하여 원래 객체로 응답하여 정상 작동
responseWrapper.copyBodyToResponse();
}
}
위기 : 하지만 여기서 주의사항이 두 가지 있다.
!!주의!! ContentCachingResponseWrapper의 메서드 copyBodyToResponse를 통해 응답 본문을 다시 복사하여 원래 객체로 응답해야 정상적으로 작동한다.
이 메서드를 호출하지 않으면 빈 응답이 반환된다..!
!!주의!! 요청 본문과 응답 본문 로그는 반드시 메서드 실행 이후 불러와야 한다.
요청에 관하여 ContentCachingRequestWrapper는 본문을 즉시 복사하는 게 아니고, 실제 스트림을 읽을 때, 바이트 배열로 복사한다.
결국 메서드 실행 전에 로그를 남길 경우 아무것도 남겨지지 않는다.
응답 본문은 당연히 메서드 실행 이후 응답이 생기니 실행 후에만 확인이 가능하다.
결말 : 안전하게 요청과 응답에 대한 본문을 로그로 남길 수 있게 filter에서 설정했으니 본격적인 AOP를 생성해보자
@Aspect
@Component
@Slf4j
public class AdminCheckAop {
@Around("execution(* org.example.main.user.service..*(..))")
public Object executionAdminCheck(ProceedingJoinPoint joinPoint) throws Throwable {
// 메서드 실행
Object result = joinPoint.proceed();
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
if (attrs instanceof ServletRequestAttributes sra) {
HttpServletRequest request = sra.getRequest();
HttpServletResponse response = sra.getResponse();
}
// 요청 본문
if (request instanceof ContentCachingRequestWrapper requestWrapper) {
byte[] requestBody = requestWrapper.getContentAsByteArray();
log.info("Request Body: {}", new String(requestBody, StandardCharsets.UTF_8));
}
// 응답 본문
if (response instanceof ContentCachingResponseWrapper responseWrapper) {
byte[] responseBody = responseWrapper.getContentAsByteArray();
log.info("Response Body: {}", new String(responseBody, StandardCharsets.UTF_8));
}
return result;
}
}
요청과 응답에 대한 본문을 로그로 한 번 남겨보았다.
이번 경험을 바탕으로 AOP에 대해서 많이 알아가는 유익한 시간이었다고 생각한다.
처음에는 요청과 응답 본문을 그냥 간단히 로그로 남기면 될 줄 알았는데, 그게 안된다고 하니까 막막했었다. 근데 점차 배울수록 재밌다고 생각이 든 경험이었다.
AOP이외에도 다양한 지식이 연계되어 배울 수 있는 점들이 많아서 그랬던 것 같다.
앞으로의 과정에 있어 도움이 많이 될 경험이었다고 생각한다.