스토리텔링 개발자

[Effective C++] 29. 예외 안전성 본문

개발/Effective C++

[Effective C++] 29. 예외 안전성

김디트 2024. 6. 25. 11:36
728x90

항목 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);
}

 

 

 

예외 안전성을 확보하기 위한 두 가지 요구사항
  1. 자원이 새도록 만들지 않는다.
    • 하지만 위의 코드는 자원이 샌다.
    • "new Image(imgSrc)" 표현식에서 예외를 던지면?
      • unlock 함수가 실행되지 않게 되어 뮤텍스가 계속 잡힌 상태로 남게 된다.
    • 해결법
      • 자원 관리 객체를 사용한다.(항목 14 참조)
      • {
            Lock ml(&mutex); // 유효 범위 내에서만 동작하는 자원 관리 객체 사용
            
            delete bgImage;
            ++imageChanges;
            bgImage = new Image(imgSrc);
        }
  2. 자료구조가 더럽혀지는 것을 허용하지 않는다.
    • 자료구조가 의도한 바를 거스른다. 
    • "new Image(imgSrc)" 표현식에서 예외를 던지면?
      • bgImage가 가리키는 객체는 이미 삭제된 후이다.(쓰레기 포인터가 된다.)
      • bgImage가 invalid함에도 imageChanges변수가 증가한다.

 

 

 

예외 안전성을 갖춘 함수가 선택할 수 있는 세 가지 보장
  1. 기본적인 보장(basic guarantee)
    • 함수 동작 중 예외가 발생하면, 실행 중인 프로그램에 관련된 모든 것들을 유효한 상태로 유지하겠다고 보장한다.
    • 모든 객체 상태는 일관성을 유지한다.(즉, 모든 클래스 불변속성이 만족된 상태이다.)
    • 하지만 프로그램 상태를 예측하는 건 불가할 수 있다.
      • 예컨대, changeBackground 함수 동작 중 예외가 발생했을 경우.
      • PrettyMenu 객체는 이전 배경그림을 그대로 계속 그릴 수도 있고, 아니면 초기값 기본 배경그림을 사용할 수도 있을 것이다.
      • 즉, 유효성이 보장되긴 하나 결과 예측이 불가능하다.
  2. 강력한 보장(strong guarantee)
    • 함수 동작 중 예외가 발생하면, 프로그램 상태를 절대로 변경하지 않겠다고 보장한다.
    • 호출 성공 시 마무리까지 완벽히 성공하지만, 실패 시 호출이 없었던 것처럼 프로그램 상태가 되돌아간다.
  3. 예외 불가 보장(nothrow guarantee)
    • 예외를 절대로 던지지 않겠다고 보장한다.
    • 약속한 동작은 언제나 끝까지 완수한다.
    • 기본 제공 타입(int, 포인터 등)에 대한 모든 연산은 예외를 던지지 않도록 되어있다.
    • 어떤 예외도 던지지 않게 예외 지정이 된 함수는 예외 불가 보장을 제공한다고 생각하는 것은 잘못된 생각이다.
      • int doSomething() throw(); // 비어있는 예외 지정
        // 모던 c++에서는 noexcept 로 대체되었다.
      • 이 경우 doSomething은 절대 예외를 던지지 않겠다는 말이 아니디.
      • 만약 예외가 발생하면 매우 심각한 에러이므로 unexpected 함수(지정되지 않은 예외 발생 시 실행되는 처리자)가 호출되어야 한다는 뜻이다.
      • 함수가 어떤 특성을 갖느냐는 구현이 결정한다. 선언은 그저 선거 공약 같은 것이다.
  • 예외 불가 보장이 좋겠지만, 현실적으로는 대부분 기본적인 보장 혹은 강력한 보장 중 한 가지를 선택하게 된다.

 

 

 

위 코드도 예외 시 자료구조를 더럽히지 않도록 해결해보자
  • class PrettyMenu
    {
        ...
        shared_ptr<Image> bgImage;
        ...
    };
    
    void PrettyMenu::changeBackground(istream& imgSrc)
    {
        Lock ml(&mutex);
        
        bgImage.reset(new Image(imgSrc)); // 자원관리 포인터로 바꾸어 예외 처리를 전담시킨다.
        // 이로써 자원 할당 중 예외가 발생해도 bgImage는 이전 값이 유지된다.(강력한 보장)
        
        ++imageChanges; // bgImage가 실제 변경된 후로 옮겨 자료구조를 보장해준다.
    }
  • 강력한 보장처럼 보이지만 아직까지는 기본적인 보장에 불과하다.
    • 매개변수 imgSrc가 'new Image(imgSrc)'에서 예외가 발생할 시, 입력 스트림의 읽기 표시자가 이동한 채로 남아 있을 가능성이 충분히 있다.
    • 다양한 방법으로 보완이 가능할 것이다.
      • imgSrc를 값복사하여 전달한다.
      • img의 path를 전달해서 Image 생성자에서 istream을 생성한다.

 

 

 

복사 후 맞바꾸기(copy-andswap) 전략
  • '강력한 보장'을 쉽게 제공하는 전략이다.
  • 객체를 수정하고 싶을 때, 그 객체의 사본을 하나 만들어 놓고 그 사본을 수정한다.
  • pimpl 관용구를 사용하여 구현하는 것이 일반적이다.
  • struct PMImpl
    {
        shared_ptr bgImage;
        int imageChanges;
    };
    
    class PrettyMenu
    {
        ...
    private:
        Mutex mutex;
        shared_ptr<PMImpl> pImpl;
    };
    
    void PrettyMenu::changeBackground(istream& imgSrc)
    {
        using std::swap;
        
        Lock ml(&mutex);
        
        shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // pImpl을 복사한다.
        
        // 복사 객체에 대해 작업한다.
        pNew->bgImage.reset(new Image(imgSrc));
        ++pNew->imageChanges;
        
        swap(pImpl, pNew); // 스왑한다.
    }
  • 전부 바꾸거나 혹은 안 바꾸거나(all-or-nothing) 방식으로 유지하기 수월하다.
  • 하지만, 함수 전체를 '강력한 보장'이라 하긴 힘들다.
    • 예를 들자면 아래의 경우 '강력한 보장'이 아니다.
    • void someFunc() 
      { 
          ....  // 이 함수의 현재 상태를 사본으로 한다. 
      
          f1(); 
          f2();
          // f1(), f2()의 예외 안전성이 강력하지 않으면
          // someFunc() 역시 강력한 예외 안전성을 제공한다 볼 수 없다.
          
          ....  // 변경된 상태를 바꾸어 넣는다. 
      } 
      
       
    • 문제점
      • f1, f2가 강력한 예외 안정성을 지원한다는 보장이 없다.
      • f1, f2가 강력한 예외 안정성을 보장한다 쳐도
        • f1이 성공한 후
        • f2이 예외를 던짐
        • 의 상황이라면 f1에 의해 이미 변화한 상태를 가지므로, someFunc는 강력한 보장이라 할 수 없다.

 

 

 

  • 대다수의 함수에 있어 무리 없는 선택을 한다면 기본적인 보장이 우선이다.
  • 어떤 함수가 제공하는 예외 안정성 보장의 강도는, 그 함수가 내부적으로 호출하는 함수들이 제공하는 가장 약한 보장을 넘지 않는다.
    • 예외에 안전하거나, 예외에 뚫려 있거나 둘 중 하나이다. 일부만 예외 안전성을 갖춘 시스템 같은 것은 없다.
728x90
Comments