Proxy

김성혁·2021년 10월 22일
0
post-thumbnail

Intent

프록시는 너로 하여금 대리자나 또 다른 객체를 위한 placeholder를 제공하도록 하는 구조적인 디자인 패턴이다. 프록시는 너로 하여금 요청이 오리지널 객체에 전달되기 전이나 후로 무언가를 수행가하면서 오리지널 객체에 대한 접근을 제어한다.

Problem

왜 객체에 대한 접근을 제어하기를 원하는가? 여기 한 예가 있다: 막대한 양의 시스템 자원을 소비하는 거대한 객체가 있다. 너는 그것이 때때로 필요하지만 항상은 아니다.

Database queries can be really slow.

너는 지연 초기화(처음 필요할 때까지 객체 생성, 값 계산 또는 기타 값비싼 프로세스를 지연시키는 전술)를 구현할 수 있다: 실제로 필요할 때만 객체를 생성한다. 모든 객체의 클라이언트는 몇몇 지연된 초기화 코드를 실행해야 한다. 불행히도, 이것은 아마도 많은 코드의 중복을 야기할 것이다.

이상적인 세계에서, 우리는 이 코드를 직접 우리의 객체들의 클래스 안에 놓기를 원하지만, 항상 가능한 것만은 아니다. 예를 들어, 해당 클래스가 폐쇄된 3rd-party 라이브러리의 일부일지도 모른다.

Solution

프록시 패턴은 원래 서비스 객체와 동일한 인터페이스로 새 프록시 클래스를 생성하도록 제안한다. 그 다음 너는 너의 앱이 원래 객체들의 모든 클라이언트에 프록시 객체를 통과하기 위해서 너의 앱을 업데이트한다. 클라이언트로부터 요청을 받자마자, 프록시는 진짜 서비스 객체를 생성하고 모든 작업을 위임한다.

The proxy disguises itself as a database object. It can handle lazy initialization and result caching without the client or the real database object even knowing.

하지만 이익이 뭔가? 만약 주된 클래스 로직 전과 후에 무언가를 실행할 필요가 있다면, 프록시는 해당 클래스를 변경하지 않고 이것을 수행하도록 해준다. 프록시는 원본 클래스로서 같은 인터페이스를 수행하기 때문에, 진짜 서비스 객체를 기대하는 어떠한 클라이언트에 통과될 수 있다.

Real-World Analogy

Credit cards can be used for payments just the same as cash.

CreditCard는 Cash 묶음을 위한 프록시인 은행 계좌에 대한 프록시이다. 둘다 같은 인터페이스를 구현한다: 그들은 Payment를 만드는 것에 대해 사용될 수 있다. 소비자는 많은 현금을 가지고 다닐 필요가 없기 때문에 기분이 좋다. 가게 주인은 예금을 읽어버리거나 은행으로 가는 길에 강도를 당하는 위험이 없이 가게의 은행 계좌로 전자적으로 거래로부터 오는 수입이 더해지기 때문에 기분이 좋다.

Structure

The Service Interface declares the interface of the Service. The proxy must follow this interface to be able to disguise itself as a service object.

The Service is a class that provides some useful business logic.

The Proxy class has a reference field that points to a service object. After the proxy finishes its processing (e.g., lazy initialization, logging, access control, caching, etc.), it passes the request to the service object.

Usually, proxies manage the full lifecycle of their service objects.

The Client should work with both services and proxies via the same interface. This way you can pass a proxy into any code that expects a service object.

Pseudocode

이 예제는 어떻게 프록시 패턴이 지연 초기화와 3rd-party 유튜브 통합 라이브러리를 캐싱하는지 소개하는 것을 도와줄 수 있다.

Caching results of a service with a proxy.

