Elixir Korea

한국 엘릭서 사용자 모임 블로그


Elixir v1.10이 출시 되었습니다!

Friday, 31 Jan 2020 Tags: elixir

Elixir v1.10은 표준 라이브러리와 컴파일러를 개선했고, v1.9에서 추가된 mix release에 몇가지 기능을 추가했습니다. 특히, configuration과 sorting API를 정교하게 다듬었습니다.

Elixir v1.10은 Erlang/OTP 21+만을 지원하도록 바뀌었습니다. (역주: v1.9는 OTP 20+을 지원했음) 이 변경으로 인해 Erlang/OTP의 새로운 logger 시스템을 쓸 수 있게 되었습니다. (역주: logger는 OTP 21에서 추가됨) 앞으로 Erlang과 Elixir API는 logger의 level, metadata, message들을 모두 공유하게 됩니다.

추가된 개선 사항은 아래와 같습니다.

Releases 개선

Elixir v1.9는 독립형 애플리케이션으로 패키징하는 방법으로 releases 기능을 도입했습니다. Elixir v1.10은 커뮤니티의 피드백을 토대로 아래와 같은 개선을 이뤘습니다.

  • 이제 임베디드 시스템처럼 부팅시간이 중요한 환경을 위해 이중 부트 시스템을 해제할 수 있습니다.
  • 컴파일타임 configuration이 실행중 변경되는지 추적하고, 실행을 중단하게 할 수 있습니다.
  • 패키지된 release에 새로운 파일을 쉽게 추가할 수 있도록 overlay 기능을 추가했습니다.
  • 분산 시스템 기능을 완전히 끌 수 있도록 RELEASE_DISTRIBUTION 을 none 으로 설정할 수 있습니다.
  • :tar step 을 추가하여, 패키지를 tar 파일로 자동으로 묶을 수 있게 됐습니다.

Enum sort API 개선

Enum.sort/1는 기본적으로 낮은 값순으로 정렬합니다.

iex> Enum.sort(["banana", "apple", "pineapple"])
["apple", "banana", "pineapple"]

높은 값순으로 정렬하려면, Enum.sort(collection, &>=/2)처럼 값비교 함수와 함께 Enum.sort/2를 호출해야 하는데, 이런 방식은 코드 가독성을 해칩니다.

iex> Enum.sort(["banana", "apple", "pineapple"], &>=/2)
["pineapple", "banana", "apple"]

게다가 <=, >= 같은 비교 연산자는 struct를 비교할때 각 필드의 의미를 배제한채, 필드명순으로 필드 비교를 합니다. 예를 들어 Date를 비교할때 year, month, day 순으로 비교하는 것이 아니라, 필드명 순인 day, month, year 순으로 비교합니다.

iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]])
[~D[2020-01-01], ~D[2019-12-31]]

이를 방지하기 위해서는, 아래와 같이 값비교 함수를 전달해야 합니다.

iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], &(Date.compare(&1, &2) != :lt))
[~D[2019-12-31], ~D[2020-01-01]]

첫번째 문제에 대한 해법으로 Elixir v1.10에서는 :asc 또는 :desc로 정렬 방향을 지정할 수 있도록 개선되었습니다.

iex> Enum.sort(["banana", "apple", "pineapple"], :asc)
["apple", "banana", "pineapple"]
iex> Enum.sort(["banana", "apple", "pineapple"], :desc)
["pineapple", "banana", "apple"]

두번째 문제에 대한 해법으로 의미적 비교를 할 수 있도록 모듈을 전달할 수 있습니다. 위의 예에서 의미적 비교 기준으로 Date 모듈을 전달할 수 있고, 추가로 {:desc, Date}처럼 의미적 비교 기준과 정렬 방향을 동시에 지정할 수 있습니다.

iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], Date)
[~D[2019-12-31], ~D[2020-01-01]]
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], {:desc, Date})
[~D[2020-01-01], ~D[2019-12-31]]

개선된 API를 사용하면 코드가 더 간결해지고 가독성이 높아집니다. 해당 개선점은 Enum.sort_by, Enum.min_by, Enum.max_by 같은 동료 API에도 적용됩니다.

컴파일타임 설정 추적

Elixir는 코드를 애플리케이션으로 구조화합니다. 프로젝트가 의존하는 라이브러리나, 독립적으로 만든 다른 프로젝트들 모두 애플리케이션입니다. 이런 애플리케이션들은 모두 설정을 동반합니다.

애플리케이션 설정은 키-값 저장소입니다. 애플리케이션 설정은 실행시에 읽는것이 옳으나, 가끔은 컴파일타임에 설정을 읽는 경우도 있습니다. 컴파일타임에 설정을 읽기 위해서는 함수 정의 밖에서 Application.get_env/3를 사용합니다.

defmodule MyApp.DBClient do
 @db_host Application.get_env(:my_app, :db_host, "db.local")

  def start_link() do
    SomeLib.DBClient.start_link(host: @db_host)
  end
end

이 방식에는 코드가 컴파일되고 난 후에 설정을 바꾸는 경우, 실행시 바뀐 설정이 적용되지 않는다는 아주 큰 문제가 있습니다. 예를 들어, mix release로 패키징 하는 프로젝트에서 config/releases.exs 파일이 아래와 같을 때를 가정해 봅시다.

config :my_app, :db_host, "db.production"

config/releases.exs 파일은 컴파일 과정 이후에 읽히기 때문에, (역주: releases.exs는 릴리즈에 복사된후 Erlang 부트 과정에서 실행됨) 코드는 컴파일타임에 결정된 “db.local”로 연결하게 됩니다.

물론, 분명한 해결책으로 애플리케이션 설정을 컴파일타임에 읽지 않고 실행시에만 읽도록 하는 방법이 있습니다.

