AppSignal에서는 Ruby 애플리케이션에 대한 오류 추적을 제공합니다. 이를 위해 우리는 애플리케이션에서 발생하는 모든 예외를 캡처하고 발생 시 개발자에게 알립니다.
예외 처리를 올바르게 하는 것은 어려울 수 있습니다. 이 기사에서는 작동 방식, 잘못된 처리로 인해 발생할 수 있는 문제 및 예외를 올바르게 복구하는 방법에 대해 설명합니다.
예외 구제
Ruby에서 예외를 구하면 문제가 발생하는 순간 애플리케이션이 충돌하는 것을 방지할 수 있습니다. begin .. rescue
로 블록을 사용하면 오류가 발생할 때 애플리케이션에 대한 대체 경로를 지정할 수 있습니다.
begin
File.read "config.yml"
rescue
puts "No config file found. Using defaults."
end
어떤 예외를 구제해야 하는지 지정할 수도 있습니다. 예외 클래스를 지정할 때 이 예외의 모든 하위 클래스도 캡처됩니다.
begin
File.read "config.yml"
rescue SystemCallError => e
puts e.class # => Errno::ENOENT
puts e.class.superclass # => SystemCallError
puts e.class.superclass.superclass # => StandardError
end
위의 예에서 Errno::ENOENT
예외를 볼 수 있습니다. 부모 SystemCallError
구조되고 있습니다.
예외 사슬에서 너무 높은 곳에서 구출
예외 체인에서 너무 높은 예외를 구하지 않는 것이 중요합니다. 그렇게 하면 모든 하위 클래스 예외도 catch되어 구조 블록의 캡처가 너무 일반적입니다.
다음은 프로그램에 전달된 인수를 기반으로 구성 파일을 읽는 프로그램입니다.
# $ ruby example.rb config.yml
def config_file
ARGV.firs # Note the typo here, we meant `ARGV.first`.
end
begin
File.read config_file
rescue
puts "Couldn't read the config file"
end
오류 메시지는 구성 파일을 읽을 수 없다고 표시되지만 실제 문제는 코드의 오타였습니다.
begin
File.read config_file
rescue => e
puts e.inspect
end
#<NoMethodError: undefined method `firs' for []:Array>
begin .. rescue
에 의해 포착된 기본 예외 클래스 블록은 StandardError입니다. 특정 클래스를 전달하지 않으면 Ruby는 StandardError 및 모든 하위 클래스 오류를 구제합니다. NoMethodError는 이러한 오류 중 하나입니다.
특정 예외 클래스를 구하면 관련 없는 오류가 실수로 실패 상태를 표시하는 것을 방지하는 데 도움이 됩니다. 또한 최종 사용자에게 더 도움이 되는 보다 구체적인 사용자 지정 오류 메시지를 허용합니다.
config_file = "config.yml"
begin
File.read config_file
rescue Errno::ENOENT => e
puts "File or directory #{config_file} doesn't exist."
rescue Errno::EACCES => e
puts "Can't read from #{config_file}. No permission."
end
예외 구조
여전히 예외 사슬의 높은 곳에서 구출하려는 유혹을 받을 수 있습니다. 응용 프로그램에서 발생할 수 있는 모든 오류를 구하면 충돌을 방지할 수 있습니다. (100% 가동 시간이 왔습니다!) 그러나 많은 문제가 발생할 수 있습니다.
Exception 클래스는 Ruby의 주요 예외 클래스입니다. 다른 모든 예외는 이 클래스의 하위 클래스입니다. 예외가 구조되면 모든 오류가 포착됩니다.
대부분의 응용 프로그램에서 구하고 싶지 않은 두 가지 예외는 SignalException 및 SystemExit입니다.
<블록 인용>SignalException은 외부 소스가 애플리케이션에 중지를 지시할 때 사용됩니다. 이것은 종료하려는 운영 체제이거나 응용 프로그램을 중지하려는 시스템 관리자일 수 있습니다. 예
<블록 인용>
SystemExit는 exit
일 때 사용됩니다. Ruby 애플리케이션에서 호출됩니다. 이것이 발생하면 개발자는 응용 프로그램이 중지되기를 원합니다. 예
예외를 구제하고 애플리케이션이 현재 begin ... rescue ... end
를 실행하는 동안 이러한 예외가 발생하면 차단할 수 없습니다.
일반적으로 정상적인 상황에서 예외를 구하는 것은 좋지 않습니다. 예외를 구할 때 SignalException 및 SystemExit가 작동하지 않도록 할 뿐만 아니라 LoadError, SyntaxError 및 NoMemoryError도 방지할 수 있습니다. 대신 더 구체적인 예외를 구하는 것이 좋습니다.
테스트 실패
예외가 구조되면 rescue Exception => e
사용 , 응용 프로그램 이외의 다른 항목이 중단될 수 있습니다. 테스트 스위트는 실제로 일부 오류를 숨길 수 있습니다.
minitest 및 RSpec에서 실패한 주장은 테스트에 실패한 실패한 주장에 대해 알려주는 예외를 발생시킵니다. 그렇게 할 때 Exception에서 서브클래싱된 고유한 사용자 정의 예외를 발생시킵니다.
테스트 또는 애플리케이션 코드에서 예외가 구조되면 어설션 실패를 침묵시킬 수 있습니다.
# RSpec example
def foo(bar)
bar.baz
rescue Exception => e
puts "This test should actually fail"
# Failure/Error: bar.baz
# <Double (anonymous)> received unexpected message :baz with (no args)
end
describe "#foo" do
it "hides an 'unexpected message' exception" do
bar = double(to_s: "")
foo(bar)
end
end
예외 예상
일부 코드는 예외를 발생시키기 위한 것입니다. 테스트 스위트에서는 예외가 발생했을 때 테스트가 실패하지 않도록 단순히 예외를 침묵시킬 수 있습니다.
def foo
raise RuntimeError, "something went wrong"
end
foo rescue RuntimeError
그러나 예외가 발생했는지 여부는 테스트하지 않습니다. 예외가 발생하지 않으면 테스트에서 동작이 여전히 올바른지 알 수 없습니다.
예외가 발생하면 어떤 예외가 발생했는지 확인할 수 있습니다.
# expecting_exceptions_spec.rb
# RSpec example
def foo
raise NotImplementedError, "foo method not implemented"
end
describe "#foo" do
it "raises a RuntimeError" do
expect { foo }.to raise_error(RuntimeError)
end
end
1) #foo raises a RuntimeError
Failure/Error: expect { foo }.to raise_error(RuntimeError)
expected RuntimeError, got #<NotImplementedError: foo method not implemented> with backtrace:
# ./expecting_exceptions_spec.rb:4:in `foo'
# ./expecting_exceptions_spec.rb:9:in `block (3 levels) in <top (required)>'
# ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>'
# ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>'
예외 다시 발생
애플리케이션은 매우 타당한 이유가 있는 경우에만 Exception 클래스만큼 상위 체인에서 예외를 캡처해야 합니다. 예를 들어, 실제로 제거해야 하는 임시 파일을 제거하는 것과 같이 코드 블록을 종료하기 전에 일부 정리 작업이 필요한 경우입니다.
예외를 절대적으로 구출해야 하는 경우에 대한 한 가지 권장 사항은 오류 처리를 완료한 후 다시 발생시키는 것입니다. 이런 식으로 Ruby 예외 처리가 나중에 프로세스의 운명을 결정할 수 있습니다.
File.open("/tmp/my_app.status", "w") { |f| "running" }
begin
foo
rescue Exception => e
Appsignal.add_error e
File.open("/tmp/my_app.status", "w") { |f| "stopped" }
raise e
end
무엇을 구해야 할지 잘 모르시겠습니까?
앞서 언급했듯이 어떤 오류를 구제할 것인지 구체적으로 지정하는 것이 좋습니다.
작업에서 어떤 예외가 발생할 수 있는지 확실하지 않은 경우 StandardError를 구하는 것이 좋은 출발점이 될 수 있습니다. 다양한 시나리오에서 코드를 실행하고 어떤 예외가 발생하는지 확인하세요.
begin
File.open('/tmp/appsignal.log', 'a') { |f| f.write "Starting AppSignal" }
rescue => e
puts e.inspect
end
#<Errno::EACCES: Permission denied @ rb_sysopen - /tmp/appsignal.log>
새로운 예외가 발생할 때마다 해당 예외 또는 관련 상위 클래스에 대한 특정 구조 사례를 추가하십시오. 너무 많은 예외를 구하는 것보다 구체적으로 구하는 것이 좋습니다.
begin
file = '/tmp/appsignal.log'
File.open(file, 'a') { |f| f.write("AppSignal started!") }
rescue Errno::ENOENT => e
puts "File or directory #{file} doesn't exist."
rescue Errno::EACCES => e
puts "Cannot write to #{file}. No permissions."
end
# Or, using the parent error class
begin
file = '/tmp/appsignal.log'
File.open(file, 'a')
rescue SystemCallError => e
puts "Error while writing to file #{file}."
puts e
end
이것으로 Ruby의 예외 처리에 대한 입문서를 마칩니다. 더 알고 싶거나 특정 질문이 있는 경우 @AppSignal로 알려주십시오. 앱에서 예외가 발생하는 위치와 빈도에 대한 더 나은 통찰력을 얻으려면 AppSignal을 사용해 보십시오.