포인트컷

slee2·2022년 3월 28일
0
post-custom-banner

포인트컷 지시자

애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다.

포인트컷 표현식은 execution같은 포인트컷 지시자로 시작한다. 줄여서 PCD라 한다.

종류

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

execution을 가장 많이 사용하고, 나머지는 자주 사용하지 않는다.

예제

ClassAop

MethodAop

MemberService

MemberServiceImpl

Test

execution1

execution 문법

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • 메서드 실행 조인 포인트를 매칭한다.
  • ?는 생략할 수 있다.
  • *와 같은 패턴을 지정할 수 있다.

가장 정확한 포인트컷

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

메서드와 포인트컷 표현식의 모든 내용이 정확하게 일치한다. 따라서 true를 반환한다.

가장 생략한 포인트컷

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

*는 아무 값이 들어와도 된다는 뜻.
파라미터의 ..는 파라미터의 타입과 파라미터 수가 상관없다는 뜻.
0개도 되고 여러개도 되고

그 외의 이름, 패키지 관련 매칭들

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

    @Test
    void nameMatchStar1() {
        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() {
        pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

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

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

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

execution2

타입 매칭 - 부모 타입 허용

부모는 자식을 품을 수 있기 때문에 가능

타입 매칭 - 부모가 가지고 있는 메서드만 가능

MemberServiceImpl 클래스만 소장하고 있는 internal 메서드를 확인하려는 경우 없다고 뜸. 부모는 hello라는 메서드만 가지고 있기 때문.

파라미터

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

    //파라미터 X
    @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, ..))"); // String, * 는 파라미터가 2개
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

execution 파라미터 매칭 규칙

  • (String): 정확하게 String 타입 파라미터
  • (): 파라미터 X
  • (*): 정확히 하나의 파라미터, 대신 모든 타입
  • (*, *): 정확히 두 개의 파라미터, 모든 타입
  • (..): 숫자와 무관하게 모든 파라미터. 없어도 됨.
  • (String, ..): (String), (String, Xxx), (String, Xxx, Xxx) ...
  • (String, *): 파라미터 2개만 가능. 처음은 String

within

특정 타입을 지정할 수 있고, 매칭되는 메서드를 전부 가져옴.

	@Test
    void withinExact() {
        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과 유사하다. 그래도 execution보다 기능이 적다.
하지만 차이점이 있는데,

execution과 다르게, 부모를 가져와 자식 비교를 할 수 없기 때문에 인터페이스 선정을 할 수 없다.

근데 잘 안씀. 이유는 execution이 더 좋아서.

강의중에 파일을 통채로 복사할 정도로 잘 안쓰는듯.

args

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

executionargs의 차이점

  • execution은 파라미터 타입이 정확히 매칭되어야 한다.
  • 그런데args는 부모 타입을 허용한다.

args vs execution

args의 경우에는 런타임에 전달된 인수로 보기 때문에 부모타입도 허용이 가능하다.
execution의 경우 메서드 시그니처로 판단하기 때문에 타입이 동일해야 한다.

args의 경우 단독으로는 잘 안쓴다고 함.

@target, @within

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

@target(hello.aop.member.annotation.ClassAop)
@within(hello.aop.member.annotation.ClassAop)

@target vs @within
@target은 인스턴스의 모든 메서드를 조인 포인트로 적용한다.
@within은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.

package hello.aop.pointcut;

import hello.aop.member.annotation.ClassAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;

@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 : 내꺼만. 정적

주의
args, @args, @target 는 단독으로 사용하면 안된다.
실행 시점에 일어나는 포인트컷 적용도 결국에는 프록시가 있어야 할 수 있다.
프록시 없이 사용하면, 모든 스프링 빈에 AOP를 적용하려고 한다.
그럼 오류가 발생한다.

그러므로 execution 등과 같이 범위를 정해주는 것과 함께 사용해줘야한다.

@annotation, @args

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

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

이렇게 사용한다.
쉽게 말해 @MethodAop 어노테이션이 걸린 곳을 프록시로 만들어 주는 것이다.
좀 유용하게 사용한다.

@args의 경우, 파라미터로 들어온 인수 클래스 내부에 어노테이션이 있을때 프록시를 생성해준다고 한다. 이건 잘 안쓴다고 함.
@args(test.Check)
이런식으로 사용하면 @Check가 들어온 파라미터 객체의 내부에 있을 경우, 프록시 생성.

bean

bean: 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정한다.

bean(orderService) || bean(*Respository)
*와 같은 패턴을 사용할 수 있다.

매개변수 전달

다음 포인트컷 표현식을 사용해서 어드바이스에서 매개변수를 전달할 수 있다.
this target, args, @target, @within, @annotation, @args

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

포인트컷의 이름과 매개변수의 이름을 맞춰야 한다. arg
추가로 타입의 메서드에 지정한 타입으로 제한된다. String으로 받기 때문에 String으로 제한됨.

package hello.aop.pointcut;

import hello.aop.member.MemberService;
import hello.aop.member.annotation.ClassAop;
import hello.aop.member.annotation.MethodAop;
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.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

    @Autowired
    MemberService memberService;

    @Test
    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]{}, arg={}", 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.getClass());
        }

        @Before("allMember() && @within(annotation)")
        public void atWithin(JoinPoint joinpoint, ClassAop annotation) {
            log.info("[@within]{}, obj={}", joinpoint.getSignature(), annotation.getClass());
        }

        @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,..)에서 매개변수를 받음
logArgs3: @Before를 사용하여 매개변수를 받고, 추가로 String으로 제한함
this: 프록시 객체를 받음
target: 실제 대상 객체를 받음
@target, @within: 타입의 어노테이션을 받음
@annotation: 메서드의 어노테이션을 받음. 해당 어노테이션의 값도 받을 수 있음

this, target

this: 스프링 AOP 프록시를 호출
target: 스프링 AOP 프록시가 실제로 호출하는 대상을 호출

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
* 패턴 사용할 수 없다.
부모 타입 허용

this vs target

어려운 내용인데, 또 그렇게 중요하지는 않음.

또 많이 사용하지도 않음.

JDK 동적 프록시 : 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성
CGLIB : 인터페이스가 있어도 구체 클래스를 상속받아서 프록시 객체를 생성

이때는 문제가 없다.
this : proxy 객체를 보고 판단한다.
target : target 객체를 보고 판단한다.

MemberServiceImpl 구체 클래스 지정
this : 프록시는MemberService 인터페이스를 기반으로 구현된 새로운 클래스이다. 그러므로 MemberServiceImpl을 전혀 모르기 때문에 AOP 적용 대상이 아니다.
target : MemberServiceImpl이므로 AOP 적용 대상이다.

this : proxy 객체를 보고 판단한다.
target : target 객체를 보고 판단한다.

MemberServiceImpl 구체 클래스 지정
this : 프록시는MemberServiceImpl를 상속받아 구현된 새로운 클래스이다. 그러므로 포인트컷의 대상이 된다.
target : MemberServiceImpl이므로 AOP 적용 대상이다.

정리
this의 경우 구체 클래스를 지정하면 다른 결과가 나올 수 있다.

Test

package hello.aop.pointcut;

import hello.aop.member.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

/**
 * 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=true")
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();
        }
    }
}

CGLIB 일때

JDK 일때

post-custom-banner

0개의 댓글