발생한 예외(exception)는 호출 스택(call stack)의 최상단에 도달해 애플리케이션 전체를 중단시키기 전에 구조(rescue)하여 막을 수 있습니다. Ruby에서는 이를 위해 rescue 키워드를 사용합니다.
Ruby에서 예외를 구조할 때는 어떤 에러 클래스를 구조할지 구체적으로 지정할 수 있습니다.
begin
raise 'This exception will be rescued!'
rescue StandardError => e
puts "Rescued: #{e.inspect}"
end참고: raise를 사용할 때 예외 클래스를 지정하지 않으면, Ruby는 기본적으로 RuntimeError를 발생시킵니다.
여러 예외 클래스 한 번에 처리하기
하나의 예외 클래스만 지정하는 것 외에도, rescue 키워드에 여러 예외 클래스를 나열해서 전달할 수 있습니다. 이렇게 하면 서로 다른 에러들에 대해 동일한 방식으로 대응할 수 있습니다.
begin
raise 'This exception will be rescued!'
rescue StandardError, AnotherError => e
puts "Rescued: #{e.inspect}"
end반대로 여러 개의 rescue 블록을 사용하면, 에러의 종류에 따라 각각 다른 방식으로 처리할 수 있습니다. 시나리오마다 서로 다른 예외를 발생시키는 라이브러리를 다룰 때 특히 유용합니다.
begin
raise 'This exception will be rescued!'
rescue StandardError => e
puts "Rescued: #{e.inspect}"
rescue AnotherError => e
puts "Rescued, but with a different block: #{e.inspect}"
endRuby의 예외 계층 구조
Ruby의 예외 계층 구조(hierarchy)는 다양한 종류의 에러를 구분하는 역할을 하며, 동시에 모든 에러를 하나씩 나열하지 않고도 그룹 단위로 구조할 수 있게 해줍니다.
라이브러리가 자체적인 예외 서브클래스를 정의할 수도 있지만, Ruby 2.5에 내장된 예외 서브클래스 목록은 다음과 같습니다.
- NoMemoryError
- ScriptError
- LoadError
- NotImplementedError
- SyntaxError
- SecurityError
- SignalException
- Interrupt
- StandardError (`rescue`의 기본값)
- ArgumentError
- UncaughtThrowError
- EncodingError
- FiberError
- IOError
- EOFError
- IndexError
- KeyError
- StopIteration
- LocalJumpError
- NameError
- NoMethodError
- RangeError
- FloatDomainError
- RegexpError
- RuntimeError (`raise`의 기본값)
- SystemCallError
- Errno::*
- ThreadError
- TypeError
- ZeroDivisionError
- SystemExit
- SystemStackError
- fatal (구조 불가능)
rescue 블록에서 예외 클래스를 생략하면 StandardError가 기본값으로 적용됩니다. ArgumentError와 NoMethodError는 모두 StandardError의 서브클래스이기 때문에, 해당 블록 안에서 이런 에러가 발생하면 함께 구조됩니다.
SystemCallError로 보는 계층 구조의 실제 활용
예외 계층 구조가 실제로 어떻게 동작하는지 잘 보여주는 예가 바로 SystemCallError입니다. 이는 운영체제에 의존하는 저수준(low-level) 예외 클래스로, 파일을 읽거나 쓸 때 가장 자주 마주하게 됩니다.
Ruby의 File.read 메서드는 파일 읽기에 실패하면 예외를 발생시킵니다. 실패 원인은 다양합니다. 파일이 존재하지 않거나, 프로그램에 읽기 권한이 없는 경우 등이 대표적입니다.
이런 문제들은 플랫폼마다 다르게 나타나기 때문에, Ruby는 머신에서 실행 중인 운영체제에 따라 서로 다른 예외를 발생시킬 수 있습니다. 이러한 저수준 에러를 처리하기 위해 Ruby는 플랫폼별로 서로 다른 Errno::* 예외 목록을 구현해 두었습니다.
모든 Errno::* 예외는 SystemCallError의 서브클래스입니다. 플랫폼에 특화된 예외들이지만, SystemCallError를 구조하면 rescue 블록에서 한 번에 처리할 수 있습니다.
begin
File.read("does/not/exist")
rescue SystemCallError => e
puts "Rescued: #{e.inspect}"
end예외 삼킴(Swallowing) 주의하기
일반적으로 의도치 않게 예외를 삼켜버리는(swallowing) 상황을 방지하려면, 예외를 구조할 때 최대한 구체적으로 클래스를 지정하는 것이 좋습니다.
image = nil
begin
File.read(image.filename)
rescue
puts "File can't be read!"
end이 예제에서 image 변수는 nil이므로, #filename 메서드를 호출하려는 순간 NoMethodError가 발생합니다(NoMethodError: undefined method 'filename' for nil:NilClass). 그런데 모든 StandardError 서브클래스가 구조 대상에 포함되기 때문에(NoMethodError 포함), 이 예외는 조용히 삼켜지고 "File can't be read!" 메시지만 출력됩니다. 결과적으로 코드에 잠재된 버그가 숨겨져 버립니다.
참고: 기술적으로는 가능하지만, rescue 블록에서 Exception 슈퍼클래스를 직접 사용하는 것은 매우 권장되지 않습니다.
Ruby에서 예외를 발생시키거나 구조하는 것에 대해 궁금한 점이 있으신가요? @AppSignal로 언제든지 문의해 주세요. 이 글이 도움이 되었는지, 또는 더 알아보고 싶은 주제가 있다면 소개해 주셔도 좋습니다.