Do you want to pick up from where you left of?
Take me there

비헤이비어

이전 강좌에서 타입스펙에 관해 배웠습니다. 여기서는 모듈에서 그 사양을 구현하도록 요구하는 방법을 배우겠습니다. Elixir에서 이 기능은 비헤이비어(behaviour)라는 이름으로 불립니다.

용도

가끔 공개 API를 공유하기 위한 모듈을 만들어야 할 때가 있는데, 이를 위해 Elixir에서는 비헤이비어를 사용합니다. 비헤이비어가 하는 일은 크게 두 가지입니다.

Elixir는 GenServer를 비롯해 비헤이비어를 여럿 가지고 있습니다만, 이 강좌에서는 그런 것을 사용하기보단 직접 만드는 것에 집중해 보겠습니다.

비헤이비어 정의하기

비헤이비어를 좀 더 잘 이해하기 위해 워커 모듈을 위한 비헤이비어를 구현해 보겠습니다. 이 워커는 init/1, perform/2 두 함수가 구현되어 있어야 합니다.

이를 충족하기 위해, @spec과 비슷한 문법을 가진 @callback 디렉티브를 사용하겠습니다. 이 디렉티브로 반드시 구현되어야 하는 함수를 정의합니다. 매크로는 @macrocallback를 사용해서 할 수 있습니다. 워커를 위해 init/1, perform/2 함수를 정의해 봅니다.

defmodule Example.Worker do
  @callback init(state :: term) :: {:ok, new_state :: term} | {:error, reason :: term}
  @callback perform(args :: term, state :: term) ::
              {:ok, result :: term, new_state :: term}
              | {:error, reason :: term, new_state :: term}
end

여기에 정의된 init/1는 어떤 값을 받아 튜플 {:ok, state}{:error, reason}를 반환합니다. 이는 초기화에서 꽤 일반적인 패턴입니다. perform/2 함수는 어떤 인자를 받아 워커의 상태를 초기화합니다. perform/2{:ok, result, state}{:error, reason, state}를 반환하길 기대하고 이는 GenServer와 비슷합니다.

비헤이비어 사용하기

이제 비헤이비어를 정의했으니 공개 API를 공유하는 다양한 모듈에서 이를 사용할 수 있습니다. 비헤이비어를 모듈에 추가하는 것은 @behaviour 속성을 사용하면 쉽습니다.

새로운 비헤이비어를 사용해 원격에서 파일을 다운로드해 로컬에 저장하는 모듈 테스크를 만들어 봅시다.

defmodule Example.Downloader do
  @behaviour Example.Worker

  def init(opts), do: {:ok, opts}

  def perform(url, opts) do
    url
    |> HTTPoison.get!()
    |> Map.fetch(:body)
    |> write_file(opts[:path])
    |> respond(opts)
  end

  defp write_file(:error, _), do: {:error, :missing_body}

  defp write_file({:ok, contents}, path) do
    path
    |> Path.expand()
    |> File.write(contents)
  end

  defp respond(:ok, opts), do: {:ok, opts[:path], opts}
  defp respond({:error, reason}, opts), do: {:error, reason, opts}
end

아니면 여러 파일을 압축하는 워커는 어떨까요? 그것도 가능합니다.

defmodule Example.Compressor do
  @behaviour Example.Worker

  def init(opts), do: {:ok, opts}

  def perform(payload, opts) do
    payload
    |> compress
    |> respond(opts)
  end

  defp compress({name, files}), do: :zip.create(name, files)

  defp respond({:ok, path}, opts), do: {:ok, path, opts}
  defp respond({:error, reason}, opts), do: {:error, reason, opts}
end

워커의 수행 내용은 다르지만, 공개되는 API는 같고 이 모듈을 사용하는 코드에서는 예상대로 응답할 것을 알 수 있습니다. 이는 모두 다른 일을 하지만 같은 공개 API를 따르는 워커를 만들 수 있게 합니다.

비헤이비어를 추가할 때 모든 필요한 함수를 구현하는데 실패한다면, 컴파일 시에 경고가 발생합니다. 이 동작을 확인하기 위해 Example.Compressor코드에서 init/1 함수를 제거해 봅시다.

defmodule Example.Compressor do
  @behaviour Example.Worker

  def perform(payload, opts) do
    payload
    |> compress
    |> respond(opts)
  end

  defp compress({name, files}), do: :zip.create(name, files)

  defp respond({:ok, path}, opts), do: {:ok, path, opts}
  defp respond({:error, reason}, opts), do: {:error, reason, opts}
end

이제 컴파일하면 필요한 함수가 구현되지 않았다는 경고를 확인할 수 있습니다.

lib/example/compressor.ex:1: warning: undefined behaviour function init/1 (for behaviour Example.Worker)
Compiled lib/example/compressor.ex

끝입니다! 이제 우리는 비헤이비어를 만들고 공유할 수 있습니다.

강의에 실수가 있거나 기여하고 싶은 부분이 있으신가요? GitHub에서 이 강의를 수정해보세요!