Spring/토비의 스프링

[토비의 스프링] 6.4 스프링의 프록시 팩토리 빈

한뜽규 2025. 6. 19. 23:42
728x90

이제 스프링에서 이런 프록시 설정을 어떻게 효율적으로 처리했는지 알아보자.

 

ProxyFactoryBean

스프링에서는 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다.

스프링의 ProxyFactoryBean은 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈이다.

ProxyFactoryBean이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 사용해 만든다.

기존에 사용했던 InvocationHandler과 비교하면 MethodInterceptor의 invoke() 메서드는 파리미터를 통해 타깃 오브젝트의 정보도 함께 받는다.

그렇기 때문에 InvocationHandler처럼 타깃 오브젝트를 미리 받아서 그거로 실행하는 것이 아닌, invoke()로 정보를 넘겨 받아서 실행이 가능하다.

 

public class DynamicProxyTest {

    @Test
    public void proxyFactoryBean(){
        ProxyFactoryBean proxyBean = new ProxyFactoryBean();
        proxyBean.setTarget(new HelloImpl());
        proxyBean.addAdvice(new UpperCaseAdvisor());

        Hello proxy = (Hello) proxyBean.getObject();

        Assertions.assertEquals("HELLO SEUNGKYU", proxy.sayHello("seungkyu"));
    }

    static interface Hello {
        String sayHello(String name);
        String sayHi(String name);
        String sayBye(String name);
    }

    static class HelloImpl implements Hello {
        @Override
        public String sayHello(String name) {
            return "Hello " + name;
        }

        @Override
        public String sayHi(String name) {
            return "Hi " + name;
        }

        @Override
        public String sayBye(String name) {
            return "Bye " + name;
        }
    }

    static class UpperCaseAdvisor implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            if(invocation.getMethod().getReturnType().equals(String.class)){
                String returnValue = (String) invocation.proceed();
                if (returnValue != null) {
                    return returnValue.toUpperCase();
                }
                else
                    return null;
            }
            else
                return invocation.proceed();
        }
    }
}

기존의 InvocationHandler와 비교하면

이렇게 실행을 위한 target 오브젝트가 존재하지 않는다.

오브젝트 정보 자체도 MethodInvocation에 존재하기 때문에 이런 부분을 신경쓰지 않아도 된다.

그리고 이 MethodInvocation의 proceed() 메서드로 호출된 메서드를 실행하기에, 기존의 InvocationHandler가 템플릿이 되었다고 생각하면 된다.

그리고 오브젝트에 독립적이기 때문에 JdbcTemplate처럼 싱글톤으로 만들어서 공유가 가능하다.

또한, 이렇게 어떤 인터페이스를 구현했는지의 정보를 넘기지 않는다.

타깃만 등록해도 인터페이스의 정보를 알아내고, 그 모든 인터페이스를 구현하는 프록시를 만들어준다고 한다.

 

기존에는 이렇게 pattern을 사용해, 패턴에 일치하는 메서드만 프록시를 적용했다.

이런 식으로 일단 호출된 모든 메서드를 invoke()에서 패턴을 확인해 실행할지 말지 결정하는 것이다.

(중간 그림은 Dynamic proxy가 아니라 InvocationHandler입니다...)

 

하지만 이번에는 메서드를 선별하는 어떤 정보도 넘기고 있지 않다.

MethodInterceptor는 이렇게 모든 메서드를 invoke()로 넘기지 않고 메서드 선정 알고리즘을 통해 선정한 후, 통과한 메서드만 프록시로 요청하도록 만든다.

이렇게 프록시 과정에서 메서드를 선정하는 오브젝트를 포인트컷이라고 하고, 부가기능을 제공하는 오브젝트를 어드바이스라고 한다.

 

이런식으로 모두 넘기는 것이 아니라 point cut에서 체크한 후 invocation callback을 호출한다.

