https://codenme.tistory.com/117
스프링 핵심원리-고급편 7 [스프링 AOP 사용법]
https://codenme.tistory.com/116 스프링 핵심원리-고급편 6 [스프링 AOP] https://codenme.tistory.com/115 스프링 핵심원리-고급편 6 [@Aspect AOP] https://codenme.tistory.com/114 스프링 핵심원리-고급편 6 [빈 후처리기] https:
codenme.tistory.com
위의 글과 이어지는 내용입니다.
이전의 글에서부터 아래와 같은 포인트 컷 표현식을 사용했는데, 이에 대해 정리해보자.
@Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 지시자
지금부터 포인트컷 표현식을 포함한 포인트컷에 대해서 자세히 알아보자.
애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.
예) @Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 표현식은 AspectJ pointcut expression 즉 애스펙트J가 제공하는 포인트컷 표현식을 줄여서 말하는 것이다.
포인트컷 지시자
포인트컷 표현식은 execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.
포인트컷 지시자의 종류
execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.
within : 특정 타입 내의 조인 포인트를 매칭한다.
args : 인자가 주어진 타입의 인스턴스인 조인 포인트
this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
@target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트 -> 특정 클래스 내
@within : 주어진 애노테이션이 있는 타입 내 조인 포인트 (target과 유사한데, 클래스에 어노테이션 달려있으면 거기에 프록시 적용한다. 뒤에서 자세히 정리할 것)
@annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
@args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
위는 "공식 설명" 인데, 이해하기 어려워 최대한 쉽게 아래와 같이 다시 적어보았다.
execution : 포괄적인 제한 설정 가능( 패키지? , public?, 메소드명? , 파라미터?, 예외? 등등..)
within : 특정 클래스에 맞는 메소드 전부 매칭 ex: classService내의 모든 메소드
args : 매개변수를 조건으로 ex: String 타입을 1개만 가지는 메소드
this : 스프링 빈 객체(스프링 AOP 프록시) 와 비교. 만약 프록시 적용되었다면 프록시에 비교하게 된다.
target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상) 실제 객체와 비교
@target : 주어진 애노테이션이 붙은 클래스+해당 클래스의 부모 클래스들 내의 모든 메소드
@within : 주어진 애노테이션이 붙은 클래스 내의 모든 메소드(부모는 x)
@annotation : 주어진 어노테이션이 붙은 메소드를 매칭
@args : 전달된 인수의 런타임 타입에 해당 어노테이션이 있으면 매칭
bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
하지만, 포인트컷 지시자가 무엇을 뜻하는지, 사실 글로만 읽어보면 이해하기 쉽지 않기에 직접 예제를 보며 공부해보자.
우선 execution 은 가장 많이 사용하고, 나머지는 자주 사용하지 않는다. 따라서 execution 을 중점적으로 이해하자.
예제 만들기
포인트컷 표현식을 이해하기 위해 예제 코드를 하나 추가하자.
ClassAOP
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
MethodAOP
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
AOP 는 @Target이 필요하고, 타겟의 타입을 지정해준다.
@Retention은 해당 에노테이션이 살아있는 시간을 정한다.
이제 에노테이션을 적용할 서비스를 만들어보자.
인터페이스
public interface MemberService {
String hello(String param);
}
구현 클래스
@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
@Override
@MethodAop("test value")
public String hello(String param) { // 부모 타입인 MemberService 에 존재
return "ok";
}
public String internal(String param) {// 부모타입인 MemberService에 존재 x
return "ok";
}
}
- hello, internal 메소드는 String 파라미터를 받고, String 리턴
Test
@Slf4j
public class ExecutionTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
//리플랙션으로 메소드 정보 추출
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@Test
void printMethod() {
log.info("helloMethod={}", helloMethod);
}
}
AspectJExpressionPointcut 이 바로 포인트컷 표현식을 처리해주는 클래스다. 여기에 포인트컷 표현식을 지정하면 된다. AspectJExpressionPointcut 는 상위에 Pointcut 인터페이스를 가진다.
printMethod() 테스트는 MemberServiceImpl.hello(String) 메서드의 정보를 출력해준다.
이후의 테스트는 위와 같은 테스트 코드에 하나씩 추가한다고 생각하자.
![](https://blog.kakaocdn.net/dn/bGCLdv/btskiz2NKj1/H7MN6gJu0PZGwam0Kb1wUK/img.png)
이번에 알아볼 execution 으로 시작하는 포인트컷 표현식은 이 메서드 정보를 매칭해서 포인트컷 대상을 찾아낸다.
execution1
execution 문법
- 메소드 실행 조인 포인트를 매칭한다.
- ?는 생략할 수 있다.
- * 같은패턴을지정할수있다.
실제 코드를 하나씩 보면서 execution 을 이해해보자.
우선, 가장 구체적인 케이스 부터 살표보자.
execution(public String hello.aop.member.MemberServiceImpl.hello(String))
지정된 패키지의 MemberServiceImple의 hello(String) 메소드를 의미한다.
@Test
void exactMatch() {
//public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)는 BeforeEach에서 수행
//AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut(); 로 전역 처리함
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
AspectJExpressionPointcut 에 pointcut.setExpression 을 통해서 포인트컷 표현식을 적용할 수 있다.
pointcut.matches(메서드,대상 클래스)를 실행하면 지정한 포인트컷 표현식의 매칭여부를 true, false 로 반환한다.
execution으로 표현한 매칭 조건
접근제어자?: public
반환타입: String
선언타입?: hello.aop.member.MemberServiceImpl 메서드이름: hello
파라미터: (String)
예외?: 생략
접근 제어자, 반환타입, 선언 타입, 파라미터 모두 지정되어있고, 모두 일치해야 포인트컷이 true를 반환한다.
또 다른 예시를 보자.
@Test
void allMatch() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
매칭 조건
접근제어자?: 생략
반환타입: *
선언타입?: 생략
메서드이름: *
파라미터: (..) -> 파라미터의 수, 타입 상관 없다는 뜻
예외?: 없음
* 은 아무 값이 들어와도 된다는 뜻이다.
파라미터에서 .. 은 파라미터의 타입과 파라미터 수가 상관없다는 뜻이다. ( 0..* ) 파라미터는 뒤에 자세히 정리하겠다.
패키지 매칭 주의 사항
@Test
void packageExactFalse() { // hello.aop. => 정확히 hellop.aop 패키지 안에 있어야한다!
pointcut.setExpression("execution(* hello.aop.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
void packageMatchSubPackage2() { // hello.aop.. => hello.aop의 하위 모든 패키지 포함
pointcut.setExpression("execution(* hello.aop..*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
패키지에서 . , .. 의 차이를 이해해야 한다.
. : 정확하게 해당 위치의 패키지
.. : 해당 위치의 패키지와 그 하위 패키지도 포함
hello.aop.member.*(1).*(2)
ex:
- hello.aop.member.MemberServiceImpl.hello(String))");
- hello.aop.*.*(..)
(1): 타입
(2): 메서드 이름
execution2
타입 매칭 - 부모 타입 허용
@Test
void typeExactMatch() {
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void typeMatchSuperType() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
typeExactMatch() 는 타입 정보가 정확하게 일치하기 때문에 매칭된다.
typeMatchSuperType() 을 주의해서 보아야 한다.
MemberService는 memberServiceImpl가 implements한 인터페이스, 즉 부모 타입이다.
execution 에서는 MemberService 처럼 부모 타입을 선언해도 그 자식 타입은 매칭된다.
다형성에서 부모타입 = 자식타입 이 할당 가능하다는 점을 떠올려보면 된다.
타입 매칭 - 부모 타입에 있는 메서드만 허용
상황 재설명
@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
@Override
@MethodAop("test value")
public String hello(String param) { // 부모 타입인 MemberService 에 존재
return "ok";
}
public String internal(String param) {// 부모타입인 MemberService에 존재 x
return "ok";
}
}
위와 같이, hello 는 부모 타입의 메소드를 구현한 것이지만, internal은 부모 타입에 존재하지 않는다.
Test
@Test
void typeMatchInternal() throws NoSuchMethodException {
//MemberServiceImpl을 표현식에 선언
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
//당연히 MemberServiceImpl의 internal 메소드도 매칭된다.
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
//MemberServiceImpl의 부모 타입인 MemberService를 표현식에 선언
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
//MemberService는 internal이란 메소드를 가지고 있지 않기에 실패한다.
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}
typeMatchInternal() 의 경우 MemberServiceImpl 를 표현식에 선언했기 때문에 그 안에 있는 internal(String) 메서드도 매칭 대상이 된다.
typeMatchNoSuperTypeMethodFalse() 를 주의해서 보아야 한다.
이 경우 표현식에 부모 타입인 MemberService 를 선언했다. 그런데 자식 타입인 MemberServiceImpl 의 internal(String) 메서드를 매칭하려 한다. 이 경우 매칭에 실패한다. MemberService 에는 internal(String) 메서드가 없다!
부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다. 그래서 부모 타입에 있는 hello(String) 메서드는 매칭에 성공하지만, 부모 타입에 없는 internal(String) 는 매칭에 실패한다.
파라미터 매칭
//String 타입의 파라미터 허용
//(String)
@Test
void argsMatch() {
pointcut.setExpression("execution(* *(String))");
// hello 메소드는 String 파라미터 1개를 가지기에 true
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//파라미터가 없어야 함
//()
@Test
void argsMatchNoArgs() {
pointcut.setExpression("execution(* *())");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
//정확히 하나의 파라미터 허용, 모든 타입 허용
//(Xxx)
@Test
void argsMatchStar() {
pointcut.setExpression("execution(* *(*))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//숫자와 무관하게 모든 파라미터, 모든 타입 허용
//(), (Xxx), (Xxx, Xxx)
@Test
void argsMatchAll() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
//String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용
//(String), (String, Xxx), (String, Xxx, Xxx)
@Test
void argsMatchComplex() {
pointcut.setExpression("execution(* *(String, ..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
execution 파라미터 매칭 규칙은 다음과 같다.
(String) : 정확하게 String 타입 파라미터
() : 파라미터가 없어야 한다.
(*) : 정확히 하나의 파라미터, 단 모든 타입을 허용한다.
(*, *) : 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.
(..) : 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다. 0..* 로
이해하면 된다.
(String, ..) : String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
예) (String) , (String, Xxx) , (String, Xxx, Xxx) 허용
within( 많이 사용되진 않는다.)
within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다.
쉽게 이야기해서 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다.
문법은 단순한데 execution 에서 타입 부분만 사용한다고 보면 된다.
@Test
void withinExact() {
//아래와 같이 MemberServiceImpl 지정시, 그곳에 속한 hello(), internal() 모두 ok다.
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinStar() {
pointcut.setExpression("within(hello.aop.member.*Service*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void withinSubPackage() {
pointcut.setExpression("within(hello.aop..*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
**주의
그런데 within 사용시 주의해야 할 점이 있다. 표현식에 부모 타입을 지정하면 안된다는 점이다. 정확하게 타입이 맞아야 한다.
이 부분에서 execution 과 차이가 난다.
@Test
@DisplayName("타켓의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
void withinSuperTypeFalse() {
pointcut.setExpression("within(hello.aop.member.MemberService)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
@DisplayName("execution은 타입 기반, 인터페이스 선정 가능")
void executionSuperTypeTrue() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
부모 타입(여기서는 MemberService 인터페이스) 지정시 within 은 실패하고, execution 은 성공하는 것을 확인할 수 있다.
args( 많이 사용되진 않는다.)
args : 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
기본 문법은 execution 의 args 부분과 같다.
execution과 args의 차이점
- execution 은 파라미터 타입이 정확하게 매칭되어야 한다, execution 은 클래스에 선언된 정보를 기반으로 판단한다.
- args 는 부모 타입을 허용한다. args 는 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
즉, args는 런타임에 전달된 인수로 판단 (동적)하고 execution은 메서드의 시그니처로 판단한다. (정적)
ex) args는 자식타입으로 선언하더라도, 실제 파라미터가 넘어올 때, 부모 타입으로 typeCast되어 넘어오면 매칭된다.
private AspectJExpressionPointcut pointcut(String expression) {//포인트컷 생성 메서드
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
return pointcut;
}
@Test
void args() {
//hello(String)과 매칭
assertThat(pointcut("args(String)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(Object)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args()")
.matches(helloMethod, MemberServiceImpl.class)).isFalse();
assertThat(pointcut("args(..)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(*)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(String,..)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
/**
* execution(* *(java.io.Serializable)): 메서드의 시그니처로 판단 (정적)
* args(java.io.Serializable): 런타임에 전달된 인수로 판단 (동적)
*/
@Test
void argsVsExecution() {
//Args
assertThat(pointcut("args(String)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(java.io.Serializable)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(Object)")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
//Execution
assertThat(pointcut("execution(* *(String))")
.matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("execution(* *(java.io.Serializable))") //매칭 실패
.matches(helloMethod, MemberServiceImpl.class)).isFalse();
assertThat(pointcut("execution(* *(Object))") //매칭 실패
.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
String은 Serializable 인터페이스의 구현 클래스이다( 부모) 따라서 Serializable로 선언되지 않았지만, 매칭된다.
하지만 execution은 부모 타입이 안된다. 정확하게 타입이 일치해야한다.
- pointcut() : AspectJExpressionPointcut 에 포인트컷은 한번만 지정할 수 있다. 이번 테스트에서는 테스트를 편리하게 진행하기 위해 포인트컷을 여러번 지정하기 위해 포인트컷 자체를 생성하는 메서드를 만들었다.
**참고
args 지시자는 단독으로 사용되기 보다는 뒤에서 설명할 파라미터 바인딩에서 주로 사용된다.
@target, @within
정의
- @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
- @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
위의 설명만 보면 어렵다.
간단히 말하면
@target , @within 은 다음과 같이 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.
@target(hello.aop.member.annotation.ClassAop)
@within(hello.aop.member.annotation.ClassAop)
-> 어노테이션을 타입에 적용, ClassAop에 적용되는 것.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
위와 같은 AOP에 매칭된다.
@ClassAop
class Target{}
@target vs @within
@target 은 인스턴스의 모든 메서드를 조인 포인트로 적용한다. -> 부모 타입의 메소드에도 적용
@within 은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다. -> 부모의 메소드엔 적용 x
위의 그림을 테스트 코드로 구현해서 테스트 해보자.
Test
이전과 다르게, 직접 Aspect를 만들어서 테스트를 수행한다.
@target, @within 은 실제 인스턴스로 포인트컷 판단을 수행하기에 동적인 객체 인스턴스가 실제 실행되야한다.
- 정적으로 판단하는 포인트 컷은 실제 인스턴스가 필요없기에 실행되지 않아도 된다.
- 동적으로 판단하는 포인트 컷은,실제 인스턴스가 필요하기에 실행되어야 한다.
@Autowired
Child child;
@Test
void success() {
log.info("child Proxy={}", child.getClass());
child.childMethod(); //부모, 자식 모두 있는 메서드
child.parentMethod(); //부모 클래스만 있는 메서드
}
static class Config {
@Bean
public Parent parent() {
return new Parent();
}
@Bean
public Child child() {
return new Child();
}
@Bean
public AtTargetAtWithinAspect atTargetAtWithinAspect() {
return new AtTargetAtWithinAspect();
}
}
static class Parent {
public void parentMethod(){} //부모에만 있는 메서드
}
@ClassAop
static class Child extends Parent {
public void childMethod(){}
}
@Slf4j
@Aspect
static class AtTargetAtWithinAspect {
//@target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@target] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//@within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용되지 않음
@Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop)")
public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@within] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
예상 결과
parentMethod(): parent만 정의 되어있기에, @target에서는 수행되지만, @within에서는 수행되지 않는다.
childMethod(): 당연히 @target, @within 모두에서 수행된다.
parentMethod() 는 Parent 클래스에만 정의되어 있고, Child 클래스에 정의되어 있지 않기 때문에 @within 에서 AOP 적용 대상이 되지 않는다.
실행결과를 보면 child.parentMethod() 를 호출 했을 때 [@within] 이 호출되지 않은 것을 확인할 수 있다.
**참고
@target , @within 지시자는 뒤에서 설명할 파라미터 바인딩에서 함께 사용된다.
**주의
- 다음 포인트컷 지시자는 단독으로 사용하면 안된다. args, @args, @target
- 이번 예제를 보면 execution(* hello.aop..*(..)) 를 통해 적용 대상을 줄여준 것을 확인할 수 있다.
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
args , @args , @target 은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다.
프록시가 없다면 판단 자체가 불가능하다.
그런데 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점이다.
args , @args , @target는 런타임이후 판단할 수 있기에, 이때는 일단 프록시를 걸어 놓고, 어드바이스 호출할지말지 실행시점에 넘어오면 판단한다.
이 과정에서, 모든 스프링 빈에 AOP 프록시를 적용하려고 시도하게되고, 이때 스프링이 자동으로 만드는 빈들 중 final 한정자로 지정된 빈들도 있기에, final에 프록시를 적용하려는 것에서 에러가 나서 애플리케이션이 내려가버릴 수 있다.
따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.
@annotation, @args
@annotation
정의
@annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
( 주어진 어노테이션 가지고 있는 메서드를 매칭한다. )
설명
@annotation(hello.aop.member.annotation.MethodAop)
다음과 같이 메서드(조인 포인트)에 애노테이션이 있으면 매칭한다.
@Slf4j
@Aspect
static class AtAnnotationAspect {
//hello.aop.member.annotation에 있는 MethodAop 어노테이션이 걸린 메서드들에 프록시 적용한다.
@Around("@annotation(hello.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@annotation] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
....
@Override
@MethodAop("test value")
public String hello(String param) {
return "ok";
}
....
}
위와 같이, @MethodAop("test value") 어노테이션이 걸린 메서드가 포인트컷을 통과한다.
@args
정의
@args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
(전달된 파라미터의 타입에 어노테이션이 있는 경우 매칭된다.)
설명
전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭한다. @args(test.Check)
bean
정의
bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.
설명
스프링 빈의 이름으로 AOP 적용 여부를 지정한다.
이것은 스프링에서만 사용할 수 있는 특별한 지시자이다.
- bean(orderService) || bean(*Repository)
- * 과 같은 패턴을 사용할 수 있다.
@Aspect
static class BeanAspect {
@Around("bean(orderService) || bean(*Repository)")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[bean] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
OrderService , *Repository(OrderRepository)
-> 빈이름이 orderService 혹은 *Repository에 매칭되는 OrderService, OrderRepository의 메서드에 AOP가 적용된다.
(스프링 AOP는 프록시를 사용하기에, 애초에 클래스에 AOP를 걸 수 있는게 아니다.)
매개변수 전달
다시 말하자면, 어드바이저는 포인트컷, 어드바이스로 구성되어있다.
@Around("allMember() && args(arg,..)")// 포인트 컷
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {//어드바이스
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
근데 여기서 포인트 컷을 사용해서, Advice에서 자신이 적용될 메소드의 매개변수를 가져와서 사용할 수 있다.
다음은 매개변수를 어드바이스에 전달할 수 있는 포인트컷 표현식이다.
this, target, args,@target, @within, @annotation, @args
다음과 같이 사용한다.
같이 사용할 포인트컷
@Pointcut("execution(* hello.aop.member..*.*(..))")
private void allMember() {
}
매개변수 전달 예시
@Before("allMember() && args(arg,..)")//args(arg,..) 에 주목
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
- 포인트컷의 이름과 매개변수의 이름을 맞추어야 한다. 여기서는 arg 로 맞추었다.
- 추가로 타입이 메서드에 지정한 타입으로 제한된다. 여기서는 logArgs3(String arg) 와 같이
메서드의 타입이 String 으로 되어 있기 때문에 다음과 같이 정의되는 것으로 이해하면 된다.
- args(arg,..) -> args(String,..)
@Before("allMember() && args(arg,..)")
위와 같은 포인트컷을 통해, hello.aop.member 패키지 내로 범위가 좁혀지고,
args: args(arg,..)를 통해, 1개 이상의 파라미터를 가지는데, 그 타입이 String인 것으로 좁혀진다.
이후 AOP 적용 대상으로 선택된 메소드파의 파라미터로 들어온 값을 arg에 할당한다.
이제 log.info("[logArgs3] arg={}", arg); 에서 arg는, logArgs3 어드바이스가 적용될 메서드의 파라미터가 넘어오게 된다.
이번에는 오랫만에 전체 테스트 코드를 다 올려보겠다.
다양한 매개변수 전달 예시를 확인해보자.
@Slf4j
@Import(ParameterTest.ParameterAspect.class) //어드바이저 빈에 등록
@SpringBootTest
public class ParameterTest {
@Autowired
MemberService memberService;
@Test
void success() { //AOP 적용하고 싶은 메소드 실행. ( AOP가 적용되나 테스트 )
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
@Slf4j
@Aspect
static class ParameterAspect { //Aspect 생성 (다수의 어드바이저 보유 가능)
@Pointcut("execution(* hello.aop.member..*.*(..))") //포인트컷 정의
private void allMember() {
}
//다수의 어드바이저 생성 시작
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];//joinPoint.getArgs()[0]로 매개변수를 전달 받는다.
log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
@Around("allMember() && args(arg,..)")//args(arg,..)로 매개변수를 전달 받는다.
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
//주어진 조건에 부합한 메소드의 첫번째 파라미터를arg로 가져온다.
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {//String arg를 통해 String타입 파라미터를 가진 메소드로 제한.
log.info("[logArgs3] arg={}", arg);
}
@Before("allMember() && this(obj)")//this를 통해 프록시 객체를 전달 받는다.
//this(obj) -> 프록시가 Object 타입이어야한다.
// MemberService obj-> MemberService 와 호환되는 프록시여야한다.
public void thisArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && target(obj)")//this를 통해 실제 객체를 전달 받는다.
public void targetArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && @target(annotation)")//타입의 애노테이션을 전달 받는다.(
//@target(annotation) -> 어노테이션을 가져온다는 의미 : 어노테이션이 존재해야하는 제한 발생
//ClassAop annotation -> 해당 어노테이션이 ClassAop 타입이어야한다.
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @within(annotation)")//타입의 애노테이션을 전달 받는다.(within이라 부모는 x)
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
//within(annotation), ClassAop annotation
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @annotation(annotation)")//메서드의 애노테이션을 전달 받는다.
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
//annotation.value() 로 해당 애노테이션의 값을 출력할 수 있다.
}
}
}
위와 같이, this, target, args,@target, @within, @annotation, @args에도 파라미터 전달을 사용할 수 있다.
주석에 자세히 설명을 적었으니 참고하자.
this, target
정의
this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
설명
this , target 은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
- * 같은 패턴을 사용할 수 없다.
- 부모 타입을 허용한다.
this vs target
단순히 타입 하나를 정하면 되는데, this 와 target 은 어떤 차이가 있을까?
스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.
- this 는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
- target 은 실제 target 객체를 대상으로 포인트컷을 매칭한다.
프록시 생성 방식에 따른 차이
스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있다.
둘의 프록시를 생성하는 방식이 다르기 때문에 차이가 발생한다.
- JDK 동적 프록시: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성한다.
- CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.
동적 JDK
실제 객체는 구현 클래스인 memberServiceImpl타입이다.
프록시는 인터페이스인 memberService타입이다.
(xxx.memberService)
- this() -> 프록시( memberService ): this의 파라미터로 들어온 타입인memberService가 memberService와 일치하기에 AOP가 적용된다.
- target() -> 실제 객체 (memberServiceImpl ): this의 파라미터로 들어온 타입인 memberService가 memberServiceImpl의 부모 타입이기에 AOP가 적용된다
(xxx.memberServiceImpl)
- this() -> 프록시( memberService ): this의 파라미터로 들어온 타입인 memberServiceImpl가 memberService의 자식 타입이이게, AOP 적용이 불가능하다!
- 실제 객체: 실제 객체 (memberServiceImpl ): this의 파라미터로 들어온 타입인 memberServiceImpl가 memberServiceImpl 일치하기에 AOP가 적용된다
CGLIB:
실제 객체는 구현 클래스인 memberServiceImpl 타입이다.
프록시도 구현 클래스인 memberServiceImpl 타입이다.
(xxx.memberService)
- this() -> 프록시( memberServiceImpl ): this의 파라미터로 들어온 타입인memberService가 memberServiceImpl의 부모 타입이기에 AOP가 적용된다.
- target() -> 실제 객체 (memberServiceImpl ): this의 파라미터로 들어온 타입인 memberService가 memberServiceImpl의 부모 타입이기에 AOP가 적용된다
(xxx.memberServiceImpl)
- this() -> 프록시( memberServiceImpl ): this의 파라미터로 들어온 타입인 memberServiceImpl가 memberServiceImpl와 일치하기에 AOP가 적용된다
- 실제 객체: 실제 객체 (memberServiceImpl ): this의 파라미터로 들어온 타입인 memberServiceImpl가 memberServiceImpl 일치하기에 AOP가 적용된다
정리
프록시를 대상으로 하는 this 의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다는 점을 알아두자.
이제 테스트를 통해 위의 내용을 확인해보자.
Test
/**
* application.properties
* spring.aop.proxy-target-class=true CGLIB
* spring.aop.proxy-target-class=false JDK 동적 프록시
*/
@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
//@SpringBootTest(properties = "spring.aop.proxy-target-class=false") //JDK 동적 프록시
@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB
public class ThisTargetTest {
@Autowired
MemberService memberService;
@Test
void success() {
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
@Slf4j
@Aspect
static class ThisTargetAspect {
//부모 타입 허용
@Around("this(hello.aop.member.MemberService)")
public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//부모 타입 허용
@Around("target(hello.aop.member.MemberService)")
public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("this(hello.aop.member.MemberServiceImpl)")
public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("target(hello.aop.member.MemberServiceImpl)")
public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
properties = {"spring.aop.proxy-target-class=false"} : application.properties 에 설정하는 대신에 해당 테스트에서만 설정을 임시로 적용한다. 이렇게 하면 각 테스트마다 다른 설정을 손쉽게 적용할 수 있다.
spring.aop.proxy-target-class=false : 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성한다. 물론 인터페이스가 없다면 CGLIB를 사용한다.
spring.aop.proxy-target-class=true : 스프링이 AOP 프록시를 생성할 때 CGLIB 프록시를 생성한다. 참고로 이 설정을 생략하면 스프링 부트에서 기본으로 CGLIB를 사용한다.
즉, this(~~~MemberServiceImple ) 은 올바르게 MemberServiceImple의 구현객체에 AOP를 적용하지 못한 것.
이제 아래와 같이, 인터페이스가 있어도 CGLIB를 통해 프록시를 생성하도록 설정을 주고 다시 시도해보자.
//@SpringBootTest(properties = "spring.aop.proxy-target-class=false") //JDK 동적 프록시
@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB
CGLIB 사용시, 전부 올바르게 AOP가 적용된다.
**참고
this , target 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용된다.
총정리
-다양한 포인트 컷 지시자를 알아보았다 하지만 execution이 가장 중요하고 자주 사용된다. execution은 부모 타입을 허용한다.
- 그외 몇가지는 표현식들은 주로 파라미터 바인딩에 사용된다.
- within은 execution에서 type부분만 사용하는데, execution(부모 허용)과 다르게, 부모 타입은 안되고 정확하게 자기 자신 타입만 가능
args: execution의 파라미터 문법과 같으나, execution(파라미터의 타입은 정확하게 매칭되야 한다)와 다르게,
부모 타입도 허용해준다.
@target VS @within
- @target: 해당 어노테이션이 child에 있다면, child의 부모에도 전부 AOP 적용
- @within: 해당 어노테이션이 child에 있다면, child에만 적용
포인트 컷을 통한 매개 변수 전달
AOP가 적용되는 각 메소드들은, 자신의 정보를 프록시에게 전달할 수 있다.
AOP의 부가 기능을 정의하는 Advice에서는, 적용되는 메소드로 부터 몇가지 정보를 받아올 수 있다.
- this()로 프록시 정보를 받아오거나, target() 으로 실제 객체, 그리고 annotation()로 애노테이션 정보도 받아올 수 있다.
this() VS target()
this: 프록시를 대상으로
target: 실제 객체를 대상으로
하지만 프록시를 대상으로 하는 this 의 경우 구체 클래스를 지정하면 프록시 생성 전략(CFLIB or 동적 JDK)에 따라서 다른 결과가 나올 수 있다
'Spring boot' 카테고리의 다른 글
스프링 핵심원리 13(완) [스프링 AOP 실전 주의 사항] (0) | 2023.06.19 |
---|---|
스프링 핵심원리 12 [커스텀 AOP 어노테이션 만들기] (1) | 2023.06.19 |
스프링 핵심원리 10 [스프링 AOP 사용법] (0) | 2023.06.17 |
스프링 핵심원리 9 [스프링 AOP] (0) | 2023.06.17 |
스프링 핵심원리 8 [@Aspect AOP] (1) | 2023.06.17 |