싱글턴 패턴
: 세상에서 단 하나뿐인 특별한 객체
-
들어가기 전 질문
-
- 하나만 쓰는 경우가 많은가요?
- 스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체, 프린터나 그래픽 카드 디바이스 드라이버 등등
-
- 이 클래스는 객체를 하나만 가짐으로 생각하면 되잖아요?
- 오랫동안 검증받는 방법으로 쓰는 패턴이란다
-
- 스테틱(static)으로 선언하면 되지 않을까요?
- 정적변수로 만들 경우의 여러 단점을 감수하지 않고 객체 인스턴스를 어디서나 엑세스 할 수 있다.
-
- 전역 변수의 단점은?
- 애플리케이션이 시작될 때 객체가 생성될 것이야. 객체가 자원을 많이 차지한다면 자원의 낭비이겠지. 싱글턴은 패턴을 사용하면 필요할 때만 객체를 만들 수 있단다.
-
- 간단해 보이는데?
- 싱글턴 코드 자체는 쉬워보이지만 제대로 만들고 이해하는 것은 만만치 않은 일이란다. 만약 어떻게 하면 한 클래스의 인스턴스가 두 개 이상 만들어지지 않도록 한다고 생각해보렴. 쉽지 않을걸?!
public MyClass{ private MyClass() {} public static MyClass getInstance(){ return new MyClass(); } }
-
-
이제 MyClass가 인스턴스가 하나만 만들어지도록 구현해볼까요?
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getInstance() {
if(uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
: 싱글턴 패턴의 고전적인 구현법을 배웠습니다.
-
- 싱글턴 패턴
- 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴입니다.
-
- 초콜릿 공장
- 보일러가 비어 있을 경우에만 재료를 넣고 끓이기 시작합니다. 초콜릿이 다 끓여지면 다시 보일러를 비웁니다. 보일러가 가득 차 있는 상태에서 새로운 원료를 붓는다거나 빈 보일러에 불을 지피지 않도록 합니다.
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; } } public boolean isEmpty() { return empty; } public boolean isBoiled() { return boiled; } }
-
초콜릿보일러 클래스를 싱글턴 패턴으로 바꿔보세요.
-
- 문제가 있습니다. 보일러에 있는 fill()메소드에서 아직 초콜릿이 끓고 있는데 재료를 집어넣었습니다.
- uniqueInstance가 있는 초콜릿 보일러에 두 개의 스레드가 접근한다고 했을 때 생기는 문제에 대해 생각해봅시다. 1번 스레드 - 초콜릿 보일러에서 인스턴스를 가져옴 2번 스레드 - 초콜릿 보일러에서 인스턴스를 가져옴 1번 스레드 - if문 통과 유니크 인스턴스가 널 2번 스레드 - if문 통과 유니크 인스턴스가 널 1번 스레드 - 새로운 보일러 생성 2번 스레드 - 새로운 보일러 생성 … 두 스레드가 서로다른 인스턴스를 두 개 생성했습니다.
-
- 멀티스레딩 문제 해결 방법
- sycnonized 로 동기화를 해줍니다.
public class ChocolateBoiler { private boolean empty; private boolean boiled; private static ChocolateBoiler uniqueInstance; private ChocolateBoiler() { empty = true; boiled = false; }//보일러가 비어있을 때만 돌아갑니다. public static synchronized ChocolateBoiler getInstance() { if(uniqueInstance == null) { uniqueInstance = new ChocolateBoiler(); } return uniqueInstance; } 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; } } public boolean isEmpty() { return empty; } public boolean isBoiled() { return boiled; } }
-
인스턴스를 매번 부를 때마다 동기화를 하네요? 조금 오버가 아닐까요? 인스턴스를 생성할 때만 동기화 하고 싶은데,..(참고로 메소드를 동기화 하면 성능이 100배 정도 저하된다고 합니다;)
public class Singleton { private static Singleton uniqueInstance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return uniqueInstance; } }
: 이 접근법을 사용하면 클래스가 로딩될 때 JVM에서 Singleton의 유일한 인스턴스를 생성해줍니다. JVM에서 유일한 인스턴스를 생성하기 전에는 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없습니다.
-
- “DCL(Double-Checking Locking)”을 써서 getInstance()에서 동기화되는 부분을 줄입시다.
- DCL을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있습니다. 이렇게하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 됩니다.
public class Singleton { private volatile static Singleton uniqueInstance; //volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 uniqueInstance변수가 Singleton인스턴스로 초기화되는 과전이 올바르게 진해되도록 합니다. private Singleton() {} public static Singleton getInstance() { if(uniqueInstance == null) {//인스턴스가 있는지 확인하고 없으면 동기화된 블럭으로 들어갑니다. synchronized (Singleton.class) { if(uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
: volatile 선언은 외부요인에 의해 변수의 값이 바뀔 수 있음을 나타냅니다. volatile변수를 참조할 때는 레지스터에 로드되어 있는 값을 부르지 않고 매번 메모리를 참조한다.
-
- 싱글턴 클래스는 서브클래스를 못만듭니다.
- 서브클래스에서는 기본적으로 상위클래스의 생성자를 부릅니다. 생성자가 private이기 때문에 안되겠죠? 그리고 접근지정자를 바꾸면서 서브클래스를 바꾼다면? 같은 하나의 인스턴스를 여러 서브클래스가 공유하게 됩니다. 그리 좋지 않습니다.