@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
- @Around 애노테이션 값인
execution(* hello.aop.order..*(..))
는 포인트컷이 된다.- @Around 애노테이션의 메소드인 doLog는 Advice가 된다.
execution(* hello.aop.order..*(..))
: hello.aop.order 패키지와 그 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다.- 스프링은 프록시 방식의 AOP를 사용하므로 프록시를 통하는 메소드만 적용 대상이 된다.
스프링 AOP는 AspectJ 문법을 차용하고 프록시 방식의 AOP를 제공한다.
AspectJ를 직접 사용하는 것이 아니다.
스프링 AOP를 사용할 때는 @Aspect 애노테이션을 주로 사용하는데 이 애노테이션도 AspectJ가 제공하는 애노테이션이다.
@Aspect를 포함한 org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다.
build.gradle에 spring-boot-starter-aop를 포함하면 스프링의 AOP 관련 기능과 함께 aspectjweaver.jar도 함꼐 사용할 수 있게 의존관계에 포함된다.
그런데 스프링에서는 AspectJ가 제공하는 애노테이션이나 인터페이스만 사용하는 것이고 실제 AspectJ가 제공하는 컴파일, 클래스 로드타임 위버를 사용하는 것은 아니다.
스프링은 프록시의 AOP를 사용한다.
@Slf4j
@SpringBootTest
@Import(AspectV1.class)
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
@Aspect는 애스팱트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다.
따라서 AspectV1을 AOP로 사용하려면 스프링 빈으로 등록해야 한다.
- @Bean을 사용해서 직접 등록
- @Component 컴포넌트 스캔을 사용해서 자동 등록
- @Import 주로 설정 파일을 추가할 때 사용(@Configuration)
@Import는 주로 설정 파일을 추가할 때 사용하지만 이 기능으로 스프링 빈도 등록할 수 있다.
isAopProxy, orderService = true
isAopProxy, orderService = true
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
@Around에 포인트컷 표현식을 직접 넣을 수도 있지만 @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있다.
@Slf4j
@Aspect
public class AspectV2 {
// hello.aop.order 패키지와 그 하위 패키지 전부
@Pointcut("execution (* hello.aop.order..*(..))")
private void allOrder() {}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
- @Pointcut에 포인트컷 표현식을 사용한다.
- 메소드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
- 메소드의 반환 타입은 void 여야 한다.
- 코드 내용은 비워둔다.
- 포인트컷 시그니처는 allOrder()이다. 이름 그대로 주문과 고나련된 모든 기능을 대상으로 하는 포인트 컷이다.
- @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다.
- private, public 같은 접근 제어자는 내부에서만 사용하면 private를 사용해도 되지만, 다른 Aspect를 참고하려면 public을 사용해야 한다.
결과적으로 위의 AspectV1과 같은 기능을 수행한다.
이렇게 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있다.
그리고 뒤에 설명하겠지만 다른 클래스에 있는 외부 Advice에서도 포인트컷을 함께 사용할 수 있다.
로그를 출력하는 기능에 추가로 트랜잭션을 적용하는 코드도 추가하기 (진짜 트랜잭션 실행 X, 기능이 동작하는 것 처럼 로그만 남기기)
- 핵심 로직 실행 직전에 트랜잭션 시작
- 핵심 로직 실행
- 핵심 로직 실행에 문제가 없으면 커밋
- 핵심 로직 실행에 예외가 발생하면 롤백
@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());
final 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를 대상으로 하는데 쉽게 이야기해서 XxxService처럼 Service로 끝나는 것을 대상으로 한다.
- 여기서 타입 이름 패턴이라고 한 이유는 클래스, 인터페이스에 모두 적용되기 때문이다.
- 포인트컷은 이렇게 조합할 수 있다. &&, ||, ! 3가지 조합이 가능하다.
- hello.aop.order 패키지와 하위 패키지 이면서 타입 이름 패턴이 *Service인 것을 대상으로 한다.
- 결과적으로 doTransaction() Advice는 OrderService에만 적용된다.
- doLog() 어드바이스는 OrderService, OrderRepository에 모두 적용된다.
- orderService : doLog(), doTranscation() Advice 적용
- orderRepository : doLog() Advice 적용
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());
final 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 으로 열어두어야 한다.
doLog() -> doTransaction() 을 doTranscation() -> doLog()로 변경해보자
@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());
final 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는 기본적으로 순서를 보장하지 않는다.
순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다.
문제는 이것을 Advice 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다.
그래서 지금처럼 하나의 Aspect에 여러 Advice가 있으면 순서를 보장 받을 수 없다.
따라서 Aspect를 별도의 클래스로 분리해야 한다.
- @Around : 메소드 호출 전후에 수행, 가장 강력한 어드바이스 ,조인 포인트 실행여부 선택, 반환 값 변환, 예외 변환 등이 가능
- @Before : 조인 포인트 실행 이전에 실행
- @After Returning : 조인 포인트가 정상 완료후 실행
- @After Throwing : 메소드가 예외를 던지는 경우 실행
- @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Slf4j
@Aspect
public class AspectV6Advice {
// hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인것
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
final Object result = joinPoint.proceed();
//@AfterReturning
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void deBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return = {}", joinPoint.getSignature(), result);
}
@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(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
@Around를 제외한 나머지 Advice들은 @Around가 할 수 있는 일의 일부만 제공한다.
따라서 @Around Advice만 사용해도 필요한 기능을 모두 수행할 수 있다.
모든 Advice는 org.aspectj.lang.JoinPoint를 첫번째 파라미터에 사용할 수 있다. (생략해도 된다.)
단 @Around는 ProceedingJoinPoint을 사용해야 한다.
참고로 ProceedingJoinPoint는 org.aspectj.lang.JoinPoint의 하위 타입이다.
- getArgs() : 메소드 인수를 반환한다.
- getThis() : 프록시 객체를 반환한다.
- getTarget() : 대상 객체를 반환한다.
- getSignature() : 조언되는 메소드에 대한 설명을 반환한다.
- toString() : 조언되는 방법에 대한 유용한 설명을 인쇄한다.
- proceed() : 다음 Advice나 Target을 호출한다.
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void deBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
- @Around와 다르게 작업 흐름을 변경할 수 없다.
- @Around는 ProceedingJoinPoint.proceed()를 호출해야 다음 대상이 호출된다.
만약 호출하지 않으면 다음 대상이 호출되지 않는다.
반면에 @Before는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다.
메소드 종료시 자동으로 다음 타켓이 호출된다.
물론 예외가 발생하면 다음 코드가 호출되지 않는다.
@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 속성에 사용된 이름은 Advice 메소드의 매개변수 이름과 일치해야 한다.
- returning절에 지정된 타입의 값을 반환하는 메소드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
- @Around와 다르게 반환되는 객체를 변경할 수는 없다.
반환 객체를 변경하려면 @Around를 사용해야 한다.
참고로 반환 객체를 조작할 수는 있다.
@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());
}
- throwing 속성에 사용된 이름은 Advice 메소드의 매개변수 이름과 일치해야 한다.
- throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
- 메소드 실행이 종료되면 실행된다. (finally를 생각하면 된다)
- 정상 및 예외 반환 조건을 모두 처리한다.
- 일반적으로 리소스 및 유사한 목적을 해제하는데 사용한다.
- 메소드의 실행의 주변에서 실행된다. 메소드 실행 전후에 작업을 수행한다.
- 가장 강력한 Advice
- 조인 포인트 실행 여부 선택 : joinPoint.proceed() 호출 여부 선택
- 전달 값 변환 : joinPoint.proceed(args[])
- 반환 값 변환
- 예외 변환
- 트랜잭션 처럼 : try ~ catch ~ fianlly 모두 들어가는 구문 처리 가능
- Advice의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야 한다.
- proceed()를 통해 대상을 실행한다.
- proceed()를 여러번 실행할 수도 있다.(재시도)
- 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
- 실행 순서 : @Around, @Before, @After, @AfterReturning, @AfterThrowing
- Advice가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아야 한다.
- 물론 @Aspect 안에 동일한 종류의 Advice가 2개 있으면 순서가 보장되지 않는다. 이 경우는 @Aspect를 분리하고 @Order를 적용해야 한다.
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public void deBefore(ProceedingJoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
이 코드는 타겟을 호출하지 않는 문제가 있다.
이 코드를 개발한 의도는 타겟 실행 전에 로그를 출력하는 것이다.
그런데 @Around는 항상 joinPoint.proceed()를 호출해야 한다.
만약 실수로 호출하지 않으면 타겟이 호출되지 않는 치명적인 버그가 발생한다.
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void deBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@Before는 joinPoint.proceed()를 호출하는 고민을 하지 않아도 된다.
@Around가 가장 넓은 기능을 제공하는 것은 맞지만 실수할 가능성이 있다.
반면에 @Before, @After 같은 Advice들은 기능은 적지만 실수할 가능성이 낮고 코드도 단순한다.
그리고 가장 중요한점이 있는데 바로 이 코드를 작성한 의도가 명확하게 들어난다는 점이다.
@Before라는 애노테이션을 보는 순간 타겟 실행 전에 한정해서 어떤 일을 하는 코드구나 라는 것이 들어난다.
@Around만 있으면 되는데 왜? 이렇게 제약을 두는가?
제약은 실수를 미연에 방지한다.
일종에 가이드 역할을 한다.
만약 @Around를 사용했는데 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면 큰 장애가 발생했을 것이다.
처음부터 @Before를 사용했다면 이런 문제 자체가 발생하지 않는다.
제약 덕분에 역할이 명확해진다.
다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.