Page

관련 포스팅

2023년 9월 14일 목요일

Week02 - Elixir 기초 문법 (Server 및 Counter)

이번 포스팅은 실험적으로 mark down으로 해보려 한다. 사실, 무언가를 작업할 때 마우스를 사용하는 것 자체가 굉장히 비효율적이긴 하다. 왜냐하면 손이 키보드에서 떠나서 마우스까지 가는 시간, 그리고 마우스를 움직여서 클릭을 하는데 들어가는 시간들의 누적값이 상당하기 때문이다. 게다가 연속적인 업무에 있어서 흐름이 끊기고, 피로감이 들기 때문에 고민이 많았다. 특히나 이번에 옮긴 구글 블로그는 UI가 기존의 네이버나 티스토리에 비해 상당히 떨어진다. 그래서 흔하디 흔한 기능 조차 단축키가 없어서 너무나도 효율이 떨어진 마당에 마침 markdown이 생각난 것이다.


코드1

defmodule ArithmeticServer do
  def start() do
    spawn(&loop/0)
  end

  # client API
  def square(pid, x) do
    send(pid, {self(), :square, x})
    receive do
      {:ok, x} -> x
    end
  end

  def sqrt(pid, x) do
    send(pid, {self(), :sqrt, x})
    receive do
      {:ok, x} -> x
      {:error, reason} -> IO.puts("There was an error: #{reason}")
    end
  end

  defp loop() do
    receive do
      {from, :square, x} ->
        send(from, {:ok, x * x})
      {from, :sqrt, x} ->
        reply =
          if x >= 0,
            do: {:ok, :math.sqrt(x)},
            else: {:error, "square root of negative number"}
        send(from, reply)
    end
    loop()
  end
end

Elixir에서 이 코드는 간단한 서버를 구현하는 모듈을 보여준다. ArithmeticServer 모듈에는 start와 square 두 개의 함수가 있다.

def start() do ... end

  • start 함수는 spawn(&loop/0)를 호출한다. spawn/1은 새로운 프로세스를 만들고, 주어진 함수(loop/0을 이 경우에는)를 그 프로세스에서 실행한다.
  • &loop/0는 loop/0 함수의 참조다. 이는 새로운 프로세스에서 loop 함수를 실행하라는 의미다.

def square(pid, x) do ... end

  • square 함수는 pid (Process ID)와 x를 인자로 받는다. 이 함수는 주어진 pid의 프로세스에 메시지를 보낸다. 메시지는 {self(), :square, x} 형태의 튜플이다. 여기서 self()는 메시지를 보내는 프로세스의 ID이고, :square는 명령어, x는 제곱할 수이다.
  • receive do ... end 블록은 메시지를 받아서 처리한다. {:ok, x} 형태의 메시지를 기다리고, 받으면 x를 반환한다.

def sqrt(pid, x) do ... end

  • sqrt 함수는 제곱근을 계산하기 위해 사용된다. 이 함수는 pid (Process ID)와 x (제곱근을 구하려는 숫자)를 매개변수로 받는다.
  • send(pid, {self(), :sqrt, x}): 이 부분은 pid에게 제곱근을 계산하라는 메시지를 보낸다. 메시지 형태는 {self(), :sqrt, x}이다.
  • receive do ... end: 이 블록은 메시지를 받아 처리한다. 두 가지 경우를 처리하는데, 하나는 계산이 성공적으로 이루어진 경우 {:ok, x}이고, 다른 하나는 오류가 발생한 경우 {:error, reason}이다.