라이브러리는 우리에게 비디오 다운로딩 클래스를 함께 제공해준다. 그러나, 그것은 매우 비효율적이다. 만약 클라이언트 애플리케이션이 같은 비디오를 여러 차례 요청한다면, 해당 라이브러리는 캐싱하고 첫 번째로 다운로드된 파일을 다시 사용하는 것 대신에 단지 계속해서 다운로드한다.

프록시 클래스는 원래의 다운로더로써 같은 인터페이스를 구현하고 모든 작업을 위임한다. 그러나, 앱이 동일한 비디오를 여러 차례 요청할 때 다운로드한 파일을 추적하고 캐시된 결과를 반환한다.

// The interface of a remote service.
interface ThirdPartyYouTubeLib is
    method listVideos()
    method getVideoInfo(id)
    method downloadVideo(id)
// The concrete implementation of a service connector. Methods
// of this class can request information from YouTube. The speed
// of the request depends on a user's internet connection as
// well as YouTube's. The application will slow down if a lot of
// requests are fired at the same time, even if they all request
// the same information.
class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib is
    method listVideos() is
        // Send an API request to YouTube.

    method getVideoInfo(id) is
        // Get metadata about some video.

    method downloadVideo(id) is
        // Download a video file from YouTube.
// To save some bandwidth, we can cache request results and keep
// them for some time. But it may be impossible to put such code
// directly into the service class. For example, it could have
// been provided as part of a third party library and/or defined
// as `final`. That's why we put the caching code into a new
// proxy class which implements the same interface as the
// service class. It delegates to the service object only when
// the real requests have to be sent.
class CachedYouTubeClass implements ThirdPartyYouTubeLib is
    private field service: ThirdPartyYouTubeLib
    private field listCache, videoCache
    field needReset

    constructor CachedYouTubeClass(service: ThirdPartyYouTubeLib) is
        this.service = service

    method listVideos() is
        if (listCache == null || needReset)
            listCache = service.listVideos()
        return listCache

    method getVideoInfo(id) is
        if (videoCache == null || needReset)
            videoCache = service.getVideoInfo(id)
        return videoCache

    method downloadVideo(id) is
        if (!downloadExists(id) || needReset)
            service.downloadVideo(id)
// The GUI class, which used to work directly with a service
// object, stays unchanged as long as it works with the service
// object through an interface. We can safely pass a proxy
// object instead of a real service object since they both
// implement the same interface.
class YouTubeManager is
    protected field service: ThirdPartyYouTubeLib

    constructor YouTubeManager(service: ThirdPartyYouTubeLib) is
        this.service = service

    method renderVideoPage(id) is
        info = service.getVideoInfo(id)
        // Render the video page.

    method renderListPanel() is
        list = service.listVideos()
        // Render the list of video thumbnails.

    method reactOnUserInput() is
        renderVideoPage()
        renderListPanel()
// The application can configure proxies on the fly.
class Application is
    method init() is
        aYouTubeService = new ThirdPartyYouTubeClass()
        aYouTubeProxy = new CachedYouTubeClass(aYouTubeService)
        manager = new YouTubeManager(aYouTubeProxy)
        manager.reactOnUserInput()

Applicability

