5. 싱글턴 패턴

  1. 5. 싱글턴 패턴
    1. 유일무이한 객체
    2. 고전적인 싱글턴 패턴 구현법
    3. 초콜릿 공장

유일무이한 객체

  • 싱글턴패턴은 인스턴스가 하나 뿐인 특별한 객체를 만들 수 있게 해주는 패턴이다.
  • 어떤 용도로 쓰는 건가?
    • 스레드 풀이라던가, 캐시, 대화상자, 사용자설정, 디바이스드라이버 등등 객체가 전체프로그램에서 오직 하나만 생성되어야 하는 경우에 사용한다.
  • 그럼 전역변수로 static 으로 선언해서 사용하면 되지 않느냐?
    전역 변수로 객체를 생성하면 어플리케이션이 실행 될 때 객체가 생성될 것이다.(P208 맨밑줄)
    그 객체가 자원을 많이 차지 한다면 사용도 되기전에, 괜히 자원만 차지한다. 사용하지 않는다면 아무 쓸 데 없는 객체가 되겠지.

고전적인 싱글턴 패턴 구현법

  • 조심하세요.. 이 코드에 문제가 있다는 것을 알게 될 것입니다.

public class Singleton {
  
  //private으로 Sinleton클래스의 유일한 인스턴스를 저장하기 위한 정적 변수를 선언
  private static Singleton uniqueInstance;

  //생성자를 private로 선언했기 때문에 Singleton에서만 클래스를 만들 수 있다.
  private Singleton() {}

  //클래스의 인스턴스를 만들어서 리턴해 준다.
  public static Singleton getInstance() {
    if(uniqueInstance == null) {
      uniqueInstance = new Singleton();
    }
    return uniqueInstance;
  }

}

초콜릿 공장

  • 만약 애플리케이션에서 ChocolateBoiler 인스턴스가 두 개 이상 만들어지게 되면 어떤 문제가 생길까?
Non-SingletonSingleton
{code}
public class ChocolateBoiler {
private boolean empty;
private boolean boiled;

private ChocolateBoiler() {
//이 코드는 보일러가 비어있을 때만 돌아갑니다
empty = true;
boiled = false;
}

public void fill() {
if (isEmpty()) {
//보일러가 비어있을 때만 재료를 집어 넣습니다.
//원료를 가득 채우고 나면 empty와 boiled 플래그를 false로 설정합니다.
empty = false;
boiled = false;
// 보일러에 우유/초콜릿을 혼합한 재료를 집어넣음
}
}

public void drain() {
//보일러가 가득 차 있고(비어있지 않고), 다 끓여진 상태에서만
//보일러에 들어있는 재료를 다음 단계로 넘깁니다.
//보일러를 다 비우고 나면 empty 플래그를 다시 true로 설정합니다.
if (!isEmpty() && isBoiled()) {
// 끓인 재료를 다음 단계로 넘김
empty = true;
}
}

//보일러가 가득 차 있고 아직 끓지 않은 상태에서만
//초콜릿과 우유가 혼합된 재료를 끓입니다.
//재료가 다 끓고 나면 boiled 플래그를 true로 설정합니다
public void boil() {
if (!isEmpty() && !isBoiled()) {
// 재료를 끓임
boiled = true;
}
}

public boolean isEmpty() {
return empty;
}

public boolean isBoiled() {
return boiled;
}
}

|

public class ChocolateBoiler {
private static ChocolateBoilerSingleton cb;
private boolean empty;
private boolean boiled;

private ChocolateBoiler() {
empty = true;
boiled = false;
}

public static ChocolateBoilerSingleton getInstance(){
if(cb == null)
cb = new ChocolateBoilerSingleton();

return cb;
}

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;
}
}

|

h2. 싱글턴 패턴의 정의

* 싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어진다,
* 어디서든지 그 인스턴스에 접근할 수 있도록 한다.
* 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들면 된다.

|!singleton.jpg!|


h2. 문제가 생겼다.

