디자인 패턴에 대해서 소프트웨어 설계 관점에서 정확하게 이해하고
대표적인 패턴 4가지를 정리한다.
디자인 패턴은 소프트웨어 설계에서
자주 마주치는 문제를 해결하기 위해 검증된 ‘템플릿’ 같은 해법을 의미한다.
알고리즘과 베스트 프랙티스는 구체적이고 해결에 대한 정답인 반면,
디자인 패턴은 그저 ‘이런 설계로 구현할 수 있다’이다.
[참고] : Best Practice
특정 분야나 상황에서 가장 효율적이고 효과적인 검증된 방법 또는 절차를 의미한다.
소프트웨어 개발에서 코드의 품질을 높이고, 유지보수를 용이하게 하며, 협업 효율을 극대화한다.
대표적으로
MonoBehaviour 남용 지양 → 데이터 중심 클래스(SO)와 로직 중심 클래스(MonoBehaviour)를 분리,
Update(), FixedUpdate() 사용 최적화 → 각 용도에 맞는 처리 로직 구현 등이 있다.
협업 측면에서 공통된 용어 정립을 통해 개발자들 간의 빠른 의사소통을 할 수 있다는 장점과
검증된 개발 패턴이라 개발 속도가 향상된다는 장점이 있지만
디자인 패턴은 추상적이고 범용적고, 완성된 설계가 아니다.
즉, 무기가 아니라 상황에 맞춰 적절히 선택할 수 있는 도구다.
남용할 경우 잘못 적용하는 경우가 빈번하고
오히려 프로그램을 더 복잡하게 만든다.
대표적으로 Unity에서 사용하는 디자인 패턴 4가지를 살펴보자.
클래스의 인스턴스를 오직 하나만 생성하고,
전역적으로 접근할 수 있도록 하는 설계다.
게임 전체에서 하나만 존재해야 하는 관리자를 만들기 위함.
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this) {
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
위 구조를 활용하여 매니저 클래스마다 싱글톤을 구현하는 방식도 있지만,
아래처럼 싱글톤 베이스 클래스를 활용해서 상속 구조로 관리하는 방법도 있다.
using UnityEngine;
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
private static object _lock = new object();
private static bool isShuttingDown = false;
public static T Instance
{
get
{
if (isShuttingDown)
{
Debug.LogWarning($"[Singleton] {typeof(T)} is shutting down. Returning null.");
return null;
}
lock (_lock)
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
GameObject singletonObject = new GameObject(typeof(T).Name);
instance = singletonObject.AddComponent<T>();
DontDestroyOnLoad(singletonObject);
}
}
return instance;
}
}
}
protected virtual void OnApplicationQuit() => isShuttingDown = true;
protected virtual void OnDestroy()
{
if (instance == this) isShuttingDown = true;
}
}
//---------------------------------------------------------------------------------
public class GameManager : Singleton<GameManager>
{
public int score;
protected override void OnApplicationQuit()
{
base.OnApplicationQuit();
// 추가 종료 처리
}
void Start()
{
Debug.Log("GameManager Start");
}
public void AddScore(int amount)
{
score += amount;
}
}
자주 생성/삭제되는 오브젝트를 미리 생성해 재사용하는 방식이다.
미리 FIFO 방식의 큐인 풀(Queue)에 비활성화된 상태로 생성하고,
활성화하여 꺼내 쓴 뒤,
삭제하지않고 다시 비활성화하여 풀에 집어넣는 방식이다.
Instantiate()/Destroy()는 비용을 줄여서, GC를 줄이고 FPS 드랍 방지한다.
public class ObjectPool<T> where T : Component
{
//오브젝트를 저장하는 큐
private Queue<T> pool = new Queue<T>();
//생성할 오브젝트 프리팹
private T prefab;
//생성할 오브젝트의 부모 트랜스폼
private Transform parent;
//생성자 : 풀 초기화
public ObjectPool(T prefab, int size, Transform parent = null) {
this.prefab = prefab;
this.parent = parent;
//미리 생성해두기
for (int i = 0; i < size; i++) {
T obj = GameObject.Instantiate(prefab, parent);
//비활성화 해서
obj.gameObject.SetActive(false);
//풀(큐)에 추가해둔다.
pool.Enqueue(obj);
}
}
//풀에서 하나 꺼내기 -> 기존Instantiate()처럼 사용하면 된다.
public T Get() {
//풀에 남은게 없으면 새로 생성해서 큐에 추가
if (pool.Count == 0) {
T obj = GameObject.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
//하나 꺼내서 활성화 후 리턴
T item = pool.Dequeue();
item.gameObject.SetActive(true);
return item;
}
//사용한 오브젝트 반납 -> Destory()처럼 사용하면 된다.
public void ReturnToPool(T item) {
item.gameObject.SetActive(false);
pool.Enqueue(item);
}
}
객체 생성을 클래스 내부에서 하지 않고, 전용 Factory에서 생성하게 하는 방식
객체 생성 로직을 분리하여, 다양한 서브 클래스를 유연하게 생성
public abstract class Enemy
{
public abstract void Attack();
}
public class Zombie : Enemy
{
public override void Attack() => Debug.Log("Zombie Attack!");
}
public class Skeleton : Enemy
{
public override void Attack() => Debug.Log("Skeleton Attack!");
}
public class EnemyFactory
{
public static Enemy CreateEnemy(string type)
{
switch (type)
{
case "Zombie": return new Zombie();
case "Skeleton": return new Skeleton();
default: return null;
}
}
}
한 객체의 상태 변화가, 등록된 여러 객체에게 자동으로 알리는 방식
즉, 1 : N 구조
이벤트 기반 시스템에서, UI갱신이나 게임 상태 변경, 또는 플레이어 사망 알림 등에 활용한다.
public class Player : MonoBehaviour
{
public delegate void OnHealthChanged(int hp);
public static event OnHealthChanged onHealthChanged;
private int hp = 100;
public void TakeDamage(int dmg)
{
hp -= dmg;
onHealthChanged?.Invoke(hp); // 옵저버들에게 알림
}
}
public class UI_HealthBar : MonoBehaviour
{
void OnEnable() => Player.onHealthChanged += UpdateUI;
void OnDisable() => Player.onHealthChanged -= UpdateUI;
void UpdateUI(int hp)
{
Debug.Log($"HP: {hp}"); // UI 반영
}
}