C++ - 프록시 패턴 (Proxy Pattern)
Architecture & Design Pattern 전체 링크
참고
프록시 패턴 (Proxy Pattern) - 구조 패턴
- 다른 객체에 대한 접근을 제어하거나 중재하는 역할을 하는 대리자(Proxy)를 제공하는 패턴
- 원본 객체에 대한 접근을 제어, 보안 검사, 로깅, 캐싱, 지연 로딩 등의 추가 기능을 제공할 수 있다.
구현
- Subject : 프록시와 실제 서비스에 대한 공통 인터페이스를 제공, 프록시를 실제 서비스와 동일하게 처리
- Real Subject : 프록시가 접근을 제어하거나 기능을 추가하는 대상, 실제 서비스 객체
- Proxy : 요청을 중개하고 제어하는 객체, 클라이언트는 프록시를 통해 실제 서비스에 접근
장점
- OCP(개방-폐쇄 원칙), Subject나 클라이언트 코드 변경없이 새로운 프록시를 추가할 수 있다.
- 프록시를 통해 보안 검사, 로깅, 트랜잭션 관리, 오류 처리 등의 추가 기능을 제공할 수 있다.
- 비용이 많이 드는 작업을 필요에 따라 지연시켜 성능을 향상할 수 있다.
- 결과를 캐싱하거나 중복된 요청을 제거해서 성능을 향상할 수 있다.
단점
- 프록시 객체를 추가로 정의하고 관리해야 하기 때문에 코드의 복잡성이 증가한다.
- 객체에 대한 접근을 중개하는 과정에서 성능이 저하될 수 있다.
- 프록시 객체에 대한 접근으로 추가적인 오버헤드가 발생할 수 있다.
이미지 로딩 API
RealImage 클래스는 이미지 이름을 받아서 이미지를 불러오는 API를 제공한다.
다만 API 성능이 떨어져서 약 5초의 로딩이 필요하다.
class RealImage
{
public:
RealImage(const string& fn) : filename(fn) { loadImageFromAPI(); }
void display() { cout << "Displaying Image : " << filename << endl; }
void loadImageFromAPI()
{
cout << "Load Image with API..." << endl;
this_thread::sleep_for(chrono::seconds(5)); // 이미지 로딩 시간
cout << "Loading " << filename << " from API" << endl;
}
private:
string filename;
};
image1.jpg를 불러온다고 가정하자.
실제 상황이라면 이미지를 완전히 불러오지 않더라도 버튼을 누르거나 스크롤을 하는 경우가 발생한다.
RealImage* image1 = new RealImage("image1.jpg");
image1->display();
cout << "Now, You can do another job..." << endl;
하지만 API 로딩 시간이 매우 길어 5초 동안 another job을 할 수 없다.
예시 코드는 다음과 같다.
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace std;
class RealImage
{
public:
RealImage(const string& fn) : filename(fn) { loadImageFromAPI(); }
void display() { cout << "Displaying Image : " << filename << endl; }
void loadImageFromAPI()
{
cout << "Load Image with API..." << endl;
this_thread::sleep_for(chrono::seconds(5)); // 이미지 로딩 시간
cout << "Loading " << filename << " from API" << endl;
}
private:
string filename;
};
int main()
{
cout << "Test Start" <<endl;
RealImage* image1 = new RealImage("image1.jpg");
image1->display();
cout << "Now, You can do another job..." << endl;
return 0;
}
가상 프록시 (Virtual Proxy)
프록시 패턴을 이용해서 위의 상황을 해결할 수 있다.
현재 RealImage 클래스는 이미지를 로딩하는데 비용이 많이 들고 있다.
따라서 가상 프록시(Virtual Proxy)를 이용할 수 있다.
Proxy와 RealSubject는 같은 인터페이스를 구현하기 때문에
클라이언트는 Proxy와 RealSubject를 동일하게 처리할 수 있다.
먼저 인터페이스를 만든다.
// Subject Interface
class Image
{
public:
virtual ~Image() {}
virtual void display() = 0;
};
RealImage 클래스는 Image 인터페이스를 구현하도록 수정한다.
display 뒤에 override 키워드를 추가하였다.
// Real Subject
class RealImage : public Image
{
public:
RealImage(const string& fn) : filename(fn) { loadImageFromAPI(); }
void display() override { cout << "Displaying Image : " << filename << endl; }
...
};
ImageProxy는 다음과 같이 구현하였다.
Image 인터페이스에서 구현해야 하는 display에서 스레드를 이용하여 작업을 처리하면,
이미지가 로딩이 되지 않더라도, another job을 할 수 있다.
realImage가 존재하지 않으면 스레드로 이미지를 로드하였다.
class ImageProxy : public Image
{
private:
string filename;
RealImage* realImage;
thread* loadingThread;
public:
ImageProxy(const string& fn) : filename(fn), realImage(nullptr), loadingThread(nullptr) {}
void display() override
{
if (realImage == nullptr) // 이미지 로딩은 스레드에서 처리
loadingThread = new thread(&ImageProxy::loadImage, this);
else
realImage->display();
}
void loadImage()
{
realImage = new RealImage(filename);
realImage->display();
}
~ImageProxy()
{
if (loadingThread != nullptr)
{
loadingThread->join();
delete loadingThread;
}
if (realImage != nullptr) delete realImage;
}
};
프록시 패턴을 적용한 코드를 실행해 보자.
#include <iostream>
#include <string>
#include <thread>
#include <chrono>
using namespace std;
// Subject Interface
class Image
{
public:
virtual ~Image() {}
virtual void display() = 0;
};
// Real Subject
class RealImage : public Image
{
public:
RealImage(const string& fn) : filename(fn) { loadImageFromAPI(); }
void display() override { cout << "Displaying Image : " << filename << endl; }
void loadImageFromAPI()
{
cout << "Load Image with API..." << endl;
this_thread::sleep_for(chrono::seconds(5)); // 이미지 로딩 시간
cout << "Loading " << filename << " from API" << endl;
}
private:
string filename;
};
class ImageProxy : public Image
{
private:
string filename;
RealImage* realImage;
thread* loadingThread;
public:
ImageProxy(const string& fn) : filename(fn), realImage(nullptr), loadingThread(nullptr) {}
void display() override
{
if (realImage == nullptr) // 이미지 로딩은 스레드에서 처리
loadingThread = new thread(&ImageProxy::loadImage, this);
else
realImage->display();
}
void loadImage()
{
realImage = new RealImage(filename);
realImage->display();
}
~ImageProxy()
{
if (loadingThread != nullptr)
{
loadingThread->join();
delete loadingThread;
}
if (realImage != nullptr) delete realImage;
}
};
int main()
{
cout << "Test Start" <<endl;
Image* image1 = new ImageProxy("image1.jpg");
image1->display();
cout << "Now, You can do another job..." << endl;
for (int i = 1; i <= 10; i++) // just for timer
{
cout << "Waiting " << i << " seconds..." << endl;
this_thread::sleep_for(chrono::seconds(1));
}
return 0;
}
이제 이미지를 로드하더라도, 즉시 다른 일을 할 수 있다.
속성 프록시 (Property Proxy)
C#과 같은 언어에서는 get, set 메서드(property)에서 변수 변경을 감지할 수 있는 기능을 제공한다.
C++은 기능이 없기 때문에 속성 프록시를 이용해서 프로퍼티를 만들면 된다.
template <typename T>
struct Property
{
T value;
Property(const T v)
{
cout << "init : " << v << endl;
*this = v;
}
operator T()
{
cout << "getValue : " << value << endl;
return value;
}
T operator=(T v)
{
cout << "setValue : " << v << endl;
return value = v;
}
};
아래와 같이 변수 타입에 Property로 타입이 변환되도록 하면 된다.
class Product
{
public:
Product(const string& n, int p) : name(n), price(p) {}
...
private:
Property<string> name;
Property<int> price;
};
이제 변수를 get / set 할 때, 로그가 출력된다.
전체 코드는 다음과 같다.
#include <iostream>
#include <string>
using namespace std;
template <typename T>
struct Property
{
T value;
Property(const T v)
{
cout << "init : " << v << endl;
*this = v;
}
operator T()
{
cout << "getValue : " << value << endl;
return value;
}
T operator=(T v)
{
cout << "setValue : " << v << endl;
return value = v;
}
};
class Product
{
public:
Product(const string& n, int p) : name(n), price(p) {}
void setPrice(int p) { price = p; }
int getPrice() { return price; }
void setName(const string& n) { name = n; }
string getName() { return name; }
private:
Property<string> name;
Property<int> price;
};
int main(void)
{
Product* product = new Product("blood", 5000);
cout << product->getName() << " : " << product->getPrice() << endl;
product->setName("strawberry");
product->setPrice(10000);
cout << product->getName() << " : " << product->getPrice() << endl;
return 0;
}
프록시 종류
원격 (Remote) - 원격 서버에 있는 객체에 대해 로컬 인터페이스를 제공하여 원격 서버와 통신을 추상화한다.
가상 (Virtual) - 비용이 많이 드는 작업을 지연시키는 가상 객체를 제공하여 성능을 향상시킨다.
속성 (Property) - 객체에 접근하는 방법을 추상화하여 추가 기능을 제공한다.
보호 (Protection) - 클라이언트가 객체에 대한 접근을 제어하여 보안 검사를 수행하거나 권한을 부여한다.
방화벽 (Firewall) - 클라이언트와 서버 사이에서 보안 규칙에 따라 트래픽을 필터링하고 모니터링한다.
스파트 레퍼런스 (Smart Reference) - 객체에 대한 참조를 제어하고, 필요한 경우에만 객체를 생성하거나 로드한다.
캐싱 (Caching) - 이전에 수행한 작업의 결과를 캐시에 저장하여 중복 요청을 처리할 때, 캐시된 결과를 반환한다.
스마트 포인터 또한 일반 포인터에 프록시를 적용한 형태가 된다.
- 스마트 포인터는 일반 포인터와 같은 방식으로 사용된다. (동일 인터페이스)
- 스마트 포인터가 포인터 참조 횟수, 메모리 관리를 자동화한다. (추가 기능)
ClassDiagram.md
```mermaid
classDiagram
class Subject {
request()*
}
class RealSubject {
request()
}
class Proxy {
request()
}
Subject <|.. RealSubject
Subject <|.. Proxy
```