오늘은 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}
그렇다면 이제 진짜로 각각의 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값을 전달 해줍니다. 여기서 나오는 Timeout과 hibernate는 아까 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 |
---|