본문 바로가기
백엔드

[디자인 패턴] 싱글턴 패턴

by hseong 2023. 6. 11.

1. 싱글턴 패턴이란

싱글턴 패턴은 클래스 인스턴스를 하나만 만들고, 해당 인스턴스로의 전역 접근 방법을 제공합니다.

  • 클래스에서 하나뿐인 인스턴스를 관리하도록 만듭니다. 다른 클래스에서는 해당 클래스의 인스턴스를 생성하지 못하도록 생성자는 private으로 설정합니다.
  • 싱글턴 패턴을 적용한 클래스는 어디서든 해당 인스턴스에 접근할 수 있도록 전역 접근 방법을 제공합니다. 인스턴스가 필요할 때 해당 클래스에 요청을 하면 하나뿐인 인스턴스를 건네주도록 만듭니다.
  • 싱글턴은 게으른 방식(lazy instantiation)으로 구현할 수 있습니다. 실제로 해당 인스턴스가 필요한 시점에 생성하도록 하여 불필요한 리소스의 낭비를 줄일 수 있습니다.

 

2. 싱글턴 패턴을 구현하는 방법

고전적인 방법

아래는 고전적인 싱글턴 패턴을 구현한 코드입니다. 어떤 클래스에서든지 Singleton.getInstance()를 호출하여 해당 클래스의 유일한 인스턴스에 접근이 가능합니다.

public class Singleton {
  private static nuniqueInstance;    

  private Singleton() {}  

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

실제로 Sigleton.getInstance() 메서드를 호출해보면, 해당 메서드를 통해 얻은 객체의 주소값이 서로 동일함을 확인할 수 있습니다.

public class SingleMain {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.getUniqueInstance();
        Singleton instance2 = Singleton.getUniqueInstance();

        System.out.println(instance1);
        System.out.println(instance2);
    }
}

[문제점]

그러나 위 코드에는 멀티 쓰레드 환경에서 문제가 발생할 수 있습니다. 한 번 2개의 쓰레드가 Sigleton.getInstance()를 호출하도록 해보겠습니다.

public class MultiMain {
    public static void main(String[] args) {
        Runnable r = () -> {
            Singleton instance = Singleton.getInstance();
            System.out.println(instance);
        };

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);

        t1.start();
        t2.start();
    }
}

main() 메서드를 3번 정도 실행시켰을 때, 위 와 같이 각 쓰레드가 서로 다른 Singleton 인스턴스의 주소값을 출력한 것을 확인할 수 있습니다. 아마 다음과 같은 상황이 벌어져 2개의 인스턴스가 생성되었을 겁니다.

  1. Thread0if(uniqueInstance == null)까지 수행한 후 Thread1로 컨텍스트 스위칭
  2. Thread1uniqueInstance = new Singleton()까지 수행하여 Singleton 인스턴스가 생성, 이후 Thread0로 컨텍스트 스위칭
  3. Thread0uniqueInstance = new Singleton()을 수행하여 새로운 Singleton 인스턴스가 생성

이처럼 고전적인 구현 방식으로는 멀티 쓰레드 환경에서 우리가 의도하지 않은 방식으로 로직이 작동하게 됩니다.

 

Synchronized

멀티쓰레드 환경에서 사용한다면 synchonized 키워드를 통해 동기화 처리를 해줄 수 있습니다.

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

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

이제 Singleton.getInstance() 메서드를 호출하면 언제나 동일한 인스턴스를 반환할 것입니다. 하나의 Thread가 해당 메서드를 실행 중이라면 다른 Thread가 이에 접근하는 것을 막아 동시성 문제를 해결할 수 있습니다.

[문제점]

synchrozied 키워드는 많은 비용이 듭니다. 우리가 동기화 되길 원하는 부분은 가장 처음 Singleton 인스턴스가 존재하는지 검사하여 Singleton 인스턴스를 생성하는 순간 뿐입니다.

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

바로 위의 코드 부분만 동기화 되었으면 합니다. getInstance() 메서드를 호출하면 언제나 동기화 처리되어 불필요한 성능 저하가 발생할 것입니다.

 

