에러 처리에 관한 간략한 역사
에러 처리는 프로그램이 만들어내는 에러를 잡아서 사용자에게서 감추는 과정입니다. 프로그래머에게 파이썬 에러 메시지가 노출되는 한 별 문제가 되지 않습니다 - 그 모든 테크노 어법을 이해하기만 하면 문제가 없습니다. 그러나 사용자는 프로그래머가 아니며 일이 잘못 되었을 때 멋지고 이해하기 쉬운 메시지를 보고 싶어합니다. 이상적으로 그 에러를 잡아서 아무도 눈치채지 않게 수정해 주기를 바랍니다!
그래서 에러 처리가 필요합니다. 거의 대부분의 언어는 에러가 일어나는 대로 잡아서 무엇이 잘못되었는지 알아내고 가능하면 그 문제를 수정하기 위해 적절하게 조치를 취하는 메커니즘을 제공합니다. 시간이 지나자 이를 위하여 수 많은 접근법이 나왔습니다. 이 기술의 역사적 발전을 따라 가면서 주제를 다루어 보겠습니다. 그러다 보면 왜 새로운 방법이 도입되었는지 감상하실 수 있을 것입니다. 주제의 끝에 이르면 파이썬 에러 메시지를 사용자에게 조금도 노출시키지 않는 사용자 친화적인 프로그램을 작성할 수 있으리라 믿습니다.
VBScript는 세 언어 중에서 에러를 처리하는 방식이 가장 괴이합니다. 그 이유는 그의 뿌리가 초기의 (1963 경) 프로그래밍 언어중 하나인 BASIC이기 때문입니다. VBScript 에러 처리는 그 동안 이어져온 전통이 빛나는 곳입니다. 우리의 목적을 위해서는 그것이 그렇게 나쁘지 않은데 왜냐하면 에러 처리의 역사를 BASIC 에서부터 Visual Basic을 거쳐 VBScript를 따라가면서 VBScript가 왜 그렇게 작동하는지 설명할 기회가 되기 때문입니다. 그 다음에는 JavaScript와 파이썬으로 예제를 보여주면서 훨씬 더 현대적인 접근법을 살펴보겠습니다..
전통적인 BASIC에서 프로그램은 코드 하나하나를 표식하기 위하여 줄 번호로 작성되었습니다. GOTO라는 서술문을 사용하여 특정한 줄로 점프하여 제어권이 이관되었습니다 (분기 주제에서 이런 예를 보았습니다). 본질적으로 이 방법만이 유일한 형태의 제어였습니다. 이런 환경에서 일반적인 에러 처리 모드는 errorcode 변수를 선언하여 정수 값을 저장하는 것입니다. 프로그램에서 에러가 일어날 때마다 그 문제를 반영하기 위하여 errorcode 변수가 설정됩니다 - 파일을 열수 없거나, 유형이 부합하지 않거나, 연산자 범람 등등
이 때문에 다음과 같이 보이는 코드가 탄생하였습니다. 약간 꾸며서 프로그램을 보이면:
1010 LET DATA = INPUT FILE
1020 CALL DATA_PROCESSING_FUNCTION
1030 IF NOT ERRORCODE = 0 GOTO 5000
1040 CALL ANOTHER_FUNCTION
1050 IF NOT ERRORCODE = 0 GOTO 5000
1060 REM 다음과 같이 처리를 계속한다.
...
5000 IF ERRORCODE = 1 GOTO 5100
5010 IF ERRORCODE = 2 GOTO 5200
5020 REM 계속해서 IF 서술문 나열한다.
...
5100 REM 1 번 에러코드는 여기에서 처리.
...
5200 REM 2 번 에러코드는 여기에서 처리.
보시다시피 메인 프로그램의 거의 절반이 에러가 일어나는지 탐지하는 일과 관련됩니다. 시간이 지나자 약간 더 우아한 메커니즘이 도입되었습니다. 여기에서는 에러 탐지와 처리가 부분적으로 언어 인터프리터의 책임이 되었습니다. 이는 다음과 같이 보입니다:
1010 LET DATA = INPUTFILE
1020 ON ERROR GOTO 5000
1030 CALL DATA_PROCESSING_FUNCTION
1040 CALL ANOTHER_FUNCTION
...
5000 IF ERRORCODE = 1 GOTO 5100
5010 IF ERRORCODE = 2 GOTO 5200
이렇게 하니까 단 한 줄이면 에러 처리가 코드가 위치할 곳을 나타낼 수 있습니다. 여전히 에러를 탐지하여 ERRORCODE 값을 설정하는 함수들이 요구되었지만 덕분에 코드를 (읽고) 쓰기가 더 쉬워졌습니다.
그래서 어떻게 이것이 우리에게 영향을 미치는가? 당연히 비주얼 베이직은 여전히 이런 형태의 에러 처리 방법을 제공합니다. 물론 줄 번호는 좀 더 인간 친화적인 라벨로 교체되었습니다. 비주얼 베이직의 후손으로서 VBScript는 이를 크게 줄인 버전을 제공합니다. 실제로 VBScript에서는 지역적으로 에러를 처리하거나 아니면 완전히 에러를 무시할 수 있습니다.
에러를 무시하려면 다음 코드를 사용합니다:
On Error Goto 0 ' 0은 아무데도 가지 않는다는 뜻이다
SomeFunction()
SomeOtherFunction()
....
에러를 지역적으로 처리하려면 다음과 같이 합니다:
On Error Resume Next
SomeFunction()
If Err.Number = 42 Then
' 여기에서 에러를 처리한다
SomeOtherFunction()
...
이는 약간 뒤죽박죽으로 보이지만 실제는 위에 기술한 역사적 진화 과정을 반영합니다.
에러가 탐지되면 인터프리터가 메시지를 만들어 사용자에게 보여주고 프로그램의 실행을 멈추는 것이 기본 행위입니다. 이는 GoTo 0 에러 처리에서 일어나는 일입니다. 그래서 그 효과상 GoTo 0는 지역적 제어를 끄는 방법이며 인터프리터는 평소대로 작동할 수 있습니다.
Resume Next 에러 처리는 에러가 전혀 일어나지 않은 것처럼 가장하거나 또는 Error 객체( Err)와 특히 그 번호 속성을 점검하는 것입니다 (앞의 에러코드 테크닉과 정확하게 같습니다.). Err 객체에는 그냥 프로그램을 멈추기 보다 좀 부드럽게 상황을 처리하는데 도움을 줄만한 다른 정보도 몇 가지 있습니다. 예를 들어 객체 또는 함수 등등의 관점에서 에러의 소스를 알 수 있습니다. 텍스트 설명도 얻을 수 있어서 사용자에게 보여줄 정보 메시지를 생산하는 데 이용할 수 있습니다. 또는 로그 파일에 기록도 할 수 있습니다. 마지막으로 Err 객체의 Raise 메쏘드를 사용하면 에러 유형을 바꿀 수 있습니다. 또 Raise를 사용하면 자신만의 함수 안에서 자신만의 에러를 만들 수도 있습니다.
VBScript 에러 처리 사용법의 한 예로서 0으로 나누기를 시도하는 일반적인 사례를 살펴봅시다:
<script type="text/vbscript">
Dim x,y,Result
x = Cint(InputBox("나뉠 수를 입력하시오"))
y = CINt(InputBox("나눌 수를 입력하시오"))
On Error Resume Next
Result = x/y
If Err.Number = 11 Then ' 0으로 나누었다면,
Result = Null
End If
On Error GoTo 0 ' 에러 처리를 다시 끈다.
If VarType(Result) = vbNull Then
MsgBox "에러: 연산을 수행할 수 없습니다."
Else
MsgBox CStr(x) & " divided by " & CStr(y) & " is " & CStr(Result)
End If
</script>
솔직히 별로 멋져 보이지는 않는군요. 옛날 이야기 감상이 마음의 양식이 될지는 모르겠지만, 파이썬과 자바스크리트를 포함하여 현대의 프로그래밍 언어는 훨씬 더 우아하게 에러를 처리할 수 있습니다. 그래서 이제 그 방법들을 살펴봅시다.
파이썬의 에러 처리
예외 처리
최근의 프로그래밍 환경에서 에러를 처리하는 대안적인 방식인 예외 처리(exception handling)에서는 함수가 예외(exception)를 던지거나(throw) 일으킵니다( raise). 시스템은 그러면 현재의 코드 블록을 빠져 나와 가까운 예외 처리 블록으로 강제로 제어권을 넘깁니다. 시스템은 기본 처리자를 제공합니다. 기본 처리자는 보통 다른 곳에서 미처 처리하지 못한 예외를 모두 잡아(catches) 에러 메시지를 인쇄하고 종료합니다.
이런 스타일의 에러 처리에서 큰 장점은 프로그램의 메인 함수가 에러 코드로 어지럽지 않기 때문에 훨씬 더 보기 좋다는 것입니다. 에러 코드를 전혀 보지 않고 그냥 메인 블록을 따라가면서 읽을 수 있습니다.
이런 스타일의 프로그래밍이 실제로 작동하는지 알아 봅시다.
Try/Catch
예외 처리 블록은 if...then...else 블록과 형태가 비슷합니다:
try:
# 프로그램 로직은 여기에서 시작한다.
except ExceptionType:
# 이름있는 예외의 처리는 여기에서 시작한다.
except AnotherType:
# 다른 예외의 처리는 여기에서 시작한다.
else:
# 아무 예외도 일어나지 않으면 여기에서 깔끔하게 정리한다.
파이썬은 try와 첫 except 사이에 있는 서술문들을 실행하려고 시도합니다. 에러를 맞이하면 try 블록의 실행을 멈추고 아래 except 서술문으로 점프합니다. 에러 유형(exception)에 부합하는 것을 발견할 때까지 아래로 except 서술문을 진행합니다. 부합하면 그 예외 바로 다음에 따라오는 블록의 코드를 실행합니다. 부합하는 except 서술문이 발견되지 않으면, 에러는 프로그램의 다음 수준까지 위로 전파됩니다. 일치가 발견되거나 최상위 수준의 파이썬 인터프리터가 그 에러를 잡아서 에러 메시지를 화면에 보여주고 프로그램의 실행을 멈출 때까지 말입니다 - 이것이 바로 지금까지 프로그램에서 일어나고 있는 일들입니다.
에러가 try 블록 안에서 발견되지 않으면 최후로 else 블록이 실행됩니다. 물론 실제로 이 특징은 거의 사용되지 않습니다. 특정한 에러 유형이 지정되지 않은 except 서술문은 미처 처리하지 못한 에러 유형을 모조리 잡습니다. 일반적으로 이는 나쁜 생각입니다. 프로그램의 최상위 수준에서 예외가 일어나더라도 파이썬의 무미건조한 기술적 에러 메시지를 사용자에게 보여주고 싶지는 않을 것입니다. 범용 except 서술문을 이용하면 잡지 못한 에러를 잡아서 친화적인 "셧 다운" 메시지를 화면에 보여줄 수 있습니다.
파이썬은 traceback 모듈을 제공하므로 에러의 근원에 관하여 다양한 정보를 추출할 수 있음을 주목할 가치가 있습니다. 그리하여 로그 파일 만들기 등등에 유용할 수 있습니다. 여기에서 traceback 모듈은 다루지 않겠습니다. 필요하다면 표준 모듈 문서에서 특징에 관한 모든 목록을 제공합니다.
이제 실제 예제를 살펴보겠습니다. 그냥 어떻게 작동하는지만 보세요:
value = raw_input("분모를 타자하세요: ")
try:
value = int(value)
print "42 / %d = %d" % (value, 42/value)
except ValueError:
print "값을 정수로 변환할 수 없습니다."
except ZeroDivisionError:
print "분모는 0이 되면 안됩니다."
except:
print "예상치 못한 일이 일어났습니다."
else: print "프로그램이 성공적으로 완료되었습니다."
실행하고 프롬프트에서 비-숫자를, 즉 문자열을 입력하면, ValueError 메시지를 얻습니다. 0을 입력하면 ZeroDivisionError 메시지를 얻습니다. 유효한 숫자를 입력하면 그 결과와 더불어 "프로그램이 성공적으로 완료되었습니다."라는 메시지를 얻습니다.
Try/Finally
또다른 유형의 'exception' 블록이 있습니다. 에러가 일어난 후에 깔끔하게 정리해 주는데 이른바 try...finally 블록이라고 부르며 전형적으로 파일을 닫고 버퍼를 디스크에 비우는 등등에 사용됩니다. finally 블록은 try 섹션에서 무슨 일이 일어나든 상관없이 언제나 마지막으로 꼭 실행됩니다.
try:
# 정상 프로그램 로직
finally:
# try 블록의 성공/실패 여부에 상관없이,
# 여기에서 깔끔하게 정리한다.
이는 try/except 블록과 조합되면 아주 강력해집니다. 이 경우 try 블록이 상대 블록 안에 있다고 해서 특별한 장점은 없습니다. 처리 순서는 어느 경우든 똑 같습니다. 개인적으로 보통 try/finally 블록을 바깥에 두는데 그 이유는 그렇게 해야 finally가 마지막으로 실행된다는 것을 상기시켜주기 때문입니다. 그러나 파이썬에게는 아무 차이도 없습니다. 다음과 같이 보입니다:
print "프로그램 시작"
try:
data = file("data.dat")
print "데이터 파일 개방"
try:
value = int(data.readline().split()[2])
print "The calculated value is %s" % (value/(42-value))
except ZeroDivisionError:
print "읽은 값은 42입니다"
finally:
data.close()
print "데이터 파일 폐쇄"
print "프로그램 완료"
고지: 데이터 파일은 안에 세 번째 필드에 숫자를 가진 줄이 들어 있어야 합니다. 다음과 같이:
Foo bar 42
이 경우 데이터 파일은 try/except 블록 안에서 예외가 일어나든 말든 상관없이 언제나 닫힙니다. 이는 try/except 블록의 else 절과 다른 행위임에 주목하세요. 왜냐하면 아무 예외도 일어나지 않을 경우에만 호출되기 때문입니다. 똑같이 단순하게 코드를 try/except 블록 바깥에 두는 것은 예외가 ZeroDivisionError 아닌 어떤 것이라면 그 파일이 닫히지 않는다는 것을 뜻할 수도 있습니다. 오직 try/finally 구조만 그 파일이 언제나 닫힌다는 것을 보증합니다.
file() 서술문을 try/except 블록 밖에 둔 것도 주목하세요. 그것은 close() 서술문과 줄을 맞추기 위해 순전히 스타일의 관점에서 결정한 것입니다. 실제로 파일 열림 에러를 잡고 싶었다면 그것을 try/except 블록 안으로 옮기고 또다른 except 서술문을 추가했을 것입니다.
마지막으로 강조하고 싶은 것은 try/except/finally을 단일한 구조로 조합할 수 없다는 것입니다. 반드시 각자 try 서술문을 가지도록 따로 블록을 유지해야 합니다.
에러 만들기
예를 들어 모듈에서 다른 사람이 잡도록 에외를 만들고 싶으면 어떤 일이 일어나는가? 그런 경우 파이썬에서는 raise 키워드를 사용합니다:
numerator = 42
denominator = input("어떤 값으로 42를 나눌까요?")
if denominator == 0:
raise ZeroDivisionError()
이는 ZeroDivisionError 예외를 일으킵니다. 이 예외는 try/except 블록에서 잡을 수 있습니다. 나머지 프로그램은 파이썬이 내부적으로 에러를 일으킨 것과 정확하게 똑 같습니다. raise 키워드의 또다른 사용법은 에러를 예외 블록 안으로부터 더 높은 수준으로 전파하는 것입니다. 예를 들어 지역적으로 조치를 취하고 싶지만, 예를 들어 로그를 파일에 기록하고 싶지만, 더 높은 수준에서 최종적으로 어떤 조치를 취할지 결정하도록 허용하고 싶은 경우가 있습니다. 다음과 같이 보입니다:
def f(datum):
try:
return 127/(42-datum)
except ZeroDivisionError:
logfile = open("errorlog.txt","w")
logfile.write("datum was 42\n")
logfile.close()
raise
try:
f(42)
except ZeroDivisionError:
print "0으로 나눌 수 없습니다. 다른 값을 시도해 보세요."
함수 f()가 어떻게 에러를 잡아서 메시지를 에러 파일에 기록하고 그 예외를 다시 위로 바깥의 try/except 블록에 건네서 처리하는지 주목하세요.
사용자 정의 예외
자신만의 예외 유형을 정의하여 더욱 더 세련되고 섬세하게 프로그램을 제어할 수도 있습니다. 새로운 예외 클래스를 정의하여 이렇게 합니다 (미가공 재료 주제에서 클래스를 정의하는 법을 간략하게 살펴보았습니다. 나중에 이 자습서에서 객체 지향 프로그래밍 주제에 이르면 더 자세하게 살펴보겠습니다). 보통 예외 클래스는 별거 아니며 안에 아무 내용도 없고 그냥 Exception의 하위 클래스로 정의하고 그것을 except 서술문이 탐지할 수 있는 일종의 "똑똑한 라벨"로 사용하면 됩니다. 여기에서는 짧은 예제로도 충분할 것입니다:
class BrokenError(Exception): pass
try:
raise BrokenError
except BrokenError:
print "BrokenError가 일어났습니다."
"Error"를 클래스 이름의 끝에 붙이는 이름짓기 관례를 사용한 것에 주목하세요. 총괄 Exception 클래스의 행위를 그 이름 다음에 괄호 안에 싸 넣어서 상속(inherit)받습니다 - OOP 주제에서 상속에 관하여 모든 것을 알려드리겠습니다.
에러를 일으키는 것에 관하여 주목할 마지막 요점입니다. 지금까지 프로그램을 마칠 때 sys를 반입하고 exit() 함수를 호출했습니다. 정확하게 같은 결과를 달성하는 또다른 방법은 다음과 같이 SystemExit 에러를 일으키는 것입니다:
>>> raise SystemExit
이렇게 하면 얻는 장점은 먼저 import sys를 할 필요가 없다는 것입니다.
댓글