[Effective C++] #28_예외 안전성 확보
Scott Meyers의 "Effective C++" 를 통해, C++ 구현에 필요한 개념들을 이해하고, 기록하기 위함입니다. 해당 항목은 5장 '구현', 항목 29 "예외 안전성이 확보되는 그날 위해 싸우자" 에 해당하는 내용입니다.
예외 안전성 보장에 실패한 예제
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc);
...
private:
Mutex mutex;
image *bgImage;
int imageChanges;
};
//changeBackground 함수 내부 정의
void PrettyMenu::changeBackground(std::istream& imgSrc) {
lock(&mutex); // Mutex lock -> 동기화 작업을 위한 준비
delete bgImage; // 기존의 배경화면 이미지 소스를 제거
++imageChanges;
bgImage = new Image(ImgSrc);
unlock(&mutex);
}
위 예제의 changeBackground 함수는 예외 안전성을 보장하지 못합니다. 먼저, new Image(ImgSrc) 표현식에서 예외를 던지면, "unlock"이 실행되지 않아, mutex가 계속 잡히죠. 추가적으로 bgImage는 이미 삭제되었고, imageChanges 값은 증가한 후의 상태가 되겠죠. 따라서, 예외 안전성을 보장하는 함수는 아래 2가지 항목을 만족시킵니다.
1. 자원이 새도록 만들지 않습니다.
2. 자료구조가 더럽혀지는 것을 허용하지 않습니다.
//changeBackground 함수 내부 정의
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock m1(&mutex); // 자원 관리 객체 RAII 기법을 사용합니다.
delete bgImage; // 기존의 배경화면 이미지 소스를 제거
++imageChanges;
bgImage = new Image(ImgSrc);
}
RAII 기법의 자세한 내용은 추후에 14항목에서 다루겠습니다. 우선 코드를 살펴보겠습니다. m1 자원 관리 객체를 통해 우리는 changeBackground 함수 내부에 unlock 메서드를 생략해줍니다. 따라서, new Image(ImgSrc) 문에서 예외를 던져도 mutex unlock을 보장해줍니다.
1. 기본적인 보장
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc);
...
private:
Mutex mutex;
// image *bgImage
std::tr1::shared_ptr<Image> bgImage; // image *bgImage를 수정
int imageChanges;
};
//changeBackground 함수 내부 정의
void PrettyMenu::changeBackground(std::istream& imgSrc) {
...
bgImage.reset(new Image(imgSrc));
++imageChanges;
...
}
예외 안전성을 갖춘 함수는 "기본적인 보장"을 제공합니다. "기본 적인 보장"이란, 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장입니다, 즉 내부적으로 일관성을 유지한다는 의미가 되겠죠.
위 코드를 살펴보겠습니다. changeBackground "기본적인 보장"을 제공하는 함수로 수정했습니다. 먼저 image *bgImage 즉 배경 이미지에 대한 일반 포인터를, 스마트 포인터(std::tr1::shared_ptr)로 변경하여 자원관리를 자동으로 처리하게 수정합니다. 즉, changeBackground 함수 내부에서 직접 Image 객체를 삭제하는 작업은 필요하지 않겠습니다. 추가적으로, imgSrc를 인수로 받아 새롭게 Image 객체를 동적 받는 활동을 reset 함수의 매개변수로 넣어 주었습니다. 그러므로, new Image(imgSrc) 가 제대로 작동해야만, reset작업이 실행될 수 있도록 합니다.
2. 강력한 보장
예외 안전성을 갖춘 함수는 "강력한 보장"을 제공합니다. 프로그램의 상태를 절대 변경하지 않겠다는 보장입니다. 따라서, 이러한 함수를 호출하는 것은 "원자적인 동작"으로 볼 수 있습니다. 예를들면, 함수 호출이 성공하면, 마무리까지 완벽하게 성공하고, 호출이 실패하면, 마치 함수 호출이 없었던 것처럼 이전의 프로그램 상태로 되돌아 갑니다.
class PrettyMenu {
...
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
struct PMImpl {
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
}
void PrettyMenu::changeBackground(std::istream& imgSrc){
using std::swap;
Lock m1(&mutex);
std::tr1::shared_ptr<PMImpl> pNew (new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl, pNew);
}
위 코드는 changeBackground 함수를 "강력한 보장"을 제공하는 함수로 수정한 결과입니다. 일반적인 함수를 "강력한 보장"을 제공하는 함수로 탈바꿈 하기위한 대표적인 전략은 "복사 후 맞바꾸기"가 있습니다. 원리는 간단합니다. 어떤 객체를 수정하고 싶다면, 그 객체의 사본을 만들어 놓고 그 사본을 수정합니다. 수정이 완료되면, 원본 객체와 맞바꿉니다. 이때, 전에 다루었던 "pImpl 관용구" 구현방법을 사용합니다. 아래 링크를 참조하세요.
PrettyMenu 객체 내부의 스마트 포인터를 통해 사본 객체에 대한 포인터(pNew)를 생성합니다. pNew를 통해 사본 객체를 수정하고, 원본 객체를 가리키는 포인터, 여기서는 pImpl을 다시 pNew와 "swap" 합니다. 결론적으로, "복사 후 맞바꾸기" 전략은 객체의 상태를 전부 바꾸거나 혹은 아예 안 바꾸는 방식으로 유지하려는 경우에 유용하겠죠. 하지만, 함수 전체가 강력한 예외 안전성을 보장하지 않는다고 합니다. 왜 일까요? 아래 코드를 살펴봅시다.
void someFunc (){
... // 현재 상태에 대해 사본을 생성
f1();
f2();
... // 변경된 사본을 원본 객체와 맞바꾸기
}
여기서 우리는 "함수의 부수효과"에 대해서 얘기해보죠. 책의 저자는 자기 자신에만 국한된 것들의 상태를 바꾸며 동작하는 함수의 경우, "강력한 보장"을 제공하기 수월하다고 합니다. 반면, 비지역 데이터에 대해 부수효과를 주는 함수는 반대로 "강력한 보장"을 제공하기 어렵게 되겠죠. 예를들면, f1을 호출하고 부수효과로 DB의 내용이 변경되면, someFunc는 손쓸 수 없게 됩니다. 추가적으로, 효율 문제도 존재합니다. "복사 후 맞바꾸기" 전략은 함수 호출 중 사본을 "복사" 하고 원본 객체로 다시 맞바꾸기 하는 추가적인 시간이 소요되니까요. 결론적으로, 많은 함수들이 효율과 복잡성에서 생기는 비용 때문에 "강력한 보장"을 제공하는 함수로 구현하기 힘들어집니다. 따라서, 기본적인 보장을 우선적으로 생각해 보는 것이 좋다고 저자가 설명합니다.
실용성이 확보될 때만 강력한 보장을 제공하고, 대다수의 함수에서 무리없는 기본적인 보장을 우선하자
3. 예외불가 보장
예외 안전성을 갖춘 함수는 "예외불가 보장"을 제공합니다. 예외를 절대 던지지 않겠다는 보장이죠. 예를들면, 기본 제공 타입에 대한 모든 연산은 예외를 던지지 않죠.
결론
앞으로 우리는 '어떻게 하면 예외에 안전한 코드를 만들까?'를 진지하게 고민하는 버릇을 들여야합니다. 자원관리가 필요하다면 자원 관리용 객체(std::tr1::shared_ptr<>, 등)를 사용하는것부터 시작합니다. 그리고, 위에서 공부한 3가지 예외 안전성 보장 중 실용적이고, 효율적으로 제공할 수 있는 보장이 어떤 것일지 고민해야겠죠.
예외 안전성을 보장하는 함수는 자원을 누출시키지 않으며, 자료구조를 더럽히지 않습니다.
'언어 > Effective C++' 카테고리의 다른 글
[Effective C++] #31_파일 사이의 컴파일 의존성 (0) | 2021.12.31 |
---|---|
[Effective C++] #30_인라인 함수 (0) | 2021.12.30 |
[Effective C++] #28_내부에서 사용하는 객체에 대한 핸들 반환 (0) | 2021.12.28 |
[Effective C++] #27_캐스팅 (0) | 2021.12.27 |
[Effective C++] #26_변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2021.12.27 |