U E D R , A S I H C RSS

Gof/Singleton


1. Singleton

1.1. Intent

클래스로 하여금 오직 하나의 인스턴스만을 가지게 하며, 어디서든지 접근 가능하도록 한다.

1.2. Motivation

몇몇 클래스들에 대해서 오직 하나의 인스턴스 만을 가지는 것은 중요한 일이다. 예를 들면, 어떤 시스템에선 수많은 프린터들이 있더라도 거기에는 단 하나의 프린터 스플러만이 있어야 한다. OS에서 돌아가는 화일시스템이나 윈도우 매니저의 경우도 오직 하나여야 한다 (동시에 2-3개의 윈도우매니저가 돌진 않는다.) 디지털 필터의 경우에도 A/D converter는 단 하나를 가진다.

어떻게 우리는 클래스로 하여금 단 하나의 인스턴스만을 가지도록 보장해줄 수 있을까? 그리고 그러한 인스턴스를 쉽게 접근하게 할 수 있을 것인가? global 변수로 둘 경우 어디서든지 접근가능하겠지만, global 변수는 단일 인스턴스만을 가지도록 할 수 없다.

더 좋은 방법은 클래스 자신으로 하여금 자기자신의 단일 인스턴스를 유지하도록 만드는 것이다. 이 클래스는 인스턴스가 생성될 때 요청을 가로챔으로서 단일 인스턴스로 만들어지는 것은 보증한다. 또한, 인스턴스에 접근하는 방법도 제공한다. 이것이 바로 SingletonPattern이다.

1.3. Applicability

SingletonPattern은 다음과 같은 경우에 사용한다.
  • 클래스가 정확히 오직 하나의 인스턴스만이 존재해야 할 때. 그리고 잘 알려진 엑세스 방법으로 어디서든지 접근 할 수 있어야 한다.
  • 단일 인스턴스가 서브클래싱에 의해 확장가능해야 할 경우. 그러면 클라이언트는 그들의 코드 수정없이 확장된 인스턴스를 사용할 수 있어야 한다.

1.5. Participants

  • Singleton
    • Instance operation (클래스의 메소드)을 정의한다. Instance 는 클라이언트에게 해당 Singleton의 유일한 인스턴스를 접근할 수 있도록 해준다.
    • Singleton 자신의 유일한 인스턴스를 생성하는 책임을 가진다.

1.6. Collaborations

  • 클라이언트는 오직 Singleton의 Instance operation으로만 Singleton 인스턴스에 접근할 수 있다.

1.7. Consequences

SingletonPattern은 여러가지 장점을 가진다.
  1. 클래스에 대한 접근이 오직 하나의 인스턴스에게로 제한된다. Singleton 클래스는 자기 자신의 단일 인스턴스를 캡슐화하기 때문에, 클라이언트가 언제, 어떻게 접근하던지 그 접근이 엄격하게 제어된다.
  2. namespace를 줄인다. SingletonPattern은 global variable을 줄임으로서 global variable로 인한 namespace의 낭비를 줄인다.
  3. 명령어와 표현을 확장시킬 수 있다. Singleton class는 subclass될 수 있고, 이 확장된 클래스의 인스턴스를 가지고 어플리케이션을 설정하는 것은 쉽다. run-time중에 필요한 경우에도 가능하다.
  4. 여러개의 인스턴스를 허용한다. 프로그래머의 마음에 따라 쉽게 Singleton class의 인스턴스를 하나이상을 둘 수도 있도록 할 수 있다. 게다가 어플리케이션이 사용하는 인스턴스들을 제어하기 위해 동일한 접근방법을 취할 수 있다. 단지 Singleton 인스턴스에 접근하는 것을 보장하는 operation만 수정하면 된다.
  5. class operation 보다 더 유연하다. 패키지에서 Singleton의 기능을 수행하기위한 또다른 방법은 class operation들을 사용하는 것이다. (C++에서의 static 함수나 Smalltalk에서의 class method 등등) 하지만, 이러한 언어적인 테크닉들은 여러개의 인스턴스를 허용하는 디자인으로 바꾸기 힘들어진다. 게다가 C++에서의 static method는 virtual이 될 수 없으므로, subclass들이 override 할 수 없다.

1.8. Implementation

SingletonPattern 을 사용할 때 고려해야 할 사항들이 있다.

