싱글톤 패턴

디자인패턴 2012. 11. 2. 15:08

추가

싱글톤을 안전(Thread Safe)하게 생성하는 방법 몇가지를 소개합니다.

1. 이른 초기화 방법

싱글톤 객체를 미리 생성해 놓는 방법입니다. 항상 싱글톤 객체가 필요하거나 객체 생성 비용이 크지 않을 경우 사용합니다.

1
2
3
4
5
6
7
8
9
public class Singleton {
    private static final Singleton instance = new Singleton();
  
    private Singleton() {}
  
    public static Singleton getInstance() {
        return instance;
    }
}

싱글톤 객체가 static으로 선언 되었기 때문에 해당 클래스가 클래스 로더에 의해 로딩 될 때 객체가 생성 됩니다. 클래스 로더에 의해 클래스가 처음 로딩 될 때 수행 되므로 thread safe합니다. 단점으로는 싱글톤 객체를 사용하든 안하든 해당 클래스가 로딩 되는 시점에 항상 싱글톤 객체가 생성되고 리소스(메모리)를 차지하고 있으니 상황에 따라 비효율적일 수 있겠죠.

2. 늦은 초기화 방법

싱글톤 객체를 처음 사용하는 시점에 생성합니다. 처음 getInstance() 호출시에 싱글톤 객체를 생성 시키는 방법입니다. 하지만 concurrent한 상황에서 getInstance() 메소드에 동시 접근이 가능하므로 싱글톤 객체의 특성인 ‘한개의 인스턴스만 생성’ 룰이 깨질 수 있습니다.
이 문제를 해결 하기 위해서 synchronized 키워드를 사용해서 동시성 문제를 해결하고 객체 생성이 안되어 있을 경우에만 생성 하도록 처리합니다. getInstance() 메소드에 synchronized를 걸어주거나 주로 double-checked locking 이라는 기법을 사용합니다. 전자는 메소드 전체에 synchnorized 를 걸어주기 때문에 성능상 이슈가 있고요. 후자는 아래 코드와 같이 해당 싱글톤 객체가 null 일 경우에만 해당 싱글톤 객체에 synchronized를 거는 방법으로 성능 이슈를 해결한 방법입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SingletonDemo {
        private static volatile SingletonDemo instance = null;
  
        private SingletonDemo() {       }
  
        public static SingletonDemo getInstance() {
                if (instance == null) {
                        synchronized (SingletonDemo .class){
                                if (instance == null) {
                                        instance = new SingletonDemo ();
                                }
                      }
                }
                return instance;
        }
}

장점으로는 이른 초기화와 반대겠지요? 사용시점까지 싱글톤 객체 생성을 미루기 때문에 사용하기 전까진 자원(메모리)을 점유하지 않는다는 점이네요. (double-checked locking 기법은 J2SE 5.0 이전 버전에서는 문제가 있다고 하니 주의 바랍니다.)

3. Initialization on demand holder idiom (holder에 의한 초기화 용법)

이것도 늦은 초기화인데 별도의 synchronized 키워드를 사용하지 않고 중첩 클래스(Holder)를 이용하는 방법으로 모든 자바 버젼에서 동작합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
        // Private constructor prevents instantiation from other classes
        private Singleton() { }
  
        /**
        * SingletonHolder is loaded on the first execution of Singleton.getInstance()
        * or the first access to SingletonHolder.INSTANCE, not before.
        */
        private static class SingletonHolder {
                public static final Singleton INSTANCE = new Singleton();
        }
  
        public static Singleton getInstance() {
                return SingletonHolder.INSTANCE;
        }
}

중첩 클래스(SingletonHolder)는 getInstance 메소드가 호출되기 이전에는 참조 되지 않으며, 처음 getInstance 메소드 호출시 SingletonHolder 클래스가 클래스 로더에 의해 로딩 되면서 싱글톤 객체 생성이 이루어집니다. (물론 static 이므로 클래스 로딩 시점에 한번만 호출 되겠죠?)

