- 이번 장의 특성상 따로 Diagram 을 생성하지 않았다. 그리고 책 내용에 좀 더 충실하게 포스팅해봤다. -


* Singleton Pattern

해당 클래스의 인스턴스가 오직 하나 만들어지고,

어디서든지 그 인스턴스에 전역 접근할 수 있도록 하기 위한 패턴이다.


- 어떤 상황에서 사용될까?

스레드풀이라든가 캐시, 대화상자, 사용자 설정 또는 레지스트리 설정을 처리하는 객체,

로그 기록용 객체, 프린터나 크래픽 카드 같은 디바이스를 위한 디바이스 드라이버 같은 걸 예로 들 수 있다.

이런 상황에서 2개 이상의 인스턴스를 만들게 된다면, 프로그램이 이상하게 돌아간다든가

자원을 불필요하게 잡아먹는다든가 일관성 없는 결과를 초래할 수 있다.


- 전역변수(Global Variable)도 방법이 될 수 있지 않나?

전역변수에 객체를 대입하면 애플리케이션이 시작될 때 객체가 생성될 것이다.

그런데 여기서 그 객체가 자원을 많이 차지한다고 가정했을 때, 그 객체가 애플리케이션 종료 시까지

한 번도 사용되지 않는다면 괜히 자원만 잡아먹는 쓸데없는 녀석이 될 수 있다.

Singleton 패턴의 경우에는 필요할 때만 객체를 생성할 수가 있다.



< 고전적인 구현법 >

public class Singleton {
    private static Singleton uniqueInstance;
    private Singleton() { }
    public static Single getInstance() {
        if ( uniqueInstance == null ) {
            // 인스턴스가 필요한 상황이 닥치기 전에는 아예 인스턴스를 생성하지 않는다.
            // 이 기법을 '게으른 인스턴스 생성(Lazy instantiation)' 이라 한다.
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

클래스의 객체가 자원을 많이 잡아먹는 경우에는 위와 같이 '게으른 인스턴스 생성' 기법이 꽤 유용하다.

위 코드를 읽을 때는 접근한정자와 static 키워드를 주의깊게 읽자.



하지만 주의해야 할 점이 있다. 

* 고전적인 방법이 다중 스레드 환경에서도 아무 문제 없을까?


1번 스레드 

2번 스레드 

uniqueInstance 값 

 public static Single getInstance() {

 

null

  public static Single getInstance() {

 

  if ( uniqueInstance == null ) {

 

 

   if ( uniqueInstance == null ) {

 

   uniqueInstance = new Singleton();

 

object 1 

   return uniqueInstance;

 

 

    uniqueInstance = new Singleton(); 

object 2

 

   return uniqueInstance;

 


위의 경우, 객체가 2 개가 만들어져 Singleton 의 의미가 없어지는 문제가 발생한다.

어떻게 해야할까?

public static synchronized Singleton getInstance() {
    if ( uniqueInstance == null ) {
        uniqueInstance = new Singleton();
    }
    return uniqueInstance;
}

synchronized 키워드를 추가하면, 한 스레드가 사용을 끝내기 전까지 다른 스레드는 기다려야 한다. 

다시 말해서 2개 이상의 스레드가 동시에 실행시키는 일은 발생하지 않는다.

그러나 ! !

책에 의하면... 메소드를 동기화하면 성능이 100배 정도 저하된다는 것은 기억하라고 한다.



* Singleton 패턴에서의 3 가지 해법 :

(1) getInstance() 의 속도 이슈가 그렇게 중요하지 않다면 그냥 동기화를 해도 된다.

- 애플리케이션에 큰 부담을 주지 않는다면 바로 위의 코드처럼 놔둬도 된다.

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

- 애플리케이션에서 반드시 Singleton의 인스턴스를 생성하고 항상 사용한다면 or

 인스턴스를 실행중에 수시로 만들고 관리하기 성가시다면 정적으로 초기화 해버린다.

 이 방법은 JVM 에서 유일한 인스턴스를 생성하기 전까진 그 어떤 스레드도 uniqueInstance 에 접근 불가.

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

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

- 인스턴스가 생성되어 있는지 확인한 다음, null 인 경우만 동기화 할 수 있기때문에

 처음에만 동기화를 하고 이후에는 동기화하지 않아도 된다. 

 이 방법으로 오버헤드를 극적으로 줄여 속도를 향상시킬 수 있다. 

- volatile 키워드를 사용하면 다중 스레딩을 쓰더라도 uniqueInstance = new Singleton(); 의 과정이

 올바르게 진행될 수 있다.

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

단, (3) 방법은 자바 1.4 이전 버전에는 사용할 수 없다고는 하나 

현재 14.05.20 기준으로는 버전 1.8 까지 나왔으니 크게 염려할 필요는 없지않나 싶다.


3가지 방법 모두가 다중 스레딩 환경에서 발생하는 문제를 해결할 수 있다.

하지만 속도 이슈와 자원 문제, 구현 시나리오를 염려해두면서 적절한 해법을 선택하도록 하자.



* 객체지향의 원칙 :

1) 바뀌는 부분은 캡슐화 한다.

2) 상속보다는 구성을 활용한다.

3) 구현이 아닌 인터페이스에 맞춰서 프로그래밍 한다.

4) 서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.

5) 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다(OCP).

6) 추상화된 것에 의존해라. 구상 클래스에 의존해서는 안 된다(의존성 뒤집기 법칙).



이 장에서의 정리 :

- 어떤 클래스에 Singleton 패턴을 적용하면 애플리케이션에 그 클래스의 인스턴스가

 최대 1개까지만 있도록 할 수 있다. 또한 그 유일한 인스턴스에 전역 접근할 수 있다.

- 사실 모든 애플리케이션은 다중 스레딩을 쓸 수 있다고 생각해야하기 때문에 고전적인 구현은 되도록 피하자.

- 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고 여러 개의 인스턴스가 생길 수 있다.

 (클래스 로더마다 서로 다른 네임스페이스를 정의하기 때문이다. 

 클래스 로더가 2개 이상일 때, 각 클래스 로더마다 한번씩 같은 클래스를 여러 번 로딩할 수도 있다. 

 그러니 클래스 로더를 직접 지정해서 이 문제를 피할 수 있다고 한다.)


※ 사실 클래스 로더가 뭔지 잘 모르겠다.


by kelicia 2014. 5. 20. 00:33