-
Notifications
You must be signed in to change notification settings - Fork 0
Singleton Pattern
전역 변수로 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴이다.
추상 팩토리 패턴 UML 클래스
-
instance: 생성된 객체 인스턴스 -
Singleton(): 객체를 생성하는 생성자 -
getInstance(): 객체를 반환하는 메소드
기본적인 싱글톤이다.
class Singleton {
private:
static Singleton* _instance;
Singleton() {}
Singleton(const Singleton& other) {}
~Singleton() {}
public:
static Singleton* getInstance() {
return _instance;
}
};위 싱글톤 클래스는 다음과 같은 특징을 가진다.
- 생성자, 소멸자, 복사 생성자를
private로 설정해서 객체가 외부에서 생성, 소멸, 복사되는 것을 방지한다.
위 싱글톤 클래스는 다음과 같은 문제를 가진다.
- 정적 객체이기 때문에, 다른 전역 혹은 정적 객체의 생성자에서 참조하고 싶은 경우 문제가 발생한다. 이유는 C++ 표준에서는 정적, 전역 객체들의 생성, 소멸 순서가 명확히 정의되지 않기 때문이다.
- 최초에 두 개의
Thread가 연속으로getInstance()를 호출했을때_instance의NULL체크에서 두Thread가 모두NULL이 될 가능성이 있다. (동시성 문제)
class Singleton {
private:
static Singleton* _instance;
Singleton();
Singleton(const Singleton& other);
~Singleton();
friend Destroyer;
public:
static Singleton* getInstance() {
if (_instance == nullptr) _instance = new Singleton();
return _instance;
}
};
static class Destroyer {
~Destroyer() {
delete Singleton::getInstance();
}
};최초 getInstance() 메소드가 호출될 때 _instance가 NULL일 경우 생성을 하는 Lazy Initialization 방식을 사용하여 정적 객체의 생성 순서 문제를 해결한 싱글톤 패턴이다.
하지만 friend 키워드는 정보 은닉을 깨버리는 행위임으로, 객체 지향적이지 않다는 단점이 있다.
class Singleton {
private:
Singleton();
Singleton(const Singleton& other);
~Singleton();
public:
static Singleton& getInstance() {
static Singleton _instance;
return _instance;
}
};지역적으로 static 객체로 만들 경우에는 전역으로 만든 객체와는 달리 해당 함수를 처음 호출하는 시점에 초기화와 동기에 생성이 진행된다.
위 객체를 한번도 사용하지 않을 경우에는 생성이 되지 않는다. 그러면서도 static 객체이기 때문에 프로그램 종료 시까지 객체가 남아있게 된다. 또한 프로그램 종료 시에는 마찬가지로 자동으로 소멸자가 호출된다. 그러므로 소멸자에서 자원 해체를 하도록 하게되면 자원 관리도 신경 쓸 필요가 없다.
단점은 소멸 순서도 명시되지 않았기 때문에 어떤 전역 객체가 소멸자에서 저 싱글톤 객체를 사용하려 할 때, 싱글톤 객체가 먼저 소멸했다면 문제가 발생하게 된다.
class Singleton {
private:
static bool isDestroyed;
static Singleton* pInstance;
Singleton();
Singleton(const Singleton& other);
~Singleton() {
isDestroyed = true;
}
static void create() {
static Singleton instance;
pInstance = &instance;
}
static void destroy() {
pInstance->~Singleton();
}
public:
static Singleton& getInstance() {
if (isDestroyed) {
new(pInstance) Singleton;
atexit(Destroy);
isDestroy = false;
}
else if (pInstance == nullptr) {
create();
}
return *pInstance;
}
};정적 객체가 소멸되면 ~Singleton()에 의해서 isDestroyed 변수가 true가 되면서 현재 소멸 정보를 알려준다.
그 다음, 소멸 후 getInstance() 메소드를 통해서 재 호출 시, isDestroyed 변수를 false로 바꿔줌으로써 삭제가 되지 않았다고 알려준다.
즉, 참조할 때 replacement new를 이용하여 해당 객체의 생성자를 다시 호출해서 재생성하는 방식이다.
위 방식이 가능한 이유는 컴파일러는 전역 객체 소멸 시 해당 메모리를 초기화 하지 않기 때문에 메모리를 재사용하여 객체의 생성자만 다시 호출하면 객체를 재사용할 수 있기 때문이다. 그 후, atexit() 메소드에 Destroy()를 Callback으로 등록했기 때문에 프로그램 종료시에 Singleton 객체의 소멸자를 이용해 리소스를 해제를 할 수 있게 된다.
멀티스레드 환경에서 스레드가 동시에 접근해서 동시성 문제가 발생할 가능성이 있다. 따라서 Mutex를 이용해서 한 스레드가 점유하면 다른 스레드는 대기하는 방식으로 구현하여 동시성 문제를 해결할 수 있다.
✨ Author, Glory-Day