[CS] 싱글톤 패턴

장다슬·2024년 4월 17일

CS 스터디

목록 보기
8/23

Singleton pattern
하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴

생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다.

즉, 인스턴스가 필요할 때 똑같은 인스턴스를 새로 생성하지 않고 기존의 인스턴스를 활용하는 기법이다.


장단점

장점 : 인스턴스 생성 시 비용이 줄어듦
단점 : 의존성이 높아짐


구현 기법 종류

싱글톤 패턴을 구현하는 기법에는 총 7가지가 있으며, 각 기법마다 장단점이 존재해 순서대로 조금씩 단점을 보완하는 식으로 발전해왔다.

1. Eager Initialization

  • 미리 한 번만 만들어 두는 방식으로 가장 직관적이고 간단한 기법
  • static final이라 멀티 쓰레드 환경에서도 안전
  • static 멤버는 객체를 사용하지 않더라도 메모리에 적재되기 때문에 리소스가 큰 객체일 경우 공간의 자원 낭비가 발생
  • 예외 처리 불가능
class Singleton {
	// 싱글톤 클래스 객체를 담을 인스턴스 변수
	private static final Singleton INSTANCE = new Singleton();
    
    // 생성자를 private으로 사용하여 외부에서 new 사용을 막음
    private Singleton() {}
    
    public static Singleton getInstance() {
    	return INSTANCE;
    }
}

2. Static Block Initialization

  • static block을 이용해 예외 처리 가능
    • static block? : 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행되는 블록
  • 여전히 static의 특성인 공간 낭비가 있음
class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;

    // 생성자를 private으로 사용하여 외부에서 new 사용을 막음
    private Singleton() {}
    
    // static 블록을 이용해 예외 처리
    static {
        try {
            instance = new Singleton();
        } catch (Exception e) {
            throw new RuntimeException("싱글톤 객체 생성 오류");
        }
    }

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

3. Lazy Initialization

  • 객체 생성에 대한 관리를 내부적으로 처리
  • 메서드를 호출했을 때 인스턴스 변수의 null 유무에 따라 초기화 또는 기존 인스턴스 반환
  • 미사용 객체의 메모리 공간 낭비의 단점을 극복
  • 그러나 쓰레드 세이프(Thread Sage) 하지 않는 치명적인 단점 존재
class Singleton {
    // 싱글톤 클래스 객체를 담을 인스턴스 변수
    private static Singleton instance;

    // 생성자를 private으로 사용하여 외부에서 new 사용을 막음
    private Singleton() {}
	
    // 외부에서 정적 메서드 호출 시 초기화 진행 (lazy)
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton(); // 오직 1개의 객체만 생성
        }
        
        return instance;
    }
}

4. Thread Safe Initialization

  • synchronized 키워드를 통해 메서드에 쓰레드들을 하나씩 접근하게 하도록 설정 (동기화)
  • 그러나 여러개의 모듈들이 매번 객체를 가져올 때마다 synchronized 메서드를 호출하여 동기화 처리 작업에 overhead가 발생, 성능이 하락한다.
class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // synchronized 메서드
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

5. Double-Checked Locking

  • 최초 초기화 할 때만 synchronized를 적용하고 이미 만들어진 인스턴스를 반환할 때에는 사용하지 않도록 하는 기법
  • 이때 인스턴스 필드에 volatile 키워드를 붙여주어야 I/O 불일치 문제를 해결 할 수 있다.
  • 그러나 volatile 키워드를 이용하려면 JVM 1.5 이상이어야 하며, JVM에 대한 심층적 이해를 요구하고, JVM에 따라서 여전히 쓰레드 세이프 하지 않는 경우가 발생하기 때문에 사용을 지양하는 편
class Singleton {
    private static volatile Singleton instance; // volatile 키워드 적용

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
        	// 메서드에 동기화를 적용하지 않고, Singleton 클래스에 동기화 적용
            synchronized (Singleton.class) { 
                if(instance == null) { 
                    instance = new Singleton(); // 최초 초기화 할 때만 동기화 작업이 발생, 리소스 낭비를 최소화
                }
            }
        }
        return instance; // 최초 초기화가 완료되면 앞으로 생성된 인스턴스만 반환
    }
}

6. Bill Pugh Solution (Lazy Holder)

  • 권장되는 두 가지 방법 중 하나
  • 멀티쓰레드 환경에서도 안전하며 Lazy Loading도 가능한 완벽한 싱글톤 기법
  • 클래스 안에 내부 클래스(holder)를 두어 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용
  • static 메소드에서는 static 멤버만 호출 가능한 점과, 내부 클래스의 메모리 누수라는 치명적인 문제점 해결을 위해 내부 클래스를 static으로 설정
  • 그러나 클라이언트가 Reflection API, 직렬화/역직렬화를 통해 임의로 싱글톤을 파괴할 수 있다는 단점이 존재
class Singleton {

    private Singleton() {}

    // static 내부 클래스를 이용
    // holder로 만들어, 클래스가 메모리에 로드되지 않고 getInstance 메서드가 호출되어야 로드됨
    private static class SingleInstanceHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }
}

7. Enum 이용

  • 권장되는 두 가지 방법 중 하나
  • enum은 멤버 생성 시 private로 만들고 한 번만 초기화 하기 때문에 쓰레드 세이프 하다.
  • enum 내에서 상수 뿐 아니라 변수나 메서드를 선언해 사용 가능하기 때문에 싱글톤 클래스처럼 응용 가능
  • 위의 Bill Pugh Solution 기법과 달리, 클라이언트에서 Reflection을 통한 공격에도 안전
  • 그러나 개발 스펙에 따라 싱글톤 클래스를 멀티톤(일반 클래스)으로 마이그레이션 해야할 때 처음부터 코드를 다시 작성해야 하는 단점 존재
  • 클래스 상속이 필요할 때 enum 외의 클래스 상속이 불가능한 단점 존재
enum SingletonEnum {
    INSTANCE;

    private final Client dbClient;
	
    SingletonEnum() {
        dbClient = Database.getClient();
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }

    public Client getClient() {
        return dbClient;
    }
}

public class Main {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.getInstance();
        singleton.getClient();
    }
}

따라서, 싱글톤 패턴 클래스를 만들고자 한다면 Bill Pugh Solution 기법 혹은 Enum 이용을 권장한다.

성능이 중요시 되는 환경이라면 Bill Pugh Solution 기법을, 직렬화 및 안정성이 중요시 되는 환경이라면 Enum을 사용하자!


참고:
싱글톤(Singleton) 패턴 - 꼼꼼하게 알아보자

profile
반갑습니다

0개의 댓글