구조체
구조체 (Structs)
앞선 장에서 맵(map)에 대해 배웠어요. 구조체(struct)는 바로 그 맵 위에 올라탄 확장판인데, 컴파일 시점 검사와 기본값까지 더해져요. 데이터를 더 안전하고 규칙적으로 다루고 싶을 때 써요. 이번 장에서 구조체를 정의하고, 접근하고, 갱신하는 법을 살펴볼게요.
출처: Elixir 공식 가이드
본문
이전 장에서 맵에 대해 배웠어요.
iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
iex> map[:a]
1
iex> %{map | a: 3}
%{a: 3, b: 2}
구조체는 맵 위에 만들어진 확장으로, 컴파일 시점 검사와 기본값을 제공해요.
구조체 정의하기 (Defining structs)
구조체를 정의하려면 defstruct/1 구문을 써요.
iex> defmodule User do
...> defstruct name: "John", age: 27
...> end
defstruct와 함께 쓰는 키워드 리스트는 구조체가 가질 필드들과 각각의 기본값을 정의해요. 구조체는 정의된 모듈의 이름을 따르는데, 위 예시에서는 User라는 구조체를 정의했어요.
이제 맵을 만들 때와 비슷한 문법으로 User 구조체를 만들 수 있어요.
iex> %User{}
%User{age: 27, name: "John"}
iex> %User{name: "Jane"}
%User{age: 27, name: "Jane"}
구조체는 defstruct로 정의된 필드만 존재하도록 하는 컴파일 시점 보장을 제공해요.
iex> %User{oops: :field}
** (KeyError) key :oops not found expanding struct: User.__struct__/1
구조체 접근과 갱신 (Accessing and updating structs)
구조체는 고정된 키를 가진 맵과 같은 문법으로 필드에 접근하고 갱신해요.
iex> john = %User{}
%User{age: 27, name: "John"}
iex> john.name
"John"
iex> jane = %{john | name: "Jane"}
%User{age: 27, name: "Jane"}
iex> %{jane | oops: :field}
** (KeyError) key :oops not found in: %User{age: 27, name: "Jane"}
갱신 문법(|)을 쓸 때 Elixir는 구조체에 새 키가 추가되지 않을 것임을 알고 있어서, 밑바탕이 되는 맵들이 메모리에서 구조를 공유할 수 있게 해줘요. 위 예시에서 john과 jane은 메모리에서 같은 키 구조를 공유해요.
구조체는 패턴 매칭에도 쓸 수 있어요. 특정 키의 값에 매칭할 수도 있고, 매칭 대상 값이 같은 종류의 구조체인지 확인할 수도 있죠.
iex> %User{name: name} = john
%User{age: 27, name: "John"}
iex> name
"John"
iex> %User{} = %{}
** (MatchError) no match of right hand side value: %{}
구조체를 생성하고, 갱신하고, 패턴 매칭하는 자세한 내용은 %/2 문서를 참고하세요.
동적 구조체 갱신 (Dynamic struct updates)
키워드 리스트나 맵의 데이터로 구조체를 갱신해야 할 때는 Kernel.struct!/2를 써요.
iex> john = %User{name: "John", age: 27}
%User{age: 27, name: "John"}
iex> updates = [name: "Jane", age: 30]
[name: "Jane", age: 30]
iex> struct!(john, updates)
%User{age: 30, name: "Jane"}
struct!/2는 잘못된 필드를 설정하려 하면 오류를 발생시켜요.
iex> struct!(john, invalid: "field")
** (KeyError) key :invalid not found in: %User{age: 27, name: "John"}
컴파일 시점에 정확한 필드를 알고 있다면 맵 갱신 문법(%{john | name: "Jane"})을 쓰세요. 구조체의 무결성을 지키려면 항상 Map 함수 대신 struct!/2를 쓰는 게 좋아요.
구조체는 사실 맵 (Structs are bare maps underneath)
구조체는 __struct__라는 "특수한" 필드가 구조체의 이름을 담고 있는, 그저 맵일 뿐이에요.
iex> is_map(john)
true
iex> john.__struct__
User
하지만 구조체는 맵이 가진 내장 기능들을 물려받지 않아요. 예를 들어 구조체는 열거(enumeration)도, 접근(access)도 할 수 없어요.
iex> john = %User{}
%User{age: 27, name: "John"}
iex> john[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
iex> Enum.each(john, fn {field, value} -> IO.puts(value) end)
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"} of type User (a struct)
앞으로의 장에서 배우겠지만, Elixir는 개발자가 구조체에 이름을 기반으로 커스텀 동작을 붙일 수 있게 해줘요. 커스텀 데이터 열거, 예쁜 출력(pretty printing) 등을 가능하게 하죠. 이렇게 동작을 분기시키는 메커니즘을 프로토콜(protocol) 이라고 하고, Elixir에서 확장 가능한 데이터 다형성을 가능하게 해줘요.
기본값과 필수 키 (Default values and required keys)
구조체를 정의할 때 키의 기본값을 지정하지 않으면 nil이 가정돼요.
iex> defmodule Product do
...> defstruct [:name]
...> end
iex> %Product{}
%Product{name: nil}
명시적 기본값을 가진 필드와 기본적으로 nil이 되는 필드를 섞어서 구조체를 정의할 수 있어요. 이 경우 먼저 암묵적으로 nil이 되는 필드를 앞에 지정해야 해요.
iex> defmodule User do
...> defstruct [:email, name: "John", age: 27]
...> end
iex> %User{}
%User{age: 27, email: nil, name: "John"}
순서를 반대로 하면 문법 오류가 발생해요.
iex> defmodule User do
...> defstruct [name: "John", age: 27, :email]
...> end
** (SyntaxError) iex:107: unexpected expression after keyword list. Keyword lists must always come last in lists and maps.
@enforce_keys 모듈 속성으로 구조체를 만들 때 반드시 지정해야 하는 키를 강제할 수도 있어요.
iex> defmodule Car do
...> @enforce_keys [:make]
...> defstruct [:model, :make]
...> end
iex> %Car{}
** (ArgumentError) the following keys must also be given when building struct Car: [:make]
expanding struct: Car.__struct__/1
키 강제는 구조체를 만들 때 개발자를 돕는 간단한 컴파일 시점 보장을 제공해요. 갱신 시에는 강제되지 않고, 어떤 종류의 값 검증도 제공하지 않아요.
더 알아보기
- Module attributes (이전 장)
- Recursion (다음 장)
%/2문서 (Elixir 공식 문서)- Protocols (Elixir 공식 가이드)