[Back-end]/[Spring]

[Spring] @Aspect

DevLoki 2021. 12. 10. 02:30

이전 포스팅에서는 스프링의 빈 후처리기인 AnnotationAwareAspectJAutoProxyCreator에 대해 알아보았습니다.

 

[Spring] 스프링의 빈후처리기(AnnotationAwareAspectJAutoProxyCreator)

이전 프록시에서는 직접 빈 후처리기를 구현하였고 빈으로 등록되는 객체들을 프록시로 대체하는 작업을 진행해보았습니다. [Spring] 빈 후처리기(BeanPostProcessor) 이전 글에서는 프록시 팩토리의

yejun-the-developer.tistory.com

스프링 컨테이너의 빈 저장소에 실제 타깃 객체 대신 프록시를 등록하려면 대상 타깃 클래스들과 어드바이저(advisor)를 빈으로 등록하면 됩니다. 그러면 AnnotationAwareAspectJAutoProxyCreator가 어드바이저들을 찾아 포인트 컷으로 적용 대상 판별 후 자동으로 프록시를 생성해주었습니다. 

@Aspect

스프링의 @Aspect는 포인트컷(Pointcut)과 어드바이스(Advice)로 구성된 어드바이저(Advisor)의 생성을 편리하게 해주는 기능을 가진 어노테이션입니다. 실제 실무에서는 직접 어드바이저를 생성하는 것이 아닌 @Aspect 어노테이션을 이용하여 AOP를 적용합니다.

AspectJ는 스프링에서 지원되는 기술이지만, 사실 PARC 사에서 개발한 자바 프로그래밍 언어용 관점 지향 프로그래밍 확장 기능입니다. 출시된 이후 단순함과 편리함을 기반으로 다양한 곳에서 사용되면서 스프링 프레임워크 또한 이를 차용하여 제공된 것입니다.

저번 포스팅에서 설명하지 않은 AnnotationAwareAspectJAutoProxyCreator의 기능이 하나 더 있었습니다. 바로, 이 @Aspect 어노테이션을 가진 빈들을 찾아 어드바이저로 만들어 줍니다.

구현

@Aspect
public class 클래스명{
    
    @어드바이스(AspectJ표현식)
    public Object 메서드명(ProceedingJoinPoint joinPoint) throws Throwable{
        
        //부가기능 로직 작성
        //부가기능 로직 작성
        //부가기능 로직 작성
        
        Object result = joinPoint.proceed();  //실제 타깃 요청 위임 메서드
        
        //부가기능 로직 작성
        //부가기능 로직 작성
        //부가기능 로직 작성
        
        return result;
    }
}

직접 어드바이저를 생성하는 코드와 비교해 보았을 때, 코드의 양이 눈에 띄게 줄었습니다. 기존에는 Pointcut객체와 MethodInterceptor를 확장(implements)하는 Advice객체를 생성하여 어드바이스 객체에 주입해주어야 했지만, 이제는 @Aspect를 사용하여 자동으로 어드바이저를 생성할 수 있습니다.

  • @어드바이스 : @Around, @After, @Before 등 다양한 종류가 있으며 어드바이스 로직(부가기능)을 실행 할 위치를 지정해 줍니다. 예를 들어 @After를 사용했다면, 실제 타깃 메서드를 호출한 뒤에 어드바이스 로직이 실행됩니다.
  • AspectJ표현식 : @어드바이스 내부에 표현된 문자열 형식의 표현식으로 프록시 적용 대상을 판별하는 포인트컷입니다.
  • ProceedingJoinPoint joinPoint : 기존 MethodInterceptor를 확장하여 Advice를 구현할 때 MethodInvocation invocation과 유사한 역할을 수행합니다. 내부적으로 실제 호출 대상, 호출 메서드, 전달 인자 등의 정보가 들어있습니다. 사용된 어드바이스가 @Around 일 경우에는 ProceedingJoinPoint를 사용하지만 이 외의 어드바이스를 사용하는 경우에는 JoinPoint를 사용해야 합니다.
  • joinPoint.proceed() : 실제 타깃 요청을 위임하는 메서드입니다. @Around 이외의 어드바이스를 사용한 경우 실제 타깃의 요청 위임은 자동으로 실행되므로 직접 호출하지 않습니다.

