싱글톤 패턴이란?
- 단 하나의 인스턴스를 생성해 사용하는 디자인 패턴이다.
- 인스턴스가 1개만 존재해야 한다는 것을 보장하고 싶은 경우와
- 동일한 인스턴스를 자주 생성해야 하는 경우에 주로 사용한다.
왜 싱글톤 패턴을 사용하는가?
- 인스턴스를 하나만 사용한다면 메모리 낭비를 방지할 수 있다.
- 다른 클래스 간의 데이터 공유가 쉽다.
└ 싱글톤으로 생성된 객체는 전역성을 띄기 때문에 다른 객체와 공유가 쉽다.
싱글톤 패턴의 문제점
- 싱글톤 패턴 구현을 위한 코드가 많이 필요하다.
- 테스트하기 어렵다.
└ 자원을 공유하기 때문에 테스트시, 매번 인스턴스의 상태를 초기화 해줘야한다.
- 싱글톤 객체간의 결합도가 높아져 객체 지향 설계 원칙 중 개방-폐쇄의 원칙에 어긋나게 된다.
싱글톤 패턴 구현 방식
Lazy Initialization (늦은 초기화)
- static 한 자기자신의 클래스를 필드로 만들고, 인스턴스가 필요하여 요청할 때 생성되는 형태
- 멀티 쓰레드 환경에 취약하다.
💡
문제점
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 쓰레드 A가 getInstance() 메소드의 if문을 지날 때 instance가 null이면 Singleton 인스턴스를 생성한다.
- 이때, 갑자기 쓰레드 B로 제어권이 넘어가면 쓰레드 B역시 if문이 수행되고 쓰레드 B또한 Singleton 인스턴스를 생성하게 된다.
⇒ 결국엔 인스턴스가 두번 생성되는 문제점 o
Synchronized (늦은 초기화 해결 방안)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
Synchronized : 쓰레드가 자원을 사용한다면 다른 쓰레드가 해당 자원을 사용하지 못하도록 lock을 거는 방법
- 멀티쓰레드의 동시 접근 문제가 해결된다.
- 하지만, 매번 리턴 받을 때마다 쓰레드를 동기화함으로써 성능저하가 된다.
Eager Initialization (이른 초기화)
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
- 멀티 쓰레드 환경에서 유발되는 모든 문제를 해결한다.
- 쓰레드가 getInstance를 호출하는 시점이 아닌, Class가 로딩되는 시점 (static 영역의 데이터 로딩 시점에 인스턴스 생성) = 하나의 인스턴스만 생성되는 것을 보장해준다.
- 미리 만들어 놓은 인스턴스를 리턴하므로 Thread-safe함과 동시에 간결하고 성능도 좋다
- 하지만, 인스턴스를 사용하지 않는다면 메모리 낭비에 불과하다.
DCL(Double Checked Locking)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
- getInstance() 메소드 레벨에 synchronized가 있지 않고 메소드 내부에 존재한다.
- 호출할 때마다 synchronized가 걸리지 않고, 인스턴스가 이미 존재한다면 synchronized를 쓰지 않기 대문에 성능 이슈를 피할 수 있다.
- static 필드에 volatile 키워드를 쓴다.
💡
volatile이란?
- Java 변수를 Main Memory에 저장하겠다.
- 멀티 쓰레드 환경에서 하나의 쓰레드만 Read & write이 가능하고, 나머지 쓰레드가 read만 하여 가장 최신의 값을 보장하도록 한다.
- Cache에 저장된 값이 아닌 Main Memory에서 읽고, 작성도 Main Memory에 작성한다.
💡
왜 volatile 키워드를 사용해야하는가?
- 작업을 수행하는 동안 성능 향상을 위해 각 메모리에서 읽은 변수를 CPU Cache에 저장하게 된다. 그리고 쓰레드가 변수값을 읽어오면 각 CPU Cache에서 읽기에 변수 값이 불일치되는 문제가 발생한다.
- 그래서 volatile을 추가해서 공통된 Main Memory에 저장하고 읽게끔하여 변수 값 불일치 문제를 해결한다.
Lazy Holder
public class Singleton {
private Singleton() {}
//inner class
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
- Lazy initialization 방식을 가져가면서 쓰레드간 동기화문제를 동시에 해결할 수 있다.
- SingletonHolder는 getInstance()메소드가 호출되기 전에는 참조되지 않으며, 최초로 메소드가 호출될 때 클래스 로더에 의해 Singleton 인스턴스를 생성하여 리턴한다.
- SingletonHolder 내부 인스턴스는 static 이기 때문에 클래스 로딩 시점에 한번만 호출된다는 점을 이용한 것이며, final을 사용해 다시 값이 할당되지 않도록 한다.
⇒ volatile이나 synchronized와 같은 키워드가 없어도 Thread-safe 하면서 성능도 보장하는 아주 훌륭한 방법이다.
Uploaded by N2T