Advice API in Spring
Spring AOP에서 어드바이스(Advice)를 다루는 방법을 살펴보겠습니다.
Advice Lifecycles
각 어드바이스는 Spring 빈(bean)입니다. 어드바이스 인스턴스는 모든 타겟 객체에 공유될 수 있으며, 각 타겟 객체에 고유하게 존재할 수도 있습니다. 이는 클래스별 혹은 인스턴스별 어드바이스에 해당합니다.
클래스별 어드바이스는 일반적으로 사용됩니다. 이는 프록시 객체의 상태에 의존하지 않거나 새로운 상태를 추가하지 않는 트랜잭션 어드바이저와 같은 일반적인 어드바이스에 적합합니다. 이러한 어드바이스는 메서드와 아규먼트에 대해 작동합니다.
인스턴스별 어드바이스는 믹스인을 지원하기 위해 사용됩니다. 이 경우 어드바이스는 프록시 객체에 상태를 추가합니다.
동일한 AOP 프록시에서 공유 어드바이스와 인스턴스별 어드바이스를 혼합해서 사용할 수 있습니다.
Advice Types in Spring
Spring은 여러 가지 어드바이스 타입을 제공하며 임의의 어드바이스 타입을 지원하도록 확장할 수 있습니다. 이 섹션에서는 기본 개념과 표준 어드바이스 타입에 대해 설명합니다.
Interception Around Advice
Spring에서 가장 기본적인 어드바이스 타입은 Interception Around Advice입니다.
Spring은 메서드 인터셉션을 사용하는 around 어드바이스를 위한 AOP Alliance 인터페이스를 준수합니다. around 어드바이스를 구현하는 클래스는 다음 인터페이스를 구현해야 합니다:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
invoke() 메서드에 전달되는 MethodInvocation 아규먼트는 호출된 메서드, 타겟 조인 포인트, AOP 프록시 및 메서드 아규먼트를 제공합니다. invoke() 메서드는 조인 포인트의 결과, 즉 리턴 값을 리턴해야 합니다.
다음은 간단한 MethodInterceptor 구현 예시입니다:
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 구현과의 상호운용성을 제공합니다. 이 섹션에서 다루는 다른 어드바이스 타입들은 Spring 고유의 방식으로 일반적인 AOP 개념을 구현합니다. 가장 특정한 어드바이스 타입을 사용하는 것이 이점이 있긴 하지만, 다른 AOP 프레임워크에서 이 애스펙트를 실행할 가능성이 있다면 MethodInterceptor around 어드바이스를 사용하는 것이 좋습니다. 포인트컷은 프레임워크 간에 상호운용되지 않으며, AOP Alliance는 현재 포인트컷 인터페이스를 정의하지 않습니다.
Before Advice
더 단순한 어드바이스 타입은 Before Advice입니다. 이 어드바이스는 메서드에 진입하기 전에 호출되므로 MethodInvocation 객체가 필요하지 않습니다.
Before Advice의 주요 장점은 proceed() 메서드를 호출할 필요가 없으며, 따라서 인터셉터 체인을 진행하지 못하게 될 가능성이 없다는 것입니다.
다음은 MethodBeforeAdvice 인터페이스입니다:
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method m, Object\[\] args, Object target) throws Throwable;
}
(Spring의 API 디자인은 필드 Before Advice도 허용하지만, 필드 인터셉션에 적용되는 일반적인 객체들이 있어 Spring이 이를 구현할 가능성은 낮습니다.)
리턴 타입이 void인 것을 주목하세요. Before Advice는 조인 포인트가 실행되기 전에 사용자 정의 동작을 삽입할 수 있지만, 리턴 값을 변경할 수는 없습니다. Before Advice가 예외를 던지면 인터셉터 체인의 추가 실행이 중단됩니다. 예외는 인터셉터 체인을 통해 전파됩니다. 예외가 체크되지 않거나 호출된 메서드의 시그니처에 포함된 경우, 클라이언트에 직접 전달됩니다. 그렇지 않으면 AOP 프록시에 의해 체크되지 않은 예외로 래핑됩니다.
다음은 Spring에서 메서드 호출 횟수를 세는 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는 조인 포인트가 예외를 던졌을 때 호출됩니다. Spring은 형식화된 Throws Advice를 제공합니다. org.springframework.aop.ThrowsAdvice 인터페이스에는 메서드가 없다는 점에 유의하세요. 이 인터페이스는 주어진 객체가 하나 이상의 형식화된 Throws Advice 메서드를 구현한다는 것을 식별하는 태그 인터페이스입니다. 이러한 메서드는 다음과 같은 형식이어야 합니다:
afterThrowing(\[Method, args, target\], subclassOfThrowable)
마지막 아규먼트만 필수입니다. 어드바이스 메서드는 메서드 및 아규먼트에 관심이 있는지 여부에 따라 하나 또는 네 개의 아규먼트를 가질 수 있습니다. 다음 목록은 Throws Advice의 예시 클래스입니다.
다음 어드바이스는 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
Spring에서 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
Spring은 Introduction Advice를 Interception Advice의 특별한 유형으로 처리합니다.
AOP(Aspect-Oriented Programming)의 "Introduction Advice"는 특정 클래스나 객체에 새로운 메서드나 필드(속성)를 추가하기 위한 기능입니다. 이는 AOP의 주요 기능 중 하나로, 기존의 코드에 수정 없이 새로운 기능을 주입할 수 있게 해줍니다. 이를 통해 코드의 재사용성을 높이고, 중복을 줄이며, 특정 관심사를 모듈화하는 데 큰 도움이 됩니다.
Introduction Advice의 주요 개념
- 관심사의 분리(Separation of Concerns):
- AOP의 핵심 목표 중 하나는 코드에서 서로 다른 관심사를 분리하는 것입니다. Introduction Advice는 이러한 관심사 중에서 특정 클래스에 새로운 기능을 추가하는 역할을 합니다. 예를 들어, 특정 객체가 특정 인터페이스를 구현하도록 만들거나, 새로운 속성을 추가할 수 있습니다.
- 타겟 클래스(Target Class):
- Introduction Advice가 적용될 클래스나 인터페이스를 지정합니다. 이 클래스는 직접 수정되지 않으며, AOP 프레임워크가 실행 중에 이 클래스를 확장하여 새로운 메서드나 속성을 추가합니다.
- 인터페이스 구현(Implementing an Interface):
- Introduction Advice는 주로 새로운 인터페이스를 구현하는 데 사용됩니다. 예를 들어, 클래스가 특정 인터페이스를 구현하지 않더라도, Introduction Advice를 통해 해당 인터페이스를 구현하도록 강제할 수 있습니다. 이를 통해, 런타임에 클래스에 새로운 기능을 동적으로 추가할 수 있습니다.
- AOP 프레임워크의 역할:
- AOP 프레임워크(Spring AOP, AspectJ 등)는 Introduction Advice를 사용하여 기존 클래스에 새로운 인터페이스와 메서드를 추가하는 작업을 수행합니다. 이는 주로 프록시 패턴을 사용하여 구현되며, 클라이언트 코드에서는 변경된 내용을 인지하지 못한 채 수정된 기능을 사용할 수 있습니다.
예시
예를 들어, 특정 객체가 Auditable이라는 인터페이스를 구현하지 않았지만, 해당 객체에 감사(auditing) 기능을 추가하고 싶을 때, Introduction Advice를 사용해 해당 객체가 Auditable 인터페이스를 구현하도록 만들 수 있습니다. 이렇게 하면 해당 객체에 감사 관련 메서드가 추가되고, 이를 통해 감사 기능을 사용할 수 있습니다.
장점
- 유연성: 코드의 변경 없이 런타임에 기능을 추가할 수 있습니다.
- 모듈화: 특정 기능을 별도의 모듈로 분리하여 관리할 수 있습니다.
- 재사용성: 여러 클래스에 동일한 기능을 쉽게 적용할 수 있습니다.
AOP의 Introduction Advice는 코드의 유연성과 모듈성을 높이는 데 매우 유용한 도구입니다. 이를 통해 기존 코드의 구조를 유지하면서도 새로운 기능을 동적으로 추가할 수 있어, 유지보수성과 확장성이 크게 향상됩니다.
Introduction은 IntroductionAdvisor와 IntroductionInterceptor를 필요로 하며, 다음 인터페이스를 구현해야 합니다:
public interface IntroductionInterceptor extends MethodInterceptor {
boolean implementsInterface(Class intf);
}
Spring AOP의 맥락에서 Introduction은 기존 클래스를 수정하지 않고 새로운 메서드나 인터페이스를 추가할 수 있게 해주는 메커니즘입니다. 이는 특히 횡단 관심사(cross-cutting concerns)나 추가적인 동작을 동적으로 추가하는 데 유용합니다. Introduction을 구현하기 위해서는 IntroductionAdvisor와 IntroductionInterceptor라는 두 가지 주요 구성 요소가 필요합니다. 이들의 역할을 다음과 같이 설명할 수 있습니다:
1. IntroductionAdvisor
IntroductionAdvisor는 Introduction을 관리하고 적용하는 데 중요한 역할을 합니다. 주요 역할은 다음과 같습니다:
- 도입할 인터페이스 정의: IntroductionAdvisor는 타겟 객체에 추가할 인터페이스를 지정합니다. 이는 getInterfaces() 메서드를 통해 이루어지며, 이 메서드는 도입될 인터페이스들을 반환합니다.
- 타겟 클래스 필터링: IntroductionAdvisor는 getClassFilter() 메서드를 통해 어떤 클래스에 Introduction을 적용할지 결정합니다. 이를 통해 특정 기준에 따라 선택적으로 Introduction을 적용할 수 있습니다.
- 인터페이스 유효성 검사: validateInterfaces() 메서드는 IntroductionInterceptor가 도입할 인터페이스를 구현할 수 있는지를 확인합니다. 이 유효성 검사는 설정 오류를 방지하고 Introduction이 예상대로 작동할 수 있도록 도와줍니다.
2. IntroductionInterceptor
IntroductionInterceptor는 도입된 인터페이스의 실제 구현을 담당합니다. 이 인터셉터는 도입된 메서드 호출을 처리하는 역할을 합니다. 주요 역할은 다음과 같습니다:
- 메서드 호출 가로채기: 도입된 인터페이스의 메서드가 호출되면, IntroductionInterceptor가 그 호출을 가로챕니다. 인터셉터의 invoke() 메서드는 이러한 메서드 호출을 처리합니다. 만약 메서드가 도입된 인터페이스에 속해 있다면, 인터셉터가 이 호출을 적절히 처리합니다.
- 도입된 인터페이스 구현: IntroductionInterceptor 또는 그 delegate는 도입된 인터페이스의 메서드를 실제로 구현합니다. 도입된 인터페이스의 메서드가 호출될 때, 인터셉터가 이를 처리하고 필요한 로직을 수행합니다.
- 원하지 않는 인터페이스 억제: suppressInterface(Class intf) 메서드를 사용하여 delegate가 구현했지만 AOP 프록시에 도입되지 말아야 할 인터페이스를 억제할 수 있습니다. 이는 노출할 인터페이스를 제어하는 데 유용합니다.
Introduction에서 이들의 역할 요약
- IntroductionAdvisor: 도입할 인터페이스와 이를 적용할 대상을 지정하고, 도입이 올바르게 구성되었는지 확인하여 Introduction 과정을 관리합니다.
- IntroductionInterceptor: 도입된 인터페이스의 구현을 제공하고, 그 인터페이스의 메서드 호출을 가로채어 처리합니다. 도입된 기능이 대상 객체에서 올바르게 실행되도록 보장합니다.
이 두 구성 요소는 함께 Spring AOP가 기존 코드베이스를 수정하지 않고도 객체의 기능을 동적으로 확장할 수 있게 해줍니다.
AOP Alliance의 MethodInterceptor 인터페이스에서 상속된 invoke() 메서드는 Introduction을 구현해야 합니다. 즉, 호출된 메서드가 도입된 인터페이스에 있는 경우, Introduction Interceptor는 메서드 호출을 처리해야 하며 proceed()를 호출할 수 없습니다.
Introduction Advice는 클래스 레벨에서만 적용되므로 메서드 레벨에서는 포인트컷과 함께 사용할 수 없습니다. Introduction Advisor와 함께 사용할 수 있습니다. 다음은 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가 도입된 인터페이스를 구현할 수 있는지 여부를 확인하기 위해 내부적으로 사용됩니다.
다음은 Spring 테스트 스위트의 예시로, 한 개 이상의 객체에 다음 인터페이스를 도입한다고 가정합니다:
public interface Lockable {
void lock();
void unlock();
boolean locked();
}
이는 믹스인(mixin)을 나타냅니다. 어드바이스된 객체를 Lockable로 캐스팅하고, 객체의 타입에 관계없이 lock 및 unlock 메서드를 호출할 수 있기를 원합니다. lock() 메서드를 호출하면 모든 setter 메서드가 LockedException을 던지도록 하고 싶습니다. 따라서 객체가 이를 전혀 인지하지 못한 상태에서 객체를 불변으로 만드는 기능을 추가할 수 있습니다. 이것은 AOP의 좋은 예입니다.
먼저, 무거운 작업을 수행할 IntroductionInterceptor가 필요합니다. 이 경우 org.springframework.aop.support.DelegatingIntroductionInterceptor 클래스를 확장합니다. IntroductionInterceptor를 직접 구현할 수 있지만, 대부분의 경우 DelegatingIntroductionInterceptor를 사용하는 것이 가장 좋습니다.
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 인스턴스 변수를 사용하는 것을 주목하세요. 이는 대상 객체에 저장된 상태에 추가적인 상태를 효과적으로 추가합니다.
다음은 LockMixin 클래스의 예시입니다:
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 인스턴스를 보유하고, 도입된 인터페이스(이 경우 Lockable만)를 지정하기만 하면 됩니다. 더 복잡한 예는 introduction interceptor에 대한 참조를 가져올 수 있습니다(이는 프로토타입으로 정의됩니다). 이 경우 LockMixin에 대한 구성은 필요 없으므로 new를 사용하여 생성합니다. 다음은 LockMixinAdvisor 클래스의 예입니다:
public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
public LockMixinAdvisor() {
super(new LockMixin(), Lockable.class);
}
}
이 어드바이저는 설정이 필요하지 않으므로 매우 간단하게 적용할 수 있습니다. (그러나 IntroductionInterceptor를 IntroductionAdvisor 없이 사용할 수 없습니다.) 소개된 바와 같이, 어드바이저는 상태를 가지므로 인스턴스별로 관리해야 합니다. LockMixinAdvisor와 따라서 LockMixin의 다른 인스턴스가 각 어드바이스된 객체에 필요합니다. 어드바이저는 어드바이스된 객체 상태의 일부로 구성됩니다.
이 어드바이저는 Advised.addAdvisor() 메서드를 사용하여 프로그래밍 방식으로 적용할 수 있으며(권장되는 방법), XML 구성에서 다른 어드바이저처럼 적용할 수 있습니다. 아래에서 논의할 모든 프록시 생성 옵션, 즉 "자동 프록시 생성기"는 도입 및 상태가 있는 믹스인을 올바르게 처리합니다.