개발 기록

> [GOF Design Pattern] 구조 패턴 : 프록시(Proxy) 패턴 - 개념과 구현 본문

디자인 패턴

> [GOF Design Pattern] 구조 패턴 : 프록시(Proxy) 패턴 - 개념과 구현

1z 2024. 1. 17. 15:37

1. 프록시 패턴 에 대해 알아보자 

 

(1) 개념

프록시 패턴은 하나의 객체(프록시)가 다른 객체(주체 또는 서비스)에 대한 액세스를 제어할 수 있도록 하는 기술이다.

Proxy는 '~을 대신하여', 대표하다' 또는 '대신하여' 또는 '대신하여'라는 사전적 의미로 지니고 있다.
즉, 누군가에게 어떤 일을 대신 시키는 것을 뜻한다. 이를 객체 지향 프로그래밍에 접목하면 클라이언트가 대상 객체를 직접 쓰는게 아니라 중간에 프록시 객체를 거치는 구조라고 보면 된다

 

 

(2) 구조

 


 1. Subject 

: 실제 개체와 해당 프록시 모두에 대한 인터페이스로 Proxy와 RealSubject 모두 Subject 인터페이스를 구현한다. Subject 을 사용하면 실제 객체를 필요로 하는 모든 곳에서 프록시를 사용할 수 있다.

 

 2. RealSubject

: Proxy 가 제어하는 원본 객체로, 진짜 작업 대부분을 처리하는 한다.

 

 3.  Proxy

: RealSubject 에 대한 참조를 갖고 요청을 전달한다.

RealSubject에 대한 접근을 제어하며 경우에 따라 생성과 삭제도 제어할 수 있다.

: 프록시는 흐름제어만 할 뿐 결과값을 조작하거나 변경시키면 안 된다.


2. 프록시 패턴 유형

 

(1) 가상 프록시 (Virtual Proxy)

가상 프록시는 생성하는데 많은 비용이 드는 객체를 대신한다. 실제 객체의 사용이 필요한 순간까지 프록시는 실제 객체를 생성하지 않고 요청이 들어온 순간 실제 객체를 생성하기 시작하며, 생성이 완료될 때 까지 응답을 지연시키거나, 생성 중이라는 정보를 클라이언트에게 알려준다거나 하는 작업을 제공한다.

ex. 애플리이션에 기본 결과를 먼저 제공하고, RealSubject 작업이 완료되면

이전에 기본 결과를 제공했던 클라이언트에 실제 데이터를 푸시한다.

 

(2) 보호 프록시 (Protection Proxy) 

자원에 대하여 권한을 지정하고 접근을 제어하기 위한 프록시 패턴

ex. 보고서 뷰어 애플리케이션에서 보고서 생성기 개체는 보호 프록시가 관리자 역할을 가진 사용자에게만 액세스를 허용한 후에 보고서를 생성한다..

 

(3) 원격 프록시 (Remote Proxy)

제어하기 위한 개체가 사용자와 위치가 다른경우에 사용자가 원격지에 있는 개체에 접근을 쉽게 하기 위한 프록시 패턴.

원격지 프록시는 서버측에 있는 실제 개체와 같은 인터페이스를 갖는 원격지 개체와 통신을 담당하여 서버측에 있는 개체를 제어하게 하는 원리다. 동일한 물리적 또는 가상 공간에 있지 않은 시스템을 로컬에 있는 것 처럼 표현할 수 있다.

 

 

(4) 스마트 프록시

 

 


3. 보호 프록시 (Protection Proxy)구현 예

«Scenario»

음식주문을 생각해보자.  손님이 음식 주문을 요청했을 때, 홀에서는 바로 요리를 내놓는것이 아니라 주방의 재료 유무 여부를 파악해서 주문을 받고 요리를 내놓는다. 이 예시를 아래의 프록시 패턴으로 구현한 물류 주문 기능에 대입해보자.

☞ proxy_fulfillment : 창고의 재고를 확인하고, 재고가 있을 시 창고에 주문을 전달한다. -> 홀 역할

☞ Storage : 상품 배송 등 과 같은 주문을 처리한다. -> 주방 역할

 

 

(창고) 로 전달되는 요청을 프록시에서 걸러서 보냄으로써 (창고) 에 처리할 수 없는 요청이 가지 않도록 막을 수 있다.

 제어 로직을 추가하고 대상 객체의 메서드를 위임 호출해줌으로써 보호 프록시를 구성할 수 있게 된다.

 

 

 

(1) Order <<Interface>> - Subject

 

- 서비스 인터페이스 생성, 서비스 인터페이스를 하나 생성하여 프록시와 대상 객체 간의 상호 교환을 가능하게 만든다. 이 인터페이스는 Proxy_fulfillment  , Storage  클래스가 구현한다.

 

 

(2) ProxyFulfillment - Proxy Class

 

- 프록시 클래스 생성. 이 클래스에는 RealSubject 에 대한 참조를 저장하기 위한 필드가 있어야 한다.

  * new 명령어로 RealSubject 객체 생성 vs   클라이언트가 RealSubject obj 를 프록시의 생성자에 전달하는 방식 

- 프록시 클래스에서는 주문이 가능한지 확인하는 모든 작업이 이루어지며, 주문을 이행할 수 있는 경우에만 주체 클래스에 요청을 위임한다.

 

(3) Stroage - Real Subject Class

 

