- 오브젝트 풀링(Object Pooling)이란?

반복적으로 사용되는 객체를 효율적으로 관리하는 생성 패턴으로

게임을 예시로 들자면 슈팅, FPS, TPS 게임 같이 같은 탄환이 여러 개가 발사되는 경우

탄환 객체 생성을 최적화 하기 위해 적용하는 객체 재사용 기법이다!


- 오브젝트 풀링 예시 [Unity]

- 탄환 오브젝트 풀링 코드

public class BulletPool : MonoBehaviour
{
    [SerializeField]
    private GameObject bulletPrefab;
    [SerializeField]
    private int poolSize = 10;  // 풀 사이즈

    [SerializeField] 
    private Transform parentTransform;  // 부모 오브젝트의 Transform
    [SerializeField] List<GameObject> bulletPool;


    void Awake()
    {
        bulletPool = new List<GameObject>();

        for (int i = 0; i < poolSize; i++)
        {
            // GameObject bullet = Instantiate(bulletPrefab);
            GameObject bullet = Instantiate(bulletPrefab, parentTransform);  // 부모를 지정하여 총알 생성
            bullet.SetActive(false);
            bulletPool.Add(bullet);
        }
    }

    public GameObject GetBullet()
    {
        foreach (var bullet in bulletPool)
        {
            if (!bullet.activeInHierarchy)
            {
                bullet.SetActive(true);
                return bullet;
            }
        }
        GameObject newBullet = Instantiate(bulletPrefab);
        bulletPool.Add(newBullet);
        return newBullet;
    }

    public void ReturnBullet(GameObject bullet)
    {
        bullet.SetActive(false);
    }
}

오브젝트 풀링을 적용한 부분

1. 미리 총알을 프리팹으로 저장한다!  -   Awake( ) 부분

    void Awake()
    {
        bulletPool = new List<GameObject>();

        for (int i = 0; i < poolSize; i++)
        {
            // GameObject bullet = Instantiate(bulletPrefab);
            GameObject bullet = Instantiate(bulletPrefab, parentTransform);  // 부모를 지정하여 총알 생성
            bullet.SetActive(false);
            bulletPool.Add(bullet);
        }
    }

 

poolSize만큼 Instantiate()를 호출해서 총알 프리팹을 미리 생성해 리스트에 저장시킨다!


2. 비활성화 된 총알을 재사용 시킨다!  -   GetBullet() 부분

    public GameObject GetBullet()
    {
        foreach (var bullet in bulletPool)
        {
            if (!bullet.activeInHierarchy)
            {
                bullet.SetActive(true);
                return bullet;
            }
        }
        GameObject newBullet = Instantiate(bulletPrefab);
        bulletPool.Add(newBullet);
        return newBullet;
    }

 

foreach 문을 통해 Pool에서 비활성화된 총알을 찾아 활성화 후 반환시킨다!


3. 사용이 끝난 총알을 다시 비활성화 시킨다!   -   ReturnBullet() 부분

    public void ReturnBullet(GameObject bullet)
    {
        bullet.SetActive(false);
    }

SetActive(false)를 호출시켜서 다시 Pool에 반환한다는 뜻이다!


 

총알뿐만 아니라 등장하는 적이 같은 종류일때 오브젝트 풀링을 적용시킬 수 있다!

 

오브젝트 풀링 예시 테스트

 

- 프로토타입(Prototype) 패턴이란?

초간단 프로토타입 예시

 

게임으로 예시를 들자면 몬스터 원형이 있을거고 이 원형에서 변종인 녀석이 있다 치자

(몬스터 기본형, 정예형 몬스터)

 

정예형 몬스터일 경우 사용되는 패턴이나 기술 등은 유사하지만 능력치가 좀 더 강화된 특징을 갖는다면

새로운 몬스터 클래스를 만들필요없이 기본 객체를 복사해서 사용하는 방식이다!


- 프로토타입(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)를 만들었다!

기존 객체를 복제해서 재사용하는 기법이다!

[Prototype 테스트 결과]

Builder를 간단하게 이해해보자!

 

 

