C++ 에서 싱글톤 패턴을 구현하는 방법들을 아는데로 나열하고 각각의 장/단점을 말해보세요.전 이전 회사에서부터 면접 때 항상 이 질문을 하곤 했습니다. 왜냐하면 싱글톤을 구현하는 방법에는 C++ 에서 필수적으로 알아야 하는 생성/소멸자, 권한, static의 특성 등 기본적인 문법 사항을 고루 담고 있기 때문입니다. 그런데 비교적 해묵은 주제임에도 불구하고 면접을 보신 분 중 한 분도 제대로 대답을 못해 좀 의외였습니다. 따라서 한번 쯤 공유차원에서 정리해봐야겠다고 벼르고 있었는데 생각난 김에 지금 정리해 봅니다.
Singleton(const Singleton& other);
static Singleton inst;
게다가 위와 같은 정적 객체는 다른 전역 객체의 생성자에서 참조하고 싶은 경우 문제가 발생할 수 있습니다. 왜냐하면 C++표준에서는 전역 객체들의 생성 순서에 대해서 명확하게 정의하고 있지 않기 때문입니다. 그저 main() 함수가 실행하기 전에만 생성되면 될 뿐입니다. 따라서 어떤 전역 객체의 생성자에서 위 싱글톤 객체를 참조하려고 하는 경우 싱글톤 객체가 미처 생성되기 전인 경우가 발생할 수 있습니다. 결국 객체의 생성 시점을 조절할 필요가 있죠.
아마 effective 시리즈 류의 책을 보신 분들이라면 늦은 초기화에 대해 들어 보셨을 겁니다. 위의 문제점을 피하기 위해선 늦은 초기화 방법을 사용해 다음과 같이 동적 생성을 하면 됩니다.
DynamicSingleton() {}
DynamicSingleton(const DynamicSingleton& other);
~DynamicSingletone() {} // 외부에서 싱글톤 객체를 강제 delete 하는 것을 막기 위해 필요함
}
여기서 '동적 생성한 객체는 그럼 언제 해제하나요?' 라는 질문을 던질 수 있습니다. 그러나 프로그램이 종료되는 순간 동적 객체는 자동으로 해제되기 때문에 굳이 명시적으로 해제할 필요가 없습니다. 메모리 릭 문제는 지속적으로 메모리 할당이 일어나는데 해제는 안되는 상황에서 발생하는 문제이지 이 객체처럼 한번만 생성되어 프로그램 종료 시까지 유지되는 객체는 문제가 되지 않습니다.
물론 명시적으로 해제해야 하는 경우도 있습니다. 가령 위 객체가 반드시 프로그램 종료 시 반납해야 하는 외부 시스템 자원을 사용하는 경우가 그렇습니다. 이를 위해서는 atexit() 함수에 해제 함수를 등록하거나 혹은 다른 전역 객체의 소멸자를 이용해야 합니다. 각각의 구현 방법은 아래와 같습니다.
// atexit() 이용 방법
class DynamicSingleton {
...
private:
static void destroy() { delete inst; }
inst = new DynamicSingleton();
atexit(destroy);
}
}
// 전역 객체의 소멸자 이용 방법
// .h
class _SingletonDestroyer;
class DynamicSingleton {
...
friend _SingletonDestroyer;
};
// .cpp
static class _SingletonDestroyer {
public:
~_SingletonDestroyer() {
delete DynamicSingleton::getInstance();
}
} destroyer;
보시다시피 좀 귀찮습니다. 따라서 이런 명시적인 해제 작업을 피하기 위해서는 static 지역 객체를 사용하면 됩니다. 방법은 아래와 같습니다.
LocalStaticSingleton(const LocalStaticSingleton& other);
};
이 문제를 해결하기 위해선 다소 고난이도 방법이 필요합니다. 그 중 재밌는 것이 Andrei Alexandrescu가 쓴 Modern C++ Design 이라는 책에 나오는 피닉스 싱글톤입니다. 이 싱글톤은 우선 싱글톤 참조 시 해당 객체의 소멸 여부를 파악하고 만약 소멸되었다면 다시 되살립니다. 구현 코드는 아래와 같습니다.
// .h
class PhoenixSingleton {
public:
if (destroyed) {
new(pInst) PhoenixSingleton; // 2)
atexit(killPhoenix);
destroyed = false;
} else if (pInst == 0) {
create();
}
PhoenixSingleton(const PhoenixSingleton & other);
~PhoenixSingleton() {
destroyed = true; // 1)
}
static void create() {
}
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++에서는 클래스의 상속을 막기 위해서 어떤 방법을 사용할 수 있을까요? 힌트는 위의 코드들에 나온 문법 중에 하나를 사용하면 된다는 것입니다.
RECENT COMMENT