CS

[JAVA] Singleton Design Pattern 개념 및 구현

teamzamong 2023. 11. 4. 21:16

Singleton Design Pattern이란 클래스가 단 하나만의 인스턴스를 생성하도록 제한하는 패턴이다. privatestatic 제어자를 활용해 클래스 내부에서 미리 인스턴스 하나를 생성해두고, 외부에서 접근할 때는 해당 인스턴스에 대한 참조값만 전달한다.

보통 여러 개의 인스턴스가 필요 없는 경우에 사용한다. 예를 들자면,

  • 수정 가능한 멤버변수가 없고 기능만 있는 stateless 객체의 경우
  • 인스턴스를 계속 생성 및 삭제하는데 비용이 많이 들어서 재사용이 유리한 경우

 

구현

자바에서 Singleton Design Pattern을 여러 가지 방법으로 구현할 수 있다.

 

공통 구현 내역

  1. 생성자를 private으로 선언해 내부에서만 인스턴스 생성할 수 있도록 제한
  2. private Singleton() { }
  3. 클래스 내부에 인스턴스를 저장할 참조변수를 private static으로 생성
  4. private static Singleton instance;
  5. 해당 인스턴스에 대한 참조값을 리턴해주는 public static getter 제공
  6. public static Singleton getInstance() { return instance; }

 

기본 구현

클래스가 메모리에 로드될 때, 인스턴스를 생성하는 방법이다.

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() { }

    public static Singleton getInstance() {
        return s;
    }
}

 

 

인스턴스를 사용할 때가 되지 않았는데, 클래스가 메모리에 로드될 때 이미 함께 인스턴스가 생성되어 메모리 공간을 차지하고 있는 문제가 있다. 

 

Lazy Loading with null

인스턴스가 실제로 사용될 때, 메모리에 생성되도록 Getter에서 null을 사용해 확인 후 생성하는 방식이다.

public class Singleton {
    private static Singleton instance;

    private Singleton() { }

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

 

 

사용할 때 인스턴스가 생성되는 장점이 있지만, thread unsafe하다는 단점이 있다. 멀티스레딩 환경에서 한 스레드가 인스턴스 생성을 완료하기 이전에 다른 스레드가 getInstance()에 접근한다면 인스턴스가 중복 생성되는 문제가 발생할 수 있다.

 

Lazy Loading with synchronized

Getter에 synchronized 제어자를 사용해 thread safe하게 인스턴스를 생성한다.

 

Synchronized Method

메서드 전체에 synchronized 선언하는 방식이다.

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

 

임계영역이 메서드 전체로 잡혀서 오버헤드 증가하는 문제가 있다

 

Synchronized Block (double-checked locking)

synchronized (this), synchronized (A.class)와 같은 형식으로 선언하는 방식으로, 해당하는 변수나 클래스에만 synchronized 선언한다.

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

 

 

메서드 전체보다는 해당하는 변수나 클래스에만 synchronized를 선언하는 것이 바람직하다. 하지만 아무리 synchronized block으로 영역을 제한한다고 해도, 싱글 스레드 상황에서는 synchronized를 쓰지 않는 것이 제일 효율적이다.

 

Lazy Loading with initialization-on-demand holder

추가적인 클래스를 사용해 thread safe하면서도 효율적인 방식으로 인스턴스를 생성하는 방식이다.

public class Singleton {
    private Singleton() { }

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

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

 

Singleton 클래스가 메모리에 로드될 때, 아무런 인스턴스도 생성되지 않는다. SingletongetInstance 함수가 불릴 때에야 JVM이 LazySingletonHolder 클래스를 메모리에 로드하고 초기화하며 인스턴스를 생성한다. 클래스를 메모리에 로드하고 초기화하는 과정은 순차적으로 발생하도록 보장되어 있기 때문에 추가적인 thread safety를 보장하기 위한 추가 조치가 필요하지 않다.

 

원래 inner class에서 top level class의 private 생성자나 멤버변수, 메서드에 접근하려면 클래스가 static이 아니어야 하지만, class LazySingletonHolder 의 경우, 멤버변수가 static 이라 괜찮다.

 

테스트

public class SingletonTest {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        System.out.println(s1 == s2); // true
    }
}

 

기타

 

Spring도 Singleton Design Pattern을 사용한다. 기본적으로 Spring 컨테이너가 객체를 생성할 때, 하나의 객체만 생성되도록 한다.

 

참고 자료