Spring AOP APIs 2[The Advisor API in Spring, Using the ProxyFactoryBean to Create AOP Proxies]

2023. 5. 16. 17:26Spring Framework/Spring AOP

The Advisor API in Spring

Spring에서 Advisor는 포인트컷 표현식과 연관된 단일 어드바이스 객체만을 포함하는 애스펙트입니다.

도입(introductions)의 특별한 경우를 제외하고, 모든 어드바이저는 모든 어드바이스와 함께 사용할 수 있습니다. `org.springframework.aop.support.DefaultPointcutAdvisor`는 가장 일반적으로 사용되는 어드바이저 클래스입니다. 이 클래스는 `MethodInterceptor`, `BeforeAdvice`, 또는 `ThrowsAdvice`와 함께 사용할 수 있습니다.

Spring에서는 동일한 AOP 프록시에서 어드바이저와 어드바이스 타입을 혼합할 수 있습니다. 예를 들어, 하나의 프록시 구성에서 인터셉션 around 어드바이스, throws 어드바이스, 그리고 before 어드바이스를 사용할 수 있습니다. Spring은 자동으로 필요한 인터셉터 체인을 생성합니다.

Using the ProxyFactoryBean to Create AOP Proxies

Spring IoC 컨테이너(예: ApplicationContext 또는 BeanFactory)를 비즈니스 객체에 사용하는 경우(Spring에서는 이를 권장합니다), Spring의 AOP FactoryBean 구현 중 하나를 사용하는 것이 좋습니다. FactoryBean은 간접적인 레이어를 도입하여 다른 유형의 객체를 생성할 수 있게 해줍니다.

Spring AOP 지원도 내부적으로 FactoryBean을 사용합니다. Spring에서 AOP 프록시를 생성하는 기본적인 방법은 org.springframework.aop.framework.ProxyFactoryBean을 사용하는 것입니다. 이를 통해 포인트컷, 적용할 어드바이스, 어드바이스의 순서에 대해 완전한 제어를 할 수 있습니다. 그러나 이러한 제어가 필요하지 않다면 더 간단한 옵션을 사용하는 것이 좋습니다.

Basics

ProxyFactoryBean은 다른 Spring FactoryBean 구현과 마찬가지로 간접적인 레벨을 도입합니다. foo라는 이름의 ProxyFactoryBean을 정의하면, foo를 참조하는 객체는 ProxyFactoryBean 인스턴스 자체를 보는 것이 아니라, ProxyFactoryBeangetObject() 메서드 구현에서 생성된 객체를 보게 됩니다. 이 메서드는 타겟 객체를 감싸는 AOP 프록시를 생성합니다.

ProxyFactoryBean 또는 다른 IoC-aware 클래스를 사용하여 AOP 프록시를 생성하는 가장 중요한 이점 중 하나는 어드바이스와 포인트컷도 IoC에 의해 관리될 수 있다는 점입니다. 이는 다른 AOP 프레임워크로는 달성하기 어려운 접근 방식을 가능하게 하는 강력한 기능입니다. 예를 들어, 어드바이스 자체가 애플리케이션 객체(타겟 객체 이외의)를 참조할 수 있으며, 이는 의존성 주입(Dependency Injection)이 제공하는 모든 플러그 가능성을 활용할 수 있게 합니다.

JavaBean Properties

Spring에서 제공하는 대부분의 FactoryBean 구현과 마찬가지로 ProxyFactoryBean 클래스 자체도 자바빈입니다. 이 클래스의 속성은 다음을 위해 사용됩니다:

  • 프록시할 타겟을 지정합니다.
  • CGLIB를 사용할지 여부를 지정합니다(JDK 및 CGLIB 기반 프록시 참고).

