Ash Framework 튜토리얼 따라하기 (2)

https://hexdocs.pm/ash/get-started.html 를 따라가며 Ash 프레임워크를 살펴 본다. 이전 글에 이어서 Ticket 리소스를 개선한다.

Helpdesk.Support.Ticket |> Ash.Changeset.for_create(:create) |> Ash.create!() 를 통해 Ticket 데이터를 만들 수 는 있었지만 subject 를 입력할 수도 상태를 지정할 수 도 없었다. 보통 create 함수에 처음 세팅할 값을 전달하거나 create 함수 내부에서 기본 값을 설정하는 등 일정 로직이 필요하다.

먼저 Ticket 리소스의 attribute 들에 몇가지 속성을 추가하자.

# lib/helpdesk/support/ticket.ex

attributes do
  ...
  attribute :subject, :string do
    allow_nil? false

    public? true
  end

  attribute :status, :atom do
    constraints [one_of: [:open, :closed]]

    default :open

    allow_nil? false
  end
end

subject attribute에는 nil 을 허용하지 않는 속성이 추가되었고 public 속성이 이 true 가 되었다. 여기서 public/private 은 REST/GraphQL 등 외부 API로 노출되었을때에 대한 가시성을 의미한다. public 이면 나중에 API 로 접근할 경우 필드로 나타날 것이다. public? 으로 명시하지 않으면 기본적으로 모두 private 이다. 내부적 접근 (예를들어 subject = ticket.subject 같은) 과는 상관이 없다.

status attribute 가 추가되었고 자료형은 atom, 값에 대한 제약조건이 :open, :closed 중 한개로 제약 조건이 붙었다. 기본 값은 :open 이다.

attribute 를 보완했으니 create 액션도 보완할 차례다.

# lib/helpdesk/support/ticket.ex

actions do
  ...
  create :open do
    accept [:subject]
  end
end

create :create 에서 create :open 으로 변경되었다. create 액션의 이름이 :open 로 변경된 것이다. Ash.Changeset.for_create 호출 파라미터도 :open 으로 바뀌어야 한다. accept 은 값을 인자로 전달받을 attribute 를 지정한다. attribute 로 지정되어 있지 않은 이름을 atom 으로 작성하면 거절(컴파일 오류) 된다.

변경된 Ticket 리소스의 형태는 다음과 같다.

defmodule Helpdesk.Support.Ticket do
  use Ash.Resource, domain: Helpdesk.Support

  actions do
    defaults [:read]

    create :open do
      accept [:subject]
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :subject, :string do
      allow_nil? false

      public? false
    end

    attribute :status, :atom do
      constraints one_of: [:open, :closed]

      default :open

      allow_nil? false
    end
  end
end

iex 를 재실행하거나 recompile() 해서 코드를 업데이트 하고 다음과 같이 값을 전달하여 Ticket을 만든다.

iex> recompile()

iex> Helpdesk.Support.Ticket
|> Ash.Changeset.for_create(:open, %{subject: "My mouse won't click!"})
|> Ash.create!()

%Helpdesk.Support.Ticket{
  id: "d29d5348-ff98-4cc2-adbd-bffe29cef64f",
  subject: "My mouse won't click!",
  status: :open,
  __meta__: #Ecto.Schema.Metadata<:built, "">
}

입력 값이 잘못되거나 액션 이름이 잘못되었다면 에러가 발생한다.

iex> ticket = Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:open, %{subject1: "My mouse won't click!"})|> Ash.create!()
** (Ash.Error.Invalid) 
Bread Crumbs:
  > Error returned from: Helpdesk.Support.Ticket.open

Invalid Error

* No such input `subject1` for action Helpdesk.Support.Ticket.open

No such attribute on Helpdesk.Support.Ticket, or argument on Helpdesk.Support.Ticket.open

Did you mean:

* subject


Valid Inputs:

* subject
iex> ticket = Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:open)|> Ash.create!()
** (Ash.Error.Invalid) 
Bread Crumbs:
  > Error returned from: Helpdesk.Support.Ticket.open

