[Design Pattern] 싱글톤 패턴 더 알아보기 - 1

Eager Initialization Singleton

앞선 싱글톤 패턴에서 제공한 싱글톤 패턴은 문제가 있다. 실제 인스턴스를 사용하기 전에 싱글톤 인스턴스가 생성이 되어버리는 문제가 있다. 이를 이른 초기화 방식 싱글톤이라 하며 인스턴스가 미리 생성됨을 확인하는 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class EagerInitializationSingleton
{
static EagerInitializationSingleton* instance;
protected:
EagerInitializationSingleton()
{
std::cout<<"EagerInitializationSingleton Instance Constructed"<<std::endl;
}
public:
static EagerInitializationSingleton* getInstance();
static void someStaticFunction();
};

EagerInitializationSingleton* EagerInitializationSingleton::instance = new EagerInitializationSingleton();

EagerInitializationSingleton* EagerInitializationSingleton::getInstance()
{
return instance;
}

void EagerInitializationSingleton::someStaticFunction()
{
std::cout<<"EagerInitializationSingleton Some Static Function Called"<<std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class EagerInitializationSingleton {
private static EagerInitializationSingleton instance = new EagerInitializationSingleton();
private EagerInitializationSingleton() {
System.out.println("EagerInitializationSingleton Constructor Called");
}

public static EagerInitializationSingleton getInstance() {
return instance;
}
public static void someStaticMethod() {
System.out.println("Some Static Method Called");
}
}
1
2
EagerInitializationSingleton Instance Constructed
EagerInitializationSingleton Some Static Function Called

static 함수를 사용하기위해 초기화 하는 과정에서 인스턴스가 생성되어 버린다. 이런 방식은 thread-safe하나 실제 인스턴스 사용 유무에 상관없이 메모리를 점유하기 때문에 비효율적이다.

Lazy Initialization Singleton

이는 앞선 이른 초기화 방식을 개선하기 위한 싱글톤 인스턴스 생성 방법이다. 인스턴스 생성을 실제 인스턴스가 필요해지는 시점까지 미룰 수 있다. 이 방식을 늦은 초기화 방식 싱글톤이라 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class LazyInitializationSingleton
{
static LazyInitializationSingleton* instance;
protected:
LazyInitializationSingleton()
{
std::cout<<"LazyInitializationSingleton Instance Constructed"<<std::endl;
}
public:
static LazyInitializationSingleton* getInstance();
static void someStaticFunction();
};

LazyInitializationSingleton* LazyInitializationSingleton::instance = nullptr;

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

void LazyInitializationSingleton::someStaticFunction()
{
std::cout<<"LazyInitializationSingleton Some Static Function Called"<<std::endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazyInitializationSingleton {
private static LazyInitializationSingleton instance;
private LazyInitializationSingleton() {
System.out.println("LazyInitializationSingleton Constructor Called");
}

public static LazyInitializationSingleton getInstance() {
if(instance == null)
instance = new LazyInitializationSingleton();

return instance;
}
public static void someStaticMethod() {
System.out.println("Some Static Method Called");
}
}
1
2
LazyInitializationSingleton Some Static Function Called
LazyInitializationSingleton Instance Constructed

이 방식은 싱글톤 인스턴스가 필요해지는 시점까지 생성을 미룰 수 있다. 하지만 이 방식은 thread-unsafe라는 문제가 다시 생겼다.

Thread Safe Lazy Initialization

이 방식은 앞선 방식에 thread-safe함을 더했다. 이 방식을 스레드 안전 늦은 초기화 방식 싱글톤이라 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ThreadSafeLazyInitializationSingleton
{
static ThreadSafeLazyInitializationSingleton* instance;
static std::mutex Mutex;
protected:
ThreadSafeLazyInitializationSingleton()
{
std::cout<<"ThreadSafeLazyInitializationSingleton Instance Constructed"<<std::endl;
}
ThreadSafeLazyInitializationSingleton(const ThreadSafeLazyInitializationSingleton&) = delete;
ThreadSafeLazyInitializationSingleton* operator=(const ThreadSafeLazyInitializationSingleton*) = delete;
public:
static ThreadSafeLazyInitializationSingleton* getInstance();
};

ThreadSafeLazyInitializationSingleton* ThreadSafeLazyInitializationSingleton::instance = nullptr;

ThreadSafeLazyInitializationSingleton* ThreadSafeLazyInitializationSingleton::getInstance()
{
std::lock_guard<std::mutex> lock(Mutex);
if(instance == nullptr)
instance = new ThreadSafeLazyInitializationSingleton();
return instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadSafeLazyInitializationSingleton {
private static ThreadSafeLazyInitializationSingleton instance;
private static Lock lock = new ReentrantLock();
private ThreadSafeLazyInitializationSingleton() {
System.out.println("ThreadSafeLazyInitializationSingleton Constructor Called");
}

public static ThreadSafeLazyInitializationSingleton getInstance() {
lock.lock();
if(instance == null)
instance = new ThreadSafeLazyInitializationSingleton();
lock.unlock();

return instance;
}
}

이 방식은 인스턴스 생성을 원하는 시점까지 미룰 수 있다. 또한 동시성에 대해서도 안전하다. 즉 thread-safe하다. 하지만 다른 문제가 있다. 매번 인스턴스를 얻을 때 인스턴스가 생성되어있든 되어있지 않든 스레드들은 락을 획득하기 위해 대기를 할 것이며 이는 성능 저하를 불러온다. 자바의 경우 synchronized 키워드를 사용하면 더욱 끔찍해진다. 객체 기반 locking이 되어버리기 때문에 따라오는 추가적 성능 저하는 덤이다.

Thread Safe Double Locking Lazy Initialization

이 방식은 앞선 방식에서 인스턴스가 생성이 되었다면 락을 획득하지 않고 인스턴스를 반환하는 방식이다. 이 싱글톤은 thread-safe하며 원하는 시점에 인스턴스를 생성할 수 있고 동시 접근에 따른 성능 저하를 낮추는 효과가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ThreadSafeDoubleLockingLazyInitialization
{
static ThreadSafeDoubleLockingLazyInitialization* instance;
static std::mutex Mutex;
protected:
ThreadSafeDoubleLockingLazyInitialization()
{
std::cout<<"ThreadSafeDoubleLockingLazyInitialization Instance Constructed"<<std::endl;
}
ThreadSafeDoubleLockingLazyInitialization(const ThreadSafeDoubleLockingLazyInitialization&) = delete;
ThreadSafeDoubleLockingLazyInitialization* operator=(const ThreadSafeDoubleLockingLazyInitialization*) = delete;
public:
static ThreadSafeDoubleLockingLazyInitialization* getInstance();
};

ThreadSafeDoubleLockingLazyInitialization* ThreadSafeDoubleLockingLazyInitialization::instance = nullptr;

ThreadSafeDoubleLockingLazyInitialization* ThreadSafeDoubleLockingLazyInitialization::getInstance()
{

if(instance == nullptr)
{
std::lock_guard<std::mutex> lock(Mutex);
if(instance == nullptr)
instance = new ThreadSafeDoubleLockingLazyInitialization();
}
return instance;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadSafeDoubleLockingLazyInitializationSingleton {

private static ThreadSafeDoubleLockingLazyInitializationSingleton instance;
private static Lock lock = new ReentrantLock();
private ThreadSafeDoubleLockingLazyInitializationSingleton() {
System.out.println("ThreadSafeDoubleLockingLazyInitializationSingleton Constructor Called");
}

public static ThreadSafeDoubleLockingLazyInitializationSingleton getInstance() {
lock.lock();
if(instance == null){
lock.lock();
if(instance == null)
instance = new ThreadSafeDoubleLockingLazyInitializationSingleton();
lock.unlock();
}

return instance;
}
}
Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/13/Design%20Pattern/IntermediateSingleton1/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.