싱글톤 패턴 (Singleton Pattern) [세번째]
앞서 살펴보았던 코드의 문제점을 찾으셨나요 ?
일단 저는 그 코드의 두가지가 마음에 들지 않습니다.
- 첫째, 객체 하나 생성하는데 복잡하게 mutex lock 까지 사용해야하나 ? 하는 의구심이 듭니다.
- 둘째, get_instance 함수 내에서 생성한 객체가 new 를 이용하는데 해당 객체는 명시적으로 delete 해주지 않는 한 소멸되지 않을 것입니다.
#include <stdio.h> #include <unistd.h> #include <pthread.h> class puppy_class { private: static puppy_class instance; public: puppy_class() { printf("puppy_create\n"); } static puppy_class &get_instance() { return instance; } }; puppy_class *puppy_class::instance = NULL; void *thread_func(void *arg) { int puppy_no = *(int *)arg; printf("thread create [%d]\n", puppy_no); puppy_class &puppy = puppy_class::get_instance(); } int main() { int i; pthread_t tid[10]; for(i=0; i<10; i++) { pthread_create(&tid[i], NULL, thread_func, &i); } }
$ g++ puppy_class.cpp -lpthread $ ./a.out puppy create thread create [0] thread create [1] ... thread create [9]
코드도 간단하고 명확하지 않나요 ? 이게 제일 좋은 답인 것 같죠 ?
객체도 초기에 아예 static 으로 (Global로) 하나만 만들고 그 이후로는 static 영역에 만들어진 객체를 리턴하는 방식입니다.
하지만, 애석하게도 위의 코드조차 단점이 있습니다. 다음 사항을 꼭 기억하시기 바랍니다.
- 첫째는, static 멤버변수는 static 전역변수처럼 프로그램 main 이 시작하기 전에 초기화됩니다.
즉, 프로그램이 시작도 하기 전에 객체가 생성된다는 말이지요.
이 말은 아직 객체를 사용하게 될지 말지도 결정되지 않은 시점에서 이미 객체는 만들어져 있다는 뜻이죠.
낭비의 한 요소가 됩니다. - 두번째는, 이러한 전역 객체들이 다수일 경우 c++ 에서는 해당 객체들의 생성 순서에 대해 어떤 명확한 기준을 둔 것이 없기 때문에, main 이 시작하기 이전에 누가 먼저 생성되든 상관이 없습니다.
그렇다면, 하나의 전역 객체의 생성자에서 만약 다른 전역 객체를 참조하는 일이 있다면 문제가 될 수 있겠네요.
아직 생성하지도 않은 객체를 참조하는 상황이 벌어질 수 있으니까요. ^^
#include <stdio.h> #include <unistd.h> #include <pthread.h> class puppy_class { private: public: puppy_class() { printf("puppy_create\n"); } static puppy_class &get_instance() { static puppy_class instance; return instance; } }; void *thread_func(void *arg) { int puppy_no = *(int *)arg; printf("thread create [%d]\n", puppy_no); puppy_class &puppy = puppy_class::get_instance(); } int main() { int i; pthread_t tid[10]; for(i=0; i<10; i++) { pthread_create(&tid[i], NULL, thread_func, &i); } }
윗쪽에서 살펴본 객체는 main 이전에 전역 객체를 미리 생성하기 때문에 thread create 이전에 "puppy create"가 먼저 찍힙니다.
하지만, 이번 코드는 스레드 생성 후 해당 스레드가 get_instance 함수를 호출하는 시점에 객체가 생성되기 때문에 puppy create 라는 문구가 첫번째 thread create 이후에 찍혔습니다.
$ g++ puppy_class.cpp -lpthread $ ./a.out thread create [0] puppy create thread create [1] ... thread create [9]
이번 코드에서 달라진 점이라곤 static instance 변수를 get_instance 함수의 local 로 이동한 것 뿐입니다.
하지만, 이 하나만으로 다음과 같은 이전 코드들의 단점들이 해소됩니다.
- static 변수를 사용함으로써 global instance 로 하나만 생성되는 것이 lock 없이도 보장된다.
- local 변수를 사용함으로써 main 이전에 객체가 생기지 않고 get_instance 의 첫 호출 시 생성된다.
- new 를 사용할 때처럼 명시적으로 delete 를 이용하여 객체를 해제해줄 필요가 없다.
어때요 ? 이만하면 단순하면서도 단점이 거의 없는 코드 아닌가요 ?
하지만, 위의 코드는 딱 한가지 단점이 남아있기는 합니다.
그러한 단점은 소멸 단계에서 드러날 수 있습니다. c++ 에서는 생성과 마찬가지로 소멸의 경우에도 전역객체의 소멸 순서를 명확하게 정의한 것이 없습니다.
따라서, 만약 특정 전역 객체의 소멸자에서 다른 전역 객체를 참조하는 경우, 이미 소멸된 객체를 참조할 수 있으므로 주의해야 합니다.
이러한 상황은 만들지 않는 것이 좋겠지만, 꼭 해결해야하는 과제라면 이 부분은 스스로 한번 생각해 보시기 바랍니다. (여기에서는 다루지 않겠습니다.)
자... 지금까지 싱글톤 패턴에 대해 알아보았습니다.
그러면, 이러한 패턴은 어떠한 상황에서 이용할 수 있을까요 ?
전역적으로 유일한 객체, 한번만 생성되는 객체라는 개념을 염두에 두면 사용할 수 있는 상황은 무궁무진할 것 같습니다.
일단 제가 생각하기에 싱글톤 객체가 필요한 상황들은 다음과 같을 것 같습니다.
- 프로그램 기동 시 설정을 전부 읽어 어딘가에 보관해두고 수시로 꺼내보고 싶을 때
- 프로그램 기동 시 특정 서버와 미리 Socket 등의 세션을 연결해두고 싶을 때
- 프로그램 전체에서 사용하는 특정 공유메모리의 시작 주소를 shmat 를 이용하여 attach 해둔 후 저장해두고 해당 주소를 바탕으로 여러 연산(함수)을 수행하는 class 를 구현하고 싶을 때.
(다른 객체들은 해당 공유 메모리에 대한 아무런 정보없이 Singleton 객체에게 이러한 연산들을 부탁하면 됨.)
사용자의 기호에 따라 참 많은 상황에서 이용할 수 있겠죠 ?
구현 형식도 반드시 위의 코드와 같을 필요는 없습니다.
자신의 입맛대로 상황에 맞게 변경해서 사용하면 됩니다. ^^