디자인 패턴: 에이전트 패턴의 정적 에이전트 및 동적 에이전트 이해


1. 개요

여기에 이미지 설명을 삽입하세요.

Proxy Pattern은 구조적인 디자인 패턴으로 개념은 매우 간단하며, Proxy 객체를 생성하여 원본 객체에 대한 접근을 제어하는 ​​패턴입니다. 에이전트 모델에는 주로 에이전트 역할과 실제 역할이라는 두 가지 역할이 포함됩니다. 프록시 클래스는 실제 클래스를 프록시하고 실제 클래스에 대한 액세스를 제어하는 ​​기능을 제공하는 역할을 담당하며, 실제 클래스는 특정 비즈니스 로직을 완성합니다. 이와 같이 실제 객체에 직접 접근하는 것이 불편하거나 불가능할 때 프록시 객체를 통해 간접적으로 접근할 수 있습니다. 프록시 모드를 사용하는 주요 목적은 두 가지입니다. 하나는 대상 개체를 보호하는 것이고, 다른 하나는 대상 개체를 향상시키는 것입니다.

프록시모드01

Tip: 기사에서 언급된 실제 클래스, 원 클래스, 대상 클래스는 동일한 의미를 가지며 모두 에이전트 역할을 나타냅니다.

1.1 구현 방법은 무엇입니까?

프록시 패턴의 원리는 실제 클래스의 기능을 프록시 클래스에 캡슐화하는 것이며, 가장 기본적인 방법은 프록시 클래스를 생성하는 것이다.프록시 클래스는 실제 클래스와 동일한 인터페이스를 구현하고, 실제 클래스의 인스턴스를 참조한다. 프록시 클래스의 클래스. 이러한 방식으로 실제 클래스의 코드를 수정하지 않고도 프록시 클래스를 통해 실제 클래스에 대한 접근을 제어할 수 있습니다. 프록시 클래스는 실제 클래스에 대한 액세스 제어 , 캐싱, 로깅 및 기타 기능을 구현하기 위해 실제 클래스의 메서드를 호출하기 전후에 몇 가지 추가 논리를 추가할 수도 있습니다 .

프록시 모드를 구현하는 데는 정적 프록시 와 동적 프록시 라는 두 가지 옵션이 있습니다 . 정적 프록시는 프록시 클래스가 컴파일 타임에 결정된다는 의미입니다. 즉, 프록시 클래스를 미리 수동으로 작성해야 합니다. 동적 프록시는 런타임에 프록시 클래스를 동적으로 생성합니다.

동적 프록시 솔루션에는 두 가지 구현이 있습니다. 첫째, java.lang.reflect.Proxy동적 프록시 기능은 Java 자체 클래스를 통해 구현됩니다. 둘째, 프록시 클래스를 동적으로 생성하기 위해 추가 오픈 소스 고성능 코드 생성 패키지 CGlib가 도입되었습니다. 두 가지 모두의 기본 구현은 반사 및 조작 바이트코드 기술을 사용합니다.

JDK 동적 프록시는 인터페이스 구현을 기반으로 하는 프록시이며 인터페이스를 구현하는 클래스만 프록시할 수 있습니다.

CGlib 메소드는 상속을 기반으로 하는 프록시로, 실제 클래스가 특정 상위 클래스를 상속받아야 한다는 의미는 아니지만, 생성된 프록시 클래스가 실제 클래스의 하위 클래스 역할을 하여 상위 클래스, 즉 프록시를 프록시하는 역할을 합니다. 클래스는 실제 클래스에서 상속됩니다. 이 방법은 인터페이스 구현이 필요하지 않으며 JDK 프록시 방법에 대한 보완 솔루션으로 사용할 수 있습니다.

프록시모드03

1.2 장점

  • 프록시 개체는 원본 개체의 구현 세부 정보를 숨길 수 있으므로 클라이언트는 원본 개체의 특정 구현을 알 필요가 없습니다.
  • 프록시 개체는 원본 개체를 기반으로 캐싱, 보안 확인 등과 같은 추가 기능을 추가할 수 있습니다.
  • 프록시 객체는 원본 객체에 대한 접근을 제어하고 불법적인 접근으로부터 원본 객체를 보호할 수 있습니다.
  • 프록시 개체는 클라이언트와 원본 개체 사이에서 중개 역할을 수행하여 클라이언트와 원본 개체 간의 결합을 줄일 수 있습니다.

