디자인 패턴

싱글턴 패턴 ( Singleton Pattern )

Drill_Labito 2023. 1. 9. 16:54

Reference : https://www.geeksforgeeks.org/implementation-of-singleton-class-in-cpp/?ref=gcse 

 

Implementation of Singleton Class in C++ - GeeksforGeeks

A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.

www.geeksforgeeks.org

 

싱글톤 패턴은 객체지향 프로그래밍에서 자주 쓰이는 디자인 패턴으로, 

한번에 하나의 객체 또는 인스턴스를 가질수 있는, 객체지향 프로그래밍 기법중 하나이다. 

 

바로 어떤식으로 구현하는지 알아본 후 이 패턴의 특징과, 장단점에 대해 알아보자.

 

싱글톤 클래스를 만들때 아래의 단계를 거치고자 한다.

1. Make all the constructors of the class private 

2. Delete the copy constructor of the class

3. Make a private static pointer that can point to the same class object ( singleton class ) 

4. Make a public static method that returns the pointer to the same class object ( singleton class ) 

 

아래는 싱글톤 클래스를 C++로 구현해본 코드이다. 

 

#include <iostream>
using namespace std;
// Implementation of Singleton class in C++
class Singleton
{
private:
    // class member variables
    string name;
    string loves;
    // static pointer which will points
    // to the instance of this class
    static Singleton* instancePtr;
    // Default Constructor
    Singleton() {}
public:
    // deleteing copy constructor
    Singleton(const Singleton& obj)
        = delete;
    /*
    getInstance() is a static method that returns an
    instance when it is invoked. It returns the same
    instance if it is invoked. It returns the same instance
    it it is invoked more than once as an instance of Singleton class is already
    created. It is static because we have to invoke this method without
    any object of Singleton class and static method can be invoked
    without object of class
    As constructor is private so we cannot create object of
    Singleton class without a static method as they can be
    called without objects. We have to create an instance of
    this Singleton cass by using getInstance() method.
    */
    static Singleton* getInstance()
    {
        // If there is no instance of class
        // then we can create an instance.
        if (instancePtr == NULL)
        {
            // We can access private members
            // within the class
            instancePtr = new Singleton();
            // returning the instance pointer
            return instancePtr;
        }
        else
        {
            // if instancePtr != NULL that means
            // the class already have an instance.
            // So, we are returning that instance
            // and not creating new one.
            return instancePtr;
        }
    }
    // sets values of member variables.
    void setValues(string name, string loves)
    {
        this->name = name;
        this->loves = loves;
    }
    // prints values of member variables
    void print()
    {
        cout << name << " Loves " << loves << ".\n";
    }
};
// Initializing instancePtr with NULL
Singleton* Singleton::instancePtr = NULL;
int main()
{
    Singleton* mySingleton = Singleton::getInstance();
    // setting values of member variables.
    mySingleton->setValues("Man", "Woman");
    // printing values of member variables.
    mySingleton->print();
    cout << "Address of mySingleton : " << mySingleton << '\n';
    Singleton* temp = Singleton::getInstance();
    // setting values of member variables.
    temp->setValues("Girl", "Boy");
    // printing values of memeber variables
    temp->print();
    cout << "Address of temp : " << temp << '\n';
    return 0;
}

 

===========================================

 

< 특징 > 

- 오직 한개의 클래스 인스턴스만 갖도록 보장

- 전역 접근점을 제공 

 

----------------------------------------------------------------------------

< 장점 > 

- 한번도 사용하지 않는다면 아예 인스턴스를 생성하지 않는다.

=> 싱글턴은 처음 사용될 때 초기화되므로 ,게임 내에서 전혀 사용되지 않는다면 아예 초기화되지 않는다.

 

- 런타임에 초기화된다. 

=> 보통은 싱글턴의 대안으로 정적 멤버 변수를 많이 사용한다. 그러나 정적 멤버변수는 자동 초기화( automatic initialization ) 되는 문제가 있다. 

