자바에서 많이 사용하는 싱글톤 패턴에 대해서 알아볼건데, 멀티 스레드 환경에서 어떻게 짜야 하는지 확인해보도록 하겠습니다.

싱글톤 패턴

GoF(Gang of Four) 디자인 패턴 중 하나로 생성자가 여러 차례 호출되더라도 실제로 생성되는 인스턴스는 하나이고, 최초 생성 이후에 호출된 생성자는 최초에 생성한 객체를 리턴하는 형태

일반적인 형태

class Singleton {
    private static Singleton ourInstance = null;

    public static Singleton getInstance() {

        if (ourInstance == null) {
            ourInstance = new Singleton();
        }

        return ourInstance;
    }

    private Singleton() {}
}

호출 방법

// 보통 인스턴스 호출 방법(새로운 인스턴스가 생성)
Singleton singletonInstance = new Singleton();

// 싱글톤 호출 방법(한번 생성한 인스턴스가 지속적으로 사용)
Singleton singletonInstance = Singleton.getInstance();

문제점

보통의 경우에는 해당 코드로도 전혀 문제가 되지 않지만, 멀티 스레드 환경에서는 잠재적 문제를 가지고 있다.

두 개 이상의 스레드가 인스턴스를 획득하기 위해 getInstance() 메서드에 진입하여 경합을 벌이는 과정에서 서로 다른 두 개의 인스턴스가 만들어지는 형태가 발생할 여지가 있다.

해결방법 1)

인스턴스를 필요할 때 생성하지 않고, 처음부터 인스턴스를 만들어 버린다, 단 인스턴스를 미리 만들어 버리게 되면, 불필요한 시스템 리소스를 낭비할 가능성이 있다.

public class Singleton {
	private static Singleton ourInstance = new Singleton();

	public static Singleton getInstance() {
		return ourInstance;
	}

	private Singleton() {}

해결방법 2)

getInstance() 메서드를 동기화시킨다. 대신 메서드를 동기화 시키면 일반적으로 성능이 100배 정도는 저하된다.


class Singleton {
    private static Singleton ourInstance = null;

    public synchronized static Singleton getInstance() {

        if (ourInstance == null) {
            ourInstance = new Singleton();
        }

        return ourInstance;
    }

    private Singleton() {}
}

해결방법 3)

DCL 기법을 사용한다. 현재는 권고하지 않는 방법이다.

class Singleton {
    private static Singleton ourInstance = null;

    public static Singleton getInstance() {

        if (ourInstance == null) {
            synchronized (Singleton.class) {
                if (ourInstance == null) {
                    ourInstance = new Singleton();
                }
            }
        }

        return ourInstance;
    }

    private Singleton() {}
}

해결방법 4)

LazyHolder 기법으로 synchronized도 필요 없고, 자바 버전도 상관없는 방법으로, Singleton 클래스의 getInstance() 메서드에서 LazyHolder.INSTANCE를 참조하는 순간 Class가 로딩되며 초기화가 진행된다. Class를 로딩하고 초기화하는 시점은 thread-safe를 보장하기 때문에 volatile이나 synchronized 같은 키워드가 없어도 된다.

class Singleton {
    private Singleton() {}

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }

    private static class LazyHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
}

번외

코틀린에서는 만들어진 object 클래스 선언만으로 손쉽게 싱글톤 패턴을 이용할 수 있습니다. 그럼 해당 Kotlin의 Object 클래스는 Java에서는 어떻게 표현이 되는지 한번 살펴보도록 하겠습니다.

먼저 Kotlin 코드를 보도록 하겠습니다.

object SingletoneTest {

    fun test() {
        println("Hello World")
    }

}

다음은 아래 Kotlin 코드를 자바 코드로 디컴파일 한 부분을 살펴보도록 하겠습니다. 보면 INSTANCEstatic으로 만들어 놓고 static 블록에서 할당하는 것을 볼 수 있습니다.

import kotlin.Metadata;

@Metadata(
   mv = {1, 1, 15},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"\u0000\u0012\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n\u0000\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0006\u0010\u0003\u001a\u00020\u0004¨\u0006\u0005"},
   d2 = {"LSingletoneTest;", "", "()V", "test", "", "com.jw.DataStructure.main"}
)
public final class SingletoneTest {
   public static final SingletoneTest INSTANCE;

   public final void test() {
      String var1 = "Hello World";
      boolean var2 = false;
      System.out.println(var1);
   }

   private SingletoneTest() {
   }

   static {
      SingletoneTest var0 = new SingletoneTest();
      INSTANCE = var0;
   }
}