defp loop() do ... end

  • loop 함수는 서버의 메인 루프이다. 여기서 메시지를 받아 해당 동작을 수행한다.
  • 여기서 from은 메시지를 보낸 프로세스 ID(pid)를 나타낸다. Elixir의 receive 블록에서 메시지를 받을 때, 이 메시지는 일반적으로 튜플 형태로 구성되며, 첫 번째 요소는 보통 메시지를 보낸 프로세스의 pid가 된다.
    • 이 코드에서 receive 블록은 {from, :square, x}나 {from, :sqrt, x} 형태의 메시지를 기다리고 있다. 여기서 from은 이 메시지를 보낸 프로세스의 pid를 저장하게 된다. 그리고 나서 send(from, {:ok, x * x}) 또는 send(from, reply)를 통해 계산 결과를 다시 그 프로세스에게 보낸다.
    • 간단한 예시로, 어떤 클라이언트 프로세스가 이 서버에게 제곱근을 계산해달라고 메시지를 보낼 경우, 그 클라이언트 프로세스의 pid가 from에 저장되고, 계산이 끝나면 결과가 그 클라이언트 프로세스에게 다시 보내진다.
  • 첫 번째 receive 블록 ({from, :square, x})은 이전 코드에서 설명한 것과 같다. 제곱을 계산한다.
  • 두 번째 receive 블록 ({from, :sqrt, x})은 새로 추가된 부분이다. 제곱근을 계산하는 로직을 포함한다.
    • if x >= 0, do: {:ok, :math.sqrt(x)}, else: {:error, "square root of negative number"}: 이 부분은 x가 0 이상이면 제곱근을 계산하고, 그렇지 않으면 오류 메시지를 반환한다.
  • 마지막으로 send(from, reply)를 통해 계산 결과나 오류 메시지를 보낸 프로세스에게 반환한다.

전체적인 동작

이렇게 구현하면, ArithmeticServer 모듈은 제곱과 제곱근을 계산하는 두 가지 기능을 제공하게 된다. start 함수를 호출하여 프로세스를 생성한 뒤, 그 프로세스 ID를 사용하여 square나 sqrt 함수를 호출할 수 있다. 결과나 오류 메시지는 호출한 프로세스에게 반환된다.

이 코드에서 square나 sqrt 함수는 사용자나 다른 프로세스가 명시적으로 호출해야 한다. 

ArithmeticServer 모듈은 여기에 나온 함수들을 API로 제공하는데, 이 함수들을 사용하려면 먼저 ArithmeticServer.start()를 호출하여 새로운 서버 프로세스를 생성해야 한다. start() 함수가 호출되면, spawn(&loop/0)이 실행되어 새로운 프로세스가 생성되고 loop 함수를 실행한다. 이 새로운 프로세스는 receive 블록을 통해 메시지를 대기하고 있다.

생성된 서버 프로세스의 pid를 알고 있다면, ArithmeticServer.square(pid, x)나 ArithmeticServer.sqrt(pid, x) 함수를 호출할 수 있다. 여기서 pid는 서버 프로세스의 프로세스 ID이고, x는 제곱이나 제곱근을 구하고 싶은 숫자다.

ArithmeticServer.square(pid, x)를 호출하면, send(pid, {self(), :square, x})가 실행되어 서버 프로세스에게 :square 메시지와 숫자 x를 전달한다.

ArithmeticServer.sqrt(pid, x)를 호출하면, 마찬가지로 send(pid, {self(), :sqrt, x})가 실행되어 :sqrt 메시지와 숫자 x를 전달한다.

이렇게 메시지를 받은 서버 프로세스는 loop 함수 내의 receive 블록에서 해당 메시지를 처리하고, 결과를 다시 보낸다.

정리하면, square나 sqrt 함수는 명시적으로 호출되어야 하며, 이를 위해 먼저 서버 프로세스를 생성하고 그 프로세스의 pid를 알고 있어야 한다.


먼저 ArithmeticServer.start()를 호출해서 새로운 서버 프로세스를 시작한다. 이 함수는 새로운 프로세스 ID(pid)를 반환한다.

반환된 pid를 사용해서 ArithmeticServer.square(pid, x)나 ArithmeticServer.sqrt(pid, x)를 호출한다.

코드 예시는 다음과 같다:


# 서버 프로세스 시작

