코드가 해당 예외의 생명주기를 직접 제어하지 않더라도 가장 최근에 발생한 예외에 접근할 수 있다면 매우 유용합니다. 예를 들어 애플리케이션에 기본적인 크래시 감지 기능을 추가한다고 상상해 보세요. 잡히지 않은(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 | $+ |