[C/C++] Singleton Class (싱글톤 클래스)

개발 노트 2009. 1. 29. 15:04 posted by 무병장수권력자


Singleton Class (싱글톤 클래스) in C++

작성자 : 김문규
최초 작성일 : 2009. 1.29

1. 들어가며
싱글톤은 전역 객체가 필요할 경우에 유용하게 사용할 수 있습니다. 이에 대해서 구현하는 방법을 생각해 볼까 합니다.

2. 가벼운 구현
#include <iostream>
using namespace std;
class CSingleton
{
private:
 static CSingleton *pInstance;
 CSingleton() : m_iValue(10) {};
 ~CSingleton() {}
 
 int m_iValue;
public:
 static CSingleton& getInstance()
 {
  if(pInstance == NULL) pInstance = new CSingleton;
  return *pInstance;
 }
 static CSingleton* getInstancePtr()
 {
  if(pInstance == NULL) pInstance = new CSingleton;
  return pInstance;
 }
 static void releaseInstance()
 {
  if(pInstance != NULL) delete pInstance;
 }
 void setValue(int in) { cout << m_iValue << "-->" ; m_iValue = in; cout << m_iValue << endl; }
 int getValue() { return m_iValue; }
};
CSingleton *CSingleton::pInstance = NULL;
int main(){
 CSingleton::getInstance().setValue(3);
 CSingleton::getInstance().setValue(5);
 CSingleton::releaseInstance();

 return 0;
}

구현의 핵심은 아래와 같습니다.
1) 생성자를 private으로 감추어서 새로운 객체가 외부에서 마구 생성되는 것을 방지한다.
2) 유일하게 생성된 객체에 접근하는 방법을 제공한다. getInstance()가 여기에 해당!


대부분의 경우에는 위와 같은 패턴을 그대로 적용하시면 Singleton Class를 사용하실 수 있습니다.

3. 고민 1 : 메모리 해제
new로 생성한 인스턴스를 삭제하지 않아도 될까요? 프로그램이 종료되는 시점에는 어차피 new로 생성한 메모리까지 싸그리 해제가 됩니다. 따라서, 어차피 생성한 목적이 프로그램 종료시까지 단 하나의 객체가 동작하는것이라면 그 크기가 어마어마한 것이 아니라면 명시적으로 메모리 해제를 수행할 필요는 없다는 거지요.
다만, 외부 시스템과 정리해야 하는 것이 있다면 명시적인 해제 함수(releaseInstance)를 만드시고 실행하여야 하겠습니다.

4. 고민 2
그냥 static 변수로 CSingleton을 정의하면 어떨까요? 왠지 찝찝하지 않고 깔끔하게~
static CSingleton pInstance;

좋은 생각이네요~ 하지만 다른 문제가 다시 발생합니다. static 객체는 생성의 시점이 명확하게 정의되지 않습니다. 따라서, 다른 전역 객체에서 이를 참조하고자 할 때는 생성 순서 때문에 문제가 발생할 수 있습니다. 그래서, 소위 위의 예시와 같은 '늦은초기화'를 수행합니다. (new를 이용한 동적 생성)
의견이 분분하지만, new를 이용한 방법이 조금 더 안전해 보입니다.

5. 템플릿을 활용한 구현
그런데, 앞선 구현은 영 모양새가 안 나옵니다. 이런식이라면 싱글톤으로 구현해야 하는 클래스마다 해당 패턴을 적용해야 해야 합니다.
템플릿과 상속을 이용해서 약간 폼나게 바꾸어 보겠습니다.
#include <iostream>
using namespace std;
template <typename T>
class CSingleton
{
private:
 static T *pInstance;
public:
 static T& getInstance()
 {
  if(pInstance == NULL) pInstance = new T;
  return *pInstance;
 }
 static T* getInstancePtr()
 {
  if(pInstance == NULL) pInstance = new T;
  return pInstance;
 }
 static void releaseInstance()
 {
  if(pInstance != NULL) delete pInstance;
 }
};
class Test
{
public:
 Test() : m_iValue(10) {};
 ~Test() {};
 void setValue(int in) { cout << m_iValue << "-->" ; m_iValue = in; cout << m_iValue << endl; }
 int getValue() { return m_iValue; }
private:
 int m_iValue;
};
Test* CSingleton<Test>::pInstance = NULL;
int main(){
 cout << "Singleton 객체 생성" << endl;
 CSingleton<Test>::getInstancePtr()->setValue(3);
 CSingleton<Test>::getInstancePtr()->setValue(5);
 cout << "Singleton 객체 해제" << endl;
 CSingleton<Test>::releaseInstance();
 
 return 0;
}

이제 뭔가 기능의 분리가 확실히 되었다는 느낌이 옵니다. Test라는 클래스를 Singleton 객체로 사용할 수 있게 했습니다. 제 생각에는 이 정도면 충분해요 ^^
물론 Singleton의 속성을 상속하고 있지 않기 때문에 new로 개별 생성도 가능합니다. 이 문제는 제가 고민을 많이 했지만 결론은 프로그래머의 선택에 넘겨야 할 듯합니다. 이게 정상인지..싱글톤 클래스라면 new로 생성이 안되야 하는데... Test의 생성자를 private으로 선언할 수도 없고...흠 하튼 이부분은 잘 모르겠어요 ^^;
이 부분에 대해서 좀 더 깊은 지식이 있으신 고수분께서 좋은 방법을 설명해 주시면 좋겠어요~


6. 마치며
싱글톤은 전역변수를 대신에 사용할 수 있는 구현 패턴입니다. 전역 변수 자체는 생성되는 시점과 해제되는 시점이 매우 모호하기 때문에 칼코딩시에 문제를 일으킬 수 있기 때문입니다. 이에 반해 싱글톤은 getInstance가 호출되는 순간이 해당 변수가 생성되는 시점이기 때문에 그 시점이 명확하다 할 수 있습니다.
이처럼 싱글톤은 프로그램의 상태를 관리할 때나 전역에서 관리되어야 할 변수가 있는 경우에는 편리하게 혼돈없이 에러의 가능성을 줄이면서 사용할 수 있습니다.
근데..아직도 모호해요. ㅎㅎ