원형이 되는 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성합니다. (GoF의 디자인 패턴 169p)
위와 같이 <건틀릿> 같은 게임을 만든다고 생각해보자. 몬스터들은 영웅을 잡아먹기 위해 떼지어 다닌다. 이 몬스터 들은 스포너(spawner)를 통해 게임 레벨에 등장하는데, 몬스터 종류마다 스포너가 따로 있다.
게임에 나오는 몬스터마다 Ghost, Demon, Sorcerer 같은 클래스를 만들어보자.
lass Monster {
// 기타 등등..
};
class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public 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();
}
};
// more...
이런 걸 프로토타입 패턴으로 해결할 수 있다. 핵심은 어떤 객체가 자기와 비슷한 객체를 스폰할 수 있다는 점이다. 유령 객체 하나로 다른 유령 객체를 여럿 만들 수 있다. 악마 객체로부터도 다른 악마 객레를 만들 수 있다. 이처럼 어떤 몬스터 객체든지 자신과 비슷한 몬스터 객체를 만드는 원형 객체로 사용할 수 있다.
이를 구현하기 위해, 상위 클래스인 Monster에 추상 메서드 clone() 을 추가한다.
class Monster {
public:
virtual ~Monster() {}
virtual Monster* clone() = 0;
// 그 외..
};
Monster 하위 클래스에서는 자신의 자료형과 상태와 같은 새로운 객체를 반환하도록 clone() 을 구현한다.
class Ghost : public Monster {
public:
Ghost(int health, int speed)
: health_(health),
speed_(speed) {
}
virtual Monster* clone() {
return new Ghost(health_, speed_);
}
private:
int health_;
int speed_;
};
Monster를 상속받는 모든 클래스에 clone 메서드가 있다면, 스포너 클래스를 종류별로 만들 필요 없이 하나만 만들면 된다.
class Spawner {
public:
Spawner(Monster* prototype) : prototype_(prototype) {}
Monster* spawnMonster() {
return prototype_->clone();
}
private:
Monster* prototype_;
};
Spawner 클래스 내부에는 Monster 객체가 숨어 있다. 이 객체는 벌집을 떠나지 않는 여왕벌처럼 자기오 같은 Monster 객체를 도장 찍듯 만들어내는 스포너 역할만 한다.
유령 스포너를 만들려면 원형으로 사용할 유령 인스턴스를 만든 후에 스포너에 전달한다.
Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);
프로토타입 패턴의 좋은 점은 프로토타입의 클래스뿐만 아니라 상태도 같이 복제한다는 점이다. 즉 원형으로 사용할 유령 객체를 잘 설정하면 빠른 유령, 약한 유령, 느린 유령용 스포너같은 것도 쉽게 만들 수 있다.
얼마나 잘 작동하는가?
이제는 몬스터마다 스포너 클래스를 따로 만들지 않아도 되지만 Monster 클래스마다 clone() 을 구현해야 하기 때문에 코드 양은 별반 차이가 없다. 앞에서 봤듯이 프로토타입 패턴을 써도 코드 양이 많이 줄어들지 않는 데다가, 예제부터가 현실적이지 않다. 요즘 나오는 웬만한 게임 엔진에서는 몬스터마다 클래스를 따로 만들지 않는다.
프로그래머들은 클래스 상속 구조가 복잡하면 유지보수하기 힘들다는 걸 체득했기 때문에, 요즘은 개체 종류별로 클래스를 만들기보다는 컴포넌트나 타입 객체로 모델링하는 것을 선호한다.
스폰 함수
앞에서는 모든 몬스터마다 별도의 스포너 클래스가 필요했다. 하지만 모든 일에는 답이 여러개 있다. 다음과 같이 스폰 함수를 만들어보자.
Monster* spawnGhost() {
return new Ghost();
}
몬스터 종류마다 클래스를 만드는 것보다는 행사코드가 훨씬 적다. 이제 스포너 클래스에는 함수 포인터 하나만 두면 된다.
typedef Monster* (*SpawnCallback)();
class Spawner {
public:
Spawner(SpawnCallback spawn) : spawn_(spawn) {}
Monster* spawnMonster() { return spawn_(); }
private:
SpawnCallback spawn_;
};
유령을 스폰하는 객체는 이렇게 만들 수 있다.
Spawner* ghostSpawner = new Spawner(spawnGhost);
스포너 클래스를 이용해 인스턴스를 생성하고 싶지만 특정 몬스터 클래스를 하드코딩하기는 싫다면 몬스터 클래스를 템플릿 타입 매개변수로 전달하면 된다.
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>();
데이터 모델링을 위한 프로토타입
세월이 지날수록 게임 바이너리에서 코드보다 데이터가 차지하는 옹량이 커지고 있다. 요즘 게임에서 코드는 실행하기 위한 '엔진'일 뿐, 게임 콘텐츠는 모두 데이터에 정의되어 있다.
하지만 많은 콘텐츠를 데이터로 옮기면 대규모 프로젝트의 구조 문제가 오히려 어려워진다면 모를까 저절로 해결되지는 않는다. 프로그래밍 언어를 사용하는 이유는 복잡성을 제어할 수 있는 수단을 가지고 있어서다. 같은 코드를 여기저기 붙여 넣는 대신, 하나의 함수로 만들어 호출한다. 여러 클래스에 같은 메서드를 복사하는 대신 따로 클래스를 만들어 상속받거나 믹스인 한다.
게임 데이터도 규모가 일정 이상이 되면 코드와 비슷한 기능이 필요하다. 프로토타입과 위임을 활용해 데이터를 재사용하는 기법중 하나는 JSON이다.
고블린은 이런식으로 정의된다.
{
"이름": "고블린 보병",
"체력": 20,
"내성": ["추위", "독"],
"약점": ["불", "빛"]
}
워낙 간단하기 때문에 텍스트 데이터를 꺼려하는 기획자도 쉽게 작업할 수 있다. 고블린 집안 형제들도 다음과 같이 데이터로 만들어보자.
{
"이름": "고블린 마법사",
"체력": 20,
"내성": ["추위", "독"],
"약점": ["불", "빛"],
"마법": ["화염구", "번개 화살"]
}
{
"이름": "고블린 궁수",
"체력": 20,
"내성": ["추위", "독"],
"약점": ["불", "빛"],
"공격방법": ["단구"]
}
개체에 중복이 많다. 이게 진짜 코드였다면 바로 눈에 거슬렸을 것이다. 좋은 프로그래머는 중복을 싫어한다. 코드라면 'goblin'이라는 추상 자료형을 만든 뒤 세 개의 고블린 자료형에서 재사용했을 것이다. 하지만 JSON에는 이런 개념이 없다.
객체에 '프로토타입' 필드가 있어서, 여기에서 위임하는 다른 객체의 이름을 찾을 수 있다고 정하겠다. 첫 번째 객체에서 원하는 속성이 없다면 프로토타입 필드가 가리키는 객체에서 대신 찾는다.
이제 고블린 형제의 JSON 데이터를 단순하게 만들 수 있다.
{
"이름": "고블린 보병",
"체력": 20,
"내성": ["추위", "독"],
"약점": ["불", "빛"]
}
{
"이름": "고블린 마법사",
"프로토타입": "고블린 보병",
"마법": ["화염구", "번개 화살"]
}
{
"이름": "고블린 궁수",
"프로토타입": "고블린 보병",
"공격방법": ["단궁"]
}
궁수와 마법사에서 보병을 프로토타입으로 지정했기 때문에 체력, 내성, 약점을 반복 입력하지 않아도 된다.
세 가지 실제 고블린을 위임할 '기본 고블린'같은 추상 프로토타입을 따로 만들지 않고, 대신 가장 단순한 고블린 자료형을 하나 골라서 다른 객체를 위임하게 했다.
소년코딩 - C++ 디자인 패턴 04. 프로토타입 패턴, Prototype Pattern (tistory.com)
'CS > 디자인패턴' 카테고리의 다른 글
[C++] 추상 팩토리 메서드 패턴 코드 (0) | 2024.09.22 |
---|---|
[C++] 싱글톤 패턴, Singleton Pattern (0) | 2023.08.14 |
[C++] 관찰자 패턴, 옵저버 패턴, Observer Pattern (0) | 2023.08.09 |
[C++] 경량 패턴, Flyweight Pattern (0) | 2023.08.08 |
[C++] 명령 패턴, 커맨드 패턴, Command Pattern (0) | 2023.08.08 |