예시

MyAdvice.java

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;

@Slf4j
@Aspect
public class MyAspect {

    @Around("execution(* blog.proxy.aspect.Target.*(..))")
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("advice 로직 실행");
        return joinPoint.proceed();
    }

}
  • 실제 타깃 요청 수행 전에 어드바이스의 역할을 수행하는 로그를 출력하는 간단한 @Aspect 예시입니다.
  • AspectJ의 표현식으로 blog.proxy.aspect.Target의 모든 메서드에 어드바이스를 적용하는 프록시를 생성하도록 하였습니다.
  • 빈으로 등록되면 AnnotationAwareAspectJAutoProxyCreator가 자동으로 Aspect를 어드바이저로 만들어 줍니다.

Target.java

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Target {
    public void run(){
        log.info("Target.run() 실행");
    }
}
  • 메서드 실행 로그를 출력하는 run() 메서드를 가진 타깃 클래스입니다.

AspectConfig.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class AspectConfig {

    @Bean
    public Target target(){
        return new Target();
    }

    @Bean
    public MyAspect myAspect(){
        return new MyAspect();
    }

}
  • Target 클래스와 MyAspect 클래스를 빈으로 등록합니다.

AspectTest.java

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@Slf4j
@SpringBootTest
class MyAspectTest {

    @Autowired Target target;

    @Test
    void aspectJTest(){
        log.info("target = {}", target.getClass());
        target.run();
    }

}

결과 화면

Autowiring 된 Taget 오브젝트는 .getClass()로 출력해 보았을 때, EnhancerBySpringCGLIB가 붙어있었습니다. 정상적으로 프록시 객체가 대신 빈으로 등록된 것을 확인 할 수 있습니다. 또한, target.run()을 실행한 결과 "advice 로직 실행" 로그 또한 정상적으로 출력되었습니다.

그림으로 프록시 생성 흐름을 다시 한번 복습해보겠습니다.

 

  1. 생성 : @SpringBootApplication이 실행되며 @Bean과 @Component가 적용된 모든 스프링 빈 대상이 되는 객체를 생성합니다.
  2. 전달 : 생성된 객체들을 스프링 컨테이너의 빈 저장소에 등록하기 전에 빈 후처리기에 전달합니다.
  3. @Aspect 빈 조회 : AnnotationAwareAspectJAutoProxyCreator는 스프링 컨테이너 내의 @Aspect 어노테이션이 있는 빈을 조회합니다.
  4. @Aspect -> Adivisor 생성 및 저장 : AutoProxyCreator는 조회된 @Aspect의 정보를 기반으로 어드바이저를 생성한 후 @AspectJAdvisorBuilder에 저장합니다.
  5. 어드바이저 조회 : AnnotationAwareAspectJAutoProxyCreator는 스프링 컨테이너와 Aspect어드바이저빌더 내의 모든 Advisor를 조회합니다.
  6. 프록시 적용 대상 확인 : 조회한 Advisor 마다 1. 에서 생성된 모든 스프링 빈 대상 객체를 검사하여 프록시 적용 대상인지 판별합니다. 스프링 밴 대상 객체의 모든 메서드에 포인트 컷을 매칭하여 하나의 메서드라도 적용이 되면 해당 객체는 프록시 생성 대상이 됩니다. 
  7. 프록시 생성 : 프록시 생성 대상이 된 객체들은 모두 프록시를 생성합니다. 프록시 대상 객체가 아니라면 원본 객체를 그대로 반환합니다.
  8. 등록 : 반환된 프록시나 원본 대상 객체들을 모두 스프링 컨테이너에 빈 등록합니다.