Spring AOP를 코드 레벨로 이해하려면 먼저 용어 모델을 정확히 잡아야 한다. AOP는 "어떤 지점(where)에 어떤 동작(what)을 끼워 넣는다"는 단순한 아이디어지만, Spring은 이를 여러 작은 인터페이스로 쪼개서 조합 가능하게 만들었다. 이 챕터는 그 인터페이스들이 각각 무엇을 책임지고, 어떻게 맞물리는지 설명한다. 뒤의 모든 챕터(프록시·체인·자동화·AspectJ)는 결국 이 타입들을 만들고 엮는 일이다.
Spring AOP의 두 가지 출발점을 먼저 못박자.
- 조인포인트는 "메서드 실행"뿐이다. AspectJ는 필드 접근·생성자 등도 조인포인트로 보지만, Spring AOP는 프록시 기반이라 메서드 호출만 가로챈다.
- 모든 어드바이스는 결국 around 인터셉션으로 수렴한다. before/after 같은 종류도 내부에서는
MethodInterceptor로 어댑팅되어 하나의 체인을 흐른다(챕터 6 어댑터 참조).
org.aopalliance.intercept org.springframework.aop
(벤더 중립 표준 API) (Spring 확장)
Joinpoint Advisor ─────────┐
│ proceed() : Object │ getAdvice() │ "어디에 무엇을"
│ getThis() │ │ 묶는 단위
▼ ├── PointcutAdvisor
Invocation │ getPointcut()
│ getArguments() └── IntroductionAdvisor
▼ getClassFilter()
MethodInvocation Pointcut ── "어디에"
│ getMethod() ├── ClassFilter matches(Class)
▼ └── MethodMatcher matches(Method,Class[,args])
(실행 단위) isRuntime()
Advice (org.aopalliance.aop) ── "무엇을"
MethodInterceptor ├── MethodInterceptor (around)
invoke(MethodInvocation) : Object ├── MethodBeforeAdvice
├── AfterReturningAdvice
└── ThrowsAdvice / AfterAdvice
org.aopalliance.intercept.Joinpoint는 "지금 가로챈 한 번의 호출"을 나타낸다. 핵심 메서드는 proceed() 하나다 — 체인의 다음 단계로 진행하라는 뜻이다.
public interface Joinpoint {
@Nullable Object proceed() throws Throwable; // 다음 인터셉터(또는 대상)로
@Nullable Object getThis(); // 대상 객체
AccessibleObject getStaticPart(); // 정적 부분(=Method)
}MethodInvocation은 여기에 getMethod()를 더한 서브타입으로, 메서드 호출이라는 구체적 조인포인트를 가리킨다. 어드바이스를 작성할 때 받는 인자가 바로 이 MethodInvocation이며, proceed()를 부를지 말지로 흐름을 제어한다. (구현체는 챕터 3의 ReflectiveMethodInvocation)
org.aopalliance.aop.Advice는 메서드가 하나도 없는 마커 인터페이스다. 부가 동작의 "종류"를 표현하는 태그일 뿐이고, 실제 시그니처는 하위 인터페이스가 정의한다.
MethodInterceptor(표준):invoke(MethodInvocation)— 가장 일반적인 around.proceed()호출 전후로 무엇이든 할 수 있다.MethodBeforeAdvice(Spring):before(Method, args, target)— 호출 전에만.AfterReturningAdvice(Spring):afterReturning(returnValue, ...)— 정상 반환 후.ThrowsAdvice/AfterAdvice(Spring): 예외/finally 시점.
Spring 고유 어드바이스 타입들은 그 자체로는 인터셉터가 아니므로, 체인에 합류할 때 어댑터가 MethodInterceptor로 감싼다(챕터 6). 즉 사용자 편의를 위한 여러 표현이 있지만 실행 모델은 around 하나다.
Pointcut은 두 조각의 합성이다.
public interface Pointcut {
ClassFilter getClassFilter(); // 클래스 수준 필터
MethodMatcher getMethodMatcher(); // 메서드 수준 필터
Pointcut TRUE = TruePointcut.INSTANCE; // 항상 매칭
}ClassFilter.matches(Class)로 먼저 클래스를 거르고, 통과한 클래스의 각 메서드에 MethodMatcher를 적용한다. 이렇게 두 단계로 나눈 이유는 클래스 매칭은 프록시 생성 시 한 번, 메서드 매칭은 메서드별로 평가해 성능을 최적화하기 위해서다.
MethodMatcher에는 정적/동적 두 모드가 있다. 이것이 Spring AOP 성능의 핵심 설계다.
public interface MethodMatcher {
boolean matches(Method method, Class<?> targetClass); // 2-arg: 정적
boolean isRuntime(); // 동적이 필요한가?
boolean matches(Method method, Class<?> targetClass, Object... args); // 3-arg: 동적
}isRuntime()이false면 정적 매칭: 2-argmatches만 호출하고 결과를 프록시 생성 시점에 확정·캐싱한다. 인자값과 무관하므로 매 호출마다 다시 검사하지 않는다.isRuntime()이true면 동적 매칭: 정적 매칭을 통과한 메서드라도, 호출마다 실제 인자(args)를 보고 3-argmatches로 최종 판정한다. 인자값에 따라 적용 여부가 달라지는 포인트컷(예: 특정 파라미터 값일 때만)을 지원하기 위함이다.
주의:
MethodMatcher구현체는equals/hashCode/toString을 제대로 구현해야 한다. CGLIB 프록시가 생성한 체인을 캐싱할 때 키로 쓰이기 때문이다(소스 javadoc의 WARNING).
Advisor는 "어디에(Pointcut)" + "무엇을(Advice)" 을 하나로 묶은, AOP 설정의 최소 단위다.
public interface Advisor {
Advice getAdvice();
default boolean isPerInstance() { return true; }
}가장 흔한 구현은 PointcutAdvisor로, getPointcut()을 추가로 노출한다. 또 다른 갈래인 IntroductionAdvisor는 메서드가 아니라 새 인터페이스를 대상에 도입(introduction/mixin) 하는 특수한 경우로, getClassFilter()만 가진다(메서드 단위가 아니라 타입 단위로 적용되기 때문).
Spring 내부에서 트랜잭션·캐시·@AspectJ 어드바이스는 모두 결국 Advisor 객체로 변환되어 한 줄에 세워진다. 프레임워크가 다루는 통화 단위가 바로 Advisor다.
프록시가 최종적으로 호출을 위임할 실제 대상은 직접 참조가 아니라 TargetSource를 거쳐 얻는다.
public interface TargetSource extends TargetClassAware {
@Nullable Class<?> getTargetClass();
default boolean isStatic() { return false; }
@Nullable Object getTarget() throws Exception; // 호출 직전에 대상 획득
default void releaseTarget(Object target) throws Exception {}
}이 한 겹의 간접 참조 덕분에 "대상이 매번 같은 싱글톤이냐, 풀에서 빌려오느냐, 스레드마다 다르냐, 핫스왑되느냐"를 프록시 로직과 분리할 수 있다(챕터 6). 대부분의 경우 SingletonTargetSource가 대상을 고정 보관한다.
이 타입들이 실제로 어떻게 협력하는지 큰 그림으로 보자. (세부는 챕터 3)
client.someMethod(arg)
│
▼
┌──────────────┐ 1) TargetSource.getTarget() 으로 대상 확보
│ Proxy │ 2) Advisor[] 중 이 메서드에 매칭되는 것만 추림
│ (JDK/CGLIB) │ - ClassFilter.matches(targetClass)
└──────┬───────┘ - MethodMatcher.matches(method, class) [정적]
│ 3) 매칭된 Advice들을 MethodInterceptor 체인으로
▼
┌──────────────────────────────────────────────┐
│ MethodInterceptor 체인 │
│ ┌────────┐ proceed() ┌────────┐ proceed() │
│ │ around │ ──────────▶ │ before │ ──────────▶ 대상.someMethod(arg)
│ └────────┘ └────────┘ (동적이면 호출 시 3-arg matches 재확인)
└──────────────────────────────────────────────┘
- 호출자는 프록시를 진짜 객체로 알고 메서드를 부른다.
- 프록시는
Advisor목록을 훑어 이 메서드에 적용될 어드바이스만 골라 인터셉터 체인을 만든다(정적 매칭). - 체인을
proceed()로 연쇄 실행하고, 동적 매칭이 필요한 인터셉터는 호출 시점에 인자까지 보고 다시 판정한다. - 체인 끝에서
TargetSource가 준 대상의 실제 메서드를 리플렉션으로 호출한다.
- 관심사 분리의 극단: "지점(Pointcut)"·"동작(Advice)"·"묶음(Advisor)"·"대상 공급(TargetSource)"·"실행(MethodInterceptor)"을 모두 별도 인터페이스로 쪼갰다. 덕분에 각 축을 독립적으로 교체·조합할 수 있다(예: 같은 Advice를 정규식 포인트컷과 AspectJ 포인트컷에 재사용).
- 표준 채택: 인터셉션 모델은
org.aopalliance표준을 그대로 흡수했다.MethodInterceptor를 구현하면 Spring 외 다른 AOP 컨테이너와도 호환된다. - 정적/동적 매칭 분리는 성능 최적화의 핵심 훅이다. 대부분의 포인트컷은
isRuntime()=false로 만들어 호출당 오버헤드를 0에 가깝게 유지한다. - 구체 구현은
support패키지에:NameMatchMethodPointcut(이름 매칭),JdkRegexpMethodPointcut(정규식),ComposablePointcut(union/intersection 조합),DefaultPointcutAdvisor(범용 묶음) 등이 이 인터페이스들의 즉시 사용 가능한 구현을 제공한다.Pointcuts·MethodMatchers·ClassFilters유틸은 합성 연산을 돕는다.
Spring AOP의 어휘는 Joinpoint(가로챈 호출) · Advice(무엇을) · Pointcut(어디에=ClassFilter+MethodMatcher) · Advisor(둘의 묶음) · TargetSource(대상 공급) 다섯 축으로 이뤄진다. 조인포인트는 메서드 실행으로 한정되고, 모든 어드바이스는 결국 MethodInterceptor 체인으로 수렴하며, 매칭은 정적/동적 2단계로 나눠 성능을 챙긴다. 다음 챕터에서는 이 추상화가 실제 프록시 객체로 어떻게 구체화되는지 본다.