»
S
I
D
E
B
A
R
«
[개발자 역량] 예외 안전성
Nov 2nd, 2009 by Wegra Lee

이번엔 예외 안전성(exception safety)에 대해 간략히 정리해보겠다.

좋은  플랫폼/라이브러리/모듈 등을 만드는데 꼭 고려해야할 요소들 중, exception safety 는 그 인지도가 특히 낮다고 할 수 있다. 정상적인 상황에서는 부각되지 않고, 한 번 문제가 발생하면 원인을 찾기 어렵다는 점은 thread safety 와도 비슷하다. 이러한 특성 때문에 견고한 플랫폼을 만들고자 하는 사람들은 반드시 염두해 두어야 한다.

상대적으로 가벼운 프로젝트에서라도 틈틈히 적용하여 체화시켜두면 나중에 시행착오를 줄일 수 있다. 항시 완벽하게 만들려는 것은 노력 대비 얻는 것이 적을 수도 있으니, 코드 리뷰를 하면서 종종 exception safety 관점에서 들여다보는 방식을 권해본다.

다행히도 이 주제는 이미 Wikipedia 에 잘 정리되어 있으므로[1], 한글 번역 + 약간의 부연 설명 수준에서 마무리하겠다.


Exception safety

특정 코드 블럭 안에서 실행중 실패(failure)가 발생해도 잘못된 작용을 일으키지 않는다면, 우리는 그 코드 블럭을 exception-safe 하다고 한다. 잘못된 작용의 예로는 메모리 누수, 변질된(garbled) 데이터/상태 저장, 잘못된 결과 반환 등이 있다. Exception safe 코드는 예외가 발생한 상황에서도 그 코드상에서의 불변성(invariant [2])을 만족시켜야 한다. 그럼 exception safety 를 레벨에 따라 몇 개로 나눠보자.

  1. Failure transparency (no throw guarantee): 예외 상황에서도 요청된 기능이 반드시 성공하고, 그 외 모든 요구사항(성능, 메모리  사용량 등)도 만족시킴을 보장한다. 발생한 예외도 위로 전파시키지 않고 투명하게 처리된다. (가장 이상적인 exception safety)
  2. Commit or rollback semantics (strong exception safety, no-change guarantee): 기능은 실패할 수 있으나, 그로 인한 어떠한 부작용(side effect)도 발생하지 않는다. 모든 데이터/상태는 원래의 값을 보존한다.
  3. Basic exception safety: 기능 수행 도중 일부 과정에서 부작용을 유발시킬 수 있다. 단 불변성(invariant)은 그대로 유지된다. 관련 데이터들 중 일부가 변경되었을 수는 있어도, 단위 데이터 각각은 유효한 값이어야 한다. 즉 invalid sate 에 빠지거나 스팩상 허용되지 않는 잘못된 데이터를 가지고 있어서는 안된다.
  4. Minimal exception safety (no-leak guarantee): 기능 실패의 결과 유효하지 않은 데이터를 가질 수도 있으나, 최소한 크래쉬, 자원 누수는 발생하지 않는다.
  5. No exception safety: 어떠한 것도 보장하지 않는다. (최악의 exception safety)

예를 들어, C++의 std::vector 나 Java 의 ArrayList 와 같은 벡터를 생각해보자. 아이템 x 를 벡터 v 에 넣으면, 벡터 v 는 x 가 내부 객체 리스트에 추가되고, 총 객체 수를 의미하는 count 값을 1 만큼 증가시켜야 한다. 또한 확보해놓은 메모리가 충분치 않다면 새로 메모리를 할당하는 작업도 필요하다. 이 메모리 할당 작업은 실패할 수 있고, 그렇다면 예외가 던져질 것이다. 마지막 이유로 벡터를 failure transparency 레벨로 구현하기란 굉장히 어렵거나 혹은 불가능할 수도 있다. 다행히 strong exception safety 정도를 제공하는 것은 그다지 어렵지 않은데, 동작에 실패하더라도 v 를 이전과 동일한 상태로만 유지하면 된다. 만약 basic exception safety 만 보장하도록 만들어진 벡터라면, v 는 x 를 포함할 수도, 아닐 수도 있다. 단, 어느 경우건 포함 여부와 count 값 사이는 일관된 상태를 유지한다. 반면 minimal exception safety 만 보장하는 벡터에서는 예외가 발생하면 v 는 잘못된 상태에 놓일 수 있다. 예를 들면, x 가 v 에 포함되지 못했음에도 count 는 증가된 상태로 남을 수도 있다. 마지막으로 no exception safe 백터는 프로그램을 크래쉬 시킬 수도 있다. 메모리 할당에 실패했음에도, 이를 확인하지 않고 잘못된 메모리 주소에 데이터를 쓰는 경우가 이에 해당한다.

