TIL

6.3 다이내믹 프록시와 팩토리 빈

6.3.1 프록시와 프록시 패턴, 데코레이터 패턴

트랜잭션 코드를 분리했던 기법을 살펴보면 클라이언트는 인터페이스만 보고 UserService를 사용하기에 핵심 기능을 가진 클래스를 사용할 것이라고 기대하지만 사실은 부가 기능이 적용된 클래스를 사용하게 된다.

프록시

이렇게 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트 요청을 대신 받아주는 것을 프록시라고 부른다. 프록시를 통해 최종적으로 요청을 처리하는 실제 오브젝트는 타겟(target)이라고 부른다. 프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있다는 것이다.

프록시 사용 목적에 따라 두 가지로 구분 가능한데 프록시를 사용한다는 점은 동일하지만 목적에 따라 다른 디자인 패턴으로 구분한다.

데코레이터 패턴

데코레이터 패턴은 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여하기 위해 프록시를 사용하는 패턴을 말한다. 다이내믹하게 연결하기 때문에 컴파일 시점(코드 레벨)에서 프록시와 타깃의 연결 정보를 파악할 수 없다.

데코레이터 패턴에선 프록시가 한 개로 제한되지 않고 프록시가 직접 타깃을 사용하도록 고정시킬 필요도 없는데 이는 같은 인터페이스를 구현한 타겟과 여러 프록시로 구현 가능하다. 각 데코레이터(프록시)는 위임하는 대상에도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 다음 데코레이터 프록시로 위임하는지 알지 못한다.

자바 IO 패키지의 InputStreamOutputStrem 구현 클래스는 데코레이터 패턴이 적용된 대표적인 예다.

InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));

스프링 DI를 이용하면 런타임 시 다이내믹하게 데코레이터를 주입할 수 있다. 데코레이터 패턴은 타깃의 코드와 클라이언트가 호출하는 방법을 변경하지 않고 새로운 기능을 추가할 때 유용하다.

프록시 패턴

프록시라는 용어와 프록시 패턴은 구분할 필요가 있다.

6.3.2 다이내믹 프록시

프록시는 유용하지만 프록시를 직접 만드는 일은 번거롭다.

프록시의 구성과 프록시 작성의 문제점

프록시는 다음 두 가지 기능으로 구성된다.

프록시를 만들기 번거로운 이유는 다음의 두 가지가 있다.

위 문제를 리플렉션을 이용한 JDK 다이내믹 프록시를 통해 해결할 수 있다.

다이내믹 프록시 적용

다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 동적으로 만들어지는 오브젝트다.

public interface InvocationHandler {
	Object invoke(Object proxy, Method method, Object[] args);
}
Hello proxieHello = (Hello)Proxy.newProxyInstance(
	getClass().getClassLoader(),
	new Class[] {Hello.class},
	new UppercaseHandler(new HelloTarget()) // 부가 기능을 담은 InvacationHandler
);

사용 방법을 자세히 살펴 보자.

6.3.3 다이내믹 프록시를 이용한 트랜잭션 부가 기능

트랜잭션 InvocationHandler

public class TransactionHandler implements InvocationHandler {
	private final Object target;
	private final PlatformTransactionManager transactionManager;
	private String pattern;

	public TransactionHandler(Object target, PlatformTransactionMamager transactionManager, String pattern) {
		this.target = target;
		this.transactionManager = transactionManager;
		this.pattern = pattern;
	}

	@Override
	public Object invoke(Object proxy, Method method, Object[] args} throws Throwable {
		if (method.getName().startWith(pattern) {
			return invokeInTransaction(method, args);
		}
		return method.invoke(target,args);
	}

	private Object invokeInTransaction(Method method, Object[] args) throws Throwable {
		TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
		try {
			Object result = method.invoke(target,args);
			transactionManager.commit(status);
			return result;
		} catch(InvocationTargetException e) {
			transactionManager.rollack(status);
			throw e.getTargetException();
		}
	}
}
// ...

UserService txUserService = (UserService)Proxy.newProxyInstance(
	getClass().getClassLoader(),
	new Class[] {UserService.class},
	new TransactionHandler(targetUserService, transactionManager, "upgradeLevels")
);

6.3.4 다이내믹 프록시를 위한 팩토리 빈

다이내믹 프록시는 Proxy 클래스의 newProxyInstance() 메서드로 생성할 수 있기에 일반적인 방법으로 스프링 빈으로 등록할 수는 없다.

스프링에선 팩토리 빈을 사용하여 프록시를 빈으로 등록할 수 있는 방법을 제공한다.

package org.springframework.beans.factory;

public interface FactoryBean<T> {
	T getObject() throws Exception;
	Class<? extends T> getObjectType();
	boolean isSingleton();
}

위 인터페이스를 구현한 클래스를 빈으로 등록하면 팩토리 빈으로 동작하게 되고 getObject()로 반환되는 오브젝트를 빈으로 등록할 수 있다.

트랜잭션 프록시 팩토리 빈

public class TxProxyFactoryBean implements FactoryBean<Object> {

	Object target;
	PlatformTransactionManager transactionManager;
	String pattern;
	Class<?> serviceInterface;

	// ...
	// setter를 사용해서 프로퍼티 세팅

	@override
	public Object getObject() throws Exception {
		return Proxy.newProxyInstance(
			getClass().getClassLoader(),
			new Class[] {UserService.class},
			new TransactionHandler(target, transactionManager, pattern)
		);
	}

	// ...
}

6.3.5 프록시 팩토리 빈 방식의 장점과 한계

프록시 팩토리 빈의 재사용

<bean id="userService" class="springbook.user.service.TxProxyFactoryBean">
	<Property name="target" ref="userServiceImpl" />
	<Property name="transactionManager" ref="transactionManager" />
	<Property name="pattern" value="upgradeLevels" />
	<Property name="serviceInterface" value="springbook.user.service.UserService" />
</bean>

프록시 팩토리 빈 방식의 장점

프록시 팩토리 빈의 한계