개발/Architecture & Design Pattern

C++ - 프로토타입 패턴 (Prototype Pattern)

피로물든딸기 2024. 2. 29. 21:13
반응형

C, C++ 전체 링크

Architecture & Design Pattern 전체 링크

 

참고

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

- 복사 생성자를 이용한 객체의 깊은 복사

 

프로토타입 패턴 (Prototype Pattern) - 생성 패턴

- 초기 객체 생성 비용이 많이 드는 경우 사용하는 패턴

- 객체를 생성할 때 기존 객체의 복사를 통해 생성한다.

- 생성할 객체의 유형에 대한 세부 정보를 숨기면서 복잡한 객체를 만들 수 있는 방법을 제공한다.

 

구현

- Prototype : 복제될 객체의 인터페이스를 정의, 객체를 복사하는 메서드를 제공한다.

- ConcretePrototype : 복사될 때 생성될 새로운 객체의 초기 상태를 가지게 된다.

- 클라이언트는 Concrete Class의 구체적인 내용을 몰라도 객체를 복사할 수 있다.

 

장점

- 객체를 생성할 때, 기존 객체를 복사하기 때문에 간단하게 객체를 생성한다.

- 클라이언트가 객체 생성에 필요한 정보를 구체적으로 알 필요가 없다.

- 최초 객체 생성 비용이 많은 경우, 복사를 통해 객체를 만드는 것이 효율적이다.

 

단점

- 객체가 다른 객체들과 복잡한 관계에 있다면, 객체의 복사 기능 구현이 복잡할 수 있다.


생성 비용이 큰 객체

 

아래와 같이 어떤 클래스는 객체를 생성하는데 3초 정도의 시간이 걸린다.

class SomethingClass
{
public:
	SomethingClass(int value) : id(value)
	{
		cout << "Create Start!" << endl;
		for (int i = 1; i <= 3; i++) // 
		{
			cout << "Waiting " << i << " seconds..." << endl;
			this_thread::sleep_for(chrono::seconds(1));
		}
		cout << "Create End." << endl;
	}
	
	void print() const { cout << "SomethingClass " << id << endl; }
	void setID(int value) { id = value; }

private:
	int id;
};

 

이런 객체가 많이 필요하다면, 매번 객체를 생성하는 것은 매우 비효율적인 작업이 될 수 있다.


프로토타입 패턴 적용

 

프로토타입 패턴은 복제될 객체의 인터페이스를 구현하도록 해서, 객체를 복사하는 메서드를 제공한다.

클라이언트는 구체적인 클래스를 알지 못해도 객체를 쉽게 복사할 수 있다.

단, 프로토타입 패턴은 이미 존재하는 객체를 복사하는 것이지, 객체를 생성하는 비용을 줄이는 것은 아니다.

 

프로토타입 인터페이스는 아래와 같이 정의할 수 있다.

여기서 깊은 복사(Deep Copy)가 잘 이루어지는지 확인하기 위해  setID를 추가하였다.

class Prototype
{
public:
	virtual Prototype* clone() = 0;
	virtual void print() = 0;
	virtual void setID(int value) = 0;
	virtual ~Prototype() {}
};

 

ConcretePrototype에서는 복사 생성자를 이용해 객체를 복사하였다.

그리고 프로토타입 인터페이스의 clone을 구현해서 클라이언트가 사용하도록 한다.

class ConcretePrototype : public Prototype
{
public:
	ConcretePrototype(int value) : id(value)
	{
		cout << "Create Start!" << endl;
		for (int i = 1; i <= 3; i++) // 
		{
			cout << "Waiting " << i << " seconds..." << endl;
			this_thread::sleep_for(chrono::seconds(1));
		}
        
		cout << "Create End." << endl;
	}

	ConcretePrototype(ConcretePrototype& other) : id(other.id) { cout << "Copy" << endl; }

	Prototype* clone() override { return new ConcretePrototype(*this); }
	void print() override { cout << "ConcretePrototype " << id << endl; }
	void setID(int value) override { id = value; }

private:
	int id;
};

 

여러 가지 프로토타입 객체 (ex. Monster, NPC, ...) 가 존재할 수 있다.

따라서 여기서는 PrototypeManager를 이용해서 여러 객체를 관리한다.

 

map을 이용해서 생성 비용이 많이 드는 초기 객체들을 관리하도록 한다.

class PrototypeManager
{
public:
	static void registerPrototype(string name, Prototype* prototype) { ptMap[name] = prototype; }
	static Prototype* getPrototype(string name) { return ptMap[name]->clone(); }

private:
	static map<string, Prototype*> ptMap;
};

