소프트웨어를 개발하면서 반복적으로 발생하는 구조적 문제를 해결하기 위해 정리된 재사용 가능한 설계 방법
특정 클래스의 인스턴스가 하나만 존재하도록 하고, 어디서든 접근할 수 있게 만드는 패턴
using UnityEngine;
public class GameManager : MonoBehavior
{
public static GameManager instance;
private void Awake()
{
if(instance == null)
{
instance = this;
}
else
{
Destroy(gameObject);
}
}
}
public void GameStart()
{
Debug.Log("게임시작");
}
GameManager.instance.GameStart();
어디서든 쉽게 접근 가능하다
객체를 하나만 유지 가능하다.
공통 시스템 관리에 편리하다.
전역변수처럼 많이 쓰일 수 있다.
클래스 간 결합도가 높아질 수 있다. < 주의
초기화 순서 문제가 발생할 수 있다.
객체의 상태를 별도의 클래스로 분리하고, 현재의 상태에 따라 행동이 달라지도록 만드는 패턴
인터페이스를 만들어서
public interface IState
{
void Enter();
void Update();
void Exit();
}
상태 변경하는 함수로 제어할 수 있도록 한다.
public void ChangeState(IState nextState)
{
currentState?.Exit();
currentState = nextState;
currentState?.Enter();
}
public class Attack : Istate
{
void Enter()
{
// 상태 진입 시 한 번 실행
}
void Update()
{
// 상태 유지 중 반복 실행
}
void Exit()
{
// 상태 종료 시 한 번 실행
}
}
여러 행동이나 알고리즘을 각각 분리하고, 필요한 방식을 선택하거나 교체해서 사용하는 패턴
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 PlayerAttack
{
private IAttackStrategy attStrategy;
public void SetStrategy(IAttackStrategy strategy)
{
attackStrategy = strategy;
}
public void Attack()
{
attackStrategy?.Attack();
}
}
playerAttack.SetStrategy(new SwordAttack());
조건문을 줄일 수 있다.
실행 중 행동 교체가 가능하다.
새로운 행동을 추가하기 쉽다.
전략마다 클래스가 필요하다. < 클래스가 많아짐.
간단한 기능에 적용하면 복잡해질 수 있다.
한 객체의 상태가 변경될 때, 구독한 여러 객체에 변경 사실을 알리는 패턴.
PlayerHealth에 피격 이벤트가 발생했다 → UI / Sound / Controller에서 실행하거나 변경.
using System;
public class PlayerHealth : MonoBehaviour
{
public event Action<int, int> OnHealthChanged;
public event Action OnDead;
public void TakeDamage(int damage)
{
currentHealth -= damage;
OnHealthChanged?.Invoke(currentHealth, maxHealth);
if (currentHealth <= 0)
{
OnDead?.Invoke();
}
}
}
이미 다른 클래스에서 구독된 이벤트를 사용만 하도록.
private void OnEnable()
{
playerHealth.OnHealthChanged += UpdateHealthUI;
}
private void OnDisable()
{
playerHealth.OnHealthChanged -= UpdateHealthUI;
}
객체 간의 직접 참조를 줄일 수 있다.
하나의 이벤트에 여러 객체가 반응 가능하다.
게임 로직과 UI를 분리하기가 좋다.
이벤트 흐름을 추적하기 어렵다. < 여러 클래스에 나눠진 이벤트를 찾아야함
구독 해제를 하지 않으면 중복으로 호출될 수 있다. < 구독이 계속 중첩될 수 있다.
이벤트가 많아지면 디버깅이 어려워진다.
새로운 객체를 처음부터 만드는 대신, 기존의 객체를 복제하여 새로운 객체를 만드는 패턴.
프리펩을 사용하여 객체에 복사하는 방식 / ScriptableObject로 만든 데이터를 복사해서 가져오는 방식
복잡한 객체를 쉽게 생성가능
초기 설정을 반복하지 않아도됌.
원본을 기준으로 변형하는 것이 쉬움.
참조형 데이터가 공유되어 원본 훼손 가능.
알고리즘 전체 실행 순서를 부모 클래스에서 정의하고, 일부 단계만 자식 클래스에서 변경하는 패턴.
public abstract class Weapon
{
public void Attack()
{
PrepareAttack();
ExecuteAttack();
FinishAttack();
}
protected virtual void PrepareAttack()
{
Debug.Log("공격 준비");
}
protected abstract void ExecuteAttack();
protected virtual void FinishAttack()
{
Debug.Log("공격 종료");
}
}
클래스를 상속받아 자식 클래스는 실제 공격 부분만 구현한다.
public class Sword : Weapon
{
protected override void ExecuteAttack()
{
Debug.Log("검 공격");
}
}
전체 순서는 부모 클래스가 정함. 나머지 일부 과정만 자식 클래스가 구현한다.
공통 코드 중복을 줄일 수 있다.
실행 순서를 일정하게 유지할 수 있다.
자식 클래스는 필요한 부분만 구현할 수 있다.
상속에 강하게 의존한다.
부모 클래스 변경이 자식에게 영향을 준다.
실행 중에 행동을 바꾸기 힘들다. < 위에 예시를 보면 실제 공격부분만 구현되기때문에 준비 종료 구간을 다르게 구현하기 힘들다.
여러 개의 복잡한 시스템을 하나의 단순한 인터페이스로 묶어 제공하는 패턴.
예를 들면 게임시작을 할때 여러가지 선행되서 실행되는 UI라던가 BGM같이 필수적으로 실행되는 공통 작업을 묶어둔것.
public class GameFacade : MonoBehaviour
{
[SerializeField] private UIManager uiManager;
[SerializeField] private SoundManager soundManager;
[SerializeField] private PlayerSpawner playerSpawner;
[SerializeField] private RoomManager roomManager;
public void StartGame()
{
uiManager.ShowGameUI();
soundManager.PlayBattleBGM();
roomManager.StartRoom();
playerSpawner.SpawnPlayer();
scoreManager.SetScore(0);
}
public void EndGame()
{
roomManager.StopRoom();
soundManager.PlayGameOverBGM();
uiManager.ShowGameOverUI();
}
}
복잡한 기능을 묶어두어서 간단하게 사용가능하다.
호출 순서를 통일할 수 있다.
외부 객체의 의존성을 줄일 수 있다.
Facade가 너무 커질 수 있다.
모든 기능이 하나의 클래스에 집중될 수 있다. ← god class 조심
프로그램을 Model, View, Controller로 분리하여 데이터, 화면, 제어 역할을 나누는 구조패턴.
입력 → Controller → Model변경 → View갱신 순서로 이루어짐
public class PlayerController
{
private PlayerModel model;
private PlayerView view;
public PlayerController(PlayerModel model, PlayerView view)
{
this.model = model;
this.view = view;
}
public void TakeDamage(int damage)
{
model.TakeDamage(damage);
view.UpdateHealth(model.CurrentHealth, model.MaxHealth);
}
}
Controller에서 Model과 View를 제어
데이터와 화면을 분리 가능.
역할이 명확하다.
UI가 변경되도 Model을 재사용하기 쉽다.
Controller에 코드가 집중될 수 있다.
Unity구조와 완전히 일치하지 않을 수 있다.
Model, View, Presenter로 역할을 나누고, Presenter가 Model과 View 사이의 통신을 담당하는 구조패턴.
public class PlayerPresenter
{
private PlayerModel model;
private IPlayerView view;
public PlayerPresenter(PlayerModel model, IPlayerView view)
{
this.model = model;
this.view = view;
}
public void TakeDamage(int damage)
{
model.TakeDamage(damage);
view.UpdateHealth(
model.CurrentHealth,
model.MaxHealth
);
if (model.CurrentHealth <= 0)
{
view.ShowDeadUI();
}
}
}
Presenter가 Model이랑 View를 받아서 사용
view와 model을 명확히 분리가능
UI를 교체하거나 수정하기 쉽다.
presenter를 테스트하기 쉽다.
클래스와 인터페이스가 증가한다.
간단한 UI에는 과할 수 있다.
presenter가 너무 커질 수 있다.