지난 포스팅에서 AOP 어드바이스의 종류에 대해서 알아보았다.
타겟 메서드의 전후로 작용하는 @Around
타겟 호출 전에 작용하는 @Before
타겟 메서드가 정상적으로 완료되었을 때 작용하는 @AfterReturning
@Around는 JoinPoint를 사용하는 다른 어드바이스와 달리 ProceedingJoinPoin를 사용하고, 개발자가 직접 proceed()메서드를 호출해야 타겟이 호출이 된다.
여기서 발생할 수 있는 문제 때문에 @Around이외의 어드바이스가 생겼는데, 그 이유는
어드바이스에 대해 알아보며 각 어드바이스에 포인트컷이 들어가는 것을 확인했는데, 이번 포스팅에서는 이 포인트컷과 포인트컷 지시자에 대해 알아보려고 한다.
에스팩트J에서는 포인트컷을 편하게 표현하기 위해 표현식을 제공한다. 아래의 코드를 보자.
@Pointcut("execution(* hello.aop.order..*(..))")
위의 코드에서 execution 같은 것을 포인트컷 지시자(PointCut Designator), PCD라고 한다.
지시자 | 설명 |
---|---|
execution | 메서드 실행 조인 포인트를 매칭 가장 자주 사용하고 기능도 복잡 |
within | 특정 타입 내의 조인 포인트를 매칭 |
args | 타겟 파라미터가 지정한 타입의 인스턴스인 조인 포인트 |
this | 스프링 빈(스프링 AOP 프록시)을 대상으로 하는 조인 포인트 |
target | 타겟을 대상으로 하는 조인 포인트 |
@target | 타겟에 지정한 타입의 어노테이션이 있는 조인 포인트 |
@within | 지정한 어노테이션이 있는 타입 내 조인 포인트 |
@annotation | 메서드에 주어진 어노테이션을 가지고 있는 조인 포인트 |
@args | 전달된 실제 파라미터의 런타임 타입이 주어진 타입의 어노테이션인 조인 포인트 |
bean | 스프링 전용 포인트컷 지시자로, 빈의 이름으로 포인트컷 지정 |
코드로 확인해보자.
@Target(ElementType.TYPE)
// @Retention :
//
//
//
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
public interface MemberService {
String hello(String param);
}
@ClassAop
// AOP는 스프링 빈을 대상으로 작동하므로 컴포넌트 스캔
@Component
public class MemberServiceImpl implements MemberService{
@Override
@MethodAop("test value")
public String hello(String param) {
return "ok";
}
public String internal(String param) {
return "ok";
}
}
@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() {
log.info("helloMethod={}", helloMethod);
}
}
(*) - 이게 붙은것은 실무에서 사용될법한 PCD임
가장 많이 사용되는 PCD로 메서드 실행 조인 포인트를 매칭한다.
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
패턴 | 설명 |
---|---|
* | 모든 값과 매칭 * 만 사용하면 모든 글자를 대체 글자와 함께 사용하면 일부만 대체 |
. | 패키지 지정에 사용 정확하게 해당 위치의 패키지 |
.. | 패키지 지정에 사용 해당 위치의 패키지와 그 하위 패키지 모두 포함 |
0 | 파라미터 부분에서 사용 아무런 파라미터가 들어오지 않는다는 뜻 |
* | 파라미터 부분에서 사용 모든 타입이 들어올 수 있다는 뜻 하나의 파라미터만 대체 |
.. | 파라미터 부분에서 사용 아무런 값이 들어오지 않거나, 하나만 들어오거나, 여러개가 들어올 수 있다는 뜻 |
위에서 생성한 예제를 바탕으로 테스트를 통해 사용법을 알아보자.
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
pointcut.setExpression("execution(* *(..))"); // 모든 타입 매칭
pointcut.setExpression("execution(* *hello*(..))"); // 이름으로 매칭
pointcut.setExpression("execution(* nono(..))"); // 매칭 실패
pointcut.setExpression("execution(* hello.aop.member.*.*(..))"); // 이름으로 매칭
pointcut.setExpression("execution(* hello.aop..*.*(..))"); // 하위 패키지 포함
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
포인트컷으로 지정한 타입으로 조인포인트를 매칭하는 PCD
부모 타입도 허용하는 execution과 달리 타겟의 타입과 정확히 매칭이 되어야 한다.
코드로 사용법에 대해 알아보자.
// 가장 정확히 매칭
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
// *을 이용해 매칭
pointcut.setExpression("within(hello.aop.member.*Service*)");
// aop하위 패키지의 모든 타입과 매칭
pointcut.setExpression("within(hello.aop..*)");
// 부모로 매칭을 시도해 실패함
pointcut.setExpression("within(hello.aop.member.MemberService)");
파라미터가 지정한 타입과 매칭되는 조인포인트를 매칭
코드로 확인해보자.
// hello(String)과 매칭
assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
// 부모 타입인 Object로도 매칭
assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
// 패턴은 execution과 일치
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();
// false 반환
assertThat(pointcut("execution(* *(java.io.Serializable))")
.matches(helloMethod, MemberServiceImpl.class)).isFalse();
// false 반환
assertThat(pointcut("execution(* *(Object))")
.matches(helloMethod, MemberServiceImpl.class)).isFalse();
}
@Target과 @Within은 클래스에 지정한 어노테이션이 붙어있는 타겟을 매칭하는 PCD이다.
둘의 차이는
@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(); //부모 클래스만 있는 메서드
}
// Parent, Child 스프링 빈으로 등록
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(){}
}
// AOP
@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();
}
}
}
부모 클래스인 Parent와 자식 클래스 Child를 생성하고 스프링 빈으로 등록해줬다.
자식 클래스인 Child에는 @ClassAop 어노테이션이 붙어 있으며
@Aspect에서 생성한 atTarget(), atWithin() 어드바이스 모두 @ClassAop를 포인트컷으로 설정하였다.
테스트 결과를 확인해보면,
@target은 자기 자신의 메서드와 부모 메서드 모두 호출하는 것을 확인할 수 있고,
@within은 자기 자신의 메서드만 호출하는 것을 확인할 수 있다.
args, @args, @target는 실행될 때 어드바이스 적용 여부를 확인하는데, 포인트컷 적용 여부는 프록시가 있어야 판단을 할 수 있다.
그런데 이 프록시는 스프링 컨테이너가 생성되는 어플리케이션 로딩 시점에 생성된다.
따라서 args, @args, @target 같은 포인트컷이 있으면 스프링에서는 모든 빈에 AOP를 적용하려고 한다. 프록시가 없으면 포인트컷 적용 여부를 판단할 수 없기 때문이다.
하지만 final이 붙은 빈의 경우 프록시를 생성하는 과정에서 오류가 발생할 수 있다.(cglib의 경우 상속을 이용해 프록시를 생성하기 때문이다.)
따라서 이런 지시자의 경우 예제에서처럼 프록시 적용 대상을 축소하는 지시자와 함께 사용해야 한다.
메서드가 지정한 어노테이션을 가지고 있으면 매칭
코드로 확인해보자.
// 생략
@Component
public class MemberServiceImpl implements MemberService{
@Override
@MethodAop("test value")
public String hello(String param) {
return "ok";
}
// 생략
hello()는 @MethodAop라는 어노테이션을 가지고 있다.
테스트 코드
@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 {
// @MethodAop를 포인트컷으로 지정
@Around("@annotation(hello.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@annotation] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
}
결과를 확인해보면
지정한 어드바이스가 적용된 것을 확인할 수 있다.
전달된 실제 파라미터의 런타임 타입이 지정한 어노테이션을 가지고 있을 경우 매칭
예를 들어 @Method를 포인트컷으로 지정했을 때 String타입 파라미터를 전달받았는데, String내부에 @Method 어노테이션을 가지고 있을 경우 매칭한다.
잘 사용되지 않으니 그냥 참고만 하자.
스프링에서만 지원하는 PCD, 말 그대로 빈 이름으로 지정한다.
* 같은 패턴을 사용할 수 있다.
@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();
}
}
}
다음의 포인트컷 표현식은 어드바이스에 매개변수를 전달할 수 있다.
this, target, args, @target, @within, @annotation, @args
코드로 확인해보자.
@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() {
}
// JoinPoint를 이용해 인수 전달 받기
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
// 표현식과 메서드 파라미터를 매칭시켜 전달 받기
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
// @Before로 축약함
@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());
}
// @target : 타입이 지정한 어노테이션을 가지고 있을 경우 매칭(클래스 단위)
// 지정한 타입의 어노테이션을 전달받음
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
}
// @within : 타입이 지정한 어노테이션을 가지고 있을 경우 매칭(클래스 단위)
// 지정한 타입의 어노테이션을 전달받음
@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
}
// @annotation : 메서드가 지정한 어노테이션을 가지고 있을 경우 매칭(메서드 단위)
// 지정한 타입의 어노테이션을 전달받음
// 어노테이션이 가지고 있는 값을 꺼낼 수 있음
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation.value());
}
}
}
테스트 결과를 확인해보면
각 지시자에 맞는 인자를 전달받은 것을 확인할 수 있다.
그런데 여기서 this와 target을 사용할 때 문제가 발생할 수도 있다.
this는 프록시 객체를, target은 실제 대상 객체를 인자로 전달받는다는 것까지는 구분이 되는데, JDK 동적프록시와 CGLIB 둘 중 어떤 기술을 사용해 프록시를 생성하느냐에 따라 의도하지 않는 결과가 발생할 수 있다.
앞서 말했듯이
this는 프록시(스프링 빈)를 대상으로 하는 조인 포인트
target은 타겟(프록시가 가르키는 대상)을 대상으로 하는 조인포인트이다.
* 과 같은 패턴을 사용할 수 없다.
부모 타입을 지원한다.
언뜻 봐서는 두 개의 지시자에서 헷갈릴만한 요소가 뭔지 떠오르질 않는다.
하지만 프록시 방식에 따라서 결과가 달라지게 된다.
JDK는 기본적으로 인터페이스를 필수로 하고, 인터페이스를 구현한 프록시를 생성한다.
반면 CGLIB는 인터페이스가 있든 없든 구현 클래스를 상속받아 프록시를 생성한다.
그림으로 보자면 아래와 같다
포인트컷 지정에 따라 어떤 차이가 있는지 확인해보자.
MemberService(인터페이스)를 포인트컷으로 지정했을 때
MemberServiceImpl(구체 클래스)을 포인트컷으로 지정했을 때
this(hello.aop.member.MemberServiceImpl) - 프록시를 대상으로 AOP 적용여부를 판단
target(hello.aop.member.MemberServiceImpl) - target을 대상으로 AOP 적용여부를 판단
- 포인트컷과 타겟의 타입이 동일하므로 AOP 적용 대상이 된다.
MemberService(인터페이스)를 포인트컷으로 지정했을 때
MemberServiceImpl(구체 클래스)을 포인트컷으로 지정했을 때
그런데 기본적으로 스프링은 CGLIB을 이용해 프록시를 생성하기 때문에 따로 설정을 변경하지 않으면 이런 문제를 접할 일은 별로 없을 것이다.
다만 만약 JDK 동적 프록시를 사용할 필요가 있다면 application.properties에 다음 설정을 활용하자.
# 기본 설정값(CGLIB 사용)
#spring.aop.proxy-target-class=true
# JDK 동적 프록시 사용
#spring.aop.proxy-target-class=false
일일이 properties 파일을 바꾸기 번거롭다면 아래와 같은 방법으로 설정값을 임시로 변경할 수 있다.
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시 사용
이번 포스팅에서는 여러종류의 포인트컷 지시자에 대해서 알아보았다.
포인트컷 지시자(PointCut Designator, PCD)에는 다음과 같은 종류가 있다.
execution
within
args
@target, @within
@annotation
@args
bean
this
target
포인트컷 지시자 중 this, target, @within, @target, @annotation, @args 등은 어드바이스에서 매개변수를 전달받을 수 있다.
매개변수를 꺼내는 방법은 아래와 같다.
@target(annotation)
public void test(ClassAop annotation)
프록시 생성 방식에 따라서 this에서 예상과 다른 결과가 나올 수 있다.
예를들어 Service(인터페이스)와 ServiceImpl이 있다고 했을 때,
JDK 동적 프록시는 Service를 구현한 프록시를 생성할 것이고,
CGLIB의 경우 인터페이스 여부 상관없이 구현 클래스를 활용해 프록시를 생성하므로 ServiceImpl을 상속받아 프록시를 생성한다.
만약 this(Service)를 포인트컷으로 지정했다면,
반면에 this(ServiceImpl)을 포인트컷으로 지정했다면,
츨처 : 김영한 - 스프링 핵심 원리 고급편