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++에서는 클래스의 상속을 막기 위해서 어떤 방법을 사용할 수 있을까요? 힌트는 위의 코드들에 나온 문법 중에 하나를 사용하면 된다는 것입니다.

아이온 2.5 업데이트 주신의 부름
아이온 2.5 월페이퍼 다운로드
plaync 아이온 더보기


카스토르 / 신도세카이

어제 부산가서 블앤소를 보고, 시연까지 해보았습니다.
아... 이것은 정말 말이 제대로 안나오는 놀랍고 신기하고 멋있고 재미있는 대작이 하나 나왔구나 하는
기분이 들더군요... 정말 놀라운 경이로움 그 자체였습니다.

물론... 그거 하나 시연해볼려고 3시간 서있다가 20분정도밖에 못했다는게 너무 아쉬웠으나....

정말 놀랍기만한, 그런 게임이었습니다.
기본적으로 R키로 평타를 치고 게이지를 모아서 F나 1234등으로, 연계기, 스턴, 넉백, 띄워올리기를 사용하며,
오토타겟팅된 적의 상황에 따라, 자동으로 사용 스킬이 바뀌고, 상대가 넉백등이 되었을때,
탭키를 누르면 올라타서 때린다던가 하는게 영상으로만 가능한것이 아닌 실제 플레이로 가능하다는것이...
정말 놀라웠습니다.
정말 기대됩니다! 블레이드 앤 소울 앞으로가 기대됩니다.

밑에는 가서찍은 사진, 동영상이 첨부되어있습니다

Ps. 가방이 너무 가로로 길어.......

↓움직이는 블앤소 글씨 벽!!

↓ 블앤소 플레이 영상 

0123456789