프로그래밍/C++

C++] 싱글톤(Singleton)정의 방법과 설명.

Hwan2 2020. 6. 21. 23:49
반응형

C++에서 자주는 아니지만 심심치않게 싱글톤(Singleton)을 활용한 사례들을 본적이 있습니다.

굳이 사용하지 않아도 알아두면 좋은 기법입니다.


왜냐하면 클래스의 거의 모든걸 고루 담고 있기 때문입니다.

static변수, private 생성자, 소멸자, 늦은 초기화 등.....


꽤 얻어갈 정보들이 있습니다.


때문에 소개하고 설명해보고자 합니다.


1. 싱글톤(Singleton)이란?

싱글톤은 말 그대로 혼자라는 뜻입니다. 클래스의 생성을 딱!! 1개로 정한다는 뜻이죠.

따라서 해당 클래스의 객체는 static으로 생성이 됩니다.


물론 여러개의 변수가 하나의 클래스를 가르킬 순 있겠죠.

하지만, 그 클래스의 객체가 복사가 되면 안됩니다. 즉, 똑같은 클래스 정보가 2개 이상 늘어나면 안된다는 것이죠.


이렇게 클래스를 하나로만 제안해서 얻는 이익은 무엇일까요??


1. 코드의 가독성

여러개의 클래스가아닌 하나의 클래스를 독자적으로 사용하기 때문에 코드의 가독성이 올라갑니다.


2. 불필요한 객체생성을 피하기 위해

프로그램 코드를 짜다보면 불필요한 객체를 여럿 생성하게 됩니다.

예를 들면 클래스 5개가 있는데 이 5개의 클래스가 하나의 클래스에서 인스턴스를 받아서 사용한다고 합시다.

그럼 이 클래스 5개는 똑같은 역할을 하는 객체를 1개씩 총 5개 생성해야 합니다.

이 얼마나 불필요한 방식일까요?


3. 코드가 좀 더 세밀해 진다.

싱글톤을 만들기로 마음 먹었다면 최대한 여러곳에서 사용할 수 있도록 설계를 잘 해야합니다.

그래야 의미가 있는 디자인 패턴방식이니깐요. 그렇게 되면 당연히 코드의 질이 향상될 수 밖에 없습니다.


그 외에도 있지만 제가 아는 선에선 이게 다 인것 같네요.....


그럼 코드를 보겠습니다.



2. 싱글톤(Singleton) 구현

1) 잘못된 구현 1

#include <iostream>

class Singleton {
private:
    static Singleton s;
public:
    static Singleton& getIncetance() {
        return s;
    }
};

Singleton Singleton::s; //전역변수로써 초기화

int main(void) {
    Singleton& s = Singleton::getIncetance();
    return 0;
}


해당 코드는 static Singleton 객체를 선언한 후 해당 Incetance를 반환하고 있습니다.

어디가 잘못 되었을 까요??


1. 복사 생성자와 소멸자, 생성자를 private선언을 안했습니다. 

그렇게 된다면 해당 static 객체는 복사가 되어 사용하는 의미를 상실해 버리게 됩니다.


2. 늦은 초기화

늦은 초기화에 대해 설명을 해보겠습니다. 

늦은 초기화란? 프로그램이 시작과 동시에 초기화 되는것을 방지하여 사용자가 원하는 시점에서 

객체의 초기화를 해주는 것입니다.

그럼 이것이 왜 필요할까요??


static 같은 경우는 전역변수에 마구자비로 선언이 되어도 초기화 되는 순서는 랜덤입니다.

static int num1 = 0;
static int num2 = 0;
static int num3 = 0;
static int num4 = 0;

이렇게 순서대로 있다고 한다면 과연 이 변수들은 순서대로 초기화가 될까요??

아닙니다. num4가 먼저 될 수도 있고 num2가 먼저 될수도 있고, 아무도 모릅니다.


그럼 왜 위 코드와 같은 Singleton이 문제가 되느냐??


#include <iostream>

class Singleton {
private:
    static Singleton s;
public:
    static Singleton& getIncetance() {
        return s;
    }
};

class A {
public:
    A() {
        Singleton& s = Singleton::getIncetance();
    }
};

Singleton Singleton::s; //전역변수로써 초기화

int main(void) {
    Singleton& s = Singleton::getIncetance();
    return 0;
}


위와 같은 경우 A라는 클래스는 해당 Singleton의 인스턴스 객체를 받아옵니다.

하지만 전역변수로써 초기화된 Singleton Singleton::s 이 부분이 초기화가 이뤄지지 않은 상황에서,

A생성자 안에서 Singleton 객체를 부른다면?? 에러납니다. 초기화되지 않은 변수를 참조했다고 하면서요......




2) 잘못된 되진 않았지만 권장하지 않은 예시

#include <iostream>

class Singleton {
private:
    static Singleton* s;
    Singleton() {}
    Singleton(const Singleton& ref) {}
    Singleton& operator=(const Singleton& ref) {}
    ~Singleton() {}
public:
    static Singleton* getIncetance() {
        if(s == NULL)
            s = new Singleton();
        return s;
    }
};

Singleton* Singleton::s = NULL;

int main(void) {
    Singleton* s = Singleton::getIncetance();

    return 0;
}


해당 싱글톤은 객체를 Heap에 적재한 후 포인터를 이용해 접근하고 있습니다.

언듯 보면 별 문제 없어보이지만 함정이 있습니다.


1. 프로그램이 끝날 때 까지 heap에 계속 상주하게 된다.

2. s = NULL; 을 입력하게되면 다시 인스턴스를 받아와야 한다.


구글링을 통해 찾아본 결과 위와 같은 싱글톤을 소개하는 글들이 많았습니다.

물론 잘못된건 아니지만 좋은것도 아닙니다.

Heap에 끝까지 객체가 상주해야할 필요가 있을까요? Heap은 C++에선 특히 관리를 잘 해줘야 하는 부분입니다.

판단은 이 글을 보시는 여러분들께 맏기겠습니다.




3) 사용을 권장하는 예시

#include <iostream>

class Singleton {
private:
    Singleton() {}
    Singleton(const Singleton& ref) {}
    Singleton& operator=(const Singleton& ref) {}
    ~Singleton() {}
public:
    static Singleton& getIncetance() {
        static Singleton s;
        return s;
    }
};

int main(void) {
    Singleton& s = Singleton::getIncetance();
    return 0;
}


이렇게 구현을 하면 모든 조건을 만족하는 좋은 Singleton 코드가 됩니다.


조건은 다음과 같습니다.

1. 객체를 data영역에 선언해 행여 발생될 데이터의 해제를 방지할 수 있습니다.

2. static Singleton s 변수를 getIncetance() 함수 안에 넣음으로 써 늦은 초기화가 가능해 집니다.

3. 복사생성자, 생성자, 소멸자를 private으로 선언하여 복사, 상속을 못하게 막았습니다.


이렇게 static을 통한 참조를 한다면 포인터로 받아오는 Singleton보다 더 좋은 코드가 될 것입니다.


그리고 피닉스 싱글톤이라는 개념도 존재하는데, 해당 싱글톤은 포인터로 받아오는 싱글톤입니다.

관심있는 분들은 찾아보시는것도 좋을 것 같습니다. 


반응형