
implementation 'org.springframework.boot:spring-boot-starter-aop
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
위 내용을 추가해준다.




@Around 어노테이션의 값은 포인트컷이 된다.
@Around 어노테이션의 메서드인 doLog()는 어드바이스(Adivce)가 된다.
execution(* hello.aop.order..*(..))는 hello.aop.order 패키지와 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다.
이렇게 하면 패키지 안에 있는 OrderRepositoy와 OrderService의 모든 메서드는 프록시가 적용된다.

테스트에 Import어노테이션을 통해 @Aspect가 적용된 클래스를 빈에 넣어주면, 전 후 결과는 아래와 같다.
//success()
2022-03-26 11:21:37.038 INFO 98582 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:21:37.039 INFO 98582 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
//aopInfo()
2022-03-26 11:21:37.072 INFO 98582 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=false
2022-03-26 11:21:37.074 INFO 98582 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=false
//exception()
2022-03-26 11:21:37.107 INFO 98582 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:21:37.107 INFO 98582 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
//success()
2022-03-26 11:24:23.680 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] void hello.aop.order.OrderService.orderItem(String)
2022-03-26 11:24:23.708 INFO 98792 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:24:23.709 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] String hello.aop.order.OrderRepository.save(String)
2022-03-26 11:24:23.731 INFO 98792 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
//aopInfo()
2022-03-26 11:24:23.768 INFO 98792 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=true
2022-03-26 11:24:23.768 INFO 98792 --- [ Test worker] hello.aop.AopTest : isAopProxy, orderService=true
//exception()
2022-03-26 11:24:23.796 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] void hello.aop.order.OrderService.orderItem(String)
2022-03-26 11:24:23.797 INFO 98792 --- [ Test worker] hello.aop.order.OrderService : [orderService] 실행
2022-03-26 11:24:23.798 INFO 98792 --- [ Test worker] hello.aop.order.aop.AspectV1 : [log] String hello.aop.order.OrderRepository.save(String)
2022-03-26 11:24:23.798 INFO 98792 --- [ Test worker] hello.aop.order.OrderRepository : [orderRepository] 실행
@Around에 포인트컷 표현식을 직접 넣을 수도 있지만,
@Pointcut 어노테이션을 사용해서 별도로 분리할 수도 있다.

@Pointcut
void 여야 한다.allOrder()이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.@Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다.private, public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public을 사용해야 한다.테스트 부분은 동일하기 때문에 생략
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
@Slf4j
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.
XxxService처럼 Service로 끝나는 것을 대상.@Around("allOrder() && allService()")
&&, ||, ! 3가지 조합이 가능하다.doTransaction() 어드바이스는 OrderService에만 적용된다.success()


순서가 doLog -> doTx가 된다.
그런데 반대로 하고 싶다면, 어떻게 해야할까?
는 뒤에서 알아보자.
이번에는 포인트컷을 외부에서 호출하도록 만들어보자.


이런식으로 외부 참조도 가능하다.
순서를 바꾸기 위해서 @Order를 사용해도 되지만, 이 어노테이션의 경우 @Aspect 단위로만 순서를 바꿔주기 때문에 @Aspect 내부 메서드들의 순서를 바꿀수는 없다.
@Order를 이용해서 순서를 바꾸려면 아래와 같이 하면 된다.

각 클래스를 만들고 @Aspect를 적용한 뒤 @Order를 통해 순서를 정해준다.

어쩔수 없이 클래스를 분리해야한다.
어드바이스 종류
@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
package hello.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.Joinpoint;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Slf4j
@Aspect
public class AspectV6Advice {
// @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
// public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// try {
// //@Before
// log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
// 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 doBefore(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={}", ex);
}
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
@Around를 제외한 나머지 어드바이스들은 @Around의 일부만 제공하는 것.
그러므로 @Around만 사용해도 필요한 기능을 모두 수행할 수 있다.
ProceedingJoinPoint는 org.aspectj.lang.JoinPoint의 하위 타입이다.
JoinPoint 인터페이스의 주요기능
getArgs(): 메서드 인수를 반환합니다.getThis() : 프록시 객체를 반환합니다.getTarget() : 대상 객체를 반환합니다.getSignature() : 조언되는 메서드에 대한 설명을 반환합니다. toString() : 조언되는 방법에 대한 유용한 설명을 인쇄합니다.ProceddingJoinPoin의 추가기능
proceed(): 다음 어드바이스나 타겟을 호출한다.

@Before

@Around 와 다르게 작업 흐름을 변경할 수는 없다.
@Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다. 반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다. 물론 예외가 발생하면 다음 코드가 호출되지는 않는다.
@AfterReturning

returning과 파라미터로 받는 Object의 이름과 같아야함.
Object라고 두는 이유는 어떤 반환타입이 들어올지 모르기 때문,
String타입으로 둘 경우, 반환타입이 Integer면, doReturn 메서드 호출이 안됨.
반환 객체를 변경하려면 @Around를 사용해야 한다.
@AfterThrowing

throwing = Exception
@After
@Around
proceed()의 실행 여부를 결정할 수 있음joinPoint.proceed(args[]))try - catch - finally 구문 처리 가능ProceedingJoinPoint를 사용해야 한다.proceed()를 여러번 실행할 수도 있다.
@Around 외에 다른 어드바이스가 존재하는 이유
@Around는 항상 joinPoint.proceed()를 호출해야 한다.
호출하지 않으면 타겟이 호출되지 않는다.
@Before는 joinPoint.proceed()를 호출하는 고민을 하지 않아도 된다.
알아서 해주기 때문
그리고 @Before의 경우 바로 코드실행전에 호출되는 것이라는 것을 알 수 있음
좋은 설계는 제약이 있는 것이다.
제약은 실수를 미연에 방지한다. 일종에 가이드 역할을 한다. 만약 @Around 를 사용했는데, 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면? 큰 장애가 발생했을 것이다. 처음부터 @Before 를 사용했다면 이런 문제 자체가 발생하지 않는다.
제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.