Skip to content

Latest commit

 

History

History
139 lines (108 loc) · 10.8 KB

File metadata and controls

139 lines (108 loc) · 10.8 KB

01 · AspectJ 위빙 개요 — 왜 프록시 대신 위빙인가

무엇을 / 왜

Spring의 선언적 부가 기능(@Transactional, @Cacheable, @Async, @Configurable)을 적용하는 방법은 두 갈래다.

  • 프록시 모드(기본값): 런타임에 대상 빈을 감싸는 프록시 객체를 만들고, 프록시의 메서드가 호출될 때 어드바이스를 실행한다. spring-aop가 담당한다.
  • AspectJ 모드: 클래스의 바이트코드 자체에 어드바이스를 짜 넣는다. 프록시라는 중간 객체가 없다. 이 모듈(spring-aspects)이 담당한다.

이 챕터는 후자가 언제·왜 필요한지, 그리고 spring-aspects의 모든 애스펙트가 공유하는 공통 골격을 설명한다. 개별 기능(트랜잭션/캐시/비동기/DI)은 다음 챕터들에서 다루고, 여기서는 네 애스펙트가 똑같이 따르는 패턴을 한 번에 짚는다.

프록시 모드의 한계

[프록시 모드]
  caller ──▶ ┌─────────┐  advice  ┌──────────┐
             │ Proxy   │ ───────▶ │  Target  │
             └─────────┘          └──────────┘
                ▲                      │
                │ self-invocation은    │ this.foo() ──┐
                │ 프록시를 안 거침     │◀─────────────┘ (advice 누락!)

 한계 1) Spring이 만든 빈만 프록시로 감쌀 수 있다 → new 객체엔 적용 불가
 한계 2) 객체 내부의 this.method() 호출은 프록시를 우회 → advice 누락
 한계 3) 인터페이스 프록시(JDK)는 public 메서드만, 인터페이스 호출만 가로챔

AspectJ 위빙은 이 한계를 없앤다

[AspectJ 모드]
  바이트코드에 advice가 직접 박혀 있음
  ┌────────────────────────────┐
  │ class Target {             │
  │   void foo() {             │
  │     <advice 시작 코드>     │ ◀── 위버가 삽입
  │     ...원래 본문...        │
  │     <advice 종료 코드>     │ ◀── 위버가 삽입
  │   }                        │
  │ }                          │
  └────────────────────────────┘
  → new 객체든, self-invocation이든, 비public 메서드든 전부 적용

위빙은 두 시점 중 하나에서 일어난다.

  • 컴파일 타임 위빙(CTW): AspectJ 컴파일러(ajc)가 빌드 시점에 .aj 애스펙트와 대상 클래스를 함께 컴파일하며 어드바이스를 삽입한다.
  • 로드 타임 위빙(LTW): JVM이 클래스를 로딩할 때 자바 에이전트(spring-instrument + @EnableLoadTimeWeaving)가 바이트코드를 변환한다.

@Configurable(다음 챕터)은 거의 항상 LTW와 함께 쓰인다. 트랜잭션/캐시/비동기 애스펙트는 CTW로 미리 위빙해 두는 방식이 일반적이다.

핵심 타입 — 모든 애스펙트가 공유하는 3층 구조

spring-aspects의 네 기능 애스펙트는 약속이라도 한 듯 동일한 3층 구조를 갖는다.

   ┌──────────────────────────────────────────────────────────┐
   │  (1) *AspectSupport  — 다른 모듈에 있는 "진짜 로직"        │
   │      TransactionAspectSupport / CacheAspectSupport /      │
   │      AsyncExecutionAspectSupport / BeanConfigurerSupport  │
   └───────────────────────────┬──────────────────────────────┘
                               │ extends (또는 위임)
                               ▼
   ┌──────────────────────────────────────────────────────────┐
   │  (2) Abstract*Aspect.aj — around advice 한 개 +           │
   │      추상 pointcut 한 개. "잡으면 무엇을 할지"를 정의.     │
   └───────────────────────────┬──────────────────────────────┘
                               │ extends
                               ▼
   ┌──────────────────────────────────────────────────────────┐
   │  (3) Annotation*Aspect.aj — 구체 pointcut 정의.           │
   │      "어떤 조인 포인트를 잡을지"(@Transactional 등)를 명시 │
   └──────────────────────────────────────────────────────────┘
  • (1) *AspectSupport: 트랜잭션 시작/커밋/롤백, 캐시 조회/저장, 비동기 제출 같은 실제 로직. spring-aspects가 아니라 spring-tx·spring-context에 들어 있다. 애스펙트는 이걸 extends 해서 그 메서드(invokeWithinTransaction, execute, doSubmit)를 호출할 뿐이다.
  • (2) Abstract*Aspect.aj: AspectJ around(또는 before/after) 어드바이스를 하나 갖고, 추상 pointcut을 선언한다. 어드바이스 본문은 "조인 포인트의 메서드 정보를 꺼내 (1)의 support 메서드를 부르고, 결과를 그대로 반환"하는 어댑터다.
  • (3) Annotation*Aspect.aj: 추상 pointcut을 @Transactional·@Cacheable·@Async 같은 애너테이션 매칭으로 구체화한다.

왜 애스펙트가 직접 로직을 갖지 않는가

