일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 병행성 관련 오류
- Direct12
- DirectX12
- 렌더링 파이프라인
- 스케줄링
- 알고리즘
- 멀티쓰레드
- 타입 객체
- 그리디알고리즘
- 영속성
- 멀티프로세서
- 디자인패턴
- 다이나믹프로그래밍
- 동적계획법
- 백준
- DirectX 12
- I/O장치
- 그리디 알고리즘
- OS
- directx
- 락
- 프로그래머스
- 다이나믹 프로그래밍
- 운영체제
- 병행성
- 컨디션 변수
- 자료구조
- 파일시스템 구현
- codility
- 쓰레드
- Today
- Total
기록공간
4장. Prototype Pattern (프로토타입 패턴) 본문
"원형이 되는(Prototypical) 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성합니다." (GoF의 디자인 패턴 169p)
이번 장에서는 프로토타입에 대해서 다루기도 하지만, '프로토타입' 용어와 개념이 어떻게 유래되었는지도 함께 다룬다. 우선 패턴부터 살펴보도록 하자.
프로토타입 디자인 패턴
몬스터에도 종류가 존재한다. 한 개체에서도 여러 종류로 몬스터가 나뉠 수 있다.
게임에 나오는 몬스터마다 Ghost, Demon, Sorcerer 같은 클래스를 만들어보자.
class Monster
{
// 기타 등등..
};
class Ghost : public Monster {};
class Demon : public Monster {};
class Sorcerer : public Monster {};
한 가지 스포너(Spawner)는 한 가지 몬스터 인스턴스만 만든다. 게임에 나오는 모든 몬스터를 지원하기 위해 일단 마구잡이로 몬스터 클래스마다 스포너 클래스를 만든다고 치자. 이렇게 하면 스포너 클래스 상속 구조가 몬스터 클래스 상속 구조를 따라가게 된다.
이를 구현한 코드는 대강 다음과 같다.
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();
}
};
// 뭘 하려는 건지 알 것이다...
코드를 많이 작성할수록 돈을 더 받는다면 모를까, 이 코드는 영 별로다. 클래스도 많지, 행사 코드도 많지, 반복 코드도 많지, 중복도 많지, '많지'라는 말도 많지...
이런 것은 프로토타입 패턴으로 해결할 수 있다. 핵심은 어떤 객체가 자기와 비슷한 객체를 스폰 할 수 있다는 점이다. 유령 객체 하나로 다른 유령 객체를 여럿 만들 수 있다. 악마 객체로 부터도 다른 악마 객체를 만들 수 있다. 어떤 몬스터 객체든지 자신과 비슷한 몬스터 객체를 만드는 원형(Prototypal) 객체로 사용할 수 있다.
이를 구현하기 위해, 상위 클래스인 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);
프로토타입 패턴의 좋은 점은 프로토타입의 클래스뿐만 아니라 상태도 같이 복제(clone)한다는 점이다. 즉, 원형으로 사용할 유령 객체를 잘 설정하면 빠른 유령, 약한 유령, 등의 스포너 같은 것도 쉽게 만들 수 있다.
프로토타입 패턴은 우아하면서도 놀랍다. 또한 너무 간단하기 때문에 따로 외우려 노력하지 않아도 까먹을 수가 없다.
얼마나 잘 작동하는가?
이제 몬스터마다 스포너 클래스를 따로 만들지 않아도 된다. 그래도 Monster 클래스마다 clone()을 구현해야 하기 때문에 코드 양은 별반 차이가 없다.
clone()를 만들다 보면 애매할 때도 있다. 객체를 깊은 복사(Deep clone)를 해야 될까, 얕은 복사(Shallow clone)을 해야 할까? 만약에 악마가 삼지창을 들고 있다면, 복제된 악마도 삼지창(무기)을 들고 있어야 할까?
앞에서 봤듯이 프로토타입 패턴을 써도 코드 양이 많이 줄어들지 않는 데다가, 예제부터가 현실적이지 않다. 요즘 나오는 엔진에서는 몬스터마다 클래스를 따로 만들지 않는다.
프로그래머들은 클래스 상속 구조가 복잡하면 유지보수하기 힘들다는 것을 체득했기 때문에, 요즘은 컴포넌트나 타입 객체로 모델링 하는 것을 선호한다.
스폰 함수
다음과 같이 스폰 함수를 만들어보자.
Monster* spawnGhost()
{
return new Ghost();
}
몬스터 종류마다 클래스를 만드는 것보다는 코드가 훨씬 적다. 이제 스포너 클래스에는 함수 포인터 하나만 두면 된다.
typedef Monster* (*SpawnCallback)();
class Spawner
{
public:
Spawner(SpawnerCallback 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>();
일급 자료형
앞에서 본 두 방법을 통새서 Spawner 클래스에 자료형을 매개변수로 전달할 수 있다. C++에서는 자료형이 일급 자료형이 아니다 보니 이런 곡예를 해야 한다. 클래스가 전달 가능한 일급 자료형인 동적 자료형 언어(파이썬, 자바스크립트 등)에서는 이 문제를 훨씬 더 직접적으로 풀 수 있다.
스포너를 만들기 위해서는 원하는 몬스터 클래스를, 그것도 실제 런타임 객체를 그냥 전달하면 된다.
이러한 여러 가지 선택에도 불구하고, 솔직히 말해 프로토타입 디자인 패턴이 언제 가장 이상적인지는 잘 모르겠다(사람마다 다를 순 있다).
자료의 출처는 http://gameprogrammingpatterns.com/prototype.html입니다.
'Game Design Pattern, Logic' 카테고리의 다른 글
6장. 상태 패턴 (State Pattern) (0) | 2020.03.03 |
---|---|
5장. Singleton Pattern (싱글턴 패턴) (0) | 2020.02.14 |
3장. Observer Pattern (관찰자 패턴) (0) | 2020.02.04 |
2장. Flyweight Pattern (경량 패턴) (0) | 2020.02.03 |
1장. Command Pattern (명령 패턴) (0) | 2020.02.01 |