Spring AOP를 활용한 로깅 및 예외 처리

리본24·2025년 1월 21일

Spring

목록 보기
1/7

1. AOP (Aspect-Oriented Programming) 개념

1.1 AOP의 개념 및 필요성

  • AOP란?
    • 관점 지향 프로그래밍(Aspect-Oriented Programming)의 약자.
    • 횡단 관심사(Cross-Cutting Concern)를 모듈화하여 핵심 비즈니스 로직과 분리하는 프로그래밍 패러다임.
    • 횡단 관심사 예: 로깅, 트랜잭션 관리, 보안 검증 등.
  • AOP의 필요성
    • 비즈니스 로직과 반복적인 로직(횡단 관심사)을 분리하여 코드 가독성과 유지보수성을 높임.
    • 중복 코드 제거로 생산성 향상.

1.2 AOP 주요 개념 설명

1. Aspect

  • Aspect는 횡단 관심사를 모듈화한 클래스입니다.

  • 로깅, 트랜잭션 관리, 보안 검증과 같은 공통된 기능을 별도의 모듈로 분리하여 핵심 비즈니스 로직에 영향을 주지 않도록 설계합니다.

  • 특징:
    - 여러 클래스나 메서드에 공통적으로 적용될 기능을 한 곳에 모아 재사용성을 높입니다.
    - Spring AOP에서는 @Aspect 어노테이션을 사용해 Aspect 클래스를 정의합니다.

    @Aspect
    @Component
    public class LoggingAspect {
      @Before("execution(* com.aop.domain..*(..))")
      public void logBefore() {
        log.info("메서드 실행 전 로그");
      }
    }

2. Advice

  • Advice는 Aspect에 포함된 특정 작업(기능)을 정의한 메서드입니다.
  • 언제(메서드 실행 전, 후, 혹은 실행 중) Advice가 실행될지 결정됩니다.
  • Advice 종류:
    1. @Before:
    - 메서드 실행 전에 실행.
    - 보안 검증, 데이터 유효성 검사 등에 활용.
    2. @After:
    - 메서드 실행 후에 실행.
    - 로깅, 리소스 정리 등에 활용.
    3. @AfterReturning:
    - 메서드가 정상적으로 반환된 이후에 실행.
    - 반환 값을 활용한 후처리에 사용.
    4. @AfterThrowing:
    - 메서드에서 예외가 발생한 경우에 실행.
    - 예외 로깅이나 알림 처리에 사용.
    5. @Around:
    - 메서드 실행 전후로 실행.
    - 실행 시간 측정, 트랜잭션 관리 등 복잡한 처리에 활용.
    @Before("execution(* com.aop.domain..*(..))")
    public void logBefore() {
       log.info("메서드 실행 전 로그");
    }

3. JoinPoint

  • JoinPoint는 AOP가 적용될 수 있는 지점을 의미합니다.

  • Spring AOP에서는 메서드 호출, 객체 생성 등이 JoinPoint에 해당합니다.

  • 특징:

    • JoinPoint를 활용하면 메서드 실행 시점에서 메서드 이름, 파라미터, 반환 값 등의 정보를 가져올 수 있습니다.

    • Spring AOP는 메서드 호출에만 JoinPoint를 지원합니다.

       @Before("execution(* com.aop.domain..*(..))")
       public void logMethodDetails(JoinPoint joinPoint) {
               log.info("실행된 메서드 이름: {}", joinPoint.getSignature().getName());
      
           Object[] args = joinPoint.getArgs();
      
           if (args != null && args.length > 0) {
             for (int i = 0; i < args.length; i++) {
               log.info("전달된 파라미터 [{}]: {}", i, args[i]);
             }
           }
       }
    • joinPoint.getSignature().getName()으로 호출된 메서드의 이름을 가져옵니다.

    • joinPoint.getArgs()를 통해 전달된 파라미터를 확인할 수 있습니다.

