로그란 어떠한 서비스든 항상 필요합니다. 불필요하게 괜히 추가된다고 생각하면 추후에 문제상황이 발생했을 때 피를 볼 상황이 발생합니다. 특히나 많은 유저가 사용하는 서비스라면 더더욱 로그의 중요성은 커져갑니다.
이전 회사에서 있을 때에도 에러가 발생했을 때 로그를 확인하며 에러를 추적해갔던 기억이 납니다. 물론 그때는 리눅스 환경에서의 에러여서 커널에서의 로그를 확인했지만.. 로그를 저장하고 확인하기 위해서는 결과적으로 로그를 관리해야합니다.
그렇다면 SpringBoot에서 로그를 관리하는 방법은 뭐가 있는지 확인해보겠습니다.
이전 프로젝트에서는 인터셉터를 활용해서 로그를 기록했습니다.
인터셉터를 사용하든 AOP를 사용하든 상관없다라는 말을 들었기에 프론트를 했던 저에게 조금 더 친숙한 인터셉터를 사용했던 것이었습니다.
하지만 현재 진행하는 개인 프로젝트에서 로그 로직을 미리 작성해놓고 추가되는 서비스에 곧바로 로그를 기록하는 로직 또한 추가하고 싶어서 이번엔 AOP에 대해서 좀 더 알아보고 사용방법 등을 확인해보겠습니다.
AOP
의미
Aop란 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이란 의미입니다.
관점을 통해 분리하여 프로그래밍을 하겠다라는 것을 의미합니다.
용어 정리
Aspect
- 관심사를 모듈화한 단위
Advice
- 실제 실행되는 코드
JoinPoint
- 어드바이스가 실행되는 위치 / 시점
Pointcut
- JoinPoint 중 어디에 적용할지 정하는 규칙
Target
- 어드바이스를 적용할 대상
우선 이정도만 알고 코드를 확인해보며 조금 더 살펴보겠습니다.
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before: " + ?);
}
}
위에서 개념정리를 했던 용어들을 직접 대입해서 확인해보겠습니다.
Aspect | LoggingAspect 클래스 |
Advice | logBefore() |
Pointcut | serviceMethods() |
JoinPoint | 메서드 실행 시점 |
Target | 실제 com.example.service.* 클래스 |
위 방법으로 사용하면 로그를 관리할 수 있게 되었습니다!
위 코드에서 @Before 등과 같은 어노테이션에 대해서 조금 더 알아보겠습니다.
@Before | 메서드 실행 전 | 사전 처리 |
@After | 메서드 실행 후 | 성공/실패와 관계없이 항상 실행 |
@AfterReturning | 메서드 성공 후 | 반환값 로깅 |
@AfterThrowing | 예외가 발생했을 때 | 에러 로깅 |
@Around | 메서드 실행 전/후 모두 | 전처리 후처리 등 커스텀 |
이를 통해서 Aop를 활용하여 전처리 / 후처리 / 공통 관심사를 모아 관리할 수 있게 되었습니다. 저는 로깅을 위주로 말했기 때문에 로깅을 예로 들자면 특정 유저가 결제 시스템을 사용했을 때 로깅을 하고 주문 자체가 실패하면 실패 로깅을 하여 추후에 확인할 때 빠른 일처리를 할 수 있고 유저의 요청이 추후 들어와도 기록이 남아있기 때문에 기록을 기반하여 팀과 의논할 수 있을 것입니다.
위 방법 외에도 어노테이션이 적용되어 있는 부분에 Aspect를 적용할 수 있습니다.
마지막으로 이 방법을 확인해보고 마무리하겠습니다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ManagerLogAop {
LogActions action();
}
@Aspect
@Component
@RequiredArgsConstructor
public class LogAspect {
private final LogService logService;
@Pointcut("@annotation(managerLogAop)")
private void managerLogAopPointCut(ManagerLogAop managerLogAop) {}
@Around(value = "managerLogAopPointCut(managerLogAop)", argNames = "joinPoint,managerLogAop")
public Object saveLog(ProceedingJoinPoint joinPoint, ManagerLogAop managerLogAop) throws Throwable {
AuthUser authUser = (AuthUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Long userId = authUser.getId();
Long todoId = (Long) joinPoint.getArgs()[1];
try{
Object aopResult = joinPoint.proceed();
logService.saveLog(userId, todoId, true, managerLogAop.action());
return aopResult;
}catch (Exception exception) {
logService.saveLog(userId, todoId, false, managerLogAop.action());
throw exception;
}
}
}
우선 @interface는 어노테이션을 새로 정의하기 위해 사용합니다.
이제 @Pointcut을 이용해 시점을 정의하고 저 어노테이션이 붙은 부분에 Aspect를 실행합니다.
제 코드에서는 @Around를 사용했습니다. @Around는 전/후/성공/실패 전부 처리하기에 매개변수로 ProceedingJoinPoint가 필요합니다. 그 속성을 통해서 proceed를 호출합니다. 그러면 타겟이 실행됩니다.
실행 이후 try-catch로 분리하여 성공시 true flag save, 실패시 false flag save를 하고 있는 것을 확인할 수 있습니다.
제가 작성한 Aspect는 크게 성공 / 실패로 분리 했지만 조금 더 생각해보면 이전에 말했던 전/후처리 또한 할 수 있을 겁니다. proceed 호출 이후가 후처리 이기 때문에 이전 로직에서 제가 만약 데이터를 처리했기에 호출 이전 처리를 했다고 할 수 있습니다.
이상으로 Aop를 알아봤습니다.
'SpringBoot' 카테고리의 다른 글
조인과 서브쿼리 (0) | 2025.07.04 |
---|---|
SpringBoot 대용량 데이터 조회 (Index) (1) | 2025.07.03 |
JPA 영속성 파헤치기 (0) | 2025.06.28 |
SpringEvent / EDA (2) | 2025.06.24 |
Spring 예외처리 (@ExceptionHandler, @ControllerAdvice) (0) | 2025.05.12 |