본문 바로가기

Develop/DESIGN PATTERNS

[GoF Design Pattern] 싱글톤 패턴 (Singleton pattern)

1. 싱글톤 패턴이란?

인스턴스를 오직 한 개만 제공하는 패턴이다. (객체 생성과 관련된 디자인 패턴)

시스템 설정 정보를 담고 있는 클래스와 같은 경우 인스턴스가 여러 개일 때 문제가 발생할 수 있다.

2. 구현 방법

1) private 생성자 + static 메서드

public class Settings {
  
  private static Settings instance;
  
  private Settings() {}
  
  public static Settings getInstance() {
    if (instance == null) {
      instance = new Settings();
    }
    return instance;
    }
}

생성자를 private으로 선언함으로써 new를 통한 새로운 인스턴스의 생성을 막는다.

또한 static 메서드를 제공하여 항상 같은 인스턴스를 반환한다.

(static 메서드가 아니라면 getInstance() 메서드를 호출할 때마다 새로운 인스턴스를 생성한다)

 

하지만 이 방법은 멀티 스레드 환경에서 안전하지 않다.

if (instance == null) {
  instance = new Settings();
}

만약 해당 if문에 여러 스레드가 동시에 진입한다면, if문을 통과하게 되어 서로 다른 인스턴스를 생성한다. (싱글톤이 깨진다)

2) synchronized 키워드

public static synchronized Settings getInstance() {
  if (instance == null) {
    instance = new Settings();
  }
  return instance;
}

static 메서드에 synchronized 키워드를 붙여줌으로써 한번에 하나의 스레드만 해당 메서드에 접근할 수 있게 할 수 있다.

자바에서는 synchronized 메서드는 자신이 포함된 객체에 lock을 건다. 따라서 lock을 가진 스레드만 해당 메서드에 접근이 가능하다. 단, 이때 static 메서드는 클래스 단위로 lock을 걸게 된다.

그러나 getInstance() 메서드를 호출할 때마다 동기화 작업을 하기 때문에 성능 이슈가 있을 수 있다.

3) 이른 초기화(eager initialization)

private static final Settings INSTANCE = new Settings();
private Settings() {}

public static Settings getInstance() {
  return INSTANCE;
}

인스턴스를 미리 생성해두고 static 메서드에서는 해당 인스턴스를 매번 반환한다. 따라서 스레드 세이프하다!

(클래스가 로딩되는 시점에 인스턴스가 먼저 생성된다)

 

다만 인스턴스를 만드는 생성 비용이 큰데 해당 인스턴스가 사용되지 않는 상황이라면 손해다.

4) double checked locking으로 효율적인 동기화 블럭

private static volatile Settings instance;

public static Settings getInstance() {
  if (instance == null) {
    synchronized (Settings.class) {
      if (instance == null) {
        instance = new Settings();
      }
    }
  }
  return instance;
}

따라서 다음과 같이 실제 인스턴스가 사용될 때 인스턴스를 생성하는 방법을 적용할 수 있다.

또한 if (instance == null) 이 true일 때만 동기화 작업을 하기 때문에 앞선 동기화로 인한 성능 이슈를 피할 수 있다.

그러나 다소 복잡한 이론적인 배경과 자바 1.5 이상에서만 동작한다는 단점이 있다. (근데 요즘은 다 1.5 이상을 쓰기 때문에...)

5) static inner 클래스

private Settings() {}

private static class SettingsHolder {
  private static final Settings SETTINGS = new Settings();
}

public static Settings getInstance() {
  return SettingsHolder.SETTINGS;
}

static inner 클래스를 사용하면 (SettingsHolder)

멀티 스레드 환경에서도 안전하고, 실제 인스턴스가 사용될 때 인스턴스를 생성할 수도 있다. 따라서 권장하는 방법 중 하나!

6) Enum 클래스

하지만 앞선 방법들을 활용하더라도 싱글톤 패턴은 깨져버릴 수 있다.

Settings settings = Settings.getInstance();

Constructor<Settings> declaredConstructor = Settings.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Settings settings1 = declaredConstructor.newInstance();

System.out.println(settings = settings1); // false

우선 자바 리플렉션을 활용한다면 private 생성자에 접근하여 인스턴스를 새로 생성할 수 있다. 

 

Settings settings = Settings.getInstance();
Settings settings1 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
  out.writeObject(settings);
}

try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
  settings1 = (Settings) in.readObject();
}

System.out.println(settings == settings1); // false

또한 직렬화를 구현한 클래스르 직렬화한 뒤, 다시 역직렬화하면 내부적으로 생성자를 활용하기 때문에 다른 인스턴스가 된다! (싱글톤이 깨진다) (다만 readResolve()를 구현하게 되면 싱글톤을 보장할 수 있기는 하다)

 

public enum Settings {
  INSTANCE;
}

 

따라서 Enum 클래스를 활용하면 쉽게 싱글톤을 보장할 수 있다.

Enum은 리플렉션을 사용하더라도 생성자를 통한 인스턴스를 생성할 수 없다!

 

하지만 클래스를 로딩하는 순간 인스턴스를 만든다는 단점이 있다. 따라서 생성 비용이 크지 않다면 적절하다!

또한 직렬화/역직렬화에도 안전하게 싱글톤을 보장할 수 있다.

또한 Enum 클래스는 상속을 사용할 수 없다는 단점이 있기도 하다.