Spring Proxy와 AOP

최승원·2026년 3월 3일

백엔드 개발에서 트랜잭션, 로깅, 보안, 캐싱 등은 핵심 비즈니스 로직이 아닙니다. 관심사의 분리가 필요한 기능들이며, 이 기능들을 코드에 직접 섞으면 관심사의 분리는 무너집니다.

관심사의 분리를 위해 꼭 알아야 할 것은 Spring AOP이며, 추가로 Spring이 AOP를 왜 Proxy 기반으로 구현했는지도 파악해야 합니다.

오늘은 실제로 어떻게 Proxy를 생성하고, 호출을 가로채는지 내부 구조까지 분석해보겠습니다.

오늘 배운 것

Proxy란?

Proxy란 대리 객체입니다.

Client -> Proxy -> Target

Spring 컨테이너에 등록되는 Bean은 Target이 아니라 Proxy 객체입니다.

Client -> Proxy (Bean으로 등록)
            └── Target

즉, Bean 생성 이후 Target이 Proxy로 치환됩니다. 이것을 이해해야 self-invocation 문제를 이해할 수 있습니다.

Proxy는 언제 생성될까?

Proxy 생성의 핵심은 BeanPostProcessor입니다.

Spring Bean 생성 흐름은 다음과 같습니다.

  1. Bean 인스턴스 생성
  2. Dependency Injection
  3. BeanPostProcessor.beforeInitialization
  4. 초기화
  5. BeanPostProcessor.afterInitialization

여기서 AbstractAutoProxyCreator가 afterInitialization 단계에서 개입합니다.

동작 과정은 다음과 같습니다.

  1. 해당 Bean에 적용할 Advice 존재 여부 검사
  2. 존재하면 Proxy 생성
  3. 기존 Bean 대신 Proxy 반환

즉, Bean 초기화가 끝난 후 최종적으로 Proxy로 교체됩니다.

JDK Dynamic Proxy vs CGLIB

Spring은 두 가지 방식을 사용합니다.

JDK Dynamic Proxy

  • 인터페이스 기반
  • java.lang.reflect.Proxy 사용
  • InvocationHandler로 모든 호출 위임

구조

Proxy
  -> InvocationHandler
      -> Method.invoke(target)

특징

  • 인터페이스 필수
  • 리플렉션 기반 호출
  • final 메서드 제약 없음

CGLIB

  • 클래스 상속 기반
  • 바이트코드 조작
  • Target 클래스를 상속한 새로운 클래스 생성

생성되는 클래스 예시

UserService$$SpringCGLIB$$0

구조

Target$$SpringCGLIB
   override method()
      -> MethodInterceptor
          -> target 호출

특징

  • 인터페이스 없어도 됨
  • final 클래스/메서드는 프록시 불가
  • FastClass 기반 호출로 리플렉션보다 빠름

Spring Boot 3 기준 기본값은 CGLIB입니다.


@Transactional의 실제 동작

@Transactional이 붙으면 Spring은 TransactionInterceptor를 Advice로 등록합니다.

호출 흐름은 다음과 같습니다.

Controller
  -> Proxy
      -> TransactionInterceptor
          -> PlatformTransactionManager
              -> Target

동작 순서
1. 트랜잭션 시작
2. target 메서드 실행
3. commit 또는 rollback

여기서 중요한 점은 트랜잭션 상태는 ThreadLocal 기반으로 관리된다는 점입니다.

현재 트랜잭션 정보는 TransactionSynchronizationManager에 저장됩니다.


Self Invocation이 왜 깨질까?

문제 코드:

public void outer() {
    inner();
}

실제 호출 구조

Client -> Proxy.outer()
               -> Target.inner()

inner()는 Proxy를 거치지 않습니다. 즉, Advice가 적용되지 않습니다.
이것은 프록시 기반 구조의 한계입니다.


Proxy 기반 AOP의 한계

한계점은 다음과 같습니다.

  • private 메서드 적용 불가
  • final 메서드 적용 불가 (CGLIB)
  • 내부 호출 적용 불가
  • 생성자 적용 불가

이 한계를 해결하려면 컴파일 타임 위빙 방식이 필요합니다.

대표적으로 AspectJ가 있습니다.

마무리하며

Spring AOP는 단순 어노테이션 기능이 아닙니다.

  • Bean 생성 후 Proxy로 교체
  • MethodInterceptor 체인 구조
  • ThreadLocal 기반 트랜잭션 관리
  • self-invocation이라는 구조적 한계

위 4가지를 이해해야 @Transactional이 왜 동작하지 않는지를 설명할 수 있습니다.
Spring AOP는 런타임에 Bean을 감싸는 Proxy 객체일 뿐입니다.

profile
안녕하세요. 최승원입니다.

0개의 댓글