7 Sep 2017

Toby's Spring Chap 06: AOP part.1

트랜잭션 코드의 분리

다음과 같이 비즈니스 로직을 담고 있는 코드에서 트랜잭션 경계설정을 담당하는 코드가 포함되어 있다.

public void upgradeLevels() throws Exception {

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

  try {
    upgradeLevelsInternal();
    transactionManager.commit(status);

  } catch (Exception e) {
    transactionManager.rollback(status);
    throw e;

  }
}

private void upgradeLevelsInternal() {

  List<User> userList = userDao.getAll();

  for (User user : userList) {

    if (canUpgradeLevel(user) == true) {
      upgradeLevel(user);
    }
  }
}

위 코드을 봤을 때, 트랜잭션 경계설정 코드와 비즈니스 로직 코드가 서로 주고받는 정보가 없이 깔끔하게 분리되어 있다. 비즈니스 로직 코드에서 직접 DB를 다루지 않으므로, DB Connection 직접 참조같은 것을 하지 않기 때문이다. (트랜잭션 동기화 방법을 통해서)

하지만 아직도 이 클래스에는 비즈니스 로직과는 관계가 없는, 트랜잭션을 위한 코드가 들어있다.


DI 적용을 통한 트랜잭션 분리

보통 DI를 통한 의존 오브젝트 주입은 구현 클래스를 바꿔가면서 쓰기 위함이다. 하지만 꼭 그 목적을 위해서만 DI를 쓸 필요는 없으며 다음과 같이 서로 관심이 다른 코드를 분리하기 위해서도 사용할 수 있다.


00.png

트랜잭션 경계 설정만을 담당하는 클래스인 “UserServiceTx” 를 따로 만들고, 여기서 트랜잭션 경계 설정만을 구현한 후 실제 비즈니스 로직을 담당하는 “UserServiceImpl” 에 작업을 위임시키는 것이다.

결국 UserService 인터페이스를 갖고 사용하는 클라이언트 측에서는 트랜잭션이 적용된 비즈니스 로직의 구현이라는 기대하는 동작이 일어날 것이다.


01.png

Separate transaction code using DI

이렇게 DI를 활용하여 관심사가 서로 다른 코드를 분리함으로써, 비즈니스 로직만을 담은 코드를 작성할 때 트랜잭션과 같은 기술적인 내용에는 신경쓰지 않아도 된다. 또한 비즈니스 로직에 대한 테스트를 쉽게 만들 수 있다.


고립된 단위 테스트

좋은 테스트 방법은 가능한 작은 단위(하나의 관심을 다루는)로 쪼개서 테스트하는 것이다. 테스트가 실패했을 때 그 원인을 찾기가 쉽고, 테스트의 대상이 커지면 이를 검증할만한 테스트 코드를 만들기도 쉽지 않기 때문이다.

그러나 테스트 대상이 다른 오브젝트와 환경에 의존하고 있다면 작은 단위의 테스트가 주는 장점을 얻기 힘들다.


복잡한 의존관계 속의 테스트

다음과 같이 “UserService” 클래스는 여러 의존 객체가 있다.


02.png

UserService 테스트의 단위는 UserService 클래스가 다루는 관심사이어야 하는데, 의존 객체들이 여러 개 있다면 테스트 준비하기가 힘들고 환경이 조금이라도 달라지면 테스트가 실패할 수 있다. 하고자 하는 테스트는 UserService의 비즈니스 로직인데 DB 연결이 제대로 안되거나 트랜잭션이 제대로 수행되지 않으면, UserService를 목적으로 하는 테스트가 실패하기 때문이다.

따라서 다음과 같이 테스트 대상이 외부 환경이나 다른 클래스의 코드에 의해 영향을 받지 않도록 고립시킬 필요가 있다. 테스트 대역을 사용하여, 테스트 대상이 의존하는 객체로부터 분리해서 테스트하는 것이다.


03.png


단위 테스트와 통합 테스트

