maeng0830
뇌 채우기 운동
maeng0830
전체 방문자
오늘
어제
  • maeng0830-note (85)
    • 자바 (3)
    • 스프링 (39)
      • Core (21)
      • DB (16)
      • Security (2)
      • Test (0)
    • 자료구조 & 알고리즘 (19)
      • 자료구조 (12)
      • 알고리즘 (7)
    • 다른 개발 도구들 (4)
      • Git&Github (1)
      • Redis (3)
    • 프로젝트 (9)
      • Album (9)
    • CS (10)
      • 운영체제 (5)
      • 데이터베이스 (5)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • spring security
  • 자료구조
  • 트랜잭션
  • JPQL
  • JPA

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
maeng0830

뇌 채우기 운동

스프링/Core

스프링 AOP-포인트컷 지시자

2023. 10. 20. 23:54

포인트컷 표현식은 AspectJ pointcut expression, 즉 AspectJ가 제공하는 포인트컷 표현식을 줄여서 말하는 것이다.

 

그리고 포인트컷 표현식은 execution, within, args 등의 포인트컷 지시자(Pointcut Designator)을 통해 작성된다.

 

이번 게시글에서는 스프링 AOP의 포인트컷 지시자에 대해 알아보고자 한다.

 

공통 사용 코드

우선 이번 게시글에서 포인트컷 지시자를 학습하기 위해 공통적으로 사용할 코드들을 알아보자.

 

ClassAop

ClassAop는 클래스에 적용할 애노테이션이다.

@Target(ElementType.TYPE) // 클래스에 적용할 애노테이션임을 지정
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {

}

 

MethodAop

MethodAop는 메소드에 적용할 애노테이션이다.

String value()라는 애노테이션 속성을 갖고 있다.

@Target(ElementType.METHOD) // 메소드에 적용할 애노테이션임을 지정
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {

	String value(); // 애노테이션의 속성
}

 

MemberService

MemberService는 스프링 AOP가 적용될 MemberServiceImpl이 구현할 인터페이스이다.

hello()가 정의되어 있다. 

public interface MemberService {
	String hello(String param);
}

 

MemberServiceImpl

MemberServiceImpl은 MebmerService의 구현체이다. 스프링 AOP가 적용될 타겟이다.

MemberServiceImpl에는 @ClassAop가 적용되어 있다.

 

MemberService의 hell()를 재정의하고 있다.

단순히 "ok"라는 문자열을 반환하도록 재정의되어있으며, @MethodAop가 적용되어있다.

 

그리고 "ok"라는 문자열을 반환하는 internal()이 정의되어있다.

@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";
	}
}

 

포인트컷 지시자

execution

execution 포인트컷 지시자는 execution(접근제어자? 반환타입 선언타입?메서드명(파라미터 목록) 예외?)의 형태로 표현식을 작성한다.

  • ?가 붙는 접근제어자, 선언타입, 예외는 생략할 수 있다.
  • 선언타입은 패키지+타입을 의미한다.
  • *를 사용할 수 있으며, 어떤 값이든 상관 없다는 의미이다.
  • ..는 어떤 파라미터 목록이든 상관 없다는 의미이다.
  • 상위 타입으로 포인트컷을 지정하면, 하위 타입들도 매칭된다. 그러나 하위 타입에만 존재하는 메소드에 대해서는 매칭되지 않는다.

 

아래의 예제 코드에는 다양한 execution 표현식을 작성하고, MemberServiceImpl의 hello() 또는 internal()이 해당 표현식들을 만족하는 지 여부를 테스트한다. 

@Slf4j
public class ExecutionTest {

