로또
29. 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자! 본문
다루는 내용
예외 안전성의 세 가지 보장을 알아보고 구현 전략을 알아보자. 또한 세 가지 보장 중 하나를 선택하는 과정에 대해서도 살펴보자.
예외 안전성을 가진 함수 만들기
// 배경 그림을 제공하는 GUI 메뉴 클래스
// 스레드 환경 동작 가능
class PrettyMenu{
public:
void ChangeBackground(istream& img_src){
lock(&mutex);
delete bg_image;
image_change_cnt++;
bg_image = new Image(img_src);
unlock(&mutex);
}
private:
Mutex mutex; // 해당 객체를 위한 뮤텍스
Image* bg_image; // 현재 배경 그림
int image_change_cnt; // 배경 그림이 변경 횟수
};
예외 안전성 측면에서 볼 때, 위 코드는 굉장히 나쁘다고 말할 수 있다.
예외 안전성을 가진 함수라면 예외가 발생할 때 다음과 같이 동작해야 한다.
1. 자원이 새도록 만들지 않는다.
bg_image = new Image(img_src) 표현식에서 예외가 발생하면, mutex는 lock이 걸린 채로 남게 된다.
2. 자료구조가 더럽혀지는 것은 허용하지 않는다.
bg_image = new Image(img_src) 표현식에서 예외가 발생하면, bg_image가 가리키는 객체는 삭제된 채로 남게 된다.
또한, 새 배경 사진이 올바르게 적용되지 않았는데, image_change_cnt 변수값은 증가하였다.
이는 unique_lock과 같은 RAII 클래스를 이용하면 해결할 수 있다. 이들은 소멸자에서 lock을 해제하여 중간에 예외가 발생하더라도 mutex의 lock이 자동으로 해제된다.
예외 안전성의 세 가지 보장
1. 기본적인 보장(basic guarantee)
함수 동작 중 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장이다.
어떤 객체나 자료구조도 더럽혀지지 않으며 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다.
하지만 프로그램의 상태가 정확히 어떠한지는 예측이 안 될 수도 있다.
예를 들어, PrettyMenu 객체의 ChangeBackground 함수에서 예외가 발생했을 때 해당 객체는 이전의 배경 그림을 사용할 지, 기본 배경그림을 사용할 지는 함수를 만든 사람에게 달려 있을 뿐, 사용자는 예측할 수 없다.
2. 강력한 보장(strong guarantee)
함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다.
이런 함수를 원자적인(atomic) 동작이라고 할 수 있다. 함수 호출 중 예외가 발생하면 함수 호출이 없었던 것처럼 프로그램의 상태가 돌아간다.
이는 예측할 수 있는 프로그램의 상태가 성공 / 실패 둘 중 하나이므로 사용하기 편하다.
이와 대조적으로 기본적인 보장은 예외 발생 시 프로그램이 있을 수 있는 상태가 그냥 유효하기만 하면 어떤 상태도 될 수 있다.
3. 예외불가 보장(nothrow guarantee)
예외를 절대로 던지지 않겠다는 보장이다.
기본제공 타입(int, 포인터 등)들이 이에 해당한다.
어떤 예외도 던지지 않게끔 예외 지정이 된 함수는 예외불가 보장을 제공하는 것이 아니라, 매우 심각한 에러가 발생한 것으로 판단하여 지정되지 않은 예외가 발생했을 경우에 실행되는 처리자인 unexpected 함수가 호출되어야 한다는 뜻이다.
int DoSomething() throw(); // 비어있는 예외 지정
예외 안전성을 갖춘 함수는 위 세 가지 보상 중 하나를 제공해야한다.
예외불가를 보장하는 것은 훌륭하지만 예외를 던지는 함수를 호출하지 않는 것은 현실적으로 어려워, 대부분의 함수에 있어 기본적이 보장과 강력한 보장 중 하나를 선택하게 된다.
ChangeBackground()에서 강력한 보장 제공하기
해당 함수에서 거의 강력한 보장을 제공하기 위해 변경해보자.
1. bg_image 변수의 type을 스마트 포인터로 변경하여 자원 누출을 막자.
2. 함수 내 문장을 재배치하여 배경그림이 변경되기 전에는 image_change_cnt가 증가되지 않도록 하자. 어떤 동작이 일어났는지를 나타내는 객체를 사용하는 경우, 해당 동작이 실제로 일어날 때까지 해당 객체의 상태를 바꾸지 않는 편이 일반적으로 좋다.
class PrettyMenu{
public:
void ChangeBackground(istream& img_src){
unique_lock<shared_mutex> lock(mutex); // 쓰기 전용 Lock 걸기
bg_image.reset(new Image(img_src)); // 스마트 포인터의 내부 포인터 변경
image_change_cnt++;
}
private:
shared_mutex mutex; // RAII 기반 Lock 관리 클래스
shared_ptr<Image> bg_image; // RAII 기반 Pointer 관리 클래스
int image_change_cnt;
};
이러한 코드가 "거의" 강력한 보장인 이유는, 매개변수인 istream& img_src 때문이다.
Image 클래스의 생성자에서 예외가 발생할 때, 입력 스트림의 읽기 표시자가 이동한 채로 남아 있을 가능성이 존재하고, 이는 전체 프로그램의 나머지에 영향을 미칠 수 있기 때문이다.
따라서, 해당 문제를 해소하기 전까지 ChangeBackground()가 제공하는 예외 안전성 보장은 기본적인 보장이다.
Copy and Swap(복사 후 맞바꾸기)
강력한 예외 안전성 보장을 제공하는 일반적인 설계 전략으로, 어떤 객체를 수정하고 싶을 때, 그 객체의 사본을 만들어(Copy) 수정하는 것이다. 이는 수정 중 예외가 발생하더라도 객체의 원본은 바뀌지 않은 채로 남아있다.
수정이 성공적으로 완료된 후, 수정된 객체를 원본 객체와 맞바꾼다.(Swap) 해당 작업을 '예외를 던지지 않는' 연산 내부에서 수행한다.
이 전략은 이전 항목에서 보았던 'pimpl 관용구'를 사용하여 구현할 수 있다.
struct PMImpl{
shared_ptr<Image> bg_image;
int image_change_cnt;
};
class PrettyMenu{
public:
void ChangeBackground(istream& img_src){
using std::swap; // 25. 예외를 던지지 않는 swap에 대한 지원도 생각해보자.
unique_lock<shared_mutex> lock(mutex);
shared_ptr<PMImpl> p_new(new PMImpl(*pimpl)); // Copy
p_new->bg_image.reset(new Image(img_src)); // 수정
(p_new->imange_change_cnt)++; // 수정
swap(pimpl, p_new); // Swap
}
private:
shared_mutex mutex; // RAII 기반 Lock 관리 클래스
shared_ptr<PMImpl> pimpl;
};
Side Effect
Copy and Swap은 "객체의 상태를 전부 바꾸거나, 바꾸지 않거나"를 제공하는데 굉장히 유용하지만 함수 전체가 강력한 예외 안전성을 갖도록 보장하지는 않는다.
다음 코드는 Copy and Swap을 추상화한 코드이다.
void SomeFunc(){
... // Copy
f1(); // 수정
f2(); // 수정
... // swap
}
함수 f1과 f2에서 강력한 예외 안전성을 보장하지 않는다면, Copy and Swap 전략을 사용했더라도 SomeFunc 또한 강력한 예외 안전성을 보장하지 못한다고 말할 수 있다.
사실, 함수 f1, f2가 강력한 예외 안전성을 보장한다고 하더라도 어차피 f1()이 실행된다면 프로그램의 상태는 f1()에 의해 변경될 것이고, 그 후 f2()가 실행되다 예외를 던지면 해당 프로그램의 상태는 SomeFunc()를 호출하기 전과 달라져 있을 것이다.
함수의 Side Effect로 인해 이러한 문제는 더욱 커진다. 위 예시처럼 자기 자신에 국한된 데이터들만 변경하는 경우에는 강력한 예외 안전성을 보장하기가 수월하다. 하지만 f1()의 Side Effect로 데이터베이스를 수정한다던가 하면 SomeFunc()는 할 수 있는 것이 없다.
강력한 예외 안전성 보장은 Side Effect와 더불어 Copy and Swap 시 발생하는 오버헤드 등 여러 문제가 함께한다.
물론 강력한 예외 안전성 보장은 예외 안전성 보장 중에 가장 좋다. 그러니 실용성이 확보되는 경우라면 반드시 제공하되, 그렇지 못하다면 기본적인 보장을 제공하도록 하자.
이것만은 잊지 말자!
- 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않습니다. 이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외불가 보장이 있다.
- 강력한 예외 안전성 보장은 복사 후 맞바꾸지 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.
- 어떤 함수가 제공하는 예외 안전성 보장의 강도는 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.
'책 > Effective C++' 카테고리의 다른 글
| 32. public 상속 모형은 반드시 "is-a(...는 ...의 일종이다)"를 따르도록 만들자. (1) | 2023.12.01 |
|---|---|
| 30. 인라인 함수는 미주알고주알 따져서 이해해 두자. (0) | 2023.11.29 |
| 28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자. (1) | 2023.11.27 |
| 27. 캐스팅은 절약, 또 절약! 잊지 말자 (3) | 2023.11.25 |
| 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자. (1) | 2023.11.24 |