메인 콘텐츠로 건너뛰기

고독한 통치자: C++로 구현하는 싱글톤 패턴

2025년 12월 30일
c++
고독한 통치자: C++로 구현하는 싱글톤 패턴 - 아티클 커버 이미지

고독한 통치자: C++로 구현하는 싱글톤 패턴

전세계의 중심을 하나의 나라로 통일하려는 시도는 고대부터 있어 왔습니다. 길을 잃은 항해사에게는 수만 개의 별보다 단 하나의 ‘북극성’이 절실하듯, 소프트웨어의 세계에서도 혼란을 잠재울 단 하나의 기준점이 필요할 때가 있죠. 로그를 기록하는 시스템, 환경 설정을 관리하는 주체, 혹은 시스템의 자원을 총괄하는 관리자가 바로 그 북극성의 역할을 수행합니다.

하지만 권력이 한 곳으로 집중되면 부작용이 따르기 마련입니다. 싱글톤 패턴은 우리에게 ‘편리한 전역성’이라는 달콤한 유혹을 건네는 동시에, ‘결합도’라는 무거운 멍에를 씌우기도 하죠. 과연 이 고독한 통치자를 우리는 어떻게 다뤄야 할까요?

1. 싱글톤의 정의: 오직 하나뿐인 존재

싱글톤 패턴의 정의는 명확합니다. **“어떤 클래스의 인스턴스가 오직 하나임을 보장하고, 이에 대한 전역적인 접근점을 제공하는 것”**이죠.

아마도 여러분은 “그냥 전역 변수를 쓰면 되지 않을까?”라고 생각하실 수도 있어요. 하지만 전역 변수는 누군가 실수로 다시 초기화하거나, 원치 않는 시점에 생성되는 것을 막을 방법이 없습니다. 싱글톤은 ‘생성의 통제권’을 클래스 내부로 가져옴으로써, 존재의 유일성을 스스로 증명합니다.

2. C++로 구현하는 현대적 싱글톤 (Meyers' Singleton)

C++11 이전에는 스레드 안전성(Thread-Safety)을 확보하기 위해 복잡한 이중 잠금(Double-Checked Locking) 기법을 동원해야 했습니다. 하지만 현대 C++는 훨씬 우아한 해법을 제공합니다. 바로 스콧 마이어스가 제안한 ‘지역 정적 객체’를 활용하는 방식이죠.

image

#include <iostream>
#include <string>
#include <mutex>

using namespace std;

// [전략적 자산 관리자] 시스템의 단 하나뿐인 설정 관리자입니다.
class ConfigurationManager {
private:
    // 1. 생성자를 숨깁니다. 외부에서 'new'로 통치자를 복제할 수 없게 말이죠.
    ConfigurationManager() {
        cout << ">> [시스템] 중앙 설정 관리자가 임무를 시작합니다." << endl;
    }

    // 2. 복사와 대입을 금지합니다. 유일무이한 존재는 복제될 수 없으니까요.
    ConfigurationManager(const ConfigurationManager&) = delete;
    ConfigurationManager& operator=(const ConfigurationManager&) = delete;

    string appName = "MyGlobalApp";
    int maxConnections = 100;

public:
    // 3. 유일한 접근점입니다. C++11부터는 이 방식이 스레드로부터 안전합니다.
    static ConfigurationManager& getInstance() {
        static ConfigurationManager instance; // 단 한 번만 생성되는 정적 지역 객체
        return instance;
    }

    void showConfig() {
        cout << ">> 현재 앱 이름: " << appName << endl;
        cout << ">> 최대 연결 수: " << maxConnections << endl;
    }

    void updateConfig(string name, int connections) {
        appName = name;
        maxConnections = connections;
        cout << ">> [시스템] 설정이 변경되었습니다." << endl;
    }
};

3. 빛과 그림자: 왜 싱글톤은 ‘안티 패턴’이라 불리는가?

싱글톤은 한때 모든 문제의 해결사처럼 보였습니다. 어디서든 getInstance()만 외치면 필요한 객체에 닿을 수 있었으니까요. 하지만 이 편리함은 독이 든 성배와 같습니다.

image

  • 전역 상태의 저주: 싱글톤은 전역 변수와 다를 바 없습니다. 코드 어디서든 상태를 바꿀 수 있다는 것은, 디버깅 시 “누가 이 값을 바꿨지?”라는 미궁에 빠지게 함을 의미하죠.
  • 테스트의 어려움: 앞서 우리가 공부했던 ‘의존성 주입’과 정반대의 길을 걷습니다. 객체가 싱글톤에 강하게 결합되어 있으면, 테스트용 가짜 객체(Mock)로 교체하기가 매우 까다로워집니다.
  • 야만적인 결합: 클래스 내부에서 싱글톤을 직접 호출하는 순간, 그 클래스는 싱글톤 없이는 존재할 수 없는 몸이 됩니다. 이는 우리가 그토록 경계했던 ‘강결합’의 전형입니다.

4. 확장: 싱글톤과 의존성 주입의 타협안

그렇다면 우리는 싱글톤을 버려야 할까요? 물론 아닙니다. 중요한 것은 ‘존재의 유일성’은 유지하되, ‘전역적인 접근’은 제한하는 것입니다. 여기서 지난 시간에 배운 **의존성 주입(DI)**이 다시 등장합니다. 싱글톤 인스턴스를 직접 가져오는 대신, 생성자를 통해 주입받는 것이죠.

image

// 싱글톤을 사용하지만, 의존성을 주입받는 클래스
class NetworkService {
private:
    ConfigurationManager& config; // 주입받은 설정 관리자

public:
    NetworkService(ConfigurationManager& cfg) : config(cfg) {}

    void start() {
        cout << ">> 서비스를 시작합니다. 설정값: ";
        config.showConfig();
    }
};

int main() {
    // 1. 싱글톤 인스턴스를 가져옵니다.
    ConfigurationManager& mainConfig = ConfigurationManager::getInstance();

    // 2. 주입(Injection)을 통해 사용합니다. 
    // 이렇게 하면 NetworkService는 내부에서 싱글톤을 직접 호출하지 않아도 됩니다.
    NetworkService service(mainConfig);
    service.start();

    return 0;
}

현명한 통치자를 위한 제언

싱글톤 패턴은 설계의 ‘독재자’와 같습니다. 질서가 무너진 혼돈의 시스템에서는 명확한 기준이 되어주지만, 그 권력이 비대해지면 시스템의 유연성을 갉아먹고 변화를 거부하는 야만적인 코드를 만들어냅니다.

과연 여러분의 시스템에 정말로 ‘단 하나’의 객체만 필요한지 다시 한번 자문해 보세요. 혹시 그저 전역 변수의 편리함을 위해 싱글톤이라는 이름을 빌리고 있는 것은 아닌가요?

결국 중요한 것은 **“얼마나 많은 객체가 이 고독한 통치자를 의존하고 있는가”**를 파악하는 일입니다. 싱글톤을 사용하더라도 의존성 주입의 형식을 빌려 그 범위를 제한한다면, 여러분은 통치자의 지혜와 시스템의 자유를 동시에 누릴 수 있을 거예요. 이 섬세한 균형 잡기가 바로 숙련된 설계자가 가야 할 길일지도 모르겠습니다. 여러분의 코드 속 통치자는 지금 어떤 모습인가요?

공유하기