지난 포스팅에서 AOP에 대해 알아보았다.
AOP란 핵심 기능과 부가 기능으로 관점을 나눠 관리하는 것을 말한다.
여기서 AOP를 적용하는 방법은 3가지가 있었다.
컴파일 시점, 클래스 로더 적용 시점, 런타임 시점
우리가 여태했던 방식은 런타임(프록시)시점으로 조인포인트는 메서드 실행 시점으로만 제한되고 다른 제약 사항들도 존재하지만, 별도의 컴파일러나 클래스 로더 조작기 없이 단지 스프링만 있으면 적용할 수 있다는 특징이 있었다.
이번 포스팅에서는 이 세번째 프록시를 이용한 AOP 적용 방법을 이용해 실제 AOP를 구현하는 방법에 대해 알아보자.
implementation 'org.springframework.boot:spring-boot-starter-aop'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
//저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
@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
@SpringBootTest
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);
}
}
가장 단순한 방법으로 구현해보자
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint)throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니쳐
return joinPoint.proceed();
}
조인포인트 시그니처를 로그로 남기는 로직이다.
메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
"execution( hello.aop.order..(..))"
포인트컷의 역할을 한다.
hello.aop.order와 그 하위패키지를 지정한다.
@Around
메서드 단위로 붙어 해당 메서드가 어드바이스가 된다.
@Aspect는 컴포넌트 스캔이 되지는 않으므로 따로 스프링빈으로 등록해줘야 한다.
@Slf4j
@SpringBootTest
@Import(AspectV1.class)
public class AopTest {
이제 프록시 생성 여부를 확인해보면
프록시가 생성된 것을 확인할 수 있다.
@Slf4j
@Aspect
public class AspectV2 {
//hello.aop.order. 패키지와 그 하위 패키지
@Pointcut("execution(* hello.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();
}
}
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order. 패키지와 그 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder() {
} // pointcut signature
// 클래스 이름 패턴이 *Service
@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());
}
}
}
이 경우 로직의 흐름은 다음과 같다.
public class Pointcuts {
//hello.aop.order. 패키지와 그 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {
} // pointcut signature
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {
}
// allOrder && allService를 활용한 포인트컷
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
@Slf4j
@Aspect
public class AspectV4Pointcut {
@Around("hello.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("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());
}
}
}
어드바이스는 기본적으로 순서를 보장하지 않는다.
다만 @Apect단위로 어드바이스의 순서를 지정할 수 있다.
@Aspect는 클래스 단위로 붙기 때문에 같은 @Aspect에 있는 어드바이스끼리는 순서를 보장할 수 없고,
이럴 경우 별도의 @Order()를 이용해 별도의 클래스로 나눠서 관리해야한다.
Order() : 파라미터의 수가 작을수록 우선순위가 높다.
@Slf4j
@Aspect
public class AspectV5Order {
@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()); // join point 시그니쳐
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect{
// hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
@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());
}
}
}
}
그럼 순서를 지정하기 전과 후의 결과를 확인해보자.
어드바이스 순서 조정하기 전
빨간색은 doLog(), 노란색은 doTransaction()이다.
어드바이스 순서 조정 후
@Order()로 정한 우선순위에 따라 어드바이스가 적용되는 것을 확인할 수 있다.
이것으로 여러가지 AOP 구현 방법에 대해서 알아보았다.
가장 기본적인 @Aspect 형태, 다수의 포인트컷, 포인트컷을 별도로 관리하고 적용하는 방법, @Order를 이용해 어드바이스 순서를 지정하는 방법 등에 대해서 알아보았다.
다음 포스팅에서는 @Around를 포함한 어드바이스의 종류에 대해서 알아보자.
출처 : 김영한 - 스프링 핵심 원리 고급편