본문 바로가기

Spring/토비의 스프링 3.1

6.3 다이내믹 프록시와 팩토리 빈


6.3.1 프록시와 프록시 패턴, 데코레이터 패턴

확장성을 고려한 기능의 분리

  • 전형적인 전략 패턴 사용 가능
  • 트랜잭션을 적용한다는 사실이 코드에 남아있음

적용 사실 자체의 분리가 필요

  • UserServiceTx 생성 후 UserServieImpl에 트랜잭션 관련 코드를 모두 제거
  • 부가기능 분리 시 부가기능 외 나머지 모든 기능은 원래 핵심기능을 가진 클래스로 위임해주어야 함

부가기능의 사용

  • 클라이언트가 핵심기능을 가진 클래스를 직접 사용하면 부가기능을 사용할 수 없음
  • 클라이언트는 인터페이스를 통해서만 핵심기능을 사용
  • 부가기능은 같은 인터페이스를 구현하여 끼어들어야 함
  • 즉, 클라이언트는 부가기능을 통해 핵심기능을 이용하게 됨

프록시 : 자신이 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 대리자, 대리인
타깃, 실체 : 최종적으로 요청을 위임받아 처리하는 실제 오브젝트

프록시의 사용목적

  1. 프록시 패턴 : 클라이언트가 타깃에 접근하는 방법을 제어
  2. 데코레이터 패턴 : 타깃에 부가적인 기능을 부여

데코레이터 패턴

타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여

  • 실제 내용물은 동일하지만 부가적인 효과를 부여
  • 여러 개의 프록시를 단계적으로 위임하는 구조로 사용 가능
  • ex) 클라이언트 -> 라인넘버 데코레이터 -> 컬러 데코레이터 -> 페이징 데코레이터 -> 소스코드 출력기능(타깃)

각 데코레이터의 다음 위임 대상은 인터페이스로 선언하고, 생성자는 수정자 메소드를 통해 위임 대상을 외부에서 런타임 시에 주입

  • 대표적인 예 : InputStream, OutputStream, UserService를 구현한 UserServiceImpl과 UserServiceTx

어느 데코레이터에서 타깃으로 연결될지 코드 레벨에서는 미리 알 수 없다.

프록시 패턴

타깃에 대한 접근 방법을 제어 (일반적으로 사용하는 프록시와 용어 구분)

  • 기능을 추가하거나 확장하지 않음
  • 타깃에 대한 접근 방법을 제어

타깃 오브젝트를 생성하기 복잡하거나 당장 필요하지 않은 경우, 필요한 시점까지 오브젝트를 생성하지 않는 것이 좋다.

  1. 타깃 오브젝트에 대한 레퍼런스가 미리 필요할 수 있다.
    • 실제 타깃 오브젝트를 만드는 대신 프록시를 넘겨주는 것
  2. 원격 오브젝트를 이용하는 경우
    • 다른 서버에 존재하는 오브젝트를 사용해야 하는 경우, 프록시를 이용하여 로컬에 존재하는 오브젝트를 쓰는 것처럼 사용할 수 있다.
  3. 특별한 상황에서 타깃에 대한 접근권한을 제어하는 경우
    • 원래는 수정 가능한 오브젝트인데, 특정 레이어로 넘어가서는 읽기전용으로 동작하도록 강제해야 하는 경우

구조적으로 데코레이터 패턴과 유사하지만, 프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다.

6.3.2 다이내믹 프록시

프록시를 만드는 일은 상당히 번거로움

  • java.lang.reflect 패키지 내 프록시를 쉽게 만들 수 있도록 지원하는 클래스를 제공

프록시를 구성과 프록시 작성의 문제점

프록시의 기능

  • 타깃과 같은 메소드를 구현하고 있다가 메소드가 호출되면 타깃 오브젝트로 위임
  • 지정된 요청에 대해 부가기능을 수행

UserServiceTx

  • 기능 부가를 위한 프록시
