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
-
pid = spawn_link(fn -> loop(sorted_deck()) end)
spawn_link
는 새로운 프로세스를 만든다. 이 새 프로세스는loop(sorted_deck())
함수를 실행한다.- 새 프로세스의 아이디를
pid
변수에 저장한다.
-
if Process.register(pid, __MODULE__) do
Process.register
는 프로세스 아이디(pid
)를 이 모듈의 이름(__MODULE__
, 즉CardServer
)에 연결한다.
-
:ok
- 성공하면
:ok
를 반환한다.
- 성공하면
-
{: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}
{: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)을 가능하게 한다. 이 코드는 새로운 카드 덱을 생성하라는 메시지를 받으면, 덱을 새로 만들고 그 사실을 호출한 프로세스에 알려주는 역할을 한다.
-
send(caller, :new_deck_created)
- 새 덱이 만들어졌다는 메시지를
caller
에게 보낸다.
- 새 덱이 만들어졌다는 메시지를
-
loop(sorted_deck())
- 새로운 정렬된 덱을 만들고
loop
함수를 다시 시작한다.
- 새로운 정렬된 덱을 만들고
{:shuffle, caller}
-
{:shuffle, caller} ->
- 덱을 섞으라는 메시지를 받는다.
-
send(caller, :deck_shuffled)
- 덱이 섞였다는 메시지를
caller
에게 보낸다.
- 덱이 섞였다는 메시지를
-
loop(Enum.shuffle(deck))
- 덱을 섞고,
loop
함수를 다시 시작한다.
- 덱을 섞고,
{:count, caller}
-
{:count, caller} ->
- 현재 덱에 몇 장의 카드가 있는지 물어보는 메시지를 받는다.
-
send(caller, {:count, length(deck)})
- 덱의 카드 개수를
caller
에게 보낸다.
- 덱의 카드 개수를
-
loop(deck)
- 덱 상태는 그대로 유지하고,
loop
함수를 다시 시작한다.
- 덱 상태는 그대로 유지하고,
{:deal, n, caller} when n <= length(deck) and n > 0
-
조건
n <= length(deck) and n > 0
- 덱에 충분한 카드가 있고, 요청한 카드 수(
n
)가 0보다 큰 경우.
- 덱에 충분한 카드가 있고, 요청한 카드 수(
-
{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장의 카드를 나타내고, 두 번째 리스트는 남은 카드를 나타내므로, 이렇게 나눠진 카드를 다른 함수나 변수에 저장해서 게임을 진행할 수 있다.
-
send(caller, {:ok, deal_deck})
n
장의 카드(deal_deck
)를caller
에게 보낸다.
-
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
- 카드 덱을 초기 상태로 만드는 함수다.
suits
와ranks
를 사용해서 52장의 카드 덱을 만든다.
이렇게 각 코드 라인이 무엇을 하는지 쉽게 설명해 봤다. 이해하기 쉬웠으면 좋겠다.
아래는 이 코드를 테스트하는 방법이다. 우선 서버를 가동시키고 그 서버에 명령을 내려서 각종 데이터를 가져온 다음에 특정 함수를 구동하는 방식이다.
이 코드를 테스트하기 위해서는 iex
(Interactive Elixir) 쉘을 사용하면 된다. 아래에 각 함수를 테스트하는 방법을 순서대로 나열했다.
-
먼저
iex
쉘을 실행한다.iex
-
코드를 컴파일한다.
c("path/to/card_server.ex")
-
CardServer
프로세스를 시작한다.CardServer.start()
- 이 호출로 인해 새 프로세스가 생성되고, 해당 프로세스 아이디가
CardServer
라는 이름으로 등록된다.
- 이 호출로 인해 새 프로세스가 생성되고, 해당 프로세스 아이디가
-
현재 남아있는 카드의 수를 확인한다.
send(CardServer, {:count, self()}) receive do {:count, count} -> IO.puts("Remaining cards: #{count}") end
- 초기 상태라면 52라고 출력될 것이다.
-
카드를 셔플한다.
send(CardServer, {:shuffle, self()}) receive do :deck_shuffled -> IO.puts("Deck shuffled") end
-
5장의 카드를 뽑는다.
send(CardServer, {:deal, 5, self()}) receive do {:ok, cards} -> IO.inspect(cards, label: "Dealt cards") {:error, reason} -> IO.puts("Error: #{reason}") end
-
다시 남아있는 카드의 수를 확인한다.
send(CardServer, {:count, self()}) receive do {:count, count} -> IO.puts("Remaining cards: #{count}") end
- 이제는 47이라고 출력될 것이다.
-
새로운 덱으로 시작한다.
send(CardServer, {:new, self()}) receive do :new_deck_created -> IO.puts("New deck created") end
-
마지막으로 다시 남아있는 카드의 수를 확인한다.
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로 바꿨다.
댓글 없음:
댓글 쓰기