본문 바로가기
SWE/C++ OOP

[C++][head first 디자인 패턴] Singleton pattern

by S나라라2 2021. 6. 30.
반응형

'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() 사용

반응형