- Real Subject 클래스를 구현하는 것이다. 이 주체 클래스는 실질적으로 주문을 처리하는 구현 메소드를 가지며, 프록시 클래스에서 주문이 가능한지 확인할때 사용할 메소드를 가지고 있다.

 

 

 이렇게 프록시 클래스로 인해 주문 유효성 검사, 주문의 이행 두 부분으로 분리할 수 있다. 주체 클래스에서는 유효성 검증 작업을 할 필요가 없으며, 창고들에서는 재고가 없을 경우에 대한 처리를 신경쓸 필요가 없다.

 


4 . 가상 프록시 구현 예제  

 

(1) 늦은 초기화(Lazy Initialisation)로 실제 객체의 사용 시점 제어

프록시 객체 내에서 경로 데이터를 지니고 있다가 사용자가 printData() 메서드를 호출하면 그때서야 대상 객체를 로드(lazyload)하여, 파일 데이터를를 메모리에 적재하고 대상 객체의 printData () 메서드를 위임 호출함으로써, 실제 메소드를 호출하는 시점에 메모리 적재가 이루어지진다.

 

1.  프록시 객체를 생성할때 생성자 파라미터로 파일 경로를 넘긴다.

public class VirtualProxyController {
    public void getData(){
        FileService fileService1 = new FileService("./csv/대용량csv파일_1");
        FileService fileService2 = new FileService("./csv/대용량csv파일_2");
        FileService fileService3 = new FileService("./csv/대용량csv파일_3");

        fileService2.printData();
    }
 }

 

2. 프록시 객체 내에서 path 필드에 경로를 담아두고 있다가, 대상 객체를 생성할 때 파라미터로 넘긴다.

// proxy
public class FileServiceProxy implements FileService {
    private FileService fileService;
    private String path;

    public FileServiceProxy(String path) {
        this.path = path;
    }

    @Override
    public void printData() {
        fileService = new FileServiceImpl(path);
        fileService.printData();
    }
}

 

3. 메모리를 적재하고, 출력하는 등의 실제 작업을 처리한다.

// 대상 객체 (RealSubject)
public class FileServiceImpl implements FileService {

    String List<data> data;

    FileServiceImpl(String path) {
        loadData(path);
    }

    private void loadData(String path) {
        // 데이터를 읽고 메모리에 적재 (작업 자체가 무겁고 많은 자원을 필요로함)
        try {
            Thread.sleep(1000);
            // TODO 데이터 읽는 로직 진행
            .
            .
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.printf("%s에 있는 파일 로딩 완료\n", path);
    }

    @Override
    public void printData() {
        System.out.printf("%s 읽은 데이터 출력\n", data);
    }
}

 

(2) 반복된 요청에 대한 캐싱 

 

1. 첫 요청시 listCache 에 결과값을 담아두고 두 번 이상 요청하면 담아둔 결과값를 반환한다.

public class ProxyCachedVideo implements VideoService {

    private final VideoService videoService;
    private List<?> listCache;

    public ProxyCachedVideo(VideoService videoServiceImpl) {
        this.videoService = videoServiceImpl;
    }

    @Override
    public List<?> listVideos() {
        if (listCache == null){
            listCache = videoService.listVideos();
        }
        return listCache;
    }
}

 

2.  데이터를 불러오는 실제 작업 처리

public class VideoServiceImpl implements VideoService {

    @Override
    public List<?> listVideos() {
        // 유튜브에 API 요청을 보냅니다.
        System.out.println("유튜브에 API 요청을 보냅니다.");
        return new ArrayList<>();
    }
}

5 . 다른 패턴과의 비교 

(1) 프록시 패턴 vs 데코레이터 패턴

  • 프록시 패턴(Proxy Pattern) 어떠한 클래스에 대한 접근을 제어 혹은 접근에 초점에 맞춘 용도
    • 구현에 따라 프록시는 원본 객체로 들어오는 요청을 전달하지 않을 수 있다. 또한 동작을 꾸며주는것이 아닌 별도의 확장 기능(호출 횟수 count)이 추가하는 목적이 될 수 있다.
  • 데코레이터 패턴(Decorator Pattern) 클래스에 새로운 행동 or 기능을 추가하기 위한 용도

(2) 프록시 패턴 vs 어댑터 패턴

  • 프록시 패턴과 어댑터 패턴 모두 클라이언트와 객체 사이에 위치하여 클라이언트의 요청을 받아서 다른 객체에게 전달해주는 역할을 한다.
  • 하지만, 어댑터 패턴은 다른 객체의 인터페이스를 바꿔주지만, 프록시 패턴에서는 똑같은 인터페이스를 사용한다.

 


6. 장점과 단점

1. 장점.

  • 시스템의 결합을 어느 정도 줄입니다.
  • 프록시 개체는 대상 개체의 기능을 확장할 수 있습니다.
  • 프록시 개체는 대상 개체를 보호할 수 있습니다.

2. 단점

  • 클라이언트와 실제 개체 사이에 프록시를 추가하면 요청 처리 속도가 느려질 수 있습니다.
  • 시스템 복잡성이 증가합니다.

 

 

 

 

 

 

참고

https://refactoring.guru/ko/design-patterns/proxy

https://yaboong.github.io/design-pattern/2018/10/17/proxy-pattern/

https://www.baeldung.com/spring-framework-design-patterns

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%94%84%EB%A1%9D%EC%8B%9CProxy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90