이전 게시글에서는 AOP에 대한 기본 개념을 알아보았다.
이번 게시글에서는 스프링 AOP를 어떻게 구현하고 사용할 수 있는지 알아보고자 한다.
스프링 AOP 구현 방법
가장 간단하게 스프링 AOP를 구현하고 사용하는 예제 코드를 점점 더 다양하고 복잡한 코드로 변경해보면서,
스프링 AOP의 다양한 사용 방법을 알아볼 것이다.
스프링 AOP 적용 대상
OrderRepository와 OrderService는 이번 예제에서 스프링 AOP를 적용할 대상들이다.
OrderRepository
OrderRepsotiry는 이번 예제들에 걸쳐서 사용할 리포지토리 계층 클래스이다.
save()는 파라미터인 String itemId의 값을 저장한다.
정상적으로 로직이 실행되면 "ok"를 반환한다. 만약 itemId의 값이 "ex"이면 예외를 발생시킨다.
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
OrderService
OrderService는 이번 예제들에 걸쳐서 사용할 서비스 계층 클래스이다.
orderItem()은 파라미터인 String itemId의 값을 OrderRepository.save()의 파라미터로 넘겨준다.
@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);
}
}
Aspect
AspectV1 ~ AspectV6Advice는 OrderRepository, OrderService에 대해 스프링 AOP를 적용하기 위한 @Aspect 클래스들이다.
AspectV1
AspectV1은 가장 간단하게 구현한 @Aspect 클래스이다.
@Arouond가 적용되어 있는 doLog()가 구현되어있다.
- @Around는 적용된 메소드가 어드바이스라는 것을 알려주는 애노테이션 중 하나이다.
- @Around의 값인 "execution(* hello.aop.order..*(..))"는 포인트컷 표현식이며, 그 자체로 포인트컷의 역할을 한다.
- 결과적으로 doLog()를 어드바이스로, "execution(* hello.aop.order..*(..))"를 포인트컷으로 갖는 어드바이저가 생성될 수 있다.
doLog()는 AOP 프록시가 타겟의 메소드를 호출하기 전에 로그를 찍는 부가 기능을 제공한다.
@Slf4j
@Aspect
public class AspectV1 {
// hello.aop.order 패키지와 하위 패키지
@Around("execution(* hello.aop.order..*(..))") // 포인트컷 표현식
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처
return joinPoint.proceed();
}
}
AspectV2
AspectV2는 @Around에 포함되어있던 포인트컷 표현식이 @Pointcut을 통해 분리될 수 있음을 보여준다.
@Pointcut("execution(* hello.aop.order..*(..))")이 적용된 allOrder()를 살펴보자.
- @Pointcut의 값으로 포인트컷 표현식을 사용한다.
- @Pointcut이 적용된 메소드는 반환 타입이 void여야하며, 코드 내용은 없어야한다. 그리고 내부에서만 사용하려면 private, 외부에서도 사용하려면 public 접근 제한자를 사용하면 된다.
- @Pointcut이 적용된 메소드의 메소드(파라미터)를 포인트컷 시그니처라고 한다. 포인트컷 시그니처는 포인트컷 표현식 대신 사용될 수 있다.
이제 doLog()에 적용된 @Around를 살펴보자.
- @Around의 값으로 allOrder()라는 포인트컷 시그니처가 사용된 것을 볼 수 있다.
- allOrder()의 @Pointcut 값이 포인트컷으로 사용된다.
@Slf4j
@Aspect
public class AspectV2 {
/**
* 포인트컷 표현식을 @Pointcut에 분리하여 사용할 수 있다.
* 포인트컷 시그니처: 메소드명(파라미터) -> 반환타입은 void, 코드 내용은 없어야 한다.
* 내부에서만 사용하려면 private, 외부에서도 사용하려면 public을 사용하면 된다.
*/
// hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") // 포인트컷 표현식
private void allOrder() {} // 포인트컷 시그니처
// @Around의 값으로 포인트컷 시그니처를 사용하여 포인트컷을 지정할 수 있다.
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처
return joinPoint.proceed();
}
}
AspectV3
AspectV3는 포인트컷이 연산자를 통해 조합될 수 있음을 보여준다.
AspectV1과 AspectV2에도 구현되어 있었던 allOrder()에 더불어 allService()라는 포인트컷이 구현되어있다.
- allOrder()의 포인트컷은 hello.aop.order 패키지와 하위 패키지이다.
- allService()의 포인트컷은 타입 패턴이 *Service이다.
AspectV1과 AsepctV2에도 구현되어 있었던 doLog()와 더불어 doTransaction()이라는 어드바이스가 구현되어있다.
- doLog()의 포인트컷은 allOrder()이다. hello.aop.order 패키지와 하위 패키지에 속한 대상에 대해서 동작한다.
- doTransaction()의 포인트컷은 allOrder() && allService()이다. hello.aop.order 패키지와 하위 패키지에 속하면서 타입이 *Service인 대상에 대해서 동작한다.
- allOrder() && allService()처럼 포인트컷은 &&, ll, ! 연산자를 통해 조합하여 사용할 수 있다.
OrderRepository와 OrderSerivce는 모두 hello.aop.order에 속해있다.
따라서 doLog()는 두 대상 모두에게 동작한다.
그러나 doTransaction()은 타입 패턴이 *Service라는 추가 조건이 있기 때문에 OrderService에 대해서만 동작한다.
@Slf4j
@Aspect
public class AspectV3 {
/**
* 포인트컷 표현식을 @Pointcut에 분리하여 사용할 수 있다.
* 포인트컷 시그니처: 메소드명(파라미터) -> 반환타입은 void, 코드 내용은 없어야 한다.
* 내부에서만 사용하려면 private, 외부에서도 사용하려면 public을 사용하면 된다.
*/
// 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()); // joinPoint 시그니처
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());
}
}
}
AspectV4Pointcut
AspectV4Pointcut과 Pointcuts는 외부에 구현된 포인트컷을 필요할 때 참조하여 사용하는 방법을 보여준다.
Pointcuts를 먼저 살펴보자.
- Pointcuts 내부에는 allOrder(), allService(), orderAndService()라는 포인트컷들이 구현되어있다.
- 이처럼 포인트컷들을 외부에 모아 놓고 필요할 때 참조하여 사용할 수 있다.
- 다만 포인트컷들의 접근 제한자를 public으로 설정해줘야한다.
이제 AspectV4Pointcuts를 살펴보자.
- AspectV4Pointcut은 @Aspect 클래스이며 doLog(), doTransaction()이라는 어드바이스가 구현되어있다.
- 그리고 두 어드바이스는 Pointcuts에 구현된 포인트컷을 참조하여 사용하고 있다.
- 외부의 포인트컷을 참조할 때는 해당 포인트컷의 패키지를 포함한 클래스를 함께 명시해줘야 한다.
@Slf4j
@Aspect
public class AspectV4Pointcut {
/**
* 외부의 포인트컷을 가져올 때는 패키지를 포함한 클래스를 함께 명시해줘야한다.
*/
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처
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());
}
}
}
AspectV5Order
하나의 @Aspect 클래스 내부에 구현된 어드바이스들은 적용 순서를 지정할 수 없다.
즉 하나의 @Asepct 클래스 내부에 구현된 어드바이스들로 생성되는 어드바이저들은 적용 순서를 지정할 수 없다.
어드바이스의 적용 순서는 @Aspect 클래스 단위로 지정할 수 있다.
따라서 적용 순서를 지정하고 싶은 어드바이스들을 각각 다른 @Aspect 클래스에 구현해야한다.
AspectV5Order는 개별적인 @Aspect 클래스를 사용하여 어드바이스의 적용 순서를 지정하는 방법을 보여준다.
- AspectV5Order는 @Aspect 클래스가 아니며, 내부 클래스인 LogAspect, TxAspect가 @Aspect 클래스이다.
- 그리고 LogAspect, TxAspect에는 각각 어드바이스인 doLog(), doTransaction()이 구현되어있다.
- LogAspect, TxAspect에는 @Order가 적용되어있다. @Order는 @Aspect 클래스 단위로 어드바이스 적용 순서를 지정할 수 있다. @Order의 값이 작을수록 먼저 적용된다.
- 따라서 TxAspect 내부의 어드바이스들이 먼저 적용되고, LogAspect 내부의 어드바이스들이 나중에 적용된다.
@Slf4j
public class AspectV5Order {
/**
* 어드바이스는 기본적으로 적용 순서를 보장하지 않으며, 순서를 지정하고 싶으면 @Aspect 단위로 @Order를 적용해야한다.
* 하나의 @Aspect에 여러 어드바이스가 있으면 순서를 지정할 수 없다.
* 따라서 순서를 지정하기 위한 어드바이스들을 각각의 @Asepct로 분리해야한다.
* @Order의 값이 작을수록 먼저 적용된다. TxAspect -> LogAspect
*/
@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 {
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());
}
}
}
}
AspectV6Advice
@Aspect 클래스 내부에서 사용할 수 있는 어드바이스 애노테이션은 사용한 @Around 외에도 다양한 종류가 있다.
@Around 이외의 어드바이스 애노테이션이 적용된 어드바이스들은 org.aspectj.lang.JoinPoint를 첫 번째 파라미터로 사용할 수 있다.
- JoinPoint.getArgs()는 메서드 인수를 반환한다.
- JoinPoint.getThis()는 프록시 객체를 반환한다.
- JoinPoint.getTarget()은 대상 객체를 반환한다.
- JoinPoint.getSignature()은 어드바이스가 적용되는 메서드에 대한 설명을 반환한다.
- JoinPoint.toString()은 어드바이스가 적용되는 메서드에 대한 설명을 출력한다.
@Around가 적용된 어드바이스는 org.aspectj.lang.ProceedingJoinPoint를 첫 번째 파라미터로 사용할 수 있다. ProceedingJoinPonit는 JoinPoint의 하위 클래스이다.
- ProceedingJoinPoint.proceed()는 다음 어드바이스 또는 대상 객체의 메서드를 호출한다.
@Before
- @Before 어드바이스는 대상 객체 메소드가 호출되기 전 까지의 부가 기능 로직을 담당한다.
- @Before 어드바이스는 종료 시 자동으로 ProceedingJoinPoint.proceed()를 호출한다.
- @Before 어드바이스 내부에서 예외가 발생하면 ProceedingJoinPoint.proceed()는 호출되지 않는다.
@AfterReturning
- @AfterReturning 어드바이스는 대상 객체 메소드가 정상적으로 종료된 이후의 부가 기능 로직을 담당한다.
- @AfterReturning은 returning이라는 속성을 가진다.
- returning의 값은 어드바이스의 파라미터 중 대상 객체 메소드의 반환값이 전달되는 파라미터의 이름과 일치해야한다.
- returning의 값과 대응하는 파라미터의 타입을 반환하는 대상 객체 메소드만 대상으로 실행된다.
@AfterThrowing
- @AfterThrowing 어드바이스는 대상 객체 메소드가 예외 발생으로 종료된 이후의 부가 기능 로직을 담당한다.
- @AfterThrowing은 throwing이라는 속성을 가진다.
- throwing의 값은 어드바이스의 파라미터 중 대상 객체 메소드에서 발생한 예외가 전달되는 파라미터의 이름과 일치해야한다.
- throwing의 값과 대응하는 파라미터의 타입과 일치하는 예외가 발생했을 때만 실행된다.
@After
- @After 어드바이스는 대상 객체 메소드가 종료되면 실행된다(정상 종료, 예외 종료 모두 포함).
@Around
- @Around 어드바이스는 대상 객체 메소드 실행 전후에 걸친 모든 부가 기능 로직을 담당한다.
- @Around 어드바이스는 가장 강력한 어드바이스이다. 다른 종류의 어드바이스는 @Around 어드바이스의 기능을 부분적으로 담당한다고 볼 수 있다.
- ProceedingJoinPoint.proceed() 호출 여부를 선택할 수 있다.
- joinpoint.proceed(args[])를 통해 전달 값을 변환할 수 있다.
- ProceedingJoinPoint.proceed()의 반환 값 또는 대상 객체 메소드에서 발생한 예외를 변환할 수 있다.
어드바이스 종류에 따른 적용 순서는 아래와 같다.
- 호출: @Around -> @Before -> target
- 반환: target -> @AfterReturning or @AfterThrowning -> @After -> @Around
AsepctV6Advice는 @Around 어드바이스인 doTransaction()을 다른 종류의 어드바이스들로 어떻게 구분될 수 있는 지를 보여준다.
@Slf4j
@Aspect
public class AspectV6Advice {
/**
* @Aspect에 사용할 수 있는 어드바이스의 종류로는 @Around, @Before, @AfterReturning, @AfterThrowing, @After가 있다.
* @Around를 제외한 어드바이스들은 @Around의 기능을 부분적으로 제공한다.
*/
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
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.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());
}
}