	/*
	 * execution(접근제어자? 반환타입 선언타입?메서드명(파라미터) 예외?)
	 *
	 * ?가 붙는 접근제어자와 선언타입은 생략할 수 있다. 선언타입 = 패키지 + 타입
	 * *은 어떤 값이든 상관 없다는 의미이다.
	 * ..는 어떤 파라미터 목록이든 상관 없다는 의미이다.
	 */

	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);
		// helloMethod=public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
	}

	// 가장 정확한 표현식
	@Test
	void exactMatch() {
		// 접근제어자?: public
		// 반환타입: String
		// 선언타입?: hello.aop.member.MemberServiceImpl
		// 메서드명(파라미터): hello(String)
		// 예외?: 없음
		pointcut.setExpression(
				"execution(public String hello.aop.member.MemberServiceImpl.hello(String))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 가장 많이 생략한 표현식
	void allMath() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* *(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	////////////////////////////////////////////////////////////////////////////////////////////////
	////////////////////////////////       메소드명 기준 매치       ///////////////////////////////////
	////////////////////////////////////////////////////////////////////////////////////////////////
	@Test
	void nameMatch() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): hello(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	@Test
	void nameMatchStar1() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): hel*(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hel*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	@Test
	void nameMatchStar2() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *el*(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* *el*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	@Test
	void nameMatchFalse() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): nono(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* nono(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
	}

	////////////////////////////////////////////////////////////////////////////////////////////////
	////////////////////////////////   선언타입+메소드명 기준 매치   ///////////////////////////////////
	////////////////////////////////////////////////////////////////////////////////////////////////
	// 가장 정확한 표현식
	@Test
	void packageExactMatch1() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop.member.MemberServiceImpl
		// 메서드명(파라미터): hello(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	@Test
	void packageExactMatch2() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop.member.*
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop.member.*.*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 주의! . 으로는 하위 패키지까지 포함할 수 없다.
	@Test
	void packageExactFalse() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop.*
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop.*.*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
	}

	// 주의! .. 으로 하위 패키지까지 포함할 수 있다.
	@Test
	void packageMatchSubPackage1() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop..*
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop..*.*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	////////////////////////////////////////////////////////////////////////////////////////////////
	////////////////////////////////        타입 기준 매치         ///////////////////////////////////
	////////////////////////////////////////////////////////////////////////////////////////////////
	@Test
	void typeExactMatch() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop.member.MemberServiceImpl
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 상위 타입을 기준으로 포인트컷을 구현하면, 하위 타입들도 매칭된다.
	@Test
	void typeMatchSuperType() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop.member.MemberService
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 상위 타입을 기준으로 포인트컷을 구현하면, 하위 타입들도 매칭된다.
	// 단 하위 타입에만 존재하는 메소드에 대해서는 매칭되지 않는다.
	@Test
	void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: hello.aop.member.MemberService
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");

		// MemberServiceImpl에서 정의된 internal()
		Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);

		// MemberServiceImpl의 internal()이 포인트컷을 만족하는가
		assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
	}

	////////////////////////////////////////////////////////////////////////////////////////////////
	////////////////////////////////       파라미터 기준 매치       ///////////////////////////////////
	////////////////////////////////////////////////////////////////////////////////////////////////
	// String 타입의 파라미터 허용
	// (String)
	@Test
	void argsMatch() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *(String)
		// 예외?: 없음
		pointcut.setExpression("execution(* *(String))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 파라미터가 없어야함
	// ()
	@Test
	void argsMatchNoArgs() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *()
		// 예외?: 없음
		pointcut.setExpression("execution(* *())");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
	}

	// 정확히 하나의 파라미터 허용, 모든 타입 허용
	// (*)
	@Test
	void argsMatchStar() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *(*)
		// 예외?: 없음
		pointcut.setExpression("execution(* *(*))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 모든 파라미터 목록 허용. 파라미터가 없어도 됨.
	// (..)
	@Test
	void argsMatchAll() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* *(..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}

	// 첫 번째 파라미터가 String인 모든 파라미터 목록 허용
	// (String, ..)
	@Test
	void argsMatchComplex() {
		// 접근제어자?: 생략
		// 반환타입: *
		// 선언타입?: 생략
		// 메서드명(파라미터): *(..)
		// 예외?: 없음
		pointcut.setExpression("execution(* *(String, ..))");

		// MemberServiceImpl의 hello()가 포인트컷을 만족하는가
		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}
}

 

within

within 포인트컷 지시자는 특정 타입 내의 조인 포인트(메서드)들로 매칭을 제한한다.

within(선언타입)의 형태로 표현식을 작성한다.

 

주의할 점은 within은 상위 타입으로 하위 타입까지 매칭 시킬 수 없다는 것이다. 이것이 execution과의 차이점이다. 

public class WithinTest {

	/*
	 * within(선언타입)
	 *
	 * 특정 타입 내의 조인 포인트(메서드)들로 매칭을 제한한다.
	 * within은 상위 타입으로 하위 타입까지 매칭 시킬 수 없다. 타겟 타입을 명시해줘야한다. execution과의 차이이다.
	 */

	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();
	}

	@DisplayName("within은 타켓 타입을 지정해야한다. 상위 타입을 지정하면 안된다.")
	@Test
	void withinSuperTypeFalse() {
		pointcut.setExpression("within(hello.aop.member.MemberService)");

		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
	}

	@DisplayName("execution은 타겟 타입의 상위 타입을 지정해도 된다.")
	@Test
	void executionSuperTypeTrue() {
		pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");

		assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
	}
}

 

