전체 페이지뷰

2017년 2월 14일 화요일

Singleton pattern

인스턴스가 하나뿐인  특별한 객체를 만들게 해 주는 패턴입니다. 전역 변수를 사용할 때처럼 어디서든 접근할 수 있게 해주지만 필요한 때에만 객체를 생성하게 해서 불필요하게 자원을 소모하지 않습니다.


고전적 싱글턴

public class MyClass
{
    private MyClass(){}
}

라는 코드로부터 내용이 시작됩니다. public으로 선언된 MyClass라는 클래스의 생성자가 private입니다. 생성자가 private이므로 이 상태에서는 인스턴스를 만들수가 없습니다. 그럼 다음 코드를 살펴봅시다.

public class MyClass 
{
    public static MyClass GetInstance(){}
}

MyClass 내부에 GetInstance라는 정적 메소드가 있습니다. 이 메소드는 MyClass.GetInstancs()라는 형태로 호출이 가능합니다.

저 둘을 합치면?

public class MyClass
{
    private MyClass(){}
    public static MyClass GetInstance()
    {
         return new MyClass();
    }
}

이렇게 하면 MyClass.GetInstance()라는 구문으로 이 객체의 인스턴스를 생성 가능해집니다. 결과적으로 MyClass대신 Singleton이라는 이름을 사용해서 정리해보면,

