본문 바로가기
Spring&SpringBoot/<토비의 스프링 3.1 Vol1.1>, 이일민

Step4. JDK 팩토리 빈을 추상화하는 스프링 팩토리 빈

by 민휘 2023. 4. 11.

스프링의 프록시 팩토리 빈

지금까지 알아본 프록시 생성 방법은 JDK가 제공하는 방법이다. JDK가 제공하는 프록시 팩토리 빈을 사용하면 InvokeHandler 구현 클래스가 기능 위임과 부가기능 적용 메소드 판별, 부가기능 수행 등 여러 책임을 가진다. 변경의 이유가 여러가지기 때문에 쉽게 변경되고, 확장에도 불리하다. 앞에서 살펴본 바로는 타깃에 따라 부가기능이 달라지면 새로운 InvokeHandler가 필요해서 중복 문제가 발생했다.

 

그렇다면 기능 위임과 부가기능 메소드 판별, 부가기능 수행이라는 책임을 각기 다른 객체에 할당하고, 서로 협력해서 다이나믹 프록시를 생성하도록 만들면 되지 않을까? 특히 부가기능 책임을 가진 객체를 타깃이라는 구현으로부터 분리해서, 다양한 타깃과 협력할 수 있도록 재사용하면 될 것 같다. 그래서 스프링은 이러한 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어, 프록시 오브젝트를 생성하는 기술을 추상화한 팩토리 빈을 제공한다.

 

 

책임을 할당받은 객체의 협력 구조. 스프링 다이나믹 프록시에서 타깃 위임과 부가기능 설정 책임이 분리된 것을 볼 수 있다.

 

 

JDK 프록시 빈 vs 스프링 다이나믹 프록시 빈

두 방법의 객체 협력 구조를 비교해보자.

 

JDK 프록시 빈

  • ProxyFactoryBean 구현체 : 다이나믹 프록시 생성, InvokeHanlder에 필요한 의존성 주입
  • 다이나믹 프록시 : InvocationHandler에서 정의한대로 동작하는 책임
  • InvocationHandler 구현체 : 기능 위임, 부가기능 적용 판별, 부가기능 적용
  • 타깃 : 부가기능을 적용할 최종 오브젝트

스프링의 프록시 빈

  • ProxyFactoryBean : 받아온 정보로 다이나믹 프록시 생성
  • 다이나믹 프록시 : 클라이언트의 요청을 받아서 타깃으로 연결, 중간에 부가기능을 적용하거나 접근이 필요하면 협력 요청
  • 어드바이저 : 부가기능 적용 메소드 판별, 부가기능 코드의 조합
  • 타깃 : 부가기능을 적용할 최종 오브젝트

 

어드바이스, 포인트컷, 어드바이저

이제 부가기능 관련 책임을 할당받은 객체를 알아보자. 부가기능을 제공하는 오브젝트를 어드바이스라고 하고, 메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다. 어드바이스와 포인트컷은 여러 프록시에서 공유되므로 스프링의 싱글톤 빈으로 등록할 수 있다. 여기서 핵심은 어드바이스와 포인트컷으로부터 타깃에 대한 직접적인 의존성을 제거해서 다양한 타깃과 협력할 수 있도록 부가기능과 관련된 순수한 코드만 남겼다는 점이다.

 

어드바이스 : 순수한 부가기능

어드바이스는 MethodInterceptor 인터페이스를 구현하여 만든다. 어드바이스에는 순수한 부가기능 로직만 담아서 여러 타깃이 공유하도록 만든다.

static class UppercaseAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        String ret = (String) invocation.proceed();
        return ret.toUpperCase();
    }
}

MethodInterceptor의 invoke 메소드는 MethodInvocation 타입을 매개변수로 받고, proceed를 호출해 타깃의 메소드를 호출한다. MethodInterceptor에 순수한 부가기능만을 담는다고 했는데, 사실 부가기능을 적용할 때 타깃의 위임을 어느 순서에 실행해야할지는 부가기능 로직이 알고있으므로 타깃 위임 코드를 남겨야한다. 다만 타깃 변경에 영향을 받지 않도록 타깃과 관련된 부분을 분리해서 메소드로 주입받게 하고, 인터페이스를 통해 타깃의 메소드를 호출하도록 요청한다.

 

여기서 MethodInterceptor는 템플릿, MethodInvocation는 콜백 역할을 한다. 템플릿-콜백 패턴의 목적을 떠올려보자. 중복되는 부분을 그렇지 않은 부분으로부터 분리하는 것이었다. 중복되는 부분은 부가기능을 적용하고 기능 위임을 요청하는 부분, 달라지는 부분은 타깃을 결정하고 실제 타깃의 기능을 호출하는 부분이다. 그래서 MethodInterceptor를 구현한 UppercaseAdvice를 살펴보면 타깃의 실행을 우선 요청해서 받아온 결과값을 String으로 받아오고, 부가기능에 해당하는 대문자화를 실행한다. 주입받은 MethodInvocation에는 어떤 타깃의 어떤 메소드로 기능 위임을 할지에 대한 정보가 담겨있으므로, 템플릿의 요청을 받아 실제 기능 위임을 실행할 수 있다.

 

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

메소드 선정 알고리즘을 담은 오브젝트를 포인트컷이라고 부른다. 포인트컷은 어드바이스와 달리 타깃과 관련되는 의존성을 가지지 않으므로 바로 분리 가능하다.

NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedName("sayH*");

 

어드바이저 : 어드바이스 x 포인트컷