args

args 포인트컷 지시자는 파라미터 목록을 기준으로 매칭하는 포인트컷 지시자이다.

args(파라미터 목록)으로 표현식을 작성한다.

 

주의할 점은 execution은 각 파라미터의 타입을 정확히 지정해야하지만, args는 각 파라미터의 타입으로 상위 타입을 지정해도 된다는 것이다.

public class ArgsTest {

	/**
	 * args(파라미터 목록)
	 *
	 * args는 파라미터 목록을 기준으로 매칭하는 포인트컷 지시자이다.
	 * execution는 각 파라미터의 타입을 정확히 지정해야 매칭되지만, args는 각 파라미터의 타입으로 상위 타입을 지정해도 매칭된다.
	 */

	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;
	}

	@Test
	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();
	}

	/**
	 * 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();
	}
}

 

@target, @within

@target(애노테이션)은 해당 애노테이션이 적용된 타입의 인스턴스를 기준으로 조인 포인트를 적용한다.

따라서 상속 받은 메서드도 조인 포인트가 적용된다.

 

@within(애노테이션)은 해당 애노테이션이 적용된 타입의 내부 정의 내용을 기준으로 조인 포인트를 적용한다.

따라서 상속 받은 메서드는 조인 포인트가 적용되지 않는다.

@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {

	/**
	 * @target(애노테이션)은 해당 애노테이션이 적용된 타입의 인스턴스를 기준으로 모든 메서드를 조인 포인트로 적용한다.
	 * 즉 상속 받은 메서드도 조인 포인트가 적용된다.
	 *
	 * @within(애노테이션)은 해당 애노테이션이 적용된 타입 내에서 정의된 메스드를 조인 포인트로 적용한다.
	 * 즉 상속 받은 메서드는 조인 포인트가 적용되지 않는다.
	 */

	@Autowired
	Child child;

	@Test
	void success() {
		log.info("child Proxy={}", child.getClass());
		child.childMethod(); // 상속 받은 메서드도 조인 포인트 적용
		// [@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
		// @within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()

		child.parentMethod(); // 상속 받은 메서드는 조인 포인트가 적용되지 않음
		// [@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.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

@annotation(애노테이션)은 해당 애노테이션이 적용된 메소드를 조인 포인트로 적용한다.

 

MemberService.hello()에는 @MethodAop가 적용되어있으므로, 조인 포인트로 적용된다.

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

	/**
	 * @annotation(애노테이션)은 해당 애노테이션이 적용된 메소드를 조인 포인트로 적용한다.
	 */

	@Autowired
	MemberService memberService;

	@Test
	void success() {
		log.info("memberService Proxy={}", memberService.getClass());

		memberService.hello("helloA");
		// [@annotation] String hello.aop.member.MemberServiceImpl.hello(String)
	}

	@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(beanName)은 해당 baenName을 가진 타겟의 조인 포인트를 포인트컷으로 지정한다.

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

	/**
	 * bean(beanName)은 해당 beanName을 가진 타켓의 조인포인트를 포인트컷으로 지정한다.
	 */

	@Autowired
	OrderService orderService;

	@Test
	void success() {
		orderService.orderItem("itemA");
		// [bean] String hello.aop.order.OrderService.orderItem(String)
		// [orderService] 실행
		// [bean] String hello.aop.order.OrderRepository.save(String)
		// [orderRepository] 실행
	}

	@Aspect
	static class BeanAspect {

		@Around("bean(orderService) || bean(*Repository)")
		public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
			log.info("[bean] {}", joinPoint.getSignature());

			return joinPoint.proceed();
		}
	}
}

 

this, target

