Code

Elixir – 19: try, catch and rescue

March 8, 2016

author:

Array

Elixir – 19: try, catch and rescue

Elixir Tutorial 시리즈입니다. 거의 대부분은 튜토리얼의 한글 번역에 가깝습니다만, 생략되거나 추가로 주석을 달거나 하는 부분이 많습니다. 원문은 최하단의 링크를 참고하세요.

Elixir – 19: try, catch and rescue

Elixir에는 error, throw, exit이라는 3가지의 에러에 관련된 구조를 가지고 있습니다. 이번 장에서는 이 세가지에 대해서 무엇인지, 어떻게 사용해야하는 지에 대해서 간략하게 다룹니다.

Errors

에러 (또는 예외)는 코드에서 예외적인 상황이 발생했을 때에 사용됩니다. 아톰에 숫자를 더하여 간단한 에러를 만들어보죠.

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression
     :erlang.+(:foo, 1)

raise/1를 사용해서 런타임 중에 에러를 발생시킬 수도 있습니다.

iex> raise "oops"
** (RuntimeError) oops

raise/2에 에러 이름과 키워드 매개변수를 넘겨서 에러를 만들 수도 있습니다.

iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo

아니면 모듈을 직접 만들고 defexception를 사용해서 새로운 에러를 정의할 수도 있습니다. 이 경우에는 에러를 던질 때에 예외를 정의한 모듈명과 동일한 이름을 넘겨주면 됩니다. 가장 일반적인 예외라고 한다면 메시지 필드를 하나 사용하는 에러일 것입니다.

iex> defmodule MyError do
iex>   defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message

이러한 에러는 try/rescue 구조를 사용해서 처리할 수 있습니다.

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

런타임 에러를 처리하는 이 예제에서는 에러 자신을 반환하며, iex 세션에 그대로 출력됩니다.

만약 에러를 다룰 필요가 없다면, 따로 관리할 필요가 없습니다.

iex> try do
...>   raise "oops"
...> rescue
...>   RuntimeError -> "Error!"
...> end
"Error!"

하지만 Elixir 개발자들은 try/rescue를 관습저긍로 사용하지 않습니다. 예를 들어, 많은 언어들이 파일을 정상적으로 열지 못했을 때 에러를 처리하도록 강요합니다. Elixir는 그 대신에 File.read/1 함수가 파일이 성공적으로 열렸는지에 대한 정보가 포함된 튜플을 반환합니다.

iex> File.read "hello"
{:error, :enoent}
iex> File.write "hello", "world"
:ok
iex> File.read "hello"
{:ok, "world"}

여기에는 try/rescue가 존재하지 않습니다. 여기에서 파일이 어떻게 열렸는지에 대한 정보를 얻으려면 그냥 case를 사용하여 패턴 매칭을 하면 됩니다.

iex> case File.read "hello" do
...>   {:ok, body}      -> IO.puts "Success: #{body}"
...>   {:error, reason} -> IO.puts "Error: #{reason}"
...> end

마지막으로 파일을 열 때에 에러를 던질지 말지는 전적으로 애플리케이션의 구현에 달려 있으며, 이는 바로 Elixir의 File.read/1와 같은 함수들이 예외를 노출하지 않는 이유입니다. 대신에 이 결정을 개발자가 가장 최선인 방식을 선택할 수 있도록 남겨둡니다.

만약 파일이 존재하기를 기대하고 문제가 있는 경우에 반드시 에러를 던지기를 바란다면, File.read!/1를 사용하세요.

iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory
    (elixir) lib/file.ex:305: File.read!/1

표준 라이브러리에 있는 많은 함수들은 튜플을 반환하는 함수와, 이 대신에 에러를 던지는 함수를 모두 가지고 있습니다. 함수(foo)를 정의할 때에 일반적인 관습으로는 {:ok, result}이나 {:error, reason}이라는 튜플을 반환하도록 만들며, 그리고 다른 함수(foo!, 같은 함수지만 !를 어미에 사용)에서 같은 매개 변수를 받고, 에러가 있는 경우에 에러를 던지게끔 합니다. foo!는 아무런 문제가 없는 경우, 반드시 (튜플로 감싸지지 않은) 결과를 반환해야 합니다. File 모듈이 이 관습이 좋은 예시입니다.

