본문 바로가기

프로그래밍/디자인패턴

[디자인패턴] 싱글턴 패턴(Singleton Patterns)-이론

 

 

목 차

1.  싱글턴 패턴(Singleton Patterns) 란     (해당 글로 이동)

2.  싱글턴 패턴(Singleton Patterns) 예제     (해당 글로 이동)

3.  싱글턴 패턴(Singleton Patterns) 문제점 (소멸자)     (해당 글로 이동)

4.  싱글턴 패턴(Singleton Patterns) 문제점 (멀티스레드)     (해당 글로 이동)

5.  싱글턴 패턴(Singleton Patterns) 대안     (해당 글로 이동)

 

 

싱글턴 패턴(Singleton Patterns)란

싱글턴 패턴(Singleton Patterns)란
오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공합니다. (GoF의 디자인 패턴) (GoF의 디자인 패턴)

싱글턴 패턴의 특징은 다음과 같다.

  • 오직 한 개의 클래스 인스턴스만 갖도록 보장
  • 전역 접근점을 제공
  • 한 번도 사용하지 않는다면 아예 메모리를 할당하지 않아서 메모리를 아낄 수 있다.(게으른 초기화)
  • 코드를 이해하기 어렵고 유지보수가 힘들다. Singleton::Getinstance() 같은 코드가 있다면 전체 코드에서 이 함수를 접근하는 모든 코드를 살펴야 한다
  • 전역 변수는 커플링(결합)을 조장한다.
  • 전역 변수는 멀티스레딩 같은 동시성 프로그래밍에 알맞지 않다.

 

 

싱글턴 패턴(Singleton Patterns) 예제

 

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
#include<iostream>
 
class Singleton {
private:
    static Singleton* instance_;
    Singleton() {}
    ~Singleton() {}
 
public:
    static Singleton* GetInstacne() {
        //게으른 초기화
        if (instance_ == nullptr) {
            instance_ = new Singleton();
        }
        return instance_;
    }
    void Function()const {
        std::cout << "Instance Function\n";
    }
};
 
Singleton* Singleton::instance_ = nullptr;
 
int main() {
    Singleton* handle = Singleton::GetInstacne();
 
    handle->Function();
}
 

 

싱글턴을 사용한 간단한 예제이다. instance_ 정적 멤버 변수는 클래스 인스턴스를 저장한다. 생성자와 소멸자가 Private

이기 때문에 밖에서는 생성할 수 없다. public에 있는 GetInstance() 정적 메서드는 코드 어디에서나 싱글턴 인스턴스에

접근할 수 있게 하고, 싱글턴을 실제로 필요로 할 때까지 인스턴스 초기화를 미루는(게으른 초기화)를 하고 있다.

 

 

싱글턴 패턴(Singleton Patterns) 문제점(소멸자)

위 예제는 소멸자를 호출할 수 없는 메모리 릭(Memory leak)이 발생하는 예제이다.  싱글턴은 한 개의 인스턴스만을 보장하기 때문에 메모리가 부족할 일은 없지만 프로젝트를 진행하다 보면 싱글턴의 메모리를 해제해야 할 때 가 있을 수 있다.

 

그렇다면 어떻게 소멸자를 호출해야 할까?

이때 사용할 수 있는 함수가 atexit() 함수이다. atexit()는 함수 포인터를 매개변수로 받는 함수로 써 프로그램이 종료될 때 호출되는 함수이다.

 

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
29
30
31
32
33
34
35
36
37
#include<iostream>
class Singleton {
private:
    static Singleton* instance_;
    Singleton() {}
    ~Singleton() {}
 
public:
    static Singleton* GetInstacne() {
        //게으른 초기화
        if (instance_ == nullptr) {
            instance_ = new Singleton();
            atexit(DeleteInstance);
        }
        return instance_;
    }
    static void DeleteInstance() {
        if (instance_ != nullptr) {
            delete instance_;
            instance_ = nullptr;
            std::cout << "Delete Instance \n";
        }
    }
 
    void Function()const {
        std::cout << "Instance Function\n";
    }
};
 
Singleton* Singleton::instance_ = nullptr;
 
int main() {
    {
        Singleton* handle = Singleton::GetInstacne();
        handle->Function();
    }
}
 

 

 

명시적으로 호출도 가능하지만 이 와 같이 Scope에서 벗어나면 자동으로 호출되는 모습을 볼 수 있다.

 

싱글턴 패턴(Singleton Patterns) 문제점(멀티스레드)

싱글턴 패턴의 또 다른 문제는 멀티스레드 환경에서 오류가 발생한다는 것이다.

 

Thread A가 먼저 Getinstance() 함수를 통해 메모리를 할당하고자 하고 있다고 해보자 Thread A가 메모리 할당받기 직전에 OS의 스케줄링으로 인해 문맥 교환(Context Switcing)이 일어났다고 해보자 그러면 이제 CPU는 Thread B에게 할당이 되었고 Thread B는 Getinstance()함수에서 메모리까지 할당을 받고 문맥 교환(Context Switcing)이 일어나면 Thread A가 다시 메모리를 할당 받는 문제가 발생할 수 있다.

 

이러한 문제를 예방하기 위해서는

1. DCLP(Double Checked Locking pattern)

2. std::call_once()와 std::once_flag

이렇게 가 있다 이 중에서 DCLP는 Thread safe 하지 않다는 이슈가 있어서 2번 방법으로 문제를 해결하고자 한다.

 

std::call_once()는 C++11부터 생긴 표준함수이다. 이 함수는 한번만 구동이 가능하도록 하는 함수이며 스레드 환경에서도 한번만 동작을 보장한다. std::once_flag도 이와 동일한 함수로 std::call_once()와 같이 사용된다.

 

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include<iostream>
#include<thread>
#include<vector>
#include<mutex>
 
