고전적 싱글턴
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()를 보면 uniqueInstance가 null이 아니면 있는 것을 그대로 반환하고, 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을 걸어 다른 스레드의 침범을 막은 후에 다시 한 번 체크합니다.
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 |
결과)
초콜렛 보일러 인스턴스를 생성합니다.
초콜렛 보일러 인스턴스를 반환합니다.
보일러를 채웁니다.
보일러를 가동합니다.
보일러에서 재료를 옮깁니다.
초콜렛 보일러 인스턴스를 반환합니다.
댓글 없음:
댓글 쓰기