public class UserServiceTx implements UserService {
    // 타깃 오브젝트
    UserService userService;

    // ...

    // 메소드 구현과 위임
    @Override
    public void add(User user) {
        this.userService.add(user);
    }

    // 메소드 구현
    @Override
    public void upgradeLvls() {
        // 부가기능 수행
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            // 위임
            this.userService.upgradeLvls();

        // 부가기능 수행
            this.transactionManager.commit(status);
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}

프록시의 역할은 위임과 부가작업이라는 두 가지로 구분가능

프록시를 만들기 번거로운 이유

  1. 코드를 작성하기가 번거로움
    • 필요 없는 메소드도 구현해서 일일이 위임해주어야 함
  2. 부가기능 코드의 중복 가능성
    • 트랜잭션 코드는 대부분의 로직에 적용될 필요가 있음
    • 메소드가 늘어나고 트랜잭션 적용 비율이 높아지면, 유사한 코드가 여러 메소드에 중복해서 나타남

리플렉션

다이내믹 프록시 : 리플렉션 기능을 이용해 프록시를 만들어 줌

  • 자바의 코드 자체를 추상화해서 접근

Method : 리플렉션 API 중 메소드에 대한 정의를 담은 Method 인터페이스

  • String 클래스 정보 : String.class / name.getClass()
  • 길이 정보 : Method lengthMethod = String.class.getMethod("length");

invoke() : 메소드를 실행시킬 대산 오브젝트(obj)와 파라미터 목록(args)을 받아 메소드 호출 후 그 결과를 Object 타입으로 돌려줌

// int length = name.length();
int length = lengthMethod.invoke(name);

리플렉션 학습테스트

public class ReflectionTest {
    @Test
    public void invokeMethod() throws Exception {
        String name = "Spring";
        
        // length()
        assertThat(name.length(), is(6));
        
        Method lengthMethod = String.class.getMethod("length");
        assertThat((Integer)lengthMethod.invoke(name), is(6));
        
        // charAt
        assertThat(name.charAt(0), is('S'));
        
        Method charAtMethod = String.class.getMethod("charAt", int.class);
        assertThat((Character)charAtMethod.invoke(name, 0), is('S'));
    }
}

String 클래스의 length() 메소드와 charAt() 메소드를 코드에서 직접 호출하는 방법과, Method를 이용해 리플렉션 방식으로 호출하는 방법을 비교

프록시 클래스

다이내믹 프록시를 이용한 프록시

public interface Hello {
    String sayHello(String name);
    String sayHi(String name);
    String sayThankYou(String name);
}

타깃 클래스

public 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;
    }
}

테스트 클라이언트

public class ReflectionTest {
    @Test
    public void simpleProxy() {
        // 타깃은 인터페이스를 통해 접근
        Hello hello = new HelloTarget();
        assertThat(hello.sayHello("Toby"), is("Hello Toby"));
        assertThat(hello.sayHi("Toby"), is("Hi Toby"));
        assertThat(hello.sayThankYou("Toby"), is("Thank You Toby"));
    }
}

Hello 인터페이스를 구현한 프록시

  • 리턴 문자를 모두 대문자로 변환
public class HelloUppercase implements Hello {
    // 위임할 타깃 오브젝트
    // 여기서는 타깃 클래스의 오브젝트인 것을 알지만 다른 프록시를 추가할 수도 있으므로
    // 인터페이스로 접근한다.
    Hello hello;

    public HelloUppercase(Hello hello) {
        this.hello = hello;
    }

    @Override
    public String sayHello(String name) {
        // 위임과 부가기능 적용
        return hello.sayHello(name).toUpperCase();
    }

    @Override
    public String sayHi(String name) {
        return hello.sayHi(name).toUpperCase();
    }

    @Override
    public String sayThankYou(String name) {
        return hello.sayThankYou(name).toUpperCase();
    }
}

테스트

