Study with book/토비의 스프링 3.1

6장. AOP2


6.4 스프링의 프록시 팩토리 빈

6.4.1 ProxyFactoryBean

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.4.1

스프링은 서비스 추상화를 프록시 기술에도 동일하게 적용한다. 따라서 스프링은 일관된 방법으로 프록시를 만들 수 있게 도와주는 추상 레이어를 제공한다. 스프링의 ProxyFactoryBean은 프록시를 생성해서 빈 오브제트로 등록하게 해주는 팩토리 빈이다. 기존에 만들었던  TxProxyFactoryBean과 달리, ProxyFactoryBean은 순수하게 프록시를 생성하는 작업만을 담당하고 프록시를 통해 제공해줄 부가기능은 별도의 빈에 둘 수 있다.  ProxyFactoryBean이 생성하는 프록시에서 사용할 부가기능은 MethodInterceptor 인터페이스를 구현해서 만든다. MethodInterceptor는 InvocationHandler와 비슷하지만 한 가지 다른 점이 있다. InvocationHandler의 invoke() 메소드는 타깃 오브젝트에 대한 정보를 제공하지 않는다. 따라서 타깃은 InvocationHandler를 구현한 클래스가 직접 알고 있어야 한다. 반면에 MethodInterceptor의 invoke() 메소드는 ProxyFactoryBean으로부터 타깃 오브젝트에 대한 정보까지도 함께 제공받는다. 그 차이 덕분에 MethodInterceptor는 타깃 오브젝트에 상관없이 독립적으로 만들어질 수 있다. 따라서 MethodInterceptor 오브젝트는 타깃이 다른 여러 프록시에서 함께 사용할 수 있고 싱글톤 빈으로 등록 가능하다.

 

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

 InvocationHandler를 구현했을 때와 달리 MethodInterceptor로는 메소드 정보와 함께 타깃 오브젝트가 담긴 MethodInvocation 오브젝트가 전달된다. MethodInvocation은 타깃 오브젝트의 메소드를 실행할 수 있는 기능이 있기 때문에 MethodInterceptor는 부가기능을 제공하는 데만 집중할 수 있다. MethodInvocation은 일종의 콜백 오브젝트로  proceed() 메소드를 실행하면 타겟 오브젝트의 메소드를 내부적으로 실행해주는 기능이 있다. 그렇다면 MethodInvocation 구현 클래스는 일종의 공유 가능한 템플릿처럼 동작하는 것이다. 바로 이 점이 JDK의 다이내믹 프록시를 직접 사용하는 코드와 스프링이 제공하는 프록시 추상화 기능인 ProxyFactoryBean을 사용하는 코드의 가장 큰 차이점이자 ProxyFactoryBean의 장점이다. 

 ProxyFactoryBean은 작은 단위의 템플릿/콜백 구조를 응용해서 적용했기 때문에 템플릿 역할을 하는 MethodInterceptor 구현체를 싱글톤으로 두고 공유할 수 있다. 마치 SQL 파라미터 정보에 종속되지 않는 JdbcTemplate이기 때문에 수많은 DAO 메소드가 하나의 JdbcTemplate 오브젝트를 공유할 수 있는 것과 마찬가지다. 

또한 ProxyFactoryBean에는 여러 개의 부가기능을 제공해주는 프록시를 만들 수 있다. 즉 여러 개의 MethodInterceptor 구현체를 ProxyFactoryBean에 추가해줄 수 있다. 따라서 앞에서 살펴봤던 프록시 팩토리 빈의 단점 중의 하나였던, 새로운 부가기능을 추가할 때마다 프록시와 프록시 패토리 빈도 추가해줘야 한다는 문제를 해결할 수 있다.

 

마지막으로 JDK 다이내믹 프록시에서 프록시 오브젝트를 만들 때는 필요했지만 ProxyFactoryBean을 적용한 후에는 없어진 것이 있다. ProxyFactoryBean을 적용한 코드에는 프록시가 구현해야 하는 Hello라는 인터페이스를 제공해주는 부분이 없다. 어떻게 ProxyFactoryBean은 인터페이스 타입을 제공받지 않고 Hello 인터페이스를 구현한 프록시를 만들어낼 수 있을까? 물론 ProxyFactoryBean도 setInterfaces() 메소드를 통해서 구현해야 할 인터페이스를 지정할 수 있다. 하지만 인터페이스를 굳이 알져주지 않아도 ProxyFactoryBean에 있는 인터페이스 자동검출 기능을 사용해 타깃 오브젝트가 구현하고 있는 인터페이스 정보를 알아낸다. 그리고 알아낸 인터페이스를 모두 구현하는 프록시를 만들어준다. ProxyFactoryBean은 기본적으로 JDK가 제공하는 다이내믹 프록시를 만들어준다. 경우에 따라서는 CGLib이라고 하는 오픈소스 바이트코드 생성 프레임워크를 이용해 프록시를 만들기도 한다.

 

스프링은 타깃 클래스가 인터페이스를 구현한 클래스라면, JDK 다이내믹프록시를 이용하고 인터페이스를 구현하지 않은 클래스라면 CGLib 프레임워크를 이용해서 프록시를 생성한다.

 

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

