자바, 스프링 개발자들의 종착역이자, 주기적으로 다시 돌아오는 그곳, <토비의 스프링 3.1>을 드디어 읽는다.
한창 개발 걸음마를 막 떼고 스프링으로 아장아장 기어다니며 CRUD를 할 때즈음, 토비의 스프링을 찬양하고 있는 많은 개발자들을 보았고, 역시 지식이 깊어지려면, 책을 통해서 지식을 정리하고 깊이를 다지는 시간이 필요하겠구나, 생각했다. 하지만, 개발을 이제 막 배운 그때의 나에게 토비 책은 너무나도 무시무시했고, 토비책을 읽다가 개발을 포기한 사람들의 증언도 여럿 읽고 나니, 이 책은 개발자로서 레베루업이 필요할 때즈음, 다시 꺼내봐야겠다고 생각했다.
그리고 지금이다. 퇴사 후, 이직을 준비하는 동안, 지식의 깊이를 좀 더 다져놓고 싶다는 생각이 들었고, 집앞 도서관에서 (아무도 빌려가지 않는) 토비님의 책을 빌려서 읽기 시작했다. 처음에는 가볍게 읽기 시작했지만, 읽을 수록 인사이트가 쌓여, 기록하면서 제대로 읽고 싶어졌다. 그래서 나의 세컨 브레인인 이곳 블로그에 짧은 글들로 그 내용을 적어보며, 토비님으로부터 얻은 인사이트를 내것으로 만들어보고자 한다.
스프링 프록시 팩토리 빈
ProxyFactoryBean
publicclassDynamicProxyTest { @TestpublicvoidsimpleProxy() {...//jdk 다이내믹 프록시 생성 Hello proxiedHello = (Hello)Proxy.newProxyInstance(getClass().getClassLoader(),newClass[] { Hello.class},newUppercaseHandler(new HelloTarget()));assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY")); } @TestpublicvoidproxyFactoryBean() {ProxyFactoryBean pfBean =newProxyFactoryBean();pfBean.setTarget(newHelloTarget()); //타깃 설정pfBean.addAdvice(newUppercaseAdvice()); //부가기능을 담은 어드바이스 추가. 여러 개 가능Hello proxiedHello = (Hello) pfBean.getObject(); //factoryBean 이므로 getObject() 로 생성된 프록시 가져온다. assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));assertThat(proxiedHello.sayThankYou("Toby"), is("THANK YOU TOBY")); }staticclassUppercaseAdviceimplementsMethodInterceptor {publicObjectinvoke(MethodInvocation invocation) throwsThrowable {//리플렉션의 Method 와 다르게, 매소드 실행시, 다깃 오브젝트를 전달할 필요가 없다. MethodInvocation 내부에 매소드 정보와 함께 타깃 오브젝트를 알고 있기 떄문이다. String ret = (String)invocation.proceed(); returnret.toUpperCase(); } }}
Advice
타깃이 필요없는 순수한 부가기능
invocationHandler 를 구현했을 때와 다르게 methodInterceptor 를 구현할 때는 타깃 오브젝트가 등장하지 않는다.
methodInvocation 내부에는 메소드 정보 + 타깃 오브젝트 가 담겨있기 때문이다. 이는 타깃 오브젝트의 메소드를 실행할 수 있는 권한이 있기 때문에, 순수하게 부가기능에만 집중할 수 있는 것이다.
일종의 콜백 오브젝트. proceed() 메소드를 실행하면 타깃 오브젝트의 매소드를 내부적으로 실행해주는 기능이 있다.
proceed() 로 타깃 오브젝트의 매소드를 특정하지 않고 실행할 수 있기 때문에, methodInvocation 은 공유가능한 템플릿처럼 사용될 수 있다.
이 점이 JDK 다이나맥 프록시를 직접 사용하는 것 vs. 스프링 제공의 ProxyFactoryBean 을 사용하는 코드의 차이점이자 장점.
마치 jdbtTemplate 이 SQL 의 파라미터에 종속되지 않기 때문에 많은 DAO 가 하나의 JdbcTemplate 오브젝트를 공유해서 사용할 수 있는 것과 마찬가지!
addAdvice()
ProxyFactoryBean 에는 여러개의 부가기능을 담은 MethodInterceptor 들을 쉽게 추가할 수 있다. 별도로 빈을 따로 생성하고 또 등록하지 않아도, proxyFactoryBean 하나로 해결 가능하다.
Advice(메인 인터페이스) <- MethodInterceptor (서브 인터페이스)
MethodInterceptor 처럼 타깃 오브젝트에 적용하는 부가기능을 담은 오브젝트를 스프링에서는 어드바이스라고 부른다.
Hello 인터페이스가 사라지는 점도 주목해야한다.
ProxyFactoryBean 도 setInterface() 를 통해서 직접 인터페이스를 지정해줄 수도 있다.
하지만 내부에서는 인터페이스 자동검출기능을 사용해서 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아내고 이를 모두 구현하는 프록시를 만들어준다. 때문에 별도로 선언해주지 않아도 자동으로 생성되는 것이다.
JDK 다이내믹 프록시 vs. 스프링의 ProxyFactoryBean
JDK 다이내믹 프록시
스프링의 ProxyFactoryBean
InvocationHandler
MethodInterceptor
타깃 명시
타깃 내부의 매소드를 proceed() 라는 메소드 하나로 실행할 수 있음
구현해야할 인터페이스 명시
구현해야할 인터페이스 명시안함
새로운 부가기능 추가시마다 프록시와 프록시 팩토리빈 추가해주어야 함
하나의 빈에 addAdvice()로 계속 부가기능을 추가할 수 있음
포인트컷
기존의 InvocationHandler 의 경우, 부가기능을 부여하는 코드 + 적용할 메소드 선정 알고리즘 이 함께 섞여있는 구조였다.
이렇게 될 경우, 부가기능을 여러 군데에 적용하기 위해서는 메소드 선정 알고리즘을 계속 수정해야하고,
메소드 선정 알고리즘이 수정되면, 부가기능을 부여하는 코드에도 영향이 가는, 전형적인 OCP 를 위반한 구조였다.
이 경우, 부가기능을 부여하는 기능과 적용할 범위를 선정하는 기능을 아예 별도의 빈으로 분리하여 구현하는 편이 좋다. 그래야 적절하게 기능들을 재사용하며, 갈아끼워서 적용할 수도 있기 때문이다.
따라서 스프링의 ProxyFactoryBean 방식의 경우는 부가기능을 제공하는 오브젝트를 어드바이스, 메소드 선정 알고리즘을 담은 오브젝트를 포인트 컷이라고 나누었고, 아래와 같이 동작하도록 만들었다.
두 오브젝트 모두 DI 를 통해 프록시에 주입된다. 그래서 여러 프록시에 공유 될 수 있으며 당연히 싱글톤이다.
프록시가 클라이언트로부터 요청을 받으면, 우선 포인트 컷에게 부가기능을 부여할 메소드인지 아닌지 확인해달라고 요청한다.
포인트 컷이 확인 결과를 프록시에게 전달한다.
프록시는 MethodInterceptor 타입의 어드바이스를 호출한다.
어드바이스는 JDK 다이내믹 프록시의 InvocationHandler 와는 다르게 직접 타깃을 호출하지 않는다. 일종의 템플릿/콜백 구조로 구현되어있기 때문에 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출해주기만 하면 된다.
Invocation 콜백은 실제 위임 대상이되는 타깃 오브젝트의 레퍼런스를 가지고 있고, 타깃 메소드를 직접 호출할 수 있다.
...importorg.springframework.aop.support.NameMatchMethodPointcut;publicclassDynamicProxyTest { @TestpublicvoidpointcutAdvisor() {ProxyFactoryBean pfBean =newProxyFactoryBean();pfBean.setTarget(newHelloTarget());//메소드 이름을 비교해서 대상을 선정하는 알고리즘을 제공하는 포인트 컷을 생성NameMatchMethodPointcut pointcut =newNameMatchMethodPointcut();pointcut.setMappedName("sayH*"); pfBean.addAdvisor(newDefaultPointcutAdvisor(pointcut,new UppercaseAdvice()));Hello proxiedHello = (Hello) pfBean.getObject();assertThat(proxiedHello.sayHello("Toby"), is("HELLO TOBY"));assertThat(proxiedHello.sayHi("Toby"), is("HI TOBY"));assertThat(proxiedHello.sayThankYou("Toby"), is("Thank You Toby")); //마지막 메소드만 이름이 sayH 로 시작하기 않기 때문에 적용이 안된 것을 알 수 있다. }}