[Design Pattern] Singleton Pattern이란?

Junseong·2021년 5월 15일
0
post-thumbnail

목차

  1. 등장 배경
  2. 패턴 설명
  3. 패턴 정의 및 정리

본 시리즈는 'Head First Design Patterns' 책을 통해 공부한 내용을 참고 및 각색하여 작성되어졌습니다. 전체 코드는 Github 에서 다운 받을 수 있습니다.


등장 배경


개발을 하다보면 어떤 객체는 유일무이해야 되는 경우가 있습니다.

스레드 풀이라든가 로그 기록용 객체가 그 예이며 생각보다도 훨씬 많습니다.

구체적인 예시를 통해 자세히 알아보도록 하겠습니다.


여기 갓 창업한 초콜릿 공장이 있으며, 초콜릿을 만드는 프로세스는 다음과 같습니다.

  1. 보일러에 초콜릿을 채운다.
  2. 보일러에 초콜릿이 가득 차면 초콜릿을 끓인다.
  3. 다 끓었으면 초콜릿을 틀에 맞게 굳히는 기계로 보내 초콜릿을 만든다.

이 프로세스를 수행하는 초콜릿 보일러 객체는 다음과 같이 구현할 수 있을 것입니다.

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

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

    // 1. 보일러에 초콜릿을 채운다.
    public void fill() {
        if(empty) {
            empty = false;
            boiled = false;
            // 초콜릿을 채우는 로직
        }
    }

    // 2. 보일러에 초콜릿이 가득 하면 초콜릿을 끓인다.
    public void boil() {
        if(!empty && !boiled) {
            // 초콜릿을 끓이는 로직
            boiled = true;
        }
    }

    // 3. 다 끓었으면 초콜릿을 틀에 맞게 굳히는 기계로 보낸다.
    public void drain() {
        if(!empty && boiled) {
            empty = true;
            // 끓인 초콜릿을 배출하는 로직
        }
    }
}

코드를 보면 보일러가 넘치거나 끓이지 않은 초콜릿을 배출하지 않도록 주의를 기울였다는 것을 알 수 있을 것입니다.

그런데 이 공장에는 초콜릿 보일러가 하나밖에 없고 앞으로도 개수를 늘릴 생각이 없다고 합니다. 이럴 경우 위의 구현 방법에는 어떤 문제가 있을까요?

초콜릿 보일러 클래스의 생성자를 보면 public으로 선언되어 있는 것을 볼 수 있습니다.

이 말은 즉 언제 어디서든지 초콜릿 보일러 인스턴스를 새로 생성할 수 있다는 뜻입니다.

하나만 있어야되는 초콜릿 보일러 객체가 여러개가 생긴다면 실제 초콜릿 보일러 기계가 넘치거나 끓이지 않은 초콜릿을 배출하는 등의 문제가 생길것입니다.


싱글턴 패턴은 이런 상황에서 객체의 유일무이함을 보장하면서 어디서든지 그 객체에 접근할 수 있게 하기 위해 등장하였습니다.


패턴 설명


위의 예제에서 초콜릿 보일러 클래스의 생성자가 public으로 선언되어 있던것이 문제였음을 확인했습니다.

우리는 최초 한번만 인스턴스를 생성하고 그 이후로는 생성된 인스턴스를 클라이언트에게 제공만하면 됩니다.

싱글턴 패턴 등장 초기에는 이를 다음과 같이 구현하였었습니다.


고전적인 싱글턴 패턴

/**
 * 고전적인 싱글턴 패턴
 * 게으른 인스턴스 생성이 가능하다.
 * Thread-safe 하지 않다.
 */
public class ClassicSingleton {
    private static ClassicSingleton uniqueInstance;

    private ClassicSingleton() {}

    // 정적 메소드 또는 클래스 메소드라고 부른다.
    public static ClassicSingleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new ClassicSingleton();
        }
        return uniqueInstance;
    }
}

유일무이해야하는 클래스의 생성자를 private로 선언하여 클래스 외부에서 인스턴스를 생성하는 것을 막습니다.

이렇게 하면 클래스 외부에서 새로운 인스턴스를 만들 수 없으면서 어디서든지 getInstance() 클래스 메소드로 객체에 접근할 수 있습니다.

또한 게으른 인스턴스 생성(lazy instantiation)이 가능하다는 장점이 있습니다.

게으른 인스턴스 생성은 인스턴스가 필요한 시점에 인스턴스를 생성시킵니다.

인스턴스를 미리 만들어 놓으면 인스턴스를 필요로 하지 않은 시점에도 자원을 불필요하게 소모하기 때문에 게으른 인스턴스 생성은 자원을 더 아끼는 개발 방법 중 하나입니다.


그러나 이 구현법에는 치명적인 단점이 존재합니다. 바로 Thread-safe 하지 않다는 것입니다.

ClassicSingleton classicSingleton = ClassicSingleton.getInstance();

위의 코드를 두개의 쓰레드 ThreadA, ThreadB가 수행할때를 생각해 봅시다.

아직 인스턴스가 생성된 적이 없어 uniqueInstancenull 입니다.

ThreadA는 uniqueInstance가 null이기 때문에 if문 안의 내용을 실행하려합니다. 그런데 이때 Switch Context가 발생하여 ThreadB가 실행됩니다.

