6.4) 스프링의 프록시 팩토리 빈
ProxyFactoryBean
- 스프링은 어떤 해결책을 제시해서 트랜잭션 부가기능을 추가해줄 수 있을까?
- 스프링은 트랜잭션 기술과 메일 발송 기술에 적용했던 서비스 추상화를 프록시 기술에도 동일하게 적용하고 있다.
- 그러므로 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다.
- 생성된 프록시는 스프링의 빈으로 등록돼야 한다.
- 스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공해준다.
- 스프링은 트랜잭션 기술과 메일 발송 기술에 적용했던 서비스 추상화를 프록시 기술에도 동일하게 적용하고 있다.
- 스프링의 ProxyFactoryBean?
- 스프링의 ProxyFactoryBean은 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈이다.
- 기존에 만들었던 TxProxyFactoryBean과 달리,
순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해줄 부가기능은 별도의 빈에 둘 수 있다. - ProxyFactoryBean이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다.
- MethodInterceptor의 invoke()는 InvocationHandler의 invoke() 달리,
ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보도 함께 제공받기 때문에
타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. - 따라서 MethodInterceptor 오브젝트는 타깃이 다른 여러 프록시에서 함께 사용할 수 있고, 싱글톤 빈으로 등록 가능하다.
- 다이내믹 프록시 학습 테스트를 스프링의 ProxyFactoryBean을 이용하도록 수정해보자.
- InvocationHandler를 구현했을 때와 달리 MethodInterceptor를 구현할 경우
메소드 정보와 함께 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달되며
MethonInvocation은 타깃 오브젝트의 메소드를 실행할 수 있는 기능이 있기 때문에
MethodInterceptor는 부가기능을 제공하는 데만 집중할 수 있다. - MethodInvocation은 일종의 콜백 오브젝트로,
proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해준다. - 그러므로 MethodInvocation 구현 클래스는 일종의 공유 가능한 템플릿처럼 동작하게 된다.
- ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용했기 때문에
템플릿 역할을 하는 MethodInvocation을 싱클톤으로 두고 공유할 수 있다. - 또한 ProxyFactoryBean에는 여러 개의 MethodInterceptor를 추가할 수 있으며
하나만으로 여러 개의 부가기능을 제공해주는 프록시를 만들 수 있다.
그러므로 부가기능을 추가할 때마다 프록시와 프록시 팩토리 빈을 추가하지 않고
아무리 많은 부가기능을 적용하더라도 ProxyFactoryBean 하나로 충분하다. - MethodInterceptor처럼 타깃 오브젝트에 종속되지 않고
타깃 오브젝트에 적용하는 순수한 부가기능을 담은 오브젝트를 스프링에서는 어드바이스라고 부른다. - 그러므로 이를 위해서는 일반적인 DI 경우처럼 수정자 메소드를 사용하는 대신
addAdvice()라는 메소드를 사용하여 MethodInterceptor를 설정하게 된다. - 또한 ProxyFactoryBean은 인터페이스를 굳이 알려주지 않아도 인터페이스 자동검출 기능을 사용해
타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아내고, 알아낸 인터페이스를 모두 구현하는 프록시를 만들어준다.
그러므로 프록시가 구현해야 하는 인터페이스를 제공해주지 않아도 된다. - 만약 타깃 오브젝트가 구현하는 오브젝트 중에서 일부만 프록시에 적용을 원한다면 인터페이스 정보를 직접 제공해준다.
- InvocationHandler를 구현했을 때와 달리 MethodInterceptor를 구현할 경우
/**
* 스프링 ProxyFactoryBean을 이용한 다이내믹 프록시 테스트
*/
public class DynamicProxyTest {
// 자바 JDK의 다이내믹 프록시 생성
@Test
public void simpleProxy() {
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로드
new Class[] {Hello.class}, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler 구현 오브젝트
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
// 스프링의 다이내믹 프록시 생성
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
pfBean.addAdvice(new UppercaseAdvice()); // 부가기능을 담은 어드바이스를 추가, 여러 개 추가 가능
Hello proxiedHello = (Hello)pfBean.getObject(); // FactoryBean이므로 getObject()로 생성된 팩토리를 가져옴
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
}
/**
* MethodInterceptor 구현 클래스
*/
public class UppercaseAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
// MethodInvocation은 메소드 정보와 함께 타깃 오브젝트를 알고 있으므로
// 리플렉션의 Method와 달리 메소드 실행 시 타깃 오브젝트를 전달할 필요가 없다.
String ret = (String)invocation.proceed();
return ret.toUpperCase(); // 부가기능 적용
}
}
- 부가기능을 적용할 대상 메소드를 어떻게 선정할까?
- 기존에 InvocationHandler를 직접 구현했을 때는
TxProxyFactoryBean은 메소드 이름 비교용 스트링 값인 pattern을 DI 받아 TransactionHandler를 생성 시 넘겨주고,
TransactionHandler는 요청이 들어오는 메소드의 이름과 패턴을 비교해서 부가기능인 트랜잭션 적용 대상을 판별했다. - 스프링의 MethodInterceptor 오브젝트는 여러 프록시가 공유할 수 있기 위해 타깃 정보를 갖고 있지 않도록 만들었다.
그 덕분에 싱글톤 빈으로 등록할 수도 있었다.
그런데 여기에다 트랜잭션 적용 대상 메소드 이름 패턴을 넣어줄 경우 프록시마다 다를 수 있기 때문에 문제가 된다. - 그러므로 함께 두기 곤란한 성격이 다르고 변경 이유와 시점이 다르고,
생성 방식과 의존관계가 다른 코드가 함께 있다면 분리해주면 된다. - 이를 위해 MethodInterceptor에는 프록시가 클라이언트로부터 받는 요청을 일일이 전달받을 필요 없이
단순히 재사용 가능한 순수한 부가기능 제공 코드만 남겨주고, 대신 프록시에 부가기능 적용 메소드 선택 기능을 넣는다. - 물론 프록시의 핵심 가치는 타깃을 대신해서 클라이언트의 요청을 받아 처리하는 오브젝트로서의 존재 자체이므로,
메소드를 선별하는 기능은 일종의 교환 가능한 알고리즘이기 때문에 프록시로부터 분리하여 전략 패턴을 적용할 수 있다. - 이를 통해 스프링의 ProxyFactoryBean 방식은
두 가지 확장 기능인 부가기능과 메소드 선정 알고리즘을 활용하는 유연한 구조를 제공할 수 있다. - 스프링은 부가기능을 제공하는 오브젝트를 어드바이스라고 부르고,
메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다.
포인트컷이 필요 없을 때는 ProxyFactoryBenadml addAdvice() 메소드를 호출해서 어드바이스만 등록하면 됐다.
포인트컷을 함께 등록할 때는 어드바이스와 포인트컷을 어드바이저로 묶어서
addAdvisor()를 통해 모두 프록시에 DI로 주입돼서 사용된다.
이때 여러 개의 어드바이스가 등록되더라도 각 다른 포인트컷과 조합될 수 있기 때문에 각기 다른 메소드 선정을 할 수 있다.
즉, 어드바이저 = 포인트컷 (메소드 선정 알고리즘) + 어드바이스 (부가기능) - 두 가지 모두 프록시에서 공유가 가능하도록 만들어지기 때문에 스프링의 싱글톤 빈으로 등록이 가능하다.
- 프록시는 클라이언트로부터 요청을 받으면 먼저 포인트컷에세 부가기능을 부여할 메소드인지를 확인해달라고 요청하고
부가기능을 적용할 대상 메소드인지 확인받으면, MethodInterceptor 타입의 어드바이스를 호출하면 된다. - 이처럼 실제 위임 대상인 타깃 오브젝트의 레퍼런스를 갖고 있고, 이를 이용해 타깃 메소드를 직접 호출하는 것은
프록시가 메소드 호출에 따라 만드는 Invocation 콜백의 역할이며
재사용 가능한 기능은 만들어두고 바뀌는 부분만 외부에서 주입해서 작업 흐름 중에 사용하도록 하는 템플릿/콜백 구조다.
즉, 어드바이스가 일종의 템플릿이 되고 타깃을 호출하는 기능을 갖고 있는 MethodInterceptor 오브젝트가 콜백이 된다. - 템플릿은 한 번 만들면 재사용이 가능하고 여러 빈이 공유해서 사용할 수 있듯이,
어드바이스도 독립적인 싱글톤 빈으로 등록하고 DI를 주입해서 여러 프록시가 사용하도록 만들 수 있다. - 만약 구체적인 부가기능 방식이나 메소드 선정 알고리즘이 바뀔 경우 구현 클래스만 바꿔서 설정에 넣어주면 된다.
- 기존에 InvocationHandler를 직접 구현했을 때는

