본문 바로가기

01. Concurrent Programming/OTP

OTP의 기본 gen_server

오늘은 OTP의 가장 기초적인 gen_server에 대해 한번 알아봅시다. gen_server는 모든 OTP server의 기본이 되는 것이라서 이것만 잘 알아도 erlang Code를 알아보는데 큰 도움이 됩니다.

먼저 우리가 gen_server로 무엇을 하고 싶은 지부터 생각해봅시다. 저번의 게시물을 리뷰 해봅시다. 

여기서 보듯이 우리는 “서버를 켜고 → Request를 받아 그것을 처리하고 할 일을 다 했으면 서버를 내리고” 싶습니다. 매우 간단한 원리 입니다.(사실 여기서 요청을 하는 것을 처리하는 게 복잡한거지! 라고 하면 할말이 없긴 합니다.) 어찌 되었든 이러한 간단한 생각을 바탕으로 만들어진 것이 gen_server입니다.

그럼 "서버를 켜는 루틴, Request를 받아 그것을 처리하는 루틴, 할일을 다했으면 서버를 내리는 루틴"이 담긴, gen_server의 기본적인 behavior를 알아봅시다. behavior는 Java의 callback interface라고 일단 알아두시면 됩니다.(behavior와 callback은 다음에 자세히 알아볼 것입니다.)

%%%-------------------------------------------------------------------
%%% @author ktz
%%% @copyright (C) 2016, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 02. 4월 2016 오전 11:45
%%%-------------------------------------------------------------------
-module(helloworld).
-author("ktz").

-behaviour(gen_server).

%% API
-export([start_link/0]).

%% gen_server callbacks
-export([init/1,
  handle_call/3,
  handle_cast/2,
  handle_info/2,
  terminate/2,
  code_change/3]).

-define(SERVER, ?MODULE).

