Skip to main content

[Design Pattern] 행동 패턴 1. 싱글톤 (Singleton)




싱글톤 패턴 #

싱글톤

  • 객체의 인스턴스를 오직 한 개만 생성하고, 그 한 개의 인스턴스에 대한 전역적인 접근점을 제공한다.
  • 장점
    • 오직 하나의 인스턴스만 생성하기 때문에 메모리 낭비를 방지할 수 있다. 심지어 전혀 사용되지 않는다면 아예 인스턴스를 생성하지 않을 수도 있다.
    • 런타임에 초기화되므로, 프로그램이 실행된 다음에야 알 수 있는 정보들을 활용할 수 있다.
  • 단점
    • 싱글톤은 전역 변수이다.
    • 전역적으로 접근하므로, 버그를 찾는다면 연관된 모든 것을 찾아봐야 할 것이다.
    • 커플링을 조장한다.
    • 멀티 스레딩 같은 동시성 프로그래밍에 알맞지 않다. 찾기 어려운 스레드 동기화 버그가 생기기 쉽다.
    • 단일 책임 원칙을 위반한다. 즉, 한 번에 두 가지의 문제를 동시에 해결한다.
    • 이런 문제들 때문에 왠만하면 싱글톤을 쓰지 않으려고 한다. (대안 참고)



동기화 문제 해결방안 #

  • 고전적인 싱글톤 구현 방법
public class Singleton
{
  static Singleton instance = null;

  Singleton() { }
  
  public static Singleton GetInstance()
  {
    if (instance == null) instance = new Singleton(); // 게으른 초기화
    return instance;
  }
}
  • 문제점
    • 멀티 스레드 환경에서 동기화 처리를 안 하면 문제가 발생할 수 있다. 두 개 이상의 스레드가 인스턴스를 획득하기 위해서 GetInstance() 메서드에 진입해서 경합을 벌이는 과정에서 서로 다른 두 개의 인스턴스가 만들어질 수 있다.

인스턴스를 처음부터 만들어버린다. #

public class SingletonEarly
{
  // 이른 초기화. 인스턴스를 처음부터 만들어 버린다.
  static SingletonEarly instance = new SingletonEarly();  

  SingletonEarly() { }

  public static SingletonEarly GetInstance()
  {
    return instance;
  }
}
  • 단점: 인스턴스를 미리 만들어버리면 그 인스턴스가 자원을 많이 차지하는 경우에는 시스템 리소스가 쓸데없이 낭비될 가능성이 있다.

DCL (Double-Checking Locking)을 사용해서 동기화 한다. #

  • Lock을 사용하는 방법.
    • 매번 동기화하지 않고, 인스턴스가 생성되지 않았을 때만 동기화하여 속도를 높인다.
    • 동기화 후, 다시 한 번 인스턴스가 생성되었는지 확인하고 null이라면 인스턴스를 생성한다.
public class SingletonDCL
{
  // volatile 키워드:
  // instance 변수에 대한 액세스는 모두 캐시되지 않으며, 메모리에 있는 값을 직접 액세스한다. 
  // 다른 스레드로 인해 값이 변경되지만 컴파일러가 이를 인식하지 못할 수 있기 때문에 volatile 키워드를 사용한다. 
  static volatile SingletonDCL instance = null;
  static readonly object _lock = new object();

  SingletonDCL() { }

  public static SingletonDCL GetInstance()
  {
    if (instance == null) // instance가 생성되지 않았을 때만 동기화한다. 
    {
      lock (_lock) // _lock이 다른 스레드에서 사용 중이면 사용이 끝날 때까지 기다린다. 
      {
        if (instance == null) instance = new SingletonDCL(); // 다시 한번 null 인지 확인한다. 
      }
    }

    return instance;
  }
}

  • Lazy를 사용하는 방법.
    • Lazy<T>는 선언할 때 인스턴스가 생성되지 않고, 접근하려고 할 때 생성한다.
    • 내부적으로 DCL 패턴을 사용 하고 있기 때문에 멀티 스레딩에 안전하다.
public class SingletonLazy
{
  // Lazy<T>는 사용하기 전까지는 인스턴스를 미리 생성하지 않는다. 멀티 스레드에서도 안전하다. 
  static readonly Lazy<SingletonLazy> instance = new Lazy<SingletonLazy>(() => new SingletonLazy());

  SingletonLazy() { }

  public static SingletonLazy GetInstance()
  {
    return instance.Value;
  }
}



상속을 이용한 싱글톤 (in Unity) #

  • 싱글톤을 제네릭 클래스로 만든 후, 상속해서 사용한다.
using UnityEngine;

public class  Singleton<T> : MonoBehaviour where T : Component 
{
  private static T _instance;

  public static T Instance
  {
    get
    {
      if (_instance == null)
      {
        _instance = FindObjectOfType<T>();

        if (_instance == null)
        {
          GameObject obj = new GameObject();
          obj.name = typeof(T).Name;
          _instance = obj.AddComponent<T>();
        }
      }

      return _instance;
    }
  }

  public virtual void Awake()
  {
    if (_instance == null)
    {
      _instance = this as T;
      DontDestroyOnLoad(gameObject);
    }
    else
    {
      Destroy(gameObject);
    }
  }
}



싱글톤의 대안 #

  • 관리자 클래스가 꼭 필요한지 생각해보기
    • ‘도우미’ 역할을 하는 관리자 클래스의 메서드를 원래 클래스로 옮겨서 관리자 클래스를 없앨 수 있다.

  • 전역접근 없이 인스턴스를 한 개로 제한하는 방법들
    1. 정적 클래스를 사용한다.
    2. 런타임에 인스턴스 개수를 검사한다. (아래 코드 참고)
class FileSystem
{
private:
  static bool instantiated;
  
public:
  FileSystem()
  {
    // 만약 인스턴스를 또 만들려고 하면 코드가 중지된다.
    // 단, 런타임에 인스턴스 개수를 확인한다는 단점이 있다. 
    assert(instantiated == false);
  
    instantiated = true;
  }
  
  ~FileSystem()
  {
    instantiated = false;
  }
};

bool FileSystem::instantiated = false;

  • 객체에 접근할 수 있게 만드는 방법들
    1. 객체를 매개변수로 넘겨줘서 접근할 수 있게 한다.
    2. 상속을 이용해서 접근할 수 있게 한다.
    3. 이미 전역인 객체에서 접근점을 제공한다.
    4. 중개자 패턴을 사용해서 접근점을 제공한다.



References #