이렇게 되면 아직 ThreadA에 의해 인스턴스가 생성되지 않았기 때문에 ThreadB도 uniqueInstance를 null로 인식하게 되어 if문 안의 내용을 실행하게 됩니다.

결국 ThreadA, ThreadB 둘이 각각 인스턴스를 새로 만드는 결과를 초래하게 됩니다.

아래는 Thread-safe 하면서 객체의 유일무이함을 보장하기 위해 등장한 구현법들 입니다.


인스턴스를 시작하자마자 만드는 싱글턴 패턴

/**
 * 인스턴스를 시작하자마자 만드는 싱글턴 패턴
 * 게으른 인스턴스 생성이 불가능하다.
 * Thread-safe 하다.
 */
public class StaticSingleton {
    private static StaticSingleton uniqueInstance = new StaticSingleton();

    private StaticSingleton() {}

    // 정적 메소드 또는 클래스 메소드라고 부른다.
    public static StaticSingleton getInstance() {

        return uniqueInstance;
    }
}

인스턴스의 크기가 크지 않다면 인스턴스를 시작하자마자 만들면 됩니다. 비록 인스턴스가 필요하지 않을때에도 자원을 소모하고 있다는 단점이 있지만 Thread-safe 하다는 장점이 있습니다.


synchronized 키워드를 이용한 싱글턴 패턴

/**
 * synchronized를 이용한 싱글턴 패턴
 * 게으른 인스턴스 생성이 가능하다.
 * Thread-safe 하다.
 * 불필요한 오버헤드가 증가되어 속도에 문제가 생길 수 있다.
 */
public class SynchronizedSingleton {
    private static SynchronizedSingleton uniqueInstance;

    private SynchronizedSingleton() {}

    // 함수에 사용된 synchronized는 객체(this)에 lock을 걸어 다른 Thread들의 접근을 block한다.
    public static synchronized SynchronizedSingleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new SynchronizedSingleton();
        }
        return uniqueInstance;
    }
}

자바에서 synchronized 키워드는 한번에 두개 이상의 스레드가 같은 부분을 수행하는 것을 막아줍니다.

getInstance() 메소드를 synchronized 키워드로 동기화 시키면 getInstance() 메소드를 수행중인 스레드가 작업을 마칠때까지는 다른 스레드는 이 객체에 접근을 할 수 없기 때문에 Thread-safe 하게 됩니다.

또한 이 구현법은 게으른 인스턴스 생성이 가능합니다. 그러나 다른 쓰레드의 접근을 block하는 방식이므로 속도 문제가 생길 수 있다는 단점이 있습니다.

수행할 코드의 길이가 짧거나 속도가 크게 중요하지 않다면 사용해도 되는 구현법입니다.


DCL을 이용한 싱글턴 패턴

/**
 * DCL(Double-Checking Locking)을 이용한 싱글턴 패턴
 * 자바 1.4 이후 버전부터 사용이 가능하다.
 * 게으른 인스턴스 생성이 가능하다.
 * Thread-safe 하다.
 * 불필요한 오버헤드가 없어 속도에 문제를 주지 않는다.
 * 언어, 하드웨어에 따라 작동하지 않을 수도 있어 현재는 권장하지 않는다.
 */
public class DCLSingleton {
    private volatile static DCLSingleton uniqueInstance;

    private DCLSingleton() {}

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

DCL을 쓰는 방법은 사실 현재는 권장하지 않고 있는 안티 패턴입니다. 언어와 하드웨어에 따라 작동하지 않을 수도 있다고 합니다. 이에 대한 설명은 링크에 나와있으니 궁금하시다면 읽어보시길 추천드립니다.


패턴 정의 및 정리


패턴 정의

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


패턴 정리

Thread-safe 하게 싱글턴 패턴을 구현하는 방법으로는 크게 3가지의 방법이 있습니다.

  1. 인스턴스를 시작하자마자 만드는 방법
  2. synchronized 키워드를 사용하여 getInstance() 메소드를 동기화하는 방법
  3. DCL을 쓰는 방법

인스턴스를 시작하자마자 만드는 방법은 불필요한 오버헤드를 발생시키지 않아 속도 저하 문제가 없다는 장점이 있습니다. 그러나 인스턴스를 사용하는 시간보다 사용하지 않는 시간이 훨씬 많다면 괜히 메모리만 낭비한다는 단점이 있습니다.

synchronized 키워드를 사용하는 방법은 게으른 인스턴스 생성이 가능하다는 장점이 있습니다. 그러나 불필요한 오버헤드를 발생시켜 속도 저하를 일으킬 수 있다는 단점이 있습니다. 만약 속도 문제가 그리 중요하지 않다면 충분히 사용가능한 방법이기도 합니다.

DCL을 쓰는 방법은 사실 현재는 권장하지 않고 있는 안티 패턴입니다. 언어와 하드웨어에 따라 작동하지 않을 수도 있다고 합니다. 이에 대한 설명은 링크에 나와있으니 궁금하시다면 읽어보시길 추천드립니다.

싱글턴 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 개방-폐쇄 원칙을 위배하게 됩니다. 따라서 꼭 필요한 경우에만 적절히 사용할 필요가 있습니다.

profile
#취준생 #Back-end

0개의 댓글