싱글톤 패턴 (Singleton Pattern)
프로그램 전체에서 단 하나의 인스턴스만 존재하도록 보장하고, 어디에서든 그 인스턴스에 접근할 수 있게 하는 디자인 패턴이다.
프로젝트 내의 전역 관리자 같은 역할을 하는 구조다.
- 인스턴스가 하나만 생성됨 (유일성 보장)
- static 변수를 통해 어디서든 접근 가능 (전역 접근성)
- 필요할 때 자동으로 생성되며, 이미 있으면 새로 만들지 않음
- 주로 게임에서 상태 관리나 시스템 매니저 역할에 사용
Unity에서는 씬이 분리되어 있고 여러 스크립트가 서로 다른 오브젝트 위에서 동작한다.
이때 "게임 전체 상태"를 한 곳에서 관리해야하는 경우가 있다.
- 씬이 바뀌어도 유지돼야 하는 정보 (플레이어 점수, 설정값, 능력치 등)
- 전역적으로 접근해야 하는 시스템 (사운드, 데이터, UI 등)
- 게임 전체를 제어하는 중앙 관리자 (GameManager. SceneManager 등)
이런 객체를 매번 new로 만들면 매번 새로운 인스턴스가 생기고 상태가 초기화 된다.
하지만 싱글톤은 프로그램 전체에서 단 하나만 유지하기 때문에 중복 생성이나 데이터 손실을 방지할 수 있다.
장점
| 전역 접근성 | 어디서든 쉽게 접근 가능 (GameManager.Instance) |
| 유일성 보장 | 단 하나의 인스턴스만 유지 |
| 데이터 공유 용이 | 여러 씬 간 정보 유지에 유용 (DontDestroyOnLoad) |
| 결합도 감소 | 단일 인스턴스의 전역적 접근을 보장하기 때문에 오브젝트들이 서로 참조하고 있는 결합도를 낮출 수 있다 |
단점
| 결합도 증가 | 여러 클래스가 싱글톤에 직접 의존하면 구조가 얽힘 |
| 테스트 어려움 | 인스턴스가 고정되어 단위 테스트 불리 |
| 메모리 관리 이슈 | static을 사용해 정적 메모리 할당되므로, 싱글톤 객체가 많을수록 가용 메모리가 적어짐 또한 Destroy를 잊으면 씬 전환 후에도 계속 남음 |
즉, 편리하지만 남용하면 오히려 문제를 유발할 수 있으니 주의해야한다.
구현 예시
using UnityEngine;
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); // 씬 전환 시에도 유지
}
public int score = 0;
public void AddScore(int amount)
{
score += amount;
Debug.Log("현재 점수: " + score);
}
}
이렇게 싱글톤으로 score를 관리하는 게임매니져를 구현한 상황을 가정해보자.
public class TestScript : MonoBehaviour
{
void Start()
{
Debug.Log("TestScript AddScore");
GameManager.Instance.AddScore(10);
}
}
public class ExampleScript : MonoBehaviour
{
void Start()
{
Debug.Log("ExampleScript AddScore");
GameManager.Instance.AddScore(10);
}
}
그리고 각각 다른 곳에서 AddScore 메서드를 통해 점수를 추가하는 상황일 때,
모두 같은 GameManager.Instance를 참조하기 때문에 점수가 전역적으로 공유되어 같은 score를 가리키게 되어 증가시킨다.
제네릭 (Generic)
C#을 학습하면서 제네릭에 대한 것을 익혔었다.
제네릭이란 자료형을 일반화하여 재사용성과 안정성을 높이는 문법이다.
자료형을 나중에 지정하는 클래스 또는 메서드라고 볼 수 있다.
프로젝트가 커지면 GameManager, SoundManager, UIManager 등 유사한 구조의 싱글톤이 여러개 생길 수 있다.
모두 Awake() 에서 중복 제거하고, DontDestroyOnLoad()를 호출하는 코드를 반복하게 될 것이다.
제네릭을 이용하면 이러한 반복되는 패턴을 제거하고 통일할 수 있다.
public class Box<T>
{
public T Value;
}
Box<int> intBox = new Box<int>();
Box<string> strBox = new Box<string>();
이렇게 작성하면 T 자리에 타입만 바꿔 넣어주면 하나의 코드로 여러 타입을 처리할 수 있게 된다.
장점
- 형 안정성 : 잘못된 자료형 대입을 컴파일 단계에서 차ㅏ단
- 성능 향상 : object 캐스팅 오버헤드 없음
- 재사용성 : 자료형만 바꾸면 여러 타입에서 사용 가능
- 유지보수성 : 코드 중복 없이 동일 로직 적용 가능
구현 예시
using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = FindAnyObjectByType<T>();
// 그래도 없으면 새로 생성
if (instance == null)
{
GameObject obj = new GameObject(typeof(T).Name);
instance = obj.AddComponent<T>();
}
}
return instance;
}
}
protected virtual void Awake()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (instance != this)
{
Destroy(gameObject);
}
}
}
public class GameManager : Singleton<GameManager>
{
public int score;
public void AddScore(int value)
{
score += value;
Debug.Log($"현재 점수: {score}");
}
protected override void Awake()
{
base.Awake();
Debug.Log("GameManager 초기화 완료");
}
}
이렇게 다른 매니저도 상속만 하면 동일한 싱글톤 구조를 그대로 활용할 수 있다.
제네릭 싱글톤의 한계와 주의점
| 유일성 불변성 약화 | T 타입마다 인스턴스가 별개로 존재하므로 프로그램 전체에서 유일성이 보장되지 않는다. 유일성을 추구하는 싱글톤과 확장성을 추구하는 제네릭의 개념이 서로 상충하게 되어 주의해야한다. |
| 남용 시 결합도 상승 | 여러 클래스가 전역 싱글톤에 의존하면 구조가 엉킬 수 있다. |
| 테스트 어려움 | 전역 인스턴스 단위 테스트시 독립성이 저하되고 특정 상황을 재현하는 것에 어려움이 생길 수 있다. |
| Destroy 누락 시 메모리 잔류 | DontDestroyOnLoad로 인해 씬 교체 후에도 남아있을 수 있으니 파괴 타이밍에 주의해야한다. |
활용 예시 - SceneManager
씬 전환이나 로딩 로직, 게임매니저 관리가 싱글톤의 대표적인 예다.
씬 관리를 하는 씬매니져를 예시 코드로 살펴보자
using UnityEngine;
using UnityEngine.SceneManagement;
public class SceneController : Singleton<SceneController>
{
public void LoadScene(string sceneName)
{
SceneManager.LoadScene(sceneName);
}
public void ReloadScene()
{
Scene current = SceneManager.GetActiveScene();
SceneManager.LoadScene(current.name);
}
}
SceneController.Instance.LoadScene("Stage2");
SceneController.Instance.LoadScene(1);
string 으로 직접 씬 이름을 작성해서 호출할 수도, int 값을 통해 인덱스를 호출할 수도 있다.
씬 매니저가 전역적으로 존재하기 때문에 DontDestroyOnLoad를 통해 씬이 바뀌어도 씬매니져의 기능은 사라지지않고 남으며 원하는 곳에서 사용할 수 있다.
'C#' 카테고리의 다른 글
| C# - 델리게이트(Delegae). 람다식과 캡쳐, 클로져 문제 (feat. useState) (0) | 2025.11.04 |
|---|---|
| VisualStudioCode(VSC) 에서 한글 깨짐 이슈 (0) | 2025.10.28 |
| Design Pattern - Adapter Pattern (어댑터 패턴) (0) | 2025.10.17 |
| C# - 콘솔로 "2중 버퍼링 (Double Buffering)" 구현하기 (0) | 2025.10.15 |
| C# - GetAsnycKeyState() 기반 실시간 키 입력 처리 (0) | 2025.10.14 |