본문 바로가기

Back-End/토비의 스프링3

토비의 스프링 6장(4) - 스프링의 프록시 팩토리 빈

스프링의 프록시 팩토리 빈

이전까지 자바를 이용하여 기존 코드 수정없이 부가기능을 추가해주었다. 이제 스프링에서는 어떠한 방식으로 추가해주는지 살펴보자.

ProxyFactoryBean

자바 JDK에서 제공하는 다이나믹 프록시 외에도 편리하게 프록시를 만들 수 있도록 지원해주는 다양한 기능이 있다. 스프링은 프록시 오브젝트를 생성해주는 기술을 추상화한 팩토리 빈을 제공해준다.

package com.waug.common.currency.service;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * Developer : 김태현
 * Date : 2020/02/01
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class DynamicProxyTest {

    @Test
    public void proxyFactoryBean() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloTarget());
        pfBean.addAdvice(new UppercaseAdvice());

        Hello hello = (Hello) pfBean.getObject(); // proxy 가져오기

        assertThat(hello.sayHello("Ted"), is("HELLO TED"));
    }


    static interface Hello {

        String sayHello(String name);

        String sayHi(String name);

        String sayThankYou(String name);
    }


    static class UppercaseAdvice implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            return invocation.proceed().toString().toUpperCase();
        }
    }


    static class HelloTarget implements Hello {

        @Override
        public String sayHello(String name) {
            return "Hello" + name;
        }


        @Override
        public String sayHi(String name) {
            return "sayHi" + name;
        }


        @Override
        public String sayThankYou(String name) {
            return "sayThankYou" + name;
        }
    }
}

어드바이스 : 타깃이 필요없는 순수한 부가기능

JDK 다이나믹 프록시와의 차이점

  • 타깃 오브젝트가 등장하지 않음 -> 부가기능에만 집중 가능
  • addAdvice() -> 여러 개의 부가기능을 제공해주는 프록시를 만들 수 있음 ( 기존에는 프록시, 프록시팩토리 빈을 추가해줬어야 하는 문제 해결 )
  •  

MethodInvocation은 일종의 콜백 오브젝트로, proceed() 메소드를 실행하면 타킷 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다

  • MethodInvocation 구현 클래스는 일종의 공유 가능한 템플릿처럼 동작하는 것
Advice : MethodInterceptor처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는  어드바이저라고 부른다.

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

MethodInterceptor는 여러 프록시가 공유해서 사용할 수 있다. 그러기 위해서는 해당 오브젝트에 타깃 정보를 갖지 않도록 만들었다. 그 덕분에 싱글톤 빈으로 등록할 수 있다.

 

그렇다면 앞에서 본 메소드 선정 기능은 어디에 넣어야할까?

 

' 함께 두기 곤란한 성격이 다르고 변경 이유와 시점이 다르고, 생성 방식과 의존관계가 다른 코드가 함께 있다면 분리해준다 ' 방식을 적용하자.

  •  
@Test
    public void pointcutAdvisor() {
        ProxyFactoryBean pfBean = new ProxyFactoryBean();
        pfBean.setTarget(new HelloTarget());

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

        pfBean.addAdvisor(new DefaultPointcutAdvisor(pointcut, new UppercaseAdvice()));

        Hello proxiedHello = (Hello) pfBean.getObject();

        assertThat(proxiedHello.sayHello("Ted"), is("HELLO TED"));
    }

Advisor(어드바이즈) = Pointcut(메소드 선정 알고리즘) + Advice(부가기능)

위의 코드를 보면 아래 특징 덕분에 OCP를 지키는 코드가 되었다.

  • 프록시로부터 어드바이스와 포인트컷을 독립시켜서 DI 구조 => 전략 패턴
    ( 프록시 <- 어드바이스,포인트컷(strategy) )