단위 테스트의 단위는 정하기 나름이다. 사용자 관리라는 기능 전체를 하나의 단위로 볼 수도 있고, 하나의 클래스나 하나의 메소드를 단위로 볼 수도 있다. 보통은 다음과 같이 정의한다.

  • 단위 테스트: 테스트 대상을 의존하는 객체 대신 목 객체를 사용하여 외부 환경에 영향받지 않도록 고립시켜서 하는 테스트
  • 통합 테스트: 두 개 이상의 성격이나 계층이 다른 객체가 연동되도록 만들어 테스트하거나, 외부 DB나 환경에 의존하는 테스트
    • 스프링의 테스트 컨텍스트 프레임워크를 이용하여, 컨텍스트에서 생성되고 DI된 오브젝트를 테스트하는 것도 통합 테스트이다.
  • 테스트 가이드라인
    • 항상 단위 테스트를 먼저 고려한다.
    • 단위 테스트 수행시, 항상 외부와의 의존관계를 차단하고 필요에 따라 목 객체나 스텁을 사용하여 테스트 대역을 이용하도록 만든다.
    • 외부 리소스에 의존하는 테스트는 통합 테스트로 만든다.
      • 여러 개의 단위가 서로 의존관계를 가지고 동작할 때를 위한 통합 테스트도 필요하다.
    • DAO는 그 자체로 비즈니스 로직을 담고 있다기 보다는 DB 인터페이스와 같은 역할을 한다. 고립된 테스트를 만들기가 어려우며, 만든다해도 가치가 없는 것이 대부분이다. 따라서 DAO는 DB까지 연동하는 테스트로 만드는 편이 효과적이다.
      • DAO를 테스트를 통해 검증하면, DAO를 이용하는 코드는 DAO 역할을 하는 객체를 목 객체나 스텁으로 대체하여 단위 테스트를 수행할 수 있다.

테스트는 코드가 작성되고 빠르게 진행되는 편이 좋다. 테스트를 코드가 작성된 후에 만드는 경우에도 가능한 빨리 작성하도록 해야 한다. 테스트하기 편하게 만들어진 코드는 깔끔하고 좋은 코드가 될 가능성이 높다. 스프링이 지지하고 권장하는, 깔끔하고 유연한 코드를 만들다보면 테스트도 그만큼 만들기 쉬워지고, 테스트는 다시 코드의 품질을 높여준다.


목 프레임워크

단위 테스트시에 필요한 목 객체나 스텁을 쉽게 만들 수 있도록 도와주는, Mockito 와 같은 목 객체 지원 프레임워크가 있다.

Mockito와 같이 목 프레임워크의 특징은 의존 객체를 대신하는 목 객체를 일일이 준비해둘 필요가 없다는 것이다. 간단한 메소드 호출만으로도, 다음과 같이 테스트용 목 객체를 만들 수 있다.

UserDao mockUserDao = mock(UserDao.class);
when(mockUserDao.getAll()).thenReturn(userList);


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


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

트랜잭션 경계설정과 같은 비즈니스 로직이 아닌 코드는 전략 패턴을 통해 비즈니스 로직을 담은 클래스 외부로 빼낼 수가 있다.


04.png

위와 같은 분리된 “부가기능” 을 담은 클래스는 원래 비즈니스 로직을 담은 핵심 기능을 가진 클래스로 위임한다. 비즈니스 로직을 담은 “핵심기능” 을 담당하는 클래스는 부가기능을 가진 클래스의 존재 자체를 모른다. 부가기능이 핵심기능을 사용하는 구조이다.

그런데 클라이언트에서 핵심기능을 가진 클래스를 직접 사용할 때, 부가기능이 적용될 기회가 없다. 따라서 부가기능을 가진 클래스는 마치 자신이 핵심기능을 가진 클래스인 것처럼 꾸며서 자신을 거쳐서 핵심기능을 사용하도록 해야한다.

이를 위해 클라이언트는 인터페이스를 통해서만 핵심기능을 사용하게하고, 부가기능 자신도 같은 인터페이스를 구현 한 뒤에, 자신이 그 사이에 끼어들어야 한다.


05.png

위와 같이 적용되었을 때, 자신이 가진 부가적인 기능을 적용해줄 수 있다. 비즈니스 로직 코드에서 트랜잭션 기능을 부여해준 것이 바로 그런 경우이다.

이렇게 마치 자신이 클라이언트가 사용하려는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 것을 프록시(Proxy) 라고 하며, 프록시를 통해 최종적으로 요청을 받아 처리하는 실제 객체를 타깃 또는 실체 라고 한다.


