요즘 흔히 얘끼하는 프로토타입은 디자인패턴의 그것과는 다르다. 이번에는 프로토타입 패턴도 다루지만 더 흥미로운 주제인 ‘프로토 타입’용어와 개념이 어떻게 유래되었는지 알아볼것이다.
프토토타입 디자인 패턴
영웅을 저녁 식사 삼으려는 이 음침 녀서들은 게임마다 등장하는데 몬스터 종류마다 따로 존재한다.
몬스터(Ghost, Demon, Sorcerer)같은 클래슬 만들어보자
class Monster
{
// Stuff...
};
class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};
C++
복사
한 가지 스포너는 한 가지 몬스터 인스터스만 만든다. 게임에 나오는 모든 몬스터를 지원하기 위해 일단 마구잡이로 몬스터 클래스마다 스포너클래스를 만든다고 치자. 이렇게하면 스포너클래스가 상속구조인 몬스터를래스를 따라서 비슷한 형태로 만들어지게 된다.
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();
}
};
// You get the idea...
C++
복사
코드를 많이 작성할수록 돈을 더 많는다면 위방식이 개발자한테는 더 좋은 방식일 것이다. 클래스도 많아지지… 행사코드도 길어지지… 반복코드도 많아지지… 중복도 많지…
이런걸 프로토 타입으로 해결할 수 있다. 핵심은 어떤 객체가 자기와 비슷한 객체를 스폰할수 있다는 점이다. 유령 객체 하나로는 다른 유령객체를 여럿 만들수 있다. 악마 객체로 부터도 다른 악마개체를 만들 수 있다. 어떤 몬스터 객체든지 자신하고 비슷한 몬스터를 만드는 prototype(원형)객체로 사용 할 수 있다.
이를 구현하기 위해서 몬스터 클래스에 순수가상함수인 clone()을 추가한다.
class Monster
{
public:
virtual ~Monster() {}
virtual Monster* clone() = 0;
// Other stuff...
};
C++
복사
몬스터 하위클래스에서는 자신과 자료형과 상태가 같은 새로운 객체를 반환하도록 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_;
};
C++
복사
몬스터를 상속받는 모든 클래스에 clone메서드가 있다면, 스포너에서 종류별로 마들필요 없이 하나만 만들면 가상함수이기 떄문에 알아서 각자의 클래스의 clone 호출될것이다.
class Spawner
{
public:
Spawner(Monster* prototype)
: prototype_(prototype)
{}
Monster* spawnMonster()
{
return prototype_->clone();
}
private:
Monster* prototype_;
};
C++
복사
스포너 클래스는 내부에 몬스터 객체가 숨어 있다. 이 객체는 벌집을 떠나지 않는 여왕벌처럼 자기와 같은 Monster를 도장처럼 찍어낸다.
유령 스포너를 마들려면 원형으로 사용할 유령 스포너를 만든후에 스포너에 전달한다.
Monster* ghostPrototype = new Ghost(15, 3);
Spawner* ghostSpawner = new Spawner(ghostPrototype);
Monster* demonPrototype = new Demon(15, 3);
Spawner* demonSpawner = new Spawner(demonPrototype);
// to do ....
// 계속 추가 될것이다 몬스터 종류가 늘어날 때마다.
C++
복사
프로토타입 패턴의 좋은 점은 클래스 뿐만 아니라 상태도 같이 복제한다는 점이다. 즉 원형으로 사용할 유령 객체를 잘 설정하면 빠른 유령, 느린 유령 등등의 스포너같은 것도 쉽게 만들 수 있다.
프로토 타입 패턴은 우아하면서도 놀랍다. 또한 간단하기 떄문에 따로 외우려 하지 않아도 이해가 된다.
잘 작동 하는가?
이제 몬스터마다 스포너 클래스를 따로 만들지 않아도 된다. 그래도 몬스터마다 clone()은 따로 구현은 해줘야한다.
clone()만들다 보면 애매할 떄가 있따. 객체를 깊은 복사를 해야 할까? 얕은 복사를 해야 할까?
악마 몬스터가 삼지창을 들고있다. 복제된 악마도 삼지창을 들고 있어야 할까?
프로토타입 패턴은 써도 코드양자체는 크게 줄어들지 않는다. 예부터가 현실적이지 않다. 요즘 나오는 왠만한 게임들은 몬스터마다 클래스르 따로 만들지 않는다.
우리프로그래머들은 오랜시간동안 노가다를 통해서 상속구조가 복잡하면 유지보수가 힘들다는 점을 체득했다. 요즘은 개체 종류별로 클래스를 만들기 보다는 컴포넌트, 타입 객체로 모델링 하는 것을 선호한다.
스폰함수를 활용 하는 방법
앞에서 모든 몬스터마다 별도의 스포너 클래스가 필요 했다. 하지만 이 모든 일에는 답이 여러개 있는 법이다. 다음과 같은 스폰함수를 만들어보자
Monster* spawnGhost()
{
return new Ghost();
}
Monster* spawnDemon()
{
return new Demon();
}
....
C++
복사
몬스터 종류마다 클래스를 만드는 것보다 코드양이 더 적다. 이제 스포너클래스는 하나의 포인터 변수만 두면 된다.
typedef Monster* (*SpawnCallback)();
class Spawner
{
public:
Spawner(SpawnCallback spawn)
: spawn_(spawn)
{}
Monster* spawnMonster()
{
return spawn_();
}
private:
SpawnCallback spawn_;
};
C++
복사
유령을 스폰하는 객체는 이렇게 만들 것이다.
Spawner* ghostSpawner = new Spawner(spawnGhost);
Spawner* demonSpawner = new Spawner(spawnDemon);
...
C++
복사
템플릿을 활용하는 방법
요즘에는 템플릿도 많이 활용한다. 스포너클래스를 이용해 인스턴스를 생성하고 싶지만 특정 몬스터 클래스를 하드코딩하기 싫다면 몬스터 클래스 템플릿 타입매개변수를 전달하면 된다.
class Spawner
{
public:
virtual ~Spawner() {}
virtual Monster* spawnMonster() = 0;
};
template <class T>
class SpawnerFor : public Spawner
{
public:
virtual Monster* spawnMonster() { return new T(); }
};
C++
복사
사용방법은 다음과 같다.
Spawner* ghostSpawner = new SpawnerFor<Ghost>();
Spawner* demonSpawner = new SpawnerFor<Demon>();
...
C++
복사
앞에서 본 두 방법을 통해서 Spawner 클래스에 자료형을 매개변수로 전달 할 수 있다. c++에서는 자룧여이 일급 자료형이 아니다 보니 이런 곡예를 해야한다. 파이선, 자바스크립트 등등의 언어에서는 동적 자료형이 있기때문에 더 쉽게 해결할수 있다.
이부분 부터는 프로토타입 의 역사 입니다. 참고사항
프로토 타입 언어 패러다임
OOP(객체지향 프로그래밍)에서 가장 중요한건 상태(변수), 행동(함수)을 함께 묶는데 있다.
클래스만이 이를 위한 유일한 방법일 거라고 생각 할지 모르나, 몇몇사람들은 다르게 생각했다. 이들은 Self라는 언어를 마들었다. oop에서 할수 있는건 다할 수 있지만 클래스는 없는 언어이다.
셀프
순수하게 의미만 놓고 보면 셀프는 클래스 기반 언어보다 더 객체지향적이다. 상태와 동작을 같이 묶어놓은것을 oop라고 할때 클래스 기반 언어는 상태와 동작사이에 분명한 구별이 있다.
친숙한 클래스 기반 언어를 하나 떠올려보자. 객체 상태를 알기 위해서는 해당 인스턴스의 메모리에 접근하여 들여돠바야한다. 즉 상태는 객체가 가지고 있다.
반대로 메서드를 호출한떄는 클래스에 접근한다. 즉 동작은 클래스 (멤버함수) → 코드영역에 존재한다.
셀프에는 이런 구별이 없다. 무엇이든 객체에서 바로 찾을 수 있다. 인스턴스는 상태와 동작 둘다 가질 수 있다. 유일무이한 메서드를 가진 객체도 만들수 있다.
이게 셀프의 전부라면 그다지 유용하지 않을수도 있다. 클래스 기반 언어에서는 상속은 나름 단점도 있지만 , 다형성 통해서 코드를 재사용하고 중복코드를 줄일수 잇다는 장점이 있었다. 클래스 없이 이러한 일을 수행하기 위해 셀프에서는 위임(deligation)이 있다.
먼저 해당 객체에서 필드나 메서드를 찾아본다. 있다면 그걸 쓰고 없다면 상위 객체를 찾아본다.
상위 객체는 그냥 다른 객체의 레퍼런스(&) 일 뿐이다. 첫 번째 객체에 속성이 없다면 상위 객체를 살펴보고, 그래도 없다면 상위 객체의 상위 객체에서 또 찾아보고, 이를 반복하낟. 다시말해 찾아보고 없으면 상위객체 위임한다.
클래스는 new GameObject() 식으로 생성한다. 자기자신의 인스턴스 생서기이다.(factory)
클래스가 없다면 어떻게 객체를 만들수 있을까? 프로타타입 패턴에서 본것처럼 복제하면된다.
셀프에서는 모든 객체가 프로토타입 패턴을 저절로 지원하게 하는것과 같다. 모든 객체가 복사될 수 있기 때문에 비슷한 객체를 여럿 만들려면 다음과 같다.
1.
객체 하나를 원하는 상태로 만든다. 시스템에서 제공하는 기본 Object 객체를 복사한 뒤에 필드와 메서드를 넣어준다.
2.
원하는 만큼 복사한다.
셀프에서는 귀찮게 직접 클론 메서드를 구현하지 않아도 시스템적으로 프로토타입 패턴기반으로 제공해준다.
이방법으로 프로그래밍을 하다보니 생각보다 재미없는 사실을 발견했다. 직접 그런방식의 언어를 만들어보니 클래스가 제공해주는 구조(상속,, 가상함수,, 템플릿)가 아쉬워서 결국에는 언어에서 제공하지 않는 기능을 전부 프로그래머가 만들어 줘야 했다.
데이터모델링을 위한 프로토 타입
시간이 지날수록 게임 데이터는 용량이 엄청 커지고 있다. 초창기 게임은 플로피디스크, cd 등 작은용량에서도 실행하기위해서 게임을 만들어야 했다. 요즘게임에서 코드는 게임을 실행하기 위한 엔진일뿐 콘텐츠는 모두 데이터 정의 되어 있다.
하지만 많은 데이터를 옮기면 대규모 프로젝트의 구조 문제가 오히려 어려워진다면 모를까 저절로 해결되지 않는다. 프로그래밍 언어를 사용하는 이유는 복잡성을 제어 할 수 있는 수단을 가지고 있어서다. 같은 코드를 여기저기 붙여 넣는 대신, 하나의 함수를 만들어 호출한다. 여러 클래스에서 같은 메서드를 복사하는 대신에 따로 클래스를 만들어 합친다.
게임 데이터도 일정 규모이상 커지면 코드와 비슷한 기능한다. 데이터 모델링은 가볍게 다룰만한 주제가 아니긴하지만 프로토타입과 위임을 활용해서 재사용하는 기법을 소개한다.
예를들어 많이사용하는 구조는 json 구조이다. 키/값으로 이루어진 데이터다.
고블린을 정의한다면 다음과같이 정의가 될것이다.
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}
JSON
복사
워낙 간단하기 떄문에 텍스트 데이터를 꺼려하는 기획자도 쉽게 각업할수 있다.
{
"name": "goblin wizard",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"spells": ["fire ball", "lightning bolt"]
}
{
"name": "goblin archer",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"],
"attacks": ["short bow"]
}
JSON
복사
이렇게 놓고보니까 중복된 데이터들이 많다. 이게 만약에 코드였다면 거슬렷을 것이다. 상속이나 이런것들 이용해서 중복을 제거했을 터이다. 하지만 json에는 이런 기능이 없다.
그러면은 조금더 효율적으로 만들어보자
{
"name": "goblin grunt",
"minHealth": 20,
"maxHealth": 30,
"resists": ["cold", "poison"],
"weaknesses": ["fire", "light"]
}
{
"name": "goblin wizard",
"prototype": "goblin grunt",
"spells": ["fire ball", "lightning bolt"]
}
{
"name": "goblin archer",
"prototype": "goblin grunt",
"attacks": ["short bow"]
}
JSON
복사
궁수와 마법사의 프로토타입으로 보병을 지정했기 때문에 중복된 데이터들을 따로 입력하지 않아도 된다.
세가지 실제 고블린을 위임할 기본 고블린 같은 프로토타입을 따로 만들지 않았다는 점에서 흥미롭다. 대신 가장 단순한 고블린 자료형을 하나 더 골라서 다른객체 위임했다.
프로토타입 기반 시스템에서는 새로 정의되는 객체를 만들 때 어떤 객체든 복제로 사욯 하는게 당연한데, 이번 데이터 예제에서도 마찬가지다. 특히 일회성 특수 객체가 자주나오는 게임에 잘 맞는 방식이다. 보스와 유니크 아이템은 일반 아이템을 약간 다듬어서 만들 떄가 많으므로 프로토타입 방식의 위임을 써먹기 좋다.
{
"name": "Sword of Head-Detaching",
"prototype": "longsword",
"damageBonus": "20"
}
JSON
복사
데이터 모델링 시스템에 기능을 약간 추가 했을 뿐이지만, 기획자는 기존 무기나 몬스터를 약간 변형해 게임월드를 쉽게 풍성하게 만들어 줄수 있다.