관리 메뉴

드럼치는 프로그래머

[안드로이드] Multi Thread 환경에서의 올바른 Singleton 본문

★─Programing/☆─Android

[안드로이드] Multi Thread 환경에서의 올바른 Singleton

드럼치는한동이 2018. 2. 27. 11:04

[출처] https://medium.com/@joongwon/multi-thread-%ED%99%98%EA%B2%BD%EC%97%90%EC%84%9C%EC%9D%98-%EC%98%AC%EB%B0%94%EB%A5%B8-singleton-578d9511fd42


해당 URL의 소중한 자료 정독 후 프로그래밍 학습에 도움이 되었음을 밝힙니다.


일반적으로 하나의 인스턴스만 존재해야 할 경우 Singleton 패턴을 사용하게 된다. 물론 Single Thread에서 사용되는 경우에는 문제가 되지 않지만 Multi Thread 환경에서 Singleton 객체에 접근 시 초기화 관련하여 문제가 있다.

보통 Singleton 객체를 얻는 static 메서드는 getInstance()로 작명하는 게 일반적이다.

그렇다면 어떻게 코드를 작성해야 Singleton 객체를 생성하는 로직을 thread-safe 하게 적용할 수 있을까? 정말 단순하게 별로 신경 쓰고 싶지 않다면 메서드에 synchronized 키워드만 추가해도 무방할 것이다.

그렇지만 좋은 개발자가 되기 위해선 효율적인 코드에 대해서 고민을 해 봐야 한다. 메서드에 Singleton 클래스의 getInstance() 메서드에 synchronized키워드를 추가하는 건 역할에 비해서 동기화 오버헤드가 심하다고 생각한다.

그래서 개발자들 사이에서 Singleton 초기화 관련하여 많은 이디엄들이 연구되었고 몇몇 이디엄들을 소개하려고 한다.


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를 빼면서 동기화 오버헤드를 줄여보고자 하는 의도로 설계된 이디엄이다. instance null 인지 체크하고 null 일 경우 동기화 블럭에 진입하게 된다. 그래서 최초 객체가 생성된 이후로는 동기화 블럭에 진입하지 않기 때문에 효율적인 이디엄이라고 생각할 수 있다. 하지만 아주 안 좋은 케이스로 정상 동작하지 않을 수 있다. 그래서 broken 이디엄이라고 하는 것이다.

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


Enum

Java에 지대한 공헌을 한 Joshua Bloch가 언급한 이디엄이다. 그냥 간단하게 class가 아닌 enum으로 정의하는 것이다.


public enum Singleton {
  INSTANCE;  
}

Enum은 인스턴스가 여러 개 생기지 않도록 확실하게 보장해주고 복잡한 직렬화나 리플렉션 상황에서도 직렬화가 자동으로 지원된다는 이점이 있다. 하지만 Enum에도 한계는 있다. 보통 Android 개발을 하게 될 경우 보통 Singleton의 초기화 과정에 Context라는 의존성이 끼어들 가능성이 높다. Enum의 초기화는 컴파일 타임에 결정이 되므로 매번 메서드 등을 호출할 때 Context 정보를 넘겨야 하는 비효율적인 상황이 발생할 수 있다. 결론은 Enum은 효율적인 이디엄이지만 상황에 따라 사용이 어려울 수도 있다는 점이다.


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 하면서 성능도 보장하는 아주 훌륭한 이디엄이라고 할 수 있다

현재까지 LazyHolder에 대해서 문제점은 나타나지 않고 있다. 혹시나 Multi Thread 환경에서 Singleton을 고려하고 있다면 LazyHolder 기법을 이용하자.


Comments