Dynamic Proxy
Dynamic Proxy
Proxy class는 기존 코드에 영향을 주지 않으면서 타깃의 기능을 확장하거나 , 접근 방법을 제어할 수 있는 유용한 방법임에도 불구하고 ,
다음과 같은 단점을 가지고 있다.- 매번 새로운 Proxy class를 만들어야 한다.
- 모든 Method를 일일히 구현해서 타겟 객체에게 위임해주어야 한다.
- 부가 기능 코드가 중복될 가능성이 존재한다. 즉 부가 기능역할을 하는 코드가 여러 메소드에서 사용된다면 중복코드가 계속 들어갈 것이다.
아래와 같은 인터페이스가 존재한다고 가정하자.
1
2
3
4
5
6
7
8public interface Hello {
String sayHello(String name);
String sayHi(String name);
String sayThank(String name);
}구체 class는 다음과 같다.
1 | public class HelloTarget implements Hello{ |
구체 class에 대한 Proxy 클래스는 구체 class와 동일한 인터페이스를 구현하고 , 부가기능 적용후 , 구체 클래스에게 위임한다.
1 | public class HelloUppercase implements Hello{ |
위처럼 부가기능이 모든 method에 대해 중복되서 나타는 문제점이 나타나는 것을 확인할 수 있다.
Dynamic Proxy를 통해 이런 문제를 해결할 수 있다. Dynamic Proxy란 ProxyFactory에 의해서 Reflection을 통해 런타임에 생성되는 Proxy객체를 말한다.
JDK Dynamic Proxy
JDK Dynamic Proxy는 타겟클래스가 구현한 인터페이스와 동일한 타입으로 만들어지는데,
ProxyFactory
에게 인터페이스 정보만 제공해주면 해당 인터페이스를 구현한 클래스의 객체를 자동으로 만들어준다.물론 부가 기능 코드는 직접 작성해야되는데, 이는 Proxy 객체와는 독립적으로 InvocationHandler 인터페이스를 구현한 오브젝트에 포함된다.
1 |
|
- Dynamic Proxy 객체는 클라이언트의 모든 요청을 Reflection으로 변환해서 InvocationHandler 구현 객체의 invoke 메소드로 넘긴다.
즉 다음과 같은 실행 흐름을 가진다.
1 | - 프록시 생성 |
- 위와 같은 실행흐름을 가지기 떄문에 모든 method 호출은 InvocationHandler.invoke()를 거치게된다. 즉 중복코드를 제거할 수 있다.
InvocationHandler를 앞서 만든 Proxy Class의 부가기능 코드에 적용해보면 다음과 같다.
1 | public class UppercaseHandler implements InvocationHandler { |
이제 실제로 Dynamic Proxy 를 생성해주는 코드를 보면 Proxy의 정적 팩토리 메소드를 통해 생성할 수 있다.
1 | Hello proxy(){ |
Dynamic Proxy의 장점
인터페이스의 메소드가 늘어나도 , 클래스로 직접 구현한 프록시와 다르게 수정이 일어나지 않는다.
부가 기능 코드는 InvocationHandler 구현체에 들어있어서 , 타겟의 종류와 상관없이 적용가능하다.
꼭 특정타입의 타겟이 아니라 , 다른 종류의 타겟에도 적용이 가능하다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class UppercaseHandler implements InvocationHandler {
private final Object target; // 특정 타입이 아니라 , Object 타입
public UppercaseHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object returnValue = method.invoke(target, args);
if (returnValue instanceof String){
return ((String) returnValue).toUpperCase();
}
return returnValue;
}
}
Dynamic Proxy 객체를 Spring Bean으로 어떻게 등록해야할까?
- Spring 은 지정된 클래스 이름을 가지고 , Reflection을 통해서 해당 클래스의 객체를 생성한다.
1 | Date now = (Date) Class.forName("java.util.Date").newInstance(); |
반면 Dynamic Proxy 객체 생성 방식은 Proxy 클래스의 newProxyInstance 정적 팩토리 메소드에 의해 생성되며 , 클래스 자체도 런타임에 결정된다. Spring은 어떤 클래스 타입을 해당 Proxy객체가 가질지 , 컴파일 타임에는 알수가 없다.
Factory Bean 을 통해 Dynamic Proxy를 Spring Bean으로 등록한다.
부제 그대로 Spring은 Factory Bean을 통해 Dynamic Proxy 를 Spring Bean으로 등록한다.
Factory Bean이란 Spring을 대신해서 객체의 생성 로직을 담당하도록 만들어진 빈을 말하며 , Spring이 제공해주는 FactoryBean 인터페이스를 구현함으로서 만들 수 있다.
1 | package org.springframework.beans.factory; |
FactoryBean 인터페이스는 3가지 method로 구성되어있는데 ,
getObject() method에 Spring Bean으로 등록할 객체 생성로직이 들어가고 , 해당 객체 어떤 클래스 타입인지는 getObjectType() method에 들어간다.
isSingleton method는 getObject() method가 반환해주는 객체가 항상 동일한 객체인지 , 즉 싱글톤인지 여부를 명시한다.
아래와 같이 정적 팩토리 메소드를 통해서만 객체 생성을 할 수 있는 경우에 FactoryBean을 활용할 수 있다.
1 | public class Message { |
private 생성자이긴 하지만 Reflection으로 강제로 객체 생성을 하려면 할 수는 있다.
하지만 대부분의 경우 이렇게 private 생성자로 막아두는 경우는 사용하지말라는 이유가 있기 떄문에, 개발자가 열어둔 정적 팩토리 메소드를 통해서 생성하는게 안전하다.
따라서 아래와 같이 FactoryBean을 통해서 의도한 정적팩토리 메소드를 통해 객체 생성 & Spring Bean 등록을 할 수 있다.
1 | public class MessageFactoryBean implements FactoryBean<Message> { |
Factory Bean 설정
- 일반 Bean과 다르게 Factory Bean의 경우 bean class property는 FactoryBean 이지만 , 실제로 반환되는 타입은 getObjectType method에 명시된 타입이다. 즉 위 예제에서는 Message 타입이 반환된다.
1 | <bean id="message" class="MessageFactoryBean"> |
1 | class MessageFactoryBeanTest { |
Proxy Factory Bean 방식의 한계점
- 한번에 여러 개의 클래스에 공통적인 부가 기능을 제공하는 것은 불가능
1 | public class UppercaseHandler implements InvocationHandler { |
- 하나의 타깃에 여러 부가기능을 적용하기 힘들다. 부가 기능 개수만큼 설정이 추가되야 한다.
Spring의 Proxy Factory Bean
ProxyFactoryBean이 생성하는 Proxy에서 사용할 부가 기능은 JDK Dynamic Proxy 와 다르게 MethodInterceptor 인터페이스를 구현해서 만든다.
JDK Dynamic Proxy가 사용하는 InvocationHandler의 invoke method는 타겟 객체에 대한 정보를 제공하지 않으므로 , InvocationHandler 구현체에서 타겟를 주입받아 알고 있어야 한다.
1 | public class UppercaseHandler implements InvocationHandler { |
- 반면에 MethodInterceptor의 invoke method 는 ProxyFactoryBean으로부터 타겟 객체에 대한 정보까지도 함께 제공받는다. 따라서 타겟을 주입받을 필요가 없다.
1 | public class UppercaseAdvice implements MethodInterceptor { |
타겟 정보는 MethodInterceptor 구현체가 아닌 ProxyFactoryBean에서 물고 있다.
MethodInterceptor로는 메소드 정보와 함께 타겟 객체가 담긴 MethodInvocation 객체가 전달된다.
따라서 MethodInvocation 자체만으로도 타겟 객체의 메소드를 실행할 수 있으며 , MethodInterceptor는 타겟이 무엇이든지 순수 부가기능에만 집중할 수 있는 구조다.
1 | public class DynamicProxyTest { |
하나의 ProxyFactoryBean로 여러개의 부가기능을 하나의 타겟에 대해서 적용이 가능하다. addAdivce() method를 통해 MethodInterceptor 구현체를 넣어주기만 하면 된다.
ProxyFactoryBean 코드를 보면 타겟의 인터페이스를 설정하는 부분이 없다. 이 이유는 내부적으로 타겟의 인터페이스를 검출하는 기능을 가지고 있기 떄문인데 , 만약 인터페이스가 없더하더라도 , CGLIB라고 하는 오픈소스 바이트코드 생성 프레임워크를 통해 Proxy를 만들어준다.
Advice
- Spring 에서는
타겟 객체에 종속적이지 않는 순수한 부가기능
을 담은 객체를 Advice라고 부른다. - MethodInterceptor 인터페이스를 구현하는데 Advice라고 부르는 이유는 최상단에는 Advice 인터페이스가 존재하기 떄문이다. 즉 MethodInterceptor도 Advice의 서브인터페이스일뿐이다.
- Spring의 Advice는 템플릿/콜백 구조이다. Advice 자체가 템플릿 기능을 수행하고 , MethodInvocation이라고 하는 타겟 객체의 메소드를 호출시킬수 있는 콜백이 되는 구조이다.
Poincut
- Advice가 어떤 method 에 적용될지에 대한 정보는 Advice에서 처리하게 되면 특정 타겟 객체에 종속적인 구조가 된다. 앞서 Advice는 순수한 부가기능을 담은 코드이고 , 따라서 여러 ProxyFactoryBean에서 공유해서 사용할수 있어야만한다. 근데 만약 Advice에 특정 타겟 객체의 특정 method인지 여부를 판별하는 코드가 들어가면 어떨까?
1 | public class UppercaseAdvice implements MethodInterceptor { |
위 코드는 메소드 선정 방식 , 타겟 객체의 메소드 이름 변경에 모두 영향을 받는다. 즉 변경에 취약하고 , 확장에는 닫혀있는 코드이다. 만약 메소드가 추가된다면 IF문이 또 하나 추가되야할것이다.
- Spring은 OCP 원칙을 잘지키기 위해
메소드 선정 알고리즘을 추상화한 PointCut interface
를 제공한다.
1 | public interface Pointcut { |
- Proxy는 Client에게 요청을 받으면 먼저 PointCut에게 부가기능을 적용할 메소드인지 여부를 판별하고, 판별된 메소드에 한해서 Advice의 부가기능 코드가 적용된다.
Advisor
- 실제 ProxyFactoryBean에 Pointcut을 등록하는 부분을 보면 아래와 같다.
1 |
|
- 확인해보면 setPointCut()과 같은 메소드를 통해 PointCut을 등록하는 게 아니라 , PointCut과 Adivce를 하나로 묶어서 Advisor로 등록하는 것을 볼 수 있다. 왜 이렇게 하나로 묶어서 등록할까?
생각해보면 이유는 간단하다. ProxyFactoryBean에는 여러개의 Advice , 여러개의 PointCut이 추가될 수 있는데 , 어떤 Advice가 어떤 PointCut을 적용시킬지 알 수 없기 떄문에 하나로 묶어서 Advisor로 넣어준다.
즉 Advisor는 Adivce(순수 부가기능 코드) + PointCut(타겟 메소드 선정 알고리즘) 를 묶은 객체이다.