Log

(AOP) 메세지 큐 요청/응답 로깅

땍땍 2025. 5. 11. 16:37

이전 Filter 를 활용하여 Http 요청과 응답 값에 대한 로깅을 진행했었다.

하지만, RabbitMQ 와 Kafka 등을 활용하게 되며 Http 요청 이외의 값에도 로깅이 필요하게 되었고, 최종적으로 AOP 를 활용해 이러한 코드를 구현하게 되었다.

( 로깅을 위해서는 Filter ,Interceptor 를 활용할 수도 있으나, 이들은 Http 요청만을 가로챌 수 있다는 문제가 존재해 선택하지 않았다. )

AOP 에서는 구현한 코드(메소드)를 어떤 시점에 작동시킬지 아래와 같은 메소드를 활용해 지정할 수 있다.

Before 메소드를 실행하기 전 작동

After 메소드를 실행 후 작동
AfterReturn 메소드가 [정상] 실행 후 작동 ( 반환 값을 가져올 수 있음 )
AfterThrowing 메소드가 예외 처리 되었을때 작동
Around 메소드의 전/후 작동

이중, Before/AfterReturn 을 활용해도 Around 를 사용해도 동일한 결과를 낼 수 있겠지만

이번에는 요청/응답 값에 식별을 위한 고유한 키를 부여하기 용이한 Around 를 선택해 코드를 구현하게 되었다.

== 구현 ==

우선, 클래스를 생성하고 Aspect/Component 어노테이션을 활용해 AOP 로 작동하도록 지정한다.

@Aspect
@Component
public class LogAspect {

}

이후 해당 AOP 가 작동할 환경을 지정할 수 있는데, 이번에는 메세지 큐를 활용한 리스너를 대상으로 하기에 어노테이션을 기준으로 작동하도록 하였다.

@Aspect
@Component
public class LogAspect {

  @Pointcut("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener)")
  public void rabbitListenerMethods() {}

}

메소드 실행 전/후에 작동하도록 하는 Around 어노테이션은 필수적으로 ProceedingJoinPoint 라는 매개변수를 갖고 있어야한다.

이때, 요청 값이 DTO 와 같은 객체일 경우 매개변수에서 요청 값을 추출하기 위해서는 별도의 조치가 필요하다.

가장 간단한 방법으로는 해당 객체(DTO) 를 바로 읽을 수 있도록 ToString 어노테이션 혹은 Data 어노테이션을 추가하는 방안이다.

이외에도 Reflection 을 활용한 방법 등도 존재하지만, 이번에는 ObjectMapper 라이브러리를 활용해 Json 으로 변환 시켜 이를 출력하도록 하였다.

추가적으로 이후 응답 값을 추출하는 과정에서 Object가 null 일 경우 오류가 발생하기에, 이를 위한 메소드도 함께 작성해주었다.

public class LogMapper {

  private final ObjectMapper objectMapper;

  public LogMapper() {
    this.objectMapper = new ObjectMapper();
    this.objectMapper.registerModule(new JavaTimeModule());
  }

  public String convertToJson(Object object) {
    try {
      return objectMapper.writeValueAsString(object);
    } catch (Exception e) {
      throw new RuntimeException("Json 변환 실패 : " + e.getMessage());
    }
  }

  public String converToString(Object object) {
    return (object == null) ? null : object.toString();
  }
}

다음은 로그를 출력하기 위한 별도의 클래스를 생성한다.

( 하나의 클래스 내에서 관리해도 무관 )

@Slf4j
public class LogPrinter {

  private final LogMapper logMapper = new LogMapper();

  public void printRequestLog(String methodName, GenericMessage<?> message, String uuid) {
    String payloadJson = logMapper.convertToJson(message.getPayload());
    printLog(methodName, uuid, payloadJson);
  }

  public void printResponseLog(Object result, String methodName, String uuid) {
    String value = logMapper.converToString(result);
    printLog(methodName, uuid, value);
  }

  private void printLog(String methodName, String uuid, String value) {
    log.info("[{}:{}}] {}", methodName, uuid, value);
  }

}

마지막으로 로깅을 위한 로직을 작성해주면 끝이다.

  @Around("rabbitListenerMethods()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

    // 고유값 생성
    String uuid = UUID.randomUUID().toString();

    // 메소드명 추출
    String methodName = joinPoint.getSignature().getName();

    // 요청 값 로깅
    logMessagePayload(joinPoint, methodName, uuid);

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

    // 응답 코드 로깅
    logPrinter.printResponseLog(result, methodName, uuid);

    return result;
  }

  private void logMessagePayload(JoinPoint joinPoint, String methodName, String uuid) {
    Object[] args = joinPoint.getArgs();
    for (Object arg : args) {
      if (arg instanceof GenericMessage<?> message) {
        logPrinter.printRequestLog(methodName, message, uuid);
      }
    }
  }

전체 코드

// LogAspect

@Aspect
@Component
public class LogAspect {

  private final LogPrinter logPrinter = new LogPrinter();

  @Pointcut("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener)")
  public void rabbitListenerMethods() {}

  @Around("rabbitListenerMethods()")
  public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

    // 고유값 생성
    String uuid = UUID.randomUUID().toString();

    // 메소드명 추출
    String methodName = joinPoint.getSignature().getName();

    // 요청 값 로깅
    logMessagePayload(joinPoint, methodName, uuid);

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

    // 응답 코드 로깅
    logPrinter.printResponseLog(result, methodName, uuid);

    return result;
  }

  private void logMessagePayload(JoinPoint joinPoint, String methodName, String uuid) {
    Object[] args = joinPoint.getArgs();
    for (Object arg : args) {
      if (arg instanceof GenericMessage<?> message) {
        logPrinter.printRequestLog(methodName, message, uuid);
      }
    }
  }

}

// LogPrinter

@Slf4j
public class LogPrinter {

  private final LogMapper logMapper = new LogMapper();

  public void printRequestLog(String methodName, GenericMessage<?> message, String uuid) {
    String payloadJson = logMapper.convertToJson(message.getPayload());
    printLog(methodName, uuid, payloadJson);
  }

  public void printResponseLog(Object result, String methodName, String uuid) {
    String value = logMapper.converToString(result);
    printLog(methodName, uuid, value);
  }

  private void printLog(String methodName, String uuid, String value) {
    log.info("[{}:{}}] {}", methodName, uuid, value);
  }

}
// LogMapper

public class LogMapper {

  private final ObjectMapper objectMapper;

  public LogMapper() {
    this.objectMapper = new ObjectMapper();
    this.objectMapper.registerModule(new JavaTimeModule());
  }

  public String convertToJson(Object object) {
    try {
      return objectMapper.writeValueAsString(object);
    } catch (Exception e) {
      throw new RuntimeException("Json 변환 실패 : " + e.getMessage());
    }
  }

  public String converToString(Object object) {
    return (object == null) ? null : object.toString();
  }
}