기존에 InvocationHandler를 직접 구현했을 때는 부가기능 적용 외에도 메소드의 이름을 가지고 부가기능을 적용 대상 메소드를 선정하는 것이었다. 그렇다면 ProxyFactoryBean과 MethodInterceptor를 사용하는 방식에서도 메소드 선정 기능을 넣을 수 있을까? MethodInterceptor에서 부가기능을 제공하고 있으니 그 안에서 판별하게 하면 될 것 같지만 실제로는 불가능하다. 앞에서 살펴봤듯이 MethodInterceptor 오브젝트는 여러 프록시가 공유해서 사용할 수 있다. 그러기 위해서 MethodInterceptor 오브젝트는 타깃 정보를 갖고 있지 않도록 만들었다. 그 덕분에 MethodInterceptor를 스프링의 싱글톤 빈으로 등록할 수 있었다. 그런데 여기에다 트랜잭션 적용 대상 메소드 이름 패턴을 넣어주는 것은 곤란하다. 트랜잭션 적용 메소드 패턴은 프록시마다 다를 수 있기 때문에 여러 프록시가 공유하는 MethodInterceptor에 특정 프록시에만 적용되는 패턴을 넣으면 문제가 된다. 어떻게 해야할까? 함께 두기 곤란한 성격이 다르고 변경 이유와 시점이 다르고, 생성 방식과 의존관계가 다른 코드가 함께 있다면 분리해주면 된다.

MethodInterceptor는 InvocationHandler와는 다르게 프록시가 클라이언트로부터 받는 요청을 일일이 전달받을 필요는 없다. MethodInterceptor에는 재사용 가능한 순수한 부가기능 제공 코드만 남겨주는 것이다. 대신 프록시에 부가기능 적용 메소드를 선택하는 기능을 넣자. 물론 프록시의 핵심 가치는 타깃을 대신해서 클라이언트의 요청을 받아 처리하는 오브젝트로서의 존재 자체이므로, 메소드를 선별하는 기능은 프록시로부터 다시 분리하는 편이 낫다. 메소드를 선정하는 일도 일종의 교환 가능한 알고리즘이므로 전략 패턴을 적용할 수 있기 때문이다.

기존 JDK 다이내믹 프록시를 이용한 방식

하지만 문제는 부가기능을 가진 InvocationHandler가 타깃과 메소드 선정 알고리즘 코드에 의존하고 있다는 점이다. 만약 타깃이 다르고 메소드 선정 방식이 다르다면 InvocationHandler 오브젝트를 여러 프록시가 공유할 수 없다. 타깃과 메소드 선정 알고리즘은 DI를 통해 분리할 수는 있지만 한번 빈으로 구성된 InvocationHandler 오브젝트는 오브젝트 차원에서 특정 타깃을 위한 프록시에 제한된다는 뜻이다. 그래서 InvocationHandler는 굳이 따로 빈으로 등록하는 대신 TxProxyFactoryBean 내부에서 매번 생성하도록 만들었던 것이다.

 

반면에 스프링의 ProxyFactoryBean 방식은 두 가지 확장 기능인 부가기능(Advice)과 메소드 선정 알고리즘(Pointcut)을 활용하는 유연한 구조를 제공한다.

스프링의 ProxyFactoryBean을 이용한 방식

스프링은 부가기능을 제공하는 오브젝트어드바이스라고 부르고, 메소드 선정 알고리즘을 담은 오브젝트포인트컷이라고 부른다. 어드바이스와 포인트컷은 모두 프록시에 DI로 주입돼서 사용된다. 두 가지 모두 여러 프록시에서 공유가 가능하도록 만들어지기 때문에 싱글톤 빈으로 등록이 가능하다.

 1) 프록시는 클라이언트로부터 요청을 받으면 먼저 포인트컷에게 부가기능을 부여할 메소드인지 확인한다.

 2) 프록시는 포인트컷으로부터 부가기능을 적용할 대상 메소드인지 확인받으면, MethodInterceptor 타입의 어드바이스를 호출한다.

 

 여기서 중요한 점은 JDK의 다이내믹 프록시의 InvocationHandler와 달리 MethodInterceptor는 타깃을 직접 호출하지 않는 다는 것이다. 자신은 여러 타깃에 공유되어야하므로 타깃 정보라는 상태를 가질 수 없다. 따라서 타깃에 직접 의존하지 않도록 일종의 템플릿 구조로 설계되어 잇다. 어드바이스가 부가기능을 부여하는 중에 타깃 메소드의 호출이 필요하면 프록시로부터 전달받은 MethodInvocation 타입 콜백 오브젝트의 proceed() 메소드를 호출해주기만 하면 된다.

 실제 위임 대상인 타깃 오브젝트의 레퍼런스를 갖고 있고, 이를 이용해 타깃 메소드를 직접 호출하는 것은 프록시가 메소드 호출에 따라 만드는 MethodInvocation 콜백의 역할이다. 재사용 가능한 기능을 만들어두고 바뀌는 부분(콜백 오브젝트 - 메소드 호출정보)만 외부에서 주입해서 이를 작업 흐름(부가기능) 중에 사용하도록 하는 전형적인 템플릿/콜백 구조이다.

 

6.4.2 ProxyFactoryBean 적용

실습 코드 링크 : https://github.com/vvshinevv/toby-spring/tree/feature/6.4.2


[참고자료]

토비 스프링 vol.1 - 이일민

'Study with book > 토비의 스프링 3.1' 카테고리의 다른 글

6장. AOP4  (0) 2020.04.26
6장. AOP3  (0) 2020.04.26
6장. AOP1  (0) 2020.04.12
5장. 서비스 추상화  (0) 2020.04.05
4장. 예외  (0) 2020.03.29