오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공합니다.
GOF의 디자인 패턴
singleton 패턴
singleton은 '단독 개체', '독신자'라는 뜻 말고도 '정확히 하나의 요소만 갖는 집합' 등의 의미가 있다. singleton 패턴은 객체의 생성과 관련된 패턴으로서 특정 클래스의 객체가 오직 한 개만 존재하도록 보장한다. 즉 클래스의 객체를 하나로 제한한다. 프로그램에서 이런 개념이 필요할 때는 언제일까? 프린터 드라이버의 예를 들어보자.
여러 컴퓨터에서 프린터 한 대를 공유하는 경우, 한 대의 컴퓨터에서 프린트하고 있을 때 다른 컴퓨터가 프린트 명령을 내려도 현재 프린트하는 작업을 마치고 그다음 프린트를 해야지 두 작업이 섞여 나오면 문제가 될 것이다. 즉 여러 클라이언트(컴퓨터)가 동일 객체(공유 프린터)를 사용하지만 한 개의 객체(프린트 명령을 받은 출력물)가 유일하도록 상위 객체가 보장하지 못한다면 singleton 패턴을 적용해야 한다. 이처럼 동일한 자원이나 데이터를 처리하는 객체가 불필요하게 여러 개 만들어질 필요가 없는 경우에 주로 사용한다.
4. singleton 패턴 지금 읽는 중
20. chain of responsibility 패턴
클래스 인스턴스가 하나만 만들어지도록 하고, 그 인스턴스에 대한 전역 접근을 제공한다.
즉 싱글턴 패턴으로 만든 인스턴스는 두 개 이상 만들어지지 않는 유일한 인스턴스이다.
싱글턴 패턴은 개발자들에게 많이 알려진 패턴이기도 하다.
JAVA 싱글턴
우선 고전적인 싱글턴 패턴에 대해 알아보자
public class Singleton { private static Singleton uniqueInstance; private Singleton () {} public static Singleton getInstance(){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; } }
고전적인 싱글턴 패턴은 위와 같이 만들어진다.
하지만 여기에는 문제가 있다.
애플리케이션이 다중 스레드를 사용하게 되면 문제가 발생한다.
1. 1번 스레드가 getInstance를 호출한다.
2. 2번 스레드도 동시에 getInstance를 호출한다.
3. 1번 스레드에서 uniqueInstance의 null체크를 한다.
4. 2번 스레드도 uniqueInstance의 null체크를 한다.
5. 1번 스레드에서 인스턴스를 생성한다.
6. 2번 스레드도 uniqueInstance가 null이었으므로 인스턴스를 생성한다.
그렇다면 멀티스레딩 문제를 어떻게 해결할 것인가?
getInstance()를 동기화 시키면 멀티스레딩 문제를 해결 할 수 있다.
public class Singleton { private static Singleton uniqueInstance; private Singleton () {} public static synchronized Singleton getInstance(){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } return uniqueInstance; } }
synchronized키워드를 추가하면 한 스레드가 메소드 사용을 끝내기 전까지
다른 스레드는 기다려야한다.
하지만 getInstance()메소드를 동기화시키려면 적지 않은 대가를 치뤄야한다.
메소드를 동기화하게 되면 성능이 100배 정도 저하되고 애플리케이션에서 병목으로 작용할 수도 있다.
그렇다면 더 효율적인 방법은 없을까?
인스턴스를 필요할 때 생성하지 말고 처음부터 만들어 버리자!
public class Singleton { private static Singleton uniqueInstance = new Singleton(); private Singleton () {} public static Singleton getInstance(){ return uniqueInstance; } }
이런식으로 클래스가 로딩될 때 JVM에서 Singleton의 유일한 인스턴스를 생성하는것도 괜찮은 방법이다.
이렇게 하면 JVM에서 유일한 인스턴스를 생성하기 전에는 어떤 스레드도 uniqueInstance 변수에 접근 할 수 없다.
다른 방법도 있다.
DCL(Double-Checking Locking)을 써서 getInstance에서 동기화 되는 부분을 줄이는 방법이다.
DCL을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 떄만 동기화를 할 수 있다.
이렇게 하면 처음에만 동기화 하고 나중에는 동기화 하지않아도 되는 방법이다.
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton () {} public static Singleton getInstance(){ if(uniqueInstance == null){ // 인스턴스가 있는지 확인 후 없으면 동기화 블럭으로 synchronized (Singleton.class){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
volatile키워드를 사용하면 멀티스레딩을 쓰더라도
uniqueInstance 변수가 Singleton 인스턴스로 초기화 되는 과정이
올바르게 진행되도록 할 수 있다.
[출처] 싱글턴 패턴 (Singleton Pattern)|작성자 ljseokd
C에서 싱글턴
꼭 한 개만 존재해야 하는 클래스가 있다고 해보자. (예를 들어 오디오 시스템)
class AudioManager { public: static AudioManager& instance() { if(_instance == nullptr) { _instance = new AudioManager(); } return *_instance; } private: AudioManager() {} static AudioManager* _instance; }
C++ 11 이상에서는 아래와 같이도 사용할 수 있다.
C++11 부터는 정적 지역변수에 한해서 한번만 초기화가 진행되기에 가능하다.
class AudioManager { public: static AudioManager& instance() { static AudioManager* instance = new AudioManager(); // 정적 지역변수 초기화 return *instance; } private: AudioManager() {} }
외부에서 AudioManager 객체를 얻고 싶다면 아래와 같이 사용하면 된다.
AudioManager a = AudioManager.instance();
싱글턴을 사용함으로써 얻을 수 있는 이점은 아래와 같다.
1. 늦은 초기화
instance 메서드가 처음으로 호출되기 전까지 객체가 생성되지 않는다.
C++에서 정적 변수들은 자동으로 초기화가 된다.
이러한 점은 정적 변수들끼리 초기화 순서를 보장할 수 없다.
하지만 싱글턴 패턴을 활용하여 이렇게 런타임 도중에 원하는 시점에 초기화를 할 수 있으므로 다른 클래스 객체와의 어떤 의존 관계에서도 순서를 명확히 하여 문제를 발생시키지 않게 해준다.
2. 싱글턴 상속으로 좀 더 편리하게 코드 작성이 가능하다.
class AudioManager { ... } class PCAudioManager: public AudioManager { ... } class PS4AudioManager: public AudioManager { ... } ... AudioManager& instance() { #if PLATFORM == PC static AudioManager* instance = new PCAudioManager(); #elif PLATFORM == PS4 static AudioManager* instance = new PS4AudioManager(); #endif return *instance; }
늦은 초기화로 동적으로 생성한다는 점 덕분에 다형성을 이용할 수 있다.
이를 이용하여 전처리문 등을 이용해 플랫폼별로 다른 AudioManager 객체를 싱글턴으로 이용할 수 있다.
하지만 이러한 싱글턴에는 단점이 많다.
일단 싱글턴은 사실상 전역변수이다.
그렇기에 발생하는 문제점을 적어보자면...
1. 코드를 이해하기 어렵다
어떠한 함수에서 전역변수의 상태를 변경시키고, 그것으로 인해 다른 곳에서 문제가 발생했다고 하자.
이 문제를 해결하기 위해선 전역변수를 사용하는 모든 곳을 훑어봐야 한다.
2. 멀티스레드 같은 동시성 프로그래밍에 적절하지 않다.
전역이라는 점은 여러 스레드가 동시에 접근할 수 있다는 뜻이고, 이는 정말로 찾기 어려운 버그들을 일으킬 수 있다.
3. 게으른 초기화는 메모리 단편화의 원인이 될 수 있다.
모바일 게임처럼 메모리를 빡빡하게 써야되는 경우 메모리 관리를 위해 오브젝트 풀 패턴 등을 쓴다.
위 코드 예시처럼 AudioManager 같은 코드가 늦은 초기화가 될 때 메모리 어디에 할당될지는...
문제없이 할당될 수 있도록 초기화 시점을 잘 잡아야 할 것이다.
따라서 싱글턴 패턴은 강력하지만 정말 꼭 필요할 때만 사용해야 한다.
이러한 싱글턴 패턴 대신 사용 가능한 몇 가지 방법들이 있다.
1. 오직 하나의 클래스 인스턴스만을 가지도록 하고 싶을 때
class AudioManager { public: AudioManager() { assert(!_instantiated); _instantiated = true; } private: static bool _instantiated; }
단언문 assert는 괄호 안의 내용이 참이 아니라면 코드를 중지시킨다.
이를 이용하여 인스턴스 갯수가 하나임을 보장할 수 있다.
2. 인스턴스에 접근할 수 있도록 하고 싶을 때
1) 의존성 주입
class Monster { private: AudioManager& _audioManager; public: ... setAudioManager(AudioManager AM) {_audioManager = AM}; }
AudioManager가 필요한 클래스가 밖으로부터 AudioManager 객체를 넘겨받아 사용하는 방법이다.
2) 상위 객체로부터 얻기
class Monster { protected: AudioManager& getAudioManager() { return _audioManager; } private: static AudioManager& _audioManager; } class Orc: public Monster { public: void dead() { AudioManager AM = getAudioManager(); // 상위 객체로부터 받는다 ... } }
상위 객체에서 접근할 수 있도록 구현하여 상속받은 객체들이 접근할 수 있도록 한다.
3) 이미 전역인 객체로부터 얻기
class GameManager { public: static GameManager& instance() { return _instance; } AudioManager& getAM() {return *_audioManager;} private: static GameManager _instance; AudioManager* _audioManager }
[출처] [디자인 패턴] 싱글턴 (Singleton)|작성자 강경웅
'SW프로그래밍' 카테고리의 다른 글
MVC (Model, View, Controller) 의 개념 (0) | 2020.07.09 |
---|