서적 정리/Effective C++

29.예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!

민돌이2 2021. 12. 19. 18:07

예외 안전성(exception safety)은 코드 설계에 있어서 힘든 일이지만, 코딩을 하면 할수록 이런 상황때문에 터지던데.. 하면서 예외 처리를 생각하는 것이 당연시하고 있다.

 

배경그림이 나오는 GUI 메뉴를 구현하는 클래스를 만든다고 가정하자.

class PrettyMenu
{
public:
	void ChangeBackground(std::istream& imgSrc); //배경을 바꾸는 멤버 함수

private:
	Mutex mutex;

	Image* bgImage; //현재 배경그림
	int imageChanges; //배경그림이 바뀐 횟수
};

void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
	lock(&mutex); //뮤텍스 잠금

	delete bgImage; //현재 배경 삭제
	++imageChanges; //변경 횟수 증가
	bgImage = new Image(imgSrc); //현재 배경 재할당

	unlock(&mutex); //뮤텍스 잠금 해제
}

 

예외 안전성을 가진 함수라면 두 가지 요구사항을 맞춰야한다.

1.자원이 새도록 만들지 않는다(메모리 누수).

위 코드에서 new Image(imgSrc);에서 예외가 발생하면 mutex는 해제되지 않아 메모리 누수가 발생한다.

메모리 누수를 막는 방법으로 자원 관리를 객체가 맡도록 하는 방법(13장 참고)을 사용할 수 있다.

void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
	Lock m1(&mutex); //뮤텍스를 대신 획득하고 필요 없어질 시점에 바로 해제

	delete bgImage;
	++imageChanges;
	bgImage = new Image(imgSrc);
}

 

2.자료구조가 더렵혀지는 것을 허용하지 않는다.

new Image(imgSrc);에서 예외가 발생하면 이미지는 변경되지 않았지만, imageChanges는 증가 되어 imageChanges가 갖고 있는 의미를 훼손될 수 있다.

자료구조를 더렵혀진다는 말은 각 데이터가 갖는 의미나 상황이 오염된다는 말이다.

 

예외 안전성을 갖춘 함수는 3가지 보장 중 하나를 제공한다.

1.기본적인 보장

실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장이다.

객체나 자료구조는 더렵혀지지 않으며, 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다(모든 클래스 불변속성이 만족된 상태). 하지만 프로그램의 상태가 정확히 어떠한지는 예측이 안 될 수 있다.

만약 ChangeBackground 함수가 동작하다 예외가 발생했을 때 PrettyMenu 객체는 바로 이전 배경그림을 그대로 사용할 수도 있고, 아니면 처음부터 마련해 둔 기본 배경그림을 사용할 수도 있다. 이는 함수 구현자에 따를 수 밖에 없다.

메모리 소비 또는 성능 면에서 강력한 보증에 대한 비용이 너무 높을 경우에는 최상의 선택이 될 수 있다.

 

2.강력한 보장

프로그램의 상태를 절대로 변경하지 않겠다는 보장이다. 이런 함수를 호출하는 것은 원자적인(atomic) 동작이라 할 수 있다. 호출이 성공하면 마무리까지 완벽하게 성공하고, 실패하면 함수 호출이 없었던 것처럼 프로그램을 롤백한다.

커밋(성공) 혹은 롤백(실패) 두 가지 경우만 있다.

 

3.예외불가 보장

예외를 절대로 던지지 않겠다는 보장이다. 약속한 동작은 언제나 끝까지 완수하는 함수라는 뜻이다.

기본제공 타입에 대한 모든 연산은 예외를 던지지 않게 되었기에 예외불가 보장이다. 예외에 안전한 코드를 만들기 위한 가장 기본적이며 핵심 요소이다. 하지만 어떤 예외도 던지지 않게끔 지정이 된 함수가 예외불가 보장을 제공한다고 생각하면 안된다.

 

기본적인 보장, 강력한 보장, 예외불가 보장 셋 중에 한가지를 선택하라고 하면 예외불가 보장이 제일 깔끔해 보이고 탐스럽긴 하지만 현실적으로 구현하기 힘들다. STL 컨테이너에서 동적 할당 메모리를 사용하는 쪽을 보면 요청에 맞는 메모리를 확보할 수 없으면 bad_alloc 예외를 던지도록 구현되어 있다(49장 참고).

결론은 가능하다면 예외불가 보장을 선택하지만, 현실적으로 기본적인 보장 vs 강력한 보장 둘 중 하나를 골라야 한다.

 

