모듈 속성

모듈 속성 (Module attributes)

Elixir의 모듈 속성은 세 가지 목적을 위해 쓰여요.

  • 모듈과 함수의 주석(annotation)
  • 컴파일 중에 사용되는 임시 모듈 저장소
  • 컴파일 타임 상수

이 예시들을 하나씩 확인해 볼게요.

출처: Elixir 공식 가이드

주석으로서 (As annotations)

Elixir는 모듈 속성의 개념을 Erlang에서 가져왔어요. 예를 들어:

defmodule MyServer do
  @moduledoc "My server code."
end

위 예제에서는 모듈 속성 문법으로 모듈 문서를 정의하고 있어요. Elixir에는 예약된 속성(reserved attributes)이 몇 가지 있어요. 가장 흔히 쓰이는 것들 몇 가지를 보여드릴게요.

  • @moduledoc — 현재 모듈의 문서를 제공해요.
  • @doc — 속성 뒤에 오는 함수나 매크로의 문서를 제공해요.
  • @spec — 속성 뒤에 오는 함수의 타입스펙(typespec)을 제공해요.
  • @behaviour — (영국식 철자임을 주의하세요) OTP 또는 사용자 정의 behaviour를 지정하는 데 사용해요.

@moduledoc@doc는 단연코 가장 많이 쓰이는 속성이고, 여러분도 많이 쓰게 될 거예요. Elixir는 문서를 일급(first-class)으로 취급하며 문서에 접근하는 다양한 함수를 제공해요. 이것들은 별도의 장에서 다룰게요.

문서는 컴파일된 모듈에서만 접근할 수 있어요. 그래서 시도해 보려면 Math 모듈을 다시 정의해 볼게요. 이번에는 math.ex라는 파일 안에서요.

defmodule Math do
  @moduledoc """
  Provides math-related functions.

  ## Examples

      iex> Math.sum(1, 2)
      3

  """

  @doc """
  Calculates the sum of two numbers.
  """
  def sum(a, b), do: a + b
end

Elixir는 읽기 좋은 문서를 쓰기 위해 Markdown과 히어독을 함께 사용하는 것을 권장해요. 히어독은 여러 줄 문자열로, 세 개의 더블쿼트로 시작하고 끝나며 내부 텍스트의 형식을 유지해요.

이제 컴파일해 볼게요. iex를 시작하고 c/2 헬퍼를 호출하세요.

iex> c("math.ex", ".")
[Math]

이제 문서에 접근할 수 있어요.

iex> h Math # Docs for module Math
...
iex> h Math.sum # Docs for the sum function
...

모듈을 컴파일할 때 Elixir가 Elixir.Math.beam 파일을 만들었다는 걸 눈치챘을 거예요. 그것이 모듈의 바이트코드이고, 문서가 저장되는 곳이에요.

일상에서 Elixir 개발자는 Mix 빌드 도구로 코드를 컴파일하고, ExDoc 같은 프로젝트로 문서로부터 HTML과 EPUB 페이지를 생성해요.

지원되는 속성의 전체 목록은 Module 문서를 확인해 보세요.

임시 저장소로서 (As temporary storage)

지금까지 속성을 정의하는 방법을 봤어요. 그럼 어떻게 읽을까요? 예시를 볼게요.

defmodule MyServer do
  @service URI.parse("https://example.com")
  IO.inspect(@service)
end

줄바꿈 (Newlines)

속성과 그 값 사이에 줄바꿈을 넣지 마세요. 그렇지 않으면 Elixir는 값을 설정(setting)하는 대신 값을 읽고 있다고 가정해요.

정의되지 않은 속성에 접근하려 하면 경고가 출력돼요.

defmodule MyServer do
  @unknown
end
warning: undefined module attribute @unknown, please remove access to @unknown or explicitly set it before access

속성은 함수 안에서도 읽을 수 있어요.

defmodule MyApp.Status do
  @service URI.parse("https://example.com")
  def status(email) do
    SomeHttpClient.get(@service)
  end
end

모듈 속성은 컴파일 타임에 정의되고, 속성에 치환되는 것은 함수 호출 자체가 아니라 그 반환값이에요. 그래서 위 코드는 사실상 이렇게 컴파일돼요.

defmodule MyApp.Status do
  def status(email) do
    SomeHttpClient.get(%URI{
      authority: "example.com",
      host: "example.com",
      port: 443,
      scheme: "https"
    })
  end
end

