프록시 패턴도 데커레이터 패턴처럼 어떤 객체의 기능을 수정/확장한다는 목적이 비슷합니다.
기존 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);
}
이 코드도 앞서의 로컬 버젼과 동일한 결과를 출력합니다.
요약
데커레이터 패턴과는 다르게 프록시 패턴은 객체에 새로운 멤버를 추가하여 기능을 확장하지 않습니다.
프락시는 단지 이미 존재하는 멤버들의 동작을 목적에 맞게 변형합니다.
'프로그래밍 일반 > 디자인 패턴' 카테고리의 다른 글
반복자 패턴(Iterator Pattern) (0) | 2020.03.04 |
---|---|
책임 사슬(Chain of Responsibility Pattern) (0) | 2020.03.04 |
플라이웨이트 패턴(Flyweight Pattern) (0) | 2020.01.28 |
퍼사드 패턴(Façade Pattern) (0) | 2020.01.28 |
데커레이터 패턴(Decorator Pattern) (0) | 2020.01.28 |