GenServer(범용 서버)는 Elixir 프로세스 간의 흔한 클라이언트-서버 상호작용을 추상화한 behaviour예요.
프로세스를 배웠을 때의 receive 루프를 기억하나요? GenServer behaviour는 이런 루프를 구현하고, 그런 루프를 실행하는 프로세스와 메시지를 주고받기 위한 추상화를 제공해요. 덕분에 상태를 유지하고 비동기 코드를 실행하기가 더 쉬워져요.
GenServer라는 이름은 여러 뜻으로 쓰인다는 점을 알아두세요. GenServer behaviour를 사용하는 _모듈_을 가리킬 때도 쓰이고, GenServer behaviour를 사용하는 모듈에서 시작된 _프로세스_를 가리킬 때도 쓰여요.
GenServer behaviour는 필수 콜백 하나인 init/1과, 몇 가지 흥미로운 선택적 콜백인 handle_call/3, handle_cast/2, handle_info/3을 정의해요. GenServer를 사용하는 _클라이언트_는 그 콜백들을 직접 호출하지 않아요. 대신 GenServer 모듈이 클라이언트가 GenServer 프로세스와 통신할 때 쓸 수 있는 함수들을 제공해요.
흔히 하나의 모듈이 _클라이언트 API_와 _서버 콜백 구현_을 모두 정의해요. 클라이언트 API는 Elixir 앱의 다른 부분에서 이 GenServer 프로세스와 통신하려고 호출할 수 있는 함수들의 모음이고, 서버 콜백 구현은 이 GenServer의 로직을 담고 있어요.
먼저 GenServer의 간단한 예제를 살펴보고, 그다음 각 콜백이 무슨 뜻인지 알아봐요.
이건 긴 자동차 여행 중에 짜증 나는 동승자가 반복해서 묻는 질문, 그러니까 "다 왔어?"라는 질문에 응답할 수 있는 예제 서버예요. 이 서버는 이 질문이 몇 번이나 나왔는지 세고, 갈수록 짜증 섞인 응답을 돌려줘요.
defmodule AnnoyingPassengerAutoresponder do
use GenServer
# Client API
def start_link(init_arg) do
GenServer.start_link(__MODULE__, init_arg)
end
def are_we_there_yet?(pid) do
GenServer.call(pid, :are_we_there_yet?)
end
# Server callbacks
@impl GenServer
def init(_init_arg) do
# the initial count of questions asked is always 0
state = 0
{:ok, state}
end
@impl GenServer
def handle_call(:are_we_there_yet?, _from, state) do
reply =
cond do
state <= 3 -> "No."
state <= 10 -> "I told you #{state} times already. No."
true -> "..."
end
# increase the count of questions asked
new_state = state + 1
# reply to the caller
{:reply, reply, new_state}
end
end
init/1서버는 GenServer.start/3 또는 GenServer.start_link/3을 호출해서 시작할 수 있어요. 이 두 함수의 차이는 링크 개념에서 배웠어요.
이 두 함수는 다음과 같아요:
GenServer behaviour를 구현한 모듈을 받아요.init_arg라고 부르는 아무 값이나 받아요. 이름에서 알 수 있듯이, 이 인자는 init/1 콜백으로 전달돼요.GenServer.start/3 또는 GenServer.start_link/3을 호출해서 서버를 시작하면 init/1 콜백이 블로킹 방식으로 호출돼요. init/1의 반환값이 서버를 성공적으로 시작할 수 있는지를 결정해요.
init/1 콜백은 보통 다음 값 중 하나를 반환해요:
{:ok, state}. 서버는 state를 초기 상태로 삼아 receive 루프를 시작해요. state는 어떤 타입이든 될 수 있어요.{:stop, reason}. reason은 어떤 타입이든 될 수 있어요. 서버는 receive 루프를 시작하지 않아요. 프로세스는 주어진 이유와 함께 종료돼요.지금 다루지 않는 더 고급 방법도 있어요.
서버의 receive 루프가 시작되면 GenServer.start/3와 GenServer.start_link/3 함수는 {:ok, pid} 튜플을 반환해요. 그렇지 않으면 {:error, reason}을 반환해요
handle_call/3응답이 필요한 메시지는 GenServer.call/2로 서버 프로세스에 보낼 수 있어요. 이 함수는 첫 번째 인자로 실행 중인 서버 프로세스의 pid를, 두 번째 인자로 메시지를 받아요. 메시지는 어떤 타입이든 될 수 있어요.
handle_call/3 콜백은 동기 메시지를 처리하고 응답하는 역할을 해요. 세 개의 인자를 받아요:
message - GenServer.call/2에 두 번째 인자로 전달된 값이에요.from - GenServer.call/2를 호출한 프로세스의 pid예요. 대부분의 경우 이 인자는 무시할 수 있어요.state - 서버의 현재 상태예요. 초깃값은 init/1 콜백에서 설정된다는 걸 기억해요.handle_call/3 콜백은 보통 {:reply, reply, state}라는 3-튜플을 반환해요. 이 말은 튜플의 두 번째 요소인 reply가 호출한 쪽으로 다시 전송된다는 뜻이에요. reply는 어떤 타입이든 될 수 있어요. 튜플의 세 번째 요소인 state는 이 메시지를 처리한 뒤의 서버의 새 상태예요.
지금 다루지 않는 더 고급 방법도 있어요.
이 콜백이 하는 일을 이름으로 기억하려면, 누군가에게 전화를 "거는" 것이라고 생각해요.
그 사람이 통화 가능하면, 곧바로 (동기적으로) 응답을 받아요.
handle_cast/2응답이 필요 없는 메시지는 GenServer.cast/2로 서버 프로세스에 보낼 수 있어요. 인자는 GenServer.call/2와 똑같아요.
handle_cast/2 콜백은 이런 메시지를 처리하는 역할을 해요. message와 state 두 인자를 받는데, handle_call/3 콜백과 같은 인자예요 (from만 빼고요).
handle_cast/2 콜백은 보통 {:noreply, state}라는 2-튜플을 반환해요.
지금 다루지 않는 더 고급 방법도 있어요.
이 콜백이 하는 일을 이름으로 기억하려면, "cast"가 "던지다"라는 뜻도 있다는 걸 떠올려요.
바다에 편지병을 던지면, 곧바로 답장을 받기를 기대하지 않아요. 어쩌면 영영 못 받을지도 몰라요.
call을 쓸까요, cast를 쓸까요?클라이언트 코드가 서버의 응답을 필요로 하지 않더라도 거의 항상 call을 써요.
call을 쓰면 응답을 기다리게 되는데, 이게 백프레셔(backpressure) 역할을 해요 (클라이언트가 한꺼번에 너무 많은 메시지를 보내는 걸 막아줘요). 서버로부터 응답을 받는 것은 서버가 클라이언트의 메시지를 받아 처리했다는 걸 확신할 수 있는 유일한 방법이기도 해요.
handle_info/2메시지는 GenServer.call/2나 GenServer.cast/2를 호출하는 것 말고도 다른 방식으로 서버의 메일박스에 도착할 수 있어요. 예를 들어 그냥 send/2 함수를 호출하는 경우예요.
이런 메시지를 처리하려면 handle_info/2 콜백을 써요. 이 콜백은 handle_cast/2와 정확히 같은 방식으로 동작해요.
GenServer behaviour는 예상치 못한 메시지에 대한 오류를 로그로 남기는 handle_info/2의 포괄적인 구현을 제공해요. 그 기본 구현을 재정의한다면, 항상 자신만의 포괄적인 구현을 포함하도록 해요. 잊어버리면, 서버가 예상치 못한 메시지를 받았을 때 크래시가 나요.
위에서 설명한 네 콜백의 반환값은 타임아웃이라는 튜플 요소를 하나 더 붙여 확장할 수 있어요. 예를 들어 init/1에서 {:ok, state} 대신 {:ok, state, timeout}을 반환할 수 있어요.
타임아웃은 특정 기간 동안 메일박스에 메시지가 없는 상황을 감지하는 데 쓸 수 있어요. 서버가 콜백 중 하나에서 타임아웃을 반환했는데, 지정한 밀리초가 지날 때까지 메시지가 도착하지 않으면, handle_info/2가 첫 번째 인자로 :timeout을 받으며 호출돼요.