(본 게시물은 저작권의 문제 발생시 출판사의 요청에 의해 삭제될 수 있습니다.)
예외 처리
현실 세계의 프로그램에서는 에러가 발생한다. 좋은 프로그램은 이러한 에러를 예견하고 깔끔하게 해결해야만 한다. 전통적인 접근 방법은 반환 코드를 이용하는 것이다.
파일을 열고 시도하는 예를 들어보자. open 메서드는 실패를 나타내는 특별한 값을 반환한다. 이 에러값은 누군가가 처리해 줄 때까지 메서드 호출 단계를 거슬러 올라가며 전달된다. 이 접근법의 문제점은 모든 에러 코드를 관리하는 것이 어렵다는 것이다. 어떤 함수가 open을 호출하고, 그 다음 read, 마지막으로 close를 호출하는데 각각이 에러 코드를 반환하면 이 함수는 호출자에게 건넬 반환값에서 이 에러 코드들을 어떻게 구분할 것인가?
대부분 예외가 좋은 해결책이다. 예외를 이용하면 오류에 관한 여러 정보를 객체에 담을 수 있다. 이렇게 생성된 예외 객체는 해당 종류의 예외를 처리하도록 선언된 곳까지 호출 스택을 거슬러 올라가며 전달된다.
Exception 클래스
예외에 대한 정보를 가진 것은 Exception 클래스 또는 그 자식 클래스의 객체다. 루비의 예외 클래스 상속 계보는 아래와 같다.
Exception
NotMemoryError
ScriptError
LoadError
Gem::LoadError
NotImplementedError
SyntaxError
SecurityError
SignalException
Interrupt
StandardError
ArgumentError
Gem::Requirement::BadRequirementError
EncodingError
Encoding::CompatibilityError
Encoding::ConverterNotFoundError
Encoding::InvalidByteSequenceError
Encoding::UndefinedConversionError
FiberError
IndexError
KeyError
StopIteration
IOError
EOFError
LocalJumpError
Math::DomainError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
Gem::Exception
SystemCallError
ThreadError
TypeError
ZeroDivisionError
SystemExit
Gem::SystemExitException
SystemStackError
예외를 발생시키려면 내장된 Exception 클래스를 쓸 수도 있고, 새로 클래스를 만들수 있다. 직접 만드는 경우 StandardError를 상속하는 자식 클래스로 만드는 것이 좋다. 그렇지 않으면 기본 예외 처리 루틴에서 이 예뢰를 잡아주지 않을 것이기 때문이다. 모든 Exception은 그 예외와 연관된 메시지, 그리고 스택 역추적 정보를 가지고 있다. 직접 정의한 예외라면 추가 정보를 담을 수도 있다.
예외 처리하기
open-uri 라이브러리르 사용해서 웹 페이지를 다운로드 하고, 한 줄씩 파일에 쓰는 예제다.
require 'open-uri'
web_page = open("http://pragprog.com/podcasts")
output = File.open("podcasts.html", "w")
while line = web_page.gets
output.puts line
end
output.close
다운로드한 페이지를 파일에 쓰는 과정에서 치명적인 에러가 발생하면 어떻게 될까? 이를 위해 예외를 처리하자. 예외 처리를 하려면 예외가 발생할 가능성이 있는 부분을 begin/end 블록으로 감싸주고, 처리하고자 하는 예외의 타입들을 rescue 절에 지정한다. rescue 절에 Exception을 지정했으므로 Exception과 그 자식 클래스가 나타내는 모든 예외를 처리한다. 예외 처리 블록에서는 에러를 보고하고 출력 파일을 닫고 삭제한다. 그리고 에러를 다시 발생시킨다.
require 'open-uri'
page = "podcasts"
file_name = "#{page}.html"
web_page = open("http://pragprog.com/#{page}")
output = File.open(file_name, "w")
begin
while line = web_page.gets
output.puts line
end
output.close
rescue Exception
STDERR.puts "Failed to download #{page}: #{$!}"
output.cloe
File.delete(file_name)
raise
end
예외가 발생하면 루비는 이후의 예외 처리 과정과 별도로 관련된 예외 객체 참조를 전역 변수 $!에 담는다. $!변수를 에러 메시지를 출력하는데 사용했다.
파일을 닫고 지운 뒤, raise를 매개 변수 없이 호출해서 $!에 담긴 예외를 다시 발생시킨다. 이는 예외를 거르거나, 다룰 수 없는 예외를 더 높은 계층으로 넘기는 코드를 작성해주는 유용한 기법이다. 예외처리를 위해 계층 상속을 구현하는 것과 비슷하다.
begin 블록 안에 여러 개의 rescue 절을 적어줘도 되고, 또한 각 rescue 절에는 처리하려는 예외를 여러 개 쓸 수도 있다. rescue 절의 끝에는 예외 객체를 받을 지역변수 이름을 쓸 수 있는데 이는 $!를 쓰는 것보다 가독성이 좋다.
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compile: " + boom
rescue StandardError => bang
print "Error running script: " + bang
end
루비가 어느 rescue 절을 실행할지는 case 구문의 동작 방식과 비슷하다. begin 블록의 rescue 절에 대해 하나씩 차례로 발생된 예외와 rescue의 매개 변수를 비교한다. 매개 변수에 해당한다면 그 부분을 실행하고 탐색을 멈춘다. 비교는 parameter === $! 표현식으로 이루어진다. rescue 절을 아무런 매개변수 없이 쓴다면, 매개 변수의 기본값은 StandardError가 된다.
적절한 recue 문을 못찾거나 예외가 begin/end 블록 바깥에서 발생한 경우 호출자에서 예외 처리 구문을 찾는다. 호출자에도 없으면, 호출자의 호출자를 찾는 식으로 예외 처리를 찾아나간다.
시스템 에러
시스템 에러는 운영 체제가 에러 코드를 반환했을 때 일어난다. POSIX 시스템에서는 EAGAIN이나 EPERM 등의 이름을 가진다.
루비는 이 에러들을 감싸서 특정 예외 객체로 감싼다. 예외 객체는 SystemCallError의 하위 클래스이며 Errno 모듈에서 정의하고 있다. 이 말은 Errno:EAGAIN, Errno::EIO, Errno::EPERM 같은 클래스가 있다는 것이다.
시스템 에러 코드 자체를 알고 싶은 경우를 위해, Errno 예외 객체는 에러 코드를 직접 반환하는 Errono라는 클래스 상수를 가진다.
Errno::EAGAIN::Errno # => 35
Errno::EPERM::Errno # => 1
Errno::EWOULDBLOCK::Errono # => 35
말끔히 치우기
코드 블록의 끝부분에 특정 작업을 반드시 실행해야 하는 경우가 있다. 예를 들어 블록을 시작할 때 파일을 열었다면 블록이 끝나면 파일이 닫힌다는 사실을 보장해야 한다.
ensure 절이 바로 이런 일을 해준다. ensure는 rescue 절 뒤에 오는데, 블록이 끝날 때 반드시 실행되어야 하는 코드를 여기에 적어준다. ensure 블록은 예외가 발생하건 안하건 어떤 경우에던 실행된다. (like try catch final)
f = File.open("testfile")
begin
# .. 프로세스
rescue
# .. 에러 처리
ensure
f.close
end
초심자들은 보통 File.open 을 begin 절 안에 넣는 실수를 저지른다. 이 경우엔 open 자체가 오류를 발생시킬 수 있기 때문에 그렇게 사용해서는 안 된다. open 시 예외가 발생하면 close 할 파일이 존재하지 않기 때문에 ensure 절을 실행할 필요가 없어진다.
else 절이 하는 일도 비슷하다. else절은 rescue다음에 그리고 ensure절 앞에 위치한다. else 절은 본문 코드에서 에러가 발생되지 않았을 때 실행된다.
f = File.open("testfile")
begin
# .. 프로세스
rescue
# .. 에러 처리
else
puts "Congratulations-- no errors!"
에러 처리 후 재시도
예외의 원인이 고쳐진 경우 rescue 절에 retry구문을 사용해 begin/end 블록을 다시 실행할 수 있다. 하지만 무한 루프에 빠질 가능성이 있으므로 주의를 요한다.
다음은 예외가 발생하면 재시도하는 예제 코드이다.
@esmtp = true
begin
# 먼저 확장 로그인을 시도한다. 실패하면 일반적인 로그인을 시도한다.
if @esmtp then @command.ehlo(helodom)
else @command.helo(helodom)
end
rescue ProtocolError
if @esmtp then
@esmtp = false
retry
else
raise
end
end
처음 EHLO 명령을 이용해 smtp 접속을 시도하고 접속이 실패하면 @esmtp 변수를 false로 설정하고 접속을 재시도한다. 두번 째 접속 시도도 실패할 경우 예외가 호출된다.
예외 발생시키기
예외를 발생시키는 방법은 Object#raise 메서드를 이용한다.
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
첫번째는 단순히 현재의 예외($!)를 다시 발생시킨다. 이는 예외처리구문에서 해당 예외를 다음으로 넘기기 전에 가로채어 다른 작업을 할 필요가 있을 때 이용한다.
두 번째 형태는 RuntimeError 예외를 새로 만든다. 주어진 문자열을 예외 메시지로 설정하는데 이 예외는 호출 스택을 따라 올라간다.
세 번째 형태는 첫 번째 매개 변수로 예외 클래스를 만들고, 두 번째 매개 변수를 관련된 메시지로 설정한다. 세 번째 매개 변수를 스택 추적으로 지정한다. 일반적으로 첫 번째 매개 변수는 Exception 계층 클래스 중 하나거나 이 클래스 중 하나의 인스턴스를 가리키는 참조다. 스택 추적은 일반적으로 Object#caller 메서드를 이용하여 만들어진다. 다음은 raise에 대한 일반적인 예제다.
raise
raise "Missing name" if name.nil?
if i >= names.size
raise IndexError, "#{i} >= size (#{names.size})"
end
raise ArgumentError, "name too big", caller
마지막 예제로 스택 백트레이스에서 현재 루틴을 제거해 보자. 이는 라이브러리 모듈에서 유용하게 사용된다. 먼저 caller 메서드를 사용해 현재 스택 추적을 가져온다. 한걸음 더 나아가 아래와 같이 새로운 예외 콜 스택의 일부만을 넘겨서 백트레이스에서 두 개의 루틴을 제거할 수 있다.
raise ArgumentError, "Name too big", caller[1..-1]
예외에 정보 추가하기
새로운 예외를 정의함으로 에러가 난 시점에서 알 수 있는 정보 중 전달할 필요가 있는 정보를 추가로 담을 수 있다. 예를 들어 어떤 네트워크 에러는 상황에 따라 일시적인 것일 수도 있다.
class RetryException < RuntimeError
attr :ok_to_retry
def initialize(ok_to_retry)
@ok_to_retry = ok_to_retry
end
end
코드 어딘가에 일시적인 에러가 일어났다.
def read_data(socket)
data = socket.read(512)
if data.nil?
raise RetryException.new(true), "transient read error"
end
# .. 일반적인 처리
end
호출 스택의 한 단계 위쪽에서 예외를 처리한다.
begin
stuff = read_data(socket)
# .. 처리한다.
rescue
retry if detail.ok_to_retry
raise
end
끄읕.