본문 바로가기

Spring/토비의 스프링 3.1

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


6.4.1 ProxyFactoryBean

스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공

  • 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공
  • ProxyFactoryBean : 프록시를 생성해서 빈 오브젝트로 등록하게 해주는 팩토리 빈
  • 순수하게 프록시 생성을 위한 작업만을 담당하고, 부가 기능은 별도의 빈에 둘 수 있음

MethodInterceptor 인터페이스를 구현하여 만듦

  • InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않음
  • MethodInterceptor의 invoke() 메소드는 타깃 오브젝트에 대한 정보까지 함께 제공받음
  • 타깃 오브젝트에 상관없이 독립적으로 만들 수 있음
  • 싱글톤 빈으로 등록 가능

학습 테스트

package com.david.learningtest.jdk.proxy;

public class DynamicProxyTest {
    @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.sayHello("Toby"), is("HI TOBY"));
        assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
    }

    static class UppercaseAdvice implements MethodInterceptor {
        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            // 타깃 오브젝트를 전달할 필요 없음. 메소드 정보와 타깃 오브젝트를 이미 알고 있음
            String ret = (String)invocation.proceed();
            // 부가 기능 적용
            return ret.toUpperCase();
        }
    }

    // 타깃과 프록시가 구현할 인터페이스
    static interface Hello {
        String sayHello(String name);
        String sayHi(String name);
        String sayThankYou(String name);
    }

    // 타깃 클래스
    static class HelloTarget implements Hello {
        @Override
        public String sayHello(String name) {
            return "Hello " + name;
        }

        @Override
        public String sayHi(String name) {
            return "Hi " + name;
        }

        @Override
        public String sayThankYou(String name) {
            return "Thank You " + name;
        }
    }
}

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

MethodInterceptor를 구현한 UppercaseAdvice에는 타깃 오브젝트가 등장하지 않는다.

  • 메소드 정보와 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달되기 때문
  • 따라서 MethodInterceptor는 부가기능을 제공하는 데 집중 가능

MethodInvocation : 일종의 콜백 오브젝트

  • proceed() 메소드 실행 시 타깃 오브젝트의 메소드를 내부적으로 실행
  • MethodInvocation 구현 클래스 : 일종의 공유 가능한 템플릿처럼 동작

addAdvice()

  • ProxyFactoryBean에는 여러 개의 MethoInterceptor를 추가할 수 있다.
  • 아무리 많은 부가기능을 적용하더라도 ProxyFactoryBean 하나로 충분하다.

addMethodInterceptor()가 아닌 addAdvice()

  • MethodInterceptor : Advice 인터페이스를 상속하는 서브인터페이스
  • 어드바이스(Advice) : 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트

Hello라는 인터페이스를 제공하는 부분이 없음

  • setInterfaces()를 통해 직접 지정할 수도 있음
  • 자동거물 기능을 사용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아냄
  • 알아낸 인터페이스를 모두 구현하는 프록시를 만들어 줌

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

메소드의 이름을 가지고 부가기능 적용 대상 메소드를 선정하는 방법

  • MethodInterceptor는 여러 프록시가 공유해서 사용
  • 타깃 정보를 가지고 있지 않다.
  • 싱글톤 빈으로 등록이 가능하다.
  • 특정 프록시에만 적용이 되는 메소드 이름 패턴을 넣을 수 없다.

코드 개선 전략

  • 프록시가 클라이언트로부터 받는 요청을 일일이 전달받을 필요는 없다.
  • MethodInterceptor에 재사용 가능한 순수한 부가기능 제공 코드만 남기면 된다.
  • 대신 프록시에 부가기능 적용 메소드를 선택하는 기능을 추가 (분리할 필요 있음)

ProxyFactoryBean 방식

  • 부가기능(Advice)과 메소드 선정 알고리즘(Pointcut) 두 가지 확장 기능을 활용하는 유연한 구조를 제공
  • 어드바이스(Advice) : 부가기능을 제공하는 오브젝트
  • 포인트컷(Pointcut) : 메소드 선정 알고리즘을 담은 오브젝트