06.png

프록시의 특징은 타깃과 같은 인터페이스를 구현했다는 것과 타깃을 제어할 수 있는 위치에 있다는 것이다. 프록시의 사용 목적은 클라이언트가 타깃에 접근하는 방법을 제어하거나 부가기능을 부여해주기 위해서이다.


데코레이터 패턴

타깃에 부가기능을 런타임에 다이내믹하게 부여해주기 위하여, 프록시를 사용하는 패턴이다. 즉 코드 상에서는 어떤 방법과 순서로 프록시와 타깃이 연결되어 있는지 나타나지 않는다. 부가기능을 담당하는 데코레이터는 여러 개 있을 수 있다.


07.png

프록시로서 동작하는 각 데코레이터들은 자신이 위임하는 대상에도 인터페이스로 접근하므로, 최종 타깃으로 위임하는 것인지 다음 단계의 데코레이터 프록시로 위임하는지 알지 못한다.

Java의 InputStream과 OutputStream 구현 클래스는 데코레이터 패턴이 사용된 대표적인 예이다.

스프링에서는 다음과 같이 데코레이터의 정의와 런타임때 구성 방법을 DI를 통해 쉽게 적용할 수 있다.

<!-- Decorator -->
<bean id="userService" class="org.service.UserServiceTx">
  <property name="transactionManager" ref="transactionManager" />
  <property name="userService" ref="userServiceImpl" />
</bean>

<!-- Target -->
<bean id="userServiceImpl" class="org.service.UserServiceImpl" />
  <property name="userDao" ref="userDao" />
</bean>

위와 같이 코드 레벨에서 데코레이터와 타깃 간의 연결은 나타내지 않고, 다이내믹하게 연결을 할 수 있다. 데코레이터 패턴은 타깃의 코드를 손대지 않고, 클라이언트가 호출하는 방법도 변경하지 않은 채로 새로운 기능을 추가할 때 유용한 방법이다.


프록시 패턴

  • 프록시: 클라이언트와 타깃 사이에 대리 역할을 맡은 객체를 총칭
  • 프록시 패턴: 프록시를 사용하는 방법 중, 타깃에 대한 접근방법을 제어하려는 목적 을 가진 경우

프록시 패턴의 프록시는 타깃의 기능을 추가하거나 확장하지 않는다. 대신 클라이언트가 타깃에 접근하는 방식을 변경해준다.

예를 들어 타깃 객체가 생성하기 복잡하거나 당장 필요하지 않은 경우, 필요한 시점까지 생성하지 않고 참조가 필요한 클라이언트 쪽에 프록시를 대신 넘겨줄 수 있다. 프록시의 메소드를 통해 실제 호출할 때, 그 시점에 타깃 객체를 생성하고 위임해주는 것이다.


접근 권한을 제어 하기 위해 프록시 패턴을 사용할 수도 있다. 특정 계층에서 타깃 객체가 읽기 전용으로 강제되어야 할 때, 프록시를 사용하여 데이터 변경을 유발하는 메소드는 예외를 던지게 할 수도 있다. (ex: Collections의 unmodifiableCollection())

프록시 패턴은 타깃의 기능 자체에는 관여하지 않으면서 접근하는 방법을 제어해주는 프록시를 이용하는 것이다.

프록시는 코드에서 자신이 만들거나 접근할 타깃 클래스 정보를 알고 있는 경우가 많다. 생성을 지연시키는 프록시라면 타깃에 대한 구체적인 생성 방법을 알아야 하기 때문이다. 물론 프록시 패턴을 사용하는 경우라도 인터페이스를 통해 위임하게 만들 수도 있다.


다이내믹 프록시

프록시는 기존 코드에 영향을 주지 않으면서 기능을 확장하거나 접근 방법을 제어할 수 있는 유용한 방법이다. 그러나 매번 프록시를 위해 인터페이스를 상속받아 클래스를 정의한다면 번거롭기 때문에, 자바의 리플렉션을 통해 프록시를 만든다.

  • 프록시를 직접 작성할 때의 문제점
    • 타깃의 인터페이스를 구현하고 위임하는 코드를 직접 구현해야 한다. 타깃의 메소드가 추가되거나 변경되면 함께 수정해야 한다.
    • 부가기능 코드가 중복될 가능성이 높다. 보통 부가기능은 일반적인 코드가 많아 여러 곳에서 쓰일 수 있다.

