일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- 자료구조
- 비교 기반 정렬 알고리즘
- Set
- object channel
- BFS
- Unreal Collision
- 외적
- command pattern
- 스택
- Queue
- flyweight pattern
- Factory method pattern
- Union-Find
- 생성패턴
- 명령패턴
- 정렬 알고리즘
- 경량 패턴
- 분포 기반 정렬 알고리즘
- 트리
- 깊이 우선 탐색
- 유니온-파인드
- 팩토리패턴
- 관찰자(Observer) 패턴
- 트리순회
- Abstract Factory pattern
- 두 직선사이 교점
- Trie
- C++ STL 정리
- 디자인패턴
- 동적 계획법
- Today
- Total
KimMK
싱글턴(Singleton) 패턴 본문
디자인패턴 중 생성패턴인 싱글턴은 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며 생성된 객체를 어디에서든지 참조할 수 있도록 하는 디자인패턴이다.
오직 하나의 객체만을 생성할 수 있는 클래스를 의미한다. 따라서 단일 인스턴스를 생성해 공유해서 사용해야 하는 상황에서 유용하다.
싱글턴을 정교하게 구현하고 사용한다면 객체 유일성을 보장하고 어디에서나 싱글턴 객체에 접근할 수 있는 접근성의 장점이 있지만, 득보다 실이 많은 패턴이므로 유의하자!!
싱글턴을 왜 사용할까? (장점)
- 객체를 하나만 생성해서 공유하기 때문에 메모리 낭비를 방지할 수 있다. 또한, 한 번도 사용하지 않는다면 아예 인스턴스 자체를 생성하지 않는다. 예를 들어 DB연결, NW연결, 파일 입출력 등과 같은 리소스를 사용하는 경우 여러 객체가 이를 생성하면 자원 낭비가 심해질 수 있다. 이를 방지하기 위해 하나의 객체만 생성해서 공유하는 방식을 선택하는 것이다.
- 전역 변수보다 안전한 방식으로 객체 인스턴스를 제공한다. 전역 변수는 여러 곳에서 접근 가능하기 때문에 의도치 않게 값을 변경할 수 있다. 하지만 싱글턴 패턴을 사용하면 객체 인스턴스를 전역 변수 대신 메서드를 통해 접근하므로 더 안전하다.
득보다 실이 많은 이유 (단점)
- 싱글턴 객체가 전역적인 상태를 유지하므로, 다른 객체와의 결합도가 높아지게 된다. 프로그램의 결합도가 높아지면 테스트나 리팩토링 등의 작업을 어렵게 만들 수 있다. 즉, 테스트를 하기 어렵고 변경에 취약해진다.
- 멀티 스레딩과 같은 동시성 프로그래밍에 알맞지 않다. 전역으로 만들면 모든 스레드에게 노출되고 수정할 수 있는 메모리 공간이 생긴 것이다. 다른 스레드가 전역 데이터에 어떤 작업을 하는지 모를 때도 있다. 그렇기 때문에 교착 상태(Dead Lock), 경쟁 상태(Race Condition)과 같은 동기화 버그가 생기기 쉽다.
- 싱글턴 객체를 초기화 하는 방법으로 게으른 초기화(Lazy Initialization)는 제어할 수 없다. 게임 프로그래밍에서 이를 적용한다고 했을 때, 시스템을 초기화할 때 메모리 할당, 리소스 로딩 등 시간이 걸릴 수 있다. 만약 오디오 시스템 초기화 시간이 많이 소요된다면 초기화 시점을 제어해줘야 한다. 게으른 초기화를 하게 만들면 처음 소리가 재생될 때 게임 내에서 초기화가 시작되는 바람에 프레임이 떨어질 수 있다.
또한, 게임에서 메모리 단편화를 막기 위해 힙에 메모리를 할당해 세밀하게 제어하는데 시스템 초기화를 할 때 상당한 메모리를 힙에 할당하면, 힙의 어디에 할당할 지 제어할 수 있도록 적절한 초기화 시점까지 찾아야 한다.
따라서, 반드시 싱글턴이 필요한 상황이 아니라면 지양하는 것이 좋다.
간단한 예시
싱글턴을 구현하는 일반적인 방법은 정적 멤버 변수와 정적 메서드를 사용하는 것이다.
class Singleton {
private:
static Singleton* instance; // 정적 멤버 변수로 싱글턴 객체의 포인터를 저장
Singleton() {} // 생성자를 private로 만들어 외부에서 객체를 생성하는 것을 방지
public:
static Singleton* getInstance() { // 정적 메서드로 싱글턴 객체의 포인터를 반환
if (instance == nullptr) { // 객체가 아직 생성되지 않은 경우, 생성
instance = new Singleton();
}
return instance;
}
};
Singleton* Singleton::instance = nullptr; // 정적 멤버 변수를 초기화
int main() {
Singleton* obj1 = Singleton::getInstance(); // 첫 번째 객체를 생성
Singleton* obj2 = Singleton::getInstance(); // 이미 생성된 객체를 가져옴
assert(obj1 == obj2); // 두 객체가 동일한 주소를 가리키는지 검증
}
멀티 스레드 환경을 위한 Double checked Locking과 Lazy Initialization을 이용한 구현
#include <mutex>
class Singleton {
private:
static Singleton* instance;
static std::mutex mutex;
Singleton() {}
public:
static Singleton& getInstance() {
// double-checked locking을 사용한 lazy initialization
if (!instance) {
std::lock_guard<std::mutex> lock(mutex);
if (!instance) {
instance = new Singleton();
}
}
return *instance;
}
};
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;
lazy initialization을 사용하면, getInstance()메서드를 호출한 시점에 정적 필드가 아직 초기화되지 않았으면 객체를 생성하고, 그 후에는 전에 생성한 객체의 참조를 그대로 반환한다.
Double Checked Locking을 사용해 멀티 스레드 환경에서 getInstance()를 여러 스레드가 동시에 호출했을 때 하나의 스레드만 접근할 수 있도록 동기화를 해준다. 처음 getInstance()가 호출될 때는 인스턴스가 생성되지 않았으므로, mutex를 사용해 스레드 안전성을 보장한 후에 인스턴스를 생성하고 이후에는 인스턴스가 이미 생성되어 있으므로 mutex를 사용하지 않고 바로 인스턴스를 반환한다.
'C++ > 디자인 패턴' 카테고리의 다른 글
상태(State) 패턴 (0) | 2023.03.19 |
---|---|
관찰자(Observer) 패턴 (0) | 2023.03.14 |
경량(Flyweight) 패턴 (1) | 2023.03.11 |
디자인 패턴 (0) | 2023.03.11 |
명령(Command) 패턴 (0) | 2023.03.11 |