this(선언타입)은 선언타입과 일치하는 빈으로 등록된 객체(스프링 AOP 프록시)의 조인 포인트를 포인트컷으로 지정한다.

target(선언타입)은 선언타입과 일치하는 타겟 객체(스프링 AOP 프록시가 호출하는 대상)의 조인 포인트를 포인트컷으로 지정한다.

 

this, target은 타입 하나를 정확하게 지정해야한다. *와 같은 패턴을 사용할 수 없다.

상위 타입을 지정하면, 하위 타입도 적용된다.

 

this에 대한 주의 사항이 있다.

스프링 AOP 프록시는 JDK 동적 프록시 또는 CGLIB를 통해 생성된다.

JDK 동적 프록시는 인터페이스가 필수이고, 인터페이스를 구현한 프록시를 생성한다.

반면에 CGLIB는 인터페이스가 필수적이지 않으며, 인터페이스가 있더라도 구현 클래스를 상속 받아 프록시를 생성한다.

이것 때문에 스프링 AOP 프록시를 생성한 기술(JDK 동적 프록시 or CGLIB)에 따라 this를 사용할 때 차이가 발생한다.

 

인터페이스가 없이는 JDK 동적 프록시는 사용 자체를 할 수 없기 때문에, 인터페이스가 있는 구현 클래스에 스프링 AOP를 적용하는 상황에서의 차이를 말한다.

  • JDK 동적 프록시는 인터페이스를 구현해서 프록시를 생성하기 때문에, 구현 클래스는 프록시의 상위 클래스가 아니다. 따라서 this(구현클래스 선언타입)으로 표현식을 작성하면, 프록시의 조인 포인트는 포인트컷으로 지정되지 않는다.
  • CGLIB는 인터페이스가 있더라도 구현 클래스를 상속해서 프록시를 생성하기 떄문에, 구현 클래스는 프록시의 상위 클래스이다. 따라서 this(구현 클래스 선언타입)으로 포현식을 작성하면, 프록시의 조인 포인트는 포인트컷으로 지정되지 않는다.
@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 {

	/*
	 * this(선언타입)은 선언타입과 일치하는 빈으로 등록된 객체(스프링 AOP 프록시)의 조인 포인트를 포인트컷으로 지정한다.
	 * target(선언타입)은 선언타입과 일치하는 타겟 객체(스프링 AOP 프록시가 참조하는 객체)의 조인 포인트를 포인트컷으로 지정한다.
	 * <p>
	 * 타입 하나를 정확하게 지정해야한다. *와 같은 패턴을 사용할 수 없다.
	 * <p>
	 * 상위 타입을 지정하면, 하위 타입도 적용된다.
	 */

	/*
	 * 스프링 AOP 프록시는 JDK 동적 프록시 또는 CGLIB를 통해 생성된다.
	 * JDK 동적 프록시는 인터페이스가 필수이고, 인터페이스를 구현한 프록시를 생성한다.
	 * CGLIB는 인터페이스가 필수적이지 않으며, 인터페이스가 있더라도 구현 클래스를 상속 받아 프록시를 생성한다.
	 *
	 * 이것 때문에 스프링 AOP 프록시를 생성한 기술(JDK 동적 프록시 or CGLIB)에 따라 this를 사용할 떄 차이가 발생한다.
	 *
	 * 인터페이스가 없이는 JDK 동적 프록시는 사용 자체를 할 수 없기 때문에,
	 * 인터페이스가 있는 구현 클래스에 스프링 AOP를 적용하는 상황에서의 차이를 말한다.
	 *
	 * JDK 동적 프록시는 인터페이스를 구현해서 프록시를 생성하기 때문에, 구현 클래스는 프록시의 상위 클래스가 아니다.
	 * 따라서 this(구현클래스 선언타입)으로 표현식을 작성하면, 프록시는 조인포인트로 적용되지 않는다.
	 *
	 * 반대로 CGLIB는 인터페이스가 있더라도 구현 클래스를 상속해서 프록시를 생성하기 때문에, 구현 클래스는 프록시의 상위 클래스이다.
	 * 따라서 this(구현클래스 선언타입)으로 표현식을 작성하면, 프록시는 조인포인트로 적용된다.
	 */

	/*
	 * JDK 동적 프록시 결과
	 * [this-interface] String hello.aop.member.MemberService.hello(String)
	 * [target-interface] String hello.aop.member.MemberService.hello(String)
	 * [target-impl] String hello.aop.member.MemberService.hello(String)
	 *
	 * CGLIB 결과
	 * [this-interface] 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)
	 * [target-impl] String hello.aop.member.MemberServiceImpl.hello(String)
	 */

	@Autowired
	MemberService memberService;

	@Test
	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();
		}

		//this: 스프링 AOP 프록시 객체 대상
		//JDK 동적 프록시는 인터페이스를 기반으로 생성되므로 구현 클래스를 알 수 없음
		//CGLIB 프록시는 구현 클래스를 기반으로 생성되므로 구현 클래스를 알 수 있음
		@Around("this(hello.aop.member.MemberServiceImpl)")
		public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
			log.info("[this-impl] {}", joinPoint.getSignature());

			return joinPoint.proceed();
		}

		//target: 실제 target 객체 대상
		@Around("target(hello.aop.member.MemberServiceImpl)")
		public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
			log.info("[target-impl] {}", joinPoint.getSignature());

			return joinPoint.proceed();
		}
	}
}

 

