카테고리 없음

[C++] 싱글톤 패턴의 메모리 할당과 해제 with C++

nowkoes 2023. 8. 17. 00:00

할당과 해제

개요

 앞서 설명한 내용들을 토대로 싱글톤 패턴으로 디자인된 클래스에서 소멸자에 메모리를 해제하는 코드를 추가하는 것은 주의해야 한다는 점을 알았다. 따라서 new 키워드로 호출된 인스턴스를 할당 해제 하는 방법들에 대해 알아보자.


본문

 싱글톤으로 디자인된 클래스의 인스턴스 메모리를 해제하려면 다음과 같은 방법을 사용할 수 있다. 

 

정적 메서드 추가

 인스턴스를 불러오는 공개된 정적 멤버 함수처럼, 인스턴스의 메모리를 해제하는 정적 메서드를 추가하면 된다. 이를 테면, 

#include <iostream>

class singleton
{
    static singleton* _instance;
	
    singleton();
    ~singleton();
    singleton(const singleton&) = delete;
    singleton operator = (const singleton&) = delete;

public:
    static singleton* GetInstance();
    static void DestroyInstance();
};

singleton* singleton::_instance = nullptr;

singleton::singleton()
{
    std::cout << "생성자 호출\n";
}

singleton* singleton::GetInstance()
{
    if (!_instance)
        _instance = new singleton();

    return _instance;
}

void singleton::DestroyInstance()
{
    if (_instance)
    {
        delete _instance;
        _instance = nullptr;
    }
}
        
singleton::~singleton()
{
    std::cout << "소멸자 호출\n";
}

 다음과 같이 DestroyInstacne() 함수를 작성한 후, 필요한 시점에서 이 함수를 호출하여 인스턴스의 메모리를 해제하면 된다. 이 방법은 코드가 명시적으로 메모리를 해제하는 시점을 결정할 수 있지만, 개발자가 메모리를 해제하는 시점을 잘 관리해야 한다.


스마트 포인터 활용

 스마트 포인터(smart pointer)는 C++에서 제공하는 포인터와 유사한 객체로, 동적으로 할당된 메모리의 수명을 자동으로 관리하는 포인터를 의미한다. RAII(Resource Acquisition Is Initialization) 디자인 원칙을 따르며, 객체의 수명이 끝나면 자동으로 관련 메모리를 해제한다. 

 

출처: cppreference

 

 스마트 포인터에 대한 자세한 내용은 추후에 다루기로 하고, 우리가 싱글톤 클래스에 사용할 포인터는 std::shared_ptr이다. C++ 공식 문서에 따르면 객체의 공유 소유권을 유지하는 스마트 포인터인데, 여러 shared_ptr 객체들은 동일한 객체를 소유할 수 있다고 한다. 이때 객체는 다음과 같은 상황에서 파괴되고 그 메모리가 할당 해제된다.

 

  • 객체를 소유하고 있는 마지막 shared_ptr이 파괴될 때
  • 객체를 소유하고 있는 마지막 shared_ptr이 operator = 또는 reset()을 통해 다른 포인터를 할당받을 때

 

 이러한 shared_ptr은 내부적으로 참조 카운트를 유지하는 참조 카운팅이 가능하고, 메모리를 자동으로 관리해 준다. 그리고 여러 포인터가 동일한 객체를 안전하게 공유할 수 있다. 해당 특징들로 인해 싱글톤 패턴을 디자인할 때 shared_ptr을 사용하기도 하지만, 전통적인 방식이 아니라는 것에 유의하자.

 

#include <iostream>

class singleton
{
    static std::shared_ptr<singleton> _instance;

    singleton();
    ~singleton();
    singleton(const singleton&) = delete;
    singleton operator = (const singleton&) = delete;

public:
    static std::shared_ptr<singleton> GetInstance();
    static void DestroyInstance();
};

std::shared_ptr<singleton> singleton::_instance = nullptr;

singleton::singleton()
{
    std::cout << "생성자 호출\n";
}

std::shared_ptr<singleton> singleton::GetInstance()
{
    if (!_instance)
        _instance = std::make_shared<singleton>();

    return _instance;
}

void singleton::DestroyInstance()
{
    _instance.reset();
}