다이내믹 프록시는 리플렉션 기능을 활용하여 프록시로 만드는 것이다.

Reflection Sample Test


다이내믹 프록시가 동작하는 방식은 다음과 같다.


08.png

다이내믹 프록시는 프록시 팩토리에 의해 런타임때 다이내믹하게 만들어진다. 이 프록시는 타깃의 인터페이스와 같은 타입으로 만들어지므로 클라이언트는 이 프록시 객체를 코드 변경없이 사용할 수 있다. 따라서 프록시 생성시 인터페이스를 모두 구현할 필요가 없고 프록시 팩토리에게 인터페이스 정보만 알려주면 된다.

다이내믹 프록시가 인터페이스 구현 클래스의 객체로 만들어지긴 하지만, 필요한 부가기능 코드는 직접 작성해야 한다. 부가기능은 프록시와 독립적인 InvocationHandler 를 구현한 객체에 담는다. InvocationHandler 인터페이스는 다음과 같이 메소드 하나만 가진 간단한 인터페이스이다.

public Object invoke(Object proxy, Method method, Object[] args);

위의 invoke 메소드는 Method 인터페이스 및 메소드 파라미터인 args를 받는다. 다이내믹 프록시 객체는 클라이언트의 모든 요청을 리플렉션 정보로 변환하여 InvocationHandler 구현 객체의 invoke 메소드로 넘긴다. InvocationHandler 인터페이스 구현체에서는 Method 인터페이스 구현체 및 args를 통해 실제 타깃 객체의 메소드를 호출할 수 있다. 중복되는 부가적인 기능을 InvocationHandler을 통해 쉽게 작성할 수 있다.


09.png

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UppercaseHandler implements InvocationHandler {

  private Hello target;

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

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 만약 타깃 메소드에 대한 예외발생시 여기에서는 "InvocationTagetException" 으로 잡아야 한다.
    // 타깃 객체에서 발생하는 예외가 "InvocationTagetException" 으로 한 번 포장되서 전달된다.
    String ret = (String)method.invoke(target, args);
    return ret.toUpperCase();
  }
}


Hello proxyHello = (Hello)Proxy.newProxyInstance(getClass().getClassLoader(), //다이내믹 프록시 클래스의 로딩에 사용할 클래스 로더
                        new Class[] { Hello.class }, // 구현할 인터페이스, 다이내믹 프록시는 하나이상의 인터페이스를 구현할 수 있다.
                        new UppercaseHandler(new HelloTarget())); // 부가기능과 위임코드를 담은 InvocationHandler

위의 코드와 같이 리플렉션 API를 활용하여 타깃 오브젝트의 메소드를 호출한다.

Dynamic Proxy

UserServiceTx using Dynamic Proxy

이렇게 다이내믹 프록시를 이용하면 타깃 인터페이스를 일일이 구현해야 하는 번거로움을 피할 수 있고, 하나의 핸들러 메소드를 구현하는 것만으로 수많은 메소드에 부가기능을 추가할 수 있어 코드 중복도 사라진다.


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

다이내믹 프록시 객체는 일반적인 스프링 빈으로 등록할 방법이 없다. 스프링 빈은 기본적으로 미리 정의된 클래스 이름과 프로퍼티를 가지고 객체를 생성한다.

Date now = (Date)Class.forName("java.util.Date").newInstance();

위와 같이 클래스 이름을 가지고 리플렉션을 이용하여 파라미터가 없는 디폴트 생성자를 호출하고 오브젝트를 생성한다.

다이내믹 프록시 객체는 이런 식으로 생성되지 않는다. 다이내믹 프록시 클래스는 내부적으로 다이내믹하게 새로 정의되어 사용되므로 사전에 애플리케이션 컨텍스트를 이용하여 스프링 빈으로 정의할 방법이 없다. 다이내믹 프록시 객체는 Proxy.newProxyInstance() 를 통해서만 만들 수 있다.


