헤드퍼스트 디자인패턴 Chapter5

헤드퍼스트 디자인패턴

  • 본 책을 읽고 책의 내용을 간략하게 정리한 글입니다.

Chapter 5. 하나뿐인 특별한 객체 만들기 - 싱글턴 패턴

싱글턴 패턴의 용도

인스턴스를 하나만 만들어야 하는 객체

  • 스레드 풀, 캐시, 대화상자, 사용자 설정, 레지스트리 설정을 처리하는 객체, 로그 기록용 객체와 같이 하나만 있을 때 잘 동작하는 객체들이 있다.
  • 이러한 객체들은 인스턴스가 2개 이상이면 프로그램이 이상하게 돌아간다거나, 결과에 일관성이 없어지는 등 문제가 발생할 수 있다.

전역 변수로 사용하거나 정적 변수를 사용하는 방법은?

  • 싱글턴 패턴은 특정 클래스에 객체 인스턴스가 하나만 만들어지도록 해주는 패턴
  • 싱글턴 패턴을 사용하면 전역 변수를 사용할 때와 마찬가지로 객체 인스턴스를 어디서든 액세스할 수 있게 만들 수 있으며, 전역 변수의 단점을 감수할 필요가 없어진다.
    • 전역 변수가 자원을 많이 잡아먹는 객체라면 애플리케이션이 종료될 때까지 자원을 차지하고 있게 되는 점

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

public class Singleton {
    private static Singleton uniqueInstance;
    
    // 기타 인스턴스 변수
    
    // 생성자를 private로 선언했으므로 Singleton 에서만 클래스의 인스턴스를 만들 수 있다.
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

싱글턴 패턴의 정의

  • 싱글턴 패턴은 클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공한다.
    • 싱글턴 객체의 인스턴스가 필요하다면 반드시 클래스 자신을 거치도록 해야 한다.
    • 어디서든 인스턴스에 접근할 수 있도록 전역 접근 지점을 제공한다.
  • 자원을 많이 잡아먹는 인스턴스가 있다면 싱글턴 패턴 기법이 유용하다.
    |       Singleton       |
    | --------------------- |
    | static uniqueInstance |
    | // 기타 데이터         |
    | --------------------- |
    | static getInstance()  |
    | // 기타 메서드        |
    

고전적인 싱글턴 패턴의 초콜릿 보일러

  • 초콜릿 보일러는 초콜릿과 우유를 받아서 끓이고 초콜릿을 만드는 단계로 넘겨준다.
  • 아직 끓지 않은 재료를 넘겨준다던가, 보일러가 가득 찬 상태에서 새로운 재료를 붓는다거나, 빈 보일러에 불을 올리는 실수를 하지 않도록 주의하여 작성했다.
    public class ChocolateBoiler {
      private boolean empty;
      private boolean boiled;
        
      private 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;
      }
    }
    

멀티스레딩 문제

  • 2개의 스레드에서 아래 코드를 실행해본다고 가정해보자.
    ChocolateBoiler boiler = ChocolateBoiler.getInstance();
    boiler.fill();
    boiler.boil();
    boiler.drain();
    
  • 두 스레드가 다른 보일러 객체를 사용하게 될 가능성은 없는지 따져본다.

멀티스레딩 문제 해결해보기 - synchronized

public class Singleton {
    private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    // synchronized 키워드를 추가하면 한 스레드가 메서드 사용을 끝내기 전까지 다른 스레드는 기다린다. (동기화)
    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
  • 동기화가 꼭 필요한 시점은 getInstance() 메서드가 시작되는 때 뿐
    • 동기화를 하게 되면 속도 이슈가 발생할 수 있다!

더 효율적으로 멀티스레딩 문제 해결하기

  1. getInstance()의 속도가 그리 중요하지 않다면 동기화로 해결한다.
  2. 인스턴스가 필요할 때는 생성하지 말고 처음부터 만든다.
    • 싱글턴 인스턴스를 생성하고 계속 사용하거나, 인스턴스 실행 중에 수시로 만들고 관리하기가 성가시다면 처음부터 인스턴스를 만든다. ```java public class Singleton { // 정적 초기화 부분에서 싱글턴 인스턴스를 생성한다. 이러면 스레드를 써도 문제 없을 수 있다. private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() { // 인스턴스는 이미 초기화되었으니 반환만 해준다. return uniqueInstance; } } ```

이 방법을 사용하면 클래스가 로딩될 때 JVM에서 싱글턴의 하나뿐인 인스턴스를 생성해준다. JVM에서 하나뿐인 인스턴스를 생성하기 전까지 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.

  1. DCL을 써서 getInstance()에서 동기화되는 부분을 줄인다.
    • DCL (Double-Checked Locking)을 사용하면 인스턴스가 생성되어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화할 수 있다.
    • 이러면 처음에만 동기화하고 나중에는 동기화하지 않아도 된다.
      • DCL은 자바 1.4 이전 버전에서는 사용할 수 없다. ```java public class Singleton { // volatile을 사용하면 멀티스레딩을 사용하더라도 uniqueInstance 변수가 Singleton 인스턴스로 초기화가 잘 된다. private volatile static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() { if (uniqueInstance == null) { // 인스턴스가 없으면 동기화 블록 진행 // 처음에만 동기화하도록 구현 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } } } ```

초콜릿 보일러 다시 작성해보기

  • DCL 방식을 사용하여 초콜릿 보일러를 다시 작성해본다.
    public class CholocateBoiler {
      private volatile static CholocateBoiler uniqueBoiler;
      private boolean empty;
      private boolean boiled;
        
      private CholocateBoiler() {
          empty = true;
          boiled = false;
      }
        
      public static CholocateBoiler getInstance() {
          if (uniqueBoiler == null) {
              synchronized (CholocateBoiler.class) {
                  if (uniqueBoiler == null) {
                      uniqueBoiler = new CholocateBoiler();
                  }
              }
          }
      }
        
      public void fill() {
          if (uniqueBoiler.isEmpty()) {
              uniqueBoiler.empty = false;
              uniqueBoiler.boiled = false;
              // 재료를 넣는 로직
          }
      }
        
      public void drain() {
          if (!uniqueBoiler.isEmpty() && uniqueBoiler.isBoiled()) {
              // 끓인 재료를 다음 단계로 넘김
              uniqueBoiler.empty = true;
          }
      }
        
      public void boil() {
          if (!uniqueBoiler.isEmpty() && !uniqueBoiler.isBoiled()) {
              // 재료를 끓임
              uniqueBoiler.boiled = true;
          }
      }
        
      public boolean isEmpty() {
          return uniqueBoiler.empty;
      }
        
      public boolean isBoiled() {
          return uniqueBoiler.boiled;
      }
    }
    

Enum으로 싱글턴 구현하기

  • Enum으로 싱글턴을 생성하면 동기화 문제, 클래스 로딩 문제, 리플렉션, 직렬화와 역직렬화 문제를 해결할 수 있다. ```java public enum Singleton { UNIQUE_INSTANCE; // 기타 필요한 필드 }

public class SingletonClient { public static void main(String args[]) { Singleton singleton = Singleton.UNIQUE_INSTANCE; // 싱글턴 사용 } } ```

  • 앞에서 getInstance() 메서드를 구현했던 부분은 싱글턴 작동 원리를 알아보기 위해 따라가본 것으로 생각하자.
    • 싱글턴이 필요할 때면 Enum을 사용해 구현하자.