Strategy Pattern
전략 패턴은 동일한 실행 구조를 가지지만, 실행하는 내용이 런타임에 달라질 수 있도록 만드는 패턴이다.
예를 들어, AI가 행동을 할 때, 행동을 한다는 것은 동일하지만 실제로 어떤 행동을 하는지는 상태에 따라 다를 수 있다.
간단하게 공격과 방어를 한다고 가정해 보자.
공격을 할 때는 플레이어를 찾아 추격할 것이다.
반대로 방어를 할 때는 플레이어와 일정 거리를 유지하도록 할 것이다.
두 상태 모두 동작을 한다는 것은 동일하나 실제로 실행하는 동작은 다르다.
// 전략 인터페이스
public interface IEnemyBehavior
{
void Act(Transform enemyTransform, Transform playerTransform);
}
// 구체적인 전략 클래스들
public class AggressiveBehavior : IEnemyBehavior
{
public void Act(Transform enemyTransform, Transform playerTransform)
{
// 플레이어를 향해 빠르게 추격하고 공격하는 로직
Vector3 direction = (playerTransform.position - enemyTransform.position).normalized;
enemyTransform.position += direction * Time.deltaTime * 5f;
}
}
public class DefensiveBehavior : IEnemyBehavior
{
private float safeDistance = 5f;
public void Act(Transform enemyTransform, Transform playerTransform)
{
// 일정 거리를 유지하며 원거리 공격을 하는 로직
float distance = Vector3.Distance(enemyTransform.position, playerTransform.position);
if (distance < safeDistance)
{
Vector3 direction = (enemyTransform.position - playerTransform.position).normalized;
enemyTransform.position += direction * Time.deltaTime * 3f;
}
}
}
// 컨텍스트 클래스
public class Enemy : MonoBehaviour
{
private IEnemyBehavior currentBehavior;
private Transform playerTransform;
private void Start()
{
playerTransform = GameObject.FindGameObjectWithTag("Player").transform;
currentBehavior = new AggressiveBehavior();
}
public void SetBehavior(IEnemyBehavior behavior)
{
currentBehavior = behavior;
}
private void Update()
{
if (currentBehavior != null)
{
currentBehavior.Act(transform, playerTransform);
}
}
}
이렇게 전략 패턴을 사용하면 다형성을 이용하여 동일한 구조로 다른 동작을 런타임에 변경할 수 있게 되어 유연성이 증가한다.
같은 구조로 다른 동작을 한다는 의미에서 템플릿 메서드 패턴과 혼동할 수 있다.
템플릿 메소드 패턴은 어떠한 로직이 실행되는 순서나 구조를 미리 정해두고 그 로직이 실행되는 내용은 상속을 통해 변경하는 패턴이다.
어떻게 보면 템플릿 메서드 패턴은 정적으로 다형성을 제공한다고 할 수 있다.
즉, 미리 구조를 짜놓고 이를 상속받은 하위 클래스에서 동작을 정의하는 것이다.
// 추상 클래스
public abstract class Item : MonoBehaviour
{
[SerializeField] protected string itemName;
[SerializeField] protected float cooldown;
// 템플릿 메소드
public void Use(GameObject player)
{
if (!CanUse()) return;
BeforeUse();
ApplyEffect(player);
AfterUse();
StartCoroutine(CooldownRoutine());
}
protected virtual void BeforeUse()
{
Debug.Log($"{itemName} 사용 시작");
}
protected abstract void ApplyEffect(GameObject player);
protected virtual void AfterUse()
{
Debug.Log($"{itemName} 사용 완료");
}
protected virtual bool CanUse()
{
return !isOnCooldown;
}
private bool isOnCooldown = false;
private IEnumerator CooldownRoutine()
{
isOnCooldown = true;
yield return new WaitForSeconds(cooldown);
isOnCooldown = false;
}
}
// 구체 클래스들
public class HealthPotion : Item
{
[SerializeField] private float healAmount;
protected override void ApplyEffect(GameObject player)
{
PlayerHealth health = player.GetComponent<PlayerHealth>();
if (health != null)
{
health.Heal(healAmount);
}
}
}
public class SpeedBoost : Item
{
[SerializeField] private float speedMultiplier;
[SerializeField] private float duration;
protected override void ApplyEffect(GameObject player)
{
PlayerMovement movement = player.GetComponent<PlayerMovement>();
if (movement != null)
{
StartCoroutine(SpeedBoostRoutine(movement));
}
}
private IEnumerator SpeedBoostRoutine(PlayerMovement movement)
{
float originalSpeed = movement.moveSpeed;
movement.moveSpeed *= speedMultiplier;
yield return new WaitForSeconds(duration);
movement.moveSpeed = originalSpeed;
}
}
주요 차이점:
- 구현 방식
- 전략 패턴: 구성(Composition)을 사용하여 런타임에 동적으로 알고리즘을 교체할 수 있다.
- 템플릿 메소드 패턴: 상속(Inheritance)을 사용하여 컴파일 타임에 알고리즘의 구조가 결정된다.
- 유연성
- 전략 패턴: 더 유연하며, 실행 중에도 전략을 변경할 수 있다.
- 템플릿 메소드 패턴: 상대적으로 덜 유연하지만, 알고리즘의 구조를 더 명확하게 정의할 수 있다.
- 사용 시기
- 전략 패턴: 비슷한 동작을 하는 다양한 알고리즘이 필요하고, 이들을 동적으로 교체해야 할 때 사용한다.
- 템플릿 메소드 패턴: 알고리즘의 구조는 고정되어 있고, 일부 단계만 다르게 구현해야 할 때 사용한다.
Object Pool Pattern
오브젝트 풀 패턴은 오브젝트를 여러 개 만들어 놓고 필요할 때 가져와 사용한 뒤 다시 반납하는 패턴이다.
오브젝트 풀 패턴을 사용하면 오브젝트를 사용할 때 계속하여 메모리를 할당해야 하는 문제를 해결할 수 있다.
즉, GameObject의 Instantiate와 Destroy 호출을 줄여 가비지 컬렉션 부하를 감소시킬 수 있다.
자주 사용되며 계속해서 메모리를 할당하고 해제하는 오브젝트들에게 적용하면 좋아 보인다.
// 풀링할 객체가 구현해야 하는 인터페이스
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}
// 오브젝트 풀 매니저
public class ObjectPool : MonoBehaviour
{
[System.Serializable]
public class Pool
{
public string tag;
public GameObject prefab;
public int size;
}
[SerializeField] private List<Pool> pools;
private Dictionary<string, Queue<GameObject>> poolDictionary;
public static ObjectPool Instance { get; private set; }
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
return;
}
poolDictionary = new Dictionary<string, Queue<GameObject>>();
// 풀 초기화
foreach (Pool pool in pools)
{
Queue<GameObject> objectPool = new Queue<GameObject>();
for (int i = 0; i < pool.size; i++)
{
GameObject obj = Instantiate(pool.prefab);
obj.SetActive(false);
objectPool.Enqueue(obj);
}
poolDictionary.Add(pool.tag, objectPool);
}
}
public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation)
{
if (!poolDictionary.ContainsKey(tag))
{
Debug.LogWarning($"Pool with tag {tag} doesn't exist.");
return null;
}
Queue<GameObject> pool = poolDictionary[tag];
// 풀이 비어있으면 새로운 객체 생성
if (pool.Count == 0)
{
Pool poolInfo = pools.Find(p => p.tag == tag);
GameObject newObj = Instantiate(poolInfo.prefab);
pool.Enqueue(newObj);
}
GameObject objectToSpawn = pool.Dequeue();
objectToSpawn.SetActive(true);
objectToSpawn.transform.position = position;
objectToSpawn.transform.rotation = rotation;
IPoolable poolable = objectToSpawn.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnSpawn();
}
pool.Enqueue(objectToSpawn);
return objectToSpawn;
}
public void ReturnToPool(string tag, GameObject objectToReturn)
{
if (!poolDictionary.ContainsKey(tag))
{
Debug.LogWarning($"Pool with tag {tag} doesn't exist.");
return;
}
IPoolable poolable = objectToReturn.GetComponent<IPoolable>();
if (poolable != null)
{
poolable.OnDespawn();
}
objectToReturn.SetActive(false);
}
}
// 풀링될 객체의 예시 (총알)
public class Bullet : MonoBehaviour, IPoolable
{
[SerializeField] private float speed = 10f;
[SerializeField] private float lifetime = 3f;
private Coroutine lifetimeCoroutine;
public void OnSpawn()
{
// 총알이 재사용될 때 초기화
if (lifetimeCoroutine != null)
{
StopCoroutine(lifetimeCoroutine);
}
lifetimeCoroutine = StartCoroutine(LifetimeRoutine());
}
public void OnDespawn()
{
// 총알이 풀로 반환될 때 정리
if (lifetimeCoroutine != null)
{
StopCoroutine(lifetimeCoroutine);
lifetimeCoroutine = null;
}
}
private void Update()
{
transform.Translate(Vector3.forward * speed * Time.deltaTime);
}
private IEnumerator LifetimeRoutine()
{
yield return new WaitForSeconds(lifetime);
ObjectPool.Instance.ReturnToPool("Bullet", gameObject);
}
private void OnTriggerEnter(Collider other)
{
// 충돌 처리 후 풀로 반환
ObjectPool.Instance.ReturnToPool("Bullet", gameObject);
}
}
// 사용 예시 (총 발사)
public class Gun : MonoBehaviour
{
[SerializeField] private Transform firePoint;
[SerializeField] private float fireRate = 0.1f;
private float nextFireTime;
private void Update()
{
if (Input.GetButton("Fire1") && Time.time >= nextFireTime)
{
Fire();
nextFireTime = Time.time + fireRate;
}
}
private void Fire()
{
ObjectPool.Instance.SpawnFromPool("Bullet", firePoint.position, firePoint.rotation);
}
}
Builder Pattern
복잡한 객체를 단계적으로 생성할 수 있게 설계된 패턴이다.
객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차로 다양한 객체를 생성할 수 있다.
public class Character
{
public string Name { get; set; }
public string Class { get; set; }
public int Health { get; set; }
public int Mana { get; set; }
public List<string> Skills { get; set; }
public void DisplayInfo()
{
Debug.Log($"Name: {Name}, Class: {Class}, Health: {Health}, Mana: {Mana}");
Debug.Log("Skills: " + string.Join(", ", Skills));
}
}
public interface ICharacterBuilder
{
void SetName(string name);
void SetClass(string className);
void SetStats(int health, int mana);
void AddSkill(string skill);
Character GetCharacter();
}
public class WarriorBuilder : ICharacterBuilder
{
private Character character;
public WarriorBuilder()
{
character = new Character { Skills = new List<string>() };
}
public void SetName(string name) => character.Name = name;
public void SetClass(string className) => character.Class = className;
public void SetStats(int health, int mana)
{
character.Health = health;
character.Mana = mana;
}
public void AddSkill(string skill) => character.Skills.Add(skill);
public Character GetCharacter() => character;
}
public class MageBuilder : ICharacterBuilder
{
private Character character;
public MageBuilder()
{
character = new Character { Skills = new List<string>() };
}
public void SetName(string name) => character.Name = name;
public void SetClass(string className) => character.Class = className;
public void SetStats(int health, int mana)
{
character.Health = health;
character.Mana = mana;
}
public void AddSkill(string skill) => character.Skills.Add(skill);
public Character GetCharacter() => character;
}
public class CharacterDirector
{
private ICharacterBuilder builder;
public CharacterDirector(ICharacterBuilder builder)
{
this.builder = builder;
}
public void ConstructWarrior(string name)
{
builder.SetName(name);
builder.SetClass("Warrior");
builder.SetStats(200, 50);
builder.AddSkill("Slash");
builder.AddSkill("Shield Block");
}
public void ConstructMage(string name)
{
builder.SetName(name);
builder.SetClass("Mage");
builder.SetStats(100, 200);
builder.AddSkill("Fireball");
builder.AddSkill("Ice Shield");
}
}
using UnityEngine;
public class GameManager : MonoBehaviour
{
void Start()
{
// Create a Warrior
ICharacterBuilder warriorBuilder = new WarriorBuilder();
CharacterDirector director = new CharacterDirector(warriorBuilder);
director.ConstructWarrior("Aragorn");
Character warrior = warriorBuilder.GetCharacter();
warrior.DisplayInfo();
// Create a Mage
ICharacterBuilder mageBuilder = new MageBuilder();
director = new CharacterDirector(mageBuilder);
director.ConstructMage("Gandalf");
Character mage = mageBuilder.GetCharacter();
mage.DisplayInfo();
}
}
빌더 패턴의 장점
- 재사용성: 동일한 빌더를 사용해 다양한 캐릭터를 생성할 수 있습니다.
- 유지보수 용이성: 새로운 캐릭터 클래스를 추가할 때 기존 빌더나 디렉터를 재사용할 수 있습니다.
- 코드 가독성: 객체 생성 로직이 명확하게 분리되어 코드가 읽기 쉽습니다.
Mediator Pattern
객체 간의 결합을 줄여주는 중재자를 두는 방법이다.
객체들이 서로 직접 통신하지 않고 중재자를 통해 간접적으로 통신하기 때문에, 시스템의 복잡성을 줄이고 객체 간의 결합도를 낮출 수 있다.
시나리오:
RPG 게임에서 인벤토리 시스템의 UI가 있다고 가정
- 버튼 A를 클릭하면 선택된 아이템을 사용한다.
- 버튼 B를 클릭하면 선택된 아이템을 버린다.
- 버튼 C를 클릭하면 아이템의 정보를 보여준다.
버튼들은 서로 독립적이지만, 동일한 선택된 아이템 데이터를 공유한다.
Mediator 패턴을 활용하여 이 상호작용을 관리할 수 있다.
public interface IInventoryMediator
{
void Notify(object sender, string eventCode);
}
public class InventoryMediator : IInventoryMediator
{
private UseButton useButton;
private DiscardButton discardButton;
private InfoButton infoButton;
private string selectedItem;
public void RegisterUseButton(UseButton button) => useButton = button;
public void RegisterDiscardButton(DiscardButton button) => discardButton = button;
public void RegisterInfoButton(InfoButton button) => infoButton = button;
public void SetSelectedItem(string item)
{
selectedItem = item;
Debug.Log($"Selected item: {item}");
}
public void Notify(object sender, string eventCode)
{
switch (eventCode)
{
case "Use":
if (!string.IsNullOrEmpty(selectedItem))
{
Debug.Log($"Using {selectedItem}");
SetSelectedItem(null); // Clear selection after use
}
break;
case "Discard":
if (!string.IsNullOrEmpty(selectedItem))
{
Debug.Log($"Discarding {selectedItem}");
SetSelectedItem(null); // Clear selection after discard
}
break;
case "Info":
if (!string.IsNullOrEmpty(selectedItem))
{
Debug.Log($"Showing info for {selectedItem}");
}
else
{
Debug.Log("No item selected.");
}
break;
}
}
}
public abstract class InventoryButton
{
protected IInventoryMediator mediator;
public InventoryButton(IInventoryMediator mediator)
{
this.mediator = mediator;
}
public abstract void Click();
}
public class UseButton : InventoryButton
{
public UseButton(IInventoryMediator mediator) : base(mediator) { }
public override void Click()
{
Debug.Log("Use button clicked.");
mediator.Notify(this, "Use");
}
}
public class DiscardButton : InventoryButton
{
public DiscardButton(IInventoryMediator mediator) : base(mediator) { }
public override void Click()
{
Debug.Log("Discard button clicked.");
mediator.Notify(this, "Discard");
}
}
public class InfoButton : InventoryButton
{
public InfoButton(IInventoryMediator mediator) : base(mediator) { }
public override void Click()
{
Debug.Log("Info button clicked.");
mediator.Notify(this, "Info");
}
}
using UnityEngine;
public class GameManager : MonoBehaviour
{
void Start()
{
// Create Mediator
InventoryMediator mediator = new InventoryMediator();
// Create Buttons and Register them with the Mediator
UseButton useButton = new UseButton(mediator);
DiscardButton discardButton = new DiscardButton(mediator);
InfoButton infoButton = new InfoButton(mediator);
mediator.RegisterUseButton(useButton);
mediator.RegisterDiscardButton(discardButton);
mediator.RegisterInfoButton(infoButton);
// Simulate interactions
mediator.SetSelectedItem("Health Potion");
useButton.Click(); // Output: Using Health Potion
discardButton.Click(); // Output: No item selected.
mediator.SetSelectedItem("Mana Potion");
infoButton.Click(); // Output: Showing info for Mana Potion
}
}
Mediator 패턴의 장점
- 객체 간 의존성 감소: 버튼들이 서로를 직접 호출하지 않고, 중재자를 통해 통신하기 때문에 결합도가 낮아진다.
- 중앙 집중화: 모든 통신 로직이 중재자 클래스에 집중되어 있어, 유지보수가 용이하다.
- 확장성: 새로운 버튼을 추가하거나 기존 버튼의 기능을 변경해도 다른 버튼에 영향을 미치지 않는다.
Mediator 패턴의 단점
- 중재자 복잡성 증가: 시스템이 커질수록 중재자의 로직이 복잡해질 수 있다.
- 추가 추상화 계층: 객체 간 직접 통신보다 약간의 오버헤드가 생길 수 있다.
책임 연쇄 Pattern
요청을 처리하는 객체들의 체인을 구성하여, 요청이 처리될 때까지 각 객체가 순차적으로 요청을 전달하며 처리할 기회를 갖도록 하는 행동 디자인 패턴이다.
책임 연쇄 패턴의 주요 특징
- 동적 처리 결정: 요청이 처리될 객체를 런타임에 결정할 수 있다.
- 결합도 감소: 요청을 처리하는 객체를 명시적으로 지정하지 않아도 된다.
- 유연성: 새로운 처리자를 체인에 쉽게 추가하거나 제거할 수 있다.
시나리오:
RPG 게임에서 캐릭터에게 버프를 적용할 때, 여러 버프가 우선순위에 따라 순차적으로 처리된다.
- 체력 증가 버프
- 공격력 증가 버프
- 속도 증가 버프
public abstract class BuffHandler
{
protected BuffHandler nextHandler;
public void SetNext(BuffHandler handler)
{
nextHandler = handler;
}
public void Handle(Character character)
{
if (CanHandle(character))
{
ApplyBuff(character);
}
nextHandler?.Handle(character); // 다음 처리자로 요청 전달
}
protected abstract bool CanHandle(Character character);
protected abstract void ApplyBuff(Character character);
}
public class HealthBuffHandler : BuffHandler
{
protected override bool CanHandle(Character character)
{
return !character.HasBuff("Health"); // 이미 버프가 적용되었는지 확인
}
protected override void ApplyBuff(Character character)
{
character.Health += 50;
character.AddBuff("Health");
Debug.Log("Health Buff applied. Current Health: " + character.Health);
}
}
public class AttackBuffHandler : BuffHandler
{
protected override bool CanHandle(Character character)
{
return !character.HasBuff("Attack");
}
protected override void ApplyBuff(Character character)
{
character.Attack += 10;
character.AddBuff("Attack");
Debug.Log("Attack Buff applied. Current Attack: " + character.Attack);
}
}
public class SpeedBuffHandler : BuffHandler
{
protected override bool CanHandle(Character character)
{
return !character.HasBuff("Speed");
}
protected override void ApplyBuff(Character character)
{
character.Speed += 5;
character.AddBuff("Speed");
Debug.Log("Speed Buff applied. Current Speed: " + character.Speed);
}
}
using System.Collections.Generic;
public class Character
{
public int Health { get; set; }
public int Attack { get; set; }
public int Speed { get; set; }
private HashSet<string> activeBuffs = new HashSet<string>();
public bool HasBuff(string buffName)
{
return activeBuffs.Contains(buffName);
}
public void AddBuff(string buffName)
{
activeBuffs.Add(buffName);
}
}
using UnityEngine;
public class GameManager : MonoBehaviour
{
void Start()
{
// 캐릭터 생성
Character character = new Character
{
Health = 100,
Attack = 20,
Speed = 10
};
// 버프 체인 생성
BuffHandler healthBuff = new HealthBuffHandler();
BuffHandler attackBuff = new AttackBuffHandler();
BuffHandler speedBuff = new SpeedBuffHandler();
// 체인 연결
healthBuff.SetNext(attackBuff);
attackBuff.SetNext(speedBuff);
// 버프 처리 시작
healthBuff.Handle(character);
// 출력 확인
Debug.Log($"Final Stats - Health: {character.Health}, Attack: {character.Attack}, Speed: {character.Speed}");
}
}
책임 연쇄 패턴의 장점
- 유연성: 객체 간의 직접적인 의존성을 줄이고, 처리 로직을 변경하거나 확장하기 쉽게 만든다.
- 새로운 처리자 추가: 기존 체인을 수정하지 않고 새로운 처리자를 추가할 수 있다.
- 가독성 향상: 각 처리 로직이 개별 클래스에 캡슐화되어 코드가 명확해진다.
책임 연쇄 패턴의 단점
- 디버깅 어려움: 요청이 처리되지 않을 경우, 체인 내에서 어떤 처리자가 요청을 무시했는지 추적하기 어렵다.
- 체인 순서에 의존: 체인의 처리 순서가 잘못되면 예상치 못한 결과가 발생할 수 있다.