스프링은 미리 정의된 클래스 정보를 가지고 디폴트 생성자를 통해 오브젝트를 만드는 방법 외에, 빈을 만들 수 있는 여러가지 방법을 제공하는데 그 중 하나가 팩토리 빈 을 이용한 빈 생성 방법을 들 수 있다.

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

팩토리 빈을 만드는 방법은 여러가지가 있는데 다음과 같은 FactoryBean 인터페이스를 구현하는 것이다.

public interface FactoryBean<T> {
  T getObject() throws Exception; // 빈 객체를 생성해서 리턴
  Class<? extends T> getObjectType(); // 생성되는 객체 타입을 리턴
  boolean isSingleton(); // getObject가 리턴하는 객체가 싱글톤인지 여부
}

위의 인터페이스를 구현한 클래스를 스프링 빈으로 등록하면 팩토리 빈으로 동작한다. 스프링 빈으로 등록하기 힘든 클래스에 대해서는 FactoryBean을 활용하여 빈 객체를 생성할 수 있다.

import org.springframework.beans.factory.FactoryBean;

public class MessageFactoryBean implements FactoryBean<Message> {

  private String text;

  // 객체 생성시 필요한 정보를 팩토리 빈의 필드로 설정하여 DI 받을 수 있도록 한다.
  // 주입된 정보는 실제 사용하고자 하는 클래스의 객체를 생성할 때 사용한다.
  public void setText(String text) {
    this.text = text;
  }

  // 실제 빈으로 사용될 객체를 직접 생성한다. 코드를 이용하므로 복잡한 초기화 방식을 가진 객체도 생성할 수 있다.
  public Message getObject() throws Exception {
    return Message.newMessage(this.text);
  }

  // 빈으로 사용되는 객체 타입을 리턴한다.
  public Class<? extends Message> getObjectType() {
    return Message.class;
  }

  // getObject 메소드가 돌려주는 오브젝트가 싱글톤인지 알려준다.
  public boolean isSingleton() {
    return false;
  }
}
<bean id="message" class="ch06.springbook.factorybean.MessageFactoryBean">
    <property name="text" value="Factory Bean" />
</bean>
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class FactoryBeanTest {

  @Autowired
  private ApplicationContext applicationContext;

  @Test
  public void getMessageFromFactoryBeanTest() {
    Object message = applicationContext.getBean("message"); // "message" 빈 타입이 Message.class 이다.
    assertThat(Message.class.isInstance(message), is(true));
    assertThat(((Message)message).getText(), is("Factory Bean"));
  }

  @Test
  public void getFactoryBeanTest() {
    Object factory = applicationContext.getBean("&message"); // &을 붙이면 팩토리 빈을 가져온다.
    assertThat(MessageFactoryBean.class.isInstance(factory), is(true));
  }
}

위와 같이 FactoryBean을 구현한 클래스가 빈 클래스로 지정되면, 팩토리 빈 객체의 getObject 메소드를 통해 실제 사용할 객체를 가져와서 이를 빈으로 등록한다.

스프링은 Private 생성자를 가진 클래스도 빈으로 등록하면 리플렉션을 통해 객체를 만들어주기는 한다. 그러나 Private 생성자를 가진 클래스를 빈으로 등록하는 일은 권장되지 않으며 바르게 동작하지 않을 수 있다. 따라서 이 팩토리 빈을 활용하여 빈으로 등록하는 것이다.

FactoryBean example


미리 클래스가 정의되지 않아서 일반적인 방법으로는 스프링 빈으로 등록할 수 없는 다이내믹 프록시 객체를 팩토리 빈을 활용하면 스프링 빈으로 만들어줄 수 있다. 팩토리 빈의 getObject 메소드에 다이내믹 프록시 객체 생성 코드를 넣으면 된다.


10.png

Dynamic Proxy using FactoryBean

  • 프록시 팩토리 빈의 한계
    • 한 클래스의 여러 메소드에 InvocationHandler를 써서 한 번에 부가기능을 제공할 수는 있는데 여러 개의 클래스에 부가기능을 추가하는 것은 불가능하다. 각 타깃 인터페이스를 구현하는 프록시를 만들기 위해 각각 팩토리 빈을 추가해줘야 한다.
      • 부가기능을 수행하고 타깃에 위임하는 InvocationHandler 객체도 프록시 개수만큼 만들어진다.
    • 하나의 타깃에 여러 개의 부가기능을 추가할 경우, 각 부가기능을 담당하는 프록시를 만들기 위해 각각 팩토리 빈을 추가해줘야 한다.


