[트러블 슈팅] AOP에서 사용된 요청/응답 값이 이후 단계에서 사용되지 못하는 문제

이규정·2025년 2월 26일
0

1. 문제 상황

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;
    }
}

문제 발생

  • AOP에서 request.getInputStream().readAllBytes()로 요청 바디를 읽은 후,
    컨트롤러에서는 요청 바디가 비어 있음.
  • AOP에서 response.getWriter().toString()으로 응답 바디를 읽은 후,
    클라이언트는 빈 응답을 받음.
  • 즉, AOP에서 한 번 읽은 요청/응답 값이 이후 단계에서 사라지는 현상 발생.

2. 사실 수집

(1) 요청 바디 문제

  1. AOP에서 요청 바디를 읽기 위해 request.getInputStream().readAllBytes()를 사용했다.
  2. 하지만 getInputStream()은 한 번만 읽을 수 있으며, 이후 컨트롤러에서 다시 읽으려 하면 바디가 비어 있음.
  3. 따라서 컨트롤러에서 정상적으로 데이터를 받을 수 없었다.

(2) 응답 바디 문제

  1. AOP에서 response.getWriter().toString()으로 응답을 읽었다.
  2. 하지만 getWriter() 역시 한 번만 사용할 수 있으며, AOP에서 사용한 후에는 클라이언트에게 응답이 전달되지 않음.
  3. 결과적으로 클라이언트는 빈 응답을 받았다.

3. 원인 분석

(1) 요청 바디 - getInputStream()은 한 번만 읽을 수 있음

  • HttpServletRequest.getInputStream()은 한 번만 읽을 수 있도록 설계되어 있음.
  • AOP에서 요청 바디를 읽으면, 이후 컨트롤러에서는 다시 읽을 수 없음.

(2) 응답 바디 - getWriter()는 한 번만 사용할 수 있음

  • HttpServletResponse.getWriter()를 호출하면 스트림이 닫히며, 이후 다시 응답을 작성할 수 없음.
  • AOP에서 getWriter().toString()으로 응답을 읽어버리면, 클라이언트까지 전달되지 않음.

4. 해결 방법

(1) 요청 바디 캐싱 (CachedBodyHttpServletRequest)

  • 요청 바디를 캐싱된 래퍼 클래스에 저장하여 여러 번 읽을 수 있도록 함.

변경된 코드 (요청 바디 캐싱 적용)

if (request instanceof CachedBodyHttpServletRequest cachedRequest) {
    requestBody = new String(cachedRequest.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
} else {
    requestBody = "[요청 바디 캐싱 실패]";
}
  • CachedBodyHttpServletRequest는 요청 바디를 한 번 읽어서 저장해두고, 이후에도 다시 읽을 수 있도록 함.

(2) 응답 바디 캐싱 (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()를 호출해야 클라이언트에게 정상적으로 전달됨.

(3) 캐싱 필터 (BodyCachingFilter) 추가

  • 요청과 응답을 캐싱하는 필터를 추가하여, 모든 요청이 AOP를 거치기 전 캐싱되도록 변경.

필터 추가

@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);
    }
}
  • 필터에서 요청을 캐싱된 객체로 변경하여 이후 모든 과정에서 사용 가능하게 함.
  • 응답도 캐싱하여 AOP 이후에도 클라이언트로 정상 전달되도록 처리.

참고 자료

profile
반갑습니다. 백엔드 개발자가 되기 위해 노력중입니다.

0개의 댓글

관련 채용 정보