Architecture & Design Pattern 전체 링크
참고
- 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_once와 std::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;
}
'개발 > Architecture & Design Pattern' 카테고리의 다른 글
C++ - 복합체, 컴포지트 패턴 (Composite Pattern) (1) | 2024.02.09 |
---|---|
C++ - 반복자, 이터레이터 패턴 (Iterator Pattern) (0) | 2024.02.04 |
C++ - 옵저버 패턴 (Observer Pattern) (1) | 2024.01.30 |
C++ - 전략, 스트래티지 패턴 (Strategy Pattern) (0) | 2024.01.29 |
아키텍처 & 디자인 패턴 용어 정리 (0) | 2024.01.26 |
댓글