본문 바로가기
개발/Architecture & Design Pattern

C++ - 싱글턴 패턴 (Singleton Pattern)

by 피로물든딸기 2024. 1. 26.
반응형

C, C++ 전체 링크

Architecture & Design Pattern 전체 링크

 

참고

- call_once로 함수를 한 번만 호출하기

- atomic으로 원자적 연산 처리하기 (vs mutex)

 

싱글턴 패턴 (Singleton Pattern) - 생성 패턴

- 특정 클래스에 대해 객체 인스턴스가 하나만 만들어지도록 하고, 전역 접근을 제공하는 패턴

- 전역 변수에 객체를 대입하면 Application이 시작되고 종료되기까지 자원을 차지하지만, 

  싱글턴 패턴은 필요할 때만 객체를 만들 수 있다.

- 실제로 객체가 필요하면 인스턴스를 직접 만들지 않고, 인스턴스를 요청하도록 구현한다. (getInstance)

 

구현

- 다른 객체에서 new 연산자를 사용하지 못하도록 생성자를 private으로 정의

- 생성자 역할을 하는 static 메서드를 제공해서 전역 접근을 제공

- static 메서드 내에서 지연된 초기화를 구현

 

장점

- 인스턴스가 하나만 존재하는 것을 보장한다. (유일성)

- 싱글턴 인스턴스에 대한 전역 접근을 제공한다. 

- 전역 변수와 달리 인스턴스가 필요한 시점까지 생성을 지연할 수 있다. (지연 초기화)

 

단점

- 단일 책임 원칙(하나의 클래스는 한 가지만 책임져야 한다.)을 무시한다.

- 다중 스레드 환경에서 동기화 구현이 필요하다.

- 서브 클래스를 만들 수 없다.

- 전역 상태를 관리하므로 코드의 의존성이 높아지고 테스트가 까다롭다.


싱글턴 구현 (thread unsafe)

 

아래 코드를 실행해 보자.

#include <iostream>

using namespace std;

class Singleton 
{
public:
	Singleton() { cout << "create" << endl; }
};

int main(void)
{
	Singleton *s = new Singleton();

	return 0;
}

 

이제 생성자를 private으로 변경해 보자.

class Singleton 
{
private:
	Singleton() { cout << "create" << endl; }
};

 

private 멤버에는 액세스 할 수 없다는 에러가 발생한다.

 

생성자를 호출하려면 클래스의 인스턴스가 있어야 한다.

하지만 다른 클래스에서 Singleton 클래스의 인스턴스를 만들 수 없기 때문에 인스턴스를 만드는 것이 불가능하다.

Singleton 클래스에서만 private 생성자에 접근할 수 있는데,

다른 클래스에서 new Singleton()을 할 수 없으므로 현재 Singleton 클래스를 만들 수 없다.

 

따라서 private 생성자에 접근하기 위해 static 변수와 static 메서드를 추가한다.

class Singleton 
{
public:
	static Singleton* getInstance();
private:
	Singleton() { cout << "create" << endl; }
	static Singleton *instance;
};

 

instance는 null로 초기화하였다.

Singleton* Singleton::instance = nullptr;

 

그리고 instance에 접근할 메서드를 만든다.

instance가 존재 하지 않으면 new를 이용해 클래스를 생성하고, 그렇지 않으면 instance를 리턴한다.

Singleton* Singleton::getInstance()
{
	if (instance == nullptr)
		instance = new Singleton();
	return instance;
}

 

전체 코드는 다음과 같다.

#include <iostream>

using namespace std;

class Singleton 
{
public:
	static Singleton* getInstance();
private:
	Singleton() { cout << "create" << endl; }
	static Singleton *instance;
};

Singleton* Singleton::instance = nullptr;

Singleton* Singleton::getInstance()
{
	if (instance == nullptr)
		instance = new Singleton();
	return instance;
}

int main(void)
{
	Singleton *s1 = Singleton::getInstance(); 
	Singleton *s2 = Singleton::getInstance();

	return 0;
}

 

싱글턴 s1을 실행할 때만 생성자가 실행되며, 그 이후 getInstance()는 만들어진 instance에 접근하게 된다.


