Inspect

Inspect (protocol)

Inspect 프로토콜은 Elixir 자료 구조를 대수 문서(algebra document)로 바꿔 줘요. 보통 자신이 만든 구조체(struct)가 로그와 터미널에서 어떻게 표시될지 커스터마이즈하고 싶을 때 사용해요. 이 문서는 자신의 자료 구조를 위해 Inspect 프로토콜을 구현하는 방법을 다뤄요. inspect를 사용하는 법을 배우고 싶다면 Kernel.inspect/2IO.inspect/2를 참고하세요.

출처: Inspect

본문

Inspect 표현 (Inspect representation)

보통 inspect 표현에는 세 가지 선택지가 있어요. 이해를 위해 다음과 같은 User 구조체가 있다고 상상해 봅시다.

defmodule User do
  defstruct [:id, :name, :address]
end

우리의 선택지는 다음과 같아요.

  • Elixir의 구조체 문법으로 구조체를 출력하는 거예요. 예: %User{address: "Earth", id: 13, name: "Jane"}. 이건 기본 표현이고, 모든 구조체 필드가 공개(public)라면 최선의 선택이에요.
  • #User<...> 표기법으로 출력하는 거예요. 예: #User<id: 13, name: "Jane", ...>. 이 표기법은 유효한 Elixir 코드를 만들어내지 않고, 보통 구조체에 비공개(private) 필드가 있을 때 사용해요(예: 개인 식별 정보를 가리는 :address 필드를 숨기고 싶을 수 있어요).
  • 표현식 문법으로 구조체를 출력하는 거예요. 예: User.new(13, "Jane", "Earth"). 이건 User.new/3 함수가 있다고 가정해요. 이 선택지는 MapSet, Date.Range 등 커스텀 자료 구조를 표현하기 위한 선택지 2의 대안으로 주로 사용돼요.

자체 구조체를 위해 위 관례를 따르면서 Inspect 프로토콜을 구현할 수 있어요. 선택지 1이 기본 표현이고, 선택지 2는 Inspect 프로토콜을 derive하면 빠르게 얻을 수 있어요. 선택지 3은 커스텀 구현이 필요해요.

Deriving

Inspect 프로토콜을 derive해 필드 순서(기본은 알파벳순)를 커스터마이즈하고 구조체에서 특정 필드를 숨겨서 로그·inspect 등에 나타나지 않게 할 수 있어요. 후자는 비공개 정보가 담긴 필드에 특히 유용해요. 지원되는 옵션은 다음과 같아요.

  • :only — inspect할 때 주어진 필드만 포함해요.
  • :except — inspect할 때 주어진 필드를 제거해요.
  • :optional — (v1.14.0부터) 기본값과 일치할 때 포함하지 않을 필드 목록이에요. 정보를 숨기는 대가로 구조체 표현을 단순화하는 데 쓰여요. v1.19.0부터 :all 아톰을 넘겨 모든 필드를 선택적으로 표시할 수 있어요.

:only:except로 필드를 제한하면 구조체를 더 이상 유효한 Elixir 코드로 복사·붙여넣기할 수 없으므로, #User<...> 표기법으로 출력돼요. 예시를 볼게요.

defmodule User do
  @derive {Inspect, only: [:id, :name]}
  defstruct [:id, :name, :address]
end

inspect(%User{id: 1, name: "Jane", address: "Earth"})
#=> #User<id: 1, name: "Jane", ...>

:optional 옵션만 사용하면 구조체는 여전히 유효한 구조체로 출력돼요.

defmodule Point do
  @derive {Inspect, optional: [:z]}
  defstruct [x: 0, y: 0, z: 0]
end

inspect(%Point{x: 1})
%Point{x: 1, y: 0}

커스텀 구현 (Custom implementation)

inspect/2 함수를 정의해 자신만의 커스텀 프로토콜 구현을 만들 수도 있어요. 함수는 inspect할 대상과, Inspect.Opts 구조체로 표현되는 inspect 옵션을 받아, 갱신된 옵션과 함께 대수 문서를 반환해야 해요(선택적으로 대수 문서만 반환해도 됩니다). 대수 문서는 Inspect.Algebra로 만듭니다. 구조체를 inspect하는 건 기존 엔티티에 대한 함수로 구현할 수 있는 경우가 많아요. 예를 들어 MapSetinspect/2 구현이 여기 있어요.

defimpl Inspect, for: MapSet do
  import Inspect.Algebra

  def inspect(map_set, opts) do
    {doc, opts} = to_doc_with_opts(MapSet.to_list(map_set), opts)
    {concat(["MapSet.new(", doc, ")"]), opts}
  end
end

먼저 to_doc_with_opts/2를 사용해 다른 자료 구조를 대수 문서로 바꾸고, concat/1로 대수 문서를 서로 이어 붙여요. 위 예시에서는 문자열 "MapSet.new(", to_doc_with_opts/2가 반환한 문서, 마지막 문자열 ")"를 연결해요. 그래서 숫자 1, 2, 3이 든 MapSet은 이렇게 출력돼요.

iex> MapSet.new([1, 2, 3], fn x -> x * 2 end)
MapSet.new([2, 4, 6])

MapSet의 inspect 표현은, 평가하면 그 MapSet 자체를 만드는 표현식을 반환해요.

오류 처리 (Error handling)

자신의 구조체를 inspect하는 중 오류가 생기면, Elixir는 ArgumentError를 일으키고 구조체를 출력하기 위해 자동으로 원시(raw) 표현으로 돌아가요. 게다가 자신의 Inspect 구현을 디버깅할 때는 주의해야 해요. IO.inspect/2dbg/1 호출이 무한 루프를 유발할 수 있으니까요(자료 구조를 inspect/디버그하려면 inspect 자신을 호출해야 하기 때문이에요). 몇 가지 팁이에요.

  • 디버깅에는 structs: false 옵션과 함께 IO.inspect/2를 쓰세요. 이 옵션은 커스텀 출력을 비활성화해 Inspect 구현을 재귀적으로 호출하는 걸 피해요.
  • 커스텀 Inspect 구현에서 밑바탕 오류에 접근하려면 프로토콜을 직접 호출하면 돼요. 예를 들어 위 Inspect.MapSet 구현을 이렇게 호출할 수 있어요.
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})

Elixir v1.19부터 inspect 프로토콜은 문서와 갱신된 옵션을 가진 두 요소 튜플을 프로토콜에서 반환할 수 있도록 확장됐다는 점을 기억하세요.

더 알아보기