- @참고 : https://refactoring.guru/design-patterns/singleton

 

Singleton

Real-World Analogy The government is an excellent example of the Singleton pattern. A country can have only one official government. Regardless of the personal identities of the individuals who form governments, the title, “The Government of X”, is a globa

refactoring.guru

🙌 취지

싱글톤은 클래스가 하나의 인스턴스만 갖도록 보장하며 이 인스턴스에 대한 글로벌 액세스 포인트를 제공할 수 있는 생성 디자인 패턴입니다.

🤔 문제점

싱글톤 패턴은 단일 책임 원칙을 위반하여 동시에 두가지 문제를 해결합니다.

1. 클래스가 한 인스턴스만 가지는지 확인하십시오. 왜 클래스의 인스턴스 수를 제어하고 싶어할까요? 가장 대표적인 이유는 데이터베이스나 파일과 같은 공유자원에 대한 접근 제어를 위해서입니다.

작동 방식은 다음과 같습니다. 객체를 생성하고, 잠시 후 새로이 생성하려한다고 해봅시다. 새로운 객체를 받는 대신 이미 생성된 객체를 받을 것입니다.

이 동작은 생성자 호출이 디자인에 의해 항상 새로운 객체를 반환해야하기 때문에 일반 생성자로 구현하는게 불가능합니다.

쿨라이언트는 항상 같은 객체로 작업하고 있음을 인식하지 못할 수도 있습니다.

2. 해당 인스턴스에 전역적 접근 포인트를 제공하십시오. 필수 객체를 저장하는데 사용한 전역 변수를 기억합니까? 다루기 매우 편리하지만, 코드가 그 변수의 내용을 덮어 쓰고 앱을 충돌시킬 수 있어서 안전하지 않습니다.

전역 변수와 마찬가지로, 싱글톤 패턴은 프로그램 어디에서든 일부 객체에 접근할 수 있게 합니다. 그러면서 다른 코드로 해당 인스턴스를 덮어 쓰지 않도록 보호합니다.

이 문제의 또 다른 측면이 있습니다. 당신은 #1 문제를 해결하는 코드가 프로그램 전체에 흩어져 있는 것을 원하지 않습니다. 특히 나머지 코드가 이미 의존적일 때 그 코드를 한 클래스 안에 갖는게 더 낫습니다.

요즘, 싱글톤 패턴이 매우 유명해져 나열된 문제 중 하나만 해결하더라도 싱글톤 패턴이라 부를지 모릅니다.

😊 해결책

싱글톤의 모든 구현에는 다음 두 단계가 공통적으로 있습니다.

  • 다른 객체가 싱글톤 클래스에 대해 new 연산자를 사용하지 못하도록 기본 생성자를 private으로 만듭니다.
  • 생성자 역할을 하는 정적 생성 메소드를 만듭니다. 후드에서 이 메소드는 private 생성자를 호출하여 객체를 생성하고 정적 필드에 저장합니다. 이 메소드에 대한 다음의 모든 호출은 캐시 된 객체를 반환합니다.

코드가 싱글톤 클래스에 대해 접근할 수 있으면, 싱글톤의 정적 메소드를 호출 할 수 있습니다. 따라서 해당 메소드가 호출될 때마다 동일한 객체가 항상 반환됩니다.

🚗 실세계에 비유

정부는 싱글톤 패턴의 훌륭한 예입니다. 국가는 하나의 공식 정부만을 가질 수 있습니다. 정부를 구성하는 개개인의 개인 신원에 관계없이, 타이틀 "X 정부"는 담당자 그룹을 식별하는 전역적 접근 지점입니다.

🔦구조

1. 싱글톤 클래스는 자체 클래스와 동일한 인스턴스를 반환하는 getInstance 정적 메소드를 선언합니다.

싱글톤의 생성자는 클라이언트 코드에서 감춰져야 합니다. getInstance 메소드를 호출하는 것만이 싱글톤 객체를 얻는 유일한 방법이어야 합니다.

 

 

 

 

