Spring AOP APIs 3[Concise Proxy Definitions, Creating AOP Proxies Programmatically with the ProxyFactory, Manipulating Advised Objects, Using the "auto-proxy" facility]

2023. 5. 3. 15:11Spring Framework/Spring AOP

Concise Proxy Definitions

특히 트랜잭션 프록시를 정의할 때, 유사한 프록시 정의가 많이 생길 수 있습니다. 부모-자식 빈 정의와 내부 빈 정의를 사용하면 훨씬 깔끔하고 간결한 프록시 정의를 만들 수 있습니다.

먼저, 다음과 같이 프록시를 위한 부모(템플릿) 빈 정의를 생성합니다:

XML 기반 구성:

<bean id="txProxyTemplate" abstract="true"
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
    <property name="transactionAttributes">
        <props>
            <prop key="*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

자바 기반 구성:

@Configuration
public class AppConfig {

    @Bean
    public TransactionProxyFactoryBean txProxyTemplate(TransactionManager transactionManager) {
        TransactionProxyFactoryBean proxyFactoryBean = new TransactionProxyFactoryBean();
        proxyFactoryBean.setTransactionManager(transactionManager);

        Properties transactionAttributes = new Properties();
        transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
        proxyFactoryBean.setTransactionAttributes(transactionAttributes);

        // 이 Bean은 템플릿이므로 추상적으로 처리됩니다.
        proxyFactoryBean.setProxyTargetClass(true);

        return proxyFactoryBean;
    }
}

이 빈은 실제로 인스턴스화되지 않으므로 불완전할 수도 있습니다. 그런 다음, 생성해야 하는 각 프록시는 자식 빈 정의로 생성되며, 프록시의 대상은 어차피 독립적으로 사용되지 않으므로 내부 빈 정의로 감쌉니다.

XML 기반 구성:

<bean id="myService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MyServiceImpl">
        </bean>
    </property>
</bean>

자바 기반 구성:

@Bean
public TransactionProxyFactoryBean myService(TransactionManager transactionManager) {
    TransactionProxyFactoryBean proxyFactoryBean = new TransactionProxyFactoryBean();
    proxyFactoryBean.setTransactionManager(transactionManager);

    // 대상 설정
    MyServiceImpl target = new MyServiceImpl();
    proxyFactoryBean.setTarget(target);

    // 부모 템플릿 설정 사용
    Properties transactionAttributes = new Properties();
    transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
    proxyFactoryBean.setTransactionAttributes(transactionAttributes);

    return proxyFactoryBean;
}

부모 템플릿의 속성을 재정의할 수도 있습니다. 다음 예제에서는 트랜잭션 전파 설정을 재정의합니다:

XML 기반 구성:

<bean id="mySpecialService" parent="txProxyTemplate">
    <property name="target">
        <bean class="org.springframework.samples.MySpecialServiceImpl">
        </bean>
    </property>
    <property name="transactionAttributes">
        <props>
            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>
            <prop key="store*">PROPAGATION_REQUIRED</prop>
        </props>
    </property>
</bean>

자바 기반 구성:

@Bean
public TransactionProxyFactoryBean mySpecialService(TransactionManager transactionManager) {
    TransactionProxyFactoryBean proxyFactoryBean = new TransactionProxyFactoryBean();
    proxyFactoryBean.setTransactionManager(transactionManager);

    // 대상 설정
    MySpecialServiceImpl target = new MySpecialServiceImpl();
    proxyFactoryBean.setTarget(target);

    // 트랜잭션 속성 재정의
    Properties transactionAttributes = new Properties();
    transactionAttributes.setProperty("get*", "PROPAGATION_REQUIRED,readOnly");
    transactionAttributes.setProperty("find*", "PROPAGATION_REQUIRED,readOnly");
    transactionAttributes.setProperty("load*", "PROPAGATION_REQUIRED,readOnly");
    transactionAttributes.setProperty("store*", "PROPAGATION_REQUIRED");
    proxyFactoryBean.setTransactionAttributes(transactionAttributes);

    return proxyFactoryBean;
}

