프로그래밍 일반/디자인 패턴

프락시 패턴(Proxy Pattern)

지노윈 2020. 3. 4. 17:24
반응형

프록시 패턴도 데커레이터 패턴처럼 어떤 객체의 기능을 수정/확장한다는 목적이 비슷합니다.

기존 API의 사용 방식을 정확히 동일하게 유지하면서 그 내부 동작만 다르게 한다는 점에서 다릅니다.

같은 API에 대해서 서로 다른 종류의 서로 다른 목적의 완전히 다른 프로시들이 여러 개발자에 의해서 만들어질 수 있습니다.

 

스마트 포인터

가장 단순하면서도 직접적인 프록시 패턴의 예는 스마트 포인터입니다.

스마트 포인터는 일반적인 포인트를 사용할 때와 완전히 동일한 방식으로 사용할 수 있습니다. 즉, 보통의 포인터가 가진 인터페이스를 유지합니다.

일반 포인터의 인터페이스를 그대로 유지하면서 다른 목적의 기능이 구현되었습니다.

속성 프록시

C++에서 어떤 필드에 필별히 지정된 접근자/변경자를 부여하고 싶다면 속성 프록시를 만듭니다.

속성 프록시 구현은 다음과 같습니다.

template <typename T> struct Property
{
  T value;
  Property(const T initialValue)
  {
    *this = initialValue;
  }
  operator T()    // get 작업 수행
  {
    return value;
  }
  T operator =(T newValue)    // set 작업 수행
  {
    return value = newValue;
  }
};
 
struct Creature
{
  Property<int> strength{ 10 };
  Property<int> agility{ 5 };
};

이 프록시 변수들을 아래와 같이 일반 필드 변수처럼 사용 할 수 있습니다.

void property_proxy()
{
  Creature creature;
  creature.agility = 20;
  cout << creature.agility << endl;
}

가상 프록시

느긋한 인스턴스화(Lazy instantiation)를 할때 인스턴스화 해야할 정확한 시점을 모른다면 실제 객체를 대리하는 프록시를 만들어 Lazy 동작을 하게 할 수 있습니다.

실제 존재하지 않는 객체를 나타내기 때문에 이러한 프록시를 가상 프록시라 부릅니다.

 

다음 코드는 Bitmap을 바로 로딩하는 코드입니다.

struct Image
{
  virtual void draw() = 0;
};
 
struct Bitmap : Image
{
  Bitmap(const string& filename)
  {
    cout << "Loading image from " << filename << endl;
  }
 
  void draw() override
  {
    cout << "Drawing image" << endl;
  }
};

객체 생성과 동시에 그림 파일을 로딩합니다.

Bitmap img{"pokemon.png"};

실제 그림을 그리는 draw() 메서드가 호출될 때 그림 파일이 로딩되길 원합니다.

Lazy 동작 방식으로 바꾸고 싶지만 Bitmap이 외부 라이브러리여서 코드를 수정할 수 없다고 가정합니다.

그리고 다른 여러 가지 이유로 상속을 이용할 수도 없다고 가정합니다.

 

이런 상황에서 가상 프록시를 활용할 수 있습니다.

struct LazyBitmap : Image
{
  LazyBitmap(const string& filename): filename(filename) {}
  ~LazyBitmap() { delete bmp; }
  void draw() override
  {
    if (!bmp)
      bmp = new Bitmap(filename);
    bmp->draw();
  }
 
private:
  Bitmap* bmp{nullptr};
  string filename;
};
 
void draw_image(Image& img)
{
  cout << "About to draw the image" << endl;
  img.draw();
  cout << "Done drawing the image" << endl;
}

다음과 같이 Lazy 동작 방식으로 그림을 로딩하고 그릴 수 있다.

LazyBitmap img{ "pokemon.png" };
draw_image(img); // 그림 파일의 로딩은 여기서 일어난다.

커뮤니케이션 프록시

동일 컴퓨터에서 동작하던 객체를 네트워크로 연결된 다른 컴퓨터로 옮길 경우 이전과 동일하게 동작 시킬때 필요한 것이 커뮤니케이션 프록시 입니다.

 

다음은 하나의 프로세스에서의 핑-퐁 구현입니다.

struct Pingable
{
  virtual wstring ping(const wstring& message) = 0;
};
 
struct Pong : Pingable
{
  wstring ping(const wstring& message) override
  {
    return message + L" pong";
  }
};

하나의 프로세스 안에서 핑-퐁은 다음과 같이 사용할 수 있습니다.

void tryit(Pingable& pp)
{
  wcout << pp.ping(L"ping") << "\n";
}
 
Pong pp;
for (int i = 0; i < 3; ++i)
{
  tryit(pp);
}

"ping pong"이 세 번 출력됩니다.

 

이제 핑 서비스를 멀리 떨어진 웹 서버로 옮기기로 결정하였습니다.

옮겨간 컴퓨터에서는 C++대신 ASP.NET 플랫폼을 사용합니다.

[Route("api/[controller]")]
public class PingPongController : Controller
{
  [HttpGet("{msg}")]
  public string Get(string msg)
  {
     return msg + " pong";
  }
}

이러한 상황에서 이용될 수 있는 커뮤니케이션 프록시 RemotePong을 만들어보자.

struct RemotePong : Pingable
{
  wstring ping(const wstring& message) override
  {
    wstring result;
    http_client client(U("http://localhost:9149/"));
    uri_builder builder(U("/api/values/"));
    builder.append(message);
    auto task = client.request(methods::GET, builder.to_string())
      .then([=](http_response r)
    {
      return r.extract_string();
    });
    task.wait();
    return task.get();
  }
};

이러한 구현을 기반으로 하여 다음과 같이 사용자 코드를 딱 한 곳만 수정합니다.

RemotePong pp;      // Pong에서 RemotePong으로 변경
for (int i = 0; i < 3; ++i)
{
  tryit(pp);
}

이 코드도 앞서의 로컬 버젼과 동일한 결과를 출력합니다.

 

요약

데커레이터 패턴과는 다르게 프록시 패턴은 객체에 새로운 멤버를 추가하여 기능을 확장하지 않습니다.

프락시는 단지 이미 존재하는 멤버들의 동작을 목적에 맞게 변형합니다.