객체를 생성하는 방법

EHminShoov2J·2024년 10월 16일
0

Design Pattern

목록 보기
6/7
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];
	}
};

2.3 핵심정리

  • Singleton pattern은 class에 대하여 단 한개의 객체를 생성하지만, flyweight은 class의 맴버 변수의 값이 다르다면 여러번 생성할 수 있다.
  • 결국 하나의 객체 안에 동일한 값들을 중복적으로 저장하는 것을 막는 pattern 이다.

3. Factory 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.

4. Prototype Pattern

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. Abstract factory

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();
}

동일한 기능을 가진 서로 다른 제품군이 있을때 유용.

0개의 댓글