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
를 사용했다면 이런 문제 자체가 발생하지 않는다.
제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.