[Design Pattern] Singleton Pattern

younghyun·2022년 10월 25일
0

Design Pattern

목록 보기
4/14
post-thumbnail

Singleton Pattern이란

클래스가 한 개의 인스턴스만을 만들 수 있도록 하고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴이다.

고전적인 싱글턴

public class Singleton{

    // Singleton 클래스의 유일한 인스턴스를 저장 (private, 외부에서 사용 불가)
    // static 변수이므로 객체를 생성하지 않고도 사용 가능
    // private이므로 외부에서 사용 불가
    private static Singleton uniqueInstance;
    
    // 생성자를 private 형태로 만듦 (Singleton 클래스가 인스턴스화 되지못함)
    // 객체 생성을 할 수 없도록 디폴트 생성자 제거 (new Singleton() 불가능)
    private Singleton(){ }
    
    // static 메소드이므로 static 멤버 변수 접근 가능
    // 객체 생성 가능
    public static Singleton getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();  // 객체 생성
        }
        
        // 이미 객체를 생성한 경우
        return uniqueInstance;
    }
}
  • private 디폴트 생성자 구현
  • 싱글턴 인스턴스를 저장하는 정적 멤버 변수 생성
  • 싱글턴 인스턴스를 반환하는 정적 팩토리 메소드 구현
  • 생성자를 private로 함으로써 인스턴스 생성을 막고, 내부 메소드 getInstance를 통해 인스턴스를 생성할 수 있음

예시

초콜릿 공장

  • 기능: 초콜릿과 우유를 받아서 끓이고 초코바를 만드는 단계로 넘겨줌
    • 보일러가 빈 상태: 보일러에 우유/초콜릿 혼합 재료 넣음 fill()
    • 보일러가 차있고, 다 끓여진 상태: 보일러에 있는 재료를 다음 단계로 넘기고 보일러를 비움 drain()
    • 보일러가 차있고, 끓지 않은 상태: 끓임 boil()

Bad Case

public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;
 
    public 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;
    }
 
}

Better Case (고전적 싱글턴 패턴 활용)

: ChocolateBoiler 인스턴스를 하나로 유지하도록 변경

public class ChocolateBoiler {

    private boolean empty;
    private boolean boiled;
    
    // ChocolateBoiler static 객체 생성
    private static ChocolateBoiler uniqueInstance;
    
    // public --> private
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
    }
    
    // 인스턴스 생성을 위한 static 메소드 정의
    public static ChocolateBoiler getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }    
 
 
    // 채우기
    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;
    }
 
 
}
  • 문제점: 여러 개의 스레드에서 밑의 작성한 코드를 실행하여, 앞에서 작성한 코드가 여러 개의 스레드에서 동시에 실행되면 문제가 발생
    "항상 인스턴스가 1개만 생성됨"이 보장되지 않음
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
 
boiler.fill();
boiler.boil();
boiler.drain();

Good Case (Thread-safe 버전의 싱글턴)

1. sysnchronized

: 여러 쓰레드에서 사용하려고 할 때 locking 메커니즘을 제공해서 한 번에 한 개 쓰레드만 사용할 수 있도록 함

  • 단점: 불필요한 오버헤드를 발생시켜 속도 저하 발생 가능성
    (해당 메소드가 속한 클래스에 lock이 걸려서 getInstance메소드를 수행하는 동안에는 다른 메소드에 접근할 수 없음)
public class Singleton {

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

2. 인스턴스를 시작하자마자 만드는 방법

  • 장점: 불필요한 오버헤드를 발생시키지 않아 속도 저하 문제가 없음
  • 단점: 인스턴스를 사용하는 시간보다 사용하지 않는 시간이 훨씬 많다면 메모리 낭비
public class Singleton {

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

3. DCL(Double-Checking Locking)

  • volatile
    • 변수를 CPU cache에 저장하지 않고 메모리에서 읽고 저장
    • 쓰레드(thread)를 사용할 때 다른 프로세서에 있는 cache에 변수값이 저장되어 서로 다른 값을 사용하는 것을 방지
    • 언어, 하드웨어에 따라 작동하지 않을 수도 있어 현재는 권장하지 않음
public class Singleton {
    private volatile static Singleton inst;
    private Singleton() { }
    public static Singleton getInstance() {
        if (inst == null) {
            synchronized (Singleton.class) {
                if (inst == null) {
                    inst = new Singleton();
                }
        }
    }
    return inst;
    }
}

4. 내부 정적 클래스 사용

  • JVM에서는 해당 싱글턴 클래스를 메모리에 load할 때, 정적 멤버 변수가 없으므로 싱글턴 인스턴스를 생성하지 않음
  • getInstance()를 호출하면 내부에 있는 정적 클래스의 인스턴스를 생성해서 반환
public class Singleton {

    // inner static class
    private static class InnerSingleton {
        static final Singleton inst = new Singleton();
    }
    private Singleton() { }
    public static Singleton getInstance() {
        return InnerSingleton.inst;
    }
}
profile
🌱 주니어 백엔드 개발자입니당

0개의 댓글