프록시는 사용 목적에 따라 두 가지로 구분할 수 있다.
두 가지 모두 대리 오브젝트라는 개념의 프록시를 두고 사용한다는 점은 동일하지만, 목적에 따라서 디자인 패턴에서느 다른 패턴으로 구분한다.
클라이언트 -> 라인넘버 데코레이터 -> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스 코드 출력 기능
자바 IO 패키지의 InputStream, OutputStream 구현 클래스는 데코레이터 패턴의 대표적인 예이다.
InputStream is = new BufferedInputStream(new FileInputStream("a.txt"));
UserService 인터페이스를 구현한 타킷인 UserServiceImpl에 트랜잭션 부가기능을 제공해주는 UserServiceTx를 추가한 것도 데코레이터 패턴을 적용한 것이라고 볼 수 있다.
인터페이스를 통한 데코레이터 정의와 런타임 시의 다이내믹한 구성 방법은 스프링의 DI를 이용하면 아주 편리하다. 데코레이터 빈의 프로퍼티로 같은 인터페이스를 구현한 다른 데코레이터 또는 타깃 빈을 설정하면 된다.
데코레이터 패턴은 타깃의 코드를 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때 유용한 방법이다.
클라이언트 -> 접근제어 프록시 -> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스코드 출력 기능
프록시는 다음의 두 가지 기능으로 구성된다.
public class UserServiceTx implements UserService {
UserService userService;
...
// 메소드 구현과 위임
public void add(User user) {
this.userService.add(user);
}
// 메소드 구현
public void upgradeLevels() {
// 부가 기능 수행
TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 위임
userService.upgradeLevels();
// 부가 기능 수행
this.transactionManager.commit(status);
} catch(RuntimeException e) {
this.transactionManage.rollback(status);
throw e;
}
}
}
프록시를 만들기가 번거로운 이유는 두 가지가 있다.
두 번째 문제인 부가기능의 중복 문제는 중복되는 코드를 분리해서 어떻게든 해결해 보면 될 것 같지만, 첫 번째 문제인 인터페이스 메소드의 구현과 위임 기능 문제는 간단해 보이지 않는다. 바로 이런 문제를 해결하는 데 유용한 것이 바로 JDK의 다이내믹 프록시다.
다음은 리플렉션 학습테스트이다.
package springbook.learningtest.jdk;
...
public class ReflectionTest {
@Test
public void invokeMethod() throws Exception() {
String name = "Spring";
// length()
assertThat(name.length(), is(6));
Method lengthMethod = String.class.getMethod("length");
assertThat((Integer)lengthMethod.invoke(name), is(6));
// charAt()
assertThat(name.charAt(0), is('S'));
Method charAtMethod = String.class.getMethod("charAt", int.class);
assertThat((Character)charAtMethod.invoke(name, 0), is('S'));
}
}
다이내믹 프록시를 이용한 프록시를 만들어보자.
interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
타깃클래스
public class HelloTarget implements Hello {
public String sayHello(String name){
return "Hello " + name;
}
public String sayHi(String name){
return "Hi " + name;
}
public String sayThankYou(String name){
return "Thank you " + name;
}
}
클라이언트 역할의 테스트
@Test
public void simpleProxy() {
Hello hello = new HelloTarget();
asserThat(hello.sayHello("Toby"), is("Hello Toby"));
asserThat(hello.sayHi("Toby"), is("Hi Toby"));
asserThat(hello.sayThankYou("Toby"), is("Thank you Toby"));
}
Hello 인터페이스를 구현한 프록시를 만들어보자. 프록시에는 데코레이터패턴을 적용해 타깃인 HelloTarget에 부가기능을 추가하겠다.
public class HelloUppercase implements Hello {
// 위임할 타깃 오브젝트, 여기서는 타깃 클래스의 오브젝트인 것은 알지만 다른 프록시를 추가할 수도 있으므로 인터페이스로 접근한다.
Hello hello;
public HelloUppercase(Hello hello){
this.hello = hello;
}
public String sayHello(String name){
// 위임과 부가기능 적용
return hello.sayHello(name).toUpperCase();
}
public String sayHi(String name){
// 위임과 부가기능 적용
return hello.sayHi(name).toUpperCase();
}
public String sayThankYou(String name){
// 위임과 부가기능 적용
return hello.sayThankYou(name).toUpperCase();
}
}
HelloUppercase 프록시 테스트
@Test
public void simpleProxy() {
HelloUppercase proxyHello = new HelloUppercase(new HelloTarget());
asserThat(proxyHello.sayHello("Toby"), is("HELLO TOBY"));
asserThat(proxyHello.sayHi("Toby"), is("HI TOBY"));
asserThat(proxyHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만, 프록시로서 필요한 부가기능 제공 코드는 직접 생성해야 한다. 부가기능은 프록시 오브젝트와 독립적으로 InvocationHandler를 구현한 오브젝트에 담는다. InvocationHandler 인터페이스는 다음과 같은 메소드 한 개만 가진 간단한 인터페이스다.
public Object invoke(Object proxy, Method method, Object[] args)
invoke() 메소드는 리플렉션의 Method 인터페이스를 파라미터로 받는다. 메소드를 호출할 때 전달되는 파라미터도 args로 받는다. 다이내믹 프록시 오브젝트는 클라이언트의 모든 요청을 리플렉션 정보로 변환해서 InvocationHandler 구현 오브젝트의 invoke() 메소드로 넘기는 것이다. 타깃 인터페이스의 모든 메소드 요청이 하나의 메소드로 집중되기 때문에 중복되는 기능을 효과적으로 제공할 수 있다.
다이내믹 프록시로부터 메소드 호출정보를 받아서 처리하는 InvocationHandler를 만들어보자. 아래는 모든 요청을 타깃에 위임하면서 리턴값을 대문자로 바꿔주는 부가기능을 가진 InvocationHandler 구현 클래스이다.
public class UppercaseHandler implements InvocationHandler {
// 다이내믹 프록시로부터 전달받은 요청을 다시 타깃 오브젝트에 위임해야 하기 때문에 타깃 오브젝트를 주입받아둔다.
Hello target;
private UppercaseHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) thorws Throwable
{
String ret = method.invoke(target, args);
return ret.toUpperCase();
}
}
프록시 생성
// 생성된 다이내믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로 Hello 타입으로 캐스팅해도 안전하다.
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더
new Class[] { Hello.class }, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler
public class UppercaseHandler implements InvocationHandler {
// 어떤 종류의 인터페이스를 구현한 타깃에도 적용 가능하도록 Object 타입으로 수정
Object target;
private UppercaseHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args)
thorws Throwable {
//호출한 메소드의 리턴타입이 String인 경우만 대문자로 변경 기능을 적용하도록 수정
Object ret = method.invoke(target, args);
if (Ret instanceof String) {
return ((String)ret).toUpperCase();
} else {
return ret;
}
}
}
public class TransactionHandler implements InvocationHandler {
private Object target; // 부가기능을 제공할 타깃 오브젝트, 어떤 타입의 오브젝트에도 적용 가능하다.
private PlatformTransactionManager transactionManager; // 트랜잭션 기능을 제공하는 데 필요한 트랜잭션 매니저
private String pattern; // 트랜잭션을 적용할 메소드 이름 패턴
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 트랜잭션 적용 대상 메소드를 선별해서 트랜잭션 경계설정 기능을 부여해준다.
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeTransaction(Method method, Object[] args) throws Throwable {
TransactionStatus = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// 트랜잭션을 시작하고 타깃 오브젝트의 메소드를 호출한다. 예외가 발생하지 않았다면 커밋한다.
Object ret = method.invoke(target, args);
this.transactionManager.commit(status);
return ret;
} catch (InvocationTargetException e) {
// 예외가 발생하면 트랜잭션을 롤백한다.
this.transactionManager.rollback(status);
throw e.getTargetException();
}
}
}
@Test
public void upgradeAllOrNothing() throws Exception {
...
TransactionHandler txHandler = new TransactionHandler();
//트랜잭션 핸들러에 필요한 정보와 오브젝트를 DI해준다.
txHandler.setTarget(testUserService);
txHandler.setTransactionManager(transactionManager);
txHandler.setPattern("upgradeLevels");
// UserService 인터페이스 타입의 다이내믹 프록시 생성
UserService txUSerService = (UserService)Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] {UserService.class }, txHandler);
...
}