프로토타입 패턴(Prototype Pattern)

 

※ Prototypal : 원형

 

인스턴스를 복제해서 새로운 객체를 생성하는 방식의 디자인 패턴

쉽게 말하면 이미 만들어져있는 객체 A가 있을때, A 원형 객체를 복제해서 새로운 객체를 생성하는 것

 

게임 프로그래밍쪽에서 시작된 개념인듯 하며 현대에는 프로토타입 패턴은 그다지 효율적인 방법으로 생각되지는 않는다.

아래의 예제도 읽다보면 굳이 이렇게 해야하나? 라는 생각과 함꼐 다른 방법들이 떠오를 것이다.

 

그래도 알아는 두면 좋은점
1) 객체의 현재 상태까지 복제한다는 점
  - 생성자로 생성시 초기설정값으로 멤버변수값이 정해지겠지만 현재 객체의 멤버변수값까지 복사가능
2) 복제하는 스포너는 객체 내부의 멤버함수이면 안된다는 사실
  - 멤버가 자기자신을 복제하는것이 아니라 스포너가 복제하는 것이므로 아예 새로운 스포너 클래스를 만들어야 한다는 사실

 

 

 

프로토타입 패턴이 없다면?

 

다양한 몬스터가 나오는 게임이 있다고 할 때 각 몬스터마다 하나의 클래스로 만들 수 있다.

Ghost, Demon, Sorcerer 3종류의 몬스터가 있다면 

class Monster
{
	// ...
};
class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};

위처럼 몬스터를 구현할 수 있을것이다.

 

이제 몬스터를 젠시켜주는 스포너(Spawner)를 만들려 한다.  
전통적인 클래스의 기준에 맞게 한 가지의 스포너는 한 가지 몬스터 인스턴스만 만든다.  
다양한 몬스터 종류를 한번에 스폰해야 할 경우도 있으므로 Monster 클래스의 생성자를 호출하는 방식이 아니라 스포너 객체를 따로 두고 스포너 객체가 스폰을 담당하게 구현한다.

class Spawner
{
public:
	virtual ~Spawner();
	virtual Monster* spawnMonster() = 0;
};

class GhostSpawner : public Spawner
{
public:
	virtual Monster* spawnMonster()	{
		return new Ghost();
	}
};

class DemonSpawner : public Spawner
{
public:
	virtual Monster* spawnMonster()	{
		return new Demon();
	}
};

 

게임에 나오는 모든 몬스터를 지원하려면 모든 몬스터클래스와, 모든 몬스터스포너클래스가 필요해질것이다.

즉 모든 몬스터종류는 Monster 클래스를 상속받고, 모든 스포너종류는 Spawner클래스를 상속받는 구조가 된다.

 

위처럼 작성된 코드는 클래스도 많고, 반복되는 코드도 많다.

몬스터의 종류가 수천 수만가지라면 클래스는 너무 많아지고, 코드도 엄청나게 길어질것이고, 비효율적일것이다.

 

 

 

프로토타입 패턴을 사용한다면?

 

어떤 객체가 자신과 비슷한 객체를 스폰하게 할 수 있다.

위 예를 가져온다면 Ghost객체 하나로 다른 Ghost객체를 여렇 만들 수 있다.

 

이를 구현하기 위해 Monster 클래스에 clone() 메서드를 추가한다.

추가되는 위치가 Monster인 이유는 몬스터객체가 복제되는 것이므로

반면 몬스터가 스폰을 하지 않았던 이유는 스폰한 결과물이 몬스터인것이지, 몬스터가 있고나서 스폰이 있는것이 아니므로

class Monster
{
public: 
	virtual ~Monster() {}
	virtual Monster* clone() = 0;

	// ...
};

class Ghost : public Monster
{
private:
	int _health;
	int _speed;
    
public:
	Ghost(int health, int speed)
		: _health(health), _speed(speed) {}
	
    virtual Monster* clone() {
		return new Ghost(_health, _speed);
	}    
};

 

몬스터마다 clone 메서드가 있으므로 스포너 클래스는 몬스터 종류마다 만들 필요 없이 clone()을 호출해줄 스포너 하나만 있으면 된다.

class Spawner
{
private:
	Monster* _prototype;
    
public:
	Spawner(Monster* prototype) : _prototype(prototype) {}

