개요

  • Unity에서 Singleton 사용법을 알아보자
  • 시간 없으면, 제네릭 방법만 참고

Singleton Pattern은?

  • 어떤 클래스의 객체가 최대 n개만 생성되어야 할때
  • 전역에서 접근 가능한 객체를 만들려고 할때 사용되는 디자인 패턴이다.

Monobehaviour를 사용한 싱글톤

  • Awake가 호출되면서 객체가 초기화 된다.
  • Awake의 호출이 필요하기 때문에, Monobehaviour를 상속해야 한다.
  • 당연하지만, 이 방식을 사용하면 경우에 따라 NullPointException이 발생한다.
public class DataManager : Monobehaviour
{
    public static DataManager Instance;

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }

        DontDestroyOnLoad(this.gameObject); 
    }
}

Thread-Safe 싱글톤

  • Thread만 Safe하고, 실제로는 별로 안전하지 않은 싱글톤
  • 이렇게 구현하면 당연하게도 "some objects were not cleaned up when closing the scene"가 발생한다.
    • 씬이 close되는데, 정리되지 않은게 있어서 leak이 발생하는 경우다.
    • 친구들이 가끔 저렇게 짜오면, "콘솔 안보이냐 병신아?" 라고 하는데..
  • 이전 직장에서는 게임사들을 위한 SDK를 개발할때, 이 방식으로 구현했다. (게임회사들 미안)
    • 10년차고 C#을 해본적이 책임급이 묻지도 따지지도 않고 회의 시간때마다 wiki 내용은 틀린 방식이라고 지랄하는데..
    • 정신병 걸릴뻔...
public class DataManager : MonoBehaviour
{
    private const string ObjectName = "DataManager";
    private static readonly object _lock = new object();

    private static DataManager _instance;
    public static DataManager Instance
    {
        get
        {
            lock (_lock)
            {
              if (_instance == null)
              {
                  // 컴포넌트 타입으로 검색하여, 처음 발견한 오브젝트를 반환한다.
                  _instance = FindObjectOfType(typeof(DataManager)) as DataManager;

                  // 컴포넌트를 못찾았으면
                  if (!_instance)
                  {
                      // 약속된 이름의 GameObject를 찾는다.
                      var container = GameObject.Find(ObjectName);

                      // 약속된 이름의 GameObject가 없으면, 생성한다.
                      if (container == null)
                      {
                          container = new GameObject(ObjectName);
                      }

                      // 약속된 이름의 GameObject에 컴포넌트를 추가하고, 
                      // 씬이 변경되어도 사라지지 않도록 DontDestroyOnLoad로 지정한다.
                      _instance = container.AddComponent<DataManager>();
                      DontDestroyOnLoad(_instance);

                      // 객체가 생성될때, 최초 1회만 호출되는 초기화 함수
                      _instance.Initialize();
                  }
              }
            }

            return _instance;
        }
    }

    public void Initialize()
    {
        UnityEngine.Debug.Log("Initialize instance");
    }
}

제네릭을 이용한 싱글톤

  • 개인적으로 싱글톤을 구현할때, 이게 가장 합리적인 방법이라고 생각한다.
  • 난 대학교 강의 나가면 아직도 Unity 싱글톤은 이렇게 쓰는거라고 추천한다.
using UnityEngine;
using System.Collections;

// abstract 추상 클래스고
// MonoBehaviour를 상속 받았고
// T는 MonoBehaviour를 상속받은 클래스여야 한다.
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance = null;
    private static object _lock = new object();
    private static bool _applicationQuit = false;

    public static T Instance
    {
        get
        {
            // 고스트 객체 생성 방지용
            // 위에서 설명한 leak을 방지한다.
            if (_applicationQuit)
            {
                // null 리턴
                return null;
            }

            // thread-safe
            lock (_lock)
            {  
                if (_instance == null)
                {
                    // 현재 씬에 싱글톤이 있나 찾아본다.
                    _instance = FindObjectOfType<T>();

                    // 없으면
                    if (_instance == null)
                    {
                        // 해당 컴포넌트 이름을 가져온다.
                        string componentName = typeof(T).ToString();

                        // 해당 컴포넌트 이름으로 게임 오브젝트 찾기
                        GameObject findObject = GameObject.Find(componentName);

                        // 없으면
                        if (findObject == null)
                        {
                            // 생성
                            findObject = new GameObject(componentName);
                        }

                        // 생성된 오브젝트에, 컴포넌트 추가
                        _instance = findObject.AddComponent<T>();

                        // 씬이 변경되어도 객체가 유지되도록 설정
                        DontDestroyOnLoad(_instance);
                    }
                }

                // 객체 리턴
                return _instance;
            }
        }
    }

    // 원칙적으로 싱글톤은 응용 프로그램이 종료될때, 소멸되어야 한다.
    // 유니티에서 응용 프로그램이 종료되면 임의 순서대로 오브젝트가 파괴된다.
    // 만약 싱글톤 오브젝트가 파괴된 이후, 싱글톤 오브젝트가 호출된다면
    // 앱의 재생이 정지된 이후에도, 에디터 씬에서 고스트 객체가 생성된다.
    // 고스트 객체의 생성을 방지하기 위해서 상태를 관리한다.

    // 앱이 종료될때 호출
    protected virtual void OnApplicationQuit()
    {
        _applicationQuit = true;
    }

    // 객체가 파괴될때 호출
    public virtual void OnDestroy()
    {
        _applicationQuit = true;
    }
}
public class Manager : Singleton<Manager> 
{
    // 이 생성자는 사용하려고 선언한건 아니다.
    // 비록 Singleton으로 열심히 정의해 놨지만
    // Manager manager = new Manager() 이렇게 생성하는 것을 방지 할 수 없다.
    // 저러면 Manager는 항상 Singleton 클래스라고 볼 수 없는데, Manager의 생성자를 protected로 해줌으로써
    // Manager가 항상 Singleton 임을 보장한다.
    protected Manager() {}
}

참고

+ Recent posts