프록시의 동작 순서

  1. 클라이언트로부터 요청
  2. 포인트컷에 부가기능 부여할 메소드인지 확인 요청
  3. 부가기능 부여 오브젝트인 경우 MethodInterceptor 타입의 어드바이스 호출
    • 직접 타깃을 호출하지는 않음
    • MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출

전형적인 전략 패턴 구조

  • 프록시로부터 어드바이스와 포인트컷을 독립
  • DI를 이용

학습 테스트

  • 스프링이 제공하는 NameMatchMethodPointcut을 UppercaseAdvice와 함께 사용하도록 만든 테스트 코드
public class DynamicProxyTest {
        @Test
    public void pointcutAdvisor() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloTarget());

        // 메소드 이름을 비교해서 대상을 선정하는 알고림을 제공하는 포인트컷 생성
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        // 이름 비교조건 설정
        // sayH로 시작하는 모든 메소드를 선택하게 한다.
        pointcut.setMappedName("sayH*");

        // 포인트컷과 어드바이스를 Advisor로 묶어서 한 번에 추가
        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"));
        // 메소드 이름이 포인트컷 선정조건에 맞지 않으므로 부가기능이 적용되지 않는다.
        assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby"));
    }
}

어드바이스와 포인트컷을 Advisor 타입을 묶어서 addAdvisor() 메소드를 호출해야함

  • ProxyFactoryBean에는 여러 개의 어드바이스와 포인트컷이 추가될 수 있기 때문
  • 따로 등록하면 어떤 어드바이스(부가기능)에 대해 어떤 포인트컷(메소드 선정)을 적용할지 애매

어드바이저 = 포인트컷(메소드 선정 알고리즘) + 어드바이스(부가기능)

6.4.2 ProxyFactoryBean 적용

TxProxyFactoryBean을 ProxyFactoryBean으로 수정

TransactionAdvice

어드바이스 : MethodInterceptor라는 Advice 서브인터페이스를 구현

  1. 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와 달리 예외가 포장되지 않고 전달된다.
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}

스프링 XML 설정파일

test-applicationcontext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="userServiceImpl" />
        <!--
            어드바이스와 어드바이저를 동시에 설정해줄 수 있는 프로퍼티
            리스트에 어브바이스나 어드바이저의 빈 아이디를 넣어줌
            기존 ref 애트리뷰트 사용법과 다름에 주의
        -->
        <property name="interceptorNames">
            <list>
                <!-- 하나 이상의 value 태그를 넣을 수 있음 -->
                <value>transactionAdvisor</value>
            </list>
        </property>
    </bean>

    <!-- 어드바이스 -->
    <bean id="transactionAdvice" class="com.david.tobysspring.user.service.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>
</beans>

테스트

upgradeAllOrNothing()

  • ProxyFactoryBean 역시 팩토리 빈이므로 기존 TxProxyFactoryBean과 같은 방식으로 테스트 가능
  • 캐스팅 타입만 변경
public class UserServiceTest {
    @Test
    // 컨텍스트 설정 변경을 위해 여전히 필요
    @DirtiesContext
    public void upgradeAllOrNothing() throws Exception {
        TestUserService testUserService = new TestUserService(users.get(3).getId());
        testUserService.setUserDao(userDao);
        testUserService.setMailSender(mailSender);

        // userService 빈은 이제 스프링의 ProxyFactoryBean
        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.upgradeLvls();
            fail("TestUserServiceException expected");
        } catch (TestUserServiceException e) {
        }

        checkLvlUpgraded(users.get(1), false);
    }
}

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

ProxyFactoryBean

  • 스프링의 DI, 템플릿/콜백 패턴, 서비스 추상화 등 기법이 모두 적용
  • 독립적이며, 여러 프록시가 공유할 수 있는 어드바이스와 포인트컷으로 확장 기능을 분리
  • 포인트컷이 필요하면 이름 패턴만 지정해서 ProxyFactoryBean에 등록


' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

6.5 스프링 AOP  (0) 2019.02.27
6.3 다이내믹 프록시와 팩토리 빈  (0) 2019.02.13
6.2 고립된 단위 테스트  (0) 2019.02.12
6.1 트랜잭션 코드의 분리  (0) 2019.02.07
6장. AOP  (0) 2019.02.07