pid = ArithmeticServer.start()


# 제곱 계산

result_square = ArithmeticServer.square(pid, 4)

IO.puts("The square of 4 is #{result_square}")  # 출력: "The square of 4 is 16"


# 제곱근 계산

result_sqrt = ArithmeticServer.sqrt(pid, 9)

IO.puts("The square root of 9 is #{result_sqrt}")  # 출력: "The square root of 9 is 3.0"


# 음수에 대한 제곱근 계산 (에러 케이스)

ArithmeticServer.sqrt(pid, -9)  # 출력: "There was an error: square root of negative number"


이 예시에서 ArithmeticServer.start()를 호출하여 서버 프로세스를 시작하고 그 pid를 pid 변수에 저장한다. 그 다음에 이 pid를 사용하여 ArithmeticServer.square와 ArithmeticServer.sqrt 함수를 호출한다. 결과값이나 에러 메시지는 콘솔에 출력된다.



defmodule CounterServer do
  def start(n \\ 0) do
    spawn(fn -> loop(n) end)
  end

  def inc(pid) do
    send(pid, :inc)
  end

  def dec(pid) do
    send(pid, :dec)
  end

  def value(pid) do
    send(pid, {:value, self()})
    receive do
      x -> x
    end
  end

  defp loop(n) do
    receive do
      :inc ->
        loop(n + 1)
      :dec ->
        loop(n - 1)
      {:value, from} ->
        send(from, n)
        loop(n)
    end
  end
end

이 코드는 Elixir로 작성된 간단한 카운터 서버를 구현한 것이다. CounterServer 모듈 내에는 여러 함수들이 정의되어 있다.

def start(n \\ 0) do ... end

  • start 함수는 초기 카운터 값을 인자로 받을 수 있으며, 기본값은 0이다. 이 함수는 spawn(fn -> loop(n) end)을 호출하여 새로운 프로세스를 생성한다.
  • 새 프로세스는 loop(n) 함수를 실행한다. 이 함수는 아래에서 더 자세히 설명하겠다.

\ 문법

\\ 0는 함수의 매개변수에 대한 기본값을 설정하는 Elixir의 문법이다. 이것은 n이라는 매개변수에 명시적으로 값이 전달되지 않았을 경우 기본값으로 0을 사용하겠다는 의미이다.

예를 들어, start() 함수를 다음과 같이 두 가지 방식으로 호출할 수 있다:

  1. CounterServer.start(5)와 같이 호출하면 n의 값으로 5가 설정된다.
  2. CounterServer.start()와 같이 호출하면 n의 값으로 기본값인 0이 설정된다.

기본값을 설정하는 이유는 다양할 수 있지만, 주로 다음과 같은 몇 가지 이유 때문에 사용한다.

  1. 유연성: 함수를 호출하는 측에서 모든 매개변수를 명시적으로 제공하지 않아도 되므로 코드가 더 유연해진다.
  2. 명확성: 기본값이 명시되어 있으면 함수를 처음 보는 사람도 이 함수가 어떤 값으로 동작하는지 더 쉽게 이해할 수 있다.
  3. 호환성: 기본값을 제공하면 이전에 이 함수를 사용하던 코드에서 새로운 매개변수가 추가되어도 쉽게 대응할 수 있다.

이 예에서는 카운터의 초기값을 설정할 수 있지만, 초기값이 없을 경우에는 0으로 시작하도록 하고 있다. 이렇게 하면 start 함수를 사용하는 코드가 더 간결하고 유연해진다.

def inc(pid) do ... end

  • inc 함수는 프로세스 ID(pid)를 인자로 받아, 해당 프로세스에 :inc라는 메시지를 보낸다. 이 메시지는 카운터 값을 증가시키라는 명령이다.

def dec(pid) do ... end

  • dec 함수도 프로세스 ID를 인자로 받고, 해당 프로세스에 :dec라는 메시지를 보낸다. 이 메시지는 카운터 값을 감소시키라는 명령이다.

