스프링 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) ...
- argsMatch() : String 타입 파라미터만 허용한다.
- 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은 타입 기반으로 인터페이스를 지정해도 상관없다.
- within 사용 시 표현식에 부모 타입을 지정하면 안되고 정확한 타입을 지정해줘야 한다.
- 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로 매칭할 수 있다.
- args는 execution의 파라미터 매칭과 유사하지만 상위 타입으로도 매칭할 수 있다.
- 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() 어드바이스도 적용된다.
- Parent, Child 클래스를 만들고 스프링 빈으로 등록 후 주입을 받아서 사용
- @target, @within도 파라미터 바인딩에서 사용된다.
- 포인트컷 지시자 사용 시 주의사항
- args, @args, @target 지시자는 단독으로 사용하면 안된다.
- args, @args, @target 지시자는 실제 객체 인스턴스가 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
실행 시점에 일어나는 포인트컷 적용 여부도 프록시가 있어야 실행 시점에 판단할 수 있다.
스프링 컨테이너가 프록시를 생성하는 시점은 애플리케이션 로딩 시점이기에 프록시를 생성하는 시점에 최적화를 할 수 없게되고 모든 스프링 빈을 프록시로 생성하게 되기에 문제가 된다.- 스프링 내부에서 사용하는 빈들 중 final로 지정된 빈들로 인해 오류가 발생할 수 있다.
- args, @args, @target 지시자는 실제 객체 인스턴스가 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
- args, @args, @target 지시자는 단독으로 사용하지 않고 execution 등을 사용해서 최대한 프록시 적용 대상을 축소하고 사용해야 한다.
- ex) @Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
- args, @args, @target 지시자는 단독으로 사용하면 안된다.
@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 형태의 빈을 모두 포함
- OrderService, *Repository의 메소드에 AOP 적용
매개변수 전달
- 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
- 포인트컷의 이름과 매개변수의 이름을 맞춰야한다.
- 타입이 메소드에 지정한 타입으로 제한된다.
@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 동적 프록시)에 따라 다른 결과가 나올 수 있다.
- 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가 적용되지 않는다.
- @SpringBootTest(properties = "spring.aop.proxy-target-class=false") : application.properties에 설정하는 대신 해당 테스트에만 설정을 임시로 적용할 수 있다.
- this, target 지시자는 단독으로 사용되기보다는 파라미터 바인딩에서 주로 사용된다.
정리
- 포인트컷 지시자 종류
- execution : 메소드 실행 조인 포인트를 매칭한다. (가장 많이 사용하며 기능이 복잡)
- within : 특정 타입 내의 조인 포인트를 매칭한다.
- execution에서 타입 부분만 사용하는 것과 같다.
- args : 인자가 주어진 타입의 인스턴스 조인 포인트
- 기본 문법은 execution의 args(파라미터) 부분과 같다.
- execution은 파라미터 타입이 정확하게 매칭되어한다.
- args는 부모 타입을 허용하며 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.
- 기본 문법은 execution의 args(파라미터) 부분과 같다.
- this : 스프링 빈 객체(프록시 객체)를 대상으로 하는 조인 포인트
- 프록시 생성 전략(CGLIB, JDK 동적 프록시)에 따라 AOP 적용 결과가 달라진다.
- JDK 동적 프록시 사용 시 this(구체클래스)로 지정하면 프록시는 인터페이스를 기반으로 생성된 클래스이기에 구체클래스를 알지 못하고 AOP 적용이 되지 않는다.
- CGLIB 사용 시 this(구체클래스)로 지정하면 프록시는 구체클래스를 기반으로 생성된 클래스이기에 구체클래스를 알고 있고 AOP 적용 대상이 된다.
- 프록시 생성 전략(CGLIB, JDK 동적 프록시)에 따라 AOP 적용 결과가 달라진다.
- target : Target 객체(실제 객체)를 대상으로 하는 조인 포인트
- @target : 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인 포인트
- @target은 인스턴스의 모든 메소드(부모 클래스의 메소드 포함)를 조인 포인트로 적용한다.
- @within : 주어진 어노테이션이 있는 타입 내 조인 포인트
- @within은 해당 타입(자기 자신만 포함, 부모 타입은 해당X) 내에 있는 메소드만 조인 포인트로 적용한다.
- @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
'Spring > [인프런 김영한 스프링 핵심 원리 - 고급편]' 카테고리의 다른 글
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP - 실무 주의사항 (2) | 2024.12.27 |
---|---|
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP - 실전 예제 (0) | 2024.12.27 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP 구현 (0) | 2024.12.17 |
[인프런 김영한 스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2024.12.16 |
[인프런 김영한 스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2024.12.15 |