Today's special moments become memories of tomorrow.

ETC

싱글톤 패턴(Singleton Pattern)

lotus lee 2021. 5. 21. 20:31

싱글톤 패턴 : 인스턴스를 하나만 생성하고 싶을 때 사용하는 디자인 패턴

 

싱글톤 인스턴스, 생성자, 인스턴스를 반환하는 메서드 

이렇게 세가지가 필요하다.

 

싱글톤 인스턴스

private static Singleton instance;

인스턴스가 하나만 생성되어야 하므로 외부에서 함부로 접근할 수 없도록 private static으로 선언한다.

클래스가 로드될 때 한번만 생성된다.

 

싱글톤 생성자

private Singleton(){}

외부에서 생성자를 통해 인스턴스를 생성하지 못하도록 private으로 지정한다.

 

싱글톤 getter

public static Singleton getInstance(){ return instance;}

외부에서 인스턴스를 얻기 위한 유일한 통로

외부에서 접근할 수 있어야 하므로 public으로 선언하고, 싱글톤 객체를 생성하지 않고도 메서드에 접근하기 위해서 static 멤버로 설정한다.

 

 

싱글톤 구현 방법

싱글톤을 구현하는 방법은 보통 4가지가 있다. 각각 코드의 장단점을 알아보자.

 

1. Eager initialization (이른 초기화 방식)

class Singleton {

	private static Singleton instance = new Singleton();

	private Singleton() {

	}

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

instance 변수를 초기화할 때 싱글톤 객체를 생성하는 방법이다.

클래스가 로드될 때 인스턴스가 생성된다.

 

- 장점 : 멀티 스레드 환경에서도 안전하다. 

- 단점 : 인스턴스가 필요하지 않은 경우에도 항상 생성되므로 메모리를 차지하게 되어 비효율적이다.

 

 

 

2. Lazy initialization (늦은 초기화 방식)

class Singleton {

	private static Singleton instance;

	private Singleton() {

	}

	public static Singleton getInstance() {

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

		return instance;
	}
}

1번의 경우, 인스턴스의 사용유무에 관계없이 항상 생성된다는 것이 문제였다.

따라서 "늦은 초기화 방식"에서는 클래스가 로드될 때 인스턴스가 생성되는 것이 아니라 getInstance() 메서드를 호출할 때 인스턴스가 생성된다.

 

getInstance() 호출 시,

instance가 null이라면, instance에 생성자를 통해 객체를 생성하고 instance를 반환한다.

instance가 null이 아니라면, instance를 반환한다.

 

그런데 한 가지 문제가 있다. 만약 멀티 스레드 환경이라면 어떻게 될까?

예를 들어 다음과 같은 상황을 보자.

 

t1, t2 스레드가 동시에 작업을 진행중이다.

 

1. t1 스레드가 getInstance() 내에서 instance==null을 만족하여 조건문에 진입했다.

2. 그 순간, 인터럽트가 발생하여 t1 스레드가 작업을 중단하고, t2 스레드 차례가 된다.

3. t2 스레드도 조건문을 확인하는데, 역시 마찬가지로 instance==null이므로 조건문을 만족하여 진입한다.

4. instance = new Singleton(); 이 실행되어서 인스턴스가 생성되었다.

5. 이 때, 인터럽트가 발생하여 t2 스레드가 작업을 중단하고, t1 스레드 차례가 된다.

6. t1 스레드도 조건문에 진입한 상태였으므로 마찬가지로 instance = new Singleton();을 실행하여서

   인스턴스가 생성되었다.

 

위의 경우를 보면 결과적으로 인스턴스가 두 번 생성되기 때문에 싱글톤을 만족하지 못하게 된다.

 

즉, 늦은 초기화 방식의 경우 멀티 스레드 환경에서 싱글톤 패턴을 실현할 수 없다.

 

 

3. Thread safe Lazy initialization (스레드 안전한 늦은 초기화)

class Singleton {

	private static Singleton instance;

	private Singleton() {

	}