def value(pid) do ... end

  • value 함수는 현재 카운터 값을 가져오는 함수다.
  • 먼저 send(pid, {:value, self()})를 통해 현재 카운터 값을 요청한다. 여기서 self()는 메시지를 보내는 현재 프로세스의 ID다.
  • receive do ... end 블록에서 값을 받아 반환한다.

defp loop(n) do ... end

  • loop 함수는 프라이빗 함수(defp로 정의된)로, 외부에서 호출할 수 없다.
  • 이 함수는 receive do ... end 블록을 통해 메시지를 받고, 그에 따라 동작을 수행한다.
    • :inc 메시지를 받으면, 카운터 값을 1 증가시킨다.
    • :dec 메시지를 받으면, 카운터 값을 1 감소시킨다.
    • {:value, from} 메시지를 받으면, 현재 카운터 값을 from 프로세스에게 보낸다.

전체적인 동작

  1. CounterServer.start()를 호출하여 새 프로세스를 생성한다. 이때 초기 카운터 값을 설정할 수 있다.
  2. 생성된 프로세스 ID를 이용하여 CounterServer.inc(pid)CounterServer.dec(pid)를 호출하여 카운터 값을 조작한다.
  3. CounterServer.value(pid)를 호출하여 현재 카운터 값을 가져온다.

이 모듈은 병렬 처리를 위해 Elixir의 프로세스를 활용하고 있으며, 메시지 패싱을 통해 상태를 관리하고 있다. 이는 Elixir와 Erlang의 Actor 모델을 잘 활용한 예이다.


defmodule RegisteredCounterServer do
  def start(n \\ 0) do
    Process.register(spawn(fn -> loop(n) end), __MODULE__)
  end

  def inc() do
    send(__MODULE__, :inc)
  end

  def dec() do
    send(__MODULE__, :dec)
  end

  def value() do
    send(__MODULE__, {:value, self()})
    receive do
      x -> x
    end
  end

  defp loop(n) do
    receive do
      :inc ->
        loop(n + 1)
      :dec ->
        loop(n - 1)
      {:value, from} ->
        send(from, n)
        loop(n)
    end
  end
end

이 코드는 CounterServer의 변형으로 RegisteredCounterServer라는 모듈 안에 정의되어 있다. 여기서는 프로세스를 모듈 이름(__MODULE__)으로 등록하여 프로세스 ID(pid)를 명시적으로 관리하지 않아도 되게 했다.

def start(n \\ 0) do ... end

  • start 함수는 카운터의 초기값을 설정하며, 기본값은 0이다.
  • spawn(fn -> loop(n) end)을 호출하여 새 프로세스를 생성한다. 이 생성된 프로세스는 loop(n) 함수를 실행한다.
  • 생성된 프로세스는 Process.register를 사용하여 현재 모듈의 이름(__MODULE__)으로 등록된다.

def inc() do ... end / def dec() do ... end

  • incdec 함수는 카운터 값을 1 증가시키거나 감소시킨다.
  • 이 함수들은 send(__MODULE__, :inc)send(__MODULE__, :dec)를 호출하여 메시지를 보낸다. __MODULE__은 현재 모듈의 이름을 나타내므로, 프로세스 ID 대신 모듈 이름을 사용한다.

def value() do ... end

  • value 함수는 현재 카운터 값을 조회한다.
  • send(__MODULE__, {:value, self()})를 호출하여 현재 값을 요청하고, receive do ... end를 통해 값을 받아 반환한다.

defp loop(n) do ... end

  • loop는 이전과 동일하게 메시지를 받아 카운터 값을 관리한다.

전체적인 동작

  1. RegisteredCounterServer.start()를 호출하여 새 프로세스를 생성하고, 이 프로세스를 모듈 이름으로 등록한다.
  2. RegisteredCounterServer.inc()RegisteredCounterServer.dec()를 호출하여 카운터 값을 조작한다. 프로세스 ID 대신 모듈 이름을 사용한다.
  3. RegisteredCounterServer.value()를 호출하여 현재 카운터 값을 조회한다.