Invalid Error

* attribute subject is required

다음 글에서는 레코드의 변경과 검증에 대해 알아 보자.

Ash Framework 튜토리얼 따라하기 (1)

https://hexdocs.pm/ash/get-started.html 을 따라해 보며 Ash 프레임워크의 기본 기능을 배워 본다.

시나리오

간소화된 HelpDesk 시스템을 가정해 보자.

시스템에는 두 개의 리소스가 있다.

  • Helpdesk.Support.Ticket (티켓)
  • Helpdesk.Support.Representative (담당자)

리소스에는 다음과 같은 액션을 할 수 있다.

  • 티켓을 열 수 있다
  • 티켓을 닫을 수 있다
  • 담당자에게 티켓을 배정할 수 있다

Ash 를 사용하여 구현

새 프로젝트 생성

mix 프로젝트가 없다면 새로 만들 수 있다. Ash 만 사용하는 프로젝트도 만들 수 있으나 향후 JSON API 까지 고려해 Phoenix도 같이 설치 한다.

# install the archive
mix archive.install hex phx_new
mix archive.install hex igniter_new

# use the `--with` flag to generate the project with phx.new and add Ash
mix igniter.new helpdesk --install ash,ash_phoenix --with phx.new && cd helpdesk

igniter 는 Elixir 로 만들어진 코드 생성기 이다. 지금 처럼 프레임워크 초반 세팅이나 버전 업그레이드를 할 때 사용할 수 있다.

첫 번째 도메인

먼저 도메인 폴더와 파일을 만든다.

mkdir -p lib/helpdesk/support && touch $_/ticket.ex && touch lib/helpdesk/support.ex

다음과 같은 폴더 구조일 것이다. lib/helpdesk 내부에 support 도메인이 만들어졌다.

lib/helpdesk/support.ex 파일에 다음 코드를 작성한다.

# lib/helpdesk/support.ex

defmodule Helpdesk.Support do
  use Ash.Domain

  resources do
    resource Helpdesk.Support.Ticket
  end
end

Helpdesk.Support 는 Ash.Domain 이고 Helpdesk.Support.Ticket 이라는 Resource 를 가지는 것으로 선언한다.

defmodule Helpdesk.Support.Ticket do
  # 이것은 이 모듈을 리소스로 만듭니다
  use Ash.Resource, domain: Helpdesk.Support

  actions do
    # :read 액션의 기본 구현을 사용합니다
    defaults [:read]
    # 그리고 나중에 커스터마이징할 create 액션
    create :create
  end

  # 속성들은 리소스에 존재하는 간단한 데이터 조각들입니다
  attributes do
    # `:id`라는 자동 생성되는 UUID 기본 키를 추가합니다
    uuid_primary_key :id
    # `:subject`라는 문자열 타입 속성을 추가합니다
    attribute :subject, :string
  end
end

Helpdesk.Support.Ticket 은 Ash.Resource 이고 Helpdesk.Support 도메인에 속한다. 이 리소스를 다루는 몇가지 action 들을 선언할 수 있고 속성 데이터도 정의할 수 있다.

실행

% iex -S mix

iex(1)> Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:create)|> Ash.create!()

Helpdesk.Support.Ticket 리소스를 한 개 만들어 내기 위해 먼저 Changeset 을 만들고 Changeset 을 가지고 Ash.create! 함수를 통해 레코드를 만든다. Ecto.Changeset의 역할을 Ash 에선 Ash.Changeset이 담당하는 것 같다.

warning: Domain Helpdesk.Support is not present in

    config :helpdesk, ash_domains: [].


To resolve this warning, do one of the following.

1. Add the domain to your configured domain modules. The following snippet can be used.

    config :helpdesk, ash_domains: [Helpdesk.Support]

2. Add the option `validate_config_inclusion?: false` to `use Ash.Domain`

3. Configure all domains not to warn, with `config :ash, :validate_domain_config_inclusion?, false`

warning 발생해도 결과는 얻어지지만 warning 을 없애기 위해 다음 코드를 config.exs 에 추가한다.

