객체를 생성하는 방법

EHminShoov2J·1일 전
0

Design Pattern

목록 보기
6/6
post-thumbnail

1. Singleton Pattern

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. flyweight pattern

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 이다.

0개의 댓글