29: 예외 안정성이 확보되는 그날 위해 싸우고 또 싸우자!
class PrettyMenu {
public:
void changeBackground(istream& imgSrc); // 배경그림을 바꾸는 멤버 함수
private:
Mutex mutex; // 이 객체 하나를 위한 뮤텍스
Image* bgImage; // 현재의 배경그림
int imageChanges; // 배경그림이 바뀐 횟수
};
void PrettyMenu::changeBackground(istream& imgSrc)
{
lock(&mutex); // 뮤텍스 획득
delete bgImage; // 이전의 배경그림을 없앤다.
++imageChanges; // 그림 변경 횟수를 갱신
bgImage = new Image(imgSrc); // 새 배경그림을 깔아 놓는다.
unlock(&mutex); // 뮤텍스 해제
}
예외 안정성 측면에서 이 함수는 이보다 더 나쁠 수 없다. 예외 안정성을 가진 함수라면 예외가 발생할 때 이렇게 동작해야 한다.
- 자원이 새도록 만들지 않는다.
- 위의 코드에서 new Image(imgSrc)가 예외를 던진다면 unlock 함수가 실행되지 않게 되어 뮤텍스가 계속 잡힌 상태로 남는다.
- 자료구조가 더럽혀지는 것을 허용하지 않는다.
- new Image(imgSrc)가 예외를 던진다면 bgImage가 가리키는 객체는 이미 삭제된 후이고, imageChanges는 이미 증가된 상태이다.
객체를 써서 자원 관리를 전담케 하는 방법은 항목 13에 있다.
void PrettyMenu::changeBackground(istream& imgSrc)
{
Lock m1(&mutex); // 항목 14에서 가져온 객체
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
일단 자원 누출 문제는 꼬리를 내렸다.
예외 안정성을 갖춘 함수는 아래의 세 가지 보장(guarantee) 중 하나를 제공한다.
- 기본적인 보장(basic guarantee)
- 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장이다. 어떤 객체나 자료구조도 더럽혀지지 않으며, 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다.
- 강력한 보장(strong guarantee)
- 함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다. 원자적인(atomic) 동작이라고 할 수 있다. 호출이 성공하면(예외가 발생하지 않으면) 마무리까지 완벽하게 성공하고, 호출이 실패하면 함수 호출이 없었던 것처럼 프로그램의 상태가 되돌아간다.
- 예외불가 보장(nothrow guarantee)
- 예외를 절대로 던지지 않겠다는 보장이다. 약속한 동작은 언제나 끝까지 완수하는 함수라는 뜻이다. 기본제공 타입에 대한 모든 연산은 예외를 던지지 않게 되어 있다.
이제 우리가 선택해야 하는 것은 '어떤 보장을 제공할 것인가'이다. 위의 세 가지 보장 중에 하나를 고르라면 아무래도 실용성이 있는 강력한 보장이 괜찮아 보일 것이다. 하지만 현실적으로는 대부분의 함수에 있어서 기본적인 보장과 강력한 보장 중 하나를 고르게 된다.
changeBackground 함수의 경우엔 강력한 보장을 거의 제공하는 것은 어렵지 않다. 우선 첫째로, PrettyMenu의 bgImage 데이터 멤버의 타입을 기본제공 타입인 Image*에서 자원관리 전담용 포인터로 바꾼다. 둘째로, changeBackground 함수 내의 문장을 재배치하여 배경그림이 진짜로 바뀌기 전에는 imageChanges를 증가시키지 않도록 만든다.
class PrettyMenu {
public:
void changeBackground(istream& imgSrc);
private:
Mutex mutex;
tr1::shared_ptr<Image> bgImage;
int imageChanges;
};
void PrettyMenu::changeBackground(istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
이제는 이전 배경그림(Image 객체)을 프로그래머가 직접 삭제할 필요가 없게 되었다. 지금은 배경그림이 스마트 포인터의 손에서 관리되고 있기 때문이다. 게다가, 새로운 배경그림이 제대로 만들어졌을 때만 이전 배경그림의 삭제 작업이 이루어지도록 바뀌었다. 그리고 함수의 길이까지 줄어들었다. 하지만 매개변수 imgSrc가 옥의 티이다. Image 클래스의 생성자가 실행되다가 예외를 일으킬 수 있다.
강력한 예외 안정성 보장을 제공하는 함수로 만드는 일반적인 설계 전략인 '복사-후-맞바꾸기(copy-and-swap)'를 알아보자. 어떤 객체를 수정하고 싶으면 그 객체의 사본을 하나 만들어 놓고 그 사본을 수정하는 것이다. 이렇게 하면 수정 동작 중에 실행되는 연산에서 예외가 던져지더라도 원본 객체는 바뀌지 않는 채로 남는다.
이 전력은 대개 '진짜'객체의 모든 데이터를 별도의 구현(implementation) 객체에 넣어두고, 그 구현 객체를 가리키는 포인터를 진짜 객체가 물고 있게 하는 식으로 구현한다.
struct PMImpl {
tr1::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu {
public:
void changeBackground(istream& imgSrc);
private:
Mutex mutex;
tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(istream& imgSrc)
{
using std::swap;
Lock m1(&mutex);
tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // 객체의 데이터 부분을 복사
pNew->bgImage.reset(new Image(imgSrc)); // 사본을 수정
++pNew->imageChanges;
swap(pImpl, pNew); // 데이터 swap
}
'복사-후-맞바꾸기' 전략은 객체의 상태를 전부 바꾸거나 혹은 안 바꾸거나(all-or-nothing) 방식으로 유지하려는 경우에 아주 그만이다. 그러나 함수 전체가 강력한 예외 안정성을 갖도록 보장하지는 않는다는 것이 일반적인 정설이다.
void someFunc()
{
... // 이 함수의 현재 상태에 대해 사본을 만들어 놓는다.
f1();
f2();
... // 변경된 상태를 바꾸어 놓는다.
}
f1 혹은 f2에서 보장하는 예외 안정성이 '강력'하지 못하면, 위의 구조로는 someFunc 함수 역시 강력한 예외 안정성을 보장하기 힘들어진다.
여기서 불거지는 문제가 바로 함수의 부수효과(side effect)이다. 자기 자신에만 국한된 것들의 상태를 바꾸며 동작하는 함수의 경우에는 강력한 보장을 제공하기가 비교적 수월하지만 비지역 데이터에 대해 부수효과를 주는 함수는 이렇게 하기가 무척 까다롭다.
효율 문제도 무시할 수 없다. 수정하고 싶은 객체를 복사해 둘 공간과 복사에 걸리는 시간을 감수해야 한다. 어쨌든 예외 안정성 보장 중에는 강력한 보장이 가장 좋다. 실용성이 확보되는 경우라면 반드시 제공하는게 맞다. 그러나 언제나 실용적인 것은 아니다. 강력한 보장을 제공할 수 없다면 기본적인 보장 쪽으로 눈을 돌릴 수 밖에 없다.
someFunc가 호출하는 함수가 예외 안정성 보장을 전혀 제공하지 않으면, someFunc 역시 어떤 보장도 제공할 수 없게 된다. 예외에 안전하거나 예외에 뚫려 있거나 둘 중 하나이다. 예외 안정성이 없는 함수가 한 개라도 쓰이고 있으면 그 시스템은 전부가 예외에 안전하지 않은 시스템이다.
새로운 함수를 만들거나 기존의 코드를 고칠 때 '어떻게 하면 예외에 안전한 코드를 만들까'를 진지하게 고민하는 버릇을 가지자. 자원 관리가 필요할 때 자원 관리용 객체를 사용하는 것부터가 시작이다. 그리고 이번 항목에서 공부한 예외 안정성 보장 세 가지 중에 여러분이 만드는 함수에서 실용적으로 제공할 수 있는 보장은 어떤 것일지 결정하자.
이것만은 잊지 말자!
- 예외 안정성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않는다. 이런 함수들이 제공할 수 있는 예외 안정성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.
- 강력한 예외 안정성 보장은 '복사-후-맞바꾸기' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.
- 어떤 함수가 제공하는 예외 안정성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.
'C++ > Effective C++' 카테고리의 다른 글
[Effective C++] 31: 파일 사이의 컴파일 의존성을 최대로 줄이자 (1) | 2023.10.31 |
---|---|
[Effective C++] 30: 인라인 함수는 미주알고주알 따져서 이해해 두자 (0) | 2023.10.29 |
[Effective C++] 28: 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자 (0) | 2023.10.26 |
[Effective C++] 27: 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2023.10.26 |
[Effective C++] 26: 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2023.10.23 |