erlang 공부 : Rage Against The Finite-State Machines 1부

    참고

     


    1. FSM(Finite State Machine)과 간단한 예시 

    FSM (유한상태머신)은 특정한 수의 State를 가지고, 이 중 하나의 State가 항상 Active되는 추상적인 모델을 의미한다. 

    Dog에 대한 세 가지 State를 정의한 FSM를 살펴보면 다음과 같다.

    • barks
    • wag tail
    • sits

    짖고, 꼬리를 흔들고, 앉아있는 상태가 있다. 그리고 각 상태는 특정 작업이 트리거 되었을 때 다른 상태로 전이된다. 예를 들어 'sits' 상태의 개는 'see squirrels' 입력이 들어왔을 때 'barks' 상태로 전이된다.

    위는 또 다른 예시인 고양이의 FSM이다. 

    • don't give a crap

    고양이는 어떠한 이벤트가 들어오건 항상 don't give a crap이라는 상태를 유지한다. 각각 유한한 State를 가지고 특정한 입력(Event)를 맞았을 때, 다른 State로 전환하는 추상적인 동작을 FSM (Finite State Machine)이라고 한다. 

     


    2. Dog, Cat의 FSM 구현 

    위의 FSM을 코드로 구현하면 어떤 모습이 될까? 우선 erlang 코드로 Dog, Cat을 구현해보고자 한다. 

     

    2.1 Cat의 FSM 구현 → 동기 방식의 요청

    -module(cat_fsm).
    -author("ojt90").
    
    %% API
    -export([start/0, event/2]).
    
    start() ->
      spawn(fun() -> dont_give_crap() end).
    
    event(Pid, Event) ->
      Ref = make_ref(),
      Pid ! {self(), Ref, Event},
      receive
        {Ref, Msg} -> {ok, Msg}
      after 5000 ->
        {error, timeout}
      end.
    
    
    
    dont_give_crap() ->
      receive
        {Pid, Ref, _Msg} -> Pid ! {Ref, meh};
        _                -> ok
      end,
      io:format("Switching to 'dont_give_crap' state~n"),
      dont_give_crap().

    Cat의 FSM은 다음과 같이 구현할 수 있다.

    1. dont_give_crap() 메서드는 고양이의 상태를 의미한다. 이 메서드에서 고양이는 어떤 메세지를 받아도 dont_give_crap()을 호출하며 꼬리재귀를 이어간다. 
    2. 클라이언트는 event()를 이용해 고양이에게 상태 변경 요청 이벤트를 보낼 수 있다. 

    이 때, event()는 메세지를 보낸 후 receive를 이용해 메세지가 올 때까지 기다린다. 즉, 이 코드는 Synchronized 형태로 동작하는 코드다. 

    1> c(cat_fsm).
    {ok,cat_fsm}
    
    2> Cat = cat_fsm:start().
    <0.67.0>
    
    3> cat_fsm:event(Cat, pet).
    Switching to 'dont_give_crap' state
    {ok,meh}
    
    4> cat_fsm:event(Cat, love).
    Switching to 'dont_give_crap' state
    {ok,meh}
    
    5> cat_fsm:event(Cat, cherish).
    Switching to 'dont_give_crap' state
    {ok,meh}

    작성한 코드가 정상동작하는지 얼랭 쉘에서 다음 코드를 사용해서 확인해 볼 수 있다. 

     

    2.2 Dog의 FSM 구현 → 비동기 방식의 요청

    -module(dog_fsm).
    -author("ojt90").
    
    %% API
    -export([start/0, squirrel/1, pet/1]).
    
    start() ->
      spawn(fun() -> bark() end).
    
    
    % Request API
    squirrel(Pid) -> Pid ! squirrel.
    
    pet(Pid) -> Pid ! pet.
    
    % State Local Method.
    bark() ->
      io:format("Dog says: BARK! BARK!~n"),
      receive
        pet -> wag_tail();
        _   ->
          io:format("Dog is confused~n"),
          bark()
      after 2000 ->
        bark()
      end.
    
    wag_tail() ->
      io:format("Dog wags its tail~n"),
      receive
        pet -> sit();
        _ ->
          io:format("Dog is confused~n"),
          wag_tail()
      after 30000 ->
        bark()
      end.
    
    sit() ->
      io:format("Dog is sitting. Gooooooood boy! ~n"),
      receive
        squirrel -> bark();
        _ ->
          io:format("Dog is confused~n"),
          sit()
      end.

    위에서 그렸던 Dog의 FSM을 erlang 코드로 구현하면 다음과 같다.

    1. 상태 전이 요청을 보내는 Public API인 squirrel(), pet()을 제공한다. Pid는 상태머신이고, 상태 머신에게 단순히 메세지를 전송한다.
    2. 상태를 나타내는 메서드인 bark(), wag_tail(), sit()을 구현했다. 
      • 각 메서드에서는 어떤 메세지를 받느냐에 따라 다른 상태로 전환한다. 예를 들어 bark() 상태에서 pet이라는 메세지를 받으면 wag_tail로 상태 전환된다. 

    위 코드는 비동기로 동작한다. Public API인 squirrel(), pet()이 메세지를 전송한 프로세스로부터 응답을 기다리지 않기 때문이다. 

    1> c(dog_fsm).
    {ok,dog_fsm}
    
    2> Pid = dog_fsm:start().
    Dog says: BARK! BARK!
    <0.46.0>
    Dog says: BARK! BARK!
    
    3> dog_fsm:pet(Pid).
    pet
    Dog wags its tail
    
    4> dog_fsm:pet(Pid).
    Dog is sitting. Gooooood boy!
    pet
    
    5> dog_fsm:pet(Pid).
    Dog is confused
    pet
    Dog is sitting. Gooooood boy!
    
    6> dog_fsm:squirrel(Pid).
    Dog says: BARK! BARK!
    squirrel
    Dog says: BARK! BARK!   
    
    7> dog_fsm:pet(Pid).
    Dog wags its tail
    pet
    
    8> %% wait 30 seconds
    Dog says: BARK! BARK!
    Dog says: BARK! BARK!
    Dog says: BARK! BARK!    
    
    9> dog_fsm:pet(Pid).    
    Dog wags its tail
    pet
    
    10> dog_fsm:pet(Pid).
    Dog is sitting. Gooooood boy!
    pet

    작성한 코드가 정상동작하는지 얼랭 쉘에서 다음 코드를 사용해서 확인해 볼 수 있다. 

     

    2.3 정리

    위 코드들이 Erlang 프로세스로 구현된 FSM의 핵심이다. 이전에 사용했던 Server의 메인 루프와 마찬가지로, State Function에서 메인 루프를 돌게 된다. 여기서는 추가되지 않았지만 FSM은 현재 State가 무엇이든 상관없이 특정 State로 전환해야하는 Global State 같은 것들도 존재한다. 

    그런데 위 코드에서 볼 수 있지만, gen_server처럼 꽤나 일반화된 부분이 존재한다. 예를 들어 메인 루프를 돌면서 메세지를 수신하고 상태를 전환하는 부분인데, OTP에서는 이런 일반화된 부분을 gen_fsm으로 제공해준다. 아래에서 해당 내용을 좀 더 살펴본다. 

     


    3. Generic Finite-State Machines 

    앞서 이야기한 것처럼 직접 구현한 FSM에는 일반적인 부분 / 비즈니스 로직 부분이 혼재되어있었다. OTP 프레임워크는 일반적으로 일반적인 부분을 구현해주는데, OTP 프레임워크에서 gen_fsm을 통해 FSM의 일반화된 버전을 제공해준다. 

     

    3.1 gen_fsm behaviour의 callback

    gen_fsm은 behaviour로 제공되고, 이것을 올바르게 사용하려면 gen_fsm의 callback을 각각 구현해야한다. gen_fsm에 필요한 callback은 다음과 같다. 

    • init/1
    • StateName/2, StateName/3
    • handle_event/3
    • handle_sync_event/4
    • code_change
    • terminate

    기존에 사용하던 gen_server와 다른 부분이 상당히 있기 때문에 조금씩 살펴보고자 한다. 

     

    init/1

    -callback init(Args :: term()) ->
        {ok, StateName :: atom(), StateData :: term()} |
        {ok, StateName :: atom(), StateData :: term(), timeout() | hibernate} |
        {stop, Reason :: term()} | ignore.

    gen_fsm:start() / gen_fsm:start_link() 등으로 상태 머신 프로세스를 기동했을 때, 초기화에 사용되는 콜백이다. 반환값으로 {ok, StateName, StateData}를 반환해야한다. 여기서 StateName은 시작 State를 의미하고, gen_fsm을 사용하는 개발자는 StateName과 동일한 메서드를 구현해야한다. 

    예를 들어 StateName이 bark이면, bark/2, bark/3 같은 상태 메서드를 구현해야한다. 

     

    StateName/2, StateName/3

    StateName은 일종의 PlaceHolder이다. StateName이라는 PlaceHolder는 상태를 의미하는 State Function이고, 사용자가 임의의 이름을 정해서 구현해야한다. 예를 들어 짖는 상태를 표현하기 위하 bark/2, bark/3 같은 State Function을 구현해 둘 수 있다. 간단히 말해 State Function이라는 것이다.

    StateName/2, StateName/3

    StateName/2와 StateName/3의 차이는 응답을 바로할 것인지에 대한 차이이다. StateName/3는 StateName/2의 인풋 파라메터에 From을 함께 전달받는다. 메세지를 보내온 Pid가 From에 저장되고, 동기방식으로 응답이 필요한 경우 응답을 해주면 된다. 

    기본적으로 가장 많이 사용되는 반환값은 {next_state, NewStateName, NewStateData}이다. 이 값을 반환하면 현재 State에서 NewState로 전환하게 된다. NewStateData에는 gen_server의 메인 루프에서처럼 State를 넣어서 전달해 줄 수도 있다. 

    각 State Function을 호출하는 API는 다음과 같다. 

    • StateName/2 → gen_fsm:send_event/2
    • StateName/3 → gen_fsm:sync_send_event/3

     

    handle_event/3

    -callback handle_event(Event :: term(), StateName :: atom(),
                           StateData :: term()) ->
        {next_state, NextStateName :: atom(), NewStateData :: term()} |
        {next_state, NextStateName :: atom(), NewStateData :: term(),
         timeout() | hibernate} |
        {stop, Reason :: term(), NewStateData :: term()}.

    FSM의 글로벌 이벤트(어떤 상태든 특정 상태로 변경되는)가 비동기 방식으로 처리되어야 할 때 사용하는 콜백이다. 대응되는 gen_fsm 제공 API는 gen_fsm:send_all_state_event/2이다.

     

    handle_sync_event/4

    -callback handle_sync_event(Event :: term(), From :: {pid(), Tag :: term()},
                                StateName :: atom(), StateData :: term()) ->
        {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term()} |
        {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term(),
         timeout() | hibernate} |
        {next_state, NextStateName :: atom(), NewStateData :: term()} |
        {next_state, NextStateName :: atom(), NewStateData :: term(),
         timeout() | hibernate} |
        {stop, Reason :: term(), Reply :: term(), NewStateData :: term()} |
        {stop, Reason :: term(), NewStateData :: term()}.

    이 콜백은 FSM 글로벌 이벤트 동기식 요청을 처리하는데 사용된다. 

     

    3.2 gen_fsm 코드 살짝 살펴보기

    gen_fsm:send_event() 같은 Public API를 이용해 FSM 프로세스에 이벤트를 전송할 수 있다. 이 요청 메세지는 OTP 프레임워크 내부 코드 흐름을 따라 동작한다. 

    궁극적으로는 gen_fsm 모듈의 handle_msg()가 호출된다. 여기서 어떤 State Function / handle_event Function이 호출될지는 dispatch() 메서드를 통해 결정된다. 

    dispatch() 메서드는 어떤 atom이 전달되느냐에 따라 서로 다른 Method에 Delegation을 하는 것을 볼 수 있다. 예를 들어 'gen_event'라는 atom이 전달되는 경우, Mod:StateName/2가 호출된다. 예를 들어 dog:barks/2가 호출되는 것과 동일하다고 볼 수 있다. 

    또한 gen_all_state_event라는 아톰이 전달되면 해당 모듈의 handle_event/3을 호출하는 것을 볼 수 있다. 


    4. Dog, Cat FSM을 gen_fsm으로 옮기기

    앞서 순수 얼랭 코드로만 작성한 Dog, Cat FSM을 gen_fsm으로 옮기려고 한다. 

     

    4.1 Cat FSM

    -module(cat_fsm).
    -author("ojt90").
    -behaviour(gen_fsm).
    
    %% API
    -export([init/1, handle_event/3, handle_sync_event/4]).
    -export([event/2, dont_give_crap/3, start_link/0]).
    
    start_link() ->
      gen_fsm:start_link(?MODULE, [], []).
    
    init([]) ->
      {ok, dont_give_crap, []}.
    
    handle_event(_Event, _StateName, _StateData) ->
      erlang:error(not_implemented).
    
    handle_sync_event(_Event, _From, _StateName, _StateData) ->
      erlang:error(not_implemented).
    
    
    %%% StateName Callback.
    dont_give_crap(_Event, From, State) ->
      io:format("Switching to 'dont_give_crap' state~n"),
      gen_fsm:reply(From, meh),
      {next_state, dont_give_crap, State}.
    
    
    %%% State API. 기존 Cat API가 동기로 처리됨.
    event(Pid, Event) ->
      gen_fsm:sync_send_event(Pid, Event, 5000).

    위 코드는 cat_fsm을 gen_fsm으로 포팅한 코드다.

    1. event() 메서드를 이용해 FSM에 요청한다. 이 때, gen_fsm:sync_send_event()에 요청을 Delegation 한다.
    2. dont_give_crap은 StateName/3을 구현한 코드다. gen_fsm:sync_send_event()는 동기식으로 동작하기 때문에 동기식으로 응답해줘야한다. 따라서 StateName/3이 호출될 것이기 때문에 dont_give_crap/3으로 구현했다. 
    1> {ok, Pid} = cat_fsm:start_link()
    2> cat_fsm:event(Pid, hello).
    3> cat_fsm:event(Pid, is_it_good).

    위 코드가 정상동작하는지는 다음 코도를 얼랭 쉘에서 실행해보면 된다.

     

    4.2 Dog FSM

    -module(dog_fsm).
    -author("ojt90").
    -behaviour(gen_fsm).
    
    
    %% API
    -export([init/1, handle_event/3, handle_sync_event/4]).
    -export([bark/2, wag_tail/2, sit/2, handle_info/2]).
    -export([squirrel/1, pet/1]).
    -export([start_link/0]).
    
    start_link() ->
      gen_fsm:start_link(?MODULE, [], []).
    
    init([]) ->
      {ok, bark, [], 2000}.
    
    handle_event(Event, StateName, StateData) ->
      erlang:error(not_implemented).
    
    handle_sync_event(Event, From, StateName, StateData) ->
      erlang:error(not_implemented).
    
    
    %%% StateName callback.
    bark(pet, State) ->
      io:format("Dog says: Bark! Bark! ~n"),
      {next_state, wag_tail, State, 2000};
    bark(_, State) ->
      io:format("Dog is confused. Here ~n"),
      {next_state, bark, State, 2000}.
    
    wag_tail(pet, State) ->
      io:format("Dog wags its tail~n"),
      {next_state, sit, State, 30000};
    wag_tail(_, State) ->
      io:format("Dog is confused. ~n"),
      {next_state, wag_tail, State, 30000}.
    
    sit(squirrel, State) ->
      io:format("Dog is sitting. Gooooooood boy! ~n"),
      {next_state, bark, State};
    sit(_, State) ->
      io:format("Dog is confused. ~n"),
      {next_state, sit, State}.
    
    %% Timeout -> bark
    handle_info(_, State) ->
      {next_state, bark, State}.
    
    
    %% FSM API : 기존 dog_fsm이 비동기로 처리됨.
    squirrel(Pid) ->
      gen_fsm:send_event(Pid, squirrel).
    
    pet(Pid) ->
      gen_fsm:send_event(Pid, pet).

    위 코드는 erlang으로만 작성했던 dog_fmsm을 gen_fsm으로 포팅한 코드다. 아래는 다음과 같이 동작한다.

    1. init()의 반환값을 통해 초기 State는 bark()로 설정한다.
    2. 모든 요청은 비동기로 처리되기 때문에 StateName/2로 bark(), wag_tail(), sit()을 구현했다. 
    1> {ok, Pid} = dog_fsm:start_link().
    2> dog_fsm:squirrel(Pid, hello).
    3> dog_fsm:pet(Pid, is_it_good).
    4> flush().

    위 코드가 정상 동작하는지는 다음 코드를 얼랭 쉘에서 실행해보면 된다. 

     


     

    댓글

    Designed by JB FACTORY