본문 바로가기

IT

스프링 트랜잭션 (Spring Transaction) step4 : Spring ProxyFactoryBean

728x90

기존 JDK 다이내믹 프록시를 이용한 방식

JDK 다이내믹 프록시 생성 예제와 스프링에서 제공하는 ProxyFactoryBean의 차이를 살펴보자.

//JDK 다이내믹 프록시 생성
public void simpleProxyTest{
	Hello proxiedHello = (Hello)Proxy.newProxyInstance(
	getClass().getClassLoader(), -> 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용될 클래스 로더
    new Class[]{ Hello.class }, -> 구현할 인터페이스
    new UppercaseHandler(new HelloTarget()) -> 부가 기능과 위임 코드를 담은 InvocationHandler);
    ...
}

//스프링의 ProxyFactoryBean 사용
public void proxyFactoryBean(){
	ProxyFactoryBean pfBean = new ProxyFactoryBean();
    pfBean.setTarget(new HelloTarget());
    pfBean.addAdvice(new UppercaseAdvide();
   
   	Hello proxiedHello = (Hello) pfBean.getObject();
}

static class UppercaseAdvice implements MethodInterceptor{
	public Object invoke(MethodInvoaction invacation) throws Throwable{
    // 리플렉션의 Method와 달리 메소드 실행 시 타깃 오브젝트를 전달할 필요가 없다. 
    // MethodInvacation은 메소드 정보와 함께 타깃 오브젝트를 알고 있다.
    	String ret = (String) invocation.proceed();
        return ret.toUppercase(); -> 부가기능 적용
    }
}

MethodInterceptor를 구현한 UppercaseAdvice는 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달된다.

MethodInvocation은 타깃 오브젝트의 메소드를 실행할 수 있는 기능이 있기 때문에 MethodInterceptor는 부가기능을 제공하는 데만 집중할 수 있다. MethodInvocation은 일종의 콜백 오브젝트로 proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다. 일종의 공유 가능한 템플릿처럼 동작하는 것이다. 이것이 JDK의 다이내믹 프록시를 직접 사용하는 것과의 차이다. 이 뿐만 아니라 제공해줘야할 인터페이스 정보도 ProxyFactoryBean에 있는 인터페이스 자동검출 기능을 사용해 따로 추가할 필요가 없다.

 

 MethodInterceptor는 Advice 인터페이스를 상속하고 있기 때문에 addAdvice로 추가한다. 이름에서 알 수 있듯이 MethodInterceptor처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 어드바이스(Advice)라고 부른다. 

 

기존에 InvacationHandler를 직접 구현했을 때 메소드 이름(pattern)을 가지고 적용 대상 메소드를 선정했다. 스프링 ProxyFactoryBean은 포인트컷(메소드 선정 알고리즘)을 제공한다. 포인트컷과 어드바이스는 여러 프록시에서 공유가 가능하도록 만들어지기 때문에 싱글톤 빈으로 등록이 가능하다. 

 

프록시는 클라이언트로부터 요청을 받으면 먼저 포인트컷에게 부가기능을 부여할 메소드인지 확인을 한 이후 어드바이스를 호출한다. 어드바이스는 JDK의 다이내믹 프록시의 Interceptor와 달리 타깃을 직접 호출하지 않는다. 자신이 공유되어야 하므로 타깃 정보라는 상태를 가질 수 없다. 따라서 타깃에 직접 의존하지 않도록 일종의 템플릿 구조로 설계되어 있다. 어드바이스가 부가기능을 부여하는 중에 타깃 메소드의 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출해주기만 하면 된다. 

 

실제 위임 대상인 타깃 오브젝트의 레퍼런스를 가지고 있고 이를 이용해 타깃 메소드를 직접 호출하는 것은 프록시가 메소드 호출에 따라 만드는 Invocation 콜백의 역할이다. 재사용 가능한 기능을 만들어두고 바뀌는 부분(콜백 오브젝트와 메소드 호출정보)만 외부에서 주입해서 이를 작업 흐름(부가기능 부여)중에 사용하도록 하는 전형적인 템플릿/콜백 구조이다. 

스프링 ProxyFactoryBean을 이용한 방식

포인트컷까지 적용한 ProxyFactoryBean의 예제 코드를 보자.

ProxyFactoryBean bfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());

// 메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 생성
NameMatchPointcut pointcut = new NameMatchPointcut();
pointcut.setMappedName("say*");

//포인트컷과 어드바이스를 Advisor로 묶어서 한번에 추가
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));

Hello proxiedHello = (Hello) pfBean.getObject();

프록시로부터 어드바이스와 포인트컷을 독립시키고 DI를 사용하게 한 것은 전략 패턴 구조다. 덕분에 여러 프록시에서 공유해서 사용할 수 있고 프록시와 ProxyFactoryBean 등의 변경 없이도 기능을 자유롭게 확장할 수 있는 OCP를 지키는 구조이다. 