4. Pointcut

  • Pointcut은 Advice를 적용할 JoinPoint를 정의하는 표현식입니다.

  • 특정 패키지, 클래스, 메서드에만 Advice를 적용하거나 제외할 수 있습니다.

  • Pointcut 표현식:

    • execution: 메서드 실행을 기준으로 Pointcut 정의.
      - 예: execution(* com.example.service..*(..))com.example.service 패키지의 모든 클래스의 모든 메서드.

      @Before("execution(* com.aop.domain..api..*(..))")
      public void logBeforeExecution() {
        log.info("[execution] 메서드 실행 전 로그");
      }
    • 리턴 타입 지정 : * 모든 리턴 타입을 의미합니다.

    • 패키지 지정 : com.aop.domain 메서드가 속한 클래스가 이 패키지 또는 그 하위 패키지에 있어야 한다는 조건을 지정합니다.

    • 하위 패키지 포함 : ..는 0개 이상의 하위 패키지를 의미합니다.

    • 클래스 이름의 와일드카드 : 클래스 이름이 무엇이든 상관없이 모든 클래스의 메서드를 대상으로 합니다.

    • 메서드 이름과 파라미터 지정 :

      • *는 메서드 이름을 지정하는 부분입니다. 여기서 *는 모든 이름의 메서드를 의미합니다.
      • (..) 는 메서드의 파라미터 지정과 0개 이상의 모든 파라미터를 허용합니다.
  • within: 특정 클래스 또는 패키지 내의 모든 메서드에 적용.

    • 예: within(com.example.service..*)com.example.service 패키지와 하위 패키지의 모든 메서드.

      @Before("within(com.aop.domain..*)")
      public void logBeforeWithin() {
        log.info("[within] 메서드 실행 전 로그");
      }
  • @annotation: 특정 어노테이션이 붙은 메서드에만 적용.

    • 예: @annotation(com.example.annotation.Loggable).

      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.METHOD)
      public @interface Loggable {
      }
      @Before("@annotation(com.aop.common.annotation.Loggable)")
      public void logBeforeAnnotation() {
        log.info("[annotation] 메서드 실행 전 로그");
      }
      package com.example.service;
      
      import com.example.annotation.Loggable;
      
      public class UserService {
      
          @Loggable
          public void getUser() {
              // 이 메서드에만 logAnnotatedMethod 적용
          }
      }
      표현식적용 대상주요 사용 사례
      execution특정 메서드 실행 시점을 기준으로 AOP 적용특정 패키지/메서드의 실행 시점에 대한 공통 로직 적용.
      within특정 클래스/패키지 내의 모든 메서드에 AOP 적용특정 영역 내 모든 메서드에 대해 포괄적인 동작 정의.
      @annotation특정 어노테이션이 붙은 메서드에만 AOP 적용메서드에 맞춤형 동작(예: 로깅, 검증, 보안 등) 적용.

2. Spring AOP 설정 및 적용

2.1 AOP 예제 코드

  1. LoggingAspect 클래스
@Slf4j
@Aspect
@Component
public class LoggingAspect {

  @Before("execution(* com.aop.domain..api..*(..))")
  public void logBeforeExecution() {
    log.info("[execution] 메서드 실행 전 로그");
  }

  @Before("within(com.aop.domain..*)")
  public void logBeforeWithin() {
    log.info("[within] 메서드 실행 전 로그");
  }

  @Before("@annotation(com.aop.common.annotation.Loggable)")
  public void logBeforeAnnotation() {
    log.info("[annotation] 메서드 실행 전 로그");
  }


  @Before("execution(* com.aop.domain..*(..))")
  public void logMethodDetails(JoinPoint joinPoint) {
    Object[] args = joinPoint.getArgs();

    if (args != null && args.length > 0) {
      for (int i = 0; i < args.length; i++) {
        log.info("전달된 파라미터 [{}]: {}", i, args[i]);
      }
    }

    log.info("실행된 메서드 이름: {}", joinPoint.getSignature().getName());
  }
}
  1. Service 클래스
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {

  private final ProductRepository productRepository;
  private final CategoryRepository categoryRepository;
  private final ProductQueryRepository productQueryRepository;

  @Loggable
  public List<ProductResponse> getAll() {
    ...
  }
}
  1. 결과 로그
