[디자인패턴 수업 10주차 2차시] Singleton Pattern

Jin Hur·2021년 11월 3일
0
post-custom-banner

싱글턴 패턴

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

  • 클래스에서 자신의 단 하나뿐인 인스턴스를 관리하도록 만들면 됨. 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하도록 해야 한다. 인스턴스가 필요하면 반드시 클래스 자신을 거치도록 해야됨.

  • 그리고 어디서든 그 인스턴스(한번 생성된)에 접근할 수 있도록 만들어야 한다. => getInstance()
    요청이 들어오면 그 하나뿐인 인스턴스를 건네주도록 만들어야 한다.

  • 또한 싱글턴을 통해 게으른 생성(lazy instantiation)이 되도록 구현할 수도 있다. 클래스의 객체가 자원을 많이 잡아먹는 경우 이런 게으른 생성 기법이 꽤나 유용할 수 있다.



  • getInstance() 메서드는 정적 메서드. 즉 클래스 메서드이다. 그저 SingletonClass.getInstance()라는 코드만 사용하면 언제 어디서든 이 메소드를 호출하여 유일하게 생성된 객체에 접근할 수 있다. 전역 변수에 접근하는 것 만큼이나 쉬우면서도 게으른 인스턴스 생성 기법을 적용할 수 있다는 장점을 제공.
  • uniqueInstace 클래스 변수(정적 변수)에 싱글턴의 유일무이한 인스턴스가 저장됨.
  • 싱글턴 패턴을 구현한다고 해서 여기 있는 SingletonClass처럼 간단해야 하는 것은 아님. 그냥 일반적인 클래스를 만들 때와 마찬가지로 다양한 데이터와 메서드를 사용할 수 있다.

고전적 싱글턴 패턴 구현법

public class OldSingleton {
    private static OldSingleton uniqueInstance;	// 멤버변수(추상자료형) 디폴트는 null
    
    // 기타 인스턴스 변수...
    
    // private 키워드로 생성자 메서드에 접근 지정
    // 따라서 OldSigleton 클래스에서만 클래스 인스턴스를 만들 수 있다. 
    private OldSingleton(){}
    
    // 인스턴스 생성 또는 반환 메서드
    public OldSingleton getInstance(){
    	// uniqueInstance == null 이란 것은 아직 인스턴스가 생성되지 않았다는 뜻
        if(uniqueInstance == null)
        	// uniqueInstance에 하나 밖에 없는 인스턴스가 저장됨. 
            // 아직 인스턴스가 생성되지 않았다면 private으로 선언된 생성자를 이용해 유일한 객체를 생성한다. 
            // 이러한 방식으로 인스턴스가 필요한 상황이 생기기 전에는 아예 인스턴스를 생성하지 않게 된다. 
            // => "Lazy Instantiation"
            uniqueInstance = new OldSingleton();
        
        return uniqueInstance;
    }
}

위 코드는 멀티스레드 환경에서 문제가 발생한다.
이 후 소개되고, 몇가지 해결법이 존재한다.

일단 위 코드에서 두 가지를 기억하자. (싱글 스레드임을 가정)
1) 반드시 하나의 객체만을 생성하게 된다.
2) Lazy Instantiation이 가능하다.


초콜릿 공장 예시

초콜릿 공장은 하나의 초콜릿 보일러(객체)가 필요하다. 하지만 아래의 클래스 코드를 통해선 하나 이상의 보일러(객체)를 만들 수 있다.

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

    public ChocolateBoiler() {
        empty = true;
        boiled = false;
        System.out.println(this);
    }
    
    
    public void fill() {
        if (isEmpty()) {
            empty = false;
            boiled = false;
            // fill the boiler with a milk/chocolate mixture
        }
    }
    public void drain() {
        if (!isEmpty() && isBoiled()) {
            // drain the boiled milk and chocolate
            empty = true;
        }
    }
    public void boil() {
        if (!isEmpty() && !isBoiled()) {
            // bring the contents to a boil
            boiled = true;
        }
    }
    public boolean isEmpty() {
        return empty;
    }
    public boolean isBoiled() {
        return boiled;
    }
}

