[패턴] Singleton, Object Pool, State, EventBus, Observer, Strategy, Command

이정은·2025년 8월 6일

C#

목록 보기
7/8

Singleton

: 클래스를 하나의 인스턴스로만 존재하도록 만드는 패턴

  • 다른 클래스에서 쉽게 접근 가능 ⭕ → 코드 간결성 ⬆
  • 유일성 보장

간단한 방식의 싱글턴

using UnityEngine;

public class StudySingleton : MonoBehaviour
{
    // 외부 접근을 허용, 내부에서 설정 가능
    public static StudySingleton Instance { get; private set; }

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this; // 현재 객체를 싱글턴 인스턴스로 설정
        }
        else
        {
            Destroy(gameObject); // 중복 생성 방지
        }
    }
}

instance라는 변수가 static인 것임. 이는 static class와 다름.
-> static class(정적 클래스) 이미 메모리에 위치함
-> singleton은 new로 생성하기 전까지는 그냥 변수 자체만 있음. 즉, 원하는 시점에 유연하게 사용 가능하다는 뜻.


Generic <T>를 활용한 Singleton

public class SingletonMonobehaviour<T> : MonoBehaviour where T : MonoBehaviour
{
  static T instance = null;
  public static T Instance
  {
      get
      {
          if (instance == null)
          {
              instance = FindObjectOfType<T>();
              if (instance == null)
              {
                  var newObj = new GameObject(typeof(T).ToString());
                  instance = newObj.AddComponent<T>();
              }
          }
          return instance;
      }
  }

  protected virtual void Awake()
  {
      if (instance == null)
      {
          instance = this as T;
		        DontDestroyOnLoad(this.gameObject);
				}
      else
		        Destroy(this.gameObject);
  }
}

제너릭 싱글턴

  • 공통된 싱글턴 로직을 제너릭 기반 부모 클래스로 만들어서 재사용

  • 다양한 매니저 클래스들이 상속만으로 싱글턴 구현 가능







Object Pool

: 필요한 오브젝트를 미리 만들어 두고 재사용하는 패턴

Instantiate() & Destroy()를 반복하면 GC(가비지 컬렉션)와 CPU 부하가 커짐
→ 한 번 만든 객체를 꺼냈다가 다시 넣는 방식으로 재사용


Queue 활용한 오브젝트 풀

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StudyObjectPool : StudyGenericSingleton<StudyObjectPool>
{
    public Queue<GameObject> objQueue = new Queue<GameObject>(); // 오브젝트가 들어갈 풀
    public GameObject objPrefab; // 생성될 오브젝트

    public int poolSize = 500;
    
    void Start()
    {
        CreateObject();
    }

    private void CreateObject()
    {
        for (int i = 0; i < poolSize; i++)
        {
            GameObject newObj = Instantiate(objPrefab, transform);
            EnqueueObject(newObj);
        }
    }

    public void EnqueueObject(GameObject obj) // 오브젝트를 넣는 기능
    {
        objQueue.Enqueue(obj);
        obj.SetActive(false);
    }

    public GameObject DequeueObject() // 오브젝트를 뽑는 기능
    {
        GameObject obj = objQueue.Dequeue();
        
        return obj;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (objQueue.Count < 10)
                CreateObject();
            
            GameObject obj = DequeueObject(); // 풀에서 오브젝트를 뽑아서 사용
            obj.transform.SetPositionAndRotation(transform.position, Quaternion.identity);
        }
    }
}

유니티에 내장된 Pool 기능

using UnityEngine;
using UnityEngine.Pool;

public class PoolManager : MonoBehaviour
{
    public ObjectPool<GameObject> pool;
    public GameObject prefab;

    void Awake()
    {
        pool = new ObjectPool<GameObject>(CreateObject, OnGetObject, OnReleaseObject, OnDestroyObject, maxSize: 100);
    }
    
    private GameObject CreateObject()
    {
        GameObject obj = Instantiate(prefab);
        obj.SetActive(false);
        
        return obj;
    }

    private void OnGetObject(GameObject obj)
    {
        obj.SetActive(true);
    }

    private void OnReleaseObject(GameObject obj)
    {
        obj.SetActive(false);
    }

    private void OnDestroyObject(GameObject obj)
    {
        Destroy(obj);
    }
    
    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            GameObject obj = pool.Get();
        }
    }
}

using UnityEngine;

public class PoolItem : MonoBehaviour
{
    private PoolManager poolManager;

    private bool isInit = false;

    void Awake()
    {
        poolManager = FindFirstObjectByType<PoolManager>();
    }
    
    void OnEnable()
    {
        if (!isInit)
            isInit = true;
        else
            Invoke("ReturnObject", 2f);
    }
    
    void ReturnObject()
    {
        poolManager.pool.Release(gameObject);
    }
}







State (상태 패턴)

: 객체의 상태(Behavior)를 클래스로 분리해서 관리하는 패턴

  • 상태가 바뀌면 객체의 동작도 바뀜

  • if/else나 switch문으로 상태를 관리하는 대신, 상태별 클래스를 따로 두어 깔끔하게 관리


간단 예시

public interface IState { void Enter(); void Update(); void Exit(); }

public class IdleState : IState {
    public void Enter() { Debug.Log("Idle 시작"); }
    public void Update() { Debug.Log("Idle 중"); }
    public void Exit() { Debug.Log("Idle 끝"); }
}

