참고용 예제 저장소: Unity Design Patterns (Sungkuk Park)
최종 업데이트: 2019년 1월 2일

싱글턴 (Singleton) 패턴

의도 (Intention)

클래스에 대해 단 하나의 인스턴스(instance)만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공한다.

동기 (Motivation)

어떤 클래스의 경우는 단 하나의 인스턴스만을 가져야 한다. 프린터가 여러 개 있다고 하더라도 프린터 관리 프로그램(printer spooler)은 단 한 개여야 하듯이 말이다. 파일 시스템도 하나여야 하고, 윈도우 매니저도 하나여야만 한다. 디지털 필터도 단 하나의 A/D 변환기를 가질 것이고, 각 회사는 단 하나의 통합된 회계 시스템을 가질 것이다.

그럼 어떻게 클래스가 단 하나의 인스턴스를 갖고 이에 대한 전역적인 접근점을 쉽게 제공할 수 있을까? 전역 변수를 사용하는 방법에는 문제가 있다. 전역 변수는 쉽게 접근이 가능하지만, 여러 개의 객체들을 생성하는 것은 막을 수 없기 때문이다.

더 좋은 방법은, 클래스로 하여금 자신의 단일한 인스턴스를 관리하는 책임을 지게 하는 것이다. 해당 클래스는 새로운 객체를 생성해달라는 요청을 가로챔으로써 자신에 대한 다른 인스턴스가 생기지 않도록 한다. 그리고 해당 클래스는 단일한 인스턴스에 접근하는 전역적인 접근점을 제공한다. 이것이 싱글턴 패턴이다.

분석 (Analysis)

싱글턴 패턴의 핵심은 다음 세 가지이다.

  1. 클래스는 단일한 인스턴스를 갖는다.
  2. 이는 클래스가 객체 생성에 대한 요청을 가로챔(intercept)으로써 이루어진다.
  3. 클래스는 단일한 인스턴스에 대한 전역적인 접근점(global access point)을 제공한다.

구조 (Structure)

Singleton

구현 (Implementation)

최소한의 기능만을 가진 싱글턴 패턴 구현

위의 핵심 사항들만을 구현하면 다음과 같이 싱글턴 패턴을 구현할 수 있다. 아래 선언된 private 변수 “_instance”가 static인 것에 주목하라. 또한, Unity는 씬(scene)을 게임 레벨(level)의 단위로서 일반 GameObject들은 씬 이동 시 자동으로 destroy되므로, DontDestroyOnLoad 호출을 통해 신규 씬을 불러와도 싱글턴 객체가 destroy되지 않도록 처리해줘야 한다는 점에 유의하라.

using UnityEngine;

public class SingletonClass : MonoBehaviour
{
    // 3. 클래스는 단일한 인스턴스에 대한 전역적인 접근점(global access point)을 제공한다.
    public static SingletonClass Instance
    {
        get
        {
            return _instance;
        }
    }
    // 1. 클래스는 단일한 인스턴스를 갖는다.
    private static SingletonClass _instance;

    private void Awake()
    {
        // 2. 이는 클래스가 객체 생성에 대한 요청을 가로챔(intercept)으로써 이루어진다.
        if (_instance != null)
        {
            Destroy(gameObject);
            return;
        }
        
        _instance = this;
        
        // 신규 씬(scene)을 불러와도 싱글턴 객체가 destroy되지 않도록 처리한다.
        DontDestroyOnLoad(gameObject);
    }
}

하지만, 위의 구현은 문제를 안고 있다. 해당 SingletonClass를 컴포넌트로 가지는 GameObject가 Hierarchy 상에 단 하나라도 미리 존재하지 않는다면 SingletonClass.Instance는 null 값을 참조하게 될 위험성을 지니는 것이다. 게다가, 만약 하나 이상의 해당 GameObject가 존재하더라도 비활성화 상태이면 역시 SingletonClass.Instance는 null 값을 참조하게 된다. 왜냐하면 Unity의 이벤트 함수 Awake는 비활성화 상태인 오브젝트에서 호출되지 않기 때문이다.

