이전 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();
}
}
'Log' 카테고리의 다른 글
로그 구분 ID 추가 logback-spring.xml + Filter (0) | 2024.08.12 |
---|---|
Filter 로그 처리 후 Controller에 요청 전달하기 (0) | 2024.08.08 |
Filter doFilter, ContentCachingWrapper, copyBodyToResponse 사용 이유 및 예시 (0) | 2024.08.06 |
ContentCachingRequestWrapper 동작 원리 (0) | 2024.08.04 |
스프링부트 Filter를 이용한 로그(Request,Response ) 출력 (0) | 2024.08.02 |