elevne's Study Note
토비의 Spring (9: AOP(4)) 본문
자바에서는 JDK 에서 제공하는 다이나믹 프록시 외에도 편리하게 프록시를 만들 수 있는 다양한 기술이 존재한다. 그 중 하나는 ProxyFactoryBean 이다. 이는 프록시를 생성해서 빈 오브젝트로 등록하게 해준다. 기존에 작성했던 TxProxyFactoryBean 과 달리 ProxyFactoryBean 은 순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해줄 부가기능은 별도의 빈에 둘 수 있다.
ProxyFactoryBean 이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다. 이전에 사용한 InvocationHandler 은 타깃에 대한 정보를 알아야했지만, MethodInterceptor 은 ProxyFactoryBean 으로부터 타깃 오브젝트에 대한 정보까지 함께 제공받기에 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. 따라서, MethodInterceptor 오브젝트는 타깃이 다른 여러 프록시에서 함께 사용할 수 있으며, 싱글톤 빈으로 등록이 가능한 것이다.
package org.example.sixthchapter;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
public class TransactionAdvice implements MethodInterceptor {
PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TransactionStatus status = this.transactionManager.getTransaction(
new DefaultTransactionDefinition()
);
try {
Object ret = invocation.proceed();
this.transactionManager.commit(status);
return ret;
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
@Bean(name = "transactionAdvice")
public TransactionAdvice transactionAdvice() throws Exception {
TransactionAdvice transactionAdvice = new TransactionAdvice();
transactionAdvice.setTransactionManager(platformTransactionManager());
return transactionAdvice;
}
위 코드를 보게되면 InvocationHandler 을 사용했을 때와 다르게 타깃 오브젝트가 따로 등장하지 않는다. MethodInterceptor 로는 메소드 정보와 함께 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달된다. MethodInvocation 은 타깃 오브젝트의 메소드를 실행할 수 있는 기능이 있는 객체로, 일종의 콜백 오브젝트이다 (proceed() 메소드로 타깃 오브젝트의 메소드를 내부적으로 실행). ProxyFactoryBean 에 MethodInterceptor 을 설정해줄 때에는 일반적인 DI 의 경우처럼 수정자 메소드를 사용하는 대신 addAdvice() (MethodInterceptor 은 Advice 인터페이스를 상속하고 있는 서브인터페이스이다) 메소드를 사용한다. MethodInterceptor 처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 Advice 라고 부른다. 하나의 ProxyFactoryBean 에는 여러 개의 MethodInterceptor 을 추가해줄 수 있다. 앞서 사용했던 FactoryBean 의 단점 중의 하나를 해결해주는 것이다. (매번 새로운 부가기능이 생길 때마다 팩토리 빈도 추가해줘야하는 문제)
또, 기존에 만들었던 적용대상 메소드를 선정하는 부가기능도 ProxyFactoryBean 에서는 기본으로 제공한다. 메소드 선정 알고리즘을 담은 오브젝트를 따로 사용하는데, 이를 포인트컷이라고 부른다. 어드바이스와 포인트컷 모두 프록시에 DI 되어 사용되는 것이다. 두 가지 모두 여러 프록시에서 공유가 가능하도록 만들어져 싱글톤 빈으로 사용이 가능하며, 둘 다 하나의 프록시에 여러 종류가 들어갈 수 있다. 그리고 어드바이스와 포인트컷을 묶은 오브젝트를 인터페이스 이름을 따서 어드바이저라고 부른다. 포인트컷은 Pointcut 인터페이스를 구현해서 만들게된다. 프록시는 포인트컷으로부터 부가기능을 적용할 대상 메소드인지 확인받으면, MethodInterceptor 타입의 어드바이스를 호출한다. 포인트컷은 아래와 같이 익명클래스로 만들어서 바로 Bean 으로 등록해보았다.
@Bean(name = "transactionPointcut")
public NameMatchMethodPointcut transactionPointcut() throws Exception {
NameMatchMethodPointcut nameMatchMethodPointcut = new NameMatchMethodPointcut(){
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz.getSimpleName().startsWith("UserService");
}
};
}
};
nameMatchMethodPointcut.setMappedName("upgrade*");
return nameMatchMethodPointcut;
}
또, 위 Pointcut 에서는 본래 모든 클래스를 다 받아주는 클래스 필터를 리턴하던 getClassFilter() 메소드를 오버라이드하여 이름이 UserService 로 시작하는 클래스만을 선정해주는 필터로 만들었다.
이제 위 둘을 활용하여 어드바이저와 ProxyFactoryBean 을 Bean 으로 등록해줄 수 있다.
@Bean(name = "transactionAdvisor")
public DefaultPointcutAdvisor transactionAdvisor() throws Exception {
DefaultPointcutAdvisor defaultPointcutAdvisor = new DefaultPointcutAdvisor(transactionPointcut(), transactionAdvice());
return defaultPointcutAdvisor;
}
@Bean
public ProxyFactoryBean factoryBean() throws Exception {
ProxyFactoryBean factoryBean = new ProxyFactoryBean();
factoryBean.setTarget(userServiceImpl());
factoryBean.setInterceptorNames(new String[] {"transactionAdvisor"});
return factoryBean;
}
어드바이저는 interceptorNames 라는 프로퍼티를 통해 넣게된다. 테스트 코드는 전과 같은 코드를 사용할 수 있다.
@Autowired
private ProxyFactoryBean factoryBean;
@Test
public void pointcutAdvisor() {
UserService userService = (UserService) factoryBean.getObject();
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
userService.upgradeLevels();
} catch (Exception e) {
}
checkLevelUpgraded(users.get(1), true);
}
스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트를 제공해준다. 그 중에서 BeanPostProcessor 이라는 것에 대해 알아본다. 이는 스프링 빈 오브젝트로 만들어지고 난 후, 빈 오브젝트를 다시 가공할 수 있게 해준다. 책에서는 이러한 빈 후처리기 중 하나인 DefaultAdvisorAutoProxyCreator 에 대해 설명한다. 이는 어드바이저를 이용한 자동 프록시 생성기로, 빈 후처리기 자체를 빈으로 등록하여 스프링에 적용한다. 스프링은 빈 후처리기가 빈으로 등록되어 있으면 빈 오브젝트가 생성될 때마다 빈 후처리기에 보내서 후처리 작업을 요청한다. 이는 빈 오브젝트의 프로퍼티를 강제로 수정할 수도, 별도의 초기화 작업을 수행할 수도 있다. 이를 이용하여 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수 있다. 이를 자동 프록시 생성 빈 후처리기라고 말한다.
Reference:
토비의 스프링
'Backend > Toby Spring' 카테고리의 다른 글
토비의 Spring (10: Spring Annotations) (0) | 2023.06.20 |
---|---|
토비의 Spring (9: AOP(5)) (0) | 2023.06.13 |
토비의 Spring (9: AOP(3)) (0) | 2023.06.08 |
토비의 Spring (9: AOP(2)) (0) | 2023.06.07 |
토비의 Spring (9: AOP(1)) (0) | 2023.05.22 |