'head first Design Patterns 스토리가 있는 패턴 학습법' 책을 이용하여 디자인 패턴을 공부해보자
위 책은 java 기반으로 코드가 작성되어 있는데, 나한테 익숙한 c++로 변환하여 예제 코드를 작성하고, 개념을 설명할 예정이다.
예를 들면, 책에서는 interface라고 나와있으면 c++은 순수가상함수로 표현할 수 있다.
예제 코드들은 책을 기준으로 java로 작성하지만 C++문법에서 차이가 있으면 설명을 달아놓도록 할 것이다.
싱글턴 패턴 Singleton Pattern
[개념] 싱글톤 패턴이란
싱글톤 패턴이란, 프로그램 내에서 오직 하나의 객체만 생성되는 것을 보장하고, 프로그램 어디에서든 이 객체에 접근할 수 있도록 하는 패턴이다.
[필요성] 싱글톤 패턴이 필요한 이유
프로그램 실행에 있어서 하나만 있으면 되는 객체가 있다. 예를 들면 로그 기록용 객체가 그렇다. 로그 기록용 객체는 두 개 이상 만들게 되면 프로그램이 이상하게 돌아가거나 자원을 불필요하게 잡아먹거나 결과에 일관성이 없어진다.
[구현 방법]
객체 생성자를 private으로 지정하고, getInstance() 라는 정적 메소드가 호출되면 그 안에서 인스턴스를 생성한다.
즉, 인스턴스가 필요하면 반드시 getInstance를 거쳐야 한다. 그리고 어디서든 그 인스턴스에 접근할 수 있도록 static으로 선언되어야 한다.
(코드를 보는게 더 빠르게 이해 간다!)
private static ChocolateBoiler self;
private ChocolateBoiler() {}
public static ChocolateBoiler getInstance()
{
if(!self)
{
self = new Chocolate();
}
return self;
}
[C++] self를 포인터로 선언해야 위의 문법이 올바르게 실행된다.
p.s. 게으른 인스턴스 생성 (lazy instantiation)
위의 코드처럼 애플리케이션 실행 처음부터 인스턴스를 생성하는게 아니라,
필요한 상황이 닥쳤을 때 인스턴스를 생성하는 것을 '게으른 인스턴스 생성'이라고 일컫는다.
[단점]
싱글턴 패턴의 가장 큰 단점은 멀티스레드에서 getInstance 호출 순서에 따라 객체가 두 개 생성되어 꼬일 수 있다는 것이다.
예를 들면 thread1에서 if(!self) 조건문을 타고 이제 막 인스턴스를 생성할 시점에 (self = new Chocolate())
thread2에서 if(!self) 조건문을 확인하는 것이다. thread1에서 아직 인스턴스를 생성한 것은 아니므로 thread2에서도 생성자를 호출할 수 있다.
이렇게 되면 한 애플리케이션에 두 개의 객체를 가지고 있는 것이다.
데이터 공유를 위해 하나의 객체만을 허용하였는데! 막상 두 개의 객체가 돌아가고 있으니 데 프로그램이 이상하게 돌아갈 수 있다.
[해결 방법] 객체가 두 개 생길 수 있는 문제점을 해결하는 방법 3가지 (책 기준 java)
(1) Synchronized 동기화 처리
객체를 호출하는 getInstance 메소드를 동기화 처리한다.
한 스레드가 메소드 사용을 끝내기 전까지 다른 스레드는 기다려야 한다. 따라서 성능이 100배 정도 저하된다는 걸 감안하고 적용해야한다.
(2) 인스턴스를 필요할 때 생성하지 말고, 처음부터 만들어 버린다.
private static ChocolateBoiler self = new Chocolate();
private ChocolateBoiler() {}
public static ChocolateBoiler getInstance()
{
return self;
}
이러면 전역변수를 사용하여 인스턴스를 하나만 만드는 것과 동일해진다. 이렇게 구현할 때의 단점은 애플리케이션이 시작될 때 이 객체가 생성되고, 애플리케이션이 끝날 때까지 이 객체를 한 번도 쓰지 않는다면 괜히 자원만 잡아먹게 된다.
사실 이러면 싱글톤 패턴을 쓰는 이유가 무의미해진다. 전역변수로 객체를 생성하는 것과 별반 차이가 없으니까...
(3) DCL (Double checking Locking) 을 써서 getInstance에서 동기화되는 부분을 줄인다.
DCL을 사용하면 일단 인스턴스가 생성되어 있는지 확인한 다음, 생성되어 있지 않을 때만 동기화할 수 있다.
이렇게 하면 처음에만 동기화를 하고 나중에는 동기화를 하지 않아도 되서 getInstance 메소드를 호출할 때의 오버헤드를 극적으로 줄일 수 있다.
private volatile static ChocolateBoiler self;
private ChocolateBoiler() {}
public static ChocolateBoiler getInstance()
{
if(!self)
{
synchronized(ChocolateBoiler.class)
{
if(!self)
{
self = new Chocolate();
}
}
}
return self;
}
volatile 키워드를 사용하면 멀티스레딩을 쓰더라도 self 변수가 singleton 인스턴스로 초기화되는 과정이 올바르게 진행되도록 할 수 있다고 한다.
(올바르게 진행되지 않으면 어떻게 된다는걸까...? volatile 키워드를 사용하지 않고, 멀티스레딩의 경우 어떤 문제점이 발생할 수 있는지 예제를 찾아봐야겠다.)
해결 방법 (C++기준)
mutex 사용
synchronized() 사용