싱글톤 패턴

강한친구·2022년 4월 7일
0

OOP Desing Pattern

목록 보기
8/15

유일무일한 객체를 만들기

싱글톤 패턴은 하나의 객체만 만들어서 이를 공유해서 쓰게 만드는 패턴이다.

객체를 계속 생성하는것은 불필요한 부담이 되기도하고, 하나만 관리하는것이 유리하기 때문이다.

고전적인 싱글톤 구현

고전적인 싱글톤에서는, private 생성자를 만들고, public static get_instance 메서드를 통해서만 객체를 받을 수 있도록 설계한다.

package singleton;

public class classicSingleton {
    private static classicSingleton uniqueInstance;

    private classicSingleton() {
    }

    public static classicSingleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new classicSingleton();
        }
        return uniqueInstance;
    }
}

초콜릿 공장

package singleton;

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;
    }
        
}

이러한 초콜릿 공장 코드가 있다고 하자. 각 상황별 조건에 맞게 함수들이 작동하도록 설정했다.

하지만 public 생성자로 인해 두개 이상의 객체가 만들어질 수 있고, 이는 좋지 못하다.

이를 싱글톤으로 변경하면 다음과 같다.

package singleton;

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

    private ChocolateBoiler() {
        empty = true;
        boiled = false;
    }
    
    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;
    }

}

고전적 싱글톤의 문제

하나의 객체를 쓰레드 두개가 동시에 접근한다고 하자. 싱글톤은 하나의 객체이기에, 이때 공유하는 empty, boiled값은 똑같다.

하지만 두개의 쓰레드가 이를 동시에 실행한다면 어떨까?

매번 그런건 아니겠지만 일정 확률로
uniqueInstance==null -> uniqueInstance==null -> new ChocolateBoiler1() --> new ChocolateBoiler2()

같은 경우가 생길 수 있다. 분명 싱글톤인데 두개가 생기는것이다.

해결책

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

이렇게 싱크로나이즈를 써서 동기화를 시키면 가능하다. 다만, 이러한 방식은 속도가 엄청 느리다.

  1. 만약 속도가 크게 안중요하다면 놔둬도 된다.
  2. 아예 처음부터 만들어둔다.

public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();

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

    public static ChocolateBoiler getInstance() {
        return uniqueInstance;
    }

이런식으로 만들어두면 된다.

  1. Double Checking Locking
    인스턴스가 생성되었을때만 동기화하는 방식이다.
package singleton;

public class ChocolateBoilerDCL {
    private boolean empty;
    private boolean boiled;
    private volatile static ChocolateBoilerDCL uniqueInstance;

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

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

public class ChocolateBoilerDCL {
    private boolean empty;
    private boolean boiled;
    private volatile static ChocolateBoilerDCL uniqueInstance;

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

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

정리

싱글톤은 많은 개발자들이 익숙한 방식이기도하고, 객체가 다중생성되어 리소스를 많이 사용하는일을 사전에 방지할 수 있다.

다만 private 생성자를 가져서 상속이 어렵고 (protected를 써야한다) 엄밀히 말하자면 SRP원칙을 위배하게 된다.

다만 장점이 뚜렷해서 사용되고 있으며, 특히 현대 웹 환경은 동시다발적 객체호출이 많기에 이러한 디자인이 권장된다.

이에 웹 개발툴인 스프링에서는 이를 CGLIB 기술을 통해, 지원하고 관리해준다.

0개의 댓글