Mix 소개
Mix 소개 (Introduction to Mix)
이번 가이드에서는 자체 감독 트리(supervision tree), 설정, 테스트 등을 갖춘 완전한 Elixir 애플리케이션을 하나 함께 만들어 볼 거예요. 이 가이드의 요구 사항은 (elixir -v로 확인할 수 있어요) 다음과 같아요.
- Elixir 1.18.0 이상
- Erlang/OTP 27 이상
본문
만들 애플리케이션은 **분산 키-값 저장소(distributed key-value store)**로 동작해요. 키-값 쌍을 버킷(bucket)으로 묶고, 그 버킷을 여러 노드에 나눠 담을 거예요. 그리고 이 노드들 중 아무 노드에나 연결해 아래처럼 요청을 보낼 수 있는 간단한 클라이언트도 만들 거예요.
CREATE shopping
OK
PUT shopping milk 1
OK
PUT shopping eggs 3
OK
GET shopping milk
1
OK
DELETE shopping eggs
OK
키-값 애플리케이션을 만들기 위해 세 가지 주요 도구를 사용할 거예요.
- OTP(Open Telecom Platform) — Erlang에 딸려 오는 라이브러리 묶음이에요. Erlang 개발자들은 OTP로 견고하고 결함을 견디는(fault-tolerant) 애플리케이션을 만들어요. 이번 장에서는 감독 트리, 이벤트 매니저 등 OTP의 여러 측면이 Elixir와 어떻게 어우러지는지 살펴볼 거예요.
- Mix — Elixir에 딸려 오는 빌드 도구로, 애플리케이션 생성·컴파일·테스트, 의존성 관리 등 다양한 작업(task)을 제공해요.
- ExUnit — Elixir에 딸려 오는 단위 테스트 기반 프레임워크예요.
이번 장에서는 Mix로 첫 프로젝트를 만들고, 진행하면서 OTP·Mix·ExUnit의 다양한 기능을 살펴볼 거예요.
소스 코드
이 가이드에서 만드는 애플리케이션의 최종 코드는 이 저장소에 있어요. 참고 자료로 활용하면 좋아요.
이 가이드가 필수 읽을거리인가요?
이 가이드는 Elixir를 배우는 과정에서 필수 읽을거리는 아니에요. 설명을 드릴게요. Elixir 개발자라면 Elixir 코드를 작성할 때 기존에 있는 여러 프레임워크 중 하나를 쓰는 경우가 대부분이에요. Phoenix는 웹 애플리케이션을, Ecto는 데이터베이스 연동을, Nerves로 임베디드 소프트웨어를, Nx는 머신러닝·AI 프로젝트를, Membrane은 오디오·비디오 처리 파이프라인을, Broadway는 데이터 수집·처리를 담당하죠. 이런 프레임워크가 동시성·분산·결함 허용 같은 저수준 세부 사항을 처리해 주기 때문에, 여러분은 자신의 요구에 집중하면 돼요.
반대로, 이런 프레임워크가 바탕으로 삼은 기반과 Elixir 생태계를 움직이는 추상화를 직접 배우고 싶다면, 이 가이드가 여러 중요한 개념을 훑어 보는 좋은 길이 될 거예요.
첫 프로젝트 만들기
Elixir를 설치하면 elixir, elixirc, iex 실행 파일과 함께 mix라는 Elixir 스크립트 실행 파일도 얻게 돼요. 명령줄에서 mix new를 호출해 첫 프로젝트를 만들어 볼게요. 인자로 프로젝트 경로(kv)를 넘기면, 기본적으로 애플리케이션 이름과 모듈 이름이 경로에서 얻어져요. 그래서 기본값이었던 Kv 대신 우리 주 모듈이 전부 대문자인 KV가 되도록 Mix에 알려줄게요.
$ mix new kv --module KV
Mix는 kv라는 디렉터리를 만들고 그 안에 몇 개 파일을 생성해요.
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/kv.ex
* creating test
* creating test/test_helper.exs
* creating test/kv_test.exs
생성된 파일들을 잠깐 살펴볼게요.
PATH에 있는 실행 파일들
Mix는 Elixir 실행 파일이에요. 즉 mix를 실행하려면 mix와 elixir 실행 파일이 둘 다 PATH에 있어야 해요. Elixir를 설치하면 바로 그 상태가 되어 있어요.
프로젝트 컴파일
새 프로젝트 폴더(kv) 안에 mix.exs라는 파일이 생성됐어요. 이 파일의 주된 역할은 프로젝트를 설정하는 거예요. 한번 살펴볼게요.
defmodule KV.MixProject do
use Mix.Project
def project do
[
app: :kv,
version: "0.1.0",
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
]
end
end
우리 mix.exs는 공개 함수 두 개를 정의해요. project는 프로젝트 이름과 버전 같은 프로젝트 설정을 반환하고, application은 애플리케이션 파일을 생성하는 데 쓰여요. project 함수에서 호출되는 deps라는 비공개 함수도 있는데, 프로젝트 의존성을 정의해요. deps를 별도 함수로 정의하는 게 필수는 아니지만, 프로젝트 설정을 깔끔하게 유지하는 데 도움이 돼요.
Mix는 lib/kv.ex에도 hello라는 함수 하나를 정확히 가진 모듈이 담긴 파일을 생성해요.
defmodule KV do
@moduledoc """
Documentation for KV.
"""
@doc """
Hello world.
## Examples
iex> KV.hello()
:world
"""
def hello do
:world
end
end
이 구조면 프로젝트를 컴파일할 수 있어요.
$ cd kv
$ mix compile
그리고 다음이 출력돼요.
Compiling 1 file (.ex)
Generated kv app
lib/kv.ex 파일이 컴파일되고 kv.app라는 애플리케이션 매니페스트가 생성됐어요. 모든 컴파일 산출물은 mix.exs 파일에 정의된 옵션을 따라 _build 디렉터리 안에 놓여요. 프로젝트가 컴파일되면 아래 명령으로 프로젝트 안에서 iex 세션을 시작할 수 있어요. -S mix는 인터랙티브 셸에 프로젝트를 로드하기 위해 필요해요.
$ iex -S mix
우리는 이 kv 프로젝트를 계속 수정하고, 그 변경 사항을 iex 세션에서 바로 시험해 볼 거예요. 프로젝트 소스 코드가 바뀔 때마다 새 세션을 시작해도 되지만, iex 안에서 recompile 헬퍼로 프로젝트를 다시 컴파일할 수도 있어요.
iex> recompile()
Compiling 1 file (.ex)
:ok
iex> recompile()
:noop
컴파일할 게 있다면 안내 문구와 함께 :ok 아톰을 돌려받고, 그렇지 않으면 함수가 조용히 :noop을 반환해요.
테스트 실행하기
Mix는 프로젝트 테스트를 실행할 구조도 함께 생성했어요. Mix 프로젝트는 보통 lib 디렉터리의 각 파일마다 test 디렉터리에 <filename>_test.exs 파일을 두는 관례를 따르죠. 그래서 이미 lib/kv.ex에 대응하는 test/kv_test.exs가 있어요. 지금은 별로 하는 게 없어요.
defmodule KVTest do
use ExUnit.Case
doctest KV
test "greets the world" do
assert KV.hello() == :world
end
end
여기서 몇 가지 짚고 넘어갈게요.
- 테스트 파일은 Elixir 스크립트 파일(
.exs)이에요. 실행 전에 테스트 파일을 컴파일할 필요가 없어서 편리해요. KVTest라는 테스트 모듈을 정의하고, use ExUnit.Case로 테스트 API를 주입해요.- 가져온 매크로 중 하나인 ExUnit.DocTest.doctest/1을 사용해
KV모듈에 doctest가 있음을 나타내요(나중 장에서 다룰 거예요). - ExUnit.Case.test/2 매크로로 간단한 테스트 하나를 정의해요.
Mix는 test/test_helper.exs라는 파일도 생성했는데, 테스트 프레임워크를 설정하는 역할을 해요.
ExUnit.start()
테스트를 실행할 때마다 Mix가 이 파일을 먼저 요구(require)해요. 테스트는 이렇게 실행해요.
$ mix test
Compiled lib/kv.ex
Generated kv app
Running ExUnit with seed: 540224, max_cases: 16
..
Finished in 0.04 seconds
1 doctest, 1 test, 0 failures
mix test를 실행하면 Mix가 소스 파일을 컴파일하고 애플리케이션 매니페스트를 다시 생성하는 걸 볼 수 있어요. Mix가 여러 환경(environment)을 지원하기 때문인데, 이번 장에서 곧 다룰게요. 또 ExUnit이 성공한 테스트마다 점(dot)을 출력하고 테스트를 자동으로 무작위 섞는다는 것도 볼 수 있어요. 이제 일부러 테스트를 실패시켜 볼게요. test/kv_test.exs의 단언(assertion)을 아래처럼 바꿔 보세요.
assert KV.hello() == :oops
다시 mix test를 실행해 볼게요(이번에는 컴파일이 없을 거예요).
1) test greets the world (KVTest)
test/kv_test.exs:5
Assertion with == failed
code: assert KV.hello() == :oops
left: :world
right: :oops
stacktrace:
test/kv_test.exs:6: (test)
.
Finished in 0.05 seconds
1 doctest, 1 test, 1 failure
실패할 때마다 ExUnit은 테스트 케이스가 있는 테스트 이름, 실패한 코드, == 연산자의 왼쪽(left)·오른쪽(RHS) 값이 담긴 상세 보고서를 출력해요. 실패 보고서의 두 번째 줄, 테스트 이름 바로 아래에는 테스트가 정의된 위치가 있어요. 파일과 줄 번호를 포함한 테스트 위치 전체를 복사해 mix test 뒤에 붙이면, Mix가 그 특정 테스트 하나만 로드해 실행해요.
$ mix test test/kv_test.exs:5
이 지름길은 프로젝트를 만들며 단일 테스트만 빠르게 반복 실행할 수 있게 해 주는, 매우 유용한 도구가 돼요. 마지막으로 스택트레이스(stacktrace)는 실패 자체에 대한 정보를 주고, 테스트와 종종 소스 파일 안에서 실패가 발생한 위치를 알려줘요.
자동 코드 포맷팅
mix new가 생성하는 파일 중 하나가 .formatter.exs예요. Elixir에는 일관된 스타일에 맞춰 코드베이스를 자동으로 포맷팅하는 코드 포맷터가 딸려 오고, mix format 작업으로 실행돼요. 생성된 .formatter.exs 파일은 mix format 실행 시 어떤 파일을 포맷팅할지 설정해요. 포맷터를 시험해 보려면 lib나 test 디렉터리의 파일을 def hello do처럼 공백이나 새 줄을 추가해 바꾼 뒤 mix format을 실행하면 돼요.
대부분의 편집기는 포맷터와 기본 통합을 지원해서, 파일을 저장할 때나 선택한 키 바인딩으로 포맷팅할 수 있어요. Elixir를 배우는 중이라면 편집기 통합은 Elixir 문법을 익힐 때 유용하고 빠른 피드백을 줘요. 회사나 팀에서는 지속적 통합(CI) 서버에서 mix format --check-formatted를 실행해 현재와 앞으로의 모든 코드가 표준을 따르도록 권장해요.
코드 포맷터에 대해 더 알고 싶다면 format 작업 문서를 확인하거나, 포맷터가 처음 포함된 Elixir v1.6 릴리스 공지를 읽어 보세요.
환경 (Environments)
Mix는 "환경(environment)"이라는 개념을 제공해요. 특정 시나리오에 맞춰 컴파일이나 다른 옵션을 커스터마이즈할 수 있게 해 주죠. 기본적으로 Mix는 세 가지 환경을 이해해요.
:dev—compile같은 Mix 작업이 기본으로 실행되는 환경:test— mix test가 사용하는 환경:prod— 프로덕션에서 프로젝트를 실행할 때 사용하는 환경
환경은 현재 프로젝트에만 적용돼요. 앞으로의 장에서 보겠지만, 프로젝트에 추가하는 어떤 의존성도 기본적으로 :prod 환경에서 실행돼요. 환경별 커스터마이즈는 mix.exs 파일에서 Mix.env/0에 접근해 할 수 있는데, 이 함수가 현재 환경을 아톰으로 반환해요. 바로 그걸 :start_permanent 옵션에서 사용했죠.
def project do
[
...,
start_permanent: Mix.env() == :prod,
...
]
end
:start_permanent 옵션이 true면 애플리케이션을 영구(permanent) 모드로 시작해요. 즉 애플리케이션의 감독 트리가 종료되면 Erlang VM이 크래시해요. dev·test 환경에서는 문제 해결 목적으로 VM 인스턴스를 계속 띄워 두는 게 유용하므로, 이런 동작을 원하지 않는다는 점에 주의하세요. Mix는 기본적으로 :dev 환경을 사용하고, test 작업만 기본적으로 :test 환경을 사용해요. 환경은 MIX_ENV 환경 변수로 바꿀 수 있어요.
$ MIX_ENV=prod mix compile
Windows에서는 이렇게 해요.
> set "MIX_ENV=prod" && mix compile
프로덕션에서의 Mix
Mix는 **빌드 도구(build tool)**이기 때문에 프로덕션에서 사용 가능할 것으로 기대되지 않아요. 그래서 Mix.env/0는 설정 파일과 mix.exs 안에서만 접근하고, 애플리케이션 코드(lib)에서는 절대 접근하지 않는 걸 권장해요.
더 탐구하기 (Exploring)
Mix에는 더 많은 것이 있고, 프로젝트를 만들면서 계속 탐구하게 될 거예요. 전반적인 개요는 Mix 문서에서 확인할 수 있고, 언제든 help 작업을 호출해 사용 가능한 모든 작업을 나열할 수 있어요.
$ mix help
$ mix help compile
이제 첫 모듈과 함수를 애플리케이션에 추가해 보도록 할게요.