지난 글 [https://techforme.tistory.com/33]
지난번 트랜잭션 테스트를 위해 의도적으로 예외를 던지는 테스트 스텁을 Proxy 패턴, Decorator 패턴을 통해 구현하였다. 이 때 프록시가 적용되는 대상의 interface 를 구현 하거나 구현체를 상속하는 방식으로 프록시 객체를 생성하였는데, 상당히 번거로운 작업이었다. 만약 인터페이스의 구현체가 아니라면 해당 작업은 더욱 번거로워질 것이다.
이번 포스팅에서는 자동으로 프록시 객체를 생성하여 스프링 빈으로 등록해주는 ProxyFactoryBean 을 사용하여 이 문제를 해결해 볼 것이다.
개념 정리
FactoryBean
우선 FactoryBean 에 대한 설명이 선행되어야 할 것 같다. FactoryBean은 Bean 을 추가하는 한가지 방법으로 이해할 수 있다. 보통 Bean 을 등록 할 때는 대상 Bean 의 인터페이스나 클래스를 직접 등록해준다. 그러나 이 방법 외에도 Bean 을 추가하는 다른 방법이 있는다. Bean 을 직접 추가하는 대신, Bean 을 생성해주는 클래스인 FactoryBean 을 구현하여 추가해주는 것이다. FactoryBean 은 FactoryBean 의 특정 메서드를 통해 생성된 객체를 Bean 으로 추가해준다. (getObject, getObjectType, isSingleton 이라는 세가지 메서드를 구현해야 한다.)
ProxyFactoryBean
FactoryBean 중, Proxy 객체를 전문적으로(?) 생성해주는 FactoryBean 이 있는데, 이것이 바로 ProxyFactoryBean 이다. ProxyFactoryBean 은 다이내믹 프록시를 이용하여 프록시를 자동으로 생성해주는데, 다이내믹 프록시는 리플랙션 api 를 이용하여 타겟이 되는 클래스 정보를 로드하고, 클래스의 각 메소드에 원하는 부가기능을 추가한 다음(Decorate), 지정된 클래스 로더를 통해 다시 객체를 재조립하는 방식으로 동작한다.
이전 글에서 트랜젝션 테스트를 위해 프록시 객체를 만들고, 특정 메서드에 원하는 기능(예외 던지기)을 추가해주는 테스트 스텁을 작성하였는데, ProxyFactoryBean 또한 이와 동일한 일을 하는 셈이다. 프록시 객체를 만드는 과정을 다시 이야기 해보자면, <인터페이스로 추상화>하고, <해당 인터페이스를 구현 또는 대상 구현체를 상속>하여, <특정 메서드>에 <원하는 부가기능>을 넣는다. 라고 할 수 있다. 여기서 변하지 않는 것과 변하는 것이 있다면, 프록시 객체를 만드는 것(첫 두가지)은 변하지 않은 것이고, 어떤 부가기능을 어떤 메소드에 추가할 지가 변하는 것이라고 할 수 있다. ProxyFactoryBean 은 여기서 변하지 않는 두가지 일을 자동으로 처리해준다.
정리하자면 ProxyFactoryBean 은 1. 어떤 메소드에 2. 어떤 부가기능을 부여할지 만 정해주면 자동으로 프록시를 생성한다.
Pointcut, Advice, Advisor
변하는 것 두 가지가 각각 1. Pointcut 과 2. Advice 이다. Pointcut 은 어떤 메서드에 부가기능을 적용할지를 정하며, Advice 는 어떤 부가기능을 부여할지를 정해준다. 이 둘을 묶어 Advisor 라고 한다.
위의 개념을 이용하면 인터페이스를 직접 구현하지 않고도 Proxy, Decorator 패턴이 적용된 테스트 스텁을 생성할 수 있다.
프로젝트에 적용
자동으로 테스트 프록시를 생성하는 코드
@Configuration
@Profile("TestProxyBeanConfig")
public class TestProxyBeanConfig {
@Bean
@Primary
public ProxyFactoryBean testSubscribeRepository(RedisSubscribeRepository redisSubscribeRepository) {
ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
proxyFactoryBean.setTarget(redisSubscribeRepository);
proxyFactoryBean.addAdvisor(testAdvisor());
return proxyFactoryBean;
}
private DefaultPointcutAdvisor testAdvisor(){
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.addMethodName("deleteSubscriptionIdBySessionId");
pointcut.addMethodName("saveSubscriptionIdBySessionId");
pointcut.addMethodName("deleteSessionId");
advisor.setPointcut(pointcut);
advisor.setAdvice((MethodInterceptor) invocation -> {
switch(invocation.getMethod().getName()){
case "saveSubscriptionIdBySessionId":
if(String.valueOf(invocation.getArguments()[1]).equals("subscribe")) throw new TestException();
break;
case "deleteSubscriptionIdBySessionId":
if(String.valueOf(invocation.getArguments()[1]).equals("unsubscribe")) throw new TestException();
break;
case "deleteSessionId":
if(String.valueOf(invocation.getArguments()[0]).equals("disconnect")) throw new TestException();
break;
}
return invocation.proceed();
});
return advisor;
}
}
ProxyFactory 를 빈으로 등록하여 RedisSubscribeRepository 에 대한 프록시 객체를 생성함으로써 테스트 스텁을 대체하였다. 번거로운 인터페이스의 구현이나 상속 없이, ProxyFactory 에 프록시 객체를 넣어줄 타겟을 지정하고(setTarget) 어떤 메서드에 어떤 부가기능을 넣을 것인지를 지정하는 advisor 를 추가해주면 완성이다.(addAdvisor)
target 은 RedisSubscribeRepository 로 지정하고 Pointcut 은 NameMatchMethodPointcut 을 이용하여 예외를 반환할 메서드 명을 추가하였으며, advice 는 MethodInterceptor 를 익명 클래스로 생성하여 예외로직을 구현하였다. 각각의 사용법은 생략...
이제 번거롭게 인터페이스를 구현한다거나 프록시 대상 객체를 상속하지 않아도 프록시를 생성할 수 있게 되었다. 그런데 코드의 수도 별로 줄지 않았기도 하고 무엇보다 확장에 열려있는가? 하면 쉽게 대답을 하지 못하겠다. 왜냐하면 트랜잭션 테스트가 필요한 일이 또 생겼을 경우에는 또 다시 타겟을 정해주고, 메서드명을 추가해주고, 예외 반환 로직을 또 작성 해야하기 때문이다.
여기서 한 발 더 나아가서 이렇게 부가기능을 넣어줄 클래스와 메서드를 맵핑시켜주는 번거로운 작업 마저 없앨 수는 없을까? 이제 본격적으로 AOP 가 등장해야 하는데, 이건 또 다음 포스팅에 써야겠다.