일부 중요한 속성은 org.springframework.aop.framework.ProxyConfig(Spring의 모든 AOP 프록시 팩토리의 슈퍼클래스)에서 상속됩니다. 이 중요한 속성에는 다음이 포함됩니다:

  • proxyTargetClass: true로 설정되면, 타겟 클래스의 인터페이스가 아닌 타겟 클래스 자체가 프록시됩니다. 이 속성 값이 true로 설정되면 CGLIB 프록시가 생성됩니다(JDK 및 CGLIB 기반 프록시도 참조).
  • optimize: CGLIB을 통해 생성된 프록시에 공격적인 최적화를 적용할지 여부를 제어합니다. 이 설정을 무심코 사용하지 말아야 하며, 관련 AOP 프록시가 최적화를 처리하는 방법을 충분히 이해한 경우에만 사용해야 합니다. 이 설정은 현재 CGLIB 프록시에만 사용되며, JDK 동적 프록시에는 영향을 미치지 않습니다.
  • frozen: 프록시 구성이 frozen으로 설정된 경우, 구성 변경이 더 이상 허용되지 않습니다. 이는 약간의 최적화로 유용하며, 프록시 생성 후 호출자가 프록시를 조작(Advised 인터페이스를 통해)할 수 없도록 하려는 경우에도 유용합니다. 이 속성의 기본값은 false이므로, 추가 어드바이스 추가와 같은 변경이 허용됩니다.
  • exposeProxy: 현재 프록시를 ThreadLocal에 노출하여 타겟에서 액세스할 수 있도록 할지 여부를 결정합니다. 타겟이 프록시를 얻어야 하고 exposeProxy 속성이 true로 설정된 경우, 대상은 AopContext.currentProxy() 메서드를 사용할 수 있습니다.

ProxyFactoryBean에 고유한 다른 속성은 다음과 같습니다:

  • proxyInterfaces: String 타입의 인터페이스 이름들의 배열입니다. 이 속성이 제공되지 않으면 타겟 클래스에 대한 CGLIB 프록시가 사용됩니다(JDK- and CGLIB-based proxies도 참조).
  • interceptorNames: 적용할 어드바이저, 인터셉터 또는 기타 어드바이스 이름의 String 배열입니다. 순서는 중요하며, 먼저 나열된 인터셉터가 호출을 가로챌 수 있는 첫 번째 인터셉터입니다.

이름은 현재 팩토리의 빈 이름이며, 상위 팩토리의 빈 이름도 포함됩니다. 여기서 빈 참조를 언급할 수는 없습니다. 그렇게 하면 ProxyFactoryBean이 어드바이스의 싱글톤 설정을 무시하게 되기 때문입니다.

인터셉터 이름에 별표(*)를 추가할 수 있습니다. 이렇게 하면 별표 앞 부분과 일치하는 이름을 가진 모든 어드바이저 빈이 어드바이저 체인에 추가됩니다. 이 기능을 사용하는 예제는 “Global” 어드바이저 사용하기에서 찾을 수 있습니다.

  • singleton: getObject() 메서드가 호출될 때마다 동일한 객체를 반환할지 여부를 설정합니다. 여러 FactoryBean 구현에서 이 메서드를 제공합니다. 기본값은 true입니다. 상태 저장 어드바이스를 사용하려는 경우(예: 상태 저장 믹스인에 대해) singleton 값을 false로 설정하고 프로토타입 어드바이스를 함께 사용합니다.

JDK- and CGLIB-based proxies

이 섹션은 특정 타겟 객체에 대해 ProxyFactoryBean이 JDK 기반 프록시 또는 CGLIB 기반 프록시를 생성하는 방법에 대한 결정적 문서입니다.

