본문 바로가기

Library/C/C++

Singleton Pattern

싱글턴은 단순히 static 메서드와 static 데이터만으로 구성될 수도 있지만, 이런 방식의 구현은 그야말로 가볍게 싱글턴을 구현할 목적이 아니라면 여러가지 약점을 보인다.

싱글턴을 구현하는 가장 손 쉬운 방법은 static 메서드에서, 현재 자신의 인스턴스에 대한 포인터의 널 값 여부를 검사한 뒤, 이미 존재하는 인스턴스라면 그 포인터를 넘기고, 그렇지 않다면 힙에서 새로운 인스턴스를 생성하여 그 주소값을 넘기는 방법이다.

물론, 이 방법은 대부분의 경우에 제대로 동작하지만, 스레드 환경에서는 특정 상황에서 개체가 오직 하나만 존재한다는 사실을 보장해주지 못한다. 사실, 특정한 동기화 방법을 제공하지 않는 이상 스레드 환경에서는 어떤 방법이라도 개체의 유일성을 보장해주는 것은 아니므로, 이것은 굳이 단점이라고 할 수는 없을 것이다. 문제는, 싱글턴의 파괴 순서이다. 싱글턴 패턴에 의해 생성된 개체들은, 파괴되는 순서가 서로에게 의존적일 수 있다. 즉, 어떤 개체 A가 파괴되기 전에 다른 싱글턴 개체 B에 대한 접근을 필요로 할 때, 이미 그 개체 B가 파괴되어 버렸다면, A가 얻는 것은 쓰레기 주소값이며, 정의되지 않은 동작이라는 어두운 세계로 빠져들게 된다.

또, 이미 개체를 생성하는데 큰 비용이 들기 때문에, 필요하지 않다면 개체를 생성하지 않는 경우를 생각해보자. 즉, A라는 개체가 파괴되면서 B 개체의 인스턴스를 요구할 때, 이들 싱글턴 개체들을 나중에 일괄적으로 파괴해버린다면, 이것 또한 문제가 된다. 즉, 싱글턴의 생성과 다르게, 파괴 순서를 결정하는 것은 매우 괴로운 일이며 단순히 언어적인 메카니즘에 의해 싱글턴을 구현하는 것은 이런 부분에 있어서 약점을 보이게 된다. 즉, 어떤 방식으로든지, 이들 개체가 파괴되는 순서를 지정할 수 있어야 이런 문제를 방지할 수 있다.

이 문제를 해결하는 기본적인 아이디어는, 특정 시점에서 파괴되어야 하는 개체를 관리할 스택이나 우선순위 큐를 작성하는 것이다. 그리고, std::atexit() 함수는 재진입을 허용하지 않는 함수이지만, 호출된 횟수만큼, 프로그램이 종료될 때 호출되는 것이 보장되는 함수이다. 이것을 이용해, 종료 함수로 지정되는 정리 함수를 std::atexit() 함수로 등록하고, 이 정리 함수 내에서, 스택이나 큐에서 파괴되어야 하는 개체를 하나씩 꺼내와 청소하는 것이다. 이 방법의 장점은 파괴되는 도중에서 스택에 새로운 싱글턴 인스턴스를 생성할 수 있다는 것이다. 즉, 파괴되는 도중에 어떤 문제가 생겼다면, 이 개체는 다른 싱글턴 개체의 인스턴스를 요청하며, 이때 요청된 싱글턴이 새로 생성되면서 자신의 파괴 함수를 std::atexit()를 통해 새롭게 등록하게 된다.

다른 방법으로는, 싱글턴의 수명을 제어하는 외부 개체 C를 하나 작성하고, 이것은 정상적인 언어적 메카니즘에 의해 파괴되도록 싱글턴으로 구현한다. 개체 C는 등록된 싱글턴들의 우선순위에 따른 적절한 개체들의 파괴 순서를 가지고 있으며, 이 개체의 소멸자에서 등록된 싱글턴들을 파괴 순서에 따라 차례대로 파괴한다. 이 방법은, 특정 개체가 파괴되면서 다른 싱글턴 인스턴스를 얻고자하면 문제가 되지만, 애초에 어느 싱글턴 개체가 더 오래 살아남아야 하는지 여부를 사용자에게 맡길 수 있다. 즉, 로그 개체가 최후까지 살아남아야 한다면, 사용자는 로그 개체에 가장 높은 우선순위를 할당하여, 이 개체가 최후에 파괴되도록 설정해야 한다.
 
개인적으로는 이 방법이 훨씬 더 마음에 드는데, 로그 개체와 같은 경우, 첫번째 방법으로 싱글턴을 구현한다면, 로그 개체로의 참조를 원하는 개체가 파괴되는 시점에서 이미 로그 개체가 파괴되었다고 하자. 그렇다면 새로운 로그 개체가 생성되고, 새로운 로그 개체는 자신의 파괴 함수를 다시 std::atexit()를 통해 등록하며, 파괴 순서를 지정하는 자료구조에 추가된다. 로그 개체가 영속적으로 정보를 계속해서 가질 필요가 있다면 이것은 재생성 비용을 무시한다고 하더라도 좋은 상황이 아니다. 물론, 파괴 시점에 동적으로 새로운 싱글턴 참조에 대한 처리를 할 수 있다는 점은 장점이 되겠지만, 개체 파괴 우선 순위를 지정했다면, 파괴 순서에 어긋나지 않도록 사용자에게 명시적 코드 작성을 유도하는게 더 낫다.


여튼, 싱글턴은 호락호락하게 구현할 수 있는 것이 아니며, 특히 개체 자체를 싱글턴으로 구현하는 것이 아닌, 일반화된 싱글턴 패턴을 구현하는 것은 결코 쉬운 일이 아닐 것이다.