스프링의 프록시 팩토리 빈

스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다. 스프링의 ProxyFactoryBean 은 프록시를 생성해서 빈 객체로 등록하게 해주는 팩토리 빈이다. 이 팩토리 빈은 순수하게 프록시를 생성하는 작업만 담당하고 제공해줄 부가기능은 별도의 빈에 둘 수 있다.

부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다. InvocationHandler 와 다른 점은 MethodInterceptor의 invoke 메소드에서는 타깃 객체 정보까지도 함께 받는다. 따라서 MethodInterceptor를 구현한 클래스는 타깃 객체에 상관없이 독립적으로 만들 수 있으며 여러 프록시에서 함께 사용할 수 있어서 싱글톤 빈으로 등록 가능하다.

InvocationHandler는 타깃 객체에 대한 참조를 가지고 있어야 한다.

static class UppercaseAdvice implements MethodInterceptor {
  public Object invoke(MethodInvocation invocation) throws Throwable {
    // 타깃 객체를 가지고 있을 필요가 없다. 
    // MethodInvocation에는 메소드 정보와 함께 타깃 객체를 알고 있다.
    String ret = (String)invocation.proceed();
    return ret.toUpperCase();
  }
}

@Test
public void proxyFactoryBeanTest() {
  ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
  proxyFactoryBean.setTarget(new HelloTarget());    // 타깃 설정
  proxyFactoryBean.addAdvice(new UppercaseAdvice());  // 부가기능을 담은 어드바이스를 추가, 여러 개 추가가능

  Hello proxyHello = (Hello)proxyFactoryBean.getObject(); //getObject를 통해 프록시를 가져온다.
  assertThat(proxyHello.sayHello("Toby"), is("HELLO TOBY"));
  assertThat(proxyHello.sayHi("Toby"), is("HI TOBY"));
  assertThat(proxyHello.sayThankYou("Toby"), is("THANK YOU TOBY"));
}

ProxyFactoryBean example


어드바이스

MethodInterceptor 를 구현한 클래스에는 타깃 객체가 등장하지 않는다. 파라미터로 받는 MethodInvocation에 타깃 객체의 메소드를 실행할 수 있는 기능이 있어서, MethodInterceptor는 부가기능을 구현하는데만 신경쓰면 된다.

즉, MethodInterceptor는 템플릿, MethodInvocation은 콜백처럼 동작하는 것이다.

또한 ProxyFactoryBean 하나로 MethodInterceptor를 구현한 객체를 타깃에 한 번에 여러 개 추가할 수 있다.

ProxyFactoryBean은 인터페이스 자동 검출 기능이 있어 타깃 객체가 구현하는 인터페이스 정보를 자동으로 알 수 있으므로, 프록시를 직접 만들거나 JDK 다이내믹 프록시를 만들 때처럼 인터페이스 정보를 별도로 제공할 필요없다.

스프링에서는 MethodInterceptor 처럼 부가기능을 담은 객체를 어드바이스(advice) 라고 한다.


포인트컷


11.png

InvocationHandler를 구현할 때는 메소드의 이름을 가지고 부가기능을 적용할 대상 메소드를 선정하는 것이 있었는데, 이로 인해 InvocationHandler 객체를 여러 프록시에서 공유할 수가 없었다. 타깃과 메소드 선정하는 코드 자체는 분리할 수는 있지만 한번 빈으로 등록된 InvocationHandler 객체는 특정 타깃을 위한 프록시에 제한되기 때문이다.

부가기능을 적용할 메소드를 선정하는 코드를 MethodInterceptor에 넣는 것은 불가능하다. MethodInterceptor 객체는 타깃 정보를 갖지 않고 프록시들 간에 공유될 수 있는 싱글톤 빈으로서 동작할 수 있다. 즉, 특정 프록시에만 적용되는 메소드 선정 코드를 넣으면 문제가 된다.

