Spring/[인프런 김영한 스프링 핵심 원리 - 고급편]

[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP - 포인트컷

h2boom 2024. 12. 27. 17:40

스프링 AOP - 포인트컷

포인트컷 지시자

  • AspectJ는 포인트컷을 편리하게 표현하기 위해 특별한 표현식을 제공한다.
    • ex) @Pointcut("execution(* hello.aop.order..*(..))")
    • 포인트컷 표현식 = AspectJ가 제공하는 포인트컷 표현식

 

  • 포인트컷 지시자(PCD) : 포인트컷 표현식에서 시작 부분을 의미
  • 포인트컷 지시자 종류
    • execution : 메소드 실행 조인 포인트를 매칭한다. (가장 많이 사용하며 기능이 복잡)
    • within : 특정 타입내의 조인 포인트를 매칭한다.
    • args : 인자가 주어진 타입의 인스턴스 조인 포인트
    • this : 스프링 빈 객체(프록시)를 대상으로 하는 조인 포인트
    • target : Target 객체(원본 객체)를 대상으로 하는 조인 포인트
    • @target : 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
    • @within : 주어진 어노테이션이 있는 타입 내 조인 포인트
    • @annotation : 메소드가 주어진 어노테이션을 가지고 있는 조인 포인트 매칭
    • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트
    • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정

execution

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
  • execution 문법
    • 메소드 실행 조인 포인트를 매칭한다.
    • 형태 - execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
      • ?는 생략 가능
      • * 같은 패턴을 지정할 수 있다.
      • 반환 타입, 메소드 이름, 파라미터은 필수이다.
      • ex) execution(public java.lang.String hello.aop.order.aop.memeber.MemberServiceImpl.hello(java.lang.String) throws java.lang.NoSuchFieldError)

 

@Slf4j
public class ExecutionTest {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach()
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

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

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

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

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

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

    @Test
    public void packageMatchSubPackage1() {
        pointcut.setExpression("execution(* hello.aop.member..*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    @Test
    public void packageMatchSubPackage2() {
        pointcut.setExpression("execution(* hello.aop..*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
}
  • 포인트컷 테스트 코드 예제
    • AspectJExpressionPointcut : 포인트컷 표현식을 처리해주는 클래스이다.
    • init()
      • @BeforeEach를 통해서 모든 테스트 수행 전에 helloMethod에 MemberServiceImpl의 hello() 메소드 정보를 대입한다.
    • exactMatch() - 가장 구체적인 포인트컷 예제 
      • pointcut.setExpression() : 포인트컷 표현식을 지정
      • pointcut.matches(메소드, 대상 클래스) : 메소드가 포인트컷 표현식 부합하는지, 메소드가 포함된 대상 클래스가 포인트컷 표현식에 부합하는지 여부를 boolean 타입으로 반환
    • allMatch() - 가장 많이 생략한 포인트컷 예제
      • * : 아무 값이나 들어가도 된다는 의미
      • 파라미터 .. : 파라미터 타입과 수가 상관없다는 의미
    • nameMatch~() - 메소드 이름으로만 포인트컷과 매치 예제
      • 메소드 이름 앞 뒤에 *을 사용해서 매치할 수 있다.
    • package~() - 패키지 경로로 포인트컷과 매치 예제
      • 패키지에서의 . : 정확하게 해당 위치의 패키지만 포함
      • 패키지에서의 .. : 해당 위치 패키지와 그 하위 패키지도 포함

 

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

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

@Test
public void typeMatchInternal() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}
  • 타입 매칭 예제
    • typeExactMatch()에서는 타입 정보가 정확히 일치하므로 매칭된다.
    • typeMatchSuperType()에서는 상위 타입인 인터페이스를 선언해도 자식 타입은 매칭된다.
      • 다형성에서 부모타입 = 자식타입을 할당 가능한 것과 같다.
    • typeMatchInternal()에서는 부모 타입인 인터페이스에는 선언되지 않은 메소드(자식 타입에서만 선언된 메소드) 는 매칭되지 않는다.

 

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

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

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

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

@Test
public void argsMatchComplex() {
    pointcut.setExpression("execution(* *(String, ..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
  • 파라미터 매칭 예제
    • argsMatch() : String 타입 파라미터만 허용한다.
      • ex) (String)
    • argsMatchNoArgs() : 파라미터가 없어야 한다.
      • ex) ()
    • argsMatchStar() : 정확히 하나의 파라미터만 허용하며 모든 타입을 허용한다.
      •  ex) (Xxx)
    • argsMatchAll() : 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
      • ex) (), (Xxx), (Xxx, Xxx) ...
    • argsMatchComplex() : String 타입으로 시작해야하며 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
      • ex) (String), (String, Xxx), (String, Xxx, Xxx) ...
  • execution 파라미터 매칭 규칙
    • (타입) : 정확하게 해당 타입의 파라미터만
    • () : 파라미터가 없어야 한다.
    • (*) : 정확히 하나의 파라미터, 단 모든 타입 허용
    • (*, *) : 정확히 두 개의 파라미터, 단 모든 타입 허용
    • (..) : 숫자와 무관하게 모든 파라미터 모든 타입 허용, 파라미터가 없어도 된다.
    • (타입, ..) : 해당 타입으로 시작해야하며 숫자와 무관하게 모든 파라미터, 모든 타입 허용