# in config/config.exs
import Config

config :helpdesk, :ash_domains, [Helpdesk.Support] # 추가
% iex -S mix

iex(1)> Helpdesk.Support.Ticket|> Ash.Changeset.for_create(:create)|> Ash.create!()

%Helpdesk.Support.Ticket{
  id: "7d3d0639-5201-4eb3-9532-a7c837c98eaa",
  subject: nil,
  __meta__: #Ecto.Schema.Metadata<:built, "">
}

도메인과 리소스를 정의하고 몇가지 선언만 추가해도 기본 뼈대가 되는 기능을 얻을 수 있다.

다음 글에서 계속…

다른 모듈의 기능을 흡수(?) 하는 use

Elixir로 만들어진 프레임워크를 가지고 개발을 할 때 다음과 같이 use 구문을 자주 보게 된다.

defmodule MyAppWeb.DemoLive do
  use Phoenix.LiveView

  ...
end

구문이 익숙하지 않더라도 뭔가 모듈에 해당 프레임워크 기능을 추가? 사용? 한다는 느낌이 든다. use 키워드가 어떤 기능을 하는지 알아 보자.

Elixir School 에서는 use 키워드를 다음과 같이 설명하고 있다.

use 매크로로 현재 모듈의 정의를 다른 모듈이 수정할 수 있게 합니다. 코드에서 use를 호출하면 실제로 제공된 모듈에 의해 정의된 __using__/1 콜백을 호출합니다. __using__/1 매크로의 결과는 모듈 정의의 일부가 됩니다.

사용하는 모듈 입장에서는 use 구문을 통해 외부 모듈이 만들어 놓은 코드(기능)를 내 모듈로 가져오는 효과가 있다. Elixir School 의 예제 코드를 살펴 보자.

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

어떤 외부 모듈이 미리 만들어 매크로로 hello 라는 기능(함수)을 정의해 놨다.

defmodule Example do
  use Hello
end

Example 모듈에서 Hello 모듈이 만들어 놓은 기능을 사용할 수 있도록 use 구문을 사용했다.

iex> Example.hello("Sean")
"Hi, Sean"

Example 모듈에서 마치 본인 모듈의 기능인 것 처럼 함수를 호출할 수 있다.


use 키워드를 살펴보면서 자바/스프링에서 어노테이션이 자연스럽게 떠올랐다. 자바/스프링 개발 경험이 있다면 데이터 클래스에 @Data 를 붙이거나 컨트롤러에 @RestController 를 붙이는 예를 생각하면 이해가 쉬울 것 같다. 개발자가 프레임워크를 쓰는 이유는 “이미 잘 만들어진 걸 가져다 쓰자”인데, Elixir 에서는 use 키워드가 그걸 가능하게 해 준다.

Elixir: alias vs import

Elixir 에서 모듈이 외부의 다른 모듈 또는 함수를 참조하는 키워드로 aliasimport 가 있다.

defmodule MyApp.Context do
  alias MyApp.Repo
  alias MyApp.Users.User
  
  import Ecto.Query, only: [from: 2, where: 3]
  
  def list_active_users do
    from(u in User, where: u.active == true)
    |> Repo.all()  
  end
end

둘 다 외부 모듈을 참조할수 있게 하는 키워드인 것 같은데 언제 alias 를 쓰고 언제 import 를 쓸까?

alias?

alias 는 이름과 같이 모듈에 별칭을 지어줄 수 있다. 다음은 Elixir School 에서 예로 든 alias 코드이다.

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

# 별칭을 사용하지 않는 경우

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

별칭을 사용하지 않으면 모듈의 Namespace 를 정확히 지정(Sayings.Greetings)해야 하지만 alias 를 사용하면 Greetings 로도 해당 모듈에 접근할 수 있다.

아래와 같이 아예 다른 별명을 붙여 사용할 수도 있다.

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

import?

import 는 기본적으로 모듈의 모든 함수와 매크로를 불러오지만 일부만 불러올 수 도 있다.

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3
iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3
iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