public class ReflectionTest {
    @Test
    public void simpleProxy() {
        Hello hello = new HelloTarget();
        assertThat(hello.sayHello("Toby"), is("Hello Toby"));
        assertThat(hello.sayHi("Toby"), is("Hi Toby"));
        assertThat(hello.sayThankYou("Toby"), is("Thank You Toby"));

        Hello proxiedHello = new HelloUppercase(new HelloTarget());
        assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));
        assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));
        assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
    }
}

코드의 문제점

  1. 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 함
  2. 부가기능이 모든 메소드에 중복돼서 나타남

다이내믹 프록시 적용

다이내믹 프록시 : 프록시 팩토리에 의해 런타임 시 다이내믹하게 만들어지는 오브젝트

  • 타깃의 인터페이스와 같은 타입으로 만들어짐
  • 클라이언트는 타깃 인터페이스를 통해 사용 가능

다이내믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어 줌

  • 프록시로서 필요한 부가기능은 직접 작성해야 함
  • 부가기능 : InvocationHandler를 구현한 오브젝트에 담음
public Object invoke(Object proxy, Method method, Object[] args)

invoke()

  • 리플렉션의 Method 인터페이스를 파라미터로 받음
  • 메소드 호출 시 전달되는 파라미터도 args로 받음

각 메소드 요청을 어떻게 처리할 것인가

  • Hello 인터페이스의 메소드가 아무리 많아도 invoke() 메소드 하나로 처리 가능

리턴 값을 대문자로 바꿔주는 InvocationHandler 구현 클래스

public class UppercaseHandler implements InvocationHandler {
    Hello target;

    public UppercaseHandler(Hello target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String ret = (String)method.invoke(target, args);
        return ret.toUpperCase();
    }
}

InvocationHandler 구현 클래스 : 다이내믹 프록시로부터 요청을 전달받음

  • 메소드 : invoke()
  • 타깃 오브젝트의 메소드 호출이 끝나면 부가기능인 리턴 값을 대문자로 변환하고, 결과 리턴
public class ReflectionTest {
    @Test
    public void simpleProxy() {
        // 생성된 다이내믹 프록시 오브젝트는 Hello 인터페이스를 구현하고 있으므로
        // Hello 타입으로 캐스팅해도 안전
        Hello proxiedHello = (Hello)Proxy.newProxyInstance(
            // 동적으로 생성되는 다이내믹 프록시 클래스 로딩에 사용할 클래스로더
            getClass().getClassLoader(),
            // 구현할 인터페이스
            new Class[] { Hello.class },
            // 부가기능과 위임 코드를 담은 InvocationHandler
            new UppercaseHandler(new HelloTarget()));

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

사용 방법

  1. 첫 번째 파라미터 : 클래스 로더 제공
  2. 두 번째 파라미터 : 다이내믹 프록시가 구현해야 할 인터페이스
  3. 마지막 파라미터 : 부가기능과 위임 관련 코드를 담은 InvocationHandler 구현 오브젝트

Hello 인터페이스의 메소드가 늘어나도 수정하지 않아도 된다.

다이내믹 프록시의 확장

String 외의 리턴 타입을 갖는 메소드가 추가된다면?

  • 현재 강제로 String 타입으로 캐스팅을 하고 있으므로 캐스팅 오류 발생
  • 스트링인 경우만 대문자로 변경

타깃의 종류에 상관없이도 적용 가능

  • Hello 타입의 타깃으로 제한할 필요 없음

수정된 UppercaseHandler

public class UppercaseHandler implements InvocationHandler {
    // 어떤 종류의 인터페이스를 구현한 타깃에도 적용 가능하도록 Object 타입으로 수정
    Object target;

    public UppercaseHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret = method.invoke(target, args);

        // 호출한 메소드의 리턴 타입이 String인 경우에만 대문자 변경 기능을 적용
        if (ret instanceof String) {
            return ((String)ret).toUpperCase();
        } else {
            return ret;
        }
    }
}

InvocationHandler : 부가적인 기능을 적용할 메소드를 선택 가능

