싱글턴 패턴(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. 게으른 초기화의 문제점
- 게으른 초기화는 제어할 수가 없다.
- 게으른 초기화는 괜찮은 기법이지만 초기화 할때 메모리할당량이 크거나 로딩해야할 리소스가 아주 많다면 초기화 시간이 오래걸릴 수 있고, 렉이 걸리게 될 수 있다.
코드 정리 |
사용하면 안되는 복사생성자, 대입생성자 등을 굳이 살려둘 필요가 없다.
private로 선언하게 만드는것보다 명확하게 삭제해버리는게 좋다.
class FileSystem {
private:
// 생성자 소멸자를 private로 선언해서 상속을 막음
FileSystem() {}
~FileSystem() {}
FileSystem(const FileSystem& ref) = delete; // 유일한 객체여야 하니 복사가 되면 안되므로 delete
FileSystem& operator=(const FileSystem& ref) = delete; // 복사해서 대입되면 안되므로 delete
FileSystem(FileSystem&& ref) = delete; // 전역적인 접근점을 이미 제공하는데 이동시킬 이유가 없으므로 delete
public:
static FileSystem& instance() { // Modern Cpp에서는 이렇게 해도 괜찮음
static FileSystem instance; // 이전버전의 C++이나 컴파일러라면 Thread-Safe하지 않을 수 있음
return instance;
}
};
int main()
{
FileSystem& fs = FileSystem::instance();
return 0;
}
※ 참고문헌
https://boycoding.tistory.com/109