2024. 11. 17. 12:26ㆍSpring Framework/Aspect Oriented Programming with Spring
Proxying Mechanisms
Spring AOP는 지정된 타겟 객체에 대해 JDK 동적 프록시 또는 CGLIB을 사용하여 프록시를 생성합니다. JDK 동적 프록시는 JDK에 내장되어 있는 반면, CGLIB은 일반적인 오픈 소스 클래스 정의 라이브러리로, Spring Core에 재패키징되어 있습니다.
프록시 타겟 객체가 하나 이상의 인터페이스를 구현하는 경우 JDK 동적 프록시가 사용됩니다. 이때 타겟 타입이 구현하는 모든 인터페이스가 프록시됩니다. 반면, 타겟 객체가 인터페이스를 구현하지 않는 경우 CGLIB 프록시가 생성됩니다.
모든 메서드(인터페이스에서 구현한 메서드뿐만 아니라 타겟 객체에 정의된 모든 메서드)를 프록시하려면 CGLIB 프록시를 강제로 사용하도록 설정할 수 있습니다. 그러나 다음과 같은 문제를 고려해야 합니다:
- CGLIB을 사용하는 경우, final 메서드는 런타임에서 생성된 서브클래스에서 오버라이드할 수 없으므로 advice를 적용할 수 없습니다.
- Spring 4.0부터는 더 이상 프록시된 객체의 컨스트럭터가 두 번 호출되지 않습니다. 이는 CGLIB 프록시 인스턴스가 Objenesis를 통해 생성되기 때문입니다. 단, JVM이 컨스트럭터 우회를 허용하지 않는 경우, 컨스트럭터가 두 번 호출되는 것과 관련된 디버그 로그 항목을 볼 수 있습니다.
- CGLIB 프록시 사용은 JDK 9+ 플랫폼 모듈 시스템에서 제한될 수 있습니다. 일반적인 경우로, 모듈 경로에서 배포할 때 java.lang 패키지의 클래스를 프록시로 생성할 수 없습니다. 이러한 경우 JVM 부트스트랩 플래그 --add-opens=java.base/java.lang=ALL-UNNAMED가 필요하지만, 이는 모듈에서는 사용할 수 없습니다.
CGLIB 프록시 사용을 강제하려면 <aop:config> 요소의 proxy-target-class 속성 값을 true로 설정하세요:
<aop:config proxy-target-class="true">
<!-- 다른 빈 정의는 여기... -->
</aop:config>
@AspectJ 자동 프록시 지원을 사용할 때 CGLIB 프록시를 강제하려면 <aop:aspectj-autoproxy> 요소의 proxy-target-class 속성 값을 true로 설정하세요:
<aop:aspectj-autoproxy proxy-target-class="true"/>
여러 <aop:config/> 섹션이 런타임 시 단일 통합 자동 프록시 제너레이터로 병합되며, 이는 각 <aop:config/> 섹션(일반적으로 다른 XML 빈 정의 파일에서 지정됨)이 지정한 가장 강력한 프록시 설정을 적용합니다. 이 규칙은 <tx:annotation-driven/> 및 <aop:aspectj-autoproxy/> 요소에도 적용됩니다.
따라서 <tx:annotation-driven/>, <aop:aspectj-autoproxy/>, 또는 <aop:config/> 요소에 proxy-target-class="true"를 사용하면 세 요소 모두에 대해 CGLIB 프록시 사용이 강제됩니다.
Understanding AOP Proxies
Spring AOP는 프록시 기반입니다. 이 문장이 실제로 의미하는 바를 충분히 이해하는 것이 매우 중요합니다. 그래야 자신만의 aspect를 작성하거나 Spring 프레임워크에서 제공하는 AOP 기반 aspect를 사용할 때 제대로 활용할 수 있습니다.
먼저 프록시가 적용되지 않은 일반 객체 참조가 있는 시나리오를 생각해 보겠습니다. 다음 코드 조각을 참고하세요:
public class SimplePojo implements Pojo {
public void foo() {
// 다음 메서드 호출은 'this' 참조에 대한 직접 호출[Self-invocation]입니다.
this.bar();
}
public void bar() {
// 일부 로직...
}
}
객체 참조에서 메서드를 호출하면 해당 객체 참조에서 메서드가 직접 호출됩니다. 다음 그림과 코드는 이를 보여줍니다:

public class Main {
public static void main(String[] args) {
Pojo pojo = new SimplePojo();
// 이것은 'pojo' 참조에서의 직접 메서드 호출입니다.
pojo.foo();
}
}
클라이언트 코드가 가진 참조가 프록시인 경우 상황이 약간 달라집니다. 다음 다이어그램과 코드 조각을 참조하세요:

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();
// 이것은 프록시에 대한 메서드 호출입니다!
pojo.foo();
}
}
ProxyFactory 클래스
프로그램 방식으로 AOP 프록시를 생성하기 위한 팩토리 클래스이며,
BeanFactory를 통한 선언적인 방식이 아닌 직접 코드를 통해 사용하도록 설계되었습니다.
이 클래스는 사용자 정의 코드에서 AOP 프록시 인스턴스를 간단하게 생성하고 구성할 수 있는 방법을 제공합니다.
여기서 이해해야 할 핵심은 Main 클래스의 main(..) 메서드 내 클라이언트 코드가 프록시에 대한 참조를 가진다는 점입니다. 이는 객체 참조에서의 메서드 호출이 프록시에 대한 호출임을 의미합니다. 그 결과, 프록시는 해당 메서드 호출과 관련된 모든 인터셉터(즉, advice)에 위임할 수 있습니다. 그러나 호출이 결국 타겟 객체에 도달하면(이 경우 SimplePojo 참조), 그 객체가 자신에게 호출하는 모든 메서드(this.bar() 또는 this.foo() 등)는 프록시가 아닌 this 참조에 대해 호출됩니다. 이는 중요한 함축적인 의미를 가지고 있습니다. 즉, 자기 호출(self-invocation)에서는 메서드 호출과 관련된 advice가 실행될 기회를 얻지 못하게 됩니다.
이 문제를 해결하기 위해 다음과 같은 선택지가 있습니다.
Avoid self invocation
가장 좋은 접근 방식(여기서 '좋은'이라는 용어는 다소 유연하게 사용됨)은 코드를 리팩터링하여 자기 호출이 발생하지 않도록 만드는 것입니다.이 접근 방식은 약간의 작업이 필요하지만, 가장 덜 침습적인 최선의 방법입니다.
※ 침습적인 최선의 방법이란? 프레임워크의 내부 동작을 우회하거나, 클래스 설계를 변경해야 하지만, 그렇게 해서라도 AOP가 제대로 적용되도록 만들 수 있는 가장 바람직한 해결책.
Inject a self reference
대안으로는 자기 자신[ self injection]을 주입받아, this 대신 프록시를 통해 메서드를 호출하는 방식이 있습니다.
Use AopContext.currentProxy()
이 마지막 접근 방식은 강력히 권장되지 않으며, 앞서 제시한 옵션들을 우선적으로 사용하는 것이 좋습니다.
하지만 최후의 수단으로, 클래스 내 로직을 Spring AOP에 결합시키는 방법을 선택할 수도 있습니다.
다음 예제는 이를 보여줍니다.
public class SimplePojo implements Pojo {
public void foo() {
// 이렇게 하면 되지만... 정말 별로입니다!
((Pojo) AopContext.currentProxy()).bar();
}
public void bar() {
// 일부 로직...
}
}
이는 AOP의 취지에 어긋납니다. 또한 프록시를 생성할 때 추가 구성을 필요로 하며, 다음 예에서 볼 수 있듯이 다음과 같은 추가 구성을 요구합니다:
public class Main {
public static void main(String[] args) {
ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());
factory.setExposeProxy(true);
Pojo pojo = (Pojo) factory.getProxy();
// 이것은 프록시에 대한 메서드 호출입니다!
pojo.foo();
}
}
마지막으로, AspectJ는 프록시 기반의 AOP 프레임워크가 아니기 때문에 자기 호출 문제를 가지고 있지 않다는 점에 유의해야 합니다.
'Spring Framework > Aspect Oriented Programming with Spring' 카테고리의 다른 글
Aspect Oriented Programming with Spring (1) | 2024.11.17 |
---|---|
Choosing which AOP Declaration Style to Use (0) | 2024.11.17 |
Introductions (1) | 2024.11.17 |
Declaring a Pointcut (0) | 2024.11.17 |
Declaring an Aspect (0) | 2024.11.17 |