within

  • within 지시자 : 특정 타입 내의 조인 포인트에 대한 매칭을 제한한다.
    • 해당 타입이 매칭되면 그 안의 메소드(조인포인트)들이 자동으로 매칭된다.
    • execution에서 타입 부분만 사용하는 것과 같다.
    • ex) within(hello.aop.member.MemberServiceImpl) => hello.aop.member.MemberServiceImpl의 모든 메소드(조인포인트)들이 자동으로 매칭된다.

 

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

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

@Test
public void withinSubPackage() {
    pointcut.setExpression("within(hello.aop..*)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
  • within으로 타입 매칭 예제
    •  *, .. 등 표현식 사용 가능

 

@Test
@DisplayName("타겟의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
public void withinSuperTypeFalse() {
    pointcut.setExpression("within(hello.aop.member.MemberService)");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}

@Test
@DisplayName("execution은 타입 기반, 인터페이스 선정 가능")
public void executionSuperTypeTrue() {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
  • within 주의사항 예제
    • within 사용 시 표현식에 부모 타입을 지정하면 안되고 정확한 타입을 지정해줘야 한다.
      • 표현식에 상위 타입인 인터페이스를 지정하면 안된다.
    • execution은 타입 기반으로 인터페이스를 지정해도 상관없다.

 

  • execution을 통해 기능을 사용할 수 있기에 within은 잘 사용되지 않는다.

args

  • args : 인자가 주어진 타입의 인스턴스인 조인포인트로 매칭한다.
    • 기본 문법은 execution의 args(파라미터) 부분과 같다.

 

  • execution과 args의 차이점
    • execution은 파라미터 타입이 정확하게 매칭되어한다.
    • args는 부모 타입을 허용하며 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.

 

@Test
public 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();
}
  • args로 파라미터 매칭
    • args는 execution의 파라미터 매칭과 유사하지만 상위 타입으로도 매칭할 수 있다.
      • 파라미터 타입이 String이여도 상위 타입인 Object로 매칭할 수 있다.
  • execution은 정적으로 클래스에 선언된 정보만 보고 판단하기에 상위 타입으로 매칭할 수 없다.
  • args는 동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하기에 상위 타입을 매칭할 수 있다.

 

  • args는 단독으로 사용하기 보다 파라미터 바인딩에서 주로 사용된다.

@target, @within

  • @target : 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
  • @within : 주어진 어노테이션이 있는 타입 내 조인 포인트
    • @target, @within 둘 다 타입(클래스)에 있는 어노테이션으로 AOP 적용 여부를 판단한다.

 

  • @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.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();
        }
    }
}
  • @target, @within 사용 예제
    • Parent, Child 클래스를 만들고 스프링 빈으로 등록 후 주입을 받아서 사용
      • Parent는 부모 클래스, Child는 Parent를 상속하는 자식 클래스
    • success()
      • parentMethod()는 Parent 부모 클래스에만 정의되어 있고 Child 클래스에는 정의되어 있지 않다.
      • child.childMethod() 실행 시 @target를 사용한 atTarget() 어드바이스와 @within를 사용한 atWithin() 어드바이스 모두 적용된다.
      • child.parentMethod() 실행 시 @within은 자기 자신의 타입에 정의된 메소드에만 어드바이스를 적용하기에 atWithin() 어드바이스는 적용되지 않고 atTarget() 어드바이스만 적용된다.
        • 만약 Child 클래스에 parentMethod()를 오버라이딩(재정의)하면 atWithin() 어드바이스도 적용된다.

 

  • @target, @within도 파라미터 바인딩에서 사용된다.

 

  • 포인트컷 지시자 사용 시 주의사항
    • args, @args, @target 지시자는 단독으로 사용하면 안된다.
      • args, @args, @target 지시자는 실제 객체 인스턴스가 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
        실행 시점에 일어나는 포인트컷 적용 여부도 프록시가 있어야 실행 시점에 판단할 수 있다.
        스프링 컨테이너가 프록시를 생성하는 시점은 애플리케이션 로딩 시점이기에 프록시를 생성하는 시점에 최적화를 할 수 없게되고 모든 스프링 빈을 프록시로 생성하게 되기에 문제가 된다.
        • 스프링 내부에서 사용하는 빈들 중 final로 지정된 빈들로 인해 오류가 발생할 수 있다.
    • args, @args, @target 지시자는 단독으로 사용하지 않고 execution 등을 사용해서 최대한 프록시 적용 대상을 축소하고 사용해야 한다.
      • ex) @Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")

