[디자인 패턴] Proxy Pattern (JDK Dynamic Proxy / CGLIB)

종현·2023년 12월 6일
0

디자인패턴

목록 보기
1/2

Proxy Pattern

프록시 패턴(Proxy Pattern)은 원본(Target) 객체 대신 로직의 흐름을 제어할 수 있는 패턴을 말한다.
Client가 원본 객체를 호출할 때, 직접 대상을 호출하는 것이 아니라 중간에 Proxy 객체를 거쳐서 실행되는 것을 의미한다.


Proxy Pattern의 사용처

  • 지연 초기화
  • 캐싱
  • 로깅
  • 유효성 체크
  • 등등....

Proxy Pattern 장단점

- 장점

  • OCP : 기존(Target) 코드의 변경 없이 새로운 기능을 추가할 수 있다.
  • SRP : 기존 코드가 해야 하는 일을 유지할 수 있다.
  • 유연성 향상 : 객체 간의 결합도를 줄일 수 있다.

- 단점

  • 성능 저하 : 객체 생성을 한 단계 더 거치므로 빈번한 객체 생성이 필요한 경우에는 성능저하가 발생할 수 있다.
  • 가독성 : Client -> Proxy -> Target 과 같은 구조로 진행이 되어 파악하기가 어려울 수 있다.

Proxy Pattern 구현

  • Proxy : Proxy용 Interface
public interface Proxy {
    void execute();
}

  • TargetClass : 원본 Target 객체
public class TargetClass implements Proxy {

    @Override
    public void execute() {
        System.out.println("원본 객체 실행");
    }
}
  • ProxyClass : 프록시 객체
public class ProxyClass implements Proxy {

    private Proxy target;

    public ProxyClass(Proxy target) {
        this.target = target;
    }

    @Override
    public void execute() {
        System.out.println("프록시 객체 실행");
        this.target.execute();
        System.out.println("프록시 객체 종료");
    }
}
  • 실행
public static void main(String[] args) {
	Proxy target = new TargetClass();		// 원본객체
	Proxy proxy = new ProxyClass(target);	// 프록시 객체
	proxy.execute();						// 실행
}
-- 작업결과 --
프록시 객체 실행
원본 객체 실행
프록시 객체 종료

Dynamic Proxy

  • 프록시 패턴을 사용하면 대상 클래스의 수만큼 프록시 클래스를 만들어야 한다는 단점이 존재.
  • 동적 프록시 기법으로는 JDK Dynamic Proxy / CGLIB와 같은 기술이 존재.
  • Runtime에 동적으로 프록시 생성

JDK Dynamic Proxy

  • 자바의 reflection을 통해 동적 프록시 기법을 이용하여 해결 가능.
  • 리플렉션을 사용하기 때문에 성능상 이슈 존재.
  • 인터페이스 기반.
  • Proxy 객체는 InvocationHandler 인터페이스를 구현해서 작성.
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;

Object : 프록시 객체
Method : 호출한 메서드
Object[] : 인수

  • Proxy 생성은 Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 를 이용하여 생성 가능.
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h) {
    Objects.requireNonNull(h);

    final Class<?> caller = System.getSecurityManager() == null
            ? null
            : Reflection.getCallerClass();

    /*
     * Look up or generate the designated proxy class and its constructor.
     */
    Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);

    return newProxyInstance(caller, cons, h);
}

ClassLoader : 클래스 로더 지정
Class<?>[] : 프록시 객체가 구현해야 할 인터페이스를 나타내는 배열
InvocationHandler : Proxy Handler

JDK Dynamic Proxy 구현

  • JdkDynamicProxy : Proxy용 Interface
public interface JdkDynamicProxy {
    void execute();
}
  • TargetClass : 원본 Target 객체
public class TargetClass implements JdkDynamicProxy {

    @Override
    public void execute() {
        System.out.println("원본 객체 실행");
    }
}
  • ProxyClass : 프록시 객체
public class ProxyClass implements InvocationHandler {

    private final JdkDynamicProxy target;

    public ProxyClass(JdkDynamicProxy target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("프록시 객체 실행");
        Object result = method.invoke(target, args);
        System.out.println("프록시 객체 종료");
        return result;
    }
}
  • 실행