int main()
{
	PrototypeManager::registerPrototype("Monster", new ConcretePrototype(1));
	PrototypeManager::registerPrototype("NPC", new ConcretePrototype(2));
    
	...

 

초기 객체는 PrototypeManager에 등록하고, 각 객체를 PrototypeManager를 이용해 생성해 보자.

그리고 ID를 변경해서 각 객체의 ID가 고유한 값을 유지하는지 확인해 보자.

int main()
{
	PrototypeManager::registerPrototype("Monster", new ConcretePrototype(1));
	PrototypeManager::registerPrototype("NPC", new ConcretePrototype(2));

	Prototype* prototypeA = PrototypeManager::getPrototype("Monster");
	Prototype* prototypeB = PrototypeManager::getPrototype("NPC");

	cout << "\nPrint A, B" << endl;
	cout << "A : "; prototypeA->print();
	cout << "B : "; prototypeB->print();

	cout << "\nCreate C, D" << endl;
	Prototype* prototypeC = PrototypeManager::getPrototype("Monster");
	Prototype* prototypeD = PrototypeManager::getPrototype("NPC");

	cout << "\nSet ID" << endl;
	prototypeC->setID(100);
	prototypeD->setID(200);

	cout << "\nPrint A, B, C, D" << endl;
	cout << "A : "; prototypeA->print();
	cout << "B : "; prototypeB->print();
	cout << "C : "; prototypeC->print();
	cout << "D : "; prototypeD->print();

	return 0;
}

 

이제 초기에 생성된 객체에 대해서만 비용이 발생하고, 해당 객체의 복사는 즉시 이루어진다.

 

전체 코드는 다음과 같다.

#include <iostream>
#include <string>
#include <map>
#include <thread>
#include <chrono>

using namespace std;

class Prototype
{
public:
	virtual Prototype* clone() = 0;
	virtual void print() = 0;
	virtual void setID(int value) = 0;
	virtual ~Prototype() {}
};

class ConcretePrototype : public Prototype
{
public:
	ConcretePrototype(int value) : id(value)
	{
		cout << "Create Start!" << endl;
		for (int i = 1; i <= 3; i++) // 
		{
			cout << "Waiting " << i << " seconds..." << endl;
			this_thread::sleep_for(chrono::seconds(1));
		}

		cout << "Create End." << endl;
	}

	ConcretePrototype(ConcretePrototype& other) : id(other.id) { cout << "Copy" << endl; }

	Prototype* clone() override { return new ConcretePrototype(*this); }
	void print() override { cout << "ConcretePrototype " << id << endl; }
	void setID(int value) override { id = value; }

private:
	int id;
};

class PrototypeManager
{
public:
	static void registerPrototype(string name, Prototype* prototype) { ptMap[name] = prototype; }
	static Prototype* getPrototype(string name) { return ptMap[name]->clone(); }

private:
	static map<string, Prototype*> ptMap;
};

map<string, Prototype*> PrototypeManager::ptMap;

int main()
{
	PrototypeManager::registerPrototype("Monster", new ConcretePrototype(1));
	PrototypeManager::registerPrototype("NPC", new ConcretePrototype(2));

	Prototype* prototypeA = PrototypeManager::getPrototype("Monster");
	Prototype* prototypeB = PrototypeManager::getPrototype("NPC");

	cout << "\nPrint A, B" << endl;
	cout << "A : "; prototypeA->print();
	cout << "B : "; prototypeB->print();

	cout << "\nCreate C, D" << endl;
	Prototype* prototypeC = PrototypeManager::getPrototype("Monster");
	Prototype* prototypeD = PrototypeManager::getPrototype("NPC");

	cout << "\nSet ID" << endl;
	prototypeC->setID(100);
	prototypeD->setID(200);

	cout << "\nPrint A, B, C, D" << endl;
	cout << "A : "; prototypeA->print();
	cout << "B : "; prototypeB->print();
	cout << "C : "; prototypeC->print();
	cout << "D : "; prototypeD->print();

	return 0;
}

ClassDiagram.md

```mermaid
  classDiagram    
    class Prototype {
      clone()*
    }    
    class ConcretePrototypeA {
      clone()
    }
    class ConcretePrototypeB {
      clone()   
    }

    <<interface>> Prototype
    
    Client --> Prototype
    Prototype <|.. ConcretePrototypeA
    Prototype <|.. ConcretePrototypeB 
```
반응형