RPG 게임에서 캐릭터가 가진 속성을 생각해보자

체력, 방어, 속도, 마나게이지 뿐만 아니라 가지고 있는 기술, 아직 해금되지 않는 기술들도

전부 캐릭터가 가진 속성이다. 이 캐릭터생성자로 만든다고 생각해보자.

 

- 생성자로 캐릭터를 만들 경우

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}");
        }
    }

2. 빌더 인터페이스를 정의한다.

    // 빌더 인터페이스
    public interface ICharaBuilder
    {
        ICharaBuilder SetName(string name);
        ICharaBuilder SetJob(string job);
        ICharaBuilder SetWeapon(string weapon);
        ICharaBuilder SetHealth(int health);
        ICharaBuilder SetMana(int mana);
        PlayerCharacter Build();
    }

 


3. 생성하는 방법(How)을 정의한다!

    // 빌더 구현 방법
    public class CharacterBuilder : ICharaBuilder
    {
        private PlayerCharacter player = new PlayerCharacter();

        public ICharaBuilder SetName(string name)
        {
            player.Name = name;
            return this;
        }

        public ICharaBuilder SetJob(string job)
        {
            player.Job = job;
            return this;
        }

        public ICharaBuilder SetWeapon(string weapon)
        {
            player.Weapon = weapon;
            return this;
        }

        public ICharaBuilder SetHealth(int health)
        {
            player.Health = health;
            return this;
        }

        public ICharaBuilder SetMana(int mana)
        {
            player.Mana = mana;
            return this;
        }

        public PlayerCharacter Build()
        {
            return player;
        }
    }

 

생성하는 방법 설계가 끝났다면 이제 구현할 준비가 다 끝났다는 소리다!

마지막으로 Main에서 구현해서 실행시켜보자!


3. Main에서 구현(What)한다!

    internal class Program
    {
        static void Main(string[] args)
        {
            PlayerCharacter player = new CharacterBuilder()
                .SetName("전사")
                .SetJob("Warrior")
                .SetWeapon("대검")
                .SetHealth(150)
                .SetMana(50)
                .Build();

            player.PlayerShow();
        }
    }

 

테스트 결과1


- 어? 속도 (Speed) 속성을 추가해야해요!

추후 게임이 개발되거나 운영할 때 새로운 속성이 필요하는 경우가 발생할 수 있다!

새롭게 추가될 때 기존에 작성했던 코드들을 다 바꿔줘야 하는가? 이러면 너무 힘들다....ㅠㅠ

 

이 부분이 Builder 패턴이 유지보수가 용이하다는 점이다!

Speed 관련된 속성과 메서드만 추가하고 기존에 미리 만들었던 메서드들은 일절 건들지 않기 때문이다! (OCP 법칙)

ICharaBuilder SetSpeed(int speed);

먼저 빌더 인터페이스 부분에 Speed를 추가하고

        public ICharaBuilder SetSpeed(int speed)
        { 
            player.Speed = speed;
            return this;
        }

그 다음 빌더 코드에서 속도 코드를 새로 추가한다!

Main에서 속도 추가하기

마지막으로 Main에서 캐릭터를 생성할 때 속도만 추가하면

 

테스트 결과2

 

이렇게 기존 코드는 건들지 않으면서 속도 속성만 추가된 것을 확인 할 수 있다!

 

 

- 최종 정리

  코드 설명
객체를 생성하는 방법 CharacterBuilder 클래스 객체를 어떻게 생성할지(How)를 정의한다!
객체를 구현하는 방법 new CharacterBuilder().SetName()... 부분 빌더를 사용해서 실제 객체를 구현한다!

 

 

- 추상 팩토리(Abstract Factory)란?

 

추상 팩토리 게임 예시

 

RPG 게임을 예시로 들자면 각 스테이지 별로 등장하는 몬스터가 다르다고 하자

몬스터를 생성하는 곳한군데에 몰아넣고

이곳에 있는 팩토리 중 1개를 골라서 생성하는 것!

 

스테이지에 따라 몬스터를 관리하는 공장이 있고(팩토리1, 팩토리2...)

