[Sprint 3] Design Pattern - SWPP-2025SPRING/team-project-for-2025-spring-swpp-team-10 GitHub Wiki
디자인 패턴 적용 보고서
1. 개요
이 보고서에서는 Sprint 3에서 Mission Hampossible 프로젝트에 적용된 디자인 패턴을 분석하고, 해당 패턴이 각 기능에 어떻게 활용되었는지를 설명한다. SOLID 원칙 및 GoF 디자인 패턴을 중심으로 분석하며, 코드 구조의 유지보수성, 확장성, 테스트 용이성 측면에서의 효과도 포함한다.
2. Singleton Pattern
Singleton 패턴은 특정 클래스의 인스턴스가 단 하나만 존재하도록 보장하고, 전역적으로 접근할 수 있도록 하는 디자인 패턴이다. 프로젝트 전반에서 일관된 상태를 유지하고, 데이터 관리의 효율성을 높이기 위해 사용된다. 공통 싱글톤 기반 클래스를 만들어 여러 매니저 클래스가 이를 상속받도록 구성했으며, 씬 전환 시 객체 유지 여부에 따라 파괴형/비파괴형 두 가지 Singleton 방식이 존재한다.
도입 목적
- 상태의 일관성 보장
- 어디서든 접근 가능한 중앙 데이터 허브 제공
- 중복 인스턴스 생성 방지 및 메모리 안정성 확보
적용 클래스
ItemManager.cs
게임 내 아이템, 코인, 인벤토리 상태를 통합 관리하는 매니저 클래스. PersistentSingleton를 상속받아 싱글톤으로 구현되어 있으며, 게임 전체에서 유일하게 존재한다.
// PersistentSingleton.cs
public class PersistentSingleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
// 현재 씬에 존재하는 T 타입의 오브젝트(컴포넌트)를 찾아 반환
_instance = FindObjectOfType<T>();
if (_instance == null)
{
var obj = new GameObject(typeof(T).Name);
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
// ItemManager.cs
public class ItemManager : PersistentSingleton<ItemManager>
LaserShootBluePool.cs
, LaserShootYellowPool.cs
개별 레이저탄 오브젝트를 오브젝트풀링으로 관리하는 스크립트이다. 각 레이저 대포에서는 본 스크립트를 호출하여 해당하는 색깔의 레이저탄을 얻고 관리할 수 있다. 여러 레이저 대포에서 쉽게 접근하도록 하기 위해 싱글톤으로 구현했다. RuntimeSingleton<>
를 상속받아 싱글톤으로 구현하였다. PersistentSingleton<>
과 RuntimeSingleton<>
은 Sprint2에서 구현한 사항이다.
GameObject obj = LaserShootBluePool.Instance.GetObject(); // 레이저탄 얻기
LaserShootBluePool.Instance.ReturnObject(gameObject); // 레이저탄 반환
2. Observer pattern
Observer 패턴은 특정 객체의 상태가 변경될 때 관련된 모든 관찰자들에게 자동으로 알림을 보내는 디자인 패턴이다. 이 패턴을 통해 객체 간의 느슨한 결합이 가능해지고, 유연한 이벤트 기반 구 조를 구현할 수 있다.
적용 클래스 및 설명
ItemManager.OnCoinCountChanged
UnityEvent를 통해 UI 등 외부 시스템에 코인 수 변화를 알린다.
private void InitializeCoins()
{
_coinWallet ??= new DefaultCoinWallet(initialCoinCount, OnCoinCountChanged);
HLogger.General.Info($"코인 초기화 완료. 시작 코인: {_coinWallet.GetBalance()}개", this);
OnCoinCountChanged.Invoke(_coinWallet.GetBalance());
}
CheckpointManager
, INextCheckpointObserver
:
checkpoint가 활성화되어 진행도가 업데이트되면 NotifyProgressUpdated()와 NotifyTargetChanged()를 호출하여 각각 INextCheckpointObserver에서 OnCheckpointProgressUpdated와 OnNextCheckpointChanged를 호출한다. 다른 스크립트에서 이 인터페이스를 사용하여 진행도 변화 시 이벤트에 대한 구체적인 내용을 구현할 수 있다.
OnTriggerEvent.cs
: 트리거 영역에 Enter/Exit할 때 호출할 유니티이벤트들을 등록할 수 있다.
void OnTriggerEnter(Collider other)
{
(생략)
enterEvent?.Invoke();
(생략)
}
SafeBoxWarningController.cs
금고를 열었을 때 호출할 UnityEvent들을 등록할 수 있다.
public void StartWarning()
{
(생략)
warningEvent?.Invoke();
(생략)
}
3. SRP 및 모듈화
SRP(단일 책임 원칙)는 하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 즉, 클래스는 변경될 이유가 하나뿐이어야 하며, 특정 기능에만 집중해야 한다. 이 원칙을 지키면 각 클래스가 독립적이고 명확한 역할을 가지므로 유지보수성과 재사용성이 높아진다.
SetTransformScale.cs
오브젝트의 스케일을 원하는 대로 조정하는 public 함수 제공
- `public void SetScaleZero()`
- `public void SetScaleZeroAndDelete()`
- `public void SetScaleFromZero(float scale)`
DestroyObject.cs
: 스크립트에 등록된 여러 오브젝트들을 Destroy하는 public 함수 제공
- `GameObject[] destroyObjs`
- `public void OnDestroy()`
SetResetPoint.cs
: 오브젝트의 위치를 ResetPoint로 조정하는 public 함수 제공
// Transform resetPoint를 인스펙터에 위치시키고 함수 호출
private void OnTriggerEnter(Collider other)
{
GoToResetPoint();
}
public void GoToResetPoint() {}
SimpleRotate.cs
: 특정 축을 기준으로 회전하는 기능을 제공
// rotationAxis, rotationSpeed 변수를 기준으로 Update함수에서 회전
void Update()
{
transform.Rotate(rotationAxis.normalized, rotationSpeed * Time.deltaTime);
}
FollowController.cs
: follow 트랜스폼을 등록하여 해당 오브젝트의 위치와 동기화하는 스크립트이다. 바로 동기화되거나, 자연스럽게 위치를 이동시키는 옵션이 있다.
void FixedUpdate()
{
if (_rigid != null)
{
if (isLerp)
_rigid.MovePosition(Vector3.Lerp(_rigid.transform.position,
follow.position + offset,
lerpSpeed * Time.deltaTime));
else
_rigid.MovePosition(follow.position + offset);
}
else
{
if (isLerp)
transform.position = Vector3.Lerp(transform.position,
follow.position + offset,
lerpSpeed * Time.deltaTime);
else
transform.position = follow.position + offset;
}
}
SetTransformScale.cs, DestroyObject.cs, SetResetPoint.cs, SimpleRotate.cs 등은 각각 명확한 하나의 책임(SRP) 을 갖는 스크립트로 분리되어 있으며, 이러한 구조는 SOLID 원칙 중 단일 책임 원칙(SRP) 을 잘 실현하고 있다.
또한, 유니티의 이벤트 기반 시스템(특히 OnTriggerEnter)과의 연계를 통해 각 기능을 재사용성 높게 활용할 수 있어, SOLID 원칙 중 개방-폐쇄 원칙(OCP) 도 자연스럽게 적용되고 있다.
4. DIP (Dependency Inversion Principle)
DIP(의존 역전 원칙)은 고수준 모듈이 저수준 모듈에 의존하지 않고, 추상화(인터페이스)에 의존해야 한다는 원칙이다. 이를 통해 구현의 변경이 상위 모듈에 영향을 미치지 않도록 하며, 시스템의 유연성과 테스트 용이성을 극대화한다. ItemManager는 내부적으로 직접 DefaultCoinWallet이나 StubInventoryStorage와 같은 구현체에 의존하지 않고, 이들을 ICoinWallet, IInventoryStorage 라는 추상화된 인터페이스를 통해 접근함으로써 DIP를 실현하고 있다.
- 고수준 모듈: ItemManager – 게임의 핵심 로직을 담는 관리 클래스
- 추상화 계층: ICoinWallet, IInventoryStorage – 기능 정의만 포함
- 저수준 모듈: DefaultCoinWallet, StubCoinWallet, StubInventoryStorage 등 – 실제 동작 구현
// 추상화에 의존하는 구조
public void SetCoinWallet(ICoinWallet wallet)
{
_coinWallet = wallet;
}
public void SetInventoryStorage(IInventoryStorage storage)
{
_inventoryStorage = storage;
}
// 테스트용 StubWallet 사용
_itemManager.SetCoinWallet(new StubCoinWallet(100));
private class StubCoinWallet : ICoinWallet
{
private int _balance;
public StubCoinWallet(int initial) => _balance = initial;
public int GetBalance() => _balance;
public void Add(int amount) => _balance += amount;
public bool Spend(int amount) => _balance >= amount ? (_balance -= amount) >= 0 : false;
}