1. unique instance임을 보증하는 것. SingletonPattern의 경우도 일반 클래스와 마찬가지로 인스턴스를 생성하는 방법은 같다. 하지만 클래스는 늘 단일 인스턴스가 유지되도록 프로그래밍된다. 이를 구현하는 일반적인 방법은 인스턴스를 만드는 operation을 class operations으로 두는 것이다. (static member function이거나 class method) 이 operation은 unique instance를 가지고 있는 변수에 접근하며 이때 이 변수의 값 (인스턴스)를 리턴하기 전에 이 변수가 unique instance로 초기화 되어지는 것을 보장한다. 이러한 접근은 singleton이 처음 사용되어지 전에 만들어지고 초기화됨으로서 보장된다.

다음의 예를 보라. C++ 프로그래머는 Singleton class의 Instance operation을 static member function으로 정의한다. Singleton 또한 static member 변수인 _instance를 정의한다. _instance는 Singleton의 유일한 인스턴스를 가리키는 포인터이다.

Singleton class는 다음과 같이 선언된다.
~cpp 
class Singleton {
public:
	static Singleton* Instance ();
protected:
	Singleton ();
private:
	static Singleton* _instance;
};

대응되는 실제 구현부는 다음과 같다.
~cpp 
Singleton* Singleton::_instance = 0;

Singleton* Singleton::Instance () {
	if (_instance == 0) {
		_instance = new Singleton;
	}
	return _instance;
}

클래스를 사용하는 Client는 singleton을 Instance operation을 통해 접근한다. _instance 는 0로 초기화되고, static member function 인 Instance는 단일 인스턴스 _Instance를 리턴한다. 만일 _instance가 0인 경우 unique instance로 초기화시키면서 리턴한다. Instance는 lazy-initalization을 이용한다. (Instance operation이 최초로 호출되어전까지는 리턴할 unique instance는 생성되지 않는다.)

생성자가 protected 임을 주목하라. client 가 직접 Singleton을 인스턴스화 하려고 하면 compile-time시 에러를 발새할 것이다. 생성자를 protected 로 둠으로서 늘 단일 인스턴스로 만들어지도록 보증해준다.

더 나아가, _instance 는 Singleton 객체의 포인터이므로, Instance member function은 이 포인터로 하여금 Singleton 의 subclass를 가리키도록 할 수 있다.

C++ 구현에 대해서는 생각해야 할 것이 더 있다. singleton 을 global이나 static 객체로 두고 난 뒤 자동 초기화되도록 놔두는 것으로 충분하지 않다. 이에 대해서는 3가지 이유가 있다.
  • (a) static 객체의 유일한 인스턴스가 선언되어질 것이라고 보장할 수 없다.
  • (b) 모든 singleton들이 static initialization time 대 인스턴스되기 위한 충분한 정보를 가지고 있지 않을수도 있다. singleton은 프로그램이 실행될 때 그러한 정보를 얻을 수 있다.
  • (c) C++ 은 global 객체의 생성자가 translation unit를 통하면서 호출될때의 순서를 정의하지 않는다ES90. 이러한 사실은 singleton 들 간에는 어떠한 의존성도 존재할 수 없음을 의미한다. 만일 그럴 수 있다면, 에러를 피할 수 없다.
약간 첨언을 하면, global/static 객체의 접근은 singleton들이 사용되건 사용되지 않건 간에 모든 singleton이 만들어지도록 한다는 것이다. static member function 를 사용함으로서 이러한 모든 문제들을 피할 수 있다.

Smalltalk에서 unique instance를 리턴하는 functiond은 Singleton 클래스의 class method로 구현된다. 단일 인스턴스가 만들어지는 것을 보장하기 위해서 new operation을 override한다. The resulting Singleton class might have the following two class methods, where SoleInstance is a class variable that is not used anywhere else:
~cpp 
new
	self error:	'cannot create new object'

default
	SoleInstance isNil ifTrue:  [SoleInstance := super new].
	^ SoleInstance

2. Singleton class를 subclassing 하기 관련. 주된 주제는 클라이언트가 singleton 의 subclass를 이용할 수 있도록 subclass들의 unique instance를 설정하는 부분에 있다. 필수적으로, singleton 인스턴스를 참조하는 변수는 반드시 subclass의 인스턴스로 초기화되어져야 한다. 가장 단순한 기술은 Singleton의 Instance operation에 사용하기 원하는 singleton을 정해놓는 것이다. Sample Code에는 환경변수들을 가지고 이 기술을 어떻게 구현하는지 보여준다.

