[면접질문]싱글톤 패턴 안전하게 사용하기 - bloodfinger8/AlgorithmStudy GitHub Wiki

싱글톤

일반적으로 하나의 인스턴스만 존재해야 할 경우 Singleton 패턴을 사용하게 된다. 물론 Single Thread에서 사용되는 경우에는 문제가 되지 않지만 Multi Thread 환경에서 Singleton 객체에 접근 시 초기화 관련하여 문제가 있다. 그렇다면 어떻게 코드를 작성해야 Singleton 객체를 생성하는 로직을 thread-safe 하게 적용할 수 있을까? synchronized 키워드를 추가하는 건 역할에 비해서 동기화 오버헤드가 심하다고 한다.

그렇다면 아래 3가지 방법 을 통해서 singleton을 thread-safe하게 만들어보자!

Double Checked Locking

일명 DCL이라고 불리는 이 기법은 현재 broken 이디엄이고 사용을 권고하지 않지만 이러한 기법이 있었다는 걸 설명하고 싶다. 코드는 다음과 같다.

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

메서드에 synchronized를 빼면서 동기화 오버헤드를 줄여보고자 하는 의도이다.

문제점

  • Thread A와 Thread B가 있다고 하자. Thread A가 instance의 생성을 완료하기 전에 메모리 공간에 할당이 가능하기 때문에 Thread B가 할당된 것을 보고 instance를 사용하려고 하나 생성과정이 모두 끝난 상태가 아니기 때문에 오동작할 수 있다는 것이다. 물론 이러할 확률은 적겠지만 혹시 모를 문제를 생각하여 쓰지 않는 것이 좋다.

Enum

그냥 간단하게 class가 아닌 enum으로 정의하는 것

public enum Singleton {
  INSTANCE;  
}

Enum은 인스턴스가 여러 개 생기지 않도록 확실하게 보장해주고 복잡한 직렬화나 리플렉션 상황에서도 직렬화가 자동으로 지원된다는 이점이 있다.

Enum에도 한계 보통 Android 개발을 하게 될 경우 보통 Singleton의 초기화 과정에 Context라는 의존성이 끼어들 가능성이 높다. Enum의 초기화는 컴파일 타임에 결정이 되므로 매번 메서드 등을 호출할 때 Context 정보를 넘겨야 하는 비효율적인 상황이 발생할 수 있다

LazyHolder

현재까지 가장 완벽하다고 평가 받고있다. synchronized도 필요 없고 Java 버전도 상관없고 성능도 뛰어나다.

public class Singleton {
  private Singleton() {}
  public static Singleton getInstance() {
    return LazyHolder.INSTANCE;
  }
  
  private static class LazyHolder {
    private static final Singleton INSTANCE = new Singleton();  
  }
}

객체가 필요할 때로 초기화를 미루는 것이다. Lazy Initialization이라고도 한다. Singleton 클래스에는 LazyHolder 클래스의 변수가 없기 때문에 Singleton 클래스 로딩 시 LazyHolder 클래스를 초기화하지 않는다. LazyHolder 클래스는 Singleton 클래스의 getInstance() 메서드에서 LazyHolder.INSTANCE를 참조하는 순간 Class가 로딩되며 초기화가 진행된다. Class를 로딩하고 초기화하는 시점은 thread-safe를 보장하기 때문에 volatile이나 synchronized 같은 키워드가 없어도 thread-safe 하면서 성능도 보장된다.