본문 바로가기

토비의 스프링 정리

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

6.4.1 ProxyFactoryBean

  • 스프링에서 제공하는 깔끔한 부가기능 추가를 위한 클래스
  • JDK에서 제공하는 다이내믹 프록시 단점 해결
  • 서비스 추상화와 동일하게 스프링은 일관된 방법으로 프록시를 만들 수 있는 추상 레이어
  • 스프링의 ProxyFactoryBean는 프록시를 생성해서 빈 오브젝트로 등록하게 하는 팩토리 빈
  • ProxyFactoryBean는 순수하게 프록시 생성 작업만 담당하기 때문에, 부가기능은 어드바이스라 불리면서 MethodInterceptor를 구현한 별도의 빈에 둘 수있음
  • 즉, ProxyFactoryBean가 생성하는 프록시부가기능은 MethodInterceptor를 구현해서 만듦
  • MethodInterceptor는 InvocationHandler와 비슷하지만 두 가지 다른 점이 있음
    1. 타깃 오브젝트 전달 유무
      • InvocationHandler의 invoke()는 타깃 오브젝트가 필요해 전달받음
      • Object ret = method.invoke(target, args);
      • MethodInterceptor의 invoke()는 타깃 오브젝트가 필요하지 않아 전달받지 않음
      • String ret = (String) invocation.proceed();
    2. 프록시가 구현할 인터페이스 지정 유무
      • InvocationHandler는 프록시가 구현할 인터페이스를 지정해야 함
      • new Class[] {Hello.class}
      • MethodInterceptor는 프록시가 구현할 인터페이스를 지정하지 않아도 됨
      • 💡 ProxyFactoryBean에 인터페이스 자동검출 기능을 이용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아내기 때문임. 만약 구현 인터페이스 중 일부 프록시만 지정해야 한다면 setInterface()로 지정할 수 있음
  • 때문에, 타깃 오브젝트에 독립적으로 만들어 질 수 있어 싱글톤 빈으로 등록할 수 있으며 여러 프록시에서 함께 사용할 수 있음
public class DynamicProxyTest {
    @Test
    public void simpleProxy(){
        //JDK에서 제공하는 다이내믹 프록시 만드는 방법
        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()); //부가기능. 여러개 가능

        //FactoryBean이므로 getObject()로 생성된 프록시를 가져옴
        Hello proxiedHello = (Hello) pfBean.getObject();

        assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
        assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
        assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
    }

    private static class UppercaseAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            //InvocationHandler와 달리 target이 필요 없음
            String ret = (String)invocation.proceed();
            return ret.toUpperCase();
        }
    }
}

6.4.2 어드바이스 : 타깃이 필요 없는 순수한 부가기능

  • MethodInterceptor를 구현한 UppercaseAdvice는 타깃 오브젝트가 등장하지 않음
  • MethodInterceptor의 파라미터인 MethodInvocation오브젝트로 메소드 정보타깃 오브젝트가 전달됨
  • 즉, 파라미터로 전달되기 때문에 별도로 전달하지 않아도 됨
  • 또한, MethodInvocation는 proceed()로 타깃 오브젝트의 메소드를 실행할 수 있기 때문에 MethodInterceptor는 부가기능을 제공하는 데만 집중할 수 있음
  • MethodInvocation는 일종의 템플릿 역할을 하는 콜백 오브젝트임
    • 템플릿 : 재사용을 하기 위한 것(ex.JdbcTemplate)
    • 콜백 : 특정 메소드를 호출해서 알림
    💡 3.5의 템플릿/콜백 참고
  • MethodInvocation는 콜백으로 특정(실행할) 메소드를 호출해서 알려주면 proceed()가 해당 특정 메소드를 내부적으로 실행시킴
  • 이 특징이 ProxyFactoryBean의 장점이자 JDK에서 제공하는 다이내믹 프록시 방법과 가장 큰 차이점임
  • ProxyFactoryBean는 작은 단위의 템플릿/콜백 구조를 응용해서 적용했고, 템플릿 역할을 하는 MethodInvocation을 싱글톤으로 공유하여 재사용할 수 있는것임
  • ProxyFactoryBean는 위 예제 코드 주석에 적힌것과 같이 addAdvice()를 이용하여 부가기능을 위한 MethodInterceptor를 구현한 클래스를 여러개 추가할 수 있음
  • 즉, ProxyFactoryBean하나만으로 여러 개의 부가기능을 제공해 주는 프록시를 만들 수 있어 JDK에서 제공하는 방법의 문제를 해결함
  • 그런데 부가기능을 추가하는 메소드의 이름이 addMethodInterceptor가 아니라 addAdvice()임
  • 이유는, MethodInterceptor Advice 인터페이스 상속하기 있는 서브인터페이스이기 때문임
  • 또한, 이름에서 알 수 있듯이 MethodInterceptor처럼 타깃 오브젝트에 부가기능추가하는 오브젝트를 어드바이스(Advice)라 함