singleton::~singleton()
{
    std::cout << "소멸자 호출\n";
}

 인스턴스를 스마트 포인터로 지정하고, 메모리 해제를 reset() 함수를 통해 명시적으로 나타내었다. 자동으로 메모리르 관리해 주고, 메모리의 안전성이 높지만 참조 카운팅을 수행하여 오버헤드가 추가되며, C++11 이후 버전에서만 쓸 수 있다는 단점이 있다.


정적 변수로 싱글톤 구현

 정적 변수로 싱글톤을 구현하여 소멸자에서 메모리를 해제하는 방법에 대해 알아보자. 정적으로 할당된 변수는 수동으로 지우거나 해제할 수 없으므로(프로그램이 종료될 때 자동으로 소멸됨), 정적 변수에 대해 메모리를 할당을 해제할 필요가 없다.

 

#include <iostream>

class singleton
{
    static singleton _instance;

    singleton();
    ~singleton();
    singleton(const singleton&) = delete;
    singleton operator = (const singleton&) = delete;

public:
    static singleton& GetInstance();
};

singleton singleton::_instance;

singleton::singleton()
{
    std::cout << "생성자 호출\n";
}

singleton& singleton::GetInstance()
{
    return _instance;
}

singleton::~singleton()
{
    std::cout << "소멸자 호출\n";
}

프로그램 종료 시점에서 메모리 해제

출처: cppreference

 

 C++에서는 std::atexit라는 프로그램 종료 시점에서 특정 함수를 자동으로 호출할 수 있게 해주는 함수가 있다. 사용법도 간단한데, 코드가 작성된 위치에 구애받지 않고 프로그램이 종료될 때 자동으로 해당 함수를 호출해 준다. 이때 여러 함수를 atexit로 등록하였다면, 역순으로, 즉, 마지막에 등록된 함수가 먼저 호출된다. 따라서 리소스 정리를 할 때 유용하게 사용된다.

 

 따라서 인스턴스를 해제하는 메서드를 작성한 후, atexit 함수로 호출하면 명시적으로 메모리를 프로그램 종료 시 해제할 수 있다.

#include <iostream>

class singleton
{
    static singleton* _instance;

    singleton();
    ~singleton();
    singleton(const singleton&) = delete;
    singleton operator = (const singleton&) = delete;

public:
    static singleton* GetInstance();
    static void DestroyInstance();
};

singleton* singleton::_instance = nullptr;

singleton::singleton()
{
    std::cout << "생성자 호출\n";
}

singleton* singleton::GetInstance()
{
    if (!_instance)
        _instance = new singleton();

    return _instance;
}

void singleton::DestroyInstance()
{
    if (_instance)
    {
        delete _instance;
        _instance = nullptr;
    }
}

singleton::~singleton()
{
    std::cout << "소멸자 호출\n";
}

int main()
{
    atexit(singleton::DestroyInstance);
}

마무리

 지금까지 총 3편에 거쳐 싱글톤으로 디자인된 클래스에서 생성자와 소멸자의 동작에 대해 알아보았다. 주안점은 static 키워드, new, delete 연산자로 인해 소멸자가 동작하지 않을 수 있으니 소멸자 함수 작성에 유의해야 한다는 점과, 운영체제에서 인스턴스 메모리를 자동으로 할당 해제해 주지만 필요에 따라 명시적으로 이를 행할 수 있다는 점이다. 해당 특징들을 잘 파악해서 효과적으로 클래스를 디자인하도록 하자.


요약

싱글톤 패턴의 메모리 할당 해제
1. 정적 메서드
 - 인스턴스의 메모리를 해제하는 명시적인 코드를 추가하거나, 운영체제가 자동으로 할당 해제
2. 스마트 포인터(std::shared_ptr)
 a. 정의: 자동 메모리 관리를 제공하는 포인터
 b. 특징: RAII 디자인 원칙을 따름
 c. 여러 객체들이 하나의 포인터를 공유함
3. 정적 변수
 - 프로그램이 자동으로 메모리를 해제하므로 따로 메모리를 관리하지 않아도 됨
4. std::atexit
 a. 정의: 프로그램 종료 시점에 호출되는 함수를 등록하는 함수
 b. 특징: 프로그램 종료 시 자동으로 등록된 함수를 호출
반응형