페이지

2015년 1월 4일 일요일

예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!

class PrettyMenu {
public:
    void changeBackgrounf(std::istream& imgSrc);
private:
    Mutex m;
    Image* bg;
    int cnt;   // 배경그림이 바뀐 횟수
};

void PrettyMenu::changeBackground(std::istream& s)
{
    lock(&m);
    delete bg;
    ++cnt;
    bg = new Image(s);
    unlock(&m);
}

 위의 Code는 예외 안전성(Exception Safety) 측면에서 볼 때 "이보다 더 나쁠 수는 없다" 고 말할 수 있다.

* 예외 안전성을 갖춘 함수는 실행 중 예외가 발생되더라도 자원을 누출시키지 않으며 자료구조를 더럽힌 채로 내버려 두지 않습니다. 이런 함수들이 제공할 수 잇는 예외 안전성 보장은 기본적인 보장, 강력한 보장, 예외 금지 보장이 있습니다.

 예외 안전성을 가진 함수라면 예외가 발생할 때 이렇게 동작해야 한다.

 1. 자원이 새도록 만들지 않는다.
     위 Code는 new Image에서 예외가 발생하면 unlock이 되지 않는다.

 2. 자료구조가 더럽혀지는 것을 허용하지 않는다.
     위 Code는  new Image에서 예외가 발생하면 bg는 이미 삭제되어서 의미없게 된다.

 그럼 어떻게 해결하느냐 ? 간단하다. 자원관리 전담 Class를 사용하면 해결된다.


class Lock {
public:
    explicit Lock(Mutex *pm) : mutexPtr(pm) { lock(mutexPtr); }
    ~Lock() { unlock(mutexPtr); }
private:
    Mutex *mutexPtr;
};

void PrettyMenu::changeBackground(std::istream& s)
{
    Lock l(&m);

    delete bg;
    ++cnt;
    bg = new Image(s);
}




 Lock 객체를 사용하면 중간에 예외가 발생하더라도 unlock이 자동으로 실행된다. 심지어 code 상에 unlock을 호출 할 필요도 없어진다.
이로서 자원 누출 문제는 해결이 되었는데 여전히 자료구조 오염 문제가 남아있다.

 예외 안전성을 갖춤 함수는 다음 세 가지 보장(guarantee) 중 하나를 반드시 제공해야 한다.

 1. 기본적인 보장(basic guarantee)
     함수 동작 중에 예외가 발생하면, 실행중에 관련된 모든 것을 유효한 상태로 유지하겟다는 보장이다. 하지만 프로그램의 상태가 정확히 어떠한지는 예측이 안 죌 수도 있다.

 2. 강력한 보장(string guarantee)
     함수 동작 중에 예외가 발생하면, 프로그램 상태를 절대로 변경하지 않겠다는 보장이다. 이러한 동작을 원자적(atomic) 동작이라고 할 수 있다. D/B 제어의 Transaction을 생각하면 이해가 편하겠다. 예측할 수 있는 상태가 두 개 밖에 되지 않아서 쓰기 편하다.

 3. 예외불가 보장(nothrow guarantee)
     예외를 절대로 던지지 않겠다는 보장이다. 이 함수는 언제나 끝까지 완수한다. 기본제공 타입에 대한 모든 연산이 이에 해당한다.

* 강력한 예외 안전성 보장'복사 후 맞바꾸기(copy and swap)' 방법을 써서 구현할 수 있지만, 모든 함수에 대해 강력한 보장이 실용적인 것은 아닙니다.

class PrettyMenu {
    ...
    std::tr1::shared_ptr<Image> bg;
    ...
};

void PrettyMenu::changeBackground(std::istream& s)
{
    Lock l(&m);
    bg.reset(new Image(s));
    ++cnt;
}


 Image*를 자원관리 전담용 포인터로 바꾸는 것 만으로로도 강력한 예외 안전성 보장을 제공할 뿐 아니라 자원 관리까지 해준다.

 일반적인 경우 복사 후 맞바꾸기(copy and swap)만으로도 강력한 예외 안전성 보장을 구현할 수 있다. 그 원리는 무척 간단하다. 어떤 객체를 수정하고 싶으면 그 객체의 사본을 하나 복사해놓고 그 사본을 수정한다. 그래서 수정중 예외가 발생하면 원본 객체는 바뀌지 않은 채로 남아 있는거고 성공적으로 모두 오나료되면 수정된 객체를 원본 객체와 맞바꾸는 것이다. 단 이 맞바꾸는 작업은 예외를 던지지 않는 연산 내부에서 수행해야 한다.

 이 경우는 대개 '진짜' 객체의 모든 데이터를 별도의 구현(implementation) 객체에 넣어두고 그 구현 객체를 가리키는 포인터를 진짜 객체가 물고 있게 하는 식으로 구현한다.

struct PMImpl {
    std::tr1::shared_ptr<Image> bg;
    int cnt;
};

class PrettyMenu {
    ...
private:
    Mutex m;
    std::tr1::shared_ptr<PMImpl> pImpl;
};

void PrettyMenu::changeBackground(std::istream& s)
{
    using std::swap;
    Lock l(&m);
    std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // 복사
    pNew->bg.reset(new Image(s)); // 사본을 수정
    ++pNew->cnt;
    swap(pImpl, pNew); // 맞바꾸기
}


* 어떤 함수가 제공하는 예외 안전성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않습니다.

void someFunc() {
    ... // 사본 복사
    f1();
    f2();
    ... // 변경된 상태 맞바꾸기
}


 위의 code의 경우 f1(), f2() 가 각각 강력한 예외 안전성을 보장한다고 하더라도 f1()이 성공적으로 끝났을 때 프로그램의 어떤 부분이 바뀌어 있다면 f2()에서 오류가 발생했을 시 f1()이 실행되기 전의 상태로 돌아 가지는 않는다. 예를들어서 데이터베이스에 뭔가 값을 바꾸고 commit까지 했을 경우라고 생각을 하면 이해가 쉬울것이다. 여기서 불거지는 문제가 함수의 부수효과 (side effect)이다.

 그래도 되도록이면 강력한 보장이 가장 좋다. 하지만 강력한 보장이 제공할 수 없다면 기본적인 보장쪽으로 눈을 돌릴 수밖에 없을 것이다. 실용성이 확보될 때만 강력한 보장을 제공하는데 힘써라. 일부만 예외 안전성을 갖춘 시스템은 없다. 예외에 안전하거나 뚤려 있거나 둘 중 하나다. 예외 안전성이 없는 함수가 한 개라도 쓰이면 그 시스템은 전부가 예외에 안전하지 않은 시스템이다.



댓글 없음:

댓글 쓰기