elevne's Study Note
토비의 Spring (9: AOP(2)) 본문
이전 시간에는 트랜잭션 기능, 즉 비즈니스 로직과 직접적으로 연관이 없는 코드를 서비스 밖으로 빼주는 작업을 진행해주려 하였다. 부가기능 전부를 UserServiceTx 라는, UserService 인터페이스를 구현한 클래스로 옮겨두고 UserServiceImpl 에서는 트랜잭션과 관련된 기능을 전부 제거해줄 수 있었다. 이 때, 부가기능은 마친 해당 클래스가 핵심 기능을 가진 클래스인 것처럼 꾸며서, 클라이언트가 해당 클래스를 거쳐 핵심 기능을 사용하도록 만들어져야 한다. (이를 위해 UserServiceTx 또한 UserService 인터페이스를 구현하도록 만든 것) 이와 같이 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장하여 클라이언트의 요청을 받아주는 것을 프록시 (Proxy) 라고 부른다. (프록시를 통해 실제로 요청을 위임받아 처리하는 실제 오브젝트는 타깃 또는 실체라고 부른다) 이러한 프록시는 사용 목적에 따라 두 가지로 분류될 수 있다고 한다. 첫째는 클라이언트가 타깃에 접근하는 방식을 제어하기 위해, 둘째는 타깃에 부가적인 기능을 부여하기 위해서이다.
데코레이터 패턴이 타깃에 부가적인 기능을 부여해주기 위해 프록시를 사용하는 패턴을 말하는 것이다. 자바 IO 패키지의 InputStream, OutputStream 이 데코레이터 패턴이 적용된 대표적인 예시라고 한다. BufferedInputStream 등의 클래스를 사용할 때 보조스트림을 주입해주는 것이 그 활용 예시인 것이다. 데코레이터 패턴은 UserServiceTx 에서 작성했듯이, 인터페이스를 통해 위임하는 방식을 사용하기 때문에 코드 레벨에서 어느 데코레이터에서 어느 타깃으로 연결되는지 알 수 없으며, 코드를 손대지 않고 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때 유용한 방법이다.
두 번째, 프록시 패턴은 타깃에 대한 접근 방식을 제어하려는 목적을 가진 디자인패턴이다. 이는 타깃의 기능을 확장하거나, 추가하진 않는다. 타깃 오브젝트가 생성하기 복잡하거나, 리소스 소모가 클 때는 필요한 시점까지는 생성하지 않는 편이 좋은데 이 때 타깃 오브젝트에 대한 레퍼런스만 미리 필요할 수도 있다. 그 때 직접 해당 오브젝트를 생성하는 것이 아니라 프록시를 넘겨주는 것이다. Collections 의 unmodifiableCollection() 이 그 대표적인 예시라고 한다. 파라미터로 전달된 Collection 오브젝트의 프록시를 생성하여 add() 나 remove() 같은 정보를 수정하는 메소드를 호출하는 경우 UnsupportedOperationException 이 발생하도록 만든다. 프록시는 데코레이터와 상당히 유사하지만, 자신이 만들거나 접근할 타깃 클래스 정보를 알고있어야 하는 경우가 많다. 생성을 지연하는 프록시라면 구체적인 생성 방법을 알아야하기 때문이다. (여기서도 인터페이스가 사용될 수 있긴 하다)
이렇게 프록시는 개발자에게 유용한 기능을 제공해주지만, 매번 프록시 클래스를 작성해주는 것은 번거로운 일이 될 수 있다. 목 프레임워크를 사용했을 때처럼, 조금 더 편리하게 프록시를 만들 수 있는 방법이 있다. java.lang.reflect 를 사용하는, 다이나믹 프록시 방식을 사용하는 것이다. 이는 타깃 인터페이스에서 메소드가 추가되는 등, 변경이 생기게 될 때마다 코드를 수정해야하는 직접 구현한 프록시와 다르게 수정해줄 필요가 없다. UserServiceTx 를 다이나믹 프록시 방식으로 아래와 같이 변경해볼 수 있다.
package org.example.sixthchapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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 invokeInteraction(method, args);
} else {
return method.invoke(target, args);
}
}
private Object invokeInteraction(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();
}
}
}
다이나믹 프록시를 구현할 때는 InvocationHandler 인터페이스를 구현하도록 한다. InvocationHandler 은 invoke(Object proxy, Method method, Object[] args) 메소드 하나만 가지고있는 인터페이스이다. invoke() 메소드는 리플렉션의 Method 를 파라미터로 받는다. 메소드를 호출할 때 전달되는 파라미터도 args 로 받는다. 타깃 오브젝트의 모든 메소드 요청이 하나의 메소드로 집중되기에 중복되는 기능을 효과적으로 제공할 수 있다. 이러한 InvocationHandler 을 구현한 다이나믹 프록시는 Proxy 클래스의 newProxyInstance() 스태틱 팩토리 메소드로 생성된다.
위 클래스는 요청을 위임할 타깃을 DI 받는다. 타깃을 저장할 변수를 Object 로 선언했기에 UserService 외의 다른 어떠한 클래스라도 트랜잭션이 적용되어야 한다면 위 클래스를 사용할 수 있다. 또, 타깃 오브젝트의 모든 메소드에 무조건 트랜잭션이 적용되지는 않게끔 트랜잭션을 적용할 메소드 이름의 패턴을 DI 받는다. 아래 Test 코드를 실행해본다.
@Test
public void 다이나믹프록시테스트() throws Exception {
TransactionHandler transactionHandler = new TransactionHandler();
transactionHandler.setTarget(userServiceImpl);
transactionHandler.setTransactionManager(platformTransactionManager);
transactionHandler.setPattern("upgradeLevels");
UserService userService = (UserService) Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] {UserService.class}, transactionHandler
);
for (User user : this.users) userDao.add(user);
userService.upgradeLevels();
checkLevelUpgraded(users.get(0), false);
checkLevelUpgraded(users.get(1), true);
checkLevelUpgraded(users.get(2), false);
checkLevelUpgraded(users.get(3), true);
checkLevelUpgraded(users.get(4), false);
}
테스트가 잘 통과하는 것을 확인할 수 있다.
Reference:
토비의 스프링
'Backend > Toby Spring' 카테고리의 다른 글
토비의 Spring (9: AOP(4)) (0) | 2023.06.09 |
---|---|
토비의 Spring (9: AOP(3)) (0) | 2023.06.08 |
토비의 Spring (9: AOP(1)) (0) | 2023.05.22 |
토비의 Spring (8: 서비스 추상화(2)) (0) | 2023.05.18 |
토비의 Spring (8: 서비스 추상화(1)) (0) | 2023.05.16 |