Singleton의 subclass를 선택하는 또 다른 방법은 Instance 를 Parent class에서 빼 낸뒤, (e.g, MazeFactory) subclass 에 Instance를 구현하는 것이다. 이는 C++ 프로그래머로 하여금 link-time시에 singleton의 class를 결정하도록 해준다. (e.g, 각각 다른 구현부분을 포함하는 객체화일을 linking함으로써.)

이러한 link-approach 방법은 link-time때 singleton class 의 선택을 고정시켜버리므로, run-time시의 singleton class의 선택을 힘들게 한다. subclass를 선택하기 위한 조건문들 (switch-case 등등)은 프로그램을 더 유연하게 할 수 있지만, 그것 또한 이용가능한 singleton class들을 묶어버리게 된다. 이 두가지의 방법 다 그다지 유연한 방법은 아니다.

더욱더 유연한 접근 방법으로 registry of singletons 이 있다. 가능한 Singleton class들의 집합을 정의하는 Instance operation을 가지는 것 대신, Singleton class들을 잘 알려진 registry 에 그들의 singleton instance를 등록하는 것이다.

registry 는 string name 과 singletons 을 mapping 한다. singleton의 instance가 필요한 경우, registry에 string name으로 해당 singleton 을 요청한다. registry는 대응하는 singleton을 찾아서 (만일 존재한다면) 리턴한다. 이러한 접근방법은 모든 가능한 Singleton class들이나 instance들을 Instance operation이 알 필요가 없도록 한다. 필요한 것은 registry에 등록될 모든 Singleton class들을 위한 일반적인 interface이다.
~cpp 
class Singleton {
public:
	static void Register (const char* name, Singleton*);
	static Singleton* Instance ();
protected:
	static Singleton* Lookup (const char* name);
private:
	static Singleton* _instance;
	static List<NameSingletonPair>* _registry;
};

Register operation은 주어진 string name으로 Singleton instance를 등록한다. registry를 단순화시키기 위해 우리는 NameSingletonPair 객체의 리스트에 instance를 저장할 것이다. 각 NameSingletonPair는 name과 instance를 mapping한다. Lookup operation은 주어진 이름을 가지고 singleton을 찾는다. 우리는 다음의 코드에서 environment variable이 원하는 singleton의 이름을 명시하고 있음을 생각할 수 있다.
~cpp 
Singleton* Singleton::Instance () {
	if (_instance == 0) {
		const char* singletonName = getenv("SINGLETON");
		// user or environment supplies this at startup

		_instance = Lookup (singletonName);
		// Lookup returns 0 if there's no such singleton
	}
	return _instance;
}
어디에서 Singleton class들이 그들을 등록하는가? 한가지 가능성은 그들의 생성자에서다. 예를들어 singleton의 subclass인 MySingleton 은 다음과 같이 구현할 수 있다.
~cpp 
MySingleton::MySingleton () {
	// ...
	Singleton::Register ("MySingleton", this);
}
물론, 코드 어디에선가 클래스를 인스턴스화하지 않으면 생성자는 호출되지 않을 것이다. C++에서는 MySingleton의 static instance를 정의함으로서 이 문제를 잘 해결할 수 있다. 예를 들어, MySingleton 클래스의 구현부를 포함하는 화일에 다음과 같이 정의하면 된다.
~cpp 
static MySingleton theSingleton;

더 이상 Singleton class 는 singleton 객체를 만들 책임이 없다. 그 대신 이제 Singleton 의 주된 책임은 시스템 내에서 선택한 singleton 객체를 접근가능하도록 해주는 것이다. static object approach는 여전히 단점이 존재한다. 모든 가능한 Singleton subclass들의 인스턴스들이 생성되어지던지, 그렇지 않으면 register되어서는 안된다는 것이다.

1.9. Sample Code

미로를 만드는 MazeFactory 클래스를 정의했다고 하자. MazeFactory 는 미로의 각각 다른 부분들을 만드는 interface를 정의한다. subclass들은 더 특별화된 product class들의 instance들을 리턴하기 위한 opeation들을 재정의할 수 있다. 예를 들면 BombedWall 객체는 일반적인 Wall객체를 대신한다.

