개발/Architecture & Design Pattern

C++ - 프록시 패턴 (Proxy Pattern)

피로물든딸기 2024. 2. 28. 16:51
반응형

C, C++ 전체 링크

Architecture & Design Pattern 전체 링크

 

참고

- 클래스 다이어그램 그리기

- 변수 변경시 이벤트 발생 (C#)

 

프록시 패턴 (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)를 이용할 수 있다.

 

ProxyRealSubject는 같은 인터페이스를 구현하기 때문에

클라이언트ProxyRealSubject를 동일하게 처리할 수 있다.

 

 

먼저 인터페이스를 만든다. 

// 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 
```
반응형