AOP - 적용(어드바이스 종류)

박찬우·2024년 2월 16일

스프링

목록 보기
65/88

종류

  • @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
  • 모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다
    • 단 @Around 는 ProceedingJoinPoint 을 사용해야 한다.
      • ProceedingJoinPoint 는 org.aspectj.lang.JoinPoint 의 하위 타입

에스팩트

  • @Before
    • 조인 포인트 실행 전
    • @Around 와 다르게 작업 흐름을 변경할 수는 없다. @Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음대상이 호출되지 않는다. 반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다. 물론 예외가 발생하면 다음 코드가 호출되지는 않는다.
  • @AfterReturning
    • 메서드 실행이 정상적으로 반환될 때 실행
    • returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
    • returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식타입은 인정된다.)
    • @Around 와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around 를 사용해야 한다. 참고로 반환 객체를 조작할 수 는 있다.
    • 반환 값이 String인데 Integer를 지정 하면 안됨
  • @AfterThrowing
    • 메서드 실행이 예외를 던져서 종료될 때 실행
    • throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
    • throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
  • @After
    • 메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)
    • 정상 및 예외 반환 조건을 모두 처리한다.
    • 일반적으로 리소스를 해제하는 데 사용한다.
  • @Around
    • 메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다.
    • 가장 강력한 어드바이스
      • 조인 포인트 실행 여부 선택 joinPoint.proceed() 호출 여부 선택
      • 전달 값 변환: joinPoint.proceed(args[])
      • 반환 값 변환
      • 예외 변환
      • 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
    • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다.
    • proceed() 를 통해 대상을 실행한다.
    • proceed() 를 여러번 실행할 수도 있음(재시도)
@Slf4j
@Aspect
public class AspectV6Advice {

    /*

    @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.allOrderAndAllService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable
    {
        try {

            // @Before
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());

            Object result = joinPoint.proceed();

            // @AfterReturning
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            // @AfterThrowing
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            // @After
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

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

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

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

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

}

TEST

@Slf4j
@SpringBootTest
@Import(AspectV6Advice.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() {
        assertThatThrownBy(() -> orderService.orderItem("ex"))
                .isInstanceOf(IllegalStateException.class);
    }
}

결과

  • 위 예제에서는 결과 확인을 위해 @Around를 주석처리 하였음
  • 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
  • 실행 순서: @Around , @Before , @After , @AfterReturning , @AfterThrowing
  • 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자.
  • 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다. 이 경우 앞서 배운 것 처럼 @Aspect 를 분리하고 @Order 를 적용하자.

@Around 외에 다른 어드바이스가 존재하는 이유

  • @Around 하나만 있어도 모든 기능을 수행할 수 있다
  • @Around 는 항상 joinPoint.proceed() 를 호출해야 한다. 만약 실수로 호출하지 않으면 타켓이 호출되지 않는 치명적인 버그가 발생한다.
  • 그러나 @Before 는 joinPoint.proceed() 를 호출하는 고민을 하지 않아도 된다
  • @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다. 그리고 가장 중요한 점이 있는데, 바로 이 코드를 작성한 의도가 명확하게 드러난다는 점이다.
  • 좋은 설계는 제약이 있는 것이다
profile
진짜 개발자가 되어보자

0개의 댓글