여기서 SingletonPattern과 관련 되는 내용은 Maze application은 단 하나의 maze factory를 필요로 한다는 것과 그 maze factory의 인스턴스는 어디서든지 maze의 부분을 만들 수 있도록 존재해야 한다는 것이다. 이러할 때가 바로 SingletonPattern을 도입할 때이다. MazeFactory를 Singleton으로 구현함으로써, global variable에 대한 재정렬을 할 필요가 없이 maze 객체를 만들때 필요한 MazeFactory를 global하게 접근할 수 있다.

일단 단순하게, MazeFactory의 subclassing이 필요없다고 가정하자. (잠시 후 subclassing과 관련, 대안적인 방법에 대해 고려해 볼 것이다.) C++ 에서는 static operation인 Instance와 unique instance를 참조하는 static member인 _instance 를 추가함으로서 Singleton 클래스를 구현할 수 있다. 위의 Implementation에서도 언급했듯이 반드시 생성자는 protected로 둠으로서 우발적으로 하나이상의 인스턴스가 생성되는 것을 막는다.

~cpp 
class MazeFactory {
	public:
		static MazFactory* Instance ();

		// existing interface goes here
	protected:
		MazeFactory ();
	private:
		static MazeFactory* _instance;
	};

대응되는 클래스의 실제 구현부분은 다음과 같다.

~cpp 
MazeFactory* MazeFactory::_instance = 0;

MazeFactory* MazeFactory::Instance () {
	if (_instance == 0) {
		_instance = new MazeFactory;
	}

	return _instance;
}

자, 이제 MazeFactory의 subclassing에 대해 생각해보자. MazeFactory의 subclass가 존재할 경우, application은 반드시 사용할 singleton을 결정해야 한다. 여기서는 환경변수를 통해 maze의 종류를 선택하고, 환경변수값에 기반하여 적합한 MazeFactory subclass를 인스턴스화하는 코드를 덧붙일 것이다. Instance operation은 이러한 코드를 구현할 좋은 장소이다. 왜냐하면 Instance operation은 MazeFactory를 인스턴스하는 operation이기 때문이다.
~cpp 
MazeFactory* MazeFactory::Instance () {
	if (_instance == 0) {
		const char* mazeStyle = getenv ("MAZESTYLE");

		if (strcmp (mazeStyle, "bombed") == 0) {
			_instance = new BombedMazeFactory;

		} else if (strcmp (mazeStyle, "enchanted") == 0) {
			_instance = new EnchantedMazeFactory;
		

		// ... other possible subclasses

		} else {		// default
			_instance = new MazeFactory;
		}
	}

	return _instance;
}
새로운 MazeFactory의 subclass를 정의할때 매번 Instance 가 반드시 수정되어야 한다는 것에 주목하자. 이 application에서야 별다른 문제가 발생하지 않겠지만, 이러한 구현은 framework 내에 정의된 abstract factory들 내에서만 한정되어버린다. (Implementation의 subclass 관련 부분 참조)

가능한 해결책으로는 Implementation에서 언급한 registry approach를 사용하는 것이다. Dynamic linking 방법도 또한 유용한 방법이다. Dynamic linking 은 application으로 하여금 사용하지 않는 subclass 도 전부 load해야 할 필요성을 덜어준다.

1.10. Known Uses

Smalltalk-80Par90SingletonPattern의 예는 ChangeSet current라는 코드에 대한 change들 집합들이다. 더 적합한 예제로는 클래스들과 그들의 metaclass 간의 관계이다. metaclass는 클래스의 클래스로, 각 metaclass는 하나의 인스턴스를 가진다. metaclass들은 이름을 가지지 않지만 (그들의 단일인스턴스를 통한 간접적인 방법을 ㅔㅚ하고), 그들의 단일 인스턴스를 유지하며 일반적으로 다른 클라이언트에 의해 생성되지 않는다.

InterViews user interface toolkitLCI+92는 toolkit의 Session과 WidgetKit 클래스의 unique instance에 접근하지 위해 SingletonPattern을 이용한다. Session은 application의 메인 이벤트를 dispatch하는 루프를 정의하고 사용자 스타일관련 데이터베이스를 저장하고, 하나나 그 이상의 물리적 display 에 대한 연결들(connections)을 관리한다. WidgetKit은 user interface widgets의 look and feel을 정의한다. WidgetKit::instance () operation은 Session 에서 정의된 환경변수에 기반하여 특정 WidgetKit 의 subclass를 결정한다. Session의 비슷한 operation은 지원하는 display가 monochrome display인지 color display인지 결정하고 이에 따라서 singleton 인 Session instance를 설정한다.

