싱글톤 (Singleton) 패턴

  • 인스턴스를 오직 한개만 생성 하도록 하며, 생성된 인스턴스를 어디에서든지 참조할 수 있도록 제공하는 방법이다.
  • ‘생성 (Creational) 패턴’의 하나이다.
  • 하나의 인스턴스만을 생성하는 책임이 있으며, getInstance 메서드를 통해 모든 클라이언트에게 동일한 인스턴스를 반환할 수 있도록 해야한다.

예시

  • 기존 객체 생성 시)
  • 아래와 같이 계속해서 new를 통해 객체를 생성할 수 있다. 그러나 아래 각 인스턴스는 동일한 인스턴스가 아니다.
  • new Singleton이 반드시 한 번만 호출 되도록 해야한다.
Singleton instance1 = new Singleton();
Singleton instance2 = new Singleton();
  • 싱글톤 패턴 구현 시)
  • private 생성자에 static 메소드로 구현
public class Main {
    public static void main(String[] args) {
      Singleton instance = Singleton.getInstance();
      System.out.println("check : " + instance == Singleton.getInstance());
    }
}
public class Singleton {
    // 외부에 제공할 인스턴스
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        // 인스턴스가 null인 경우에만 만들기 때문에, 같은 인스턴스를 넘겨주게된다.
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

문제점

  • 다중 스레드에서 위 Singleton Class를 이용할 때, 인스턴스가 1개 이상 생성되는 경우가 발생할 수 있다. 즉, Thread-safe하지 않다.
  • 경합 조건(Race Condition)을 발생시키는 예시
    • A 스레드, B 스레드가 있다고 할 때, A 스레드가 if문에 들어와 null == instance임을 확인하고 new Singleton에 진입하는 순간
    • B 스레드가(A 스레드에서 아직 인스턴스를 만들기 전에) if문 안으로 들어와 new Singleton을 실행한다.
    • 이럴경우, 1개 이상의 인스턴스가 만들어질 수 있다.
public class Main {
    public static void main(String[] args) {
        SingletonThread[] singletonThreads = new SingletonThread[5];
        for (int i = 0; i < 5; i++) {
            singletonThreads[i] = new SingletonThread((i+1));
            singletonThreads[i].start();
        }
    }
}
public class Singleton {

    // 외부에 제공할 인스턴스
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (null == instance) {
            try {
                // 스레드 변경(스레드 실행 1ms동안 정지)
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            instance = new Singleton();
        }
        return instance;
    }
}
public class SingletonThread extends Thread {
    public SingletonThread (int threads) {
        super(String.valueOf(threads));
    }

    public void run() {
        Singleton singleton = Singleton.getInstance();
        System.out.println(Thread.currentThread().getName() + " : " + singleton.toString());
    }
}

해결방법 1

  • 인스턴스를 만드는 메서드에 동기화하는 방법 (Thread-Safe Initialization)
  • synchronized키워드를 붙여 하나에 인스턴스만 보장할 수 있도록한다.
  • 그러나 완전한 해결 방법은 될 수 없는 이유는 synchronized(동기화)라는 것 자체가 Lock을 사용하기 때문에 getInstance를 사용할 때마다 부하가 생길 수 있다.
public class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        // 인스턴스가 null인 경우에만 만들기 때문에, 같은 인스턴스를 넘겨주게된다.
        if (null == instance) {
            instance = new Singleton();
        }
        return instance;
    }
}

해결방법 2

  • 정적 변수에 인스턴스를 만들어 바로 초기화하는 방법 (Eager Initialization)
  • 만약에 쓰지 않는다면, 안쓰는 객체를 미리 만들어 놓아 리소스 낭비가 될 수 있다.
	// static 변수에 외부에 제공할 자기 자신의 인스턴스를 만들어 초기화
  // 로딩되는 시점에 미리 만들어 놓음
  private static final Singleton INSTANCE = new Singleton();

  private Singleton() {}

  public static Singleton getInstance() {
      return INSTANCE;
  }

해결방법 3

  • Double Checked Locking 방법
  • getInstance 메소드를 호출할 때마다 매번 synchronized가 걸리는 것이 아니다.
  • 이미 인스턴스가 있는 경우엔 동기화를 사용하지 않게된다. 따라서 성능에 유리하게 된다.
private static volatile Singleton instance;

private Singleton() {}

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

해결방법 4

  • Enum을 사용한다.
  • 위 해결방법 1,2,3의 경우 직렬화 역직렬화, Reflection을 통한 공격으로 싱글톤 패턴을 뚫을 수 있다.
  • Enum을 사용하여 아래와 같이 생성하여 사용한다면, Reflection을 통한 공격으로부터 안전하다.
public enum Singleton {
    INSTANCE;
}

참조