Don't give up!

[Design Pattern] 싱글톤 패턴(Singleton Pattern) 본문

개발서적/Head First Design Patterns

[Design Pattern] 싱글톤 패턴(Singleton Pattern)

Heang Lee 2021. 11. 2. 22:22
이 글은 에릭 프리먼의 'Head First Design Patterns'를 읽고 TIL(Today I Learned)로써 정리한 내용을 기록하는 글입니다.
자세한 내용은 책을 통해 확인하실 수 있습니다.

알라딘: Head First Design Patterns (aladin.co.kr)

 

Head First Design Patterns

볼 거리가 많고 재미있으면서도, 머리 속에 쏙쏙 들어오는 방식으로 구성된 Head First 시리즈. 패턴의 근간이 되는 객체지향 디자인 원칙, 중요한 패턴, 디자인 적용 방법, 쓰지 말아야 하는 이유

www.aladin.co.kr

싱글턴 패턴

싱글턴 패턴은 특정 클래스에 대해서 객체 인스턴스가 하나만 만들어질 수 있도록 해주는 패턴입니다.

싱글턴 패턴을 사용하면 전역 변수를 사용할 때와 마찬가지로 객체 인스턴스를 어디서든지 액세스할 수 있도록 할 수 있습니다.

스레드 풀, 캐시, 사용자 설정 등 인스턴스를 하나만 만들어야 하는 클래스에 사용합니다.

싱글턴 패턴 구현

싱글턴 패턴은 구현방법이 여러가지이며 각 방법마다 장/단점이 존재합니다.

 

Lazy Initialization

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

생성자를 private로 선언하여 Singleton에서만 클래스 인스턴스 생성을 할 수 있도록 제한합니다.

 

장점 : getInstance 함수에서 인스턴스 체크를 하기 때문에 첫 호출시에만 인스턴스가 생성됩니다.

단점 : Multi-Thread 환경에서 uniqueInstance의 인스턴스가 초기화되지 않았을 때 여러 스레드가 getInstance 메서드를 호출할 경우 인스턴스가 중복 생성될 수 있습니다.

Eager Initialization

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

클래스 로딩 단계에서 인스턴스 생성이 이루어지도록 하여 새로운 인스턴스 생성을 막습니다.

 

장점 : Lazy Initialization에서 발생하는 문제를 예방할 수 있습니다.

단점 : 해당 클래스를 사용하지 않아도 클래스 로딩 단계에서 인스턴스가 생성되어 메모리가 낭비됩니다.

Thread-Safe Lazy Initialization

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

synchronized 키워드를 사용하여 getInstance 메서드를 동기화하는 구현 방식입니다.

하나의 스레드가 메서드 사용을 끝내기 전까지 다른 스레드가 기다리도록 하여 두 개 이상의 스레드가 메소드를 동시에 실행시키는 일을 방지할 수 있습니다.

 

장점 : Multi-Thread에 의한 문제를 방지할 수 있습니다.

단점 : synchronzied 키워드가 적용된 함수는 객체에 lock을 적용합니다. 만약 객체에 synchronized 키워드가 적용된 함수가 두 개 이상일 경우 하나의 synchronzied 함수에서 발생한 lock으로 인해 다른 synchronized 함수에의 접근이 불가능한 케이스가 발생할 수 있습니다.

동기화를 시킴으로 인하여 성능이 크게 저하됩니다.

Double-Checking Locking Initialization

public class Singleton {
    private static volatile Singleton uniqueInstance;
    
    private Singleton() { }
    
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
            	if (uniqueInstance == null) {
            	    uniqueInstance = new Singleton();
                    
                    //<low-level에서의 객체 생성>
                    //some_space = allocate space for Singleton object
                    //uniqueInstance = some_space
                    //create real object in some_space
                }
            }
        }
        return uniqueInstance;
    } 
}

synchronized를 함수 내부의 블록에 적용하여 인스턴스가 초기화되어있지 않을 때에만 동기화를 하는 구현 방식입니다.

인스턴스 생성에 대한 if문이 동기화 블럭 내부와 외부에 모두 존재하기 때문에 Double-Checking Locking이라는 명칭이 붙었으며, 인스턴스 생성을 진행하는 도중 다른 스레드가 동기화 블럭 외부의 if문을 통과하고, 인스턴스를 호출할 경우를 방지하기 위해 volatile 키워드를 변수에 적용한다는 점이 특징입니다.

 

장점 : synchronized가 첫 if문을 통과한 이후에만 적용되기 때문에 동기화로 인한 성능 문제를 개선할 수 있습니다.

단점 : volatile는 자바 1.5 미만의 버전에서 변수 자체에만 업데이트가 이루어지도록 되어 있습니다.

'메모리 공간 할당 -> 변수에 메모리 주소 대입 -> 할당받은 메모리 공간에 실제 인스턴스 생성'의 3단계에서 자바 1.5 미만의 버전은 변수에 메모리 주소가 대입되있는 것만 확인하기 때문에 다른 스레드에서 생성되지 않은 인스턴스를 사용할 위험이 존재합니다.

Bill Pugh Solution

public class Singleton {    
    private Singleton() { }
    
    private static class SingletonHelper{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE;
    } 
}

 

Bill Pugh가 고안한 inner static class를 사용하는 방식입니다.

private inner static class는 Singleton 클래스의 로드 시기에 로드되지 않고, getInstance() 메서드가 호출될 때만 로드 및 인스턴스 생성이 이루어집니다.

 

장점 : 동기화에 의한 성능 저하를 해결할 수 있으며, Eager Initialization의 메모리 낭비도 피할 수 있습니다.

Enum Singleton

public enum SingletonEnum {
    INSTANCE;
    
    //실제 Singleton의 변수 및 메서드 정의
    ...
}

Enum Singleton은 Reflection에 의해 private 생성자, 메소드에 접근이 가능해지는 문제를 해결합니다.

 

장점 : Reflection과 Serialization에 의한 인스턴스 생성 문제를 해결합니다.

단점 : Eager Initialization과 마찬가지로 메모리 낭비의 문제를 가지고 있습니다.