[Effective C++] 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); // 뮤텍스 해제
}
이 클래스는 스레딩 환경에서 동작할 수 있도록 설계되어 병행성 제어를 위해 뮤텍스를 갖고 있다. changeBackground 함수를 통해 예외 안전성에 대해 알아보자. 예외 안전성의 조건은 다음과 같다.
1. 자원이 새도록 만들지 않는다.
위 코드에서 new Image(imgSrc)에서 예외를 던지면 unlock 함수가 실행되지 않기 때문에 뮤텍스를 획득했던 자원이 샌다.
2. 자료구조가 더럽혀지는 것을 허용하지 않는다.
new Image(imgSrc)에서 예외를 던지면 bgImage가 가리키는 객체는 이미 삭제된 후고 새 그림이 깔린 것이 아닌데도, imageChanges 변수는 증가한다.
이 2가지 조건을 저 함수가 만족시키지 못하기 때문에 예외 안전성을 만족하지 못하며 설계를 고쳐야 한다.
자원 누출 문제 해결
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex); // 자원관리 객체로 뮤텍스 획득, 이 객체가 소멸될 때 알아서 해제됨
delete bgImage; // 이전 배경그림 없앰
++imageChanges; // 그림 변경횟수 증가
bgImage = new Image(imgSrc); // 새 배경그림 갱신
}
Lock이라는 자원관리 객체를 만들어서 이 객체가 소멸될 때 unlock 함수로 해제시키면 자원 누출 문제는 해결된다.
예외 안전성을 갖춘 함수가 제공하는 보장
예외 안전성을 갖춘 함수는 세 가지 보장 중 하나를 제공한다.
즉, 아무 보장도 제공하지 않으면 예외에 안전한 함수가 아니며 현실적으로 대부분의 함수에 있어서 기본적인 보장과 강력한 보장 중 하나를 고르게 된다.
기본적인 보장
함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장이다. 어떤 객체나 자료구조도 더럽혀지지 않으며, 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다. 하지만 프로그램의 상태가 정확히 어떤지는 예상이 안될 수도 있는데, 예를 들면 changeBackground 함수가 동작하다가 예외가 발생했을 때 PrettyMenu 객체는 바로 이전의 배경그림을 그대로 계속 그릴 수도 있고 아니면 기본 배경그림을 사용할 수도 있기 때문에 사용자는 예측할 수 없다.
강력한 보장
함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다. 이런 함수를 호출하는 것을 원자적인(atomic) 동작이라고 하며 호출이 성공하면 마무리까지 완벽하게 성공하고, 실패하면 호출이 없었던 것처럼 프로그램의 상태가 되돌아간다.
예외 불가 보장
예외를 절대로 던지지 않겠다는 보장이다. 약속한 동작은 언제나 끝까지 완수하는 함수라는 뜻으로 기본 제공 타입에 대한 모든 연산은 예외를 던지지 않게 되어 있다. 예외에 안전한 코드를 만들기 위한 가장 기본적이며 핵심적인 요소라고 할 수 있다. 그러나 어떤 예외도 던지지 않게끔 예외 지정이 된 함수는 예외 불가 보장을 제공한다고 생각해서는 안된다.
자료구조 오염 문제 해결 전략
기본적인 보장
class PrettyMenu
{
std::tr1::shared_ptr<Image> bgImage;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock m1(&mutex);
bgImage.reset(new Image(imgSrc));
++imageChanges;
}
bgImage를 스마트 포인터에서 관리하게 하여 알아서 해제시킨다. 그리고 배경그림이 제대로 만들어졌을 때만 reset 함수가 호출되기 때문에 제대로 안 만들어졌으면 delete도 호출되지 않는다.(reset안에 delete가 있음)
이 방법은 충분히 강력한 보장이지만 imgSrc의 생성자 호출 과정에서 에러 발생 시 문제가 일어나기 때문에 "기본적인 보장"이다.
강력한 보장
struct
{
std::tr1::shared_ptr<Image> bgImage;
int imageChanges;
};
class PrettyMenu
{
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
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)
}
복사 후 맞바꾸기(copy-and-swap)를 사용하여 새로운 객체의 사본을 만든 뒤 그걸 수정해서 바꿔버리는 것이다.
'진짜' 객체의 모든 데이터를 별도의 구현(implementation)객체에 넣어두고 그 구현 객체를 가리키는 포인터를 진짜 객체가 물고 있게 한다.
즉, 연산에서 예외가 던져지더라도 원본 객체는 바뀌지 않고, 바꾸는 작업도 '예외를 던지지 않는' 연산 내부에서 수행한다.(ex. std::swap)
이 전략은 객체의 상태를 '전부 바꾸거나, 아예 안 바꾸거나'방식으로 유지하는데유지하는데 아주 좋지만, 함수 전체가 강력한 예외 보장을 갖도록 보장해주지는 않는다.
예시를 살펴보자. (copy paste 수법을 쓰되, f1 및 f2라는 함수의 호출문이 들어 있는 형태이다.)
void someFunc()
{
... // 이 함수의 현재 상태에 대한 사본을 만들어 놓는다.
f1();
f2();
... // 변경된 상태를 바꾸어 놓는다.
}
만약 f1, f2가 예외 안전성이 강력하지 못하면, someFunc()도 강력하지 못하다. 그리고 f1, f2가 강력한 예외 안전성을 보장하더라도 상황은 나아지지 않는다.
f1이 끝까지 실행되면 어쨌든 f1에 의해 뭔가(ex. 데이터베이스데이터 값) 변경되어있을 것이고 db값이라면, 사용자가 이미 확인해버렸을 수도 있으니 돌이킬 수 없다.
또한 효율도 무시할 수 없다. 사본을 만들다 보니 복사 공간과 소모시간을 감수해야 한다. 어쨌든, 안전성 측면에서는 강력한 보장이 제일 좋긴 하다.
C++ legacy코드는 예외 안전성 자체를 고려하지 않고 만들어진 게 많으니, 앞으로 새로운 함수를 만들거나, 기존의 코드를 고칠 때에는 어떻게 하면, 예외에 안전한 코드를 만들까를 진지하게 고민하는 버릇을 들여야 한다.
요약
- 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않는다. 이런 함수들이 제공할 수 있는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.
- 강력한 예외 안전성 보장은 '복사 후 맞바꾸기' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.
- 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 31. 파일 사이의 컴파일 의존성을 최대로 줄이자 (0) | 2022.06.25 |
---|---|
[Effective C++] 30. 인라인 함수는 미주알고주알 따져서 이해해 두자 (0) | 2022.06.19 |
[Effective C++] 28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자 (0) | 2022.06.18 |
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자 (0) | 2022.06.18 |
[Effective C++] 26. 변수 정의는 늦출 수 있는 데까지 늦추는 근성을 발휘하자 (0) | 2022.06.18 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 31. 파일 사이의 컴파일 의존성을 최대로 줄이자
[Effective C++] 31. 파일 사이의 컴파일 의존성을 최대로 줄이자
2022.06.25 -
[Effective C++] 30. 인라인 함수는 미주알고주알 따져서 이해해 두자
[Effective C++] 30. 인라인 함수는 미주알고주알 따져서 이해해 두자
2022.06.19 -
[Effective C++] 28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자
[Effective C++] 28. 내부에서 사용하는 객체에 대한 '핸들'을 반환하는 코드는 되도록 피하자
2022.06.18 -
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자
[Effective C++] 27. 캐스팅은 절약, 또 절약! 잊지 말자
2022.06.18