트랜잭션 경계설정 코드를 비즈니스 로직 코드에서 분리해낼 때 적용했던 기법을 다시 검토해보자.
트랜잭션 기능은 사용자 관리 비즈니스 로직과는 성격이 다르기 때문에 아예 그 적용 사실 자체를 밖으로 분리할 수 있다. 아래 그림과 같이 부가기능 전부를 핵심코드가 담긴 클래스에서 독립시킬 수 있다.

<그림 6-4>.

문제는 이렇게 구성했더라도 클라이언트가 핵심기능을 가진 클래스를 직접 사용해버리면 부가기능이 적용될 기회가 없다는 점이다. 그래서 부가기능은 클라이언트가 자신을 거쳐서 핵심기능을 사용하도록 만들어야 한다. 부가기능 코드에서는 핵심기능으로 요청을 위임해주는 과정에서 자신이 가진 부가적인 기능을 적용해줄 수 있다.

이를 위한 방법이 프록시이다.

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

1.1 프록시와 프록시 패턴

  • 프록시 : 마치 자신이 클라이언트가 사용하려는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 대리자, 대리인 (UserServiceTx)
  • 타깃(target) : 실제 오브젝트 (UserServiceImpl)

프록시의 사용 목적

  1. 클라이언트가 타깃에 접근하는 방법을 제어
  2. 타깃에 부가적인 기능을 부여

1.2 데코레이터 패턴

데코레이터 패턴은 타깃에 부가적인 기능을 런타임 시 다이내믹하게 부여해주기 위해 프록시를 사용하는 패턴을 말한다.
프록시가 꼭 한개로 제한되지 않는다.
부가 기능을 각각 프록시로 만들어두고 런타임 시에 이를 적절한 순서로 조합해서 사용하면 된다.

<그림 6-11>.
  • 데코레이터 : userService빈
  • 타깃 : UserServiceImpl 클래스

UserServiceImpl 클래스로 선언된 타깃 빈이 DI를 통해 데코레이터인 userService 빈에 주입되도록 설정되어 있다.

<!-- Decorator -->
<bean id="userService" class="springbook.user.service.UserServiceTx">
 <property name="transactionManager" ref="transactionManager" />
 <property name="userService" ref="userServiceImpl" />  // 주입설정
</bean>

<!-- target -->
<bean id="userServiceImpl" class="springbook.user.service.UserServiceImpl">
 <property name="userDao" ref="userDao"/>
</bean>

1.3 프록시 패턴

프록시란? 기존코드에 영향을 주지 않으면서 타깃의 기능을 확장하거나 접근 방법을 제어할 수 있는 유용한 방법

프록시 기능

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

프록시 문제점

  1. 부가기능이 필요 없는 메소드도 구현해서 타깃으로 위임하는 코드를 일일이 만들어줘야 하는 번거로움
  2. 부가기능 코드가 중복될 가능성이 많다.

위와 같은 문제를 해결하는 데 유용한 것이 바로 JDK의 다이내믹 프록시이다.

2. 다이내믹 프록시

2.1 리플렉션

다이내믹 프록시는 리플렉션 기능을 이용해서 프록시를 만들어준다. 리플렉션은 자바의 코드 자체를 추상화해서 접근하도록 만든 것이다.
reflection에 관련한 자세한 설명은 아래 링크에 있다.
https://codechacha.com/ko/reflection/

2.2 프록시 예제

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 "Thankyou "+name;
  }
}
public class HelloUppercase implements Hello {

  Hello hello;  // Target Object. 다른 프록시를 추가할 수도 있으므로 interface로 접근한다.

  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 void test(){
  Hello proxiedHello = new HelloUppercase(new HelloTarget());
  assertThat(proxiedHello.sayThankyou("JinYoung"),is("THANKYOU JINYOUNG"));
}

2.3 다이내믹 프록시 적용

다이나믹 프록시 동작하는 방식은 아래와 같다.

<그림 6-13>.
  1. 다이내믹 프록시 : 런타임 시 다이내믹하게 만들어지는 오브젝트
  2. 클라이언트는 다이내믹 프록시 오브젝트를 타깃 인터페이스를 통해 사용할 수 있다.
    -> 인터페이스를 모두 구현해가면서 클래스를 정의하는 수고를 덜 수 있다.
  3. 다이나믹 프록시가 인터페이스 구현 클래스의 오브젝트는 만들어주지만, 프록시로 필요한 부가기능 제공 코드는 직접 작성해야 한다.
    부가기능은 InvocationHandler를 구현한 오브젝트에 담는다.

아래 그림은 다이내믹 프록시 오브젝트InvocationHandler 오브젝트, 타깃 오브젝트 사이의 메소드 호출이 일어나는 과정을 나타낸다.

<그림 6-14>.

InvocationHandler 구현

public class UppercaseHandler implements InvocationHandler {

  Object target;  // 어떤 종류의 인터페이스를 구현한 타깃에도 적용 가능

  private UppercaseHandler(Object target){
    this.target=target;
  }
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object ret = method.invoke(target,args);
    if(ret instanceof String)
      return ret.toString().toUpperCase();
    else
      return ret;
  }
}
public void createProxy(){
  Hello proxiedHello = (Hello) Proxy.newProxyInstance(
    getClass().getClassLoader(), 
    new Class[] {Hello.class}, // 구현할 인터페이스 
    new UppercaseHandler(new HelloTarget())); // 부가기능과 위임 코드를 담은 InvocationHandler
}

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

3.1 예제

public class TransactionHandler implements InvocationHandler {

  @Autowired
  private PlatformTransactionManager transactionManager;

  private Object target;
  private String pattern;

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

  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();
    }
  }
}

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

이제 TransactionHandler와 다이내믹 프록시를 스프링의 DI를 통해 사용할 수 있도록 만들어야 할 차례다.
문제는, 다이내믹 프록시는 일반적인 스프링의 빈으로는 등록(리플렉션)할 수 없다. 클래스 자체도 내부적으로 다이내믹하게 새로 정의해서 사용하기 때문이다.

다이내믹 프록시는 Proxy클래스의 newProxyInstance()라는 스태틱 팩토리 메소드를 통해서만 만들 수 있다.
링크 > newProyInstance()

팩토리 빈이란 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말한다.

팩토리 빈이 만드는 다이내믹 프록시는 구현 인터페이스나, 타깃의 종류에 제한이 없다. 따라서 얼마든지 재사용이 가능하다.

댓글남기기