# 짜가코드

이 예제에서, 데이터베이스 연결 클래스는 싱글톤 역할을 합니다. 이 클래스는 public 생성자가 없으므로 객체를 얻는 유일한 방법은 getInstance 메소드를 호출하는 것뿐입니다. 이 메소드는 처음 생성된 객체를 캐시하고 모든 후속 호출에서 이를 반환합니다.

//데이터베이스 클래스는 클라이언트가 프로그램 전체에서 동일한 데이터베이스 연결 인스턴스에 접근하게 하는 'getInstance' 메소드를 정의합니다.
class Database is 
	//싱글톤 인스턴스를 저장하기 위한 필드는 정적으로 선언되어야 합니다.
    private static field instance: Database
    
    //싱글톤의 생성자는 'new' 연산자로 직접 생성 호출을 방지하기 위해 항상 프라이빗이어야 합니다.
    private constructor Database() is
    	//데이터베이스 서버로의 실제 연결과 같은 어떤 시작 코드...
        
	//싱그론 인스턴스에 접근을 제어하는 정적 메소드
    public static method getInstance() is
    	if (Database.instance == null) then
        	acquiredThreadLock() and then
            	//이 스레드가 잠금 해제를 기다리는 동안 다른 스레드가 인스턴스를 초기화되지 않았음을 확인하십시오.
                if (Database.instance == null) then
                	Database.instance = new Database()
		return Database.instance
        
	//마지막으로, 싱글톤은 인스턴스에서 실행될 수 있는 비즈니스 로직을 정의해야 한다.
    public method query(sql) is
    	//예를 들어, 앱의 모든 데이터베이스 쿼리는 이 메소드를 거칩니다. 그러므로, 여기에 제한 또는 캐싱 로직을 배치할 수 있습니다.
        //...
        
class Application is
	method main() is
    	Database foo = Database.getInstance()
        foo.query("SELECT ...")
        //...
        Database bar = Database.getInstance()
        bar.query("SELECT ...")
        //변수 'bar'는 변수 'foo'와 동일한 객체를 포함합니다.
    	

💡 적용

  • 프로그램의 클래스가 모든 클라이언트가 사용할 수 있는 단일 인스턴스가 있어야 하는 경우 싱글톤 패턴을 사용하십시오. 예를 들면, 프로그램의 다른 부분에서 공유되는 단일 데이터베이스 객체가 있습니다.
    • 싱글톤 패턴은 특수 생성 메소드를 제외하고 클래스 객체를 생성하는 모든 방법을 비활성화합니다. 이 메소드는 새로운 객체를 생성하거나, 이미 생성된 경우 기존 객체를 반환합니다.
  • 전역 변수에 대한 엄격한 제어가 필요할 때 싱글톤 패턴을 사용하십시오.
    • 전역 변수와 달리, 싱글톤 패턴은 클래스의 단 하나의 인스턴스가 있음을 보장합니다. 싱글톤 클래스 외의 어떤 것도 캐쉬된 인스턴스를 대체할 수 없습니다.
  • 언제든지 이 제한을 조정할 수 있고 여러 개의 싱글톤 인스턴스를 만들 수 있습니다. 변경이 필요한 유일한 코드는 getInstance 메소드의 본문입니다.

📋 구현 방법

  1. 싱글톤 인스턴스를 저장하기 위해 클래스에 프라이빗 정적 필드를 추가하십시오.
  2. 싱글톤 인스턴스를 얻기 위한 퍼블릭 정적 생성 메소드를 선언합니다.
  3. 정적 메소드 내부에 "지연 초기화"를 구현하십시오. 첫번째 호출에 새 객체를 만들어 정적 필드에 담아야 합니다. 메소드는 항상 모든 후속 호출에서 해당 인스턴스를 반환해야 합니다.
  4. 클래스의 생성자를 비공개로 만듭니다. 클래스의 정적 메소드는 여전히 생성자 호출이 가능하지만 다른 객체는 호출 할 수 없습니다.
  5. 클라이언트 코드를 살펴보고 싱글톤 생성자에 대한 모든 직접 호출을 정적 생성 메소드 호출로 대체하십시오.

