Simple&Natural

Multi-threaded 환경에서 싱글톤 패턴 사용 방법 내용 보강(feat. DCL) 본문

안드로이드(Android)/연구 및 프로젝트

Multi-threaded 환경에서 싱글톤 패턴 사용 방법 내용 보강(feat. DCL)

Essense 2020. 9. 24. 04:10
728x90

이전에 멀티스레드 환경에서 싱글톤 패턴을 사용할 때 할때 유의해야 할 코드 패턴에 대해 작성했던 글이 있다.

이후 사용할 일이 없어서 잠시 잊고 지냈다가 최근 Repository나 DataSource를 구현할 때 싱글톤을 쓰게 되는 일이 자주 있어서 이 부분에 대해 추가적으로 학습하고 정리한 내용을 기록해보고자 한다.

 

우선 싱글톤 패턴 사용시 기존에 가장 잘 알려져 있는 코드 작성 방법 중 하나는 DCL(Double Checked Locking)이다.

 

class Singleton(
    private val param: SomeParameter
) {

    ...

    companion object {
        private var instance: Singleton? = null

        fun getInstance(param: SomeParameter) =
            instance ?: synchronized(this) {
                instance ?: Singleton(param).also { instance = it }
            }
    }

}

 

위의 코드에서 instance가 null인지 한번 체크한 뒤 synchronized 블록으로 진입하고 이후 다시 한번 null체크를 하고 객체를 생성한다. synchronized 사용 시 블로킹으로 인한 오버헤드가 크기 때문에 getInstance 전체를 synchronized로 처리하지 않고 내부에 null 체크를 거쳐서 조금이라도 오버헤드를 줄여보고자 하는 목적으로 사용하는 이디엄이다.

 

이때, 두 번의 확인(Double Checked)과정을 거치는 이유는 다음과 같다.

 

1) ThreadA가 이미 synchronized 블록으로 진입한 상태에서 lock을 획득하고 ThreadB가 synchronized 블록 앞에서 대기

2) ThreadA가 인스턴스를 생성하고 나서 lock을 반환하고 ThreadB가 다시 lock을 획득하여 동기화 블록으로 진입

3) 인스턴스가 이미 생성되었으나 이미 블록으로 진입한 ThreadB도 인스턴스를 생성

 

위의 3번의 과정이 발생하는 것을 막고자 블록 안으로 진입한 뒤에 다시 한 번 null 체크를 거쳐서 인스턴스를 생성하게 된다.

 

그러나 이러한 방법도 아직 문제가 발생할 소지가 있다.

바로 인스턴스가 메인 메모리에 완전히 할당되지 못한 상태에서 ThreadB가 인스턴스에 접근하는 경우이다.

 

 

Multi-Core & Multi-Thread 환경에서 각 스레드는 CPU코어의 캐시를 사용한다.

 

 

Thread1이 인스턴스를 만들었으나 아직 캐시에서 메인 메모리에 적재되지 않은 경우 발생할 수 있는 문제를

Volatile이라는 키워드를 사용해 Memory R/W에 대한 인스턴스의 원자성을 보장함으로써 해결한다.

 

class Singleton(
    private val param: SomeParameter
) {

    ...

    companion object {
        @Volatile private var instance: Singleton? = null

        fun getInstance(param: SomeParameter) =
            instance ?: synchronized(this) {
                instance ?: Singleton(param).also { instance = it }
            }
    }

}

 

이제 Thread-safe 한 코드가 완성되었다.

 

그러나 이 방법은 관용구 코드가 상당히 많기도 하고 성능상의 이슈와 더불어 Volatile이 JDK1.4 이하에서는 제대로 호환되지 않는 문제가 있기 때문에 자바에서는 아래의 LazyHolder 패턴이 권장되고 있는 것 같다.

 

다만, 위의 DCL 예제처럼 만약 인스턴스에 파라미터를 넘겨주어야 할 필요가 있는 경우 Holder의 인스턴스가 함수가 아닌 변수로 선언되었기 때문에 코드 구현이 복잡해지게 된다.

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

 

 

 

이밖에도 Enum 클래스를 이용한 방식이나 코틀린의 경우 Object를 이용하여 최상위 객체를 만드는 방법도 있으니
필요한 경우 이를 이용해도 된다. 웬만한 경우는 DCL과 Volatile로도 충분히 커버가 가능하다고 보기 때문에 굳이 크게 신경쓰지 않고 사용해도 될 것 같다. 또한 안드로이드 Codelab에 있는 프로젝트도 주로 DCL+Volatile 을 조합한 방법을 많이 사용하고 있어서 나도 주로 이 방법을 사용하는 편이다.

 

 

 

728x90