TOOLS GAMES DEVLOG DOCS ABOUT CONTACT
HOME > DOCS > Unity / C# > 씬 전환에도 파괴되지 않는 제네릭 영속 싱글톤(Persistent Singleton) 패턴
Unity / C# Beginner Unity 2021.3+ / Unity 6

씬 전환에도 파괴되지 않는 제네릭 영속 싱글톤(Persistent Singleton) 패턴

DontDestroyOnLoad와 지연 초기화, 씬 종료 시 고스트 오브젝트 생성을 원천 차단하는 스레드 세이프 유니티 C# 제네릭 싱글톤 템플릿.

#Unity #C# #Design Pattern #Architecture #Optimization
핵심 개념유니티에서 싱글톤을 순진하게 구현하면 게임 종료 시 파괴 순서 문제로 인해 씬에 유령(Ghost) 인스턴스가 생성되는 문제와 씬 전환 시 중복 인스턴스가 살아남아 데이터가 꼬이는 문제가 발생합니다. 종료 감지 플래그와 DontDestroyOnLoad를 상속 단계에서 캡슐화한 제네릭 기반 싱글톤으로 이를 해결합니다.

1. 문제 정의: 일반적인 구현의 치명적 결함

  • 고스트 인스턴스 버그: 에디터 플레이를 중단하거나 씬을 나갈 때, 다른 객체의 OnDestroy()에서 싱글톤을 참조하면 이미 파괴된 싱글톤이 재호출되면서 new GameObject가 씬에 영구 잔류합니다.
  • 중복 오브젝트 누적: 씬 A에서 DontDestroyOnLoad 된 매니저가 존재하는 상태에서 씬 A로 다시 로드될 때 새로 스폰된 매니저가 파괴되지 않고 중복 공존하여 이벤트 중복 호출이 발생합니다.

2. 검증된 제네릭 싱글톤 구현

PersistentSingleton.cs
using UnityEngine;

/// <summary>
/// 씬이 바뀌어도 유지되며, 종료 시 고스트 오브젝트를 생성하지 않는 범용 싱글톤 베이스 클래스입니다.
/// </summary>
public abstract class PersistentSingleton<T> : MonoBehaviour where T : Component
{
    private static T _instance;
    private static bool _isApplicationQuitting = false;
    private static readonly object _lock = new object();

    public static T Instance
    {
        get
        {
            if (_isApplicationQuitting)
            {
                Debug.LogWarning($"[PersistentSingleton] 애플리케이션 종료 중 '{typeof(T).Name}'에 접근하여 null을 반환합니다.");
                return null;
            }

            lock (_lock)
            {
                if (_instance == null)
                {
                    _instance = FindAnyObjectByType<T>();
                    if (_instance == null)
                    {
                        var go = new GameObject($"[{typeof(T).Name}]");
                        _instance = go.AddComponent<T>();
                        DontDestroyOnLoad(go);
                    }
                }
                return _instance;
            }
        }
    }

    protected virtual void Awake()
    {
        if (_instance == null)
        {
            _instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else if (_instance != this)
        {
            Debug.LogWarning($"[PersistentSingleton] 중복 생성된 '{typeof(T).Name}' 인스턴스를 파괴합니다.");
            Destroy(gameObject);
        }
    }

    protected virtual void OnApplicationQuit()
    {
        _isApplicationQuitting = true;
    }

    protected virtual void OnDestroy()
    {
        if (_instance == this)
        {
            _instance = null;
        }
    }
}

3. 실무 사용 예제

SoundManager.cs
using UnityEngine;

public class SoundManager : PersistentSingleton<SoundManager>
{
    [SerializeField] private AudioSource bgmSource;

    public void PlayBGM(AudioClip clip)
    {
        if (bgmSource == null) return;
        bgmSource.clip = clip;
        bgmSource.Play();
    }
}

// 호출처:
// SoundManager.Instance.PlayBGM(introMusic);

4. 실무 주의점 및 가이드

  • DontDestroyOnLoad 루트 제약: DontDestroyOnLoad는 반드시 최상위(Root) GameObject에만 적용됩니다. 부모 Transform이 있는 자식 오브젝트에 이 스크립트를 붙이면 유니티 엔진 경고가 발생하므로 transform.SetParent(null);을 사전에 호출해야 합니다.
  • 도메인 리로드 비활성화 대응: Unity 에디터의 'Enter Play Mode Settings'에서 Domain Reload를 껐다면 static _instance = null;, _isApplicationQuitting = false;를 [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]에서 리셋해주어야 합니다.