본문 바로가기

Fundamental/Design Pattern

[디자인 패턴] 5. 싱글턴 패턴(Singleton Pattern)

 

 

싱글턴 패턴(Singleton Pattern)
클래스의 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공합니다. 코드 전역적으로 클래스의 상태가 동기화되어야 할 때 유용합니다.

 


🍫  초콜릿 공장, 초코홀릭

 

 

 

요즘 대다수의 초콜릿 공장에서는 초콜릿을 끓이는 장치를 컴퓨터로 제어합니다. 아래는 주식회사 초코홀릭의 초콜릿 보일러를 제어하는 클래스입니다.

 

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 boil() {
        if (!isEmpty() && !isBoiled()) {
            boiled = true;
        }
    }

    // 끓여진 초콜릿을 추출하기
    public void drain() {
        if (!isEmpty() && isBoiled()) {
            // 끓인 재료를 다음 단계로 넘김
            empty = true;
        }
    }

    public boolean isEmpty() {
        return empty;
    }

    public boolean isBoiled() {
        return boiled;
    }
}

 

 

위 클래스를 보면, 초콜릿 보일러에 초콜릿이 채워져 있는지와 끓여졌는지 등 상태에 따라서 동작하도록 하는 세심한 주의가 기울여져 있는 걸 확인할 수 있습니다.

그런데 하나의 초콜릿 보일러를 대상으로 두 개의 ChocolateBoiler 인스턴스를 만들고 각자 따로 제어하면 큰일이 날 수도 있겠네요..

 

이러한 문제를 해결하기 위해서 인스턴스는 하나로만 생성되고 제어될 수 있도록 도와줘봅시다!

 

public class ChocolateBoiler {
    // static 메소드인 getInstance()에서 객체를 만들어주려면 문맥 상 static으로 선언해야 합니다.
    private static ChocolateBoiler uniqueInstance;
    
    ...

    // 생성자가 private이므로 클래스 내부에서만 인스턴스를 만들 수 있습니다.
    private ChocolateBoiler() {
        empty = true;
        boiled = false;
    }

    // 클래스 외부에선 이 메소드를 사용해서 인스턴스를 얻을 수 있습니다.
    public static ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }
	
    ...
}

 

이제 ChocolateBoiler 인스턴스를 하나만 가지게 되므로, 클래스 외부에서 여러 인스턴스를 생성하게 되는 일이 없게 되었습니다.

이렇게 코드 전역적으로 하나의 인스턴스만 만들어지도록 하는 패턴을 싱글턴 패턴(Singleton Pattern)이라고 합니다.

 

🔥 그런데 며칠 후 초콜릿 보일러에서 또 제어가 엉뚱하게 되는 문제가 생겼습니다!

초코홀릭 관계자에게 물어보니 평소엔 의도한 대로 완벽하게 동작했지만, 성능을 높이기 위해 멀티 스레드를 적용한 이후로 다시 문제가 발생했다고 하네요.

 

멀티 스레드 환경에서 아래 코드가 여러번 실행됐다고 생각해봅시다.

 

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

 

 

스레드 간 동시성 문제 때문에 인스턴스가 두 개 생성이 되어 버렸었네요..

이러한 동시성 문제를 해결하기 위한 방법은 3가지입니다.

 

1. syncronized 키워드 사용

public class ChocolateBoiler {
   ...

    // 이제 한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야 합니다.
    public static synchronized ChocolateBoiler getInstance() {
       ...
    }
	
    ...
}

 

가장 간편한 방법이지만, 스레드 간 동기화를 위한 처리 때문에 속도가 약 100배 정도 저하됩니다. 인스턴스를 생성하는 속도가 중요하지 않은 프로그램의 경우엔 이 방법을 사용해도 무관합니다.

 

 

2. 처음부터 인스턴스를 만들어버리기

public class ChocolateBoiler {
    // 클래스를 로드할 때 그냥 JVM이 만들게 해버리죠!
    private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();
    
    ...

    public static ChocolateBoiler getInstance() {
       return uniqueInstance;
    }
	
    ...
}

 

 

 

3. DCL(Double-Checked Locking) 사용

public class ChocolateBoiler {
    ...

    public static ChocolateBoiler getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singletone.class) {
                if (uniqueInstance == null) {
                    chocolateBoiler = new ChocolateBoiler();
                }
            }    
        }
        return chocolateBoiler;
    }
}

 

syncrhronized 블록을 사용하면 처음에만 동기화하고 이후엔 동기화하지 않아도 됩니다. 이 방법이 가장 원하던 방법이긴 하네요.

 

하지만 위와 같은 방법들을 사용하더라도 동기화, 클래스 로딩, 리플렉션, 직렬화, 역직렬화와 같은 동작을 수행할 때 발생되는 문제들에 대해서 계속 고민하고 관리해야 합니다.

 

이러한 문제들을 한 번에 해결해주는 방법이 바로 enum을 사용하는 방법입니다. ☀️

public enum ChocolateBoiler {
    UNIQUE_INSTANCE;
    
    private boolean empty;
    private boolean boiled;

    public void fill() { ... }
    public void boil() { ... }
    public void drain() { ... }
    public boolean isEmpty() { ... }
    public boolean isBoiled() { ... }
}

public class Main {
    public static void main(String[] args) {
        ChocolateBoiler chocolateBoiler = ChocolateBoiler.UNIQUE_INSTANCE;
        chocolateBoiler.fill();
        chocolateBoiler.boil();
        chocolateBoiler.drain();
    }
}

 

위와 같이 enum으로 싱글턴을 만들면 인스턴스 생성과 관련된 문제가 전부 사라지게 되므로, 다른 방법들을 사용할 필요가 없겠네요.

 

그러므로 싱글턴을 만들 땐 아묻따 enum을 사용하면 됩니다 👌

 


 

싱글턴 패턴(Singleton Pattern)
클래스의 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공합니다. 코드 전역적으로 클래스의 상태가 동기화되어야 할 때 유용합니다.