gimmesilver's blog

Agbird.egloos.com

포토로그



C++에서 싱글톤 구현하기 프로그래밍

 작년에 다른 팀에 면접 지원을 나간적이 있습니다. 윈도우 프로그래밍 경력자를 뽑고 있었는데 그 팀에는 윈도우 프로그래밍 경험을 가지신 분들이 없었기 때문이죠. 면접을 위해 윈도우 프로그래밍과 C++ 문법, 그리고 알고리즘 질문을 각각 준비했었는데 그 중 C++ 언어 관련 질문으로 제가 준비한 것은 다음과 같습니다.
C++ 에서 싱글톤 패턴을 구현하는 방법들을 아는데로 나열하고 각각의 장/단점을 말해보세요.
 전 이전 회사에서부터 면접 때 항상 이 질문을 하곤 했습니다. 왜냐하면 싱글톤을 구현하는 방법에는 C++ 에서 필수적으로 알아야 하는 생성/소멸자, 권한, static의 특성 등 기본적인 문법 사항을 고루 담고 있기 때문입니다. 그런데 비교적 해묵은 주제임에도 불구하고  면접을 보신 분 중 한 분도 제대로 대답을 못해 좀 의외였습니다.  따라서 한번 쯤 공유차원에서 정리해봐야겠다고 벼르고 있었는데 생각난 김에 지금 정리해 봅니다.

 C++ 에서 싱글톤을 구현하는 방법에는 우선 다음과 같은 방법이 있습니다.

// .h
class Singleton {
  private:
    Singleton() {}
    Singleton(const Singleton& other);
    static Singleton inst;
  public:
    static Singleton& getInstance() { return inst; }
};

// .cpp
Singleton Singleton::inst;

 위처럼 생성자를 private으로 하고 static 멤버 변수를 하나 생성해서 그 객체를 반환하도록 하면 외부에서는 해당 전역 객체만을 참조할 수 있습니다. 간단하지요...클래스 접근 권한과 클래스 내에서의 static 지시 한정자의 역할을 이해하고 있다면 충분히 구현할 수 있는 방법입니다.

 그런데 위 방식은 단순한 반면 몇 가지 단점이 있습니다. static 클래스 멤버 변수는 static 전역 변수처럼 프로그램 시작 시 main() 함수 호출 이전에 초기화됩니다.  따라서 위 객체는 만약 프로그램의 진행 상황에 따라 필요가 없는 경우에도 무조건 생성되기 때문에 때에 따라서 비효율적입니다.
 게다가 위와 같은 정적 객체는 다른 전역 객체의 생성자에서 참조하고 싶은 경우 문제가 발생할 수 있습니다. 왜냐하면 C++표준에서는 전역 객체들의 생성 순서에 대해서 명확하게 정의하고 있지 않기 때문입니다. 그저 main() 함수가 실행하기 전에만 생성되면 될 뿐입니다. 따라서 어떤 전역 객체의 생성자에서 위 싱글톤 객체를 참조하려고 하는 경우 싱글톤 객체가 미처 생성되기 전인 경우가 발생할 수 있습니다. 결국 객체의 생성 시점을 조절할 필요가 있죠.
 아마 effective 시리즈 류의 책을 보신 분들이라면 늦은 초기화에 대해 들어 보셨을 겁니다. 위의 문제점을 피하기 위해선 늦은 초기화 방법을 사용해 다음과 같이 동적 생성을 하면 됩니다.

// .h
class DynamicSingleton {
  private:
    DynamicSingleton() {}
    DynamicSingleton(const DynamicSingleton& other);
    ~DynamicSingletone() {}    // 외부에서 싱글톤 객체를 강제 delete 하는 것을 막기 위해 필요함
    static DynamicSingleton* inst;
  public:
    static DynamicSingleton* getInstance() {
      if (inst == 0) inst = new DynamicSingleton();
      return inst;
    }
};