1.3 단점

  • 에이전트 클래스를 도입하면 시스템의 복잡성이 증가하고 학습 및 이해 비용이 증가합니다.
  • 프록시 레이어 추가로 인해 요청 처리 속도가 느려집니다.

1.4 적용 가능한 시나리오

프록시 모드는 주로 개체 액세스를 제어, 강화 또는 숨겨야 하는 시나리오에 적합합니다. 프록시 액세스에 적합한 특정 시나리오는 다음과 같습니다.

  • 원격 프록시: 클라이언트가 원격 개체(다른 주소 공간 또는 네트워크에 위치)에 액세스해야 하는 경우 프록시 패턴을 사용하여 기본 네트워크 통신의 복잡성을 숨길 수 있습니다. 프록시 개체는 네트워크 통신을 처리하고 클라이언트에게 결과를 반환합니다.
  • 가상 프록시: 객체를 생성하고 초기화하는 데 비용이 많이 드는 경우 프록시 패턴을 사용하여 객체 인스턴스화를 지연하고 객체가 실제로 필요할 때만 초기화할 수 있습니다. 이를 통해 시스템 성능과 리소스 활용도가 향상됩니다.
  • 보안 프록시: 프록시 모드를 사용하여 민감한 리소스에 대한 액세스를 제어할 수 있습니다. 프록시 개체는 리소스에 액세스하기 전에 클라이언트의 권한을 확인하거나 일부 보안 검사를 수행하여 실제 개체를 보호할 수 있습니다.
  • 로깅 프록시: 프록시 모드를 통해 실제 개체의 메서드 실행 전후에 로깅하여 로깅, 디버깅 및 성능 모니터링과 같은 기능을 달성할 수 있습니다.
  • 지연 로딩 프록시: 사용해야 하는 객체에 오버헤드가 큰 경우 프록시 모드를 사용하여 지연 로딩을 구현하고, 리소스를 절약하고 응답 속도를 향상시키기 위해 실제로 필요할 때만 객체를 로드할 수 있습니다.
  • Caching Proxy: 프록시 모드를 사용하여 객체 캐싱을 구현할 수 있으며, 클라이언트가 객체를 요청하면 프록시 객체는 먼저 객체가 캐시에 존재하는지 확인하고, 존재하면 직접 반환하고, 그렇지 않으면 새로운 객체를 생성하여 캐시에 저장합니다. 캐시되어 시스템 성능이 향상됩니다.

2 정적 프록시 구현

구현 방법은 비교적 간단하고 이해하기 쉬우며 코드를 직접 입력할 수 있습니다.

NO.1 추상 인터페이스 : 비디오 플레이어 인터페이스 정의 Player

public interface Player {
    
    
    void loadVideo(String filename);
    void playVideo(String filename);
}

NO.2 Real class : 인터페이스 구현 클래스 VPlayer를 정의합니다.

public class VPlayer implements Player {
    
    
    @Override
    public void loadVideo(String filename) {
    
    
        System.out.println("加载MP4视频文件:"+filename);
    }

    @Override
    public void playVideo(String filename) {
    
    
        System.out.println("播放MP4视频:"+filename);
    }
}

NO.3 Proxy 클래스 : 동일한 인터페이스를 구현하기 위해 VPlayerProxy 프록시 클래스를 정의합니다.

public class VPlayerProxy implements Player {
    
    

    private Player player;

    public VPlayerProxy(Player player) {
    
    
        this.player = player;
    }

    @Override
    public void loadVideo(String filename) {
    
    
        player.loadVideo(filename);
    }

    @Override
    public void playVideo(String filename) {
    
    
        player.playVideo(filename);
    }
}

NO.4 고객전화

public class Client1 {
    
    
    public static void main(String[] args) {
    
    
        //直连方式
        Player vplay=new VPlayer();
        vplay.playVideo("aaa.mp4");
        System.out.println();

        //代理方式
        Player proxy=new VPlayerProxy(vplay);
        proxy.loadVideo("aaa.mp4");
        proxy.playVideo("aaa.mp4");

    }
}

