이전 step2 에서 비즈니스 로직과 트랜잭션 로직을 분리하기 위해
UserService(인터페이스) -> UserServiceTx(트랜잭션 로직) -> UserServiceImpl(비즈니스 로직)을 구현했다.
이 구성에는 번거러운 코드 작성과 중복 코드 작성 등의 문제가 있었다. 이를 자바의 리플렉션 API 기능을 이용해 다이내믹 프록시를 만들어 해결할 수 있다.
리플렉션이란 런타임 시점에 객체의 클래스 타입, 필드, 메서드 등의 정보를 동적으로 가져올 수 있는 기능이다.
예를 들어 String name = "LEE_SEUNGHUN" 일 때 이 문자열의 길이를 알고 싶다면 length()라는 메소드를 호출할 것이다. 자바의 모든 클래스는 그 클래스 자체의 구성정보를 담은 Class 타입으 오브젝트를 하나씩 가지고 있다. 클래스이름.class 또는 오브젝트의 getClass() 메소드를 호출하면 클래스 코드에 대한 메타정보를 가져오거나 오브젝트를 조작할 수 있다. String의 length() 메소드라면 다음과 같이 하면 된다. Method lengthMethod = String.class.getMethod("length");
자세한 사항은 java.lang.reflect 패키지의 자바문서를 읽어보자.
우리가 활용할 리플렉션 API는 invoke라는 메소드이다.
invoke() 메소드는 리플렉션 Method 인터페이스를 파라미터로 받고 호출할 때 전달되는 args도 인자로 받는다.
간단한 예시를 보면서 이해해보자. 타깃의 메소드 중 return 값이 문자열이면서 메소드명이 go로 시작할 때 대문자로 출력해주도록 앞단에 프록시 객체에게 역할을 부여하는 코드 예시이다.
public class UppercaseHandler implements InvocationHandler{
Hello target;
//다이내믹 프록시로부터 전달받은 요청을 다시 타깃 오브젝트에 위임해야 하기 때문에 타깃 오브젝트를 주입받아 둔다.
public UppercaseHandler(Hello target){
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
//타깃으로 위임. 인터페이스의 메소드 호출에 모두 적용된다.
String ret = (String)method.invoke(target, args)
if(ret instanceof String && method.getName().startWith("go")){
return ret.toUpperCase(); -> 부가 기능 제공
}
else{
return ret; -> 조건이 일치하지 않으면 타깃 오브젝트 호출 결과를 그대로 호출
}
}
}
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(), -> 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용될 클래스 로더
new Class[]{ Hello.class }, -> 구현할 인터페이스
new UppercaseHandler(new HelloTarget()) -> 부가 기능과 위임 코드를 담은 InvocationHandler
);
InvocationHandler는 프록시 객체가 호출된 메서드의 정보를 처리하는 기능을 제공한다. 프록시 객체인 proxiedHello가 Hello 인터페이스의 정의된 메소드 중 return 값이 문자열이면서 메소드명이 go로 시작하는 메소드를 호출하면 이를 대문자로 변형하여 리턴할 것이다.
이 논리를 그대로 트랜잭션 로직 분리에 녹인다면 공통된 TransactionHandler 을 생성하고 proxyUserService, proxyDogService 등 만들어 트랜잭션을 탈 메소드와 타지 않을 메소드를 쉽게 분리하고 공통된 코드를 작성할 일도 없어진다.
다이나믹 프록시를 위한 트랜잭션 부가기능 코드는 다음과 같을 것이다.
public class TransactionHandler implements InvocationHandler{
private Object target; -> 부가 기능을 제공할 타깃 오브젝트
private PlatformTransactionManager transactionManager; -> 트랜잭션 기능을 제공하는 데 필요한 트랜잭션 매니저
private String pattern; -> 트랜잭션을 적용할 메소드 이름 패턴
public void setTarget(Object target){
this.target = target;
}
,,,setTransactionManager, setPattern
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{
//트랜잭션 적용 대상을 선별
if(metohd.getName().startWith(pattern)){
return invokeInTransaction(method, args);
}
else{
return method.invoke(target, args);
}
}
private Oject invokeInTransaction(Method method, Object[] args) throws Throwable{
TransactionsStatus status = this.transactionsManager.getTransactions(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();
}
}
}
요청을 위임할 타깃을 DI로 제공받도록 한다. 타깃을 저장할 변수는 Object로 선언했다. 따라서 UserServiceImpl 외에 트랜잭션 적용이 필요한 어떤 타깃 오브젝트에도 적용할 수 있다. 이제 UserServiceTx보다 코드는 복잡하지 않으면서도 UserService 뿐 아니라 모든 트랜잭션이 필요한 오브젝트에 적용 가능한 트랜잭션 프록시 핸들러를 만들었다. 이제 User 용이 아닌 공용으로 사용이 가능해진 트랜잭션 단독 로직을 생성한 것이다.
호출하는 클라이언트 쪽 예시를 보자.
TransactionHandler txhandler = new TransactionHandler();
txhandler.setTarget(userServiceImpl);
txhandler.setTransactionManager(transactionManager);
txhandler.setPattern("upgradeLevels");
UserService txUserService = (UserService)Proxy.new ProxyInstance(
getclass().getClassLoader(), new Class[]{ UserService.class }, txHander);
...
현재구조는 다이내믹 프록시 -> invocationHandler -> userSerivce 이다.
UserServiceTx 객체 대신 TransactionHandler를 만들고 타깃 오브젝트와 트랜잭션 매니저, 메소드 패턴을 주입해준다. 이렇게 준비된 TransactionHandler 오브젝트를 이용해 UserService 타입의 다이내믹 프록시를 생성하면 모든 필요한 작업은 끝이다.
그런데 문제는 다이내믹 프록시 오브젝트는 일반적인 스프링 빈 등록 형식으로는 불가능하다. 스프링 빈은 기본적으로 클래스 이름과 프로퍼티로 정의된다. 스프링은 지정된 클래스 이름을 가지고 리플렉션을 이용해서 해당 클래스의 오브젝트를 만든다. 클래스의 이름을 갖고 있다면 다음과 같은 방법으로 새로운 오브젝트를 생성할 수 있지만 다이내믹 오브젝트의 클래스가 어떤 것인지 알 수가 없다. 따라서 다이내믹 프록시를 만들어주는 팩토리 빈을 생성해서 빈을 등록해줘야한다.
스프링의 팩토리 빈에 관련해서는 따로 포스팅하였다.
https://sh970901.tistory.com/118
이를 참고하여 TransactionHandler를 이용하는 다이내믹 프록시를 생성하는 팩토리 빈 클래스이다.
public class TxProxyFactoryBean implements FactoryBean<Object>{
Object target;
PlatformTransactionManager transactionManager;
String pattern;
//UserService 외의 인터페이스를 가진 타깃에도 적용할 수 있다.
Class<?> serviceInterface;
... setTarget, setTransactionManager, setPattern, setServiceInterface 등 DI 주입
public Object getObject() throws Exception{
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(target);
txHander.setTransactionManager(transactionManager);
txHander.setPattern(pattern);
return Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[]{ serviceInterface },
txHandler);
}
public Class<?> getObjectType(){
return serviceInterface;
}
// getObject()가 메소드가 돌려주는 오브젝트가 싱글톤인지 알려준다. 이 팩토리 빈은 매번 요청할 때마다 새로운 오브젝트를 만들기 때문에 false로 설정했다.
// 이것은 팩토리 빈의 동작방식에 관한 설정이고 만들어진 빈 오브젝트는 싱글톤으로 스프링이 관리해줄 수 있다.
public boolean isSingleton(){
return false;
}
}
<bean id = "userService" class = "package.example.TxProxyFactoryBean">
<property name = "target" ref = "userServiceImpl" />
<property name = "transactionManager" ref = "transactionManager" />
<property name = "pattern" value = "upgradeLevels />
<property name = "serviceInterface" value = "pacakage.example.UserService" />
</bean>
이제 기존의 UserService(인터페이스) -> UserServiceTx(트랜잭션 로직) -> UserServiceImpl(비즈니스 로직) 구조에서 단점을 보완한 팩토리 빈을 이용한 다이내믹 프록시 구조로 변경했다.
다이내믹 프록시(팩토리 빈)-> UserServiceImpl 구조가 되어 공통으로 트랜잭션 로직을 처리할 수 있는 TransactionsHandler가 user 도메인에서 확실하게 분리되었으며 기존 코드에 부가적인 기능을 코드 한줄 적지 않고 설정 변경만으로 추가해줄 수 있게 되었다.
기존에 클라이언트 -> ServiceImpl 관계에 ProxyFactoryBean만 추가된 것이기 때문이다.
하지만 욕심은 끝이 없다. 프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일이다. 하나의 클래스 안에 존재하는 여러 개의 메소드에 부가 기능을 한번에 제공했지만 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는건 이 방법으로는 불가능하다.
또한 트랜잭션과 같이 비즈니스 로직을 담은 많은 클래스의 메소드에 적용할 필요가 있다면 거의 비슷한 프록시 팩토리 빈의 설정이 중복될 것이다. 하나의 타깃에 여러 개의 부가 기능을 적용할 때도 문제가 생긴다. 코드에 변경 없이 적용할 수 있다는 것은 대단한 일이기도 하지만 다수의 클래스에 다수의 기능이라면 xml파일 설정이 상당히 길어지고 관리하기 힘들 것이다. 또 한가지 문제점은 TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 생성된다.
과연 스프링의 프록시 팩토리 빈은 어떻게 이를 해결했을까? step 4에서 확인해보자.
참고 자료 - 토비의 스프링 3.1 | 저자 이일민
'IT' 카테고리의 다른 글
스프링 트랜잭션 (Spring Transaction) step4 : Spring ProxyFactoryBean (0) | 2023.05.03 |
---|---|
메이븐 라이프사이클(Maven LifeCycle) (0) | 2023.05.01 |
자바 리플렉션(reflection) (0) | 2023.04.30 |
스프링 팩토리 빈(Factory Bean) (0) | 2023.04.29 |
스프링 트랜잭션 (Spring Transaction) step2 : proxy, decorate (0) | 2023.04.29 |