일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch5
- @Aspect
- 스프링 mvc2 - 타임리프
- 자바의 정석 기초편 ch6
- 자바의 정석 기초편 ch2
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch12
- 코드로 시작하는 자바 첫걸음
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch9
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch4
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch8
- jpa 활용2 - api 개발 고급
- 타임리프 - 기본기능
- 스프링 mvc1 - 스프링 mvc
- 자바의 정석 기초편 ch1
- 게시글 목록 api
- 자바의 정석 기초편 ch13
- 2024 정보처리기사 시나공 필기
- 스프링 mvc1 - 서블릿
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch11
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch3
- 스프링 입문(무료)
- 스프링 db1 - 스프링과 문제 해결
- Today
- Total
나구리의 개발공부기록
스프링 AOP - 포인트컷, 포인트컷지시자, 예제 만들기, execution, within, args, @target/@within, @annotation/@args, bean, 매개변수 전달, this/target 본문
스프링 AOP - 포인트컷, 포인트컷지시자, 예제 만들기, execution, within, args, @target/@within, @annotation/@args, bean, 매개변수 전달, this/target
소소한나구리 2024. 11. 14. 22:34출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 포인트컷 지시자
1) 포인트컷 지시자
(1) 포인트컷 표현식
- 애스펙트J는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공하는데 이를 AspectJ pointcut expression 이라고 함
- 애스펙트J가 제공하는 포인트컷 표현식이라는 뜻으로 줄여서 포인트컷 표현식이라고함
2) 종류 및 요약 설명
(1) execution
- 메소드 실행 조인 포인트를 매칭
- 스프링 AOP에서 가장 많이 사용하고 기능도 복잡함
(2) within
- 특정 타입 내의 조인 포인트를 매칭
(3) args
- 인자가 주어진 타입의 인스턴스 조인 포인트
(4) target
- 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
(5) @target
- 주어진 애노테이션이 있는 타입 내 조인 포인트
(6) @within
- 주어진 애노테이션이 있는 타입 내 조인 포인트
(7) @annotation
- 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
(8) @args
- 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
(9) bean
- 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정함
** 참고
- 포인트컷 지시자가 무엇을 뜻하는지는 글로만 읽어보면 이해하기가 쉽지 않으므로 실습이 필요함
- execution을 가장 많이 사용하고 나머지는 자주 사용하지는 않으므로 execution을 중점적으로 이해하는 것을 권장함
2. 예제 만들기
1) Main
(1) ClassAop - 애노테이션
- member.annotation 패키지를 생성하여 코드를 작성
- @Target(ElementType.TYPE): 클래스, 인터페이스, 열거형(enum)에 애노테이션을 적용할 수 있도록 타겟을 설정
- @Retention(RetentionPolicy.RUNTIME): 런타임 시점까지 애노테이션이 유지됨(애노테이션 정보가 유지)
package hello.aop.member.annotation;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
(2) MethodAop - 애노테이션
- 해당 애노테이션은 메서드에만 적용되도록 @Target을 설정
- 애노테이션에 변수를 선언하면 애노테이션을 적용할 때 이 속성에 값을 지정할 수 있음
package hello.aop.member.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
(3) MemberService
package hello.aop.member;
public interface MemberService {
String hello(String param);
}
(4) MemberServiceImpl
- 직접만든 @ClassAop 애노테이션을 적용하고 MemberService 인터페이스를 구현한 클래스
- 적용한 AOP를 자동으로 스프링빈으로 등록되도록 @Component를 적용
- MemberService의 메서드를 구현한 hello()메서드에는 @MethodAop 애노테이션을 "test value"라는 값을 입력하여 적용
- MemberServiceImpl이 가지고있는 internal()메서드는 아무것도 적용하지 않음
package hello.aop.member;
@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
@Override
@MethodAop("test value")
public String hello(String param) {
return "ok";
}
public String internal (String param) {
return "ok";
}
}
2) Test
(1) ExecutionTest 생성
- 테스트 하위에 pointcut패키지를 생성하여 작성
- AspectJExpressionPointcut: 상위에 Pointcut 인터페이스를 구현하고 있는 포인트컷 표현식을 처리해주는 클래스로 여기에 포인트컷 표현식을 지정하면 됨
- @BeforeEach로 MemberServiceImpl의 메서드 정보를 가져오고 테스트로 출력
- execution으로 시작하는 포인트컷 표현식은 메서드 정보를 매칭해서 포인트컷을 대상으로 찾아냄
package hello.aop.pointcut;
@Slf4j
public class ExecutionTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@Test
void printMethod() {
// execution 매칭이 아래의 로그와 매칭됨
// helloMethod=public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
log.info("helloMethod={}", helloMethod);
}
}
/* 출력결과
helloMethod=public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
*/
3.execution
1) 기본사용법
(1) 공식 문서 문법
- 메소드 실행 조인 포인트를 매칭
- ?는 생략할 수 있음
- *같은 패턴을 지정할 수 있음
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
(2-1) 가장 정확한 포인트컷 - exactmatch 추가
- MemberServiceImpl.hello(String)메서드와 가장 정확하게 모든 내용이 매칭되는 표현식
- pointcut.setExpression을 통해서 포인트컷 표현식을 적용하고, pointcut.matches(메서드, 대상 클래스)를 입력하여 메서드와 적용한 포인트컷의 매칭 여부를 true, false로 반환함
@Test
void exactMatch() {
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
(2-2) 매칭 조건
- MemberServiceImpl.hello(String)의 메서드와 포인트컷 표현식의 모든 내용이 100%일치
- 접근 제어자?: public
- 반환타입: String
- 선언타입?: hello.aop.member.MemberServiceImpl
- 메서드이름: hello
- 파라미터:(String)
- 예외?: 생략
(3-1) 가장 많이 생략한 포인트컷
- 생략할 수 있는 매칭조건을 전부 생략한 포인트컷을 적용
- 모든 조건을 다 만족하기 때문에 당연히 테스트가 성공함
- *은 아무값이 들어와도 된다는 뜻이며 파라미터의 ..은 파라미터의 타입과 파라미터의 수가 전부 상관없다는 뜻임
@Test
void allMatch() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
(3-2) 매칭 조건
- 접근 제어자?: 생략
- 반환타입: *
- 선언타입?: 생략
- 메서드이름: *
- 파라미터:(..)
- 예외?: 생략
(4) 메서드 이름 매칭 관련 포인트컷
- 메서드 이름의 앞이나 뒤에 *을 사용하여 특정 단어가 메서드 이름의 시작, 끝, 또는 중간에 포함될 때도 매칭되도록 설정할 수 있음
@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();
}
(5) 패키지 매칭 관련 포인트컷
- 패키지를 매칭하여 포인트컷을 지정할 수 있음
- hello.aop.member.*.* : 첫번째 *은 타입, 두번째 *은 메서드 이름을 *로 지정
- 패키지로 매칭할 때 점이 하나(.)이면 정확하게 해당 위치의 패키지만 대상으로하고 점이 두개(..)이면 대상 패키지와 하위 패키지도 포함함
// 패키지 이름 매칭
@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 packageExactMatchFalse() {
pointcut.setExpression("execution(* hello.aop.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Test // ..(점 2개)면 해당 위치의 패키지와 그 하위 패키지를 포함함
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();
}
2) 타입 매칭
(1) 부모 타입 허용
- typeExactMatch()는 포인트컷과 타입 정보가 정확하게 일치하기 때문에 당연히 매칭됨
- typeMatchSuperType()에 적용한 포인트컷을 보면 MemberService으로 부모타입인 인터페이스를 선언하였음에도 다형성이 적용되어 자식타입도 매칭됨(인터페이스도 되니 상속도 당연히 됨)
// 타입 매칭 - 부모 타입에 있는 메서드만 허용
@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();
}
(2) 부모 타입에 있는 메서드만 허용함
- typeMatchInternal()은 internal(String)메서드가 MemberServiceImpl에있기 때문에 당연히 매칭에 성공함
- typeMatchNoSuperTypeMethodFalse()는 pointcut.matches의 결과가 false로 반환되는데, 부모 타입인 MemberService에는 internal(String) 메서드가 없고 MemberServiceImpl에만 있기 때문임
- 즉, 부모 타입을 포인트컷 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야만 매칭에 성공함
// 상속받지 않은 internal 메서드를 비교
@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();
}
(3-1) 파라미터 매칭
- 다양한 케이스의 파라미터 매칭 방법
// 파라미터 매칭
// String 타입의 파라미터 허용
@Test
void argsMatch() {
pointcut.setExpression("execution(* *(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
// 파라미터가 없어야 함
@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();
}
// 모든 타입의 파라미터가 허용되며 파라미터의 개수도 상관없음(파라미터가 없어도됨)
@Test
void argsMatchAll() {
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
// String 타입으로 시작, 파라미터 개수는 무관하고 모든 타입을 허용
// (String), (String, ...)
@Test
void argsMatchComplex() {
pointcut.setExpression("execution(* *(String, ..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
(3-2) 파라미터 매칭 규칙
- (String) : 정확하게 String 타입 파라미터를 매칭
- () : 파라미터가 없어야함
- (*) : 정확히 하나의 파라미터를 매칭, 타입은 모두 허용
- (*, *) : 정확히 두개의 파라미터를 매칭, 타입은 모두 허용
- (..) : 파라미터의 개수도 무관하고 타입도 모두 허용, 파라미터가 없어도됨, (0..*)과 동일
- (String, ..) : String타입으로 시작해야함, 두번째가 .. String 타입으로 시작만 하면 그이후의 파라미터의 개수와 타입은 무관함
- 만약 (String, *)이면 파라미터의 개수는 정확히 2개여야하지만 첫번째는 String타입 두번째는 타입은 상관없다는 뜻이며, *대신에 특정 타입을 입력할 수도 있음
4. within
1) 설명 및 예제
(1) 설명
- 특정 타입 내의 조인 포인트들로 매칭을 제한, 즉 해당 타입이 매칭되면 그안의 메서드(조인 포인트)들이 자동으로 매칭됨
- execution에서 타입 부분만 사용한다고 보면 됨
- 필요하면 사용할 수 있으나 execution으로 대부분의 기능이 되며 표현식일 뿐이기에 거의 사용하지는 않음
(2) WithinTest 생성
- 다른 테스트는 크게 어려운 점은 없으나 주의할 점은 execution과 다르게 표현식에 부모타입을 지정하면 안되고 정확하게 타입이 맞아야함
- withinSupertypeFalse()메서드를 보면 within으로 표현식을 작성하면 isFalse()로 테스트가 통과하는 것을 알 수 있으며 execution으로 작성한 것은 테스트가 통과함
package hello.aop.pointcut;
public class WithinTest {
// 초기화 코드는 동일
@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 withinPackage() {
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();
}
@Test
@DisplayName("execution은 타입 기반이므로 인터페이스를 선정 가능")
void executionSuperTypeFalse() {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}
}
5. args
1) 설명 및 예제
(1) 설명
- 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭
- 기본 문법은 execution의 args부분과 같음
- execution은 클래스에 선언된 정보를 기반으로 판단하며 파라미터 타입이 정확하게 매칭되어야 하지만 args는 실제 넘어온 파라미터의 객체 인스턴스를 보고 판단하며 부모 타입을 허용함
(2) ArgsTest
- pointcut(): 테스트를 편리하게 진행하기 위해 포인트컷 자체를 생성하는 메서드를 생성
- String은 Object, java.io.Serializable의 하위타입인데, 정적으로 클래스에 선언된 정보만 보고 판단하는 execution은 상위 타입을 포인트컷으로 지정하면 매칭에 실패하지만 동적으로 실제 파라미터로 넘어온 객체로 판단하는 args는 매칭에 성공함
** 참고
- args 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용됨
package hello.aop.pointcut;
public class ArgsTest {
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
// 테스트를 편리하기 위해 포인트컷을 생성하는 메서드를 작성
private AspectJExpressionPointcut pointcut(String expression) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(expression);
return pointcut;
}
// args()만 false, 나머지는 true
@Test
void args() {
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();
}
/**
* execution(* *(java.io.Serializable)): 메서드의 시그니처로 판단 (정적)
* * args(java.io.Serializable): 런타임에 전달된 인수로 판단 (동적)
*/
@Test
void argsVsExecution() {
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, 타입이 정확히 일치해야함, (String)만 true 나머지는 false
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();
}
}
6. @target, @within
1) 설명 및 예제
(1) @target
- 실행 객체의 클래스가 주어진 타입의 애노테이션이 있는 조인 포인트
- @target(hello.aop.member.annotation.ClassAop)
(2) @within
- 주어진 애노테이션이 있는 타입 내 조인 포인트
- @within(hello.aop.member.annotation.ClassAop)
(3) @target vs @within
- @target은 인스턴스의 모든 메서드를 조인 포인트로 적용
- @within인 해당 타입 내에 있는 메서드만 조인 포인트로 적용
- 즉 @target은 부모 클래스의 메서드까지 어드바이스를 전부 적용하고 @within은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용함
(4) AttargetAtwithinTest
- 자식클래스인 Child에 @ClassAop 애노테이션을 적용하고 Child와 Parent 모두 스프링빈으로 등록
- @Around에 포인트컷 표현식으로 @target과 @within을 각각 적용한 애스펙트를 @Aspect 애노테이션으로 생성 후 스프링 빈으로 등록하여 AOP가 적용되는 로그를 확인하는 테스트를 진행
package hello.aop.pointcut;
@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();
}
}
}
(5) 실행 결과
- 실행 결과를 보면 parentMethod()는 Parent 클래스에만 정의되어있고 Child 클래스에는 정의되어있지 않기 때문에 @within에서 AOP 적용 대상이 되지 않은것을 로그로 확인할 수 있음
- @target은 부모클래스의 메서드까지도 AOP가 적용되어 로그가 전부 출력되는 것을 확인할 수 있음
child Proxy=class hello.aop.pointcut.AtTargetAtWithinTest$Child$$SpringCGLIB$$0
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()
** 참고
- @target, @within 지시자도 args와 마찬가지로 파라미터 바인딩에서 함께 사용됨
- @target, args, @args는 단독으로 사용하면 안됨
- @Around에서 적용한 포인트컷을 확인해보면 execution(* hello.aop.*(..))를 통해서 AOP가 적용되는 대상을 줄여주었음
- args, @args, @target은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있음
- 실행 시점에 일어나는 포인트컷 적용 여부도 결국 프록시가 있어야 실행 시점에 판단할 수 있으므로 프록시가 없다면 판단 자체가 불가능함
- 스프링 컨테이너가 프록시를 생성하는 시점은 스프링 컨테이너가 만들어지는 애플리케이션 로딩 시점에 적용할 수 있으므로 args, @args, @target 같은 포인트컷 지시자가 있으면 스프링은 모든 스프링빈에 AOP를 적용하려고 시도함
- 이렇게 모든 스프링 빈에 AOP 프록시를 적용하려고 하면 스프링이 내부에서 사용한 빈 중에는 final로 지정된 빈들도 있기 때문에 이런 빈들은 프록시 생성이 되지 않으므로 오류가 발생할 수 있음
- 따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야함
7. @annotation, @args
1) 설명 및 예제
(1) @annotation
- 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
- @annotation(hello.aop.member.annotation.MethodAop)
(2) AtAnnotationTest
- MemberService의 구현체인 MemberServiceImpl의 hello 메서드에 적용한 @MethodAop를 조인포인트로 적용하는 테스트
- @annotation으로 특정 애노테이션을 조인포인트로 지정하여 편리하게 포인트컷을 적용할 수 있어 간혹 사용함
package hello.aop.pointcut;
@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {
@Autowired
MemberService memberService;
@Test
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();
}
}
}
/* 실행결과
memberService Proxy=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[@annotation] String hello.aop.member.MemberServiceImpl.hello(String)
*/
(3) @args
- 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
- 전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭이 됨
- @args(test.Check)
- 거의 사용하지 않음
8. bean
1) 설명 및 예제
(1) 설명
- 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정함
- 스프링 빈의 이름으로 AOP 적용 여부를 지정할 수 있으며 스프링에서만 사용할 수 있으므로 AspectJ에서는 사용할 수 없음
- bean(orderService) || bean(*Repository)처럼 적용할 수 있으며 *과 같은 패턴을 사용할 수 있음
(2) BeanTest
- @Around에 bean(빈이름)으로 포인트컷을 지정하여 적용하여 실행해보면 OrderService와 OrderRepository에 AOP가 정상적으로 적용되어 로그가 출력된 것을 확인할 수 있음
- 빈이 변하지 않는 상황에서는 사용할 수 있겠으나 실무에서 주로 사용하지는 않음
package hello.aop.pointcut;
@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {
@Autowired
OrderService orderService;
@Test
void success() {
log.info("bean {}", orderService.getClass());
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 class hello.aop.order.OrderService$$SpringCGLIB$$0
[bean] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[bean] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
*/
9. 매개변수 전달
1) 어드바이스에 매개변수를 전달
(1) 매개변수를 전달 가능한 포인트컷 표현식
- this, target, args, @target, @within, @annotation, @args
(2) 사용방법
- 포인트컷의 이름과 매개변수의 이름을 맞추어야 함
- 타입이 메서드에 지정한 타입으로 제한됨
- 아래의 예제에서는 arg로 이름을 맞추었고 메서드의 타입이 String으로 되어있기 때문에 args(String, ..) 처럼 정의된다고 이해하면 됨
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
(3) ParameterTest
- logArgs1: joinPoint.getArgs()[0]으로 매개변수를 전달 받을 수 있음
- logArgs2: args(arg, ..)를 사용하여 logArgs1보다 코드를 더 간결하게 전달 받음
- logArgs3: @Before를 사용하고, 메서드의 매개변수 타입을 String으로 제한, logArgs메서드들 중에서 가장 깔끔함
- thisArgs: this를 사용하면 프록시 객체를 전달 받음
- targetArgs: target을 사용하면 실제 객체를 전달받음
- atTarget, atWithin: @target과 @within은 타입의 애노테이션을 전달받을 수 있음(적용 범위의 차이는 위에서 설명함)
- atAnnotation: @annotation으로 메서드의 애노테이션을 전달받을 수 있으며 MethodAop에는 값이 선언되어있어 해당 값이 로그로 출력됨(@target, @within도 애노테이션의 속성값을 전달할 수 있음)
package hello.aop.pointcut;
@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();
}
// args(arg, ..) 적용
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
// 메서드 타입이 String, String 으로 타입이 제한됨
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
// 이후에 배울 this 적용, 프록시 객체가 전달됨
@Before("allMember() && this(obj)")
public void thisArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
// 이후에 배울 target 적용, 실제 객체가 전달됨
@Before("allMember() && target(obj)")
public void targetArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
// annotation 정보를 가져옴, @target 적용
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
}
// annotation 정보를 가져옴, @within 적용
@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
}
// 메서드에 적용된 annotation 정보를 가져옴, @annotation 적용
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation);
}
}
}
(4) 실행 결과
memberService Proxy=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[logArgs1]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA
[logArgs2]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA
[@annotation]String hello.aop.member.MemberServiceImpl.hello(String), annotationValue=@hello.aop.member.annotation.MethodAop("test value")
[@target]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
[@within]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
[logArgs3] arg=helloA
[target]String hello.aop.member.MemberServiceImpl.hello(String), obj=class hello.aop.member.MemberServiceImpl
[this]String hello.aop.member.MemberServiceImpl.hello(String), obj=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
10. this, target
1) 설명
(1) this
- 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
- this(hello.aop.member.MemberService)
(2) target
- target객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
- target(hello.aop.member.MemberService)
(3) 공통 적용
- *같은 패턴을 사용할 수 없음
- 부모 타입을 허용함
(4) this vs target
- 스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록됨
- this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭함
- target은 실제 target객체를 대상으로 포인트컷으로 매칭함
(5) 프록시 생성 방식에 따른 차이
- 스프링은 프록시를 생성할 때 JDK 동적 프록시와 CGLIB를 선택할 수 있는데, 둘은 프록시를 생성하는 방식이 다르기 때문에 차이가 발생함
- JDK 동적 프록시는 인터페이스가 필수이고 인터페이스를 구현한 프록시 객체를 생성함
- CGLIB는 인터페이스가 있어도 구체 클래스를 상속받아서 프록시 객체를 생성함
(6) JDK 동적 프록시 적용시 this, target 동작
- MemberService 인터페이스에 this, target을 적용하면 둘다 부모 타입을 허용하기 때문에 AOP가 적용됨
- MemberServiceImpl 구체 클래스에 지정할 경우에 this는 AOP가 적용되지 않음
- this는 프록시 객체를 대상으로 판단하는데 JDK 동적 프록시로 만들어진 proxy 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스이므로 MemberServiceImpl을 알지 못하기 때문임
- 반면 target은 실제 target객체를 보고 판단하기 때문에 MemberServiceImpl이 실제 target이기 때문에 AOP 적용 대상이 됨
(7) CGLIB 프록시 적용시 this, target 동작
- MemberService 인터페이스에 적용하든, MemberServiceImpl 구체클래스에 적용 하든 this, target 모두 AOP가 적용됨
- 인터페이스일경우에는 this, target 둘다 부모 타입을 허용하기 때문에 AOP가 적용됨
- 구체클래스인경우에는 CGLIB로 만들어진 프록시 객체가 구체클래스인 MemberServiceImpl을 상속받아서 만들어졌기 때문에 this로도 적용이 되며, target은 동작 메커니즘상 당연히 적용이됨
(8) 정리
- 프록시를 대상으로하는 this의 경우 구체 클래스를 지정하면 프록시 생성 전략에 따라서 다른 결과가 나올 수 있다는 점이 포인트임
2) 예제
(1) ThisTargetTest
- 위에서 설명한 상황을 모두 예제로 구현하여 this와 target으로 인터페이스와 구체클래스를 각각 포인트컷으로 적용한 테스트를 시행
- @SpringBoot(properties = "spring.aop.proxy-target-class=false") : application.properties에 적용하는 대신에 해당 테스트에서만 설정을 임시로 적용할 수 있으며, 이렇게 하면 각 테스트마다 다른 설정을 손쉽게 할 수 있음
- spring.aop.proxy-target-class=false: 스프링이 AOP 프록시를 생성할 때 JDK 동적 프록시를 우선 생성하며 인터페이스가 없으면 CGLIB를 사용함 즉, 둘다 사용함
- spring.aop.proxy-target-class=true: 스프링이 AOP 프록시를 생성할 때 CGLIB 프록시를 생성하며 스프링 부트로 프로젝트를 생성했다면 이설정은 기본값으로 적용되어 기본으로 CGLIB를 사용함
- 이부분은 강의 제일 마지막에서 스프링 부트가 왜 이런 전략을 선택했는지에 대해서 자세히 설명함
package hello.aop.pointcut;
/**
* 스프링 부트로 프로젝트를 생성하면 기본으로 프록시를 CGLIB 를 사용함
* application.properties
* spring.aop.proxy-target-class=true CGLIB 사용(기본값)
* spring.aop.proxy-target-class=false JDK 동적 프록시, CGLIB 사용
*/
@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
// 테스트에서도 바로 설정할 수 있음
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시, CGLIB 동작
//@SpringBootTest(properties = "spring.aop.proxy-target-class=true") // CGLIB 로만 동작
public class ThisTargetTest {
@Autowired
MemberService memberService;
@Test
void success() {
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
@Slf4j
@Aspect
static class ThisTargetAspect {
// 인터페이스, this - 부모 타입 허용
@Around("this(hello.aop.member.MemberService)")
public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
// 인터페이스, target - 부모 타입 허용
@Around("target(hello.aop.member.MemberService)")
public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-interface] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
// 구체클래스, this - AOP 적용 안됨
@Around("this(hello.aop.member.MemberServiceImpl)")
public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[this-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
// 구체클래스, target - 부모 타입 허용
@Around("target(hello.aop.member.MemberServiceImpl)")
public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[target-impl] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
(2) 실행결과
- JDK 동적 프록시로 동작하도록 설정 후 테스트를 실행해보면 this-impl 로그는 출력되지 않는것을 확인할 수 있음
memberService Proxy=class jdk.proxy3.$Proxy53
[target-impl] String hello.aop.member.MemberService.hello(String)
[target-interface] String hello.aop.member.MemberService.hello(String)
[this-interface] String hello.aop.member.MemberService.hello(String)
(3) CGLIB로 동작 설정 후 실행
- CGLIB 프록시로 동작하면 this로 구체클래스를 포인트컷으로 적용하여도 프록시가 구체클래스를 상속받아서 만들어졌기 때문에 this-impl 로그도 AOP가 적용되어 로그가 출력된 모습을 확인할 수 있음
memberService Proxy=class hello.aop.member.MemberServiceImpl$$SpringCGLIB$$0
[target-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[target-interface] String hello.aop.member.MemberServiceImpl.hello(String)
[this-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[this-interface] String hello.aop.member.MemberServiceImpl.hello(String)
** 참고
- this, target 지시자는 단독으로 사용되기 보다는 파라미터 바인딩에서 주로 사용됨
- 해당 내용이 이해가 잘 되지 않으면 AOP 실무 주의사항에서 프록시 기술과 한계를 듣고 다시한번 복습해보면 좋음