부모 빈 예제에서 abstract 속성을 true로 설정하여 부모 빈 정의를 명시적으로 추상적으로 표시한 것에 주목하세요. 이는 이 빈이 실제로 인스턴스화되지 않도록 하기 위함입니다. 자바 기반 구성에서는 이 부분을 코드로 직접 처리하며, 특정 클래스를 템플릿으로만 사용하려는 경우에도 마찬가지로 설정할 수 있습니다.

Creating AOP Proxies Programmatically with the ProxyFactory

Spring을 사용하면 AOP 프록시를 프로그래밍 방식으로 쉽게 생성할 수 있습니다. 이를 통해 Spring IoC에 의존하지 않고도 Spring AOP를 사용할 수 있습니다.

대상 객체가 구현한 인터페이스는 자동으로 프록시됩니다. 아래 예시는 하나의 인터셉터와 하나의 어드바이저를 사용하여 대상 객체에 대한 프록시를 생성하는 방법을 보여줍니다:

ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl);
factory.addAdvice(myMethodInterceptor);
factory.addAdvisor(myAdvisor);
MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy();

첫 번째 단계는 org.springframework.aop.framework.ProxyFactory 타입의 객체를 생성하는 것입니다. 이전 예제처럼 대상 객체를 사용하여 이를 생성하거나, 다른 생성자를 사용하여 프록시할 인터페이스를 지정할 수 있습니다.

프록시팩토리(ProxyFactory)의 수명 동안 어드바이스(특화된 형태의 어드바이스로 인터셉터 포함)나 어드바이저, 또는 둘 다 추가하고 조작할 수 있습니다. 만약 IntroductionInterceptionAroundAdvisor를 추가하면 프록시가 추가적인 인터페이스를 구현하도록 할 수 있습니다.

ProxyFactory(AdvisedSupport에서 상속됨)에는 before 어드바이스와 throws 어드바이스와 같은 다른 어드바이스 타입을 추가할 수 있는 편의 메서드도 있습니다. AdvisedSupport는 ProxyFactoryProxyFactoryBean의 슈퍼클래스입니다.

대부분의 애플리케이션에서는 IoC 프레임워크와 AOP 프록시 생성을 통합하는 것이 최선의 방법입니다. 일반적으로 AOP와 함께 자바 코드의 구성을 외부화하는 것을 권장합니다.

Manipulating Advised Objects

AOP 프록시를 생성하는 방법에 관계없이, org.springframework.aop.framework.Advised 인터페이스를 사용하여 프록시를 조작할 수 있습니다. AOP 프록시는 구현한 다른 인터페이스에 관계없이 이 인터페이스로 캐스팅할 수 있습니다. 이 인터페이스에는 다음과 같은 메서드들이 포함되어 있습니다:

Advisor[] getAdvisors();

void addAdvice(Advice advice) throws AopConfigException;

void addAdvice(int pos, Advice advice) throws AopConfigException;

void addAdvisor(Advisor advisor) throws AopConfigException;

void addAdvisor(int pos, Advisor advisor) throws AopConfigException;

int indexOf(Advisor advisor);

boolean removeAdvisor(Advisor advisor) throws AopConfigException;

void removeAdvisor(int index) throws AopConfigException;

boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException;

boolean isFrozen();

getAdvisors() 메서드는 팩토리에 추가된 어드바이저, 인터셉터, 또는 다른 어드바이스 타입에 대한 Advisor 객체를 반환합니다. 만약 어드바이저를 추가했다면, 해당 인덱스에서 반환된 어드바이저는 추가한 객체입니다. 인터셉터나 다른 어드바이스 타입을 추가했다면, Spring은 이를 포인트컷이 항상 true를 반환하는 어드바이저로 감쌉니다. 예를 들어, MethodInterceptor를 추가한 경우, 해당 인덱스에서 반환된 어드바이저는 DefaultPointcutAdvisor이며, 이 어드바이저는 MethodInterceptor와 모든 클래스 및 메서드에 매칭되는 포인트컷을 반환합니다.

