본문 바로가기
Java/[스프링 핵심 원리]

[스프링] AOP 적용을 위해 @Aspect를 사용해보자

2024. 3. 14.
반응형

스프링을 사용하는 백엔드 개발자로서 AOP는 놓칠 수 없는 중요한 기능이다. @Aspect를 통해서 AOP를 편리하게 적용하는 방법 및 어드바이스 종류별 특징과 사용법을 알아보자.

 

@Aspect 기본 사용법

@Aspect
public class AspectV1 {
    @Around("execution(* hello.aop.order..*(..))")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
        log.info("[log] {}", joinPoint.getSignature()); // joinpoint 시그니처
        return joinPoint.proceed();
    }
}
  • 클래스에 @Aspect 애노테이션을 붙이고, 이 클래스를 스프링 빈으로 등록해야 한다.
  • @Around 애노테이션을 붙이면 애노테이션 값에 있는 표현식이 포인트컷이 되고, 해당 메서드가 어드바이스가 된다.
    • @Around 내부에서는 jointPoint.proceed()로 타겟 객체를 호출할 수 있다.
  • 스프링 AOP는 AspectJ 라이브러리를 직접 사용하지 않고, AspectJ의 애노테이션이나 관련 인터페이스만 사용한다. → 추후 AspectJ 라이브러리를 직접 사용하도록 변경하는 경우에도 편하게 변경할 수 있다.

 

@Pointcut으로 포인트컷 모듈화하기

@Around에 포인트컷 표현식을 직접 넣지 않고, @Pointcut 애노테이션을 통해 분리해서 사용할 수 있다.

 

같은 클래스의 포인트컷 사용

@Aspect
public class AspectV2 {
    // hello.aop.order 패키지와 하위 모든 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    private void allOrder() {}

    // 클래스 이름 패턴이 *Service
    @Pointcut("execution(* *..*Service.*(..))")
    private 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{
        ...
        return joinPoint.proceed();
    }
}
  • 메서드 반환 타입은 void여야 하고, 함수 바디는 비워 놓는다.
  • 같은 클래스에서 사용할 포인트컷은 private을 사용해도 되는데, 외부(다른 애스펙트)에서 사용하려면 public을 사용해야 한다.
  • 메서드 이름 + 파라미터 ⇒ 포인트컷 시그니처 (allOrder())
    • 포인트컷을 사용할 어드바이스 애노테이션 값에 포인트컷 시그니처를 적어서 적용한다.
  • 포인트컷 표현식 또는 시그니처를 &&, ||, !으로 조합하여 사용할 수 있다.

 

다른 클래스의 포인트컷 사용

public class Pointcuts {
    // hello.aop.order 패키지와 하위 모든 패키지
    @Pointcut("execution(* hello.aop.order..*(..))")
    public void allOrder() {}

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

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

@Aspect
public class AspectV3 {
    @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{
        ...
        return joinPoint.proceed();
    }
}
  • 위 코드는 포인트컷들을 한 클래스에 모아놓고 애스펙트에서 불러와 사용한 예제이다.
    • 외부 애스펙트에서 사용하기 위해 포인트컷 메서드를 public으로 지정하였다.

 

Advice 어드바이스

어드바이스의 종류

  • 어드바이스는 @Around, @Before, @AfterReturning, @AfterThrowing, @After로 총 5가지가 있다. 
  • ProceedJoinPoint를 파라미터로 받는 @Around와 달리 나머지는 JoinPoint 객체를 받는다

 

@Before

@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
    log.info("[Before] {}", joinPoint.getSignature());
}
  • 조인포인트 실행 이전에 실행된다.
  • 어드바이스 실행 후 자동으로 타켓 메서드가 호출되므로 직접 호출할 필요가 없다.

@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);
}
  • 조인포인트가 정상적으로 완료된 후 실행된다.
  • @AfterReturning의 retuning 속성값 = 메서드 파라미터 이름
    • 반환 타입이 타겟 메서드와 맞지 않으면 어드바이스가 실행되지 않는다.
  • 반환되는 result 객체를 변경할 수는 없고, 조작할 수는 있다.

@AfterThrowing

@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrow(JoinPoint joinPoint, Exception ex) {
    log.info("[Throw] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
  • 조인포인트가 예외를 던지는 경우에 실행된다.
  • @AfterThrowing의 throwing 속성값 = 메서드 파라미터 이름
    • 예외 타입이 실제 발생한 예외와 일치하지 않으면 어드바이스가 실행되지 않는다.

@After

@After("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
    log.info("[After] {}", joinPoint.getSignature());
}
  • 조인포인트가 정상 완료되든 예외가 발생하든 실행된다. (finally 같은 역할)

@Around

  • 메서드 실행의 주변에서 실행된다.
  • 다른 어드바이스 애노테이션의 기능을 모두 포함하는 가장 강력한 어드바이스이다.
  • 첫번째 파라미터로 ProceedingJoinPoint를 사용해야 한다.
    • joinPoint.proceed()를 통해 타겟 메서드를 호출할 수 있다.
  • 기능
    • 조인포인트 실행 여부 선택 가능
    • 전달값, 반환값, 예외 변환 가능
    • 타겟 메서드의 결과에 따른 try/catch/finally 같은 복잡한 구문도 처리 가능
  • @Around는 모든 경우를 구현할 수 있는 가장 강력한 어드바이스이며, 나머지는 어라운드를 부분별로 나눠놓은 어드바이스이다.
    • @Around로 모든 기능을 수행할 수 있지만, 항상 proceed()를 호출해야 하기 때문에 실수를 할 수 있다. @Before를 사용하면 알아서 호출해주기 때문에 실수를 줄여준다.
    • 다른 어드바이스들은 기능은 적지만 실수할 가능성이 낮고 코드도 단순해진다. 또한 작성한 의도가 명확하게 드러나며 역할이 명확해진다는 장점이 있다.

 

어드바이스 적용 순서

@Aspect별 순서 지정

  • 어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶다면 @Aspect 단위로 @Order 애노테이션을 이용해 지정할 수 있다.
    • 클래스 단위로 지정할 수 있기 때문에 같은 애스펙트 내의 여러 어드바이스에 대해서는 순서를 지정할 수 없다.
public class AspectOrder {

    @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()); // joinpoint 시그니처
            return joinPoint.proceed();
        }
    }

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

위 코드처럼 각 애스펙트 클래스에 @Order를 적용하여 순서를 지정할 수 있다. 순서는 숫자가 작을수록 먼저 실행된다.

 

@Aspect 내에서의 순서

  • 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위가 정해져 있다.

클라이언트 - @Around - @Before - @After - @AfterReturning - @AfterThrowing - 타겟

  • 위는 어드바이스가 적용되는 순서이며, 호출 순서와 리턴 순서는 반대이므로 실제 실행되는 순서는 아래와 같다.
    • @Around → @Before → @AfterThrowing → @AfterReturning → @After → @Around
  • 한 @Aspect 내에 동일한 종류의 어드바이스가 2개 이상 있다면 순서가 보장되지 않으므로, 순서를 지켜야 한다면 별도의 @Aspect 클래스로 분리하고 @Order를 적용해야 한다.
728x90
반응형

댓글