정예형 몬스터일 경우 사용되는 패턴이나 기술 등은 유사하지만 능력치가 좀 더 강화된 특징을 갖는다면
새로운 몬스터 클래스를 만들필요없이기본 객체를 복사해서 사용하는 방식이다!
- 프로토타입(Prototype)의 특징
기존 객체를 기본값으로 활용하기에 매번 DB에서 불러오지 않아도 되서 개발 비용을 줄일 수 있다!
단 객체 간의 의존성이 클경우 관리하기 어려울 수 있다.
- 프로토타입(Prototype) 예제 만들어보자! (C#)
1. 먼저 몬스터 프로토타입 인터페이스를 정의한다.
// 몬스터 프로토타입
public abstract class MonsterPrototype
{
public string Name { get; set; }
public int MaxHealth { get; set; }
public int CurHealth { get; set; }
public int Strength { get; set; }
public abstract MonsterPrototype Clone();
}
2. 몬스터 클래스를 만든다.
public class Monster : MonsterPrototype
{
public Monster(string name, int health, int strength)
{
Name = name;
MaxHealth = health;
CurHealth = MaxHealth;
Strength = strength;
}
public override MonsterPrototype Clone()
{
return (Monster)this.MemberwiseClone();
}
public void Show() {
Console.WriteLine($"[몬스터] {Name} - 체력: {CurHealth}, 공격력: {Strength}");
}
}
3. Main에서 몬스터 프로토타입과 정예형 몬스터를 만든다.
internal class Program
{
static void Main(string[] args)
{
// 기본 몬스터 원형
Monster goblin = new Monster("고블린", 100, 15);
goblin.Show();
// 고블린 정예폼
Monster goblin_Elite = (Monster)goblin.Clone();
goblin_Elite.Name = "정예 고블린"; // 이름 변경 가능
goblin_Elite.Strength += 10; // 공격력 증가
goblin_Elite.Show();
}
}
이 부분이 프로토타입 패턴을 사용한 예시이다!
Monster goblin_Elite = (Monster)goblin.Clone();
기존 고블린 객체를 복제해서 새로운 정예 고블린 객체(goblin_Elite)를 만들었다!
체력, 방어, 속도, 마나게이지 뿐만 아니라 가지고 있는 기술, 아직 해금되지 않는 기술들도
전부 캐릭터가 가진 속성이다. 이 캐릭터를 생성자로 만든다고 생각해보자.
- 생성자로 캐릭터를 만들 경우
new 캐릭터1("전사", "근접", 100, 50, 20, 150, ...);
이런식으로 생성할텐데, 일단 생성자에 너무 많은 매개변수가 포함되고, 속성의 순서를 기억하기가 어렵다!
이 코드를 만든 당사자면 이해할 순 있겠지만 프로그래밍에선 협업이 중요하다!
다른 사람이 내 코드를 봤을때 이 코드가 무엇인지 확실히 알아야 되는데 저 생성자 코드만 보고
캐릭터를 만들 때 무슨 속성을 설정할지 단번에 파악할 수 있을까?
- Builder 패턴을 적용할경우
Character warrior = new CharacterBuilder()
.SetName("전사")
.SetType("근접")
.SetHealth(100)
.SetDefense(50)
.SetSpeed(20)
.SetMana(150)
.Build();
이 방법은 속성의 순서를 기억할 필요가 없고, 해당 속성이 무엇인지 명확해 보인다!
- Builder란 무엇인가?
복잡한 인스턴스를 조립하는 방식!
객체를 생성하는 방법(How)과 객체를 구현하는 방법(What)을 분리하여, 동일한 생성 절차에서 다양한 표현 결과를 만들 수 있도록 하는 패턴이다.
복잡한 객체를 생성과 구현을 분리해서 관리해주는 느낌으로 생각하면 된다!
-Builder 패턴을 사용해보자! (c#)
1. 먼저 캐릭터 클래스를 설계한다!
캐릭터는 이름, 직업, 무기, 체력, 마나, 속도 속성이 있다!
public class PlayerCharacter
{
public string Name { get; set; }
public string Job { get; set; }
public string Weapon { get; set; }
public int Health { get; set; }
public int Mana { get; set; }
public int Speed { get; set; }
public void PlayerShow()
{
Console.WriteLine($"[캐릭터 생성 완료] 이름: {Name}, 직업: {Job}, 무기: {Weapon}, " +
$"체력: {Health}, 마나: {Mana}, 속도: {Speed}");
}
}
// 초원 스테이지 팩토리
class StageGrass_Factory : IStageFactory
{
public IMonster CreateMonster() => new Slime();
}
// 동굴 스테이지 팩토리
class StageCave_Factory : IStageFactory
{
private Random random = new Random();
public IMonster CreateMonster()
{
if (random.Next(2) == 0)
return new Goblin();
else
return new Troll();
}
}
5. 만든 예시코드로 게임을 실행해본다
// 게임 매니저
class GameManager
{
private IMonster monster;
public GameManager(IStageFactory factory)
{
monster = factory.CreateMonster();
}
public void Play()
{
monster.Attack();
}
}
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("초원 스테이지 시작!");
GameManager grassGame = new GameManager(new StageGrass_Factory());
grassGame.Play();
Console.WriteLine("\n동굴 스테이지 시작!");
GameManager caveGame = new GameManager(new StageCave_Factory());
caveGame.Play();
}
}
실행결과
추상 팩토리 패턴은 연관된 객체(몬스터 + 아이템 등)를 세트로 생성할 때 사용되는
디자인 패턴이다. 게임에서 스테이지, 종족, 장비 등의 세트를 한 번에 생성해야 할 때 사용된다.
지난번에는 싱글톤에 대해 알아보았다면 이번에는 생성 디자인 패턴 중 팩토리 메서드에 대해서 알아보도록 하자!
Q1. 팩토리 메서드(Factory Method)가 뭘까?
팩토리 메서드의 핵심: 객체 생성의 책임을 서브 클래스에게 맡기는 생성 패턴
팩토리 메서드 간단 설명
이번에도 게임으로 비유해보자!
게임에서 적 캐릭터 종류에는 여러가지가 있다고 치자 (좀비, 슬라임, 고블린, 오크, 트롤.... 등등)
이 객체들을 하나하나 다 지정해서 만들자고 생각하니 복잡할거 같다...
그러면 그냥 몬스터 공장에서 만들라고 요청만 하면 어떨까?
우리가'좀비 3마리만 만들어 주세요!' 라는 주문을 공장 클래스에 요청하면
몬스터 공장은좀비 3마리를 만들어 준다. 이 방법이 바로팩토리 메서드(Factory Method)이다!
팩토리 메서드(Factory Method)는 객체 생성의 책임을 서브 클래스(하위 클래스)에서 맡기는생성 패턴.
즉, 객체를 Main에서 직접 생성하지 않고,
Main에서서브클래스에 어떤 객체를 생성할지만 요청해주는 패턴이다.
(무언가를 만드는 공장이니까 당연히 디자인 패턴 중 생성인 건 덤!)
Q2. 그럼 팩토리 메서드는 어떻게 사용하는 건가요?
일단 코드를 통해서 구현해보자! C#: 콘솔앱(. NET Framework)
// 팩토리 메서드: 객체 생성을 서브 클래스에서 결정
// 직접 new를 사용하지 않고 팩토리 메서드를 통해 객체를 생성함
namespace CreatePattern_02_FactoryMethod
{
abstract class Enemy
{
public string name;
public abstract void Attack(int damage);
}
class Enemy_Goblin : Enemy
{
public Enemy_Goblin()
{
name = "고블린";
}
public override void Attack(int damage)
{
Console.WriteLine($"{name}이 {damage} 데미지로 공격!");
}
}
class Enemy_Orc : Enemy
{
public Enemy_Orc()
{
name = "오크";
}
public override void Attack(int damage)
{
Console.WriteLine($"{name}가 검을 휘둘러 {damage} 데미지로 공격!!");
}
}
// 이곳이 몬스터 공장
class EnemyFactory
{
public static Enemy CreateEnemy(string type)
{
if (type == "Goblin")
return new Enemy_Goblin();
if (type == "Orc")
return new Enemy_Orc();
throw new ArgumentException("존재하지 않는 적 유형입니다...ㅠㅠ");
}
}
internal class Program
{
static void Main(string[] args)
{
Enemy enemy1 = EnemyFactory.CreateEnemy("Goblin");
Enemy enemy2 = EnemyFactory.CreateEnemy("Orc");
enemy1.Attack(10);
enemy2.Attack(25);
}
}
}
실행결과
Q3-1. 코드를 하나하나 해석해보자!
먼저 몬스터 개별 클래스이다. (고블린과 오크)
class Enemy_Goblin : Enemy
{
public Enemy_Goblin()
{
name = "고블린";
}
public override void Attack(int damage)
{
Console.WriteLine($"{name}이 {damage} 데미지로 공격!");
}
}
class Enemy_Orc : Enemy
{
public Enemy_Orc()
{
name = "오크";
}
public override void Attack(int damage)
{
Console.WriteLine($"{name}가 검을 휘둘러 {damage} 데미지로 공격!!");
}
}
부모 클래스(Enemy)에서 상속받은 몬스터 클래스(고블린, 오크)이며,
각각 공격 메서드 Attack() 를 가지고 있다(오버라이딩).
몬스터 공장(EnemyFactory)은 새로운 몬스터를 생성하는 것을 담당한다.
// 이곳이 몬스터 공장
class EnemyFactory
{
public static Enemy CreateEnemy(string type)
{
if (type == "Goblin")
return new Enemy_Goblin();
if (type == "Orc")
return new Enemy_Orc();
throw new ArgumentException("존재하지 않는 적 유형입니다...ㅠㅠ");
}
}
Enemy enemy1 = new Goblin();
Enemy enemy2 = new Orc();
만약 팩토리 메서드를 사용하지 않으면, 이렇게 일일히 Main에서 객체를 직접 생성해야 한다.
팩토리 메서드를 사용하면 위에 나온 요청 방식으로 활용할 수 있는 것이다!
Q4. 코드에서는 별차이가 없어보이는데요??
지금은 간단하게 몬스터만 구현한 방식이기 때문에 별차이가 없어보일 수 있다.
하지만 생각을 해보자... 게임에는 몇 종류의 몬스터 존재할까?
좀비, 오크, 흡혈귀, 스켈레톤, 고블린, 하이고블린, 엘리트 고블린... 이렇게 수많은 몬스터가 존재할 수 있다.
이들을 Main에서 하나하나 전부 관리를 할 수 있을까?
그럼 비교를 위해 몬스터 생성을 예시로 들어보자!
4-1. (대량의 몬스터 종류가 있는 경우 객체 생성) 팩토리 메서드를 사용하지 않는 방법
internal class Program
{
static void Main(string[] args)
{
// 서버에서 받은 몬스터 타입 리스트
string[] monsterTypes = { "Goblin", "Orc", "Zombie", "Dragon", "Vampire", "Skeleton" };
// 몬스터 객체를 직접 생성하기
Enemy[] monsters = new Enemy[monsterTypes.Length];
for (int i = 0; i < monsterTypes.Length; i++)
{
if (monsterTypes[i] == "Goblin")
monsters[i] = new Goblin();
else if (monsterTypes[i] == "Orc")
monsters[i] = new Orc();
else if (monsterTypes[i] == "Zombie")
monsters[i] = new Zombie();
else if (monsterTypes[i] == "Dragon")
monsters[i] = new Dragon();
else if (monsterTypes[i] == "Vampire")
monsters[i] = new Vampire();
else if (monsterTypes[i] == "Skeleton")
monsters[i] = new Skeleton();
}
// 몬스터 공격 실행
foreach (var monster in monsters)
{
monster.Attack(20);
}
}
}
이렇게 새로운 몬스터가 추가될 때마다 else if를 계속 써줘야 한다.
이는 SOLID 원칙중 OCP 원칙에 위배되는 방식이다!
이 방식에서 새로운 몬스터 종류가 추가된다고 가정하자!
monsterTypes 리스트에서 확장을 하고, else if를 통해서 수정도 해야한다는 것!
또 몬스터마다 특별한 특성이나 공격 기술을 가지게 된다면 이 코드만으로 다룰 수 있을까?
그렇다면 이번엔 팩토리 메서드 방식을 사용한 코드를 알아보자.
4-2.(대량의 몬스터 종류가 있는 경우 객체 생성)팩토리 메서드를 사용한 방법
internal class Program
{
static void Main(string[] args)
{
// 서버에서 받은 몬스터 타입 리스트
string[] monsterTypes = { "Goblin", "Orc", "Zombie", "Dragon", "Vampire", "Skeleton" };
// 팩토리 메서드를 이용해 객체 생성
List<Enemy> monsters = new List<Enemy>();
foreach (var type in monsterTypes)
{
Enemy monster = EnemyFactory.CreateEnemy(type);
if (monster != null)
monsters.Add(monster);
}
// 몬스터 공격 실행
foreach (var monster in monsters)
{
monster.Attack(20);
}
}
}
객체 생성을 Main이 아닌 EnemyFactory 한 곳에서만 생성을 담당하기에
새로운 몬스터 종류가 추가된다면 팩토리 클래스에처 추가만 하면 된다!
마지막으로 이를 통해서 알 수 있는 팩토리 메서드의 특징을 정리해보자!
1. 객체 생성 책임을 하위클래스에 맡긴다.
팩토리 메서드는 객체 생성 방식과 종류를 하위 클래스에서 결정하며, 상위 클래스는 요청 작업만 한다.
2. 객체 생성의 캡슐화(Encapsulation) → 상위 클래스는 요청만 하기때문에 생성 방법을 알 수 없다.
게임 스테이지마다 등장하는 몬스터의 종류와 숫자가 다를 때, 다양한 몬스터들을 EnemyFactory에서 관리하면,
어떤 몬스터를 생성할지에 대한 결정을 한 곳에서 모아서 관리할 수 있다.
새로운 몬스터가 추가된다면, 팩토리 클래스에서 관리하면 되므로 객체 생성 방식이 일관성 있게 유지한다는 뜻!
다만.... 너무 많은 몬스터 종류가 추가되면, 팩토리 메서드 방식만으로는 코드량이 급증할 수 있다.
왜냐하면 수 많은 몬스터 클래스를 정의하고 관리해야 때문에, 클래스가 지나치게 많아지는 문제가 발생할 수 있는 것이다!
몇년 전에 유행한 뱀서라이크로 예시를 들어보자!
몬스터가 한꺼번에 떼지어서 등장하고, 한 번 생성된 몬스터가 반복적으로 등장하는 특징이 있다.
이런 경우, 팩토리 메서드보다는 오브젝트 풀링방식을 사용하는 것이 더 적합할 수 있다.
객체 생성과 관리를 어디서 어떻게 할지에 따라 디자인 패턴을 선택하는 것이 중요한 부분이다!
실제로도 자주 사용하는 디자인 패턴 - 생성 패턴(Creational Patterns) 중 싱글톤(SingleTon)에 대해서 알아보자
- 싱글톤(Singleton)은 딱 2가지만 알면 쉽다!
1. 단 하나의 인스턴스(Instance)만 생성한다!
2. 전역적으로 관리해야 하는 객체(Object)에서 주로 사용된다!
Q1. 그래서 싱글톤이 뭐냐니까 ???
GameManger 이다!
무언가를 설명할 때 예를 들어서 설명해주면 이해하기가 쉽다 (앞으로도 예시를 통해서 글을 작성할 것이다)
우리가 게임을 하면 캐릭터, 적캐릭터, 아이템 등 게임 오브젝트가 있을텐데 이들을 관리하는 시스템이 필요하기 마련이다.
게임을 개발할 때 이들을 매니저라고 부르는데, 이 때 사용하는 방법이 Singleton 이다!
게임의전반적인 상황을 알려주고 관리 해주는 GameManger가 있다 치자
(현재 스테이지, 플레이어 점수, 게임 진행도... 등을 관리해주는 매니저)
플레이어가 메인 메뉴에서 게임 시작 버튼을 누르면, GameManger는 게임 상태를 설정하고,
게임 화면이 변경되더라도 (이거를 씬(Scene)이 변경된다고 한다),
GameManager는 유지 상태로 남아 점수나 진행 상황을관리한다.
즉, 여러 Scene에서 GameManager가 있어야 하기 때문에 (GameMamager가 공유되어야 하기 때문에)
싱글톤(Singleton)을 사용하는 것!
Q2. 그러면 어떻게싱글톤 패턴을 사용하는 건데 ???
코드를 통해서 알아보도록 하자! (맨 아래 전체 코드가 있습니다!)
먼저 GameManager 클래스부터 선언한다!
public class GameManager
{
private static GameManager instance;
private GameManager()
{
// 나는 매니저 생성자야!
}
public static GameManager Instance
{
get
{
if (instance == null) // 객체가 없을 때만 생성
{
instance = new GameManager();
}
return instance;
}
}
public void WriteGameMessage()
{
Console.WriteLine("GameManager 싱글톤 인스턴스 등장!");
}
}
인스턴스와, 생성자의 접근제한자를 private로 설정해 외부에서 인스턴스 생성을 방지한다!
Q3-1. 클래스는 public인데요?? 왜 인스턴스는 private인가요?
클래스가 public으로 설정되어야, 외부에서 GameManger 클래스를 참조하고 사용할 수 있다!
싱글톤의 핵심은 하나의 인스턴스만 생성하도록 보장되는 것이다!
인스턴스는private로 선언해야 instance 변수는 외부에서 직접 접근할 수 없기에
객체가 외부에서 임의로 생성되거나 수정이불가능하다는 것이다!!
마찬가지로 생성자도 private으로 하는 이유가 외부에서 객체를 생성하지 못하기 위해서 다!
단, 공유는 해야되기 때문에 클래스만public인 것이다!!
Q3-2. public static GameManager Instance ← 그럼 이건 뭔데요?
public static GameManager Instance { }
이 녀석은 싱글톤 패턴에서 유일한 인스턴스 생성을 관리하는 프로퍼티(Property)이다.
public static GameManager Instance
{
get
{
if (instance == null) // 객체가 없을 때만 생성
{
instance = new GameManager(); // 최초로 객체 생성
}
return instance; // 이미 생성된 객체 반환
}
}
객체가 없으면 새로운 인스턴스를 만들고, 만약 인스턴스가 있으면 자신을 반환한다.
이미 생선된 인스턴스만 반환해서 동일한 객체를 계속 재사용한다는 뜻이다!
자 그럼 이제 테스트를 해보자!
internal class Program
{
static void Main(string[] args)
{
// 싱글톤 객체 호출하기
GameManager gm1 = GameManager.Instance;
GameManager gm2 = GameManager.Instance;
// 생성 확인
gm1.WriteGameMessage();
gm2.WriteGameMessage();
// 이 둘이 같은 객체인지 확인
if (Object.ReferenceEquals(gm1, gm2))
{
Console.WriteLine($"저흰 같아요! 값:" + Object.ReferenceEquals(gm1, gm2));
}
else
{
Console.WriteLine($"저런 다른 놈이네요! 값:" + Object.ReferenceEquals(gm1, gm2));
}
}
}
gm1과 gm2라는 객체를 만들었다. 만약 저 둘이 같다면 True를 반환, 다르다면 False를 출력할 것이다.
< 출력 결과 >
오 같은 인스턴스네요!
마지막으로 이를 통해서 알 수 있는 싱글톤 패턴의 특징을 정리해보자!
1. 인스턴스가 하나만존재한다!
Instance 프로퍼티를 통해서만 객체를 가져올 수 있다.
즉, new GameManager()가 단 한번만 실행되니까 모든 코드에서 같은 GameManager 인스턴스를 공유한다.
따라서 데이터의 일관성이 유지된다!
2. 전역적 접근이 가능하다!
static을 활용해 GameManager.Instance를 통해서 어디서든 쉽게 접근이 가능하다.
단, 너무 남용하면 결합도가 높아지기에 유지보수가 좀 까다로울 수 있다 (나중에 코드가 쌓이고 쌓여지면..?)
게임 개발함에 있어 다양한 매니저가 존재한다.
게임 매니저, 캐릭터 매니저, 오디오 매니저, UI 매니저, 데이터 저장 매니저, 네트워크 매니저 등...
수많은 매니저들이 존재하는데, 이들을 다루기 쉽게 해주는 싱글톤 기법에 대해서 알아보았다.
문제는 다른 사이트에서도 싱글톤은 남용하면 힘들다 했는데 이건 직접 실습을 진행한 후에 왜 남용하면 안되는지