✅ 자동 프록시 생성
부가 기능이 타겟 오브젝트마다 새로 생성되는, 즉 InvocationHandler를 구현한 구체 클래스가 타겟 오브젝트에 종속되어 매번 새롭게 생성되는 것은 Advice를 통해서 해결했습니다. 이제 남은 것은 타겟 오브젝트를 변경할 때마다 거의 비슷한 내용의 ProxyFactoryBean의 빈 설정 정보를 매번 복사해서 등록해야 하는 문제입니다. target을 제외하면 Advisor 정보가 동일하기 때문에 중복을 제거할 수 있을 것입니다.
🔹빈 후처리기 -DefaultAdvisorAutoProxyCreator
BeanPostProcessor을 구현해서 만드는 빈 후처리기는 스프링 빈 오브젝트로 만들어지고 난 후에, 빈 오브젝트를 다시 가공할 수 있게 해줍니다. 여기서는 DefaultAdvisorAutoProxyCreator을 통해서 살펴보겠습니다.
DefaultAdvisorAutoProxyCreator 빈 후처리기가 Spring IoC Container에 등록되어 있으면 빈 오브젝트를 만들 때마다 후처리기에게 빈을 보냅니다. 빈 후처리기는 Advisor내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지를 확인합니다. 프록시 적용 대상이면 그때는 내장된 프록시 생성기에게 현재 빈에 대한 프록시를 만들게 하고, 만들어진 프록시에게 Advisor을 연결해 줍니다.
🔹포인트컷 확장과 학습테스트
기존의 포인트컷이란 타겟 오브젝트의 메소드 중에서 어떤 메소드에 부가기능을 적용할지를 선정해주는 역할을 한다고 했습니다. 그런데 빈 후처리기에서는 빈 오브젝 자체를 선택하는 기능도 보입니다. Pointcut Interface를 살펴보겠습니다.
- Pointcut
public interface Pointcut { ClassFilter getClassFilter(); //프록시를 적용할 클래스인지 확인 MethodMatcher getMethodMatcher(); //어드바이스를 적용할 메소드인지 확인 }
➡️ pintcut은 아래 두 가지 기능을 모두 가지고 있습니다.
- 프록시를 적용할 클래스인지 확인하는 기능
- 어드바이스를 적용할 메소드인지 확인하는 기능
DefaultAdvisorAutoProxyCreator에는 클래스와 메소드를 선정하는 알고리즘을 모두 갖고 있는 pointcut과 advice가 결합된 advisor이 등록되어야만 합니다.
- DynamicProxyTest
@Test public void classNamePointcutAdvisor() { //클래스를 선정하는 알고리즘 NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() { public ClassFilter getClassFilter() { return new ClassFilter() { public boolean matches(Class<?> clazz) { return clazz.getSimpleName().startsWith("HelloT"); } }; } }; NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut() { public ClassFilter getClassFilter() { return ((clazz) -> clazz.getSimpleName().startsWith("HelloT")); } }; //메소드를 선정하는 알고리즘 classMethodPointcut.setMappedName("sayH*"); //advisor 적용 checkAdviced(new HelloTarget(), classMethodPointcut, true); //advisor 적용 x class HelloWorld extends HelloTarget {}; checkAdviced(new HelloWorld(), classMethodPointcut, false); //advisor 적용 class HelloToby extends HelloTarget {}; checkAdviced(new HelloToby(), classMethodPointcut, true); } private void checkAdviced(Object target, Pointcut pointcut, boolean adviced) { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(target); pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice())); //Proxy 객체 준비 Hello proxiedHello = (Hello) pfBean.getObject(); // advisor 적용 대상 if (adviced) { assertEquals(proxiedHello.sayHello("Toby"), "HELLO TOBY"); assertEquals(proxiedHello.sayHi("Toby"), "HI TOBY"); assertEquals(proxiedHello.sayThankYou("Toby"), "Thank You Toby"); } else { assertEquals(proxiedHello.sayHello("Toby"), "Hello Toby"); assertEquals(proxiedHello.sayHi("Toby"), "Hi Toby"); assertEquals(proxiedHello.sayThankYou("Toby"), "Thank You Toby"); } }
NameMatchMethodPointcut을 내부 익명 클래스 방식으로 확장해서 만들어 클래스 필터를 적용했습니다. 이로써 포인트컷을 통해 클래스의 이름으로 어드바이스 적용 여부를 판단하고, 메소드의 이름으로 어드바이스 적용 여부를 판단하는 포인트컷이 완성되었습니다.
✅ DefaultAdvisorAutoProxyCreator
위 방식을 ProxyFactoryBean에 적용하여 pointcut을 사용하여 프록시 객체를 자동으로 생성하도록 만들어 보겠습니다. 먼저 포인트컷을 확장한 클래스는 다음과 같습니다.
- NameMatchClassMethodPointcut
package com.jhcode.spring.ch6.user.service; import org.springframework.aop.ClassFilter; import org.springframework.aop.support.NameMatchMethodPointcut; import org.springframework.util.PatternMatchUtils; 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) { //와일드카드(*)가 들어간 문자열 비교를 지원하는 스프링의 유틸리티 메소드 //*name, name*, *name* 3가지 방식을 지원함 return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName()); } } }
- DefaultAdvisorAutoProxyCreator
@Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { return new DefaultAdvisorAutoProxyCreator(); }
DefaultAdvisorAutoProxyCreator은 등록된 빈 중에서 Advisor 인터페이스를 구현한 것을 모두 찾습니다. 그리고 생성되는 모든 빈에 대해 어드바이저 포인트컷을 적용해보면서 프록시 적용 대상을 선정한 후 프록시를 생성하여 원래 빈 오브젝트와 바꿔치기 합니다.
- TestServiceFactory
@Bean public TransactionAdvice transactionAdvice() { TransactionAdvice advice = new TransactionAdvice(); advice.setTransactionManager(transactionManager()); return advice; } @Bean public NameMatchMethodPointcut transactionPointcut() { // NameMatchClassMethodPointcut pointcut = new NameMatchClassMethodPointcut(); // //pointcut.setMappedClassName("*Service"); // pointcut.setMappedName("upgrade*"); NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); pointcut.setMappedName("upgrade*"); return pointcut; } @Bean public NameMatchClassMethodPointcut transactionClassPointcut() { NameMatchClassMethodPointcut pointcut = new NameMatchClassMethodPointcut(); pointcut.setMappedClassName("*ServiceImpl"); pointcut.setMappedName("upgrade*"); return pointcut; } @Bean public DefaultPointcutAdvisor transactionAdvisor() { DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(); advisor.setAdvice(transactionAdvice()); advisor.setPointcut(transactionClassPointcut()); return advisor; } @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { return new DefaultAdvisorAutoProxyCreator(); } @Bean public UserService userService() { UserServiceImpl userServiceImpl = new UserServiceImpl(); userServiceImpl.setUserDao(userDao()); userServiceImpl.setMailSender(mailSender()); return userServiceImpl; } @Bean public UserService userServiceImpl() { UserServiceImpl userServiceImpl = new UserServiceImpl(); userServiceImpl.setUserDao(userDao()); userServiceImpl.setMailSender(mailSender()); return userServiceImpl; } @Bean //@Qualifier("testUserService") public UserService testUserService() { TestUserServiceImpl testuserServiceImpl = new TestUserServiceImpl(); testuserServiceImpl.setUserDao(userDao()); testuserServiceImpl.setMailSender(mailSender()); return testuserServiceImpl; } //== UserServiceTest 클래스 내의 static 클래스로 정의되었다면 아래와 같이 정의해야 한다 ==// // @Bean // @Qualifier("testUserService") // public UserServiceImpl testUserService() { // UserServiceImpl testUserServiceImpl = new UserServiceTest.TestUserServiceImpl(); // testUserServiceImpl.setUserDao(userDao()); // testUserServiceImpl.setMailSender(mailSender()); // return testUserServiceImpl; // }
기존에 upgradeAllorNoting() 메소드 내에서 testUserService 객체를 생성하고 ProxyFactoryBean에 직접 target으로 설정하여 Proxy 객체를 만드는 것이 아니라, pointcut을 통해서 자동으로 Proxy 객체를 생성하게 하기 위해 Bean으로 등록했습니다.
현재 pointcut은 비교를 위해 2가지 빈이 등록되어있습니다.
- NameMatchMethodPointcut : 메소드 선별 알고리즘
- NameMatchClassMethodPointcut : 클래스 & 메소드 선별 알고리즘
이제는 pointcut의 클래스 선별 알고리즘을 통해 해당 이름을 가진 클래스가 Proxy 객체를 생성하고 Bean으로 등록되어 호출하는 Client에게 DI 하게 됩니다. Clinet는 target의 메소드를 호출하지만 실질적으로는 proxy 객체를 통해서 target 메소드를 호출하게 되는 것입니다. 이는 target과 proxy가 동일한 인터페이스를 구현함으로써 가능합니다.
DefaultAdvisorAutoProxyCreator은 BeanPostProcessor을 구현해서 만든 빈 후처리기입니다. pointcut에 등록된 “*ServiceImpl”에 해당하는 클래스 이름으로 생성되는 객체를 찾아서 Proxy를 생성하고 기존의 Bean으로 등록되는 객체를 대신하여 Client에게 주입됩니다.
🔹 Test 수정
DefaultAdvisorAutoProxyCreator을 통해서 자동으로 프록시 객체가 생성되도록 하였습니다. 기존의 테스트 클래스를 수정하여 제대로 동작하는지 테스트해보겠습니다.
- TestUserService → TestUserServiceImpl
package com.jhcode.spring.ch6.user.service; import com.jhcode.spring.ch6.user.domain.User; public class TestUserServiceImpl extends UserServiceImpl { private String id = "user4"; //예외를 내부 클래스로 선언하여 사용한다 static class TestUserServiceException extends RuntimeException{}; @Override protected void upgradeLevel(User user) { if (user.getId().equals(this.id)) throw new TestUserServiceException(); super.upgradeLevel(user); } }
클래스의 이름을 testUserServiceImpl로 변경함으로써 pointcut에 등록된 클래스 선별 알고리즘에 적용되어 Advisor이 적용될 수 있도록 변경합니다. 또한 기존의 생성자를 통해서 id 값을 지정하는 것을 기본적으로 초기화할 수 있도록 수정했습니다. 이는 Test 클래스에서 TestUserServiceImpl 객체를 new 연산자를 통해서 생성하는 것이 아니라 Bean 객체로 등록하여 사용하기 위함입니다.
- UserSerivceTest
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {TestServiceFactory.class}) public class UserServiceTest { @Autowired private UserDao userDao; @Autowired private UserService userService; @Autowired @Qualifier("testUserService") private UserService testUserService; @Autowired private PlatformTransactionManager transactionManager; @Autowired private MailSender mailSender; //... 생략 //예외 발생 시 작업 취소 여부 테스트 @Test @DirtiesContext public void upgradeAllorNothing() throws Exception { userDao.deleteAll(); for (User user : users) { this.testUserService.add(user); } try { this.testUserService.upgradeLevels(); //테스트가 제대로 동작하게 하기 위한 안전장치, 로직을 잘못짜서 upgradeLevels() 메소드가 통과되도 무조건 실패함. //fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { System.out.println("TestUserServiceException 예외 발생함"); } finally { checkLevel(users.get(1), false); } } @Test public void advisorAutoProxyCreator() { //주입받는 객체가 proxy 객체인지 검증 assertTrue(userService instanceof java.lang.reflect.Proxy); assertTrue(testUserService instanceof java.lang.reflect.Proxy); }
testUserServiceImpl을 Bean으로 등록할 때 @Qualifier("testUserService") 어노테이션을 사용하여서 명시적으로 testUseServiceImpl 객체를 Bean으로 등록하였고, @Autowired로 주입 받을 때도 명시적으로 @Qualifier("testUserService") 동일한 빈을 주입 받을 수 있도록 하였습니다.
이로써 upgradeAllOrNothing() 테스트 메소드 내에서도 주입 받은 Bean 객체를 사용하도록 합니다. 하지만 이때 주입 받은 Bean은 TestUserSerivceImpl 객체가 아니라 빈 후처리기를 통해서 생성된 Proxy 객체입니다. 이를 통해 Advice의 정의한 순수한 부가 기능인 트랜잭션 기능을 pointcut에 등록한 “upgrade*”로 시작하는 모든 메소드들에게 부가합니다.
👉 따라서 위 테스트 클래스는 정상적으로 동작합니다.
advisorAutoProxyCreator() 테스트 메소드는 주입 받은 객체가 Proxy 객체인지 확인하는 테스트 입니다.
📖 토비 스프링 3.1 -p475~488
🚩jhcode33의 toby-spring-study.git으로 이동하기
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T