// .cpp
DynamicSingleton* DynamicSingleton::inst;

 이렇게 하면 최초 getInstance()를 호출하는 시점에 객체가 생성되므로 상황에 따라(한번도 해당 객체를 사용하지 않으면) 생성이 되지 않기 때문에 자원을 효율적으로 사용할 수 있을 뿐더러 물론 다른 전역 객체의 생성자에서 참조하는 것도 가능합니다.
 여기서 '동적 생성한 객체는 그럼 언제 해제하나요?' 라는 질문을 던질 수 있습니다. 그러나 프로그램이 종료되는 순간 동적 객체는 자동으로 해제되기 때문에 굳이 명시적으로 해제할 필요가 없습니다. 메모리 릭 문제는 지속적으로 메모리 할당이 일어나는데 해제는 안되는 상황에서 발생하는 문제이지 이 객체처럼 한번만 생성되어 프로그램 종료 시까지 유지되는 객체는 문제가 되지 않습니다.
 물론 명시적으로 해제해야 하는 경우도 있습니다. 가령 위 객체가 반드시 프로그램 종료 시 반납해야 하는 외부 시스템 자원을 사용하는 경우가 그렇습니다. 이를 위해서는 atexit() 함수에 해제 함수를 등록하거나 혹은 다른 전역 객체의 소멸자를 이용해야 합니다. 각각의 구현 방법은 아래와 같습니다.

// atexit() 이용 방법
class DynamicSingleton {
    ...
  private:
    static void destroy() { delete inst; }
  public:
    static DynamicSingleton* getInstance() {
      if (inst == 0) {
       inst = new DynamicSingleton();
       atexit(destroy);
     }
      return inst;
    }
};

// 전역 객체의 소멸자 이용 방법
// .h
class _SingletonDestroyer;
class DynamicSingleton {
    ...
    friend _SingletonDestroyer;
};

// .cpp
static class _SingletonDestroyer {
  public:
    ~_SingletonDestroyer() {
      delete DynamicSingleton::getInstance();
    }
} destroyer;

보시다시피 좀 귀찮습니다. 따라서 이런 명시적인 해제 작업을 피하기 위해서는 static 지역 객체를 사용하면 됩니다. 방법은 아래와 같습니다.

class LocalStaticSingleton {
  public:
    static LocalStaticSingleton& getInstance() {
      static LocalStaticSingleton inst;
      return inst;
    }
  private:
    LocalStaticSingleton() {}
    LocalStaticSingleton(const LocalStaticSingleton& other);
};

 지역 static 객체는 전역 객체와 달리 해당 함수를 처음 호출하는 시점에 초기화됩니다. 따라서 위 객체를 한번도 사용하지 않으면 생성도 되지 않습니다. 그러면서도 static 객체이기 때문에 프로그램 종료 시까지 객체가 유지되며 종료시에는 자동으로 소멸자가 호출됩니다. 따라서 소멸자에서 자원 해제를 하도록 구현해놓으면 자원 관리도 신경쓸 필요가 없습니다.  

 하지만 위 세번째 구현에도 문제가 하나 있습니다. 만약 저 싱글톤 객체를 다른 전역 객체의 소멸자에서 사용하려고 하면 문제가 발생합니다. 왜냐하면 C++ 표준에서는 전역 객체들의 생성 순서만 명시하지 않은 것이 아니라 소멸 순서에 대해서도 명시해 놓지 않았기 때문입니다. 따라서 어떤 전역 객체가 소멸자에서 저 싱글톤 객체를 사용하려고 할 때 싱글톤 객체가 먼저 소멸했다면(이것을 참조 무효화 현상이라고 합니다) 문제가 발생합니다.

 이 문제를 해결하기 위해선 다소 고난이도 방법이 필요합니다. 그 중 재밌는 것이 Andrei Alexandrescu가 쓴 Modern C++ Design 이라는 책에 나오는 피닉스 싱글톤입니다. 이 싱글톤은 우선 싱글톤 참조 시 해당 객체의 소멸 여부를 파악하고 만약 소멸되었다면 다시 되살립니다. 구현 코드는 아래와 같습니다.

