싱글턴 패턴(Singleton Pattern)

 

오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공

쉽게 말하면 클래스로 객체를 단 한개만 만들게하고, 만들어진 단 한개의 객체를 public으로 접근해서 사용하는 방법

 

 

 

싱글턴 패턴 구현 핵심

 

1) 오직 한개의 인스턴스만을 갖도록 보장

인스턴스가 여러개면 제대로 작동하지 않는 경우가 종종 있다.

      ex) 외부 시스템과 상호작용하면서 전역 상태를 관리하는 클래스

아무데서나 이런 객체를 만들 수 있다면 여러 객체가 전역 변수를 건드므로 동기화 문제가 생길것이다.

이런때에 싱글턴으로 클래스가 인스턴스를 하나만 가지도록 컴파일 단계에서 강제할 수 있다.

 

2) 전역적인 접근점을 제공

인스턴스는 하나만 존재하지만, 해당 객체를 사용하는것은 여러 위치일것이므로 해당 인스턴스를 전역에서 접근할 수 있는 메서드를 제공한다.

※ 단, 전역적인 접근점을 제공하는 것이지 전역변수로 사용한다는 뜻이 아니다.


3) 주의점

- 생성자, 복사 생성자, 소멸자를 private 선언해야 함
- 싱글턴 객체를 전역변수로 쓰면 안됨
   게으른 초기화때문에 초기화 순서를 보장하지 못하므로 get이 더 빠를 수 있음

 

 

 

사용 예시

class FileSystem {
private:
    FileSystem() {} // 생성자가 private
    FileSystem(const FileSystem& ref) {} // 복사생성자도 잊지않고 private 해야함, 복사가 되면 안되므로
    FileSystem& operator=(const FileSystem& ref) {} // 마찬가지로 복사해서 대입되면 안됨
    ~FileSystem() {} // 소멸자도 private, 생성자 소멸자를 private로 선언해서 상속을 막음

public:
    static FileSystem& instance() { // Modern Cpp에서는 이렇게 해도 괜찮다고 함
        static FileSystem instance; // 이전버전의 C++이나 컴파일러라면 Thread-Safe하지 않을 수 있다고 함
        return instance;            // 구버전이라면 아래쪽 코드블럭처럼 처리
    }
};

int main()
{
    FileSystem& fs = FileSystem::instance();
    
    return 0;
}

 

아래는 잘못되진 않았지만 권장하지 않는 예시

아래 코드는 전역변수로 _instance 변수를 선언하고 해당 변수에 NULL을 넣고 시작하며, main함수에서 instance() 함수를 사용해서 해당 변수에 값을 넣어준 뒤 사용함

단점 1) _instance는 new로 동적할당 했으므로 프로그램이 끝날때까지 heap에 상주함

단점 2) _instance = NULL;이 입력되면 인스턴스를 다시 얻어와야 함

class FileSystem {
private:
    FileSystem() {} // 생성자가 private
    FileSystem(const FileSystem& ref) {} // 복사생성자도 잊지않고 private 해야함, 복사가 되면 안되므로
    FileSystem& operator=(const FileSystem& ref) {} // 마찬가지로 복사해서 대입되면 안됨
    ~FileSystem() {} // 소멸자도 private, 생성자 소멸자를 private로 선언해서 상속을 막음
    static FileSystem* _instance; // 하나뿐인 인스턴스(객체)
    
public:
    static FileSystem* instance() { // 이 메서드로 어디에서나 싱글턴 객체에 접근 가능
        // 게으른 초기화(실제로 필요할때까지 인스턴스 초기화를 미루는 것)
        if(_instance == NULL) {
            _instance = new FileSystem();
        }

        return _instance;
    }
};

FileSystem* FileSystem::_instance = NULL;

int main()
{
    FileSystem* _instance = FileSystem::instance();
    
    return 0;
}

 

 

 

사용하는 이유

 

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

  - 게으른 초기화

 

2. 런타임에 초기화된다.

  - 싱글턴의 대안으로 사용되는 static 멤버 변수의 경우 자동으로 초기화가 되거나, 반드시 초기화 해야한다.

  - 정적 변수는 main함수 호출보다도 이전에 초기화된다고 하며, 이렇게되면 프로그램이 실행된 후에야 알 수 있는 정보를 활용할 수 없다.

  - 또한 정적 변수가 여러개라면 초기화 순서도 보장할 수 없으므로 한 정적변수가 다른 정적변수에 의존할 수가 없다.

 

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

  - 기본적으로는 생성자와 소멸자를 private로 선언해서 상속을 막는다. 하지만 상속이 필요한 경우 protected로 사용한다.

  - 위에서 작성한 FileSystem 코드가 크로스 플랫폼을 지원해야 한다면 instance()함수를 플랫폼에 맞게 구현해야 한다.

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 PLATFROM == PLAYSTATION3
    static FileSystem* instance = new PS3FileSystem();
#elif PLATFORM == WII
    static FileSystem* instance = new WiiFileSystem();
#endif
    return *instance;
}

class PS3FileSystem : public FileSystem {
public:
    virtaul char* readFile(char* path) {
        // 소니의 파일 IO API를 사용한다...
    }
    virtaul void writeFile(char* path, char* contents) {
        // 소니의 파일 IO API를 사용한다...
    }
};

class WIIFileSystem : public FileSystem {
public:
    virtaul char* readFile(char* path) {
        // 닌텐도의 파일 IO API를 사용한다...
    }
    virtaul void writeFile(char* path, char* contents) {
        // 닌텐도의 파일 IO API를 사용한다...
    }
};

 

위의 다양한 이유와 장점에도 불구하고 남용되는 경우가 아주 많고, 그렇다보니 의도와는 달리 득보다 실이 많다고 한다.

남용을 주의해야 한다.

 

 

 

싱글턴의 남용

 

싱글턴이 왜 문제가 되는가?

 

1. 전역 변수의 문제점

- 함수 중에 public으로 SomeClass::getSomeData() 같은 코드가 있을때 이 함수쪽에서 버그가 발생한다면 SomeData 변수는 전역변수나 다름없고, 버그를 찾기 위해서는 SomeData 변수에 접근하는 모든곳을 확인해야한다. 만약 전역변수가 아니거나 함수가 전역 상태를 건드리지 않는다면 해당 함수만 살피면 되므로 훨씬 쉬워질것이다.

- 전역변수는 커플링을 조장한다. 인스턴스에 대한 접근을 통제함으로써 커플링을 통제할 수 있다.

- 전역변수는 멀티스레딩같은 동시성 프로그래밍에 부적합하다. 동기화문제나 교착상태/경쟁상태등의 문제가 생기기 쉽다.

 

2. 게으른 초기화의 문제점

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

- 게으른 초기화는 괜찮은 기법이지만 초기화 할때 메모리할당량이 크거나 로딩해야할 리소스가 아주 많다면 초기화 시간이 오래걸릴 수 있고, 렉이 걸리게 될 수 있다.

 

 

 

 

 

 

※ 참고문헌

https://boycoding.tistory.com/109

http://gameprogrammingpatterns.com/singleton.html

https://hwan-shell.tistory.com/227