
컴퓨터공학에서 용어를 정의할 때는 보통 그 개념을 가장 잘 설명할 수 있는 단어를 쓰려고 한다. 디자인 패턴도 마찬가지다. 처음 보면 막막하고 헷갈리기 쉬운데, 이름과 역할을 연관지어 생각하면 훨씬 쉽게 이해된다.
Singleton Pattern은 말 그대로 프로그램 전체에서 하나만 존재해야 하는 객체를 만들 때 사용하는 패턴이다. 따라서 객체를 생성하는 생성 패턴(Creational Pattern)이다. 참고로 먼저 생성 패턴을 차례대로 살펴볼 생각이다.
이런 구조는 다양한 곳에서 자주 등장한다. 예를 들어:
보통 Singleton 클래스는 자기 자신을 가리키는 instance 포인터를 내부에 가지고 있다. 외부에서는 getInstance()를 호출해서 접근하는 구조고, 새로운 객체 생성을 막고 기존 객체를 반환하게 된다.
이 Singleton 객체는 클라이언트가 직접 뭔가를 하기보다는 시스템 전체를 대표하는 역할로 많이 쓰인다.
리팩토링이 쉬움
나중에 이 객체를 더 이상 싱글톤으로 쓰고 싶지 않을 때 구조를 바꾸기 편하다.
전역 변수보다 훨씬 안전
전역 객체는 의존성도 높고 유지보수도 어렵지만, Singleton은 전역적으로 관리하면서도 더 안전하게 쓸 수 있다.
#include <iostream>
using namespace std;
class Singleton {
public:
static Singleton* getInstance() {
if (_instance == nullptr) {
_instance = new Singleton();
}
return _instance;
}
void doSomething() {
cout << "싱글톤 객체 동작 중" << endl;
}
private:
Singleton() {
cout << "Singleton 생성됨" << endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* _instance;
};
Singleton* Singleton::_instance = nullptr;
int main() {
Singleton* s1 = Singleton::getInstance();
s1->doSomething();
Singleton* s2 = Singleton::getInstance();
s2->doSomething();
cout << "주소 비교: " << s1 << " == " << s2 << endl;
}
getInstance()에서 _instance가 nullptr이면 객체를 생성하고, 이미 있으면 그걸 그대로 반환한다.private으로 막아서 new Singleton()처럼 직접 생성하는 걸 방지한다.delete 해서 복사로 다른 인스턴스를 만드는 것도 막는다.하지만 위 코드는 멀티스레드 환경에서는 안전하지 않다.
여러 스레드가 동시에 getInstance()를 호출하면, 싱글톤 객체가 두 개 생길 수도 있다.
이를 막기 위해 mutex와 DCL(Double-Checked Locking) 기법을 쓴다:
#include <iostream>
#include <mutex>
using namespace std;
class Singleton {
private:
Singleton() {
cout << "Singleton 생성됨" << endl;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
static Singleton* _instance;
static mutex mtx;
public:
static Singleton* getInstance() {
if (_instance == nullptr) {
lock_guard<mutex> lock(mtx);
if (_instance == nullptr) {
_instance = new Singleton();
}
}
return _instance;
}
void doSomething() {
cout << "싱글톤 객체에서 메서드 호출됨" << endl;
}
};
Singleton* Singleton::_instance = nullptr;
mutex Singleton::mtx;
if (_instance == nullptr)는 빠르게 통과하기 위한 checkstd::call_once#include <mutex>
class Singleton {
public:
static Singleton* getInstance() {
call_once(_flag, []() {
_instance = new Singleton();
});
return _instance;
}
private:
Singleton() {}
static Singleton* _instance;
static once_flag _flag;
};
call_once()는 _flag를 기준으로 단 한 번만 해당 람다를 실행한다.Singleton Pattern은 진짜 하나만 존재해야 하는 경우에 쓰는 강력한 도구다.
하지만 남발하면 테스트 어려움, 의존성 증가, 결합도 상승 같은 부작용도 있으니 정말 필요한 경우에만 쓰는 게 핵심이다.
다음에 살펴볼 패턴은 Factory Method Pattern이다. 해당 패턴은 객체 생성 로직을 직접 쓰는 대신, 객체 생성을 서브클래스에 위임하는 방식을 사용한다. 싱글톤이 "객체를 하나만 만들자"는 패턴이라면, 팩토리 메서드는 "객체 생성을 유연하게 만들자"는 개념이다.
Factory Method Pattern을 적절한 예제와 함께 살펴볼 생각이다.