Project

숙련 CRUD 기반의 테스트, 개선 과제

JABHACK 2025. 1. 6. 12:16

다른 부분은 저번 과제의 연속이라 잘 해결이 되었지만,

package org.example.expert.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;

@Slf4j
@Aspect
@Component
public class AdminAPILoggingAspect {

    private final ObjectMapper objectMapper;

    public AdminAPILoggingAspect(ObjectMapper objectMappers) {
        this.objectMapper = objectMappers;
    }

    @Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.*(..)) || " +
            "execution(* org.example.expert.domain.user.controller.UserAdminController.*(..))")
    public Object logAdminApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        // 요청 및 응답 정보 로깅
        HttpServletRequest request = getHttpServletRequest();
        HttpServletResponse response = getHttpServletResponse();

        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);

        // 요청 본문 로깅
        String requestBody = new String(wrappedRequest.getContentAsByteArray());
        log.info("Admin API Request: Time={}, URL={}, UserId={}, Body={}",
                LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), requestBody);

        Object result = joinPoint.proceed(); // 실제 메서드 실행

        // 응답 본문 로깅
        String responseBody = new String(wrappedResponse.getContentAsByteArray());
        log.info("Admin API Response: Time={}, URL={}, UserId={}, Body={}",
                LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), responseBody);

        wrappedResponse.copyBodyToResponse(); // 응답 본문을 클라이언트에 전달

        return result;
    }

    private HttpServletRequest getHttpServletRequest() {
        // HttpServletRequest 가져오는 로직
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    private HttpServletResponse getHttpServletResponse() {
        // HttpServletResponse 가져오는 로직
        return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
    }
}

 

현재 짰던 코드를 확인해 보았지만, 정확히는 파악이 안되어서 여러가지 찾아본 결과 나온 코드이다. 하지만 아직  정확하게는 판단이 안서서 하나하나 분석해 보려고 한다.

 

 


1. 클래스 정의

@Slf4j
@Aspect
@Component
public class AdminAPILoggingAspect {
  • @Slf4j: 로깅을 위해 사용하는 Lombok 애노테이션. log.info, log.error 같은 로깅 메서드를 제공.
  • @Aspect: 이 클래스가 AOP 역할을 수행하는 클래스라는 의미.
  • @Component: Spring이 이 클래스를 Bean으로 관리

2. 생성자

public AdminAPILoggingAspect(ObjectMapper objectMappers) {
    this.objectMapper = objectMappers;
}
  • ObjectMapper: JSON 처리를 위한 Jackson 라이브러리 객체야. 요청이나 응답 본문을 JSON으로 직렬화하거나 역직렬화할 때 사용.

3. AOP 핵심: @Around

@Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.*(..)) || " +
        "execution(* org.example.expert.domain.user.controller.UserAdminController.*(..))")
public Object logAdminApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
  • @Around: 특정 메서드 실행 전후에 로직을 실행하도록 설정.
  • 여기서는 CommentAdminController와 UserAdminController 클래스의 모든 메서드에 AOP를 적용
  • ProceedingJoinPoint joinPoint: 실제 대상 메서드를 호출할 수 있는 객체. AOP 적용 메서드 전후로 추가 로직을 실행 가능.

4. 요청 및 응답 객체 가져오기

HttpServletRequest request = getHttpServletRequest();
HttpServletResponse response = getHttpServletResponse();
  • 현재 요청과 응답 객체를 가져오는 거야. getHttpServletRequest()와 getHttpServletResponse()는 아래에서 따로 정의돼 있음.

5. 요청 로깅

ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
String requestBody = new String(wrappedRequest.getContentAsByteArray());
log.info("Admin API Request: Time={}, URL={}, UserId={}, Body={}",
        LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), requestBody);
  • 요청 본문(Body)을 읽기 위해 ContentCachingRequestWrapper를 사용.
  • getContentAsByteArray()로 요청 데이터를 가져온 뒤 로그로 출력.
  • 로그에 포함된 정보:
    • 요청 시간 (LocalDateTime.now()).
    • 요청 URL (request.getRequestURI()).
    • 요청자 ID (request.getHeader("User-Id")).
    • 요청 본문 내용 (requestBody).

6. 대상 메서드 실행

Object result = joinPoint.proceed();
  • AOP가 적용된 원래 메서드를 실행.
  • 여기선 CommentAdminController나 UserAdminController의 메서드가 실행됨.

7. 응답 로깅

ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
String responseBody = new String(wrappedResponse.getContentAsByteArray());
log.info("Admin API Response: Time={}, URL={}, UserId={}, Body={}",
        LocalDateTime.now(), request.getRequestURI(), request.getHeader("User-Id"), responseBody);
  • 응답 본문을 로깅. ContentCachingResponseWrapper로 응답 데이터를 읽음.
  • 로그에 포함된 정보:
    • 응답 시간.
    • 요청 URL.
    • 요청자 ID.
    • 응답 본문 내용.

8. 응답 복원

wrappedResponse.copyBodyToResponse();
  • ContentCachingResponseWrapper가 데이터를 읽으면 스트림이 닫히기 때문에, 복원해서 클라이언트에 다시 보냄.

9. 요청/응답 객체 가져오기

private HttpServletRequest getHttpServletRequest() {
    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}

private HttpServletResponse getHttpServletResponse() {
    return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
}
  • RequestContextHolder를 사용해서 현재 요청(Request)과 응답(Response) 객체를 가져옴
  • AOP는 컨트롤러 메서드 앞뒤에서 동작하니까, 여기서 현재 요청 정보를 얻음

코드 전체 흐름

  1. 특정 관리자 API가 호출되면, AOP가 작동.
  2. 요청 정보를 가져와서 로깅.
  3. 원래 메서드를 실행 (joinPoint.proceed()).
  4. 응답 정보를 로깅.
  5. 요청과 응답 모두 클라이언트로 전달.