이 포스팅은 간단한 예제를 이용해서, Spring AOP를 구현하며 이해하는 것이 목적이다
JPA 또는 Spring AOP를 이용해 구현한 것을 프로젝트의 dependency에 이미 가지고 있는 경우, 이미 AOP의존성을 사용하고 있기 때문에, dependency를 명시적으로 넣어주지 않아도 된다.
그렇지 않을 경우, build.gradle에 아래 내용을 추가하여 AOP를 사용하겠다고 명시 해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
@Aspect
를 사용하기 위해서는, main함수 위에 아래 내용을 추가해야 한다. 그러나, 스프링 부트를 사용할 경우 자동으로 추가되어 있으니 따로 명시해주지 않아도 된다.
@EnableAspectJAutoProxy
orderItem이 실행되면 log가 찍히고, repository에 save한다.
@RequiredArgsConstructor
@Service
@Slf4j
public class OrderService {
private final OrderRepository orderRepository;
public void orderItem(String itemId) {
log.info("[orderService] 실행");
orderRepository.save(itemId);
}
}
save가 실행되면 Log가 찍히고, 올바를 경우 "ok" 옳지 않을 경우 예외를 반환한다.
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
}
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* isyoudwn.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 타깃 메서드 호출
}
}
@Around("execution(* isyoudwn.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 타깃 호출
}
@Around 애너테이션의 doLog()
("execution(* isyoudwn.aop.order..*(..))")
1. @Component
2. @Bean
3. @Import(@Configuration)
위 세가지 중, test 코드에 간단하게 import해서 확인할 것이기 때문에 Import를 사용하였지만, Component Scan을 자동으로 하는 Spring Boot의 특성을 살려, Component를 이용하는 것도 좋은 방법이다.
@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() {
Assertions.assertThatThrownBy(()
-> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
지금까지는 Advcie와 PointCut이 결합된 상태였는데, PointCut은 따로 분리할 수 있다.
@Aspect
@Slf4j
public class AspectV2 {
@Pointcut("execution(* isyoudwn.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(); // 타깃 호출
}
}
@Aspect
@Slf4j
public class AspectV3 {
@Pointcut("execution(* isyoudwn.aop.order..*(..))")
private void allOrder() {};
@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());
}
}
}
AOP 실행 순서는, 직접 지정해주지 않는 이상 순서가 보장되지 않는다. 더 자세한 내용은 'Advice 순서 지정'에서 설명하도록 하겠다.
public class PointCuts {
@Pointcut("execution(* isyoudwn.aop.order..*(..))")
public void allOrder() {};
// 클래스 이름 패턴이 *Service 인 것
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {};
@Pointcut("allOrder() && allService()")
public void orderAndService() {};
}
Advice에서는 포인트 컷을 참조를 통해 부르면 된다
@Slf4j
@Aspect
public class AspectV4 {
@Around("isyoudwn.aop.order.aop.PointCuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 타깃 호출
}
//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인 것
@Around("isyoudwn.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());
}
}
}
Advice가 실행되는 순서가 보장되어 있지 않다(즉, 한 메서드에 여러 AOP를 지정하면, 어떤 것이 먼저 실행될지는 모른다는 것이다)
따라서 Order 어노테이션을 이용해서 순서를 직접 지정해 주어야 한다.
그러나 Order 순서는 클래스 단위로 보장된다 → 즉, Aspect 단위로 보장된다.
아래와 같이, 하나의 Aspect 안에 여러 Advice가 존재할 경우 메서드에 Order을 작성해도 순서가 보장되지 않는다는 것이다.
@Slf4j
@Aspect
public class AspectV4
@Around("isyoudwn.aop.order.aop.PointCuts.allOrder()")
@Order(2)
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 타깃 호출
}
//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인 것
@Around("isyoudwn.aop.order.aop.PointCuts.orderAndService()")
@Order(1)
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 AspectV5 {
@Aspect
@Order(2)
public static class LogAspect {
@Around("isyoudwn.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("isyoudwn.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());
}
}
}
}