Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

Ruby Exception#cause: 중첩된 오류의 원인 추적하기

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.