개발을 하다보면 프로그램 내에서 단 하나의 인스턴스만을 구성해야 하는 순간을 맞이하게 된다.
간단하게 생각해보면 crtl+alt+delete를 통해 작업관리자만 켜봐도 알 수 있다. 작업관리자는 굉장히 많은 프로세스들을 관리하며 이를 종료하거나 다시 실행시킬 수 있다. 근데 기를 쓰고 해봐도 두개의 작업관리자가 켜지는 일은 없을 것이다.
만약 킬때마다 이 작업관리자의 ID가 달라지고, 관리하는 데이터도 새로 받는다면? 더군다나 자원충돌이나 데이터의 일관성에 문제가 생길 수 있다는걸 쉽게 짐작 할 수 있다.
예시로 1번 작업관리자와 2번 작업관리자가 있는데, 1번에서 종료한 프로세스는 2번에서도 종료되어야 한다.
두개의 인스턴스가 시스템의 목록을 각각 가져오고 관리한다. 이 문장에서 벌써 엄청나게 불필요한 메모리 낭비와 시스템 부하가 발생함을 알 수 있다.
불필요한 사본 데이터가 메모리에 계속 존재하는건 개발자로써 끔찍함을 느낄것이다.
1번에서 종료했는데 2번에서 종료되지 않았다면, 생각만 해도...
이는 게임개발에서도 마찬가지이다.

두 개의 환경설정 창이 켜질 수 있다면.
예시로 환경설정에서 사운드를 줄였는데, 다른 하나의 설정환경창에는 반영되지 않는다면?
관리해야 될 일관성 문제가 한두가지가 아닐것이다.
우리는 앞선 두 개의 케이스를 통해, 만약 어떤 관리자 클래스가 있다면 단 하나만 존재하는게 여러 측면에서 유리함을 직관적으로 알 수 있을 것이다.
이러한 객체의 생성과 소멸을 통제하기 위해 만든 디자인 패턴이 바로 싱글톤이다.
싱글톤 패턴은, 클래스의 인스턴스가 오직 하나만 생성되도록 보장한다. 더불어서 이 인스턴스는 언제 어디서든 접근할 수 있어야 한다.
위 글에서 볼 수 있듯이 하나만 생성해야 한다는 것은 알겠는데 왜 어디서든 접근할 수 있어야 하나?에 대한 질문을 던질 수 있다. 이러한 의문을 가진채로 코드를 살펴보자.
class InputManager
{
public:
bool GetKey(int key) { ... }
};
void Player::Update()
{
InputManager input1; // 인스턴스 1
if (input1.GetKey(KEY_W))
Move();
}
void Enemy::Update()
{
InputManager input2; // 인스턴스 2
if (input2.GetKey(KEY_A))
Turn();
}
벌써 보이는 문제점이 한 두가지가 아니다.
우리는 Input을 관리하는 InputManager클래스를 만들었지만 각 객체마다 인스턴스를 던져두고 해당 인스턴스는 어딘가의 메모리에 계속 저장될 것이다.
이를 정리하면 다음과 같다.
자 그럼 싱글톤을 사용한 코드를 살펴보자.
class InputManager
{
DECLARE_SINGLE(InputManager); // 흔히 사용되는 싱글톤 매크로
public:
bool GetKey(int key) { ... }
};
void Player::Update()
{
if (INPUT->GetKey(KEY_W)) // Singleton객체(Manager)는 보통 대문자로 관리한다.
Move();
}
void Enemy::Update()
{
if (INPUT->GetKey(KEY_A)) // 같은 INPUT 인스턴스
Turn();
}
앞선 1번 2번 3번의 모든 문제점이 해결된다.
싱글톤을 구현하는데 대표적인 두가지가 있다.
Eager Initialization : 프로그램이 시작할때 미리 생성해둔다.
-> 간단하며 잘못된 접근을 할 필요가 없기에, 스레드측면에서 안전하다.
Lazy Initialization : 인스턴스가 실제로 필요할 때 최초 생성한다.
-> 호출되지 않는 매니저가 있는 케이스의 경우 자원절약에 유리하다. 다만 스레드 안전성에서는...
매크로를 통해 싱글톤에 익숙해져보자.
// Define.h
#define DECLARE_SINGLE(Type) \
private: \
Type() {} \
~Type() {} \
public: \
static Type* GetInstance() \
{ \
static Type instance; \
return &instance; \
}
#define GET_SINGLE(Type) Type::GetInstance()
//GET_SINGLE을 매크로로 정의하면 더 쉽게 사용할 수 있다.
//매크로에 익숙하지 않다면 InputManager를 예시로 든 아래 코드를 참조.
class InputManager
{
DECLARE_SINGLE(InputManager);
private:
InputManager() {}
~InputManager() {}
//생성 소멸을 private로 선언하였기에 외부에선 이를 생성해내지 못한다.
public:
static InputManager* GetInstance()
{
static InputManager instance; // static 특성상 GetInstance는 최초 한번만 실행
return &instance;// 위의 static Manager가 생성되던 말던, Get은 유효하다.
}
};
보통 싱글톤 패턴은 위처럼 매크로와 생성자 함수를 define.h파일에 저장해둔다. 해당 매크로(DECLARE_SINGLE(TYPE))을 우리가 부를 Manager에 등록하기만 하면 이를 static처럼 단 하나의 변수로써 활용할 수 있다.
//InputManager.h
class InputManager{
DECLARE_SINGLE(InputManager);
...
}
이렇게 열심히 떠들긴 했으나, 개발에 정답은 존재하지 않는다고 생각한다.
설계는 주어진 환경, 퍼포먼스, 그리고 팀의 협업 방식에 따라 고려하는 방향이 달라지기 때문이다.
다만 디자인 패턴은 옛부터 개발자가 헤딩하며 발견한 가장 효율적인 문제해결 방식에 대한 집합이다.
그 중에서 이 싱글톤 패턴이 게임개발과 같은 동적이고, 자원을 활용하는 프로젝트에 있어 유난히 자주 활용된다는 사실을 기억해야 한다.
마지막으로 싱글톤의 목적에 대해 다시 한번 정리하며 포스팅을 마친다.
싱글톤의 목적