싱글톤 패턴 (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 객체에게 이러한 연산들을 부탁하면 됨.)

사용자의 기호에 따라 참 많은 상황에서 이용할 수 있겠죠 ?
구현 형식도 반드시 위의 코드와 같을 필요는 없습니다.
자신의 입맛대로 상황에 맞게 변경해서 사용하면 됩니다. ^^

You may also like...

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x