kimyu0218
  • [spring] 스프링 AOP
    2024년 11월 10일 15시 43분 11초에 업로드 된 글입니다.
    작성자: @kimyu0218

    AOP; Aspect-Oriented Programming

    애스펙트프레임워크가 메서드 호출을 가로채고 그 메서드의 실행을 변경할 수 있는 방법이다. 애스펙트를 활용하면 비즈니스 로직과 함께 실행되는 로직을 분리하여 코드 중복을 줄이고 관심사를 명확하게 할 수 있다.
    여러 메서드에 로깅을 적용하는 과정을 가정해보자. 각 메서드에 중복된 로깅 코드를 추가하는 대신, 애스펙트를 적용하여 해당 메서드가 실행될 때 자동으로 로깅되도록 할 수 있다.

    • 애스펙트 (aspect) :  특정 메서드를 호출할 때 실행되는 코드 (= `execute` 메서드)
    • 어드바이스 (advice) : 언제 애스펙트를 실행해야 하는지 정의 (= `@Around` 어노테이션)
    • 포인트컷 (pointcut) : 어떤 메서드를 가로채야 하는지 정의 (= `@Around` 어노테이션에 전달된 값)
    • 조인 포인트 (join point)


    애스펙트 구현을 살펴보기 전에 스프링이 어떻게 메서드 호출을 가로채는지 살펴보자. 스프링이 메서드를 가로채기 위해서는 스프링이 관리할 수 있는 객체여야 한다. 즉, 객체가 스프링의 빈이어야 한다. (대상 객체라고도 한다!)

     

    객체를 애스펙트 대상으로 만들면, 스프링은 빈에 대한 인스턴스 참조를 직접 제공하지 않는다. 실제 빈 대신 프록시 객체를 제공한다. 프록시 객체는 가로챈 메서드에 대한 호출을 관리하고 애스펙트 로직을 적용한다.

    애스펙트 구현

    애스펙트를 생성하기 위해서는 다음 작업들을 수행해야 한다.

    1. 구성 클래스에 `@EnableAspectJAutoProxy` 어노테이션을 추가하여 애스펙트를 활성화한다.
    2. 애스펙트 클래스에 `@Aspect` 어노테이션을 추가하고, 컨텍스트에 빈으로 추가한다.
    3. 애스펙트 로직을 구현할 메서드를 정의하고, 어드바이스 어노테이션을 사용하여 언제, 어떤 메서드를 가로챌 것인지 정의한다.
    🚨 `@Aspect`는 스프링에게 해당 클래스가 애스펙트를 구현한다고 알려줄 수는 있지만, 스테레오 타입 어노테이션이 아니다. 따라서 별도로 `@Bean`이나 스테레오 타입 어노테이션을 활용하여 컨텍스트에 등록해야 한다.  

     
    나는 스케줄러의 잡이 실행될 때 잡의 실행시간을 출력할 것이다. 먼저 AOP를 위한 구성 클래스를 작성했다.

    @Configuration
    @EnableAspectJAutoProxy // 애스펙트를 활성화한다
    public class AopConfig {
    }
    💡  `@SpringBootApplication` 어노테이션은 `@EnableAutoConfiguration`을 포함하고 있다. 이로 인해 classpath에 존재하는 다양한 자동 구성 클래스를 감지하여 구성한다.

    스프링 부트 어플리케이션에서 `spring-boot-starter-aop` 의존성을 추가하면, 스프링 부트는 자동 설정을 통해 AOP 관련 설정을 자동으로 활성화한다. 따라서 `@EnableAspectJAutoProxy`를 따로 명시하지 않아도 된다. 

     
    다음으로, 애스펙트를 정의하는 `JobLoggingAspect` 클래스를 정의했다. 

    @Aspect // 스프링에게 애스펙트 로직을 구현했음을 알린다 
    @Component // 스프링 컨텍스트에 빈으로 추가한다
    public class JobLoggingAspect {
    }

    이제 언제 어떤 메서드를 가로챌 것인지 정의해야 한다. 애스펙트 클래스 내에 애스펙트 로직을 정의한다.

    @Slf4j
    @Aspect
    @Component
    public class JobLoggingAspect {
    
        @Around("execution(* com.nexters.goalpanzi.schedule.*.executeInternal(..))")
        public void execute(final ProceedingJoinPoint joinPoint) throws Throwable {
            String jobName = joinPoint.getTarget().getClass().getSimpleName(); // 가로챈 메서드의 클래스명 추출
    
            log.info("{} started.", jobName); // 가로챈 메서드의 클래스명 출력
    
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
    
            try {
                joinPoint.proceed(); // 가로챈 메서드 실행
            } catch (Exception e) {
                log.error("Error occurred while executing {}", jobName, e);
            }
    
            stopWatch.stop();
            log.info("{} finished. Elapsed time: {} ms", jobName, 0); // 가로챈 메서드의 실행시간 출력
        }
    }

    위 코드에서  `@Around` 어노테이션이 어드바이스를 의미한다. 스프링은 `@Advice` 외에도 다양한 어드바이스 어노테이션을 제공한다.

    • `@Before` : 가로챈 메서드가 실행되기 전에 애스펙트 로직 호출
    • `@AfterReturning` : 가로챈 메서드가 성공적으로 반환한 후 애스펙트 로직 호출
    • `@AfterThrowing` : 가로챈 메서드가 예외를 던진 후 애스펙트 로직 호출
    • `@After` : 가로챈 메서드가 실행된 후 애스펙트 로직 호출
    • `@Around` : 가로챈 메서드의 실행 전후로 애스펙트 로직 호출

     
    어드바이스 어노테이션 내부에는 어떤 메서드를 가로채야 하는지 포인트컷을 정의한다. 위의 코드에서는 다음과 같이 정의했다.

    // execution([RETURN_TYPE] [PACKAGE].[CLASS].[METHOD]([ARGUMENTS]))
    @Around("execution(* com.nexters.goalpanzi.schedule.*.executeInternal(..))")

    이는 `com.nexters.goalpanzi.schedule` 패키지의 어떤 클래스에서 `executeInternal` 메서드가 어떤 매개변수와 어떤 리턴 타입을 가지고 호출될 때를 의미한다.

    💡 위 코드는  AspectJ 표현식으로 포인트컷을 정의하고 있다. 하지만 복잡한 AspectJ 포인트컷 대신 어노테이션으로 특정 메서드를 가로챌 수도 있다.
    `@[ADVICE_ANNOTATION]("@annotation([POINTCUT_ANNOTATION])")`

     
    위의 애스펙트 로직은 단순 로깅만을 수행하고 있지만, 애스펙트는 가로챈 메서드의 매개변수의 값을 변경하는 등 가로챈 메서드의 실행을 변경할 수 있다.
     

    애스펙트 실행 체인

    메서드는 두 개 이상의 애스펙트로 가로채기 될 수 있다. 이 경우, 애스펙트의 실행 순서가 중요하다. 애스펙트가 매번 다른 순서로 실행되면 실행 결과를 예측할 수 없기 때문이다.
     
    애스펙트의 실행 순서를 일관되게 유지하기 위해 `@Order` 어노테이션을 사용할 수 있다. 값이 낮을수록 우선순위가 높기 때문에 작은 값의 애스펙트가 먼저 실행된다. (여러 애스펙트가 같은 우선순위를 가지는 경우 실행 순서가 지정되지 않는다!) 

    @Aspect
    @Order(1) // 첫번째로 실행
    @Component
    public class FirstAspect {
    	...
    }

    참고자료

    댓글