Page

관련 포스팅

2023년 9월 15일 금요일

Week02 - Code02 분석 (CardDeck in Server)


이번 포스팅에서는 Elixir에서 52장의 카드 deck을 초기화하고, 섞고, 뽑고, 남은 카드를 카운팅하는 코드에 대해서 분석해보려 한다.

코드는 아래와 같다.


defmodule CardServer do
  def start do
    pid = spawn_link(fn -> loop(sorted_deck()) end)
    if Process.register(pid, __MODULE__) do
      :ok
    else
      {:error, "Failed to register process"}
    end
  end

  defp loop(deck) do
    receive do
      {:new, caller} ->
        send(caller, :new_deck_created)
        loop(sorted_deck())

      {:shuffle, caller} ->
        send(caller, :deck_shuffled)
        loop(Enum.shuffle(deck))

      {:count, caller} ->
        send(caller, {:count, length(deck)})
        loop(deck)

      {:deal, n, caller} when n <= length(deck) and n > 0 ->
        {deal_deck, remaining_deck} = Enum.split(deck, n)
        send(caller, {:ok, deal_deck})
        loop(remaining_deck)

      {:deal, n, caller} when n > length(deck) ->
        send(caller, {:error, "Not enough cards"})
        loop(deck)

      {:deal, n, caller} when n <= 0 ->
        send(caller, {:error, "Number must be greater than zero"})
        loop(deck)
    end
  end

  defp sorted_deck do
    suits = [:clover, :diamond, :heart, :spade]
    ranks = 2..14
    for suit <- suits, rank <- ranks, do: {suit, rank}
  end
end





mark down으로 포스팅을 하는 것은 정말 좋다. 하지만, 여기에도 단점이 하나 있다. 그것은 바로 Elixir의 코드를 code block에 넣는것 까지는 좋은데, VSCode에서 복붙하는 것 대비 가독성이 너무나도 떨어진다는 것이다. 그래서 양쪽의 장점만을 따서 가져오는 hybrid 방식을 택하기로 했다.


우선 코드 분석을 해보자.

이 코드는 카드 덱을 관리하는 Elixir 모듈인 CardServer를 정의한다. 카드 덱을 셔플하고, 카드를 나눠주고, 카드의 개수를 세는 등의 작업을 할 수 있다. 코드는 크게 세 부분으로 나뉜다: start/0, loop/1, 그리고 sorted_deck/0. 하나씩 살펴보자.

start/0

  def start do
    pid = spawn_link(fn -> loop(sorted_deck()) end)
    if Process.register(pid, __MODULE__) do
      :ok
    else
      {:error, "Failed to register process"}
    end
  end
  1. pid = spawn_link(fn -> loop(sorted_deck()) end)

    • spawn_link는 새로운 프로세스를 만든다. 이 새 프로세스는 loop(sorted_deck()) 함수를 실행한다.
    • 새 프로세스의 아이디를 pid 변수에 저장한다.
  2. if Process.register(pid, __MODULE__) do

    • Process.register는 프로세스 아이디(pid)를 이 모듈의 이름(__MODULE__, 즉 CardServer)에 연결한다.
  3. :ok

    • 성공하면 :ok를 반환한다.
  4. {:error, "Failed to register process"}

    • 실패하면 에러 메시지와 함께 튜플을 반환한다.

loop/1

  defp loop(deck) do
    receive do
      {:new, caller} ->
        send(caller, :new_deck_created)
        loop(sorted_deck())

      {:shuffle, caller} ->
        send(caller, :deck_shuffled)
        loop(Enum.shuffle(deck))

      {:count, caller} ->
        send(caller, {:count, length(deck)})
        loop(deck)

      {:deal, n, caller} when n <= length(deck) and n > 0 ->
        {deal_deck, remaining_deck} = Enum.split(deck, n)
        send(caller, {:ok, deal_deck})
        loop(remaining_deck)

      {:deal, n, caller} when n > length(deck) ->
        send(caller, {:error, "Not enough cards"})
        loop(deck)

      {:deal, n, caller} when n <= 0 ->
        send(caller, {:error, "Number must be greater than zero"})
        loop(deck)
    end
  end
  • loop(deck)
    • loop 함수는 현재 덱 상태(deck)를 받아서 무한히 돈다. 여기서는 다양한 메시지를 받아 처리한다.

loop/1 함수는 CardServer 모듈의 핵심 부분이다. receive do ... end 블록 안에서 이벤트 루프가 돌면서 외부에서 보낸 메시지를 받아 처리한다. 카드 덱(deck)을 인자로 받아 다양한 명령을 처리한다.

{:new, caller}

  1. {:new, caller} ->
    • 새로운 덱을 만들라는 메시지를 받는다. 메시지의 형태는 {:new, caller} 이다. 여기서 caller는 메시지를 보낸 프로세스의 아이디다.

이 코드 블록은 Elixir의 receive 키워드 내부에서 동작하는 패턴 매칭과 메시지 전송을 사용하고 있다. 이해를 돕기 위해 코드를 부분별로 나눠 설명하겠다.