이런 식이라면 SingletonClass는 싱글턴 패턴으로 볼 수 없다. SingletonClass를 컴포넌트로 가지는 활성화된 GameObject가 하나도 존재하지 않더라도, 싱글턴 패턴은 0개가 아니라 1개의 인스턴스를 제공할 책임(responsibility)을 지니까 말이다.

게으른 초기화를 적용한 싱글턴 패턴 구현

이때 필요한 것이 게으른 초기화(Lazy initialization) 기법이다. SingletonClass를 가진 GameObject가 하나도 존재하지 않더라도, SingletonClass.Instance가 호출될 때(싱글턴 인스턴스가 필요해질 때) SingletonClass 내부에서 단일한 인스턴스를 직접 생성해주면 되는 것이다. 아래의 게으른 초기화를 적용한 싱글턴 패턴 구현을 참고하라.

using UnityEngine;

public class LazySingletonClass : MonoBehaviour
{
    // 3. 클래스는 단일한 인스턴스에 대한 전역적인 접근점(global access point)을 제공한다.
    public static LazySingletonClass Instance
    {
        get
        {
            if (_instance == null)
            {
                // 2. 이는 클래스가 객체 생성에 대한 요청을 가로챔(intercept)으로써 이루어진다.
                _instance = GameObject.FindObjectOfType<LazySingletonClass>();
                if (_instance == null)
                {
                    // 게으른 초기화 (Lazy initialization) 구간
                    var go = new GameObject();
                    _instance = go.AddComponent<LazySingletonClass>();
                    _instance.name = typeof(LazySingletonClass).ToString() + " (Singleton)";
                }

                // 신규 씬(scene)을 불러와도 싱글턴 객체가 destroy되지 않도록 처리한다.
                DontDestroyOnLoad(_instance.gameObject);
                
                return _instance;
            }
            
            return _instance;
        }
    }
    // 1. 클래스는 단일한 인스턴스를 갖는다.
    private static LazySingletonClass _instance;

    private void Awake()
    {
        // 2. 이는 클래스가 객체 생성에 대한 요청을 가로챔(intercept)으로써 이루어진다.
        if (Instance != null && Instance.gameObject != gameObject)
        {
            Destroy(gameObject);
        }
    }
}

일반화된 싱글턴 패턴 구현

그러나, 싱글턴 클래스 타입 별로 위와 같은 싱글턴 패턴을 구현하는 것은 번거로운 일이다. 이 때문에 타입과 무관하게 일반화된 싱글턴 패턴을 구현하기 위해서는 해당 싱글턴 클래스를 타입 인자(type parameter)를 활용해 일반화 클래스로 바꾸면 된다. 아래의 일반화된 싱글턴 패턴의 구현을 참고하라.

using UnityEngine;

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    // 3. 클래스는 단일한 인스턴스에 대한 전역적인 접근점(global access point)을 제공한다.
    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                // 2. 이는 클래스가 객체 생성에 대한 요청을 가로챔(intercept)으로써 이루어진다.
                _instance = GameObject.FindObjectOfType(typeof(T)) as T;
                if (_instance == null)
                {
                    // 게으른 초기화 (Lazy initialization) 구간
                    var go = new GameObject();
                    _instance = go.AddComponent<T>();
                    _instance.name = typeof(T).ToString() + " (Singleton)";
                }

                // 신규 씬(scene)을 불러와도 싱글턴 객체가 destroy되지 않도록 처리한다.
                DontDestroyOnLoad(_instance.gameObject);
                
                return _instance;
            }
            
            return _instance;
        }
    }
    // 1. 클래스는 단일한 인스턴스를 갖는다.
    private static T _instance;

    private void Awake()
    {
        // 2. 이는 클래스가 객체 생성에 대한 요청을 가로챔(intercept)으로써 이루어진다.
        if (Instance != null && Instance.gameObject != gameObject)
        {
            Destroy(gameObject);
        }
    }
}

일반화된 싱글턴 패턴을 적용해 실제 싱글턴 클래스를 생성할 때는 다음과 같이 사용하면 된다.

 public class GenericSingleton : Singleton<GenericSingleton>
 {
 }
<끝>