본문 바로가기

C/C++

Singleton Pattern

Singleton(싱클턴) Pattern

 

<패턴 그리고 객체지향적 코딩의 법칙책의 Story 16 - ‘오직 하나뿐인 그대의 내용을 토대로 재 작성한 것입니다.

 

 

린터를 사용하려면 어떤 코드를 만드는 것이 좋을까요?

시스템을 통틀어서 한대 밖에 없는 자원이니까 객체 역시 하나만 존재해야 합니다.

먼저 떠오르는 건프린터를 전역(global) 변수로 사용하는 것입니다.

그러나 전역 변수 사용은 흔히 나쁜 습관이라고 합니다아래와 같은 코드처럼 말이죠

 

먼저 선언을 하고,

main

CPrinterg_pPrinter;

// 프린터객체는필요할때NULL 검사를한뒤생성해서사용하세요

 

int main()

{

       ...

}

 

아래와 같이 사용할 수 있겠죠

CDocument.cpp / CImageViewer.cpp

void CDocument::Print()

{

       if (!g_pPrinter)

             g-pPrinter = new CPrinter;

       ...

}

 

void CImageViewer::Print()

{

       if (!g_pPrinter)

             g-pPrinter = new CPrinter;

       ...

}

 

 

단 위 코드의 문제점은,

다른 개발자뿐만 아니라 코드를 작성한 개발자도 실수할 여지가 있습니다.

다른 클래스에서 CPrinter를 사용할 때마다 객체의 생성여부를 매번 체크해줘야 하며매번 delete를 해줘야 하고그 생성 및 삭제에 대한 비용도 문제가 될 수 있습니다특히 프린트와 같은 무거운 자원을 사용할 때 그렇습니다.

위의 코드처럼 ‘NULL 검사를 한 뒤 생성하세요라는 주석이 그나마 위안이 될 것 같나요천만에요.

 

언어가 발전하면서 점점 변수의 유효 범위(scope)를 줄이고 있어요최대한 접근을 차단해서 안정성을 높이기 위해서죠.

 

제의 핵심은 CPrinter 객체를 전역으로 만들면 안된다도 아니고 필요할 때 동적으로 생성한다도 아니에요. CPrinter 클래스의 사용이 언제나 동일한 객체를 가리키고 있어야 한다는 점이죠종합하면아래와 같은 책임을 가져야 한다는 거에요

-       객체를 여러 곳에서 만들 수 없도록 제약한다.

-       객체가 하나뿐이어야 한다.

 

와 같은 문제점을 객체지향적 설계로 해결 할 수 있습니다단계별로 CPrinter 클래스를 변화시켜 보죠.

 

먼저객체를 여러 곳에서 만들 수 없도록 생성자를 수정합니다.

private:

CPrinter();

 

위처럼 ‘private’로 선언하면 외부에서 객체를 생성할 수 없겠죠?

그리고 객체 생성을 위한 함수를 준비합니다.

class CPrinter

{

public:

       CPrinter();    //생성자    

       static CPrinterInstance()

       {

             if (!m_pInstance)

                    m_pInstance = new CPrinter;

 

             return m_pInstance;

       }

private:

       static CPrinterm_pInstance;

}

 

CPrinter* CPrinter::m_Instance = NULL; //반드시NULL로초기화

 

위처럼 Instance 함수 내에서 new를 해서 리턴하면 됩니다.

Instance함수를 static로 만든 이유는 외부에서 CPrinter 클래스 객체를 생성할 수 없어서 입니다. Static 멤버 함수에서는 static 변수밖에 접근할 수 없으니까 m_pInstance static으로 변수를 선언했구요그리고 생성자에서 m_pInstance를 초기화할 수 없으니 클래스 외부에서 해줘야 하구요

 

이렇게 구현하면 절대 객체가 하나밖에 생성될 수 밖에 없겠죠

 

