오류 처리

오류 처리 (Errors)

D 언어 사양의 이번 챕터는 오류를 어떻게 다룰지에 대한 이야기예요. 오류를 검출하고 보고하는 전통적인 방식이 왜 골칫거리인지부터, D가 내린 해법이 무엇인지까지 차근차근 짚어볼게요. 코드는 거의 나오지 않지만, D의 예외 처리 철학 전체를 이해하는 데 꼭 필요한 배경이니까 끝까지 따라와 주세요.

출처: https://dlang.org/spec/errors.html

본문

I came, I coded, I crashed. — Julius C'ster

모든 프로그램은 어쩔 수 없이 오류를 마주해요. 오류(error)란 프로그램의 정상적인 동작에는 속하지 않는 예상 밖의 상황을 말하죠. 흔히 겪는 오류를 몇 가지 꼽아볼게요.

  • 메모리가 부족하다.
  • 디스크 공간이 부족하다.
  • 파일 이름이 잘못됐다.
  • 읽기 전용 파일에 쓰기를 시도했다.
  • 존재하지 않는 파일을 읽으려 한다.
  • 지원하지 않는 시스템 서비스를 요청했다.

오류 처리의 문제 (The Error Handling Problem)

오류를 검출하고 보고하는 전통적인 C 방식은 사실 '전통적'이라고 부르기 어려워요. 함수마다 제각각이라 임시방편에 가깝죠. 그 방식들을 나열해보면 이래요.

  • NULL 포인터를 반환한다.
  • 0 값을 반환한다.
  • 0이 아닌 오류 코드를 반환한다.
  • errno를 확인하라고 요구한다.
  • 이전 함수가 실패했는지 확인하려고 별도의 함수를 호출하라고 요구한다.

이런 오류들을 처리하려면 매 함수 호출마다 지루한 오류 처리 코드를 덧붙여야 해요. 오류가 발생하면 복구하는 코드를 짜야 하고, 사용자에게는 알아보기 쉬운 방식으로 오류를 알려야 하죠. 오류를 해당 함수 안에서 처리할 수 없다면 명시적으로 호출한 쪽으로 다시 전파(propagate)해야 해요. 그리고 길게 늘어선 errno 값 목록을 화면에 보여줄 적절한 문구로 바꿔야 하고요. 이 모든 코드를 붙이는 일은 프로젝트 코딩 시간의 상당 부분을 잡아먹어요. 게다가 런타임 시스템에 새 errno 값이 추가되면, 기존 코드는 그 오류를 제대로 된 메시지로 보여줄 수조차 없게 돼요.

좋은 오류 처리 코드는 그렇지 않으면 깔끔하게 정돈됐을 구현에 잡음(clutter)을 끼워 넣는 경향이 있어요.

더 심각한 건, 좋은 오류 처리 코드 자체가 오류에 취약하다는 점이에요. 프로젝트에서 가장 테스트가 덜 되고(그래서 버그가 많고) 흔히 아예 빼먹히는 부분이기도 하죠. 그 결과는 프로그램이 예상치 못한 오류를 다루지 못해 '죽음의 파란 화면(blue screen of death)'이 튀어나오는 일이 많아요.

급조한 프로그램은 지루한 오류 처리 코드를 짜기엔 아깝죠. 그래서 그런 유틸리티들은 마치 칼날 가드 없는 톱을 쓰는 것과 비슷해요.

여기서 필요한 건 오류 처리에 대한 철학과 방법론이에요. 그 조건을 정리해볼게요.

  • 표준화되어 있어야 한다 — 일관된 사용이 더 유용하게 만든다.
  • 프로그래머가 오류를 확인하지 않아도 결과가 합리적이어야 한다.
  • 새 오류 타입과 호환되도록 기존 코드를 고치지 않아도, 옛 코드를 새 코드와 함께 재사용할 수 있어야 한다.
  • 오류가 실수로 무시되지 않아야 한다.
  • '급조한' 유틸리티도 오류를 올바르게 처리하도록 작성할 수 있어야 한다.
  • 오류 처리 소스 코드가 보기 좋게 되도록 만들기 쉬워야 한다.

D의 오류 처리 해법 (The D Error Handling Solution)

오류에 대한 관찰과 가정을 먼저 몇 가지 살펴볼게요.

  • 오류는 프로그램의 정상 흐름에 속하지 않는다. 오류는 예외적이고, 드물고, 예상 밖이다.
  • 오류는 드물기 때문에, 오류 처리 코드의 실행은 성능에 크게 중요하지 않다.
  • 프로그램 로직의 정상 흐름은 성능에 중요하다.
  • 모든 오류는 어떤 식으로든 처리되어야 한다. 명시적으로 작성된 코드로 처리하거나, 시스템의 기본(default) 처리 방식으로 처리하거나.
  • 오류를 검출하는 코드는 오류에 대해, 그 오류에서 복구해야 하는 코드보다 더 많은 것을 안다.