public static void main(String[] args) {
    JdkDynamicProxy target = new TargetClass();
    ProxyClass handler = new ProxyClass(target);
    JdkDynamicProxy proxy = (JdkDynamicProxy) Proxy.newProxyInstance(JdkDynamicProxy.class.getClassLoader(),
            new Class[]{JdkDynamicProxy.class},
            handler
    );

    System.out.println(proxy.getClass());
    proxy.execute();
}
-- 작업결과 --
class com.sun.proxy.$Proxy0
프록시 객체 실행
원본 객체 실행
프록시 객체 종료
  • JDK 동적 프록시의 클래스가 com.sun.proxy.$Proxy0 와 같이 정상적으로 적용된 것을 확인할 수 있다.

CGLIB

  • JDK Dynamic Proxy는 인터페이스 기반이다.
  • 클래스에 동적 프록시를 적용하고 싶을 경우에는??
  • CGLIBEnhancer 의존성을 추가하여 사용할 수 있다.
  • 코드 생성 라이브러리로 런타임에 동적으로 프록시를 생성해주는 기능을 제공.
  • 리플렉션이 아닌 바이트코드를 조작한다. (리플렉션을 사용하는 JDK 동적 프록시보다 성능이 좋다)
  • CGLIB는 Target을 상속받아 Proxy 객체를 생성한다.
  • Default 생성자가 필요하다
  • 클래스가 final일 경우 생성불가능.
  • Target의 생성자가 두 번 호출된다.

CGLIB 구현

  • 의존성 추가 : spring aop 의존성을 받아서 처리..
implementation 'org.springframework.boot:spring-boot-starter-aop'
  • TargetClass : 원본 Target 객체
public class TargetClass {
    public void execute() {
        System.out.println("원본 객체 실행");
    }
}
  • CglibProxy : 프록시 객체
public class CglibProxy implements MethodInterceptor {

    private final Object target;

    public CglibProxy(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("프록시 객체 실행");
        proxy.invoke(target, args);
        System.out.println("프록시 객체 종료");
        return null;
    }
}
  • 실행
public static void main(String[] args) {
    TargetClass target = new TargetClass();

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(TargetClass.class);
    enhancer.setCallback(new CglibProxy(target));
    TargetClass proxy = (TargetClass)enhancer.create();
    System.out.println(proxy.getClass());
    proxy.execute();
}
--- 작업결과 ---
class com.example.demo.proxy.TargetClass$$EnhancerByCGLIB$$4b768b74
프록시 객체 실행
원본 객체 실행
프록시 객체 종료
  • 인터페이스 없이 Proxy 적용에 성공하였다.

Spring 에서의 Proxy

  • Spring에서는 Bean 생성 시 Target객체 대신 Proxy 객체를 리턴시켜 Proxy를 사용할 수 있다.
  • 하지만, 모든 Bean에 대해 직접 Proxy를 생성해주기엔 힘들다.

ProxyFactory

  • Dynamic Proxy를 생성한다.
  • Interface 유무를 체크하여 JDK Dynamic Proxy / CGLIB 중에서 Proxy를 생성한다.

ProxyFactory 구현

  • Interface : JDK Dynamic Proxy 로 Proxy 생성
Target target = new TargetImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new Proxy());
Target proxy = (Target) proxyFactory.getProxy();
proxy.execute();
  • Class : CGLIB로 Proxy 생성
Target target = new Target();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new Proxy());
Target proxy = (Target) proxyFactory.getProxy();
  • proxyFactory.setProxyTargetClass(true); : 인터페이스로 구현하더라도 이 설정이 되어있으면 CGLIB로 구현
  • spring.aop.proxy-target-class=true : Spring에서는 application.yml 파일과 같은 곳에 이 설정값으로 CGLIB로 구현할 지 여부를 정할 수 있다.
  • Spring 에서는 위와같은 방식으로 AOP를 구현하여 Proxy 객체를 이용할 수 있다.
  • Pointcut을 이용하여 어떤 Target 객체에 Proxy 를 적용할 지 간단하게 지정할 수 있다.

이상.

0개의 댓글