Thread Safe

 

thread 헤더를 추가하여 두 개의 스레드에서 싱글턴을 생성해 보자.

#include <thread>

using namespace std;

...

void makeSingleton()
{
	for (volatile int i = 0; i < 100000; i++);
	Singleton *s = Singleton::getInstance();
}

int main(void)
{
	thread thread1(makeSingleton);
	thread thread2(makeSingleton);

	thread1.join();
	thread2.join();

	return 0;
}

 

멀티스레딩 상황에서 아래와 같이 싱글턴 객체 두 개가 만들어질 수 있다.

 

해결 방법은 여러 가지가 있다.

 

1) 처음부터 싱글턴을 생성한다.

2) mutex를 사용해서 getInstance를 보호한다.

3) Double Checked Locking으로 인스턴스가 생성되지 않은 경우에만 동기화를 사용한다.

4) std::call_once, std::once_flag를 이용하여 한 번만 실행되어야 하는 코드를 구현한다.

5) std::atomic으로 원자적인 연산을 수행한다.

6) 유일한 static 인스턴스를 리턴한다. (C++11 이상 스레드 세이프)

 


방법 1 - 처음부터 싱글턴을 생성한다.

 

nullptr을 할당했던 instance에 미리 싱글턴 객체를 할당하면 된다.

변수가 모두 초기화되고 main이 실행되므로 위와 같은 상황이 발생하지 않는다.

// Singleton* Singleton::instance = nullptr;
Singleton* Singleton::instance = new Singleton();

방법 2 - mutex를 사용해서 getInstance를 보호한다.

 

뮤텍스를 추가한 코드는 다음과 같다.

#include <mutex>

using namespace std;

class Singleton 
{
public:
	static Singleton* getInstance();
private:
	Singleton() { cout << "create" << endl; }
	static Singleton *instance;
	static mutex mtx;
};

Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx;

Singleton* Singleton::getInstance()
{
	lock_guard<mutex> lock(mtx);

	if (instance == nullptr)
		instance = new Singleton();
	return instance;
}

방법 3 - Double-Checked Locking 패턴으로 인스턴스가 생성되지 않은 경우에만 동기화를 사용한다.

 

방법 3을 코드로 구현하면 다음과 같다.

instance가 존재하지 않은 경우만 mutex로 보호하면 된다.

	if (instance == nullptr) 
	{
		lock_guard<mutex> lock(mtx);

		if (instance == nullptr)
			instance = new Singleton();
	}

 

전체 코드는 다음과 같다.

#include <iostream>
#include <thread>

#include <mutex>

using namespace std;

class Singleton 
{
public:
	volatile static Singleton* getInstance();
private:
	Singleton() { cout << "create" << endl; }
	volatile static Singleton *instance;
	static mutex mtx;
};

volatile Singleton* Singleton::instance = nullptr;
mutex Singleton::mtx;

volatile Singleton* Singleton::getInstance()
{
	if (instance == nullptr) 
	{
		lock_guard<mutex> lock(mtx);

		if (instance == nullptr)
			instance = new Singleton();
	}

	return instance;
}

void makeSingleton()
{
	for (int i = 0; i < 100000; i++);
	volatile Singleton *s = Singleton::getInstance();
}

int main(void)
{
	thread thread1(makeSingleton);
	thread thread2(makeSingleton);
	
	thread1.join();
	thread2.join();

	return 0;
}

방법 4 - std::call_once, std::once_flag를 이용하여 한 번만 실행되어야 하는 코드를 구현한다.

 

Double-Checked Locking 패턴 (DCLP)은 매우 간단하지만 C++ 멀티스레드 환경에서는 권장되지 않는다.

멀티스레딩에서 발생할 수 있는 경합 조건(race condition)에 안전성을 보장하지 않고,

DCLP 초기화를 지연시키기 때문에 다른 스레드에서 해당 객체를 사용하기 전까지 초기화가 지연될 수 있다.

 

또한 volatile 키워드를 추가해서 컴파일러에게 변수의 최적화를 요청하지 않더라도,

해당 변수는 예상하지 못한 방식으로 변경될 수 있기 때문에, 최적화를 막아 항상 변수의 현재 상태를 사용하도록 한다.

 

