#elixirlang 환경 변수로부터 N개의 설정을 읽기
프로그램에 실행 옵션을 넘기는 방법
자주 쓰는 프로그램 중에 옵션을 제공하지 않는 프로그램을 생각해 봤다. 예를 들려고 생각해 봤는데, 좀 생각해 보다 말았다. 떠올리는 게 어려웠다. 힘들여서 짰으니 당연히 옵션을 제공해서 활용 범위를 넓히고 싶을 거다.
실행 옵션을 넘기는 일반적인 방법을 알아보자.
첫 번째로는 커맨드라인으로 실행할 때, 많이 사용하는 인자(argument)가 있다. ls -la
와 같이 프로그램인 ls
뒤에 -la
처럼 인자로 옵션을 넘기는 방법이다. elixir에서는 escript로 만들 경우 main/1
함수 인자를 사용한다. 혹은 System.argv/0 함수를 호출해 커맨드라인 인자를 가져온다. 이렇게 가져온 인자를 OptionParser 모듈 함수를 사용해 파싱한다.
두 번째 방법은 설정 파일을 사용하는 방법이다. 커맨드라인 실행 인자로 넘기기에 많은 설정이 있을 때, 유용하게 사용할 수 있다. elixir에서는 config/runtime.exs 파일이 이 역할을 한다. 혹은 elixir 1.9, elixir 1.10 버전을 사용하고 있다면 config/release.exs 파일을 사용하면 된다.
세 번째 방법은 환경 변수를 사용하는 방법이다. 모든 프로그램이 접근할 수 있는 key-value 저장소를 실행 인자로 사용하는 방법이다. 옵션을 설정하는 방법이 명시적이지가 않다. 실행한 커맨드라인 인자를 보거나 설정 파일을 열어보는 게 아니라 많은 환경 변수 중에서 이 프로그램이 사용하는 변수의 값을 찾아봐야 한다. 어라 단점만 있네? 아니다. 간편하다. elixir에서는 System.get_env/2 함수를 사용해서 읽을 수 있다.
왜 환경 변수를 사용했는가? 그리고 생긴 문제
실행 옵션을 넘기는 방법 중 하나의 방법만 죽어라 고집할 필요는 없다. 개인적으로는 실행 인자로 어떻게든 비벼보고 이게 정 안되면 파일로 실행 옵션을 넘긴다. 만약 환경 변수 세팅을 아주 잘 지원하는 Heroku로 서비스를 실행해야 한다면? 뭘 어째? 환경 변수에서 설정을 읽을 수 있게 짜면 된다.
tbot-800.ex 프로젝트에 필요한 설정을 환경 변수로 넘겨야 한다. tbot-800.ex
는 일정 주기로 트윗하는 프로그램이다. 트윗하기 위한 key와 token 등을 환경 변수로 세팅한다. 이건 문제가 안 되는데, 진짜 문제는 트위터 계정 여러 개를 지원하면서 생겼다. ACCOUNT1_KEY
, ACCOUNT2_KEY
처럼 숫자를 붙여서 구분하면 되긴 하는데, 이걸 몇 개까지 지원해야 하지? 지금 2개니깐 하드코딩? 아니면 10개 정도만 지원하게 할까?
리스트 정보를 key-value 로 넣으려다 보니 생긴 문제다. 하드코딩은 싫고 어느 정도는 예쁘게 해결하고 싶다.
runtime.exs
파일에서 환경 변수 읽어서 가공
프로그램을 시작하면 config/runtime.exs
파일을 평가(evaluation)해서 설정을 읽어 들인다. 즉, 이 파일도 elixir 소스 코드라서 환경 변수를 읽어 여기서 가공하면 된다.
트위터 계정을 설정한 개수만큼 모두 지원하는 코드를 살펴보자.
Stream.iterate(1, &(&1 + 1)) #<-- 1
|> Stream.map(fn index -> #<-- 2
[
# 환경 변수로부터 트위터 계정을 사용하기 위한 여러 정보를 읽는다
consumer_key: System.get_env("ACCOUNT#{index}_KEY")
]
end)
|> Enum.take_while(fn account -> #<-- 3
Enum.all?(Keyword.values(account), &(!is_nil(&1)))
end)
1번 코드로 1부터 1씩 증가시키는 index를 무한히 만들어 낸다. 2번 코드로 ACCOUNT1_KEY
, ACCOUNT2_KEY
와 같은 환경 변수에서 값을 읽는다. 만약 해당 환경 변수가 정의되지 않으면 nil
값이 들어간다. 3번 코드로 모든 값이 nil
이 아닌 설정까지만 읽게 한다. 1번 코드에서 지연 열거형(lazy enumerable) Stream 모듈을 사용해서 3번 코드로 가져가는 만큼만 index를 1씩 증가시키며 만들어 내게 했다.
만약에 ACCOUNT1_KEY
, ACCOUNT2_KEY
, ACCOUNT4_KEY
이런 식으로 3을 빼먹고 정의하면 ACCOUNT4_KEY
를 읽지 않는다. ACCOUNT3_KEY
를 읽어보고 없어서 nil
값이 되므로 여기서 멈추기 때문이다. 복잡하게 이빨 빠진 것까지 지원할 필요는 없어서 이대로 놔두기로 했다. 이 정도 룰은 지키면 된다. 이상한 룰도 아니다.
accounts =
Stream.iterate(1, &(&1 + 1))
|> Stream.map(fn index ->
[
consumer_key: System.get_env("ACCOUNT#{index}_KEY"),
consumer_secret: System.get_env("ACCOUNT#{index}_SECRET"),
access_token: System.get_env("ACCOUNT#{index}_TOKEN"),
access_token_secret: System.get_env("ACCOUNT#{index}_TOKEN_SECRET"),
interval:
String.to_integer(System.get_env("ACCOUNT#{index}_INTERVAL_MINUTE", "60")) * 1000 * 60,
tweet_items_url: System.get_env("ACCOUNT#{index}_TWEET_ITEMS_URL")
]
end)
|> Enum.take_while(fn account ->
Enum.all?(Keyword.values(account), &(!is_nil(&1)))
end)
읽는 전체 코드다. index 숫자로 그룹핑한 환경 변수를 elixir Keyword 리스트로 만들어 준다. elem[:access_token]
과 같은 식으로 접근해서 값을 가져올 수 있다. 여러 계정을 지원하므로 Keyword 리스트의 리스트가 accounts
변수에 바인딩 된다. 0aaac2879b 커밋 내용을 참고한다.
환경 변수에서 값을 읽어서 elixir term으로 변환하는 역할을 runtime.exs
에서 하는 게 모범 사례(best practice)라는 생각이 들었다. 프로그램에 영향을 끼칠 수 있는 환경 변수라는 외부 종속성을 runtime.exs
파일까지로 제한할 수 있다.
개발 중에는 환경 변수 말고 파일로 설정할 수 있게 한다
로컬에서 테스트할 때는 파일로 설정하는 게 편하다. 환경 변수로 세팅하는 건 순전히 Heroku 때문이다. Heroku가 대시보드에서 환경 변수 세팅을 할 수 있게 잘 지원해 주기 때문이다. 로컬에서 파일로 설정할 수 있게 지원해 주자. 그리 힘든 일도 아니다.
if accounts != [] do
config :tbot800, tbot_accounts: accounts
end
환경 변수에서 읽은 트위터 계정 정보가 있을 때만 설정으로 저장한다. 분기 없이 무조건 저장하면 될 것 같은데 이렇게 하는 이유는 환경 변수가 아닌 파일로 직접 세팅을 지원하기 위해서다.
로컬에서 테스트할 때는 git 버전 컨트롤에서 제외한 dev.secret.exs
같은 파일에 설정을 넣어서 테스트한다.
config :tbot800,
tbot_accounts: [
[
consumer_key: "consumer_key",
consumer_secret: "consumer_secret",
access_token: "access_token",
access_token_secret: "access_token_secret",
interval: 1000 * 60 * 60,
tweet_items_url: "http://ohrepos.github.io/pquotes-repo/quotes/tweet_items.exs"
],
[
consumer_key: "consumer_key",
consumer_secret: "consumer_secret",
access_token: "access_token",
access_token_secret: "access_token_secret",
interval: 1000 * 60 * 60,
tweet_items_url: "http://ohrepos.github.io/quotes-repo/quotes/tweet_items.exs"
]
]
0aaac2879b 커밋 내용을 참고한다.
링크
- 10년 전에 Clojure로 짠 트위터 인용봇을 Elixir로 재작성한 후기 - ohyecloudy.com
- Elixir v1.9 released - The Elixir programming language - elixir-lang.org
- Configuration and releases - The Elixir programming language - elixir-lang.org
- ohyecloudy/tbot-800.ex - github.com
- Stream — Elixir v1.14.5 - hexdocs.pm
- Keyword — Elixir v1.15.4 - hexdocs.pm
- OptionParser — Elixir v1.15.4 - hexdocs.pm
- System — Elixir v1.15.4 - hexdocs.pm
- mix escript.build — Mix v1.16.0-dev - hexdocs.pm
- Cloud Application Platform - heroku.com