/**
* 스프링 ProxyFactoryBean을 이용한 다이내믹 프록시 테스트
*/
public class DynamicProxyTest {
// 자바 JDK의 다이내믹 프록시 생성
@Test
public void simpleProxy() {
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로드
new Class[] {Hello.class}, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler 구현 오브젝트
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
// 스프링의 다이내믹 프록시 생성
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
pfBean.addAdvice(new UppercaseAdvice()); // 부가기능을 담은 어드바이스를 추가, 여러 개 추가 가능
Hello proxiedHello = (Hello)pfBean.getObject(); // FactoryBean이므로 getObject()로 생성된 팩토리를 가져옴
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
// 포인트컷까지 적용한 ProxyFactoryBean
@Test
public void pointcutAdvisor() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
// 메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 생성
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
// 이름 비교조건 설정
pointcut.setMappedName("sayH*"); // sayH로 시작하는 모든 메소드를 선택
// 포인트컷과 어드바이스를 Advisor로 묶어서 한 번에 추가
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
Hello proxiedHello = (Hello)pfBean.getObject();
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
// 메소드 이름이 포인트컷의 선정조건에 맞지 않으므로, 부가기능이 적용되지 않는다.
assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
}
}
ProxyFactoryBean 적용
- JDK 다이내믹 프록시의 구조를 그대로 이용해서 만들었던 TxProxyFactoryBean을
스프링이 제공하는 ProxyFactoryBean을 이용하도록 수정해보자.
- 부가기능을 담당하는 어드바이스는 MethodInterceptor라는 Advice 서브인터페이스를 구현해서 TransactionAdvice를
만들게 되면 JDK 다이내믹 프록시의 InvocationHandler를 이용해서 만들었을 때보다 코드가 간결하다. - 리플렉션을 통한 타깃 메소드 호출 작업의 번거로움은 MethodInvocation 타입의 콜백을 이용한 덕분에 대부분 제거된다.
또한 타깃 메소드가 던지는 예외도 포장돼서 오는 것이 아니기 때문에 그대로 잡아서 처리하면 된다. - 다음으로 설정파일에서 어드바이스를 등록하기 위해 트랜잭션 기능 적용을 위한 transactionManager만 DI한다.
- 그리고 트랜잭션 전용 메소드 선정을 위한 포인트컷 빈을 등록한다.
- 이제 어드바이스와 포인트컷을 담을 어드바이저를 빈으로 등록한다.
- 마지막으로 ProxyFactoryBean을 등록하고 프로퍼티에 타깃 빈과 어드바이저 빈을 지정해주면 된다.
- 어드바이스, 포인트컷, 어드바이저 등으로 빈의 숫자가 늘어나서 설정이 더 복잡해진 것 같기도 하지만,
어드바이스와 포인트컷은 여러 ProxyFactoryBean에서 재사용 가능하기 때문에 복잡해진 건 아니다.
- 부가기능을 담당하는 어드바이스는 MethodInterceptor라는 Advice 서브인터페이스를 구현해서 TransactionAdvice를
/**
* 트랜잭션 어드바이스
*/
public class TransactionAdvice implements MethodInterceptor { // 스프링의 어드바이스 인터페이스 구현
PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// 타깃을 호출하는 기능을 가진 콜백 오브젝트를 프록시로부터 받는다.
// 덕분에 어드바이스는 특정 타깃에 의존하지 않고 재사용 가능하다.
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;
}
// 스프링의 MethodInvocation을 통한 타깃 호출은 예외가 포장되지 않고 타깃에서 보낸 그대로 전달한다.
catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- 트랜잭션 어드바이스 빈 설정 -->
<bean id="transactionAdvice" class="com.gaga.springtoby.user.service.TransactionAdvice">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<!-- 포인트컷 빈 설정 -->
<bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="upgrade*"/>
</bean>
<!-- 어드바이저 빈 설정 -->
<bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="transactionAdvice"/>
<property name="pointcut" ref="transactionPointcut"/>
</bean>
<!-- ProxyFactoryBean 설정 -->
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="userServiceImpl"/>
<property name="interceptorNames">
<list>
<value>transactionAdvisor</value>
</list>
</property>
</bean>
<bean id="userServiceImpl" class="com.gaga.springtoby.user.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="mailSender" class="com.gaga.springtoby.user.service.DummyMailSender"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/toby"/>
<property name="username" value="toby"/>
<property name="password" value="gaga"/>
</bean>
</beans>
- 테스트 코드도 정리하자.
- 대부분의 테스트 코드는 트랜잭션을 신경 쓰지 않고 고립된 테스트로 만들면 되기 때문에 문제가 되지 않는다.
- 하지만 학습 테스트로 만든 upgradeAllOrNothing()의 경우 트랜잭션이 적용됐는지를 확인하는 테스트이므로
팩토리 빈을 가져올 때 캐스팅할 타입만 ProxyFactoryBean으로 변경해주면 된다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
// 예외 발생 시 작업 취소 여부 테스트
@Test
@DirtiesContext // 다이내믹 프록시 팩토리 빈을 직접 만들어 사용할 때는 없앴다가 다시 등장한 컨텍스트 무효화 애노테이션
public void upgradeAllOrNothing() throws Exception {
// 예외를 발생시킬 네 번째 사용자의 id를 넣어서 테스트용 UserService 대역 오브젝트를 생성한다.
TestUserService testUserService = new TestUserService(users.get(3).getId());
// userDao를 수동 DI 해준다.
testUserService.setUserDao(this.userDao);
// 테스트용 UserService를 위한 메일 전송 오브젝트 빈인 MailSender를 수동 DI 해준다.
testUserService.setMailSender(this.mailSender);
// ProxyFactoryBean을 이용하여 트랜잭션을 테스트한다.
// 팩토리 빈 자체를 가져와야 하므로 빈 이름에 &를 반드시 넣어야 한다.
ProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", ProxyFactoryBean.class);
txProxyFactoryBean.setTarget(testUserService);
// 변경된 타깃 설정을 이용해서 트랜잭션 다이내믹 프록시 오브젝트를 다시 생성한다.
UserService txUserService = (UserService)txProxyFactoryBean.getObject();
userDao.deleteAll();
for (User user : users)
userDao.add(user);
// TestUserService는 업그레이드 작업 중에 예외가 발생해야 한다.
try {
// 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야 한다.
txUserService.upgradeLevels(); // UserService (TxProxyFactoryBean) -> UserServiceImpl
fail("TestUserServiceException expected");
}
// TestUserService가 던져주는 예외를 잡아서 계속 진행되도록 한다.
catch (TestUserServiceException e) {
}
// 예외가 발생하기 전에 레벨 변경이 있었던 사용자의 레벨이 처음 상태로 바뀌었나 확인한다.
checkLevelUpgraded(users.get(1), false);
}
}
- 어드바이스와 포인트컷을 재사용하려면?
- ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.
그 덕분에 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있었다. - 이제 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 이미 만들어둔 어드바이스를 그대로 재사용할 수 있다.
- 메소드 선정을 위한 포인트컷이 필요하면 이름 패턴만 지정해서 ProxyFactoryBean에 등록해주면 된다.
- 즉, 트랜잭션 부가기능을 담은 TrasactionAdvice는 하나만 만들어서 싱글톤 빈으로 등록해준 후
DI 설정을 통해 모든 서비스에 적용하며
만약 메소드 선정 방식이 달라지는 경우에만 포인트컷의 설정을 따로 등록하고 어드바이저로 조합해서 적용해주면 된다.
- ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.

