예외 처리
예외 처리 (Exception handling)
Nim의 예외 처리는 try, except, finally 블록을 중심으로 동작해요. 문제가 생길 수 있는 코드를 보호하고, 실패했을 때 어떻게 대응할지를 구조적으로 표현하는 방식이죠. defer 문이나 raise 문처럼 예외를 더 유연하게 다루는 도구도 함께 제공돼요.
본문
Try 문 (Try statement)
예시:
# read the first two lines of a text file that should contain numbers
# and tries to add them
var
f: File
if open(f, "numbers.txt"):
try:
var a = readLine(f)
var b = readLine(f)
echo "sum: " & $(parseInt(a) + parseInt(b))
except OverflowDefect:
echo "overflow!"
except ValueError, IOError:
echo "catch multiple exceptions!"
except CatchableError:
echo "Catchable exception!"
finally:
close(f)
try 다음에 오는 문들은 예외 e가 발생하지 않는 한 순서대로 실행돼요. 예외 e의 타입이 어떤 except 절에 나열된 타입과 일치하면, 해당 절의 문들이 실행돼요. except 절 다음에 오는 문들을 예외 처리기(exception handlers)라고 불러요.
finally 절이 있으면 그 절은 예외 처리기들이 실행된 뒤 항상 실행돼요.
예외는 예외 처리기 안에서 _소비(consumed)_돼요. 다만 예외 처리기 자체가 또 다른 예외를 발생시킬 수도 있어요. 예외가 처리되지 않으면 그 예외는 호출 스택을 통해 전파(propagate)돼요. 이는 예외가 발생하면 (예외가) finally 절 밖에 있는 프로시저의 나머지 부분이 종종 실행되지 않는다는 뜻이에요.
Try 표현식 (Try expression)
try는 표현식으로도 쓸 수 있어요. 이때 try 분기의 타입은 except 분기들의 타입과 맞아야 하지만, finally 분기의 타입은 항상 void여야 해요:
from std/strutils import parseInt
let x = try: parseInt("133a")
except ValueError: -1
finally: echo "hi"
혼란스러운 코드를 막기 위해 파싱 제한이 하나 있어요. try가 ( 바로 다음에 오면 한 줄로 작성해야 해요:
from std/strutils import parseInt
let x = (try: parseInt("133a") except ValueError: -1)
Except 문 (Except clauses)
except 문 안에서는 다음 문법을 사용해 현재 예외에 접근할 수 있어요:
try:
# ...
except IOError as e:
# Now use "e"
echo "I/O error: " & e.msg
또는 getCurrentException을 사용해 발생한 예외를 가져올 수도 있어요:
try:
# ...
except IOError:
let e = getCurrentException()
# Now use "e"
getCurrentException은 항상 ref Exception 타입을 돌려준다는 점에 주의하세요. (위 예시처럼) 적절한 타입의 변수가 필요하다면 명시적으로 변환해줘야 해요:
try:
# ...
except IOError:
let e = (ref IOError)(getCurrentException())
# "e" is now of the proper type
하지만 이런 경우는 거의 필요하지 않아요. 가장 흔한 경우는 e에서 오류 메시지를 추출하는 것이고, 그런 상황에서는 getCurrentExceptionMsg만 사용해도 충분해요:
try:
# ...
except CatchableError:
echo getCurrentExceptionMsg()
커스텀 예외 (Custom exceptions)
커스텀 예외를 만들 수 있어요. 커스텀 예외는 하나의 커스텀 타입이에요:
type
LoadError* = object of Exception
커스텀 예외의 이름은 Error로 끝나는 것이 권장돼요.
커스텀 예외는 다른 예외와 마찬가지로 발생시킬 수 있어요. 예를 들면:
raise newException(LoadError, "Failed to load data")
Defer 문 (Defer statement)
try finally 문 대신 defer 문을 쓸 수도 있어요. defer는 어휘적 중첩(lexical nesting)을 피할 수 있고, 아래에서 보듯 스코프 측면에서 더 많은 유연성을 제공해요.
defer 다음에 오는 모든 문들은 현재 블록 안의 암시적 try 블록에 들어 있는 것으로 간주돼요:
proc main =
var f = open("numbers.txt", fmWrite)
defer: close(f)
f.write "abc"
f.write "def"
위 코드는 다음과 같이 다시 쓰여져요:
proc main =
var f = open("numbers.txt")
try:
f.write "abc"
f.write "def"
finally:
close(f)
defer가 템플릿/매크로의 최외곽 스코프에 있으면, 그 스코프는 템플릿/매크로가 호출된 블록까지 확장돼요:
template safeOpenDefer(f, path) =
var f = open(path, fmWrite)
defer: close(f)
template safeOpenFinally(f, path, body) =
var f = open(path, fmWrite)
try: body # without `defer`, `body` must be specified as parameter
finally: close(f)
block:
safeOpenDefer(f, "/tmp/z01.txt")
f.write "abc"
block:
safeOpenFinally(f, "/tmp/z01.txt"):
f.write "abc" # adds a lexical scope
block:
var f = open("/tmp/z01.txt", fmWrite)
try:
f.write "abc" # adds a lexical scope
finally: close(f)
최상위(top-level) defer 문은 지원되지 않아요. 그런 문이 무엇을 가리켜야 할지 명확하지 않기 때문이에요.
Raise 문 (Raise statement)
예시:
raise newException(IOError, "IO failed")
배열 인덱싱, 메모리 할당 같은 내장 연산을 제외하면, raise 문은 예외를 발생시키는 유일한 방법이에요.
예외 이름이 지정되지 않으면 현재 예외가 다시 발생(re-raised)돼요. 다시 발생시킬 예외가 없다면 ReraiseDefect 예외가 발생해요. 즉 raise 문은 항상 예외를 발생시킨다는 뜻이에요.
예외 계층 (Exception hierarchy)
예외 트리는 system 모듈에 정의돼 있어요. 모든 예외는 system.Exception을 상속해요. 프로그래밍 버그를 나타내는 예외는 system.Defect(Exception의 하위 타입)를 상속하며, 이는 전체 프로세스를 종료시키는 연산에 매핑될 수도 있기 때문에 엄밀히 말해 잡을 수 없어요. 패닉이 예외로 변환되면 이 예외들은 Defect를 상속해요.
그 밖에 잡을 수 있는 런타임 오류를 나타내는 예외는 system.CatchableError(Exception의 하위 타입)를 상속해요.
Exception
|-- CatchableError
| |-- IOError
| | `-- EOFError
| |-- OSError
| |-- ResourceExhaustedError
| `-- ValueError
| `-- KeyError
`-- Defect
|-- AccessViolationDefect
|-- ArithmeticDefect
| |-- DivByZeroDefect
| `-- OverflowDefect
|-- AssertionDefect
|-- DeadThreadDefect
|-- FieldDefect
|-- FloatingPointDefect
| |-- FloatDivByZeroDefect
| |-- FloatInvalidOpDefect
| |-- FloatOverflowDefect
| |-- FloatUnderflowDefect
| `-- InexactDefect
|-- IndexDefect
|-- NilAccessDefect
|-- ObjectAssignmentDefect
|-- ObjectConversionDefect
|-- OutOfMemoryDefect
|-- RangeDefect
|-- ReraiseDefect
`-- StackOverflowDefect
가져온 예외 (Imported exceptions)
가져온(imported) C++ 예외를 발생시키거나 잡는 것이 가능해요. importcpp로 가져온 타입은 발생시키거나 잡을 수 있어요. 예외는 값으로 발생되고 참조로 잡혀요. 예시:
type
CStdException {.importcpp: "std::exception", header: "<exception>", inheritable.} = object
## does not inherit from `RootObj`, so we use `inheritable` instead
CRuntimeError {.requiresInit, importcpp: "std::runtime_error", header: "<stdexcept>".} = object of CStdException
## `CRuntimeError` has no default constructor => `requiresInit`
proc what(s: CStdException): cstring {.importcpp: "((char *)#.what())".}
proc initRuntimeError(a: cstring): CRuntimeError {.importcpp: "std::runtime_error(@)", constructor.}
proc initStdException(): CStdException {.importcpp: "std::exception()", constructor.}
proc fn() =
let a = initRuntimeError("foo")
doAssert $a.what == "foo"
var b = ""
try: raise initRuntimeError("foo2")
except CStdException as e:
doAssert e is CStdException
b = $e.what()
doAssert b == "foo2"
try: raise initStdException()
except CStdException: discard
try: raise initRuntimeError("foo3")
except CRuntimeError as e:
b = $e.what()
except CStdException:
doAssert false
doAssert b == "foo3"
fn()
참고: getCurrentException()과 getCurrentExceptionMsg()는 C++에서 가져온 예외에는 사용할 수 없어요. except ImportedException as x: 문법을 사용하고, 예외 상세 정보를 얻으려면 x 객체의 기능에 의존해야 해요.
더 알아보기
- 예외는
try ... except ... finally블록으로 처리해요.try다음 문들은 예외가 발생하지 않는 한 순서대로 실행되고, 예외 타입이 일치하는except절이 그 예외를 처리해요.finally절은 예외 여부와 무관하게 항상 실행돼요. try는 표현식으로도 쓸 수 있어요. 이때try분기의 타입은except분기들의 타입과 맞아야 하고,finally분기의 타입은void여야 해요.defer: 표현식을 쓰면 현재 블록의 나머지 부분이 암시적try블록이 되어 종료 시점에 항상 실행돼요.try finally의 어휘적 중첩을 피하고 싶을 때 유용해요.- 예외는
raise문으로 발생시켜요. 예외 이름을 생략하면 현재 예외를 다시 발생시키고, 없으면ReraiseDefect가 발생해요. - 모든 예외는
system.Exception을 상속해요. 프로그래밍 버그 계열은Defect, 잡을 수 있는 런타임 오류는CatchableError를 상속해요.