1.11. Related Patterns

많은 pattern들이 SingletonPattern을 사용하여 구현될 수 있다. AbstractFactoryPattern, BuilderPattern, PrototypePattern을 참조하라.




  • 제가 테스트 용으로 n-class singleton을 구현하려 합니다. 그런데 다음과 같은 문제가 발생하는데 어떻게 해결해야 될까요?

~cpp 
----- CNSingleton.h ------

#include <afxtempl.h>

class CNSingleton : public CObject  
{
public:
         class CSingletonList; //  C2248 해결
         friend CSingletonList; //  C2248 해결 
	static CNSingleton* Instance();

private:
	CNSingleton();	
	virtual ~CNSingleton();

	////////////////////////////////////////////
	// inner class
	static class CSingletonList : public CObject
	{
	public:
		CSingletonList() throw();
		virtual ~CSingletonList() throw();

		CNSingleton* Next();

	private:
		void Init();
		void Destory();

		int m_Count;
		int m_Index;
		CList <CNSingleton*, CNSingleton*>* m_ContainerOfSingleton;
	};

	static CSingletonList* m_Instances;
};

----- CNSingleton.cpp ------

#include "stdafx.h"
#include "NSingleton.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CNSingleton::CSingletonList::CSingletonList() throw()
{
	Init();
}

CNSingleton::CSingletonList::~CSingletonList() throw()
{
	Destory();
	delete m_ContainerOfSingleton;
}

////////////////////////////////////////////////////////////////////////
// 객체 초기화
void CNSingleton::CSingletonList::Init()
{
	m_Index = 0;
	m_Count = 3;//임시
	m_ContainerOfSingleton = new CList <CNSingleton*, CNSingleton*>;
	for (int i = 0; i < m_Count; i++)
	{
		m_ContainerOfSingleton->AddTail(new CNSingleton());
	}
}

////////////////////////////////////////////////////////////////////////
// 객체 destory
void CNSingleton::CSingletonList::Destory()
{
	POSITION position = m_ContainerOfSingleton->GetHeadPosition();
	while(position)
	{
		delete m_ContainerOfSingleton->GetAt(position);
		m_ContainerOfSingleton->GetNext(position);
	}

	m_ContainerOfSingleton->RemoveAll();
}

////////////////////////////////////////////////////////////////////////
// index에 해당하는 CNSingleton를 Container에서 찾아 반환 
CNSingleton* CNSingleton::CSingletonList::Next()
{
	if (m_Index == m_ContainerOfSingleton->GetCount())
		m_Index = 0;

	POSITION position = m_ContainerOfSingleton->FindIndex(m_Index);
	m_Index++;

	return m_ContainerOfSingleton->GetAt(position);
}



//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CNSingleton::CSingletonList* CNSingleton::m_Instances = NULL;
CNSingleton::CNSingleton()
{
	m_Instances = new CSingletonList();
}

CNSingleton::~CNSingleton()
{
	delete m_Instances;
}

////////////////////////////////////////////////////////////////////////
// n-th instance를 얻어온다.
CNSingleton* CNSingleton::Instance()
{
	return m_Instances->Next();
}

이 소스를 컴파일하면, outer class의 생성자를 호출하는 부분, 즉 Init()과 Destroy()에서
error C2248: 'CNSingleton::CNSingleton' : cannot access private member declared in class 'CNSingleton' 라고 에러가 발생합니다.
inner class에서는 outer class의 private 생성자에 접근할 권한이 없나요?
만약 저 코드를 제대로 수정하려 한다면 어떻게 해야 되나요? 참고로 저는 http://www.javaworld.com/javaworld/javaqa/2001-11/01-qa-1102-singleton.html에 있는 자바소스를 cpp로 포팅했습니다. -- FredFrith

제가 추가한 다음 두줄
~cpp 
         class CSingletonList; //  C2248 해결
         friend CSingletonList; //  C2248 해결 
을 보십시오. 이렇게 하면 됩니다. -- imays
DeleteMe) imays 이면, 혹시 현직이형이신가요? --기억을 하실지 모르겠는 1002 -- 넵 imays
Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2021-02-07 05:23:19
Processing time 0.0479 sec