상세 컨텐츠

본문 제목

항목 13) 객체를 활용한 자원 관리 ( RAII 패턴 )

C++/Effective C++

by DeaKyungLee 2021. 12. 5. 14:09

본문

자원이란?

프로그래밍 분야에서 말하는 자원은 여러가지가 존재한다.

대표적으로 동적 할당되는 메모리 외에도 파일 서술자 ( File Descriptor ), 뮤텍스 ( Mutex ), GUI, 폰트, 브러시, 데이터 베이스 연결 및 네트워크 소켓, 여러 Handler 역시 모두 자원이라고 할 수 있다.

앞서 말한 모든 자원들에는 절대적이고 공통된 규칙이 하나 존재하는데, 모든 자원은 사용된 이 후에 반드시 해제 해주어야 한다는 점이다.

이 간단한 규칙을 지키는 것이 굉장히 간단해 보이지만 실상은 그렇지 않다.

아래의 예시를 보자.

class Investment {...} ;
 
 
Investment* createInvestment(); // Investment 객체에 대한 팩토리 함수

위와 같이 객체 포인터를 반환하는 팩토리 함수가 있다고 가정해보자.

이것을 사용한다면 아래와 비슷한 형식의 코드가 사용될 것이다.

void someFunction()
{
	Investment *pInv = createInvestment();
	
	// ... some process...
 
 
	delete pInv;
}

어쩌면 당연한 말이겠지만, 객체에 대한 해제는 해당 객체를 사용하려고 호출한 호출자(caller)가 하는 것이 맞다.

그렇기 때문에 someFunction 내부에서 delete pInv; 해당 코드가 사용된 것이다.

그런데 문제는 delete 코드가 무조건 실행된다는 보장이 없다는 점인데,
some process 내부에서 return, 예외 발생, go to 등의 어떠한 일이 일어날지 장담할 수 없기 때문이다.

물론 그러한 상황을 방지하기 위해서 하나하나 모두 따져가면서 프로그램을 작성한다면 괜찮겠지만,

오랜 시간 유지 보수가 이루어지고 프로그래머가 몇 번이나 바뀐 상황이라면 사실상 모든 구조를 파악하기는 힘들다.

더군다나 delete 문이 실행되지 않아서 발생하는 것은 메모리 문제라는 재앙 덩어리이다.

이러한 문제를 해결하는 간단한 방식이 존재한다.

사용하는 자원을 객체에 넣어서 해당 자원에 대한 해제를 소멸자가 맡도록 하는 것이다.

그리고 이러한 용도로 설계된 것이 auto_ptr, 이른바 smart pointer 이다.

void someFunction()
{
	std::auto_ptr<Investment> pInv(createInvestment());
 
	// ... some process...
 
	// delete pInv;
 
}

auto_ptr은 하나의 클래스이고 인자로 받은 객체를 가리키고 있다가 소멸자에서 자동으로 해당 객체에 대해 delete를 호출하도록 설계되어 있다.

위의 예제에서 자원 관리에 객체를 사용하는 방법의 두 가지 중요한 특징을 볼 수 있다.

  1. Resource Acquisition is Initalizatiopn : RAII , "자원 획득은 초기화"
    std::auto_ptr<Investment> pInv(createInvestment()); 해당 문장을 다시 한 번 자세히 살펴보자.
    pInv 이라는 auto_ptr 자원 관리 객체에 대한 초기화를 createInvestment() 함수를 통한 자원 획득을 통해서 진행하고 있다.
    즉, 획득한 자원을 해당 자원을 관리하는 객체의 초기화로 넘겨준다는 점이다.
    더 풀어서 설명하자면, 획득한 자원은 가능한 빠르게 해당 자원을 관리하는 객체에게 넘겨주라는 것이다.
  2. 자원 관리 객체는 반드시 자신의 소멸자에서 해당 자원을 확실하게 해제해야 한다.
    소멸자는 어떤 객체가 소멸될 때에 자동적으로 호출되기 때문에, 실행 제어가 어떠한 경위로 블록을 떠나는 것에 관계 없이 자원 해제가 가능해진다.
    ( 물론 소멸자 내부에서 발생하는 예외에 대해서도 생각해야 하는데, 해당 내용은 항목 8 참고 )

