인스턴스의 포인터를 전역으로 접근하면서 오직 하나의 인스턴스만을 생성하는 것을 보장하는 패턴입니다.
역사상 가장 많이 미움을 받고 있는 디자인 패턴입니다.
패턴을 통틀어 구조적으로 가장 간단한 패턴입니다.
전역변수와 같은 역할을 하지만 호출 될때 객체를 만들 수 있어서 필요 할때만 만들 수 있습니다.
한 클래스의 인스턴스가 하나만 생기도록 하는 구현은 생각보다 까다롭다.
전역 객체는 초기화 순서가 정의 되어 있지 않습니다. 전역 객체가 다른 전역 객체를 참조 한다면 문제가 발생합니다.
static Database database{};
이 함수는 스레드 안전성이 C++11 이상 버전에서만 보증됩니다.
Database& get_datatabase()
{
static Database database;
return database;
}
전통적인 구현
위의 구현은 객체가 추가로 생성되는 것을 막는 장치가 없습니다.
static 카운터 변수를 두고 값이 증가될 경우 예외를 발생시키는 방법은 사용자는 한번만 생성되어야 한다는 것을 알 수가 없습니다.
복사 생성자, 복사 연산자, 이동 생성자, 이동 연산자를 모두 private으로 하거나 삭제(= delete)합니다.
이렇게 한 후 get() 함수에서 힙 메모리 할당으로 객체를 생성하게 합니다.
멀티스레드 안정성
- Mermoy Order와 atomic
이전에는 atomic한 실행을 위해 volatile, interlocked 계열 함수를 이용하여 일일이 구현 하였었습니다.
여기서 atomic은 무엇을 의미할까요? 원자는 더이상 쪼갤 수 없는 것을 의미 합니다. 데이터의 값을 넣을때 쪼개서 넣을 수 없이 온전한 상태로 넣을 수 있다는 의미입니다.
연산을 위해 cpu는 메모리로 부터 데이터를 읽습니다. 그런데 cpu 연산 속도에 비해 메모리는 상당히 느립니다.
이를 보완하기 위해 cpu에는 L1, L2, L3와 같은 캐시가 있으며 성능을 높입니다. 이러한 캐시는 cpu의 각 core 마다 존재합니다.
한정적인 캐시를 효율적으로 사용하기 위해 컴파일러는 실행을 재배치 할 수도 있습니다.
예를 들면 아래 코드의 실행 순서가 실행되는 환경에 따라 실행 순서가 보장 되지 않을 수 있습니다.
멀티 쓰레드 환경에서 생각 해보면 b값이 1이라고 해서 항상 a값이 1이라는 것을 보장하지 않습니다.
그 이유는 b = 0실해으로 cpu 캐시에 b가 올라가 있고 캐시에 올라가있는 상태에서 b=1을 a=1보다 더 먼저 하는 것이 수행 속도가 더 빠를 수 있습니다.
연속적으로 b에 대한 데이터 처리를 하면 캐시 사용률이 올라 갈테니까요.
int a = 0;
int b = 0;
void foo()
{
a = 1;
b = 1;
int c = a + b;
}
이쪽 주제는 심오하고 힘든 주제여서 이쯤에서 마치는 것이 좋겠습니다.atomic 객체들의 경우 원자적 연산 시에 메모리에 접근할 때의 방식을 정할 수 있습니다. 이것이 바로 Memory Order입니다. (Memory Barrier 또는 Memory fence를 들어 봤을 것입니다.)
아래 이미지로 자세한 설명은 주제의 범위를 벗어나므로 생략하도록 하겠습니다.
어차피 우리의 개발 환경은 memory_order_seq_cst(sequential consistency)이므로 이러한 Memory Order 설정이 필요없습니다.
자료를 찾아 보니 memory_order_consume은 구현이 바뀌고 있어서 사용을 권장하지 않고 memory_order_acquire를 사용합니다.
5.3 싱글턴 문제
SingletonRecordFinder가 SingletonDatabase에 밀접하게 의존하고 있습니다.
아래와 같이 Singleton을 호출하여 사용하지 맙시다.
struct SingletonRecordFinder
{
int total_population(std::vector<std::string> names)
{
int result = 0;
for (auto& name : names)
result += SingletonDatabase::get().get_population(name);
return result;
}
};
Database 참조하여 직접적으로 의존하지 않도록 합시다.
개선 코드는 다음과 같습니다.
struct ConfigurableRecordFinder
{
explicit ConfigurableRecordFinder(Database& db) : db{db} { }
int total_population(std::vector<std::string> names) const
{
int result = 0;
for (auto& name : names)
result += db.get_population(name);
return result;
}
Database& db;
};
싱글턴과 제어 역전
싱글턴의 사용은 결국 과도한 종속성을 유발합니다. 싱글턴을 다시 일반 클래스로 바꿔야 할 때는 많은 비용이 듭니다.
Boost.DI(의존성 주입 프레임워크)를 이용하면 아래와 같이 싱글턴 컴포넌트를 정의할 수 있습니다.
auto injector = di:make_injector(
di::bind<IFoo>.to<Foo>.in(di::singleton)
);
이렇게 하면 싱글턴 객체를 다른 것으로 바꿔야 할 경우 이 곳만 수정하면 됩니다.
요약
싱글턴 패턴은 직접적인 사용법(Singleton.GetInstance().foo()를 호출)을 피하자.
종속성을 주입하는 방식(생성자의 인자)으로 모든 종속성이 한 곳에서 관리 될 수 있도록 사용하자.
코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
'프로그래밍 일반 > 디자인 패턴' 카테고리의 다른 글
브릿지 패턴(Bridge Pattern) (0) | 2019.12.08 |
---|---|
어댑터 패턴(Adapter Pattern) (0) | 2019.12.08 |
프로토타입 패턴(Prototype Pattern) (0) | 2019.12.08 |
팩토리 패턴(Factory pattern) (2) | 2019.11.17 |
빌더 패턴(Builder Pattern) (0) | 2019.11.17 |