-record(state, {}).

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @end
%%--------------------------------------------------------------------
-spec(start_link() ->
  {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
  gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
-spec(init(Args :: term()) ->
  {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term()} | ignore).
init([]) ->
  {ok, #state{}}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
    State :: #state{}) ->
  {reply, Reply :: term(), NewState :: #state{}} |
  {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
  {noreply, NewState :: #state{}} |
  {noreply, NewState :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
  {stop, Reason :: term(), NewState :: #state{}}).
handle_call(_Request, _From, State) ->
  {reply, ok, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_cast(Request :: term(), State :: #state{}) ->
  {noreply, NewState :: #state{}} |
  {noreply, NewState :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Request, State) ->
  {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
  {noreply, NewState :: #state{}} |
  {noreply, NewState :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term(), NewState :: #state{}}).
handle_info(_Info, State) ->
  {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
    State :: #state{}) -> term()).
terminate(_Reason, _State) ->
  ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
    Extra :: term()) ->
  {ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================

위의 Code는 Intellij에서 gen_server Template으로 생성한 결과입니다. 여기서 gen_server callback이라는 Code를 보면 다음과 같은 function들이 있습니다.

%% gen_server callbacks
-export([init/1,
  handle_call/3,
  handle_cast/2,
  handle_info/2,
  terminate/2,
  code_change/3]).

이 Function들은 각각 Server를 시작하고,(init/1) Server가 받은 Request를 처리하고,(handle_call/3, handle_cast/2, handle_info/2) 할일이 끝나 서버를 내리는(terminate/2)루틴을 기입할 수 있는 곳입니다. 물론 Runtime에 Code Change를 할 수 있는 Function(code_change/3) 또한 갖고 있습니다.(Runtime상의 Code Change는 나중에 한번 알아봅시다.) 

다만, 이러한 Function들의 기입이 Mandatory한것은 아닙니다. 만약에 여러분이 handle_call/3과 handle_info/2가 필요없다고 생각하여, 쓰지 않았다고 하면 다음과 같이 나타납니다.


1> c(helloworld).

helloworld.erl:12: Warning: undefined callback function handle_call/3 (behaviour 'gen_server')

helloworld.erl:12: Warning: undefined callback function handle_info/2 (behaviour 'gen_server')

{ok,helloworld}


이렇게, warning은 주지만 compile은 됩니다. 그러나 이때, gen_server:call(helloworld. {atom}).을 요청할 경우 구현이 안되었기 때문에 error가 납니다.

2> helloworld:start_link().
3> gen_server:call(helloworld, hi).  

=ERROR REPORT==== 2-Apr-2016::12:09:35 ===
** Generic server helloworld terminating 
** Last message in was hi
** When Server state == {state}
** Reason for termination == 
** {'function not exported',
       [{helloworld,handle_call,[hi,{<0.34.0>,#Ref<0.0.7.102>},{state}],[]},
        {gen_server,try_handle_call,4,[{file,"gen_server.erl"},{line,629}]},
        {gen_server,handle_msg,5,[{file,"gen_server.erl"},{line,661}]},
        {proc_lib,init_p_do_apply,3,[{file,"proc_lib.erl"},{line,240}]}]}
** exception exit: undef
     in function  helloworld:handle_call/3
        called as helloworld:handle_call(hi,{<0.34.0>,#Ref<0.0.7.102>},{state})
     in call from gen_server:try_handle_call/4 (gen_server.erl, line 629)
     in call from gen_server:handle_msg/5 (gen_server.erl, line 661)
     in call from proc_lib:init_p_do_apply/3 (proc_lib.erl, line 240)

그렇다면 이제 진짜로 각각의 callback Function이 어떠한 일을 하는지 알아봅시다.


Init/1

 init/1은 서버가 시작할 때 사용자가 Code를 넣는 Function 입니다. 여기서 전달되는 Parameter는 gen_server:start/3,4 나, gen_server:start_link/3,4에서 Passing 되는 Argument를 받아온 것입니다. 이러한 Argument를 이용하여 초기화를 합니다.

return 값은 다음과 같을 수 있습니다.

{ok,State} | {ok,State,Timeout} | {ok,State,hibernate} | {stop,Reason} | ignore

 여기서 ok는 정상적인 리턴 값을 의미 합니다. 현재 초기화가 정상적으로 이루어 졌고 끝났다고 gen_server에게 알려주는 역할을 합니다. stop 이나, ignore은 무언가 초기화가 비정상적으로 끝나, 실패했다는 것을 의미합니다. 여기서 Timeout은 몇초 후 안에 gen_server에 Request가 오지 않으면, handle_info/2로 timeout atom message가 가게 됩니다. hibernate의 경우, Timeout을 infinity로 설정하는 대신, hibernate로 설정하여 call stack을 지울고, erlang process로 하여금 full sweep을 수행하게 합니다. State는 현재 gen_server가 저장하고 있을 상태 정보입니다.


terminate/2

 terminate는 Server가 할일을 끝내거나, 문제가 생겨서 중단할 때, 불리우는 Code Routine입니다. 끝내는 Code이므로, 사실상 리턴값이 의미가 없습니다. 대신 전달되는 Parameter는 다음과 같습니다.

Reason = normal | shutdown | {shutdown,term()} | term()

State = term()

여기서 normal은 정상 종료의 경우를 이야기 합니다. shutdown은 비정상 종료인데, term은 비정상 종료의 이유를 나타냅니다. State는 gen_server가 저장하고 있는 상태정보입니다.


handle_call/3

Client 입장에서 어떠한 요청을 하고, 그 후, 그 요청에 대한 응답을 받을 필요가 있는 경우가 생깁니다. Client가 gen_server:call/3 | 4로 요청됩니다. 이럴때 사용하는 Routine Code 입니다. Function에 들어가는 Parameter는 다음과 같습니다.

Request, From, State


여기서 Request는 사용자가 요청하는 Request의 종류입니다. 보통 Tuple을 Request로 전달합니다. {term(), 전달할 Data} 형식으로 전달합니다. From은 요청을 한 caller의 pid가 들어 있습니다. 마지막으로 State는 gen_server가 갖고 있는 상태값입니다.

리턴 값은 다음과 같습니다.


{reply,Reply,NewState} | {reply,Reply,NewState,Timeout}
 | {reply,Reply,NewState,hibernate}
 | {noreply,NewState} | {noreply,NewState,Timeout}
 | {noreply,NewState,hibernate}
 | {stop,Reason,Reply,NewState} | {stop,Reason,NewState}


reply를 리턴할 경우 caller에게 Reply값을 전달 해줍니다. 여기서 나오는 Timeouthibernate는 아까 init/1에서 나온 것과 같습니다. noreply의 경우는 우리가 예측할 수 있는데로, caller에 답장을 해주지 않습니다. stop의 경우,  Reason의 이유와 함께 Server를 종료 시킵니다.

이제 이 callback function을 부를 수 있는 gen_server의 function, gen_server:call/3 | 4를 살펴 봅니다. Parameter는 다음과 같습니다.


ServerRef, Request, Timeout


ServerRef는 요청을 할 gen_server의 이름을 전달합니다. Request는 아까 언급했던, {term(), 전달할 Data} 형식의 요청을 말합니다. Timeout은 리턴값을 기다릴 시간을 전달합니다, Millisecond이며, 그 후가 지나면, timeout을 발생시킵니다. 만약 전달하지 않으면 server를 시작했을 때의 timeout시간을 사용합니디. Default 값은 5초입니다.


gen_server:reply/2

handle_call은 리턴값이 필요한데, noreply가 있는 이유가 궁금하실 수도 있습니다. 만약 루틴 중간에 Client에게 답을 주고 더 Code를 실행하고 싶을경우, gen_server:reply를 이용하여 먼저 답을 하고, 계속 Code를 이어 나갈 수 있습니다. 그렇게 될 경우, 그전에 Client에게 답장을 했으므로, 답장을 할 필요가 없으므로, noreply를 사용합니다.


handle_cast/2

handle_call과 다르게 어떤 Client의 Call은 리턴값을 필요로 하지 않을 수 있습니다. 이렇게 될 경우는 gen_server:cast를 이용해서 Server에 Call을 합니다. Function에 들어가는 것은 handle_call과 같지만, 다시 리턴을 할 필요가 없기 때문에,  From이 들어가 있지 않습니다. 리턴값은 handle_call과 다르게 다시 Client에게 리턴을 할 필요가 없기 때문에 reply가 없습니다.


이제 예제를 보면서 한번 적용을 해봅시다.


%%%-------------------------------------------------------------------
%%% @author ktz
%%% @copyright (C) 2016, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 01. 4월 2016 오전 10:47
%%%-------------------------------------------------------------------
-module(ping).
-author("ktz").

-behaviour(gen_server).

%% API
-export([start_link/0]).

%% gen_server callbacks
-export([init/1,
  handle_call/3,
  handle_cast/2,
  handle_info/2,
  terminate/2,
  code_change/3]).

-define(SERVER, ?MODULE).

-define(TIMEOUT, 5000).

-record(state, {}).

%%%===================================================================
%%% API
%%%===================================================================

%%--------------------------------------------------------------------
%% @doc
%% Starts the server
%%
%% @end
%%--------------------------------------------------------------------
-spec(start_link() ->
  {ok, Pid :: pid()} | ignore | {error, Reason :: term()}).
start_link() ->
  gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Initializes the server
%%
%% @spec init(Args) -> {ok, State} |
%%                     {ok, State, Timeout} |
%%                     ignore |
%%                     {stop, Reason}
%% @end
%%--------------------------------------------------------------------
-spec(init(Args :: term()) ->
  {ok, State :: #state{}} | {ok, State :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term()} | ignore).
init([]) ->
  {ok, null, ?TIMEOUT}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling call messages
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_call(Request :: term(), From :: {pid(), Tag :: term()},
    State :: #state{}) ->
  {reply, Reply :: term(), NewState :: #state{}} |
  {reply, Reply :: term(), NewState :: #state{}, timeout() | hibernate} |
  {noreply, NewState :: #state{}} |
  {noreply, NewState :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term(), Reply :: term(), NewState :: #state{}} |
  {stop, Reason :: term(), NewState :: #state{}}).
handle_call(start, _From, LoopData) ->
  {reply, started, LoopData, ?TIMEOUT};
handle_call(pause, _From, LoopData) ->
  {reply, pause, LoopData};
handle_call(_Request, _From, State) ->
  {reply, ok, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling cast messages
%%
%% @end
%%--------------------------------------------------------------------
-spec(handle_cast(Request :: term(), State :: #state{}) ->
  {noreply, NewState :: #state{}} |
  {noreply, NewState :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term(), NewState :: #state{}}).
handle_cast(_Request, State) ->
  {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Handling all non call/cast messages
%%
%% @spec handle_info(Info, State) -> {noreply, State} |
%%                                   {noreply, State, Timeout} |
%%                                   {stop, Reason, State}
%% @end
%%--------------------------------------------------------------------
-spec(handle_info(Info :: timeout() | term(), State :: #state{}) ->
  {noreply, NewState :: #state{}} |
  {noreply, NewState :: #state{}, timeout() | hibernate} |
  {stop, Reason :: term(), NewState :: #state{}}).
handle_info(timeout, LoopData) ->
  {_Hour, _Min, Sec} = time(),
  io:format("~2.w~n", [Sec]),
  {noreply, LoopData, ?TIMEOUT};
handle_info(_Info, State) ->
  {noreply, State}.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any
%% necessary cleaning up. When it returns, the gen_server terminates
%% with Reason. The return value is ignored.
%%
%% @spec terminate(Reason, State) -> void()
%% @end
%%--------------------------------------------------------------------
-spec(terminate(Reason :: (normal | shutdown | {shutdown, term()} | term()),
    State :: #state{}) -> term()).
terminate(_Reason, _State) ->
  ok.

%%--------------------------------------------------------------------
%% @private
%% @doc
%% Convert process state when code is changed
%%
%% @spec code_change(OldVsn, State, Extra) -> {ok, NewState}
%% @end
%%--------------------------------------------------------------------
-spec(code_change(OldVsn :: term() | {down, term()}, State :: #state{},
    Extra :: term()) ->
  {ok, NewState :: #state{}} | {error, Reason :: term()}).
code_change(_OldVsn, State, _Extra) ->
  {ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================


1. 먼저, start_link/0를 이용하여 gen_server를 실행 시킵니다.

2. 그, 후 init/1이 실행이 되어, {ok, null, ?TIMEOUT}을 리턴합니다. 이때, Timeout 값을 넣었으므로, 그 동안에 아무런 메시지가 오지 않으면 Timeout이 걸리게 됩니다. 이것은 handle_info에 timeout이라는 atom()을 보내게 됩니다.

3. timer가 Timeout이 걸려서 handle_info(timeout, LoopData)에 패턴 매칭이 되게 되면, 초를 출력하게 되고, 다시 Timeout을 걸면서 리턴합니다.

4. 만약 Client가 gen_server:call(ping, pause)를 부르게 되면, Timer가 꺼집니다.

5. 그 후, 만약 Client가 gen_server:call(ping, start)를 부르게 되면 다시 Timer가 켜지게 됩니다.

6. 만약 Client가 exit(ping)을 부르게 되면 ping module은 terminate/2 Routine을 타게 되면서 꺼지게 됩니다.


예제 실행을 보면 다음과 같습니다.


14> c(ping).

{ok,ping}

15> ping:start_link().

{ok,<0.70.0>}

55  

 0  

 5  

10                    

15                    

16> gen_server:call(ping,pause).

pause

17> gen_server:call(ping, start).

started

31  

36  

18> exit(ping).

** exception exit: ping

19> gen_server:call(ping, start).

** exception exit: {noproc,{gen_server,call,[ping,start]}}

     in function  gen_server:call/2 (gen_server.erl, line 204)


다음은 Designing for Scalability with Erlang-OTP에서 발췌한 client call과 작동하는 callback function들입니다. 매우 유용해서 같이 삽입합니다.



아... 엄청 길었네요... gen_fsm도 이렇게 길텐데.. 큰일입니다.... 보니까 나중에 많이 고쳐야 할것 같습니다. 용두사미의 글이 되었어요..

'01. Concurrent Programming > OTP' 카테고리의 다른 글

erlang OTP란  (0) 2016.03.29