ProxyFactoryBean에는 여러 개의 포인트컷과 어드바이스가 추가되기에 유연하도록 Advisor 타입의 오브젝트에 담아서 조합을 만들어서 등록할 수 있다. 이렇게 어드바이스와 포인트컷을 묶은 오브젝트를 인터페이스 이름을 따서 어드바이저 라고 한다.

어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)

 

이제 JDK 다이내믹 프록시의 구조를 그대로 이용해서 만들었던 TxProxyBean을 스프링의 ProxyFactoryBean을 이용하도록 수정해보자.

public class TransactionAdvice implements MethodInterceptor{
	PlatformTransactionManager transactionManager;
    
    public void setTransactionManager(PlatformTransactionManager transactionManager){
    	this.transactionManager = transactionManager;
    }
    
    public Object invoke(MethodInvocation invocation) throws Throwable{
    	TransactionStatus status = this.transactionManager.getTransaction(new DefalutTransactionDefinition());
        try{
        /**
        콜백을 호출해서 타깃의 메소드를 실행한다.
        타깃 메소드 호출 전후로 필요한 부가기능을 넣을 수 있다.
        경우에 따라서 타깃이 아예 호출되지 않게 하거나 재시도를 위한 반복적인 호출도 가능하다.
        **/
        	Object ret = invocation.proceed();
            this.transactionManager.commit(status);
            return ret;
        }catch (RuntimeException e){
        /**
        JDK 다이내믹 프록시가 제공하는 Method와는 달리 스프링의 MethodInvocation을 통한 타깃호출은
        예외가 포장되지 않고 타깃에서 보낸 그대로 전달된다.
        **/
        	this.transactionManager.rollback(status);
            throw e;
        }
    }
}

직접 DI해서 사용했던 코드를 XML 설정을 바꿔주도록 하자.

<bean id = "transactionAdvice" class = "package.example.TransactionAdvice">
	<property name = "transactionManager" ref = "transactionManager" />
</bean>

<bean id = "transactionPointcut" class = "springframework.aop.support.NameMatchMethodPoincut">
	<property name = "mappedName" value = "upgrade*" />
</bean>

<bean id = "transactionAdvisor" class = "springframework.aop.support.DefalutPointcutAdvisor">
	<property name = "advice" ref = "transactionAdvice" />
    <property name = "pointcut" ref = "transactionPointcut" />
</bean>

<bean id = "userService" class = "springframework.aop.framework.ProxyFactoryBean">
	<property name = "target" ref = "userServiceImpl" />
    <property name = "interceptorNames">
    	<list>
        	<value>transactionAdvisor</value>
        </list>
    </property>
</bean>

리플렉션을 통한 타깃 메소드 호출 작업의 번거로움은 MethodInvocation 타입의 콜백을 이용한 덕분에 대부분 제거할 수 있다. 

이제 한번 생성한 TransactionAdvice를 새로운 포인트컷과 조합을 해서 그대로 가져다 쓸 수 있는 아주 편리한 상황까지 왔다. 마찬가지로 부가기능을 만들어도 모든 타깃과 메소드에 재사용이 가능하고 타깃의 적용 메소드를 선정하는 방식도 독립적으로 작성할 수 있도록 분리되었다. 

 

프록시팩토리 빈 방식의 문제 중 부가기능이 타깃 오브젝트마다 새로 만들어지는 문제는 스프링 ProxyFactoryBean의 어드바이스를 통해 해결했다. TransactionHandler 오브젝트가 계속 생성되는 문제도 해결했다. 하지만 부가기능의 적용이 필요한 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFactoryBean 빈 설정 정보를 추가해주는 부분은 개선이 필요하다. 새로운 타깃이 등장했다고 해서 코드를 손댈 필요는 없어졌지만 설정은 매번 복붙하고 target 프로퍼티의 내용을 수정해주어야 한다. target 프로퍼티를 제외하면 빈 클래스의 종류, 어드바이스, 포인트컷의 설정이 동일하다. 이런 류의 중복을 제거할 필요가 있다. 

 

애초에 실제 빈 오브젝트가 되는 것은 ProxyFactoryBean을 통해 생성되는 프록시 그 자체이다. 타깃 메소드의 역할이 필요할 때는 단지 위임하고 부가 기능에 대해서 이 프록시에게 역할을 부여하는 구조인 셈이다.

그렇다면 목적이 다른 부가 기능을 대입하기 위해서 기존의 userService를 위한 다이내믹 프록시를 계속 생성해주어야 하며 orderService라면 이를 위한 다이내믹 프록시가 필요하다. 필자는 나의 목적이 기존 도메인인가 proxy인가 헷갈려지기 시작했다. 그렇다면 새로운 타깃은 계속 생겨날 것을 고민해본다면 타깃에 추가에 따른 프록시 빈을 자동으로 생성해주는 방법은 없을까?

 

다음 step5에서 이를 해결하는 방법을 소개하려고 한다. 

 

 

 

 

 

 

 

참고 자료 - 토비의 스프링 3.1 | 저자 이일민