[토비의 스프링] 다이내믹 프록시를 이해해보자
토비 AOP까지 읽는게 목표였고 해당 파트인 6장을 읽고 있는데 다이내믹 프록시가 매우 매우 어렵다..
책 내용이 아니라 혼자 이해한 내용을 정리한거니 혹시라도 틀린 부분을 알려주시면 감사하겠습니다. ㅜㅜ
다이내믹 프록시를 사용하는 이유
프록시 디자인 패턴의 치명적인 단점을 보완하기 위해 사용한다. 프록시 디자인 패턴은 원본 클래스 수만큼 프록시 클래스를 하나하나 만들어줘야 한다. 프록시 적용 대상 객체가 1억 개면 프록시 객체도 1억 개 만들어줘야 한다.
이러한 코드의 복잡도 한계를 해결하기 위해 사용하는 것이 다이내믹 프록시다. 다이내믹 프록시는 컴파일 시점이 아닌 런타임 시점에 자바 가상 머신이 지원해 준다. 애플리케이션 실행 도중 java.lang.reflect.Proxy 패키지에서 제공해 주는 API를 이용하여 동적으로 프록시 인스턴스를 만들어 등록한다.
newProxyInstance() 메서드
java.lang.reflect.Proxy 클래스 안에는 프록시 객체를 생성하는 메서드인 newProxyInstance() 메서드가 들어있다. 이 메서드를 호출하면 프록시 클래스 정의 없이 자동으로 프록시 객체를 등록한다. 이때 3가지 매개변수를 받는다.
public class Proxy implements java.io.Serializable {
public static Object newProxyInstance(
ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h
) throws IllegalArgumentException {
// ...
}
}
1. ClassLoader loader
프록시 클래스를 만들 클래스 로더
2. Class<?>[] interfaces
프록시 클래스가 구현하고자 하는 인터페이스 목록
메서드를 통해 생성될 Proxy 객체가 구현할 인터페이스를 정의한다.
3. InvocationHandler h
프록시 메서드(invoke)가 호출되었을 때 실행되는 핸들러 메서드
이때 3번 매개변수인 InvocationHandler 인터페이스를 유심히 봐야 한다. InvocationHandler 인터페이스는 newProxyInstance() 메서드의 3번째 매개변수이자, 핸들러 메서드를 정의하는 함수형 인터페이스이다. 해당 인터페이스에는 invoke()라는 추상메서드 하나만 정의되어 있다.
InvocationHandler 인터페이스의 invoke() 메서드
invoke() 메서드는 다이내믹 프록시의 메서드가 호출되었을 때 이를 낚아챈 후 대신 실행되는 메서드이다. 이 메서드 덕분에 코드가 중복되는 프록시 패턴의 단점을 보완할 수 있다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
1. Object proxy
프록시 객체
2. Method method
호출한 메서드 정보
3. Object[] args
메서드에 전달된 매개변수
내가 원하는 것은 위 개념을 토대로 해당 블로그에 나와있는 다이내믹 프록시 코드를 트랜잭션 경계설정에 적용하는 것이다.
기존 테스트 코드이다. UserServiceTx가 트랜잭션 코드를 처리하고 TestUserServiceImpl의 upgradeLevels를 호출하여 비즈니스 로직으로 넘어간다. 즉, UserServiceTx가 수동적으로 프록시 역할을 하고 있다.
프록시 역할을 부여하여 핵심 코드에 대한 접근을 제어하는 건 좋았다. 하지만 수동 프록시에는 단점이 있다.
먼저, 인터페이스의 메서드가 늘어날 때마다 부가 기능(여기서는 트랜잭션 경계설정) 코드를 중복으로 작성해주어야 한다. 중복되는 메서드를 하나로 분리해 줘도 인터페이스의 메서드가 늘어날 때마다 코드를 추가하는 건 마찬가지다. 그리고 또 다른 단점은 UserService가 아닌 다른 클래스가 들어오는 경우를 적용하기 위해 필드를 추가해야 한다는 점이다.. 쉽게 말해서 정적이다. 변화를 받아들이지 못한다!!!!
이를 보완하기 위해 사용하는 것이 다이내믹 프록시이다. 다이내믹 프록시는 인터페이스의 모든 메서드가 InvocationHandler의 invoke 메서드를 거치기 때문에 하나하나 고려할 필요가 없다. 그리고 객체 타입 상관없이 모두 적용가능하다. 위에 작성한 invoke() 메서드의 매개변수 중 method로 받아들이기 때문이다.
이제 트랜잭션 경계설정(UserSerivceTx)을 다이내믹 프록시로 작성해 보자..
public class TransactionHandler implements InvocationHandler {
private Object target;
private PlatformTransactionManager transactionManager;
private String pattern;
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().startsWith(pattern)) {
return invokeInTransaction(method, args);
}
return method.invoke(target, args);
}
private Object invokeInTransaction(Method method, Object[] args) throws Throwable {
TransactionStatus status = 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();
}
}
// setter
}
먼저 InvocationHandler를 구현한 TransactionHandler를 만든다.
필드
- Object target : 트랜잭션 기능을 제공할 객체
- PlatformTransactionManeger transactionManager : 트랜잭션 기능을 제공하는데 필요한 트랜잭션 매니저 (프록시와 직접적인 연관 X)
- String pattern : 실행할 메서드 이름 (트랜잭션을 적용할 메서드)
메서드
- invoke : InvocationHandler에서 제공하는 pattern 메서드 실행 메서드
- invokeInTransaction : 트랜잭션 기능을 제공하기 위해 따로 구현한 메서드
TransactionHandler를 사용하면 테스트 코드를 위와 같이 수정할 수 있다. 메서드가 바뀌어도 setPattern만 건드리면 되고, 객체가 바뀌면 setTarget만 건드리면 된다.
이제 TransactionHandler를 의존관계 주입에 사용하고 싶어졌다. 하지만 문제가 발생한다. 다이내믹 프록시 객체는 스프링 빈으로 등록할 수 없다. 설정될 오브젝트의 클래스가 어떤 건지 알 수 없고, 클래스 자체도 내부적으로 정의해서 사용하기 때문이다.
이때 사용해야 하는 게 팩토리 빈이다. FactoryBean 인터페이스를 구현하면 다이내믹 프록시 빈을 생성할 수 있다.
public class TxProxyFactoryBean implements FactoryBean<Object> {
Object target;
PlatformTransactionManager transactionManager;
String pattern;
Class<?> serviceInterface;
@Override
public Object getObject() {
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(target);
txHandler.setTransactionManager(transactionManager);
txHandler.setPattern(pattern);
return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { serviceInterface }, txHandler);
}
@Override
public Class<?> getObjectType() {
return serviceInterface;
}
@Override
public boolean isSingleton() {
return false;
}
// setter
}
이렇게 TransactionFactoryBean을 만들어주면
의존관계 주입만으로 테스트가 가능해진다. 팩토리빈의 getObject()를 호출하면 자동으로 인터페이스 메서드가 호출된다.
직접 코드를 작성하고 개념을 이해하고자 했지만, 프록시 패턴의 엄청난(?) 장점이 아직까지 와닿지 않는다..
코드를 리팩터링 하면서 완벽하게 이해해야겠다.