Proxying Mechanisms

2024. 11. 17. 12:26Spring Framework/Aspect Oriented Programming with Spring

Proxying Mechanisms

Spring AOP는 지정된 타겟 객체에 대해 JDK 동적 프록시 또는 CGLIB을 사용하여 프록시를 생성합니다. JDK 동적 프록시는 JDK에 내장되어 있는 반면, CGLIB은 일반적인 오픈 소스 클래스 정의 라이브러리로, Spring Core에 재패키징되어 있습니다.

프록시 대상 객체가 하나 이상의 인터페이스를 구현하는 경우 JDK 동적 프록시가 사용됩니다. 이때 타겟 타입이 구현하는 모든 인터페이스가 프록시됩니다. 반면, 타겟 객체가 인터페이스를 구현하지 않는 경우 CGLIB 프록시가 생성됩니다.

모든 메서드(인터페이스에서 구현한 메서드뿐만 아니라 타겟 객체에 정의된 모든 메서드)를 프록시하려면 CGLIB 프록시를 강제로 사용하도록 설정할 수 있습니다. 그러나 다음과 같은 문제를 고려해야 합니다:

  • CGLIB을 사용하는 경우, final 메서드는 런타임 생성된 서브클래스에서 오버라이드할 수 없으므로 조언을 적용할 수 없습니다.
  • 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' 참조에 대한 직접 호출입니다.
        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();
    }
}

여기서 이해해야 할 핵심은 Main 클래스의 main(..) 메서드 내 클라이언트 코드가 프록시에 대한 참조를 가진다는 점입니다. 이는 객체 참조에서의 메서드 호출이 프록시에 대한 호출임을 의미합니다. 그 결과, 프록시는 해당 메서드 호출과 관련된 모든 인터셉터(즉, 조언)에 위임할 수 있습니다. 그러나 호출이 결국 타겟 객체에 도달하면(이 경우 SimplePojo 참조), 그 객체가 자신에게 호출하는 모든 메서드(this.bar() 또는 this.foo() 등)는 프록시가 아닌 this 참조에 대해 호출됩니다. 이는 중요한 함의를 가지고 있습니다. 즉, 자기 호출(self-invocation)에서는 메서드 호출과 관련된 조언이 실행될 기회를 얻지 못하게 됩니다.

그렇다면 이 문제에 대해 무엇을 해야 할까요? 가장 좋은 접근 방식(여기서 '좋은'이라는 용어는 다소 유연하게 사용됨)은 자기 호출이 발생하지 않도록 코드를 리팩터링하는 것입니다. 이는 일부 작업을 필요로 하지만, 가장 적게 침입하는 최선의 접근 방식입니다. 다음 접근 방식은 매우 끔찍한데, 우리가 이 접근 방식을 언급하기를 주저하는 이유는 바로 그만큼 끔찍하기 때문입니다. 다음 예에서 볼 수 있듯이, 클래스 내 로직을 완전히 Spring AOP에 결합할 수 있습니다:

public class SimplePojo implements Pojo {

    public void foo() {
        // 이렇게 하면 되지만... 정말 별로입니다!
        ((Pojo) AopContext.currentProxy()).bar();
    }

    public void bar() {
        // 일부 로직...
    }
}

이 접근 방식은 코드와 Spring AOP를 완전히 결합시키며, 클래스 자체가 AOP 컨텍스트에서 사용된다는 사실을 인지하게 만듭니다. 이는 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