public class Player {
    IState currentState;
    public void ChangeState(IState newState) {
        currentState?.Exit();
        currentState = newState;
        currentState.Enter();
    }
}

MonoBehaviour가 없는 경우

  • IState 인터페이스가 MonoBehaviour를 매개변수로 받음
    (StateEnter(MonoBehaviour mono) 이런 식)

  • IdleState, MoveState, AttackState는 일반 C# 클래스

  • StudyState(컨텍스트)가 상태 인스턴스를 new로 직접 생성


MonoBehaviour가 있는 경우

  • IState는 MonoBehaviour 상속 안 받음, 그러나 상태 클래스가 MonoBehaviour를 직접 상속

  • IdleState, MoveState, AttackState는 MonoBehaviour 컴포넌트

  • StudyState가 gameObject.AddComponent()처럼 상태를 GameObject에 붙여서 사용

+) Unity에서 MonoBehaviour는 일반 클래스가 아니라 Unity 엔진이 직접 관리하는 "컴포넌트"이기 때문에 "new"로 만들면 안된다.







EventBus

: 객체가 게시(Publiish)/구독(Subscribe)할 수 있는 전역 이벤트를 관리하는 중앙 허브 방식 패턴

  • 이벤트 버스 패턴은 전역 이벤트 시스템을 만들어서, 오브젝트 간 느슨한 결합(Loose Coupling)을 유지하면서 메시지를 주고받을 수 있도록 하는 디자인 패턴
  • 발신자(Sender)와 수신자(Receiver) 간의 직접적인 의존성을 없애고, 이벤트를 중앙에서 관리하는 방식

간단한 예시

public static class EventBus {
  public static event Action OnGameStart;
  public static void PublishGameStart() => OnGameStart?.Invoke();
}

public class GameUI {
  void OnEnable() => EventBus.OnGameStart += ShowStartUI;
  void ShowStartUI() => Debug.Log("게임 시작 UI 표시");
}

public class GameManager {
  void StartGame() => EventBus.PublishGameStart();
}

추가 예시

  • StudyEventBus = 모든 곳에서 이벤트를 등록할 수 있도록 설정된 이벤트 버스
  • ScoreManager = 이벤트 버스에 탑승하는 스코어 매니저
  • ScoreController = 이벤트를 발생시키는 컨트롤러







Observer

: 주체(Subject)가 있고, 여러 관찰자(Observer)가 주체의 상태 변화를 감시하는 패턴

  • 주체가 상태 변화를 알리면, 구독 중인 관찰자들이 반응

  • EventBus는 Observer를 일반화한 구현체 중 하나라고 볼 수 있음

  • EventBus와 다른 점 : 옵저버는 주체가 있고(옵저버가 주체에 붙어있음) static이 아님.

간단한 예시

public interface IObserver { void UpdateState(int value); }

public class Subject {
    List<IObserver> observers = new();
    int state;
    
    public void Attach(IObserver observer) => observers.Add(observer);
    public void SetState(int value) {
        state = value;
        Notify();
    }
    void Notify() {
        foreach (var o in observers) o.UpdateState(state);
    }
}

public class HUD : IObserver {
    public void UpdateState(int value) => Debug.Log($"HUD 업데이트: {value}");
}

추가 예시

  • interface IObserver = 알림 기능이 있는 인터페이스
  • ObserverListner = 관측하는 옵저버
  • QuestManager = 관측하는 옵저버 (퀘스트 관리자)
  • interface ISubject = 관측 대상을 설정하는 인터페이스
  • Subject = 관측 대상







Strategy

: 동작을 캡슐화하여 런타임에 알고리즘을 쉽게 교체하는 패턴

  • 상황에 맞는 전략(알고리즘)을 바꾸면, 동작 방식이 달라짐

  • State 패턴과 비슷하지만, State는 상태 변화에 따른 행동 변경, Strategy는 행동(알고리즘) 자체를 바꾸는 것이 목적


구조 예시

public interface IAttackStrategy {
    void Attack();
}

public class SwordAttack : IAttackStrategy {
    public void Attack() => Debug.Log("검 공격!");
}

public class BowAttack : IAttackStrategy {
    public void Attack() => Debug.Log("활 공격!");
}

public class Character {
    private IAttackStrategy attackStrategy;
    
    public void SetAttackStrategy(IAttackStrategy strategy) {
        attackStrategy = strategy;
    }
    
    public void PerformAttack() {
        attackStrategy?.Attack();
    }
}

사용

Character player = new Character();
player.SetAttackStrategy(new SwordAttack());
player.PerformAttack(); // 검 공격!

player.SetAttackStrategy(new BowAttack());
player.PerformAttack(); // 활 공격!







Command

: 명령(행위)을 객체로 만들어 캡슐화 하는 것

  • 리모컨에 비유할 수 있음
  • 입력 처리, Undo/Redo, 매크로 실행 등에 적합

구조 예시

public interface ICommand {
    void Execute();
}

public class JumpCommand : ICommand {
    public void Execute() => Debug.Log("점프!");
}

public class FireCommand : ICommand {
    public void Execute() => Debug.Log("발사!");
}

public class InputHandler {
    private ICommand command;
    
    public void SetCommand(ICommand cmd) {
        command = cmd;
    }
    
    public void HandleInput() {
        command?.Execute();
    }
}

사용

InputHandler handler = new InputHandler();
handler.SetCommand(new JumpCommand());
handler.HandleInput(); // 점프!

handler.SetCommand(new FireCommand());
handler.HandleInput(); // 발사!







0개의 댓글