erlang 공부 : Event handlers
- 프로그래밍 언어/erlang
- 2023. 12. 26.
참고
1. Event Handlers
이전 글(https://ojt90902.tistory.com/1672)에서 작성했던 Pub - Sub 모델은 특정 이벤트가 발생했을 때, EventServer가 구독자에게 이벤트를 전송해준다. 이벤트를 받은 구독자들이 그 이벤트에 대한 각각의 작업을 진행한다. 이 방식은 각 구독자 프로세스가 이벤트를 따로 처리하기 때문에 '처리하는데 오랜 시간이 걸리는 함수'를 실행하는 경우에는 유용하다. 그러나 간단한 작업일 경우, 이벤트 구독을 기다리는 프로세스에 CPU 스케쥴러를 소모하기 때문에 컴퓨팅 파워를 효율적으로 사용하지 못할 수 있다.
이벤트마다 처리해야 할 작업이 짧은 경우라면 EventManager에서 EventHandler를 가지고 있도록 하고, Event가 수신된 경우 EventHandler로 처리해주는 것이 효율적일 수 있다. 이 경우, 구독자 모델에서처럼 여러 프로세스를 만들지 않아도 되기 때문에 컴퓨팅 파워를 좀 더 효율적으로 사용할 수 있다.
위의 그림이 그 예시다.
- EventManager는 EventHandler인 f(), y(), g()를 가지고 있음.
- 이벤트가 EventManager에게 전달되면 f(Event), y(Event), g(Event)를 수행함.
- f(), y(), g()는 EventManager와 같은 프로세스에서 동작함.
1.1 EventManager 모델의 장단점
EventManager 모델의 장단점을 살펴보면 다음과 같다.
- 장점
- 서버에 많은 구독자가 있는 경우, 이벤트를 단 한번만 EventManager에 전달하면 됨.
- 전송할 데이터가 많은 경우 한번만 수행하면, 모든 콜백(EventHandler)가 동일한 데이터 인스턴스로 동작함.
- 수명이 짧은 여러 프로세스를 생성할 필요 없음. (Pub - Sub 형식에서 발생할..)
- 단점
- EventHandler 실행 시간이 오래 소요된다면, 하나의 EventHandler가 다른 EventHandler의 실행을 block할 수 있음.
- 특정 EventHandler가 무한히 loop를 도는 경우, EventManager에 Crash가 발생할 때 까지 어떠한 이벤트도 Accept 하지 않음.
마지막 단점은 꽤 치명적인 부분이다. 따라서 일반적으로는 Pub - Sub 구조로 접근하는 것이 좋다.
2. Generic Event Handler
gen_event 모듈을 이용해 EventManager 구조를 손쉽게 구현할 수 있다. EventManager 구조는 다음과 같이 동작한다.
- gen_event:start_link()로 생성된 프로세스는 add_handler()를 수락하는 동작을 함.
- gen_event:start_link()로 생성된 프로세스는 Event를 받아들이고, EventHandler들을 순차적으로 호출하는 동작을 함.
큰 흐름은 다음과 같다.
- gen_event:start_link()를 호출하면 EventManager 프로세스가 생성된다. (Spawn event Manager)
- gen_event:add_handler()를 호출하면 EventManager에게 Handler가 추가된다. (attach handler)
- EventManager는 loop를 돌며 gen_event:notify() / gen_event:call() 호출 메세지를 기다리고, 메세지를 받으면 eventHandler로 처리해줌.
EventHandler는 사이클은 다음과 같다.
- EventHandler는 add_handler()가 호출되었을 때, eventManager에 추가되기 위해 init() 메서드가 호출된다.
- EventManager는 이벤트가 전달되었을 때, EventHandler 모듈의 handle_event()를 호출한다.
- EventHandler는 제 각각 State를 가질 수 있다. (EventManager와 별개의 State)
2.1 gen_event의 callback 인터페이스
gen_event를 사용하기 위해서는 gen_event behaviour의 callback 인터페이스를 구현해야한다. 구현해야하는 콜백은 다음과 같다.
- init()
- terminate()
- handle_event()
- handle_call()
- handle_info()
- code_change()
두 가지 콜백을 조금 살펴보면 다음과 같다.
handle_event()
- gen_server:handle_cast()처럼 비동기로 동작함.
- EventHandler와 EventManager는 동일한 프로세스에서 동작함.
- gen_event:notify()를 통해 전달되는 메세지를 handle_event()에서 처리함.
handle_call()
- gen_server:handle_call()과 유사함.
- gen_event:call()로 전달되는 메세지를 처리함.
- 동기식으로 동작함.
3. 코드 작성 - It's Curling Time!
gen_event 모듈을 이용한 간단한 코드를 작성해본다.
3.1 전광판 하드웨어 모듈 작성
-module(curling_scoreboard_hw).
%% API
-export([set_teams/2, next_round/0, add_point/1, reset_board/0]).
set_teams(TeamA, TeamB) ->
io:format("Scoreboard: Team ~s vs. Team ~s~n", [TeamA, TeamB]).
next_round() ->
io:format("Scoreboard: round over~n").
add_point(Team) ->
io:format("Scoreboard: increased score of team ~s by 1~n", [Team]).
reset_board() ->
io:format("Scoreboard: All teams are undefined and all scores ar 0~n").
- 위 코드는 전광판 하드웨어 모듈 코드다.
- 전광판 하드웨어 모듈은 팀을 추가하고, 다음 라운드로 넘어가고, 점수를 더하는 등의 기능을 제공한다.
3.2 전광판 모듈 작성
-module(curling_scoreboard).
-author("ojt90").
-behaviour(gen_event).
%% API
-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, terminate/2]).
init([]) ->
{ok, []}.
handle_event({set_teams, TeamA, TeamB}, State) ->
curling_scoreboard_hw:set_teams(TeamA, TeamB),
{ok, State};
handle_event({add_point, Team, N}, State) ->
[curling_scoreboard_hw:add_point(Team) || _ <- lists:seq(1, N)],
{ok, State};
handle_event(next_round, State) ->
curling_scoreboard_hw:next_round(),
{ok, State};
handle_event(_, State) ->
{ok, State}.
handle_call(_, State) ->
{ok, ok, State}.
handle_info(_, State) ->
{ok, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
terminate(_Reason, _State) ->
ok.
- 다음은 전광판 모듈을 작성한 코드다.
- gen_event behaviour를 구현한 코드다. 이 코드는 EventHandler로 동작한다.
- handle_event()는 gen_event:notify()로 전달된 메세지를 처리한다.
- set_teams
- add_points
- next_round
- 위 작업을 진행한다.
EventHandler 코드 살펴보기
- EventManager를 gen_event:notify()로 호출하면 EventManager는 server_notify()를 호출한다.
- server_notify()는 재귀적으로 Handler를 실행한다.
- [Handler | T]를 보건데 Handler가 링크드리스트에 연결되어서 하나씩 실행되고 꼬리재귀로 하나씩 호출되는 것을 알 수 있음.
- Handler 자체는 server_update()에서 실행됨.
- server_update는 각 EventHandler를 실행함. Mod1:Func로 실행.
- 실행 후, Mod1:Func가 반환하는 State1는 #handler{state = State1}를 통해서 State가 유지되는 것을 확인할 수 있음.
3.3 curling_score_board 추상화 하기
1> c(curling_scoreboard_hw).
{ok,curling_scoreboard_hw}
2> c(curling_scoreboard).
{ok,curling_scoreboard}
3> {ok, Pid} = gen_event:start_link().
{ok,<0.43.0>}
4> gen_event:add_handler(Pid, curling_scoreboard, []).
ok
5> gen_event:notify(Pid, {set_teams, "Pirates", "Scotsmen"}).
Scoreboard: Team Pirates vs. Team Scotsmen
ok
6> gen_event:notify(Pid, {add_points, "Pirates", 3}).
ok
Scoreboard: increased score of team Pirates by 1
Scoreboard: increased score of team Pirates by 1
Scoreboard: increased score of team Pirates by 1
7> gen_event:notify(Pid, next_round).
Scoreboard: round over
ok
8> gen_event:delete_handler(Pid, curling_scoreboard, turn_off).
ok
9> gen_event:notify(Pid, next_round).
ok
현재 작성된 curling_score_board를 이용해 동작하기 위해서는 얼랭 쉘에서 다음과 같이 실행해야한다. 그런데 아래처럼 각 모듈을 직접 호출하는 방식은 추후 확장하는데 단점으로 다가온다. 따라서 내부적인 프로토콜을 추상화를 통해서 감추어야 한다.
-module(curling).
-author("ojt90").
%% API
-export([start_link/2, set_teams/3, add_points/3, next_round/1, join_feed/2, leave_feed/2, game_info/1]).
start_link(TeamA, TeamB) ->
{ok, Pid} = gen_event:start_link(),
gen_event:add_handler(Pid, curling_scoreboard, []),
gen_event:add_handler(Pid, curling_accumulator, []),
set_teams(Pid, TeamA, TeamB),
{ok, Pid}.
set_teams(Pid, TeamA, TeamB) ->
gen_event:notify(Pid, {set_teams, TeamA, TeamB}).
add_points(Pid, Team, N) ->
gen_event:notify(Pid, {add_points, Team, N}).
next_round(Pid) ->
gen_event:notify(Pid, next_round).
위 코드를 이용해서 각 프로세스를 호출하는 부분을 추상화시켜 프로토콜을 내부로 감출 수 있다.
1> c(curling).
{ok,curling}
2> {ok, Pid} = curling:start_link("Pirates", "Scotsmen").
Scoreboard: Team Pirates vs. Team Scotsmen
{ok,<0.78.0>}
3> curling:add_points(Pid, "Scotsmen", 2).
Scoreboard: increased score of team Scotsmen by 1
Scoreboard: increased score of team Scotsmen by 1
ok
4> curling:next_round(Pid).
Scoreboard: round over
ok
위의 추상화된 API를 사용할 때는 다음과 같이 간단하게 사용할 수 있게 된다.
4. 코드 작성 → EventHandler를 이용한 Pub - Sub 구조
앞서 이야기 했던 것처럼 EventManager는 특정 Handler가 무한히 루프를 타는 경우, 다른 EventHandler의 실행이 차단되는 치명적인 단점이 존재한다. EventHandler에 Pub - Sub 패턴을 적용해서 이 부분을 개선할 수 있다. 이를 위해 한 가지 EventHandler를 추가로 작성하고자 한다.
4.1 새로운 EventHandler → Event 구독자.
-module(curling_feed).
-author("ojt90").
-behaviour(gen_event).
-export([init/1, handle_info/2, handle_call/2, handle_event/2, code_change/3, terminate/2]).
init([Pid]) ->
io:format("init called.~n"),
{ok, Pid}.
handle_event(Event, Pid) ->
io:format("Handle Event Called.~n"),
Pid ! {curling_feed, Event},
{ok, Pid}.
handle_call(_, State) ->
{ok, ok, State}.
handle_info(_, State) ->
{ok, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
terminate(_Reason, _State) ->
ok.
새로운 EventHandler 모듈 코드다.
- handle_event() 메서드에서는 전달되는 모든 Event를 Pid ! {curling_feed, event}로 전송함.
이 코드는 EventHandler는 Pub - Sub 형태로 추가하는 코드로 볼 수 있다. 구독자는 위와 같은 EventHandler를 구현해서 EventManager에게 추가한다. EventManager에게 이벤트가 전달되면 구독자가 추가한 EventHandler를 호출할 것이고, EventHandler는 구독자에게 단순히 ! 연산자를 이용해 이벤트를 포워딩한다.
구독자는 EventHandler가 전송한 Event를 메세지로 Receive 해서 처리하기만 하면 된다.
-module(curling).
...
join_feed(EventManagerPid, ToPid) ->
HandlerId = {curling_feed, make_ref()},
gen_event:add_handler(EventManagerPid, HandlerId, [ToPid]),
HandlerId.
leave_feed(EventManagerPid, HandlerId) ->
gen_event:delete_handler(EventManagerPid, HandlerId, leave_feed).
- curling 모듈에는 EventHandler 추가하는 프토토콜을 추상화한 메서드인 join_feed(), leave_feed()를 추가한다.
추가한 모듈을 이용해서 실행하는 코드를 살펴보면 다음과 같다.
1> c(curling), c(curling_feed).
{ok,curling_feed}
2> {ok, Pid} = curling:start_link("Saskatchewan Roughriders", "Ottawa Roughriders").
Scoreboard: Team Saskatchewan Roughriders vs. Team Ottawa Roughriders
{ok,<0.165.0>}
3> HandlerId = curling:join_feed(Pid, self()).
{curling_feed,#Ref<0.0.0.909>}
4> curling:add_points(Pid, "Saskatchewan Roughriders", 2).
Scoreboard: increased score of team Saskatchewan Roughriders by 1
ok
Scoreboard: increased score of team Saskatchewan Roughriders by 1
5> flush().
Shell got {curling_feed,{add_points,"Saskatchewan Roughriders",2}}
ok
6> curling:leave_feed(Pid, HandlerId).
ok
7> curling:next_round(Pid).
Scoreboard: round over
ok
8> flush().
ok
- 이 프로세스는 curling:join_feed()를 메서드를 호출해서 EventManager가 Publish 하는 Event를 구독하는 프로세스로 스스로를 등록했다. 이 때, EventHandler를 등록해서 이벤트를 구독하는 형태다.
- 따라서 flush()를 했을 때, Shell got {curling_feed, ...} 같은 메세지를 이 프로세스가 수신하는 것이다.
5. 동일한 EventHandler가 여러 개인 경우?
하나의 EventManager에게 동일한 EventHandler가 여러 개 등록된 경우, 어떤 EventHandler가 내가 등록한 EventHandler인지 모를 수 있다. 등록한 EventHandler를 구별하기 위해서 Refernece를 이용할 수 있다.
join_feed(EventManagerPid, ToPid) ->
% HandlerID를 유니크하게 작성하기 위해 make_ref() 이용.
HandlerId = {curling_feed, make_ref()},
gen_event:add_handler(EventManagerPid, HandlerId, [ToPid]),
HandlerId.
위와 같이 등록할 수 있는데, 그 이유는 다음과 같다.
-type handler() :: atom() | {atom(), term()}.
add_handler() 메서드에서 요청한 handler()의 타입이 atom 이거나 {atom, term} 형식이다. 따라서 단순히 이름으로 등록하는 것도 가능하고, Refernece를 함께 등록해서 유니크한 이름으로 Handler를 사용할 수 있게 된다.
server_add_handler({Mod,Id}, Args, MSL) ->
Handler = #handler{module = Mod,
id = Id},
server_add_handler(Mod, Handler, Args, MSL);
server_add_handler(Mod, Args, MSL) ->
Handler = #handler{module = Mod},
server_add_handler(Mod, Handler, Args, MSL).
Handler를 등록하는 gen_event:server_add_handler() 내부에서도 {Mod, Id} / {Mod}에 대한 패턴매칭을 이용해서 핸들러를 등록해주는 것을 볼 수 있다.
6. Link된 EventHandler
gen_event:add_handler()를 호출하더라도 EventHandler / 프로세스 사이에 Link가 생성되지는 않는다. Pub - Sub 구조에서 구독 프로세스가 EventHandler를 추가하였는데, 이미 구독 프로세스가 죽은 경우라면 프로그램이 원하는대로 동작하지 않을 수 있다. 이런 것들을 개선하기 위해 gen_event:add_sup_handler()를 이용해 EventHandler를 추가할 수 있고, 이것은 EventManager 프로세스 - gen_event:add_sup_handler()를 호출한 프로세스에 Link를 만들어주는 역할을 한다.
add_sup_handler()를 추가했을 때, 각 프로세스가 죽은 경우 어떻게 동작할까?
- add_sup_handler() 호출한 프로세스가 죽은 경우 → EventHandler가 Module:teminate() 호출을 통해 terminated됨.
- EventHandler가 죽은 경우 → 아무 문제 없음. {gen_event_EXIT, HandlerId, Reason}을 프로세스가 메세지로 전달받음.
- EventManager가 죽은 경우 → {gen_event_EXIT, HandlerId, Reason}으로 전달받음.
7. EventHandler 추가 → Accumulator
위에서 작성한 EventHandler는 이벤트가 수신될 때 마다, 그 이벤트를 구독자 프로세스에게 전송하도록 구성된 코드였다. 이 Handler가 처음부터 추가되어 있는 경우에는 문제가 없지만, 중간에 Handler가 추가되는 경우 일부 이벤트를 놓치게 된다. 이 부분을 개선하기 위해서 Accumulator EventHandler를 추가하고자 한다. 목적은 game_info라는 요청을 전송하면, 그동안의 경기에 대한 정보를 응답하는 것이다. (현재 라운드, 현재 점수)
-module(curling_accumulator).
-author("ojt90").
-behaviour(gen_event).
%% API
-export([init/1, handle_event/2, handle_call/2, handle_info/2, code_change/3, terminate/2]).
-record(state, {teams=orddict:new(), round=0}).
init([]) ->
{ok, #state{}}.
handle_event({set_teams, TeamA, TeamB}, S=#state{teams=T}) ->
Teams = orddict:store(TeamA, 0, orddict:store(TeamB, 0, T)),
{ok, S#state{teams=Teams}};
handle_event({add_points, Team, N}, S=#state{teams=T}) ->
Teams = orddict:update_counter(Team, N, T),
{ok, S#state{teams=Teams}};
handle_event(next_round, S=#state{}) ->
{ok, S#state{round=S#state.round+1}};
handle_event(_Event, Pid)->
{ok, Pid}.
handle_call(game_data, S=#state{teams=T, round=R}) ->
{ok, {orddict:to_list(T), {round, R}}, S};
handle_call(_, State) ->
{ok, ok, State}.
handle_info(_, State) ->
{ok, State}.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
terminate(_Reason, _State) ->
ok.
- 위 코드는 EventAccumulator 역할을 하는 EventHandler다.
- 각 Event가 전송될 때 마다 orrdict에 각 호출에 대한 결과를 저장하고, 그것을 State에 보관한다.
- gen_event:call()로 game_data 프로토콜이 요청되면, 그동안의 결과를 {orddict:to_list(), {round, R}}.을 이용해 응답한다.
-module(curling).
..
start_link(TeamA, TeamB) ->
{ok, Pid} = gen_event:start_link(),
gen_event:add_handler(Pid, curling_scoreboard, []),
% EventHandler 추가하기
gen_event:add_handler(Pid, curling_accumulator, []),
set_teams(Pid, TeamA, TeamB),
{ok, Pid}.
...
game_info(Pid) ->
gen_event:call(Pid, curling_accumulator, game_data).
이 EventHandler를 EventManager에 추가해서 사용하도록 curling 모듈을 수정한다.
요약
- gen_event:start_link()를 통해 EventManager 프로세스를 생성할 수 있다.
- gen_event:add_handler(ManagerPid, ...)를 통해 생성된 EventManager에 eventHandler를 추가할 수 있다.
- EventManager / EventHandler는 동일한 프로세스에서 생성된다.
- EventManager는 이벤트를 수신하면 가지고 있는 모든 EventHandler를 순차적으로 실행한다. 따라서 각 EventHandler의 실행 시간은 짧아야 한다.
- 동일한 Handler가 여러 개 추가되었을 때 구분할 수 있도록 HandlerId는 make_ref()를 이용해 Reference를 생성해서 유니크하게 구별할 수 있도록 한다.
'프로그래밍 언어 > erlang' 카테고리의 다른 글
erlang 공부 : Rage Against The Finite-State Machines 2부 (0) | 2023.12.27 |
---|---|
erlang 공부 : Rage Against The Finite-State Machines 1부 (0) | 2023.12.27 |
erlang 공부 : Building an Application With OTP (0) | 2023.12.17 |
erlang 공부 : Who Supervises The Supervisors (2) | 2023.12.16 |
erlang 공부 : What is OTP? (0) | 2023.12.13 |