class Singleton {
private:
    static Singleton* instance_;
    static std::once_flag once_flag;
    Singleton() {}
    ~Singleton() {}
 
public:
    static Singleton* GetInstacne() {
 
        //한번만 실행
        std::call_once(once_flag, []() {
            if (instance_ == nullptr) {
                instance_ = new Singleton();
                std::cout << "Getinstance(): "<< &(*instance_) <<"\n";
                atexit(DeleteInstance);
            }
        });
 
        return instance_;
    }
    static void DeleteInstance() {
        if (instance_ != nullptr) {
            delete instance_;
            instance_ = nullptr;
            std::cout << "Delete Instance \n";
        }
    }
    void Function()const {
        std::cout << "Instance Function:"<< &(*instance_) <<"\n";
    }
};
 
Singleton* Singleton::instance_ = nullptr;
std::once_flag Singleton::once_flag;
std::mutex lock;
 
void TestThread() {
 
    Singleton* handle = Singleton::GetInstacne();
    lock.lock();
    handle->Function();
    lock.unlock();
}
 
int main() {
 
   std::vector<std::thread>threads;
 
   for (int i = 0; i < 100++i) {
       threads.emplace_back(std::thread(TestThread));
   }
 
   for (auto& i : threads) {
       i.join();
   }
}
 
 

 

위와 같이 한 개의 인스턴스를 가지는 것을 멀티 스레드 환경에서 보장할 수 있다. 하지만 생성만 보장할 뿐 실행까지는 아니다. 여러 스레드가 한개의 공유 자원을 가지고 있는 상태에서 상호 배제는 필수라고 생각하며 이걸 실용적으로 사용하기에는 좀 무리가 있지 않을까 라는 생각이 든다.

 

 

싱글턴 패턴(Singleton Patterns)의 대안

이러한 특징들로 인해 득보다는 실이 많은 패턴이며 실제로 많은 프로그래머가 남용하고 있는 패턴이라고 한다.

이러한 싱글턴을 대신할 수 있는 대안들은 다음과 같다.

 

1. 클래스가 꼭 필요한가?

 

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
29
30
31
32
33
class Bullet {
private:
    int x_;
    int y_;
public:
    void SetX(const int x) {
        x_ = x;
    }
    void SetY(const int y) {
        y_ = y;
    }
    int GetX()const {
        return x_;
    }
    int GetY()const {
        return y_;
    }
 
};
 
class BulletManager {
private:
public:
    Bullet* CreateBullet(const int x,const int y) {
        Bullet* bullet = new Bullet();
        bullet->SetX(x);
        bullet->SetY(y);
        return bullet;
    }
    void Move(Bullet* bullet) {
        bullet->SetX(bullet->GetX() + 5);
    }
};
 

 

싱글턴 클래스 중에는 애매하게 다른 객체 관리용으로만 존재하는 객체 관리자가 많다. 이러한 관리 클래스가 필요할 때도 있지만 OOP를 제대로 이해하지 못해 만드는 경우도 많다. 언뜻 보면 BulletManager를 싱글턴으로 만들어야겠다는 생각이 들 수 있지만 아래와 같이 만들면 싱글턴을 사용할 필요가 없다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Bullet {
private:
    int x_;
    int y_;
public:
    void SetX(const int x) {
        x_ = x;
    }
    void SetY(const int y) {
        y_ = y;
    }
    int GetX()const {
        return x_;
    }
    int GetY()const {
        return y_;
    }
    void Move() {
        x_ += 5;
    }
};
 

 

관리자 클래스를 없애고 나니 문제도 같이 없어졌다. 관리자 클래스를 만들기보다는 대도록이면 원래 클래스로 옮기는 것이

좋다. 객체가 스스로 자기를 챙기는 게 OOP이다.

 

2. 오직 한 개의 인스턴스만 갖도록 보장하기

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<assert.h>
class System {
public:
    System() {
        assert(uniqueInstance == true);
        uniqueInstance = false;
    }
 
    static bool uniqueInstance;
};
 
bool System::uniqueInstance = true;
 
 
int main() {
    System s;
    System s1;
 
}
 

이 클래스는 어디서나 인스턴스를 생성할 수 있지만 인스턴스가 둘 이상 되는 문제를 assert()을 통해 방지한다.

이 외의 대안은 책을 구매 후 살펴보면 좋을 거 같다.

 

후기

싱글턴 패턴에 대해서 알아보았다.  이번 패턴은 따로 게임을 만들어 진행하지 않고 이론적인 부분과 문제점에 대해서 작성하였다.

싱글턴은 패턴을 공부하지 않아도 사용할 수 있을 정도로 진입장벽이 낮은 패턴이다 나 조차도 패턴을 몰랐을 때에는 싱글턴을 어느 블로그에서 참고 후 바로 사용했을 정도이기 때문이다. 그러한 이유 때문에 남용과 문제점이 있는 패턴이며 그러한 방법의 해결방법과 대안을 작성했다.  대안은 출처에 있는 책에 더 많은 내용과 방법이 있으니 구매 후 참고하면 좋을 거 같다.

 

출처 및 소스코드 링크

atexit(): https://docs.microsoft.com/ko-kr/cpp/c-runtime-library/reference/atexit?view=vs-2019

std::call_once: https://en.cppreference.com/w/cpp/thread/call_once

Book: goF의 디자인 패턴(프로젝트 미디어), 게임 프로그래밍 패턴(한빛미디어)

 

 

* 오타 및 틀린 점 알려주시면 고치겠습니다.