이러한 몬스터를 생성하는 공장들동시에 관리하는 게 추상 팩토리라고 생각하면 된다!

 


- 추상 팩토리 사용 예시(C#)

1. 먼저 몬스터 인터페이스를 정의한다

    // 몬스터 인터페이스
    interface IMonster
    {
        void Attack();	// 공격 정의
    }

2. 인터페이스를 상속받은 몬스터들을 만든다

    // 초원 스테이지에 등장하는 몬스터들
    class Slime : IMonster
    {
        public void Attack() => Console.WriteLine("슬라임의 점프 공격!");
    }

    // 동굴 스테이지에 등장하는 몬스터들
    class Goblin : IMonster
    {
        public void Attack() => Console.WriteLine("고블린이 단검을 던졌다!");
    }

    class Troll : IMonster
    {
        public void Attack() => Console.WriteLine("트롤이 격분했다!");
    }


3. 추상 팩토리 인터페이스를 정의한다

// 추상 팩토리
interface IStageFactory
{
    IMonster CreateMonster();
    IItem CreateItem();
}

4. 추상 팩토리를 상속받아 각 스테이지별 팩토리를 만든다

    // 초원 스테이지 팩토리
    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();
        }
    }

실행결과

 

 

 

추상 팩토리 패턴연관된 객체(몬스터 + 아이템 등)를 세트로 생성할 때 사용되는

디자인 패턴이다. 게임에서 스테이지, 종족, 장비 등의 세트를 한 번에 생성해야 할 때 사용된다.

(예를 들어 게임 클리어 시 드랍되는 장비)


하지만 '새로운 객체 유형을 추가해야 될 때'유지보수가 어려울 수 있으며,

특히 객체를 대량으로 생성해야 할 때 는 다른 방식을 설계해야 한다.

 

- 전략 패턴(Strategy Pattern)이란?

객체가 수행하는 여러가지 행동(행위) 중에서 상황에 맞는 적절한 전략을 선택해서 실행하는 패턴을 말한다.

쉽게 말해 "알고리즘을(행위를) 통째로 바꿔서 적용할 수 있는 패턴" 이라고 생각하면 된다.

 

좀 더 쉽게 이해하기 위해서 게임으로 예시를 들어보자!

 

전략 패턴의 게임 예시

 

RPG 게임에서 적 캐릭터(몬스터)가 있다고 가정해보자.

몬스터는 플레이어와의 거리가 가까우면 근접으로 공격하고 멀다면 원거리에서 공격한다.

이 몬스터만이 가지고 있는 고유의 패턴을 만들때 전략 패턴을 사용하는 것!

 

또는 보스 몬스터만이 가지고 있는 패턴이 존재할 수 있는데 이 때에도 전략 패턴을 사용할 수 있다.

 

  • HP 100%~50% → 원거리 공격 전략
  • HP 50%~20% → 돌진 공격 전략
  • HP 20% 이하 → 발악 기술 전략

이런 식으로 패턴을 설계해서 구현할 수 있는 것!


 

- 그렇다면 전략 패턴을 어떻게 사용하나요?

이번에도 간단한 c# 게임 코드를 통해서 예를 들어보자!

1. 우선 기술 전략 인터페이스를 먼저 정의한다!

// 스킬 전략
public interface ISkillStrategy
{
    string SkillName { get; }
    void Skill();
}

2. 전략 인터페이스상속받아 다양한 스킬 전략을 만든다!

// 근접 기술 전략
public class Skill_Melee : ISkillStrategy
{
    public string SkillName => "근접 기술 A";

    public void Skill()
    {
        Console.WriteLine($"{SkillName}로 공격!");
    }
}

// 원거리 기술 전략
public class Skill_Range : ISkillStrategy
{
    public string SkillName => "원거리 기술 B";

    public void Skill()
    {
        Console.WriteLine($"멀리서 {SkillName}로 공격!");
    }
}

// 치유 기술 전략
public class Skill_Heal : ISkillStrategy
{
    public string SkillName => "치유 기술 C";

    public void Skill()
    {
        Console.WriteLine($"{SkillName}로 상처를 치료!");
    }
}

 


