트랜잭션 코드를 분리했던 기법을 살펴보면 클라이언트는 인터페이스만 보고 UserService
를 사용하기에 핵심 기능을 가진 클래스를 사용할 것이라고 기대하지만 사실은 부가 기능이 적용된 클래스를 사용하게 된다.
이렇게 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트 요청을 대신 받아주는 것을 프록시라고 부른다. 프록시를 통해 최종적으로 요청을 처리하는 실제 오브젝트는 타겟(target)이라고 부른다. 프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 프록시가 타깃을 제어할 수 있다는 것이다.
프록시 사용 목적에 따라 두 가지로 구분 가능한데 프록시를 사용한다는 점은 동일하지만 목적에 따라 다른 디자인 패턴으로 구분한다.
데코레이터 패턴은 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여하기 위해 프록시를 사용하는 패턴을 말한다. 다이내믹하게 연결하기 때문에 컴파일 시점(코드 레벨)에서 프록시와 타깃의 연결 정보를 파악할 수 없다.
데코레이터 패턴에선 프록시가 한 개로 제한되지 않고 프록시가 직접 타깃을 사용하도록 고정시킬 필요도 없는데 이는 같은 인터페이스를 구현한 타겟과 여러 프록시로 구현 가능하다. 각 데코레이터(프록시)는 위임하는 대상에도 인터페이스로 접근하기 때문에 자신이 최종 타깃으로 위임하는지, 다음 데코레이터 프록시로 위임하는지 알지 못한다.
자바 IO 패키지의 InputStream
과 OutputStrem
구현 클래스는 데코레이터 패턴이 적용된 대표적인 예다.
InputStrem
인터페이스를 구현한 타깃인 FileInputStream
에 버퍼 읽기 기능을 제공하는 BufferedInputStream
데코레이터를 적용한 예InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
스프링 DI를 이용하면 런타임 시 다이내믹하게 데코레이터를 주입할 수 있다. 데코레이터 패턴은 타깃의 코드와 클라이언트가 호출하는 방법을 변경하지 않고 새로운 기능을 추가할 때 유용하다.
프록시라는 용어와 프록시 패턴은 구분할 필요가 있다.
Collecitions.unmodifiableCollection()
으로 만들어지는 컬렉션 객체 또한 프록시 패턴을 적용한 프록시의 예다.프록시는 유용하지만 프록시를 직접 만드는 일은 번거롭다.
프록시는 다음 두 가지 기능으로 구성된다.
프록시를 만들기 번거로운 이유는 다음의 두 가지가 있다.
위 문제를 리플렉션을 이용한 JDK 다이내믹 프록시를 통해 해결할 수 있다.
다이내믹 프록시는 프록시 팩토리에 의해 런타임 시 동적으로 만들어지는 오브젝트다.
InvocationHandler
인터페이스 구현체를 사용한다.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
);
사용 방법을 자세히 살펴 보자.
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();
}
}
}
InvocationHandler
는 단일 메서드에서 모든 요청을 처리하기 때문에 어떤 메서드에 어떤 기능을 적용할지 선택하는 과정이 필요할 수도 있다.
Method.invoke()
에서 예외가 발생하면 InvocationTargetException
으로 한 번 포장돼서 전달된다.
getTargetException()
메서드로 실제 예외를 가져올 수 있다.// ...
UserService txUserService = (UserService)Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] {UserService.class},
new TransactionHandler(targetUserService, transactionManager, "upgradeLevels")
);
다이내믹 프록시는 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)
);
}
// ...
}
FactoryBean
제네릭 타입을 Object로 지정serviceInterface
를 지정하지 않았기 때문에 다양한 타입에 트랜잭션 부가 기능을 적용할 수 있다.UserServiceTx
와 같은 프록시 클래스를 작성하는 번거로움을 완벽히 제거할 수 있다.TxProxyFactoryBean
은 코드 수정 없이 다양한 클래스에 적용할 수 있다.
<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>
InvocationHandler
)만 작성하면 여러 메서드에 부가 기능을 적용할 수 있다.InvocationHandler
) 오브젝트가 프록시 팩토리 빈 개수만큼 많아진다.