Contents

싱글톤 패턴 구현

싱글톤 패턴은 프로세스(Java에서는 JVM) 내에 1개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역 접근점을 제공하는 패턴이다.

즉시 초기화(Eager Initialization)

싱글톤 패턴을 가장 단순하게 구현하는 방법은 외부에서 해당 클래스의 인스턴스를 생성할 수 없도록 private 생성자를 만들고, 1개의 인스턴스만을 초기화(public static final)하여 이에 대한 접근을 제공하는 방식이다.

1
2
3
4
5
6
7
8
public class SimpleSingleton {
  //public static final으로 1개의 인스턴스만을 초기화하여 접근점을 제공하고, 2개 이상의 인스턴스를 못 만들게 한다.
  public static final SimpleSingleton INSTANCE = new SimpleSingleton();

  private SimpleSingleton(){
    //생성자는 private으로 만들어 외부에서 새로운 인스턴스를 만들 수 없도록 한다.
  }
}

지연 초기화(Lazy Initialization)

Singleton의 static 필드나 메소드를 사용해야 하지만, 싱글톤 객체는 초기화시키지 않고 싶은 경우가 있을 수 있다. 이러한 경우에는 Lazy Initialization 방식을 사용할 수 있다.

Lazy Initialization 방식은 Singleton 객체가 필요할 때 이를 초기화(초기화되어 있지 않은 경우에만)하고, Singleton 객체를 반환받을 수 있는 진입점(static 메소드)를 제공하는 방식이다.

Singleton.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Singleton {
  public static Singleton INSTANCE;
  
  private Singleton() {
    System.out.println("Simple singleton instance initialized.");
  }
  
  public static Singleton getInstance(){
    if(INSTANCE == null)
      INSTANCE = new Singleton();
    return INSTANCE;
  }
  
  public static void main(String[] args) throws Exception {
    System.out.println("Hello world");
    Singleton singleton = Singleton.getInstance();
  }  
}

실행 결과

1
2
Hello world
Simple singleton instance initialized.

명시적으로 getInstance 메소드를 호출하지 않는 이상 Singleton 객체(INSTANCE)가 초기화되지 않기 때문에, Eager Initialization 방식의 단점을 보완할 수 있다.

Multi Thread 환경에서의 문제점

Lazy Initialization 방식은 Multi Thread 환경에서의 문제점을 가지고 있다. Multi Thread 환경에서 서로 다른 Thread들이 동시에 getInstance를 호출한다고 생각해보자. 위의 getInstance는 Multi Thread 환경에 대한 고려가 되어 있지 않기 떄문에, if(INSTANCE == null)이 여러번 참인 경우가 발생하여 여러 개의 객체가 생성될 수 있다.

Singleton.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Singleton {
  public static final AtomicInteger NUM_INSTANCE = new AtomicInteger(0);
  private static Singleton INSTANCE;

  private Singleton() {
    System.out.println("Constructor called");
  }

  public static Singleton getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
      NUM_INSTANCE.incrementAndGet();
    }
    return INSTANCE;
  }

  public static int getNumberOfInstance() {
    return NUM_INSTANCE.get();
  }

  public static void main(String[] args) throws Exception {
    Thread[] threads = new Thread[1000];
    for (int i = 0; i < threads.length; i++) {
      threads[i] = new Thread(new Runnable() {
        public void run() {
          Singleton.getInstance();
        }
      });
    }
    for (int i = 0; i < threads.length; i++) {
      threads[i].start();
    }


    for (int i = 0; i < threads.length; i++) {
      threads[i].join();
    }

    System.out.println(Singleton.getNumberOfInstance());
  }
}

실행 결과

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
Constructor called
24

Multi Thread 환경에서 Lazy Initialization 방법

  • 정적 팩터리 메서드에 동기화 키워드 사용하기
    • Singleton 객체를 생성하는 정적 팩터리 메서드 자체에 synchronized를 붙여주거나, 내부 초기화 구문을 synchronized 구문으로 감싸주는 방법이 있다.
  • 정적 팩터리 메서드 자체에 synchronized 붙이기
    • 기존의 public static Singleton getInstance()public static synchronized Singleton getInstance()와 같이 바꾸는 방법이다.
  • 내부 초기화 구문을 synchronized로 감싸주기
    • 초기화에 사용할 Lock 객체를 만들고, Singleton 객체의 초기화 여부 확인 시 동기화하는 방법이다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private static Object lock = new Object();

