Skip to content

Latest commit

 

History

History
156 lines (116 loc) · 11.2 KB

File metadata and controls

156 lines (116 loc) · 11.2 KB

01. 핵심 개념 — Advice · Pointcut · Advisor

무엇을 / 왜

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

Joinpoint / MethodInvocation — 가로챈 호출의 표현

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)

Advice — "무엇을" 할 것인가

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 — "어디에" 적용할 것인가

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-arg matches만 호출하고 결과를 프록시 생성 시점에 확정·캐싱한다. 인자값과 무관하므로 매 호출마다 다시 검사하지 않는다.
  • isRuntime()true동적 매칭: 정적 매칭을 통과한 메서드라도, 호출마다 실제 인자(args)를 보고 3-arg matches로 최종 판정한다. 인자값에 따라 적용 여부가 달라지는 포인트컷(예: 특정 파라미터 값일 때만)을 지원하기 위함이다.

주의: MethodMatcher 구현체는 equals/hashCode/toString을 제대로 구현해야 한다. CGLIB 프록시가 생성한 체인을 캐싱할 때 키로 쓰이기 때문이다(소스 javadoc의 WARNING).

Advisor — Pointcut + Advice 묶음

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 — 대상 객체를 공급하는 인터페이스

프록시가 최종적으로 호출을 위임할 실제 대상은 직접 참조가 아니라 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 재확인)
   └──────────────────────────────────────────────┘
  1. 호출자는 프록시를 진짜 객체로 알고 메서드를 부른다.
  2. 프록시는 Advisor 목록을 훑어 이 메서드에 적용될 어드바이스만 골라 인터셉터 체인을 만든다(정적 매칭).
  3. 체인을 proceed()로 연쇄 실행하고, 동적 매칭이 필요한 인터셉터는 호출 시점에 인자까지 보고 다시 판정한다.
  4. 체인 끝에서 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단계로 나눠 성능을 챙긴다. 다음 챕터에서는 이 추상화가 실제 프록시 객체로 어떻게 구체화되는지 본다.