오류 처리
오류 처리 (Errors)
모든 프로그램은 오류를 다뤄야 해요. D는 오류를 예외 처리(exception handling)로 보고하도록 설계했어요. 모든 오류는 추상 클래스 Error에서 파생된 객체이며, 이 철학이 C 스타일의 임시방편적 오류 처리와 어떻게 다른지를 설명하는 페이지예요.
출처: Errors
본문
"I came, I coded, I crashed." — Julius C'ster
모든 프로그램은 오류를 다뤄야 해요. 오류는 프로그램의 정상적인 동작의 일부가 아닌 예상치 못한 조건이에요. 흔한 오류의 예는 다음과 같아요:
- 메모리 부족(out of memory).
- 디스크 공간 부족(out of disk space).
- 잘못된 파일 이름.
- 읽기 전용 파일에 쓰기를 시도함.
- 존재하지 않는 파일 읽기를 시도함.
- 지원되지 않는 시스템 서비스를 요청함.
The Error Handling Problem
전통적인 C 방식의 오류 감지·보고는 전통적이지 않아요. 그것은 임시방편적(ad-hoc)이고 함수마다 다르며, 다음과 같은 방식들을 포함해요:
- NULL 포인터를 반환.
- 0 값을 반환.
- 0이 아닌 오류 코드를 반환.
- errno를 확인하도록 요구.
- 이전 함수가 실패했는지 확인할 함수를 호출하도록 요구.
이런 가능한 오류들을 다루려면 각 함수 호출마다 지루한 오류 처리 코드를 추가해야 해요. 오류가 발생하면 오류에서 복구하는 코드를 작성해야 하고, 오류를 사용자에게 사용자 친화적인 방식으로 보고해야 해요. 오류를 지역적으로 처리할 수 없다면 명시적으로 호출자에게 다시 전파(propagate)해야 해요. errno 값들의 긴 목록은 표시할 적절한 텍스트로 변환해야 해요. 이 모든 코드를 추가하는 일은 프로젝트 코딩 시간의 상당 부분을 소비할 수 있어요 — 그리고 설령 그렇게 해도 런타임 시스템에 새 errno 값이 추가되면, 옛 코드는 의미 있는 오류 메시지를 제대로 표시하지 못해요.
좋은 오류 처리 코드는 그 외에는 단정하고 깔끔해 보이는 구현을 어지럽히는 경향이 있어요.
설상가상으로 좋은 오류 처리 코드는 그 자체로 오류가 발생하기 쉬워요. 프로젝트에서 가장 덜 테스트되고(따라서 버그가 많은) 부분이 되기 쉬우며, 흔히 그냥 생략되기도 해요. 최종 결과는 프로그램이 예상치 못한 오류를 처리하지 못해 "블루 스크린"이 되는 경우가 많아요.
빠르고 대충 만든(quick and dirty) 프로그램은 지루한 오류 처리 코드를 작성할 가치가 없어서, 그런 유틸리티는 날 보호대(blade guard) 없는 테이블 쏘우를 쓰는 것과 같아요.
필요한 것은 다음과 같은 오류 처리 철학과 방법론이에요:
- 표준화돼야 한다 — 일관된 사용은 더 유용하게 만든다.
- 프로그래머가 오류를 확인하지 못해도 결과가 합리적이어야 한다.
- 새 오류 타입과 호환되도록 옛 코드를 수정할 필요 없이 옛 코드를 새 코드와 재사용할 수 있어야 한다.
- 오류가 우연히 무시되지 않아야 한다.
- 오류를 여전히 올바르게 처리하는 '빠르고 대충 만든' 유틸리티를 작성할 수 있어야 한다.
- 오류 처리 소스 코드를 보기 좋게 만들기 쉬워야 한다.
The D Error Handling Solution
먼저 오류에 대한 몇 가지 관찰과 가정을 해볼게요:
- 오류는 프로그램의 정상 흐름의 일부가 아니에요. 오류는 예외적이고, 평소답지 않고, 예상치 못한 것이에요.
- 오류는 평소답지 않으므로 오류 처리 코드의 실행은 성능에 중요하지 않아요.
- 프로그램 논리의 정상 흐름은 성능에 중요해요.
- 모든 오류는 어떤 방식으로든 처리되어야 해요 — 명시적으로 처리하도록 작성된 코드로든, 어떤 시스템 기본 처리로든.
- 오류를 감지하는 코드는 오류에서 복구해야 하는 코드보다 그 오류에 대해 더 많이 알고 있어요.
해결책은 오류를 보고하는 데 예외 처리를 사용하는 것이에요. 모든 오류는 추상 클래스 Error에서 파생된 객체예요. Error는 오류의 사람이 읽을 수 있는 설명을 담은 문자열을 만드는 순수 가상 함수 toString()을 가져요.
코드가 "메모리 부족" 같은 오류를 감지하면 "Out of memory"라는 메시지와 함께 Error가 던져져요. 함수 호출 스택이 풀리면서(unwind) Error에 대한 처리기(handler)를 찾아요. 스택이 풀리는 동안 finally 블록이 실행돼요. 오류 처리기가 발견되면 거기서 실행이 재개돼요. 없으면 기본 Error 처리기가 실행되며, 이는 메시지를 표시하고 프로그램을 종료해요.
이것은 우리의 기준을 어떻게 충족할까요?
- 표준화돼야 한다 — 일관된 사용은 더 유용하게 만든다.
- 이것이 D의 방식이고, D 런타임 라이브러리와 예제에서 일관되게 사용돼요.
- 프로그래머가 오류를 확인하지 못해도 결과가 합리적이어야 한다.
- 오류에 대한 catch 처리기가 없으면 프로그램은 적절한 메시지와 함께 기본 오류 처리기를 통해 정상적으로 종료돼요.
- 새 오류 타입과 호환되도록 옛 코드를 수정할 필요 없이 옛 코드를 새 코드와 재사용할 수 있어야 한다.
- 옛 코드는 모든 오류를 잡을지, 특정 오류만 잡고 나머지를 위로 전파할지를 결정할 수 있어요. 어떤 경우든 오류 번호를 메시지와 연관시킬 필요가 더는 없어요 — 올바른 메시지가 항상 제공되기 때문이에요.
- 오류가 우연히 무시되지 않아야 한다.
- 오류 예외는 어떤 방식으로든 처리돼요. 오류를 나타내는 NULL 포인터 반환 후 그 NULL 포인터를 사용하려는 것 같은 일은 없어요.
- '빠르고 대충 만든' 유틸리티를 여전히 올바르게 처리할 수 있어야 한다.
- 빠르고 대충 만든 코드는 오류 처리 코드를 전혀 작성할 필요가 없고, 오류를 확인할 필요도 없어요. 오류는 잡히고, 적절한 메시지가 표시되며, 프로그램은 모두 기본 동작으로 정상 종료돼요.
- 오류 처리 소스 코드를 보기 좋게 만들기 쉬워야 한다.
- try/catch/finally 문은 끝없는
if (error) goto errorhandler;문보다 훨씬 보기 좋아요.
- try/catch/finally 문은 끝없는
이것은 오류에 대한 우리의 가정을 어떻게 충족할까요?
- 오류는 프로그램의 정상 흐름의 일부가 아니다.
- D 예외 처리는 그것과 정확히 맞아떨어져요.
- 오류는 평소답지 않으므로 오류 처리 코드 실행은 성능에 중요하지 않다.
- 예외 처리 스택 풀기는 상대적으로 느린 과정이에요.
- 프로그램 논리의 정상 흐름은 성능에 중요하다.
- 정상 흐름 코드는 모든 함수 호출에서 오류 반환을 확인할 필요가 없으므로, 오류에 예외 처리를 사용하는 것이 현실적으로 더 빨라질 수 있어요.
- 모든 오류는 어떤 방식으로든 처리되어야 한다.
- 특정 오류에 대한 처리기가 없으면 런타임 라이브러리 기본 처리기가 처리해요. 오류가 무시된다면, 그것은 프로그래머가 오류를 무시하는 코드를 구체적으로 추가했다는 뜻이며, 아마 의도적이었을 거예요.
- 오류를 감지하는 코드는 오류에서 복구해야 하는 코드보다 그 오류에 대해 더 많이 알고 있다.
- 오류 코드를 사람이 읽을 수 있는 문자열로 번역할 필요가 더는 없어요. 올바른 문자열은 오류 복구 코드가 아니라 오류 감지 코드가 생성해요. 이것은 애플리케이션 사이에서 같은 오류에 대한 일관된 오류 메시지로 이어져요.
예외로 오류를 처리하는 것은 또 다른 문제로 이어져요 — 예외 안전(exception safe) 프로그램을 작성하는 방법. 여기를 참고하세요.
더 알아보기
- try/catch/finally 문장의 문법은 TryStatement 문서를 참고하세요.
- 예외와 Error 계층의 실제 구현은 object 모듈에서 볼 수 있어요.
- 예외 안전 코드 작성 방법은 Exception Safe 문서를 참고하세요.
- dlang.org의 원문에서 최신 내용을 확인할 수 있어요.