늦은 초기화의 장점 + 중첩 클래스를 이용해야하지만 synchronized의 부담이 없다는 장점이 있겠죠?

4. Enum을 이용한 방법

이펙티브 자바 2nd Edition에서 나온 방법입니다. 모든 Enum type들은 프로그램 내에서 한번 초기화 되는 점을 이용합니다.

1
2
3
4
5
6
public enum Singleton {
        INSTANCE;
        public void execute (String arg) {
                //... perform operation here ...
        }
}

실제 위 코드의 컴파일 된 클래스 파일을 javap 명령어로 확인하면 다음과 같습니다.

1
2
3
4
5
6
7
public final class Singleton extends java.lang.Enum{
    public static final Singleton INSTANCE;
    public static Singleton[] values();
    public static Singleton valueOf(java.lang.String);
    public void execute(java.lang.String);
    static {};
}

위에서 본 코드들과 비슷하지요? 클래스 로딩 시점에 static 블럭에서 싱글톤 객체인 INSTANCE에 대한 초기화가 이루어집니다. 물론 이른 초기화의 장점과 동일하겠죠?

[참고]
http://en.wikipedia.org/wiki/Singleton_pattern

출처 - http://unabated.tistory.com/entry/%EC%8B%B1%EA%B8%80%ED%86%A4-%ED%8C%A8%ED%84%B4


싱글턴 패턴

유일무이한 객체

  • 싱글턴패턴은 인스턴스가 하나 뿐인 특별한 객체를 만들 수 있게 해주는 패턴이다.
  • 어떤 용도로 쓰는 건가?
    • 스레드 풀이라던가, 캐시, 대화상자, 사용자설정, 디바이스드라이버 등등 객체가 전체프로그램에서 오직 하나만 생성되어야 하는 경우에 사용한다.
  • 그럼 전역변수로 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
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;
  }
}

싱글턴 패턴의 정의

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

문제가 생겼다.

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

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

ChocolateBoiler boiler = ChocolateBoiler.getInstance();
 fill();
 boil();
 drain();
  • page 226 참고

멀티스레딩 문제 해결 방법

  • getInstance()를 동기화시키기만 하면 멀티스레딩과 관련된 문제가 간단하게 해결된다.
public class Singleton {
  private static Singleton uniqueInstance;
  // 기타 인스턴스 변수
  private Singleton() {}
  
  //synchronized 키워드만 추가하면 두 스레드가 이 메소드를 동시에 실행시키는 일은 일어나지 않게 된다.
  public static synchronized Singleton getInstance() {
    if (uniqueInstance == null) {
       uniqueInstance = new Singleton();
    }
    return uniqueInstance;
  }
// 기타 메소드
}
  • 이렇게 하면 문제가 해결되긴 하겠지만, 동기화를하면 속도 문제가 생기지 않나?
    동기화는 불필요한 오버헤드만 증가시킬 수 있다.

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

1. getInstance()의 속도가 그리 중요하지 않다면 그냥 내비 둔다.

  • 메소드를 동기화하면 성능이 100배 정도 저하된다는 것은 기억해 두자
  • 만약 getInstance( )가 애플리케이션에서 병목으로 작용한다면 다른 방법을 생각해봐야 한다.

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

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

  private Singleton() {}

  public static Singleton getInstance() {
    return uniqueInstance;
  }
}
  • 이런 접근법을 사용하면 클래스가 로딩될 때 JVM에서 Singleton의 유일한 인스턴스를 생성해 준다.

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

핵심 정리

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

문서에 대하여

'디자인패턴' 카테고리의 다른 글

팩토리 패턴(Factory Pattern)  (0) 2014.11.11
옵저버 패턴(Observer Pattern)  (0) 2014.11.11
데코레이터 패턴(Decorator Pattern)  (0) 2014.11.11
어댑터 패턴(Adapter Pattern)  (0) 2014.11.11
데코레이터 패턴  (0) 2012.11.02
: