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]; } };
결국 하나의 객체 안에 동일한 값들을 중복적으로 저장하는 것을 막는 pattern 이다.