Elixir에서는 제어 흐름에서 에러를 사용하지 않는다는 이유로, try/rescue를 사용을 피하고 있습니다. 에러는 기대되지 않은 예외적인 상황을 위해서 예약되어 있으며, 에러를 문자 그대로의 의미로써 사용하고 있습니다. 정말로 제어 구조를 사용하고 싶다면, 다음에서 볼 throw를 사용해야 합니다.

Throws

Elixir에서는 값을 던지고 잡을 수 있습니다. throwcatch가 이러한 경우를 위해서 예약어로 지정 되어 있습니다.

이러한 상황은 라이브러리가 올바른 API를 제공하지 않는 경우가 아니면 맞닥뜨리기 쉽지 않습니다. 예를 들어, Enum 모듈이 값을 찾기 위한 API를 전혀 제공하지 않는 상황에서, 리스트에 있는 숫자에서 첫번째 13의 배수를 찾을 필요가 있다고 해보죠.

iex> try do
...>   Enum.each -50..50, fn(x) ->
...>     if rem(x, 13) == 0, do: throw(x)
...>   end
...>   "Got nothing"
...> catch
...>   x -> "Got #{x}"
...> end
"Got -39"

Enum은 이에 알맞는 API을 이미 제공하고 있기 때문에, 실제로는 Enum.find/2를 사용해서 구현하면 됩니다.

iex> Enum.find -50..50, &(rem(&1, 13) == 0)
-39

Exits

모든 Elixir의 코드는 프로세스 내에서 동작하며, 각 프로세스 간에 통신을 하게 됩니다. 처리되지 않은 예외 같은 “일반적인 이유”로 프로세스가 죽었을 때, 이 프로세스는 exit 신호를 보냅니다. 아니면 명시적으로 종료 신호를 보내고 죽을 수도 있습니다.

iex> spawn_link fn -> exit(1) end
#PID<0.56.0>
** (EXIT from #PID<0.56.0>) 1

이 예제에서는 연결된 프로세스가 exit 신호를 값 1과 함께 전송하고 죽습니다. Elixir 셸은 이 메시지를 자동으로 처리하고 터미널에 출력해줍니다.

exittry/catch로도 “잡을 수” 있습니다.

iex> try do
...>   exit "I am exiting"
...> catch
...>   :exit, _ -> "not really"
...> end
"not really"

try/catch를 사용하는 것은 흔하지 않으며, 이를 사용해서 종료 신호를 잡는 것은 더욱 흔하지 않습니다.

exit 신호는 Erlang VM 기반의 fault tolerant 시스템에 있어서 무척 중요한 역할을 담당합니다. 프로세스는 보통 관리 트리 하에서 실행되며, 관리 프로세스는 exit 신호가 하위 프로세스로부터 보내질 때까지 기다리게 됩니다. 일단 종료 신호를 받게 되면 관리 전략이 동작하며, 해당 프로세스를 재시작하게 됩니다.

Elixir에서는 이러한 관리 시스템이 try/catchtry/rescue와 같은 구조를 일반적이지 않게 만듭니다. 관리 트리가 애플리케이션을 에러가 발생하기 전의 상태로 되돌려주기 때문에 에러를 처리하는 대신 “빠르게 실패합니다”.

After

때때로 에러를 발생시킬 가능성이 있는 행동 후에 리소스의 관리를 보장해야할 때가 있습니다. try/after 구조가 이를 가능하게 해주는데, 예를 들어, 파일을 열고, 어떤 작업을 한 후에 이를 닫는 동작을 보장해야할 때가 있을 겁니다.

iex> {:ok, file} = File.open "sample", [:utf8, :write]
iex> try do
...>   IO.write file, "olá"
...>   raise "oops, something went wrong"
...> after
...>   File.close(file)
...> end
** (RuntimeError) oops, something went wrong

Variables scope

try/catch/rescue/after 블럭 내부에서 정의한 변수들이 외부에 반영되지 않는다는 점을 명심하세요. 만약 try 블럭이 실패하는 경우, 그러한 변수들은 절대 처음으로 돌아가지 않습니다. 다시 말해서 이 코드는 동작하지 않습니다.

iex> try do
...>   raise "fail"
...>   what_happened = :did_not_raise
...> rescue
...>   _ -> what_happened = :rescued
...> end
iex> what_happened
** (RuntimeError) undefined function: what_happened/0

대신 try 표현식의 값을 저장할 수 있습니다.

iex> what_happened =
...>   try do
...>     raise "fail"
...>     :did_not_raise
...>   rescue
...>     _ -> :rescued
...>   end
iex> what_happened
:rescued

Reference

Array