目次

the Definition of Singleton Pattern

싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고,

어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴입니다.

특별히 대단한 내용은 없지만, 하나씩 짚고 넘어가보도록 하겠습니다.

그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 합니다. 인스턴스가 필요하면 반드시 클래스
자신을 거치도록 해야 되겠죠.

클래스한테 요청을 할 수 있게 만들고, 요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만들어야 하죠. 싱글턴이
'게으르게' 생성되도록 구현할 수도 있습니다. 그 클래스의 객체가 자원을 많이 잡아먹는 경우에는 이런 게으른 생성
기법이 꽤 유용하죠.

Implementation 1

고전적인 싱글턴 구현법을 소개합니다.

public class Singleton {
     private static Singleton uniqueInstance;
 
     private Singleton() {}
 
     public static Singleton getInstance() {
         if (uniqueInstance == null) {
              uniqueInstance = new Singleton();
         }
         return uniqueInstance;
     }
}

생성자를 private으로 선언함으로써 다른 클래스에서 instance를 함부로 생성하지 못하게 합니다. 언뜻 보기에는 그럴듯 해 보이죠. 하지만 이 구현법에는 몇가지 문제점이 있습니다.
무엇이 문제일까요?

스레드가 하나라면 이 코드는 잘 작동할 것입니다. 스레드가 하나 더 추가되면 어떤 일이 벌어질까요? 운 나쁘게 uniqueInstance가 null임을 확인하고 instance를 생성하려는 사이 다른
스레드에서 instance를 만들어버리게 되는 상황이 발생할 수도 있습니다.

Multi-thread problem

getInstance()를 동기화시키기만 하면 멀티스레딩과 관련된 문제가 간단하게 해결됩니다.
다음의 코드를 살펴보세요.

public class Singleton {
     private static Singleton uniqueInstance;
 
     private Singleton() {}
 
     public static synchronized Singleton getInstance() {
         if (uniqueInstance == null) {
             uniqueInstance = new Singleton();
         }
         return uniqueInstance;
     }
}

이렇게 하면 문제가 해결되겠지만, 동기화를 하면 속도 문제가 생기지 않을까요?
좋은 지적입니다. 그리고 조금 더 생각해 보면 동기화를 하기가 정말 아깝다는 느낌이 들 수도 있습니다.
사실 동기화가 꼭 필요한 시점은 이 메소드가 시작되는 때 뿐입니다. 바꿔 말하자면, 일단 uniqueInstance
변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메소드를 동기화된 상태로 유지시킬 필요가 없다는 거죠.
첫 번째 과정을 제외하면 동기화는 불필요한 오버헤드만 증가시킬 뿐입니다.

Alternative

더 효율적인 방법은 없을까요?

대부분의 자바 애플리케이션에서 싱글턴이 다중 스레드 환경에서 돌아갈 수도 있도록 만들어야 합니다.
하지만 getInstance()메소드를 동기화시키려면 적지 않은 대가를 치뤄야 합니다. 혹시 다른 방법은 없을까요?

몇가지 방법을 생각해 볼 수 있을 겁니다.

1.getInstance()의 속도가 그리 중요하지 않다면 그냥 둡니다.
그래도 됩니다. getInstance()메소드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 놔눠도 됩니다.
getInstance()를 동기화시키는 게 그리 어려운 일도 아니고, 효율 면에서도 괜찮을 수 있습니다.
메소드를 동기화하면 성능이 100배정도 저하된다는 것은 기억해 둡시다. 만약 getInstance()가 애
플리케이션에서 병목으로 작용한다면 다른 방법을 생각해봐야 합니다.

2.인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버립니다.
애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고, 그 인스턴스를 항상 사용한다면, 또는
인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면 다음과 같은 식으로 처음부터 Singleton
인스턴스를 만들어버리는 것도 괜찮은 방법입니다.

public class Singleton {
     private static Singleton uniqueInstance = new Singleton();
 
     private Singleton() {}
 
     public static Singleton getInstance() {
         return uniqueInstance;
     }
}

이런 접근법을 사용하면 클래스가 로딩될 때 JVM에서 Singleton의 유일한 인스턴스를 생성해 줍니다.
JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 uniqueInstance를 정적변수에 접근할 수
없습니다.

3.“DCL(Double-Checking Locking)“을 써서 getInstance()에서 동기화되는 부분을 줄입니다.
DCL(Double-Checking Locking)을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어
있지 않았을 때만 동기화를 할 수 있습니다. 이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지
않아도 됩니다. 바로 우리가 원하던 거죠.

public class Singleton {
    private volatile static Singleton uniqueInstance;
 
    private Singleton() {}
 
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized(Singleton.class) {
                if (uniqueInstance == null) {
                     uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

인스턴스를 생성하기에 앞서 인스턴스가 있는지 확인하고, 없으면 동기화된 블럭으로 들어갑니다.
이렇게 하면 처음에만 동기화가 되겠죠. 블럭으로 들어온 후에도 다시 한 번 변수가 null인지 확인
한 다음 인스턴스를 생성합니다. volatile키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance
변수가 Singleton인스턴스로 초기화되는 과정이 올바르게 진행되도록 할 수 있습니다.

하지만 DCL을 이용할 경우 몇가지 주의할 점이 있습니다.

  1. DCL을 사용하는 방법은 자바2 버전 5(자바 1.5)보다 전에 나온 버전에서는 쓸 수 없습니다.(volatile)
  2. 클래스 로더가 여러개 있으면 싱글턴이 제대로 작동하지 않고, 여러개의 인스턴스가 생길 수 있습니다.
  3. 1.2버전보다 전에 나온 JVM을 사용하는 경우에는 가비지 컬렉터 관련 버그1)때문에 싱글턴 레지스트리를 사용해야 할 수도 있습니다.
1)
자바 1.2가 나오기 전까지는 가비지 컬렉터의 버그 때문에 싱글턴에 대한 전역 레퍼런스가 없는 경우에 아직 다 쓰지도 않는 싱글턴이 가비지 컬렉터에 의해 제거되는 일이 있었습니다. 즉, 싱글턴을 만들 수는 있지만 그 싱글턴에 대한 유일한 레퍼런스가 싱글턴 자체에만 있는 경우에는 가비지 컬렉터에 의해 그 인스턴스가 제거될 수도 있었죠. 싱글턴이 제거되고 나면 getInstance()를 호출했을 때 갓 만들어진 새 싱글턴이 리턴되기 때문에 아주 골치아픈 버그가 생길 수 있었습니다. 조금 전까지 잘 쓰고 있었는데, 갑자기 상태가 초기값으로 리셋되거나 네트워크 연결이 리셋되는 것 같은 이상한 일들이 벌어지곤 했습니다.
하지만 자바 1.2부터는 이 버그가 수정돼서 전역 레퍼런스가 없어도 가비지 컬렉터에 의해 제거되는 일은 생기지 않습니다. 피치 못할 사정이 있어서 자바 1.2가 나오기 전에 나왔던 JVM을 사용해야 한다면 이 문제를 잘 기억해 두는 것이 좋습니다. 하지만 1.2 이후 버전을 사용한다면 괜히 가비지 컬렉터가 싱글턴을 잡아먹지는 않을까 하는 걱정을 할 필요가 없습니다.