해법은 예외 처리(exception handling)를 사용해 오류를 보고하는 것이에요. 모든 오류는 추상 클래스 Error에서 파생된 객체예요. ErrortoString()이라는 순수 가상 함수를 갖고 있는데, 이 함수가 오류에 대한 사람이 읽을 수 있는 설명 문자열을 만들어내죠.

"메모리 부족" 같은 오류를 코드가 검출하면, "Out of memory"라는 메시지를 담은 Error가 던져져요(thrown). 그러면 함수 호출 스택이 풀리면서(unwound) 그 Error를 처리할 핸들러를 찾아요. 스택이 풀리는 동안 finally 블록들이 실행되고, 오류 핸들러를 찾으면 그곳에서 실행이 이어져요. 찾지 못하면 기본 Error 핸들러가 실행되는데, 그 핸들러는 메시지를 보여준 뒤 프로그램을 종료해요.

그럼 우리가 세운 조건들을 이 해법이 어떻게 만족시키는지 볼까요?

표준화되어 있다 — 일관된 사용이 더 유용하게 만든다.

이 방식이 D의 방식이고, D 런타임 라이브러리와 예제들에서 일관되게 쓰여요.

프로그래머가 오류를 확인하지 않아도 결과가 합리적이다.

오류에 대한 catch 핸들러가 하나도 없다면, 프로그램은 기본 오류 핸들러를 거쳐 적절한 메시지와 함께 조용히 종료돼요.

새 오류 타입과 호환되도록 기존 코드를 고치지 않아도 옛 코드를 새 코드와 재사용할 수 있다.

옛 코드는 모든 오류를 잡을지, 아니면 특정 오류만 잡고 나머지는 위로 전파할지 결정할 수 있어요. 어느 쪽이든 오류 번호와 메시지를 일일이 대응시킬 필요가 없어요. 올바른 메시지는 항상 함께 딸려 오니까요.

오류가 실수로 무시되지 않는다.

오류 예외는 어떤 식으로든 처리돼요. 오류를 나타내는 NULL 포인터 반환이 있고, 그 NULL 포인터를 이어서 쓰려는 것 같은 상황은 존재하지 않아요.

'급조한' 유틸리티도 오류를 올바르게 처리하도록 작성할 수 있다.

급조한 코드는 오류 처리 코드를 전혀 작성하지 않아도 되고, 오류를 확인할 필요도 없어요. 오류는 알아서 잡히고, 적절한 메시지가 표시되며, 프로그램이 기본 동작만으로 조용히 종료되죠.

오류 처리 소스 코드를 보기 좋게 만들기 쉽다.

try/catch/finally 문은 끝없이 이어지는 if (error) goto errorhandler; 문보다 훨씬 보기 좋아요.

이번엔 우리가 세운 오류에 대한 가정들을 어떻게 충족하는지 볼게요.

오류는 프로그램의 정상 흐름에 속하지 않는다. 오류는 예외적이고, 드물고, 예상 밖이다.

D의 예외 처리는 정확히 그 관점과 잘 맞아떨어져요.

오류는 드물기 때문에, 오류 처리 코드의 실행은 성능에 크게 중요하지 않다.

예외 처리의 스택 풀림(stack unwinding)은 비교적 느린 과정이에요.

프로그램 로직의 정상 흐름은 성능에 중요하다.

정상 흐름 코드는 매 함수 호출마다 오류 반환 여부를 확인하지 않아도 되므로, 오류에 예외 처리를 쓰는 쪽이 실제로 더 빠를 수 있어요.

모든 오류는 어떤 식으로든 처리되어야 한다. 명시적인 코드로, 또는 시스템의 기본 처리로.

특정 오류에 대한 핸들러가 없다면 런타임 라이브러리의 기본 핸들러가 처리해요. 오류를 무시하는 경우가 있다면, 그건 프로그래머가 오류를 무시하는 코드를 일부러 추가했다는 뜻이고, 그러면 아마 의도적인 것일 거예요.

오류를 검출하는 코드는 오류에 대해, 복구해야 하는 코드보다 더 많은 것을 안다.

오류 코드를 사람이 읽을 수 있는 문자열로 번역할 필요가 더 없어요. 올바른 문자열은 복구 코드가 아니라 오류 검출 코드가 만들어내니까요. 그래서 같은 오류에 대해 어플리케이션마다 오류 메시지가 일관되게 나오는 효과도 있어요.

예외로 오류를 처리하면 또 하나의 문제가 따라와요 — 예외에 안전한(exception safe) 프로그램을 어떻게 작성할 것인가 하는 문제죠. 여기까지가 그 해법에 대한 소개였고, 이어지는 챕터에서 이어집니다.

더 알아보기 (Learn more)

  • 이 챕터의 원문: https://dlang.org/spec/errors.html
  • 오류를 던지고 잡는 실제 구문은 D 언어 사양의 Exception Handling 챕터와 Try-Catch-Finally 문법에서 다뤄요.
  • 끝없는 오류 코드 확인 대신 예외를 쓰는 이유에 대한 전반적인 흐름은 이 챕터의 "The D Error Handling Solution" 파트가 요약해 줘요.