	public static synchronized Singleton getInstance() {

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

		return instance;
	}
}

자바에서 공유 자원에 동시에 접근하는 것을 막기 위해 synchronized 라는 키워드가 존재한다.

synchronized 블럭에서는 상호 배제가 보장되어서 여러 스레드가 동시에 자원에 접근하는 것을 막아준다.

 

위의 수정된 코드를 보면 getInstance() 메서드에 synchronized 키워드가 있기 때문에 한번에 하나의

스레드만 실행이 가능하므로 멀티 스레드에서의 문제를 해결할 수 있다.

 

그런데 synchronized 키워드를 통한 동기화는 자바 입장에서 부담이 큰 작업이다. 

매번 lock을 설정하고, unlock을 하는 등 처리해야 할 일이 많기 때문이다.

따라서 위 코드는 싱글톤 인스턴스를 사용하고자 getInstance()를 호출할 때마다 동기화 작업이 필요하기 때문에 비효율적이다.

 

- 장점 : 멀티 스레드 환경에서 안전하다.

- 단점 : getInstance()를 호출할 때마다 비효율적이다.

 

 

4. Lazy Initialization + Double-checked Locking

class Singleton {

	private static Singleton instance;

	private Singleton() {

	}

	public static Singleton getInstance() {

		if (instance == null) {

			synchronized (Singleton.class) {

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

		return instance;
	}
}

이번에는 synchronized를 조건문 바깥이 아닌 조건문 안쪽에 설정하였다.

그리고 synchronized 블럭 안에 다시 한번 instance가 null인지를 체크하는 조건문을 넣어준다.

즉, synchronized 블럭 바깥쪽에 한 번, 안쪽에 한 번 총 두 번 체크한다.

 

이렇게 하면 instance가 null일 때만 동기화 작업을 하기 때문에 "스레드 안전한 초기화" 방법보다 더 효율적이다. 또한, 멀티 스레드 환경에서 여러 스레드가 동시에 첫 번째 조건문 안에 들어아서 synchronized 블럭을 거친다고 해도, 내부에서 instance가 null인지를 한 번 더 확인하기 때문에 인스턴스가 여러 개 생성될 일이 없다.

 

ex) t1, t2 스레드가 동시에 작업을 진행중이다.

 

1. t1 스레드가 getInstance() 내에서 instance==null을 만족하여 조건문에 진입했다.

2. 그 순간, 인터럽트가 발생하여 t1 스레드가 작업을 중단하고, t2 스레드 차례가 된다.

3. t2 스레드도 조건문을 확인하는데, 역시 마찬가지로 instance==null이므로 조건문을 만족하여 진입한다.

4. sychronized 블럭에 진입하여 instance = new Singleton(); 이 실행되어서 인스턴스가 생성되고,

  sychronized 블럭을 빠져나온다.

5. 이 때, 인터럽트가 발생하여 t2 스레드가 작업을 중단하고, t1 스레드 차례가 된다.

6. t1 스레드도 조건문에 진입한 상태였으므로 마찬가지로 sychronized 블럭에 진입한다.

   하지만, 이미 인스턴스가 생성되었으므로 instance==null 조건을 만족하지 않아서 아무 처리도 하지 않고

   블럭을 빠져나온다.

 

 

4. Initialization on demand holder idiom (holder에 의한 초기화)

class Singleton {

	private Singleton() {

	}

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

	public static Singleton getInstance() {

		return Holder.instance;
	}
}

Singleton 클래스 내에 Holder 내부 클래스를 하나 더 만든다.

그리고 Holder 클래스의 멤버 변수로 instance를 만들어서 싱글톤 객체를 생성한다.

그러면 이 인스턴스는 Holder클래스가 로드되는 시점에 생성된다.

외부에서 getInstance()를 호출하면 Holder 클래스의 instance를 반환한다.

 

이렇게 하면,

1. getInstance()가 호출될 때(즉, 인스턴스가 필요한 상황에서) Holder 클래스가 로드되므로 1번의 문제를

  해결 가능하다.

2. 또한, 멀티 스레드 환경에서도 안정적이다.

3. 마지막으로 synchronized 키워드를 사용하지 않기 때문에 동기화로 인한 부담도 줄어든다.

 

여태까지의 문제를 다 해결하기 때문에 현재 가장 많이 사용되는 싱글톤 구현 방법이다.

 

 

'ETC' 카테고리의 다른 글

면접 자바(Java) 예상 질문 리스트  (1) 2022.12.04
기업 인성면접 대비 예상 질문 리스트  (1) 2022.12.04
PNG 와 JPEG(JPG) 차이  (0) 2021.04.13