싱글턴(Singleton) 패턴은 1995년 발간된 소프트웨어 개발 고적 서적 Design Patterns 통하여 세상에 알려지게 되었다.
오랜동안 많은 개발자들이 사용을 즐겼지만 2000년 중반부터 코드를 망가트리는 원인으로 주목을 받으며 안티패턴(Anti-Pattern)의 하나로 간주되고 있다.
다음은 싱글톤에 대한 증오를 보여주는 초장기(온라인에서 검색가능한) 블로그 글이다.
오랜 기간동안 안티 패턴으로 간주되고 있지만 아니러니(irony) 그 편리함으로 인하여 많은 개발자들에 의하여 사용되고 있다.
싱글턴 패턴(Singleton) 이란 ?
싱글턴은 인스턴스가 하나뿐인 객체를 만들 수 있게 해주는 디자인 패턴으로 클래스 디자인 관점에서 보면 아주 간단해보이지만 구현하는 데 있어서는 쉽지않은 장애물들이 있다.
“싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하는 패턴이다.”“Singletons are classes which have a restriction to have only one instance.”
다음 코드는 단일 인스턴스 객체를 구현하는 고전적인 싱글턴 코드이다.
public class Singleton {
// ❸ 유일한 인스턴스 저장을 위한 변수
private static Singleton instance ;
// ❷ 외부에서 인스턴스 생성이 불가하게 생성자를 private 로 선언
private Singleton (){ }
// ❶ Singleton 클래스 인스턴스를 생성하여 리턴.
public static Singleton getInstance (){
if(instance == null ){
instance = new Singleton();
}
return instance ;
}
}
동시성(Concurrency) 문제
간단하게 테스트 코드를 만들어 실행해보았다.
import java.lang.reflect.Constructor;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SingletonTest {
public static void main(String[] args) {
new SingletonTest().test();
}
private static Logger log = LoggerFactory.getLogger(SingletonTest.class);
public void test() {
System.out.println("start sigleton test.");
new MultiThread("thread01").start();
new MultiThread("thread02").start();
new MultiThread("thread03").start();
};
public class MultiThread extends Thread {
private String name;
public MultiThread(String name) {
this.name = name;
}
public void run() {
int count = 0;
for(int i=0; i<5; i++) {
Singleton singleton = Singleton.getInstance();
System.out.println( name + " : " + singleton.toString());
try {
Thread.sleep(400);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
① Earger Initialzation
public class Singleton {
private static Singleton instalnce = new Singleton() ; // ❷ 유일한 인스턴스를 처음부터 생성
private Singleton (){ // ❶ 외부에서 인스턴스 생성이 불가하게 생성자를 private 로 선언
}
public static Singleton getInstance (){ // ❸ Singleton 클래스 인스턴스를 리턴.
return instance ;
}
}
② Thread-Safe Initialzation (Lazy Initialzation)
public class Singleton {
private static Singleton instalnce ; // ❷ 유일한 인스턴스 저장을 위한 변수
private Singleton (){ // ❶ 외부에서 인스턴스 생성이 불가하게 생성자를 private 로 선언
}
public static synchronized Singleton getInstance (){ // ❸ Singleton 클래스 인스턴스를 리턴. (인스턴스가 필요할 때 생성.)
if( instance == null )
instance = new Singleton();
return instance ;
}
}
③ Thread-Safe Initialzation (Lazy Initialzation) : DCL(Double-Checking Locking)
public class Singleton {
private volatile static Singleton instance ; // ❸ 유일한 인스턴스 저장을 위한 변수
private Singleton (){ // ❷ 외부에서 인스턴스 생성이 불가하게 생성자를 private 로 선언
}
public static Singleton getInstance (){ // ❶ Singleton 클래스 인스턴스를 생성하여 리턴.
if( instance == null ) {
synchronized( Singleton.class ) { // 객체를 생성하는 부분만 동기화를 적용한다.
instance = new Singleton();
}
}
return instance ;
}
}
- 첫번째는 순서 일관성의 문제가 아니라 최적화를 통해 코드가 옮겨지는 문제(reordering),
- 두번째는 많은 JVM이 volatile에 대한 순서 일관성조차 제대로 구현하고 있지 않다.
public class Singleton {
private volatile static Singleton instance ; // ❸ 유일한 인스턴스 저장을 위한 변수
private Singleton (){ // ❷ 외부에서 인스턴스 생성이 불가하게 생성자를 private 로 선언
}
public static Singleton getInstance (){ // ❶ Singleton 클래스 인스턴스를 생성하여 리턴.
Singleton instanceToUse = instance ;
if( instanceToUse != null ) { // First check (no locking)
return instanceToUse ;
}
synchronized( this ) { // 객체를 생성하는 부분만 동기화를 적용한다.
if( instance == null ) // second checking (with locking)
instance = new Singleton();
return instance ;
}
}
}

- Thread 1은 getInstance() 메소드로 들어간다.
- Thread 1은 synchronized 블록으로 들어간다. instance가 null이기 때문이다.
- Thread 1은 ❶ ❷ non-null 인스턴스를 만든다. 생성자를 실행하기 전이다.
- Thread 1은 thread 2에 선점된다.
- Thread 2는 인스턴스가 null인지를 점검한다. null이 아니기 때문에, thread 2는 instance 레퍼런스를 (부분적으로 초기화된 Singleton 객체)를 리턴한다.
- Thread 2는 thread 1에 선점된다.
- Thread 1은 ❷ 생성자를 실행함으로서 Singleton 객체의 초기화를 완료하고 레퍼런스를 리턴한다.
④ Lazy Initialization. LazyHolder(Thread-safe)
public class Singleton {
private static Singleton instance ;
private Singleton (){ }
private static class Holder(){
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance (){
return Holder.instance ;
}
}
import java.lang.reflect.Constructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SingletonTest {
public static void main(String[] args) {
new SingletonTest().test();
}
private static Logger log = LoggerFactory.getLogger(SingletonTest.class);
public void test() {
System.out.println("start sigleton test.");
Singleton instance = Singleton.getInstance();
log.debug("sigleton instance : " + instance.hashCode());
Singleton instance2 = null;
try {
Constructor[] cstr = Singleton.class.getDeclaredConstructors();
for (Constructor constructor: cstr) {
constructor.setAccessible(true);
instance2 = (Singleton) constructor.newInstance();
break;
}
} catch (Exception e) {
System.out.println(e);
}
log.debug("sigleton instance : " + instance2.hashCode());
};
}
⑤ Lazy Initailization. Enum(Thread-safe)
public enum Singleton {
INSTANCE;
}
싱글턴의 문제
싱글턴이 처음 소개되었는 시대와 다르게 프로그램은 더 커지고 복잡해졌다. 개발 팀 규모는 더 커졌고 자동화된 테스트가 일반화되었다. 아마도 싱글턴 패턴의 남용과 오용이 난무했을 것이다. 많은 개발자들의 사랑을 받았던 싱글터은 이제 안티 패턴으로 불리고 있다.- 싱글턴에 의존하는 객체는 테스트를 위한 분리가 매우 어렵다.
- 코드들이 매우 강하게 연결되어 있어 재구성이 어렵다. (싱글턴은 인테페이스가 아닌 구현 클래스를 미리 생성하고 정적 함수를 이용하여 인스턴스에 접근하기 때문에 구조적으로 높은 결합도를 갖는다.)
- 참조되는 모든 클래스를 수정해야 하기 때문에 전역 객체(싱글턴)에서 비전역 객체로 변경이 어렵다.
싱글턴 문제 해결하기
- 객체 생성에 대한 책임이 개체 자신이 되면 않된다.
- 싱글턴을 참조하는 클래스는 직접적인 강한 연결이 되면 않된다.(구현 클래스를 직접 참조하는 것을 의미)
참고자료
- 『Head First Design Patterns』
- Singleton Pattern Pitfalls
- Your wrong about singletons
- Singleton Pattern Pitfalls
- How to solve the “Double-Checked Locking is Broken” Declaration in Java?
- Double-checked locking: Clever, but broken
- Singletons: Bill Pugh Solution or Enum
- Design Patterns in the Spring Framework
















