KimMK

싱글턴(Singleton) 패턴 본문

C++/디자인 패턴

싱글턴(Singleton) 패턴

KimMK 2023. 3. 12. 18:31

디자인패턴 중 생성패턴인 싱글턴은 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며 생성된 객체를 어디에서든지 참조할 수 있도록 하는 디자인패턴이다.

오직 하나의 객체만을 생성할 수 있는 클래스를 의미한다. 따라서 단일 인스턴스를 생성해 공유해서 사용해야 하는 상황에서 유용하다.

싱글턴을 정교하게 구현하고 사용한다면 객체 유일성을 보장하고 어디에서나 싱글턴 객체에 접근할 수 있는 접근성의 장점이 있지만, 득보다 실이 많은 패턴이므로 유의하자!!

 

 

싱글턴을 왜 사용할까? (장점)

  • 객체를 하나만 생성해서 공유하기 때문에 메모리 낭비를 방지할 수 있다. 또한, 한 번도 사용하지 않는다면 아예 인스턴스 자체를 생성하지 않는다. 예를 들어 DB연결, NW연결, 파일 입출력 등과 같은 리소스를 사용하는 경우 여러 객체가 이를 생성하면 자원 낭비가 심해질 수 있다. 이를 방지하기 위해 하나의 객체만 생성해서 공유하는 방식을 선택하는 것이다.
  • 전역 변수보다 안전한 방식으로 객체 인스턴스를 제공한다. 전역 변수는 여러 곳에서 접근 가능하기 때문에 의도치 않게 값을 변경할 수 있다. 하지만 싱글턴 패턴을 사용하면 객체 인스턴스를 전역 변수 대신 메서드를 통해 접근하므로 더 안전하다.

 

 

득보다 실이 많은 이유 (단점)

  • 싱글턴 객체가 전역적인 상태를 유지하므로, 다른 객체와의 결합도가 높아지게 된다. 프로그램의 결합도가 높아지면 테스트나 리팩토링 등의 작업을 어렵게 만들 수 있다. 즉, 테스트를 하기 어렵고 변경에 취약해진다.
  • 멀티 스레딩과 같은 동시성 프로그래밍에 알맞지 않다. 전역으로 만들면 모든 스레드에게 노출되고 수정할 수 있는 메모리 공간이 생긴 것이다. 다른 스레드가 전역 데이터에 어떤 작업을 하는지 모를 때도 있다. 그렇기 때문에 교착 상태(Dead Lock), 경쟁 상태(Race Condition)과 같은 동기화 버그가 생기기 쉽다.
  • 싱글턴 객체를 초기화 하는 방법으로 게으른 초기화(Lazy Initialization)는 제어할 수 없다. 게임 프로그래밍에서 이를 적용한다고 했을 때, 시스템을 초기화할 때 메모리 할당, 리소스 로딩 등 시간이 걸릴 수 있다. 만약 오디오 시스템 초기화 시간이 많이 소요된다면 초기화 시점을 제어해줘야 한다. 게으른 초기화를 하게 만들면 처음 소리가 재생될 때 게임 내에서 초기화가 시작되는 바람에 프레임이 떨어질 수 있다.
    또한, 게임에서 메모리 단편화를 막기 위해 힙에 메모리를 할당해 세밀하게 제어하는데 시스템 초기화를 할 때 상당한 메모리를 힙에 할당하면, 힙의 어디에 할당할 지 제어할 수 있도록 적절한 초기화 시점까지 찾아야 한다.

 

따라서, 반드시 싱글턴이 필요한 상황이 아니라면 지양하는 것이 좋다.

 

 

간단한 예시

싱글턴을 구현하는 일반적인 방법은 정적 멤버 변수와 정적 메서드를 사용하는 것이다.

class Singleton {
private:
    static Singleton* instance;  // 정적 멤버 변수로 싱글턴 객체의 포인터를 저장
    Singleton() {}  // 생성자를 private로 만들어 외부에서 객체를 생성하는 것을 방지

public:
    static Singleton* getInstance() {  // 정적 메서드로 싱글턴 객체의 포인터를 반환
        if (instance == nullptr) {  // 객체가 아직 생성되지 않은 경우, 생성
            instance = new Singleton();
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;  // 정적 멤버 변수를 초기화

int main() {
    Singleton* obj1 = Singleton::getInstance();  // 첫 번째 객체를 생성
    Singleton* obj2 = Singleton::getInstance();  // 이미 생성된 객체를 가져옴
    assert(obj1 == obj2);  // 두 객체가 동일한 주소를 가리키는지 검증
}

 

 

멀티 스레드 환경을 위한 Double checked Locking과 Lazy Initialization을 이용한 구현

#include <mutex>

class Singleton {
private:
    static Singleton* instance;
    static std::mutex mutex;
    Singleton() {}
    
public:
    static Singleton& getInstance() {
        // double-checked locking을 사용한 lazy initialization
        if (!instance) {
            std::lock_guard<std::mutex> lock(mutex);
            if (!instance) {
                instance = new Singleton();
            }
        }
        return *instance;
    }
};

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

lazy initialization을 사용하면, getInstance()메서드를 호출한 시점에 정적 필드가 아직 초기화되지 않았으면 객체를 생성하고, 그 후에는 전에 생성한 객체의 참조를 그대로 반환한다.

Double Checked Locking을 사용해 멀티 스레드 환경에서 getInstance()를 여러 스레드가 동시에 호출했을 때 하나의 스레드만 접근할 수 있도록 동기화를 해준다. 처음 getInstance()가 호출될 때는 인스턴스가 생성되지 않았으므로, mutex를 사용해 스레드 안전성을 보장한 후에 인스턴스를 생성하고 이후에는 인스턴스가 이미 생성되어 있으므로 mutex를 사용하지 않고 바로 인스턴스를 반환한다.

'C++ > 디자인 패턴' 카테고리의 다른 글

상태(State) 패턴  (0) 2023.03.19
관찰자(Observer) 패턴  (0) 2023.03.14
경량(Flyweight) 패턴  (1) 2023.03.11
디자인 패턴  (0) 2023.03.11
명령(Command) 패턴  (0) 2023.03.11