`cowboy_clock` Module
cowboy는 erlang으로 작성한 대표적인 오픈소스 웹 서버 코드이다. 그 중 cowboy_clock
모듈은 http 통신과 직접 관련은 없지만 gen_server
behaviour에 따라 독립 프로세스로 실행되면서 RFC1123 포맷의 시간 문자열을 내놓는다. RFC1123 표준 문서에 대한 설명은 생략하고, 쉽게 말해 다음과 같은 형식의 문자열이라고 생각하면 된다.
"Sat, 14 May 2011 14:25:33 GMT"
ETS-related Functions
ETS는 erlang OTP 프레임워크에서 지원하는 key-value 저장소라고 보면 된다. in-memory 저장소이며, disk 저장소 버전은 DETS라고 따로 있다. 초기화는 gen_server
용 init/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/0
및 rfc1123/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 초보인 나에게는 많은 공부가 된 소스 코드이다.