도대체 무슨 일이 일어난 거지? 새로 만든 싱글턴 코드가 얼마 전까지만 해도 문제 없이 잘 돌아가고 있었는데. 
조금 전에 다중 스레드를 사용하도록 초콜릿 보일러 컨트롤러를 최적화시킨 걸 빼면 이런 문제를 일으킬 만한 게없는데...

두 개의 스레드에서 여기에 있는 코드를 실행시킨다고 가정해 보면. 두 스레드가 다른 보일러 객체를 사용하게 될 가능성은 없는지 따져 보자.

ChocolateBoiler boiler = ChocolateBoiler.getInstance();
fill();
boil();
drain();


* page 226 참고 h2. 멀티스레딩 문제 해결 방법 * getInstance()를 동기화시키기만 하면 멀티스레딩과 관련된 문제가 간단하게 해결된다.

public class Singleton {
private static Singleton uniqueInstance;
// 기타 인스턴스 변수
private Singleton() {}

//synchronized 키워드만 추가하면 두 스레드가 이 메소드를 동시에 실행시키는 일은 일어나지 않게 된다.
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
// 기타 메소드
}



* 이렇게 하면 문제가 해결되긴 하겠지만, 동기화를하면 속도 문제가 생기지 않나?
  동기화는 불필요한 오버헤드만 증가시킬 수 있다.

h2. 더 효율적인 방법은 없을까요?

h3. 1. getInstance()의 속도가 그리 중요하지 않다면 그냥 내비 둔다.
 * 메소드를 동기화하면 성능이 100배 정도 저하된다는 것은 기억해 두자
 * 만약 getInstance( )가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해봐야 한다.

h3. 2. 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버린다.


public class Singleton {
private static Singleton uniqueInstance = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return uniqueInstance;
}
}



* 이런 접근법을 사용하면 클래스가 로딩될 때 JVM에서 Singleton의 유일한 인스턴스를 생성해 준다.

h3. 3. DCL(Double-Checking Locking)을 써서 getInstance()에서 동기화되는 부분을 줄인다.

* DCL(Double-Checking Locking)을 사용하면, 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 할 수 있다.
* volatile 키워드를 사용하여 멀티스레딩을 쓰더라도 uniqueInstance 변수가 Singleton 인스턴스로 초기화 되는 과정이 올바르게 할 수 있다.
* DCL은 자바 1.4 이전 버전에서는 쓸 수 없다


public class Singleton {
private volatile static Singleton uniqueInstance;

private Singleton() {}

public static Singleton getInstance() {
if (uniqueInstance == null) {
//이렇게 하면 처음에만 동기화 된다
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}




h2. 핵심 정리
* 어떤 클래스에 싱글턴 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가 최대 한 개 까지만 있도록 할 수 있다.
* 싱글턴 패턴을 이용하면 유일한 인스턴스를 어디서든지 접근할 수 있도록 할 수 있다.
* 자바에서 싱글턴 패턴을 구현 할 때는 private 생성자와 정적 메소드, 정적 변수를 사용 한다.
* 다중 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용해야 한다.
* DCL을 사용하는 방법은 자바2 버전 5(자바 1.5)보다 전에 나온 버전에서는 쓸 수 없다는 점에 주의.
* 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고, 여러 개의 인스턴스가 생길 수 있다.



h2. 문서에 대하여
* 최초작성자 : [김정식]
* 최초작성일 : 2008년 03월 01일
* 이 문서는 [HeadFirst Design Patterns|http://book.naver.com/bookdb/book_detail.php?bid=1882446]을 정리한 내용 입니다.
* 이 문서는 [오라클클럽|http://www.gurubee.net] [자바 웹개발자 스터디|제3차 자바 웹개발자 스터디] 모임에서 작성하였습니다.
* 이 문서를 다른 블로그나 홈페이지에 퍼가실 경우에는 출처를 꼭 밝혀 주시면 고맙겠습니다.~^\^