Advance I/Spring

22.10.21

kinggora 2022. 10. 21. 16:37

execution1

execution 문법

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?namepattern(param-pattern)
 	throws-pattern?)
 
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

 

  • 메소드 실행 조인 포인트를 매칭한다. (메서드 정보)
  • ?는 생략할 수 있다.
  • * 같은 패턴을 지정할 수 있다.

가장 정확한 포인트컷

@Test
void exactMatch(){
    //public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
    pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

AspectJExpressionPointcut

  • pointcut.setExpression( 포인트컷 표현식 ) : 포인트컷 표현식 지정
  • pointcut.matches( Method, 대상 클래스 ) : 지정한 포인트컷 표현식의 매칭 여부를 true , false 로 반환한다.

매칭 조건

  • 접근제어자?: public
  • 반환타입: String
  • 선언타입?: hello.aop.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터: (String)
  • 예외?: 생략

가장 많이 생략한 포인트컷

@Test
void allMatch(){
    pointcut.setExpression("execution(* *(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

생략 가능한 조건은 모두 생략하고, 필요한 조건은 모두 허용(*, ..)

 

매칭 조건

  • 접근제어자?: 생략
  • 반환타입: *
  • 선언타입?: 생략
  • 메서드이름: *
  • 파라미터: (..)
  • 예외?: 없음

* 은 아무 값이 들어와도 된다는 뜻이다.

파라미터에서 .. 은 파라미터의 타입과 파라미터 수가 상관없다는 뜻이다.

패키지 매칭 관련 포인트컷

@Test
void nameMatch(){
    pointcut.setExpression("execution(* hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar(){
    pointcut.setExpression("execution(* hel*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar2(){
    pointcut.setExpression("execution(* *el*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchFalse(){
    pointcut.setExpression("execution(* nono(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
void packageExactMatch1(){
    pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactMatch2(){
    //hello.aop.member 에 있는 모든 클래스와 메서드
    pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void packageExactFalse(){
    //hello.aop 에 있는 모든 클래스와 메서드 -> member 패키지 미포함
    pointcut.setExpression("execution(* hello.aop.*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
void packageMatchSubPackage(){
    //hello.aop 와 하위에 있는 모든 클래스와 메서드 -> member 패키지 포함
    pointcut.setExpression("execution(* hello.aop..*.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

hello.aop.member.*(1).*(2)

  • (1): 타입 (클래스)
  • (2): 메서드 이름

 

패키지에서 . , .. 의 차이를 이해해야 한다. ( packageExactFalse(), packageMatchSubPackage() )

  • . : 정확하게 해당 위치의 패키지
  • .. : 해당 위치의 패키지와 그 하위 패키지도 포함

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() : execution 에서는 MemberService 처럼 부모 타입을 선언해도 그 자식 타입은 매칭된다. 다형성에서 부모타입에 자식타입을 할당할 수 있다는 점을 떠올려보면 된다.

타입 매칭 - 부모 타입에 있는 메서드만 허용

@Test
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    
    //자식 타입(MemberServiceImpl)에만 있는 메서드
    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}

 

  • execution(* hello.aop.member.MemberService.*(..)) : 표현식에 부모 타입 지정
  • pointcut.matches(internalMethod, MemberServiceImpl.class) : 타입까지는 매칭되지만, 부모 클래스에 선언되지 않은 메서드는 매칭되지 않는다. -> 매칭 실패
  • 부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다. 그래서 부모 타입에 있는 hello(String) 메서드는 매칭에 성공하지만, 부모 타입에 없는 internal(String) 는 매칭에 실패한다. ( 상속의 경우 자식 타입에서 오버라이드 하지 않더라도 부모의 메서드를 포함하고 있으므로 매칭 가능 )

파라미터 매칭

//String 타입의 파라미터 허용
//(String)
@Test
void argsMatch(){
    pointcut.setExpression("execution(* *(String))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

//파라미터 없음
//()
@Test
void argsMatchNoArgs(){
    pointcut.setExpression("execution(* *())");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

//정확히 하나의 파라미터만 허용, 모든 타입 허용
@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 또는 1개 이상의 파라미터, 모든 타입 => 0..* 로 이해하면 된다.
  • (String, ..) : String 타입으로 시작하는 1개 이상의 파라미터
  •   예) (String) , (String, Xxx) , (String, Xxx, Xxx) ..

within

within 지시자는 특정 타입 내의 조인 포인트에 대한 매칭을 제한한다.

쉽게 이야기해서 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다.

문법은 단순한데 execution 에서 타입 부분만 사용한다고 보면 된다.

 

@Test
void withinExact(){
    pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void withinStar() throws NoSuchMethodException {
    pointcut.setExpression("within(hello.aop.member.*Service*)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();

    Method helloMethodSuper = MemberService.class.getMethod("hello", String.class);
    assertThat(pointcut.matches(helloMethodSuper, MemberService.class)).isTrue();
}

@Test
void withinSubPackage() {
    pointcut.setExpression("within(hello.aop..*)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

withinStat() : *Service* -> MemberService 가능

타입만 매칭되면 타입 안의 모든 메서드 자동 매칭

주의 - 부모 타입 비허용

execution 과 차이점

표현식에 부모 타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다.

@Test
@DisplayName("타겟의 타입에만 적용, 인터페이스 지정 안됨")
void withinSuperTypeFalse() {
    pointcut.setExpression("within(hello.aop.member.MemberService)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
@DisplayName("execution은 타입 기반, 인터페이스 지정 가능")
void executionSuperTypeFalse() {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

  • MemberService* -> within과 execution 모두 MemberService, MemberServiceImpl 매칭 가능
  • MemberService -> within은 MemberService 만, execution은 MemberService, MemberServiceImpl 매칭 가능

args

인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭

기본 문법은 execution 의 args 부분과 같다.

 

execution과 차이점

  • execution 은 파라미터 타입이 정확하게 매칭되어야 한다. (String -> String 만 가능, Obejct 불가) execution 은 정적 클래스에 선언된 정보를 기반으로 판단한다.
  • args 는 부모 타입을 허용한다. 동적으로 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.

 

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();
}

@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(); //매칭 실패

}

 

  • pointcut() : AspectJExpressionPointcut 에 포인트컷은 한번만 지정할 수 있다. 포인트컷을 여러번 지정하기 위해 포인트컷 자체를 생성하는 메서드를 만들었다.
  • 자바가 기본으로 제공하는 String 은 Object , java.io.Serializable 의 하위 타입이다.
  • 정적으로 클래스에 선언된 정보만 보고 판단하는 execution(* *(Object)) 는 매칭에 실패한다.
  • 동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 args(Object) 는 매칭에 성공한다. (부모 타입 허용)

참고: args 지시자는 단독으로 사용되기 보다는 뒤에서 설명할 파라미터 바인딩에서 주로 사용된다.

@target, @within

클래스에 애노테이션 -> AOP 적용 대상

 

정의

  • @target : 실행 객체(인스턴스)의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트 

설명

@target , @within 은 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.

@ClassAop
class Target{}
  • @target(hello.aop.member.annotation.ClassAop) -> @ClassAop 가 붙은 클래스의 인스턴스
  • @within(hello.aop.member.annotation.ClassAop) -> @ClassAop 가 붙은 클래스

@target vs @within

@target 은 동적으로 인스턴스의 모든 메서드를 조인 포인트로 적용한다. 

@within 은 정적으로 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.

 

적용 범위

 

@target 으로 지정된 애노테이션이 붙은 자식 인스턴스의 메서드 (부모의 메서드 포함)

@within 으로 지정된 애노테이션이 붙은 자기 클래스의 메서드

 

@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {

    @Autowired Child child;

    @Test
    void success(){
        log.info("child proxy={}", child.getClass()); //Child 가 아닌 프록시 인스턴스
        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: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
        //childMethod(), parentMethod()
        @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: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용되지 않음
        //childMethod()
        @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();
        }
    }
}

 

실제 인스턴스의 타입을 판단해야 하므로 스프링 관련 여러 설정들이 추가되었다.

 

success() 실행 결과

[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()

 

childMethod() -> @target, @within 모두 걸림. atTarget(), atWithin() 어드바이스 모두 적용

parentMethod() -> @within 에서 AOP 적용 대상이 안됨. atTarget() 어드바이스만 적용

 

참고: @target , @within 지시자는 뒤에서 설명할 파라미터 바인딩에서 함께 사용된다.

주의: 다음 포인트컷 지시자는 단독으로 사용하면 안된다. args, @args, @target

이번 예제를 보면 execution(* hello.aop..*(..)) 를 통해 적용 대상을 줄여준 것을 확인할 수 있다.

args , @args , @target 은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.

실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있다.

 

*포인트컷 적용 시점*

1. 프록시 생성 여부 -  애플리케이션 로딩 시점

2. 어드바이스 적용 여부 - 프록시 호출(런타임) 시점 (즉, 이미 프록시가 있음) -> args, @args, @target 이때 의미가 있음

  반대로 이야기 하면, 프록시 없이는 판단 자체가 불가능하기 때문에 모든 스프링 빈에 대한 프록시를 생성한다.

 

스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점이다. 이때 args , @args , @target 같은 포인트컷 지시자가 있으면 스프링은 모든 스프링 빈에 AOP를 적용하려고 시도한다.

문제는 이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면 스프링이 내부에서 사용하는 빈 중에는 final 로 지정된 빈들도 있기 때문에 오류가 발생할 수 있다.

따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

@annotation, @args

@annotation

메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭

@annotation(hello.aop.member.annotation.MethodAop)

 

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

    @Test
    void success(){
        log.info("memberService Proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Aspect
    static class AtAnnotationAspect {
        @Around("@annotation(hello.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 

@Component
public class MemberServiceImpl implements MemberService{

    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }
    ...

 

스프링 빈의 메서드(조인 포인트)에 @annotation 지시자로 지정한 애노테이션을 붙임 -> AOP 적용 대상

 

success() 실행 결과

memberService Proxy=class hello.aop.member.MemberServiceImpl$$EnhancerBySpringCGLIB$$6f15840a
[@annotation] String hello.aop.member.MemberServiceImpl.hello(String)

@args

전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

 

@args(test.Check) : 전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭된다.