이것은 값을 미리 계산한 다음 그 결과를 모듈에 주입하는 데 유용해요. 이것이 우리가 "임시 저장소"라고 부르는 뜻이에요. 모듈이 컴파일된 후에는, 속성을 읽은 함수를 제외하고 모듈 속성은 폐기돼요. 주의할 점은, 속성 자체의 일부로 같은 모듈에 정의된 함수를 호출할 수 없다는 거예요. 그 함수들이 아직 정의되지 않았기 때문이에요.

함수 안에서 속성을 읽을 때마다 Elixir는 그 현재 값의 스냅샷을 찍어요. 그래서 여러 함수에서 같은 속성을 여러 번 읽으면, Elixir가 모든 스냅샷을 컴파일해야 하므로 컴파일 시간이 늘어나요. 일반적으로 같은 속성을 여러 번 읽는 것을 피하고, 대신 함수로 옮기는 게 좋아요. 예를 들어 이것 대신:

def some_function, do: do_something_with(@example)
def another_function, do: do_something_else_with(@example)

이걸 선호하세요.

def some_function, do: do_something_with(example())
def another_function, do: do_something_else_with(example())
defp example, do: @example

컴파일 타임 상수로서 (As compile-time constants)

모듈 속성은 컴파일 타임 상수로도 유용할 수 있어요. 일반적으로 코드베이스에서 상수의 역할에는 함수 자체로 충분해요. 예를 들어 이렇게 정의하는 대신:

@hours_in_a_day 24

이걸 선호하세요.

defp hours_in_a_day(), do: 24

모듈 간에 공유해야 한다면 public 함수로 정의할 수도 있어요. 많은 프로젝트에서 코드베이스 전체에 쓰이는 모든 상수를 정의하는 MyApp.Constants라는 모듈을 두는 것이 흔해요.

다른 데이터 타입만으로 이루어져 있다면(함수 호출, 연산자, 다른 표현식이 없다면) 합성 데이터 구조를 상수로 가질 수도 있어요. 예를 들어 시스템 구성 상수를 이렇게 지정할 수 있어요.

defp system_config(), do: %{timezone: "Etc/UTC", locale: "pt-BR"}

Elixir에서 데이터 구조는 불변이므로, 위 데이터 구조가 실행 가능한 표현식을 담고 있지 않다면, 단일 인스턴스만 할당되어 모든 함수 호출에서 공유돼요.

모듈 속성의 사용 사례는 컴파일 타임에 어떤 작업을 한 다음 그 결과를 함수 안에 주입해야 할 때 생겨요. 흔한 시나리오는 패턴과 가드 안의 모듈 속성이에요(defguard/1의 대안으로), 이들은 제한된 표현식 집합만 지원하기 때문이에요.

# Inside pattern
@default_timezone "Etc/UTC"
def shift(@default_timezone), do: ...

# Inside guards
@time_periods [:am, :pm]
def shift(time, period) when period in @time_periods, do: ...

상수로서의 모듈 속성과 임시 저장소로서의 모듈 속성은 함께 쓰이는 경우가 가장 많아요. 모듈 속성은 비싼 값을 계산해 저장하고, 그다음 그 모듈에서 상수로 노출되죠.

더 나아가기 (Going further)

라이브러리와 프레임워크는 커스텀 주석을 제공하기 위해 모듈 속성을 활용할 수 있어요. 좋은 예시를 보려면 Elixir의 유닛 테스트 프레임워크인 ExUnit을 보면 돼요. ExUnit은 여러 다른 목적에 모듈 속성을 사용해요.

defmodule MyTest do
  use ExUnit.Case, async: true

  @tag :external
  @tag os: :unix
  test "contacts external service" do
    # ...
  end
end

위 예제에서 ExUnit은 async: true의 값을 모듈 속성에 저장해 모듈이 컴파일되는 방식을 바꿔요. 태그(tag)도 주석으로 동작하며, Elixir의 속성 축적(accumulate) 기능 덕분에 여러 번 제공될 수 있어요. 그다음 태그를 사용해 테스트를 설정하고 필터링할 수 있어요. 예를 들어 Windows에서 테스트 스위트를 실행할 때 Unix 전용 테스트를 피하는 식이죠.

ExUnit이 어떻게 동작하는지 완전히 이해하려면 매크로가 필요해요. 그래서 이 패턴은 메타프로그래밍(Meta-programming) 가이드에서 다시 살펴보고, 모듈 속성을 커스텀 주석의 저장소로 사용하는 방법을 배울게요.

다음 장들에서는 구조체와 프로토콜을 탐험하고, 그다음 예외 처리와 시길, 컴프리헨션 같은 다른 구조로 넘어갈게요.

더 알아보기