프록시 모드와 AspectJ 모드는 같은 어드바이스 로직을 공유해야 한다. 트랜잭션 전파 규칙이나 캐시 키 생성 규칙이 두 모드에서 다르면 안 되기 때문이다. 그래서 로직은 *AspectSupport에 한 번만 구현하고, 프록시용 인터셉터(TransactionInterceptor 등)와 AspectJ용 애스펙트가 둘 다 그것을 상속한다. spring-aspects는 그중 AspectJ 진입점만 책임진다.

동작 흐름 — aspectOf()로 싱글톤 애스펙트를 Spring 빈에 연결

AspectJ 애스펙트는 일반 객체처럼 new로 만들 수 없다. AspectJ 런타임이 애스펙트마다 싱글톤 인스턴스를 관리하고, AspectName.aspectOf() 정적 메서드로 그 인스턴스를 꺼낸다. 문제는 이 싱글톤에 TransactionManager·CacheManager·Executor 같은 협력 객체를 주입해 줘야 한다는 것이다.

이를 잇는 것이 각 패키지의 @Configuration 클래스다. 예를 들어 트랜잭션:

// AspectJTransactionManagementConfiguration
@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME)
public AnnotationTransactionAspect transactionAspect(TransactionAttributeSource tas) {
    AnnotationTransactionAspect txAspect = AnnotationTransactionAspect.aspectOf(); // 위버가 만든 싱글톤
    txAspect.setTransactionAttributeSource(tas);
    if (this.txManager != null) {
        txAspect.setTransactionManager(this.txManager);
    }
    return txAspect; // 그 싱글톤을 Spring 빈으로 노출 → 의존성 주입 완료
}

흐름을 정리하면:

@EnableTransactionManagement(mode = ASPECTJ)
        │
        ▼  ConfigurationSelector가 mode 보고 분기
AspectJTransactionManagementConfiguration  (@Configuration 등록)
        │
        ▼  @Bean 메서드 실행
AnnotationTransactionAspect.aspectOf()  ──▶ AspectJ 위버가 보관 중인 싱글톤 획득
        │
        ▼  setTransactionManager(...) 등 주입
싱글톤 애스펙트가 Spring 빈으로 등록됨
        │
        ▼  (별도로) 위빙된 대상 클래스의 @Transactional 메서드가 호출되면
그 싱글톤 애스펙트의 around advice가 발동 → 주입받은 txManager로 트랜잭션 수행

핵심은 "위빙은 코드에, 설정은 빈에" 라는 분리다. 어떤 조인 포인트를 잡을지는 컴파일/로드 타임에 바이트코드로 결정되지만, 그 잡힌 지점에서 사용할 협력 객체는 런타임에 Spring이 aspectOf() 싱글톤에 주입한다.

핵심 메서드 / 관용구

  • AspectName.aspectOf(): AspectJ가 합성해 주는 정적 메서드. 위버가 관리하는 애스펙트 싱글톤을 반환한다. 네 패키지의 모든 @Configuration이 이걸로 빈을 만든다.
  • 추상 pointcut: Abstract*Aspectprotected abstract pointcut transactionalMethodExecution(Object txObject);처럼 "잡을 지점의 형태"만 선언하고, 구체 애스펙트가 애너테이션 조건으로 채운다. 전형적인 템플릿 메서드 패턴의 pointcut 판이다.
  • @SuppressAjWarnings("adviceDidNotMatch"): 어드바이스가 어떤 클래스에도 매칭되지 않을 때 AspectJ가 내는 경고를 끈다. 라이브러리 애스펙트는 사용처를 알 수 없으니 당연히 붙인다.
  • @Role(BeanDefinition.ROLE_INFRASTRUCTURE): 등록되는 애스펙트 빈이 사용자용이 아닌 인프라용임을 표시한다. 네 @Configuration 모두 이 표식을 단다.
  • checked 예외 우회: AspectJ around는 선언된 예외만 던질 수 있다. 그러나 위빙 대상 메서드는 임의의 checked 예외를 던질 수 있으므로, Rethrower(트랜잭션)·AnyThrow(캐시) 같은 제네릭 트릭으로 원래 예외를 그대로 다시 던진다. 이는 AspectJ 어드바이스 어댑터에서 반복되는 관용구다.

설계 포인트

  • 템플릿 메서드 + 전략: 상위 애스펙트는 advice 골격을, 하위 애스펙트는 pointcut 전략을 제공한다. Spring 애너테이션용(AnnotationTransactionAspect)과 표준 스펙용(JtaAnnotationTransactionAspect, JCacheCacheAspect)이 같은 골격을 공유한다.
  • DRY across 모드: 로직을 *AspectSupport에 모아 프록시 모드와 공유함으로써, 사용자가 모드를 바꿔도 의미가 동일하게 유지된다.
  • 확장점: GenericInterfaceDrivenDependencyInjectionAspect(다음 챕터)처럼 사용자가 직접 서브 애스펙트를 작성해 자기 도메인 타입에 DI를 거는 길도 열려 있다.

정리

spring-aspects는 "프록시로는 닿지 않는 곳까지 Spring의 횡단 기능을 넣기 위해" AspectJ 위빙 진입점을 제공하는 모듈이다. 네 기능 애스펙트는 모두 *AspectSupport(진짜 로직) → Abstract*Aspect(advice 골격) → Annotation*Aspect(구체 pointcut)의 3층 구조를 따르고, @Enable...(mode = ASPECTJ)@ConfigurationaspectOf() 경로로 위버 싱글톤에 협력 객체를 주입받는다. 이 공통 골격을 머릿속에 두고 다음 챕터의 각 기능을 보면 된다.