라이브러리 추가
build.gradle 에 의존성 추가를 해준다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
위의 라이브러리 추가로 aspectJ 관련 라이브러리를 등록하고 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록해준다.
@Aspect 사용 예제
스프링은 @Aspect 어노테이션으로 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원하며
어드바이저로 사용할 클래스에 @Aspect 어노테이션을 붙여줌으로써 스프링 AOP 를 적용할 수 있다.
주의할 점으로 스플이 AOP 적용시에는 private, final 메소드는 AOP 적용이 불가능하다.
@Slf4j
@Aspect
public class AspectV1 {
//hello.aop.order 패키지와 하위 패키지
@Around("execution(* hello.aop.order..*(..))") // 포인트컷 (AspectJ 표현식)
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable { // 어드바이스
// 어드바이스 로직
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
@Import(AspectV1.class)
@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);
}
}
스프링 빈을 등록 하는 방법에는 아래 3가지 방법이 있다.
- @Bean 을 사용한 직접 등록
- @Component 컴포넌트 스캔을 사용한 자동 등록
- @Import 주로 설정 파일을 추가할 때 사용 (@Configuration)
테스트에서는 버전을 올려가면서 사용하기 때문에 @Import 을 사용한다.
@Pointcut 사용 예제
@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();
}
}
- @Pointcut에 포인트 표현식을 사용한다.
- 주석에 pointcut signature 부분은 메서드 이름과 파라미터를 합쳐서 포인트 시그니처라고 한다.
- 메서드 반환타입은 void이며 코드 내용은 비워둔다.
- 다른 Aspect에서 참고하기 위해 public 접근 제어자를 사용한다.
위의 내용으로 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있으며 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용할 수 있다.
어드바이스를 추가하는 예제
로그 출력 기능에 트랜잭션을 적용하는 코드를 추가한 예제로 진짜 트랜잭션을 실행하는 것은 아닌 예제를 작성해본다.
트랜잭션 기능은 보통 다음과 같이 동작한다.
- 로직 실행 전 트랜잭션 시작 -> 로직 실행 -> 로직에 문제가 없으면 커밋 -> 로직에 예외 발생하면 롤백
@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());
}
}
}
- allOrder() 포인트컷은 hello.aop.order 패키지와 하위 패키지를 대상으로 한다.
- allService() 포인트컷은 Service 대상으로 한다. (*Servi* 와 같은 패턴도 가능)
@Around("allOrder() && allService()")
- &&(AND), ||(OR), !(NOT) 포인트 컷 조합이 가능하다.
- doTransaction() 는 Service 에만 적용되고 doLog() 는 Service, Repository 모두 적용된다.
- Service : doLog(), doTransaction() 적용
- Repository : doLog() 적용
AOP 적용 전 : orderService.orderItem() => orderRepository.save()
AOP 적용 후 : [doLog() -> doTransaction()] => orderService.orderItem() => [doLog()] => orderRepository.save()
Pointcut 참조하는 예제
포인트멋을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 되며
외부에서 호출하기 위해 public 으로 열어 둔다.
orderAndService() : allOrder(), allService() 두개의 포인트컷을 조합해서 새로운 포인트컷을 만들었다.
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();
}
@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());
}
}
}
어드바이스 순서 지정하는 예제
어드바이스는 기본적으로 순서를 보장하지 않는데 순서를 지정하고 싶으면 클래스 단위로 @Order 를 적용해야 한다.
위의 예제들처럼 하나의 Aspect에 여러 어드바이스가 있으면 순서를 보장받을 수 없기 때문에 Aspect를 별도의 클래스로 분리해서 사용해야 한다.
순서는 Order()에 적힌 작은 순서대로 실행된다.
@Slf4j
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 {
@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());
}
}
}
}
@Import 클래스에 사용하고자 하는 클래스로 변경해준다.
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@SpringBootTest
public class AopTest {
}
어드바이스 종류
@Around : 가장 강력한 어드바이스로 메서드 호출 전후에 실행한다.
조인 포인트 실행 여부 선택, 반환값 변환, 예외 변환 등이 가능하다.
@Before : 조인 포인트 실행 이전에 실행된다.
@AfterReturning : 조인 포인트가 정상 완료 후 실행된다. @Around 와 달리 반환되는 객체를 변경할 수는 없고 조작은 가능하다.
@AfterThrowing : 메서드가 예외를 던지는 경우에 실행된다.
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행한다. (finally)
@Around
메서드 실행 전후에 작업을 수행
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try {
//@Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}
}
어드바이스 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 하고, proceed()를 통해 대상을 실행한다. proceed()를 여러번 실행할 수도 있다.
@Before
조인 포인트 실행 전
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
- @Around 와 다르게 작업 흐름을 변경할 수 없다.
- @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않으며 메서드 종료시 자동으로 다음 대상이 호출된다. 예외가 발생하면 다음 코드는 호출되지 않는다.
@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);
}
- returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야한다.
- 부모 타입을 지정하면 모든 자식 타입은 인정된다.
- @Around와 다르게 반환되는 객체를 변경할 수 없지만 조작할 수 는 있다.
@AfterThrowing
메서드 실행이 예외를 던져서 종료될 때 실행
@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
메서드 실행이 종료되면 실행
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
어드바이스 호출 순서
- 스프링 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
- 실행 순서 : @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
- 어드바이스 리턴 순서는 호출 순서와 반대이다.
- @Aspect 안에 동일한 종류의 어드바이스가 있으면 순서가 보장되지 않기 때문에 @Aspect를 분리하고 @Order를 적용한다.
'공부 > Spring' 카테고리의 다른 글
[Spring] MongoDB 조회 - Stream, Aggregation 비교 (0) | 2024.11.04 |
---|---|
[Spring] 스프링 AOP 포인트컷 (0) | 2024.05.21 |
[Spring] AOP 개념 및 용어 정리 (0) | 2024.05.13 |
[Spring] @Aspect란, 생성 과정 설명 (0) | 2024.05.12 |
[Spring] 빈 후처리기 (0) | 2024.05.08 |