elevne's Study Note
토비의 Spring (9: AOP(3)) 본문
이전까지 작업한 TransactionHandler, 다이나믹 프록시를 스프링의 DI 를 통해 사용할 수 있어야 할 것이다. 하지만 DI 의 대상이 되는 다이나믹 프록시 오브젝트는 일반적인 스프링의 빈으로는 등록할 방법이 없다. 스프링은 내부적으로 리플렉션 API 를 사용하여 빈 정의에 나오는 클래스 이름을 가지고 오브젝트를 생성한다. 하지만 다이나믹 프록시 오브젝트는 이런 식으로 프록시 오브젝트가 생성되지 않는다. (어떤 클래스를 구체적으로 사용하는지도 알 수 없다) 다이나믹 프록시를 사용하게 되면 프록시 오브젝트 클래스 정보를 미리 알아내서 스프링 빈에 정의할 방법이 없다는 뜻이다. 다이나믹 프록시는 Proxy 클래스의 newProxyInstance() 메소드를 통해서만 생성할 수 있다.
이 때, FactoryBean 이라는 것을 사용하여 이 문제를 해결할 수 있다. 팩토리빈은 스프링을 대신하여 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈이다. FactoryBean 인터페이스는 세 가지 메소드로 구성되어있다. 빈 오브젝트를 생성해 돌려주는 getObject(), 생성되는 오브젝트의 타입을 알려주는 getObjectType(), 그리고 getObject() 가 돌려주는 오브젝트가 항상 같은 싱글톤 오브젝트인지에 대한 여부를 알려주는 isSingleton() 으로 이루어진다. FactoryBean 인터페이스를 구현한 클래스를 스프링의 빈으로 등록하면 팩토리 빈으로 동작하게 된다. 아래와 같이 TransactionHandler 을 이용하여 프록시를 생성하는 팩토리 빈을 작성할 수 있다.
package org.example.sixthchapter;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import java.lang.reflect.Proxy;
public class TxProxyFactoryBean implements FactoryBean<Object> {
Object target;
PlatformTransactionManager platformTransactionManager;
String pattern;
Class<?> serviceInterface;
public void setTarget(Object target) {
this.target = target;
}
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.platformTransactionManager = transactionManager;
}
public void setPattern(String pattern) {
this.pattern = pattern;
}
public void setServiceInterface(Class<?> serviceInterface) {
this.serviceInterface = serviceInterface;
}
public Object getObject() throws Exception {
TransactionHandler txHandler = new TransactionHandler();
txHandler.setTarget(target);
txHandler.setTransactionManager(platformTransactionManager);
txHandler.setPattern(pattern);
return Proxy.newProxyInstance(
getClass().getClassLoader(), new Class[] {serviceInterface}, txHandler
);
}
public Class<?> getObjectType() {
return serviceInterface;
}
public boolean isSingleton() {
return false;
}
}
오브젝트를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정해서 대신 DI 받을 수 있게끔 한다. 주입된 정보는 오브젝트 생성 중에 사용되는 것이다. 그리고 getObject() 메소드 내에 실제 빈으로 사용할 오브젝트를 직접 위와 같이 생성해주는 것이다. 서적에서는 계속 xml 파일에서 빈 정의를 해주고 있지만, @Configuration 어노테이션을 사용하는 자바 파일 내에서 아래와 같이 빈 정의를 해줄 수 있다.
@Bean(name = "userService")
public UserService userService() throws Exception {
TxProxyFactoryBean factory = new TxProxyFactoryBean();
factory.setTarget(userServiceImpl());
factory.setPattern("upgradeLevels");
factory.setTransactionManager(platformTransactionManager());
factory.setServiceInterface(UserService.class);
return (UserService) factory.getObject();
}
이렇게 생성해준 빈을 이용하여 테스트를 진행해볼 수 있다. UserSerive 라는 이름으로 등록한 빈을 @Autowire 하여 가져와서 아래 테스트를 실행해본다.
@Autowired
private UserService userService;
@Test
public void 프록시팩토리테스트() throws Exception {
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
userService.upgradeLevels();
} catch (Exception e) {
}
checkLevelUpgraded(users.get(1), true);
}
다이나믹 프록시를 생성해주는 팩토리 빈을 사용하는 것은 여러 장점이 있다고 한다. 우선 위와 같이 Object 타입으로 지정해주면 UserSerivce 뿐만 아니라 이후에 다른 서비스 클래스들에 대해서도 위 팩토리 빈을 이용하여 프록시를 이용한 트랜잭션 처리를 해줄 수 있다. 또 앞서 알아본 데코레이터 패턴의 두 가지 문제점 (일일이 만들어야 하는 번거로움, 코드 중복 문제) 을 해결해준다.
하지만 위 방법 또한 한계가 있다고 한다. 프록시를 통해 타깃에 부가기능을 제공하는 것은 메소드 단위로 일어나는 일인데, 하나의 클래스 안에 존재하는 여러 개의 메소드에 부가기능을 한 번에 제공하는 것은 어렵지 않게 가능하지만 한 번에 여러 개의 클래스에 공통적인 부가기능을 제공하는 것은 지금까지의 방식으로 불가능하다. 또, 하나의 타깃에 여러 개의 부가기능을 적용하려고 할 때에도 문제가 생길 수 있다. 같은 타깃 오브젝트에 대해 트랜잭션 뿐만 아니라 보안 기능 혹은 그 외의 부가기능을 담은 프록시도 추가하고자 한다면 빈 정의가 매우 복잡해질 것이다. 더구나, 위 방식에서는 TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어진다. 이를 싱글톤 빈으로 만들어서 사용할 수 있다면 더 효율적일 것이다.
Reference:
토비의 스프링
'Backend > Toby Spring' 카테고리의 다른 글
토비의 Spring (9: AOP(5)) (0) | 2023.06.13 |
---|---|
토비의 Spring (9: AOP(4)) (0) | 2023.06.09 |
토비의 Spring (9: AOP(2)) (0) | 2023.06.07 |
토비의 Spring (9: AOP(1)) (0) | 2023.05.22 |
토비의 Spring (8: 서비스 추상화(2)) (0) | 2023.05.18 |