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이 들어가있는데. 모든 패키지 이름, 클래스이름에 적용한거다?

    • 포인트컷 표현식의 클래스 이름에 적용되는 패턴은 클래스 이름 패턴이 아니라 타입패턴이다
Written on May 27, 2020