AppSignal은 Ruby 애플리케이션을 위한 에러 추적 서비스를 제공합니다. 애플리케이션에서 발생하는 모든 예외를 수집하고, 문제가 생기는 즉시 개발자에게 알려주는 것이 우리의 역할입니다.
예외 처리를 올바르게 구현하는 것은 생각보다 까다롭습니다. 이 글에서는 Ruby에서 예외 처리가 어떻게 동작하는지, 잘못된 처리가 어떤 문제를 일으킬 수 있는지, 그리고 예외를 제대로 rescue 하는 방법을 차근차근 살펴보겠습니다.
예외 rescue 하기
Ruby에서 예외를 rescue 하면 오류가 발생하는 순간 애플리케이션이 종료되는 것을 막을 수 있습니다. begin .. rescue 블록을 사용하면 에러 발생 시 애플리케이션이 따라갈 대체 경로를 지정할 수 있습니다.
begin
do_something_risky
rescue
puts '오류가 발생했습니다'
end어떤 예외를 rescue 할지 명시적으로 지정할 수도 있습니다. 특정 예외 클래스를 지정하면 해당 클래스의 모든 하위(subclass) 예외 역시 함께 처리됩니다.
begin
File.read('config.yml')
rescue SystemCallError
puts '파일을 읽는 데 실패했습니다'
end위 예제에서 부모 클래스인 SystemCallError를 rescue 했지만, 자식 예외인 Errno::ENOENT 역시 함께 잡히는 것을 확인할 수 있습니다.
예외 체계의 너무 상위 클래스를 rescue 하는 문제
Exception 계층 구조에서 너무 상위에 있는 클래스를 rescue 해서는 안 됩니다. 그렇게 하면 모든 하위 예외가 함께 잡혀 rescue 블록의 범위가 지나치게 포괄적이 되기 때문입니다.
프로그램 인수로 전달된 경로의 설정 파일을 읽는 간단한 프로그램을 예로 들어 보겠습니다.
begin
config = File.read(ARGV[0])
puts '설정 파일을 읽었습니다'
rescue
puts '설정 파일을 읽을 수 없습니다'
end실행 결과는 "설정 파일을 읽을 수 없습니다"라고 출력하지만, 실제 원인은 코드에 있는 오타였습니다.
File.red(ARGV[0])
# NoMethodError: undefined method 'red' for Filebegin .. rescue 블록이 기본으로 잡는 예외 클래스는 StandardError입니다. 별도의 클래스를 지정하지 않으면 Ruby는 StandardError와 그 하위 예외들을 모두 rescue 하는데, NoMethodError도 여기에 포함됩니다.
구체적인 예외 클래스를 지정하면 무관한 오류가 의도치 않은 실패 상태로 이어지는 것을 방지할 수 있습니다. 또한 최종 사용자에게 더 유용하고 구체적인 커스텀 에러 메시지를 제공할 수 있습니다.
begin
config = File.read(ARGV[0])
rescue Errno::ENOENT
puts "설정 파일 '#{ARGV[0]}'을(를) 찾을 수 없습니다"
endException 클래스를 rescue 하는 위험성
그럼에도 불구하고 계층 구조의 상위 클래스를 rescue 하고 싶은 유혹이 있을 수 있습니다. 애플리케이션에서 발생할 수 있는 모든 오류를 잡으면 프로그램이 절대 크래시하지 않겠죠. (100% 가동률, 여기 오늘이야!) 하지만 이는 심각한 문제를 초래합니다.
Ruby에서 Exception은 모든 예외의 최상위 클래스입니다. 다른 모든 예외가 이 클래스의 하위이므로, Exception을 rescue 하면 모든 오류가 잡힙니다.
대부분의 애플리케이션이 절대로 rescue 해서는 안 되는 두 가지 예외가 있는데, 바로 SignalException과 SystemExit입니다.
SignalException은 외부에서 애플리케이션의 중지를 요청할 때 발생합니다. 운영체제가 종료 프로세스를 진행하거나, 시스템 관리자가 애플리케이션을 중단하려는 경우가 그 예입니다.
SystemExit는 Ruby 코드에서 exit가 호출될 때 발생합니다. 이 예외가 raise 된다는 것은 개발자가 애플리케이션의 종료를 의도했다는 뜻입니다.
만약 Exception을 rescue 한다면, 애플리케이션이 begin ... rescue ... end 블록 안에서 실행 중일 때 이러한 종료 신호를 받더라도 프로세스가 빠져나가지 못합니다.
일반적인 상황에서 Exception을 rescue 하는 것은 좋지 않은 습관입니다. Exception을 잡으면 SignalException과 SystemExit뿐 아니라 LoadError, SyntaxError, NoMemoryError 같은 중요한 예외마저 동작하지 못하게 됩니다. 더 구체적인 예외를 rescue 하는 편이 훨씬 안전합니다.
테스트에서의 실패 감춰짐
rescue Exception => e 형태로 Exception을 잡으면 애플리케이션 외의 다른 것들도 고장날 수 있습니다. 대표적으로 테스트 스위트가 일부 오류를 숨겨버릴 수 있습니다.
minitest와 RSpec에서 실패한 assertion은 예외를 발생시켜 테스트 실패를 알립니다. 이때 각 라이브러리는 Exception을 상속한 자체 커스텀 예외를 사용합니다.
따라서 테스트 코드나 애플리케이션 코드에서 Exception을 rescue 하면 assertion 실패가 조용히 묻혀버릴 수 있습니다.
begin
assert_equal expected, actual
rescue Exception => e
# assertion 실패가 무시되고 테스트가 통과해 버림
end예외 발생 검증하기
어떤 코드는 의도적으로 예외를 발생시키도록 설계됩니다. 테스트 스위트에서 단순히 예외를 삼키면, 예외가 발생해도 테스트가 실패하지 않습니다.
def test_parse_invalid_input
begin
parse_input(invalid_input)
rescue
# 아무 처리도 하지 않음 — 테스트가 항상 통과함
end
end그러나 이런 테스트는 예외가 실제로 발생했는지조차 확인하지 못합니다. 예상한 예외가 발생하지 않아도 테스트는 통과하므로, 동작이 여전히 올바른지 판단할 수 없습니다.
예외가 발생했는지, 그리고 발생하지 않았다면 어떤 예외가 나왔는지를 assert 할 수 있습니다.
def test_raises_argument_error
assert_raises(ArgumentError) do
parse_input(invalid_input)
end
end예외 다시 발생시키기(Re-raise)
애플리케이션이 Exception 클래스까지 거슬러 올라가 예외를 잡아야 하는 경우는 아주 특별한 사유가 있을 때뿐입니다. 예를 들어 코드 블록을 빠져나가기 전에 반드시 수행해야 하는 정리 작업이 남아 있을 때입니다. 꼭 삭제해야 하는 임시 파일을 제거하는 것이 대표적인 예입니다.
어쩔 수 없이 Exception을 rescue 해야 한다면, 오류 처리를 마친 후 예외를 다시 raise 할 것을 권장합니다. 그렇게 하면 그 이후의 프로세스 운명을 Ruby의 기본 예외 처리 메커니즘에 맡길 수 있습니다.
temp_file = create_temp_file
begin
process(temp_file)
rescue Exception => e
temp_file.delete # 임시 파일 정리
raise # 예외를 다시 발생시켜 기본 처리에 맡김
end무엇을 rescue 해야 할지 모르겠다면?
앞서 말했듯이, 어떤 오류를 rescue 할지 구체적으로 명시하는 것이 좋습니다.
어떤 연산이 어떤 예외를 발생시킬지 확실하지 않다면, 우선 StandardError를 rescue 하는 것에서 시작하는 것이 좋은 출발점입니다. 다양한 시나리오에서 코드를 실행하며 어떤 예외가 발생하는지 관찰해 보세요.
begin
run_operation
rescue StandardError => e
puts "#{e.class}: #{e.message}"
end새로운 예외를 발견할 때마다 해당 예외나 적절한 부모 클래스에 대한 구체적인 rescue 절을 추가하세요. 너무 많은 예외를 한꺼번에 잡는 것보다, rescue 대상을 좁혀 구체화하는 편이 훨씬 안전합니다.
begin
run_operation
rescue Errno::ENOENT => e
# 파일을 찾을 수 없는 경우 처리
rescue ArgumentError => e
# 잘못된 인수 처리
end지금까지 Ruby 예외 처리 입문 과정을 마쳤습니다. 더 궁금한 점이나 구체적인 질문이 있다면 @AppSignal로 알려주세요. 애플리케이션에서 예외가 언제, 얼마나 자주 발생하는지 더 깊이 파악하고 싶다면 AppSignal을 직접 사용해 보시기 바랍니다.
