Language/C++

[C++] 싱글톤 패턴 (1) with C++

nowkoes 2023. 8. 9. 00:00

Singleton Pattern

개요

 프로젝트 진행 중, 특정 클래스가 실행 동안 한 번만 초기화되어야 하는 상황이 발생할 때가 있다. 예를 들어, MySQL과의 연결을 관리하는 클래스는 여러 번 생성될 경우 메모리와 같은 자원의 낭비가 초래될 수 있으며, 데이터 일관성 또한 손상될 가능성이 있다.

 

출처: 위키피디아

 

 다수의 프로그래밍 언어에서는 이와 같은 상황을 대비하여 싱글톤 패턴(Singleton Pattern)을 도입하였다. 이 패턴의 핵심은 애플리케이션 내에서 해당 클래스의 인스턴스가 유일하게 하나만 존재하도록 보장하는 것이다. 이를 통해 앞서 언급된 문제점을 해결할 수 있으며, 전역적으로 참조 가능한 액세스 포인트를 제공하여 다른 객체들이 이를 쉽게 활용할 수 있게 된다.

 

 그러나, 이 패턴의 도입은 전역 상태를 생성하게 되어 시스템의 동작 예측이 어려워질 수 있다는 단점을 가진다. 여기서 전역 상태란 프로그램의 전체 영역에서 접근 가능한 데이터나 상태를 의미한다. 특히 단위 테스트 환경에서는 여러 테스트 간의 상태 간섭으로 인한 문제가 발생할 가능성이 있다. 이러한 패턴의 특성을 충분히 이해하고, 상황에 맞게 적절히 적용하는 것이 필요하다.


구현

 싱글톤 패턴을 생성하는 방법은 크게 4단계로 분류할 수 있다.

 

  1. 생성자를 private으로 변경
  2. 복사 생성자와 복사 대입 연산자 삭제
  3. 정적 멤버 변수로 자기 자신의 인스턴스를 가리키는 포인터 선언
  4. 공개된 정적 함수를 선언하여 이를 통해 인스턴스에 접근하게 함

 

 지금부터 주어진 클래스를 싱글톤으로 디자인해 보자.

 

class DB
{
    MYSQL _mysql;

public:
    DB();
    ~DB();
};

DB::DB()
{
    mysql_init(&_mysql);

    if (!mysql_real_connect(&_mysql, "localhost", "root", "password", "example", 3306, NULL, 0))
    {
        std::cerr << "Failed to connect to MySQL\n";
        return;
    }
    
    else
        std::cout << "Successfully Connected with MySQL\n";    
}

DB::~DB()
{
    mysql_close(&_mysql);
}

 다음과 같이 MySQL과 visual studio를 연동하는 DB 클래스가 있다고 가정해 보자. 

 

class DB
{
    MYSQL _mysql;
    static DB* _instance;
    DB();
    
public:
	...
};

1. 생성자를 private으로 변경

 생성자의 접근 지정자를 private으로 설정하여, 클래스 외부에서 new 연산자를 통해 객체를 직접 생성할 수 없게 막아둔다. 이렇게 함으로써 인스턴스 생성을 제한하고, 클래스 내부의 특정 메서드에서만 인스턴스를 생성할 수 있게 한다.

 

class DB
{
    ...

public:
    DB(const DB&) = delete;
    DB& operator = (const DB&) = delete;
    ...
};

2. 복사 생성자와 복사 대입 연산자 삭제

 여기서 복사 생성자(Copy Constructor)란 객체가 다른 객체로부터 초기화될 때 호출되는 생성자를 의미하고, 복사 대입 연산자(Copy Assignment Operator)는 객체에 다른 객체의 값을 할당할 때 사용되는 연산자다. 이에 대한 내용은 추후에 자세히 다루게 하고, 싱글톤 패턴의 원칙으로 인해 인스턴스를 복제하는 것을 막았다는 정도로 알고 넘어가자.

 

class DB
{
    ....
    static DB* _instance;
    
public:
	...
};

DB* DB::_instance = nullptr;

3. 정적(static) 멤버 변수로 자기 자신의 인스턴스를 가리키는 포인터 선언

 인스턴스는 어디서든 접근할 수 있어야 하므로, 클래스와 독립적으로 메모리에 할당되며 모든 객체에서 공유되는 정적 멤버 변수를 선언한다.

 

 이때 포인터를 클래스 외부에서 초기화해야 하는데, 이는 프로그램 내에서 동일한 변수나 함수에 대한 정의는 한 번만 있어야 한다는 One Definition Rule(ODR) 때문이다. 클래스 선언은 헤더 파일에 있기 때문에, 여러 .cpp 파일에서 포함될 수 있다. 만약 클래스 내부에서 static 멤버 변수를 초기화하면, 그 헤더 파일을 포함하는 각 .cpp 파일에서 해당 변수의 정의가 반복되게 되어 ODR을 위반하게 된다. 

 또한 프로그램 시작 시점에 전역 변수와 static 멤버 변수의 초기화 순서가 명확하게 정해져 있지 않기 때문에, 의존성 있는 다른 static 변수들과의 초기화 순서에 문제가 발생할 수 있다. 그리고 모든 인스턴스에 공유되기 때문에, 별도의 메모리 영역에 할당되어야 하므로 클래스 외부에 선언되는 것이다.

 

class DB
{
    ...

public:
    static DB* GetInstance();
    ...
};
...

DB* DB::GetInstance()
{
    if (_instance == nullptr)
        _instance = new DB();

    return _instance;
}

4. 공개된 정적 함수를 선언하여 이를 통해 인스턴스에 접근

 생성자가 private으로 설정되어 있기 때문에, 외부에서 직접 인스턴스를 생성할 수 없다. 따라서 정적 함수를 통해 내부에서 인스턴스를 생성하거나, 이미 생성된 인스턴스에 접근할 수 있도록 제공되어야 한다. 정적 함수는 객체의 인스턴스 없이 호출될 수 있으므로, public 접근 지정자로 static 함수를 선언된다.

 

총합본

더보기
#include <mysql.h>

class DB
{
    MYSQL _mysql;
    static DB* _instance;
    DB();

public:
    DB(const DB&) = delete;
    DB& operator = (const DB&) = delete;

    static DB* GetInstance();
    ~DB();
};

DB* DB::_instance = nullptr;

DB* DB::GetInstance()
{
    if (_instance == nullptr)
        _instance = new DB();

    return _instance;
}

DB::DB()
{
    mysql_init(&_mysql);

    if (!mysql_real_connect(&_mysql, "localhost", "root", "password", "example", 3306, NULL, 0))
    {
        std::cerr << "Failed to connect to MySQL\n";
        return;
    }
    
    else
       std::cout << "Successfully Connected with MySQL\n";
}

DB::~DB()
{
    mysql_close(&_mysql);
}

 구체적인 예제는 다음 포스팅 [C++] 싱글톤 패턴(2) with C++에서 다루겠다.


요약

싱글톤 패턴
1. 정의: 클래스의 인스턴스가 단 하나만 생성되도록 보장하는 디자인 패턴
2. 방법
 a. 생성자를 private으로 변경
 b. 복사 생성자와 복사 대입 연산자를 삭제
 c. 정적 멤버 변수로 자기 자신의 인스턴스를 가리키는 포인터 선언
 d. 공개된 정적 함수를 선언하여 이를 통해 인스턴스에 접근
3. 장점
 - 리소스 절약 및 전역 액세스 포인트 제공
4. 단점
 - 전역 상태로 인한 시스템 동작 예측이 어려움
 - 멀티 스레드 환경에서의 구현이 어려움
반응형