FireDrago

[Spring] 스프링 AOP 구현 본문

프로그래밍/Spring

[Spring] 스프링 AOP 구현

화이용 2024. 5. 18. 15:19

스프링 AOP 구현 

@Slf4j
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public void orderItem(String itemId) {
        log.info("[orderService] 실행");
        orderRepository.save(itemId);
    }
}
@Slf4j
@Repository
public class OrderRepository {

    public String save(String itemId) {
        log.info("[orderRepository] 실행");
        // 저장로직
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}

OrderService OrderRepository 클래스를 작성한다.

이제 @Aspect , @Around 사용하여 AOP 구현해보자

@Slf4j
@Aspect
public class AspectV1 {

    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}

@Around 애노테이션의 값인 execution(* hello.aop.order..*(..))포인트컷이 된다.

@Around 애노테이션의 메서드인 doLog 는 어드바이스(Advice)가 된다.

 

@Slf4j
@SpringBootTest
@Import(AspectV1.class)
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);
    }
}

 

포인트컷 분리

@Slf4j
@Aspect
public class AspectV2 {

    // 포인트컷 분리
    @Pointcut("execution(* hello.aop.order..*(..))")
    // 반환타입 void, 다른 곳에서 사용하려면 public 사용
    // 메서드명 + 파라미터 = 포인트컷 시그니처
    private void allOrder() {}

    // 포인트컷 시그니처 호출 -> 재사용 가능
    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}

@Pointcut

  • @Pointcut 에 포인트컷 표현식을 사용한다.
  • 메서드의 반환 타입은 void 여야 한다. 코드의 내용은 비원둔다.

 

어드바이스 추가

@Slf4j
@Aspect
public class AspectV3 {

    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder() {}

    @Pointcut("execution(* *..*Service.*(..))")
    public void allService() {}

    @Around("allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }

    /**
     *  어드바이스 추가
     */
    @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());
        }
    }
}

 

  • && (AND), || (OR), ! (NOT) 3가지 조합이 가능하다
  • doTransaction() 어드바이스는 OrderService 에만 적용된다.

 

포인트컷 참조

public class Pointcuts {

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

    //타입 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    public void allService() {}

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

외부에서 호출할 때는 포인트컷의 접근 제어자를 public 으로 열어두어야 한다.

 

@Slf4j
@Aspect
public class AspectV4 {

    @Around("hello.aop.order.aop.Pointcuts.allOrder()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        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());
        }
    }
}

 

어드바이스 순서

@Slf4j
public class AspectV5Order {
    /**
     * 어드바이스 순서 정하려면 -> 클래스 분리해야한다. @Order는 클래스 단위로 적용하기때문
     */

    @Aspect
    @Order(2) // 숫자 작을수록 먼저 실행된다.
    static class LogAspect {

        @Pointcut("hello.aop.order.aop.Pointcuts.allOrder()")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

    @Aspect
    @Order(1) // 숫자 작을수록 먼저 실행된다.
    static class TxAspect {

        @Pointcut("hello.aop.order.aop.Pointcuts.orderAndService()")
        public Object doLog(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());
            }
        }
    }
}

어드바이스는 순서를 보장하지 않는다.

순서를 지정하려면 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다. 

@Order클래스 단위로 적용할 수 있다. 애스펙트를 별도의 클래스로 분리해야 한다.

 

어드바이스 종류

어드바이스 종류

  • @Around : 메서드 호출 전후에 수행, 조인포인트 실행 여부 선택, 반환 값 반환, 예외변환 등이 가능
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Slf4j
@Aspect
public class AspectV6Advice {

    @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());
        }
    }

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

    @AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()",
            returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info("[return] {} return={}", joinPoint.getSignature(), result);
    }

    @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(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }
}

모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. (생략해도 된다.)
단 @Around 는 ProceedingJoinPoint 을 사용해야 한다.

@Before

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

@AfterReturning

  • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
  • returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다
  • @Around 와 다르게 반환되는 객체를 변경할 수는 없다. 
    반환 객체를 변경하려면 @Around 를 사용해야 한다. 참고로 반환 객체를 조작할 수 는 있다.

@AfterThrowing

  • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
  • throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다.

@After

  • 메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)
  • 정상 및 예외 반환 조건을 모두 처리한다.
    일반적으로 리소스를 해제하는 데 사용한다.

@Around

  • 메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다.
  • 조인 포인트 실행 여부 선택 joinPoint.proceed() 호출 여부 선택
    전달 값 변환: joinPoint.proceed(args[])
    반환 값 변환
    예외 변환
    트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
  • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다.

실행 순서: @Around , @Before , @After , @AfterReturning , @AfterThrowing