프록시 패턴

심야·2022년 11월 16일
0

웹 개발

목록 보기
19/47

프록시 패턴

프록시는 다른 객체에 대한 대체 또는 자리표시자를 제공할 수 있는 구조 디자인 패턴이다. 프록시는 원래 객체에 대한 접근을 제어하므로, 요청이 원래 객체에 전달되기 전 또는 후에 무언가를 수행할 수 있도록 한다.

문제

객체에 대한 접근을 제어하는 이유가 무엇일까? 방대한 양의 시스템 자원을 소비하는 객체가 있다. 이 객체는 항상 필요하지 않다. 필요할 때만 객체를 만들어서 지연된 초기화를 구현할 수 있으나 객체의 모든 클라이언트 코드들이 지연된 초기화 코드를 실행해야 한다. 이는 많은 코드 중복을 일으킨다.

해결책

프록시 패턴은 원래 서비스 객체와 같은 인터페이스로 새 프록시 클래스를 생성한다. 생성하면 프록시 객체를 원래 객체의 모든 클라이언트에 전달하도록 업데이트 할 수 있다. 클라이언트로부터 요청을 받으면 이 프록시는 실제 서비스 객체를 생성하고 모든 작업을 이 객체에 위임한다.

클래스의 메인 로직 이전이나 이후에 무언가를 실행해야 하는 경우 프록시 패턴은 원래 클래스와 같은 인터페이스를 구현하므로 실제 서비스 객체를 기대하는 모든 클라이언트에 전달될 수 있다.

적용 예시

신용 카드는 은행 계좌의 프록시이며, 은행 계좌는 현금의 프록시이다. 둘 다 같은 인터페이스를 구현하며 둘 다 결제에 사용될 수 있다.

구조

  1. 서비스 인터페이스는 서비스 인터페이스를 선언한다. 프록시가 서비스 객체로 위장할 수 있으려면 이 인터페이스를 따라야 한다.
  2. 서비스는 비즈니스 로직을 제공하는 클래스이다.
  3. 프록시 클래스에는 서비스 객체를 가리키는 참조 필드가 있다. 프록시가 요청 처리(초기화 지연, 로깅, 액세스 제어, 캐싱 등)를 완료하면, 그 후 처리된 요청을 서비스 객체에 전달한다.
  4. 클라이언트는 같은 인터페이스를 통해 서비스 및 프록시와 함께 작동해야 한다. 서비스 객체를 기대하는 모든 코드에 프록시를 전달할 수 있기 때문이다.

Example

아래 예제는 제삼자 유튜브 통합 라이브러리에서 지연된 초기화 및 캐싱을 프록시 패턴으로 해결하는 방법을 보여준다.

이 라이브러리는 동영상 다운로드 클래스를 제공하나 매우 비효율적이다. 클라이언트 앱이 같은 동영상을 여러 번 요청하면 라이브러리는 처음 다운로드한 파일을 캐싱하고 재사용하는 대신 계속해서 같은 비디오를 다운로드하기 때문이다.

따라서 프록시 클래스는 원래 다운로드 클래스와 같은 인터페이스를 구현하고 이 다운로드 클래스에 모든 작업을 위힘하나, 앱이 같은 비디오를 두 번 이상 요청하면 이미 다운로드한 파일을 추적해 캐시된 결과를 반환한다.

ThirdPartyYoutubeLib.java (원격 서비스 인터페이스)

import java.util.HashMap;

public interface ThirdPartyYouTubeLib {
    HashMap<String, Video> popularVideos();

    Video getVideo(String VideoId);
}

ThirdPartyYouTubeClass.java (원격 서비스 구현)

import java.util.HashMap;

public class ThirdPartyYoutubeClass implements ThirdPartyYouTubeLib{

    @Override
    public HashMap<String, Video> popularVideos() {
        connectToServer("http://www.youtube.com");
        return getRandomVideos();
    }

    @Override
    public Video getVideo(String videoId) {
        connectToServer("http://www.youtube.com/" + videoId);
        return getSomeVideo(videoId);
    }

    //Fake methods to simulate network activity. They as slow as a real life.

    private int random(int min, int max){
        return min + (int) (Math.random() * ((max - min) + 1));
    }

    private void experienceNetworkLatency(){
        int randomLatency = random(5, 10);
        for (int i = 0; i < randomLatency; i++) {
            try{
                Thread.sleep(100);
            } catch (InterruptedException ex){
                ex.printStackTrace();
            }
        }
    }

    private void connectToServer(String server){
        System.out.print("Connecting to " + server + " ... ");
        experienceNetworkLatency();
        System.out.print("Connected!" + "\n");
    }

    private HashMap<String, Video> getRandomVideos(){
        System.out.print("Downloading populars...");

        experienceNetworkLatency();
        HashMap<String, Video> hmap = new HashMap<String, Video>();
        hmap.put("catzzzzzzzzz", new Video("sadgahasgdas", "Catzzzz.avi"));
        hmap.put("mkafksangasj", new Video("mkafksangasj", "Dog play with ball.mp4"));
        hmap.put("dancesvideoo", new Video("asdfas3ffasd", "Dancing video.mpq"));
        hmap.put("dlsdk5jfslaf", new Video("dlsdk5jfslaf", "Barcelona vs RealM.mov"));
        hmap.put("3sdfgsd1j333", new Video("3sdfgsd1j333", "Programing lesson#1.avi"));

        System.out.print("Done!" + "\n");
        return hmap;
    }