// .h
class PhoenixSingleton {
  public:
    static PhoenixSingleton& getInstance() {
      if (destroyed) {
        new(pInst) PhoenixSingleton; // 2)
        atexit(killPhoenix);
        destroyed = false;
      } else if (pInst == 0) {
        create();
      }
      return *pInst;
    }
  private:
    PhoenixSingleton() {}
    PhoenixSingleton(const PhoenixSingleton & other);
    ~PhoenixSingleton() {
      destroyed = true;  // 1)
    }

    static void create() {
      static PhoenixSingleton inst;
      pInst = &inst;
    }

    static void killPhoenix() {
      pInst->~PhoenixSingleton();  // 3)
    }

    static bool destroyed;
    static PhoenixSingleton* pInst;
};

// .cpp 
bool PhoenixSingleton::destroyed = false;
PhoenixSingleton* PhoenixSingleton::pInst = 0;

 갑자기 굉장히 복잡해졌는데 핵심만 간단히 설명하자면(자세한 내용은 위에 소개한 책을 참조하세요) 정적 객체가 소멸되면 1) 소멸자에 의해 destroyed 변수가 true가 되면서 소멸 여부를 알 수 있습니다. 그리고 소멸 후에 getInstance() 함수를 통해 해당 객체를 참조하려 하면 2) replacement new 를 이용해서 해당 객체의 생성자를 재호출해서 객체를 되살립니다. 이것이 가능한 이유는 컴파일러는 전역 객체 소멸 시에 해당 메모리를 초기화하지 않기 때문에 해당 메모리를 재 사용해서 객체의 생성자만 다시 호출하면 객체를 재 사용할 수 있기 때문입니다. 그 후 atexit() 함수에 killPhoenix() 함수를 등록해서 3) 프로그램 종료 시에 PhoenixSingleton 객체의 소멸자를 호출해서 리소스 해제를 합니다.

 물론 마지막에 소개한 PhoenixSingleton 방법은 상당히 tricky 하며 실제로는 거의 쓸일이 없습니다. 제 경우는 예전에 어떤 윈도우용 프로그램에서 딱 한번 어쩔 수 없이 사용했습니다. 실제 중요한 것은 static 객체의 생성/소멸 시점에 대해 정확히 파악해서 싱글톤 객체를 전역 객체의 생성/소멸자에서 마구잡이로 참조하는 일이 없도록 주의해서 프로그래밍하는 것입니다.

p.s. 물론 구두 면접에서 이 정도까지 상세한 답을 기대하진 않았습니다...
p.p.s. 역시나 실전에 별 쓸일은 없지만 난이도 있는 다른 문제를 하나 내보겠습니다. C++에서는 자바의 final 처럼 상속을 막는 키워드가 아쉽게도 없습니다. 그렇다면 C++에서는 클래스의 상속을 막기 위해서 어떤 방법을 사용할 수 있을까요? 힌트는 위의 코드들에 나온 문법 중에 하나를 사용하면 된다는 것입니다.