이미지-20230610194243722

3 JDK 동적 프록시 구현

JDK 동적 프록시는 Java 표준 라이브러리에서 제공하는 프록시 메소드로 런타임 시 동적으로 프록시 객체를 생성할 수 있으며, 프록시 객체는 원본 클래스와 동일한 인터페이스를 구현하고 메소드 호출을 프록시 객체로 전달하는 동시에 프록시 객체를 생성합니다. 메소드 호출 전후에 추가 개선 처리를 수행할 수도 있습니다. JDK 동적 프록시는 반사 메커니즘을 통해 프록시 기능을 구현하며 그 원리는 다음 단계로 구분됩니다.

  1. InvocationHandler 인터페이스를 구현하는 프록시 클래스 팩토리를 만듭니다. Proxy 클래스의 정적 메서드 newProxyInstance를 호출하면 프록시 클래스가 동적으로 생성됩니다. 프록시 클래스는 대상 인터페이스를 구현하고 InvocationHandler 유형의 참조를 보유합니다.
  2. InvocationHandler 인터페이스: InvocationHandler는 메소드 호출이 하나만 있는 인터페이스입니다. 프록시 객체의 메소드가 호출되면 JVM은 자동으로 프록시 클래스의 호출 메소드를 호출하고 호출된 메소드 이름, 매개변수 및 기타 정보를 메소드에 전달합니다.
  3. 프록시 객체의 메소드 호출: 프록시 객체의 메소드가 호출되면 JVM은 자동으로 프록시 클래스의 호출 메소드를 호출합니다. Invoke 메소드에서는 로그 추가, 성능 통계, 트랜잭션 관리 등 필요에 따라 다양한 로직을 실행할 수 있습니다.
  4. Invoke 메소드 호출: Invoke 메소드에서는 리플렉션 메커니즘을 통해 대상 객체의 메소드를 호출하고 해당 메소드의 반환 값을 반환합니다. 대상 개체의 메서드를 호출하기 전후에 추가 논리를 수행할 수 있습니다.

공통 구현 코드

public class JDKProxyFactory implements InvocationHandler {
    
    

    //需要被代理的对象
    private Object object;

    public JDKProxyFactory(Object object) {
    
    
        this.object = object;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy(){
    
    
        return (T) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),//当前线程的上下文ClassLoader
                object.getClass().getInterfaces(), //代理需要实现的接口
                this); // 处理器自身
    }
	
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
        Object result = null;
		//进行方法匹配,调用对应方法名的方法
        if ("loadVideo".equals(method.getName())) {
    
    
            result=method.invoke(object, args);
        }

        if ("playVideo".equals(method.getName())) {
    
    
            System.out.println("前置增强");
            result=method.invoke(object, args);
            System.out.println("后置增强");
        }
        return result;
    }
}

고객 통화

public class Client2 {
    
    
    public static void main(String[] args) {
    
    
        Player player=new VPlayer();
        Player proxy=new JDKProxyFactory(player).getProxy();
        proxy.loadVideo("aaa.mp4");
        proxy.playVideo("aaa.mp4");

/*      或者
        Player p=new VPlayer();
        Player o = (Player) Proxy.newProxyInstance(
                p.getClass().getClassLoader(),
                p.getClass().getInterfaces(),
                new VPlayerProxyFactory(p)
        );
        o.loadVideo("aaaa.mp4");
*/
    }
}

이미지-20230610200031639

4 CGlib 동적 프록시 구현

