Ruby 개발을 하다 보면 에러 메시지를 자주 마주하게 됩니다. 기술적인 용어로 이를 예외(exception)라고 부릅니다.
예외는 크게 두 가지로 나눌 수 있습니다. 파일이 어떤 때는 존재하고 어떤 때는 없는 경우처럼 예상 가능한 예외, 그리고 API가 일시적으로 제한되는 등 예측하기 어려운 예외입니다.
이번 글에서는 이러한 예상 가능한 에러를 관리하는 방법을 알아보겠습니다.
어떻게요?
Ruby의 begin과 rescue, 바로 에러 상황을 처리하기 위해 사용하는 두 가지 핵심 키워드를 소개합니다.
예외 처리란 무엇인가?
먼저 한 가지 이해해야 할 것이 있습니다.
Ruby 프로그램은 실행 중 여러 지점에서 에러를 일으킬 수 있습니다.
대표적인 예시는 다음과 같습니다:
- 존재하지 않는 파일을 읽으려고 시도할 때
- 숫자를 0으로 나눌 때
- 사용 중인 웹 서버의 SSL 인증서가 만료되었을 때
에러가 발생하면 Ruby는 곧바로 프로그램이 종료되지 않습니다!
에러로부터 복구할 기회가 주어집니다. 이것을 바로 "예외 처리(exception handling)"라고 부릅니다.
Ruby는 코드에서 에러 복구를 구현할 수 있도록 몇 가지 키워드를 제공하며, 그중 대표적인 것이 begin과 rescue입니다.
Ruby 예외 처리 기본 방법
그럼 예외는 어떻게 처리할까요?
예외를 발생시키는 코드를 begin / rescue 블록으로 감싸면 됩니다.
동작 방식은 다음과 같습니다.
첫 번째 영역(begin)에는 실행할 코드, 즉 예외가 발생할 가능성이 있는 코드를 작성합니다.
예제:
begin
IO.sysopen('/dev/null')
rescue
# ...
end
여기서는 sysopen으로 파일을 열려고 시도합니다. 파일을 열 수 없다면 예외가 발생합니다.
바로 이럴 때 rescue 키워드가 빛을 발합니다!
rescue를 사용하면 예외가 발생했을 때 어떤 동작을 할지 직접 지정할 수 있습니다. 즉, 실패 상황에 대한 대응을 개발자가 통제할 수 있게 되는 것입니다.
예제:
begin
IO.sysopen('/dev/null')
rescue
puts "Can't open IO device."
end
일반적으로는 에러를 로그로 남기거나, 기본값(default value)을 제공하는 식으로 대응합니다.
주의하세요. 절대로 에러를 무시하지 마세요.
여러 예외를 한 번에 처리하기
rescue에는 선택적 인자(optional argument)를 전달할 수 있다는 점을 알아두세요.
이 인자가 무엇이냐면, 바로 처리하고자 하는 예외 클래스(exception class)입니다.
어떤 예외 클래스를 지정할지는 실행하는 코드에 따라 달라집니다.
IO 작업의 경우:
- 파일이 없을 때는
Errno::ENOENT - 권한 오류일 때는
Errno::EACCES
가장 좋은 점은 무엇일까요?
하나의 begin/rescue 블록에서 여러 예외를 각각 다르게 처리할 수 있습니다.
이렇게요:
begin
IO.sysopen('/dev/null')
rescue Errno::ENOENT
puts "File not found."
rescue Errno::EACCES
puts "Insufficient permissions, not allowed to open file."
end
반대로 여러 예외에 대해 동일한 동작을 수행하고 싶다면...
이렇게 작성할 수 있습니다:
begin
IO.sysopen('/dev/null')
rescue Errno::ENOENT, Errno::EACCES
puts "There was an error opening the file."
end
계속해서 더 알아보겠습니다!
메서드와 블록 안에서 예외 처리하기
항상 begin 키워드를 사용해야 하는 것은 아닙니다.
begin을 생략할 수 있는 경우가 있습니다.
어디서냐고요?
바로 메서드와 블록 내부입니다.
메서드 예제:
def get_null_device
IO.sysopen('/dev/null')
rescue Errno::ENOENT
puts "Can't open IO device."
end
메서드 정의 자체가 begin의 역할을 해주기 때문에 생략할 수 있습니다.
블록에서도 마찬가지로 적용됩니다.
블록 예제:
["a.txt", "b.txt", "c.txt"].map do |f|
IO.sysopen(f)
rescue Errno::ENOENT
puts "Can't open IO device: #{f}."
end
begin 없이 rescue를 사용하는 방법이 하나 더 있습니다.
어떻게 동작하는지 살펴보겠습니다.
인라인 rescue와 그 위험성
rescue는 한 줄(inline) 형태로도 사용할 수 있습니다.
드물지만 특정 상황에서는 이런 형태의 예외 처리가 유용할 수 있습니다.
예제:
["a.txt", "b.txt", "c.txt"].select { |f| File.open(f) rescue nil }.map(&:size)
이 코드는 실제로 존재하는 파일만 열고, 존재하지 않는 파일은 무시합니다.
그 결과, 존재하는 파일들의 크기만 얻을 수 있습니다.
예외가 발생하지 않도록 말이죠.
왜 이런 방식을 쓸까요?
코드를 한 줄로 깔끔하게 유지할 수 있기 때문입니다. 하지만 장점은 그게 전부입니다.
사실 이 형태의 rescue에는 숨겨진 위험(hidden danger)이 있습니다. StandardError를 상속하는 거의 모든 예외를 잡아버리기 때문입니다.
왜 좋지 않을까요?
광범위한 예외를 한꺼번에 처리하기보다는, 필요한 구체적인 예외만 처리하는 것이 모범 사례이기 때문입니다.
그렇게 해야 에러가 스스로에게 숨겨지는 것을 막을 수 있습니다!
숨겨진 에러는 온갖 기묘한 동작과 디버깅하기 어려운 문제로 이어질 수 있습니다.
마무리 정리
이번 글에서는 Ruby에서 발생하는 에러, 기본적인 예외 처리 방법, 그리고 begin / rescue 키워드의 사용법에 대해 알아보았습니다.
begin블록에 예외가 발생할 수 있는 코드를 작성한다rescue로 특정 예외 클래스를 지정해 대응 로직을 작성한다- 메서드와 블록 안에서는
begin을 생략할 수 있다 - 인라인 rescue는 편리하지만 광범위한 예외를 잡아 위험할 수 있다
이 글이 도움이 되었다면 주변에 공유해 주세요 🙂
읽어주셔서 감사합니다!