[C++] 항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 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에서 데이터베이스 변경사항이 일어난다면, 방법이 없다 (함수의 부수효과)
🧐 정리
- 예외 안정성을 갖춘 함수는 실행 중 예외가 발생하더라도, 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려두지 않는다.
- 예외 안전성 보장의 수준은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.
- 강력한 예외 안전성 보장은 복사 후 맞바꾸기(copy-and-swap) 방법을 써서 구현할 수 있지만, 모든 함수에 대해 실용적인 것은 아니다.
- 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 최소 예외 안전성을 넘지 않는다.
- 예외 안전성 보장 세가지 중 실용적으로 제공할 수 있는 보장을 결정하고, 문서로 남겨 이후의 사용자가 파악할 수 있도록 하자.
댓글남기기