addAdvisor() 메서드는 어떤 어드바이저도 추가하는 데 사용할 수 있습니다. 일반적으로 포인트컷과 어드바이스를 보유하는 어드바이저는 DefaultPointcutAdvisor이며, 이는 어떤 어드바이스나 포인트컷과도 사용할 수 있습니다(도입(introductions) 제외).

기본적으로, 프록시가 생성된 후에도 어드바이저나 인터셉터를 추가하거나 제거할 수 있습니다. 단, 도입 어드바이저를 추가하거나 제거하는 것은 불가능한데, 이는 팩토리에서 생성된 기존 프록시가 인터페이스 변경을 반영하지 않기 때문입니다(이 문제를 피하려면 팩토리에서 새 프록시를 얻을 수 있습니다).

다음 예제는 AOP 프록시를 Advised 인터페이스로 캐스팅한 후, 그 어드바이스를 검사하고 조작하는 방법을 보여줍니다:

Advised advised = (Advised) myObject;
Advisor[] advisors = advised.getAdvisors();
int oldAdvisorCount = advisors.length;
System.out.println(oldAdvisorCount + " advisors");

// 포인트컷 없이 인터셉터와 같은 어드바이스 추가
// 모든 프록시된 메서드에 매칭됨
// 인터셉터, before, after returning, throws 어드바이스에 사용할 수 있음
advised.addAdvice(new DebugInterceptor());

// 포인트컷을 사용하여 선택적인 어드바이스 추가
advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice));

assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length);

프로덕션 환경에서 비즈니스 객체의 어드바이스를 수정하는 것이 바람직한지는 의문이지만, 분명히 정당한 사용 사례가 있을 수 있습니다. 그러나 개발 중(예: 테스트 시)에는 매우 유용할 수 있습니다. 메서드 호출을 테스트하기 위해 인터셉터나 다른 어드바이스 형태로 테스트 코드를 추가할 수 있는 기능은 매우 유용합니다. (예를 들어, 어드바이스가 해당 메서드에 대해 생성된 트랜잭션 내부에서 SQL을 실행하여 데이터베이스가 올바르게 업데이트되었는지 확인하고, 트랜잭션을 롤백하도록 설정할 수 있습니다.)

프록시를 생성한 방법에 따라, 일반적으로 frozen 플래그를 설정할 수 있습니다. 이 경우, AdvisedisFrozen() 메서드는 true를 반환하며, 어드바이스를 추가하거나 제거하려는 시도는 AopConfigException을 발생시킵니다. 어드바이스된 객체의 상태를 고정(freeze)하는 기능은 일부 경우(예: 호출 코드가 보안 인터셉터를 제거하지 못하도록 방지하는 경우)에 유용할 수 있습니다.

Using the "auto-proxy" facility

지금까지 우리는 ProxyFactoryBean 또는 유사한 팩토리 빈을 사용하여 명시적으로 AOP 프록시를 생성하는 방법을 살펴보았습니다.

Spring에서는 선택된 빈 정의를 자동으로 프록시할 수 있는 "auto-proxy" 빈 정의를 사용할 수도 있습니다. 이는 컨테이너가 로드될 때 모든 빈 정의를 수정할 수 있는 Spring의 "bean post processor" 인프라에 기반합니다.

이 모델에서는 XML 빈 정의 파일에서 몇 가지 특별한 빈 정의를 설정하여 auto-proxy 인프라를 구성합니다. 이를 통해 자동 프록시 대상 자격이 있는 객체를 선언할 수 있으며, ProxyFactoryBean을 사용할 필요가 없습니다.

이를 수행하는 두 가지 방법이 있습니다:

  1. 현재 컨텍스트에 있는 특정 빈을 참조하는 auto-proxy 생성기를 사용하는 방법.
  2. 소스 레벨 메타데이터 속성에 의해 구동되는 auto-proxy 생성의 특별한 경우.