@annotation, @args

  • @annotation : 메소드(조인 포인트)에 어노테이션이 있으면 매칭한다. 

 

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {
    @Autowired
    MemberService memberService;

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

    @Slf4j
    @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();
        }
    }
}
  • @annotation 사용 예제
    • Aspect는 항상 스프링 빈으로 등록해줘야 한다.
    • @Around("@annotation ~ MethodAop") : MethodAop라는 어노테이션이 달린 메소드에만 어드바이스를 적용한다.
      • 해당 어노테이션이 없는 메소드를 실행했을 때 로그로 getClass()를 찍어보면 원본 객체가 나오지만 해당 어노테이션이 있는 메소드를 실행하면 CGLIB 프록시 객체가 찍히는 것을 확인할 수 있다.

 

  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트
    • = 전달된 인수의 런타임 타입(클래스)에 해당 어노테이션을 갖고 있는 경우 매칭한다.

bean

  • bean : 스프링 전용 포인트컷 지시자로 빈의 이름으로 지정한다.
    • 스프링 빈 이름으로 AOP 적용 여부를 지정한다.
    • ex) bean(orderService) => orderService 라는 이름으로 등록된 스프링 빈을 지정

 

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {
    @Autowired
    OrderService orderService;

    @Test
    public void success() {
        orderService.orderItem("itemA");
    }

    @Aspect
    static class BeanAspect {
        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}
  • bean 사용 예제
    • OrderService, *Repository의 메소드에 AOP 적용
      • *Repository는 ~Repository 형태의 빈을 모두 포함

매개변수 전달

  • 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
    • 포인트컷의 이름과 매개변수의 이름을 맞춰야한다.
    • 타입이 메소드에 지정한 타입으로 제한된다.

 

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {
    @Autowired
    MemberService memberService;

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

    @Slf4j
    @Aspect
    static class ParameterAspect {
        @Pointcut("execution( * hello.aop.member..*.*(..))")
        private void allMember() {
        }

        @Around("allMember()")
        public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
            Object arg1 = joinPoint.getArgs()[0];
            log.info("[logArgs1]={}, args={}", joinPoint.getSignature(), arg1);
            return joinPoint.proceed();
        }

        @Around("allMember() && args(arg, ..)")
        public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
            log.info("[logArgs2]={}, arg={}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }

        @Before("allMember() && args(arg,..)")
        public void logArgs3(String arg) {
            log.info("[logArgs3] arg={}", arg);
        }

        @Before("allMember() && this(obj)")
        public void thisArgs(JoinPoint joinPoint, MemberService obj) {
            log.info("[this]={}, obj={}", joinPoint.getSignature(), obj.getClass());
        }

        @Before("allMember() && target(obj)")
        public void targetArgs(JoinPoint joinPoint, MemberService obj) {
            log.info("[target]={}, obj={}", joinPoint.getSignature(), obj.getClass());
        }

        @Before("allMember() && @target(annotation)")
        public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@target]={}, obj={}", joinPoint.getSignature(), annotation);
        }

        @Before("allMember() && @within(annotation)")
        public void atWithin(JoinPoint joinPoint, 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());
        }
    }
}
  • 매개변수 전달 예제
    • logArgs1() : joinPoint.getArgs()[0]과 같이 매개변수를 전달받는다.
    • logArgs2() : args(arg, ..)과 같이 args로 매개변수를 전달받는다.
    • logArgs3() : logArgs2()에서 @Before를 사용한 축약 버전으로 타입을 String으로 제한하고 proceed()로 별도의 원본 객체를 호출하지 않아도 된다.
    • thisArgs() : this로 프록시 객체를 전달받는다.
      • this는 스프링 컨테이너에 등록된 프록시 객체를 전달받는다.
    • targetArgs() : target으로 실제 원본 객체를 전달받는다.
      • target은 실제 원본 객체를 전달받는다.
    • atTarget(), atWithin() : @target, @within으로 타입(클래스)의 어노테이션 정보를 전달받는다.
      • @target은 부모 타입의 메소드도 포함, @within은 자기 자신 타입의 메소드만 포함
    • atAnnotation() : 메소드의 어노테이션 정보를 전달받는다.
      • annotation.value()로 어노테이션의 값을 확인할 수 있다.