프록시 패턴을 사용하기 위한 수십개의 방법이 있습니다.

  • 지연 초기화(가상 프록시). 이것은 비록 너가 때때로 사용할 지라도, 항상 가동되어 시스템 자원을 낭비하는 heavyweight 서비스 객체가 있는 경우입니다. → 앱이 시작될 때 객체를 생성하는 대신에, 객체의 초기화를 그것이 진짜 필요한 시점으로 지연시킬 수 있다.
  • 접근 제어(보호 프록시). 이것은 특정 사용자들이 서비스 객체를 사용할 수 있도록 하는 경우입니다; 예를 들어, 너의 객체가 운영체제의 중요한 부분이고 클라이언트가 다양한 실행된 응용 프로그램일 경우(악의적인 것들을 포함하여) → 프록시는 클라이언트의 자격 증명이 몇몇 기준과 일치하는 경우에만 서비스 객체에 요청을 전달할 수 있다.
  • 원격 서비스의 지역적 실행(remote proxy). 서비스 객체가 원격 서버에 있는 경우입니다. → 이 경우에, 프록시는 네트워크 작업의 모든 불쾌한 세부 사항을 처리하면서 네트워크를 통해 클라이언트 요청을 전달한다.
  • 로깅 요청(logging proxy). 서비스 객체에 대해 요청의 기록을 유지하기를 원하는 경우입니다. → 프록시는 서비스로 전달되기 전 각각의 요청을 로그할 수 있습니다.
  • 캐싱 요청 결과(caching proxy). 클라이언트 요청의 결과를 캐시하는 것이 필요하고 이 캐시의 생명 주기를 관리해야하는 경우, 특히 결과가 꽤 크다면 → 프록시는 항상 같은 결과를 생성하는 반복되는 요청을 캐싱하는 것을 구현할 수 있습니다. 프록시는 캐시 키로써 요청의 파라미터를 사용할지도 모릅니다.
  • 스마트 참조. heavyweight 객체를 사용하는 클라이언트가 없을 때 해제할 수 있어야 하는 경우입니다. → 프록시는 서비스 객체에 대한 참조나 그것의 결과를 획득했던 클라이언트를 추적할 수 있습니다. 때때로, 프록시는 클라이언트를 넘어서, 그들이 여전히 작동 중인지를 체크할지도 모릅니다. 만약 클라이언트 리스트가 비어 있는 경우, 프록시는 서비스 객체를 해체하고 기본 시스템 자원을 해체할지도 모릅니다.

How to Implement

  1. 기존 서비스 인터페이스가 없다면, 프록시와 서비스 객체를 상호교환가능하게 만들기 위해 생성합니다. 서비스 클래스로부터 인터페이스를 추출하는 것은 항상 가능하지 않습니다 왜냐하면 해당 인터페이스를 사용하기 위해 모든 서비스의 클라이언트들을 교환할 필요가 있기 때문에. Plan B는 프록시를 서비스 클래스의 하위 클래스로 만드는 것이고, 서비스의 인터페이스를 상속하는 방식으로.
  2. 프록시 클래스를 생성합니다. 서비스에 참조를 저장하는 것에 대한 필드를 가지고 있어야만 합니다. 대게, 프록시들은 그들의 서비스의 전체 생명 주기를 생성하고 관리합니다. 드문 경우이지만, 한 서비스가 클라이언트에 의한 생성자를 통해 프록시에 전달합니다.
  3. 그들의 목적에 따라서 프록시 메서드를 구현합니다. 대부분의 경우에, 몇몇 작업들을 수행한 후, 프록시는 일을 서비스 객체에 위임해야만 합니다.
  4. 클라이언트가 프록시나 진짜 서비스를 얻는지 아닌지 결정하는 생성 메서드를 소개하는 것을 고려해보자. 이것은 프록시 클래스나 본격적인 팩터리 메서드에 있어서 간단한 정적 메서드일 수 있습니다.
  5. 서비스 객체에 대한 지연 초기화를 구현하는 것을 고려해보자.

Pros and Cons

(pros)

  • 클라이언트가 알지 못하는 상태에서 서비스 객체를 제어할 수 있다.
  • 클라이언트가 신경 쓰지 않을 때 서비스 개체의 생명 주기를 관리할 수 있다.
  • 프록시는 서비스 객체가 준비되지 않았거나 사용할 수 없는 경우에도 작동한다.
  • 개방/폐쇄 원칙. 서비스나 클라이언트를 변경하는 것 없이 새로운 프록시를 도입할 수 있다.

(cons)

  • 많은 새로운 클래스를 도입했기 때문에 코드가 좀 더 복잡해질 수 있다.
  • 서비스로부터 온 응답이 지연될 수 있다.

0개의 댓글