erlang 공부 : What is OTP?

    참고


    1. What is OTP? 

    erlang에서 안정적인 코드 작성을 위해서 link, monitor, servers, timeouts, trapping exits, hot cod loading, naming processe, supervisor 등의 기능을 사용해야했었다. 그런데 이런 코드들은 비즈니스 로직과 관련되었다기 보다는 부수적인 기능이다. 

    OTP Framework는 이런 기능들을 프레임워크 형태로 지원해주고, 덕분에 개발자는 비즈니스 로직에만 집중할 수 있게 된다. 따라서 OTP Framework를 사용하는 것이 좋다. 

     


    2. The Common Process, Abstracted

    얼랭에서 일반적인 프로세스는 다음과 같은 생애주기를 가진다. OTP에서는 이런 생애주기를 프레임워크에 포함시켰다. 이번 글에서는 수동으로 Server를 개발하고, 그 Server를 프레임워크로 옮기는 작업을 해본다. 

     

     


    3. The Basic Server

    여기서는 가장 기본적인 서버인 kitty_server.erl을 제공한다.

    -module(kitty_server).
    -record(cat, {name, color=green, description}).
    
    %% API
    -export([]).
    
    
    % Client API
    start_link() ->
      spawn_link(?MODULE, init, []).
    
    
    %% Synchronous call
    order_cat(Pid, Name, Color, Description) ->
      Ref = erlang:monitor(process, Pid),
      Pid ! {self(), Ref, {order, Name, Color, Description}},
      receive
        {Ref, Cat} ->
          erlang:demonitor(Ref, [flush]),
          Cat;
        {'DOWN', Ref, process, Pid, Reason} ->
          erlang:error(Reason)
      after 5000 ->
        erlang:error(timeout)
      end.
    
    close_shop(Pid) ->
      Ref = erlang:demonitor(process, Pid),
      Pid ! {self(), Ref, terminate},
      receive
        {Ref, ok} ->
          erlang:demonitor(Ref, [flush]),
          ok;
        {'DOWN', Ref, process, Pid, Reason} ->
          erlang:error(Reason)
      after 5000 ->
        erlang:error(timeout)
      end.
    
    % Asynchronous
    return_cat(Pid, Cat = #cat{}) ->
      Pid ! {return, Cat},
      ok.
    
    
    
    %% Server function.
    init() -> loop([]).
    
    loop(Cats) ->
      receive
        {Pid, Ref, {order, Name, Color, Description}} ->
          if
          Cats =:= [] ->
            Pid ! {Ref, make_cat(Name, Color, Description)},
            loop(Cats);
          Cats =/= [] ->
            Pid ! {Ref, hd(Cats)},
            loop(tl(Cats))
          end;
        {return, Cat = #cat{}} ->
          loop([Cat|Cats]);
        {Pid, Ref, terminate} ->
          Pid ! {Ref, ok},
          terminate(Cats);
        Unknown ->
          io:format("Unknown message: ~p~n", [Unknown]),
          loop(Cats)
      end.
    
    % private function.
    make_cat(Name, Col, Desc) ->
      #cat{name=Name, color=Col, description=Desc}.
    
    terminate(Cats) ->
      [io:format("~p was set free.~n", [C#cat.name]) || C <- Cats],
      ok.
    • kitty_server.erl은 Server / 프로토콜로 코드가 나누어져있다. 그리고 프로토콜은 동기 / 비동기로 나누어져있다. 
    • 고양이 상점을 열고 닫음.
    • 고양이 상점에 고양이를 주문 / 추가 /  반품이 가능함.

    이런 형태로 코드가 동작한다.

    1> c(kitty_server).
    {ok,kitty_server}
    
    2> rr(kitty_server).
    [cat]
    
    3> Pid = kitty_server:start_link().
    <0.57.0>
    
    4> Cat1 = kitty_server:order_cat(Pid, carl, brown, "loves to burn bridges").
    #cat{name = carl,color = brown,
    description = "loves to burn bridges"}
    
    5> kitty_server:return_cat(Pid, Cat1).
    ok
    
    6> kitty_server:order_cat(Pid, jimmy, orange, "cuddly").
    #cat{name = carl,color = brown,
    description = "loves to burn bridges"}
    
    7> kitty_server:order_cat(Pid, jimmy, orange, "cuddly").
    #cat{name = jimmy,color = orange,description = "cuddly"}
    
    8> kitty_server:return_cat(Pid, Cat1).
    ok
    
    9> kitty_server:close_shop(Pid).
    carl was set free.
    ok
    
    10> kitty_server:close_shop(Pid).
    ** exception error: no such process or port
    in function  kitty_server:close_shop/1

    위 코드를 동작시켜보자.

    1. 서버를 시작한다.
    2. 고양이를 order한다..
    3. order한 고양이를 return한다. 
    4. 고양이 가게를 닫는다. 

    아주 기초적으로 동작하는 코드다. 

     

    3.1 공통 인터페이스 찾아보기

    위 코드에서는 공통적으로 사용되는 패턴, 중복되는 코드들이 있다. 예를 들어 Monitor를 생성 / 삭제하거나 타이머를 적용, 메세지 수신, 메인 루프 돌기, 그리고 서버에게 요청 보내기 등등이다. 각각을 하나씩 Generic하게 뽑아보려고 한다.

     

     

    3.1.1 동기식 요청의 Generic

    -module(my_server).
    -compile(export_all).
    
    call(Pid, Msg) ->
      Ref = erlang:monitor(process, Pid),
      Pid ! {self(), Ref, Msg},
      receive
        {Ref, Reply} ->
          erlang:demonitor(Ref, [flush]),
          Reply;
        {'DOWN', Ref, process, Pid, Reason} ->
          erlang:error(Reason)
      after 5000 ->
        erlang:error(timeout)
      end.

    동기식 요청을 보내던 메서드인 order_cat(), close_shop() 코드는 큰 틀은 위와 같다. 

    1. 모니터를 생성한다.
    2. 모니터와 함께 서버에 요청을 보낸다.
    3. 요청을 Receive로 기다린 후, 못 받으면 Timeout 한다. 

    즉, 서버에 보내는 요청(Request)의 값만 적절히 바꿔주고 서버에서 그걸 잘 핸들링 하도록 하면 된다는 것이다. 동기 요청하는 공통적인 부분은 my_server:call()로 extract했다. kitty_server에서는 이 API를 사용하기만 하면 된다. 

    %% Synchronous call
    order_cat(Pid, Name, Color, Description) ->
    	my_server:call(Pid, {order, Name, Color, Description}).
     
    close_shop(Pid) ->
    	my_server:call(Pid, terminate).

     

     

    3.1.2 Main Loop의 Generic.

    -module(kitty_server).
    
    loop(Cats) ->
      receive
        {Pid, Ref, {order, Name, Color, Description}} ->
          if
          Cats =:= [] ->
            Pid ! {Ref, make_cat(Name, Color, Description)},
            loop(Cats);
          Cats =/= [] ->
            Pid ! {Ref, hd(Cats)},
            loop(tl(Cats))
          end;
        {return, Cat = #cat{}} ->
          loop([Cat|Cats]);
        {Pid, Ref, terminate} ->
          Pid ! {Ref, ok},
          terminate(Cats);
        Unknown ->
          io:format("Unknown message: ~p~n", [Unknown]),
          loop(Cats)
      end.

    기존의 kitty_server를 프로세스로 돌리던 코드는 loop 코드다. 이 loop 코드를 일반화 하기는 어렵지만, 다음 형식을 보면 바로 loop 코드 역시 generic이 가능하다는 것을 알 수 있게 된다.

    loop(Module, State) ->
      Receive
        Message -> Module:handle(Message, State)
      end.

    결국 loop문은 메세지를 수신하고 메세지를 처리해주는 handle() 메서드를 호출해주는 형태가 된다. 

    handle(Message1, State) -> NewState1;
    handle(Message2, State) -> NewState2;
    ...
    handle(MessageN, State) -> NewStateN.

    그리고 loop문에서 호출하는 handle() 메서드는 Message에 대한 패턴매칭을 통해서 Dispatch 할 수 있게 된다.

     

     

    3.1.3 동기, 비동기 요청의 분리 및 Generic

    동기 요청, 비동기 요청은 서로 다른 Generic을 가지고 있다. 그러나 서버에서는 '요청'을 받는 형태로 Generic이 된다. 따라서 동기 요청 / 비동기 요청은 요청을 보낼 때 '요청 내부'에 서버가 구별할 수 있는 값을 넣어줘야한다. 따라서 동기 요청과 비동기 요청은 다음 형식으로 Generic으로 바꿀 수 있다. 

    % 동기 요청
    call(Pid, Msg) ->
      Ref = erlang:monitor(process, Pid),
      Pid ! {sync, self(), Ref, Msg},
      receive
        {Ref, Reply} ->
          erlang:demonitor(Ref, [flush]),
          Reply;
        {'DOWN', Ref, process, Pid, Reason} ->
          erlang:error(Reason)
      after 5000 ->
        erlang:error(timeout)
      end.
      
    % 비동기 요청
    cast(Pid, Msg) ->
      Pid ! {async, Msg},
      ok.

    동기 요청 / 비동기 요청은 요청을 서버로 전송할 때, 가장 앞부분에 atom으로 async / sync를 통해 동기, 비동기 요청이 구별되도록 했다. 

    loop(Module, State) ->
      receive
        {async, Msg} ->
          loop(Module, Module:handle_cast(Msg, State));
        {sync, Pid, Ref, Msg} ->
          loop(Module, Module:handle_call(Msg, {Pid, Ref}, State))
      end.

    서버에서는 요청을 async / sync를 분리해서 처리할 수 있도록 했다.

    • async → handle_cast() 호출
    • sync → handle_call() 호출

     

    3.1.4 응답의 Generic

    reply({Pid, Ref}, Reply) ->
      Pid ! {Ref, Reply}.

    서버는 요청을 받았을 때 응답을 하는 형태가 된다. 이 응답을 하는 부분도 추상화 해볼 수 있다. 

     

    3.1.5 모듈을 시작 / 초기화의 Generic

    앞에서 서버 - 클라이언트 구조에서 핵심이 되는 부분을 모두 일반화했다. 지금부터 추가할 부분은 모듈을 생성하고, 초기화 하는 과정이다. 

    -module(my_server).
    start(Module, InitialState) ->
      spawn(fun() -> init(Module, InitialState) end).
     
    start_link(Module, InitialState) ->
      spawn_link(fun() -> init(Module, InitialState) end).
      
    init(Module, InitialState) ->
      loop(Module, Module:init(InitialState)).

    서버 프레임워크를 통해서 실행할 모듈과 초기 상태값을 전달받는다. 이 파라메터를 이용해 Module:init()를 통해서 모듈의 loop문을 시작하는데 필요한 초기값을 초기화한다. 

     

    3.1.6 완성된 프레임워크 

    -module(my_server).
    -export([start/2, start_link/2, call/2, cast/2, reply/2]).
     
    %%% Public API
    start(Module, InitialState) ->
      spawn(fun() -> init(Module, InitialState) end).
     
    start_link(Module, InitialState) ->
      spawn_link(fun() -> init(Module, InitialState) end).
     
    call(Pid, Msg) ->
      Ref = erlang:monitor(process, Pid),
      Pid ! {sync, self(), Ref, Msg},
      receive
        {Ref, Reply} ->
        erlang:demonitor(Ref, [flush]),
        Reply;
      {'DOWN', Ref, process, Pid, Reason} ->
        erlang:error(Reason)
      after 5000 ->
        erlang:error(timeout)
      end.
     
    cast(Pid, Msg) ->
      Pid ! {async, Msg},
      ok.
     
    reply({Pid, Ref}, Reply) ->
      Pid ! {Ref, Reply}.
     
    %%% Private stuff
    init(Module, InitialState) ->
      loop(Module, Module:init(InitialState)).
     
    loop(Module, State) ->
      receive
        {async, Msg} ->
          loop(Module, Module:handle_cast(Msg, State));
        {sync, Pid, Ref, Msg} ->
          loop(Module, Module:handle_call(Msg, {Pid, Ref}, State))
      end.
    • 완성된 프레임워크는 다음과 같다. 
    • 이 프레임워크는 기존 kitty_server.erl에서 일반화 할 수 있는 부분을 추출해서 만든 것이다. 

    kitty_server2.erl이라는 파일을 만들고, my_server 프레임워크를 사용하도록 코드를 수정하면 된다.

     

    3.1.7 my_server 프레임워크 사용하기 - Client API

    -module(kitty_server2).
     
    -export([start_link/0, order_cat/4, return_cat/2, close_shop/1]).
    -export([init/1, handle_call/3, handle_cast/2]).
     
    -record(cat, {name, color=green, description}).
     
    %%% Client API
    start_link() -> my_server:start_link(?MODULE, []).
     
    %% Synchronous call
    order_cat(Pid, Name, Color, Description) ->
      my_server:call(Pid, {order, Name, Color, Description}).
     
    %% This call is asynchronous
    return_cat(Pid, Cat = #cat{}) ->
      my_server:cast(Pid, {return, Cat}).
     
    %% Synchronous call
    close_shop(Pid) ->
      my_server:call(Pid, terminate).
    • 클라이언트 → 서버로 보내는 프로토콜을 API로 추상화한 부분들을 my_server 프레임워크를 사용하도록 한다. 
    • 앞서 일반화 할 때도 이야기 작성했지만, 비슷한 형태로 요청을 하는데 '요청 내용'만 바뀌도록 작성한다.

    여기서 요청 내용이 바뀌는 것은 Request 파라메터 안에 들어가는 파라메터들이 바뀌는 것이다. 예를 들어 order_cat() 에서는 {order, Name, Color, Description}이 들어갔다. return_cat() 요청에서는 {return, Cat}이 들어갔다. 

     

    3.1.8 my_server 프레임워크 사용하기 - Server Handler

    %%% Server functions
    init([]) -> []. %% no treatment of info here!
     
    handle_call({order, Name, Color, Description}, From, Cats) ->
      if 
        Cats =:= [] ->
          my_server:reply(From, make_cat(Name, Color, Description)),
          Cats;
        Cats =/= [] ->
          my_server:reply(From, hd(Cats)),
          tl(Cats)
      end;
     
    handle_call(terminate, From, Cats) ->
      my_server:reply(From, ok),
      terminate(Cats).
     
    handle_cast({return, Cat = #cat{}}, Cats) ->
      [Cat|Cats].

    서버쪽에서는 handle_call(), handle_cast() 메서드를 구현해야하는데, erlang에서는 패턴매칭을 통해서 요청이 Dispatch 되도록 한다고 했다. 따라서 위처럼 코드를 작성하면 된다. 

    위 코드에서 주의깊게 봐야할 부분은 메서드 시그니처의 가장 앞부분에 있는 값들이 조금씩 다르다는 것이다. 요청을 보낼 때, Request 파라메터에 어떤 atom(order, teminate, return)을 보내는지에 따라 특정 메서드에 Dispatch 되어서 동작하는 것을 의미한다. 

     


    4. Specific vs Generic

    이렇게 Generic 된 부분을 프레임워크로 뽑아냈을 때의 장점이 있다.

    • 프레임워크에 의존하는 코드들은 프레임워크가 최적화 될 때 마다 같이 성능 개선이 됨.
    • behaviour() 등을 통해서 특정 모듈이 어떤 동작을 하는 모듈인지 알 수 있음. 
    • 프레임워크에 공통적인 부분을 맡기고, 개발자는 비즈니스 로직에 집중할 수 있음. 
    • 테스트 하기 편리해짐.

    위에서 우리는 요청에 응답하기 위한 코드를 각각 handle_call() 메서드로 격리했다. 이 말은 handle_call() 메서드를 단위 테스트하기 좋아졌다는 것을 의미한다. handle_call()은 단순한 함수이고, 테스트 하기 위해서는 적절한 파라메터를 만들어서 호출하기만 하면 되기 때문이다.

     

     

    댓글

    Designed by JB FACTORY