어떻게 하면 하나의 객체만의 생성이 보장될까?
바로 앞서의 고전적 싱글톤 패턴을 적용해보자.

public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    // 유일한 객체를 참조할 변수
    private static ChocolateBoiler uniqueInstance;

    // 생산자의 접근지정자를 private으로 변경
    //public ChocolateBoiler() {
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
        System.out.println(this);
    }

    // getInstance() 메서드 추가
    public static ChocolateBoiler getInstance(){
        if(uniqueInstance == null)
            uniqueInstance = new ChocolateBoiler();
        
        return uniqueInstance;
    }
    
    //...
    //...
}

멀티쓰레드 환경에서 고전적 싱글턴 패턴의 문제점

유일한 객체가 생성되지 않는 시점에 두 개의 쓰레드가 동작한다고 가정해보자, 각 쓰레드 getInstace() 메서드에 거의 동시에 진입하고, if문까지 거의 동시에 진입한다고 생각해보자, 한 쓰레드가 if문의 조건을 먼저 통과하더라도 new 연산자가 있는 코드를 수행하기 전 다른 쓰레드도 if문을 통과하면 두 개의 인스턴스가 생성되는 문제(싱글톤의 목적이 사라지는)가 발생한다.
다중 쓰레드 상황에서는 동기화에도 신경을 써야한다.

싱글 코어, 문맥 교환으로 인한 동기화 깨짐 상황

멀티 코어, 쓰레드의 동시 수행에서 동기화 깨짐 상황


다중 쓰레드 상황 속 동기화 문제 해결 방법

해결책은 getInstance()를 동기화시키기만 하면 된다.

1. synchronized 키워드 사용 (동기화 방법)

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

        return uniqueInstance;
    }

자바에서 제공하는 synchronized 키워드만 추가하면 한 쓰레드가 메소드 사용을 끝내기 전까지 다른 쓰레드는 기다려야 한다. 즉 두 쓰레드가 동시에 이 메서드를 실행시키는 일이 일어나지 않는다.

단점

uniqueInstance 변수에 생성된 인스턴스를 대입하고 나면 굳이 이 메서드를 동기화된 상태로 유지시킬 필요가 없다. 유일한 객체가 생성되고 난 후 여러 쓰레드가 이 유일한 객체에 대한 접근을 요구할 때에는 동기화가 필요없기 때문이다. 이러한 방식은 결국 추후 불필요한 오버헤드만 증가시킬 뿐이다.

하지만,

getInstance() 메서드가 애플리케이션에 큰 부담을 주지 않는다면 그냥 냅두는 것도 방법 중 하나이긴 하다.

2. 인스턴스를 아예 처음부터 만들어버린다.

	// 유일한 객체를 참조할 변수
    private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();

    // 생산자의 접근지정자를 private으로 변경
    //public ChocolateBoiler() {
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
        System.out.println(this);
    }


    public static ChocolateBoiler getInstance(){
		// 유일한 인스턴스가 이미 존재하니 그저 반환만 하면 됨 
        return uniqueInstance;
    }

애플리케이션 작동에 있어 반드시 인스턴스를 생성하고, 그리고 이 인스턴스를 항상 사용한다면 (항상 사용함이 보장되어있다면) (또는 인스턴스를 실행중에 수시로 만들고 관리하기가 성가시다면) 위와 같이 처음부터 유일한 인스턴스를 만들어버리는 것도 괜찮은 방법이다.

3. DCL(Double-Checking Locking)

일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않았을 때만 동기화를 하는 방법이다. 즉 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 된다. (마치 synchronized 동기화 방법이 Coarse-grained라면, DCL은 Fine-grained와 같다.)

    // getInstance() 메서드 추가
    // DCL 기법
    public static ChocolateBoiler getInstance(){
        if(uniqueInstance == null) {    
            synchronized (ChocolateBoiler.class){
                if(uniqueInstance == null)
                    uniqueInstance = new ChocolateBoiler();
            }
        }
        
        return uniqueInstance;
    }

반드시 자바 5이상 버전에서만 쓸 수 있다.

post-custom-banner

0개의 댓글