2025.01.06 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 7편 - Spring AOP
2025.01.06 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 6편 - Proxy
2025.01.06 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 5편 - Template Method
2025.01.06 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 4편 ThreadLocal사용기
2024.12.26 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 3편
2024.12.25 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 2편
2024.12.25 - [Spring/Logging] - [Logging] 로그 출력을 효율적으로 생성 및 추적하기 1편
스프링에서 제공하는 Spring AOP를 활용해서 로깅이라는 공통 관심사를 분리하여 공통 로직을 하나의 클래스에서 관리하도록 하는 방식을 드디어 적용해 보았다.
@Pointcut으로 AOP가 적용될 시점을 Controller와 Service가 포함된 클래스로 지정하고 @Around로 수행시점의 메서드와 매개변수값, 요청 uri 등을 출력하도록 했다. 예민한 정보인 토큰값, 계정 비밀번호, 키 값들은 출력이 안되도록 설정 후 나머지 내용들에 대해서 로그를 출력하도록 했다.
서비스는 @AfterReturning 어노테이션을 활용해서 종료시 반환값 또한 출력하도록 설정해 줬다.
스프링 AOP가 없었다면 CGLIB 또는 동적 JDK 프록시를 사용해서 상황에 맞게 커스텀해줘야 하는 번거로움이 생겼을 텐데 이러한 편리함을 주는 프레임워크가 있어 정말 다행이었다.
traceId를 직접 부여하고 로그를 출력해 보는 방법부터 디자인패턴인 Method Template, Startegy 전략, 동적 프록시 등 다양한 방법을 수행해 보면서 느꼈던 점은 자바의 객체지향적인 특성을 살리되 상속을 최대한 피하고 합성을 해야 하며, 객체 간의 의존성을 최대한 약화시켜결합도를 낮추고 응집도를 높여야 한다는 점이었다. 객체간의 영향이 아예 없을 순 없지만 서로 간의 의존성이 낮을수록 코드의 유연성, 유지 보수성 등이 이렇게 향상될 수 있구나 하는 점을 느꼈다.
@Pointcut("execution(* pupket.togedogserver.domain.*.controller.*.*(..))")
private void cut() {}
@Around("cut()")
public Object aroundLog(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 메서드 정보 받아오기
Method method;
String uri;
try{
method = getMethod(proceedingJoinPoint);
uri = getRequestURI();
}catch (Exception e){
return proceedingJoinPoint.proceed();
}
// 요청 URI 확인
if ("/health-check".equals(uri)) {
return proceedingJoinPoint.proceed(); // /health-check API는 로그 출력하지 않음
}
// 클래스 정보 받아오기
log.info("class name = {}", proceedingJoinPoint.getTarget().getClass().getName());
log.info("======= method name = {} =======", method.getName());
// 파라미터 받아오기
Object[] args = proceedingJoinPoint.getArgs();
if (args.length == 0) {
log.info("no parameter");
} else {
for (Object arg : args) {
if (arg == null) {
log.info("parameter = null");
} else {
log.info("parameter type = {}", arg.getClass().getSimpleName());
log.info("parameter value = {}", arg);
}
}
}
// 메서드 실행 및 결과 로깅
Object returnObj = proceedingJoinPoint.proceed();
if (returnObj == null) {
log.info("return value = null");
} else {
log.info("return type = {}", returnObj.getClass().getSimpleName());
log.info("return value = {}", returnObj);
}
return returnObj;
}
private Method getMethod(ProceedingJoinPoint proceedingJoinPoint) {
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
return signature.getMethod();
}
private String getRequestURI() {
// 현재 스레드의 요청 URI를 가져오는 코드
return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
.getRequest().getRequestURI();
}
// Service의 메서드를 포인트컷으로 지정
@Pointcut("within(pupket.togedogserver.domain..*Service)")
public void service() {}
// Service 메서드 호출 전후 로깅
@Around("service()")
public Object loggingService(ProceedingJoinPoint joinPoint) throws Throwable {
// RedisSortedSetService의 addToSortedSet 메소드는 로깅 제외
if (joinPoint.getSignature().getDeclaringType().getSimpleName().equals("RedisSortedSetService")
&& joinPoint.getSignature().getName().contains("addToSortedSet")) {
return joinPoint.proceed();
}
String serviceName = joinPoint.getSignature().getDeclaringType().getSimpleName();
String methodName = joinPoint.getSignature().getName();
// 서비스 시작 로그
log.info("Service method started: {}.{}()", serviceName, methodName);
// 메서드 파라미터 로깅 (민감한 정보 제외)
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
if (args[i] != null && !containsSensitiveData(args[i])) {
log.info("Arg[{}]: {}", i, args[i]);
}
}
}
long startTime = System.currentTimeMillis();
Object result = null;
try {
result = joinPoint.proceed();
return result;
} finally {
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("Service method finished: {}.{}() [Execution time: {} ms]",
serviceName, methodName, elapsedTime);
// 결과 로깅 (민감한 정보 제외)
if (result != null && !containsSensitiveData(result)) {
log.info("Return value: {}", result);
}
}
}
private boolean containsSensitiveData(Object obj) {
// 민감한 정보를 포함하는 객체인지 확인하는 로직
return obj.toString().contains("password") ||
obj.toString().contains("token") ||
obj.toString().contains("key");
}
@AfterReturning(pointcut = "service()", returning = "returnValue")
public void afterReturningServiceLogging(JoinPoint joinPoint, Object returnValue) {
if (joinPoint.getSignature().toShortString().contains("addToSortedSet")) {
return;
}
log.info("### Service method finished: {}", joinPoint.getSignature().toShortString());
if (returnValue != null) {
log.info("Service return value: {}", returnValue);
}
}
'Spring > Logging' 카테고리의 다른 글
[Logging] 로그 출력을 효율적으로 생성 및 추적하기 7편 - Spring AOP (0) | 2025.01.06 |
---|---|
[Logging] 로그 출력을 효율적으로 생성 및 추적하기 6편 - Proxy (0) | 2025.01.06 |
[Logging] 로그 출력을 효율적으로 생성 및 추적하기 5편 - Template Method (1) | 2025.01.06 |
[Logging] 로그 출력을 효율적으로 생성 및 추적하기 4편 ThreadLocal사용기 (0) | 2025.01.06 |
[Logging] 로그 출력을 효율적으로 생성 및 추적하기 3편 (1) | 2024.12.26 |