Auto-proxy 빈 정의

이 섹션에서는 org.springframework.aop.framework.autoproxy 패키지에서 제공하는 auto-proxy 생성기에 대해 설명합니다.

BeanNameAutoProxyCreator

BeanNameAutoProxyCreator 클래스는 빈 이름이 리터럴 값이나 와일드카드와 일치하는 빈에 대해 자동으로 AOP 프록시를 생성하는 BeanPostProcessor입니다. 다음 예제는 BeanNameAutoProxyCreator 빈을 생성하는 방법을 보여줍니다:

XML 기반 구성:
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="beanNames" value="jdk*,onlyJdk"/>
    <property name="interceptorNames">
        <list>
            <value>myInterceptor</value>
        </list>
    </property>
</bean>
자바 기반 구성:
@Configuration
public class AppConfig {

    @Bean
    public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
        BeanNameAutoProxyCreator creator = new BeanNameAutoProxyCreator();
        creator.setBeanNames("jdk*", "onlyJdk");
        creator.setInterceptorNames("myInterceptor");
        return creator;
    }

    @Bean
    public MyInterceptor myInterceptor() {
        return new MyInterceptor();
    }
}

위의 자바 기반 구성에서는 BeanNameAutoProxyCreator를 정의하고, 빈 이름 패턴에 맞는 빈들에 대해 자동으로 프록시를 생성합니다. interceptorNames 속성은 XML 기반 구성에서와 마찬가지로 설정됩니다.

DefaultAdvisorAutoProxyCreator

더 일반적이고 매우 강력한 auto-proxy 생성기는 DefaultAdvisorAutoProxyCreator입니다. 이는 현재 컨텍스트에서 적격한 어드바이저를 자동으로 적용하며, auto-proxy 어드바이저의 빈 정의에 특정 빈 이름을 포함할 필요가 없습니다. 이는 BeanNameAutoProxyCreator와 마찬가지로 일관된 구성을 제공하며 중복을 피할 수 있는 장점이 있습니다.

XML 기반 구성:
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

<bean class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
    <property name="transactionInterceptor" ref="transactionInterceptor"/>
</bean>

<bean id="customAdvisor" class="com.mycompany.MyAdvisor"/>

<bean id="businessObject1" class="com.mycompany.BusinessObject1">
    <!-- Properties omitted -->
</bean>

<bean id="businessObject2" class="com.mycompany.BusinessObject2"/>
자바 기반 구성:
@Configuration
public class AppConfig {

    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        return new DefaultAdvisorAutoProxyCreator();
    }

    @Bean
    public TransactionAttributeSourceAdvisor transactionAdvisor(TransactionInterceptor transactionInterceptor) {
        TransactionAttributeSourceAdvisor advisor = new TransactionAttributeSourceAdvisor();
        advisor.setTransactionInterceptor(transactionInterceptor);
        return advisor;
    }

    @Bean
    public MyAdvisor customAdvisor() {
        return new MyAdvisor();
    }

    @Bean
    public BusinessObject1 businessObject1() {
        return new BusinessObject1();
    }

    @Bean
    public BusinessObject2 businessObject2() {
        return new BusinessObject2();
    }

    @Bean
    public TransactionInterceptor transactionInterceptor() {
        // TransactionInterceptor 설정 로직
        return new TransactionInterceptor();
    }
}

위의 자바 기반 설정에서는 DefaultAdvisorAutoProxyCreator를 빈으로 등록하여 모든 적격 어드바이저가 자동으로 프록시를 적용할 수 있도록 합니다. 트랜잭션 어드바이저 및 커스텀 어드바이저도 별도로 설정하며, 비즈니스 객체는 필요에 따라 프록시가 생성됩니다.

이 자바 기반 설정은 XML 구성에서 설정했던 것과 동일한 기능을 제공합니다. 이를 통해 Spring IoC 컨텍스트에서 빈들이 자동으로 프록시되며, 이와 관련된 구성을 깔끔하게 유지할 수 있습니다.