this, target

  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(스프링 AOP 프록시의 원본 객체, 실제 대상)를 대상으로 하는 조인 포인트

 

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
  • this, target 사용 예시
    • this, target은 적용 타입 하나를 정확하게 지정해줘야 한다.
    • * 같은 패턴을 사용할 수 없으며 부모 타입(인터페이스)은 허용한다.

 

  • this vs target
    • this는 스프링 빈으로 등록되어있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
    • target은 실제 target 객체를 대상으로 포인트컷을 매칭한다.
    • 프록시 생성 방식에 따른 차이
      • JDK 동적 프록시 : 인터페이스가 필수이며 인터페이스를 구현한 프록시 객체를 생성
        • JDK 동적 프록시 사용 시 프록시 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스이기에 프록시 객체의 부모 타입이 MemberService이다.
        • JDK 동적 프록시로 생성하며 MemberService 인터페이스로 지정하는 경우
          • this(MemberService) : this는 프록시 객체를 보고 판단하며 부모 타입(인터페이스)을 허용하기에 AOP가 적용된다.
          • target(MemberService) : target은 실제 객체를 보고 판단하며 부모 타입을 허용하기에 AOP가 적용된다.
        • JDK 동적 프록시로 생성하며 MemberServiceImpl 구체 클래스로 지정하는 경우
          • this(MemberServiceImpl) : 프록시 객체를 보고 판단, JDK 동적 프록시로 만들어진 프록시 객체는 인터페이스를 기반으로 구현된 새로운 클래스이기에 구체 클래스를 전혀 알지 못한다. 따라서 AOP의 적용대상이 아니다.
          • target(MemberServiceImpl) : 실제 객체를 보고 판단, 실제 객체가 구체 클래스 타입이기에 AOP 적용 대상이다.
      • CGLIB : 인터페이스가 있어도 구체 클래스를 상속받아서 프록시 객체를 생성
        • CGLIB 사용 시 프록시 객체는 MemberServiceImpl 구체 클래스를 기반으로 구현된 새로운 클래스이기에 프록시 객체의 부모 타입이 MemberServiceImpl이며 그 상위 타입은 MemberService이다.
        • CGLIB로 생성하며 인터페이스로 지정하는 경우
          • this(MemberService) : 프록시 객체를 보고 판단, 부모 타입을 허용하기 때문에 AOP가 적용된다.
          • target(MemberService) : 실제 객체를 보고 판단, 부모 타입을 허용하기에 AOP가 적용된다.
        • CGLIB로 생성하며 구체 클래스로 지정하는 경우
          • this(MemberServiceImpl) : 프록시 객체를 보고 판단, CGLIB로 만들어진 프록시 객체는 MemberServiceImpl을 상속받아서 만들었기에 AOP가 적용된다. 
          • target(MemberServiceImpl) : 실제 객체를 보고 판단, 실제 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.
      • 프록시를 대상으로 하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략(CGLIB, JDK 동적 프록시)에 따라 다른 결과가 나올 수 있다.

 

