cowboy_clock Module

cowboy는 erlang으로 작성한 대표적인 오픈소스 웹 서버 코드이다. 그 중 cowboy_clock 모듈은 http 통신과 직접 관련은 없지만 gen_server behaviour에 따라 독립 프로세스로 실행되면서 RFC1123 포맷의 시간 문자열을 내놓는다. RFC1123 표준 문서에 대한 설명은 생략하고, 쉽게 말해 다음과 같은 형식의 문자열이라고 생각하면 된다.

"Sat, 14 May 2011 14:25:33 GMT"

ETS는 erlang OTP 프레임워크에서 지원하는 key-value 저장소라고 보면 된다. in-memory 저장소이며, disk 저장소 버전은 DETS라고 따로 있다. 초기화는 gen_serverinit/1 콜백 함수에서 한다.

init([]) ->
  ?MODULE = ets:new(?MODULE, [set, protected,
    named_table, {read_concurrency, true}]),
  T = erlang:universaltime(),
  B = update_rfc1123(<<>>, undefined, T),
  TRef = erlang:send_after(1000, self(), update),
  ets:insert(?MODULE, {rfc1123, B}),
  {ok, #state{universaltime=T, rfc1123=B, tref=TRef}}.

ETS 초기화를 한 후 현재 시간을 T에 저장하고 RFC1123 문자열 형식을 B에 저장한다. 그 후 ETS에 {rfc1123, B} key-value 쌍을 저장한다. erlang:send_after/3 함수에 의해 1초 뒤에 자신의 handle_info(update, ...) 함수가 호출된다.

handle_info(update, #state{universaltime=Prev, rfc1123=B1, tref=TRef0}) ->
  _ = erlang:cancel_timer(TRef0),
  T = erlang:universaltime(),
  B2 = update_rfc1123(B1, Prev, T),
  ets:insert(?MODULE, {rfc1123, B2}),
  TRef = erlang:send_after(1000, self(), update),
  {noreply, #state{universaltime=T, rfc1123=B2, tref=TRef}};
...

우선 1초 전에 걸었던 타이머를 캔슬한다. 그 후 동작은 init/1 함수에서와 유사하다. 새로운 RFC1123 문자열을 ETS에 넣은 다음 다시 erlang:send_after/3 함수에 의해 다시 1초 뒤에 handle_info(update, ...) 함수가 호출된다.

그러면 ETS에 저장된 key-value 데이터는 언제 사용하는가? 다음의 rfc1123/0 함수에서 사용한다.

rfc1123/* Functions

모듈의 주요 인터페이스는 rfc1123/0rfc1123/1 함수로, 이 중 rfc1123/0 함수는 ETS로부터 현재 시간의 RFC1123 문자열을 꺼내 반환하며, rfc1123/1 함수는 특정 시간에 대한 값을 파라미터로 받아 이에 대한 RFC1123 문자열을 만들어서 반환한다.

rfc1123() ->
  ets:lookup_element(?MODULE, rfc1123, 2).

rfc1123(DateTime) ->
  update_rfc1123(<<>>, undefined, DateTime).

이 중 update_rfc1123/3 함수는 내부 함수로 나중에 다시 살펴본다.
DateTime 파라미터는 다음과 같은 형식을 가진다.

% {{YYYY, MM, DD}, {hh, mm, ss}}
{{2011, 5, 14}, {14, 25, 33}}

Internal Functions

update_rfc1123/3 Function

두 개의 시간 정보를 비교하여 바뀐 시간만큼 RFC1123 문자열을 수정한다. 첫번째 파라미터는 RFC1123 문자열이고, 두번째 파라미터는 RFC1123 문자열의 DateTime 값이다. 세번째 파라미터는 갱신된 DateTime 값이다.

update_rfc1123(Bin, Now, Now) ->
  Bin;
update_rfc1123(<<Keep:20/binary, _/bits>>,
    {Date, {H, M, _}}, {Date, {H, M, S}}) ->
  <<Keep/binary, (pad_int(S))/binary, " GMT">>;
% ...

소스 상에서 update_rfc1123/3 함수를 보면 꽤 복잡하게 작성되어 있는데, 시간 차의 정도에 따라 RFC1123 문자열 중 최소한만 수정하기 때문이다. 두 개의 시간 정보가 동일하다면 문자열 수정 없이 반환할 것이다. 만약 초 단위의 차이만 있다면 RFC1123 문자열 중 ‘GMT’ 앞의 초 값만 수정될 것이다. 그렇지 않고 연도까지 바뀔 정도의 시간 차이라면 RFC1123 문자열을 새로 만들다시피 할 것이다. 여기서는 구체적인 코드 분석은 생략하고 넘어간다.

pad_int/1 Function

한 자리 정수인 경우 앞에 ‘0’을 붙여주는 함수이다. 스니펫으로 저장해두면 유용하게 사용할 수 있을 듯 하다.

pad_int(X) when X < 10 ->
  <<$0, ($0 + X)>>;
pad_int(X) ->
  integer_to_binary(X).

사실 C로 작성하면 다음과 같이 한 줄로 간단하게 작성할 수 있다.

sprintf(buf, "%02d", X);

저렇게 간단하게 바꿀 수 있다는 사실에 일견 허무해보이기도 하지만 C 언어의 약점은 저 buf에 있다. 메모리 할당이라든가 포인터 관리라든가...

weekday/1 and month/1 Functions

전형적으로 패턴 매칭을 활용한 함수 선언이다.

weekday(1) -> <<"Mon">>;
weekday(2) -> <<"Tue">>;
% ...
weekday(7) -> <<"Sun">>.

month( 1) -> <<"Jan">>;
month( 2) -> <<"Feb">>;
% ...
month(12) -> <<"Dec">>.

Conclusion

cowboy_clock은 cowboy 내에서 독립적으로 실행되는 gen_server 기반의 작은 프로세스이면서 ETS를 활용하고, 패턴 매칭을 많이 사용하는 재미있는 모듈이다. 개인적으로는 update_rfc1123/3 함수의 패턴 매칭이 좀 과하다고 생각하지만, 코드 분석을 하면서 erlang 초보인 나에게는 많은 공부가 된 소스 코드이다.