1. Singleton 패턴이란

1.1 정의

특정 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근 할 수 있게 하기 위한 패턴.


싱글톤 패턴은 일반적으로 클래스의 생성자를 private으로 선언하고, 클래스 안에 static으로 자기 자신에 대한 레퍼런스를 두어 그 레퍼런스를 참조하게 하는 식으로 구현된다.


1.2 UML





Singleton 클래스는 instance라는 이름으로 자기 자신에 대한 레퍼런스를 static으로 가지고 접근 제한자는 private이다. 그리고 Singleton 클래스의 외부에서 이 클래스에 접근할 때는 getInstance()라는 static메소드를 통해 이 레퍼런스에 접근한다.


1.3 구현예시

UML 보다는 구현 예시로 보는 것이 이해가 빠를 듯 하다.

class Singleton { public: //싱글톤 객체를 받는다. static Singleton* getInstance()

{

if(instance == NULL)

instance = new Singleton();

return instance;

}

static Singleton* instance;

};

*C++에서 static Singleton* instance; 라고 선언만 해두면 instance가 정의되지 않았기 때문에 컴파일 오류가 난다. 원래라면 .cpp 파일에 Singleton::Singleton* instance; 라고 한 줄 적어주어야 오류가 안 나는데, 여기서는 이해를 위해서 해당 내용은 생략하였다.


getInstance() 메소드는 내부에 static으로 선언된 instance 변수가 NULL인지 확인하고, NULL이라면 새로운 Singleton 객체를 생성한 후 그 주소를 리턴하고, NULL이 아니라면 instance 변수에 있는 객체에 대한 주소를 리턴한다.

이런식으로 하면, 외부의 어떤 클래스에서든지 Singleton::getInstance()->method() 이런 식으로 Singleton 객체에 접근 할 수 있다.


1.4 싱글톤 패턴 적용시 고려사항


개인적인 경험으로, 싱글톤 패턴을 한번 사용해 보면 구현하기가 굉장히 편하고, 또 여기저기에 적용하기가 쉬워서 싱글톤 패턴을 남용하게 될 수도 있다.

싱글톤 패턴도 당연히 단점을 가지고 있기 때문에 싱글톤 패턴은 남용하지 않도록 다음의 원칙을 준수해야 한다.


- 클래스의 인스턴스가 유일함을 보장해야한다.

- 다른 모든 클래스에서 접근 가능해야한다.


싱글톤 패턴은 위의 두 가지가 모두 만족되는 경우에만 사용해야한다.


싱글톤 패턴을 적용한 클래스는 2개 이상의 인스턴스가 존재 할 수 있도록 설계를 변경하기 어렵다. 그렇기 때문에 싱글톤 패턴을 적용 할 때는 해당 클래스의 인스턴스가 하나여야함을 보장 할 수 있을 때에만 적용해야한다.


다른 모든 클래스들에서 접근 되는 것이 아니라면, 굳이 싱글톤 패턴을 적용할 필요가 없다.


2. 적용 예제

이 예제는 Head First Design Pattern의 예제를 가져온 것이다.


요즘은 거의 모든 초콜릿 공장에서 초콜릿을 끓이는 초콜릿 보일러를 컴퓨터로 제어한다. 초콜릿 보일러는 초콬릿을 채워 넣고, 끓이고, 보일러 안의 초콜릿을 비워내는 작업을 한다.


초콜릿 보일러는 한개만으로도 충분한 양의 초콜릿을 끓일 수 있고, 비싸기 떄문에 공장마다 단 하나의 초콜릿 보일러만을 가진다고 한다.


그리고 초콜릿 보일러는 다른 모든 클래스에서 접근 가능하게 구현되어야 한다. 초콜릿 보일러는 뜨거운 초콜릿을 관리하기 때문에 사고가 나게 하지 않기 위해 여러 안전장치들이 관리하고, 추후에 시스템에 이런 안전장치들을 클래스화하여 추가할 것이다.


그래서 초콜릿 보일러에 싱글톤 패턴을 적용하여 다음과 같이 구현한다.

<ChocolateBoiler.h>

#pragma once #include <iostream> using namespace std; //초콜릿을 끓이는 초콜릿 보일러 클래스 //싱글톤 패턴을 적용하여 구현됨 class ChocolateBoiler { public: //싱글톤 객체를 받는다. static ChocolateBoiler* getInstance(); //보일러에 초콜릿을 채워 넣는다. void fill(); //초콜릿을 끓인다. void boil(); //보일러 안에 담긴 초콜릿을 비워낸다. void drain(); bool isEmpty(); bool isBoiled(); private: bool empty; bool boiled; ChocolateBoiler(); };


<ChocolateBoiler.cpp>

#include "ChocolateBoiler.h" ChocolateBoiler::ChocolateBoiler() { empty = true; boiled = false; } ChocolateBoiler* ChocolateBoiler::getInstance() { static ChocolateBoiler* instance; if (instance == NULL) { cout << "ChocolateBoiler 객체 생성!" << endl; instance = new ChocolateBoiler(); } return instance; } void ChocolateBoiler::fill() { if (!isEmpty()) return; empty = false; boiled = false; cout << "Chocolate Boiler 채워짐" << endl; } void ChocolateBoiler::boil() { if (isEmpty() || isBoiled()) return; boiled = true; cout << "Chocolate Boiler 끓여짐" << endl; } void ChocolateBoiler::drain() { if (isEmpty() || !isBoiled()) return; empty = true; cout << "Chocolate Boiler 비워짐" << endl; } bool ChocolateBoiler::isEmpty() { return empty; } bool ChocolateBoiler::isBoiled() { return boiled; }