alias 와는 다르게 모듈 이름을 지정하지 않고 함수를 바로 사용할 수 있다.


import 가 모듈 기능을 다 불러올 수 있으면 import 만 쓰면 되지 않나? 하는 의문이 들 수 있다. 그 의문에 대해 Claude Opus 4.1 이 대답 해준 내용이 깔끔해서 일부를 첨부한다.

핵심 차이점

alias: 모듈 이름을 단축하거나 변경

  • 참조시 별칭을 붙여 모듈 이름을 바꿀 수 있음
  • 함수 호출 시 모듈명 필요 (A.function())

import: 함수/매크로를 현재 네임스페이스로 가져옴

  • 모듈명 없이 직접 함수 호출 가능 (function())
  • 컴파일러가 함수 이름을 원본 모듈로 자동 연결

분리한 이유

1. 네임스페이스 오염 방지

# import를 남용하면
import Module1
import Module2
import Module3

some_function() # 어느 모듈의 함수인지 불명확

2. 코드 가독성과 추적성

# alias 사용 - 출처가 명확
alias MyApp.Users.UserService, as: Users
Users.create_user(params)  # 모듈이 명시되어 있음

# import 사용 - 출처가 불명확
import MyApp.Users.UserService
create_user(params)  # 어디서 온 함수인지 즉시 알기 어려움

3. 선택적 가져오기의 유연성

# Ecto.Query의 경우 - DSL처럼 사용하고 싶을 때
import Ecto.Query, only: [from: 2, where: 3]

# 일반 모듈은 alias로 충분
alias MyApp.Accounts
Accounts.get_user(id)

4. 충돌 관리

defmodule MyModule do
  # 두 모듈에 같은 이름의 함수가 있을 때
  alias ModuleA
  alias ModuleB
  
  ModuleA.process(data)  # 명확함
  ModuleB.process(data)  # 명확함
  
  # import 하면 충돌 발생
  # import ModuleA
  # import ModuleB  # process/1 충돌!
end

실무 관례

일반적으로:

  • alias: 대부분의 모듈 참조에 사용
  • import: DSL 스타일 API (Ecto.Query 등)나 자주 사용하는 헬퍼 함수에만 제한적 사용
defmodule MyApp.Context do
  # 모듈은 alias
  alias MyApp.Repo
  alias MyApp.Users.User
  
  # DSL은 import
  import Ecto.Query, only: [from: 2, where: 3]
  
  def list_active_users do
    from(u in User, where: u.active == true)
    |> Repo.all()  # Repo는 alias로 참조
  end
end

alias 는 사용처에서 이름도 직접 지정할 수 있고 기본적으로 모듈 이름을 명시적으로 사용해야 하는 반면 import 는 참조에 있어서 더 유연한 기능을 제공하지만 모듈 이름을 사용하지 않기 때문에 alias 에 비해 좀 더 암묵적인 특성이 있다. 기본적으로 alias 를 주로 사용하겠지만 상황에 따라 DSL 같이 함수 이름 자체가 특정 모듈을 확정할 수 있다면 import 를 통해 코드 축약을 기대할 수 있을 것 같다.

처음 배우는 엘릭서 프로그래밍 5장 연습문제 풀이

처음 배우는 엘릭서 프로그래밍 책에 포함된 연습문제에 대한 풀이를 공유합니다.

문제 5-2, 5-3 이고 조건문을 사용하지 않고 FizzBuzz를 구현하는 방법입니다.

# 5-2 인자 세 개를 받는 함수
fizz_buzz = fn
0, 0, _ -> "FizzBuzz"
0, _, _ -> "Fizz"
_, 0, _ -> "Buzz"
_, _, a -> a
end
# 5-3 FizzBuzz 함수
fizzbuzz = fn
n -> fizz_buzz.(rem(n, 3), rem(n, 5), n)
end
IO.puts fizzbuzz.(10)
IO.puts fizzbuzz.(11)
IO.puts fizzbuzz.(12)
IO.puts fizzbuzz.(13)
IO.puts fizzbuzz.(14)
IO.puts fizzbuzz.(15)
IO.puts fizzbuzz.(16)
view raw 5-FizzBuzz.exs hosted with ❤ by GitHub

