공부/Spring

[Spring] Spring AOP 구현 예제

작은소행성 2024. 5. 17. 22:01

라이브러리 추가

build.gradle 에 의존성 추가를 해준다. 

implementation 'org.springframework.boot:spring-boot-starter-aop'

위의 라이브러리 추가로 aspectJ 관련 라이브러리를 등록하고 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록해준다. 

 

@Aspect 사용 예제

스프링은 @Aspect 어노테이션으로 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원하며 

어드바이저로 사용할 클래스에 @Aspect 어노테이션을 붙여줌으로써 스프링 AOP 를 적용할 수 있다. 

주의할 점으로 스플이 AOP 적용시에는 private, final 메소드는 AOP 적용이 불가능하다. 

@Slf4j
@Aspect
public class AspectV1 {

    //hello.aop.order 패키지와 하위 패키지
    @Around("execution(* hello.aop.order..*(..))") // 포인트컷 (AspectJ 표현식)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { // 어드바이스
        // 어드바이스 로직
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }
}

 

@Import(AspectV1.class)
@Slf4j
@SpringBootTest
public class AopTest {
    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Test
    void exception() {
        Assertions.assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

 

스프링 빈을 등록 하는 방법에는 아래 3가지 방법이 있다. 

  • @Bean 을 사용한 직접 등록 
  • @Component 컴포넌트 스캔을 사용한 자동 등록
  • @Import 주로 설정 파일을 추가할 때 사용 (@Configuration) 

테스트에서는 버전을 올려가면서 사용하기 때문에 @Import 을 사용한다. 

 

 

@Pointcut 사용 예제

@Slf4j
@Aspect
public class AspectV2 {

    //hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){} //pointcut signature

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }

}

 

  • @Pointcut에 포인트 표현식을 사용한다.
  • 주석에 pointcut signature 부분은 메서드 이름과 파라미터를 합쳐서 포인트 시그니처라고 한다. 
  • 메서드 반환타입은 void이며 코드 내용은 비워둔다. 
  • 다른 Aspect에서 참고하기 위해 public 접근 제어자를 사용한다.

위의 내용으로 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있으며 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용할 수 있다. 

 

 

어드바이스를 추가하는 예제 

로그 출력 기능에 트랜잭션을 적용하는 코드를 추가한 예제로 진짜 트랜잭션을 실행하는 것은 아닌 예제를 작성해본다. 

트랜잭션 기능은 보통 다음과 같이 동작한다. 

  • 로직 실행 전 트랜잭션 시작 -> 로직 실행 -> 로직에 문제가 없으면 커밋 -> 로직에 예외 발생하면 롤백 
@Slf4j
@Aspect
public class AspectV3 {

    //hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder(){} //pointcut signature

    //클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    private void allService(){}

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }

    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("allOrder() && allService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

}
  • allOrder() 포인트컷은 hello.aop.order 패키지와 하위 패키지를 대상으로 한다. 
  • allService() 포인트컷은 Service 대상으로 한다. (*Servi* 와 같은 패턴도 가능)

 @Around("allOrder() && allService()")

  • &&(AND), ||(OR), !(NOT) 포인트 컷 조합이 가능하다. 
  • doTransaction() 는 Service 에만 적용되고 doLog() 는 Service, Repository 모두 적용된다. 
    • Service : doLog(), doTransaction() 적용
    • Repository : doLog() 적용

AOP 적용 전 : orderService.orderItem() => orderRepository.save()

AOP 적용 후 : [doLog() -> doTransaction()] => orderService.orderItem() =>  [doLog()] => orderRepository.save()

 

Pointcut 참조하는 예제 

포인트멋을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 되며

외부에서 호출하기 위해 public 으로 열어 둔다.

orderAndService() : allOrder(), allService() 두개의 포인트컷을 조합해서 새로운 포인트컷을 만들었다. 

public class Pointcuts {

    //hello.aop.order 패키지와 하위 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder(){} //pointcut signature

    //클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    public void allService(){}

