[Spring] 스프링 핵심 원리 - 고급편 섹션11 스프링 AOP - 포인트컷
⬜섹션11 스프링 AOP - 포인트컷
◼️ AOP 개념
포인트컷 표현식: 에스펙트J가 제공하는 포인트컷 표현식
실습 사전 준비
- ClassAop, MethodAop, MemberService, MemberServiceImpl을 만든다. (상세 코드 in 교안)
@Slf4j
public class ExecutionTest {
// AspectJExpressionPointcut; 포인트컷 표현식을 처리해주는 클래스
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@Test
void printMethod() {
log.info("helloMethod={}",helloMethod);
}
@Test
void exactMatch() {
pointcut.setExpression("execution(public String\n" +
" hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
}
}
◼️ Exectuion
🟢 Execution 문법
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
메서드 이름 앞 뒤에 *을 사용해서 매칭할 수 있다.
🔶 메서드 이름 매칭 관련 포인트컷
// 메서드 이름 매칭 관련 포인트컷
@Test
void nameMatch() {
// 포인트컷 표현식 설정: 모든 리턴타입 + 메서드 이름이 정확히 "hello"인 메서드를 매칭
pointcut.setExpression("execution(* hello(..))");
// 설정한 포인트컷이 'helloMethod' 메서드와 매칭되는지 확인
// MemberServiceImpl 클래스의 'helloMethod'가 포인트컷 표현식과 일치하는지 검증
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar1() {
// 포인트컷 표현식 설정: 메서드 이름이 "hel"로 시작하는 모든 메서드를 매칭
// '*'는 와일드카드로, "hel"로 시작하고 그 뒤에 어떤 글자가 와도 매칭
pointcut.setExpression("execution(* hel*(..))");
// 설정한 포인트컷이 'helloMethod' 메서드와 매칭되는지 확인
// "hello"는 "hel"로 시작하기 때문에, 이 테스트는 true가 되어야 함
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void nameMatchStar2() {
// 포인트컷 표현식 설정: 메서드 이름에 "el"이 포함된 모든 메서드를 매칭
// '*'는 와일드카드로, 앞이나 뒤에 어떤 글자가 와도 상관없음
pointcut.setExpression("execution(* *el*(..))");
// 설정한 포인트컷이 'helloMethod' 메서드와 매칭되는지 확인
// "hello"는 "el"을 포함하기 때문에, 이 테스트는 true가 되어야 함
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
부모 타입을 표현식에 선언한 경우, 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다. 아래에서 MemberServiceImpl안에는 internal메서드가 있어 매칭 대상이 되지만, MemberService에는 internal메서드가 없어 매칭에 실패한다.
// 타입 매칭 - 부모 타입에 있는 메서드만 허용
// 자식타입 MemberServiceImpl 을 표현식에 선언했기 때문에 그 안에 있는 internal(String) 메서드도 매칭 대상이 된다.
@Test
void typeMatchInternal() throws NoSuchMethodException {
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
}
// 포인트컷 표현식에서 부모 타입을 지정한 경우, 해당 부모 타입에 선언된 메서드만 포인트컷에 매칭될 수 있다.
// 자식 클래스에만 있는 메서드는 매칭되지 않으므로, 이 테스트에서는 매칭이 실패해야 한다.
@Test
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}
🟢 execution 파라미터 매칭 규칙
- (String) : 정확하게 String 타입 파라미터
- (): 파라미터가 없어야 한다.
- (*) : 정확히 하나의 파라미터, 단 모든 타입을 허용한다.
- (*, *) : 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.
- (..) : 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다.
- (String, ..) : String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다.
- 예) (String) , (String, Xxx), (String, Xxx, Xxx) 허용
◼️ Within
within 지시자는 해당 타입이 매칭되면 그 안 메서드(조인포인트)들이 자동으로 매칭된다.
public class WithinTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@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();
}
@Test
@DisplayName("타겟의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
void withinSuperTypeFalse() {
pointcut.setExpression("within(hello.aop.member.MemberService)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
부모타입(ex_인터페이스) 지정시 within은 매칭이 실패하고, execution은 성공한다.
@Test
@DisplayName("타겟의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
void withinSuperTypeFalse() {
pointcut.setExpression("within(hello.aop.member.MemberService)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
@DisplayName("execution 은 타입 기반, 인터페이스를 선정 가능.")
void executionSuperTypeTrue() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
🟢 within VS execution
within:
- 범위 기반: 특정 클래스나 패키지 범위를 지정
- 모든 메서드 매칭: 해당 클래스나 패키지의 모든 메서드가 매칭됨
execution:
- 메서드 시그니처,타입 기반: 특정 메서드 시그니처에 따라 매칭
- 정밀한 매칭: 메서드의 이름, 반환 타입, 파라미터 등에 따라 매칭을 설정할 수 있음
◼️ args
execution: 파라미터 타입이 정확하게 매칭되어야 한다.
args: 부모타입 허용, 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.자바가 기본으로 제공하는 `String` 은 `Object` , `java.io.Serializable` 의 하위 타입이다.
/**
* execution(* *(java.io.Serializable)): 메서드의 시그니처로 판단 (정적)
* args(java.io.Serializable): 런타임에 전달된 인수로 판단 (동적)
*/
@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();
}
정적으로 클래스에 선언된 정보만 보고 판단하는 `execution(* *(Object))` 는 매칭에 실패한다.
동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 `args(Object)` 는 매칭에 성공한다. (부모 타입 허용)
◼️ @target,@within
🟢 @target vs @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();
}
}
}
◼️ @annotation, @args
@annotation: 메서드(조인 포인트)에 애노테이션이 있으면 매칭한다.
@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {
@Autowired
MemberService memberService;
@Test
void success() {
log.info("memberService Proxy={}",memberService.getClass());
memberService.hello("helloA");
}
// @annotation 지시자를 사용하여 특정 애노테이션이 달린 메서드에만 AOP 적용
@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();
}
}
}
◼️ bean
bean이름으로 포인트컷 지정
@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {
@Autowired
OrderService orderService;
@Test
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();
}
}
}
◼️ 매개변수 전달
아래 포인트컷 표현식으로 advice에 매개변수 전달이 가능하다. 타입은 메서드에 지정한 타입으로 제한된다.
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
◼️ this, target
this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target : Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
JDK 동적 프록시: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성한다.
CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.
this에서, JDK 동적 프록시는 인터페이스 기반이므로 구현 클래스를 알 수 없다.
CGLIB 프록시는 구현 클래스 기반으로 생성되므로 구현 클래스를 알 수 있다.