Spring 버전 1.2.x에서 2.0으로 업그레이드되면서 ProxyFactoryBean의 JDK 또는 CGLIB 기반 프록시 생성에 관한 동작이 변경되었습니다. 이제 ProxyFactoryBean은 인터페이스 자동 감지와 관련하여 TransactionProxyFactoryBean 클래스와 유사한 동작을 합니다.
프록시할 타겟 객체의 클래스가 인터페이스를 구현하지 않는 경우, CGLIB 기반 프록시가 생성됩니다. 이는 JDK 프록시가 인터페이스 기반이므로 인터페이스가 없으면 JDK 프록시를 생성할 수 없기 때문에 가장 쉬운 시나리오입니다. 타겟 빈을 플러그인하고 interceptorNames 속성을 설정하여 인터셉터 목록을 지정할 수 있습니다. ProxyFactoryBeanproxyTargetClass 속성을 false로 설정해도 CGLIB 기반 프록시가 생성됩니다. (이렇게 설정하는 것은 의미가 없으므로 빈 정의에서 제거하는 것이 좋습니다. 그렇지 않으면 혼동을 초래할 수 있습니다.)

타겟 클래스가 하나 이상의 인터페이스를 구현하는 경우, 생성된 프록시의 유형은 ProxyFactoryBean의 구성에 따라 달라집니다.

  • ProxyFactoryBeanproxyTargetClass 속성을 true로 설정한 경우, CGLIB 기반 프록시가 생성됩니다. 이는 놀라지 않게 하는 원칙에 부합합니다. ProxyFactoryBeanproxyInterfaces 속성이 하나 이상의 인터페이스 이름으로 설정된 경우에도 proxyTargetClass 속성이 true로 설정되면 CGLIB 기반 프록시가 생성됩니다.
  • ProxyFactoryBeanproxyInterfaces 속성이 하나 이상의 인터페이스 이름으로 설정된 경우, JDK 기반 프록시가 생성됩니다. 생성된 프록시는 proxyInterfaces 속성에 지정된 모든 인터페이스를 구현합니다. 타겟 클래스가 이 속성에 지정된 것보다 훨씬 더 많은 인터페이스를 구현하는 경우에도 반환된 프록시는 추가 인터페이스를 구현하지 않습니다.
  • ProxyFactoryBeanproxyInterfaces 속성이 설정되지 않았지만 대상 클래스가 하나 이상의 인터페이스를 구현하는 경우, ProxyFactoryBean은 타겟 클래스가 실제로 하나 이상의 인터페이스를 구현했다는 사실을 자동 감지하고 JDK 기반 프록시를 생성합니다. 실제로 프록시된 인터페이스는 타겟 클래스가 구현하는 모든 인터페이스입니다. 이는 타겟 클래스가 구현하는 모든 인터페이스 목록을 proxyInterfaces 속성에 제공하는 것과 동일한 효과가 있습니다. 그러나 이는 훨씬 덜 번거롭고 오타 오류의 가능성이 적습니다.

Proxying Interfaces

ProxyFactoryBean의 간단한 사용 예를 살펴보겠습니다. 이 예제에는 다음이 포함됩니다:

  • 프록시된 대상 빈. 이 예제에서는 personTarget 빈 정의입니다.
  • 어드바이스를 제공하는 어드바이저와 인터셉터.
  • 대상 객체(personTarget 빈), 프록시할 인터페이스 및 적용할 어드바이스를 지정하는 AOP 프록시 빈 정의.

다음은 예제입니다:

<bean id="personTarget" class="com.mycompany.PersonImpl">
    <property name="name" value="Tony"/>
    <property name="age" value="51"/>
</bean>

<bean id="myAdvisor" class="com

.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor">
</bean>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <property name="target" ref="personTarget"/>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>
@Configuration
public class AppConfig {

    @Bean
    public PersonImpl personTarget() {
        PersonImpl person = new PersonImpl();
        person.setName("Tony");
        person.setAge(51);
        return person;
    }

    @Bean
    public MyAdvisor myAdvisor() {
        MyAdvisor advisor = new MyAdvisor();
        advisor.setSomeProperty("Custom string property value");
        return advisor;
    }

    @Bean
    public DebugInterceptor debugInterceptor() {
        return new DebugInterceptor();
    }

    @Bean
    public ProxyFactoryBean person() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(personTarget());
        proxyFactoryBean.setInterceptorNames("myAdvisor", "debugInterceptor");
        proxyFactoryBean.setProxyInterfaces(new Class<?>[]{Person.class});
        return proxyFactoryBean;
    }
}