'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (6) (0) | 2024.01.03 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (5) (0) | 2023.12.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (3) (0) | 2023.12.19 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (2) (0) | 2023.12.16 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (1) (0) | 2023.12.15 |
6.4) 스프링의 프록시 팩토리 빈
ProxyFactoryBean
- 스프링은 어떤 해결책을 제시해서 트랜잭션 부가기능을 추가해줄 수 있을까?
- 스프링은 트랜잭션 기술과 메일 발송 기술에 적용했던 서비스 추상화를 프록시 기술에도 동일하게 적용하고 있다.
- 그러므로 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다.
- 생성된 프록시는 스프링의 빈으로 등록돼야 한다.
- 스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공해준다.
- 스프링은 트랜잭션 기술과 메일 발송 기술에 적용했던 서비스 추상화를 프록시 기술에도 동일하게 적용하고 있다.
- 스프링의 ProxyFactoryBean?
- 스프링의 ProxyFactoryBean은 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈이다.
- 기존에 만들었던 TxProxyFactoryBean과 달리,
순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해줄 부가기능은 별도의 빈에 둘 수 있다. - ProxyFactoryBean이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다.
- MethodInterceptor의 invoke()는 InvocationHandler의 invoke() 달리,
ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보도 함께 제공받기 때문에
타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. - 따라서 MethodInterceptor 오브젝트는 타깃이 다른 여러 프록시에서 함께 사용할 수 있고, 싱글톤 빈으로 등록 가능하다.
- 다이내믹 프록시 학습 테스트를 스프링의 ProxyFactoryBean을 이용하도록 수정해보자.
- InvocationHandler를 구현했을 때와 달리 MethodInterceptor를 구현할 경우
메소드 정보와 함께 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달되며
MethonInvocation은 타깃 오브젝트의 메소드를 실행할 수 있는 기능이 있기 때문에
MethodInterceptor는 부가기능을 제공하는 데만 집중할 수 있다. - MethodInvocation은 일종의 콜백 오브젝트로,
proceed() 메소드를 실행하면 타깃 오브젝트의 메소드를 내부적으로 실행해준다. - 그러므로 MethodInvocation 구현 클래스는 일종의 공유 가능한 템플릿처럼 동작하게 된다.
- ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용했기 때문에
템플릿 역할을 하는 MethodInvocation을 싱클톤으로 두고 공유할 수 있다. - 또한 ProxyFactoryBean에는 여러 개의 MethodInterceptor를 추가할 수 있으며
하나만으로 여러 개의 부가기능을 제공해주는 프록시를 만들 수 있다.
그러므로 부가기능을 추가할 때마다 프록시와 프록시 팩토리 빈을 추가하지 않고
아무리 많은 부가기능을 적용하더라도 ProxyFactoryBean 하나로 충분하다. - MethodInterceptor처럼 타깃 오브젝트에 종속되지 않고
타깃 오브젝트에 적용하는 순수한 부가기능을 담은 오브젝트를 스프링에서는 어드바이스라고 부른다. - 그러므로 이를 위해서는 일반적인 DI 경우처럼 수정자 메소드를 사용하는 대신
addAdvice()라는 메소드를 사용하여 MethodInterceptor를 설정하게 된다. - 또한 ProxyFactoryBean은 인터페이스를 굳이 알려주지 않아도 인터페이스 자동검출 기능을 사용해
타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아내고, 알아낸 인터페이스를 모두 구현하는 프록시를 만들어준다.
그러므로 프록시가 구현해야 하는 인터페이스를 제공해주지 않아도 된다. - 만약 타깃 오브젝트가 구현하는 오브젝트 중에서 일부만 프록시에 적용을 원한다면 인터페이스 정보를 직접 제공해준다.
- InvocationHandler를 구현했을 때와 달리 MethodInterceptor를 구현할 경우
/**
* 스프링 ProxyFactoryBean을 이용한 다이내믹 프록시 테스트
*/
public class DynamicProxyTest {
// 자바 JDK의 다이내믹 프록시 생성
@Test
public void simpleProxy() {
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로드
new Class[] {Hello.class}, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler 구현 오브젝트
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
// 스프링의 다이내믹 프록시 생성
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
pfBean.addAdvice(new UppercaseAdvice()); // 부가기능을 담은 어드바이스를 추가, 여러 개 추가 가능
Hello proxiedHello = (Hello)pfBean.getObject(); // FactoryBean이므로 getObject()로 생성된 팩토리를 가져옴
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
}
/**
* MethodInterceptor 구현 클래스
*/
public class UppercaseAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
// MethodInvocation은 메소드 정보와 함께 타깃 오브젝트를 알고 있으므로
// 리플렉션의 Method와 달리 메소드 실행 시 타깃 오브젝트를 전달할 필요가 없다.
String ret = (String)invocation.proceed();
return ret.toUpperCase(); // 부가기능 적용
}
}
- 부가기능을 적용할 대상 메소드를 어떻게 선정할까?
- 기존에 InvocationHandler를 직접 구현했을 때는
TxProxyFactoryBean은 메소드 이름 비교용 스트링 값인 pattern을 DI 받아 TransactionHandler를 생성 시 넘겨주고,
TransactionHandler는 요청이 들어오는 메소드의 이름과 패턴을 비교해서 부가기능인 트랜잭션 적용 대상을 판별했다. - 스프링의 MethodInterceptor 오브젝트는 여러 프록시가 공유할 수 있기 위해 타깃 정보를 갖고 있지 않도록 만들었다.
그 덕분에 싱글톤 빈으로 등록할 수도 있었다.
그런데 여기에다 트랜잭션 적용 대상 메소드 이름 패턴을 넣어줄 경우 프록시마다 다를 수 있기 때문에 문제가 된다. - 그러므로 함께 두기 곤란한 성격이 다르고 변경 이유와 시점이 다르고,
생성 방식과 의존관계가 다른 코드가 함께 있다면 분리해주면 된다. - 이를 위해 MethodInterceptor에는 프록시가 클라이언트로부터 받는 요청을 일일이 전달받을 필요 없이
단순히 재사용 가능한 순수한 부가기능 제공 코드만 남겨주고, 대신 프록시에 부가기능 적용 메소드 선택 기능을 넣는다. - 물론 프록시의 핵심 가치는 타깃을 대신해서 클라이언트의 요청을 받아 처리하는 오브젝트로서의 존재 자체이므로,
메소드를 선별하는 기능은 일종의 교환 가능한 알고리즘이기 때문에 프록시로부터 분리하여 전략 패턴을 적용할 수 있다. - 이를 통해 스프링의 ProxyFactoryBean 방식은
두 가지 확장 기능인 부가기능과 메소드 선정 알고리즘을 활용하는 유연한 구조를 제공할 수 있다. - 스프링은 부가기능을 제공하는 오브젝트를 어드바이스라고 부르고,
메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다.
포인트컷이 필요 없을 때는 ProxyFactoryBenadml addAdvice() 메소드를 호출해서 어드바이스만 등록하면 됐다.
포인트컷을 함께 등록할 때는 어드바이스와 포인트컷을 어드바이저로 묶어서
addAdvisor()를 통해 모두 프록시에 DI로 주입돼서 사용된다.
이때 여러 개의 어드바이스가 등록되더라도 각 다른 포인트컷과 조합될 수 있기 때문에 각기 다른 메소드 선정을 할 수 있다.
즉, 어드바이저 = 포인트컷 (메소드 선정 알고리즘) + 어드바이스 (부가기능) - 두 가지 모두 프록시에서 공유가 가능하도록 만들어지기 때문에 스프링의 싱글톤 빈으로 등록이 가능하다.
- 프록시는 클라이언트로부터 요청을 받으면 먼저 포인트컷에세 부가기능을 부여할 메소드인지를 확인해달라고 요청하고
부가기능을 적용할 대상 메소드인지 확인받으면, MethodInterceptor 타입의 어드바이스를 호출하면 된다. - 이처럼 실제 위임 대상인 타깃 오브젝트의 레퍼런스를 갖고 있고, 이를 이용해 타깃 메소드를 직접 호출하는 것은
프록시가 메소드 호출에 따라 만드는 Invocation 콜백의 역할이며
재사용 가능한 기능은 만들어두고 바뀌는 부분만 외부에서 주입해서 작업 흐름 중에 사용하도록 하는 템플릿/콜백 구조다.
즉, 어드바이스가 일종의 템플릿이 되고 타깃을 호출하는 기능을 갖고 있는 MethodInterceptor 오브젝트가 콜백이 된다. - 템플릿은 한 번 만들면 재사용이 가능하고 여러 빈이 공유해서 사용할 수 있듯이,
어드바이스도 독립적인 싱글톤 빈으로 등록하고 DI를 주입해서 여러 프록시가 사용하도록 만들 수 있다. - 만약 구체적인 부가기능 방식이나 메소드 선정 알고리즘이 바뀔 경우 구현 클래스만 바꿔서 설정에 넣어주면 된다.
- 기존에 InvocationHandler를 직접 구현했을 때는

