BackEnd/Design Patterens

[Design Patterns] Singleton Pattern : 싱글턴 패턴

샤아이인 2022. 1. 13.

Head First Design Patterns 책을 읽으며 정리한 내용입니다. 문제가 될 시 글을 내리도록 하겠습니다!

Singleton Pattern 이란?

싱글턴 패턴은 간단히 말하면 특정 클래스에 대해서 객체 인스턴스를 하나만 만드는 패턴입니다.

어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴입니다.

 

또한 이 패턴을 이용하면 필요할때만 객체를 만들어 사용하기 때문에 자원낭비를 막을 수 있습니다.

 

우선 간단한 구조를 살펴봅시다!

이 말이 무슨 의미인지 다음 단락부터 예시를 통하여 설명해 보겠습니다!


패턴 소개

우선 고전적인 싱글턴 패턴은 다음과 같습니다.

public class Singleton {
    private static Singleton uniqueInstance; // Singleton class의 유일한 인스턴스를 저정하기 위한 참조변수

    private  Singleton() {} // 생성자가 private!

    public static Singleton getInstance(){ // 이 메소드를 통하여 인스턴스 얻을수 있음
        if(uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
 

위의 코드에서 uniqueInstance 변수는 static 변수입니다. 따라서 하나의 인스턴스 만을 저장하게 됩니다.

기존에 인스턴스를 생성하던 방식인 Singleton temp = new Singleton(); 같은 방식으로 생성자를 호출할 수 없게 되었습니다.

생성자가 private이기 때문이죠!

 

대신 Singleton.getInstance() 와 같이 메소드를 호출하여 인스턴스를 반환받을 수 있게되었습니다!

 

이처럼 싱글턴 패턴의 개념을 간단합니다. 지금부터는 예시를 생각하면서 문제점을 고쳐봅시다!

 

싱글턴 패턴을 적용한 초콜릿공장이 있다고 생각해 봅시다!

public class ChocolateBoiler{
    private boolean empty;
    private boolean boiled;

    private static ChocolateBoiler uniqInstance;

    private ChocolateBoiler() {
        empty = true;
        boiled = false;
    }

    public static ChocolateBoiler getInstance(){
        if(uniqInstance == null){
            uniqInstance = new ChocolateBoiler();
        }
        return uniqInstance;
    }

    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 isBoiled(){
        return boiled;
    }

    public boolean isEmpty() {
        return empty;
    }
}
 

위의 코드를 멀티스레드 환경에서 작동시키면 어떻게 될까요? 인스턴스는 1개만 생겨 정상작동 할까요?

(정상 작동 했다면 물어봤을리가 없겠죠?)

 

두개의 스레드 에서 다음 코드를 실행하고 있다고 가정해 봅시다!

ChocolateBoiler boiler = ChocolateBoiler.getInstance();
fill();
boil();
drain();
 

이 코드를 2개의 스레드에서 실행한다면 다음과 같은 문제가 발생하게 될 것 입니다.

각 스레드는 자신의 실행단위를 기억하면서 코드를 읽어갑니다.

따라서 thread1이 읽어가다 obj1을 반환한 후, thread2 에서 다시 자신의 실행단위를 읽어 새로운 인스턴스를 생성하여 반환하면 obj2가 반환되버립니다. 이는 하나의 객체만을 이용하려고 했던 원래의 의도와는 달라져 버린 것 입니다.

 

이를 해결하는 가장 간단한 방법을 synchronized를 사용한 동기화 입니다.

이처럼 동기화가 되어있으면 thread1 이 한 메소드의 코드를 다 읽고 수행하기 전까지 다른 thread2에서 이 메소드의 코드에 접근할수가 없게됩니다!

 

하지만 이러한 동기화는 overhead가 있어 성능에 영향을 주죠 ㅠ.ㅠ. 간단한 어플리케이션 이라면 그냥 둬도 되지만, 많은 자원을 사용한다면 성능 하락에 심한 영향을 미치게 될 것 입니다.

 

이러한 overhead를 개선한 효율적인 방법이 없는 것 일까요??

 

DCL (Double-Checking Locking)을 써서 getInstance()에서 동기화 되는 부분을 줄이면 됩니다!

 

인스턴스가 생성되어 있는지 확인한 후, 생성되어 있지 않을때만 동기화를 수행하게 만들면 됩니다!

public class Singleton {
    private volatile static Singleton uniqueInstance; // volatile 사용!

    private  Singleton() {}

    public static Singleton getInstance(){
        if(uniqueInstance == null){ // 인스턴스가 있는지 확인한다!
            synchronized (Singleton.class){ // 없다면 동기화!
                if(uniqueInstance == null){
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

댓글