    //allOrder && allService
    @Pointcut("allOrder() && allService()")
    public void orderAndService() {}

}

 

 

포인트 컷을 여러 어드바이스에서 함께 사용할 때 해당 방법을 사용하면 효과적이다. 

실행결과는 위에 결과 이미지와 동일하다.

@Slf4j
@Aspect
public class AspectV4Pointcut {

    @Around("hello.aop.order.aop.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
        return joinPoint.proceed();
    }

    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

}

 

 

 

어드바이스 순서 지정하는 예제 

어드바이스는 기본적으로 순서를 보장하지 않는데 순서를 지정하고 싶으면 클래스 단위로 @Order 를 적용해야 한다. 

위의 예제들처럼 하나의 Aspect에 여러 어드바이스가 있으면 순서를 보장받을 수 없기 때문에 Aspect를 별도의 클래스로 분리해서 사용해야 한다.
순서는 Order()에 적힌 작은 순서대로 실행된다.

@Slf4j
public class AspectV5Order {

    @Aspect
    @Order(2)
    public static class LogAspect {
        @Around("hello.aop.order.aop.Pointcuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {
        @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

            try {
                log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
                Object result = joinPoint.proceed();
                log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
                return result;
            } catch (Exception e) {
                log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
                throw e;
            } finally {
                log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
            }
        }
    }

}

 

@Import 클래스에 사용하고자 하는 클래스로 변경해준다. 

 @Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
 @SpringBootTest
 public class AopTest {
 }

 

 

 

어드바이스 종류

@Around : 가장 강력한 어드바이스로 메서드 호출 전후에 실행한다.
           조인 포인트 실행 여부 선택, 반환값 변환, 예외 변환 등이 가능하다.

@Before : 조인 포인트 실행 이전에 실행된다. 

@AfterReturning : 조인 포인트가 정상 완료 후 실행된다. @Around 와 달리 반환되는 객체를 변경할 수는 없고 조작은 가능하다.

@AfterThrowing : 메서드가 예외를 던지는 경우에 실행된다.

@After : 조인 포인트가 정상 또는 예외에 관계없이 실행한다. (finally)

 

 

 

@Around 

메서드 실행 전후에 작업을 수행 

   @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
        try {
            //@Before
            log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature()); 
            Object result = joinPoint.proceed();
            
            //@AfterReturning
            log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature()); 
            return result;
        } catch (Exception e) {
            //@AfterThrowing
            log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            //@After
            log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature()); 
        }
    }

어드바이스 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 하고, proceed()를 통해 대상을 실행한다. proceed()를 여러번 실행할 수도 있다. 

 

 

@Before

조인 포인트 실행 전

 @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
 public void doBefore(JoinPoint joinPoint) {
     log.info("[before] {}", joinPoint.getSignature());
 }

 

  • @Around 와 다르게 작업 흐름을 변경할 수 없다. 
  • @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않으며 메서드 종료시 자동으로 다음 대상이 호출된다. 예외가 발생하면 다음 코드는 호출되지 않는다. 

 

@AfterReturning

메서드 실행이 정상적으로 반환될 때 실행

@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
 public void doReturn(JoinPoint joinPoint, Object result) {
     log.info("[return] {} return={}", joinPoint.getSignature(), result);
 }
  • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야한다. 
  • 부모 타입을 지정하면 모든 자식 타입은 인정된다. 
  • @Around와 다르게 반환되는 객체를 변경할 수 없지만 조작할 수 는 있다. 

 

@AfterThrowing

메서드 실행이 예외를 던져서 종료될 때 실행 

@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
 public void doThrowing(JoinPoint joinPoint, Exception ex) {
     log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
 }

 

 

@After

메서드 실행이 종료되면 실행

 @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
  public void doAfter(JoinPoint joinPoint) {
    log.info("[after] {}", joinPoint.getSignature());
  }

 

 

 

어드바이스 호출 순서 

  • 스프링 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다. 
  • 실행 순서 : @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
  • 어드바이스 리턴 순서는 호출 순서와 반대이다.
  • @Aspect 안에 동일한 종류의 어드바이스가 있으면 순서가 보장되지 않기 때문에 @Aspect를 분리하고 @Order를 적용한다. 
반응형