  • 호출하는 메소드의 이름
  • 파라미터의 개수와 타입
  • 리턴 타입

6.3.3 다이내믹 프록시를 이용한 트랜잭션 부가기능

UserServiceTx를 다이내믹 프록시 방식으로 변경

  • 현재 UserServiceTx는 인터페이스의 모든 메소드를 구현하여야하며, 트랜잭션이 필요한 메소드마다 트랜잭션 처리코드가 반복됨
  • InvocationHandler를 정의하여 다이내믹 프록시를 적용

트랜잭션 InvocationHandler

TransactionHandler.java

public class TransactionHandler implements InvocationHandler {
    // 부가기능을 제공할 타깃 오브젝트
    // 어떤 타입의 오브젝트에도 적용 가능
    private Object target;
    // 트랜잭션 기능을 제공하는데 필요한 트랜잭션 매니져
    private PlatformTransactionManager transactionManager;
    // 트랜잭션을 적용할 메소드 이름 패턴
    private String pattern;

    public void setTarget(Object target) {
        this.target = target;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 트랜잭션 적용 대상 메소드를 선별해서 트랜잭션 경계설정 기능을 부여해준다.
        if (method.getName().startsWith(pattern)) {
            return invokeInTransaction(method, args);
        } else {
            return method.invoke(target, args);
        }
    }

    private Object invokeInTransaction(Method method, Object[] args) throws Throwable {
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());

        try {
            // 트랜잭션을 시작하고 타깃 오브젝트의 메소드를 호출
            Object ret = method.invoke(target, args);
            // 정상적으로 처리되면 커밋
            this.transactionManager.commit(status);
            return ret;
        } catch (InvocationTargetException e) {
            // 예외 발생 시 롤백
            this.transactionManager.rollback(status);
            throw e.getTargetException();
        }
    }
}

TransactionHandler의 워크플로우

  1. 요청을 위임할 타깃을 DI 받음
    • 타깃을 저장할 변수는 Object로 선언
    • PlatformTransactionManager를 DI
    • 메소드 이름을 DI
  2. 적용할 대상 선별
  3. 일치할 경우 트랜잭션 적용 메소드 호출
  4. 일치하지 않을 경우 타깃 오브젝트 메소드 호출 후 결과 리턴

Method.invoke() 메소드 사용 시 예외가 InvocationTargetException으로 포장돼서 전달 됨

TransactionHandler와 다이내믹 프록시를 이용하는 테스트

UserServiceTest.java

public class UserServiceTest {
        @Test
    public void upgradeAllOrNothing() throws Exception {
        // ...

        TransactionHandler txHandler = new TransactionHandler();
        // 트랜잭션 핸들러가 필요한 정보와 오브젝트를 DI
        txHandler.setTarget(testUserService);
        txHandler.setTransactionManager(transactionManager);
        txHandler.setPattern("upgradeLvls");

        // UserService 인터페이스 타입의 다이내믹 프록시 생성
        UserService txUserService = (UserService)Proxy.newProxyInstance(
            getClass().getClassLoader(), 
            new Class[] { UserService.class }, 
            txHandler
        );

        // ...
    }
}

UserServiceTx.java 삭제 및 설정 파일 수정 후 테스트

6.3.4 다이내믹 프록시를 위한 팩토리 빈

TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 사용할 수 있도록 해야 함

  • 다이내믹 프록시 오브젝트는 일반적인 스프링 빈으로 등록할 방법이 없음
  • 스프링의 빈은 기본적으로 클래스 이름과 프로퍼티로 정의됨
  • 그러나, 다이내믹 프록시는 Proxy 클래스의 newProxyInstance()라는 스태틱 팩토리 메소드를 통해서만 생성 가능

팩토리 빈