처음 배우는 엘릭서 프로그래밍 2장 연습문제 풀이

처음 배우는 엘릭서 프로그래밍 책에 포함된 연습문제에 대한 풀이를 공유합니다.

# 처음 배우는 엘릭서 프로그래밍 연습문제
# 2-1 다음 중 매칭에 성공하는 식은 무엇일까?
a = [1, 2, 3] # 성공
a = 4 # 성공
4 = a # 성공. 단, a 가 4로 할당되어 있다면
[a, b] = [1, 2, 3] # 실패
a = [[1, 2, 3]] # 성공
[a] = [[1, 2, 3]] # 성공
[[a]] = [[1, 2, 3]] # 실패
# 2-2 다음 중 매칭에 성공하는 식은 무엇인가?
[a, b, a] = [1, 2, 3] # 실패
[a, b, a] = [1, 1, 2] # 실패
[a, b, a] = [1, 2, 1] # 성공. a = 1, b = 2
# 2-3 변수 a에 값 2가 바인딩 되어 있다면 다음 중 매칭에 성공하는 식은 무엇인가?
[a, b, a] = [1, 2, 3] # 실패
[a, b, a] = [1, 1, 2] # 실패
a = 1 # 1로 새로 바인딩 됨
^a = 2 # 성공
^a = 1 # 실패
^a = 2 - a # 실패
view raw 2.exs hosted with ❤ by GitHub

Phoenix Up and Running

Phoenix 는 Elixir 를 위한 웹 개발 프레임워크이다. 피닉스 가이드 문서를 따라서 간단하게 피닉스 웹 서버를 띄운다.

설치

Elixir 설치
brew install elixir

다른 플랫폼의 경우 https://elixir-lang.org/install.html 참조

Hex 설치

Elixir 설치가 처음이면 Hex 설치가 필요하다. Hex 는 Erlang 에코시스템의 패키지 매니저이다.

mix local.hex

mix 는 Elixir 의 빌드 도구이다.

Mix is a build tool that ships with Elixir that provides tasks for creating, compiling, testing your application, managing its dependencies and much more

Phoenix 생성기 설치

Phoenix 프로젝트를 생성하기 위해 다음의 명령 실행. Phoenix 생성기를 설치한다.

mix archive.install hex phx_new

프로젝트 생성

hello 라는 이름의 Phoenix 프로젝트를 만든다.

mix phx.new hello

데이터베이스를 사용하지 않을 것이라면 –no-ecto 옵션을 추가한다. Ecto 는 Elixir 를 위한 데이터베이스 래퍼와 쿼리 생성기이다.

Fetch and instll dependencies? 를 Y 로 넘어가면 다음과 같은 결과를 얻을 수 있다.

Fetch and install dependencies? [Yn] Y

* running mix deps.get
* running mix deps.compile

We are almost there! The following steps are missing:

&nbsp; &nbsp; $ cd hello

Then configure your database in config/dev.exs and run:

&nbsp; &nbsp; $ mix ecto.create

Start your Phoenix app with:

&nbsp; &nbsp; $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

&nbsp; &nbsp; $ iex -S mix phx.server

프로젝트 폴더로 이동 한 다음 ecto 설정을 실행한다.

cd hello
mix ecto.create

ecto 명령어 실행 중 에러가 발생하면 다음 문서를 참고한다.

postgresql 을 처음 설치했다면 role 이 없어서 오류가 발생한다. psql 에서 다음 명령어를 실행한다.

=# CREATE ROLE postgres LOGIN CREATEDB;

실행

모두 완료가 되었다면 Phoenix 서버를 실행한다.

mix phx.server
[info] Running HelloWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
[info] Access HelloWeb.Endpoint at http://localhost:4000

위와 같은 로그를 확인했다면 127.0.0.1:4000 에서 첫 화면을 확인.

완료!