싱글턴 패턴
정의 - 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴
용도
- 스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정를 처리하는 객체, 로그 객체, 디바이스 드라이버 등 인스턴스가 2개 이상만들게 되면 프로그램이 이상하게 작동하거나, 자원을 불필요하게 잡아먹거나, 결과에 일관성이 없어지는 심각한 문제가 생길수 있음
고전적인 싱글턴 패턴
public class Singleton {
private static Singleton uniqueInstance; //유일한 인스턴스를 저장하기 위한 정적 변수
private Singleton () { } //private으로 선언하여 외부에서 객체생성 못함 Singleton에서만 클래스의 인스턴스를 만들수 있음
public static Singleton getInstance() { //클래스의 인스턴스를 만들어서 리턴
if (uniqueInstance == null) { // 인스턴스 생성되지 않았는지
uniqueInstance = new Singleton(); //private으로 선언된 생성자를 이용해서 객체를 만든다음 대입, 인스턴스가 필요한 상황이 오기전에는 인스턴스를 생성하지 않는다. 이런방식을 게으른 인스턴스 방식 lazy instantiation
}
return uniqueInstance;
}
}
예제) 초코릿 공장
package singleton;
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
public ChocolateBoiler() {
empty = true;
boiled = false;
}
public void fill() {
if(isEmpty()) {
empty = false;
boiled = false;
//보일러에 우유, 초코릿을 혼합한 재료를 넣음
}
}
public void drain() {
if(!isEmpty() && isBoiled()) {
//끓인 재료를 다음 단계로 넘김
empty = true;
}
}
public void boil() {
if(!isEmpty() && !isBoiled()) {
//재료 끓임
boiled = true;
}
}
private boolean isEmpty() {
return empty;
}
private boolean isBoiled() {
return boiled;
}
}
기본적인 초코릿공장 클래스
package singleton;
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;
private static ChocolateBoiler instance;
private ChocolateBoiler() {
empty = true;
boiled = false;
}
public static ChocolateBoiler getInstance() {
if(instance == null) {
instance = new ChocolateBoiler();
}
return instance;
}
public void fill() {
if(isEmpty()) {
empty = false;
boiled = false;
//보일러에 우유, 초코릿을 혼합한 재료를 넣음
}
}
public void drain() {
if(!isEmpty() && isBoiled()) {
//끓인 재료를 다음 단계로 넘김
empty = true;
}
}
public void boil() {
if(!isEmpty() && !isBoiled()) {
//재료 끓임
boiled = true;
}
}
private boolean isEmpty() {
return empty;
}
private boolean isBoiled() {
return boiled;
}
}
싱글턴으로 변경
이슈발생
스레드가 추가된경우 (멀티스레드경우)
getInstance() 메서드에 작업이 처리되는 순서와 instance의 값이 겹칠 수 있는 경우가 발생
해결방법은
synchronized 동기화를 시켜준다.
public static synchronized ChocolateBoiler getInstance() {
if(instance == null) {
instance = new ChocolateBoiler();
}
return instance;
}
동기화가 꼭 필요한 시점은 이 메서드가 시작되는 때 뿐이다.
instance 변수에 Singleton 인스턴스를 대입하고 나면 굳이 이 메서드를 동기화된 상태로 유지시킬 필요가 없다.
첫번째 과정을 제외하면 동기화는 불필요한 오버헤드만 증가시킴.
효율적인 방법
1. getInstance()의 속도가 그리 중요하지 않다면 그냥 둔다.
=> 동기화화면 성능이 100배정도 저하된다.
/**
* 1. 기본으로 사용하는 싱글톤 패턴
* 2. private 생성자로 생성자를 만들수 없다. 내부에서만 가능
* 3. intance를 체크하여 없다면 new 생성자로 만든다.
*/
public class Singleton {
private Singleton() { }
private static Singleton instance;
public static Singleton getInstance() {
if(instance == null)
instance = new Singleton();
return instance;
}
}
2. 인스턴스를 필요할 때 생성하지 말고 처음부터 아예 만들어 버린다.
=> 클래스가 로딩될때 JVM에서 Singleton의 유일한 인스턴스를 생성해 준다. JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 instance 정적 변수에 접근할 수 없다.
/**
* 1. 멀티스레드로 바로 instance로 바로 생성한다.
*/
public class Singleton1 {
private Singleton1() { }
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
}
3. DCL(Double-checking Locking)을 써서 getInstance에서 동기화되는 부분을 줄인다.
=> DCL을 사용하면 일단 인스턴스가 생성되어 있는지 확인 한 다음, 생성되지 않을 때만 동기화할 수 있다.
처음에만 동기화하고 나중에는 동기화를 하지 않아도 된다.
/**
* 1. 멀티스레드로 DCL 더블클릭락킹
* 2. volatile 멀티스레딩
* 3. DCL 체크
* 4. 하지만 안됨
*/
public class Singleton3 {
private Singleton3() { }
private volatile static Singleton3 instance;
public static Singleton3 getInstance() {
if(instance == null){
synchronized (Singleton3.class) {
if(instance == null) {
instance = new Singleton3();
}
}
}
return instance;
}
}
4. enum으로 만든다.
=> class가 아닌 enum으로 사용
/**
* Enum은 인스턴스가 여러 개 생기지 않도록 확실하게 보장해주고 복잡한 직렬화나 리플렉션 상황에서도 직렬화가 자동으로 지원된다는 이점이 있다.
* 하지만 Enum에도 한계는 있다.
* 싱글톤이 Enum 외으 클래스를 상속해야한다면 사용할 수 없다.
*/
public enum Singleton5 {
INSTANCE;
}
5. SingletonHolder를 사용한다.
=> 내부 스태틱 클래스를 사용하여 처리
/**
* LazyHolder
* 1. final은 한번 초기화 되면 값을 변경할 수 없다.
* 2. static은 Class가 로딩되는(즉, Access되는) 시점에 해당 객체가 JVM의 Class Area에 저장된다.
* 3. Inner class를 사용함으로서 SingletonHolder.instance 이 부분이 실행되기 전까지는 2.의 법칙에 의해서 Load 되지 않는다.
*/
public class Singleton4 {
private Singleton4() { }
private static class SingletonHolder {
private static final Singleton4 instance = new Singleton4();
}
public static Singleton4 getInstance() {
return SingletonHolder.instance;
}
}
핵심정리
- 어떤 클래스에 싱글톤 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가 최대 한개까지만 있도록 할 수 있다.
- 싱글톤 패턴을 사용하면 유일한 인스턴스를 어디서든지 접근 할 수 있도록 할 수 있다.
- 싱글톤 패턴을 구현할 때는 private 생성자와, 정적메서드, 정적변수를 사용한다.
- 다중 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용한다.
(사실 모든 애플리케이션에서 멀티스레딩을 쓸 수 있다고 생각해야 한다)
- 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고 여러 개의 인스턴스가 생길 수 있다.
출처 - Head First Design Patterns 저자- 에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