Module — 모듈을 다루는 함수들
Module — 모듈을 다루는 함수들
Module은 컴파일 타임에 모듈을 다루는 함수들을 제공해요. 속성을 동적으로 추가·삭제·등록하고, 문서를 붙이는 등의 작업을 할 수 있죠.
출처: Module
본문
Module은 컴파일 타임에 모듈을 다루는 함수들을 제공해요. 개발자가 속성을 동적으로 추가·삭제·등록하고, 문서(documentation)를 붙이는 등의 작업을 할 수 있게 해 주죠.
모듈이 컴파일된 뒤에는 이 모듈의 많은 함수를 쓰면 오류가 발생해요. 런타임 데이터를 검사하는 건 이 모듈의 범위 밖이기 때문이에요. 대부분의 런타임 데이터는 각 컴파일된 모듈에 붙어 있는 __info__/1 함수로 검사할 수 있어요.
모듈 속성 (Module attributes)
각 모듈은 하나 이상의 속성으로 장식(데코레이션)될 수 있어요. 다음은 Elixir가 현재 정의하고 있는 속성들이에요.
@after_compile
현재 모듈이 컴파일된 바로 직후에 호출되는 훅이에요. 모듈이나 {module, function_name}을 받아요. 아래의 "Compile callbacks" 절을 참고하세요.
@after_verify (since v1.14.0)
현재 모듈이 정의되지 않은 함수나 deprecation 등을 위해 검증된 직후에 호출되는 훅이에요. 모듈이나 {module, function_name}을 받아요. 아래의 "Compile callbacks" 절을 참고하세요.
@before_compile
모듈이 컴파일되기 전에 호출되는 훅이에요. 모듈이나 {module, function_or_macro_name} 튜플을 받아요. 아래의 "Compile callbacks" 절을 참고하세요.
@behaviour
영국식 철자(behaviour)라는 점에 주의하세요!
모듈은 @behaviour로 참조해서, @callback이 정의한 필요한 함수 시그니처를 구현했는지 보장할 수 있어요.
예를 들어 URI.Parser behaviour를 이렇게 지정할 수 있어요:
defmodule URI.Parser do
@doc "Defines a default port"
@callback default_port() :: integer
@doc "Parses the given URL"
@callback parse(uri_info :: URI.t()) :: URI.t()
end
그러면 어떤 모듈은 이렇게 사용할 수 있어요:
defmodule URI.HTTP do
@behaviour URI.Parser
def default_port(), do: 80
def parse(info), do: info
end
behaviour가 바뀌거나 URI.HTTP가 콜백 중 하나를 구현하지 않으면 경고가 발생해요. 자세한 문서는 behaviour typespec 문서를 참고하세요.
@impl (since v1.5.0)
behaviour를 올바르게 구현하는 것을 돕기 위해, 구현한 콜백에 @impl을 선택적으로 선언할 수 있어요. 이렇게 하면 콜백이 명시적으로 드러나고 코드의 오류를 잡는 데 도움이 돼요. 컴파일러는 다음 경우에 경고해요:
- 함수가 콜백이 아닌데
@impl로 표시했을 때 - 다른 함수들이
@impl로 표시됐는데 특정 함수만@impl로 표시하지 않았을 때. 함수 하나를@impl로 표시하면, 그 behaviour의 다른 콜백들도 전부@impl로 표시해야 해요
@impl은 문맥별(per-context)로 동작해요. 매크로로 함수를 생성해 @impl로 표시해도, 그 함수가 생성되는 모듈에는 영향을 주지 않아요.
@impl은 다른 개발자들에게 "이 함수는 콜백을 구현하는 거다"는 걸 분명히 해서 유지보수에도 도움이 돼요.
@impl을 쓰면 위 예시를 이렇게 다시 쓸 수 있어요:
defmodule URI.HTTP do
@behaviour URI.Parser
@impl true
def default_port(), do: 80
@impl true
def parse(info), do: info
end
@impl에는 false, true, 또는 특정 behaviour를 넘길 수 있어요:
defmodule Foo do
@behaviour Bar
@behaviour Baz
# Bar도 Baz도 bar/0라는 콜백을 지정하지 않으면 경고한다.
@impl true
def bar(), do: :ok
# Baz가 baz/0라는 콜백을 지정하지 않으면 경고한다.
@impl Baz
def baz(), do: :ok
end
어떤 함수가 당신의 API에 속하고 어떤 게 콜백 구현인지가 이제 명확해져서 코드가 더 읽기 좋아져요. 이 아이디어를 강화하기 위해, @impl true는 자동으로 함수를 @doc false로 표시해서 @doc이 명시적으로 설정되지 않는 한 문서화를 비활성화해요.
@compile
모듈 컴파일 옵션을 정의해요. Elixir와 Erlang 컴파일러 둘 다, 그리고 외부 도구가 추가한 다른 컴파일 패스까지 설정하는 데 쓰여요. 예를 들어:
defmodule MyModule do
@compile {:inline, my_fun: 1}
def my_fun(arg) do
to_string(arg)
end
end
@compile을 여러 번 쓰면 이전 것을 덮어쓰는 대신 누적돼요. 아래의 "Compile options" 절을 참고하세요.
@deprecated (since v1.6.0)
함수에 대한 deprecation 사유를 제공해요. 예를 들어:
defmodule Keyword do
@deprecated "Use Kernel.length/1 instead"
def size(keyword) do
length(keyword)
end
end
Mix 컴파일러는 자동으로 deprecate된 모듈에 대한 호출을 찾아 컴파일 중에 경고를 내요.
@deprecated 속성을 쓰는 것은 함수와 매크로의 문서에도 반영돼요. 하드 deprecation(경고 포함)과 소프트 deprecation(경고 없음) 중에 고를 수 있는데, @deprecated 속성과 문서 메타데이터 중 하나를 택하면 돼요.
이것은 소프트 deprecation이에요. 단순히 문서를 deprecated로 주석만 달죠:
@doc deprecated: "Use Kernel.length/1 instead"
def size(keyword)
이것은 경고를 내고 문서를 deprecated로 주석 다는 하드 deprecation이에요:
@deprecated "Use Kernel.length/1 instead"
def size(keyword)
현재 @deprecated는 함수와 매크로만 지원해요. 하지만 annotation 메타데이터의 :deprecated 키를 쓰면 모듈, 타입, 콜백의 문서에도 주석을 달 수 있어요.
이 기능은 특히 라이브러리 저자에게 조심해서 쓰기를 권장해요. 코드를 deprecate하는 것은 항상 부담을 라이브러리 사용자에게 떠넘기니까요. 또 deprecated 기능은 deprecate된 후에도 오랜 기간 유지해서 개발자들이 업데이트할 충분한 시간을 주는 걸 권장해요. 다만 보안 문제 같은, deprecated API를 유지하는 게 바람직하지 않은 경우는 예외예요.
@doc과 @typedoc
뒤따르는 엔티티에 대한 문서를 제공해요. @doc은 함수, 매크로, 콜백, 매크로 콜백과 함께 쓰고, @typedoc은 타입(public 또는 opaque)과 함께 써요.
다음 중 하나를 받아요:
- 문자열(주로 heredoc)
false— 그 엔티티를ExDoc같은 문서 추출 도구에서 보이지 않게 함- 키워드 리스트 — Elixir 1.7.0부터
예를 들어:
defmodule MyModule do
@typedoc "This type"
@typedoc since: "1.1.0"
@type t :: term
@doc "Hello world"
@doc since: "1.1.0"
def hello do
"world"
end
@doc """
Sums `a` to `b`.
"""
def sum(a, b) do
a + b
end
end
위 예시에서 볼 수 있듯이, Elixir 1.7.0부터 @doc과 @typedoc은 엔티티에 대한 임의 메타데이터를 제공하는 키워드 리스트도 받아요. ExDoc과 IEx 같은 도구가 이 정보를 사용해 annotation을 표시할 수 있어요. 흔한 사례는 함수가 도입된 버전을 주석으로 다는 :since 키예요.
예시에서 보여 주듯 엔티티 앞에 이 속성을 두 번 이상 쓸 수도 있어요. 하지만 binary를 두 번 쓰면 컴파일러가 경고해요. 앞선 사용의 문서 텍스트를 대체하기 때문이에요. 키워드 리스트를 여러 번 쓰면 리스트들이 하나로 병합돼요.
컴파일러가 추가 메타데이터도 일부 정의하므로, 무시되고 쓰면 경고되는 예약 키가 몇 개 있어요. 현재는 :opaque와 :defaults예요.
이 모듈이 컴파일되면 이 정보는 Code.fetch_docs/1 함수로 얻을 수 있어요.
@dialyzer
:dialyzer를 쓸 때 요청하거나 억제할 경고를 정의해요.
atom, 튜플, 또는 atom과 튜플의 리스트를 받아요. 예를 들어:
defmodule MyModule do
@dialyzer {:nowarn_function, [my_fun: 1]}
def my_fun(arg) do
M.not_a_function(arg)
end
end
지원되는 경고 목록은 :dialyzer 모듈을 참고하세요. @dialyzer를 여러 번 쓰면 이전 것을 덮어쓰는 대신 누적돼요.
@external_resource
현재 모듈의 외부 리소스를 지정해요.
때로 모듈이 외부 파일에서 정보를 임베드하기도 해요. 이 속성은 모듈이 어떤 외부 리소스를 사용했는지 주석으로 남길 수 있게 해 줘요.
도구는 이 정보를 사용해서 외부 리소스 중 하나가 바뀌면 모듈을 다시 컴파일하도록 보장할 수 있어요. 예: mix compile.elixir.
지정된 파일 경로는 프로젝트의 mix.exs가 있는 폴더, 즉 현재 작업 디렉터리를 기준으로 해석돼요. @external_resource가 선언된 파일이 기준이 아니에요.
외부 리소스가 존재하지 않아도 모듈은 여전히 그 리소스에 의존해서, 파일이 추가되는 즉시 모듈이 다시 컴파일돼요.
모듈이 다시 컴파일되는 시점을 더 세밀하게 제어하려면 __mix_recompile__?/0을 참고하세요.
@file
뒤따르는 함수나 매크로의 스택트레이스에 쓰는 파일명을 바꿔요. 이렇게요:
defmodule MyModule do
@doc "Hello world"
@file "hello.ex"
def hello do
"world"
end
end
주의할 점은, 이는 정의의 내부 스코프(패턴과 가드를 포함)에서 오는 예외/진단에만 유효하다는 거예요. 예를 들어:
defmodule MyModule do # <---- 모듈 정의
@file "hello.ex"
defp unused(a) do # <---- 함수 정의
"world" # <---- 함수 스코프
end
@file "bye.ex"
def unused(_), do: true
end
두 번째 "unused" 정의를 주석 처리하고 이 코드를 실행하면, 경고를 보고할 때 hello.ex가 스택트레이스로 쓰이는 걸 볼 수 있어요. 하지만 그 주석을 풀면 오류가 bye.ex를 언급하지 않는데, 그건 표현식 수준 오류가 아니라 모듈 수준 오류이기 때문이에요.
@moduledoc
현재 모듈에 대한 문서를 제공해요.
defmodule MyModule do
@moduledoc """
A very useful module.
"""
@moduledoc authors: ["Alice", "Bob"]
end
문자열(주로 heredoc)이나 false를 받아요. @moduledoc false는 모듈을 ExDoc 같은 문서 추출 도구에서 보이지 않게 해요.
@doc과 비슷하게, 모듈에 대한 메타데이터를 제공하는 키워드 리스트도 받아요. 자세한 내용은 위의 @doc 문서를 참고하세요.
이 모듈이 컴파일되면 이 정보는 Code.fetch_docs/1로 얻을 수 있어요.
@nifs (since v1.16.0)
네이티브 구현(NIF)으로 덮어쓸 함수들과 그 arity의 리스트예요.
defmodule MyLibrary.MyModule do
@nifs [foo: 1, bar: 2]
def foo(arg1), do: :erlang.nif_error(:not_loaded)
def bar(arg1, arg2), do: :erlang.nif_error(:not_loaded)
end
자세한 정보는 Erlang 문서를 참고하세요: https://www.erlang.org/doc/man/erl_nif
@on_definition
현재 모듈의 각 함수나 매크로가 정의될 때 호출되는 훅이에요. 함수에 주석을 달 때 유용해요.
모듈이나 {module, function_name} 튜플을 받아요. 그 함수는 다음 6개의 인자를 받아야 해요:
- 모듈 환경
- 함수/매크로의 종류:
:def,:defp,:defmacro,:defmacrop - 함수/매크로 이름
- 인용된 인자 리스트
- 인용된 가드 리스트
- 인용된 함수 본문
정의되는 함수/매크로에 여러 절(clause)이 있으면, 그 절마다 훅이 호출돼요.
다른 훅과 달리 @on_definition은 함수만 호출하고 매크로는 절대 호출하지 않아요. 이는 @on_definition 콜백이 방금 정의된 함수를 더 명시적인 접근을 대신해 다시 정의하는 일을 피하기 위해서예요.
모듈만 제공되면 함수는 __on_definition__/6으로 간주돼요.
예시
defmodule Hooks do
def on_def(_env, kind, name, args, guards, body) do
IO.puts("Defining #{kind} named #{name} with args:")
IO.inspect(args)
IO.puts("and guards")
IO.inspect(guards)
IO.puts("and body")
IO.puts(Macro.to_string(body))
end
end
defmodule MyModule do
@on_definition {Hooks, :on_def}
def hello(arg) when is_binary(arg) or is_list(arg) do
"Hello" <> to_string(arg)
end
def hello(_) do
:ok
end
end
@on_load
모듈이 로드될 때마다 호출되는 훅이에요.
현재 모듈에 있는 함수의 이름(atom)을 받아요. 그 함수는 arity 0(인자 없음)이어야 해요. 함수가 :ok를 반환하지 않으면 모듈 로딩이 중단돼요. 주된 용도는 NIF를 로드하는 거예요:
defmodule MyModule do
@on_load :load_external_code
def load_external_code do
:erlang.load_nif(~c"path/to/extension.so_or_dll")
end
end
on_load에 주어진 함수는 다른 모듈의 함수를 호출하는 걸 피해야 해요. mix release를 실행할 때 on_load는 어떤 애플리케이션도 시작되기 전에 아주 이른 시점에 실행되기 때문이에요. 그래서 Logger나 IO 같은 시스템조차 아직 사용할 수 없어요.
@vsn
모듈 버전을 지정해요. 유효한 Elixir 값이면 무엇이든 받아요. 예를 들어:
defmodule MyModule do
@vsn "1.0"
end
구조체 속성 (Struct attributes)
@derive— 현재 모듈에 정의된 구조체에 대해 주어진 프로토콜의 구현을 파생(derive)해요@enforce_keys— 현재 모듈에 정의된 구조체를 만들 때 주어진 키들이 항상 설정되도록 보장해요
구조체를 만들고 사용하는 방법은 defstruct/1을 참고하세요.
타입스펙 속성 (Typespec attributes)
다음 속성들은 타입스펙의 일부이고 Elixir에 내장되어 있어요:
@type—@spec에서 쓸 타입을 정의해요@typep—@spec에서 쓸 비공개 타입을 정의해요@opaque—@spec에서 쓸 불투명(opaque) 타입을 정의해요@spec— 함수에 대한 스펙을 제공해요@callback— behaviour 콜백에 대한 스펙을 제공해요 (그리고 아래 참조대로 모듈에behaviour_info/1함수를 생성해요)@macrocallback— 매크로 behaviour 콜백에 대한 스펙을 제공해요@optional_callbacks— 어떤 behaviour 콜백과 매크로 behaviour 콜백이 선택적(optional)인지 지정해요@impl— 콜백 함수나 매크로의 구현을 선언해요
자세한 문서는 typespec 문서를 참고하세요.
커스텀 속성 (Custom attributes)
위에 설명한 내장 속성 외에도 커스텀 속성을 추가할 수 있어요. 커스텀 속성은 @/1 연산자 다음에 유효한 변수 이름으로 표현돼요. 커스텀 속성에 주어진 값은 유효한 Elixir 값이어야 해요:
defmodule MyModule do
@custom_attr [some: "stuff"]
end
커스텀 속성을 정의할 때 쓸 수 있는 더 고급 옵션은 register_attribute/3을 참고하세요.
컴파일 콜백 (Compile callbacks)
세 가지 컴파일 콜백이 있고, 이 순서로 호출돼요: @before_compile, @after_compile, @after_verify. 각각 아래에서 설명할게요.
@before_compile
모듈이 컴파일되기 전에 호출되는 훅이에요. 현재 모듈이 컴파일되는 방식을 바꿀 때 자주 쓰여요.
모듈이나 {module, function_or_macro_name} 튜플을 받아요. 그 함수/매크로는 인자 하나(모듈 환경)를 받아야 해요. 매크로라면, 그 반환값이 컴파일이 시작되기 전에 모듈 정의 끝에 주입돼요.
모듈만 제공되면 함수/매크로는 __before_compile__/1로 간주돼요.
콜백은 등록된 순서대로 실행돼요. 오버라이드 가능한 정의는 첫 콜백이 실행되기 전에 구체화(concrete)돼요. 정의는 다른 before compile 콜백에서 다시 오버라이드 가능하게 만들 수 있고, 모든 콜백이 끝난 뒤 마지막으로 한 번 구체화돼요.
참고: 콜백 함수/매크로는 별도의 모듈에 두어야 해요. 콜백이 호출될 때 현재 모듈은 아직 존재하지 않기 때문이에요.
예시
defmodule A do
defmacro __before_compile__(_env) do
quote do
def hello, do: "world"
end
end
end
defmodule B do
@before_compile A
end
B.hello()
#=> "world"
@after_compile
현재 모듈의 바이트코드와 함께 호출되는 훅이에요. 모듈이 이미 컴파일됐지만, 그 바이트코드가 메모리에 로드되거나 디스크에 쓰이지 않았을 수 있어요. 그런 이유로, @after_verify 콜백을 쓰거나 Code.ensure_compiled!/1를 써서 모듈이 검사·호출에 완전히 사용 가능해질 때까지 기다리는 게 좋아요.
모듈이나 {module, function_name} 튜플을 받아요. 함수는 두 인자(모듈 환경과 그 바이트코드)를 받아야 해요. 모듈만 제공되면 함수는 __after_compile__/2로 간주돼요.
콜백은 등록된 순서대로 실행돼요.
definitions_in/1처럼 아직 컴파일되지 않은 모듈을 기대하는 Module 함수들은 @after_compile이 호출되는 시점에도 여전히 사용 가능해요.
예시
defmodule MyModule do
@after_compile __MODULE__
def __after_compile__(env, _bytecode) do
IO.inspect(env)
end
end
@after_verify
현재 모듈이 정의되지 않은 함수나 deprecation 등을 위해 검증된 직후에 호출되는 훅이에요. 모듈은 항상 컴파일 후에 검증돼요. Mix 프로젝트에서 모듈은 런타임 의존성 중 하나가 바뀔 때도 검증돼요. 그래서 컴파일 타임 의존성을 피하면서 현재 모듈의 검증을 수행하는 데 유용해요. 콜백이 여러 시나리오에서 호출되므로, Elixir는 컴파일 주기 중 언제, 어떤 프로세스에서 콜백이 실행될지에 대해 보장하지 않아요.
게다가 검증 후 콜백은 raise할 것으로 기대되지 않아요. 코드가 컴파일된 뒤 실행되므로 아티팩트가 이미 디스크에 쓰여 있어서, raise해도 컴파일을 실제로 멈추지 못하고 사용하지 않는 아티팩트를 디스크에 남길 수 있기 때문이에요. 반드시 raise해야 한다면 @after_compile이나 다른 콜백을 쓰세요. 모듈이 이미 컴파일됐으므로, get_attribute/2처럼 모듈이 아직 컴파일되지 않았음을 기대하는 이 모듈의 함수들은 @after_verify 콜백에서 동작하지 않아요.
모듈이나 {module, function_name} 튜플을 받아요. 함수는 인자 하나(모듈 이름)를 받아야 해요. 모듈만 제공되면 함수는 __after_verify__/1로 간주돼요.
콜백은 등록된 순서대로 실행돼요.
예시
defmodule MyModule do
@after_verify __MODULE__
def __after_verify__(module) do
IO.inspect(module)
:ok
end
end
컴파일 옵션 (Compile options)
@compile 속성은 Elixir와 Erlang 컴파일러 둘 다가 쓰는 다양한 옵션을 받아요. 흔한 사용 사례 몇 가지를 아래에서 설명할게요:
@compile :debug_info—Code.get_compiler_option/1의 해당 설정과 무관하게:debug_info를 포함해요@compile {:debug_info, false}—Code.get_compiler_option/1의 해당 설정과 무관하게:debug_info를 비활성화해요.:debug_info를 비활성화하는 건 Elixir 컴파일러와 다른 도구가 코드를 정적으로 분석하는 능력을 없애므로 권장하지 않아요. 배포 시:debug_info를 제거하고 싶다면mix release같은 도구가 기본적으로 그렇게 해 줘요@compile {:inline, some_fun: 2, other_fun: 3}— 주어진 이름/arity 쌍을 인라인해요. 인라이닝은 로컬에 적용되며, 다른 모듈에서의 호출은 이 옵션의 영향을 받지 않아요@compile {:autoload, true}— 정의 후 모듈이 자동으로 로드될지 설정해요. 모듈을 디스크의.beam파일로 컴파일할 땐 기본값이false예요 (모듈이 디스크에서 지연 로드되므로). 모듈이 디스크로 컴파일되지 않으면 이 플래그와 무관하게 항상 로드돼요@compile {:no_warn_undefined, Mod}또는@compile {:no_warn_undefined, {Mod, fun, arity}}— 주어진 모듈이나Mod.fun/arity가 정의되지 않아도 경고하지 않아요
생성 함수 (Generated functions)
때로 컴파일러는 모듈 안에 public 함수를 생성해요. 아래에서 문서화할게요.
behaviour_info/1
이 함수는 behaviour를 정의하는 모듈, 즉 @callback 정의를 하나 이상 가진 모듈을 위해 생성돼요. 이 함수의 시그니처(스펙으로 표현)는:
@spec behaviour_info(:callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec behaviour_info(:optional_callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
behaviour_info(:callbacks)는 선택적 콜백을 포함해요.
예를 들어:
iex> Enum.sort(GenServer.behaviour_info(:callbacks))
[
code_change: 3,
format_status: 1,
format_status: 2,
handle_call: 3,
handle_cast: 2,
handle_continue: 2,
handle_info: 2,
init: 1,
terminate: 2
]
module_info/0
모든 모듈을 위해 생성되는 함수예요. module_info/1이 반환하는 모든 속성을 반환하되, 단일 키워드 리스트로 반환해요. Erlang 문서도 참고하세요.
module_info/1
모든 모듈을 위해 생성되고 모듈에 대한 정보를 반환해요. 이 함수의 시그니처(스펙으로 표현)는:
@spec module_info(:module) :: module() # 모듈 자신을 반환
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
예를 들어:
iex> URI.module_info(:module)
URI
iex> {:decode_www_form, 1} in URI.module_info(:exports)
true
module_info/1에 대한 더 자세한 정보는 Erlang 문서도 참고하세요.
info/1
모든 모듈을 위해 생성되는 함수예요. module_info/1과 비슷하지만 구조체(struct)와 매크로 정보 같은 Elixir 특유의 정보를 추가로 포함해요. 문서는 Module.__info__/1을 참고하세요.
더 알아보기
Code.fetch_docs/1— 컴파일된 모듈의 문서·메타데이터 조회- Typespec —
@type,@spec,@callback사용법 - defstruct/1 — 구조체 정의
- Kernel.SpecialForms —
@/1,defmodule/2등 특수 형태 - Macro — 속성을 다루는 함수들을 일부 제공