스프링에서 빈을 만드는 방법

  1. 디폴트 생성자를 통해 생성
  2. 팩토리 빈을 이용해 생성
    • 팩토리 빈 : 스프링을 대신하여 오브젝트의 생성 로직을 담당하도록 만들어진 특별한 빈

FactoryBean

public interface FactoryBean<T> {
    // 빈 오브젝트를 생성해서 돌려줌
    T getObject() throws Exception;
    // 셍성되는 오브젝트의 타입을 알려줌
    Class<? extends T> getObjectType();
    // getObject()가 돌려줒는 오브젝트가 항상 싱글톤 오브젝트인지 알려줌
    boolean isSigleton();
}

스프링 빈 학습테스트

// 생성자를 제공하지 않는 클래스
public class Message {
    String text;

    // 생성자가 private으로 선언되어 외부에서 생성자를 통해 오브젝트를 만들 수 없다.
    private Message(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    // 생성자 대신 사용할 수 있는 스태틱 팩토리 메소드 제공
    public static Message newMessage(String text) {
        return new Message(text);
    }
}

MessageFactoryBean

public class MessageFactoryBean implements FactoryBean<Message> {
    String text;

    /*
     * 오브젝트를 생성할 때 필요한 정보를 팩토리 빈의 프로퍼티로 설정하여 대신 DI
     * 주입된 정보는 오브젝트 생성 중 사용됨
    */
    public void setText(String text) {
        this.text = text;
    }

    /*
     * 실제 빈으로 사용될 오브젝트를 직접 생성
     * 코드를 이용하므로 복잡한 방식의 오브젝트 생성과 초기화 작업도 가능
    */
    @Override
    public Message getObject() throws Exception {
        return Message.newMessage(this.text);
    }

    @Override
    public Class<?> getObjectType() {
        return Message.class;
    }

    /*
     * getObject()가 돌려주는 오브젝트가 싱글톤인지 알려준다.
     * 이 팩토리 빈은 요청할 때마다 새로운 오브젝트를 만들어주므로 false
     * 이것은 팩토리 빈의 동작방식에 관한 설정이고, 
     * 만들어진 빈 오브젝트는 싱글톤으로 스프링이 관리해줄 수 있다.
    */
    @Override
    public boolean isSingleton() {
        return false;
    }
}

팩토리 빈

  • 팩토리 메소드를 가진 오브젝트
  • FactoryBean을 구현한 클래스가 빈으로 등록되면, 팩토리 빈 클래스의 오브젝트를 getObject()를 이용해 가져오고, 이를 빈 오브젝트로 사용함

팩토리 빈의 설정 방법

일반 빈과 비슷함

<bean id="message" class="com.david.learningtest.spring.factorybean.MessageFactoryBean">
    <property name="text" value="Factory Bean" />
</bean>

message 빈 오브젝트의 타입은 class 애트리뷰트에 정의된 MessageFactoryBean이 아닌 Message 타입임

  • Message 빈의 타입은 getObjectType() 메소드가 돌려주는 타입으로 결정됨
  • getObject() 메소드가 생성해주는 오브젝트가 message 빈의 오브젝트가 됨

FactoryBeanTest-context.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="message" class="com.david.learningtest.spring.factorybean.MessageFactoryBean">
        <property name="text" value="Factory Bean" />
    </bean>
</beans>

FactoryBeanTest.java

@RunWith(SpringJUnit4ClassRunner.class)
// 설정파일 이름을 지정하지 않으면 클래스이름 + "-context.xml" 파일을 찾아서 사용함
// 같은 패키지 내에 위치해있어야 함
@ContextConfiguration
public class FactoryBeanTest {
    @Autowired
    ApplicationContext context;

    @Test
    public void getMessageFromFactoryBean() {
        Object message = context.getBean("message");
        // 타입 확인
        // is(class) Deprecated -> is(instanceOf(class))
        assertThat(message, is(instanceOf(Message.class)));
        // 설정과 기능 확인
        assertThat(((Message)message).getText(), is("Factory Bean"));
    }
}

테스트 워크플로우

