[C/C++] 싱글톤 패턴(Singleton Pattern)

할랑말랑·2026년 3월 10일

C/C++

목록 보기
13/45

싱글톤 패턴(Singleton Pattern)

싱글톤 패턴의 핵심은 클래스의 인스턴스가 단 하나만 생성되도록 보장하고, 그 인스턴스에 대한 전역적인 접근점을 제공하는 패턴이다

  • 하나의 인스턴스 : 해당 클래스의 객체는 프로그램 전체를 통틀어 한개만 만들어진다.
  • 전역 접근 : 프로그램 어디서든 이 유일한 인스턴스에 접근할 수 있는 통일된 방법을 제공한다.

프로그램 전체에서 유일하게 존재해야 하며, 여러 곳에서 공유하고 참조해야 하는 자원(설정, 데이터베이스 연결, 로그 기록 등)을 관리할 때 싱글턴 패턴이 아주 유용하다

1. 구현 3가지 핵심 요소

  • 생성자를 private로 만든다.

오직 클래스 자신만이 생성자를 호출할 수 있게 한다.

  • 클래스 내부의 private static 선언으로 유일한 인스턴스를 만든다.

static 멤버는 클래스의 모든 객체가 공유하는 멤버이다. 프로그램이 시작될 때 메모리의 특정 공간(데이터 영역)에 딱 한번만 할당된다. 이 성질을 이용해 유일한 인스턴스를 저장하는 변수를 만든다.

  • 인스턴스에 접근할 수 있는 public static 함수를 제공한다.

생성자가 private 이므로 외부에서 객체를 얻을 방법이 없다. 따라서 유일한 인스턴스를 외부로 반환해 줄 함수가 필요하다.

2. 장점

  • 유일한 인스턴스 보장 : 메모리 낭비를 방지하고, 데이터 일관성을 유지할 수있다.
  • 전역 접근 : 어디서든 쉽게 접근할 수 있어 사용이 편리
  • 지연 초기화 : getInstance()가 처음 호출될 때 객체를 생성하게 하면, 프로그램 시작 시의 부담을 줄일 수 있다.

3. 단점

  • 전역 상태 : 전역 변수와 비슷한 문제를 가진다. 어디서든 접근하고 수정할 수 있기 때문에 데이터가 언제 어떻게 변했는지 추적하기 어려워지고, 코드 간의 의존성이 높아진다.
  • 테스트 : 단위 테스트를 수행하기 어렵다. 싱글톤 객체는 상태를 계속 유지하므로, 각 테스트가 서로에게 영향을 미칠 수 있다.
#include <iostream>

using namespace std;

class Airplane
{
private:
    static Airplane* instance; // 유일한 비행기 객체를 가리킬 정적 포인터
    int positionX;             // 비행기의 X 위치
    int positionY;             // 비행기의 Y 위치

    // private 생성자: 외부에서 객체 생성 금지
    Airplane() : positionX(0), positionY(0)
    {
        cout << "Airplane Created at (" << positionX << ", " << positionY << ")" << endl;
    }

public:
    // 복사 생성자와 대입 연산자를 삭제하여 복사 방지
    Airplane(const Airplane&) = delete;
    Airplane& operator=(const Airplane&) = delete;

    // 정적 메서드: 유일한 비행기 인스턴스를 반환
    static Airplane* getInstance()
    {
        // 인스턴스가 아직 없으면 새로 생성
        if (instance == nullptr)
        {
            instance = new Airplane();
        }
        // 이미 존재하면 기존 인스턴스를 반환
        return instance;
    }

    // 비행기 위치 이동
    void move(int deltaX, int deltaY)
    {
        positionX += deltaX;
        positionY += deltaY;
        cout << "Airplane moved to (" << positionX << ", " << positionY << ")" << endl;
    }

    // 현재 위치 출력
    void getPosition() const
    {
        cout << "Airplane Position: (" << positionX << ", " << positionY << ")" << endl;
    }
};

// Static member //
// static 키워드를 사용해 정의된 정적 멤버 변수는 클래스의 모든 인스턴스에서 공유된다
// 클래스의 모든 객체가 같은 메모리 위치를 참조
// 정적 멤버 변수는 클래스 내부에서 선언되지만, 클래스 외부에서 정의하고 초기화 할 수 있다.
Airplane* Airplane::instance = nullptr;

int main()
{
    // 유일한 비행기 인스턴스를 가져옴
    Airplane* airplane = Airplane::getInstance();
    airplane->move(10, 20); // 비행기 이동
    airplane->getPosition();

    // 또 다른 요청도 같은 인스턴스를 반환
    Airplane* sameAirplane = Airplane::getInstance();
    sameAirplane->move(-5, 10); // 비행기 이동
    sameAirplane->getPosition();

    return 0;
}
  • 비공개(private) 생성자 : Airplane() 생성자를 private으로 선언하여 클래스 외부에서 new Airplane()을 통해 객체를 생성하는 것을 원천적으로 차단한다.
  • 정적(static) 멤버 변수 : static Airplane* instance는 클래스 자체에 속하는 변수로, Airplane 객체가 몇 개가 있든(여기서는 하나겠지만) 이 변수는 단 하나만 존재, 이 변수가 유일한 인스턴스를 저장하는 역할을 한다.
  • 정적(static) 접근 메서드 : static Airplane* getInstance()는 유일한 인스턴스를 얻을 수 있는 유일한 창구이다. 이 메서드는 instance가 nullptr일 때(즉, 아직 아무도 인스턴스를 요청한 적이 없을 때) 단 한 번만 생성자를 호출하여 객체를 생성, 그 이후의 모든 getInstance() 호출은 이미 만들어진 동일한 객체의 주소를 반환한다.

