ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 6장) 6.4 스프링의 프록시 팩토리 빈
    Java & Spring/토비의 스프링 3.1 2021. 7. 16. 18:22
    반응형

    6장 AOP

    6.4 스프링의 프록시 팩토리 빈

    • ProxyFactoryBean
      • 자바 JDK에서 제공하는 다이나믹 프록시 외에도 프록시를 만들도록 지원하는 다양한 기술 존재
        • 스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어 제공
          • 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈 제공
      • 프록시를 생성하는 작업만 담당
        • 부가기능은 MethodInterceptor 인터페이스를 구현해서 사용
          • InvocationHandler의 invoke() 는 타깃 오브젝트 정보를 제공하지 않아서, 자체적으로 타깃을 알고 있어야함
          • MethodInterceptor의 invoke()는 ProxyFactoryBean으로 부터 타깃 오브젝트 정보를 제공받음
            • 타깃 오브젝트에 상관없이 독립적으로 만들어 질 수 있고, 다른 여러 프록시에서 함께 사용 가능하며, 싱글톤 빈으로 등록 가능
        • public class DynamicProxyTest {
              @Test
              public void SimpleProxy() {
                  Hello proxiedHello = (Hello)Proxy.newProxyInstance(
                      getClass().getClassLoader(),
                      new Class[] { Hello.class },
                      new UppercaseHandler(new HelloTarget())
                  );
                  ...
              }
          
              @Test
              public void proxyFactoryBean() {
                  ProxyFactoryBean pfBean = new ProxyFactoryBean();
                  pfBean.setTarget(new HelloTarget());
                  pfBean.addAdvice(new UppercaseAdvice());
          
                  Hello proxiedHello = (Hello) pfBean.getObject();
                  assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
                  assertThat(proxiedHello.sayHi("Toby"), is("Hi TOBY"));
              }
          
              static class UppercaseAdvice implements MethodInterceptor {
                  public Object invoke(MethodInterceptor invocation) throws Throwable {
                      String ret = (String)invocation.proceed();
                      return ret.toUpperCase();
                  }
              }
          
              static interface Hello {
                  String sayHello(String name);
                  String sayHi(String name);
              }
          
              static class HelloTarget implements Hello {
                  public String sayHello(String name) {
                      return "Hello " + name;
                  }
          
                  public String sayHi(String name) {
                      return "Hi " + name;
                  }
              }
          }
      • 어드바이스: 타깃이 필요 없는 순수한 부가기능
        • MethodInvocation은 일종의 콜백 오브젝트로, proceed() 메소드를 실행해서 타깃 오브젝트 메소드를 내부적으로 실행
          • 프록시 추상화 기능인 ProxyFactoryBean을 사용하는 코드의 가장 큰 차이점이자 장점
          • ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용
            • 템플릿 역할인 MethodInvocation을 싱글톤으로 두고 공유 가능
        • MethodInterceptor는 Advice 인터페이스를 상속하고있는 서브 인터페이스
          • 따라서, ProxyFactoryBean의 addAdvice()로 여러 MethodInterceptor 추가 가능
            • ProxyFactoryBean 하나만으로 여러 부가 기능을 제공하는 프록시를 만들 수 있음
          • 부가기능을 담은 오브젝트를 스프링에서는 Advice라고 부름
        • ProxyFactoryBean은 인터페이스 자동검출 기능을 사용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아내서, 인터페이스를 모두 구현하는 프록시를 만들어 반환
          • 인터페이스 중 일부만 프록시에 적용하기 원한다면, setInterfaces()를 통해 구현할 인터페이스 정보를 직접 제공
      • 포인트컷: 부가기능 적용 대상 메소드 선정 방법
        • ProxyFactoryBean과 MethodInterceptor를 사용하는 방식에 메소드 선정 기능을 추가할 수 있을까???
          • 트랜잭션 적용 메소드 패턴은 프록시마다 다를 수 있기 때문에, 여러 프록시가 공유하는 MethodInterceptor에 특정 프록시에만 적용되는 패턴을 넣으면 문제 발생
          • 프록시에 부가기능 적용 메소드를 선택하는 기능을 넣어 해결 가능
            • 프록시의 핵심 가치는 타깃을 대신해서 클라이언트 요청을 받아 처리하는 오브젝트 존재 자체로, 메소드 선별 기능은 프록시로부터 다시 분리
            • 전략 패턴 적용
          • 부가기능(Advice)와 메소드 선정 알고리즘(Pointcut)을 활용한 유연한 구조
            • 포인트컷 : 메소드 선정 알고리즘 오브젝트
              어드바이스 : 부가기능 제공 오브젝트
            • 타깃 메소드를 직접 호출하는 것은 Invocation 콜백의 역할로, 전형적인 템플릿/콜백 구조
            1. 클라이언트 요청 ~> 포인트컷에게 부가기능 부여할 메소드인지 확인 요청
            2. 포인트컷 응답
              1. 부가기능 부여할 메소드인 경우
                1. MethodInterceptor 어드바이스 호출
                2. 기능 추가
            3. invocation 콜백을 통해 타깃 오브젝트 메소드 실행
            • 전략 패턴 구조
              • 프록시로부터 어드바이스와 포인트 컷을 독립시키고 DI를 사용
              • 여러 프록시가 공유, 부가기능 방식이나 메소드 선정 알고리즘이 바뀌면 구현 클래스만 바꿔 설정 ~> 자유로운 확장
        • 학습 테스트
          • @Test
            public void pointAdvisor() {
                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 ..
            }
          • ProxyFactoryBean에는 여러 어드바이스와 포인트 컷이 추가될 수 있기 때문에, 어떤 어드바이스에 어떤 포인트컷을 적용할지 조합을 만들어서 등록
          • 어드바이저 = 포인트컷 + 어드바이스
    • ProxyFactoryBean 적용
      • 다이나믹 프록시를 이용해 만든 TxProxyFactoryBean을 스프링의 ProxyFactoryBean을 이용하도록 수정
      • TransactionAdvice
        • 부가기능을 담당하는 어드바이스를 MethodInterceptor라는 Advice 서브인터페이스를 구현해서 작성
        • JDK 다이나믹 프록시 방식으로 만든 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와는 달리 
                       * 스프링의 MethodInvation을 통한 타깃 호출은 예외가 포장되지 않고 
                       * 타깃에서 보낸 그대로 전달된다.
                       */
                  } catch (RuntimeException e) {
                      this.transactionManager.rollback(status);
                      throw e;
                  }
              }
          }
      • 스프링의 XML 설정 파일
        • 트랜잭션 어드바이스 및 포인트컷을 설정해주고, 포인트컷과 어드바이스를 담는 어드바이저 빈 설정을 해준다.
        • 어드바이저 빈 설정을 마치면 ProxyFactoryBean 설정에 어드바이저 빈을 지정
          • interceptorNames라는 propery에 등록
          • advisor가 아닌 이유는 어드바이스와 어드바이즈러를 혼합해서 설정할 수 있도록 하기 위함
      • 테스트
        • @Test
          @DirtiesContext
          public void upgradeAllOrNothing() {
              TestUserService testUserService = new TestUserService(users.get(3).getId());
              testUserService.setUserDao(userDao);
              testUserService.setMailSender(mailSender);
          
              ProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", ProxyFactoryBean.class);
              txProxyFactoryBean.setTarget(testUserService);
              UserService txUserService = (UserService)txProxyFactoryBean.getObject();
          }
      • 어드바이스와 포인트컷의 재사용
        • ProxyFactoryBean은 스프링의 DI, 템플릿/콜백 패턴, 서비스 추상화 등 기법이 모두 적용됨
          • 독립적이며 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리 가능
          • 메소드 선정을 위한 포인트컷이 필요하면 이름 패턴만 지정해서 ProxyFactoryBean에 등록
    반응형

    댓글

Designed by Tistory.