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은 엔티티에 대한 임의 메타데이터를 제공하는 키워드 리스트도 받아요. ExDocIEx 같은 도구가 이 정보를 사용해 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는 어떤 애플리케이션도 시작되기 전에 아주 이른 시점에 실행되기 때문이에요. 그래서 LoggerIO 같은 시스템조차 아직 사용할 수 없어요.

@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_infoCode.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 — 속성을 다루는 함수들을 일부 제공