이제 ChocoloateBoiler 객체에 접근할 때는 다음과 같이 사용하면 된다.

ChocolateBoiler::getInstance()->fill(); ChocolateBoiler::getInstance()->boil(); ChocolateBoiler::getInstance()->drain();


3. Singleton 패턴의 장점

- 다른 모든 클래스에서 접근 할 수 있다.

- 인스턴스가 하나만 생성됨이 보장된다.

- Lazy initialization(게으르게 생성) 하여 구현 될 수 있다.

- 인스턴스의 개수를 변경하기가 자유롭다.


위의 2가지 장점이 Singleton 패턴을 사용하는 주된 이유이자 Singleton 패턴 적용시 꼭 지켜야 할 점이다.


게으르게 생성할 수 있다는 것은, 프로그램 실행 중에 인스턴스를 생성 할 수 있다는 것이다.

이는 Singleton 패턴을 적용하지 않고 전역변수를 사용 했을때에 비해서 장점인데, 전역변수를 사용하면 프로그램 실행시에 인스턴스를 바로 생성해야한다.

이렇게 할 때 문제점은, 생성된 인스턴스가 프로그램이 실행 되는 동안 한번도 사용되지 않을 경우 자원이 낭비된다.

그렇기 때문에 게으르게 생성되는 것이 Singleton의 장점이라고 할 수 있다.


인스턴스의 개수를 변경하기가 자유롭다는 말이 참 혼란스럽다. 싱글톤 패턴은 클래스의 인스턴스가 유일함을 보장 할 때만 사용해야 하는데, GoF의 디자인 패턴 책에서는 인스턴스의 갯수 변경이 자유롭다는 것을 장점이라고 설명한다.

물론 싱글톤 인스턴스에 대한 레퍼런스를 배열이나 링크드 리스트 등을 사용해서 구현하면 여러개의 인스턴스를 가지는 싱글톤 클래스를 구현 할 수는 있다.

싱글톤 패턴은, 여러개의 인스턴스를 가지는 싱글톤 클래스를 구현 할 수 있긴 하지만, 기본적으로는 한개의 인스턴스만이 존재해야 할 때 적용해야한다고 생각하면 될 듯하다.


4. Singleton 패턴의 단점

- 단일 책임의 원칙을 어긴다.

- Singleton 클래스에 대한 의존도가 높아진다.

- 싱글톤 클래스에 대한 서브클래스를 만들기 어려워진다.

- 멀티 스레드 적용 시 동기화 문제가 생길 수 있다.


단일 책임의 원칙은 모든 클래스가 하나의 책임만을 가져야 한다는 원칙이다.

싱글톤 클래스는 클래스의 작업을 처리하는 역할 뿐 아니라 자신의 인스턴스에 대한 접근을 관리하는 역할에 대해서도 책임을 져야한다.

이렇게 하는게 전체적인 설계를 간단하게 만들 수 있어서 장점이기도 하지만, 단일 책임의 원칙을 어긴다는 점도 고려해 봐야한다.


싱글톤 패턴을 적용하면 여러 클래스나 컴포넌트가 싱글톤 클래스에 의존하게 된다. 이는 시스템의 Coupling(결합도)를 높이는 결과를 가져오기 때문에 주의해야한다.


싱글톤 클래스는 생성자가 private으로 지정되기 때문에 상속이 불가능 한데, 이를 protected나 public으로 고치면 다른 클래스에서 인스턴스를 만들 수 있게 되서 더이상 싱글톤이라고 할 수 없게 된다.

생성자 문제 뿐만 아니라, 싱글톤 클래스는 static 변수를 바탕으로 하기 때문에 모든 서브클래스들이 그 변수를 공유하게 된다.

이를 해결하기 위해서는 '레지스트리'를 구현해두어야 한다.


멀티 스레드를 적용하면 여러 스레드에서 한개의 싱글톤 클래스에 접근하게 된다. 이는 동기화 문제를 발생시킬 수 있으니 volatile 키워드를 통해 Double Checking Locking을 적용하거나 getInstance() 메소드에 synchronized 키워드를 적용하여 동기화 해야한다.

그런데 synchronized 키워드를 적용하면 스레드에서 getInstance() 할 떄마다 속도가 느려지는 문제가 발생한다.



5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- GoF의 디자인 패턴(Pearson Addison Wesley)

- 위키피디아 Lazy Initialization

https://en.wikipedia.org/wiki/Lazy_initialization


- Code Project Singleton Pattern – Positive and Negative Aspects

https://www.codeproject.com/Articles/307233/Singleton-Pattern-Positive-and-Negative-Aspects-2


- 위키피디아 단일 책임 원칙 

https://ko.wikipedia.org/wiki/%EB%8B%A8%EC%9D%BC_%EC%B1%85%EC%9E%84_%EC%9B%90%EC%B9%99


블로그 이미지

서기리보이

,