개념
싱글톤 패턴은 필요한 객체의 인스턴스를 오직 한개만 제공하는 클래스를 만드는 방법이다.
시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는 경우가 있다.
인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요할 때 이 패턴을 사용할 수 있다.
이 패턴의 목적은 두가지가 있다.
1. 인스턴스를 오직 하나만 만들수 있어야 한다.
2. 그렇게 만들어진 하나의 인스턴스에 글로벌하게 접근할 수 있어야 한다.
일반적으로는 아래와 같이 클래스를 만들고 객체를 두번 생성하면 두 클래스의 값은 다르다.
public class Settings {}
public class App {
public static void main(String[] args) {
Settings setting1 = new Settings();
Settings setting2 = new Settings();
System.out.println(setting1 != setting2); // 결과는 true
}
}
싱글톤 패턴에서는 객체를 한개만 생성하게 해야하며 밖에서 new를 쓰게 하는 순간 여러개의 객체를 생성하게 만들 수 있기 때문에 밖에서 new를 사용하지 못하게 해야 한다.
☞ 그러한 방법에 대한 해결책으로는 'private 생성자'를 만들면 된다.
- 접근 권한을 걸어서 생성자 자체를 밖에서 만들지 못하게 하는 것이다.
- 또한 객체를 만들더라도 단 한번만 클래스 내부에서 new를 호출하도록 해야 한다.
또한 이렇게 내부적으로 만든 객체를 글로벌하게 접근할 수 있는 방법을 제공해주어야 한다.
☞ 생성된 객체를 글로벌하게 접근할 수 있게 하기 위한 방법으로 'static' 키워드를 사용해서 접근하는 메서드를 만든다.
Naive한 싱글턴 패턴
결과적으로 위의 해결방법을 적용하면 아래와 같은 Settings 클래스가 생성된다. 이것이 가장 Naive한 싱글턴 패턴이라 말할 수 있다.
public class Settings {
private static Settings instance;
private Settings() {}
public static Settings getInstance() {
if(instance == null){
instance = new Settings();
}
return instance;
}
}
다음 코드의 결과는 false가 나오게 된다.
public class App {
public static void main(String[] args) {
Settings setting1 = Settings().getInstance();
Settings setting2 = Settings().getInstance();
System.out.println(setting1 != setting2); // 결과는 false
}
}
getInstace()를 여러번 호출하더라도 하나의 인스턴만을 반환하게 된다. 그래서 두 객체는 같게 된다.
인스턴스 객체 자체를 메모리에 한번 할당되어 프로그램 종료시 해제되는 Static 키워드를 붙이고 private 생성자를 의도적으로 사용하여 외부에서 생성자를 참조 못하게 하는 방식으로 해결한 것이다.
스레드 세이프 문제
하지만 이 방법에는 웹을 만들 때 많이들 쓰는 '멀티스레드 환경'에서 사용하기엔 심각한 문제가 있다.
위의 그림에서 빨간 스레드, 파란 스레드가 있다고 치자. 여기서 빨간 스레드가 if문을 먼저 진입하고 파란 스레드가 다음으로 if문을 진입한다고 했을 때 new는 두번 호출된다. 그래서 문제가 있다는 것이다. 그러면 이걸 어떻게 해결할까?
☞ 메서드를 동기화시키자. 방법은 여러가지가 있다.
- synchronized 키워드 사용하기 : 이 방법은 동기화를 보장하긴 하지만 한쪽이 if문 안에 있을 때 lock을 걸어버리기 때문에 부가적인 성능의 부하가 생길 여지가 있다.
- 이른 초기화(Eager initialization ) 방법 사용하기 : 인스턴스를 private 선언과 동시에 초기화시켜주고 getInstance에서는 리턴만 해주는 방식이다. 이 방식은 인스턴스 자체가 메모리를 많이 먹고 길고 오래걸리고 잘 안쓰면 비효율적인 방식이다.
- Double Checked Locking 사용하기 : 두번의 if문을 넣고어서 객체 존재 여부를 체크하고 첫번째 if문 안쪽에 synchronized 키워드로 동기화시키는 방법인데 volatile 키워드를 써야 동작하고 코드 짜는게 귀찮다...
권장하는 방식
static inner 클래스를 사용하는 방법
다음과 같이 사용하면 멀티스레드 환경에서도 안전하고 getInstance() 호출되는 시점에서 클래스가 로딩되서 인스턴스를 미리 안만들어도 된다.
public class Settings{
private Settings(){}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
하지만 ... 이방법도 Reflection API를 쓰면 싱글톤을 깨트릴 수 있다. Reflection API가 제공하는 setAccessible() 메서드를 써버리면 private도 접근이 가능하다. 이 방식으로 강제로 생성자를 호출시켜버리는 것이다. 이것에 대한 해결책으로 enum으로 싱글턴을 구현하는 방식도 있는데 이래버리면 구현이 제한적인면이 있어서 개인적으로는 또 좋지 않다고 생각한다. 그래서 일단은 static inner 방식까지만 기억해두자
출처 > 인프런 - 백기선 - 디자인 패턴