일반적으로  ’최소한’ basic exception safety 는 보장해 주어야 한다. Failure transparency 가 이상적이긴 하나 구현하긴 만만치 않다. 어플리테이션에 대한 완벽한 정보게 제공되지 않는다면 라이브러리가 이를 보장하기란 대부분 불가능하다.


첨언..

아무런 생각 없이 구현했다면 no exception safety 라 말하는 것이 안전하다. 비록 API 에 따라서는 더 높은 safety 를 보장하는 경우도 많이 있더라도, 전체의 safety 는 가장 낮을 레벨에 좌우될 수 밖에 없다. 물론 모든 API 를 리뷰하여 최소 safety 가 어디인지를 파악한다면, 그 이상의 safety 를 보장한다고 말할 수 있다.

메모리 관리와 null pointer 체크 정도를 신경썼다면 minimal exception safety 정도라 이야기할 수 있다. 또한 대부분의 정적 분석 툴들[3]은 resource leak 과 잘못된 메모리 접근 문제를 검출해 주므로, minimal exception safety 보장하는데 많은 도움이 된다.

Basic exception safety 수준까지 끌어 올리려면 메모리와 포인터뿐 아니라 인스턴스의 속성(property, field, or private member variable)의 변화까지 신경써야 한다. 한 함수 내에서 두 개 이상의 속성을 다루고, 뒷의 속성 조작 중 예외가 발생하더라도 invariant 조건이 만족되도록 신경써야 한다.

Commit or rollback semantics 는 어느 단계에서 예외가 발생했더라도 원상태 그대로 복구시킬 수 있어야 한다. 즉, 문제가 된 동작을 애초부터 시도하지 않은 것과 동일한 결과를 낳아야 한다. 종종 invariant 를 신경쓰는 것보다 수월하게 구현할 수도 있지만, file 이나 database 를 건드리거나 네트워크로 서버에 요청을 보내는 등의 동작이 포함된다면 결코 쉬운 작업이 아니다. 더욱이 multi thread 환경이라면 동기화까지 고려되어야 한다.

아주 특별한 경우가 아니면 Failure transparency 보장을 요하는 경우는 없으니, 자신이 mission critical 분야에 종사하고 있다고 생각하지 않는다면 잊어도 좋다.

추천 전략

대부분의 프로젝트에서는 아래 정도의 전략이면 충분히 만족스러운 결과를 얻을 수 있을 것이라 믿는다.

  1. 전체 API 의 basic exception safety 를 기본 목표로 한다.
  2. 핵심이 되는 모듈과 주요 데이터를 다루는 모듈을 미리 식별하여 최대한 commit or rollback semantics 을 보장하도록 노력하되, 집착할 필요는 없다. 단, 보장 레벨이 달라질 경우 문서나 코드 상에 annotation 주석을 달아두자[4].
  3. 정적 분석 툴을 적극 도입하여 human error 를 빨리 잡아내고, 최소한의 방어책으로 활용한다.

References

  1. Exception safety (Wikipedia)
  2. Invariant (Wikipedia)
  3. List of tools for static code analysis (Wikipedia)
  4. [개발자 역량] 주석.. 다느냐 마느냐 그것이 문제로다 (Bug Inside)
»  Substance: WordPress   »  Style: Ahren Ahimsa