6.4.1 ProxyFactoryBean
스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공
- 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공
- ProxyFactoryBean : 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
- 순수하게 프록시 생성을 위한 작업만을 담당하고, 부가 기능은 별도의 빈에 둘 수 있음
MethodInterceptor 인터페이스를 구현하여 만듦
- InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않음
- MethodInterceptor의 invoke() 메소드는 타깃 오브젝트에 대한 정보까지 함께 제공받음
- 타깃 오브젝트에 상관없이 독립적으로 만들 수 있음
- 싱글톤 빈으로 등록 가능
학습 테스트
package com.david.learningtest.jdk.proxy;
public class DynamicProxyTest {
@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.sayHello("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}
static class UppercaseAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 타깃 오브젝트를 전달할 필요 없음. 메소드 정보와 타깃 오브젝트를 이미 알고 있음
String ret = (String)invocation.proceed();
// 부가 기능 적용
return ret.toUpperCase();
}
}
// 타깃과 프록시가 구현할 인터페이스
static interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThankYou(String name);
}
// 타깃 클래스
static class HelloTarget implements Hello {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
@Override
public String sayHi(String name) {
return "Hi " + name;
}
@Override
public String sayThankYou(String name) {
return "Thank You " + name;
}
}
}
어드바이스: 타깃이 필요 없는 순수한 부가기능
MethodInterceptor를 구현한 UppercaseAdvice에는 타깃 오브젝트가 등장하지 않는다.
- 메소드 정보와 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달되기 때문
- 따라서 MethodInterceptor는 부가기능을 제공하는 데 집중 가능
MethodInvocation : 일종의 콜백 오브젝트
- proceed() 메소드 실행 시 타깃 오브젝트의 메소드를 내부적으로 실행
- MethodInvocation 구현 클래스 : 일종의 공유 가능한 템플릿처럼 동작
addAdvice()
- ProxyFactoryBean에는 여러 개의 MethoInterceptor를 추가할 수 있다.
- 아무리 많은 부가기능을 적용하더라도 ProxyFactoryBean 하나로 충분하다.
addMethodInterceptor()가 아닌 addAdvice()
- MethodInterceptor : Advice 인터페이스를 상속하는 서브인터페이스
- 어드바이스(Advice) : 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트
Hello라는 인터페이스를 제공하는 부분이 없음
- setInterfaces()를 통해 직접 지정할 수도 있음
- 자동거물 기능을 사용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아냄
- 알아낸 인터페이스를 모두 구현하는 프록시를 만들어 줌
포인트컷: 부가기능 적용 대상 메소드 선정 방법
메소드의 이름을 가지고 부가기능 적용 대상 메소드를 선정하는 방법
- MethodInterceptor는 여러 프록시가 공유해서 사용
- 타깃 정보를 가지고 있지 않다.
- 싱글톤 빈으로 등록이 가능하다.
- 특정 프록시에만 적용이 되는 메소드 이름 패턴을 넣을 수 없다.
코드 개선 전략
- 프록시가 클라이언트로부터 받는 요청을 일일이 전달받을 필요는 없다.
- MethodInterceptor에 재사용 가능한 순수한 부가기능 제공 코드만 남기면 된다.
- 대신 프록시에 부가기능 적용 메소드를 선택하는 기능을 추가 (분리할 필요 있음)
ProxyFactoryBean 방식
- 부가기능(Advice)과 메소드 선정 알고리즘(Pointcut) 두 가지 확장 기능을 활용하는 유연한 구조를 제공
- 어드바이스(Advice) : 부가기능을 제공하는 오브젝트
- 포인트컷(Pointcut) : 메소드 선정 알고리즘을 담은 오브젝트
프록시의 동작 순서
- 클라이언트로부터 요청
- 포인트컷에 부가기능 부여할 메소드인지 확인 요청
- 부가기능 부여 오브젝트인 경우 MethodInterceptor 타입의 어드바이스 호출
- 직접 타깃을 호출하지는 않음
- MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출
전형적인 전략 패턴 구조
- 프록시로부터 어드바이스와 포인트컷을 독립
- DI를 이용
학습 테스트
- 스프링이 제공하는 NameMatchMethodPointcut을 UppercaseAdvice와 함께 사용하도록 만든 테스트 코드
public class DynamicProxyTest {
@Test
public void pointcutAdvisor() {
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(new HelloTarget());
// 메소드 이름을 비교해서 대상을 선정하는 알고림을 제공하는 포인트컷 생성
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
// 이름 비교조건 설정
// sayH로 시작하는 모든 메소드를 선택하게 한다.
pointcut.setMappedName("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"));
}
}
어드바이스와 포인트컷을 Advisor 타입을 묶어서 addAdvisor() 메소드를 호출해야함
- ProxyFactoryBean에는 여러 개의 어드바이스와 포인트컷이 추가될 수 있기 때문
- 따로 등록하면 어떤 어드바이스(부가기능)에 대해 어떤 포인트컷(메소드 선정)을 적용할지 애매
어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)
6.4.2 ProxyFactoryBean 적용
TxProxyFactoryBean을 ProxyFactoryBean으로 수정
TransactionAdvice
어드바이스 : MethodInterceptor라는 Advice 서브인터페이스를 구현
- TransactionHandler에서 타깃과 메소드 선정부분을 제거
// 스프링의 어드바이스 인터페이스 구현
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;
// JDK 다이내믹 프록시의 Method와 달리 예외가 포장되지 않고 전달된다.
} catch (RuntimeException e) {
this.transactionManager.rollback(status);
throw e;
}
}
}
스프링 XML 설정파일
test-applicationcontext.xml
<?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.xsd">
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="userServiceImpl" />
<!--
어드바이스와 어드바이저를 동시에 설정해줄 수 있는 프로퍼티
리스트에 어브바이스나 어드바이저의 빈 아이디를 넣어줌
기존 ref 애트리뷰트 사용법과 다름에 주의
-->
<property name="interceptorNames">
<list>
<!-- 하나 이상의 value 태그를 넣을 수 있음 -->
<value>transactionAdvisor</value>
</list>
</property>
</bean>
<!-- 어드바이스 -->
<bean id="transactionAdvice" class="com.david.tobysspring.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>
</beans>
테스트
upgradeAllOrNothing()
- ProxyFactoryBean 역시 팩토리 빈이므로 기존 TxProxyFactoryBean과 같은 방식으로 테스트 가능
- 캐스팅 타입만 변경
public class UserServiceTest {
@Test
// 컨텍스트 설정 변경을 위해 여전히 필요
@DirtiesContext
public void upgradeAllOrNothing() throws Exception {
TestUserService testUserService = new TestUserService(users.get(3).getId());
testUserService.setUserDao(userDao);
testUserService.setMailSender(mailSender);
// userService 빈은 이제 스프링의 ProxyFactoryBean
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.upgradeLvls();
fail("TestUserServiceException expected");
} catch (TestUserServiceException e) {
}
checkLvlUpgraded(users.get(1), false);
}
}
어드바이스와 포인트컷의 재사용
ProxyFactoryBean
- 스프링의 DI, 템플릿/콜백 패턴, 서비스 추상화 등 기법이 모두 적용
- 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리
- 포인트컷이 필요하면 이름 패턴만 지정해서 ProxyFactoryBean에 등록
' Spring > 토비의 스프링 3.1' 카테고리의 다른 글
6.5 스프링 AOP (0) | 2019.02.27 |
---|---|
6.3 다이내믹 프록시와 팩토리 빈 (0) | 2019.02.13 |
6.2 고립된 단위 테스트 (0) | 2019.02.12 |
6.1 트랜잭션 코드의 분리 (0) | 2019.02.07 |
6장. AOP (0) | 2019.02.07 |