2023.01.30 - [0 + 프로그래밍/0 + SpringBoot(스프링부트)] - [스프링] AOP 개념
2023.01.31 - [0 + 프로그래밍/0 + SpringBoot(스프링부트)] - [스프링] AOP 실습
위 AOP 개념과 실습 포스팅을 읽고 오면 더욱 맛있게 글을 읽을 수 있습니다.^^
[스프링] AOP 포인트컷 지시자
스프링 핵심 원리 - 고급편 - 인프런 | 강의
이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다.
포인트컷 지시자에 대해서 알아봅시다.AspectJ
는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공합니다.
이번 포스팅은 내용이 다소 재미없을 수 있습니다… ㅠㅠ
필요할때 찾아봐야쥐~~ 같은 가벼운 마음으로 보시면 좋을 것 같습니다.^^
가보자 가보자!
포인트컷 지시자
먼저 가볍게(?) 포인트컷 지시자 종류에 대해서 설명해보겠습니다.
포인트컷 지시자 종류
- execution
- 메소드 실행 조인 포인트를 매칭
- 스프링 AOP에서 가장 많이 사용
- within
- 특정 타입 내의 조인 포인트를 매칭
- args
- 인자가 주어진 타입의 인스턴스인 조인 포인트
- this
- 스프링 빈 객체(
스프링 AOP 프록시
)를 대상으로 하는 조인 포인트
- 스프링 빈 객체(
- target
Target
객체(스프링 AOP 프록시가 가르키는 실제 대상
)를 대상으로하는 조인 포인트
- @target
- 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
- @within
- 주어진 애노테이션이 있는 타입 내 조인 포인트
- @annotation
- 메소드가 주어진 애노테이션을 가지고 있는조인 포인트를 매칭
- @args
- 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
- bean
- 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정
이 중에서 가장 많이 사용하는 execution
, @annotation
, this
, target
에 대해서 설명드리겠습니다.
본격적인 설명에 들어가기에 앞서서 핵심기능 역할을 하는 예제 코드를 작성하겠습니다.
핵심기능 예제 코드
PaymentService
public interface PaymentService {
String payment(int price);
}
PaymentServiceImpl
@Component
public class PaymentServiceImpl implements PaymentService {
@Override
public String payment(int price) {
return "가격은 "+price+" 입니다.";
}
public String paymentChild(int price) {
return price+" 자식입니다.";
}
}
execution
execution
문법은 다음과 같습니다.
execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
- 메소드 실행 조인 포인트를 매칭
?
는 생략 가능*
같은 패턴 지정 가능
패키지 매칭 규칙
*
은 아무 값이 들어와도 된다는 의미.
은 정확하게 해당 위치의 패키지..
은 정확하게 해당 위치의 패키지
execution
파라미터 매칭 규칙
(..)
: 파라미터의 타입과 파라미터 수가 상관없다는 의미(String)
: 정확하게 String 타입 파라미터()
: 파라미터가 없어야 함(*)
: 정확히 하나의 파라미터, 단 모든 타입을 허용(*, *)
: 정확하게 두 개의 파라미터, 단 모든 타입 허용(String, ..)
:String
타입으로 시작하고, 숫자와 무관하게 모든 파라미터, 모든 타입을 허용
먼저 정확하게 모든 내용이 매칭되는 표현식부터 살펴보겠습니다.
가장 정확한 포인트컷
@Slf4j
@SpringBootTest
public class ExecutionPaymentTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method method;
@BeforeEach
public void init() throws NoSuchMethodException {
method = PaymentServiceImpl.class.getMethod("payment", int.class);
}
@Test
void exactMath() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(public String spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.payment(int))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
}
매칭 조건
- 접근제어자?:
public
- 반환타입:
String
- 선언타입?:
pring.advanced.advancedspring.practice.payment.PaymentServiceImpl
- 메소드이름:
payment
- 파라미터:
(int)
- 예외?:
없음
가장 많이 생략한 포인트컷
@Test
void allMatch() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* *(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
매칭 조건
- 접근제어자?:
생략
- 반환타입:
*
- 선언타입?:
생략
- 메소드이름:
*
- 파라미터:
(..)
- 예외?:
없음
응용(메소드 이름 매칭)
@Test
void nameMatch() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* payment(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* *ment(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
@Test
void nameMatchFalse() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* minkai(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isFalse();
}
응용(패키지 이름)
@Test
void packageExactMath1() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.payment(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
@Test
void packageExactMatch2() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.*.*(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
@Test
void packageExactSubPackage() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice..*.*(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
@Test
void packageExactFalse() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.*.*(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isFalse();
}
참고로 타입 매칭이 부모 타입도 허용됩니다.(PaymentServiceImpl
의 부모타입은 PaymentService
)
@Test
void typeMatchSuperType() {
// execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentService.*(..))");
Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}
execution
에 PaymentService
처럼 부모타입을 선언해도 자식타입(PaymentServiceImpl
)은 매칭 됩니다.
(다형성을 떠올리면 쉽게 이해가 되실거에요^^)
부모타입에 자식타입의 메소드가 없는 경우를 생각해야 합니다.
@Test
void typeMatchChild() throws NoSuchMethodException {
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.*(..))");
Method childMethod = PaymentServiceImpl.class.getMethod("paymentChild", int.class);
Assertions.assertThat(pointcut.matches(childMethod, PaymentServiceImpl.class)).isTrue();
}
// PaymentService에는 paymentChild()라는 메소드가 존재하지 않음
@Test
void typeMatchChildFalse() throws NoSuchMethodException {
pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentService.*(..))");
Method childMethod = PaymentServiceImpl.class.getMethod("paymentChild", int.class);
Assertions.assertThat(pointcut.matches(childMethod, PaymentServiceImpl.class)).isFalse();
}
typeMatchChild()
의 경우 PaymentServiceImpl
를 표현식에 선언했기 때문에 그안에 있는 paymentChild(int)
메소드도 매칭 대상이 됩니다.
그런데 typeMatchChildFalse()
여기를 주의해서 보시기 바랍니다. 👀typeMatchChildFalse()
경우 PaymentService
을 표현식에 선언했기 때문에 PaymentServiceImpl
의 paymentChild(int)
메소드는 매칭 대상이 될수 없습니다…! PaymentService
에는 paymentChild(int)
가 없기 때문입니다.!!!!!!!!!!!!!!!!!!!
@annotation
@annotation
: 메소드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
말로는 이해가 되지 않으니 코드로 보시죠!
메소드(조인 포인트)에 애노테이션이 있으면 매칭
MyMethodAop
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) // 애노테이션의 라이프사이클
public @interface MyMethodAop {
String value();
}
먼저 애노테이션을 작성합니다.
PaymentServiceImpl
@Component
public class PaymentServiceImpl implements PaymentService {
@MyMethodAop("테스트 입니다.") // 애노테이션 추가
@Override
public String payment(int price) {
return "가격은 "+price+" 입니다.";
}
public String paymentChild(int price) {
return price+" 자식입니다.";
}
}
그리고 메소드에 애노테이션을 추가 합니다.
AnnotationPaymentTest
@Slf4j
@SpringBootTest
@Import(AnnotationPaymentTest.AtAnnotationAspect.class)
public class AnnotationPaymentTest {
@Autowired
PaymentService paymentService;
@Test
void success() {
log.info("memberService={}", paymentService.getClass());
paymentService.payment(1000);
}
@Slf4j
@Aspect
static class AtAnnotationAspect {
@Around("@annotation(spring.advanced.advancedspring.practice.payment.annotation.MyMethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@annotation] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
결과
memberService=class spring.advanced.advancedspring.practice.payment.PaymentServiceImpl$$EnhancerBySpringCGLIB$$ae59ea0c
[@annotation] String spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.payment(int)
this, target
this
: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target
: target
객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
this
, target
은 다음과 같이 적용 타입 하나를 정확하게 지정해야 합니다.
this(spring.advanced.advancedspring.practice.payment.PaymentService)
target(spring.advanced.advancedspring.practice.payment.PaymentService)
*
같은 패턴을 사용할 수 없다.
부모타입 허용
this
는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭target
은 실제 target
객체를 대상으로 포인트컷을 매칭
스프링은 프록시를 생성할 때 JDK 동적 프록시
와 CGLIB
를 선택할 수 있습니다.
JDK 동적 프록시
인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체 생성
MemberService 인터페이스 지정
@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시
@Import(ThisTargetTest.ThisTargetAspect.class)
public class ThisTargetTest {
@Autowired
PaymentService paymentService;
@Test
void test() {
log.info("paymentService Proxy={}", paymentService.getClass());
paymentService.payment(100);
}
@Slf4j
@Aspect
static class ThisTargetAspect {
/**
* JDK Proxy 객체를 보고 판단 합니다.
* this는 부모타입을 허용하기 때문에 AOP가 적용됩니다.
*/
@Around("this(spring.advanced.advancedspring.practice.payment.PaymentService)")
public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
/**
* target 객체(PaymentServiceImpl)를 보고 판단 합니다.
* target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
*/
@Around("target(spring.advanced.advancedspring.practice.payment.PaymentService)")
public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
MemberServiceImpl 구체 클래스 지정
@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시
@Import(ThisTargetTest.ThisTargetAspect.class)
public class ThisTargetTest {
@Autowired
PaymentService paymentService;
@Test
void test() {
log.info("paymentService Proxy={}", paymentService.getClass());
paymentService.payment(100);
}
@Slf4j
@Aspect
static class ThisTargetAspect {
/**
* JDK Proxy 객체를 보고 판단 합니다.
* JDK Proxy 객체는 PaymentService를 상속받아서 생성하기 때문에
* PaymentServiceImpl를 알지 못합니다.
* 따라서 AOP 적용 대상이 아닙니다.
*/
@Around("this(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
public Object doThisConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-concrete] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
/**
* target 객체(PaymentServiceImpl)를 보고 판단 합니다.
* target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
*/
@Around("target(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
public Object doTargetConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-concrete] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
CGLIB
인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성
MemberService 인터페이스 지정
@Slf4j
@SpringBootTest
@Import(ThisTargetTest.ThisTargetAspect.class)
public class ThisTargetTest {
@Autowired
PaymentService paymentService;
@Test
void test() {
log.info("paymentService Proxy={}", paymentService.getClass());
paymentService.payment(100);
}
@Slf4j
@Aspect
static class ThisTargetAspect {
/**
* CGLIB Proxy 객체를 보고 판단 합니다.
* this는 부모타입을 허용하기 때문에 AOP가 적용됩니다.
*/
@Around("this(spring.advanced.advancedspring.practice.payment.PaymentService)")
public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
/**
* target 객체(PaymentServiceImpl)를 보고 판단 합니다.
* target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
*/
@Around("target(spring.advanced.advancedspring.practice.payment.PaymentService)")
public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
MemberServiceImpl 구체 클래스 지정
@Slf4j
@SpringBootTest
@Import(ThisTargetTest.ThisTargetAspect.class)
public class ThisTargetTest {
@Autowired
PaymentService paymentService;
@Test
void test() {
log.info("paymentService Proxy={}", paymentService.getClass());
paymentService.payment(100);
}
@Slf4j
@Aspect
static class ThisTargetAspect {
/**
* CGLIB Proxy 객체를 보고 판단 합니다.
* CGLIB Proxy 객체는 PaymentServiceImpl를 상속받아서 생성하기 때문에
* AOP 적용 대상 입니다.
*/
@Around("this(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
public Object doThisConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-concrete] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
/**
* target 객체(PaymentServiceImpl)를 보고 판단 합니다.
* target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
*/
@Around("target(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
public Object doTargetConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-concrete] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
this, target 정리JDK 동적 프록시 생성
방식인지, CGLIB 프록시 생성 방식
인지에 따라서 this
의 경우 결과가 다를수 있다는 것을 알아둡시다…! 🤗
'0+ 스프링 > 0+ 스프링 AOP' 카테고리의 다른 글
[스프링 AOP] 프록시 타입 캐스팅 한계 (0) | 2023.02.07 |
---|---|
[스프링 AOP] 내부 호출 문제 해결 (0) | 2023.02.07 |
[스프링 AOP] 실습 (0) | 2023.01.31 |
[스프링 AOP] 개념 (0) | 2023.01.30 |