싱글턴 패턴은 그 아이디어가 매우 단순한 것에 비해, 이를 제대로 구현하기란 까다로운 일이다. 인스턴스가 하나만 생기면 되는 것이 아니라 반드시 하나만, 게다가 애플리케이션 라이프 사이클 전체를 통틀어서 반드시 하나만(only one ever) 존재하도록 보장해야하기 때문이다. 객체의 생성이 thread-safe해야하는 건 기본이고 도중에 없어졌다가 다시 생성되어도 안된다는 얘기다.
실제로는 이런 엄밀한 단일성을 요하는 곳에 이 패턴이 사용되기 보다는, 손쉽게 전역변수를 읽고 쓰는 방식으로 활용(오용?)되고 있어서 요즘은 안티패턴으로 취급하기도 한다.
enum을 활용하는 방법이 가장 단순하긴 하지만 lazy-initialization을 할 수 없고, double-checked locking 정도가 많이 쓰이는데 이 방식에도 결함이 있다.
initialization-on-demand holder 이디엄이라는 것을 발견했는데 아마도 자바에서 가장 효율적이면서도 엄밀하게 싱글턴을 구현하는 유일한 방법일 것이라 생각한다.
내부적으로 static class를 하나 선언해두고 이 클래스가 초기화되는 시점에 자신의 인스턴스를 생성하는 방식이다. 클래스 초기화는 동시에 여러 thread에서 실행되지 않도록 JLS에 의해 강제되고 있으므로, 프로그래머가 동기화를 위한 추가적인 코드를 작성할 필요가 없다고 한다.
메커니즘적으로 완전히 lock-free는 아닌 것 같고 JVM 내부적으로 동기화를 처리할 것같다는 생각이 들었다. 스펙 문서를 보니 역시 lock을 사용하고 있다. 그렇다면 여전히 동기화로 인한 성능 저하가 있지 않을까 하는 의문이 들었는데, 성능을 분석한 블로그를 발견했다.
http://literatejava.com/jvm/fastest-threadsafe-singleton-jvm/
성능비교표를 보면 double-checked locking 방식 보다는 빠르긴 하지만 크게 차이가 없음을 볼 수 있다. 최초에 한 번만 동기화하고 이후부터는 그냥 static field 읽기만 하는 방식인 건 둘 다 동일하고, 약간의 오버헤드 차이로 인한 것으로 보인다.
사실 요즘 JDK 5 미만 쓰는 곳은 없을 거고, double-checked locking을 volatile(immutable이면 final)과 함께 쓰는 방식에 비해서 코드 양이 그다지 줄어드는 것 같지도 않다. Holder 클래스를 따로 하나 선언하는 것도 썩 깔끔하진 않아보인다. 나로서는 1000만 번 호출했을 때 6ms 정도의 성능 차이가 발생하는 것이 문제가 될 만한 케이스를 상상하기도 어렵다.
결론은 각자의 상황과 요구조건에 맞게 사용하면 된다고 본다. JDK 4를 쓴다면 다른 대안이 없을거고, 짧은 시간에 경쟁적으로 객체에 접근하는 상황이 아니라면 엄밀하게 thread-safe할 필요도 없고, 중간에 객체가 없어졌다가 다시 생겨도 문제가 되지 않거나 이를 피할 수 없는 상황이라면 또 거기에 맞게 보완해서 쓰면 된다. 패턴을 엄밀하게 구현하느라 정작 본인이 개발하는 소프트웨어의 본질을 잊어서는 안된다. 패턴 좀 안 쓰면 어떻고 변형 좀 하면 어떤가. 어디까지나 개발의 목적에 맞게 수단을 선택해야할 것이다. 다만 이런 내용들을 알고 쓰느냐 모르고 쓰느냐는 차이가 있고, 특히 싱글턴이 아닌 것을 싱글턴이라 우겨서는 곤란하다.