📌 디자인패턴

싱글턴 패턴 (SingleTon Pattern)

j_estory 2022. 10. 12. 09:44
  • 싱글턴 패턴 이란?
    • 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴
    • 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만든다. 
    • 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 한다.
  • 책에서 나타나는 고전적 싱글턴 패턴의 구현법과 문제점에 대해 알아보자!

 

public class Singleton {

	private static Singleton uniqueInstance;

	private Singleton() {}

	public static Singleton getInstance() {

		if (uniqueInstance == null) {

			uniqueInstance = new Singleton();

		}

		return uniqueInstance;

	}

}
  • 싱글턴 패턴을 구현하기 위해서는 일반적으로 private 생성자를 가진 public 클래스를 이용한다. 
  • 싱글턴 패턴은 외부에서 인스턴스를 생성하는 것이 아닌 인스턴스를 요청하도록 설계해야 하며 이때 사용하는 메소드가 getInstance() 이다. 그리고 해당 메소드는 정적 메소드여야 한다.
    • 정적 메소드는 외부에서 클래스의 인스턴스를 생성하지 않고도 호출할 수 있기 때문이다. 

❌  하지만 위와 같은 고전적인 싱글턴은 문제가 있다.

  • 여러 스레드가 위의 싱글턴 인스턴스를 사용하려고 한다고 가정했을 때, JVM은 작업을 공평하게 수행시키기 위해 여러 쓰레드를 왔다갔다 하며 실행시킨다. 
  • 만약 1번 쓰레드에서 8번 줄의 if 조건문 수행이 끝난 뒤, 2번 쓰레드에서 8번 줄의 if 조건문을 수행한다면 두개의 쓰레드에서 모두 싱글턴 인스턴스가 생성되게 되며 싱글턴 패턴에 부합되지 않는다. 

✅ 해결 방법

  • 근본적인 해결방법으로는 두개의 쓰레드가 동시에 같은 코드에 접근하지 못하도록 하는 것이다.  -> 동기화 라고 한다.

1️⃣  Synchronized 사용

 

public class Singleton {
	private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
    	if (uniqueInstance == null) {
        	uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
  • 이렇게 해주면 두개 이상의 쓰레드가 동시에 getInstance()를 사용할 수 없다. 
  • 하나가 종료되어야 다음이 시작된다. 
  • 🚫 단점
    • 속도 문제 발생 
      • 동기화가 필요한 이유는 인스턴스가 존재하지 않을 때, 여러개의 인스턴스 생성의 예방이다.
      • 하지만 인스턴스 하나를 생성한 뒤에도 getInstance() 메소드를 호출하게 되면 불필요한 동기화로 인해 오버헤드가 증가된다. 
      • 속도의 크게 상관이 없다면 사용해도 좋지만 성능은 약 100배 정도 차이가 난다고 한다.

2️⃣  처음부터 인스턴스 생성

 

public class Singleton {
	private static Singleton uniqueInstance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return uniqueInstance;
    }
}
  • 위의 방법은 인스턴스를 필요할 때 생성하지 않고 클래스를 로딩할 때 JVM에서 그 즉시 생성하는 방법이다. 
  • JVM에서 유일한 인스턴스를 생성하기 전까지는 그 어떤 쓰레드도 uniqueInstance 정적 변수에 접근할 수 없다. 
  • 🚫 단점
    • 이 방법을 사용하게 된다면, getInstance()를 호출하지 않아도 인스턴스를 유지하고 있다는 단점이 있다. 
      • 만약 해당 인스턴스를 처음부터 사용한다면 위의 방법(Synchronized)과 크게 다르지 않지만. 처음부터 사용하지 않고 호출 시에만 사용한다면, 불필요하게 메모리를 사용할 수 있다는 것이다.

3️⃣  DCL(Double-Checking-Loncking) 으로 동기화 부분 처리

 

public class Singleton {
	private volatile static Singleton uniqueInstance; 
    
    private Singleton() {}
    
    public static Singleton getInstance() {
    	if (uniqueInstance == null) {
        	synchronized (Singleton.class) {
            	if (uniqueInstance == null) {
                	uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
  • 처음부터 동기화를 진행하는 것이 아닌, 인스턴스의 생성 여부 먼저 확인하고, 동기화 블럭에 들어가게 된다. 
  • 블록에 들어온 후에도 다시 한번 인스턴스가 있는지 확인하고 생성한다. 
  • volatile 키워드
    • 변수를 cpu cache가 아닌 메인 메모리에 저장하겠다 라는 것을 명시하는 키워드이다.
    • 변수의 값을 write 할 때마다 메인 메모리까지 써서, 쓰레드가 변수의 값을 읽어올 때 각각의 cpu cache에 저장된 값이 아닌 메인 메모리에서 가져오기 때문에 변수 값 불일치 문제를 없앨 수 있다. 

4️⃣ 내부 정적 클래스 사용

 

public class Singleton {
	private Singleton() {}
    
    private static class InnerSingleton {
    	private static final Singleton UNIQUE_INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
    	return InnerSingleton.UNIQUE_INSTANCE;
    }
}
  • Singleton 클래스를 초기화 할 수 있는 방법은 getInstance()의 호출 뿐이다.
    • 생성자가 private로 선언되어 있음 
    • 최초 인스턴스를 생성하기 위해서는 getInstance() 를 호출해야 한다. 
    • 해당 메서드를 호출하게 되면 inner 클래스가 초기화 되면서 싱글턴 클래스의 인스턴스가 생성된다.
      • jvm은 inner 클래스의 초기화 시점에 대해 동기화를 보장해주므로, 멀티쓰레드 환경에서 안전할 수 있다. 
      • 또한, 메모리 낭비 없이 클래스의 인스턴스를 사용하는 시점에 생성이 되므로 메모리 낭비에도 안전하다. 

✔️  싱글턴 패턴에 대해 공부하면서 느낀점 및 더 알아봐야 할 사항

  • 클래스의 로딩 시점과 초기화 시점에 대해 알아보기
  • JVM의 작동 원리 알아보기

'📌 디자인패턴' 카테고리의 다른 글

팩토리 패턴 (Factory Pattern)  (0) 2022.08.26