-
[Spring] AOP 알아보기 - Spring AOP 프록시 매커니즘Spring 2025. 12. 27. 20:16
Spring AOP는 순수 자바 기반의 프록시(Proxy) 기술을 사용하여 동작합니다. 이 문서는 Spring이 프록시를 생성하는 두 가지 전략(JDK Dynamic Proxy, CGLIB)과 프록시 방식이 갖는 구조적 한계인 '내부 호출(Self-Invocation)' 문제 및 해결책을 다룹니다.
1. 프록시 생성 전략 (Proxying Mechanisms)
Spring AOP는 타겟 객체의 상태(인터페이스 구현 여부)에 따라 다음 두 가지 방식 중 하나를 자동으로 선택하여 프록시 객체를 생성합니다.
① JDK 동적 프록시 (JDK Dynamic Proxy)
- 지위: Spring AOP의 기본(Default) 전략입니다.
- 기술: 표준 JDK의 java.lang.reflect.Proxy를 사용합니다.
- 조건: 타겟 객체가 하나 이상의 인터페이스를 구현하고 있을 때 선택됩니다.
- 특징: 인터페이스를 기반으로 프록시를 생성하므로, 인터페이스에 정의된 메서드만 가로챌(Interception) 수 있습니다.
② CGLIB 프록시 (CGLIB Proxy)
- 지위: 인터페이스가 없을 때 사용하는 대안 전략입니다.
- 기술: 오픈소스 라이브러리인 CGLIB(Code Generation Library)를 사용합니다. (현재는 spring-core에 내장됨)
- 조건: 타겟 객체가 인터페이스를 구현하지 않고, 구체 클래스(Concrete Class)로만 되어 있을 때 사용됩니다.
- 특징: 타겟 클래스를 상속(Inheritance)받아 런타임에 서브 클래스를 생성하는 방식으로 동작합니다.
CGLIB 사용 시 제약 사항 (Limitations)
CGLIB는 상속을 통해 프록시를 구현하므로 다음과 같은 제약이 존재합니다.
- final 클래스: 상속이 불가능하므로 프록시 생성 불가.
- final 메서드: 오버라이딩(Overriding)이 불가능하므로 AOP 적용 불가.
- private 메서드: 하위 클래스에서 접근할 수 없으므로 AOP 적용 불가.
- Java 모듈 시스템: java.lang 패키지 클래스 등은 모듈 보안상 프록시 생성이 제한될 수 있음.
2. 두 방식 비교 및 설정 가이드
JDK Dynamic Proxy vs CGLIB 비교
구분 JDK Dynamic Proxy CGLIB Proxy
대상 인터페이스 구현체 클래스 (인터페이스 없는 경우) 생성 원리 인터페이스 구현 (Implements) 클래스 상속 (Extends) 기술 Java 표준 라이브러리 (reflect) 바이트코드 조작 라이브러리 Spring 기본값 Yes (인터페이스 존재 시) Yes (인터페이스 부재 시) 권장 사항 인터페이스 기반 프로그래밍 권장 상속 불가 요소(final) 주의 프록시 방식 강제하기 (Forcing Proxy Types)
Spring은 기본적으로 인터페이스가 있으면 JDK 방식을 쓰지만, 설정을 통해 강제로 CGLIB(클래스 기반 프록시)를 사용하게 할 수 있습니다.
- 사용 사례: 인터페이스에 없는 메서드에 AOP를 적용해야 하거나, 구체적인 클래스 타입으로 의존성을 주입해야 할 때.
- XML 설정
proxy-target-class="true" 속성을 추가합니다.
<aop:config proxy-target-class="true"> </aop:config> <aop:aspectj-autoproxy proxy-target-class="true"/>애플리케이션 내에 여러 설정이 흩어져 있어도, 단 하나라도 proxy-target-class="true"가 있다면 전체적으로 CGLIB 방식이 강제됩니다.
- 어노테이션 설정 (Spring 7.0+)
개별 Bean 단위로 제어할 수 있는 @Proxyable 어노테이션이 도입되었습니다.
- @Proxyable(TARGET_CLASS): 강제 CGLIB 사용
- @Proxyable(INTERFACES): 인터페이스 기반 사용
3. 핵심 주의사항: 내부 호출(Self-Invocation) 문제
Spring AOP가 프록시 기반(Proxy-based)이라는 사실은 매우 중요한 의미를 가집니다. 이는 "프록시 내부에서 자신의 메서드를 호출할 때는 AOP가 적용되지 않는다"는 치명적인 한계를 내포하기 때문입니다.
상황 분석: 왜 AOP가 적용되지 않는가?
1. 프록시가 없는 일반 객체 (Plain Object)
public class SimplePojo implements Pojo { public void foo() { // 'this'는 자기 자신 객체를 가리킴 this.bar(); } public void bar() { ... } }- foo() 호출 시 내부의 this.bar()는 자연스럽게 자신의 메서드를 호출합니다.
- 프록시가 적용된 객체
클라이언트는 실제 객체가 아닌, 프록시 객체를 통해 메서드를 호출합니다.
public class Main { public static void main(String[] args) { ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.addInterface(Pojo.class); factory.addAdvice(new RetryAdvice()); Pojo pojo = (Pojo) factory.getProxy(); // this is a method call on the proxy! pojo.foo(); } }3. 실행 흐름과 문제 발생
- 외부 호출 (pojo.foo()): 클라이언트가 프록시를 호출합니다. 프록시는 Advice(트랜잭션 등)를 실행한 후, 실제 타겟의 foo()를 호출합니다. (AOP 적용 O)
- 내부 호출 (this.bar()): 타겟 객체의 foo() 안에서 this.bar()를 호출합니다. 여기서 this는 프록시가 아니라 **실제 타겟 객체(Target Object)**입니다.
- 결과: 프록시를 거치지 않고 직접 메서드가 호출되므로, bar()에 걸려 있는 Advice는 실행되지 않습니다. (AOP 적용 X)
4. 내부 호출 문제 해결 방법
이 문제를 해결하기 위해 Spring 공식 문서는 다음 3가지 방법을 제시합니다.
방법 1: 내부 호출 피하기 (Refactoring) ★ 권장
구조를 변경하여 내부 호출이 발생하지 않도록 합니다. 보통 내부 호출되는 메서드(bar)를 별도의 Service Bean으로 분리하여 주입받아 사용하는 것이 가장 깔끔합니다.
방법 2: 자기 자신 주입 (Self-Injection)
Setter나 @Autowired를 사용하여 자기 자신의 프록시 빈을 주입받습니다. 그 후 this.bar() 대신 주입받은 프록시의 메서드를 호출합니다.
방법 3: AopContext 사용 (비권장)
현재 쓰레드에 바인딩된 프록시 객체를 강제로 가져와서 호출하는 방법입니다.
설정 필요:ProxyFactory factory = new ProxyFactory(new SimplePojo()); factory.setExposeProxy(true); // 프록시 노출 설정 필수코드 수정:
public class SimplePojo implements Pojo { public void foo() { // 현재 프록시를 가져와서 bar() 호출 -> AOP 적용됨 ((Pojo) AopContext.currentProxy()).bar(); } public void bar() { ... } }주의: 이 방식은 비즈니스 로직이 Spring 프레임워크(AopContext)에 강하게 결합(Coupling)되므로, 가급적 사용을 피해야 합니다.
💡 참고: AspectJ와의 비교
만약 AspectJ (컴파일 타임 위빙 또는 로드 타임 위빙)를 사용한다면 이러한 내부 호출 문제는 발생하지 않습니다. AspectJ는 프록시를 만드는 것이 아니라, 실제 바이트코드를 조작하여 코드를 심기 때문에 this로 호출하더라도 AOP 로직이 정상적으로 수행됩니다.
'Spring' 카테고리의 다른 글
[Spring] AOP 알아보기 - Spring AOP의 특징 (1) 2025.12.27 [Spring] AOP 알아보기 - AOP 개념 (0) 2025.12.27 [Spring] AOP 알아보기 - AOP 소개 (0) 2025.12.27 [Spring] 서비스(Service) 레이어의 본질적인 역할과 책임 (0) 2025.12.20 [스프링] 안티패턴: 완화된 레이어드 아키텍처 (0) 2025.12.19