@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
//추가 로직 수행
return joinPoint.proceed();
}
@Around
어노테이션의 값 execution..
은 포인트컷이 된다.@Around
어노테이션의 메서드인 doLog
는 어드바이스가 된다.참고로 스프링 AOP는 AspectJ의 문법을 차용하고 프록시 방식의 AOP 방식을 제공한다.
AspectJ를 직접 사용하는 것이 아니다.
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder() {} //pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
//내용 동일
}
@Pointcut
@Pointcut
에 포인트컷 표현식을 사용한다.@Around("{함수명()}")
형태로 사용할 수 있으며 &&, ||, ! 3가지 조합으로 여러 포인트컷을 조합할 수도 있다. 위 방식으로 하나의 에스펙트에 여러 어드바이스를 지정하면 순서를 보장 받을 수 없다.
따라서 애스펙트를 별도의 클래스로 분리해야한다.
@Slf4j
public class AspectOrder {
@Aspect
@Order(2)
public static class LogAspect {
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint JoinPoint) throws Throwable {
//로직
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("orderAndService()')
public Object doTran(ProceedingJoinPoint JoinPoint) throws Throwable {
//로직
}
}
}
위와 같이 @Aspect
를 클래스 내부에 위치시킨 후 @Order()
어노테이션으로 적용시키면 동일 에스펙트에 적용되는 어드바이스의 순서를 지정할 수 있다.
지금까지는 @Around
만 사용했지만 다음과 같은 어드바이스들이 존재한다.
복잡해 보이지만 사실 @Around
어드바이스를 제외한 나머지 어드바이스들은 @Around
가 할 수 있는 일의 일부만 제공할 뿐이다.
@Around
: 메서드 호출 전후에 수행, 가장 강력한 어드바이스@Before
: 조인 포인트 실행 이전에 실행@AfterReturning
: 조인 포인트가 정상 완료후 실행@AfterThrowing
: 메서드가 예외를 던지는 경우 실행@After
: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
좋은 설계는 제약이 있는 것이다.
어드바이스 구성을 보면 @Around
는 ProceedingJoinPoint
클래스를 인수로 받고 나머지는 JoinPoint
클래스를 받는데 JoinPoint
는 proceed()
메서드가 존재하지 않는다.
이는 일종의 가이드 역할을 한다. 개발자가 @Around
를 호출했는데 proceed()
를 호출하지 않으면 큰 장애가 발생할 수 있다. 이는 실수를 미연에 방지하게 해준다.
또한 이런 제약 덕분에 코드의 의도가 명확해진다. 메서드의 이름으로 로직 수행 시점을 파악할 수 있다.
애스펙트J는 포인트컷을 편리하게 사용하기 위한 특별한 표현식을 제공한다.
@Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 표현식은 execution
같은 포인트컷 지시자로 시작한다. 줄여서 PCD라고 한다.
종류는 아래와 같다.
execution
: 메소드 실행 조인 포인트를 매칭한다. (가장 많이 사용)within
: 특정 타입 내의 조인 포인트를 매칭한다.args
: 인자가 주어진 타입의 인스턴스인 조인 포인트this
: 스프링 빈 개체(AOP 프록시)를 대상으로 하는 조인 포인트target
: Target 객체(프록시가 가르키는 실제 대상)을 대상으로 하는 조인 포인트@Target
: 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트@within
: 주어진 어노테이션이 있는 타입 내 조인 포인트@annotation
: 메서드가 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭@args
: 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정
- execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
- execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
ex) execution(* hello.aop.*.*(..))
execution 문법은 위 구문으로 표현가능하다. ?
는 생략 가능하며, *
같은 패턴을 지정할 수 있다.
패턴 매칭 관련 특수 문법은 아래와 같다.
*
는 아무 내용이나 들어올 수 있다는 뜻이다..
은 정확하게 해당 위치 패키지, ..
은 해당 위치 및 하위 패키지 포함한다는 뜻이다.()
은 파라미터 없음, (*)
은 정확히 하나의 파라미터, (..)
은 타입에 무관하게 여러 파라미터 허용, (String, ..)
은 첫번째 인수는 String 타입으로 시작하고 나머지는 타입에 무관하게 여러 파라미터 허용 ex) within(hello.aop.member.*Service*)
within
은 특정 타입 내의 조인포인트로 매칭을 제한한다.
주의점은, execution
과 다르게 표현식에 부모 타입을 지정할 수 없다는 점이다.
ex) args(String,..)
args
는 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭한다.
주의점은, execution
은 정적으로 메서드의 시그니처를 보고 판단하지만 args
는 동적으로 런타임에 전달된 인수로 판단하기 때문에 상위 타입 지정이 가능하다.
args
지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용한다.
@Target
: 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트@within
: 주어진 어노테이션이 있는 타입 내 조인 포인트 둘은 차이가 있는데, 쉽게 이야기해서 @target
은 부모 클래스의(주어진 타입과 같음) 메서드까지 어드바이스를 모두 적용하고, @within
은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용한다.
주의점은, 다음 포인트컷 지시자는 단독으로 사용하면 안된다는 점이다. args, @args, @target
해당 지시자들은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.(런타임)
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 판단할 수 있다.
그런데 스프링 컨테이너가 프록시를 생성하는 시점은 컨테이너가 만들어지는 애플리케이션 로딩 시점에 적용할 수 있다.
따라서 args, @args, @target
지시자가 있으면 스프링은 모든 빈에 AOP를 적용하려고 시도한다.
문제는 이때 스프링 내부의 빈들 중 final
로 지정된 빈들도 있기 때문에 오류가 발생한다.
따라서 이런 표현식들은 다음과 같이 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.
@Around("execution( hello.aop..(..)) && @within(hello.aop.member.annotation.ClassAop)")
@annotation
: 메서드에 주어진 어노테이션을 가지고 있는 조인 포인트를 매칭@args
: 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트 bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.bean(orderService) || bean(*Repository)
처럼 연산자 사용가능*
같은 패턴을 사용할 수 있다. 다음 지시자들은 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
this, target, args, @target, @within, @annotation, @args
각 예시는 아래와 같다.
//지시자 사용X
@Around("allMember()")
public Object test(ProceedingJoinPoint joinPoint) {
log.info("arg1={}", joinPoint.getArgs()[0]);
return joinPoint.proceed();
}
//args 사용한 파라미터 전달
@Around("allMember() && args(arg,..)")
public void test(String arg) {
log.info("arg={}", arg); //첫번째 파라미터 출력
}
//this를 사용한 프록시 객체 전달
@Around("allMember() && this(obj)")
public void test(MemberService obj) {
log.info("obj={}", obj); //AOP 프록시 객체 정보 출력
}
//target을 이용한 실제 target 객체 전달
@Around("allMember() && target(obj)")
public void test(MemberService obj) {
log.info("obj={}", obj); //실제 객체 정보 출력
}
//@target을 이용한 클래스 어노테이션 정보 출력
@Around("allMember() && @target(annotation)")
public void test(ClassAop annotation) {
log.info("annotation={}", annotation); //클래스 어노테이션 정보 출력
}
//@within을 이용한 클래스 어노테이션 정보 출력
@Around("allMember() && @within(annotation)")
public void test(ClassAop annotation) {
log.info("annotation={}", annotation); //클래스 어노테이션 정보 출력
}
//@annotation을 이용한 메서드 어노테이션 정보 출력
@Around("allMember() && @annotation(annotation)")
public void test(MethodAop annotataion) {
log.info("annotation={}", annotation); //메서드 어노테이션 정보 출력
}
this
: 스프링 빈 객체(AOP 프록시)를 대상으로 하는 조인포인트target
: Target 객체(AOP 프록시의 실제 대상)을 대상으로 하는 조인포인트this
, target
은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.
부모 타입을 허용하며 *
같은 패턴을 사용할 수 없다.
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
this
, target
특정 상황에서 차이가 있다.
프록시 생성 방식에는 위와 같이 두가지 방식이 있다.
- JDK 동적 프록시 : 인터페이스를 구현한 프록시 객체를 생성
- CGLIB 프록시 : 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체 생성
이 때, this
, target
의 대상을 인터페이스, 구체 클래스로 하는 경우 다음과 같은 차이가 발생한다.
- JDK 동적 프록시 방식
- 대상을 인터페이스로 지정할 경우 :this
,target
모두 AOP 적용 대상으로 인식 (JDK 동적 프록시 방식은 인터페이스를 구현하므로)
- 대상을 구체클래스로 지정할 경우 :target
만 AOP 적용 대상으로 인식 (JDK 동적 프록시에서 인터페이스 정보만 있고 구체클래스 정보는 없기 때문)- CGLIB 프록시 방식
- 대상을 인터페이스로 지정할 경우 :this
,target
모두 AOP 적용 대상으로 인식 (부모 타입을 허용하므로)
- 대상을 구체클래스로 지정할 경우 :this
,target
모두 AOP 적용 대상으로 인식 (CGLIB 방식은 구체 클래스를 상속 받으므로)