{:new, caller} ->

  • receive 문법 안에서, {:new, caller}는 들어오는 메시지가 :new이라는 애터믹(atomic) 심볼과 caller라는 변수를 가진 튜플일 경우에만 이 블록의 코드가 실행되게 한다.
  • caller는 이 메시지를 보낸 프로세스의 PID(Process ID)가 된다.

send(caller, :new_deck_created)

  • send 함수는 caller라는 프로세스에 :new_deck_created라는 메시지를 보낸다.
  • 이렇게 하면 메시지를 보낸 프로세스는 :new_deck_created라는 메시지를 받을 수 있다.

loop(sorted_deck())

  • sorted_deck()는 새로운 정렬된 카드 덱을 반환한다.
  • loop 함수를 새로운 덱으로 다시 시작한다. 이로써 서버는 다시 메시지를 기다린다.

이 구조는 Elixir에서 많이 볼 수 있는 메시지 패턴이며, 프로세스 간의 통신(IPC, Inter-Process Communication)을 가능하게 한다. 이 코드는 새로운 카드 덱을 생성하라는 메시지를 받으면, 덱을 새로 만들고 그 사실을 호출한 프로세스에 알려주는 역할을 한다.

  1. send(caller, :new_deck_created)

    • 새 덱이 만들어졌다는 메시지를 caller에게 보낸다.
  2. loop(sorted_deck())

    • 새로운 정렬된 덱을 만들고 loop 함수를 다시 시작한다.

{:shuffle, caller}

  1. {:shuffle, caller} ->

    • 덱을 섞으라는 메시지를 받는다.
  2. send(caller, :deck_shuffled)

    • 덱이 섞였다는 메시지를 caller에게 보낸다.
  3. loop(Enum.shuffle(deck))

    • 덱을 섞고, loop 함수를 다시 시작한다.

{:count, caller}

  1. {:count, caller} ->

    • 현재 덱에 몇 장의 카드가 있는지 물어보는 메시지를 받는다.
  2. send(caller, {:count, length(deck)})

    • 덱의 카드 개수를 caller에게 보낸다.
  3. loop(deck)

    • 덱 상태는 그대로 유지하고, loop 함수를 다시 시작한다.

{:deal, n, caller} when n <= length(deck) and n > 0

  1. 조건 n <= length(deck) and n > 0

    • 덱에 충분한 카드가 있고, 요청한 카드 수(n)가 0보다 큰 경우.
  2. {deal_deck, remaining_deck} = Enum.split(deck, n)

    • 덱에서 n 장의 카드를 가져와 deal_deck에 저장하고, 남은 카드를 remaining_deck에 저장한다.
    • Enum.split(deck, n) 함수는 Elixir의 Enum 모듈에 있는 함수로, 컬렉션(deck 같은 리스트나 다른 열거형 타입)을 두 부분으로 나눈다. 나누는 기준은 두 번째 인자인 n이다.
    • 이 함수는 튜플을 반환하는데, 첫 번째 원소는 원래 컬렉션의 처음부터 n번째 원소까지를 포함한 새 컬렉션이고, 두 번째 원소는 n+1번째 원소부터 끝까지를 포함한 새 컬렉션이다.
    • 예를 들어, deck이 [1, 2, 3, 4, 5]이고 n이 3이라면, Enum.split(deck, 3)의 결과는 [{1, 2, 3}, {4, 5}]가 될 것이다. 첫 번째 리스트에는 deck의 처음부터 3번째 원소까지가 들어있고, 두 번째 리스트에는 나머지 원소들이 들어있다.
    • 이 함수는 카드 게임에서 카드를 n장만큼 나눠주는 상황에서 유용하게 사용될 수 있다. 첫 번째 리스트는 n장의 카드를 나타내고, 두 번째 리스트는 남은 카드를 나타내므로, 이렇게 나눠진 카드를 다른 함수나 변수에 저장해서 게임을 진행할 수 있다.



  1. send(caller, {:ok, deal_deck})

    • n 장의 카드(deal_deck)를 caller에게 보낸다.
  2. loop(remaining_deck)

    • 남은 카드(remaining_deck)로 loop 함수를 다시 시작한다.

{:deal, n, caller} when n > length(deck)

  • 덱에 카드가 부족한 경우, {:error, "Not enough cards"} 메시지를 caller에게 보낸다.

{:deal, n, caller} when n <= 0

  • 요청한 카드 수(n)가 0 이하인 경우, {:error, "Number must be greater than zero"} 메시지를 caller에게 보낸다.

receive do ... end

  • 이 부분은 들어오는 메시지에 따라 어떤 작업을 할지 정한다.

sorted_deck/0

  defp sorted_deck do
    suits = [:clover, :diamond, :heart, :spade]
    ranks = 2..14
    for suit <- suits, rank <- ranks, do: {suit, rank}
  end
  • 카드 덱을 초기 상태로 만드는 함수다.
  • suitsranks를 사용해서 52장의 카드 덱을 만든다.

이렇게 각 코드 라인이 무엇을 하는지 쉽게 설명해 봤다. 이해하기 쉬웠으면 좋겠다.


