본문 바로가기
프로그래밍/More Effective C++

항목9. 리소스 누수를 피하는 방법의 정공은 소멸자이다.

by 리뷰하는 (게임)프로그래머_리프TV 2011. 7. 6.


이번 항목부터 리소스 누수, 예외처리에 대한 이야기가 펼쳐진다.
와~ 신난다.

본론으로 바로 들어가자.

1. 데이터를 받아서, 기록하려 한다.

2. 데이터를 파일에 저장되어 있다.

3. 파일안에는

"개"
"고양이"
"코끼리"
...

여러 동물들에 대한 이름과 정보가 들어가 있다.

다음과 같은 상황에서 DB를 구축한다고 하였을 때,

AAA라는 추상 클래스를 생성 -> ex) "동물"

AAA를 상속받은 BBB 클래스 -> ex) "개"
AAA를 상속받은 CCC 클래스 -> ex) "코끼리"
AAA를 상속받은 DDD 클래스 -> ex) "고양이"

라고 한다면,

다음 클래스가 대충 이해가 갈 것이다.

#include <iostream>
#include <string>

class AAA
{
public:
	virtual void process() = 0;
};

class BBB : public AAA
{
public:
	virtual void process()
	{
		// 이 안에서 정보를 기록한다.
	};
};

// 정보를 읽은 후, 적절한 타임의 객체를 
// 동적 할당하여, 포인터를 반환
AAA* readAAA( std::istream& s );

void processAdoptions( std::istream& dataSource )
{
	while( dataSource )
	{
		// 데이터를 읽어서, 객체를 생성,
		// 그 포인터를 반환하면 기억해놓고.
		AAA* pa = readAAA( dataSource );
		// 기록을 수행
		pa->process();
		// 수행 후 종료(삭제)한다.
		delete pa;
	}
}

int main()
{
	// 실제로는 processAdoptions 함수에 재대로된 정보를 넘겨 주어야 할것이다.
	std::istream data(NULL);

	processAdoptions( data );

	return 0;
}

AAA* readAAA( std::istream& s )
{
	// 파일에서 데이터를 받아와, 객체를 생성
	// 그 객체의 포인터를 return 해 주는 함수 
	return 0;
}​


복잡하려나--; 가능한 쉽게 한다고 하는데..
뭐, 정 복잡하면.

void processAdoptions( std::istream& dataSource )
{
	while( dataSource )
	{
		// 데이터를 읽어서, 객체를 생성,
		// 그 포인터를 반환하면 기억해놓고.
		AAA* pa = readAAA( dataSource );
		// 기록을 수행
		pa->process();
		// 수행 후 종료(삭제)한다.
		delete pa;
	}
}​


이 부분만 봐도 된다.

readAAA 안에서는 새로운 객체를 생성하여
return AAA* 를 하게 되는데,
만약.

		pa->process();​


이 부분에 예외가 발생하면 어떻게 될까?
delete 부분은 수행되지 않을 것이고.
메모리 누수가 발생할 것이다.

그래서 예지력이 뛰어난 우리는 누수를 막기 위해,
다음과 같은 예외처리를 삽입하였다.

void processAdoptions( std::istream& dataSource )
{
	while( dataSource )
	{
		// 데이터를 읽어서, 객체를 생성,
		// 그 포인터를 반환하면 기억해놓고.
		AAA* pa = readAAA( dataSource );

		try
		{
			// 기록을 수행
			pa->process();
		}
		catch (...)			// 예외가 발생하면
		{
			delete pa;		// 누수가 일어 나지 않게 delete 하고

			throw;			// 전파
		}

		// 예외가 발생하지 않았다면, 그냥 delete를 하게 될 것이다.

		// 수행 후 종료(삭제)한다.
		delete pa;
	}
}
​


아 정말 뛰어난 예외처리다. 이제 문제가 발생해도,
메모리 누수가 일어 나지 않을꺼야.
라고 좋아 하고 있는데,
뭔가 이상하다.
만약 pa의 이름이 바뀌어서.
pa2가 되었다면.
catch안에 있는 pa도...
밖에 있는 pa도, ps2로 바꿔주는 귀찮음이 생길 수 있고.
delete pa 를 2번 쓴다는것 자체가 뭔가 무의미 하고 복잡하게 느껴진다.

라고 생각이 든다고 한다.(진짜?)
즉, 수정하고 나니 뭔가 잘못된 것 아니야? 라고 느낀다는 것이다.

쉽게 말해, 예외가 발생하든, 발생하지 않든.
pa는 삭제 되어야 한다는 것이고
구지 2번 쓸 필요 없이 다른 방법을 찾아 보자. 라는것이다.

먼저 그 해결책에 대해서 2가지 방법을 제시 하고 있는데,

그 첫번째. 스마트 포인터.