public class Singleton
{
    // Singleton 클래스의 유일한 인스턴스를 저장하는 정적변수
    private static Singleton uniqueInstance;
    private Singleton() { }
    // 클래스의 인스턴스를 만들어서 반환하는 
    public static Singleton GetInstance()
    {
        if (uniqueInstance == null)
        {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}
cs

처럼 됩니다. GetInstance()를 보면 uniqueInstancenull이 아니면 있는 것을 그대로 반환하고, null이라면 그때에서야 비로소 인스턴스를 생성합니다. 이 방법을 게으른 인스턴스 생성(lazy instantiation)이라고 부릅니다. 결과적으로 인스턴스는 어디에서나 접근할 수 있지만, 두 개 이상 새롭게 만들어질 수가 없습니다.

인스턴스를 하나 이상 사용해서는 안 되는 경우는 어떤 경우인가 하면,
하나 이상 존재할 때 프로그램이 이상하게 돌아간다든가 자원을 불필요하게 잡아먹는 경우인데, 스레드 풀, 사용자 설정, 레지스트리 설정, 로그 기록용 객체, 장치 드라이버 같은 것이 그에 속합니다.

고전적 싱글톤의 문제

한 초콜렛 공장에서 초콜렛 만드는 장비인 초콜렛 보일러를 컴퓨터로 제어하기 위해 다음과 같은 코드를 만들었습니다.

public class ChocolateBoiler
{
    private bool empty;
    private bool 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 bool IsEmpty()
    {
        return empty;
    }
    public bool IsBoiled() {
        return boiled;
    }
}
cs

그런데 ChocolateBoiler 인스턴스가 두개 만들어진다면 기계장치에 혼선이 올게 분명하므로 싱글턴으로 코드를 바꾸려고 합니다. 어떤 식으로 바꾸면 되겠습니까?

public class ChocolateBoiler
{
    private bool empty;
    private bool boiled;
 
    private static ChocolateBoiler BoilerInstance;
    private ChocolateBoiler()
    {
        empty = true;
        boiled = false;
    }
 
    public static ChocolateBoiler GetInstance()
    {
        if (BoilerInstance == null)
        {
            BoilerInstance = new ChocolateBoiler();
        }
        return BoilerInstance;
    }
        
    public void Fill()
    {
       ... 
cs

고전적 싱글턴 기법에 의하면 위와 같이 바꾸면 될 것입니다.
이런 방법으로 기계를 돌렸는데 문제가 생겼습니다. 재료가 차 있는 상태에서 또 Fill()메소드가 실행되어 흘러 넘치고 만 것입니다.

분명 하나밖에 만들어질 수 없는 객체가 멀티스레드가 적용되자 하나 더 만들어진 경우가 생긴 것입니다. 그렇다면 멀티 스레드 환경에서 돌아갈 수 있는 방법을 찾아야 합니다.

인스턴스를 필요시 생성하지 않고 처음부터 만드는 방법

public class Singleton
{
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton GetInstance()
    {
        return uniqueInstance;
    }
}

이처럼 하면 클래스 로딩 시 한번에 유일한 인스턴스가 생성됩니다. 언제나 필요하고 자주 쓰이는 싱글톤 인스턴스라면 고려 가능한 방법입니다.

DCL(Double-Checking Locking)을 쓰는 방법

이 방법을 쓰면 먼저 인스턴스가 생성되어 있는지 확인하고, 생성되지 않았을 때 동기화를 할 수 있습니다. 따라서 앱 성능을 떨어뜨리지 않고 동기화가 가능합니다.

public class Singleton
{
    private volatile static Singleton uniqueInstance;
 
    private Singleton() { }
 
    public static Singleton GetInstance()
    {
        if (uniqueInstance == null)
        {
            lock (typeof(Singleton))
            {
                if (uniqueInstance == null)
                {
                    uniqueInstance = new Singleton();
                }
            }               
        }
        return uniqueInstance;
    }
}
cs

volatile 키워드는 동시에 실행 중인 여러 스레드에 의해 필드가 수정될 수 있음을 나타냅니다(참조).  인스턴스 변수를 volatile로 수식하고, GetInsatnce() 메소드 내에서 더블 체킹을 합니다. uniqueInstance가 null인 경우에, Singleton에 대해 lock을 걸어 다른 스레드의 침범을 막은 후에 다시 한 번 체크합니다.

그럼 DCL의 방법을 써서 초콜렛 보일러 문제를 해결해 보겠습니다.

namespace SingletonPattern
{    
    public class ChocolateBoiler
    {
        private bool empty;
        private bool boiled;
 
        private volatile static ChocolateBoiler BoilerInstance;
        private ChocolateBoiler()
        {
            empty = true;
            boiled = false;
        }
 
        public static ChocolateBoiler GetInstance()
        {
            if (BoilerInstance == null)
            {
                lock (typeof(ChocolateBoiler))
                {
                    if (BoilerInstance == null)
                    {
                        Console.WriteLine("초콜렛 보일러 인스턴스를 생성합니다.");
                        BoilerInstance = new ChocolateBoiler();
                    }
                }
            }
            Console.WriteLine("초콜렛 보일러 인스턴스를 반환합니다.");
            return BoilerInstance;
        }
        
        public void Fill()
        {
            // 보일러가 비어 있을때 재료를 채움
            if (IsEmpty())
            {
                Console.WriteLine("보일러를 채웁니다.");
                empty = false;
                boiled = false;
            }
        }      
        public void Drain()
        {
            // 재료가 채워져있고 끓여지면 다음 단계로 넘김
            if (!IsEmpty() && IsBoiled())
            {
                Console.WriteLine("보일러에서 재료를 옮깁니다.");
                empty = true;
            }
        }  
        public void Boil()
        {
            // 보일러가 차 있고 끓이지 않았다면 끓임
            if (!IsEmpty() && !IsBoiled())
            {
                Console.WriteLine("보일러를 가동합니다.");
                boiled = true;
            }
        }
        public bool IsEmpty()
        {
            return empty;
        }
        public bool IsBoiled()
        {
            return boiled;
        }
    }
    
class Program
    {
        static void Main(string[] args)
        {
            ChocolateBoiler boiler = ChocolateBoiler.GetInstance();
            boiler.Fill();
            boiler.Boil();
            boiler.Drain();
 
            // 새로 생성하지 않고 기존 인스턴스를 반환해야 함
            ChocolateBoiler boiler2 = ChocolateBoiler.GetInstance();
        }
    }
}
 
cs

결과)
초콜렛 보일러 인스턴스를 생성합니다.
초콜렛 보일러 인스턴스를 반환합니다.
보일러를 채웁니다.
보일러를 가동합니다.
보일러에서 재료를 옮깁니다.
초콜렛 보일러 인스턴스를 반환합니다.



댓글 없음:

댓글 쓰기