Coding Memo

Singleton 패턴 본문

Language/C++

Singleton 패턴

minttea25 2024. 9. 26. 19:03

싱글턴 디자인 패턴은 어플리케이션 전체에서 단 하나만 존재해야하는 인스턴스를 위한 패턴이다.

 

특정 인터페이스를 제공한다거나 동일한 데이터를 사용할 때 한번만 로드할 때 사용한다. 보통은 전역적으로 접근 가능한 인스턴스를 사용한다. 또, 어플리케이션 시작 시점에 인스턴스를 로드할지, 이후에 사용할 때 로드 할지 (Lazy Intialization) 잘 고려하면 될 것이다.

 

싱글턴 패턴 구현은 언어와 개발 환경에 따라 크게 달라지는 것 같다.

인스턴스를 1개로 제한하는 방법에는 인스턴스 카운트를 관리하는 코드를 추가하거나, static 전역 변수를 두어 이 인스턴스를 반환하여 사용하는 메서드를 만들면 될 것이다.

보통 후자의 방법이 많이 사용되는 것 같다.

 

먼저 구현하는 코드를 살펴보자.


 

struct Singleton
{
public:
	static Singleton& get()
	{
		static Singleton* instance = new Singleton();
		return *instance;
        
            // static Singleton instance;
            // return instance;
	}
private:
	Singleton() {}
	Singleton(Singleton&&) = delete;
	Singleton& operator=(Singleton const&) = delete;
	Singleton& operator=(Singleton&&) = delete;
};

 

Singleton::get()을 통해 어플리케이션 내에 유일하게 존재하는 instance를 얻을 수 있다.

다른 위치에서 이동이나 복사가 일어나지 않도록 관련 생성자를 삭제(delete)한다.

확실하게 더 안전한 방법은 다른 코드에서 Singleton을 생성할 수 없도록 아예 생성자를 private로 선언하여 접근 조차 안되도록 하는 것이다!!!

 

 

Note: Monostate(모노스테이트)라는 싱글턴 패턴의 변형이 존재한다. 이는 겉으로 보면 일반 클래스처럼 보이나, 내부적으로는 static 변수들을 이용하기 때문에 클래스 내의 데이터는 어플리케이션 내에 한 가지 밖에 존재하지 않는다.

struct Monostate
{
public:
	int get_state() const { return s_state; }
	void set_state(const int state) { s_state = state; }
private:
	Monostate() = delete;
	static int s_state;
};

 


프로젝트를 수행할 때, 특히 그 규모가 점점 커져서 싱글턴 클래스가 많아질 때, 유닛 테스트하기가 너무 어려워지는 문제를 겪었었다.

 

실제로 싱글턴 디자인 패턴은 많은 종속성을 유발한다.

예를 들어 아래와 같이 ResourceSet와 ResourceManager라는 싱글턴 클래스가 각각 있다고 가정해보자.

ResourceSet는 어플리케이션에서 사용하는 리소스에 대한 정보를 가지고 있는 클래스이고, ResourceManager는 이 리소스들을 가지고 할 수 있는 특정 작업들을 구현해놓은 클래스이다.

class Resource
{
public:
	virtual int GetSomething() = 0;
};

class ResourceSet : public Resource
{
public:
	static ResourceSet& get()
	{
		static ResourceSet instance;
		return instance;
	}

	int GetSomething() override { return 0; }
};

class ResourceManager
{
public:
	static ResourceManager& get()
	{
		static ResourceManager instance;
		return instance;
	}

	void DoSomething()
	{
		ResourceSet::get().GetSomething();
	}
};

 

언뜻 보기에는 문제가 없어 보인다. 하지만, ResourceManager 클래스를 테스트 할 때, 반드시 이미 생성되어 있는 ResourceSet이라는 클래스에 의존적이게 된다. 만약 ResourceSet이 데이터 양이 많거나 무겁다면 테스트 코드를 실행 할 때, 너무 비효율 적일 것이다. 그렇다고 테스트용 리소스를 로드하도록 따로 Resources에 코드를 추가하는 것은 지저분한 코드가 될 수 있다.

 

위 문제에 대한 해결책은 ResourceManager에서 명시적으로 사용하고 있는 ResourceSet에 대한 의존성을 제거하는 것이다. 즉, ResourceSet대신 부모 클래스인 Resource를 사용하고 ResourceManager는 참고하고 있을 Resource&를 두는 것이다. 이렇게 하면 느슨한 결합이 되어서 유연하게 사용할 수  있다.

class ResourceManager2
{
public:
	ResourceManager2(Resource& r) : resource(r) {}

	void DoSomething()
	{
		resource.GetSomething();
	}
public:
	Resource& resource;
};

 

책에서는 위 내용을 데이터베이스 싱글턴과 이를 사용하는 어떤 싱글턴 인터페이스에 대한 Google Test (gtest) 테스트 코드로 설명하고 있다.

 


Reference

모던 C++ 디자인 패턴 (드미트리 네스터룩, 길벗)  - Singleton

'Language > C++' 카테고리의 다른 글

[C++20] concepts - requires  (0) 2024.10.02
std::sort와 std::list.sort  (1) 2024.09.26
std::filesystem  (0) 2024.09.19
dllexport / dllimport (MSVC)  (0) 2024.09.04
[winsock] getpeername 호출 시, WSAENOTCONN(10057) 에러  (0) 2024.05.23