public static Singleton getInstance() {
  synchronized (lock) {
    if (INSTANCE == null) {
      INSTANCE = new Singleton();
    }
  }
  return INSTANCE;
}

동기화 방식은 Multi Thread 환경에서 객체를 안전하게 1개를 생성할 수 있지만, 객체 획득 시의 병렬성을 잃어버린다는 단점이 존재한다. 또한 초기 호출 이후에는 객체 초기화 확인 구문(if(INSTANCE) == null))이 계속해서 참을 반환하기 때문에 불필요한 Lock을 거는 상황이 발생하게 된다.

DCL(Double-Checked-Locking) 방식 사용하기

Multi Thread 환경에서 병렬성을 확보하기 위해서는 동기화 구문을 필요한 곳에서만 사용해야 한다. DCL 방식은 동기화를 최소화하므로써 불필요한 Lock을 거는 단점을 없앤 방식이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private static Object lock = new Object();

public static Singleton getInstance() {
  if (INSTANCE == null) {
    synchronized (lock) {
      if (INSTANCE == null) {
        INSTANCE = new Singleton();
      }
    }
  }
  return INSTANCE;
}

첫번째 if문을 통해 객체가 초기화되었는지 검사할 때는 동기화를 하지 않고, 두번째 if문을 통해 객체가 초기화되었는지 검사할 때는 동기화를 수행한다. getInstance 호출 후 충분한 시간이 지나 동기화가 필요 없을 때는 첫번째 if문만 실행하므로써 동기화에 따른 병렬성 감소를 감소시킬 수 있는 방식이다.

언뜻 보기에는 동기화 문제도 없고 성능적으로 뛰어난 방식이라고 생각할 수 있다. 그러나 위 코드는 문제점을 가지고 있다.

위 DCL 코드의 문제점과 해결

위 DCL 코드는 JVM의 Low-Level 동작 방식때문에 문제가 발생한다.

예를 들어 2개의 Thread(A, B)가 getInstance를 호출했다고 생각해보자. Thread A는 INSTANCE = new Singleton() 구문을 수행하고 있고, 이 때 Thread B는 첫번째 if문에서 INSTANCE의 초기화 여부를 검사(if(INSTANCE == null))하고 있다. 여기서 Thread A가 수행하는 ‘객체 생성’과 ‘변수 할당’ 구문은 한번에 실행되지 않고, 특정 시점에는 INSTANCE에는 일부만 초기화된 객체가 할당되어 있을 수 있다. 이 때 Thread B는 non-null check만 하기 때문에 일부만 초기화된 객체를 가져다 쓰게 되어 Crash가 발생할 수 있다.

이러한 문제점을 해결하기 위해서는 INSTANCE 객체에 volatile 키워드를 붙여주면 된다. volatile은 원자적으로 동작하기 때문에 첫번째 if문이 완전히 초기화된 객체를 얻거나 아닌 경우 둘 중 하나로 동작하게 된다.

Lazy Holder 방식 사용하기

Multi Thread 환경에서도 잘 동작하고, Lazy Initialization도 가능한 방식이다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class LazySingletonHolder {
  public static Singleton getInstance() {
    return Singleton.INSTANCE;
  }

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

    private Singleton() {

    }
  }
}

위 코드를 보면 ‘Lazy Initialization 방식이 아니지 않은가?’ 라고 생각할 수 있다. LazySingletonHoldergetInstance에서 Singleton 클래스의 INSTANCE를 그대로 반환하기 때문이다. 그러나 Singleton 클래스의 INSTANCESingleton 클래스의 클래스 로딩이 일어날 때 발생하게 되고, 클래스 로딩은 LazySingletonHoldergetInstance가 호출되었을 때 발생한다. 또한 INSTANCEstatic final 이기 때문에 클래스 로딩에 의해 1회만 초기화되고, 이로써 Multi Thread 환경에서의 문제점도 없어지게 된다.

참고

https://leeyh0216.github.io/posts/singleton/