2-1.기본적인 보장

프로그램의 모든 것들을 유효한 상태로 유지하는 것이 목표다.

class PrettyMenu
{
	...
	shared_ptr<Image> bgImage; //Image* bgImage;에서 변경
	...
};

void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
	Lock m1(&mutex); //뮤텍스를 대신 획득하고 필요 없어질 시점에 바로 해제

	bgImage.reset(new Image(imgSrc)); //bgImage의 내부 포인터를 new Image 표현식의 실행 결과로
	++imageChanges;
}

bgImage를 스마트포인터로 선언하고 스마트포인터의 reset 함수를 사용하여 new Image(imgSrc) 표현식이 제대로 실행 됐을 때만 이전 배경 그림이 삭제되도록 구현이 되었다. 부수적으로 스마트 포인터를 사용하여 동적 할당된 Image 객체를 관리하니까 ChangeBackground 함수의 길이도 줄었다.

얼핏 보기엔 강력한 보장 처럼 보이지만 매개변수 imgSrc이 변경된 채로 남아 있어 기본적인 보장이다.

 

2-2.강력한 보장

예외가 발생하면 원래 상태를 보장하는 것이 목표다.

일반적으로 복사-후-맞바꾸기(copy-and-swap)방법을 사용한다. 쉽게 생각해보자. 사본으로 작업하고 예외가 발생하지 않으면 원본과 스왑하면 그만 아닌가? 예외 발생시 스왑하지 않으니 원본은 그대로 보존된다.

struct PMImpl
{
	shared_ptr<Image> bgImage;
	int imageChanges;
};

class PrettyMenu
{
	...
private:
	Mutex mutex;
	shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
	using std::swap; //std의 swap 사용할 것이다 선언

	Lock m1(&mutex); //뮤텍스를 대신 획득하고 필요 없어질 시점에 바로 해제

	shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //원본 pImpl의 사본 생성
	pNew->bgImage.reset(new Image(imgSrc)); //pImpl의 사본에서 배경그림 새로 생성
	++pNew->imageChanges; //사본의 변경횟수 증가

	swap(pImpl, pNew); //사본과 원본 스왑
}

이 전략은 pimpl 관용구(31장 참고)을 주로 사용한다고 하는데, 이 책에서도 pimpl 관용구를 사용하였지만, 굳이 여기서 할 필요는 없어보인다.

 

함수의 부수효과(side effect)는 함수 내의 실행으로 인해 함수 외부가 영향을 받는 것을 의미한다.

강력한 보장을 보장한다고 해도 부수효과로 인해 보장이 무너질 수 있다.

지금까지 예로 들었던 ChangeBackground 함수로 예를 들어보자.

void PrettyMenu::ChangeBackground(std::istream& imgSrc)
{
	using std::swap; //std의 swap 사용할 것이다 선언

	Lock m1(&mutex); //뮤텍스를 대신 획득하고 필요 없어질 시점에 바로 해제

	shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //원본 pImpl의 사본 생성
	pNew->bgImage.reset(new Image(imgSrc)); //pImpl의 사본에서 배경그림 새로 생성
	++pNew->imageChanges; //사본의 변경횟수 증가

	f1();
	f2();

	swap(pImpl, pNew); //사본과 원본 스왑
}

f1, f2 함수 둘 모두 강력한 보장을 하고 ChangeBackground 함수는 위에서 사용했던 그대로니 강력한 보장을 한다고 가정한다. 그럼 ChangeBackground 함수는 강력한 보장을 하는것이라 생각할 수 있지만 아니다.

처음에 이 글귀를 보고 이해가 안갔으니 잠시 머리비우고 생각을 정리해보자. 강력한 보장이란 예외 발생시 원본 상태를 변경하지 않고 유지한다는 보장이다.

f1에서 예외가 발생하지 않고 끝까지 실행되면 프로그램 상태는 f1에 의해 변해 있을 것이고, f2가 실행되다 예외가 발생하면 그 프래그램의 상태는 ChangeBackground 함수가 호출될 때와 아예 달라져 있을 것이다.

 

이것만은 잊지 말자

예외 안전성을 갖춘 함수는 실행 중 예외가 발생하더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 냅두지 않는다. 이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.

강력한 예외 안전성 보장은 '복사-후-맞바꾸기' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.

어떤 함수가 제공하는 예외 안전성 보장의 강도는 그 함수가 내부적으로 호출하는 함수들이 제공한느 가장 약한 보장을 넘지 않는다.

728x90