/**
* 스프링 ProxyFactoryBean을 이용한 다이내믹 프록시 테스트
*/
public class DynamicProxyTest {
// 자바 JDK의 다이내믹 프록시 생성
@Test
public void simpleProxy() {
Hello proxiedHello = (Hello)Proxy.newProxyInstance(
getClass().getClassLoader(), // 동적으로 생성되는 다이내믹 프록시 클래스의 로딩에 사용할 클래스 로드
new Class[] {Hello.class}, // 구현할 인터페이스
new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler 구현 오브젝트
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
// 스프링의 다이내믹 프록시 생성
@Test
public void proxyFactoryBean() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
pfBean.addAdvice(new UppercaseAdvice()); // 부가기능을 담은 어드바이스를 추가, 여러 개 추가 가능
Hello proxiedHello = (Hello)pfBean.getObject(); // FactoryBean이므로 getObject()로 생성된 팩토리를 가져옴
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
// 포인트컷까지 적용한 ProxyFactoryBean
@Test
public void pointcutAdvisor() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget()); // 타깃 설정
// 메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트컷 생성
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
// 이름 비교조건 설정
pointcut.setMappedName("sayH*"); // sayH로 시작하는 모든 메소드를 선택
// 포인트컷과 어드바이스를 Advisor로 묶어서 한 번에 추가
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
Hello proxiedHello = (Hello)pfBean.getObject();
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
// 메소드 이름이 포인트컷의 선정조건에 맞지 않으므로, 부가기능이 적용되지 않는다.
assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
}
}
ProxyFactoryBean 적용
- JDK 다이내믹 프록시의 구조를 그대로 이용해서 만들었던 TxProxyFactoryBean을
스프링이 제공하는 ProxyFactoryBean을 이용하도록 수정해보자.
- 부가기능을 담당하는 어드바이스는 MethodInterceptor라는 Advice 서브인터페이스를 구현해서 TransactionAdvice를
만들게 되면 JDK 다이내믹 프록시의 InvocationHandler를 이용해서 만들었을 때보다 코드가 간결하다. - 리플렉션을 통한 타깃 메소드 호출 작업의 번거로움은 MethodInvocation 타입의 콜백을 이용한 덕분에 대부분 제거된다.
또한 타깃 메소드가 던지는 예외도 포장돼서 오는 것이 아니기 때문에 그대로 잡아서 처리하면 된다. - 다음으로 설정파일에서 어드바이스를 등록하기 위해 트랜잭션 기능 적용을 위한 transactionManager만 DI한다.
- 그리고 트랜잭션 전용 메소드 선정을 위한 포인트컷 빈을 등록한다.
- 이제 어드바이스와 포인트컷을 담을 어드바이저를 빈으로 등록한다.
- 마지막으로 ProxyFactoryBean을 등록하고 프로퍼티에 타깃 빈과 어드바이저 빈을 지정해주면 된다.
- 어드바이스, 포인트컷, 어드바이저 등으로 빈의 숫자가 늘어나서 설정이 더 복잡해진 것 같기도 하지만,
어드바이스와 포인트컷은 여러 ProxyFactoryBean에서 재사용 가능하기 때문에 복잡해진 건 아니다.
- 부가기능을 담당하는 어드바이스는 MethodInterceptor라는 Advice 서브인터페이스를 구현해서 TransactionAdvice를
/**
* 트랜잭션 어드바이스
*/
public class TransactionAdvice implements MethodInterceptor { // 스프링의 어드바이스 인터페이스 구현
PlatformTransactionManager transactionManager;
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
// 타깃을 호출하는 기능을 가진 콜백 오브젝트를 프록시로부터 받는다.
// 덕분에 어드바이스는 특정 타깃에 의존하지 않고 재사용 가능하다.
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;
}
// 스프링의 MethodInvocation을 통한 타깃 호출은 예외가 포장되지 않고 타깃에서 보낸 그대로 전달한다.
catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<!-- 트랜잭션 어드바이스 빈 설정 -->
<bean id="transactionAdvice" class="com.gaga.springtoby.user.service.TransactionAdvice">
<property name="transactionManager" ref="transactionManager"/>
</bean>
<!-- 포인트컷 빈 설정 -->
<bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
<property name="mappedName" value="upgrade*"/>
</bean>
<!-- 어드바이저 빈 설정 -->
<bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
<property name="advice" ref="transactionAdvice"/>
<property name="pointcut" ref="transactionPointcut"/>
</bean>
<!-- ProxyFactoryBean 설정 -->
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="userServiceImpl"/>
<property name="interceptorNames">
<list>
<value>transactionAdvisor</value>
</list>
</property>
</bean>
<bean id="userServiceImpl" class="com.gaga.springtoby.user.service.UserServiceImpl">
<property name="userDao" ref="userDao"/>
<property name="mailSender" ref="mailSender"/>
</bean>
<bean id="userDao" class="com.gaga.springtoby.user.dao.UserDaoJdbc">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="mailSender" class="com.gaga.springtoby.user.service.DummyMailSender"/>
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost/toby"/>
<property name="username" value="toby"/>
<property name="password" value="gaga"/>
</bean>
</beans>
- 테스트 코드도 정리하자.
- 대부분의 테스트 코드는 트랜잭션을 신경 쓰지 않고 고립된 테스트로 만들면 되기 때문에 문제가 되지 않는다.
- 하지만 학습 테스트로 만든 upgradeAllOrNothing()의 경우 트랜잭션이 적용됐는지를 확인하는 테스트이므로
팩토리 빈을 가져올 때 캐스팅할 타입만 ProxyFactoryBean으로 변경해주면 된다.
/**
* UserServiceTest 클래스
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserServiceTest {
...
// 예외 발생 시 작업 취소 여부 테스트
@Test
@DirtiesContext // 다이내믹 프록시 팩토리 빈을 직접 만들어 사용할 때는 없앴다가 다시 등장한 컨텍스트 무효화 애노테이션
public void upgradeAllOrNothing() throws Exception {
// 예외를 발생시킬 네 번째 사용자의 id를 넣어서 테스트용 UserService 대역 오브젝트를 생성한다.
TestUserService testUserService = new TestUserService(users.get(3).getId());
// userDao를 수동 DI 해준다.
testUserService.setUserDao(this.userDao);
// 테스트용 UserService를 위한 메일 전송 오브젝트 빈인 MailSender를 수동 DI 해준다.
testUserService.setMailSender(this.mailSender);
// ProxyFactoryBean을 이용하여 트랜잭션을 테스트한다.
// 팩토리 빈 자체를 가져와야 하므로 빈 이름에 &를 반드시 넣어야 한다.
ProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", ProxyFactoryBean.class);
txProxyFactoryBean.setTarget(testUserService);
// 변경된 타깃 설정을 이용해서 트랜잭션 다이내믹 프록시 오브젝트를 다시 생성한다.
UserService txUserService = (UserService)txProxyFactoryBean.getObject();
userDao.deleteAll();
for (User user : users)
userDao.add(user);
// TestUserService는 업그레이드 작업 중에 예외가 발생해야 한다.
try {
// 트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야 한다.
txUserService.upgradeLevels(); // UserService (TxProxyFactoryBean) -> UserServiceImpl
fail("TestUserServiceException expected");
}
// TestUserService가 던져주는 예외를 잡아서 계속 진행되도록 한다.
catch (TestUserServiceException e) {
}
// 예외가 발생하기 전에 레벨 변경이 있었던 사용자의 레벨이 처음 상태로 바뀌었나 확인한다.
checkLevelUpgraded(users.get(1), false);
}
}
- 어드바이스와 포인트컷을 재사용하려면?
- ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.
그 덕분에 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있었다. - 이제 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 이미 만들어둔 어드바이스를 그대로 재사용할 수 있다.
- 메소드 선정을 위한 포인트컷이 필요하면 이름 패턴만 지정해서 ProxyFactoryBean에 등록해주면 된다.
- 즉, 트랜잭션 부가기능을 담은 TrasactionAdvice는 하나만 만들어서 싱글톤 빈으로 등록해준 후
DI 설정을 통해 모든 서비스에 적용하며
만약 메소드 선정 방식이 달라지는 경우에만 포인트컷의 설정을 따로 등록하고 어드바이저로 조합해서 적용해주면 된다.
- ProxyFactoryBean은 스프링의 DI와 템플릿/콜백 패턴, 서비스 추상화 등의 기법이 모두 적용된 것이다.

'Java-Spring > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (6) (0) | 2024.01.03 |
---|---|
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (5) (0) | 2023.12.30 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (3) (0) | 2023.12.19 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (2) (0) | 2023.12.16 |
[토비의 스프링 3.1] Vol.1 스프링의 이해와 원리 - AOP (1) (0) | 2023.12.15 |