importcom.example.Chapter3Application;importcom.example.app.OrderRepository;importcom.example.app.OrderService;importlombok.extern.slf4j.Slf4j;importorg.junit.jupiter.api.Test;importorg.springframework.aop.support.AopUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.test.context.ContextConfiguration;importstaticorg.assertj.core.api.Assertions.assertThatThrownBy;@Slf4j@SpringBootTest@ContextConfiguration(classes=Chapter3Application.class)publicclassAopTest{@AutowiredprivateOrderServiceorderService;@AutowiredprivateOrderRepositoryorderRepository;@TestvoidaopInfo(){//AOP 적용 여부 확인log.info("isAopProxy, orderService={}",AopUtils.isAopProxy(orderService));log.info("isAopProxy, orderRepository={}",AopUtils.isAopProxy(orderRepository));}@Testvoidsuccess(){orderService.orderItem("test");}@Testvoidexception(){assertThatThrownBy(()->orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);}}
스프링 AOP 구현1 - 시작
우선 AOP를 간단하게 구현해보자.
애스펙트
@Around 애노테이션의 값인 execution(* com.example.app..*(..))는 포인트컷이 된다.
@Around 애노테이션의 메소드인 doLog는 어드바이스가 된다.
execution(* com.example.app..*(..))는 com.example.app 패키지와 그 하위 패키지를 지정한다.
AspectJ 포인트컷 표현식
app 뒤의 ..이 하위 패키지를 의미한다.
해당 애스팩트를 적용하면 OrderService와 OrderRepository의 모든 메소드는 AOP 적용의 대상이 된다.
스프링은 프록시 방식의 AOP를 사용하므로 프록시를 통하는 메소드만 적용 대상이 된다.
참고
스프링 AOP는 AspectJ의 문법을 차용하고, 프록시 방식의 AOP를 제공한다.
그렇다고 AspectJ를 직접 사용하는 것은 아니다.
스프링 AOP를 사용할 때는 @Aspect 애노테이션을 주로 사용한다.
@Aspect 애노테이션도 AspectJ가 제공하는 애노테이션이다.
스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이다.
실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다.
packagecom.example.aop;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;@Slf4j@AspectpublicclassAspectV1{//com.example.app 패키지와 하위 패키지@Around("execution(* com.example.app..*(..))")publicObjectdoLog(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[log] {}",joinPoint.getSignature());//join point 시그니처returnjoinPoint.proceed();}}
테스트 생성
AopTest에 AOP를 적용한 테스트를 생성해보자.
@Import 애노테이션으로 아까 만든 애스팩트를 적용하자.
@Import(AspectV1.class)
애스팩트는 빈으로 등록해야 한다.
@Aspect는 애스펙트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다.
그래서 애스팩트는 별도로 스프링 빈으로 등록해야 동작한다.
스프링 빈으로 등록하는 방법
@Bean을 사용해서 직접 등록
@Component 컴포넌트 스캔을 사용해서 자동 등록
@Import로 설정 파일을 추가해서 사용(@Configuration 활용)
스프링 AOP 구현2 - 포인트컷 분리
@Around 에 포인트컷 표현식을 직접 넣을 수도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수도 있다.
포인트컷을 분리한 애스팩트를 구현해보자.
애스팩트
packagecom.example.aop;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;@Slf4j@AspectpublicclassAspectV2{//com.example.app 패키지와 하위 패키지@Pointcut("execution(* com.example.app..*(..))")//pointcut expressionprivatevoidallOrder(){}//pointcut signature@Around("allOrder()")publicObjectdoLog(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[log] {}",joinPoint.getSignature());returnjoinPoint.proceed();}}
테스트 생성
아까의 AopTest에서 이번에는 AspectV1 대신에 AspectV2를 적용해보자.
@Import(AspectV2.class)
@Pointcut
@Pointcut 애노테이션에 포인트컷 표현식을 사용한다.
메소드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
메소드의 반환 타입은 voidㄴ여야 한다.
포인트컷이 되는 메소드의 코드 내용은 비워둔다.
@Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다.
private , public 같은 접근 제어자는 내부에서만 사용하면 private 을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public 을 사용해야 한다.
스프링 AOP 구현3 - 어드바이스 추가
이번에는 조금 복잡하다.
로그를 출력하는 기능에 추가로 실제 트랜잭션을 적용하는 것 같은 예제 코드도 추가해보자.
트랜잭션 기능
핵심 로직 실행 직전에 트랜잭션을 시작
핵심 로직 실행
핵심 로직 실행에 문제가 없으면 커밋
핵심 로직 실행에 예외가 발생하면 롤백
애스팩트
앞서 배웠던 것처럼 포인트컷은 여러 개의 조건을 함께 사용해도 된다.
논리 연산자 사용
AND : &&
OR : ||
NOT : ~
packagecom.example.aop;importlombok.extern.slf4j.Slf4j;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Pointcut;@Slf4j@AspectpublicclassAspectV3{//com.example.app 패키지와 하위 패키지@Pointcut("execution(* com.example.app..*(..))")publicvoidallOrder(){}//클래스 이름 패턴이 *Service@Pointcut("execution(* *..*Service.*(..))")privatevoidallService(){}@Around("allOrder()")publicObjectdoLog(ProceedingJoinPointjoinPoint)throwsThrowable{log.info("[log] {}",joinPoint.getSignature());returnjoinPoint.proceed();}//com.example.app 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service@Around("allOrder() && allService()")publicObjectdoTransaction(ProceedingJoinPointjoinPoint)throwsThrowable{try{log.info("[트랜잭션 시작] {}",joinPoint.getSignature());Objectresult=joinPoint.proceed();log.info("[트랜잭션 커밋] {}",joinPoint.getSignature());returnresult;}catch(Exceptione){log.info("[트랜잭션 롤백] {}",joinPoint.getSignature());throwe;}finally{log.info("[리소스 릴리즈] {}",joinPoint.getSignature());}}}