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

rescue 블록 밖에서 Ruby 예외 다루기: $! 변수의 한계와 우회 방법

코드가 해당 예외의 생명주기를 직접 제어하지 않더라도 가장 최근에 발생한 예외에 접근할 수 있다면 매우 유용합니다. 예를 들어 애플리케이션에 기본적인 크래시 감지 기능을 추가한다고 상상해 보세요. 잡히지 않은(uncaught) 예외로 인해 발생하는 모든 크래시에 대해 추가 정보를 로그로 남기고 싶을 것입니다.

첫 번째 단계는 애플리케이션이 종료될 때마다 실행되는 핸들러를 추가하는 것입니다. Ruby 커널의 at_exit 메서드를 사용하면 아주 간단하게 구현할 수 있습니다.

at_exit do
  puts "the app exited"
end

그런데 이 종료 콜백이 예외 때문에 호출된 것인지 어떻게 알 수 있을까요? Ruby는 $!라는 이름이 다소 난해한 전역 변수를 제공합니다. 이 변수에는 현재 호출 스택 어딘가에서 가장 최근에 raise된 예외가 담겨 있습니다.

$!를 사용하면 프로그램이 예외로 인해 종료되는지 여부를 손쉽게 감지할 수 있습니다. 코드는 대략 다음과 같습니다.

at_exit do
  save_error_to_log($!) if $!
end

$!의 한계

안타깝게도 $!는 예외가 현재 호출 스택 내 어딘가에서 발생한 경우에만 동작합니다. 예외를 rescue한 뒤 rescue 절 바깥에서 $!에 접근하면 nil이 반환됩니다.

begin
  raise "x"
rescue
  puts $! # => RuntimeError
end

puts $! # => nil

이러한 특성 때문에 $!는 IRB 같은 셸 환경에서는 사실상 무용지물입니다. IRB에서 메서드를 실행하다가 예외를 마주치는 일은 흔한데, 때로는 그 예외 객체 자체를 가져오고 싶을 때가 있습니다. 하지만 $!로는 이것이 불가능합니다.

irb(main):001:0> 1/0
ZeroDivisionError: divided by 0
    from (irb):1:in `/'
irb(main):002:0> $!
=> nil

PRY로 $!의 한계 극복하기

PRY는 자체 로컬 변수인 _ex_를 추가함으로써 $!의 한계를 우회합니다. 이 변수에는 가장 최근에 발생한 잡히지 않은(uncaught) 예외가 담겨 있습니다.

[1] pry(main)> raise "hi"
RuntimeError: hi
from (pry):1:in `__pry__'
[2] pry(main)> _ex_
=> #<RuntimeError: hi>

PRY가 이렇게 할 수 있는 이유는 사실 PRY나 IRB 내부에는 진짜 '잡히지 않은 예외'가 존재하지 않기 때문입니다. 셸 자체가 예외를 잡아서 보기 좋게 포맷팅된 에러 메시지 형태로 화면에 출력해 주는 것입니다.

아래는 PRY 소스 코드에서 관련 부분을 발췌한 것입니다. 명령어를 평가하는 코드가 begin/rescue/end 블록으로 감싸져 있는 것을 확인할 수 있습니다. 처리 가능한(rescuable) 예외가 발생하면 PRY는 그 예외를 self.last_exception에 저장하고, 이후 이 값이 _ex_에 할당됩니다.

# Excerpted from the PRY source at https://github.com/pry/pry/blob/623306966bfa86890ac182bc8375ec9699abe90d/lib/pry/pry_instance.rb#L273

begin
  if !process_command_safely(line)
    @eval_string << "#{line.chomp}\n" if !line.empty? || !@eval_string.empty?
  end
rescue RescuableException => e
  self.last_exception = e
  result = e

  Pry.critical_section do
    show_result(result)
  end
  return
end

English 라이브러리 활용하기

$!처럼 눈에 잘 들어오지 않는 변수 이름이 거슬리시나요? 다행히 Ruby에는 "English"라는 모듈이 포함되어 있어, 로봇 욕설처럼 보이는 수많은 전역 변수들을 영어식 이름으로 사용할 수 있습니다.

$!의 영어식 동의어는 $ERROR_INFO입니다. 원래 $!를 쓰던 곳이라면 어디서든 동일하게 사용할 수 있습니다.

require "English"

begin
  raise "x"
rescue
  puts $ERROR_INFO # => RuntimeError
end

아래 표의 나머지 영어식 변수들은 이 글의 주제와 큰 관련이 없지만, 참고용으로 함께 정리해 두었습니다. 왼쪽이 English 스타일 변수명, 오른쪽이 원래 변수명입니다.

$ERROR_INFO$!
$ERROR_POSITION$@
$FS$;
$FIELD_SEPARATOR$;
$OFS$,
$OUTPUT_FIELD_SEPARATOR$,
$RS$/
$INPUT_RECORD_SEPARATOR$/
$ORS$\
$OUTPUT_RECORD_SEPARATOR$\
$INPUT_LINE_NUMBER$.
$NR$.
$LAST_READ_LINE$_
$DEFAULT_OUTPUT$>
$DEFAULT_INPUT$<
$PID$$
$PROCESS_ID$$
$CHILD_STATUS$?
$LAST_MATCH_INFO$~
$IGNORECASE$=
$ARGV$*
$MATCH$&
$PREMATCH$`
$POSTMATCH'
$LAST_PAREN_MATCH$+