스프링은 컨테이너로서 제공하는 기능 중에서 변하지 않는 핵심적인 부분 외에는 대부분 확장할 수 있도록 확장 포인트
를 제공해줌
빈 후처리기
: BeanPostProcessor
인터페이스를 구현해서 만드는 확장 포인트
. 이름 그대로 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해줌.
DefaultAdvisorAutoProxyCreator
: 빈 후처리기 중 하나. 어드바이저를 이용한 자동 프록시 생성기. 이를 잘 이용하면 스프링이 생성하는 빈 오브젝트의 일부를 프록시로 포장하고, 프록시를 빈으로 대신 등록할 수도 있음.
빈 후처리기를이용한 자동 프록시 생성 방법 : DefaultAdvisorAutoProxyCreator
빈 후처리기가 등록되어 있으면 스프링은 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보냄. DefaultAdvisorAutoProxyCreator
는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용대상인지 확인. 프록시 적용 대상이면 프록시를 만들게 하고 어드바이저 연결해줌. 빈 후처리기는 프록시가 생성되면 빈 오브젝트 대신 프록시 오브젝트를 컨테이너에 돌려줌. 최종적으로 빈 후처리기가 돌려준 오브젝트를 빈으로 등록하고 사용.
public interface Pointcut {
ClassFilter getClassFilter();//프록시를 적용할 클래스인지 확인해줌
MethodMatcher getMethodMatcher();//어드바이스를 적용할 메소드인지 확인해줌
}
@Test public void classNamePointcutAdvisor() {
// 포인트컷 준비
NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
public ClassFilter getClassFilter() {
return new ClassFilter() {
public boolean matches(Class<?> clazz) {
return clazz.getSimpleName().startsWith("HelloT");
}
};
}
};
classMethodPointcut.setMappedName("sayH*");
// 테스트
checkAdviced(new HelloTarget(), classMethodPointcut, true);//적용 클래스다.
class HelloWorld extends HelloTarget {};
checkAdviced(new HelloWorld(), classMethodPointcut, false);//적용 클래스가 아니다!
class HelloToby extends HelloTarget {};
checkAdviced(new HelloToby(), classMethodPointcut, true);//적용 클래스다.
}
//적용 대상인가?
private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) { [3]
ProxyFactoryBean pfBean = new ProxyFactoryBean();
pfBean.setTarget(target);
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
Hello proxiedHello = (Hello) pfBean.getObject();
if (adviced) {
assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
} else {
assertThat(proxiedHello.sayHello("Toby"), is("Hello Toby"));
assertThat(proxiedHello.sayHi("Toby"), is("Hi Toby"));
assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
}
}
//클래스 필터가 포함된 포인트컷
package springbook.learningtest.jdk.proxy;
...
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut {
public void setMappedClassName(String mappedClassName) {
this.setClassFilter(new SimpleClassFilter(mappedClassName));
}
static class SimpleClassFilter implements ClassFilter {
String mappedName;
private SimpleClassFilter(String mappedName) {
this.mappedName = mappedName;
}
public boolean matches(Class<?> clazz) {
return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName());
}
}
}
<bean class =
"org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
//포인트컷 빈
<bean id="transactionPointcut"
class="springbook.service.NameMatchClassMethodPointcut">
<property name="mappedClassName" value="*ServiceImpl" />
<property name="mappedName" value="upgrade*" />
</bean>
//프록시 팩토리 빈을 제거한 후의 빈 설정
<bean id="userService" class="springbook.service.UserServiceImpl">
<property name="userDao" ref="userDao" />
<property name="mailSender" ref="mailSender" />
</bean>
//수정한 테스트용 UserService 구현 클래스
static class TestUserServiceImpl extends UserServiceImpl {
private String id = "madnite1";//테스트 픽스처의 users(3)의 id 값을 고정시켜버렸다.
protected void upgradeLevel(User user) {
if (user.getId().equals(this.id)) throw new TestUserServiceException();
super.upgradeLevel(user);
}
}
//테스트용 UserService의 등록
<bean id="testUserService"
class="springbook.user.service.UserServiceTest$TestUserServiceImpl"
parent="userService" />//프로퍼티 정의를 포함해서 userService 빈의 설정을 상속받는다.
//testUserService 빈을 사용하도록 수정된 테스트
public class UserServiceTest {
@Autowired UserService userService;
@Autowired UserService testUserService;
//같은 타입의 빈이 두 개 존재하기 때문에 필드 이름을 기준 으로 주입될 빈이 결정된다.
//자동 프록시 생성기에 의해 트랜잭션 부가기능이 testUserService 빈에 적용됐는지를 확인하는 것이 목적이다.
...
@Test
public void upgradeAllOrNothing() {
//스프링 컨텍스트의 빈 설정을 변경하지 않으므로 @ DirtiesContext 애노테이션은 제거됐다.
//모든 테스트를 위한 DI 작업은 설정파일을 통해 서버에서 진행되 므로 테스트 코드 자체는 단순해진다.
userDao.deleteAll();
for(User user : users) userDao.add(user);
try {
this.testUserService.upgradeLevels();
fail("TestUserServiceException expected");
}
catch(TestUserServiceException e) {
}
checkLevelUpgraded(users.get(1), false);
}
}
//클래스 필터용 이름을 변경한 포인트컷 설정
<bean id="transactionPointcut"
class="springbook.user.service.NameMatchClassMethodPointcut">
<property name="mappedClassName" value="*NotServiceImpl" />
<property name="mappedName" value="upgrade*" />
</bean>
//자동생성된 프록시 확인
@Test public void advisorAutoProxyCreator() {
assertThat(testUserService, is(java.lang.reflect.Proxy.class));
}
//프록시로 변경된 오브젝트인지 확인한다.