즉, 컴파일러는 main 함수를 호출하기 전에 정적변수를 초기화하기 때문에 프로그램이 실행된 다음에야 알 수 있는 ( 파일로 읽어 들인 설정 값 같은 ) 정보를 활용할 수 없다. 

정적 변수 초기화 순서도 컴파일러에서 보장해주지 않기 때문에 한 정적 변수가 다른 정적 변수에 안전하게 의존할 수도 없다. 

게으른 초기화는 이런 문제를 해결해준다. 싱글턴은 최대한 늦게 초기화 되기 때문에 그때쯤에는 클래스가 필요로 하는 정보가 준비되어 있다. 순환 의존만 없다면, 초기화할 때 다른 싱글턴을 참조해도 좋다.

 

- 싱글턴을 상속할 수 있다. 

이 방법은 강력한 방법임에도 잘 알려져 있지 않다. ( 싱글톤 상속 기법은 단위 테스트용 모의 객체(mock object) 를 만들때도 유용함 ) 파일 시스템 래퍼가 크로스 플랫폼을 원해야 한다면 추상 인터페이스를 만든 뒤, 플랫폼마다 구체 클래스를 만들면 된다. 

예를들어보자, 먼저 다음과 같이 상위 클래스를 만든다.

class FileSystem
{
public:
    virtual ~FileSystem() {}
    virtual char* readFile(char* path) = 0;
    virtual void writeFile(char* path, char* contents) = 0;
};
 

이제 플랫폼 별로 하위 클래스를 정의한다. 

 

class PS4FileSystem : public FileSystem
{
public:
    virtual char* readFile(char* path)
    {
        // sony 의 파일 IO API를 사용
    }

    virtual void writFile(char* path, char* contents)
    {
        // sony 의 파일 IO API를 사용
    }
};
 
class NDSFileSystem : public FileSystem
{
public:
    virtual char* readFile(char* path)
    {
        // Nintendo 파일 IO API를 사용
    }
    virtual char* writeFile(char* path)
    {
        // Nintendo 파일 IO API를 사용
    }
};

이제 FileSystem 을 싱글톤으로 만든다. 

class FileSystem
{
public:
    static FileSystem* instance();

    virtual ~FileSystem() {}
    virtual char* readFile(char* path) = 0;
    virtual void writeFile(char* path, char* contents) = 0;

protected:
    FileSystem() {}
};

핵심은 인스턴스를 생성하는 부분이다. 

 

FileSystem* FileSystem::instance()
{
#if PLATFORM == PLAYSTATION4
    static FileSystem *instance = new PS3FileSystem();
#elif PLATFORM == NDS
    static FileSystem *instance = new NDSFileSyetem();
#endif
    return *instance;
}

#if 같은 전처리기 지시문을 이용해서 간단하게 컴파일러가 시스템에 맞는 파일 시스템 객체를 만들게 할 수 있다. 

 FileSystem::instance() 를 통해 파일 시스템에 접근하기 때문에 플랫폼 전용 코든ㄴ FileSystem 클래스 내부에 숨겨놓을 수 있다. 

 

----------------------------------------------------------------------------

 

< 단점 > 

 

- 알고보니 전역변수

=> 전역변수의 경우 남발하게 될 경우, 프로그램 규모가 커질수록 설계와 유지보수성이 병목이 된다. 따라서 생산성의 한계를 맞이할 수 있음.

또 전역변수의 안좋은점으로 아래와 같이 있다.

1) 전역 변수는 코드를 이해하기 어렵게 한다. 

2) 전역 변수는 커플링을 조장한다. 

3) 전역 변수는 멀티스레딩 같은 동시성 프로그래밍에 알맞지 않다. 

 

- 싱글턴은 문제가 하나뿐일 때도 두가지 문제를 풀려고 한다.

 

- 게으른 초기화는 제어할 수 없다.