1.1 의도
- 클래스의 인스턴스는 오직 하나임을 보장하며 어디에서나 동일한 방법으로 접근 하는 방법을 제공.
- 단점과 비판
- 전역변수와 유사하다.
- 멀티스레드간의 접근 문제
- 객체간의 결합도가 증가하고 재사용성이 감소한다.
- 그럼에도 많은 오픈소스에서 애용하는 패턴이다.
1.2. 규칙
- 외부에서는 객체를 생성할 수 없어야 한다. >> private 생성자를 활용
- 한 개의 객체는 만들수 있어야 한다. >> 오직 한개의 객체를 만들어서 반환하는 static 맴버 함수를 사용
- 복사 생성자도 사용할 수 없어야 한다 >> 복사/대입을 금지 (= delete)
1.3. Meyer's singleton
- 오직 한개의 객체를 static 지역변수로 생성
- 지연된 초기화 : get_instance()를 호출할 때 초기화 된다. 사용하지 않으면 생성자가 호출되지 않고, 메모리를 사용하지 않는다.
- thread-safe : static 지역 변수이기 때문에 가능
class Cursor { private: Cursor() { std::cout << "start Cursor()" << std::endl; std::this_thread::sleep_for(3s); // 3초가 걸린다고 하더라도 늦게 도착한 애들은 앞의 생성자가 생성될 때까지 대기한다. std::cout << "finish Cursor()" << std::endl; } Cursor(const Cursor& ) = delete; Cursor& operator=(const Cursor&) = delete; public: static Cursor& get_instance() { std::cout << "start get_instance" << std::endl; static Cursor instance; std::cout << "finish get_instance" << std::endl; return instance; } };
1.4. Heap 영역에 singleton을 생성하는 방법
- 멀티스레드 접근에 안전하기 위해서 동기화가 필요 >> lock_guard를 통해서 동기화가 가능하다.
- 직접적으로 lock을 걸어주는 코드는 좋은 코드가 아닌다 ( mutex를 통해서 lock을 해주는 경우 lock이 걸린 상태로 예외가 발생하면, lock 을 해제하지 못하는 경우 발생)
- lock_guard를 사용하는 경우 생성자에서 자동으로 lock을 해줌
- 만약 예외가 발생하더라도 cpp에서는 예외 발생시 지역변수는 안전하게 파괴되며, lock_guard 소멸자에서 lock을 안전하게 해제할 수 있음
- {}을 통해서 lock을 빨리 해제하는 방법도 존재(지역을 벗어나면 소멸자가 불림)
class Cursor { private: Cursor() {} Cursor(const Cursor& ) = delete; Cursor& operator=(const Cursor&) = delete; static std::mutex m; static Cursor* instance; public: static Cursor& get_instance() { std::lock_guard<std::mutex> g(m); // g의 생성자에서 m.lock() // m.lock(); if ( instance == nullptr ) instance = new Cursor; // m.unlock(); return *instance; } }; Cursor* Cursor::instance = nullptr; std::mutex Cursor::m;
1.5. Double Check Locking Pattern (DCLP)
- 위의 코드와 같이 진행하는 경우 이미 Singleton객체가 생성된 후 불러오기만 할때도 불필요하게 lock을 걸고 해제하는 과정이 존재
- 이를 해결하기 위해 DCLP를 아래와 같이 사용 가능
static Cursor& get_instance() { if ( instance == nullptr ) { m.lock(); if ( instance == nullptr ) { instance = new Cursor; // instance = Cursor 크기 메모리할당; // Cursor::Cursor(); } m.unlock(); }
- 위와 같이 진행하면 이미 singleton 객체가 존재하는 경우 lock 없이 진행가능하나, CPP에서는 해당 방식은 오류로 정의됨.
- 컴파일러가 최적화하면서 위의 주석과 같은 일이 발생하여 아직 초기화가 끝나지 않았는데도 instance는 메모리 주소가 할당되어 있음
- 따라서 Cursor::Cursor()가 오래걸리는 경우 아직 초기화가 끝나지 않았는데 다시 접근을 한다면 intance는 nullptr이 아니기에 초기화되지 않은 intance 주소를 전달함.
- Double-Checked Locking is Fixed in C++11에서 해결되었음
1.6. Singleton 코드를 재사용하는 방법
- 매크로를 활용하는 방법
#define MAKE_SINGLETON(classname) \ private: \ classname() {} \ classname(classname&) = delete; \ void operator=(classname&) = delete; \ public: \ static classname& getInstance() \ { \ static classname instance; \ return instance; \ } \ private:// 요게 있어주는게 좋음. 디폴트가 private으로 끝나게 지정해 주는 구조 #include "singleton.h" class Cursor { MAKE_SINGLETON(Cursor) };
- 상속을 활용하는 방법
- CRTP(Curiously Recurring Template Pattern) : 기반 클래스에서 미래에 만들어질 파생 클래스의 이름을 사용할 수 있게 하는 기술
- 상속 받아서 사용할 수 있도록 생성자는 private이 아닌 protected에 위치
- intance와 mutex 객체를 만들때도 template|< typename T>로 선언해 주어야함
template<typename T> //CRTP라는 방법 class Singleton { protected: Singleton() {} // 싱속 받아서 사용할 수 있도록 변경해 주었음 private: Singleton(const Singleton& ) = delete; Singleton& operator=(const Singleton&) = delete; static std::mutex m; static T* instance; public: static T& get_instance() { std::lock_guard<std::mutex> g(m); if ( instance == nullptr ) instance = new T; // 전달받은 Type의 객체가 생성됨 return *instance; } }; template<typename T> T* Singleton<T>::instance = nullptr; //T는 위의 클래스 내에서만 토용되므로 다음과같이 적어주어야 함 template<typename T> std::mutex Singleton<T>::m; class Mouse : public Singleton< Mouse > // 상속을 통해서 싱글톤 코드를 재사용하는 방법도 가능 {};
2.1. 의도
- 속성이 동일한 객체를 공유하게 한다.
- Word에서 폰트 값은 동일하지만 값만 다른 수많은 객체들이 존재함
- 이때 일일이 동일한 속성의 font를 따로 저장하게 되면 메모리 소모가 크기 때문에, flyweight 패턴을 적용하여 동일한 속성 값은 한개만 생성하여 공유한다.
2.2. 예제
- 생성자는 private에 두고, 자신을 생성하는 함수를 factory에서 생성 ( friend class 로 지정하여 private에 접근 가능함 / cpp에서만 friend지원)
- factory는 그동안 생성한 객체의 속성을 기억하는 map을 생성하고 중복하지 않는 경우에만 새로 생성.
class Image { std::string image_url; Image(const std::string& url) : image_url(url) { std::cout << url << " Downloading...\n"; } public: void draw() { std::cout << "draw " << image_url << '\n'; } friend class ImageFactory; // 친구는 private에 접근할 수 있음 //cpp에는 가능하지만 다른곳에서는 안되는 코드임. }; class ImageFactory { std::map<std::string, Image*> image_map; public: Image* create(const std::string& url) { Image* img; auto ret = image_map.find(url); if (ret == image_map.end()) { img = new Image(url); image_map[url] = img; } return image_map[url]; } };
2.3 핵심정리
- Singleton pattern은 class에 대하여 단 한개의 객체를 생성하지만, flyweight은 class의 맴버 변수의 값이 다르다면 여러번 생성할 수 있다.
- 결국 하나의 객체 안에 동일한 값들을 중복적으로 저장하는 것을 막는 pattern 이다.
3.1. 기원
- 정통적인 Design Pattern에 factory는 없다. Abstract Facotry만 존재하지만, Abstract Factory를 이해하기 위해서 factory를 먼저 이해하는 것이 유리.
3.2. 예제
- 새로운 도형이 추가되더라도, Client의 코드는 변경이 없는 구조로 구현함.
- Factory를 통해서 객체를 생성
- 동일한 곳에서 객체를 생성 하기에 한곳에서만 관리하여 추가되더라도 factory의 수정만으로 변경 가능(큰 시스템에서는 main이 아닌 다른 곳에서 객체를 생성할 수도 있음)
- static 맴버 함수를 통한 객체 생성 : cpp에서는 클래스의 이름을 자료형에 보관이 불가능하지만, 클래스를 생성하는 생성함수는 자료구조에 보관 할 수 있음.
class Shape // 업케스팅으로 동일한 방식으로 create을 할수 있게 해줌 { public: virtual void draw() = 0; virtual ~Shape() {} }; class ShapeFactory // 모양을 찍어내는 공장 { MAKE_SINGLETON(ShapeFactory) // 싱글턴 구조를 활용하여 단 한개만 생성할수 있게 보장 using F = Shape*(*)(); // Shpae 내의 어떤 함수를 포인터로 받아오겠다. std::map<int, F> create_map; public: void register_shape(int key, F create_function) // { create_map[key] = create_function; } Shape* create(int type) { Shape* p = nullptr; if (create_map[type] != nullptr) { p = create_map[type](); // map에 등록해뒀던 입력받은 타입 객체를 생성하는 함수가 호출됨 } return p; } }; class RegisterFactory // 등록해주는 factory { public: RegisterFactory(int type, Shape* (*f)()) { ShapeFactory::get_instance().register_shape(type, f); } }; #define REGISTER(classname) \ static Shape* create() { return new classname; } \ static RegisterFactory rf; // static으로 불러 동일한 RegisterFactory에 항상 접근 하겠다 #define REGISTER_IMPL(type, classname) \ RegisterFactory classname::rf(type, &classname::create); class Rect : public Shape { public: void draw() override { std::cout << "draw Rect" << std::endl; } REGISTER(Rect) }; REGISTER_IMPL(1, Rect) int main() { std::vector<Shape*> v; ShapeFactory& factory = ShapeFactory::get_instance(); while (1) { int cmd; std::cin >> cmd; if (cmd > 0 && cmd < 8) { Shape* s = factory.create(cmd); if ( s ) v.push_back(s); } else if (cmd == 9) { for (auto s : v) s->draw(); } } }
- 각 도형은 동일한 Registor Factory에 자신을 생성하는 함수를 등록
- Registor Factory는 Shape Factory를 초기화하고, 해당 Factory의 Map(key = 입력받을 값 / value = 생성 함수)에 값을 저장 (이때 Factory는 처음 초기화되며 Singleton구조를 가지고 있음)
- factory의 create 함수에서 각각의 도형들이 Pointer로 생성되어 Return.
3.1. 의도
- 견본적(porotypical) 인스턴스를 사용하여 생성할 객체의 종류를 명시하고, 견본을 복사하여 새로운 객체를 생성
- 같은 클래스더라도 Blue circle, Red Circle과 같이 특정 속성이 다르다면 미리 해당 객체를 생성해두고 복사하여 사용 가능
3.2. 예제
- Factory는 생성 함수를 등록했다면, Prototype은 객체를 등록
- Map 또한 실제 객체를 저장.
class ShapeFactory { MAKE_SINGLETON(ShapeFactory) std::map<int, Shape*> prototype_map; public: void register_sample(int key, Shape* sample) { prototype_map[key] = sample; } Shape* create(int type) { Shape* p = nullptr; if (prototype_map[type] != nullptr) { p = prototype_map[type]->clone(); // 복사해서 사용 } return p; } }; int main() { std::vector<Shape*> v; ShapeFactory& factory = ShapeFactory::get_instance(); Rect* red_rect = new Rect; Rect* blue_rect = new Rect; Circle* blue_circle = new Circle; // 자주 사용하는 객체를 등록 factory.register_sample( 1, red_rect); factory.register_sample( 2, blue_rect); factory.register_sample( 3, blue_circle); while (1) { int cmd; std::cin >> cmd; if (cmd > 0 && cmd < 8) { Shape* s = factory.create(cmd); if ( s ) v.push_back(s); } else if (cmd == 9) { for (auto s : v) s->draw(); } } }
5.1. 의도
- 상세화된 서브 클래스를 정의하지 않고 서로 관련성이 있거나 독립적인 여러 객체군을 생성하기 위한 인터페이스를 제공
- 결국 Factory의 Interface를 둬서 어떤 제품이 생성되든지 동일한 코드로 생성하게 하자!
5.2. 예제
- 게임 엔진은 다양한 플랫폼(PC, 모바일, 콘솔 등)을 지원해야 하며, 각 플랫폼에 맞는 그래픽 렌더러, 오디오 시스템, 입력 장치 터페이스를 제공해야함
- 추상 팩토리 패턴을 통해 플랫폼별로 서로 다른 시스템을 생성하지만, 클라이언트(게임 로직)에서는 이 모든 것을 동일한 인터페이스를 통해 접근할 수 있음 ex( GraphicsFactory는 플랫폼에 맞는 그래픽 시스템(DirectXRenderer, OpenGLRenderer, VulkanRenderer)을 생성)
class RichControlFactory : public IControlFactory { public: IButton* create_button() { return new RichButton; } IEdit* create_edit() { return new RichEdit; } }; class SimpleControlFactory : public IControlFactory { public: IButton* create_button() { return new SimpleButton; } IEdit* create_edit() { return new SimpleEdit; } }; int main(int argc, char** argv) { IControlFactory* factory; if (strcmp(argv[1], "-style:rich") == 0) factory = new RichControlFactory; else factory = new SimpleControlFactory; IButton* btn = factory->create_button(); btn->draw(); }
동일한 기능을 가진 서로 다른 제품군이 있을때 유용.