	Monster* spawnMonster()	{
		return _prototype->clone();
	}
};

스포너 클래스 내부의 prototype_에 들어있는 몬스터 객체는 prototype으로써 자기와 같은 몬스터 객체를 찍어내는 스포너 역할만 한다.

Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);

 

 

 

장단점과 문제점

 

1) 장점

- 프로토타입의 클래스 뿐 아니라 상태까지 같이 복제를 함

- 원형으로 사용할 유령 객체만 잘 설정해두면 빠른 유령, 약한 유령 등의 스포너도 쉽게 만들 수 있다.

- 유닛마다 모두 Spawner 클래스를 만들 필요가 없음

 

2) 단점

- clone 메서드를 구현하게 되므로 코드의 양이 줄어들지도 않음

- clone()으로 복제시에 깊은복사를 할지 얕은복사를 할지 애매할때가 많음

 

3) 문제점

1) 현실성

위 예제만 봐도 현실적이지가 않음, 그냥 읽으면서도 굳이 이렇게 해야하나?라는 생각이 듬

 

2) 코드의 양

프로토타입을 사용함으로써 몬스터마다 스포너 클래스를 만들지 않아도 되므로 클래스의 양이 절반수준으로 줄어들었지만 clone()함수를 구현해야 하므로 코드의 전체적인 양을 크게 다르지 않다.

 

3) 클래스의 갯수

또한 현대의 게임엔진에서는 몬스터마다 클래스를 따로 만들지 않는다.

클래스 상속 구조가 복잡하면 유지보수가 힘들어진다.

 

 

 

더 효율적인 방법들

 

1) 데이터 모델링

현대 게임의 용량은 대부분 코드보다는 데이터가 차지한다.

현대의 게임에서 코드는 게임을 실행하기 위한 '엔진'일 뿐이며, 게임의 콘텐츠는 모두 데이터에 정의되어 있다.

즉, 몬스터 하나마다 클래스를 구현하지 않는다.

데이터를 모델링하는 기법 중 하나로 JSON이 있으며 아래는 데이터를 저장하는 예시이다.

{
    "이름": "고블린 보병",
    "체력": 20,
    "내성": ["추위", "독"],
    "약점": ["불", "빛"]
}

위처럼 데이터는 데이터로, 코드는 엔진으로 분리하는것이 더 효율적이다.

 

2) 스폰 함수

데이터와 코드를 분리했다면 이제 몬스터마다 클래스를 만들어줄 필요도 없어졌다.

즉 Ghost 클래스는 필요없어졌고 단지 Ghost를 스폰해줄 스포너와, Ghost 전용 스폰함수 하나면 충분하다.

Monster* spawnGhost() {
    return new Ghost();
}
// Ghost 클래스를 만들 필요 없이 데이터는 이미 따로 저장되어 있으므로 spawn 함수만 있으면 된다.
typedef Monster* (*SpawnCallback)();

class Spawner {
public:
    Spawner(SpawnCallback spawn) : spawn_(spawn) {}
    Monster* spawnMonster() { return spawn_(); }

private:
    SpawnCallback spawn_;
};
// 유령을 스폰하는 객체는 이렇게 만들 수 있다.
Spawner* ghostSpawner = new Spawner(spawnGhost);

// 스폰은 이렇게
ghostSpawner.spawnMonster();

몬스터마다의 클래스도 없어졌고, 스포너 클래스도 하나밖에 없다. 클라이언트 코드에서도 사용이 간단하다.

 

3) 템플릿

사실 2번의 스폰함수도 필요없다. Ghost가 들어갈 부분은 template 클래스로 만들면 되므로

class Spawner {
public:
    virtual ~Spawner() {}
    virtual Monster* spawnMonster() = 0;
};

template <class T>
class SpawnerFor : public Spawner {
public:
    virtual Monster* spawnMonster() {
        return new T();
    }
};
Spawner* ghostSpawner = new SpawnerFor<Ghost>();

 

이외에도 아주 많은 방법이 있겠지만 상황마다, 언어마다 다 다르지 않을까 싶다.

 

 

 

 

 

 

※ 참고문헌

https://boycoding.tistory.com/108

https://lipcoder.tistory.com/35

http://gameprogrammingpatterns.com/prototype.html