[execution] 메서드 실행 전 로그
[within] 메서드 실행 전 로그
실행된 메서드 이름: findAll

[annotation] 메서드 실행 전 로그
[within] 메서드 실행 전 로그
실행된 메서드 이름: getAll

3. AOP를 활용한 예외 처리와 로깅, 실행 시간 측정

3.1 예외 로깅

  • AOP를 활용하면 애플리케이션 전반에서 발생하는 예외를 중앙에서 처리하고 로깅할 수 있습니다.
  • Spring AOP의 @AfterThrowing 어노테이션은 특정 메서드에서 예외가 발생했을 때 동작하는 Advice를 정의할 수 있습니다.
  • 이를 통해 중복 없이 예외 처리를 일관되게 관리할 수 있습니다.

작동 원리

  • Pointcut을 사용해 예외 로깅이 필요한 메서드를 정의합니다.
  • 예외 발생 시 throwing 속성에 지정된 변수로 예외 정보를 전달받아 로그를 출력하거나 알림 시스템과 연동합니다.
@Slf4j
@Aspect
@Component
public class ExceptionLoggingAspect {

  @Pointcut("execution(* com.aop.domain..service..*(..))")
  public void serviceMethods() {
  }

  @AfterThrowing(pointcut = "serviceMethods()", throwing = "exception")
  public void logException(ServiceException exception) {
    log.error("AfterThrowing : [{}] {}", exception.getCode(), exception.getMessage());
  }
}
  1. Pointcut 정의:
    • within(com.aop.domain..*)com.aop.domain 패키지의 모든 클래스에 포함된 메서드를 대상으로 설정.
  2. throwing 속성:
    • throwing = "exception"을 통해 발생한 예외 객체를 Advice로 전달받습니다.
  3. 로깅 출력:
    • log.error로 예외 메시지와 스택 트레이스를 기록하여 디버깅 및 문제 분석에 활용.

결과 로그 예시

AfterThrowing : [NOT_FOUND_USER] User를 찾을 수 없습니다.

3.2 실행 시간 측정

  • Spring AOP의 @Around 어노테이션은 메서드 실행 전후에 특정 로직을 실행할 수 있는 Advice를 정의합니다.
  • 이를 활용하여 메서드 실행 시간 측정이 가능합니다.
  • 실행 시간 정보를 통해 성능 병목 구간을 찾아 최적화할 수 있습니다.

작동 원리

  1. @Around는 Pointcut에 정의된 메서드 호출을 가로채고, 실행 전후의 시점을 제어합니다.
  2. ProceedingJoinPoint를 사용하여 실제 메서드를 호출하고, 실행 결과를 반환받습니다.
  3. 메서드 실행 전후의 시간을 기록하여 로그로 출력합니다.
@Slf4j
@Aspect
@Component
public class ExecutionTimeAspect {

  @Pointcut("execution(* com.aop.domain..service..*(..))")
  public void serviceMethods() {
  }

  @Around("serviceMethods()")
  public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();

    Object result = joinPoint.proceed();

    long endTime = System.currentTimeMillis();
    log.info("{} 메서드 실행 시간: {} ms", joinPoint.getSignature(), (endTime - startTime));

    return result;
  }

}
  1. Pointcut 정의:
    • 서비스 계층의 모든 메서드 실행에 대해 실행 시간 측정을 적용.
  2. ProceedingJoinPoint:
    • proceed()를 호출하여 실제 메서드를 실행하고 결과를 반환.
    • 메서드 실행 전후로 시간을 측정 가능.
  3. 로깅 출력:
    • 실행된 메서드 이름(ProductService.getById())과 실행 시간(endTime - startTime)을 로그로 출력.

결과로그 예시

ProductResponse com.aop.domain.product.service.ProductService.getById(Long) 메서드 실행 시간: 32 ms
profile
기록하고 소화해보자! 소화가 안되거나 까먹으면 다시 꺼내서 보자! 오늘의 나는 어제의 나보다 강하다!

0개의 댓글