반응형
스프링을 사용하는 백엔드 개발자로서 AOP는 놓칠 수 없는 중요한 기능이다. @Aspect를 통해서 AOP를 편리하게 적용하는 방법 및 어드바이스 종류별 특징과 사용법을 알아보자.
@Aspect 기본 사용법
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // joinpoint 시그니처
return joinPoint.proceed();
}
}
- 클래스에 @Aspect 애노테이션을 붙이고, 이 클래스를 스프링 빈으로 등록해야 한다.
- @Around 애노테이션을 붙이면 애노테이션 값에 있는 표현식이 포인트컷이 되고, 해당 메서드가 어드바이스가 된다.
- @Around 내부에서는 jointPoint.proceed()로 타겟 객체를 호출할 수 있다.
- 스프링 AOP는 AspectJ 라이브러리를 직접 사용하지 않고, AspectJ의 애노테이션이나 관련 인터페이스만 사용한다. → 추후 AspectJ 라이브러리를 직접 사용하도록 변경하는 경우에도 편하게 변경할 수 있다.
@Pointcut으로 포인트컷 모듈화하기
@Around에 포인트컷 표현식을 직접 넣지 않고, @Pointcut 애노테이션을 통해 분리해서 사용할 수 있다.
같은 클래스의 포인트컷 사용
@Aspect
public class AspectV2 {
// 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());
return joinPoint.proceed();
}
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
...
return joinPoint.proceed();
}
}
- 메서드 반환 타입은 void여야 하고, 함수 바디는 비워 놓는다.
- 같은 클래스에서 사용할 포인트컷은 private을 사용해도 되는데, 외부(다른 애스펙트)에서 사용하려면 public을 사용해야 한다.
- 메서드 이름 + 파라미터 ⇒ 포인트컷 시그니처 (allOrder())
- 포인트컷을 사용할 어드바이스 애노테이션 값에 포인트컷 시그니처를 적어서 적용한다.
- 포인트컷 표현식 또는 시그니처를 &&, ||, !으로 조합하여 사용할 수 있다.
다른 클래스의 포인트컷 사용
public class Pointcuts {
// hello.aop.order 패키지와 하위 모든 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
// allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
@Aspect
public class AspectV3 {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
...
return joinPoint.proceed();
}
}
- 위 코드는 포인트컷들을 한 클래스에 모아놓고 애스펙트에서 불러와 사용한 예제이다.
- 외부 애스펙트에서 사용하기 위해 포인트컷 메서드를 public으로 지정하였다.
Advice 어드바이스
어드바이스의 종류
- 어드바이스는 @Around, @Before, @AfterReturning, @AfterThrowing, @After로 총 5가지가 있다.
- ProceedJoinPoint를 파라미터로 받는 @Around와 달리 나머지는 JoinPoint 객체를 받는다
@Before
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[Before] {}", joinPoint.getSignature());
}
- 조인포인트 실행 이전에 실행된다.
- 어드바이스 실행 후 자동으로 타켓 메서드가 호출되므로 직접 호출할 필요가 없다.
@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);
}
- 조인포인트가 정상적으로 완료된 후 실행된다.
- @AfterReturning의 retuning 속성값 = 메서드 파라미터 이름
- 반환 타입이 타겟 메서드와 맞지 않으면 어드바이스가 실행되지 않는다.
- 반환되는 result 객체를 변경할 수는 없고, 조작할 수는 있다.
@AfterThrowing
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrow(JoinPoint joinPoint, Exception ex) {
log.info("[Throw] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
- 조인포인트가 예외를 던지는 경우에 실행된다.
- @AfterThrowing의 throwing 속성값 = 메서드 파라미터 이름
- 예외 타입이 실제 발생한 예외와 일치하지 않으면 어드바이스가 실행되지 않는다.
@After
@After("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[After] {}", joinPoint.getSignature());
}
- 조인포인트가 정상 완료되든 예외가 발생하든 실행된다. (finally 같은 역할)
@Around
- 메서드 실행의 주변에서 실행된다.
- 다른 어드바이스 애노테이션의 기능을 모두 포함하는 가장 강력한 어드바이스이다.
- 첫번째 파라미터로 ProceedingJoinPoint를 사용해야 한다.
- joinPoint.proceed()를 통해 타겟 메서드를 호출할 수 있다.
- 기능
- 조인포인트 실행 여부 선택 가능
- 전달값, 반환값, 예외 변환 가능
- 타겟 메서드의 결과에 따른 try/catch/finally 같은 복잡한 구문도 처리 가능
- @Around는 모든 경우를 구현할 수 있는 가장 강력한 어드바이스이며, 나머지는 어라운드를 부분별로 나눠놓은 어드바이스이다.
- @Around로 모든 기능을 수행할 수 있지만, 항상 proceed()를 호출해야 하기 때문에 실수를 할 수 있다. @Before를 사용하면 알아서 호출해주기 때문에 실수를 줄여준다.
- 다른 어드바이스들은 기능은 적지만 실수할 가능성이 낮고 코드도 단순해진다. 또한 작성한 의도가 명확하게 드러나며 역할이 명확해진다는 장점이 있다.
어드바이스 적용 순서
@Aspect별 순서 지정
- 어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶다면 @Aspect 단위로 @Order 애노테이션을 이용해 지정할 수 있다.
- 클래스 단위로 지정할 수 있기 때문에 같은 애스펙트 내의 여러 어드바이스에 대해서는 순서를 지정할 수 없다.
public class AspectOrder {
@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 {
...
return joinPoint.proceed();
}
}
}
위 코드처럼 각 애스펙트 클래스에 @Order를 적용하여 순서를 지정할 수 있다. 순서는 숫자가 작을수록 먼저 실행된다.
@Aspect 내에서의 순서
- 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위가 정해져 있다.
클라이언트 - @Around - @Before - @After - @AfterReturning - @AfterThrowing - 타겟
- 위는 어드바이스가 적용되는 순서이며, 호출 순서와 리턴 순서는 반대이므로 실제 실행되는 순서는 아래와 같다.
- @Around → @Before → @AfterThrowing → @AfterReturning → @After → @Around
- 한 @Aspect 내에 동일한 종류의 어드바이스가 2개 이상 있다면 순서가 보장되지 않으므로, 순서를 지켜야 한다면 별도의 @Aspect 클래스로 분리하고 @Order를 적용해야 한다.
728x90
반응형
'Java > [스프링 핵심 원리]' 카테고리의 다른 글
[스프링] 관점 지향 프로그래밍 AOP란 무엇일까 (0) | 2024.03.14 |
---|---|
[스프링 핵심 원리] 컴포넌트 스캔(@ComponentScan) (0) | 2021.12.12 |
[스프링 핵심 원리] 싱글톤 컨테이너와 @Configuration (0) | 2021.12.11 |
[스프링 핵심 원리] 스프링 설정 파일 알아보기 - BeanFactory와 ApplicationContext, BeanDefinition (0) | 2021.12.08 |
[스프링 핵심 원리] 스프링 컨테이너와 스프링 빈 (0) | 2021.12.07 |
댓글