하나의 클래스가 1개의 개체만 가질 필요가 있을 때, 예를 들어 Context 또는 Manager 개체처럼 1개의 개체가 시스템 내 다른 행동을 조율해야 할 때, 사용할 수 있다. 이 패턴을 이용해 공유 개체를 생성/관리함으로써 동일한 시스템 위에 존재하는 여러 개체간 통신을 쉽게 구현할 수가 있다. 또한 종종 글로벌 변수의 완곡한 표현으로 사용되기 때문에 anti-pattern으로 간주되기도 한다.
1. 구현
2. 구조
3.1 C++ 예제
3.2 C++ (thread-safe) 예제
4. factory 메서드 패턴과 함께 사용하는 예 |
1. 구현
────────────────────────────────────────────
singleton 클래스는 개체가 없을 경우 새로 하나를 만들고, 이미 존재한다면 단순히 그 개체에 대한 참조를 리턴하는 메서드를 갖고 있다. 다른 방법으로 개체가 생성되지 않도록 하기 위해, 생성자는 private 또는 protected다. singleton은 정적 인스턴스로 구현될 수 있지만, 필요할 때 생성될 수 있고, 필요할 때까지 메모리나 리소스가 필요 없다는 점에서 단순한 정적 인스턴스와 다르다.
하지만 멀티스레드 애플리케이션에서는 신중히 사용해야 한다. singleton의 인스턴스가 아직 생성되지 않은 상황에서 2개의 스레드가 동시에 생성하려고 한다면, singleton의 인스턴스를 검사한 후 1개의 스레드만 생성 메서드를 실행하도록 해야 한다. 이에 대한 고전적인 해결책은 클래스에 mutex (mutual exclusion)를 사용하는 것이다.
2. 구조
────────────────────────────────────────────
그림 1. 논리적 모델
그림 2. 물리적 모델
3.1 C++ 예제
────────────────────────────────────────────
다음은 singleton이 정적 로컬 개체인 CRTP(Curiously Recurring Template Pattern 또는 Meyers singleton)를 사용한 코드다:
template< typename T > class Singleton
{
public:
static T& Instance()
{
static T the SingleInstance; // T가 기본 생성자를 가지고 있다고 가정
return theSingleInstance;
}
}; |
class OnlyOne : public Singleton< OnlyOne >
{
// ..나머지 인터페이스 정의
}; |
3.2 C++ (thread-safe) 예제
────────────────────────────────────────────
singleton 클래스와 관련된 흔한 thread-safe 디자인 패턴은 double-checked locking(이중 검사 락 걸기)을 사용하는 것이다. 하지만 C++ 표준은 원래 멀티스레드를 고려하지 않았고, 프로세서의 메모리 모델과 컴파일러의 명령어 재정렬 최적화 및 컴파일러와 동기화 라이브러리간의 상호작용에 의해 동작 여부가 결정되지만, 어떤 명시적인 메모리 경계에서 작동하게 만들 수는 있다. 미래의 C++ 표준에는 스레드가 포함될 수도 있지만, 현재는 제안된 사항일 뿐이다.
thread-safe 구현의 한 방법으로, 다음처럼 singleton 클래스에 mutex를 더할 수 있다. mutex와 mutex_locker라는 2개의 헬퍼함수를 정의하자:
class mutex
{
public:
mutex(){ pthread_mutex_init(&m, 0); }
void lock(){ pthread_mutex_lock(&m); }
void unlock(){ pthread_mutex_unlock(&m); }
private:
pthread_mutex_t m;
}; |
class mutex; // forward 선언
class mutex_locker
{
public:
mutex_locker(mutex &mx) // RAII 패러다임 하에서 호출되도록 설계
{
pm = &mx;
pm->lock();
}
~mutex_locker(){ pm->unlock(); }
private:
mutex *pm;
}; |
그리고나서 다음처럼 singleton 클래스를 재정의한다:
class mutex; // forward 선언
class singleton
{
public:
static singleton* instance();
protected:
singleton();
private:
static singleton *inst;
static mutex m;
}; |
singleton 클래스의 구현은 다음과 같다:
singleton::singleton(){ ... /* 필요한 초기화를 한다 */ }
singleton* singleton::instance()
{
mutex_locker lock(m);
if(inst == 0) inst = new singleton;
return inst;
} |
정적 멤버의 런타임 선언은 다음과 같다:
singleton *singleton::inst = 0;
mutex singleton::m; |
singleton::instance() 함수에서 사용된 mutex_locker 클래스는 scoped lock이라고 알려진 RAII 개체로 사용되었다. mutex_locker 생성자가 lock을 얻고, 소멸자가 이를 해제한다. 언어 수준에서 stack unwind 중에 자동으로 할당된 개체의 소멸자 (예: 스택) 가 호출되도록 하기 때문에, singleton::instance() 실행중 에러가 발생하더라도 mutex lock을 넘겨줄 수 있다.
그럼 singleton::instance()가 호출될 때마다 mutex 획득과 해제에 따르는 비용을 개선하는 방법에 대해 알아보자. 첫째로, 동기화 연산에 따르는 비용이 중요한지를 알아내기 위해 코드 프로파일링을 해야 한다 (성급한 최적화는 모든 악의 근원이다─Hoare, Knuth). 오늘날의 멀티스레드 OS는 동시처리를 염두에 두고 설계되었다 (연산 당 수백 ns에서 수 µs까지 다양하다). 따라서 당연히 런타임시 mutex locking 및 unlocking 비용은 사소할 것이다.
둘째로, 원래 singleton 클래스는 딱 한번만 생성되어야 하는 수명이 길고 프로세스에 유일한 개체를 표현한 것이다. 프로세스 초반에 이를 호출하면 locking할 필요가 없어진다 (이 때 mutex를 비활성화하는 컴파일타임 플래그가 유용할 수 있다; 단일스레드 애플리케이션에서는 mutex가 완전히 중복되기 때문이다). 마찬가지로, 애플리케이션의 동시처리 모델이 master/ slave라면, singleton은 worker 스레드가 생성되기 전 main 스레드에서 초기화될 수 있다.
마지막으로, (mutex를 인식하는) singleton 인스턴스는 스레드가 처음 시작될 때 얻을 수 있고 (이 모델에서는 pthread_once() 함수를 사용한다) thread-local storage에 할당될 수 있다.
4. factory 메서드 패턴과 함께 사용하는 예
────────────────────────────────────────────
종종 system-wide 리소스─이를 사용하는 코드는 어떤 타입의 리소스를 사용하는지 모르는─를 만드는데 singleton과 factory 메서드 패턴이 함께 사용된다. 그 예는 바로 Java AWT (Abstract Windowing Toolkit) 다.
java.awt.Toolkit은 추상 클래스로, 다양한 AWT 컴포넌트를 특정 native 툴킷에 바인딩한다. Toolkit 클래스는 platform-specific 파생 클래스를 리턴하는 Toolkit.getDefaultToolkit() factory 메서드를 갖고 있다. AWT는 바인딩을 수행할 단 1개의 개체가 필요한데 이 개체는 생성 비용이 비싸기 때문에, Toolkit 개체는 singleton으로 구현되었다. 플랫폼 독립적인 컴포넌트는 특정 툴킷 메서드의 구현 내용을 모르기 때문에, 툴킷 메서드는 클래스의 정적 메서드가 아닌 개체로 구현되어야 한다. 특정 Toolkit 파생 클래스의 이름은 System.getProperties()를 통해 얻어지는 "awt.toolkit" 환경 속성으로 알 수 있다.
툴킷은 java.awt.Window가 platform-specific java.awt.peer.WindowPeer에 바인딩되도록 한다. 이로써 Window 클래스와 이를 사용하는 애플리케이션 모두 어떤 platform-specific 파생 클래스가 사용되는지 알 필요가 없다.