프록시가 클라이언트로 받은 요청을 일일히 MethodInterceptor를 통해 타깃으로 전달할 필요는 없다. MethodInterceptor는 순수하게 타깃의 부가기능에만 관심을 가지도록 남겨둔다.

프록시는 타깃을 대신해서 클라이언트 요청을 받는 것만 책임을 지도록 하고, 메소드를 선별하는 기능 자체는 또 분리하여 프록시가 이를 활용하여 메소드를 선정할 수 있도록 한다.


ProxyFactoryBean을 활용한 방법은 부가기능과 메소드 선정 알고리즘을 활용하는 유연한 구조를 제공한다.


12.png

스프링에서 메소드 선별 코드를 담은 객체를 포인트컷(pointcut) 이라고 한다. 어드바이스와 포인트컷은 모두 프록시에 DI로 주입되어 사용된다.

프록시는 클라이언트로부터 요청받으면 먼저 포인트컷을 통해 부가기능을 부여할 메소드인지 판별한다. 적용해야 된다면 어드바이스를 호출한다. 어드바이스는 직접 타깃의 메소드를 호출하지 않으며, MethodInvocation 타입의 오브젝트에 있는 proceed 메소드를 호출하기만 하는 전형적인 템플릿/콜백 방식으로 동작한다.

프록시로부터 어드바이스(부가기능)와 포인트컷(메소드 선정)을 독립시키고 DI를 사용한 것은 전략패턴 구조이다. 따라서 여러 프록시가 공유해서 사용가능하고, 변경이 일어나면 구현 클래스만 바꾸면 된다.

@Test
public void pointcutAdvisorTest() {
  ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
  proxyFactoryBean.setTarget(new HelloTarget());  // 타깃 선정

  // 메소드 이름을 비교하여 대상을 선정하는 포인트컷
  NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
  pointcut.setMappedName("sayH*");

  // 포인트컷과 어드바이스를 advisor로 묶어 한 번에 추가
  proxyFactoryBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));

  Hello proxyHello = (Hello)proxyFactoryBean.getObject();
  assertThat(proxyHello.sayHello("Toby"), is("HELLO TOBY"));
  assertThat(proxyHello.sayHi("Toby"), is("HI TOBY"));
  assertThat(proxyHello.sayThankYou("Toby"), is("Thank You Toby")); // 포인트컷에 의해 매치가 안된다.
}

ProxyFactoryBean with pointcut

위의 코드에서 Advisor 타입의 객체로 포인트컷과 어드바이스를 한 번에 추가한 이유는 ProxyFactoryBean에 여러 포인트컷 및 어드바이스를 추가할 수 있기 때문이다. 이와 같이 포인트컷과 어드바이스를 묶은 오브젝트를 어드바이저 라고 한다.

여러 개의 어드바이스가 등록되더라도 각 다른 포인트컷과 조합할 수 있으므로 서로 다른 메소드 선정 방식을 쓸 수 있다.

public class TransactionAdvice implements MethodInterceptor {

  private PlatformTransactionManager transactionManager;

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

  // 타깃을 호출하는 기능을 가진 MethodInvocation을 프록시로부터 받는다. 메소드 호출 전후로 부가기능을 추가할 수 있다.
  public Object invoke(MethodInvocation invocation) throws Throwable {

    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());

    try {
      Object ret = invocation.proceed();
      transactionManager.commit(status);
      return ret;
    // JDK의 다이내믹 프록시와는 다르게 예외가 포장되지 않고 타깃에서 보낸 예외가 그대로 전달된다.
    } catch (RuntimeException e) {
      transactionManager.rollback(status);
      throw e;
    }
  }
}

여기서 JDK 다이내믹 프록시와는 다르게 타깃 정보를 알 필요도 없고, 콜백 패턴을 통해 타깃에 위임할 수 있다.

Transaction code with ProxyFactoryBean


ProxyFactoryBean 은 스프링의 DI와 템플릿/콜백, 서비스 추상화 등의 기법이 모두 적용된 것이다. 따라서 어드바이스를 여러 프록시가 공유할 수 있게 되었고 포인트컷과 자유롭게 조합이 가능하다. 메소드 선정 방식이 달라지는 경우에는 포인트컷의 설정을 따로 등록하고 어드바이저로 조합해서 적용하면 된다.


13.png


Tags:
Stats:
0 comments