웬디의 기묘한 이야기

글 작성자: WENDYS
반응형

Singleton Pattern


GOF의 23가지 패턴 중 가장 쉬우면서 많이쓰이며, 가장 문제가 될 소지를 가지는 패턴입니다.

먼저 Singleton Pattern의 용도는 하나의 프로그램 내에서 하나의 인스턴스만을 생성해야 하는 상황에서 사용됩니다.

공용 데이터를 관리하는 클래스나, 환경설정등을 관리하는 클래스의 경우엔 하나의 인스턴트로 관리되는 것이 일반적이며, 이때 Singleton Pattern을 적용할 수 있습니다.


class singleton {
private:
    static singleton* _instance;

    singleton() {}
    singleton(const singleton& other);
    ~singleton() {}
public:
    static singleton* instance() {
        if (_instance == nullptr) {
            _instance = new singleton();
        }
        return _instance;
    }
};


실제 사용은 간단합니다. 직접적으로 생성을 할 수 없으며,

Singleton::instance()->function(); 와 같이 instance를 통해서만 접근하여 사용할 수 있습니다.


하지만 위의 코드를 보면 약간 찝찝한 부분이 있습니다.

바로 생성해놓은 _instance 객체에 대한 해제를 하지 않아 메모리 Leak이 일어난것입니다.

실제로 Singleton 객체는 1개의 인스턴스만 생성되도록 하기때문에 메모리에 대한 Leak을 신경써야할 정도는 아니지만, 반드시 반납되어야 하는 외부 시스템자원 등을 사용하는경우엔 명시적으로 꼭 해제가 되어야합니다.


어떻게 하면 좋을까요? 소멸자에 코드를 추가?

안타깝지만 외부에서 소멸자를 직접 호출할 수 없도록 private으로 가둬놨네요

* new 로 생성된 코드는 delete 를 호출하기 전까진 절대 소멸자를 호출 하지 않습니다.


이때 이용할 수 있는 API가 atexit() 입니다.


함수 원형을 보면 다음과 같습니다.


int atexit(
   void (__cdecl *func )( void )
);


return type과 parameter가 모두 void인 함수 포인터를 전달받는 함수이며, 종료할 때 지정된 함수를 처리한다고 합니다. 또한 해당 API는 최대 32개까지 추가할 수 있으며 LIFO 형태로 마지막에 들어온 함수가 가장 먼저 실행된다.


소멸 코드를 추가한 Singleton Pattern


class singleton {
private:
    static singleton* _instance;

    singleton() {}
    singleton(const singleton& other);
    ~singleton() {}
public:
    static singleton* instance() {
        if (_instance == nullptr) {
            _instance = new singleton();
            atexit(release_instance);
        }
        return _instance;
    }
    static void release_instance() {
        if (_instance) {
            delete _instance;
            _instance = nullptr;
        }
    }
};


위에꺼보단 조금 더 다듬어진 코드가 완성되었습니다.






그렇다면 이렇게 생각해볼수도 있습니다.

왜 포인터로 만들어서 궂이 해제까지 하는 귀찮은 작업을 하지..? 그냥 static 멤버로 만들면 안되나?


why static?

* class에서 static member로 생성되면 class 객체가 몇개가 생성되던간에 1개의 member만 생성된다고 합니다.


그러면 궂이 memory leak 걱정도 안해도 되고 좋을 것 같네요!


class singleton {
private:
    static singleton _instance;

    singleton() {}
    singleton(const singleton& other);
    ~singleton() {}
public:
    static singleton& instance() {
        return _instance;
    }
};


오.. 뭔가 좀 더 간단해진 느낌이네요!


이렇게 하면 소멸자도 자동으로 호출 되겠네요


그런데 위 방법은 문제가 있다고 합니다.

Static class member 변수는 static 전역 변수와 마찬가지로 프로그램 시작시 main() 함수 호출 이전에 초기화가 된다 합니다. 그렇기때문에 만약 해당 클래스를 사용하지 않더라도 무조건 생성이 되는 현상이 발생되어 때에 따라서는 비효율적입니다.

그리고 때에 따라서 다른 전역 객체의 생성자에서 해당 member를 참조하고싶을수도 있을 때 문제가 발생할 수 있습니다.

바로 C++에선 전역 객체들의 생성 순서에 대해서 명확하게 정의하고 있지 않기 때문이죠

singleton class의 Instance는 아직 생성되지 않았는데 다른 전역 객체에서 instance를 참조를 하게되면 문제가 발생하겠죠


자 그럼 간단하게 하면서 어떻게 이 문제를 해결해볼까요?


아! member static을 지역 static으로 바꾸면 처음 함수가 호출될 때 생성되니까 어디서 접근을 시도하든 상관이 없겠네요!

* class에서 static  member 는 전역 객체와 같이 동작하지만 함수 내에 static 객체는 해당 함수가 호출되는 시점에 초기화가 됩니다.


class singleton {
private:
    singleton() {}
    singleton(const singleton& other);
    ~singleton() {}
public:
    static singleton& instance() {
        static singleton _instance;
        return _instance;
    }
};


단순히 member의 위치만 바뀌었는데 조금 더 안전한 코드가 되었다고 합니다.

* global static 객체와 Local static 객체의 차이가 이런 결과를 만들었네요.


하지만 위의 방법도 한가지 문제를 떠앉고있습니다.

바로 소멸자에서 해당 인스턴스를 참조하려 했을 경우 입니다.

C++에선 전역 객체의 생성 순서뿐만 아니라 소멸 순서에 대해서도 명확하게 정의하고 있지 않기때문에 이번엔 소멸시점에서 instance가 먼저 소멸해버렸다면.. 역시 똑같은 문제가 발생하게 됩니다.


Singleton pattern 특성상 완벽한 방법은 없지만 조금이라도 안전한 방법으로 구현해야하는건 맞는 것 같습니다.


하지만 앞서 얘기한바와 같이 Singleton Pattern은 아직도 문제를 가지고있습니다.

그중 한가지가 멀티쓰레드 환경에서 단일 인스턴스 생성이 보장 되지 못한다는 부분입니다. (thread safe issue)

이를 조금이나마 해소할 수 있도록 나온 기법이 DCLP (Double checked Locking Pattern) 이며,

DCLP를 적용하여 multi thread 환경에서 조금이나마 더 안전한 Singleton Pattern을 생성할 수 있는 방법은 다음에 소개하도록 하겠습니다.


반응형