2020-5-27 토비의 스프링 6장 AOP (2)
스프링의 프록시 팩토리빈
-
스프링에서는 JDK에서 제공하는 다이나믹 프록시 외에 편리하게 프록시를 쓸 수 있게해준다. 일관된 방법으로 프록시를 만들 수 있게해주고 이를 위해 빈으로 등록해야한다.
-
스프링 ProxyFactoryBean은 팩토리빈이다. 여기서 프록시를 생성하고 부가기능은 MethodInterceptor인터페이스를 구현한 별도의 빈에 둘 수 있다.
-
MethodInterceptor는 InvocationHandler차이는 invoke()는 타깃오브젝트 정보를 제공하지 않는다. 타깃은 InvocationHandler를 직접 알고 있어야된다. 반면 MethodInerceptor는 타깃 오브젝트와 상관없이 독립적으로 만들어질 수 있다.
-
@Test public void proxyFactoryBean() { ProxyFactoryBean pfBean = new ProxyFactoryBean(); pfBean.setTarget(new HelloTarget()); pfBean.addAdvice(new UppercaseAdvice()); Hello proxieHello = (Hello)pfBean.getObject(); assertThat(proxieHello.sayHello("Toby"), is("HELLO TOBY")); } static class UppercaseAdvice implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { String ret = (String)methodInvocation.proceed(); return ret.toUpperCase(); } }
-
MethodInvocation은 타깃 오브젝트의 메서드를 실행할 수 있는 기능이 있다. proceed()를 실행하면 타깃 오브젝트의 메서드를 내부적으로 실행하는 기능이 있다.
-
MethodInvocation의 구현체인 CglibMethodInvocation, ReflectiveMethodInvocation의 필드에는 proxy, target, method, argument, targetClass의 정보들이 포함되어있기때문이다. MethodInvocation은 Interceptor를 상속하고 있다.
-
addAdvice()를 통해 ProxyFactoryBean하나로 여러개의 부가 기능들을 추가할 수 있다. Advice는 인터셉터와 같은거다. addAdvice를 하면 AdvisedSupoorts내부에 있는 LinkedList<Advice> pos의 길이에 Advice가 추가된다.
-
Advice는 인터페이스로 스프링 AOP의 joinpoint에서 수행작업이다. 또한 poincut에서 필터링 되는걸 결정한다. Advice를 통해 Before, after과 같은 advice를 지원할 수 있다.
-
타깃 오브젝트에 적용하는 부가기능을 담고 있는 오브젝트를 어드바이스라 한다.
-
스프링의 ProxyFactoryBean은 어떻게 인터페이스 타입을 제공받지 않고도 Hello 인터페이스 구현체를 만들 수 있을까? 내부에 인터페이스 자동 검출 기능을 사용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아낸다.
- ProxyFactoryBean에는 boolean autodetectInterface라는 필드가 있다. 기본값은 true다.
-
포인트컷
- 이전 TxProxyFactoryBean에서는 pattern이라는 메소드 이름 비교하는 부분을 만들었었다.
- MethodInterceptor오브젝트는 여러 프록시가 공유해서 쓸 수 있기때문에 타깃정보를 가지고 있지 않다.
- 메서드를 선별하는 부분은 프록시로 다시 분리하는게 좋다. 문제는 부가기능을 가진 InvocationHandler가 타깃과 메서드 선정 알고리즘에 의존. 타깃과 메서드선정 방식이 다르면 InvocationHandler가 여러프록시에서 쓰일 수 없다.
- ProxyFactoryBean은 **부가기능확장(Advice)와 메서드선정알고리즘(Pointcut)을 제공한다. ** 위의 2가지가 프록시에 DI로 주입되서 사용된다.
- 프록시는 클라이언트에 요청을 받으면 포인트컷에 부가기능을 부여할 메서드인지 확인한다. 확인되면 MethodInterceptor타입의 어드바이스를 호출한다. MethodInterceptor는 InvocationHandler와 달리 타깃을 호출하지 않는다. 타깃에 의존적이지 않고 템플릿 구조로 설계되어있다.
- 포인트컷은 Pointcut인터페이스를 구현하면 된다.
@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("point"), is("HELLO POINT"));
assertThat(proxiedHello.sayHi("point"), is("HI POINT"));
assertThat(proxiedHello.sayThankYou("point"), is("thank you point"));
}
- NamedMatchMethodPointCut을 정의하고 직접생성했다. 내부적으로는 List<String> mappedNames가 링크드리스트로 구현되어있다.
- 내부에는 matchs(Method method, Class<?> targetClass)와 isMatch(String methodName, String mappedName)(가 구현되어있는데, isMatch의 경우 methodName과 mappedName에 있는 값을 패턴비교를 해서 검증한다. isMatch는 mappedName를 순회하면서 매칭을 비교하는것이다.
- 결국 mappedNames가 중요하다.
- 포인트컷이 필요없으면 addAdvice만 하면 된고 함께 등록할때는 addAdvice안에 Advice타입으로 Pointcut을 ㅎ함께 넣어야한다. 왜??? ProxyBean에는 여러개의 어드바이스와 포인트컷이 추가될 수 있기때문에 등록할때 같이 등록해야한다. 각자놀면 어떤 어드바이스에 어떤 포인트컷을 해야되는지 잘 모른다.
- 어드바이저 = 포인트컷(메서드 선정 알고리즘) + 어드바이스(부가기능)
프록시빈 적용
-
어드바이스는 MethodInterceptor를 구현해서 만든다.
-
public class TransactionAdvice implements MethodInterceptor { PlatformTransactionManager 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; } } public void setTransactionManager(PlatformTransactionManager transactionManager) { this.transactionManager = transactionManager; } } // Set XML <bean id="transactionAdvice" class="toby.aop.TransactionAdvice"> <property name="transactionManager" ref="transactionManager" /> </bean> <bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut"> <property name="mappedName" value="upgrade" /> </bean> <!-- set advisor = advice + pointcut --> <bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="advice" ref="transactionAdvice"/> <property name="pointcut" ref="transactionPointcut"/> </bean> <!-- Add ProxyFactory 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>
-
proceed()를 통해 다음 인터셉터 체인을 진행한다. 이때 타깃 메서드 전후에 부가기능을 넣을 수 있다.
- 어드바이저는 interceptorNames라는 프로퍼티로 넣는다. interceptorNames는 ProxyFactoryBean에 속성값으로 표시되어있다. 반드시 Advice/Advisor빈의 이름들이 포함되어야한다.
-
스프링 AOP
- 이전에 다양한 문제들을 제거했다. 타깃코드는 깔끔하고, 부가기능(어드바이스)는 한번 만들어 모든 타깃과 메서드에 재사용가능하고, 타깃에 적용메서드를 선정하는 방식(포인트컷)도 독립적으로 작성할 수 있다.
- 부가기능이 타깃 오브젝트마다 새로 만들어지는 부분은 스프링의 ProxyBeanFactory의 어드바이스를 통해 해결했다.
- 현재까지의 한계
- 타깃 오브젝트마다 거의 비슷한 내용의 ProxyFacoryBean 설정정보를 추가하는 것. 새로운 타깃이 등장하면 설정부분을 매번 복붙하고, target의 프로퍼티를 수정해야한다. 이때 많은 중복이 발생한다.
- 반복적인 ProxyFacoryBean 설정 문제는 설정 자동등록으로 할 수 없을까?
- 해결
- 비슷한 코드가 반복될때마다 템플릿/콜백, 클라이언트를 나누는 방법과 프록시 클래스 코드를 통해 해결했다.
- 빈 후처리기를 이용한 자동 프록시 생성기를 쓰자.
빈 후처리기를 이용한 자동 프록시 생성기
- 스프링은 OCP(Open Closed Principle)원칙에서 유연한 확장개념을 스프링 컨테이너 자신에게도 적용하고 있다. 변하지 않는 부분외에는 확장 가능하도록 확장 포인트를 제공한다. BeanPostProcessor인터페이스를 구현한 빈후처리기이다.
- 빈후처리기 : 스프링빈을 만들고 난 후 빈 오브젝트를 다시 가공. 자세한 내용은 Vol2.
- 대표적인 구현체 : DefaultAdvisorAutoProxyCreator => 어드바이저를 이용한 프록시생성기
- 사용방법
- 빈후처리기를 빈으로 등록해라. 빈으로 등록하면 빈이 생성될때마다 여기로 보내서 후처리 작업을 한다.
- 빈후처리기는 빈오프젝트의 프로퍼티를 수정할수도 있고, 별도의 초기화, 빈오브젝트를 바꿔치기를 할수도 있다.
- 빈 오브젝트를 프록시로 포장하고, 프록시를 빈으로 등록할 수 있다.
- DefaultAdvisorAutoProxyCreator는 빈으로 등록된 모든 어드바이저 내의 포인트컷을 이용해 전달받은 빈이 프록시 적용 대상인지 확인. 적용대상이면 현재빈에 대한 프록시를 만들고 어드바이저에 연결해준다.
- 어드바이스와 포인트컷이 담긴 어드바이저를 등록하고 빈후처리기에 일임하면 ProxyFacoryBean을 등록하지 않아도 타깃 오브젝트에 자동으로 프록시가 적용된다.
확장된 포인트컷
- 포인트컷의 기능
- PointCut인터페이스를 보면 getClassFilter, getMethodMatcher가 정의되어있다.
- 기존에 사용하던 NamedMatcherMethodPointcut은 메서드 선별 기능만 가진 특별한 포인트컷이다. 클래스필터는 모든 클래스를 다 받아주겠다는 의미. NamedMatcherMethodPointcut은 StaticMethodMatcherPoint를 상속하고 여기서 Pointcut인터페이스를 구현하고 있다. 구현한 내용을 보면 ClassFilter=true로 설정되어있다.
- 포인트컷을 다 사용하면 먼저 적용할 클래스인지 판단하고 나서 어드바이스를 적용할 대상인지 아닌지를 체크한다.
- ProxyFacoryBean에는 클래스레벨 필터가 없었다. DefaultAdvisorAutoProxyCreator는 클래스메소드와 메서드 선정 알고리즘 모두가 필요.
@Test
public void classNamePointCutAdvisor() {
// ready pointcut
NameMatchMethodPointcut classMethodPointcut = new NameMatchMethodPointcut() {
public ClassFilter getClassFilter() {
return new ClassFilter() {
@Override
public boolean matches(Class<?> clazz) {
return clazz.getSimpleName().startsWith("HelloT");
}
};
}
};
classMethodPointcut.setMappedName("sayH*");
//Test
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) {
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"));
}
}
DefaultAdvisorAutoProxyCreator 적용
-
클래스 필터를 적용한 포인트컷 작성
-
NamedMatchMethodPointcut을 상속해서 이름을 비교하는 ClassFilter를 추가
-
public class NameMatchClassMethodPointcut extends NameMatchMethodPointcut { public void setMappedClassName(String mappedClassName) { this.setClassFilter(new SimpleClassFilter(mappedClassName)); } static class SimpleClassFilter implements ClassFilter { private String mappedName; private SimpleClassFilter(String mappedName) { this.mappedName = mappedName; } @Override public boolean matches(Class<?> clazz) { return PatternMatchUtils.simpleMatch(mappedName, clazz.getSimpleName()); } } }
-
모든 클래스를 다 허용하던 클래스필터를 다른 프로퍼티로 변경해서 덮어씌운다.
- PatternMatchUtils.simpleMatch()는 *가 들어간 문자열 유틸리티 메서드.
-
-
어드바이저를 이용하는 자동 프록시 생성기 등록
-
ApplicationContext.xml에 아래코드 빈으로 등록
-
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
-
-
포인트컷 등록
-
<bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut"> <property name="mappedName" value="upgrade" /> <property name="classFilter" value="*ServiceImpl" /> </bean>
- ServiceImpl로 끝나는 클래스와 upgrade로 시작하는 메서드를 선정하는 포인트컷.
-
-
어드바이스와 어드바이저
- transactionAdvice, transactionAdvisor빈은 수정할께 없다. 사용방식이 바꼈다. ProxyFactoryBean처럼 transactionAdvisor를 명시적으로 DI를 하는 빈은 없다. 대신 DefaultAdvisorAutoProxyCreator에 의해 자동생성되는 프록시에 DI가 된다.
-
테스트
-
@Autowire를 통해 UserService를 주입하면 UserServiceImpl이 아니라 Proxy로 감싸진 UserService여야한다.
-
자동프록시생성기를 적용하면 ProxyFacoryBean이 필요없다. 자동프록시생성기가 알아서 프록시를 만들기때문.
-
TestServiceImpl도 빈으로 등록하자. 이전과 달리 타깃을 코드에서 바꿔치기 안해도 되고, 자동프록시생성기의 적용되는지도 확인할 수 있다.
-
TestServiceImpl은 테스트클래스 내부에 정의된 static클래스인데, 빈으로 등록하기 위한 방법이 있다.
-
<bean id="testUserService" class="toby.service.UserServiceTest$TestUserServiceImpl" parent="userService" /> // static클래스를 등록하기 위해서는 $표시를 하고 parent를 표시해야한다.
-
$는 스태틱 멤버 클래스를 지정할 때 사용한다. parent는 다른 빈 설정내용을 상속받을 수 있다.
-
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations= "/test-applicationContext.xml") public class UserServiceTest { @Autowired private ApplicationContext context; @Autowired UserService userService; @Autowired private UserService testUserService; @Test public void upgradeAllOrNothing() throws Exception { userDao.deleteAll(); for(User user : users) userDao.add(user); try { this.testUserService.upgradeLevels(); fail("TestUserServiceException expected"); } catch(TestUserServiceException e) { } } }
-
@Autowired로 UserService타입이 중복되지만 필드이름을 기준으로 주입될 빈이 결정된다.
-
테스트자체는 엄청 깔끔해졌지만, 설정파일의 DI정보까지 참고해야되서 어렵다.
- 프록시를 확인하기 위해 getBean(“userService”)로 가져온 오브젝트가 TestService가 아니라 JDK의 Proxy타입이여야한다.
-
-
이전까지는 포인트컷을 사용할때 메서드 이름과 패턴을 각각 클래스 필터와 매서드 매처 오브젝트로 비교하는 방식이였다. 복잡하고 세밀하게 사용하기 위해 리플랙션API를 쓸 수 있지만 코드를 작성하기가 번거롭다. 포인트컷표현식을 쓰면된다
포인트컷 표현식
-
포인트컷표현식
- AspectJExpressionPointcut클래스를 사용하면 된다.
- Pointcut인터페이스를 구현하는 스프링포인트컷은 클래스필터와 메서드 선정 매처를 구현해야됬다. AspectJExpressionPointcut을 사용하면 클래스, 메서드선정 알고리즘을 포인트컷표현식으로 한번에 지정할 수 있게해준다.
-
문법
- AspectJ의 표현식은 지시자를 이용한다. 대표적으로 execution()이 있다.
-
execution([접근제한자 패턴] 타입패턴 [타입패턴.] 이름패턴] (타입패턴 ”..”, …) [throw 예외패턴]) - exeuction에서 접근제어자, 클래스타입패턴, 예외패턴은 생략가능. ex) execution(int minus(int, int)) : int타입의 리턴값, minus라는 메서드이름. 두개의 int파라미터를 가진 모든 메서드를 선정해라.
- execution(* minus(int int)) *를 쓰면 리턴타입이 어떤값이든 상관없다. *
- 파라미터 갯수를 무시할거면 .. 을 써라. execution( minus(..))
- 모든 메서드에 적용할거면 execution(* *(..)) 을 쓰면된다.
-
적용
-
메서드 시그니처 비교할때 exeuction()말고 빈의 이름으로 비교하는 bean()도 있고 어노테이션 기반으로도 할 수 있다.
-
<bean id="transactionPointcut" class="org.springframework.aop.aspectj.AspectJExpressionPointcut"> <property name="expression" value="execution(* *..*ServiceImpl.upgrade*(..))" /> </bean>
-
이전에 사용하던 transactionPointcut빈을 AspectJExpressionPointcut으로 바꿔라 value에 execution이 들어가있는데. 모든 패키지 이름, 클래스이름에 적용한거다?
- 포인트컷 표현식의 클래스 이름에 적용되는 패턴은 클래스 이름 패턴이 아니라 타입패턴이다
-