그럼아래처럼 CPrinter 클래스의 기능을 사용할 수 있습니다.

CPrinter::Instance()->Print(...);

 

위와 같이 클래스가 오직 하나만 생성되도록 하고 클래스 외부에서 잘못된 접근 방식을 없애는 방법을 싱글턴(singleton)패턴이라고 합니다.

 

 코드에서 한가지 생각해 볼 문제는 new로 싱글턴 객체를 생성하면 되는데언제 delete를 해야 하는가 입니다.

아래처럼 다른 사람이 객체를 생성하고 지울 수 있다는 거죠악의를 가지고요.

CSingletonpSingleton = CSingleton::Instance();

pSigneton->...

delete pSingleton;

 

싱글턴 객체는 하나밖에 없는 객체인데 그걸 지운다는 게 이상한 거지요위와 같은 문제를 막을려면 소멸자를 private로 선언하면 되긴 합니다.

여하튼 싱글턴 객체 삭제는 delete로 지우는 것 보다 싱글턴 객체 파괴용 static 함수를 만드는 방법이 보기에 좋습니다다음을 보시죠.

class CSingleton

{

public:

       ...

       static void DestroyInstance()

       {

             if (m_pInstance)

             {

                    delete m_pInstance;

                    m_pInstance = NULL;

             }

       }

}

 

그런데 중요한 건,

시스템마다 DestroyInstance함수를 호출하는 시점을 정의하기가 힘들다는 겁니다싱글턴을 사용한다는 의미는 싱글턴 객체가 어디서 생성됐는지 또 누가 생성하는지 명확치 않다는 단점이 있어요그런데 이걸 단점으로 보기 애매한게싱글턴으로 만드는 이유가 시스템 종속적인 자원을 만든다는 것이에요. CDocument 객체 안에서 CPrinter 싱글턴 객체를 만들었다고 해서 CDocument 객체가 CPrinter 싱글턴 객체를 소유한다고 볼 수도 없고삭제해야 할 의무도 없고 삭제해서도 안되죠.

그래서 싱글턴을 삭제하는 시점은 시스템의 자원을 정리하는 시점이라고 볼 수 있습니다.

 

반적인 프로그램의 경우 Initialize(초기화부분과 Run(실제 프로그램 수행부분그리고 Finalize(종료)부분으로 나눌 수 있는데싱글턴 객체는 의미상 Run 어느 부분에서도 사용이 가능한 객체로 인식 되죠이때 이런 시점을 명확히 하기 위해 전역 변수를 효과적으로 사용하면 좋을 수도 있습니다.

예를 들어다음을 보죠

CEngineg_pEngine;

int main()

{

       g_pEngine = new CEngine;

       g_pEngine->Initialize();   //초기화

       g_pEngine->Run();                //프로그램수행

       g_pEngine->Finalize();           //종료

 

       return 0;

}

 

이때 모든 싱글턴 객체가 자신이 생성되면 g_pEngine으로 보고를 할 수 있다면 Finalize 시점에 정확하게 생성된 싱글턴 객체만 삭제가 가능합니다.

 

CSingletonCSingleton::Instance()

{

       if (!m_pInstance)

       {

             m_pInstance = new CSingleton;

             g_pEngine->RegisterSingle(싱글턴 아이디);

             //매크로로작성해도돼요

             //REGISTER_SINGLETON(싱글턴아이디);

       }

 

       return m_pInstnace;

}

 

위는 그냥 하나의 예일 뿐이지만어떤 환경이든지 싱글턴 객체는 시스템에 하나뿐이므로 시스템이 관리해야 한다는 거죠.

 

성은 어떤 방식으로 이루어지더라도 객체의 삭제는 프로그램이 전역적으로 사용했던 리소스를 정리하는 시점에 같이 이루어지는게 매우 바람직하다는 겁니다.

 

 

<참고>

1. 패턴 그리고 객체지향적 코딩의 법칙한빛미디어문우식