[Design Pattern] 싱글톤 패턴
싱글톤 패턴?
싱글톤 패턴은 애플리케이션이 시작될 때, 어떤 클래스가 최초 한 번만 메모리를 할당(static)하고 해당 메모리에 인스턴스를 만들어 사용하는 패턴이다.
즉, 생성자가 여러 번 호출되어도, 실제로 생성되는 객체는 하나이고, 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 것이다.
시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러 개일 때 문제가 생길 수 있는 경우가 있다. 따라서 인스턴스를 오직 한 개만 만들어 제공하는 클래스가 필요하다.
구현
먼저 다음은 싱글톤 패턴을 구현한 코드이다. 한 번 살펴보자.
class Settings {
private static Settings instance;
private Settings() { }
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
public class App {
public static void main(String[] args) {
Settings settings = new Settings.getInstance();
System.out.println(settings == Settings.getInstance());
}
}
그런데 여기서 문제가 하나 있다. 위 코드는 멀티 스레드 환경에서 안전할까이다.
먼저 말하자면, 위 코드는 안전하지 않다. 왜 그럴까?
예를 들어, A 스레드, B 스레드 이렇게 두 개의 스레드가 있다고 가정하자.
- A 스레드가 getInstance에 진입하여 if 문에서 instance가 null인지 먼저 체크한다.
- 이때, B 스레드도 진입하여 if 문에서 instance가 null인지 체크한다.
- B 스레드가 if 문에 진입할 때, 아직 A 스레드가 객체를 생성하기 전이라면 두 스레드 모두 instance == null 조건이 성립하게 되어 if 문 안으로 진입하게 되는 상황이 발생한다.
- 싱글톤임에도 불구하고 객체가 2개 생성되는 결과를 초래한다.
그러면 어떻게 해야 thread safety하게 작성할 수 있을지 알아보자.
Thread Safety Singleton
sychronized 키워드 사용
class Settings {
private static Settings instance;
private Settings() { }
public static synchronized Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
sychronized 를 통해 메서드에 하나의 스레드만 들어오도록 한다.
- 장점: 구현이 쉽다.
- 단점: 동기화 처리 작업으로 인해, 성능이 떨어질 수 있다.
Eager Initialization
class Settings {
private static final Settings INSTANCE = new Settings();
private Settings() { }
public static Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return INSTANCE;
}
}
인스턴스 변수에 미리 초기화 시킨다.
- 장점: synchronized 보다 성능이 좋고, 객체 생성 비용이 비싸지 않다.
- 단점: 미리 만들고 사용하지 않는다면, 리소스 낭비로 이어질 수 있다.
Double Checked Locking
class Settings {
private static volatile Settings instance;
private Settings() { }
public static Settings getInstance() {
if (instance == null) {
synchronized (Settings.class) {
if (instance == null) {
instance = new Settings();
}
}
}
return instance;
}
}
Double Checked Locking을 사용한다.
메서드에 synchronized를 선언한 것과의 차이점
여러 개의 멀티 스레드가 호출하는 상황을 생각해보자.
메서드에 synchronized를 선언한 경우에는 모든 스레드가 synchronized 처리된다. 하지만 위의 코드 같은 경우 이미 인스턴스가 있는 경우에 if 문에서 스킵이 된다. 즉, 최초 생성 시에만 synchronized 처리가 되기 때문에 효율적이다.
static inner class
class Settings {
private Settings() { }
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
static inner class를 사용한다. 위 방법들 중 이 방법을 제일 추천한다.
getInstance가 호출될 때 SettingsHolder 클래스가 로딩되고 객체가 생성되기 때문에 미리 생성하는 부담이 없다.
위 내용은 [코딩으로 학습하는 GoF의 디자인 패턴]을 참고하여 작성했습니다