6.4.1 ProxyFactoryBean
- 스프링에서 제공하는 깔끔한 부가기능 추가를 위한 클래스
- JDK에서 제공하는 다이내믹 프록시 단점 해결
- 서비스 추상화와 동일하게 스프링은 일관된 방법으로 프록시를 만들 수 있는 추상 레이어임
- 스프링의 ProxyFactoryBean는 프록시를 생성해서 빈 오브젝트로 등록하게 하는 팩토리 빈임
- ProxyFactoryBean는 순수하게 프록시 생성 작업만 담당하기 때문에, 부가기능은 어드바이스라 불리면서 MethodInterceptor를 구현한 별도의 빈에 둘 수있음
- 즉, ProxyFactoryBean가 생성하는 프록시의 부가기능은 MethodInterceptor를 구현해서 만듦
- MethodInterceptor는 InvocationHandler와 비슷하지만 두 가지 다른 점이 있음
- 타깃 오브젝트 전달 유무
- InvocationHandler의 invoke()는 타깃 오브젝트가 필요해 전달받음
- Object ret = method.invoke(target, args);
- MethodInterceptor의 invoke()는 타깃 오브젝트가 필요하지 않아 전달받지 않음
- String ret = (String) invocation.proceed();
- 프록시가 구현할 인터페이스 지정 유무
- InvocationHandler는 프록시가 구현할 인터페이스를 지정해야 함
- new Class[] {Hello.class}
- MethodInterceptor는 프록시가 구현할 인터페이스를 지정하지 않아도 됨
- 💡 ProxyFactoryBean에 인터페이스 자동검출 기능을 이용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아내기 때문임. 만약 구현 인터페이스 중 일부 프록시만 지정해야 한다면 setInterface()로 지정할 수 있음
- 타깃 오브젝트 전달 유무
- 때문에, 타깃 오브젝트에 독립적으로 만들어 질 수 있어 싱글톤 빈으로 등록할 수 있으며 여러 프록시에서 함께 사용할 수 있음
public class DynamicProxyTest {
@Test
public void simpleProxy(){
//JDK에서 제공하는 다이내믹 프록시 만드는 방법
Hello proxiedHello = (Hello) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class[] {Hello.class}, //프록시가 구현할 인터페이스
new UppercaseHandler(new HelloTarget()) //부가기능
);
}
@Test
public void proxyFactoryBean(){
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
pfBean.addAdvice(new UppercaseAdvice()); //부가기능. 여러개 가능
//FactoryBean이므로 getObject()로 생성된 프록시를 가져옴
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"));
}
private static class UppercaseAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
//InvocationHandler와 달리 target이 필요 없음
String ret = (String)invocation.proceed();
return ret.toUpperCase();
}
}
}
6.4.2 어드바이스 : 타깃이 필요 없는 순수한 부가기능
- MethodInterceptor를 구현한 UppercaseAdvice는 타깃 오브젝트가 등장하지 않음
- MethodInterceptor의 파라미터인 MethodInvocation오브젝트로 메소드 정보와 타깃 오브젝트가 전달됨
- 즉, 파라미터로 전달되기 때문에 별도로 전달하지 않아도 됨
- 또한, MethodInvocation는 proceed()로 타깃 오브젝트의 메소드를 실행할 수 있기 때문에 MethodInterceptor는 부가기능을 제공하는 데만 집중할 수 있음
- MethodInvocation는 일종의 템플릿 역할을 하는 콜백 오브젝트임
- 템플릿 : 재사용을 하기 위한 것(ex.JdbcTemplate)
- 콜백 : 특정 메소드를 호출해서 알림
- MethodInvocation는 콜백으로 특정(실행할) 메소드를 호출해서 알려주면 proceed()가 해당 특정 메소드를 내부적으로 실행시킴
- 이 특징이 ProxyFactoryBean의 장점이자 JDK에서 제공하는 다이내믹 프록시 방법과 가장 큰 차이점임
- ProxyFactoryBean는 작은 단위의 템플릿/콜백 구조를 응용해서 적용했고, 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 공유하여 재사용할 수 있는것임
- ProxyFactoryBean는 위 예제 코드 주석에 적힌것과 같이 addAdvice()를 이용하여 부가기능을 위한 MethodInterceptor를 구현한 클래스를 여러개 추가할 수 있음
- 즉, ProxyFactoryBean하나만으로 여러 개의 부가기능을 제공해 주는 프록시를 만들 수 있어 JDK에서 제공하는 방법의 문제를 해결함
- 그런데 부가기능을 추가하는 메소드의 이름이 addMethodInterceptor가 아니라 addAdvice()임
- 이유는, MethodInterceptor가 Advice 인터페이스를 상속하기 있는 서브인터페이스이기 때문임
- 또한, 이름에서 알 수 있듯이 MethodInterceptor처럼 타깃 오브젝트에 부가기능을 추가하는 오브젝트를 어드바이스(Advice)라 함
6.4.3 포인트컷 : 부가기능 적용 대상 메소드 선정 방법
- InvocationHandler를 직접 구현할 때 메소드 이름을 가지고 부가기능 적용 대상 메소드를 선정 했음
- TxProxyFactoryBean은 pattern 필드를 통해 메소드 이름 비교용 스트링 값을 DI 받아 부가기능인 트랜잭션 적용 메소드를 선정했음
- ProxyFactoryBean의 MethodInterceptor는 여러 프록시가 공유하고 있음
- 프록시마다 부가기능 적용 대상 선정하는 방법이 다르기 때문에 MethodInterceptor내부에 부가기능 판별 코드를 추가하면 안됨
- MethodInterceptor는 InvocationHandler와 달리 프록시가 클라이언트로부터 받은 요청을 일일이 전달받을 필요가 없음
- 때문에 프록시에서 부가기능 적용 대상 선정을 한 뒤에, 순수한 부가기능만 있는 MethodInterceptor를 구현한 어드바이스를 호출하면 됨
- 그림에서 알 수 있듯이 메소드 선정 알고리즘을 포인트컷(Point Cut)이라 함
- 포인트컷과 어드바이스는 모두 프록시에 DI로 주입되서 사용됨
- 포인트컷, 어드바이스 모두 프록시에 공유할 수 있도록 만들어지기 때문에 스프링의 싱글톤 빈으로 등록할 수 있음
- 전략에 맞춰 포인트컷, 어드바이스를 바꿀수 있기 때문에 전략패턴임
- 포인트컷으로부터 부가기능 적용할 대상 메소드인지 확인을 한 뒤에 MethodInterceptor를 구현한 어드바이스를 호출함
- Invocation 콜백은 실제 위임 대상인 타깃 오브젝트의 레퍼런스를 갖고 있고, 타깃 메소드를 직접 호출하는 역할임
- 스프링에서 포인트컷을 위한 클래스를 제공하고 있음
- 앞선 예제와 같이, 이름을 통한 포인트컷은 NameMatchMethodPointCut 클래스임
//포인트컷을 위한 학습 테스트
public class DynamicProxyTest {
...
@Test
public void pointcutAdvisor(){
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("sayH*"); //메소드 선정 조건
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"));
//setMappedName가 'sayH'이기 때문에 sayThankYou는 포함 안됨
assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
}
}
6.4.4 어드바이서
- 어드바이서(Advisor) = 포인트컷 + 어드바이스
- 어드바이서 등록은 포인트컷과 어드바이스를 묶어서 등록하는 것임
//pointcut : 포인트컷(메소드 선정 알고리즘)
//new UppercaseAdvice() : 어드바이스(부가기능)
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
- 여러개의 어브다이스가 등록 되더라도, 각각 다른 포인트컷과 조합될 수 있기 때문에 하나로 묶어서 등록해야 함
- 이를 어드바이서라 함
6.4.5 TransactionAdvice
- 이전에 만들었던 TransactionHandler와 유사함
- TransactionHandler에서 메소드 선정 알고리즘을 제거함. 포인트 컷에 의해 선정될 것이기 때문임
- MethodInvocation을 통해 콜백
- MethodInterceptor는 InvocationHandler와 달리 예외 포장을 하지 않음
public class TransactionAdvice implements MethodInterceptor {
private 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;
}
}
}
6.4.6 스프링 XML 설정 파일
- 트랜잭션 어드바이스, 포인트컷, 어드바이저, ProxtFactoryBean 설정
- 어드바이저는 interceptorNames라는 프로퍼티를 통해 넣음
- 이유는 어드바이스, 어드바이저를 혼합해서 설정할 수 있게 하기 위함임
- 또한, 여러개 할당할 수 있기 때문에 <list>에 추가
<beans
...>
<bean id="transactionAdvice" class="com.ksb.spring.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>
<bean id="userService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="userServiceImpl"/>
<property name="interceptorNames">
<list>
<value>transactionAdvisor</value>
</list>
</property>
</bean>
</beans>
6.4.7 테스트 코드
- upgradeAllOrNothing()는 트랜잭션 적용이 되었는지 확인하는 코드임
- TxProxyFactoryBean를 ProxyFactoryBean로 변경
public class UserServiceTest {
@Test
@DirtiesContext//컨텍스트 설정 변경하기 때문에 여전히 필요
public void upgradeAllOrNothing() {
UserServiceImpl.TestUserService testUserService =
new UserServiceImpl.TestUserService(users.get(3).getId());
testUserService.setUserDao(this.userDao);
testUserService.setMailSender(this.mailSender);
//빈 자체를 가져올 때 &사용
ProxyFactoryBean txProxyFactoryBean =
context.getBean("&userService", ProxyFactoryBean.class);
txProxyFactoryBean.setTarget(testUserService);
//FactoryBean 타입 이므로 getObject()로 프록시를 가져옴
UserService txUserService = (UserService) txProxyFactoryBean.getObject();
userDao.deleteAll();
for (User user : users) userDao.add(user);
try {
txUserService.upgradeLevels(); //txHandler를 통한 upgradeLevels()실행
fail("TestUserServiceException expected");
} catch (UserServiceImpl.TestUserServiceException e) {
}
checkLevelUpgraded(users.get(0), false);
}
}
6.4.8 어드바이스와 포인트컷의 재사용
- ProxyFactoryBean은 DI, 템플릿/콜백, 서비스 추상화 등의 기법이 적용됨
- 때문에 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있음
- UserService 이외에 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 TransactionAdvice를 그대로 재사용 할 수 있음
- TransactionAdvice는 하나만 만들어서 싱글톤 빈으로 등록하면, DI 설정을 통해 모든 서비스에 적용가능
'토비의 스프링 정리' 카테고리의 다른 글
토비의 스프링 - 6.6 트랜잭션 속성 (0) | 2022.10.20 |
---|---|
토비의 스프링 - 6.5 스프링의 AOP (0) | 2022.10.20 |
토비의 스프링 - 6.3 다이내믹 프록시와 팩토리 빈 (0) | 2022.10.07 |
토비의 스프링 - 6.2 고립된 단위 테스트 (0) | 2022.10.06 |
토비의 스프링 - 6.1 트랜젝션 코드의 분리 (0) | 2022.10.06 |