해당 프록시는 공유되기 때문에 기존처럼 target을 가질 수 없다.

그렇기에 methodInvocation으로 타깃에 대한 정보를 같이 넘기고 callback으로 메서드를 호출하는 것이다.

그렇기에 해당 어드바이스를 템플릿으로 사용하는 것이다.

 

이렇게 사용하는 포인트컷과 어드바이스는 모두 DI로 주입해서 사용한다.

    @Test
    public void pointCutAdvisor(){
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(new HelloImpl());

        NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut();
        nameMatchMethodPointcut.setMappedName("say*");

        proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(nameMatchMethodPointcut, new DynamicProxyTest.UpperCaseAdvisor()));

        Hello proxyHello = (Hello) proxyFactoryBean.getObject();

        Assertions.assertEquals("HELLO SEUNGKYU", proxyHello.sayHello("seungkyu"));
        Assertions.assertEquals("HI SEUNGKYU", proxyHello.sayHi("seungkyu"));
        Assertions.assertEquals("BYE SEUNGKYU", proxyHello.sayBye("seungkyu"));
    }

NameMatchMethodPointCut을 이용해서 필터링했다.

setMappedName에 prefix와 *을 붙여서 적용할 메서드를 선정하고, proxyFactoryBean으로 넘겨준다.

이미 PointCut 자체는 많이 있어서, 그 중에서 골라서 사용하라고 하긴 한다.

 

또한 현재 어드바이스를 등록하기 위해 포인트컷과 어드바이스를 같이 등록하고 있다.

proxyFactoryBean에는 여러개의 어드바이스를 등록 할 수 있기 때문에, 그에 맞는 포인트컷을 조합해서 올리도록 하는 것이다.

이렇게 부가기능이 어드바이스, 포인트컷이 선정 알고리즘이고 이 둘을 합친 것이 어드바이저가 된다.

 

ProxyFactoryBean 적용

이거까지만 본다면 InvocationHandler와 MethodInterceptor가 뭐가 더 나은지 잘 모르겠다.

직접 만들어서 적용해보도록 하자.

타깃과 메서드를 정해주는 부분을 제거하면 된다.

 

@RequiredArgsConstructor
public class TransactionAdvice implements MethodInterceptor {

    private final PlatformTransactionManager transactionManager;

    @Override
    public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
        TransactionStatus transactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

        try
        {
            Object ret = invocation.proceed();
            this.transactionManager.commit(transactionStatus);
            return ret;
        }
        catch (Exception e)
        {
            transactionStatus.setRollbackOnly();
            throw e;
        }
    }
}

우선 이렇게 작성 할 수 있으며, 타깃과 메서드 선정 알고리즘이 사라진 것을 볼 수 있다.

 

이렇게만 해주고, xml만 변경하면 끝이다.

    <!--transaction manager를 가지고 있는 어드바이스를 생성-->
    <bean id="transactionAdvice" class="seungkyu.TransactionAdvice">
        <constructor-arg ref="transactionManager"/>
    </bean>

    <!--메서드 선정 알고리즘을 가지는 point cut 생성-->
    <bean id="transactionPointCut" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="upgrade*"/>
    </bean>

    <!--어드바이스와 포인트컷을 사용해 어드바이저 생성-->
    <bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <constructor-arg ref="transactionAdvice"/>
        <constructor-arg ref="transactionPointCut"/>
    </bean>

    <!--ProxyFactoryBean을 사용해서 트랜잭션이 적용된 UserService 빈을 생성-->
    <bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="userServiceImpl"/>
        <property name="interceptorNames">
            <list>
                <value>transactionAdvisor</value>
            </list>
        </property>
    </bean>

 

이렇게 ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화의 기법이 모두 적용된 것이다.

그렇기에 독립적으로 동작하며, 여러 프록시가 공유할 수 있는 어드바이스와 포틴으컷으로 확장 기능을 분리 할 수 있었다.