핑백

  • gimmesilver's blog : 지난 번에 낸 퀴즈 답 2009-01-24 11:12:27 #

    ... C++에서 싱글톤 구현하기</a> 글에서 낸 '상속이 불가능한 클래스 만들기' 는 가장 쉽게 떠올릴 수 있는 게 생성자를 private 으로 만들고 대신 펙토리 메소드를 제공하는 것입니다. 아마 많은 분들이 이 방법을 떠올리셨을 거라 생각합니다.class Test { private: Test() {} public: static Test* getInst ... more

  • 복군 : Pattern : Singleton 2011-08-09 14:18:22 #

    ... 77 참고 - http://kldp.org/node/114632 - http://agbird.egloos.com/4730538 ... more

덧글

  • 처로 2009/01/20 11:19 # 삭제 답글

    실제 더 중요한 것은 가급적 static/singleton 객체를 사용하지 않는 것이라고 생각합니다. 어쩔 수 없이 쓸 경우는 singleton holder 와 같이 global 을 관리하는 단 하나의 singleton 만을 두고 그 안에서 나머지들을 control 하도록 하는게 좋습니다. 물론... 가져다 쓰는 라이브러리나 모듈에서도 static이나 singleton등이 있는 경우는 머리가 아파지지만.. -_ㅜ

    마지막 문제의 답은 비공개로 다시 -_-
  • 2009/01/20 11:19 # 삭제 답글 비공개

    비공개 덧글입니다.
  • 김형섭 2009/01/23 13:13 # 삭제 답글

    저도 싱글톤객체를 지양하는 편입니다..
    확실히 생성할 때와 소멸할 때를 파악해서, 명시적으로 누가봐도 또렷하게 코딩을 해야 한다고 생각하는 1人.. ^^

    상속을 막으려면.. 생성자에 접근을 못하게?
    그렇다면.. 반드시 상속해서 사용하게 하려면 어떻게 해야할까요?? ㅎ
  • 몽상가 2009/02/11 01:35 # 삭제

    반드시 상속을 해야되는 경우는 단순히 순수 가상함수를 만들어버리면 될것같습니다. 더미 함수던 그냥 함수던이요.. 가장 확실한 방법이 아닐까요..-_-;


    그리고 문제에대한 답변이..
    private 으로 생성또는 소멸자를 만들게되면, 상속을 받거나 heap 이 아닌 자동변수로 생성하는 것 이 일반적으로 방지됩니다.

    그리고 위와 같은 방법으로 상속을 받게 되면 컴파일 에러가... 그 에러를 클릭하면 왜 에러가 뜨게되는지 에 대한 주석을 달아놓으면 좋겠죠.
    위 내용들을 more effective C++ 본기억이 나는데 가물가물..
  • 울레리오 2009/02/09 11:25 # 삭제 답글

    잘봤습니다~^^
  • Kamu 2009/02/17 18:36 # 삭제 답글

    c++을 공부하고 있는 학생입니다..

    좋은글 정말 잘봤습니다..

    한가지 궁금한게 있는데 동적으로 생성된 싱글톤객체의 해제에 관한 부분입니다.

    첫번째방법 (atexit를 이용한..) 에서 destroy를 private로 선언한건 어떤 이유인지 궁금합니다..

    명시적으로 해제할 필요성이 있다면 destroy를 public으로 선언하여 사용하면 문제가 생길 요지가 있는건가요?
    (평소 이방법으로 싱글톤패턴을 구현해 왔습니다만;;)

    굳이 두번째방법 - friend클래스를 통해 해제하는 이유가 궁금합니다.;;
  • silverbird 2009/02/17 20:04 #

    1. 싱글톤 객체의 소멸자를 private으로 하지 않으면 실수로 혹은 악의적으로 객체를 프로그램 중간에 해제할 수 있기 때문입니다. 그러면 해제 이후에 다시 객체를 참조하려고 하면 오류가 발생하겠죠. 싱글톤은 생성과 소멸 시기를 클래스 작성자가 의도한 시점 외에는 허가하지 않는 것이 중요합니다.
    2. 비슷한 이유입니다. 외부에서 참조가 불가능한 내부 static 클래스 객체의 소멸자에서만 싱글톤 객체를 해제할 수 있도록 하기 위함입니다.
  • Kamu 2009/02/17 20:40 # 삭제 답글

    제가 원문에 대한 이해가 부족했던거 같습니다..

    덕분에 정확히 이해할수 있게 된것같네요 ^^;

    답변 감사드립니다
  • yalu 2009/03/13 10:30 # 삭제 답글

    피닉스 싱글톤은 스터디를 하면서 얻은 결론이...무용이었습니다...

    실제로 소멸뒤 필요에 의해 재생성을 하지만...재생성된 포인터는 기존의 포인터가 아닙니다...

    단지 같은 형의 포인터를 재생성해서 크리티컬한 상황만을 피할뿐 실제적인 재현이 되지 않기에...결론은 무용이었습니다...

  • silverbird 2009/03/15 21:35 #

    잘못 알고 계시는 군요. 피닉스 싱글톤은 동일한 메모리를 replacement new 를 사용해서 재할당(엄밀히 말하면 생성자만 재 호출)하기 때문에 기존의 포인터와 동일한 메모리 상태값을 갖습니다.
  • 음... 2009/03/13 11:26 # 삭제 답글

    2에서 생성된 메모리를 3에서 delete 해주지 않아도 되나요?
    기존 pInst가 있던 주소에 그대로 new 하니까 알아서 해제되는건지?

  • silverbird 2009/03/15 21:36 #

    피닉스 싱글톤은 동적 할당된 객체를 재할당하는 것이 아니라 정적 메모리에 생성된 객체에 대해서 생성자만 다시 호출하는 것이기 때문에 명시적으로 메모리를 해제하지 않고 단지 소멸자만 호출합니다.
  • 2009/04/30 03:51 # 삭제 답글

    안녕하세요. 여기저기 프로그래밍 관련 글을 읽다가 온 대학생입니다.
    저는 게임프로그래밍을 전공하고 있는데 약간은 글 내용과 다른 질문을 드리고 싶습니다.
    // .h
    class DynamicSingleton {
    private:
    DynamicSingleton() {}
    DynamicSingleton(const DynamicSingleton& other);
    ~DynamicSingletone() {} // 외부에서 싱글톤 객체를 강제 delete 하는 것을 막기 위해 필요함
    static DynamicSingleton* inst;
    public:
    static DynamicSingleton* getInstance() {
    if (inst == 0) inst = new DynamicSingleton();
    return inst;
    }
    };


    // .cpp
    DynamicSingleton* DynamicSingleton::inst;

    작년 싱글턴을 공부 할때 제가 작성 했던 소스와 비슷해서 반가웠습니다. ( 전 소멸자를 따로 지정하지 않았습니다. )
    면접을 보신다고 하셨는데 이정도 소스를 몇 점을 주실지 궁금합니다.

    그리고 알고리즘 질문은 별로 공부를 하지 않았는데 어떤 내용인지 궁금합니다.
  • silverbird 2009/04/30 09:30 #

    위 글에서도 언급했듯이 질문의 요지는 싱글톤 클래스 결과물 자체가 아니라 다양한 구현 방법과 장/단점을 알고 있는가 였습니다.
    어쨌든 작성하신 소스로 보아서 C++ 기본 문법에 대해서는 잘 이해하고 계신것 같습니다.
    세부적으로 점수를 매길 정도로 복잡한 내용이 아니라 점수는 매기기는 좀....^^;
    그리고 알고리즘 질문은 기본적인 이론을 묻는 질문이었습니다. 예를 들어 해쉬와 B-트리의 차이점과 장/단점, 대용량 데이터를 정렬하기 위한 방법 등을 물어봤었습니다.

  • 2009/05/02 06:54 # 삭제 답글

    제가 작성한게 아니라 실버버드님께서 작성한 것을 그냥 붙여 넣기 했습니다.
    작년에 컴퓨터가 사망하셔서 기록이 없어서입니다. ㅠㅠ
  • 2009/07/01 11:22 # 삭제 답글 비공개

    비공개 덧글입니다.
  • 그린티아 2010/03/23 23:06 # 삭제 답글

    좋은내용 감사합니다.. ^^ 싱글톤 공부중인데 내용이 많이 참고되었습니다.
    제 블로그로 출처 남기고 담아가겠습니다.
    문제시 블로그에 글 남겨주세요~
  • letburn 2011/02/08 10:43 # 삭제 답글

    많이 공부가 되었습니다
    좋은 내용에 감사드립니다~
  • 신도세카이 2011/05/27 16:36 # 삭제 답글

    감사합니다.
    싱글톤 공부중에 많은 참고가 되었습니다.

    블로그에 출처를 남기고 퍼가겠습니다.
    만약 퍼간것이 마음에 드시지 않을 경우엔 글 남겨주시면 바로 비공개 처리 하겠습니다.
  • 이경문 2012/04/17 21:07 # 삭제 답글

    안녕하세요, 구글에서 검색을 통해서 들어왔습니다. 좋은 글 감사합니다. 써 놓으신 글에서 약간 사실과 다른 점이 있어서 말씀을 드릴까 합니다.

    > static 클래스 멤버 변수는 static 전역 변수처럼 프로그램 시작 시 main() 함수 호출 이전에 초기화됩니다.

    이것을 가지고 예전에 심도있게 디버깅을 해 본적이 있어서 링크를 남겨 봅니다. 클래스 멤버의 static object는 lasy initialization을 하고 있음을 알 수가 있습니다.
    http://www.gilgil.net/9362

    이 사실을 이용해서 lazy initialization을 하는 singleton template(VSingleton) 및 빠른 초기(non-lazy initialization)화를 하는 singleton(VGSingleton)을 구분해 만들어 놓았으니 참고하시기 바랍니다.
    http://www.gilgil.net/15462
  • gimmesilver 2012/04/18 14:09 #

    static 지역 객체와 static 전역 객체를 혼동하고 계시군요.
    링크 걸어주신 글에서 global singleton object는 static 전역 객체이고 static singleton object라고 설명하신 부분은 static 지역 객체입니다.
    말씀하신 늦은 초기화는 static 지역 객체에만 해당합니다. 그리고 이 것은 제 글에서 세번째로 설명한 방식입니다.
    static 전역 객체는 제가 설명했듯이 main() 함수 호출 이전에 초기화됩니다.
  • 이경문 2012/04/18 20:05 # 삭제 답글

    네, 제가 잘못 이해하고 있었네요. 죄송합니다. 꾸뻑.

    static member object or (static) global object : non-lazy initialization
    static local object : lazy initialization

    이렇게 정리를 하면 되겠네요. ^^
    오랜만에 코드로 머리 좀 썼네요. 좋은 지적 감사합니다. ^^
  • 이경문 2012/04/20 22:21 # 삭제 답글

    "C++에서 클래스 상속을 방지하는 방법"으로 글을 작성해 보았습니다.
    생각보다 고려해야 할 것들이 많더군요. ^^

    http://www.gilgil.net/15819
  • Ramyen 2012/07/11 19:03 # 답글

    좋은 글 보고 갑니다.
    면접 준비하고 있었는데, 많은 도움이 된 것 같습니다.
    아주 조용히 자주 들락날락 하겠습니다. ^^
  • 구름과비 2013/11/02 08:07 # 삭제 답글

    위의 코드들도 multi-thread 환경에서는 문제가 있을 것 같습니다.
    instance 가 생겼는지를 체크하는 코드에 다수의 thread 가 동시에 진입했을 때
    자칫 다수의 객체가 생성될 여지가 있습니다.
    Java 라면 syncronized 를 사용하면 되지만 c++ 에서는 mutex 나 spinlock 등 별도의 줄세우기 알고리즘을 구현해야 합니다.
  • gimmesilver 2013/11/02 23:44 #

    말씀하신대로 위 코드는 멀티스레드 환경을 고려한 코드는 아닙니다.
    면접에서 싱글톤 문제를 통해 알고 싶었던 것은 글 처음에도 나와 있듯이 C++ 문법 및 실행 매커니즘을 잘 이해하고 있는가 였습니다.
    물론 멀티쓰레드까지 고려한 코드를 작성한다면야 금상첨화겠죠 ^^
  • 멋진넘 2014/01/22 17:16 # 삭제 답글

    싱글톤에 대해서는 그래도 제일 쉽게 설명이 되어 있는 글이네요.
    우연히 들어왔지만 잘 봤습니다. ^^
댓글 입력 영역