3. 적용할 대상에게(클래스한테) 전략을 적용시킨다.

    // 몬스터에게 전략 적용
    public class Enemy1 { 
        private string enemyName = "고블린";
        private ISkillStrategy skillStrategy;

        // 전략 변경
        public void SetSkillStrategy(ISkillStrategy skillStrategy)
        {
            this.skillStrategy = skillStrategy;
        }

        // 스킬 사용
        public void takeSkill()
        {
            Console.WriteLine($"\n{enemyName}이 {skillStrategy.SkillName} 기술을 사용했다!");
            skillStrategy.Skill();
        }
    }

3-1. 사용 테스트 1

    internal class Program
    {
        static void Main(string[] args)
        {
            Enemy1 enemy = new Enemy1();

            // 근접 기술 사용
            enemy.SetSkillStrategy(new Skill_Melee());
            enemy.takeSkill();

            // 전략 수정 후 원거리 기술 사용
            enemy.SetSkillStrategy(new Skill_Range());
            enemy.takeSkill();

            // 전략 수정 후 아군 치유 기술 사용
            enemy.SetSkillStrategy(new Skill_Heal());
            enemy.takeSkill();
        }
    }

 

테스트 결과

테스트 결과

 

이런 식으로 행동을 상황에 맞게 교체가능한 전략으로 분리해주는 패턴이다!

이제 마지막으로 상황에 맞게끔 사용한다고 예시를 들어보자면

3-2. 사용 테스트 2

    internal class Program
    {
        static void Main(string[] args)
        {
            Enemy1 enemy = new Enemy1();

            int enemyMaxHp = 30;        // 최대 체력
            int enemyCurHp = 14;        // 현재 체력
            int playerDistance = 5;     // 플레이어과의 거리

            // 상황에 따른 전략 결정
            if (enemyCurHp <= enemyMaxHp * 0.5)
            {
                enemy.SetSkillStrategy(new Skill_Heal());
            }
            else if (playerDistance <= 5)
            {
                enemy.SetSkillStrategy(new Skill_Melee());
            }
            else
            {
                enemy.SetSkillStrategy(new Skill_Range());
            }

            // 전략에 맞는 기술 사용
            enemy.takeSkill();
        }
    }

테스트 결과 2

 

이렇게 현재 체력이 최대 체력의 절반 이하라서 전략을 치유 스킬 전략으로 바꿔서 사용한 것이다!

 

그 외에도 스킬에 재사용 대기시간을 부여하거나,

랜덤값을 줘서 무작위 기술을 사용하도록 설계하거나

전략을 원하는데로 교체하도록 구현할 수 있다!

 


지난번에는 싱글톤에 대해 알아보았다면 이번에는 생성 디자인 패턴팩토리 메서드에 대해서 알아보도록 하자!

 

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 = EnemyFactory.CreateEnemy("Goblin");
Enemy enemy2 = EnemyFactory.CreateEnemy("Orc");

 

Main 에서 몬스터 생성 요청만 하고 있는 것을 확인할 수 있다.


Q3-2. ??? 팩토리 메서드를 사용하지 않는 방법과 차이점이 뭔가요?

- 팩토리 메서드를 사용하지 않는 경우, Main에서 직접 객체를 생성해야 한다.

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 싱글톤 인스턴스 등장!");
    }
}

 

이 코드에선 중요한 부분이 2가지가 있다.

private static GameManager instance;	// 인스턴스
private 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 매니저, 데이터 저장 매니저, 네트워크 매니저 등...

수많은 매니저들이 존재하는데, 이들을 다루기 쉽게 해주는 싱글톤 기법에 대해서 알아보았다.

 

문제는 다른 사이트에서도 싱글톤은 남용하면 힘들다 했는데 이건 직접 실습을 진행한 후에 왜 남용하면 안되는지

추후 이해해본 뒤에 작성해보도록 하겠습니다!

 

더보기
namespace CreatePattern_01_Singleton
{
    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 싱글톤 인스턴스 등장!");
        }
    }

    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));
            }
        }
    }
}

 

 

+ Recent posts