Eagear Initialization

만일 애플리케이션에서 Singleton 인스턴스를 생성하여 계속 사용하거나 동기화 처리가 곤란하다면 클래스가 로드되는 시점에 Singleton 인스턴스를 만들수도 있습니다.

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

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        return uniqueInstance;
    }
}

정적 초기화 부분에서 Singleton 인스턴스를 만들어주고, getInstance() 메서드는 그냥 해당 인스턴스를 반환해주기만 하면 됩니다. JVM에서 인스턴스를 생성하기 전까지 어떤 쓰레드도 uniqueInstance 변수에 접근할 수 없습니다.

[문제점]

만일 Singleton 인스턴스를 만드는데 시간이 오래 걸리고 메모리도 많이 사용한다면, 애플리케이션이 이를 사용하지 않을 경우 불필요한 리소스의 낭비가 발생합니다.

 

Double-Checked Locking

Double-Checked Locking(DCL)을 사용하면 인스턴스가 생성되어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화 처리할 수 있습니다. 이러면 인스턴스가 존재하지 않는 처음에만 동기화하고 이후에는 동기화가 이루어지지 않습니다.

public class Singleton {
    private static volatile Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {  // (1)
            synchronized (Singleton.class) {  // (2)
                if (uniqueInstance == null) {  // (3)
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
  • (1) `Thread0`가 인스턴가 있는지 확인하고, 없으면 동기화 블록으로 들어갑니다.
  • (2) `Thread0`가 동기화 블록으로 진입하면, `Thread1`은 먼저 진입한 `Thread0`가 동기화 블록을 빠져나오기 전까지 대기합니다.
  • (3) `Thread0`는 블록에서 다시 한 번 변수가 null 인지 확인한 다음 인스턴스를 생성합니다. `Thread1`은 인스턴스가 생성된 것을 확인하고 곧바로 인스턴스를 반환합니다.

[문제점]

이 방법의 경우 Java 1.5 버전부터 사용가능합니다.

또한 volatile 키워드와 Java 1.4 버전 이하의 멀티 쓰레드 환경에서의 메모리를 다루는 방법에 대한 이해가 필요하다고 합니다. 해당 부분에 대한 간단한 설명은 DCL 위키페이지에서 확인할 수 있습니다.

 

Double-checked locking - Wikipedia

From Wikipedia, the free encyclopedia Software design pattern In software engineering, double-checked locking (also known as "double-checked locking optimization"[1]) is a software design pattern used to reduce the overhead of acquiring a lock by testing t

en.wikipedia.org

 

Initialization-on-demand holder idiom

싱글턴 패턴을 코드로 구현하는 데 있어서 가장 추천되는 방법 중 하나입니다. 정적 내부 클래스를 통해서 인스턴스를 획득하는 방법입니다.

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

    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  • getInstance()를 호출할 때, Singleton 인스턴스가 생성되고 JVM 클래스 로더의 동기화 로직에 의해 오직 하나의 인스턴스가 생성되는 것을 보장할 수 있습니다.

[문제점]

추천되는 방법이지만 리플렉션을 이용한 클라이언트 코드에서의 접근, 직렬화, 역직렬화에 의한 문제는 남아있습니다. 

 

이상으로 디자인 패턴 중 생성 패턴에 해당하는 싱글턴 패턴을 구현하는 방법에 대해서 알아보았습니다.

 

[참고]

헤드퍼스트 디자인패턴

[GoF 디자인 패턴] 싱글톤 패턴 2부, 멀티쓰레드 환경에서 안전하게 구현하는 방법.

https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java

'백엔드' 카테고리의 다른 글

Parameter와 Argument  (0) 2023.06.20
[디자인 패턴] 옵저버 패턴  (0) 2023.06.19
단위 테스트를 위한 Mockito 사용법  (0) 2023.06.06
Gradle에 대한 짧은 정리  (1) 2023.06.04
[nGrinder] 부하 테스트 해보기  (0) 2023.05.22