CGLIB(Code Generation Library)는 ASM(Java Bytecode Operation Framework) 기반의 코드 생성 라이브러리로, 런타임 시 대상 클래스의 서브클래스를 프록시 클래스로 동적으로 생성하고, 그 안의 메소드를 오버라이드하여 프록시 기능을 구현할 수 있습니다. Java와 함께 제공되는 JDK 동적 프록시와 달리 CGlib 동적 프록시는 인터페이스를 구현하지 않는 클래스를 프록시할 수 있습니다. 원칙은 다음 단계로 나누어집니다.

  1. Enhancer 객체 생성: Enhancer는 하위 클래스를 동적으로 생성하는 데 사용되는 CGLIB 라이브러리의 기본 클래스입니다. Enhancer 개체를 생성하고 프록시가 필요한 대상 클래스 및 인터셉터와 같은 매개 변수를 설정하여 프록시 클래스를 생성할 수 있습니다.
  2. 콜백 인터셉터 설정: 프록시 클래스를 생성할 때 인터셉터를 지정해야 합니다. 인터셉터는 프록시 로직 구현의 핵심으로, 프록시 클래스의 메소드가 호출될 때 호출을 가로채 해당 로직을 실행한다. CGLIB에서 인터셉터는 MethodInterceptor 인터페이스를 구현해야 합니다.
  3. 프록시 객체 생성: Enhancer 객체의 create 메소드를 호출하여 프록시 객체를 생성할 수 있습니다. 프록시 객체는 대상 클래스의 메서드를 상속받으며, 프록시 객체의 메서드를 호출하면 인터셉터의 인터셉트 메서드가 먼저 호출된 후 대상 메서드가 실행됩니다.
  4. 프록시 객체 호출: 프록시 객체의 메소드를 호출하면 인터셉터의 인터셉트 메소드가 트리거됩니다. Intercept 방식에서는 로그 추가, 성능 통계, 트랜잭션 관리 등 필요에 따라 다양한 로직을 실행할 수 있습니다.

인터페이스 없이 별도의 실제 클래스 APlayer 정의

//音频播放器
public class APlayer {
    
    
    public void loadAudio(String filename) {
    
    
        System.out.println("加载MP3音频文件:"+filename);
    }

    public void playAudio(String filename) {
    
    
        System.out.println("播放MP3:"+filename);
    }
}

공통 구현 코드

public class CglibProxyFactory implements MethodInterceptor {
    
    

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> clazz) {
    
    
        Enhancer en = new Enhancer();
        //设置代理的父类
        en.setSuperclass(clazz);
        //设置方法回调
        en.setCallback(this);
        //创建代理实例
        return (T)en.create();
    }

    @Override
    //参数中的object是目标对象,method和args是目标对象的方法和参数,methodProxy是方法代理
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    
    
        Object result = null;

        if ("loadAudio".equals(method.getName())) {
    
    
            //通过继承的方法实现代理,因此这里调用invokeSuper
            result = methodProxy.invokeSuper(object, args);
        }
        if ("playAudio".equals(method.getName())) {
    
    
            result = methodProxy.invokeSuper(object, args);
        }
        return result;
    }
}

고객 통화

public class Client3 {
    
    
    public static void main(String[] args) {
    
    
        APlayer aplayer=new APlayer();
        APlayer proxy = new CglibProxyFactory().getProxy(aplayer.getClass());
        //验证代理类的父类
        System.out.println("代理类的父类:"+proxy.getClass().getSuperclass().getSimpleName());
        System.out.println();

        proxy.loadAudio("荷塘月色.mp3");
        proxy.playAudio("荷塘月色.mp3");
    }
}

이미지-20230610201932707

5 요약

프록시 모드는 실제 클래스 코드를 수정하지 않고도 실제 클래스에 대한 접근 제어, 성능 최적화 등의 기능을 구현할 수 있습니다. Java에서 프록시 패턴을 구현하는 방법에는 정적 프록시와 동적 프록시라는 두 가지 방법이 있습니다. 정적 프록시는 컴파일하기 전에 수동으로 프록시 클래스를 작성해야 하는 반면 동적 프록시는 런타임에 프록시 클래스를 동적으로 생성할 수 있습니다.

동적 프록시는 JDK 동적 프록시와 CGlib 동적 프록시로 구분되는데, 일반적으로 전자를 사용하고, 프록시 클래스에 인터페이스가 없는 경우 전자의 보완 솔루션으로 후자를 사용합니다. 프록시 패턴의 장점에는 시스템 결합 감소, 코드 확장성 향상, 시스템 보안 향상, 인터페이스 단순화 등이 있지만 시스템 복잡성이 증가하고 성능에 영향을 미칠 수도 있습니다. 프록시 모드는 액세스 제어, 원격 프록시 및 기능 향상이 필요한 시나리오에서 좋은 선택입니다.

추천

출처blog.csdn.net/ZGL_cyy/article/details/132910893