어드바이저는 포인트컷과 어드바이스를 하나씩 대응하여 구성한다. 어드바이저는 어떤 포인트컷 조건에 따라 어떤 부가기능을 적용할지 결정하는 하나의 단위이다. 그래서 프록시 빈을 만들 때 타깃 하나에 여러개의 어드바이저를 주입해서 프록시가 부가기능을 재사용할 수 있도록 만든다. 타깃 여러개에 한개의 부가기능도 가능하다.

 

아래 그림을 보면 하나의 부가기능인 TransactionAdvice에 여러개의 포인트컷을 조합해 여러개의 어드바이저를 만들었다. 그리고 타깃을 알고 있는 프록시 팩토리 빈에 여러개의 어드바이저를 주입해서 사용할 수 있다. (그림에는 안나왔는데 빈 → 어드바이저 연결선이 여러개일 수 있다) 따라서 부가기능과 메소드 조건을 담은 객체는 하나만 존재하고, 조합되어 여러 타깃에서 재사용되는 구조를 만들 수 있다.

 

프록시로부터 어드바이스와 포인트컷을 독립시키고 DI를 사용하게 한 것은 전형적인 전략 패턴 구조이다. 프록시가 수행해야하는 책임(부가기능 관련)의 구체적인 구현을 담은 여러개의 구현체를 만들어두고 인터페이스로 감춘다. 그러면 구체적인 부가기능 방식이나 메소드 선정 알고리즘이 바뀌면 구현 클래스만 바꿔서 설정에 넣어주면 된다.

 

 

ProxyFactoryBean 동작 과정 정리

다이나믹 프록시의 생성

다이나믹 프록시를 생성할 때는 어떤 타깃에 대해 어느 메소드 선정 알고리즘을 사용하여 어떤 부가기능을 적용할지 그 정보를 넘겨주어야 한다. 스프링 프록시 팩토리 빈을 사용하면 각 책임을 객체에게 할당했으므로, 포인트컷과 어드바이스에 해당하는 객체를 생성하고 어드바이저로 묶어서 다이나믹 프록시에 주입해주어야한다. 이때 타깃도 함께 주입되어야 한다.

// 부가기능 : 트랜잭션
// 트랜잭션 적용에 필요한 PlatformTransactionManager는 주입받는다
public class TranscationAdvice implements MethodInterceptor {

    PlatformTransactionManager transactionManager;

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

    public Object invoke(MethodInvocation invocation) throws Throwable {
        TransactionStatus status = this.transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
            Object ret = invocation.proceed();
            return ret;
        } catch (RuntimeException e) {
            this.transactionManager.rollback(status);
            throw e;
        }
    }
}
<!-- advice -->
<bean id="transactionAdvice" class="springbook.user.service.TranscationAdvice">
    <property name="transactionManager" ref="transactionManager"/>
</bean>

<!-- pointcut -->
<bean id="transactionPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
    <property name="mappedName" value="upgrade*"/>
</bean>

<!-- advisor -->
<bean id="transactionAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="advice" ref="transactionAdvice"/>
    <property name="pointcut" ref="transactionPointcut"/>
</bean>

<!-- proxyFactoryBean -->
<bean id="userService" class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="**target**" ref="userServiceImpl" />
    <property name="interceptorNames">
        <list>
            <value>**transactionAdvisor**</value>
        </list>
    </property>
</bean>

포인트컷과 어드바이스 빈을 생성해서 주입하고, 이 둘을 묶어서 어드바이저 빈을 생성한다. 최종적으로 프록시 팩토리 빈을 등록할 때 타깃과 생성해두 어드바이저를 묶어서 주입하면 다이나믹 프록시가 만들어진다.

 

 

다이나믹 프록시의 수행

클라이언트가 프록시의 인터페이스로 요청을 보내면 다이나믹 프록시가 그 요청을 먼저 받는다. 요청한 메소드 정보를 포인트컷에 넘기고 부가기능 적용이 가능하지 먼저 검사한다. 적용이 가능하다면 어드바이스에 담긴 부가기능 로직을 실행한다. 아니라면 다이나믹 프록시의 기본 기능 위임을 사용한다.

 

테스트에서는 다음과 같이 컨텍스트로부터 프록시 팩토리 빈을 받아서 다이나믹 프록시를 생성하고 메소드를 호출한다.

@Test
public void upgradeAllOrNothing() throws Exception {
    TestUserService testUserService = new TestUserService(users.get(3).getId());
    testUserService.setUserDao(this.userDao);
    testUserService.setMailSender(this.mailSender);

    **ProxyFactoryBean** txProxyFactoryBean = applicationContext.getBean("&userService", ProxyFactoryBean.class);
    txProxyFactoryBean.**setTarget**(testUserService);
    UserService txUserService = (UserService) **txProxyFactoryBean.getObject();**

    userDao.deleteAll();
    for(User user: users) userDao.add(user);

    try {
        txUserService.upgradeLevels();
        fail("TestUserServiceException expected");
    } catch (TestUserServiceException e) {}

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

 

결과적으로 스프링의 프록시 빈 클래스를 사용했을 때 트랜잭션 코드를 프록시로 사용하면서도 재사용 가능한 구조를 만들었다. 프록시에서 전략 패턴을 사용해 어드바이스와 포인트컷을 분리했고, 프록시와 어드바이스 사이에서 부가 기능 실행 중의 기능 위임은 템플릿 콜백 패턴을 적용했다. 결국 핵심은 여러가지 책임을 코드로 수행하던 InvokeHanlder의 구현체에서 책임을 쪼개 여러 객체에 할당한 것이었다.

 

(사실 인터페이스를 살펴보니 여기에도 분석해보고 싶은게 많은데, 시간이 너무 지체된 관계로 나중에 알아봐야겠다. 다이나믹 프록시로 런타임에 생성된 클래스를 나중에 바이트 코드로 열어서 살펴보고 싶다.)