RAII 개념을 설명하기 위해서 auto_ptr 을 사용했는데, 사실 auto_ptr은 현재는 사용되지 않는 스마트 포인터이다.

문제점은 크게 2가지가 존재한다.

  1. auto_ptr은 double pointing 개념을 지원하지 않는다.
    auto_ptr의 중요한 특성 중 하나가 소멸자에서 대상 자원에 대한 delete를 호출한다는 것이다.
    그렇기 때문에 같은 대상을 가리키는 2개 이상의 auto_ptr이 존재한다면, 이미 삭제된 자원에 대해서 다시 delete를 호출하는 상황이 발생할 수 있다.
    이것을 방지하기 위해서 auto_ptr 객체를 복사하면 ( 복사 생성자, 복사 대입 연산자 ) 원본 객체를 강제로 null로 만들어 버린다.
    std::auto_ptr<Investment> pInv1(createInvestment());
    std::auto_ptr<Investment> pInv2(pInv1); 	// pInv1 = null
    pInv1 = pInv2;	// pInv2 = null
    물론 해당 방식이 유용한 상황이 존재할 수 있지만 강제성을 띈다는 점이 상식에서 벗어난다고 할 수 있다.
  2. 소멸자에서 delete를 호출한다.
    delete[] 가 아닌 delete만을 호출하기 때문에 배열 형식의 자원에 대해서는 메모리 누수가 발생할 수 있다.
    또한 malloc 형식으로 할당된 자원에 대해서는 제대로 동작하지 않는다.

 

이렇듯 위의 2가지 치명적인 단점으로 인해서 c++11 부터는 해당 개념을 완전히 대체하는 unique_ptr 개념이 대신 사용된다.

더 자세한 내용은 아래의 링크를 참고

스마트 하지 못한 스마트 포인터

 

스마트하지 못한 스마트한 포인터 auto_ptr

C/C++은 메모리 관리가 까다로운 언어입니다. 개발자가 직접 메모리를 할당하고 해제해야 할 책임이 있습니다. C++의 STL에는 auto_ptr이라는 스마트 포인터가 존재합니다. 다만 auto_ptr은 C++11 이후에

psychoria.tistory.com

Is it possible to use a C++ smart pointers together with C's malloc?

 

Is it possible to use a C++ smart pointers together with C's malloc?

Some of my code still uses malloc instead of new. The reason is because I am afraid to use new because it throws exception, rather than returning NULL, which I can easily check for. Wrapping every ...

stackoverflow.com

또 하나의 중요한 스마트 포인터가 존재하는데, shared_ptr 이라는 참조 카운팅 방식 스마트 포인터이다. ( reference-counting smart pointer : RCSP )

특정한 자원을 가리키는 객체의 개수를 유지하고 있다가 그 개수가 0이 되면 해당 자원을 자동으로 삭제하는 방식으로 동작한다.

일종의 Garbage Collection 과 흡사하게 동작하지만, 참조 상태가 고리 ( circle ) 를 이루는 경우는 없앨 수 없다는 차이점이 존재한다.

문제점은 shared_ptr 역시 auto_ptr과 마찬가지로 배열 형식의 자원에 대해서는 제대로된 자원 해제를 지원하지 않는다.

사실 c++ 표준 라이브러리에서는 동적 할당된 배열을 위해 준비된 자원 관리 객체를 따로 제공해주지 않는다.

동적 할당 배열은 사실상 vector, string 으로 모두 대체가 가능하기 때문이다.

( boost 라이브러리의 scoped_array, shared_array 가 해당 기능을 가지고 있다. )

"자원 관리를 객체를 통해서 하자" 라는 지침을 설명하기 위해서 스마트 포인터 개념을 소개했지만, 이것은 그저 하나의 사용 예시에 불과하다.

경우에 따라서는 자신이 직접 자원 관리 클래스를 만들어서 사용해야 한다.

 

Things to Remember

  • 자원 누출을 막기 위해, 생성자 안에서 자원을 획득하고 소멸자에서는 그것을 해제하는 RAII 객체를 사용하자.
  • 일반적으로 널리 쓰이는 RAII 클래스는 unique_ptr, shared_ptr 이 존재한다.
    ( vector, string 이 아닌 배열 형식으로 사용되는 자원 관리에는 가급적 사용하지 말자. )

관련글 더보기

댓글 영역