[C++] 항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 5. 구현

👉🏻 항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!


⚠️ 문제 상황: 예외 안전성이 없는 코드

다음은 메뉴의 배경을 변경하는 함수이다:

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)에서 예외를 던지면 → 이후 코드가 실행되지 않는다 → 뮤텍스가 해제되지 않는다

2. 자원 오염

  • new Image(imgSrc)에서 예외를 던지면 → bgImage는 이미 삭제되었으며, imageChanges 변수가 증가된 상태이다

예외 안전성을 가진 함수라면 자원 누출/오염될 여지가 없어야 한다.


✅ 첫 번째 해결: 자원 관리 객체 활용

void PrettyMenu::changeBackground(std::istream& imgSrc) {
	Lock m1(&mutex); // 뮤텍스 획득/해제 관리 객체 (항목 14 확인)

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

이렇게 하면 자원 누출은 해결되지만, 자원 오염 문제는 여전히 남아있다.


✅ 예외 안전성을 위한 세 가지 보장 수준

1. 기본적인 보장(basic guarantee)

  • 함수 동작 중 예외 발생 시, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장
  • 그러나, 프로그램의 상태가 정확히 어떠한지 예측이 안될 수도 있음
  • 프로그램 상태가 유효하기만 하면, 어떠한 상태든 될 수 있음
  • 예시: changeBackground 함수에서 예외 발생 시, 이전 배경그림을 그대로 사용하거나, 기본적으로 세팅된 배경그림을 사용할 수도 있음

2. 강력한 보장(strong guarantee)

  • 함수 동작 중 예외 발생 시, 프로그램 상태를 절대로 변경하지 않겠다는 보장
  • 호출 성공 시: 마무리까지 완벽하게 성공
  • 호출 실패 시: 함수 호출 이전 상태로 완벽히 되돌아감
  • 2가지 상태만 존재 (성공적으로 실행 마침, 함수 호출될 때 상태)

3. 예외불가 보장(nothrow guarantee)

  • 예외를 던지지 않겠다는 보장
  • 현실적으로 어렵다

예외 안정성을 가진 함수는, 세가지 중 하나를 제공해야 한다.


🚨 예외 지정과 예외불가 보장의 차이

// 어떤 예외도 던지지 않도록, 예외 지정된 함수
int doSomething() throw();

주의점:

  • 이 함수는 예외불가 보장을 제공하지 않는다
  • 선언은 선언일 뿐, 구현이 좌우한다
  • 해당 함수에서 예외가 발생되면 매우 심각한 에러가 생긴 것으로 판단
  • 지정되지 않은 예외가 발생했을 경우 실행되는 처리자인 unexpected 함수 호출

✅ 두 번째 해결: 스마트 포인터와 문장 재배치

void PrettyMenu {
	...
	shared_ptr<Image> bgImage; // shared_ptr로 변경
	...
};

void PrettyMenu::changeBackground(istream& imgSrc) {
	Lock m1(&mutex);

	// 실제로 배경이 변경되기 전엔, 객체의 상태가 변경되지 않는다.
	bgImage.reset(new Image(imgSrc));
	++imageChanges;
}

해결된 문제들:

  • PrettyMenu의 bgImage 데이터 멤버의 타입을 자원관리 전담용 포인터(shared_ptr)로 변경
  • changeBackground 함수 내 문장을 재배치하여, 배경이 변경되기 전엔 객체의 상태가 변하지 않도록

남은 주의점:

  • Image 클래스의 생성자가 실행 중 예외 발생 시, imgSrc의 입력 스트림의 읽기 표시자가 이동한 상태로 남아있을 수 있다
  • istream 타입을 대체하여 해결할 수 있다

✅ 세 번째 해결: 복사 후 맞바꾸기(copy-and-swap)

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

class PrettyMenu {
	...

private:
	Mutex mutex;
	shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(istream& imgSrc) {
	using std::swap;
	Lock(&mutex); // 뮤텍스를 잡는다.

	shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // 사본을 생성한다.
	pNew->bgImage.reset(new Image(imgSrc)); // 사본을 수정한다.
	++pNew->imageChanges;

	swap(pImpl, pNew); // 실제로 배경을 변경한다.

	// 뮤텍스를 놓는다.
}

특징:

  • 복사 후 맞바꾸기(copy-and-swap) 방식 사용
  • 강력한 예외 안정성을 제공한다
  • 객체의 데이터를 별도 구현 객체(PMImpl)에 넣어둔다 (pimpl 관용구)
  • PMImpl을 클래스가 아닌 구조체로 구성하여, 데이터를 바로 캡슐화시킨다
  • 사본 생성, 교체 작업이 이루어지기에 비용 문제가 있다

🚨 예외 안전성의 한계

1. 함수 호출 체인의 문제

void someFunc() {
	...
	f1();
	f2();
	...
}

문제:

  • f1, f2 함수의 예외 안전성이 강력하지 않을 수 있다
  • 함수의 예외 안전성 수준은, 내부 함수의 최소 예외 안전성에 기반한다

2. 부수효과의 문제

문제:

  • 모든 함수가 강력한 예외 안전성을 보장하더라도, 문제가 생길 수 있다
  • f1에서 데이터베이스 변경사항이 일어난다면, 방법이 없다 (함수의 부수효과)

🧐 정리

  1. 예외 안정성을 갖춘 함수는 실행 중 예외가 발생하더라도, 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려두지 않는다.
  2. 예외 안전성 보장의 수준은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.
  3. 강력한 예외 안전성 보장은 복사 후 맞바꾸기(copy-and-swap) 방법을 써서 구현할 수 있지만, 모든 함수에 대해 실용적인 것은 아니다.
  4. 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 최소 예외 안전성을 넘지 않는다.
  5. 예외 안전성 보장 세가지 중 실용적으로 제공할 수 있는 보장을 결정하고, 문서로 남겨 이후의 사용자가 파악할 수 있도록 하자.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기