Ruby에서는 예외를 구조(rescue)한 뒤 다른 종류의 예외를 다시 발생(re-raise)시키는 패턴이 매우 흔하게 사용됩니다. ActionView가 가장 대표적인 예입니다. 이전에 TracePoint에 대해 작성한 글에서도 언급했듯이, ActionView는 템플릿 내부에서 발생하는 모든 예외를 삼켜버리고 이를 ActionView::TemplateError 형태로 다시 던집니다.
그런데 때로는 이것만으로는 부족할 때가 있습니다. 원본 예외 객체 안에 문제 해결에 꼭 필요한 데이터가 담겨 있어서 반드시 확인해야 하는 경우죠. 다행히 Ruby 2.1부터는 Exception#cause 메서드를 사용해 원본 예외에 접근할 수 있습니다.
실제로 어떻게 동작하는지 살펴보겠습니다. 아래 예제에서는 먼저 NoMethodError를 발생시킨 직후 이를 삼키고 RuntimeError를 다시 발생시킵니다. 그다음 RuntimeError를 구조하여 #cause를 통해 원래의 NoMethodError를 가져오는 방식입니다.
def fail_and_reraise
raise NoMethodError
rescue
raise RuntimeError
end
begin
fail_and_reraise
rescue => e
puts "#{ e } caused by #{ e.cause }"
end
중첩 백트레이스와 커스텀 속성
#cause 메서드는 실제로 원본 예외 객체 그 자체를 반환합니다. 즉, 원본 예외가 지니고 있던 모든 메타데이터에 자유롭게 접근할 수 있다는 뜻입니다. 물론 원본 백트레이스(backtrace)도 함께 확인할 수 있습니다.
class EatingError < StandardError
attr_reader :food
def initialize(food)
@food = food
end
end
def fail_and_reraise
raise EatingError.new("soup")
rescue
raise RuntimeError
end
begin
fail_and_reraise
rescue => e
puts "#{ e } caused by #{ e.cause } while eating #{ e.cause.food }"
puts e.cause.backtrace.first
end
무한히, 그리고 그 너머까지!
위 예제들은 한 단계 깊이의 중첩만 보여주지만, 사실 Ruby의 중첩 예외는 몇 단계든 중첩될 수 있습니다. 물론 실무에서 3~4단계보다 더 깊어지는 경우는 거의 없을 겁니다.
...하지만 재미삼아 100단계나 중첩된 예외를 만들어 보았습니다. 순전히 장난스러운 코드이니, 운영 환경(production)에서 이런 코드를 마주치는 일은 없기를 바랄 뿐입니다.
def recursively_raise(c=0)
raise "Level #{ c }"
rescue => e
if c < 100
recursively_raise(c + 1)
else
recursively_print(e)
end
end
def recursively_print(e)
if e
puts e
recursively_print(e.cause)
end
end
recursively_raise()
# ... Prints the following:
# Level 100
# Level 99
# Level 98
# etc.