이 글은 해당 유튜브를 보고 진행한 것에 대해 공부한 것을 정리한 글입니다.
싱글톤 패턴 (Singleton Pattern)
클래스가 자신의 인스턴스를 하나만 생성하도록 보장하고, 해당 단일 인스턴스에 대해 어디서든 전역(Global)으로 쉽게 접근할 수 있도록 보장하는 디자인 패턴입니다.

사용 용도
하나의 인스턴스만 있어야 하는 객체가 필요할 때 사용됩니다.
- 게임 매니저 (GameManager)
- 오디오 매니저 (AudioManager)
- UI 설정 관리자 (UISetting)
- 상태창
- 파일 관리자
- 등등
확장성이 필요하지 않은 소규모 게임 개발에서는 싱글톤 패턴이 유용하게 사용되지만,
수년 동안 유지 관리하려는 엔터프라이즈 수준의 게임은 싱글톤 패턴이 적절하지 않습니다.
특히 대규모 프로젝트나 장기 유지보수가 필요한 게임에서는 안티 패턴(Anti-pattern)으로 분류될 수 있습니다.
장점
- 직관적인 구조
- 코어 패턴 자체가 직관적이기에 이해하기 쉽고 금방 배울 수 있습니다.
- 전역 접근 가능
- 다른 스크립트에서 언제든 SingletonClass.Instance로 접근할 수 있어 편리합니다.
- 싱글톤 인스턴스는 필요할 때 씬의 어떤 오브젝트에서도 항상 사용할 수 있습니다.
- 성능 보장
- Find( )또는 GetComponent( ) 호출 없이 정적 참고로 빠르게 접근하여 성능을 보장할 수 있습니다.
단점
- 결합도 증가 (Coupling)
- 여러 컴포넌트가 싱글톤을 참조하면서 커플링이 발생하기 쉬운 구조입니다.
- 커플링에 의해 의존성이 숨어들고 코드가 복잡해질 수 있습니다.
- 결합도가 높으면 리팩토링하기가 어렵고, 컴포넌트 하나를 변경하면 연결된 컴포넌트에 영향을 줄 수 있습니다.
- 테스트 어려움
- 유닛 테스트에서 객체 간 상태가 공유되므로 독립적인 테스트가 어렵습니다.
- 초기화 시점 불명확
- 인스턴스가 언제 생성될지 예측하기 어려워, 디버깅에 혼란을 줄 수 있습니다.
문제점이 있는 구현 예시
public class Singleton : MonoBehaviour
{
private static Singleton Instance;
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
}
문제점
- 새로운 씬을 로드하면 게임 오브젝트가 파괴됩니다.
- 사용하기 전에 계층 구조(Hierarchy)에서 싱글톤을 설정해야 합니다.
개선 후 코드
public class Singleton : MonoBehaviour
{
private static Singleton instance;
public static Singleton Instance
{
get
{
if (instance == null)
{
SetupInstance();
}
return instance;
}
}
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
else
{
Destroy(gameObject);
}
}
private static void SetupInstance()
{
instance = FindObjectOfType<Singleton>();
if (instance == null)
{
GameObject gameObj = new GameObject();
gameObj.name = "Singleton";
instance = gameObj.AddComponent<Singleton>();
DontDestroyOnLoad(gameObj);
}
}
// ...
}
개선 사항
- DontDestroyOnLoad
- 씬 전환에도 인스턴스를 유지합니다.
- 더 이상 새로운 씬이 로드될 때, 게임 오브젝트가 파괴되지 않습니다.
- FindObjectOfType
- Hierachy에 존재하는 인스턴스를 검색합니다.
- 없을 경우 GameObject를 동적으로 생성하여 AddComponent를 적용합니다.
- 더 이상 사용하기 전에 따로 계층 구조(Hierarchy)에서 싱글톤을 설정할 필요가 없습니다.
제네릭 기반 싱글톤
여러 개의 싱글톤을 사용해야할 경우, 제네릭 버전으로 만들어야 합니다.
제네릭 버전으로 싱글톤을 만들면, 제네릭 싱글톤을 상속하기만 하면 싱글톤으로 만들 수 있습니다.
public class Singleton<T> : MonoBehaviour where T : Component
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
SetupInstance();
}
}
return instance;
}
}
public virtual void Awake()
{
RemoveDuplicates();
}
private static void SetupInstance()
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
GameObject gameObj = new GameObject();
gameObj.name = typeof(T).Name;
instance = gameObj.AddComponent<T>();
DontDestroyOnLoad(gameObj);
}
}
private void RemoveDuplicates()
{
if (instance == null)
{
instance = this as T;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
사용 시
public class GameManager: Singleton<GameManager> { }
public class AudioManager : Singleton<AudioManager> { }
Monobehaviour 없이 싱글톤 구현
public class SimpleSingleton
{
public static SimpleSingleton Instance;
public static SimpleSingleton Instance
{
get
{
if (instance == null)
{
instance = new SimpleSingleton();
}
return instance;
}
}
}
해당 코드는 멀티쓰레드에서 사용할 수 없습니다.
instance가 null인지 확인하고 new를 수행하는 과정에서 동기화(잠금)가 없기 때문입니다.
만약 여러 쓰레드가 동시에 Instance에 접근하면, 쓰레드 A가 null을 확인하고 new를 하기 전에, 쓰레드 B도 null을 확인하고 또 new를 수행할 수 있습니다.
이로 인해 두 개 이상의 인스턴스가 생성될 위험이 있습니다. 이는 싱글톤 패턴의 목적을 완전히 위배하기 때문에 멀티쓰레드 환경에서는 사용할 수 없는 것입니다.
멀티쓰레드 지원 버전 (Thread-safe Singleton)
public class SimpleSingleton
{
private static volatile SimpleSingleton Instance = null;
private static readonly object padlock = new object();
// ...
public static SimpleSingleton Instance
{
get
{
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
instance = new SimpleSingleton();
}
}
return instance;
}
}
}
lock 키워드
lock 키워드는 크리티컬 섹션(동기화 블록)을 만드는 역할로,
해당 코드에서는 lock (padlock) 블록을 통해 동시에 여러 쓰레드가 진입하지 못하도록 보호합니다.
이중 null 체크
또한 instance의 null 체크를 두 번함으로써 더욱 안정적으로 접근할 수 있게 해줍니다.
cpu 사이클 차이로 인해 쓰레드 A가 new SimpleSingleton()을 실행하는 도중, 쓰레드 B가 null인 걸 확인하고 또 다시 인스턴스를 생성할 수 있기 때문에, 이걸 방지하기 위해서 lock 내부에서도 null 체크를 한 번 더 하는 것입니다.
violatile
violatile은 변수의 값을 CPU 캐시가 아닌 메인 메모리에서 직접 읽고 씀을 보장합니다.
쓰레드 A가 instance = new SimpleSingleton()을 수행하더라도, CPU 캐시가 갱신되지 않으면 쓰레드 B가 여전히 null로 인식할 수 있는 문제가 있습니다.
violatile은 이런 가시성 문제(Memory Visibility)를 해결해줍니다. 즉, 다른 쓰레드에게도 변경 사항이 즉시 보이도록 강제하는 역할을 합니다.
완전 안전한 초기화 방식
public class SimpleSingleton
{
private static readonly SimpleSingleton instance = new SimpleSingleton();
// Private constructor ensures that an object is not instantiated from outside the class
private SimpleSingleton() { }
// Public static property to get the single instance of the class
public static SimpleSingleton Instance
{
get
{
return instance;
}
}
}
static readonly 필드는 클래스가 처음 로드될 때 한 번만 초기화됩니다.
이 초기화는 .NET 런타임에 의해 자동으로 처리되며, 내부적으로 동기화 처리가 완료된 상태에서 수행됩니다.
따라서 별도의 lock 없이도 멀티쓰레드 환경에서 완전히 안전합니다.
다만, 호출 시점과 관계없이 무조건 클래스 로드시 초기화되기 때문에, 메모리 사용 또는 초기화 비용이 큰 경우, 불필요하게 먼저 생성되어 로딩 시간 증가의 원인이 될 수 있습니다.
'Unity' 카테고리의 다른 글
| [Unity] 디자인 패턴 : Strategy 패턴 (0) | 2025.06.17 |
|---|---|
| [Unity] MVC, MVP, MVVM 패턴 (0) | 2025.06.17 |
| [Unity] 디자인패턴 : Object Pool (1) | 2025.06.16 |
| [Unity] 디자인 패턴 : Factory 패턴 (0) | 2025.06.16 |
| [Unity] 디자인 패턴 : Command 패턴 (0) | 2025.06.13 |