본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항
1. execution 포인트컷 지시자
AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공하는데, 이를 포인트컷 표현식(AspectJ pointcut expression)이라고 한다. 포인트컷 표현식은 포인트컷 지시자(Pointcut Designator, PCD)로 시작한다. 여기서 execution이 포인트컷 지시자에 해당한다.
@Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 지시자의 종류는 다양하지만 execution이 주로 사용되고 나머지는 자주 사용되지 않으므로 execution을 중점적으로 이해하면 된다.
1) execution
정의
- 메서드 실행 조인 포인트를 매칭한다
설명
- 스프링 AOP에서 가장 많이 사용하고 기능도 복잡하다
- 메서드 실행 조인 포인트를 매칭
- ?는 생략 가능
- *과 같은 패턴 지정 가능
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
2) execution 매칭 방법 예제
정확한 매칭
@Test
void exactMatch() {
//public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
모든 경우 허용
@Test
void allMatch() {
pointcut.setExpression("execution(* *(..))"); //반환타입 메서드이름(파라미터)
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
- "*"은 모든 값에 대해 허용한다는 의미이다 (와일드카드)
- ".."은 파라미터 타입과 파라미터 수가 상관없다는 의미이다
메서드 패턴 매칭
@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 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.*.*(..))"); //hello.aop.member.(타입).(메서드)(..)
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void packageExactFalse() {
pointcut.setExpression("execution(* hello.aop.*.*(..))"); //hello.aop 패키지에 MemberServiceImpl가 존재하지 않음
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
void packageMatchSubPackage2() {
pointcut.setExpression("execution(* hello.aop..*.*(..))"); //hello.aop의 하위 패키지에 MemberServiceImple이 존재함
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
- 패키지에서 "."과 ".."의 차이에 주의해야 한다
- "."는 정확하게 해당 위치의 패키지만을 지정한다
- ".."는 해당 위치의 패키지와 그 하위 패키지도 포함한다
부모 타입 매칭
@Test
void typeExactMatch() {
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void typeMatchSuperType() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue(); // 부모 타입을 선언해도 자식 타입이 매칭된다
}
- execution에서는 부모 타입을 선언해도 자식 타입이 매칭된다
- 다만, 자식 타입에만 존재하는 메서드는 매칭되지 않고 부모 타입에 존재하는 메서드에 한해서만 매칭된다는 점에 주의하자
파라미터 매칭
//파라미터가 없어야 함
//()
@Test
void argsMatchNoArgs() {
pointcut.setExpression("execution(* *())");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
//정확히 하나의 파라미터 허용, 모든 타입 허용
//(Xxx)
@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, ..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
2. 나머지 포인트컷 지시자
1) within
정의
- 특정 타입 내의 조인 포인트에 대한 매칭을 제한
설명
- 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다
- execution에서 타입 부분만 사용하는 개념
주의할 점
- 표현식에 부모 타입을 지정하면 안 된다
- 타입이 정확하게 일치해야 한다
- exeuction은 인터페이스 지정 가능하지만, within은 구체 클래스 타입만 지정 가능 (부모 타입만 지정가능하므로)
@Test
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
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();
}
@Test
@DisplayName("execution은 타입 기반, 인터페이스 선정 가능")
void executionSuperTypeTrue() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
2) args
정의
- 인자가 특정 타입의 인스턴스인 조인 포인트로 매칭
설명
- 기본문법은 execution의 args 부분과 같다
- execution은 파라미터 타입이 정확하게 매칭되어야 하는 반면, args는 부모 타입을 허용한다
- 이는 execution이 클래스에 선언된 정보를 기반으로 판단하는 반면(정적), args는 런타임에 인수로 넘어온 인스턴스를 기반으로 판단하기 때문이다(동적)
- 단독으로 사용되기보다는 파라미터 바인딩에 주로 사용된다
@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();
}
- 실제 인수로 String 타입의 객체가 넘어온다
- execution에선 String의 부모 타입인 Serializable과 Object가 매칭에 실패하는 것을 확인할 수 있다
3) @target, @within
정의
- @target
- 실행 객체의 클래스에 특정 타입의 애너테이션이 있는 조인 포인트
- 인스턴스의 모든 메서드를 조인 포인트로 적용
- @within
- 특정 애너테이션이 있는 타입 내 조인 포인트
- 해당 타입 내에 있는 메서드만 조인 포인트로 적용
설명
- 두 지시자 모두 파라미터 바인딩에서 함께 사용된다
- @target, @within은 타입에 있는 애너테이션(=클래스 레벨에 존재하는 애너테이션)으로 AOP 적용 여부를 판단한다
- @target(hello.aop.member.annotation.ClassAop)
- @within(hello.aop.member.annotation.ClassAop)
- @target은 부모 클래스의 메서드까지, @within은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용한다

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();
}
}
주의할 점
- args, @args, @target은 단독으로 사용이 불가능한 포인트컷 지시자이다
- 해당 애너테이션들은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다
- @within은 정적인 정보만을 바탕으로 적용 여부를 판단하기 때문에 해당되지 않는다
- 실행 시점에 일어나는 포인트컷 적용 여부는 프록시가 있어야 판단할 수 있으므로 args, @args, @target 같은 포인트컷 지시자가 있으면 스프링에 의해 모든 스프링 빈에 AOP가 적용된다
- 쉽게 말해 args, @args, @target이 AOP 적용 여부를 판단하는 데엔 프록시가 필요하므로, 판단 대상이 프록시가 아니면 스프링이 프록시로 만들어버린다는 것이다
- 이때 스프링이 사용하는 빈 중에는 final로 지정된 빈도 있으므로 모든 스프링 빈에 AOP 프록시를 적용하면 에러가 발생할 수 있다
- 따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다
4) @annotaion & @args
정의
- @annotation
- 메서드가 특정 애너테이션을 가지고 있는 조인 포인트를 매칭
- @args
- 전달된 실제 인수의 런타임 타입이 주어진 타입의 애너테이션을 갖는 조인포인트
설명
- @annotation
- 메서드(조인 포인트)에 특정 애너테이션이 있는 경우 매칭
@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();
}
}
- @args
- 전달된 인수의 런타임 타입에 특정 애너테이션이 있는 경우 매칭
@args(test.Check) // 인수의 런타임에 @Check 애너테이션이 있는 경우 매칭
5) bean
정의
- 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정
설명
- 스프링 빈의 이름으로 AOP 적용 여부 지정 (스프링에서만 사용 가능)
- *과 같은 패턴 사용 가능
@Aspect
static class BeanAspect {
@Around("bean(orderService) || bean(*Repository)")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[bean] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
3. 어드바이스에 매개변수 전달하기
다음 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
- this, target, args, @target, @within, @annotation, @args
1) args, @target, @within, @annotation, @args
설명
- 포인트컷의 이름과 매개변수의 이름을 맞춰야 한다 (아래에선 arg)
- 지시자 내 타입이 메서드에 지정한 타입으로 제한된다
- args(arg,..) → args(String,..)
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
사용 예제
//직접 joinPoint에서 매개변수를 꺼낸다
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
//args를 사용하여 매개변수를 전달받는다
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
//@Before를 사용한 축약버전이다
@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());
}
- 기본적으로 joinPoint에서 매개변수를 꺼낼 수 있지만, args를 사용하면 보다 간편하게 매개변수를 전달받을 수 있다
- this - 프록시 객체를 전달받음
- target - 실제 대상 객체를 전달받음
- @target, @within - 타입의 애너테이션을 전달받음
- @annotation - 메서드의 애너테이션을 전달받음
- 애너테이션의 값을 꺼낼 수 있음
2) this와 target
정의
- this
- 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
- target
- target 객체(스프링 AOP 프록시가 가르키는 실제 대상)을 대상으로 하는 조인 포인트
설명
- 적용 타입 하나를 정확히 지정해야 한다
- this(hello.aop.member.MemberService)
- target(hello.aop.member.MemberService)
- 부모 타입을 허용한다
- *과 같은 패턴 사용 불가
차이
- 프록시를 대상으로 하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다
- 프록시가 인터페이스 기반(JDK 동적 프록시)인지, 혹은 구체 클래스 기반(CGLIB)인지에 따라 포인트컷 매칭 여부가 달라질 수 있다
- JDK 동적 프록시: 인터페이스가 필수이며, 인터페이스를 구현한 프록시 객체를 생성
- CGLIB: 인터페이스가 있더라도 구체 클래스를 상속받는 프록시 객체를 생성
인터페이스(memberService)를 상속받은 구체 클래스(memberServiceImpl)가 있다고 가정해보자. 이를 각각 JDK 동적 프록시와 CGLIB를 통해 프록시로 만들면 다음과 같은 관계를 가질 것이다.
- 실제 객체간 상속관계
- 구체 클래스(MemberServiceImpl) -> 인터페이스(MemberService)
- JDK 동적 프록시
- 프록시(proxy) -> 인터페이스(MemberService)
- CGLIB
- 프록시(proxy) -> 구체 클래스(MemberServiceImpl)
이때 다음과 같이 this(구체클래스)의 형태로 포인트컷을 작성하면 JDK 동적 프록시로 만들어진 프록시 객체엔 해당 포인트컷이 매칭되지 않는다. 해당 프록시 객체는 인터페이스(MemberService)를 구현한 형태여서 구체 클래스(MemberServiceImpl)를 알 수가 없다.
//JDK 동적 프록시: 프록시(proxy) -> 인터페이스(MemberService)
//프록시 객체를 대상
@Pointcut("this(hello.aop.member.MemberServiceImpl)")
target으로 포인트컷 지시자를 수정하면 프록시 객체가 아닌 대상 객체(MemberServiceImpl)을 대상으로 하게 되고, 대상 객체인 구체 클래스가 인터페이스를 상속(구현)하기 때문에 매칭이 된다. 이는 this와 target이 부모 타입을 허용하기 때문에 가능한 것이다.
//CGLIB: 프록시(proxy) -> 구체 클래스(MemberServiceImpl)
//대상 객체(target)를 대상
@Pointcut("target(hello.aop.member.MemberServiceImpl)")
CGLIB로 만들어진 프록시 객체는 this 지시자에 구체 클래스를 지정하더라도 해당 프록시 객체가 구체 클래스를 상속받아서 만들어졌기 때문에 AOP가 정상적으로 적용된다.
'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
[스프링 핵심 원리 - 고급편] 실무 주의사항 (0) | 2023.03.22 |
---|---|
[스프링 핵심 원리 - 고급편] 스프링 AOP 구현 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2023.03.07 |
[스프링 핵심 원리 - 고급편] 빈 후처리기 (0) | 2023.03.06 |
본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항
1. execution 포인트컷 지시자
AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공하는데, 이를 포인트컷 표현식(AspectJ pointcut expression)이라고 한다. 포인트컷 표현식은 포인트컷 지시자(Pointcut Designator, PCD)로 시작한다. 여기서 execution이 포인트컷 지시자에 해당한다.
@Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 지시자의 종류는 다양하지만 execution이 주로 사용되고 나머지는 자주 사용되지 않으므로 execution을 중점적으로 이해하면 된다.
1) execution
정의
- 메서드 실행 조인 포인트를 매칭한다
설명
- 스프링 AOP에서 가장 많이 사용하고 기능도 복잡하다
- 메서드 실행 조인 포인트를 매칭
- ?는 생략 가능
- *과 같은 패턴 지정 가능
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
2) execution 매칭 방법 예제
정확한 매칭
@Test
void exactMatch() {
//public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
모든 경우 허용
@Test
void allMatch() {
pointcut.setExpression("execution(* *(..))"); //반환타입 메서드이름(파라미터)
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
- "*"은 모든 값에 대해 허용한다는 의미이다 (와일드카드)
- ".."은 파라미터 타입과 파라미터 수가 상관없다는 의미이다
메서드 패턴 매칭
@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 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.*.*(..))"); //hello.aop.member.(타입).(메서드)(..)
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void packageExactFalse() {
pointcut.setExpression("execution(* hello.aop.*.*(..))"); //hello.aop 패키지에 MemberServiceImpl가 존재하지 않음
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test
void packageMatchSubPackage2() {
pointcut.setExpression("execution(* hello.aop..*.*(..))"); //hello.aop의 하위 패키지에 MemberServiceImple이 존재함
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
- 패키지에서 "."과 ".."의 차이에 주의해야 한다
- "."는 정확하게 해당 위치의 패키지만을 지정한다
- ".."는 해당 위치의 패키지와 그 하위 패키지도 포함한다
부모 타입 매칭
@Test
void typeExactMatch() {
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
@Test
void typeMatchSuperType() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue(); // 부모 타입을 선언해도 자식 타입이 매칭된다
}
- execution에서는 부모 타입을 선언해도 자식 타입이 매칭된다
- 다만, 자식 타입에만 존재하는 메서드는 매칭되지 않고 부모 타입에 존재하는 메서드에 한해서만 매칭된다는 점에 주의하자
파라미터 매칭
//파라미터가 없어야 함
//()
@Test
void argsMatchNoArgs() {
pointcut.setExpression("execution(* *())");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
//정확히 하나의 파라미터 허용, 모든 타입 허용
//(Xxx)
@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, ..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
2. 나머지 포인트컷 지시자
1) within
정의
- 특정 타입 내의 조인 포인트에 대한 매칭을 제한
설명
- 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭된다
- execution에서 타입 부분만 사용하는 개념
주의할 점
- 표현식에 부모 타입을 지정하면 안 된다
- 타입이 정확하게 일치해야 한다
- exeuction은 인터페이스 지정 가능하지만, within은 구체 클래스 타입만 지정 가능 (부모 타입만 지정가능하므로)
@Test
void withinExact() {
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
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();
}
@Test
@DisplayName("execution은 타입 기반, 인터페이스 선정 가능")
void executionSuperTypeTrue() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
2) args
정의
- 인자가 특정 타입의 인스턴스인 조인 포인트로 매칭
설명
- 기본문법은 execution의 args 부분과 같다
- execution은 파라미터 타입이 정확하게 매칭되어야 하는 반면, args는 부모 타입을 허용한다
- 이는 execution이 클래스에 선언된 정보를 기반으로 판단하는 반면(정적), args는 런타임에 인수로 넘어온 인스턴스를 기반으로 판단하기 때문이다(동적)
- 단독으로 사용되기보다는 파라미터 바인딩에 주로 사용된다
@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();
}
- 실제 인수로 String 타입의 객체가 넘어온다
- execution에선 String의 부모 타입인 Serializable과 Object가 매칭에 실패하는 것을 확인할 수 있다
3) @target, @within
정의
- @target
- 실행 객체의 클래스에 특정 타입의 애너테이션이 있는 조인 포인트
- 인스턴스의 모든 메서드를 조인 포인트로 적용
- @within
- 특정 애너테이션이 있는 타입 내 조인 포인트
- 해당 타입 내에 있는 메서드만 조인 포인트로 적용
설명
- 두 지시자 모두 파라미터 바인딩에서 함께 사용된다
- @target, @within은 타입에 있는 애너테이션(=클래스 레벨에 존재하는 애너테이션)으로 AOP 적용 여부를 판단한다
- @target(hello.aop.member.annotation.ClassAop)
- @within(hello.aop.member.annotation.ClassAop)
- @target은 부모 클래스의 메서드까지, @within은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용한다

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();
}
}
주의할 점
- args, @args, @target은 단독으로 사용이 불가능한 포인트컷 지시자이다
- 해당 애너테이션들은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다
- @within은 정적인 정보만을 바탕으로 적용 여부를 판단하기 때문에 해당되지 않는다
- 실행 시점에 일어나는 포인트컷 적용 여부는 프록시가 있어야 판단할 수 있으므로 args, @args, @target 같은 포인트컷 지시자가 있으면 스프링에 의해 모든 스프링 빈에 AOP가 적용된다
- 쉽게 말해 args, @args, @target이 AOP 적용 여부를 판단하는 데엔 프록시가 필요하므로, 판단 대상이 프록시가 아니면 스프링이 프록시로 만들어버린다는 것이다
- 이때 스프링이 사용하는 빈 중에는 final로 지정된 빈도 있으므로 모든 스프링 빈에 AOP 프록시를 적용하면 에러가 발생할 수 있다
- 따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다
4) @annotaion & @args
정의
- @annotation
- 메서드가 특정 애너테이션을 가지고 있는 조인 포인트를 매칭
- @args
- 전달된 실제 인수의 런타임 타입이 주어진 타입의 애너테이션을 갖는 조인포인트
설명
- @annotation
- 메서드(조인 포인트)에 특정 애너테이션이 있는 경우 매칭
@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();
}
}
- @args
- 전달된 인수의 런타임 타입에 특정 애너테이션이 있는 경우 매칭
@args(test.Check) // 인수의 런타임에 @Check 애너테이션이 있는 경우 매칭
5) bean
정의
- 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정
설명
- 스프링 빈의 이름으로 AOP 적용 여부 지정 (스프링에서만 사용 가능)
- *과 같은 패턴 사용 가능
@Aspect
static class BeanAspect {
@Around("bean(orderService) || bean(*Repository)")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[bean] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
3. 어드바이스에 매개변수 전달하기
다음 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.
- this, target, args, @target, @within, @annotation, @args
1) args, @target, @within, @annotation, @args
설명
- 포인트컷의 이름과 매개변수의 이름을 맞춰야 한다 (아래에선 arg)
- 지시자 내 타입이 메서드에 지정한 타입으로 제한된다
- args(arg,..) → args(String,..)
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
사용 예제
//직접 joinPoint에서 매개변수를 꺼낸다
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
//args를 사용하여 매개변수를 전달받는다
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
//@Before를 사용한 축약버전이다
@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());
}
- 기본적으로 joinPoint에서 매개변수를 꺼낼 수 있지만, args를 사용하면 보다 간편하게 매개변수를 전달받을 수 있다
- this - 프록시 객체를 전달받음
- target - 실제 대상 객체를 전달받음
- @target, @within - 타입의 애너테이션을 전달받음
- @annotation - 메서드의 애너테이션을 전달받음
- 애너테이션의 값을 꺼낼 수 있음
2) this와 target
정의
- this
- 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
- target
- target 객체(스프링 AOP 프록시가 가르키는 실제 대상)을 대상으로 하는 조인 포인트
설명
- 적용 타입 하나를 정확히 지정해야 한다
- this(hello.aop.member.MemberService)
- target(hello.aop.member.MemberService)
- 부모 타입을 허용한다
- *과 같은 패턴 사용 불가
차이
- 프록시를 대상으로 하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다
- 프록시가 인터페이스 기반(JDK 동적 프록시)인지, 혹은 구체 클래스 기반(CGLIB)인지에 따라 포인트컷 매칭 여부가 달라질 수 있다
- JDK 동적 프록시: 인터페이스가 필수이며, 인터페이스를 구현한 프록시 객체를 생성
- CGLIB: 인터페이스가 있더라도 구체 클래스를 상속받는 프록시 객체를 생성
인터페이스(memberService)를 상속받은 구체 클래스(memberServiceImpl)가 있다고 가정해보자. 이를 각각 JDK 동적 프록시와 CGLIB를 통해 프록시로 만들면 다음과 같은 관계를 가질 것이다.
- 실제 객체간 상속관계
- 구체 클래스(MemberServiceImpl) -> 인터페이스(MemberService)
- JDK 동적 프록시
- 프록시(proxy) -> 인터페이스(MemberService)
- CGLIB
- 프록시(proxy) -> 구체 클래스(MemberServiceImpl)
이때 다음과 같이 this(구체클래스)의 형태로 포인트컷을 작성하면 JDK 동적 프록시로 만들어진 프록시 객체엔 해당 포인트컷이 매칭되지 않는다. 해당 프록시 객체는 인터페이스(MemberService)를 구현한 형태여서 구체 클래스(MemberServiceImpl)를 알 수가 없다.
//JDK 동적 프록시: 프록시(proxy) -> 인터페이스(MemberService)
//프록시 객체를 대상
@Pointcut("this(hello.aop.member.MemberServiceImpl)")
target으로 포인트컷 지시자를 수정하면 프록시 객체가 아닌 대상 객체(MemberServiceImpl)을 대상으로 하게 되고, 대상 객체인 구체 클래스가 인터페이스를 상속(구현)하기 때문에 매칭이 된다. 이는 this와 target이 부모 타입을 허용하기 때문에 가능한 것이다.
//CGLIB: 프록시(proxy) -> 구체 클래스(MemberServiceImpl)
//대상 객체(target)를 대상
@Pointcut("target(hello.aop.member.MemberServiceImpl)")
CGLIB로 만들어진 프록시 객체는 this 지시자에 구체 클래스를 지정하더라도 해당 프록시 객체가 구체 클래스를 상속받아서 만들어졌기 때문에 AOP가 정상적으로 적용된다.
'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
[스프링 핵심 원리 - 고급편] 실무 주의사항 (0) | 2023.03.22 |
---|---|
[스프링 핵심 원리 - 고급편] 스프링 AOP 구현 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2023.03.08 |
[스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2023.03.07 |
[스프링 핵심 원리 - 고급편] 빈 후처리기 (0) | 2023.03.06 |