[Spring] Spring Advisor와 Pointcut - 실전편
이전 글에서 DefaultPointcutAdvisor와 다양한 Pointcut들에 대한 개념을 다뤄봤습니다.
2022.10.10 - [Programming/Spring] - [Spring] Spring Advisor와 Pointcut - 개념편
[Spring] Spring Advisor와 Pointcut - 개념편
Spring AOP 컴포넌트를 이용해 Advice를 만들어 메서드의 전후처리를 프록시 객체를 통해 구현해봤습니다. 2022.05.16 - [Programming/Spring] - [Spring] Spring Advice로 커스텀 어드바이스 만들기 [Spring] Spr..
blog.neonkid.xyz
Pointcut은 인터페이스 형태로 되어 있지만 Spring AOP에서 이미 많은 구현체들이 만들어져 있기 때문에 일부러 우리가 커스텀 포인트컷을 개발하여 사용할 필요가 없었다고 이야기 했었죠.
이번 글에서는 이 구현된 8개의 포인트컷을 어떻게 사용하는지에 대해 알아보겠습니다.
StaticMethodMatcherPointcut
먼저 편의 클래스부터 시작해보겠습니다. 추상 StaticMethodMatcherPointcut 클래스를 상속하여 간단한 정적 포인트컷을 아래와 같이 만들어봅니다.
StaticMethodMatcherPointcut 클래스는 MethodMatcher 인터페이스를 구현하는 추상 클래스인 StaticMethodMatcher 클래스를 상속하므로 matches 메서드를 구현해야 합니다.
나머지 Pointcut 구현체는 이미 구현되어 있기 때문에 신경쓰지 않아도 되므로 StaticMethodMatcherPointcut을 구현할 때는 아래의 두 가지만 정의하면 됩니다.
- getClassFilter에 원하는 클래스의 메서드 정의
- matches에 원하는 메서드 정의
matches는 필수지만 getClassFilter는 필수가 아닌데, 같은 이름의 모든 클래스 메서드가 아닌 특정 클래스의 메서드만 정의하고 싶다면 getClassFilter에 원하는 클래스를 넣어서 정의할 수 있습니다.
간단한 예제를 보겠습니다. 동일한 DefaultPointcutAdvisor를 사용하여 두 클래스의 프록시를 생성하고 JavaProgrammer 클래스의 programming() 메서드에만 어드바이스를 사용해보겠습니다.
matches 메서드에서 methd.getName()을 이용해 실제 객체가 호출하는 메서드의 이름을 가져올 수 있습니다. 이 메서드 이름을 programming으로 정의하여 해당 메서드에만 어드바이스를 적용할 수 있도록 합니다.
추가로 getClassFilter 메서드를 사용해 특정 클래스에서만 작동하도록 구현할 수도 있습니다.
이제 어드바이스를 만들어보겠습니다. SimpleAdvice라는 이름의 클래스를 만들고, MethodInterceptor를 구현합니다.
MethodInvocation 인자에서는 우리가 Pointcut에 정의했던 Method 이름이 나올 것입니다. return true 코드를 작성하여 어떤 메서드든 Pointcut을 동작하도록 했다면 그 메서드 이름이 나타날 것입니다.
programming 메서드를 호출하기 전에 println을 하고, 호출하고 난 뒤, Done을 println 해보도록 하겠습니다.
이제 SimpleAdvice와 SimpleStaticPointcut 클래스를 사용해 실제 Pointcut을 어떻게 실행시킬 수 있는지 보겠습니다. 위 예제 코드를 보면 두 클래스 모두 DefaultPointcutAdvisor 인스턴스를 생성하는 간단한 테스트 애플리케이션인 것을 볼 수 있습니다. 또한 두 클래스 모두 동일하게 인터페이스를 구현하므로 구상 클래스(conctete class)가 아닌 인터페이스를 기반으로 프록시를 생성하는 것을 알 수 있습니다.
결과를 보면 SimpleAdvice는 JavaProgrammer 클래스의 programming() 메서드에 의해서만 호출되었습니다. 이와 같이 Pointcut으로 어드바이스를 적용하는 메서드를 매우 간단하게 제어할 수 있습니다.
DynamicMethodMatcherPointcut
동적 포인트컷도 정적 포인트컷과 생성하는 방법이 크게 다르지 않습니다.
위 예제는 동적 포인트컷을 적용하기 위한 클래스입니다. 아무런 인자도 받지 않는 bar 메서드와 int 정수 숫자 한 개를 인자로 받는 foo 메서드를 가지고 있습니다.
StaticPointcut과 마찬가지로 foo() 메서드에만 어드바이스를 적용하는데, 이전에 사용한 Programmer 인터페이스와는 다르게 전달된 int 인수가 100이 아닌 경우에만 이 메서드에 어드바이스를 사용해봤습니다.
스프링에서는 동적 포인트컷을 생성하는 데 편리한 기반 클래스인 DynamicMethodMatcherPointcut을 제공하는데, 이 클래스는 MethodMatcher 인터페이스를 구현체로 가지고 있어 아래의 추상 메서드를 가지고 있습니다.
boolean matches(Method method, Class<?> targetClass, Object... args);
또한, 정적 검사의 동작을 제어하기 위해 아래의 메서드를 함께 구현하는 것이 좋습니다.
boolean matches(Method method, Class<?> targetClass);
잘보면 동적 검사인 전자 matches만 구현해도 되지만 정적 검사인 후자의 matches까지를 구현한 이유는 정적 검사를 통해 특정 메서드가 어드바이스를 통하지 않도록 하기 위해서인데요.
스프링은 정적 검사 메서드를 수행하지 않으면 동적 검사 메서드에서 모든 메서드에 대해 동적 검사를 수행합니다. 따라서 필요하지 않은 메서드까지 모두 동적 검사를 수행하기 때문에 소요 시간이 많이 발생하는데, 이를 정적 검사를 통해 먼저 필요하지 않은 메서드를 검사하고, 검사 결과가 일치하지 않으면 더 이상 동적 검사를 하지 않기 때문에 성능적인 이점이 생깁니다.
게다가 정적 검사의 결과를 한 번 캐싱하기 때문에 같은 메서드의 정적 검사 결과가 있는 경우 정적 검사 또한 수행하지 않습니다.
최적화된 포인트컷을 사용하기 위해 정리하자면 다음과 같습니다.
- 동적 포인트컷의 정적 검사를 이용하여 어드바이스를 사용하지 않을 메서드를 선정.
- 동적 검사를 이용하여 확실하게 어드바이스를 쓸 메서드만 사용하도록 구현
- getClassFilter를 이용해 클래스 검사를 수행
실제로 동적 포인트컷이 동작하는 순서는 다음과 같습니다.
- getClassFilter 메서드에서 클래스 검사 수행.
- 인수를 받지 않는 matches 메서드 (정적 검사) 수행
- 인수를 받는 matches 메서드 (동적 검사) 수행
이렇게 해두면 포인트컷을 이해하고 유지보수하기가 훨씬 쉬워지며 좋은 성능을 보일 수 있습니다.
이제 포인트컷을 직접 실행해보며 위 수행 과정을 확인해보겠습니다.
어드바이스는 정적 포인트컷에서 사용했던 SimpleAdvice를 그대로 사용했습니다.
실행 결과를 보다시피 처음 두 번의 foo() 메서드 호출에만 어드바이스가 적용되었으며 처음에는 먼저 정적 검사가 수행된 것을 볼 수 있습니다. (우리가 직접 구현하지 않았던 기본적인 Object 메서드들까지 전부...)
여기서 주목해야할 점은 foo() 메서드는 모든 메서드를 검사하는 초기화 단계, 메서드가 처음 호출되는 단계에서 두 번의 정적 검사를 받았다는 것입니다.
이처럼 동적 포인트컷은 정적 포인트컷보다 더 큰 유연성을 제공합니다. 하지만 런타임의 성능 오버헤드를 고려하여 꼭 필요할 때만 동적 포인트컷을 사용해야한다는 것을 명심합시다.
NameMatchMethodPointcut
종종 포인트컷을 만들 때 메서드의 시그니처와 반환 타입의 상관없이 메서드의 이름을 기반으로 적용하고 싶을 때가 있습니다. 이 때는 StaticMethodMatcherPointcut의 하위 클래스를 생성하는 방법도 있지만 이의 하위 클래스인 NameMatchMethodPointcut을 사용해 메서드 이름 목록과 메서드 이름을 매칭하는 방법도 있습니다.
다만 이 포인트컷은 메서드 시그니처는 고려하지 않기 때문에 programming()과 programming(1.4) 메서드가 있다면 둘 다 programming이라는 이름과 매칭됩니다.
정적 포인트컷에서 사용했던 JavaProgrammer 예제 코드에 programming() 메서드를 오버로딩하여 특정 IDE를 사용해 프로그래밍하고 있는 것을 표현해보겠습니다.
이 예제에서 하고자 하는 것은 NameMatchMethodPointcut을 이용해 programming(), programming(IDE), test() 메서드에 어드바이스를 적용하려고 합니다. 그러면 이 포인트컷은 programming과 test 라는 이름으로만 매칭될 것입니다.
IDE는 열거체로 표기하여 Eclipse부터 Vim까지 넣어보겠습니다.
NameMatchMethodPointcut을 사용할 때는 인스턴스를 생성하고 addMethodName 메서드를 이용해 매칭할 메서드 이름을 추가해줍니다.
그러면 포인트컷에 추가한 programming, test 두 개의 메서드가 나오는데, 이 때 인자로 넣은 IDE 까지도 같이 포함되어 어드바이스가 적용되는 것을 볼 수 있습니다.
RegexpMethodPointcut
메서드 이름을 정확히 특정할 수 없을 때는 패턴을 사용해볼 수 있습니다. 일정한 패턴의 이름에서만 전후처리(어드바이스)를 적용하고 싶다면 정규 표현식 포인트컷인 JdkRegexMethodPointcut을 사용해 정규 표현식 기반으로 메서드 이름을 매칭시킬 수 있습니다.
비동기 프로그래밍 기법과 동기 프로그래밍 기법을 모두 사용할 줄 아는 PythonProgrammer 클래스를 구현하였습니다. 이 예제는 test 메서드를 제외한 모든 programming 메서드에서 어드바이스를 적용 해보고자 합니다.
정규 표현식 기법으로 포인트컷을 사용하면 클래스에서 이름이 programming이라고 시작하는 모든 메서드에 어드바이스를 적용할 수 있습니다. (설령 대소문자가 다르더라도)
정규 표현식을 사용해 메서드 이름을 매칭하는 포인트컷을 사용할 때도 인스턴스를 생성한 다음 setPattern 등의 패턴을 추가하여 사용할 수 있습니다.
여기서 재미있는 것은 패턴인데, 메서드 이름을 매칭할 때 스프링은 패키지와 클래스 이름이 포함된 전체 메서드 이름과 매칭합니다. 만약 패키지 이름이 xyz.neonkid.programmers.PythonProgrammer 라면 스프링은 xyz.neonkid.programmers.PythonProgrammer.programming과 매칭합니다. 그래서 정규표현식 앞에 .*을 사용했으며 대소문자 구분을 하지 않기 위해 (?i)를 추가하였습니다. 따라서 패키지에 있는 클래스와 메서드의 이름을 모두 알지 못하더라도 주어진 패키지 내의 모든 메서드를 매칭할 수 있다는 점에서 매우 유용하게 쓸 수 있습니다.
그러면 test() 메서드를 제외한 programming()과 asyncProgramming() 메서드에만 어드바이스가 적용됩니다.
AspectJexpPointcut
JDK 정규 표현식 외에도 AspectJ의 포인트컷 표현식 언어를 이용해 포인트컷을 선언할 수 있습니다. 대부분의 스프링 개발자 분들이 정규 표현식을 사용한다면 이 표현식을 사용할 것이라고 생각합니다. 왜냐하면 스프링에서는 @AspectJ 애너테이션 방식의 AOP 지원 기능을 지원하기 때문입니다.
@AspectJ 애너테이션 방식 AOP 지원 기능을 이용할 때는 AspectJ의 포인트컷 언어를 사용해야 합니다. 따라서 표현식 언어를 사용해 포인트컷을 사용할 때는 AspectJ 포인트컷 표현식을 사용하는 것이 가장 좋은 방법입니다.
Spring에서 AspectJ 포인트컷 표현식을 사용하려면 프로젝트의 classpath에 두 개의 AspectJ 라이브러리 파일인 aspectjrt.jar와 aspectjweaver.jar를 포함해야 합니다.
하지만 Spring boot의 spring-boot-starter-aop를 사용한다면 이를 별도로 추가할 필요가 없이 바로 AspectJexpPointcut을 사용해 볼 수 있습니다.
RegexMethodPointcut과 마찬가지로 인스턴스 생성 후 setExpression 메서드를 통해 패턴을 설정한 후 사용할 수 있습니다. 다만 JDK 정규 표현식과는 전혀 다른 syntax를 사용하기 때문에 AspectJ expression을 잘 모르는 분이라면 이를 먼저 익혀보고 사용하시는 걸 추천합니다.
AnnotationMatchingPointcut
애플리케이션이 애너테이션 기반일 때는 포인트컷을 정의할 때 특정 커스텀 애너테이션을 만들고 이를 사용해 포인트컷을 정의하고 싶을 때가 있습니다. 즉, 특정 애너테이션이 적용된 모든 메서드나 타입에 어드바이스 로직을 적용하고 싶을 때가 있습니다.
스프링은 애너테이션을 사용해 포인트컷을 정의할 수 있는 AnnotationMatchingPointcut 클래스를 제공합니다.
@interface 타입으로 사용해 인터페이스를 애너테이션으로 선언하고 @Target 애너테이션은 애너테이션을 타입 레벨과 메서드 레벨로 적용할 수 있게 정의합니다.
위에서 구현한 JavaProgrammer 클래스에서 programming(IDE ide)에 조금 전에 만든 애너테이션을 적용해봅니다.
forMethodAnnotation 메서드를 호출하여 애너테이션 타입을 전달해 AnnotationMatchingPointcut 인스턴스를 생성합니다. 이는 지정한 애너테이션이 적용된 모든 메서드에 어드바이스를 사용한다는 코드입니다.
forClassAnnotation 메서드를 호출해 타입 레벨에 지정한 애너테이션을 적용할 수도 있습니다. 조금 전엔 메서드 위에 @AdviceRequired를 했지만 클래스 위에 @AdviceRequired 한 경우 이 메서드를 호출해야 합니다.
@AdviceRequired를 적은 programming(IDE ide)에만 어드바이스가 적용된 것을 알 수 있습니다.
마치며...
여기까지 Pointcut에 대한 간단한 사용법을 알아봤습니다.
스프링에서는 많은 Pointcut 구현체에 대해 포인트컷 역할을 하는 편리한 Advisor 구현체를 제공합니다. 예를 들어 DefaultPointcutAdvisor와 함께 NameMatchMethodPointcut을 사용하는 대신 NameMatchMethodPointcutAdvisor를 이용할 수도 있습니다.
NameMatchMethodPointcut 인스턴스를 생성하는 대신 NameMatchMethodPointcutAdvisor 인스턴스에서 포인트컷 상세 정보를 구성하는 것을 확인할 수 있습니다.
이렇게 NameMatchMethodPointcutAdvisor는 Advisor이자 포인트컷 역할을 합니다. 두 방식에 대한 큰 성능 차이는 없으며 두 번째 코드가 더 짧다는 것에 의의가 있습니다. 하지만 좀 더 명확하고 구체적인 구현 과정을 보고자 한다면 첫 번째 방식이 좋을 수도 있기 때문에 개인 성향에 맞게 사용하는 것이 좋습니다.
다양한 Advisor 구현체에 대한 자세한 정보는 아래의 링크인 org.springframework.aop.support 패키지의 JavaDoc을 보면 확인할 수 있습니다.
org.springframework.aop.support (Spring Framework 5.3.23 API)
Package org.springframework.aop.support Description Convenience classes for using Spring's AOP API.
docs.spring.io
'Programming > Spring' 카테고리의 다른 글
[Spring] 비동기 작업과 모니터링을 위한 TaskExecutor, TaskScheduler 기본 (0) | 2022.11.06 |
---|---|
[Spring] Spring Advisor와 Pointcut - 개념편 (0) | 2022.10.10 |
[Spring] OSIV로 알아보는 Spring Transaction 헤짚기 (0) | 2022.06.26 |
[Spring] Spring Advice로 커스텀 어드바이스 만들기 (0) | 2022.05.16 |
[Spring] Spring AOP - Spring에서는 AOP를 어떻게 이용할까? (0) | 2022.05.15 |