[Design Pattern] 행동 패턴 1. 싱글톤 (Singleton)
Table of Contents
싱글톤 패턴 #
- 객체의 인스턴스를 오직 한 개만 생성하고, 그 한 개의 인스턴스에 대한 전역적인 접근점을 제공한다.
- 장점
- 오직 하나의 인스턴스만 생성하기 때문에 메모리 낭비를 방지할 수 있다. 심지어 전혀 사용되지 않는다면 아예 인스턴스를 생성하지 않을 수도 있다.
- 런타임에 초기화되므로, 프로그램이 실행된 다음에야 알 수 있는 정보들을 활용할 수 있다.
- 단점
- 싱글톤은 전역 변수이다.
- 전역적으로 접근하므로, 버그를 찾는다면 연관된 모든 것을 찾아봐야 할 것이다.
- 커플링을 조장한다.
- 멀티 스레딩 같은 동시성 프로그래밍에 알맞지 않다. 찾기 어려운 스레드 동기화 버그가 생기기 쉽다.
- 단일 책임 원칙을 위반한다. 즉, 한 번에 두 가지의 문제를 동시에 해결한다.
- 이런 문제들 때문에 왠만하면 싱글톤을 쓰지 않으려고 한다. (대안 참고)
동기화 문제 해결방안 #
- 고전적인 싱글톤 구현 방법
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);
}
}
}
싱글톤의 대안 #
- 관리자 클래스가 꼭 필요한지 생각해보기
- ‘도우미’ 역할을 하는 관리자 클래스의 메서드를 원래 클래스로 옮겨서 관리자 클래스를 없앨 수 있다.
- 전역접근 없이 인스턴스를 한 개로 제한하는 방법들
- 정적 클래스를 사용한다.
- 런타임에 인스턴스 개수를 검사한다. (아래 코드 참고)
class FileSystem
{
private:
static bool instantiated;
public:
FileSystem()
{
// 만약 인스턴스를 또 만들려고 하면 코드가 중지된다.
// 단, 런타임에 인스턴스 개수를 확인한다는 단점이 있다.
assert(instantiated == false);
instantiated = true;
}
~FileSystem()
{
instantiated = false;
}
};
bool FileSystem::instantiated = false;
- 객체에 접근할 수 있게 만드는 방법들
- 객체를 매개변수로 넘겨줘서 접근할 수 있게 한다.
- 상속을 이용해서 접근할 수 있게 한다.
- 이미 전역인 객체에서 접근점을 제공한다.
- 중개자 패턴을 사용해서 접근점을 제공한다.
References #
- 로버트 나이스트롬, 게임 프로그래밍 패턴