포인트컷 지시자의 인자 전달

this, target, args, @target, @within, @annotation, @args는 인자를 어드바이스의 파라미터에 전달할 수 있다.

  • 어드바이스의 파라미터 이름과, 포인트컷 표현식의 인자 이름을 동일하게 맞춰줘야한다.
  • 포인트컷 표현식의 인자 타입은 어드바이스의 파라미터 타입으로 정의된다.
  • args(arg, ..), advice(String arg) -> args(String, ..)

execution은 물론 JoinPoint.getArgs()를 통해 인자를 가져올 수 있다.

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

	/*
	 * this, target, args, @target, @within, @annotation, @args 포인트컷 지시자는 인자를 어드바이스의 파라미터에 전달할 수 있다.
	 *
	 * 어드바이스의 파라미터 이름과 포인트컷 표현식의 인자 이름을 맞춰줘야한다.
	 *
	 * 포인트컷 표현식의 인자 타입은 어드바이스의 파라미터 타입으로 정의된다.
	 * args(arg, ..), advice(String arg) -> args(String, ..)
	 */

	@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 {
			// 별다른 작업을 하지 않아도, joinPoint는 메서드 호출 시 사용한 인자를 갖고 있다.
			Object arg = joinPoint.getArgs()[0];

			log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg);
			// [logArgs1]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA

			return joinPoint.proceed();
		}

		@Around("allMember() && args(arg, ..)")
		public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
			// args(arg, ..)에서 arg에 해당하는 인자값이 어드바이스의 Object arg로 전달된다.

			log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
			// [logArgs1]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA

			return joinPoint.proceed();
		}

		@Before("allMember() && args(arg, ..)")
		public void logArgs3(String arg) throws Throwable {
			// args(arg, ..)에서 arg에 해당하는 인자값이 어드바이스의 String arg로 전달된다.

			log.info("[logArgs3] arg={}", arg);
			// [logArgs3] arg=helloA
		}

		@Before("allMember() && this(obj)")
		public void thisArgs(JoinPoint joinPoint, MemberService obj) {
			// this는 프록시 객체를 의미한다.
			log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
			// obj=class hello.aop.member.MemberServiceImpl$$EnhancerBySpringCGLIB$$c7e08066
		}

		@Before("allMember() && target(obj)")
		public void targetArgs(JoinPoint joinPoint, MemberService obj) {
			// target은 실제 객체(타겟)를 의미한다.
			log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
			// obj=class hello.aop.member.MemberServiceImpl
		}

		@Before("allMember() && @target(annotation)")
		public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
			log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
			// [@target]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
		}

		@Before("allMember() && @within(annotation)")
		public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
			log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
			// [@within]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
		}

		@Before("allMember() && @annotation(annotation)")
		public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
			log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
			// [@annotation]String hello.aop.member.MemberServiceImpl.hello(String), annotationValue=test value
		}
	}
}

 

    '스프링/Core' 카테고리의 다른 글
    • 스프링 AOP-구현 방법
    • 스프링 AOP-기본 개념
    • @Aspect
    • 동적 프록시를 위한 빈 후처리기
    maeng0830
    maeng0830

    티스토리툴바