6.4.3 포인트컷 : 부가기능 적용 대상 메소드 선정 방법

  • InvocationHandler를 직접 구현할 때 메소드 이름을 가지고 부가기능 적용 대상 메소드를 선정 했음
  • TxProxyFactoryBean은 pattern 필드를 통해 메소드 이름 비교용 스트링 값을 DI 받아 부가기능인 트랜잭션 적용 메소드를 선정했음
  • ProxyFactoryBean의 MethodInterceptor는 여러 프록시가 공유하고 있음
  • 프록시마다 부가기능 적용 대상 선정하는 방법이 다르기 때문에 MethodInterceptor내부에 부가기능 판별 코드를 추가하면 안됨
  • MethodInterceptor는 InvocationHandler와 달리 프록시가 클라이언트로부터 받은 요청을 일일이 전달받을 필요가 없음
  • 때문에 프록시에서 부가기능 적용 대상 선정을 한 뒤에, 순수한 부가기능만 있는 MethodInterceptor를 구현한 어드바이스호출하면 됨
  • 그림에서 알 수 있듯이 메소드 선정 알고리즘을 포인트컷(Point Cut)이라 함
  • 포인트컷과 어드바이스는 모두 프록시에 DI로 주입되서 사용됨
  • 포인트컷, 어드바이스 모두 프록시에 공유할 수 있도록 만들어지기 때문에 스프링의 싱글톤 빈으로 등록할 수 있음
  • 전략에 맞춰 포인트컷, 어드바이스를 바꿀수 있기 때문에 전략패턴
  • 포인트컷으로부터 부가기능 적용할 대상 메소드인지 확인을 한 뒤에 MethodInterceptor를 구현한 어드바이스를 호출함
  • Invocation 콜백은 실제 위임 대상인 타깃 오브젝트의 레퍼런스를 갖고 있고, 타깃 메소드를 직접 호출하는 역할임
  • 스프링에서 포인트컷을 위한 클래스 제공하고 있음
  • 앞선 예제와 같이, 이름을 통한 포인트컷은 NameMatchMethodPointCut 클래스임
//포인트컷을 위한 학습 테스트
public class DynamicProxyTest {
    ...
    @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("Toby"), is("HELLO TOBY"));
        assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
        //setMappedName가 'sayH'이기 때문에 sayThankYou는 포함 안됨
        assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
    }
}

6.4.4 어드바이서

  • 어드바이서(Advisor) = 포인트컷 + 어드바이스
  • 어드바이서 등록은 포인트컷과 어드바이스를 묶어서 등록하는 것임
//pointcut : 포인트컷(메소드 선정 알고리즘)
//new UppercaseAdvice() : 어드바이스(부가기능)
pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));
  • 여러개의 어브다이스가 등록 되더라도, 각각 다른 포인트컷과 조합될 수 있기 때문에 하나로 묶어서 등록해야 함
  • 이를 어드바이서라 함

6.4.5 TransactionAdvice

  • 이전에 만들었던 TransactionHandler와 유사함
  • TransactionHandler에서 메소드 선정 알고리즘을 제거함. 포인트 컷에 의해 선정될 것이기 때문임
  • MethodInvocation을 통해 콜백
  • MethodInterceptor는 InvocationHandler와 달리 예외 포장 하지 않음
public class TransactionAdvice implements MethodInterceptor {
    private 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;
        }catch (RuntimeException e){
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}

6.4.6 스프링 XML 설정 파일

  • 트랜잭션 어드바이스, 포인트컷, 어드바이저, ProxtFactoryBean 설정
  • 어드바이저는 interceptorNames라는 프로퍼티를 통해 넣음
  • 이유는 어드바이스, 어드바이저를 혼합해서 설정할 수 있게 하기 위함임
  • 또한, 여러개 할당할 수 있기 때문에 <list>에 추가
<beans 
    ...>
    <bean id="transactionAdvice" class="com.ksb.spring.TransactionAdvice">
        <property name="transactionManager" ref="transactionManager"/>
    </bean>
    
    <bean id="transactionPointcut"
          class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="upgrade*"/>
    </bean>
    
    <bean id="transactionAdvisor"
          class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="advice" ref="transactionAdvice"/>
        <property name="pointcut" ref="transactionPointcut"/>
    </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>
</beans>

6.4.7 테스트 코드

  • upgradeAllOrNothing()는 트랜잭션 적용이 되었는지 확인하는 코드임
  • TxProxyFactoryBean를 ProxyFactoryBean로 변경
public class UserServiceTest {
    @Test
    @DirtiesContext//컨텍스트 설정 변경하기 때문에 여전히 필요
    public void upgradeAllOrNothing() {
        UserServiceImpl.TestUserService testUserService =
                new UserServiceImpl.TestUserService(users.get(3).getId());
        testUserService.setUserDao(this.userDao);
        testUserService.setMailSender(this.mailSender);

        //빈 자체를 가져올 때 &사용
        ProxyFactoryBean txProxyFactoryBean =
                context.getBean("&userService", ProxyFactoryBean.class);
        txProxyFactoryBean.setTarget(testUserService);

        //FactoryBean 타입 이므로 getObject()로 프록시를 가져옴
        UserService txUserService = (UserService) txProxyFactoryBean.getObject();

        userDao.deleteAll();
        for (User user : users) userDao.add(user);

        try {
            txUserService.upgradeLevels(); //txHandler를 통한 upgradeLevels()실행
            fail("TestUserServiceException expected");
        } catch (UserServiceImpl.TestUserServiceException e) {
        }

        checkLevelUpgraded(users.get(0), false);
    }
}

6.4.8 어드바이스와 포인트컷의 재사용

  • ProxyFactoryBean은 DI, 템플릿/콜백, 서비스 추상화 등의 기법이 적용됨
  • 때문에 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리할 수 있음
  • UserService 이외에 새로운 비즈니스 로직을 담은 서비스 클래스가 만들어져도 TransactionAdvice를 그대로 재사용 할 수 있음
  • TransactionAdvice는 하나만 만들어서 싱글톤 빈으로 등록하면, DI 설정을 통해 모든 서비스에 적용가능