복사 생성자 ,대입 연산자 삭제를 안할시

#include <iostream>

using namespace std;

class Airplane
{
private:
    static Airplane* instance; // 유일한 비행기 객체를 가리킬 정적 포인터
    int positionX;             // 비행기의 X 위치
    int positionY;             // 비행기의 Y 위치

    // private 생성자: 외부에서 객체 생성 금지
    Airplane() : positionX(0), positionY(0)
    {
        cout << "Created at (" << positionX << ", " << positionY << ")" << endl;
    }

public:
    // 복사 생성자와 대입 연산자를 삭제하여 복사 방지
    // Airplane(const Airplane&) = delete;
    // Airplane& operator=(const Airplane&) = delete;

    // 정적 메서드: 유일한 비행기 인스턴스를 반환
    static Airplane* getInstance()
    {
        if (instance == nullptr)
        {
            instance = new Airplane();
        }
        return instance;
    }

    // 비행기 위치 이동
    void move(int deltaX, int deltaY)
    {
        positionX += deltaX;
        positionY += deltaY;
        cout << "moved to (" << positionX << ", " << positionY << ")" << endl;
    }

    // 현재 위치 출력
    void getPosition() const
    {
        cout << "Position: (" << positionX << ", " << positionY << ")" << endl;
    }
};

// Static member //
// static 키워드를 사용해 정의된 정적 멤버 변수는 클래스의 모든 인스턴스에서 공유된다
// 클래스의 모든 객체가 같은 메모리 위치를 참조
// 정적 멤버 변수는 클래스 내부에서 선언되지만, 클래스 외부에서 정의하고 초기화 할 수 있다.
Airplane* Airplane::instance = nullptr;

int main()
{
    Airplane* airplane = Airplane::getInstance();
    airplane->move(10, 20);
    airplane->getPosition();

    // 복사 생성자
    Airplane newairplane = *airplane;
    // 대입 연산자
    Airplane newairplane2 = *Airplane::getInstance();
    // 새로운 객체 생성

    newairplane.move(20, 30);
    newairplane2.move(50, 100);

    newairplane.getPosition();
    newairplane2.getPosition();
    airplane->getPosition(); // 원본 싱글턴 객체의 위치는 변하지 않음

    return 0;
}
  • airplane,newairplane,newairplane2는 모두 다른 메모리 주소를 가진다.

  • newairplane,newairplane2에서 move값을 수정하더라도 airplane에는 값이 바뀌지않고 단 하나의 객체라는 싱글톤의 규칙이 깨진다.

  • C++11에서 도입된 기능 특정 함수를 명시적으로 삭제하는 것을 의미

  • 함수 끝에 = delete; 컴파일러에게 이 함수를 사용할 수 없게 만들어라 라고 명령하는 것

  • 만약 누군가 삭제된 함수를 호출하려고 하면, 컴파일 에러를 발생시킨다.

4. Meyers' Singleton (참조 반환)

#include <iostream>

using namespace std;

class SettingsManager
{
public:
    // 1. 반환 타입을 참조(&)로 변경
    static SettingsManager& getInstance()
    {
        // 2. 함수 내부에 static 객체를 선언 (C++11부터 Thread-safe)
        static SettingsManager instance;
        return instance;
    }

    // 복사 및 대입 방지
    SettingsManager(const SettingsManager&) = delete;
    void operator=(const SettingsManager&) = delete;

    void setVolume(int vol)
    {
        volume = vol;
    }

    int getVolume() const
    {
        return volume;
    }

private:
    // 생성자를 private
    SettingsManager() : volume(70)
    {
        cout << "SettingsManager 인스턴스 생성됨" << endl;
    }
    int volume;
};

int main()
{
    // 이렇게 사용하도록 강력하게 유도됨
    SettingsManager::getInstance().setVolume(95);
    cout << "현재 볼륨: " << SettingsManager::getInstance().getVolume() << endl;

    // 물론, 여전히 변수에 담을 수는 있습니다.
    SettingsManager& settings_ref = SettingsManager::getInstance();
    settings_ref.setVolume(100);

    cout << "참조를 통한 볼륨: " << SettingsManager::getInstance().getVolume() << endl;

    return 0;
}
  • 함수 지역 정적 변수 (Function-Local Static Variable)
    static SettingsManager instance; 코드가 getInstance() 함수 내부에 있다.
    지연 초기화 (Lazy Initialization) : 이 instance 객체는 프로그램 시작 시가 아니라, getInstance() 함수가 처음으로 호출되는 시점에 단 한 번만 생성되고 초기화된다.
    자동 소멸 : new를 사용하지 않았기 때문에 delete를 호출할 필요가 없다.
    스레드 안전성 (Thread-Safety) : C++11 표준부터, 함수 지역 정적 변수의 초기화는 스레드에 안전하도록 보장된다. 즉, 여러 스레드에서 동시에 getInstance()를 호출하더라도 instance가 여러 번 생성되는 문제 없이 정확히 단 한 번만 초기화

  • 참조(Reference) 반환
    getInstance()가 포인터(*) 대신 참조(&)를 반환
    안전성 : 사용자가 실수로 delete SettingsManager::getInstance() 같은 코드를 호출하여 객체를 파괴하는 것을 막을 수 있다.
    편의성 : 포인터 연산자 -> 대신 일반 객체처럼 . 연산자를 사용하여 멤버 함수에 접근할 수 있어 코드가 더 직관적이고 간결하다.

0개의 댓글