Singleton 패턴
Singleton 패턴을 이야기 하기 전에 먼저 주의사항부터 이야기 할 필요가 있다.
1. 가급적 쓰지 마라.
이것은 실제로 자주 겪는 문제이기도 한데, Singleton으로 객체를 생성하는 경우, 이 객체는 "전역 변수"와 같다. 단 하나의 객체만이 생성되고, static 함수(getInstance() 함수)를 통해 접근이 가능하고, static 함수가 public 이기 때문에, 전역 변수와 같은 특성을 지니게 된다. 의도한 바가 아니더라도 말이다. 그래서 마치 전역 변수처럼 사용되어 버리는 문제가 생긴다. 테스트 주도 개발(켄트 벡)이라는 책에는 싱글톤에 대해 이렇게 나와 있다.
"싱글톤 : 전역 변수를 제공하지 않는 언어에서 전역 변수를 사용하려면 어떻게 해야 할까? 사용하지 마라. 프로그램은 당신이 전역 변수를 사용하는 대신 설계에 대해 고민하는 시간을 가졌던 점에 대해 감사할 것이다."
싱글톤은 전역 변수처럼 무분별하게 사용될 여지가 있고, 특히 Unit Test를 작성하기 어렵게 만든다. 전역 변수라서 이곳 저곳에서 접근이 가능하고 static final 변수로 선언되기 때문에 대체가 불가능하다. 비슷한 문제가 enum이나 Holder 패턴에서도 발생한다. 하지만 Singleton 객체는 이들 패턴보다 훨씬 무질서하게 사용되는 경향이 있다.
Singleton 패턴에는 크게 두가지 종류가 있다. 하나는 static 변수 선언 시 바로 생성하는 방식이고, 하나는 getInstance() 함수 호출 시 생성하는 방식이다.
static으로 생성하는 방식
public class StaticSingleton {
private static final StaticSingleton instance = new StaticSingleton();
private StaticSingleton() {} // 규칙 1. private 생성자.
// instance, getInstance, singleton, shared
public static StaticSingleton getInstance() // 규칙 2. 오직 한개만 만듦
{
return instance;
}
public static void main(String[] args) {
StaticSingleton instance = StaticSingleton.getInstance();
}
}
이 방식은 특별히 구현에 신경 쓸 것이 별로 없다. 생성자를 private으로 만들어서 객체를 외부에서 생성하지 못하도록 하는 것, 그리고 getInstance()라는 메소드를 static으로 제공해 줌으로써 외부에서 객체에 접근할 수 있도록 하는 것이다.
getInstance() 함수를 통해 생성하는 방식
public class Singleton {
private volatile static Singleton instance = null;
public static Singleton getInstance(){
if (instance == null) { // A
synchronized(Singleton.class) {
if (instance == null) { // B
instance = new Singleton();
}
}
}
return instance;
}
private Singleton(){} // 생성자를 통한 생성 방지
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
}
}
앞서 static 생성시와의 차이점 중 하나는 instance 변수 선언 시 Singleton 객체가 생성되지 않는다는 점이다. Singleton 객체가 매우 크고, 쓰일 가능성이 매우 낮은 경우에 보통 사용하는 방식이다. 객체의 생성은 getInstance() 함수가 호출 될 때 이루어진다.
이 경우 객체가 이미 생성되었는지 그렇지 않은지(null 여부)를 체크함으로써 중복 생성을 방지할 필요가 있다. 이 때 멀티 쓰레드 상태에서 발생할 수 있는 레이싱 문제도 고려해 주어야 한다.
위에서 보면 instance 변수가 null인지 체크하는 부분이 두 곳(A와 B)이 있다.
첫번째(A)는 일단 instance가 null이 아닐 경우 synchronized 블럭 내부로 들어가는 것을 미리 차단함으로써 Singleton 객체를 얻기 위해 발생하는 부하를 줄여 주는 역할을 한다.
일단 null임이 확인된 이후에는 synchronized 블럭으로 넘어가게 되는데, 이 때는 이 블럭이 단 하나의 쓰레드만 내부로 접근할 수 있도록 막는다.
이 후 instance 객체의 null 체크를 한번 더 한다(B). 이는 맨 처음 synchronized 블럭에 들어온 쓰레드를 위한 것이 아니고 그 다음에 들어오는 경우를 위한 것이다.
즉, A에서 두 쓰레드가 instance를 null로 판단했고, 둘 다 synchronized 블럭에 접근했다고 하자. 첫번째 쓰레드는 일단 instance 객체를 생성하고 종료 한다.
두번째 쓰레드가 B 구문에 도달했을 때에는 이미 첫번째 쓰레드가 생성을 마치고 난 다음이다. 따라서 두번째 쓰레드는 B에서 instance 객체가 null이 아니라고 판단하게 된다. 그리고 그냥 블럭을 빠져 나온다. 이를 통해서 instance 객체가 중복 생성되는 것을 막을 수 있다.
사실상 최근의 컴퓨팅 파워를 고려할 경우 두번째 방식은 실제로는 거의 의미가 없다시피 하다. 어쨌든 필요한 경우 사용해 보려면 정확한 구현 메카니즘을 이해하고 있는 것이 도움이 될 것이다.
다시 한번 강조하지만 가급적 Singleton은 사용하지 않는 편이 좋다.