싱글톤(Singleton) 패턴은 “프로그램 전체에서 하나의 인스턴스만 존재해야 하는 클래스”를 구현할 때 사용하는 대표적인 디자인 패턴이다.

로그 관리자, 설정 관리자, 자원 풀(Resource Pool) 등 “전역적으로 통일된 접근점이 필요한 기능”에 자주 쓰인다.

싱글톤 구현 예시

정적 지역 변수 방식(Meyers Singleton)

C++11 이후 표준에서는 “정적 지역 변수를 초기화할 때” 쓰레드 안전성을 보장한다.

따라서 가장 간단하면서도 안전한 방법은 함수 스코프의 static 객체를 사용하는 것.

#include <iostream>

class Singleton
{
public:
    // 인스턴스에 접근하는 정적 함수
    static Singleton& getInstance()
    {
        // 첫 호출 시점에 한 번만 초기화되고, 쓰레드 안전.
        static Singleton instance;
        return instance;
    }

    // 예시 멤버 함수
    void doSomething() {
        std::cout << "Doing something with singleton!\n";
    }

private:
    // 생성자와 소멸자를 private으로 막아 외부에서 직접 생성/파괴하지 못하도록
    Singleton() { std::cout << "Singleton Constructor\n"; }
    ~Singleton() = default;

    // 복사 및 대입 연산자를 삭제해 복제 불가능하게 함
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
};

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

    // s1, s2는 동일 인스턴스를 가리킴
    s1.doSomething();
    s2.doSomething();
    
    return 0;
}

메모리 영역 관점

  • static Singleton instance는 정적(Static) 스토리지 수명을 갖지만, “함수 스코프의 정적 지역 변수”라는 점에서 첫 실행 시점에 할당 및 생성된다.
  • “static 지역 변수”는 데이터 영역(data segment)에 저장되거나, 컴파일러 구현에 따라 “정적 영역 어딘가”에 배치되지만, 실제 객체 생성(생성자 호출)은 “처음 getInstance()가 실행될 때” 이루어진다.
  • C++11 이후에는 정적 지역 변수의 초기화가 쓰레드 안전이므로, 멀티스레드 환경에서도 안전하게 초기화가 보장된다.

소멸 시점

  • 일반적으로 프로그램이 종료될 때(정확히는 정적 객체 소멸 단계) 소멸자가 호출.
  • 우리는 수동으로 delete할 수 없다.( private 생성자/소멸자 + 정적 객체 ).

장점

  • 구현이 간단, 쓰레드 안전, 사용하기 전까지 실제 생성이 미뤄짐(지연 초기화)

단점

  • “해제 타이밍”을 내가 제어하지 못한다.
  • 프로그램 종료 시에나 자동으로 소멸되므로, 수명 관리를 세밀하게 하기 어렵다.
  • 정적 객체의 소멸 순서 문제가 발생할 수도 있다.

힙(Heap) 할당 방식

“정적 포인터 + new” 방식이다. 이 경우 힙(Heap)에 할당되며, 싱글톤 포인터를 정적 멤버 변수로 관리한다.

class Singleton
{
public:
    static Singleton* getInstance()
    {
        // 더블 체크 락 등 멀티스레드 안전 장치 필요
        if (!instance)
        {
            instance = new Singleton();
        }
        return instance;
    }

    void doSomething() { /* ... */ }
    
    void release() {
        if (instance) { 
            delete instance;
            instance = nullptr;
        }
    }

private:
    Singleton() = default;
    ~Singleton() = default;

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

private:
    static Singleton* instance;  // 정적 포인터
};

// 정적 멤버 변수 정의
Singleton* Singleton::instance = nullptr;

소멸 시점

  • 사용자가 원할 때 delete instance;로 수동 해제할 수 있다.
  • 혹은 프로세스 종료 시 OS가 메모리를 가져가므로, “굳이 해제 안 해도” 시스템적으로 메모리 누수는 남지 않지만, C++ 객체의 소멸자 호출은 하지 않고 끝낼 수 있기에 주의가 필요.

장점

  • 원하는 시점에 수동으로 소멸할 수 있다.(예: release() 호출)
  • 어떤 경우엔 프로그램이 종료하기 전에 특정 자원을 깔끔히 정리해야 할 수도 있는데, 이 방식이면 가능.

단점

  • 멀티스레드 환경에서 초기화와 파괴가 안전하려면 동기화가 필요.
  • “정말로 하나만 존재”하는지 보장하기 위해서는 private 생성자를 지키고, 복사 대입 연산자도 막고, 접근을 엄격히 관리해야 한다.

태그:

카테고리:

업데이트:

댓글남기기