⚖ 장단점

✔ 클래스가 단일 인스턴스만 가짐을 보장할 수 있습니다.

✔ 해당 인스턴스에 대한 전역적 접근 포인트를 얻습니다.

✔ 싱글톤 객체는 처음 요청된 경우에만 초기화됩니다.

단일 책임 원칙을 위반합니다. 이 패턴은 두가지 문제를 같이 해결합니다.

❌ 싱글톤 패턴은 프로그램의 구성요소가 서로에 대해 너무 많은 정보를 알 때 잘못된 디자인을 숨길 수 있습니다.

❌ 이 패턴은 다중 스레드가 싱글톤 객체를 여러번 만들지 않게 하기 위해 다중 스레드 환경에서 특별한 처리가 필요합니다.

❌ 많은 테스트 프레임워크가 목 객체를 생성할 때 상속에 의존하기 때문에 싱글톤의 클라이언트 코드를 단위 테스트하기 어려울 수 있습니다. 싱글톤 클래스의 생성자가 프라이빗하고 정적 메소드를 재정의하는 것이 대부분의 언어에서 불가능하므로, 싱글톤을 목(mock)으로 하는 창의적인 방법을 고려해야합니다. 아니면 그냥 테스트를 쓰지 마십시오. 또는 싱글톤 패턴을 사용하지 마십시오.

🐱‍🏍 다른 패턴과의 관계

  • Facade 클래스는 대부분의 경우 싱글톤 facade 객체로 충분하기 때문에 종종 싱글톤으로 변환될 수 있습니다.
  • 어떤 방식으로든 객체의 모든 공유 상태를 하나의 플라이급 객체로 줄이면 Flyweight는 싱글톤과 비슷합니다. 그러나 두 패턴에는 두가지 근본적인 차이점이 있습니다.
    1. Flyweight 클래스는 고유한 상태가 다른 여러 인스턴스를 가질 수 있는 반면, 싱글톤 인스턴스는 하나만 있어야 합니다.
    2. 싱글톤 객체는 변경 가능합니다. 플라이급 객체는 변경할 수 없습니다.
  • 추상 팩토리, 빌더, 프로토타입은 모두 싱글톤으로 구현할 수 있습니다.

</> 코드 예제

나이브 싱글톤 (단일 스레드)

조잡한 싱글톤을 구현하는 것은 꽤 쉽습니다. 생성자를 숨기고 정적 생성 메소드를 구현하면 됩니다.

더보기

📄 Singleton.java: Singleton

package com.test.singleton.example.non_thread_safe;

public final class Singleton{
	private static Singleton instance;
	public String value;
	
	private Singleton(String value){
		//다음 코드는 느린 초기화를 모방합니다.
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.value = value;
	}
	
	public static Singleton getInstance(String value){
		if (instance == null) {
			instance = new Singleton(value);
		}
		
		return instance;
	}
}

📄 DemoSingleThread.java: Client code

package com.test.singleton.example.non_thread_safe;

public class DemoSingleThread {
	public static void main(String[] args) {
		System.out.println("같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)" + "\n" +
				"다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)" + "\n\n" +
				"결과:" + "\n");
		Singleton singleton = Singleton.getInstance("FOO");
		Singleton anotherSingleton = Singleton.getInstance("BAR");
		System.out.println(singleton.value);
		System.out.println(anotherSingleton.value);
	}
}

📄 OutputDemeSingleThread.txt: Execution results

같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)
다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)

결과:

FOO
FOO

나이브 싱글톤 (멀티 스레드)

멀티스레드 환경에서 동일한 클래스가 잘못 작동합니다. 멀티 스레드는 생성 메소드를 동시에 호출하고 싱글톤 클래스의 여러 인스턴스를 가져올 수 있습니다.