  1. message 빈의 타입이 정확히 무엇인지 확실하지 않으므로 ApllicationContext를 이용해 getBean()을 사용
    • 타입을 지정하지 않으면 Object 타입으로 리턴
  2. getBean()이 리턴한 오브젝트는 Message 타입이어야 함
  3. MessageFactoryBean을 통해 text 프로퍼티의 값이 바르게 주입되었는지 확인

팩토리 빈 자체를 가져오고 싶은 경우 '&'를 이용

@Test
public void getFactoryBean() throws Exception {
    // '&'의 여부에 따라 getBean()이 돌려주는 리턴 오브젝트가 달라짐
    Object factory = context.getBean("&message");
    assertThat(factory, is(instanceOf(MessageFactoryBean.class)));
}

다이내믹 프록시를 만들어주는 팩토리 빈

팩토리 빈 사용법

  • getObject() 메소드에 다이내믹 프록시 오브젝트를 만들어주는 코드를 삽입
  • 스프링 빈에는 팩토리 빈과 UserServiceImpl만 등록

트랙잭션 프록시 팩토리 빈

TxProxyFactoryBean.java

// 생성할 오브젝트 타입을 지정할 수도 있지만 범용적으로 사용하기 위해 Object로 함
public class TxProxyFactoryBean implements FactoryBean<Object> {
    // TransactionHandler 생성 시 필요한 프로퍼티
    Object target;
    PlatformTransactionManager transactionManager;
    String pattern;
    // 다이내믹 프록시를 생성할 때 필요
    // UserService 외 인터페이스를 가진 타겟에도 적용 가능
    Class<?> serviceInterface;

    public void setTarget(Object target) {
        this.target = target;
    }

    public void setTransactionManager(PlatformTransactionManager transactionManager) {
        this.transactionManager = transactionManager;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    public void setServiceInterface(Class<?> serviceInterface) {
        this.serviceInterface = serviceInterface;
    }

    // DI 받은 정보를 이용하여 TransactionHandelr를 사용하는 다이내믹 프록시를 생성
    @Override
    public Object getObject() throws Exception {
        TransactionHandler txHandler = new TransactionHandler();
        txHandler.setTarget(target);
        txHandler.setTransactionManager(transactionManager);
        txHandler.setPattern(pattern);

        return Proxy.newProxyInstance(
            getClass().getClassLoader(), 
            new Class[] { serviceInterface }, 
            txHandler
        );
    }

    @Override
    public Class<?> getObjectType() {
        // 팩토리 빈이 생성하는 오브젝트의 타입은 DI 받은 인터페이스 타입에 따라 달라진다.
        // 다양한 타입의 프록시 오브젝트 생성에 재사용 가능
        return serviceInterface;
    }

    @Override
    public boolean isSingleton() {
        // 싱글톤 빈이 아니라는 뜻이 아니라 getObject()가 매번 같은 오브젝트를 리턴하지 않는다는 의미
        return false;
    }
}

팩토리 빈이 만드는 다이내믹 프록시는 구현 인터페이스나 타깃의 종류에 제한이 없다.

  • UserService 외에도 트랜잭션 부가기능이 필요한 오브젝트를 위한 프록시를 만들 때 재사용이 가능

빈 설정

<bean id="userService" class="com.david.tobysspring.user.service.TxProxyFactoryBean">
    <property name="target" ref="userServiceImpl" />
    <property name="transactionManager" ref="transactionManager" />
    <property name="pattern" value="upgradeLvls" />
    <property name="serviceInterface" value="com.david.tobysspring.user.service.UserService" />
</bean>

serviceInterface

  • Class 타입
  • value를 이용하여 클래스 또는 인터페이스 이름을 넣어주면 됨
  • 프로퍼티 타입이 클래스인 경우, value로 설정해 준 이름을 가진 Class 오브젝트로 변환해 줌

트랜잭션 프록시 팩토리 빈 테스트

UserServiceTest의 upgradeAllOrNothing()

