2 분 소요

Singleton pattern

목적

  • 클래스의 인스턴스화를 제한하여 JVM에 클래스의 인스턴스가 하나만 존재하도록 보장
    • 이유 : 일부 공유 자원(ex. DB나 file 등 리소스를 많이 차지하는 무거운 클래스)에 대한 접근 제어, 메모리 절약
  • 클래스의 인스턴스를 가져오기 위한 글로벌 액세스 지점을 제공
  • logging, drivers 객체, caching, thread pool에 사용됨
  • java.lang.Runtime, java.awt.Desktop에서도 사용됨

예를들어, 게임이나 IDE의 설정화면

구현 방법

총 8가지 구현 방법이 존재하며 각각이 장단점을 가진다.

  • Lazy initialization
  • Eager initialization
  • Static block initialization
  • Thread safe
  • Double-Checked Locking
  • Bill Pugh Solution (LazyHolder)
  • Enum

Lazy initialization

public class Settings {
    private static Settings instance;

    private Settings() {}

    public static Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }

        return instance;
    }
}
  • 가장 naive한 방식
  • 객체 생성에 대한 관리를 내부적으로 처리
  • Eager initialization 방식의 고정 메모리 차지 문제점 해결
  • Thread-safe하지 않음

Eager initialization

public class Settings {
    private static final Settings INSTANCE = new Settings();

    private Settings() {}

    public static Settings getInstance() {
        return INSTANCE;
    }
}
  • 클래스 로딩 시 인스턴스 생성
  • Thread-safe
  • 인스턴스가 크지 않은 경우 고려할 수 있는 방식
  • 해당 인스턴스를 사용하지 않는 경우에도 인스턴스가 생성되는 단점
  • 예외 처리할 수 없음

Static block initialization

public class Settings {
    private static Settings instance;
    
    private Settings() {}
    
    static {
        try {
            instance = new Settings();
        } catch (Exception e) {
            throw new RuntimeException("Exception occurred in creating singleton instance");
        }
    }
    
    public static Settings getInstance() {
        return instance;
    }
}
  • Eager initialization와 같이 클래스 로딩 시 인스턴스 생성
  • 예외 처리 가능함
  • 해당 인스턴스를 사용하지 않는 경우에도 인스턴스가 생성되는 단점

Thread safe

public class Settings {
    private static Settings instance;

    private Settings() {}

    public static synchronized Settings getInstance() {
        if (instance == null) {
            instance = new Settings();
        }

        return instance;
    }
}

  • synchronized 키워드를 사용하여 lock을 걸어 race condition(경쟁상태) 예방
  • 메서드 전체에 synchronized가 걸려 overhead가 발생해 성능 저하 발생

Double-Checked Locking

public class Settings {
    private static volatile Settings instance;

    private Settings() {}

    public static Settings getInstance() {
        if (instance == null) {
            synchronized (Settings.class) {
                if (instance == null) {
                    instance = new Settings();
                }
            }
        }

        return instance;
    }
}
  • 메서드 전체에 synchronized가 걸려 overhead가 발생해 성능 저하 방지하기 위한 기법
  • volatile 키워드를 멤버 변수에 붙여야 함. JDK 1.5 이상과 JVM에 대한 이해와 JVM에 따라 Thread-safe 하지 않아 지양

Bill Pugh Solution (LazyHolder)

public class Settings implements Serializable {

    private Settings() {}

    private static class SettingsHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance() {
        return SettingsHolder.INSTANCE;
    }

    /**
     * 역직렬화 대응 방안
     * @return
     */
    protected Object readResolve() {
        return getInstance();
    }
}
  • 권장되는 방법
  • Thread-safe하며, lazy loading 가능
  • 클래스 안에 내부 클래스(Holder)를 두어 JVM의 클래스 로더 방식과 클래스 로드되는 시점을 이용
  • 다만, singleton 파괴 기법(?) 뚫을 수 있음(Reflection, 직렬화/역직렬화 - 위 대응이 있기는 하지만)

Enum

public enum Settings {
    INSTANCE;
}
  • 권장되는 방법
  • enum은 개념 자체가 private 생성자이며 한번만 초기화하여 Thread-safe
  • Reflection, 직렬화/역직렬화 공격에도 안전
  • 상속이 불가능

최종 정리하자면, 싱글톤 패턴 클래스를 만들기 위해서는 Bill Pugh Solution 기법을 사용하거나 Enum으로 만들어 사용하면 된다. 다만, 이 둘의 사용 선택은 자신의 싱글톤 클래스의 목적에 따라 갈리게 된다고 보면 된다.

단점

  1. 모듈간 의존성 증가
  2. SOLID 원칙 위배 가능성 높음 (SRP, OCP, DIP)
  3. TDD 단위 테스트 어려움

댓글남기기