자세한 내용은 후에 항목에서 소개가 될테니 넘어가고.

쉽게 이야기 하면,
자신의 유효범위를 벗어나면 자신이 가리키고 있는 메모리를 삭제 할 수 있는 "포인터처럼 동작하는 객체"라고 한다.

일단 c++ 라이브러리에는 auto_ptr 이라는 "그런 일"을 하는 객체가 이미 정의되어 있다고 한다.
대충 개념만 따지면 다음처럼 말이다.

template< class T >
class auto_ptr
{
	T* ptr;		// 객체에 대한 미초기화 포인터
public:
	auto_ptr( T* p = 0 ) : ptr( p ) {}	// 객체에 대한 포인터를 ptr에 저장.
	~auto_ptr() { delete ptr; }			// 객체에 대한 ptr을 삭제.
};​


"보고 있나 플머?"
뭐 여튼, 포인터 같이 활동하는 객체 라는것을 주의하면서
생성자, 소멸자를 통해 메모리 누수를 막고 있다는 것을 파악하면 된다.

책에서는 "사실, 단순 데이터를 보관하는거면 그냥 stl의 vector를 쓰세요."
라고도 말 하고 있지만....;;
일단! 다음과 같이 auto_ptr이라는 객체를 통해
기존의 코드는 이처럼 바뀌게 될 것이다.

void processAdoptions( std::istream& dataSource )
{
	while( dataSource )
	{
		// 데이터를 읽어서, 객체를 생성,
		// 그 포인터를 반환하면 기억해놓고.
		// AAA* pa = readAAA( dataSource );

		// 2가지가 바뀐다.
		// 첫째, AAA* 가 아닌 auto_ptr<AAA> 라는점.
		// 둘째, delete를 쓰지 않는다는 점.
		// 이유에 대해서는 구지 말하지 않아도 알듯.
		auto_ptr<AAA> pa( readAAA( dataSource ) );
		pa->process();
	}
}​


자, 이렇게 함으로써,
pa->process();
에서 예외가 발생하더라도, 메모리 누수는 발생하지 않을 것이다.
"보고 있나 플머?"ㅋ

또한, 이와 같은 형식으로 해결 하는 또 하나의 누수 막기 스킬이 존재 한다고 하는데,
이는 window handle 에서도 사용할 수 있다고 한다.
코드를 보자.

void displayInfo( const Information& info )
{
	WINDOW_HANDLE w( CreateWindow() );

	// w를 핸들로 하는 윈도우에 정보를 표시한다.

	DestroyWindow( w );
}​


이 코드의 문법상이나 올바름을 보지 말고.
흐름을 보도록하자.
핸들을 통해 window를 생성하고.
작업을 진행하고.
해제하는 일을 하는 함수인 displayInfo는.
예외가 발생하면 리소스 누수를 일으킬 수 있는 여건을 가지고 있다.

이와 같은 부분에 대해서 해결책은 auto_ptr과 매우 흡사하다.
바로 코드를 보도록 하자.

// 윈도우 핸들을 획득하고 해제하는 클래스
class WindowHandle
{
public:
	WindowHandle( WINDOW_HANDLE handle ) : w( handle ) {}
	~WindowHandle() { destroyWindow(w) }

	// 싱글톤을 생각하면 편하게 이해가 가능할듯.
	operator WINDOW_HENDLE() { return w; }

private:
	WINDOW_HANDLE w;

	// 복사본이 여러번 만들어 지는 것을 막기 위해
	// private에 복사 생성자, 대입연산자 선언하여 이를 막아놓는다.
	WindowHandle( const WindowHandle& );
	WindowHandle& operator=( const WindowHandle& );
};​


마찬가지로 흐름을 보자.
문법은 조금 옳바르지 않을 수 있다.
여튼, 다음처럼 windowhandle을 받는 클래스를 생성하였고.
이제 displayInfo는 메모리 누수를 일으키지 않을 것이다.

void displayInfo( const Information& info )
{
	WindowHandle w( CreateWindow() );

	// w를 핸들로 하는 윈도우에 정보를 표시
}​


auto_ptr과 마찬가지로 추가적인 delete 작업,
즉, destroyWindow는 하지 않아도 된다.

이 처럼, 이번 항목에서 의미 하는 것은,
예외가 발생하였을 때 생기는 메모리 누수를 막기 위해선,
해당 객체를 그냥 포인터로 선언하지 말고,
생성과, 해제를 포괄적으로 담당하는 객체를 생성하라는 것이다.

하지만 이런 부분에 대해서도 문제는 존재 하는데,
바로, 소멸자에서 리소스를 자동으로 해제하다가 생기는 예외처리.

이 부분에 대해선 항목 10, 11에 대해서 다시 언급한다고 하니.
일단 이번장에서 배운것 부터 재대로 기억하고 있자.