Item 29: 예외 안정성이 확보되는 그날 위해 싸우고 또 싸우자

1
2
3
4
5
6
7
8
9
10
class PrettyMenu {
public:
    ...
    void changeBackground(std::istream& imgSrc);
    ...
private:
    Mutex mutex;
    Image* bgImage;
    int imageChanges;
};
1
2
3
4
5
6
7
8
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);
    delete bgImage;
    ++imageChanges;
    bgImage = new Image(imgSrc);
    unlock(&mutex);
}

예외 안정성을 확보하는 작업은 매우 힘든일이다. 위 코드는 예외 안정성측면에서 매우 취약하다. 예외 안정성을 확보하기 위해선 두 가지의 요구사항을 맞춰야 하는데, 이 함수는 어느 요구사항에도 맞지 않는 위험천만한 함수다.

  • 자원이 새도록 만들지 않는다.
    • 위 코드는 new Image(imgSrc)표현식에서 예외를 던지면 unlock 함수가 실행되지 않게 되어 뮤텍스가 계속 잡힌 상태로 남기 때문이다.
  • 자료구조가 더렵혀지는 것을 허용하지 않는다.
    • imgsrc가 예외를 던지면 bgImage가 가리키는 객체는 이미 삭제된 상태이다.

자원 누출의 문제는 사실 이미 다룬 Item13Item14에서도 다루었던 내용이기에 해당 아이디어로 해결할 수 있다. 최근에는 Rule of Zero를 따르는 것이 좋다.

자료구조 오염 문제

앞서 Lock 자원관리 전담 클래스를 두어 해결했다면(자원 관리 클래스를 두어 코드 스코프 내에 C++의 특성인 생성자와 소멸자를 이용해 자동으로 자원을 관리하는 방법) 이번에는 자료구조 오염 문제가 있다.

그전에 예외 안전성을 갖춘 함수는 아래 세 가지 보장 증 하나를 제공한다.

  • 기본적인 보장(basic guarantee)
    • 함수 동작 중에 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다는 보장이다. 어떤 객체나 자료구조도 더렵혀지지 않으며, 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다. (즉, 모든 클래스 불변속성이 만족된 상태, 비트상수성이 만족된 상태)
  • 강력한 보장(strong guarantee)
    • 함수 동작 중에 예외가 발생하면, 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다. 이런 함수를 호출하는 것은 원자적인(atomic) 동작이라고 할 수 있다. 호출이 성공하면(예외가 발생하지 않으면) 마무리까지 완벽하게 성공하고, 실패하면 함수 호출이 없었던 것처럼 원래 상태로 복구한다.
  • 예외불가 보장(nothrow guarantee)
    • 예외를 절대로 던지지 않겠다는 보장이다. 약속한 동작은 언제나 끝까지 완수하는 함수라는 뜻이다. 기본 제공 타입에 대한 모든 연산은 예외를 던지지 않게 되어 있다. (즉, 예외불가 보장이 제공된다.) 예외에 안전한 코드를 만들기 위한 가장 기본적이며 핵심적인 요소이다.

앞서 말한 예외 안전성을 갖춘 함수는 위의 세 가지 보장 중 하나를 반드시 제공해야 한다. 아무 보장도 제공하지 않으면 예외에 안전한 함수가 아니다. 대부분의 경우 기본적인 보장과 강력한 보장중에 선택하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PrettyMenu {
    ...
    std::tr1::shared_ptr<Image> bgImage;
    ...
};

void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock ml(&mutex);

    bgImage.reset(new Image(imgSrc));

    ++imageChanges;
}

이 코드를 보면 RAII기법과 같이 이전의 배경 그림을 프로그래머가 직접 삭제할 필요가 없다. 배경그림이 스마터 포인터에 의해 관리되고 있기 때문이다. 이렇게 예외 안전성을 보장한다.

다만 매개변수인 imgSrc는 Image클래스의 생성자를 실행하다. 에외를 일으킬 때, 그 시점에 입력 스트림의 읽기 표시자가 이동한 채로 남아있을 가능성이 충분히 있다. 이 문제를 해결하기 전까지 changeBackground 함수는 예외 안정성은 기본적인 보장이다.

복사 후 맞바꾸기(copy-and-swap) 기법

복사 후 맞바꾸기 기법은 일반적인 함수를 강력한 예외 안전성을 제공하는 함수로 만드는 설계 전략이다. 어떤 객체를 수정하고 싶다면 그 객체의 사본을 만들어 놓고 그 사본을 수정하는 것이다. 이렇게 되면 동작 중에 실행되는 연산에서 예외가 던져지더라도 원본 객체는 바뀌지 않은 채로 남아 있는 것이다.

‘진짜’ 객체의 모든 포인터를 별도의 구현 객체에 넣어두고, 그 구현 객체를 가리키는 포인터를 진짜 객체가 물고 있게 하는 식으로 구현한다.

이러한 강력한 예외를 보장하는 함수가 있더라도 side effect가 없는 함수로서 존재해야 한다. 이는 함수의 중첩이나 앞서 설명한 다른 함수에 의한(예외) 연쇄작용이 없어야 한다는 뜻이다.

정리

새로운 함수를 만들거나 기존의 코드를 고칠 때는 ‘어떻게 하면 예외에 안전한 코드를 만들까?’를 진지하게 고민하는 버릇을 들여야 한다. 최근에는 Rule of Zero를 따르는 것이 좋다.

  • 예외 안정성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않는다. 이런 함수들이 제공할 수 있는 예외 안정성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있다.
  • 강력한 예외 안정성 보장은 ‘복사 후 맞바꾸기’ 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아니다.
  • 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.

태그: ,

카테고리:

업데이트:

댓글남기기