하지만 volatile이 멀티스레딩과 관련된 동기화 문제에서 안전성을 완벽히 해결하는 것은 아니다.

 

C++11 이후에서는 std::call_oncestd::once_flag를 이용해서 싱글턴 패턴을 만들 수 있다.

두 라이브러리를 이용해서 한 번만 실행되어야 하는 코드 블록을 정의할 수 있다.

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;

class Singleton 
{
public:
	static Singleton* getInstance();

private:
	Singleton() { cout << "create" << endl; }
	static Singleton* instance;
	static once_flag flag;
};

Singleton* Singleton::instance = nullptr;
once_flag Singleton::flag;

Singleton* Singleton::getInstance() 
{
	call_once(flag, []() {
		instance = new Singleton();
	});

	return instance;
}

void makeSingleton() 
{
	for (int i = 0; i < 100000; i++);
	Singleton* s = Singleton::getInstance();
}

int main(void) 
{
	thread thread1(makeSingleton);
	thread thread2(makeSingleton);

	thread1.join();
	thread2.join();

	return 0;
}

방법 5 - std::atomic으로 원자적인 연산을 수행한다.

 

C++11 이후에는 std::atomic으로 동기화를 할 수 있다.

#include <iostream>
#include <atomic>
#include <thread>

using namespace std;

class Singleton
{
public:
	static Singleton* getInstance();

private:
	Singleton() { cout << "create" << endl; }
	static atomic<Singleton*> instance;
};

atomic<Singleton*> Singleton::instance(nullptr);

Singleton* Singleton::getInstance()
{
	Singleton* tmp = instance.load(memory_order_relaxed);

	if (tmp == nullptr)
	{
		Singleton* newSingleton = new Singleton();
		if (instance.compare_exchange_strong(tmp, newSingleton, memory_order_release))
			return newSingleton; // 성공적으로 업데이트되었을 때만 newSingleton을 반환
		else
		{
			cout << "delete newSingleton" << endl;
			delete newSingleton; // 다른 스레드가 먼저 업데이트한 경우, 삭제
		}
	}

	return tmp;
}

void makeSingleton()
{
	for (int i = 0; i < 100000; i++);
	Singleton* s = Singleton::getInstance();
}

int main(void)
{
	thread thread1(makeSingleton);
	thread thread2(makeSingleton);

	thread1.join();
	thread2.join();

	return 0;
}

방법 6 - 유일한 static 인스턴스를 리턴한다. (C++11 이상 스레드 세이프)

 

아래 메서드는 하나만 존재하는 인스턴스리턴하게 된다.

static 변수의 초기화는 전체 런타임 중에 한 번만 수행되기 때문이다.

Singleton& Singleton::getInstance()
{
	static Singleton singleton;
	return singleton;
}

 

싱글턴을 힙 메모리에 할당하려면 아래와 같이 변경할 수 있다.

이 경우는 프로그램이 종료할 때까지 소멸자의 호출이 필요없는 경우 유용하다.

Singleton& Singleton::getInstance()
{
	static Singleton* singleton = new Singleton();
	return *singleton;
}

 

아래 코드는 C++11 이상에서 스레드 세이프하다.

#include <iostream>
#include <thread>

using namespace std;

class Singleton
{
public:
	static Singleton& getInstance();

	Singleton(Singleton const&) = delete;
	Singleton(Singleton&&) = delete;
	Singleton& operator=(Singleton const&) = delete;
	Singleton& operator=(Singleton const&&) = delete;

private:
	Singleton() { cout << "create" << endl; }	
};

Singleton& Singleton::getInstance()
{
	static Singleton singleton;
	return singleton;
}

// or 힙 메모리 할당
/*
Singleton& Singleton::getInstance()
{
	static Singleton* singleton = new Singleton();
	return *singleton;
}
*/

void makeSingleton()
{
	for (int i = 0; i < 100000; i++);
	Singleton& s = Singleton::getInstance();
}

int main(void)
{
	thread thread1(makeSingleton);
	thread thread2(makeSingleton);

	thread1.join();
	thread2.join();

	return 0;
}
반응형

댓글