프락시 패턴(Proxy Pattern)
프록시 패턴도 데커레이터 패턴처럼 어떤 객체의 기능을 수정/확장한다는 목적이 비슷합니다.
기존 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);
}
이 코드도 앞서의 로컬 버젼과 동일한 결과를 출력합니다.
요약
데커레이터 패턴과는 다르게 프록시 패턴은 객체에 새로운 멤버를 추가하여 기능을 확장하지 않습니다.
프락시는 단지 이미 존재하는 멤버들의 동작을 목적에 맞게 변형합니다.