Loading...
Spring Framework Reference Documentation 7.0.2의 Proxying Mechanisms의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
Spring AOP는 주어진 target 객체에 대한 프록시를 생성하기 위해 JDK dynamic proxy 또는 CGLIB 중 하나를 사용합니다. JDK dynamic proxy는 JDK에 내장되어 있는 반면, CGLIB는 일반적인 오픈 소스 클래스 정의 라이브러리입니다 (spring-core에 재패키징됨).
프록시잉될 target 객체가 하나 이상의 인터페이스를 구현하는 경우 JDK dynamic proxy가 사용되며, target 타입이 구현한 모든 인터페이스가 프록시잉됩니다. target 객체가 어떤 인터페이스도 구현하지 않는 경우, target 타입의 런타임에 생성된 서브클래스인 CGLIB 프록시가 생성됩니다.
CGLIB 프록시잉의 사용을 강제할 수도 있습니다(예를 들어, target 객체에 대해 해당 객체의 인터페이스가 구현한 메서드뿐만 아니라 정의된 모든 메서드를 프록시잉하기 위해). 그러나 다음과 같은 이슈들을 고려해야 합니다:
final 클래스는 확장될 수 없기 때문에 프록시잉될 수 없습니다.final 메서드는 오버라이드될 수 없기 때문에 어드바이스를 적용할 수 없습니다.private 메서드는 오버라이드될 수 없기 때문에 어드바이스를 적용할 수 없습니다.java.lang 패키지의 클래스에 대해 CGLIB 프록시를 생성할 수 없습니다. 이러한 경우 JVM 부트스트랩 플래그 --add-opens=java.base/java.lang=ALL-UNNAMED가 필요하지만, 이는 모듈에서는 사용할 수 없습니다.CGLIB 프록시의 사용을 강제하려면, 다음과 같이 <aop:config> element의 proxy-target-class attribute 값을 true로 설정합니다:
1<aop:config proxy-target-class="true"> 2 <!-- other beans defined here... --> 3</aop:config>
@AspectJ auto-proxy 지원을 사용할 때 CGLIB 프록시잉을 강제하려면, 다음과 같이 <aop:aspectj-autoproxy> element의 proxy-target-class attribute를 true로 설정합니다:
1<aop:aspectj-autoproxy proxy-target-class="true"/>
여러
<aop:config/>section은 런타임 시 하나의 통합된 auto-proxy creator로 병합되며,<br>이는 각<aop:config/>section(일반적으로 서로 다른 XML bean 정의 파일로부터 옴)에서<br>지정한 프록시 설정 중 가장 강한 설정을 적용합니다.<br>이는<tx:annotation-driven/>및<aop:aspectj-autoproxy/>element에도 적용됩니다.<br>명확히 말하면,<tx:annotation-driven/>,<aop:aspectj-autoproxy/>, 또는<br><aop:config/>element에서proxy-target-class="true"를 사용하면<br>세 가지 모두에 대해 CGLIB 프록시 사용을 강제합니다.
@EnableAspectJAutoProxy, @EnableTransactionManagement 및 관련 설정 어노테이션은 이에 상응하는 proxyTargetClass attribute를 제공합니다. 이들 또한 하나의 통합된 auto-proxy creator로 병합되어, 런타임 시 효과적으로 가장 강한 프록시 설정을 적용합니다. 7.0부터는 이는 개별 프록시 프로세서에도 적용되며, 예를 들어 @EnableAsync가 주어진 애플리케이션에서 이루어지는 모든 auto-proxying 시도에 대해 통합된 글로벌 기본 설정에 일관되게 참여합니다.
글로벌 기본 프록시 타입은 설정에 따라 다를 수 있습니다. core framework는 기본적으로 인터페이스 기반 프록시를 제안하지만, Spring Boot는 설정 프로퍼티에 따라 기본적으로 클래스 기반 프록시를 활성화할 수 있습니다.
7.0부터는 개별 빈에 대해 특정 프록시 타입을 강제하는 것이 가능합니다. 주어진 @Bean 메서드 또는 @Component 클래스에 @Proxyable 어노테이션을 사용하면 되며, @Proxyable(INTERFACES) 또는 @Proxyable(TARGET_CLASS)는 글로벌하게 설정된 기본값을 오버라이드합니다. 매우 특정한 목적을 위해서는 @Proxyable(interfaces=…)를 통해 사용할 프록시 인터페이스를 지정할 수도 있으며, target 빈이 구현하는 모든 인터페이스가 아니라 선택된 인터페이스에만 노출을 제한할 수 있습니다.
Spring AOP는 프록시 기반입니다. 직접 aspect를 작성하거나 Spring Framework에서 제공하는 Spring AOP 기반 aspect를 사용하기 전에, 방금의 문장이 실제로 의미하는 시맨틱스를 확실히 이해하는 것이 매우 중요합니다.
먼저, 다음 코드 스니펫에 나타난 것처럼 plain-vanilla, 프록시잉되지 않은 객체 reference가 있는 상황을 고려해 보겠습니다:
1public class SimplePojo implements Pojo { 2 3 public void foo() { 4 // this next method invocation is a direct call on the 'this' reference 5 this.bar(); 6 } 7 8 public void bar() { 9 // some logic... 10 } 11}
1class SimplePojo : Pojo { 2 3 fun foo() { 4 // this next method invocation is a direct call on the 'this' reference 5 this.bar() 6 } 7 8 fun bar() { 9 // some logic... 10 } 11}
객체 reference에서 메서드를 호출하는 경우, 다음 이미지와 listing에서 보듯이 그 메서드는 해당 객체 reference에 직접 호출됩니다:

1public class Main { 2 3 public static void main(String[] args) { 4 Pojo pojo = new SimplePojo(); 5 // this is a direct method call on the 'pojo' reference 6 pojo.foo(); 7 } 8}
1fun main() { 2 val pojo = SimplePojo() 3 // this is a direct method call on the 'pojo' reference 4 pojo.foo() 5}
클라이언트 코드가 가지고 있는 reference가 프록시일 때 상황은 약간 달라집니다. 다음 다이어그램과 코드 스니펫을 보십시오:

1public class Main { 2 3 public static void main(String[] args) { 4 ProxyFactory factory = new ProxyFactory(new SimplePojo()); 5 factory.addInterface(Pojo.class); 6 factory.addAdvice(new RetryAdvice()); 7 8 Pojo pojo = (Pojo) factory.getProxy(); 9 // this is a method call on the proxy! 10 pojo.foo(); 11 } 12}
1fun main() { 2 val factory = ProxyFactory(SimplePojo()) 3 factory.addInterface(Pojo::class.java) 4 factory.addAdvice(RetryAdvice()) 5 6 val pojo = factory.proxy as Pojo 7 // this is a method call on the proxy! 8 pojo.foo() 9}
여기서 이해해야 할 핵심은 Main 클래스의 main(..) 메서드 내부의 클라이언트 코드가 프록시에 대한 reference를 가지고 있다는 점입니다. 이는 해당 객체 reference에서의 메서드 호출이 프록시에 대한 호출임을 의미합니다. 결과적으로 프록시는 해당 메서드 호출과 관련 있는 모든 인터셉터(어드바이스)에게 위임할 수 있습니다.
그러나 호출이 최종적으로 target 객체(이 경우 SimplePojo reference)에 도달한 후에는, this.bar() 또는 this.foo()와 같이 target 객체가 자신에게 수행하는 모든 메서드 호출은 프록시가 아니라 this reference에 대해 호출됩니다. 이는 중요한 의미를 가집니다. self invocation은 메서드 invocation에 연관된 어드바이스가 실행될 기회를 갖지 못한다는 뜻입니다. 다시 말해, 명시적 또는 암시적 this reference를 통한 self invocation은 어드바이스를 우회하게 됩니다.
이를 해결하기 위해 다음과 같은 옵션이 있습니다.
가장 좋은 접근 방식(여기서 "가장 좋은"이라는 용어는 느슨하게 사용됩니다)은 self invocation이 발생하지 않도록 코드를 리팩터링하는 것입니다. 이는 어느 정도의 작업을 수반하지만, 가장 좋고 침습성이 가장 적은 접근 방식입니다.
다른 접근 방식은
self injection
을 활용하여, this 대신 self reference를 통해 프록시에서 메서드를 호출하는 것입니다.
AopContext.currentProxy() 사용마지막 접근 방식은 매우 권장되지 않으며, 앞선 옵션들을 선호하기 때문에 언급을 주저합니다. 그러나 최후의 수단으로, 다음 예에서 보듯이 클래스 내부의 로직을 Spring AOP에 묶는 선택을 할 수 있습니다.
1public class SimplePojo implements Pojo { 2 3 public void foo() { 4 // This works, but it should be avoided if possible. 5 ((Pojo) AopContext.currentProxy()).bar(); 6 } 7 8 public void bar() { 9 // some logic... 10 } 11}
1class SimplePojo : Pojo { 2 3 fun foo() { 4 // This works, but it should be avoided if possible. 5 (AopContext.currentProxy() as Pojo).bar() 6 } 7 8 fun bar() { 9 // some logic... 10 } 11}
AopContext.currentProxy()의 사용은 코드를 Spring AOP에 완전히 결합시키며, 클래스 자체가 AOP 컨텍스트에서 사용되고 있다는 사실을 인지하게 만듭니다. 이는 AOP의 일부 장점을 감소시킵니다. 또한 다음 예에서 보듯이 ProxyFactory가 프록시를 노출하도록 설정되어야 합니다:
1public class Main { 2 3 public static void main(String[] args) { 4 ProxyFactory factory = new ProxyFactory(new SimplePojo()); 5 factory.addInterface(Pojo.class); 6 factory.addAdvice(new RetryAdvice()); 7 factory.setExposeProxy(true); 8 9 Pojo pojo = (Pojo) factory.getProxy(); 10 // this is a method call on the proxy! 11 pojo.foo(); 12 } 13}
1fun main() { 2 val factory = ProxyFactory(SimplePojo()) 3 factory.addInterface(Pojo::class.java) 4 factory.addAdvice(RetryAdvice()) 5 factory.isExposeProxy = true 6 7 val pojo = factory.proxy as Pojo 8 // this is a method call on the proxy! 9 pojo.foo() 10}
AspectJ compile-time weaving 및 load-time weaving은 프록시를 통해서가 아니라 바이트코드 내에<br>어드바이스를 적용하기 때문에 이러한 self-invocation issue가 없습니다.
Mixing Aspect Types
Programmatic Creation of @AspectJ Proxies