이렇게 하면, 프로세스 ID를 명시적으로 관리하지 않고도 프로세스 간의 메시지 패싱을 할 수 있어 코드가 더 간결하고 관리하기 쉬워진다.


defmodule UniversalServer do
  def start() do
    spawn(&become/0)
  end

  defp become() do
    receive do
      {:become, f} ->
        f.()
    end
  end

  def fact_server() do
    receive do
      {from, x} -> send(from, fact(x))
    end
    fact_server()
  end

  def fact(0), do: 1
  def fact(n), do: n * fact(n - 1)
end

이 코드는 UniversalServer라는 모듈 안에 여러 함수가 정의되어 있어서 여러 가지 동작을 수행할 수 있는 유니버설 서버를 만들고 있다. 코드를 각 부분으로 나누어 분석해보겠다.

def start() do ... end

  • start 함수는 spawn(&become/0)를 호출해서 새로운 프로세스를 생성한다. 이 프로세스는 become 함수를 실행하게 된다.

defp become() do ... end

  • become 함수는 메시지를 하나 받고, 그 메시지가 {:become, f} 형식이면 f 함수를 호출한다. 이 함수는 어떤 함수도 될 수 있다.
  • 이러한 방식으로 UniversalServer는 동적으로 행동을 바꿀 수 있다. 즉, 메시지에 따라 어떤 로직을 실행할지 바꿀 수 있다.

def fact_server() do ... end

  • fact_server 함수는 메시지를 받아 팩토리얼을 계산하고 결과를 보내는 역할을 한다.
  • receive 블록 내에서 {from, x} 형태의 메시지를 받고, fact(x)를 호출하여 결과를 계산한 뒤, 이를 from에게 보낸다.

def fact(0), do: 1 / def fact(n), do: n * fact(n - 1)

  • 이 두 함수는 팩토리얼을 계산하는 로직이다. fact(0)은 1을 반환하고, fact(n)은 재귀적으로 팩토리얼을 계산한다.

전체적인 동작

  1. UniversalServer.start()를 호출하면 become 함수가 실행되는 새 프로세스가 생성된다.
  2. 생성된 프로세스는 {:become, f} 형태의 메시지를 받을 준비가 되고, 이 메시지를 받으면 f 함수를 실행한다.
  3. fact_server 같은 함수를 f로 전달하면, 서버는 팩토리얼 계산 로직으로 바뀐다.

이렇게 하면 서버의 로직을 동적으로 바꿀 수 있다. 예를 들어, 다른 함수로 행동을 바꾸고 싶다면 {:become, 다른함수} 형태의 메시지를 보내면 된다.


이렇게 markdown으로 처음 포스팅을 작성해 봤는데, 효율이 엄청나게 올라감을 느낀다. 왜 진작에 이런 생각을 하지 못했을까? 라는 후회가 들 정도로 상당히 쾌적하고 만족스럽다.

역시나 사람은 뭔가 불편함이 있어야 그것을 개선하려는 노력을 하고, 또 그 노력이 올바른(혹은 효울적인) 방향과 맞아 떨어질 때 이렇게 또 한번의 도약이 일어나게 된다.

만약, 내가 이런 불편함을 끝까지 감수하고 묻어두면서 포스팅을 계속 해왔더라면, 아마도 스트레스를 많이 받아서 금방 지쳤을 것이다.

내가 고민이 되는 문제들은, 나보다 앞서서 길을 걸어간 누군가가 이미 고민을 했을 확률이 크다. 그리고 그 고민을 또 누군가가 해결을 했을 가능성도 상당히 높다. 따라서 불편함이 생기거든 이렇게 검색하고 또 적용하는 습관을 들이도록 해야겠다.

댓글 없음:

댓글 쓰기

관련 포스팅