interceptorNames 속성은 현재 팩토리에서 인터셉터 또는 어드바이저의 빈 이름을 포함하는 String 리스트를 받습니다. 어드바이저, 인터셉터, before, after returning, throws 어드바이스 객체를 사용할 수 있습니다. 어드바이저의 순서는 중요합니다.

리스트가 빈 참조를 가지지 않는 이유는 ProxyFactoryBeansingleton 속성이 false로 설정된 경우 독립된 프록시 인스턴스를 반환할 수 있어야 하기 때문입니다. 어드바이저 중 하나가 프로토타입인 경우 독립된 인스턴스를 반환해야 하므로 팩토리에서 프로토타입 인스턴스를 얻을 수 있어야 합니다. 참조를 보유하는 것만으로는 충분하지 않습니다.

이전에 보여준 person 빈 정의는 다음과 같이 Person 구현 대신 사용할 수 있습니다:

Person person = (Person) factory.getBean("person");

동일한 IoC 컨텍스트에 있는 다른 빈들은 일반 자바 객체처럼 강력하게 형식화된 의존성을 표현할 수 있습니다. 다음 예제는 그 방법을 보여줍니다:

<bean id="personUser" class="com.mycompany.PersonUser">
    <property name="person"><ref bean="person"/></property>
</bean>

이 예제에서 PersonUser 클래스는 Person 타입의 속성을 노출합니다. 이 클래스는 "실제" Person 구현 대신 AOP 프록시를 투명하게 사용할 수 있습니다. 그러나 이 클래스는 동적 프록시 클래스가 됩니다. Advised 인터페이스로 캐스팅할 수도 있습니다(나중에 논의됨).

익명 내부 빈을 사용하여 대상과 프록시 간의 구분을 숨길 수 있습니다. 오직 ProxyFactoryBean 정의만 다릅니다. 어드바이스는 완전성을 위해 포함됩니다. 다음 예제는 익명 내부 빈을 사용하는 방법을 보여줍니다:

<bean id="myAdvisor" class="com.mycompany.MyAdvisor">
    <property name="someProperty" value="Custom string property value"/>
</bean>

<bean id="debugInterceptor" class="org.springframework.aop.interceptor.DebugInterceptor"/>

<bean id="person" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="proxyInterfaces" value="com.mycompany.Person"/>
    <!-- Use inner bean, not local reference to target -->
    <property name="target">
        <bean class="com.mycompany.PersonImpl">
            <property name="name" value="Tony"/>
            <property name="age" value="51"/>
        </bean>
    </property>
    <property name="interceptorNames">
        <list>
            <value>myAdvisor</value>
            <value>debugInterceptor</value>
        </list>
    </property>
</bean>
@Configuration
public class AppConfig {

    @Bean
    public MyAdvisor myAdvisor() {
        MyAdvisor advisor = new MyAdvisor();
        advisor.setSomeProperty("Custom string property value");
        return advisor;
    }

    @Bean
    public DebugInterceptor debugInterceptor() {
        return new DebugInterceptor();
    }

    @Bean
    public ProxyFactoryBean person() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 익명 내부 빈 대신 자바 객체 직접 생성
        PersonImpl personTarget = new PersonImpl();
        personTarget.setName("Tony");
        personTarget.setAge(51);

        proxyFactoryBean.setTarget(personTarget);
        proxyFactoryBean.setInterceptorNames("myAdvisor", "debugInterceptor");
        proxyFactoryBean.setProxyInterfaces(new Class<?>[]{Person.class});
        return proxyFactoryBean;
    }
}

익명 내부 빈을 사용하는 장점은 Person 타입의 객체가 하나만 있다는 점입니다. 이는 애플리케이션 컨텍스트 사용자가 비어드바이스된 객체에 대한 참조를 얻지 못하게 하거나 Spring IoC 자동 연결에서 모호성을 방지하려는 경우 유용합니다. 또한 ProxyFactoryBean 정의가 자체 포함되어 있다는 점에서도 이점이 있을 수 있습니다. 그러나 팩토리에서 비어드바이스된 대상을 얻을 수 있는 것이 실제로 장점일 수 있는 경우도 있습니다(예: 특정 테스트 시나리오에서).