아래는 이 코드를 테스트하는 방법이다. 우선 서버를 가동시키고 그 서버에 명령을 내려서 각종 데이터를 가져온 다음에 특정 함수를 구동하는 방식이다.

이 코드를 테스트하기 위해서는 iex (Interactive Elixir) 쉘을 사용하면 된다. 아래에 각 함수를 테스트하는 방법을 순서대로 나열했다.

  1. 먼저 iex 쉘을 실행한다.

    iex
    
  2. 코드를 컴파일한다.

    c("path/to/card_server.ex")
    
  3. CardServer 프로세스를 시작한다.

    CardServer.start()
    
    • 이 호출로 인해 새 프로세스가 생성되고, 해당 프로세스 아이디가 CardServer라는 이름으로 등록된다.
  4. 현재 남아있는 카드의 수를 확인한다.

    send(CardServer, {:count, self()})
    receive do
      {:count, count} -> IO.puts("Remaining cards: #{count}")
    end
    
    • 초기 상태라면 52라고 출력될 것이다.
  5. 카드를 셔플한다.

    send(CardServer, {:shuffle, self()})
    receive do
      :deck_shuffled -> IO.puts("Deck shuffled")
    end
    
  6. 5장의 카드를 뽑는다.

    send(CardServer, {:deal, 5, self()})
    receive do
      {:ok, cards} -> IO.inspect(cards, label: "Dealt cards")
      {:error, reason} -> IO.puts("Error: #{reason}")
    end
    
  7. 다시 남아있는 카드의 수를 확인한다.

    send(CardServer, {:count, self()})
    receive do
      {:count, count} -> IO.puts("Remaining cards: #{count}")
    end
    
    • 이제는 47이라고 출력될 것이다.
  8. 새로운 덱으로 시작한다.

    send(CardServer, {:new, self()})
    receive do
      :new_deck_created -> IO.puts("New deck created")
    end
    
  9. 마지막으로 다시 남아있는 카드의 수를 확인한다.

    send(CardServer, {:count, self()})
    receive do
      {:count, count} -> IO.puts("Remaining cards: #{count}")
    end
    
    • 새 덱이 생성되었으므로 다시 52라고 출력될 것이다.

이런 식으로 iex 쉘에서 각 함수를 호출하고 결과를 확인해서 CardServer 모듈이 제대로 작동하는지 테스트할 수 있다.


위의 코드는 process 부분만 registered로 구현을 했고, 아래는 함수들을 전부 registered로 변환한 코드이다. 추가로 11, 12, 13, 14를 jack, queen, king, ace로 바꿨다.

defmodule CardServer do
  # CardServer 프로세스를 시작한다
  def start do
    # 새로운 프로세스를 생성하고, 정렬된 덱으로 loop 함수를 실행한다
    pid = spawn_link(fn -> loop(sorted_deck()) end)

    # 새 프로세스를 CardServer 이름으로 등록한다
    if Process.register(pid, __MODULE__) do
      :ok
    else
      {:error, "Failed to register process"}
    end
  end

  # 새로운 정렬된 덱을 생성한다
  def new do
    send(__MODULE__, {:new, self()})
    receive do
      :new_deck_created -> :ok
    end
  end

  # 현재 덱을 섞는다
  def shuffle do
    send(__MODULE__, {:shuffle, self()})
    receive do
      :deck_shuffled -> :ok
    end
  end

  # 현재 덱의 카드 개수를 반환한다
  def count do
    send(__MODULE__, {:count, self()})
    receive do
      {:count, n} -> {:ok, n}
    end
  end

  # 덱에서 'n'장의 카드를 딜링한다
  def deal(n) do
    send(__MODULE__, {:deal, n, self()})
    receive do
      {:ok, deal_deck} -> {:ok, deal_deck}
      {:error, reason} -> {:error, reason}
    end
  end

  # 비공개 함수로 상태를 유지하고 메시지를 처리한다
  defp loop(deck) do
    receive do
      {:new, caller} ->
        send(caller, :new_deck_created)
        loop(sorted_deck())
      {:shuffle, caller} ->
        send(caller, :deck_shuffled)
        loop(Enum.shuffle(deck))
      {:count, caller} ->
        send(caller, {:count, length(deck)})
        loop(deck)
      {:deal, n, caller} when n <= length(deck) and n > 0 ->
        {deal_deck, remaining_deck} = Enum.split(deck, n)
        send(caller, {:ok, deal_deck})
        loop(remaining_deck)
      {:deal, n, caller} when n > length(deck) ->
        send(caller, {:error, "Not enough cards"})
        loop(deck)
      {:deal, n, caller} when n <= 0 ->
        send(caller, {:error, "Number must be greater than zero"})
        loop(deck)
    end
  end

  # 비공개 함수로 정렬된 덱을 생성한다
  defp sorted_deck do
    suits = [:clover, :diamond, :heart, :spade]
    ranks = [2, 3, 4, 5, 6, 7, 8, 9, 10, :jack, :queen, :king, :ace]
    for suit <- suits, rank <- ranks, do: {suit, rank}
  end
end




댓글 없음:

댓글 쓰기

관련 포스팅