예외 처리
예외 처리 (Exception handling)
Crystal의 에러 처리 방식은 예외를 던지고(raise) 잡아내는(rescue) 것이에요. 문제가 생길 만한 코드를 보호하고, 실패했을 때 어떻게 대응할지를 구조적으로 표현하는 방식이죠.
출처: Crystal 공식 문서
본문
예외 던지기 (Raising exception)
예외는 최상위(top-level) raise 메서드를 호출해서 던져요. 다른 키워드들과 달리 raise는 두 개의 오버로드가 있는 일반 메서드예요. 하나는 String을 받는 것, 다른 하나는 Exception 인스턴스를 받는 것이에요:
raise "OH NO!"
raise Exception.new("Some error")
String 버전은 그 메시지로 새 Exception 인스턴스를 만들 뿐이에요.
Exception 인스턴스나 그 서브클래스만 던질 수 있어요.
사용자 정의 예외 만들기 (Defining custom exceptions)
사용자 정의 예외 타입을 만드려면 Exception을 상속하기만 하면 돼요:
class MyException < Exception
end
class MyOtherException < Exception
end
언제나 그랬듯 예외용 생성자를 정의할 수도 있고, 기본 생성자를 그냥 쓰는 것도 가능해요.
예외 잡기 (Rescuing exceptions)
어떤 예외든 잡으려면 begin ... rescue ... end 표현식을 쓰면 돼요:
begin
raise "OH NO!"
rescue
puts "Rescued!"
end
# Output: Rescued!
잡아낸 예외에 접근하려면 rescue 절에서 변수를 지정하면 돼요:
begin
raise "OH NO!"
rescue ex
puts ex.message
end
# Output: OH NO!
한 종류의 예외(또는 그 서브클래스들)만 잡을 수도 있어요:
begin
raise MyException.new("OH NO!")
rescue MyException
puts "Rescued MyException"
end
# Output: Rescued MyException
유효한 타입 제한은 ::Exception의 서브클래스, 모듈 타입, 그리고 그것들의 유니온이에요.
거기에 접근하려면 타입 제한과 비슷한 문법을 써요:
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts "Rescued MyException: #{ex.message}"
end
# Output: Rescued MyException: OH NO!
rescue 절은 여러 개를 지정할 수 있어요:
begin
# ...
rescue ex1 : MyException
# only MyException...
rescue ex2 : MyOtherException
# only MyOtherException...
rescue
# any other kind of exception
end
유니온 타입을 지정하면 여러 예외 타입을 한 번에 잡을 수도 있어요:
begin
# ...
rescue ex : MyException | MyOtherException
# only MyException or MyOtherException
rescue
# any other kind of exception
end
else
else 절은 잡아낸 예외가 없을 때만 실행돼요:
begin
something_dangerous
rescue
# execute this if an exception is raised
else
# execute this if an exception isn't raised
end
else 절은 rescue 절이 적어도 하나 이상 있을 때만 지정할 수 있어요.
ensure
ensure 절은 예외가 던져졌는지 여부와 관계없이 begin ... end나 begin ... rescue ... end 표현식의 끝에서 실행돼요:
begin
something_dangerous
ensure
puts "Cleanup..."
end
# Will print "Cleanup..." after invoking something_dangerous,
# regardless of whether it raised or not
또는:
begin
something_dangerous
rescue
# ...
else
# ...
ensure
# this will always be executed
end
ensure 절은 보통 정리(clean up), 리소스 해제 등을 위해 사용해요.
짧은 문법 형태 (Short syntax form)
예외 처리는 짧은 문법 형태가 있어요. 메서드나 블록 정의를 암시적인 begin ... end 표현식으로 보고, 그 안에 rescue, else, ensure 절을 지정하면 돼요:
def some_method
something_dangerous
rescue
# execute if an exception is raised
end
# The above is the same as:
def some_method
begin
something_dangerous
rescue
# execute if an exception is raised
end
end
ensure와 함께:
def some_method
something_dangerous
ensure
# always execute this
end
# The above is the same as:
def some_method
begin
something_dangerous
ensure
# always execute this
end
end
# Similarly, the shorthand also works with blocks:
(1..10).each do |n|
# potentially dangerous operation
rescue
# ..
else
# ..
ensure
# ..
end
rescue와 ensure의 접미사 형태 (Suffix forms)
rescue의 접미사 형태를 쓰면 한 줄로 된 catch-all 예외 처리를 만들 수 있어요. 접미사 형태에서 rescue에 예외 타입을 지정할 수는 없어요.
text = File.read("this_file_may_not_exist") rescue nil
이것은 다음과 같아요:
text = begin
File.read("this_file_may_not_exist")
rescue
nil
end
ensure의 접미사 형태를 써서 rescue와 비슷한 한 줄 보장(guarantee)을 만들 수도 있어요.
x ensure y
이것은 다음과 같아요:
begin
x
ensure
y
end
타입 추론 (Type inference)
예외 처리기의 begin 부분에서 선언된 변수는 rescue나 ensure 본문 안에서 고려될 때 Nil 타입도 갖게 돼요. 예를 들어:
begin
a = something_dangerous_that_returns_Int32
ensure
puts a + 1 # error, undefined method '+' for Nil
end
이런 일은 something_dangerous_that_returns_Int32가 절대 예외를 던지지 않더라도, 또는 a에 값을 할당한 뒤 예외를 던질 수 있는 메서드가 실행되더라도 마찬가지로 일어나요:
begin
a = 1
something_dangerous
ensure
puts a + 1 # error, undefined method '+' for Nil
end
a에는 항상 값이 할당된다는 게 분명해 보여도, 컴파일러는 여전히 a가 초기화될 기회를 얻지 못했을 수도 있다고 생각해요. 이 로직은 미래에 개선될 수도 있지만, 지금으로서는 예외 처리 코드를 필요한 최소한으로 유지하도록 강제해서 코드의 의도를 더 명확하게 만들어 줘요:
# Clearer than the above: `a` doesn't need
# to be in the exception handling code.
a = 1
begin
something_dangerous
ensure
puts a + 1 # works
end
에러 처리의 다른 방법들 (Alternative ways to do error handling)
예외가 에러 처리를 위한 메커니즘 중 하나로 제공되긴 하지만, 그것만이 유일한 선택은 아니에요. 예외를 던지는 것은 메모리 할당을 수반하고, 예외 처리기를 실행하는 것은 일반적으로 느려요.
표준 라이브러리는 보통 어떤 일을 하는 데 두 개의 메서드를 제공해요. 하나는 예외를 던지고, 다른 하나는 nil을 반환하죠. 예를 들어:
array = [1, 2, 3]
array[4] # raises because of IndexError
array[4]? # returns nil because of index out of bounds
일반적인 관례는 "물음표" 방식의 대안 메서드를 제공해서 해당 변형이 예외를 던지는 대신 nil을 반환한다는 걸 알려주는 거예요. 이를 통해 사용자는 예외를 다룰지 nil을 다룰지 선택할 수 있어요. 다만 모든 메서드에 이런 대안이 있는 건 아니라는 점을 기억하세요. 예외가 에러 처리 로직으로 코드를 오염시키지 않기 때문에 여전히 선호되는 방식이에요.
더 알아보기
- 예외는
raise로 던지고begin ... rescue ... end로 잡아요.else는 예외가 없을 때,ensure는 예외 여부와 무관하게 항상 실행돼요. - 메서드·블록 본문에 직접
rescue/else/ensure를 쓸 수 있고,expr rescue nil같은 접미사 형태도 있어요. begin안에서 선언한 변수는rescue/ensure안에서Nil타입을 갖게 될 수 있어요.- 마지막으로, 예외의 오버헤드를 피하고 싶다면
nil을 반환하는 "물음표" 메서드 대안을 쓸 수 있어요.