Singleton은 내가 처음 접한 디자인 패턴이기도 하고, 잘 사용하면 정말 편리하게 사용할 수 있는 패턴이다. 프로그래밍을 하다 보면 객체 인스턴스가 유일하게 존재해야 하는 경우가 있다. 가령 게임에서는 특정 데이터나 상태를 Managing 할 때, 예를 들어 게임의 전반적인 상태를 관리해주는 GameManager같은 경우에는 누구나 쉽게 인스턴스에 접근할 수 있으면 좋지만, 여러 개 생겨나면 안된다. 이런 경우 Singleton 패턴을 사용하게 된다.
UML
먼저 간단하게 UML을 살펴보자.
Singleton의 UML은 조금 독특하게 생겼다. 일단 노드가 하나이다. 노드가 하나인 것도 이상한데, 자기 자신을 가지고 있다…. 뭔가 독특한 형태의 UML이다. 이번 패턴은 UML보다는 소스 코드가 더 중요하다. 구현방법이 여러 가지가 있기 때문인데 각자의 장단점이 있으니 소스코드를 통해 한번 비교해 보자.
소스 코드 : Basic
위 소스는 가장 기본적인 Singleton의 형태이다. Singleton은 기본적으로 생성자를 private 영역에 숨기게 된다. 생성자를 private 영역에 구현하면 외부에서는 클래스의 생성자를 모르기 때문에 해당 클래스를 생성할 수 없다. 일단 이렇게 해서 더는 Singleton 클래스가 외부에서 생성되는 것을 막았다. 그런데 이렇게만 하면 해당 클래스가 존재하지 않게 된다. 그래서 유일하게 생성자를 알 수 있는 영역인 객체 내부에서 static 맴버 변수로 객체를 만들게 된다. 그래서 UML에 표시된 것처럼 자기 자신을 가지고 있게 되는 것이다.
일단 이렇게 구성되어 있는 것이고, 장단점을 알아보자. 이렇게 만들게되면 가장 단순하게 구현할 수있다는 장점이 있지만, 다음과 같은 단점이 있다.
1. static 맴버 변수로 인스턴스를 생성하였기 때문에 프로그램 시작시 무조건 생성된다. 즉, 필요 없을 때에도 Singleton 인스턴스가 생성되어 메모리에 올라가있게된다.
2. C++표준에서는 전역 객체들의 생성순서가 명확하지 않기 때문에 다른 전역 객체의 생성자에서 참조하고 싶은 경우에 문제가 발생한다. (흔한 초기화 타이밍 문제...)
1번 문제는 다음과 같이 소스코드를 바꾸면 해결할 수 있다.
소스 코드 : Dynamic
Singleton 인스턴스를 가져오는 getInstance 메소드에서 인스턴스가 있는 지확인하고 없으면 동적 생성하게 되는 코드를 삽입했다. 이렇게 된다면, 프로그램이 시작할 때 인스턴스가 생성되는 것이아니라 ?getInstance 메소드가 처음 호출되는 순간 인스턴스가 생성될 것이다. 이제 우리가 필요하기 시작할 때 부터만 Singleton 인스턴스를 생성할 수 있게 되었다. 한 개의 머리를 자르면 그 자리에서 두 개의 머리가 생겨난다는 하이드라의 격언(?!)처럼 Dynamic을 이용하여 문제를 해결했더니 또 다른 문제가 생기기 시작한다. 다음 상황을 가정해보자.
MultiThread 환경에서 최소 2개 이상의 Thread가 getInstance 메소드를 연속적으로 호출했을 때 OS 수업에서 MultiThread에 대한 내용을 배웠다면 금방 왜 문제가 되는지 이해될 것이다. 물론 배운 것처럼 Mutex를 이용하면 해결 가능하다.
소스 코드 : Mutex
이제 완벽(?!)해 보이나? 그런데 아쉽게도 알다시피 Mutex를 사용하여 크리티컬 섹션을 관리하게 되면, 성능 저하를 크게 일으킬 수 있다. 이중 검사 동기화 (Double-checked locking)이라는 방법을 사용하면 부담을 줄일 수 있다.
소스 코드 : Double Checked Locking
instance를 두 번 확인하면서 안전성을 확보하는 방법인 Double Checked Locking을 사용하였다. 와 이제 그럼 성능도 어느 정도 보장하고, Thread-safe한 싱글 톤이 완성되었을까? 아니다. 사실 관련 논문들을 보면 Double-Checked Locking은 결코 ?Thread-safe 하지 않다고 한다.... 뭐 어쩌라는 거지.... 사실 Thread-safe하면서 효율적인 Singleton 객체를 구현하기가 쉽지만은 않은 일이라고 한다.
그래서 어쩌라는 건지 답답하다.
여러 커뮤니티에서도 논쟁을 하고 있는 모양이다. 항간에는 getInstance에서 지역 변수를 이용하는 게 가장 안전하다는 말도 있고, volatile 키워드를 이용하는 것도 있지만, 컴파일러마다 구현이 달라서 별로 추천하지는 않는다. 혹은, 메인 Thread에서 먼저 생성하고 다른 Thread에서 사용하는 방법도 있다.
각각의 구현 방법에 따라서 각자 장단점이 있는 구조이기 때문에 어느 걸 콕 집어서 "이거 써라!"라고 할 수 없다. 상황에 맞게 구현을 선택하고 컨벤션을 잘 맞추어 사용하는 것이 위험부담을 줄이는 효과적인 방법이 될 거라고 생각한다.
결론
사실 싱글톤을 완벽하게 만드려는 여러가지 시도들이 많았지만 아직까지는(?) 완벽하게 해결한 방법이 없는 것같다. 위에서 소개한 싱글톤 이외에도 정말 여러가지 방법론들이 많지만, 결국 자신이 프로그래밍하는 상황을 잘파악하여 적절한 방법론을 사용하거나 프로그래밍을하면서 적절한 제약을 가해주는 것만이 방법일 것이다.