더보기

📄 Singleton.java: Singleton

package com.test.singleton.example.non_thread_safe;

public final class Singleton {
	private static Singleton instance;
	public String value;
	
	private Singleton(String value) {
		//다음 코드는 느린 초기화를 모방합니다.
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		this.value = value;
	}
	
	public static Singleton getInstance(String value) {
		if (instance == null) {
			instance = new Singleton(value);
		}
		
		return instance;
	}
}

📄 DemoMultiThread.java: Client code

package com.test.singleton.example.non_thread_safe;

public class DemoMultiThread {
	public static void main(String[] args) {
		System.out.println("같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)" + "\n" +
				"다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)" + "\n\n" +
				"결과:" + "\n");
		Thread threadFoo = new Thread(new ThreadFoo());
		Thread threadBar = new Thread(new ThreadBar());
		threadFoo.start();
		threadBar.start();
	}
	
	static class ThreadFoo implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("FOO");
			System.out.println(singleton.value);
		}
	}
	
	static class ThreadBar implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("BAR");
			System.out.println(singleton.value);
		}
	}
}

📄 OutputDemoMultiThread.txt: Execution results

같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)
다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)

결과:

FOO
BAR

게으른 로딩에 대한 스레드 안전 싱글톤

이 문제를 해결하려면, 싱글톤 객체를 처음 만드는 동안 스레드를 동기화해야 합니다.

더보기

📄 Singleton.java: Singleton

package com.test.singleton.example.non_thread_safe;

public final class Singleton {
	//필드는 이중 점검 잠금이 올바르게 작동하도록 휘발성으로 선언되어야 합니다.
	private static volatile Singleton instance;
	public String value;
	
	private Singleton(String value) {
		this.value = value;
	}
	
	public static Singleton getInstance(String value) {
		//여기서 취한 접근 방식을 이중 점검 잠금(double-checked locking, DCL)이라고 합니다.싱글톤 인스턴스를 동시에 얻으려 시도하는 멀티 스레드 간 경쟁 상태를 방지하여 결과적으로 별도의 인스턴스를 만듭니다.
		//
		//여기에 'result' 변수를 갖는게 완전히 무의미해 보일 수 있습니다. 그러나 자바에서 이중 점검 잠금을 구현할 때 이 로컬 변수를 도입해 해결해야하는 매우 중요한 경고가 있습니다.
		//
		//DCL 이슈에 대해 여기서 더 읽을 수 있습니다.
		//https://en.wikipedia.org/wiki/Double-checked_locking#Usage_in_Java
		Singleton result = instance;
		if (result != null) {
			return result;
		}
		
		synchronized(Singleton.class) {
			if (instance == null) {
				instance = new Singleton(value);
			}
			return instance;
		}
	}
}

📄 DemoMultiThread.java: Client code

package com.test.singleton.example.non_thread_safe;

public class DemoMultiThread {
	public static void main(String[] args) {
		System.out.println("같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)" + "\n" +
				"다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)" + "\n\n" +
				"결과:" + "\n");
		Thread threadFoo = new Thread(new ThreadFoo());
		Thread threadBar = new Thread(new ThreadBar());
		threadFoo.start();
		threadBar.start();
	}
	
	static class ThreadFoo implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("FOO");
			System.out.println(singleton.value);
		}
	}
	
	static class ThreadBar implements Runnable {

		@Override
		public void run() {
			Singleton singleton = Singleton.getInstance("BAR");
			System.out.println(singleton.value);
		}
	}
}

📄 OutputDemoMultiThread.txt: Execution results

같은 값이 보이면, 싱글톤이 재사용된 것입니다 (예!!)
다른 값이 표시되면, 2개의 싱글톤이 생성된 것입니다 (우우;;)

결과:

BAR
BAR
블로그 이미지

uchacha

개발자 일지

,