//CGLIB
spring.aop.proxy-target-class=true

//JDK 동적 프록시
spring.aop.proxy-target-class=false
  • 프록시 생성 전략 application.properties 설정
    • false 옵션을 사용하면 JDK 동적 프록시 전략으로 프록시를 생성한다.
    • true 옵션을 사용하면 CGLIB 프록시 전략으로 프록시를 생성한다.
      • 스프링 부트에서는 true가 기본이기에 별도로 설정하지 않으면 CGLIB 전략으로 생성된다.

 

/**
 * 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
    public 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();
        }
    }
}
  • 프록시 생성 전략에 따른 this, target 적용 예시
    • @SpringBootTest(properties = "spring.aop.proxy-target-class=false") : application.properties에 설정하는 대신 해당 테스트에만 설정을 임시로 적용할 수 있다.
      • 해당 속성을 통해 각 테스트마다 별도의 설정을 손쉽게 적용할 수 있다.
      • false => JDK 동적 프록시 사용, true / 별도의 설정이 없는 경우 => CGLIB를 사용
    • JDK 동적 프록시를 사용하는 경우 this(구체클래스)로 포인트컷을 지정하면 AOP가 적용되지 않는다.

 

  • this, target 지시자는 단독으로 사용되기보다는 파라미터 바인딩에서 주로 사용된다.

정리

    • 포인트컷 지시자 종류
      • execution : 메소드 실행 조인 포인트를 매칭한다. (가장 많이 사용하며 기능이 복잡)
      • within : 특정 타입 내의 조인 포인트를 매칭한다.
        • execution에서 타입 부분만 사용하는 것과 같다.
      • args : 인자가 주어진 타입의 인스턴스 조인 포인트
        • 기본 문법은 execution의 args(파라미터) 부분과 같다.
          • execution은 파라미터 타입이 정확하게 매칭되어한다.
          • args는 부모 타입을 허용하며 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
      • this : 스프링 빈 객체(프록시 객체)를 대상으로 하는 조인 포인트
        • 프록시 생성 전략(CGLIB, JDK 동적 프록시)에 따라 AOP 적용 결과가 달라진다.
          • JDK 동적 프록시 사용 시 this(구체클래스)로 지정하면 프록시는 인터페이스를 기반으로 생성된 클래스이기에 구체클래스를 알지 못하고 AOP 적용이 되지 않는다.
          • CGLIB 사용 시 this(구체클래스)로 지정하면 프록시는 구체클래스를 기반으로 생성된 클래스이기에 구체클래스를 알고 있고 AOP 적용 대상이 된다.
      • target : Target 객체(실제 객체)를 대상으로 하는 조인 포인트
      • @target : 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
        • @target은 인스턴스의 모든 메소드(부모 클래스의 메소드 포함)를 조인 포인트로 적용한다.
      • @within : 주어진 어노테이션이 있는 타입 내 조인 포인트
        • @within은 해당 타입(자기 자신만 포함, 부모 타입은 해당X) 내에 있는 메소드만 조인 포인트로 적용한다.
      • @annotation : 메소드가 주어진 어노테이션을 가지고 있는 조인 포인트 매칭
      • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트
        • 전달된 인수의 런타임 타입(클래스)에 해당 어노테이션을 갖고 있는 경우 매칭한다.
      • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정
  • 여러 포인트컷 지시자가 있지만 가장 많이 사용되는 execution 위주로 숙지할 것!

 

  • execution 문법
    • 메소드 실행 조인 포인트를 매칭한다.
    • 형태 - execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
      • ?는 생략 가능
      • * 같은 패턴을 지정할 수 있다.
      • 반환 타입, 메소드 이름, 파라미터은 필수이다.
    • 부모 타입(인터페이스)을 지정해도 자식 타입도 포함된다.
      • 부모 타입을 지정하는 경우 부모 타입에 선언된 메소드만 포함된다.

출처 : [인프런 김영한 스프링 핵심 원리 - 고급편]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런

김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기

www.inflearn.com