    private Video getSomeVideo(String videoId){
        System.out.print("Downloading video ..");

        experienceNetworkLatency();
        Video video = new Video(videoId, "some video title");

        System.out.print("Done!" + "\n");
        return video;
    }
}

YouTubeCacheProxy.java (캐싱 프록시)

import java.util.HashMap;

public class YoutubeCacheProxy implements ThirdPartyYouTubeLib{
    private ThirdPartyYouTubeLib youtubeService;
    private HashMap<String, Video> cachePopular = new HashMap<String, Video>();
    private HashMap<String, Video> cacheAll = new HashMap<String, Video>();

    public YoutubeCacheProxy(){
        this.youtubeService = new ThirdPartyYoutubeClass();
    }

    @Override
    public HashMap<String, Video> popularVideos() {
        if(cachePopular.isEmpty()){
            cachePopular = youtubeService.popularVideos();
        } else {
            System.out.println("Retrieved list from cache.");
        }
        return cachePopular;
    }

    @Override
    public Video getVideo(String videoId) {
        Video video = cacheAll.get(videoId);
        if(video == null){
            video = youtubeService.getVideo(videoId);
            cacheAll.put(videoId, video);
        } else{
            System.out.println("Retrieved video '" + videoId + "' from cache.");
        }
        return video;
    }

    public void reset(){
        cachePopular.clear();
        cacheAll.clear();
    }
}

YouTubeDownloader.java 미디어 다운로더 앱

import java.util.HashMap;

public class YoutubeDownloader {
    private ThirdPartyYouTubeLib api;

    public YoutubeDownloader(ThirdPartyYouTubeLib api){
        this.api = api;
    }

    public void renderVideoPage(String videoId){
        Video video = api.getVideo(videoId);
        System.out.println("\n-------------------------------");
        System.out.println("Video page (imagine fancy HTML)");
        System.out.println("ID: " + video.id);
        System.out.println("Title: " + video.title);
        System.out.println("Video: " + video.data);
        System.out.println("-------------------------------\n");
    }

    public void renderPopularVideos(){
        HashMap<String, Video> list = api.popularVideos();
        System.out.println("\n------------------------------");
        System.out.println("Most popular videos on YouTube (imagine fancy HTML)");
        for(Video video: list.values()){
            System.out.println("ID: " + video.id + " / Title: " + video.title);
        }
        System.out.println("-------------------------------\n");
    }
}

Demo.java

public class Demo {
    public static void main(String[] args) {
        YoutubeDownloader naiveDownloader = new YoutubeDownloader(new ThirdPartyYoutubeClass());
        YoutubeDownloader smartDownloader = new YoutubeDownloader((new YoutubeCacheProxy()));

        long naive = test(naiveDownloader);
        long smart = test(smartDownloader);
        System.out.print("Time saved by caching proxy: " +(naive - smart) + "ms");
    }


    private static long test(YoutubeDownloader downloader){
        long startTime = System.currentTimeMillis();

        // User behavior in our app:
        downloader.renderPopularVideos();
        downloader.renderVideoPage("catzzzzzzzzz");
        downloader.renderPopularVideos();
        downloader.renderVideoPage("dancesvideoo");
        // Users might visit the same page quite often.
        downloader.renderVideoPage("catzzzzzzzzz");
        downloader.renderVideoPage("someothervid");

        long estimatedTime = System.currentTimeMillis() - startTime;
        System.out.print("Time elapsed: " + estimatedTime + "ms\n");
        return estimatedTime;
    }
}

실행 결과

Time elapsed: 6121ms
Time saved by caching proxy: 2370ms

비디오 다운로드 클래스만 사용할 때 라이브러리는 처음 다운로드한 파일을 캐싱하고 재사용하는 대신 계속해서 같은 비디오를 다운로드한다.

그러나 프록시 패턴 즉, 프록시 클래스를 사용하면 앱이 같은 비디오를 두 번 이상 요청하면 이미 다운로드한 파일을 추적해 캐시된 결과를 반환한다. 따라서 실행 결과가 증명하듯이 속도 차이가 현저하다.

장단점

  1. 클라이언트들이 알지 못하는 상태에서 서비스 객체를 제어할 수 있다.
  2. 클라이언트들이 신경 쓰지 않을 때 서비스 객체 수명 주기를 관리할 수 있다.
  3. 프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동한다.
  4. 개방/폐쇄 원칙. 서비스나 클라이언트들을 변경하지 않고도 새 프록시들을 도입할 수 있다.
  5. 새로운 클래스를 많이 도입해야 해 코드가 복잡해질 수 있다.
  6. 서비스 응답이 늦어질 수 있다.

출처

디자인 패턴에 뛰어들기
스프링 입문을 위한 자바 객체 지향의 원리와 이해

profile
하루하루 성실하게, 인생 전체는 되는대로.

0개의 댓글