Proxying Classes

여러 인터페이스가 아닌 클래스를 프록시해야 하는 경우는 어떻게 해야 할까요?

이전 예제에서 Person 인터페이스가 없었고, 비즈니스 인터페이스를 구현하지 않는 Person이라는 클래스를 어드바이스해야 한다고 가정해 봅시다. 이 경우 Spring에서 동적 프록시 대신 CGLIB 프록시를 사용하도록 구성할 수 있습니다. 이를 위해 이전에 보여준 ProxyFactoryBeanproxyTargetClass 속성을 true로 설정하면 됩니다. 인터페이스보다 클래스에 프로그램하는 것이 더 좋지만, 인터페이스를 구현하지 않는 클래스를 어드바이스할 수 있는 능력은 레거시 코드 작업 시 유용할 수 있습니다. (일반적으로 Spring은 규범적이지 않습니다. Spring은 좋은 관행을 쉽게 적용할 수 있게 하지만 특정 접근 방식을 강요하지 않습니다.)

원한다면 인터페이스가 있는 경우에도 CGLIB 사용을 강제할 수 있습니다.

CGLIB 프록시는 타겟 클래스의 서브클래스를 런타임에 생성함으로써 작동합니다. Spring은 이 생성된 서브클래스를 구성하여 메서드 호출을 원래 타겟에 위임합니다. 서브클래스는 Decorator 패턴을 구현하여 어드바이스를 가미합니다.

CGLIB 프록시는 일반적으로 사용자에게 투명해야 합니다. 그러나 고려해야 할 몇 가지 문제가 있습니다:

  • final 클래스는 상속할 수 없으므로 프록시할 수 없습니다.
  • final 메서드는 오버라이드할 수 없으므로 어드바이스할 수 없습니다.
  • private 메서드는 오버라이드할 수 없으므로 어드바이스할 수 없습니다.
  • package-private 메서드와 같이 부모 클래스에서 다른 패키지의 메서드는 효과적으로 private이므로 어드바이스할 수 없습니다.

CGLIB를 클래스 경로에 추가할 필요는 없습니다. CGLIB는 리패키징되어 spring-core JAR에 포함됩니다. 즉, CGLIB 기반 AOP는 JDK 동적 프록시처럼 "바로 사용할 수 있습니다".
CGLIB 프록시와 동적 프록시 간의 성능 차이는 거의 없습니다. 이 경우 성능은 결정적인 고려 사항이 되지 않아야 합니다.

Using “Global” Advisors

인터셉터 이름에 별표를 추가하면 별표 앞 부분과 일치하는 이름을 가진 모든 어드바이저가 어드바이저 체인에 추가됩니다. 이는 표준 "Global" 어드바이저 세트를 추가해야 할 때 유용할 수 있습니다. 다음 예제는 두 개의 글로벌 어드바이저를 정의합니다:

<bean id="proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target" ref="service"/>
    <property name="interceptorNames">
        <list>
            <value>global*</value>
        </list>
    </property>
</bean>

<bean id="global_debug" class="org.springframework.aop.interceptor.DebugInterceptor"/>
<bean id="global_performance" class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor"/>
@Configuration
public class AppConfig {

    @Bean
    public Service service() {
        return new Service();
    }

    @Bean
    public DebugInterceptor globalDebug() {
        return new DebugInterceptor();
    }

    @Bean
    public PerformanceMonitorInterceptor globalPerformance() {
        return new PerformanceMonitorInterceptor();
    }

    @Bean
    public ProxyFactoryBean proxy() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(service());
        proxyFactoryBean.setInterceptorNames("globalDebug", "globalPerformance");
        return proxyFactoryBean;
    }
}