defmodule MyApp.DBClient do
  def start_link() do
    SomeLib.DBClient.start_link(host: db_host())
  end

  defp db_host() do
    Application.get_env(:my_app, :db_host, "db.local")
  end
end

이 방법이 우선시 되어야 하긴 하나, 두가지 상황에서 여전히 문제가 됩니다.

  1. 모든 사람이 이 함정을 알고 있는 것이 아니기 때문에, 컴파일타임에 설정을 읽는 코드를 작성할 것이고, 문제가 발생해야만 이 함정을 깨닫게 됩니다.
  2. 드물겠지만 정말로 컴파일타임에 설정을 읽어야 하는 경우가 있을 것이고, 컴파일타임에만 유효한 설정을 실행중에 읽으려 할때 경고 받길 원할수도 있습니다.

Elixir v1.10은 이 두가지 경우를 해결하기 위해 Application.compile_env/3 함수를 도입했습니다. 컴파일타임에 설정을 읽으려면 다음과 같이 합니다.

@db_host Application.compile_env(:my_app, :db_host, "db.local")

compile_env/3을 사용하면 Elixir가 컴파일타임에 사용된 값들을 저장해두고, 애플리케이션이 시작할때 런타임 값이 다를 경우 에러와 함께 중단시킵니다. 이를 통해 개발자는 시스템이 의도한 설정과 함께 실행된다는 것을 확신할 수 있습니다.

Elixir의 차후 버전에서는 컴파일타임에 Application.get_env/3을 사용하는 것에 대해 사용중단권고할 예정입니다.

컴파일러 추적

이번 버전에서는 컴파일러에 몇가지 기능을 추가해 개발자가 컴파일러 이벤트를 수신할 수 있도록 했습니다.

과거 버전에서 컴파일러는 사용중단권고나 정의되지않은 함수 호출등을 체크하기 위해, 모듈간 참조관계(예를 들면 함수 호출, struct 참조 등) 데이터베이스를 만들었습니다.

이 데이터베이스는 컴파일러 내부적으로 사용하기 위해 만든 것이었지만, 많은 개발자들이 이 데이터베이스를 사용하여 추가적인 체크를 하는 등의 개발을 했습니다. 그리고 추가적인 데이터를 포함해달라고 요청하기도 했는데, 이런 데이터들을 Elixir 컴파일러에게는 불필요한 데이터고, 애초에 이 데이터베이스는 내부적 사용을 위해 만든것이었기 때문에 문제가 되었습니다.

Elixir v1.10에서는 컴파일러 추적 기능을 통해 기존의 데이터베이스를 사용하지 않을 수 있도록 했습니다. 컴파일러 추적을 사용하면 개발자는 컴파일러가 보내는 모든 이벤트를 수신할 수 있고, 필요한 데이터만 저장하여 목적에 맞게 사용할 수 있습니다.

Elixir 자체도 이 기능을 사용하여 새로운 기능들을 구현하도록 설계되었습니다. 컴파일러 추적 도입을 통해 새롭게 할 수 있는 기능을 예로 들면, 정의되지 않은 함수 호출에 대한 경고를 함수 호출부에서 없애는 것입니다. 특정 환경에서는 불필요한 라이브러리에 대한 호출 등이 대상이 될 수 있고, 아래와 같은 형태입니다.

@compile {:no_warn_undefined, OptionalDependency}
defdelegate my_function_call(arg), to: OptionalDependency

이전에는 이런 정보가 함수 호출부가 아닌, 프로젝트 설정에 추가되었어야만 했습니다.

기타 개선 사항

Elixir의 캘린더 타입에 대해 3rd party 캘린더를 위한 sigil 지원과 DateTime.now!/2, DateTime.shift_zone/3, NaiveDateTime.local_now/0 추가 등 다양한 개선이 이뤄졌습니다.

Elixir AST에 대한 개선도 많았습니다. Code.string_to_quoted/2는 이제 :token_metadata:literal_encoder 옵션을 통해 Elixir parser를 좀더 정밀하게 설정할 수 있습니다. 이 정보들은 이전 버전에서도 code formatter에게 제공되었는데, 이번에 개발자들에게 공개된 것입니다. 이 개선과 컴파일러 추적 도입이 의미하는 것은 이제 Credo나 Boundary, IDE 등이 좀더 Elixir 코드를 잘 분석할 수 있도록 기반환경이 조성되었다는 것입니다.

ExUnit 또한 2가지의 작지만 중요한 개선점이 있습니다. 첫번째로 ExUnit.CaptureIO가 이제 concurrent 테스트를 지원합니다. 두번째로 assert에 pattern-matching diffing이 추가되었습니다. 아래 테스트에서

assert %{"status" => 200, "body" => %{"key" => "foo"}} = json_payload

json_payload가 아주 큰 JSON 덩어리이고, json_payload["body"]["key"]"foo" 가 아니라고 가정해 봅시다. 이전 버전에서는 assert가 실패하면 오른쪽을 모두 출력하였고, 무엇이 다른지 찾는것은 개발자의 몫이었습니다. Elixir v1.10에서는 데이터를 패턴에 비교하여 매치부분과 미스매치부분을 보여줍니다. 참고로 기존에도 데이터간 비교에는 이런 차이점 비교가 되었습니다. Elixir v1.10에서는 패턴 매칭에 대해서도 차이점 비교를 도입한 것입니다.

마지막으로, 새로운 guard 2가지가 추가 되었습니다. 바로 is_struct/1is_map_key/2 인데요, Erlang/OTP 21+ 이상만을 지원하는 덕에 가능해졌습니다.

더 자세한 내용은 전체 릴리즈 노트를 참고하시기 바랍니다.

즐코딩 하시길!