 |
얼랭(Erlang) 웹 프로그래밍, Part 3: RESTful 웹 서비스와 Erlang/OTP
|
 |


김석준 sjoonk@gmail.com
웹2.0과 루비온레일스 기반 소프트웨어 개발, 컨설팅을 하는 유스풀패러다임의 대표다. 한때 공직에 근무하다 어릴 적부터 해오던 프로그래밍의 맛을 잊을 수 없어 업종을 전환한 경력을 가지고 있으며, 항상 끊임없이 새로워지려고 노력 중이다. 번역한 책으로 『프로그래밍 얼랭』, 『레일스와 함께하는 애자일 웹 개발』, 『레일스 레시피』가 있다.
|
|
 |
난이도 : 중급 2008년 7월 15일 |
|
연재순서
1회(2008년5월): Yaws와 얼랭
2회(2008년6월): ErlyWeb을 이용한 웹 개발
3회(2008년7월): RESTful 웹 서비스와 Erlang/OTP
[오픈 developerWorks]는 여러분이 직접 필자로 참가하는 코너입니다. 이번 회에서는 Erlang/OTP로 RESTful 웹 서비스를 구현하는 법에 대해 알아봅니다.
이번 회에는 REST와 관련된 주제를 다루어 보기로 한다. REST가 중심이 되겠지만, REST가 무엇이고 어떻게 작동하는지를 장황하게 설명하기보다는, 얼랭이 REST가 제시하는 개념과 어떤 식으로 어울리고 또 얼랭에서는 REST를 어떤 식으로 처리하는지에 중심을 두어 살펴볼 것이다. 이어서 Erlang/OTP의 개념을 간략히 소개한 다음, OTP 기반으로 개발할 경우 견고한 무중단 REST 서버를 쉽게 만들 수 있음을 보이기 위해, 모치웹(mochiweb)이라는 오픈 소스 얼랭 HTTP 툴킷(toolkit)을 사용하여 간단한 OTP 기반 REST 서버를 하나 구현해 볼 예정이다. 그럼 우선 REST의 개념부터 출발해 보자.
REST와 얼랭
REST(Representational State Transfer)는 Roy Fielding 박사가 자신의 논문에서 제시한 개념으로, 웹과 같은 대규모 분산 시스템에 적합한 아키텍처 스타일을 통칭한다. 그런 의미에서 HTTP는 본질적으로 REST의 구현체다. 즉, 웹 그 자체가 바로 REST 구조를 따른다고 할 것이다. 이런 REST에서는 모든 사물이 고유한 URI를 가지는 자원(resource) 형태로 존재하고, 그 자원의 상태(state)가 HTML이나 JSON 같은 여러 표현 방식을 통해 이전(transfer)된다. 이런 방식의 웹 서비스를 가리켜 'RESTful 웹 서비스'라고 부르기도 하는데, 주로 다음과 같은 특징을 가진다.
- 자원은 고유한 URI를 가진다. 예를 들면, 번호가 5번인 책은 http://mysite.com/books/5 같은 URI를 갖는 식이다.
- 각 자원은 하나 이상의 메서드를 통해 접근할 수 있다. 웹에서라면 HTTP의 GET/POST/PUT/DELETE 등의 메서드가 여기에 해당한다.
- 클라이언트와 서버 간에는 여러 가지 표현 방식을 사용하여 자원의 상태를 교환할 수 있다(예: HTML, XML, JSON 등).
- HTTP 명세(RFC 2616)에 맞는 적절한 상태코드(status code)를 반환해야 한다.
REST에 대한 상세한 내용은 참고자료를 참조하길 바라며, 이제 이 개념을 얼랭에서는 어떻게 처리하는지를 알아보자. 지난 회에 소개한 얼랭 웹 서버인 Yaws를 사용한다고 가정하면, Yaws가 호출하는 out/1 함수에서 다음과 같은 식으로 패턴매칭을 통해 REST 호출을 처리할 수 있다.
|
out(Arg) ->
Uri = yaws:api:request_url(Arg), % Arg에서 URI를 추출한다.
Path = string:tokens(Uri#url.path, "/"), % URI에서 경로 부분을 추출하여 리스트로 만든다.
Method = (Arg#arg.req)#http_request.method, % HTTP 요청 메서드를 추출한다.
AcceptHdr = (Arg#arg.headers)#headers.accept, % HTTP Accept 헤더를 추출한다.
out(Arg, Method, Path).
|
일단 REST 응답 처리에 필요한 모든 값을 추출하였으면, 이번에는 실제 요청 URI 및 HTTP 메서드와 매치하는 패턴을 가진 함수를 다음과 같이 1:1로 작성해 주면 된다. 함수의 인자에 대해 패턴매칭을 함으로써 간단하게 해결됨을 알 수 있다.
|
out(Arg, 'GET', ["books", BookId, "comments", Id]) -> ... % GET /books/xx/comments/yy와 매칭
out(Arg, 'PUT', ["books", BookId]) -> ... % PUT /books/xx와 매칭
|
요청 내용 유형에 따라 각기 다른 응답을 제공하고 싶다면 앞서 추출한 AcceptHdr 값을 이용하면 될 것이다. 또한 Yaws의 경우, 응답 결과를 생성할 때 ehtml을 사용할 수 있고, 이 때 ehtml 속에 {status, Code} 튜플을 포함시켜 상태코드를 반환할 수도 있다. 이 부분에 대한 설명은 지면 관계상 생략하니, 필요한 독자는 지난 회를 참조하길 바란다.
Erlang/OTP
얼랭이 다른 함수형 언어들과 구별되는 주요한 특징 중 하나가 바로 이 Erlang/OTP(이하 'OTP')라는 것을 자체에 내장하고 있다는 점이다. OTP는 Open Telecom Platform의 약어지만, 실제로는 견고한 무중단(fault-tolerant) 분산 시스템을 구축하는 데 필요한 각종 라이브러리 모음 내지는 일종의 프레임워크라고 해야 옳을 것이다. 특히 다년간 실제로 99.999% 이상의 신뢰 수준으로 실행되고 있는 실세계 시스템이라는 점에서 더 후한 점수를 줘야 할 것이다.
OTP가 정확히 어떻게 작동하는지, 그리고 OTP가 왜 좋은지 하는 것에 대한 설명은 참고자료에 돌리고, 여기서는 OTP 중에서 주로 범용 서버를 만들 때 자주 사용하는 gen_server 라이브러리(얼랭에서는 이를 'gen_server 비헤비어'라고 부른다)에 대해 잠깐 소개하겠다. 다음 코드는 booksvr라고 하는 서버인데, 책 정보를 키-값 형태로 저장하고 보여주는 간단한 서버다.
|
-module(booksvr).
-behaviour(gen_server).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([start/0, stop/0, store/2, fetch/1, fetch_all/0, update/2, delete/1]).
% 클라이언트 인터페이스
start() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop() -> gen_server:call(?MODULE, stop).
store(Key, Val) -> gen_server:call(?MODULE, {store, Key, Val}).
fetch(Key) -> gen_server:call(?MODULE, {fetch, Key}).
fetch_all() -> gen_server:call(?MODULE, fetch_all).
update(Key, Val) -> gen_server:call(?MODULE, {update, Key, Val}).
delete(Key) -> gen_server:call(?MODULE, {delete, Key}).
% 콜백
init([]) ->
Db = dict:new(),
Db1 = dict:store("1", "Google", Db),
{ok, Db1}.
handle_call({store, Key, Val}, _From, Db) ->
Db1 = dict:store(Key, Val, Db),
{reply, ok, Db1};
handle_call({fetch, Key}, _From, Db) ->
Val = dict:fetch(Key, Db),
{reply, Val, Db};
handle_call(fetch_all, _From, Db) ->
Val = dict:to_list(Db),
{reply, Val, Db};
handle_call({update, Key, Val}, _From, Db) ->
Db1 = dict:store(Key, Val, Db),
{reply, ok, Db1};
handle_call({delete, Key}, _From, Db) ->
Db1 = dict:erase(Key, Db),
{reply, ok, Db1};
handle_call(stop, _From, Db) -> {stop, normal, stopped, Db}.
handle_cast(_Msg, Db) -> {noreply, Db}.
handle_info(_Msg, Db) -> {noreply, Db}.
terminate(_Reason, _Db) -> ok.
code_change(_OldVer, Db, _Extra) -> {ok, Db}.
|
이 서버는 gen_server 비헤비어로 만든 서버다. 이렇게 어떤 서버를 gen_server로 만들면, 서버 프로세스를 띄우고 동시성(concurrency)을 처리하고 트랜잭션이나 핫코드 스와핑(hot code swapping) 같은 고난도 처리를 gen_server가 모두 맡아 해주기 때문에, 프로그래머는 간단하게 gen_server 명세에 나와 있는 콜백 루틴들만 구현해 주는 것만으로 높은 품질의 서버 프로그램을 아주 쉽게 작성할 수 있게 된다. 예를 들어, 앞의 booksvr의 경우 이 서버가 gen_server 비헤비어를 구현하고 있기에, 지금 이 정도 코드만으로도 상당히 안정성을 갖춘 서버가 되는 것이다.
그렇지만 그게 다는 아니다. 앞서 설명했듯이 OTP는 그 자체가 하나의 프레임워크라고 할 수 있으며, 이렇게 말할 때 프레임워크란 바로 견고한 무중단 분산 시스템을 구축할 수 있게 해주는 프레임워크라는 의미다. 그런데 OTP가 이런 견고한 분산 시스템을 구축하기 위해 내부적으로 사용하는 메커니즘은 바로 슈퍼비전 트리(supervision tree)라고 하는 것이다. 쉽게 말해, 앞서 만든 booksvr와 같은 gen_server 모듈들은 큰 슈퍼비전 트리 속에 하나의 구성요소로 두고, 이런 구성요소 여러 개를 감독하는 슈퍼바이저(supervisor)라는 개념을 별도로 두어, 만약 감독하고 있던 어떤 시스템(이를 '워커(worker)'라고 부른다)이 멎으면 슈퍼바이저가 그 멈춘 구성요소를 다시 복구하는 메커니즘이다.
설명이 조금 어려웠을 수도 있는데, 사실 OTP는 얼랭의 핵심이며, 또 그만큼 방대해 모든 내용을 여기에 다 소개할 수는 없는 노릇이다. 대신 간단한 예제를 하나 만들어 가면서 미처 얘기하지 못한 개념을 소개하기로 한다. 예제는 앞서 보인 booksvr에 대한 프론트엔드(front-end)로 RESTful 웹 서비스를 추가하는 것이다. 물론 앞서 소개한 Yaws를 가지고 할 수도 있지만, 독자들에게 새로운 장난감을 소개한다는 취지에서 Yaws보다 경량의 오픈 소스 HTTP 툴킷인 모치웹(mochiweb)을 사용해 보겠다.
RESTful 웹 서비스와 MochiWeb
지금부터 만들어 볼 것은 앞서 소개한 슈퍼비전 트리를 사용하는 OTP 애플리케이션이다. 조금 자세히 말하면, 슈퍼바이저가 하나 있고 이 슈퍼바이저는 프로그램 두 개, 즉 하나는 앞서 만든 booksvr와 나머지는 이제부터 만들 restsvr를 감독한다. 이를 둘러싸는 애플리케이션의 이름은 restbook이라 주었으며, 애플리케이션의 전체 구조는 아래 그림을 참조하면 된다. 부연하자면 그림 속에서 사각형은 슈퍼바이저, 그리고 동그라미는 워커를 각각 표현한 것이며, 밖을 둘러 싼 테두리는 이것들이 모두 모여 하나의 '애플리케이션'을 구성한다는 의미다. 슈퍼바이저(restbook_sup)는 워커들(restsvr과 booksvr)을 감독하다 어떤 워커가 죽으면 재시작시키는 일을 한다.
그림 1. restbook의 구조

이제 차례대로 하나씩 만들어 나가자. 우선 가장 덩치가 큰 restsvr다. restsvr는 모치웹을 HTTP 서버로 사용하는 REST 서버이며, 코드의 내용은 요청 URI 및 메서드와 일치하는 핸들러 함수를 찾아 booksvr를 호출하는 것이다. 지면 관계상 코드에 대한 상세한 설명은 생략하지만, 앞서 소개한 REST와 패턴매칭의 개념만 이해한다면 읽기에 별 어려움은 없을 것이다. 참고로 호출에 대한 응답은 모치웹 속에 들어있는 mochijson 라이브러리를 이용해 JSON 형태로 출력하였다.
|
-module(restsvr).
-export([start/0, stop/0, handle_request/1]).
start() ->
mochiweb_http:start([{port, 8000}, {loop, {?MODULE, handle_request}}]).
stop() ->
mochiweb_http:stop().
handle_request(Req) ->
Path = Req:get(path),
case Req:get(method) of
'GET' ->
case Path of
"/books" ->
Val = booksvr:fetch_all(),
Req:ok({"application/json", mochijson:encode({struct, Val})});
"/books/" ++ Key ->
Val = booksvr:fetch(Key),
Req:ok({"application/json", mochijson:encode(Val)});
_ -> Req:not_found()
end;
'POST' ->
case Path of
"/books" ->
lists:foreach(fun({Key, Val}) -> booksvr:store(Key, Val) end, Req:parse_post()),
Req:ok({"text/plain", "ok"});
_ -> Req:not_found()
end;
'PUT' ->
case Path of
"/books/" ++ Key ->
booksvr:update(Key, Req:recv_body()),
Req:ok({"text/plain", "ok"});
_ -> Req:not_found()
end;
'DELETE' ->
case Path of
"/books/" ++ Key ->
booksvr:delete(Key),
Req:ok({"text/plain", "ok"});
_-> Req:not_found()
end;
_ ->
Req:respond({501, [], []})
end.
|
힘들게 일을 하는 '일꾼(worker)' 두 개를 만들었으니, 이제 감독하는 슈퍼바이저를 만들 차례다. 슈퍼바이저 역시 OTP의 비헤비어 중 하나인 supervisor 비헤비어를 구현해 주면 되기 때문에, 내용은 다음과 같이 간단하다. start_link/0를 호출하면 자동으로 호출되는 init/1 함수에서 슈퍼바이저가 감독할 워커들을 기술하고 있다. 각 옵션의 내용은 참고자료를 참조하기 바란다.
|
-module(restbook_sup).
-behaviour(supervisor).
-export([start_link/0]).
-export([init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
% Callback
init([]) ->
{ok, {{one_for_one, 1, 60},
[{booksvr, {booksvr, start, []}, permanent, 1000, worker, [booksvr]},
{restsvr, {restsvr, start, []}, permanent, 5000, worker, [restsvr]}]}}.
|
이제 일꾼도 만들었고 감독자도 만들었으니 하나의 OTP 애플리케이션으로 묶자. 역시 이번에도 비헤비어를 사용한다. application이라는 OTP 비헤비어를 사용하면 된다. 코드 내용은 애플리케이션을 시작하고 슈퍼바이저를 실행하는 간단한 것이다.
|
-module(restbook).
-behaviour(application).
-export([start/2, stop/1]).
start(_Type, _StartArgs) ->
restbook_sup:start_link().
stop(_State) ->
ok.
|
마지막으로 지금까지 만든 restbook 애플리케이션에 대한 설정 파일(애플리케이션 명세)을 하나 작성해야 한다. 이 파일은 얼랭 런타임에서 이 애플리케이션을 어떻게 읽어 들여야 하는지에 대한 정보가 들어 있으며 다음과 같다. 역시 명세 각각의 항목에 대한 자세한 내용은 참고자료를 참고하자.
|
{application, restbook,
[{description, "a simple restful web-service for booksvr"},
{vsn, "0.01"},
{modules, [
restbook,
restbook_sup,
booksvr,
restsvr
]},
{registered, []},
{mod, {restbook, []}},
{env, []},
{applications, [kernel, stdlib]}]}.
|
이것으로 끝이다. 이제 애플리케이션을 실행하기만 하면 된다. 그전에 한 가지. 모치웹만 사용할 독자라면 지금까지 해 온 것보다 훨씬 더 쉬운 방법이 있다. 모치웹 속에는 new_mochiweb.erl이란 스크립트가 들어 있어, 이 스크립트를 실행하면 방금 전까지 우리가 수행한 작업과 비슷한 모치웹 기반의 OTP 애플리케이션에 대한 골격(skeleton)을 자동으로 생성해 준다. 여기서는 OTP 자체에 대한 설명을 위해 일부러 그 방법을 사용하지 않았다. 관심 있는 독자는 한번 해 보기 바란다.
이제 파일들을 컴파일하고 다음과 같이 얼랭 셸을 시작하여 애플리케이션을 실행하자. erl 명령 다음에 -pa 옵션을 주어 mochiweb의 ebin 디렉터리를 지정하는 것을 잊지 말자(또는 .erlang 파일 속에서 code:add_patha/1 함수를 호출해도 된다).
|
restbook$ erl -pa ~/mochiweb/ebin -boot start_sasl
...
1> application:start(restbook).
|
여기까지 순조롭게 진행되었다면, 아마도 다음과 같이 restbook이 공개한 REST API들에 대한 테스트를 해 볼 수 있을 것이다. 명령행 유틸리티인 cURL을 이용하였으며, 대괄호 속은 호출한 메서드와 URI를 나타낸다.
|
restbook$ curl http://localhost:8000/books [GET /books]
{"1":"Google"}
restbook$ curl -d "2=Yahoo&3=Daum" http://localhost:8000/books [POST /books]
ok
restbook$ curl http://localhost:8000/books
{"2":"Yahoo","3":"Daum","1":"Google"}
restbook$ curl -X PUT http://localhost:8000/books/1 -d "Naver" [PUT /books/1]
ok
restbook$ curl http://localhost:8000/books
{"2":"Yahoo","3":"Daum","1":"Naver"}
restbook$ curl -X DELETE http://localhost:8000/books/1 [DELETE /books/1]
ok
restbook$ curl http://localhost:8000/books
{"2":"Yahoo","3":"Daum"}
|
지금까지의 작업으로 어엿한 RESTful 웹 서비스가 탄생했다. 물론 여기에 소개한 예제는 어디까지나 개념 수준이며, 따라서 조금 더 정교하게 만들어야겠지만, 이 서비스는 레거시(booksvr)에 대한 웹 프론트엔드를 제공하는 서버이면서, 동시에 OTP 애플리케이션이므로 확장성과 안정성 면에서 아주 놀라운 처리 성능을 발휘할 것이다.
마지막으로, 얼랭과 REST에 관심이 있는 독자들을 위해 CouchDB를 소개한다. CouchDB는 얼랭으로 작성된 문서 기반 데이터베이스인데, 이 프로그램이 REST API 부분을 처리하는 데에 있어 바로 지금 우리가 사용했던 모치웹을 사용하기 때문이다. 관심 있는 독자는 꼭 한번 보길 바란다. 많은 것을 얻을 수 있을 것이다.
준비한 내용은 여기까지다. 이것으로 '얼랭 웹 프로그래밍'이라고 하는 다소 거창한 제목으로 진행한 연재를 막을 내리고 독자 여러분과 작별을 고할 시간이 되었다. 필자에게 얼랭은 처음 접할 때 가졌던 선입견(?)과는 달리 빠지면 빠질수록 재미있는 언어였다. 여러분도 그러길 바라며, 다음 번에 또 다른 무언가를 가지고 다시 만나길 기대한다. 그 동안 부족한 글을 읽어주신 독자들에게 감사드린다.
참고자료
이 문서 북마킹 하기

|
| 이제 전문가의 글을 단순히 ‘보는 것’에서, 직접 여러분이 developerWorks의 필자가 될 수 있습니다. IBM developerWorks를 통해 공유하고 싶은 지식이 있으신 분들은 원고 기획안을 접수해주세요. 채택되신 분께는 소정의 원고료를 드립니다. |
|
|
|
[지난 Open dW 보기] |
|
 |
|