https://sundaland.tistory.com/68
[ ▶ Pointcut API in Spring ]
[ ▷ Concepts ]
스프링의 포인트컷 모델을 advice 유형과 독립적으로 포인트컷을 재사용할 수 있게 해준다. 동일판 포인트 컷으로 다양한 어드바이스를 타겟팅할 수 있다.
org.springframework.aop.Pointcut 인터페이스는 특정 클래스와 메서드를 타겟으로 어드바이스를 지정하기 위해 사용하는 핵심 인터페이스이다.
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
}
Pointcut 인터페이스를 두 부분으로 나누면 클래스와 메서드 매칭 부분을 재사용하고, 세분화된 조합 작업 (메서드 매처와 합집합 수행)을 가능하게 한다.
ClassFilter 인터페이스는 포인트컷을 특정 타겟 클래스 집합으로 제한하는 데 사용된다. matches() 메서드가 항상 ture를 반환하면 모든 타겟 클래스가 일치한다.
public interface ClassFilter {
boolean matches(Class clazz);
}
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object... args);
}
matches(Method, Class) 메서드는 특정 클래스의 메서드가 포인트컷에 의해 일치하는지 여부를 테스트하는데 사용된다. 이 평가를 AOP 프록시가 생성될 때 수행하여 모든 메서드 호출시 테스트할 필요성을 없앨 수 있다. 만약 두 개의 아규먼트를 가지는 matches 메서드가 특정 메서드에 대해 true를 반환하고, MethodMatcher의 isRuntime() 메서드가 true를 반환하면, 세 개의 아규먼트를 가지는 matches 메서드는 각 메서드 호출 시마다 호출된다. 이를 통해 포인트컷은 타겟 어드바이스가 시작되기 직전에 메서드 호출에 전달된 아규먼트를 검사할 수 있다.
대부분의 MethodMatcher 구현은 정적이어서 isRuntime() 메서드가 false를 반환한다. 이 경우 세 개의 아규먼트를 가지는 matches 메서드는 호출되지 않는다.
가능한 포인트컷을 정적으로 만들어 AOP 프레임워크가 AOP 프록시가 생성될 때 포인트컷 평과 결과를 캐시할 수 있게 하는 것이 좋다.
[ ▷ MethodMatcher 인터페이스와 AOP의 역할 ]
MethodMatcher 인터페이스는 스프링 AOP에서 메서드 단위로 포인트컷을 정의할 때 사용되는 중요한 인터페이스이다. 이 인터페이스는 특정 메서드가 포인트컷의 조건에 맞는지 여부를 결정한다.
public interface MethodMatcher {
boolean matches(Method m, Class<?> targetClass);
boolean isRuntime();
boolean matches(Method m, Class<?> targetClass, Object... args);
}
1. matches(Method m, Class<?> targetClass)
- 역할 : 이 메서드는 지정된 메서드 (m)과 해당 메서드가 속한 클래스 (targetClass)가 포인트컷의 조건에 맞는지를 검사한다.
- 사용 시점 : AOP 프록시가 생성될 때 한 번 실행된다. 이 메서드가 true를 반환하면, 해당 메서드에 대해 어드바이스 적용된다.
- 장점 : 정적 매칭을 수행하므로, 메서드 호출 시마다 이 매칭 로직을 반복할 필요가 없다.
2. isRuntime()
- 역할 : 이 메서드는 런타임 시점에서 추가적인 매칭 로직이 필요한지를 결정한다,
- 반환 값 (ture) : 메서드가 호출될 때마다 세 개의 아규먼트를 받는 matches(Method m, Class<?> targetClass, Object... args) 메서드가 호출된다.
- 반환 값 (false) : 메서드 호출 시 추가적인 매칭이 수행되지 않으면, 캐시된 결과를 사용한다.
- 특징 : 대부분의 구현에서는 정적 매칭이 더 효율적이므로 false를 반환한다.
3. matches(Method m, Class<?> targetClass, Object... args)
- 역할 : 이 메서드는 isRuntime()이 true일 때만 호출되며, 메서드의 실제 아규먼트 (args)를 포함한 보다 세부적인 매칭을 수행한다.
- 사용 시점 : 메서드 호출 시마다 실행되어, 특정 아규먼트 값에 따라 포인트컷 매칭 여부를 동적으로 결정할 수 있다.
- 성능 고려 사항 : 런타임 매칭은 매 호출 시마다 실행되므로, 성능에 영향을 미칠 수 있다. 가능한 정적 매칭을 선호나는 것이 좋다.
[ ▷ 정적 매칭과 런타임 매칭 ]
1. 정적 매칭 (Static Matching)
- 설명 : 정적 매칭은 AOP 프록시가 생성될 때 수행되는 매칭이다. 즉, 애플리케이션이 시작되거나 빈이 초기화될 때 특정 메서드가 포인트컷에 일치하는지 미리 결정한다.
- 장점 : 메서드 호출 시마다 매칭을 수행할 필요가 없기 때문에 성능이 더 좋다. matches(Method m, Class<?> targetClass) 메서드에서 매칭이 수행되고, 그 결과는 캐시된다.
- 적용 사례 : 메서드 이름이나 메서드가 속한 클래스와 같은 고정된 특성에 따라 어드바이스를 적용할 때 사용된다.
2. 런타임 매칭 (Runtime Matching)
- 설명 : 런타임 매칭은 메서드가 호출될 때마다 아규먼트 값과 같은 동적인 특성을 기반으로 매칭을 수행한다. 이 경우, 매 호출 시마다 matches(Method m, Class<?> targetClass, Object... args) 메서드가 실행된다.
- 단점 : 매번 매칭이 수행되므로, 성능에 부정적인 영향을 미칠 수 있다.
- 적용 사례 : 특정 인자 값에 따라 어드바이스 적용 여부를 동적으로 결정해야 할 때 사용된다. 예를 들어, 메서드 아규먼트가 특정 값일 때만 로깅을 수행하도록 하는 경우가 있다.
[ ▷ 포인터컷 캐싱의 중요성 ]
- 캐싱의 이점 : 포인트컷을 정적으로 만들어 AOP 프록시가 생성될 때 평가된 결과를 캐시하면, 메서드 호출 시 매칭을 다시 수행할 필요가 없어 성능이 개선된다. 캐싱된 결과를 사용하면 런타임 오버헤드를 줄일 수 있다.
- 결론 : 가능한 정적 매칭을 활용하여 AOP 프레임워크가 효율적으로 동작하도록 하는 것이 좋다.
[ ▷ 요약 ]
- 정적 매칭 : 성능에 유리하며, 대부분의 경우에 사용된다.
- 런타임 매칭 : 특정 상황에서만 사용하며, 성능에 영향을 줄 수 있다.
- isRuntime() : 런타임 매칭이 필요한지 여부를 결정한다.
- 캐싱 : 정적 매칭 결과를 캐시하여 성능을 최적화할 수 있다.
[ ▷ Operations on Pointcus ]
스프링은 포인트컷에 대한 작업 (합집합 및 교집합)을 지원한다. 합집합은 두 포인트컷 중 하나라도 일치하는 메서드를 의미하고, 교집합은 두 포인트컷 모두에 일치하는 메서드를 의미한다. 일반적으론 합집합이 더 유용하다. 포인트컷을 구성하려면 org.springframework.aop.support.Pointcuts 클래스의 정적 메서드를 사용하거나 동일한 패키지에 있는 ComposablePointcut 클래스를 사용할 수 있다. 그러나 AspectJ 포인트컷 표현식을 사용하는 것이 더 간단하다.
[ ▷ AspectJ Expression Pointcus ]
스프링에서 가장 중요한 포인트컷 유형은 org.springframework.aop.aspectj.AspectJExpressionPointcut이다. 이는 AspectJ 포인트컷 표현식 문자열을 구문 분석하기 위해 AspectJ에서 제공하는 라이브러리를 사용하는 포인트컷이다.
[ ▷ Convenience Pointcut Implementations ]
스프링은 몇 가지 편리한 포인트컷 구현을 제공한다. 이들 중 일부는 직접 사용할 수 있으며, 일부는 애플리케이션에 특화된 포인트컷에서 서브클래싱하기 위해 설계되었다.
[ ▷ Static Pointcuts ]
정적 포인트컷은 메서드와 타겟 클래스에 기반하며 메서드의 아규먼트를 고려할 수 없다. 대부분의 사용 사례에서 정적 포인트컷이 충분하며 최선의 선택이다. 스프링은 메서드가 처름 호출될 때만 정적 포이트컷을 평가할 수 있다. 그 이후에는 각 메서드 호출 시마다 포인트컷을 다시 평가할 필요가 없다.
[ ▷ Regular Expression Pointcuts ]
정적 포인트컷을 지정하는 한 가지 방법은 정규 표현식을 사용하는 것이다. 스프링 외의 여러 AOP 프레임워크도 이를 가능하게 한다.
org.springframework.aop.support.JdkRegexpMethodPointcut은 JDK의 정규 표현식 지원을 사용하는 일반 정규 표현식 포인트컷이다.
JdkRegexpMethodPointcut 클래스를 사용하면 패턴 문자열 목록을 제공할 수 있다. 이 중 하나라도 일치하면 포인트컷이 true로 평가된다. (결과적으로 지정된 패턴의 합집합이 되는 포인트컷이 생성된다.)
<bean id="settersAndAbsquatulatePointcut"
class="org.springframework.aop.support.JdkRegexpMethodPointcut">
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
스프링은 RegexpMethodPointcutAdvisor 이라는 편리한 클래스를 제공하여 어노테이션을 참조할 수 있게 해준다. 내부적으로 스프링은 JdkRegexpMethodPointcut을 사용한다. RegexpMethodPointcutAdvisor를 사용하면 하나의 빈이 포인트컷과 조언을 모두 캡슐화하므로 연결이 간단해진다.
<bean id="settersAndAbsquatulateAdvisor"
class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="advice">
<ref bean="beanNameOfAopAllianceInterceptor"/>
</property>
<property name="patterns">
<list>
<value>.*set.*</value>
<value>.*absquatulate</value>
</list>
</property>
</bean>
RegexpMethodPointcutAdvisor는 모든 어드바이스 유형과 함께 사용할 수 있다.
[ ▷ Attribute-driven Pointcuts ]
정적 포인트컷의 중요한 유형 중 하나는 메타데이터 기반 포인트컷이다. 이는 메타데이터 속성 (일반적으로 소스 수준 메타데이터)의 값을 사용한다.
[ ▷ Dynamic pointcuts ]
동적 포인트컷은 정적 포인트컷보다 평가 비용이 더 많이 든다. 이들은 메서드 아큐먼트뿐만 아니라 정적 정보를 고려한다. 이는 메서드 호출 시마다 평가해야 하며, 아규먼트가 달라지므로 결과를 캐시할 수 없다는 것을 의미한다.
주로 예시는 control flow 포인트컷이다.
[ ▷ Control Flow Pointcuts ]
스프링 제어 흐름 포인트컷은 개념적으로 AspectJ의 cflow 포인컷과 유사하지만 덜 강력하다. (현재는 다른 포인트컷에 의해 일치된 조인 포인트 아래에서 포인트컷을 실행하도록 지정할 방법이 없다) 제어 흐름 포인트컷은 현재 호출 스택을 매칭한다. 예를 들어, 조인 포인트가 com.mycompany.web 패키지 메서드에 의해 호출되었거나 SomeCaller 클래스에 의해 호출된 경우 작동할 수 있다. 제어 흐름 포인트컷은 org.springframework.aop.support.ControlFlowPointcut 클래스를 사용하여 지정한다.
제어 흐름 포인트컷은 다른 동적 포인트컷보다도 실행 시 평가 비용이 훨씬 더 비싸다. 자바 1.4에서는 그 비용이 다른 동적 포인트컷보다 약 5배 더 든다.
[ ▷ Pointcut Superclasses ]
스프링은 사용자 정의 포인트컷을 구현하는데 유용한 포인트컷 슈퍼클래스를 제공한다. 정적 포인트컷이 가장 유용하므로 StaticMethodMatcherPointcut을 서브클래싱하는 것이 좋다. 이 경우 하나의 추상 메서드만 구현하면 되며 (다른 메서드를 재정의하여 동작을 사용하 정의할 수 있다)
▼ StaticMethodMatcherPointcut를 서브클래싱하는 방법
class TestStaticPointcut extends StaticMethodMatcherPointcut {
public boolean matches(Method m, Class targetClass) {
// 사용자 정의 기준에 맞는 경우 true 반환
}
}
[ ▶ Advice API in Spring ]
[ ▷ Advice Lifecycles ]
각 어드바이스는 스프링 빈이다. 어드바이스 인스턴스는 모든 대상 객체에 공유될 수 있으며, 각 대상 객체에 고유하게 존재할 수 있다. 이는 클래스별 혹은 인스턴스별 어드바이스에 해당한다.
클래스별 어드바이스는 일반적으로 사용된다. 이는 프록시 객체의 상태에 의존하지 않거나 새로운 상태를 추가하지 않는 트랜잭션 어드바이저와 같은 일반적인 어드바이스에 적합하다. 이러한 어드바이스는 메서드와 인수에 대해 작동한다.
인스턴스별 어드바이스는 믹스인을 지원하기 위해 사용된다. 이 경우 어드바이스는 프록시 객체에 상태를 추가한다.
동일한 AOP 프록시에서 공유 어드바이스와 인스턴스벌 어드바이스를 혼합해서 사용할 수 있다.
[ ▷ Advice Types in Spring ]
스프링은 여러 가지 어드바이스 타입을 제공하며 임의의 어드바이스 타입을 지원하도록 확장할 수 있다.
[ ▷ Interception Around Advice ]
스프링에서 가장 기본적인 어드바이스 타입은 Interception Around Advice이다. 스프링은 메서드 인터셉션을 사용하는 around 어드바이스를 위한 AOP Alliance 인터페이스를 준수한다. around 어드바이스를 구현하는 클래스는 아래의 인터페이스를 구현해야 한다.
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke() 메서드에 전달되는 MethodInvocation 인수는 호출된 메서드, 대상 조인 포인트, AOP 프록시 및 메서드 인수를 제공한다. invoke() 메서드는 조인 포인트의 결과, 즉 리턴 값을 반환해야 한다.
public class DebugInterceptor implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
System.out.println("Invocation returned");
return rval;
}
}
MethodInvocation의 proceed() 메서드는 인터셉터 체인을 통해 조인 포인트로 진행된다. 대부분의 인터셉터는 이 메서드를 호출하고 반환 값을 반환한다. 그러나 MethodInterceptor는 다른 값을 반환하거나 proceed 메서드를 호출하는 대신 예외를 던질 수 있다. 하지만 특별한 이유가 없는 한, 이러한 동작은 하지 않는 것이 좋다.
MethodInterceptor 구현은 다른 AOP Alliance 준수 AOP 구현과의 상호운용성을 제공한다.
다른 어드바이스 타입들은 스프링 고유의 방식으로 일반적인 AOP 개념을 구현한다. 가장 특정한 어드바이스 타입을 사용하는 것이 이점이 있지마느 다른 AOP 프레임워크에서 이 애스텍트를 실행한 가능성이 있다면 MethodInterceptor around 어드바이스를 사용하는 것이 좋다. 포인트컷은 프레임워크 간에 상호운용되지 않으며, AOP Alliance는 현재 포인트컷 인터페이스를 정의하지 않는다.
[ ▷ Before Advice ]
더 단순한 어드바이스 타입은 Before Advice이다. 이 어드바이스는 메서드에 진입하기 전에 호출되므로 MethodInvocation 객체가 필요하지 않다. Before Advice의 장점은 proceed() 메서드를 호출할 필요가 없으며, 따라서 인터셉터 체인을 진행하지 못하게 될 가능성이 없다는 것이다.
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object\[\] args, Object target) throws Throwable;
}
스프링의 API 디자인은 필드 Before Advice도 허용하지만, 필드 인터셉션에 적용되는 일반적인 객체들이 있어 스프링이 이를 구현할 가능성이 낮다.
반환 타입은 void이다. Before Advice는 조인 포인트가 실행되기 전에 사용자 정의 동작을 삽입할 수 있지만, 반환 값을 변경할 수는 없다. Before Advice가 예외를 던지면 인터셉터 체인의 추가 실행이 중단된다. 예외는 인터셉터 체인을 통해 전파된다. 예외가 체크되지 않거나 호출된 메서드의 시그니처에 포함된 경우, 클라이언트에 직접 전달된다. 그렇지 않으면 AOP 프록시에 의해 체크되지 않은 예외로 래핑된다.
▼ 메서드 호출 횟수를 세는 Before Advice의 예시
public class CountingBeforeAdvice implements MethodBeforeAdvice {
private int count;
public void before(Method m, Object[] args, Object target) throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
Before Advice는 모든 포인트컷과 함께 사용할 수 있다.
[ ▷ Throws Advice ]
Throws Advice는 조인 포인트가 예외를 던졌을 때 호출된다. 스프링은 형식화된 Throws Advice를 제공한다. org.springframework.aop.ThrowsAdvice 인터페이스에는 메서드가 없다. 이 인터페이스는 주어진 객체가 하나 이상의 형식화된 Throws Advice 메서드를 구현한다는 것을 식별하는 태그 인터페이스이다.
afterThrowing(\[Method, args, target\], subclassOfThrowable)
마지막 인수만 필수이다. 어드바이스 메서드는 메서드 및 인수에 관심이 있는지 여부에 따라 하나 또는 네 개의 인수를 가질 수 있다.
▼ RemoteException이 발생했을 때 (하위 클래스 포함) 호출된다.
public class RemoteThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
}
이전의 어드바이스아 달리, 다음 예시는 네 개의 인수를 선언하여 호출된 메서드, 메서드 인수 및 대상 객체에 접근할 수 있다.
▼ ServletException이 발생했을 때 호출된다.
public class ServletThrowsAdviceWithArguments implements ThrowsAdvice {
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
마지막 예시는 RemoteException과 ServletException 모두를 처리하는 메서드를 단일 클래스에 결합하여 사용하는 방법을 보여준다. 여러 개의 Throws Advice 메서드를 하나의 클래스에 결합할 수 있다.
public static class CombinedThrowsAdvice implements ThrowsAdvice {
public void afterThrowing(RemoteException ex) throws Throwable {
// Do something with remote exception
}
public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) {
// Do something with all arguments
}
}
Throws Advice 메서드가 예외를 던지면 원래의 예외를 덮어쓴다. (사용자에게 전달되는 예외가 변경된다). 덮어쓰는 예외는 일반적으로 모든 메서드 시그니처와 호환되는 RuntimeException이다. 그러나 Throws Advice 메서드가 체크된 예외를 던지는 경우, 대상 메서드의 선언된 예외와 일치해야 하며, 따라서 특정 대상 메서드 시그니처에 어느 정도 결합된다.
대상 메서드의 시그니처와 호환되지 않는 선언되지 않은 체크된 예외를 던지면 안된다. Throws Advice는 모든 포인트컷과 함께 사용할 수 있다.
[ ▷ After Returning Advice ]
스프링에서 After Returning Advice는 org.springframework.aop.AfterReturningAdvice를 구현해야 한다.
public interface AfterReturningAdvice extends Advice {
void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable;
}
After Returning Advice는 리턴 값 (수정 불가능), 호출된 메서드, 메서드 아규먼트 및 타겟에 접근할 수 있다.
▼ 예외가 발생하지 않은 모든 성공적인 After Returning Advice 메서드 호출
public class CountingAfterReturningAdvice implements AfterReturningAdvice {
private int count;
public void afterReturning(Object returnValue, Method m, Object[] args, Object target)
throws Throwable {
++count;
}
public int getCount() {
return count;
}
}
이 어드바이스는 실행 경로를 변경하지 않는다. 예외를 던지면 리턴 값 대신 인터셉터 체인을 통해 전달된다. After Returning Advice는 모든 포인트컷과 함께 사용할 수 있다.
[ ▷ Introduction Advice의 주요 개념 ]
스프링은 Introduction Advice를 Interception Advice의 특별한 유형으로 처리한다.
AOP (Aspect-Oriented Programming)의 Introduction Advice는 특정 클래스나 객체에 새로운 메서드나 필드 (속성)을 추가하기 위한 기능이다. 이는 AOP의 주요 기능 중 하나로, 기존의 코드에 수정 없이 새로운 기능을 주입할 수 있게 해준다.
이를 통해 코드의 재사용성을 높이고, 중복을 줄이며, 특정 관심사를 모듈화하는데 큰 도움이 된다.
1. 관심사의 분리 (Separation of Concerns)
- AOP의 핵심 목표 중 하나는 코드에서 서로 다른 관심사를 분리하는 것이다. Introduction Advice는 이러한 관심사 중에서 특정 클래스에 새로운 기능을 추가하는 역할을 한다. 예를 들어, 특정 객체가 특정 인터페이스를 구현하도록 만들거나, 새로운 속성을 추가할 수 있다.
2. 타겟 클래스
- Introduction Advice가 적용될 클래스나 인터페이스를 지정한다. 이 클래스는 직접 수정되지 않으며, AOP 프레임워크가 실행 중에 이 클래스를 확장하여 새로운 메서드나 속성을 추가한다.
3. 인터페이스 (Implementing an Interface)
- Introduction Advice는 주로 새로운 인터페이스를 구현하는데 사용된다. 예를 들어, 클래스가 특정 인터페이스를 구현하지 않더라도, Introduction Advice를 통해 해당 인터페이스를 구현하도록 강제할 수 있다. 이를 통해, 런타임에 새로운 기능을 동적으로 추가할 수 있다.
4. AOP 프레임워크의 역할
- AOP 프레임워크 (스프링 AOP, AspectJ 등)는 Introduction Advice를 사용하여 기존 클래스에 새로운 인터페이스와 메서드를 추가하는 작업을 수행한다. 이는 주로 프록시 패턴을 사용하여 구현되며, 클라이언트 코드에서는 변경된 내용을 인지하지 못한 채 수정된 기능을 사용할 수 있다.
예를 들어, 특정 객체가 Auditable라는 인터페이스를 구현하지 않았지만, 해당 객체에 감사 (auditing) 기능을 추가하고 싶을 때, Introduction Advice를 사용해 해당 객체가 Auditable 인터페이스를 구현하도록 만들 수 있다. 이렇게 하면 해당 객체에 감사 관련 메서드가 추가되고, 이를 통해 감사 기능을 사용할 수 있다.
- 유연성 : 코드의 변경 없이 런타임에 기능을 추가할 수 있다.
- 모듈화 : 특정 기능을 별도의 모듈로 분리하여 관리할 수 있다.
- 재사용성 : 여러 클래스에 동일한 기능을 쉽게 적용할 수 있다.
AOP의 Introduction Advice는 코드의 유연성과 모듈성을 높이는데 매우 유용한 도구이다. 이를 통해 기존 코드의 구조를 유지하면서 새로운 기능을 동적으로 추가할 수 있어, 유지보수성과 확장성이 크게 향상된다.
Introduction은 IntroductionAdvisor와 IntroductionInterceptor를 필요로 하며, 다음 인터페이스를 구현해야 한다.
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
스프링 AOP의 맥락에서 Introduction은 기존 클래스를 수정하지 않고 새로운 메서드가 인터페이스를 추가할 수 있게 해주는 메커니즘이다. 이는 특히 횡단 관심사 (cross-cutting concerns)나 추가적인 동작을 동적으로 추가하는데 유용하다.
Introduction을 구현하기 위해서는 IntroductionAdvisor와 IntroductionInterceptor라는 두 가지 주요 구성 요소가 필요하다.
[ ▷ IntroductionAdvisor ]
IntroductionAdvisor는 Introduction를 관리하고 적용하는데 중요한 역할을 한다.
- 도입할 인터페이스 정의 : IntroductionAdvisor는 타겟 객체에 추가할 인터페이스를 지정한다. 이는 getInterfaces() 메서드를 통해 이루어지며, 이 메서드는 도입될 인터페이스들을 반환한다.
- 타겟 클래스 필터링 : IntroductionAdvisor는 getClassFilter() 메서드를 통해 어떤 클래스에 Introduction을 적용할지 결정한다. 이를 통해 특정 기준에 따라 선택적으로 Introduction을 적용할 수 있다.
- 인터페이스 유횽성 검사 : validateInterfaces() 메서드는 IntroductionInterceptor가 도입할 인터페이스를 구현할 수 있는지를 확인한다. 이 유효성 검사는 설정 오류를 방지하고 Introduction이 예상대로 작동할 수 있도록 도와준다.
[ ▷ IntroductionInterceptor ]
IntroductionInterceptor는 도입된 인터페이스의 실제 구현을 담당한다. 이 인터셉터는 도입된 메서드 호출을 처리하는 역할을 한다.
- 메서드 호출 가로채기 : 도입된 인터페이스의 메서드가 호출되면, IntroductionInterceptor가 그 호출을 가로챈다. 인터셉터의 ivoke() 메서드는 이러한 메서드 호출을 처리한다. 만약 메서드가 도입된 인터페이스에 속해 있다면, 인터셉터가 이 호출을 적절히 처리한다.
- 도입된 인터페이스 구현 : IntroductionInterceptor 또는 그 delegate는 도입된 인터페이스의 메서드를 실제로 구현한다. 도입된 인터페이스의 메서드가 호출될 때, 인터셉터가 이를 처리하고 필요한 로직을 수행한다.
- 원하지 않는 인터페이스 억제 : suppressInterface(Class intf) 메서드를 사용하여 delegate가 구현했지만 AOP 프록시에 도입되지 말아야할 인터페이스를 억제할 수 있다. 이는 노출할 인터페이스를 제어하는데 유용하다.
[ ▷ Introduction에서 이들의 역할 ]
- IntroductionInterceptor : 도입할 인터페이스와 이를 적용할 대상을 지정하고, 도입이 올바르게 구성되었는지를 확인하여 Introduction 과정을 관리한다.
- IntroductionInterceptor : 도입된 인터페이스의 구현을 제공하고, 그 인터페이스의 메서드 호출을 가로채어 처리한다. 도입된 기능이 대상 객체에서 올바르게 실행되도록 보장한다.
이 두 구성 요소는 함께 스프링 AOP가 기존 코드베이스를 수정하지 않고도 객체의 기능을 동적으로 확장할 수 있게 해준다.
AOP Alliance의 MethodInterceptor 인터페이스 상속된 invoke() 메서드는 Introduction을 구현해야 한다. 즉, 호출된 메서드가 도입된 인터페이스에 있는 경우, Introduction Interceptor는 메서드 호출을 처리해야 하며 proceed()를 호출할 수 없다.
Introduction Advice는 클래스 레벨에서만 적용되므로 메서드 레벨에서는 포인트컷과 함께 사용할 수 없다. Introduction Advisor과 함께 사용할 수 있다.
public interface IntroductionAdvisor extends Advisor, IntroductionInfo {
ClassFilter getClassFilter();
void validateInterfaces() throws IllegalArgumentException;
}
public interface IntroductionInfo {
Class<?>[] getInterfaces();
}
MethodMatcher가 없으므로 Introduction Advice와 관련된 포인트컷은 없다. 클래스 필터링만 논리적이다.
getInterfaces() 메서드는 이 어드바이저에 의해 도입된 인터페이스를 리턴한다.
validateInterfaces() 메서드는 설정된 IntroductionInterceptor가 도입된 인터페이스를 구현할 수 있는지 여부를 확인하기 위해 내부적으로 사용된다.
▼ 스프링 테스트 스위트의 예시
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
이는 믹스인 (mixin)을 나타낸다. 어드바이스된 객체 (타겟 클래스)를 Lockable로 캐스팅하고, 객체의 타입에 관계없이 lock 및 unlock 메서드를 호출할 수 있어야 하고, lock() 메서드를 호출하면 모든 setter 메서드가 LockedException을 던지도록 하려고 한다.
따라서 객체가 이를 전혀 인지하지 못한 상태에서 객체를 불변으로 만드는 기능을 추가할 수 있다. 이는 AOP의 좋은 예시이다.
먼저 무거운 작업을 수행할 IntroductionInterceptor가 필요하다. 이 경우 org.springframework.aop.support.DelegatingIntroductionInterceptor 클래스를 확장한다. IntroductionInterceptor를 직접 구현할 수 있지만, 대부분의 경우엔 DelegatingIntroductionIntercepto를 사용하는 것이 가장 좋다.
DelegatingIntroductionInterceptor는 도입을 실제 도입된 인터페이스의 구현에 위임하도록 설계되어 있으며, 이를 위해 인터셉션 (interception)을 사용하는 것을 숨긴다.
생성자 아규먼트를 사용하여 delegate를 임의의 객체로 설정할 수 있다. 기본 delegate (아규먼트가 없는 생성자를 사용하는 경우)는 this이다. 따라서 아래 예시의 delegate는 DelegatingIntroductionInterceptor의 LockMixin 하위 클래스이다. 기본적으로 delegate가 this-LockMixin 인스턴스일때, DelegatingIntroductionInterceptor 인스턴스는 delegate가 구현한 모든 인터페이스 (IntroductionInterceptor 제외)를 찾아 도입할 수 있다.
LockMixin과 같은 하위 클래스는 suppressInterface(Class intf) 메서드를 호출하여 노출되지 말아야할 인터페이스를 억제할 수 있다. 그러나 IntroductionInterceptor가 지원할 수 있는 인터페이스가 얼마나 많든지 간에, 사용된 IntroductionAdvisor가 실제로 노출되는 인터페이스를 제어한다. 도입된 인터페이스는 대상이 동일한 인터페이스를 구현하고 있는 경우 이를 숨긴다.
따라서 LockMixin은 DelegatingIntroductionInterceptor를 확장하고 Lockable을 자체적으로 구현한다. 슈퍼 클래스는 자동으로 Lockable이 도입될 수 있음을 감지하고 이를 명시할 필요가 없다. 이 방법을 통해 여러 인터페이스를 도입할 수 있다.
locked 인스턴스 변수 사용에 주목해야 한다. 이는 대상 객체에 저장된 상태에 추가적인 사앹를 효과적으로 추가한다.
public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {
private boolean locked;
public void lock() {
this.locked = true;
}
public void unlock() {
this.locked = false;
}
public boolean locked() {
return this.locked;
}
public Object invoke(MethodInvocation invocation) throws Throwable {
if (locked() && invocation.getMethod().getName().indexOf("set") == 0) {
throw new LockedException();
}
return super.invoke(invocation);
}
}
invoke() 메서드를 재정의할 필요가 없는 경우가 많다. DelegatingIntroductionInterceptor 메서드의 구현은 메서드가 도입된 경우 delegate 메서드를 호출하고, 그렇지 않은 경우 조인 포인트로 진행되도록 호출한다. 하지만 지금 예제에서는 setter 메서드는 잠금 모드에서 호출될 수 없도록 추가 검사를 해야한다.
필요한 introduction은 별개의 LockMixin 인스턴스를 보유하고 introduction interceptor에 대한 참조를 가져올 수 있다. (이는 프로토타입으로 정의된다). 이 경우 LockMixin에 대한 구성을 필요 없으므로 new를 사용하여 생성한다.
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
이 어드바이저는 설정이 필요하지 않으므로 매우 단하게 적용할 수 있다. (그러나 IntroductionInterceptor를 IntroductionAdvisor 없이 사용할 수 없다). 소개된 바와 같이, 이 어드바이저는 상태를 가지므로 인스턴스별로 관리해야 한다. LockMixinAdvisor와 따라서 LockMixin의 다른 인스턴스가 각 어드바이스된 객체에 필요하다. 어드바이저는 어드바이스된 객체 상태의 일부로 구성된다.
이 어드바이저는 Advised.addAdvisor() 메서드를 사용하여 프로그램ㅇ 방식으로 적용할 수 있으며 (권장하는 방식), XML 구성에서 다른 어드바이저처럼 적용할 수 있다. 아래에서 논의할 모든 프록시 생성 옵션, 즉 "자동 프록시 생성기"는 도입 및 상태가 있는 믹스인을 올바르게 처리한다.