  1. add()
    • @AutoWired로 가져온 userService 빈을 사용
    • TxProxyFactoryBean 팩토리 빈이 생성하는 다이내믹 프록시 이용
    • 트랜잭션이 적용되지 않으므로 단순 위임 방식으로만 작동
  2. upgradeLvls(), mockUpgradeLvls()
    • 목 오브젝트를 이용하므로 트랜잭션과 무관
  3. upgradeAllOrNothing()
    • 현재 수동 DI를 하고 있으므로 팩토리 빈이 적용되지 않음
    • 트랜잭션이 적용되야함
    • TestUserService 오브젝트를 타깃 오브젝트로 대신 사용해야 함
    • 설정에는 UserServiceImpl 오브젝트로 지정되어 있지만 테스트 메소드에서는 TestUserService 오브젝트가 동작해야 함

타깃 오브젝트에 대한 레퍼런스는 TransactionHandler가 갖고 있음. 그런데 TransactionHandler는 TxProxyFactoryBean 내부에서 만들어지므로 별도로 참조할 방법이 없음

  • TxProxyFactoryBean을 직접 가져와서 프록시를 만들어보자
  • 스프링 빈으로 등록된 TxProxyFactoryBean을 가져와 target 프로퍼티를 재구성 후 다시 프록시 오브젝트를 생성하도록 요청
  • 컨텍스트의 설정이 변경되므로 DirtiesContext 필요

UserServiceTest.java

public class UserServiceTest {
    // 팩토리 빈을 가져오기 위해서는 애플리케이션 컨텍스트가 필요
    @Autowired ApplicationContext context;

    @Test
    // 다이내믹 프록시 팩토리 빈을 직접 만들어 사용할 때는 없앴다가 다시 등잘
    @DirtiesContext
    public void upgradeAllOrNothing() throws Exception {
        TestUserService testUserService = new TestUserService(users.get(3).getId());
        testUserService.setUserDao(userDao);
        testUserService.setMailSender(mailSender);

        // 팩토리 빈 잧를 가져와야 하므로 '&' 필요
        // 테스트용 타깃 주입
        TxProxyFactoryBean txProxyFactoryBean = context.getBean("&userService", TxProxyFactoryBean.class);
        txProxyFactoryBean.setTarget(testUserService);
        // 변경된 타깃 설정을 이용해 트랜잭션 다이내믹 프록시를 다시 생성
        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);
    }
}

UserServiceTx를 사용할 때와 비슷한 코드

6.3.5 프록시 팩토리 빈 방식의 장점과 한계

프록시 팩토리 빈의 재사용

TransactionHandler를 이용하는 다이내믹 프록시를 생성해주는 TxProxyFactoryBean은 코드 수정 없이 다양한 클래스에 적용할 수 있다.

설정 변경을 통한 트랜잭션 기능 부가

  • 클라이언트 -----------------------> CoreServiceImpl
  • 클라이언트 -> TxProxyFactoryBean -> CoreServiceImpl

프록시 팩토리 빈을 이용하면 프록시 기법을 아주 빠르고 효과적으로 적용할 수 있다.

프록시 팩토리 빈 방식의 장점

데코레이터 패턴 적용의 문제점을 해결

  1. 타깃 인터페이스를 구현하는 클래스를 일일이 만드는 번거로움 제거
  2. 부가 기능 코드의 중복 문제 해결

프록시 팩토리 빈의 한계

프록시를 통해 부가기능을 제공하는 단위 : 메소드

  1. 한 번에 여러 개의 클래스에 공통적인 부가기능 제공이 불가
  2. 하나의 타깃에 여러 개의 부가 기능을 적용할 때 문제
    • 빈 설정이 부가 기능의 개수만큼 붙어야 함
  3. TransactionHandler 오브젝트가 프록시 팩토리 빈 개수만큼 만들어짐


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

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