----
[스페셜 리포트 | 3부]
얼랭으로 구현된문서 기반
분산 데이터베이스 CouchDB
CouchDB 소개
CouchDB는 Cluster Of Unreliable Commodity Hardware의 약어로, 2008년 2월에 아파치 인큐베이팅 프로젝트에 편입된 문서 기반 데이터베이스이다. 현재 아파치 프로젝트 중에서는 유일하게 얼랭을 언어로 사용하고 있다. 이 프로젝트를 이끌고 있는 이는 예전에 로터스에 근무했고 현재 IBM에 있는 Damien Katz 씨다. 한 번은 그가 블로그에 만우절 농담으로 CouchDB를 자바로 처음부터 다시 작성하겠다[1]고 올렸는데 진짜인 줄 알고 낚인 사람들 덕분에 큰 혼란이 발생한 적이 있다.CouchDB가 얼랭으로 구현되긴 했지만 다른 데이터베이스를 사용할 때 C/C++를 쓰는게 아니라 SQL을 쓰는 것처럼, CouchDB를 사용하기 위해 얼랭을 알아야 할 필요는 없다. 그런데 관계형 데이터베이스가 아닌 문서 기반 데이터베이스이기 때문에, SQL 대신 JSON과 자바스크립트를 사용한다는 것이 대단히 특이한 점이라 할 수 있다. JSON은 데이터 표현에 사용되고, 자바스크립트는 쿼리에 사용된다.
크로스도메인 제한만 문제되지 않는다면 데이터베이스와 통신하는데 서버 사이드 스크립트가 전혀 필요하지 않으며, ORM을 사용할 필요도 없다. 대부분의 플랫폼과 언어들이 HTTP를 지원하고, JSON 파싱도 어려운 일이 아니기 때문에 어떤 환경에서도 쉽게 CouchDB와 인터페이스 할 수 있다. 이런 면에서는 예전에 루비 세미나에서 deepblue님이 발표하신 슬러거[2]와 비슷한 점이 있다.
그러나 REST 스타일의 JSON 프로토콜을 사용한다는 것 이외에도 복제와 MapReduce 프로그래밍 모델이 자연스럽게 지원된다는 것이 가장 큰 특징이다. 2부에서 멀티 코어와 분산 처리의 힘을 충분히 느꼈겠지만, 이것이 데이터베이스에서 지원된다면 또 다른 가능성이 열리는 셈이다. 양방향 복제도 지원되고, 오프라인 모드로 사용하다가 나중에 동기화 하는 것 역시 가능하다.
문서 기반 데이터베이스인 만큼, 풀텍스트 검색 API도 지원된다. 현재 참조 구현으로 루씬을 사용하고 있다.
이번 기사에서 CouchDB의 모든 면을 다루기는 어렵겠지만, 적어도 기술적인 면과 기본적으로 사용하는데 필요한 API에 대해서는 충실히 다룰 것이다. 아직 0.9.0 버전의 인큐베이팅 프로젝트라 덥썩 실제 프로젝트에 투입하긴 곤란하겠지만, 분명 관심을 가지고 지켜볼만한 프로젝트라고 생각한다.
CouchDB 기술 개요
문서 저장소
CouchDB는 여러 개의 데이터베이스를 호스팅하고, 각 데이터베이스는 문서를 저장하도록 되어있다. 각 문서는 데이터베이스 내에서 유일한 이름을 가지고 있고, CouchDB는 이 문서들을 읽고 수정하는데 필요한 REST 스타일의 HTTP API를 제공한다.문서는 CouchDB에서 가장 기본적인 데이터 단위이며, 다수의 필드와 첨부 파일로 구성된다. 문서에는 데이터베이스 시스템이 관리하는데 필요한 메타데이터도 포함되어 있다. 각 문서 필드는 유일한 이름을 가지고 있고, 텍스트, 숫자, 불린, 리스트 등 다양한 타입의 값을 사용할 수 있으며, 텍스트 크기나 필드 수에는 아무런 제한이 없다.
CVS나 서브버전을 쓴다면 익숙하겠지만, CouchDB의 업데이트 모델은 낙관적인 모델이라 잠금을 사용하지 않는다. 만약 여러 명의 사용자가 동시에 같은 문서를 편집하는 상황이 발생하면, 나중에 저장하는 사람은 충돌이 발생했음을 알게 된다. 나중에 넣는 쪽이 최신 버전을 기반으로 다시 써서 넣도록 되어있다.
CouchDB 역시 다른 데이터베이스들처럼 ACID 속성을 가지고 있다. CouchDB는 데이터베이스 파일이 항상 무결한 상태에 있음을 보장하기 위해 절대로 이미 커밋된 데이터나 관련 자료구조를 덮어쓰지 않는다. 이것을 "crash-only" 설계라 하는데, 이런 설계 덕분에 데이터베이스를 중지할 때도 별다른 과정을 거치지 않고 바로 중단시키는 것이 가능하다. 대신에 디스크 용량을 많이 사용하게 되는데, 다행히 디스크 가격은 날이 갈수록 떨어지고 있다.
문서업데이트는 바이너리를 동시에 쓰는 경우를 제외하고는 모두 직렬화된다. 읽기는 절대로 잠금이 걸리지 않으므로, 다른 쓰기나 읽기 때문에 기다려야 할 필요가 없다. 읽기는 MVCC 모델을 사용하기 때문에 읽기의 시작부터 끝까지 일관된 스냅샷을 보게 된다.
문서는 문서 ID(DocID)와 시퀀스 ID를 키로 사용하는 B-트리로 인덱스된다. 모든 업데이트는 새로운 시퀀스 번호를 할당받게 된다. 이 시퀀스 ID는 계속 증가하는 값인데, 데이터베이스의 변경 사항을 추적할 때 사용된다. B-트리 인덱스는 문서가 저장되거나 삭제될 때에도 동시에 업데이트가 가능하다. 이것은 인덱스 업데이트가 언제나 파일 끝에서만 발생하도록 설계되었기 때문이다. 이는 루씬의 데이터 레이아웃과 비슷하다.
다른 데이터베이스는 메타데이터나 각종 데이터를 처리하기 위해 여러 개의 테이블을 참조해야 하는 반면, CouchDB의 문서 모델은 필요한 데이터가 문서 안에 모두 저장되어 있으므로 ACID를 구현하는게 단순하다는 장점이 있다. 메타데이터든 뭐든 버퍼에 몰아넣은 다음, 문서 하나씩 디스크에 쓰기만 하면 되는 것이다.
업데이트가 발생하면 모든 데이터와 연관된 인덱스는 디스크에 플러시 되고, 트랜잭션 커밋이 항상 데이터베이스가 완전 무결한 상태가 되도록 보장한다. 커밋은 아래 2단계로 일어난다.
- 모든 문서 데이터와 연관된 인덱스 업데이트는 동기적으로 디스크에 쓴다.
- 이어서 업데이트 된 데이터베이스 헤더를 쓰고, 각 청크들이 모여서 4K가 채워지면 디스크에 동기적으로 쓴다.
한편으로, 수정이나 삭제가 발생해도 계속 파일 끝에 추가하기만 하기 때문에 낭비되는 공간이 상당히 발생하게 된다. 데이터베이스 정리를 하려면 Compaction이 필요한데, 유효한 데이터만 모아서 복사본을 새로 쓰고, 다 쓰면 예전 파일을 버리는 방식으로 구현되어 있다. 적당한 시점에 데이터베이스 자체적으로 이 작업을 수행하긴 하지만 관리자가 직접 명령을 내릴 수도 있다.
뷰
CouchDB는 비정형 데이터를 다루기 위해 자바스크립트 함수를 이용하여 데이터 가공을 처리한다. 이 자바스크립트 함수를 뷰라고 하는데 MapReduce 모델로 되어있다. 뷰는 CouchDB 문서를 인수로 받아 계산을 수행하면서 키-값쌍을 추가한다. Reduce 함수가 정의되어 있다면 Map 함수에서 발생시킨 키-값쌍을 받아서 키를 기준으로 데이터를 가공한다. 조회 결과는 Map 함수에서 넘겨준 키를 기준으로 정렬된다. 키 값이 null이면 문서 ID를 기준으로 정렬한다.이렇게처리하면 비정형 데이터라도 유연하게 처리할 수 있긴 하지만, 대용량 데이터를 처리하려면 인덱스가 필요하지 않나 생각할 것이다. CouchDB는 뷰 인덱스를 지원한다. 뷰와 함수는 설계 문서 안에 저장되고, 설계 문서는 여러 개의 뷰 함수를 포함할 수 있게 되어있다. 같은 설계 문서에 포함된 뷰는 같은 그룹으로 인덱스된다. 인덱스는 문서 ID와 뷰 함수의 결과 값으로 구성된다.
뷰빌더는 시퀀스 ID를 이용하여 뷰 그룹이 최신 버전인지 확인하고, 만약에 최신 버전이 아닌 경우에는 마지막 변경을 반영했던 시점 이후의 모든 문서를 추적한다. 이렇게 인덱스 갱신 작업이 일어나는 도중에도 읽기나 쿼리는 여전히 동시에 실행할 수 있다. 변경된 문서를 모두 찾아서 반영하고 난 이후에는 뷰 인덱스에 있던 이전 값은 버려진다. 특정 문서가 뷰 함수에 의해 선택된 경우에는 그 함수의 결과 값이 인덱스에 반영된다.
인덱스또한 파일 끝에 계속 추가되기 때문에, 디스크 헤드를 최적으로 움직일 수 있으며 충돌이발생하거나 전원이 나가는 경우에도 인덱스가 깨지는 일은 없다. 인덱스를 업데이트하는 도중에 충돌이 발생한다면 불완전한 인덱스 업데이트는 그냥 버려지고 마지막으로 커밋되었던 상태로부터 다시 인덱스가 만들어진다.
보안과 유효성 검증
CouchDB는 누가 문서를 읽고 수정할 수 있는지 제한할 수 있도록, 간단하면서도 확장 가능한 보안 모델을 가지고 있다.관리자접근: 관리자 계정의 경우 다른 관리자 계정을 만들거나 설계 문서를 수정할 수 있다.
리더접근: CouchDB 문서에 리더 (Reader) 목록을 저장한다. 따로 정의된게 없으면 누구나 읽을 수 있지만, 목록이 정의되어 있는 경우에는 지정된 사용자만 읽기가 가능하다. 뷰에서 접근할 때도 이 접근 제한이 동일하게 적용된다. 접근 권한이 없으면 뷰 조회 결과에서 빠지도록 되어 있다.
업데이트접근: 디스크에 문서를 쓰는 시점에 자바스크립트 함수를 이용하여 유효성 검증이 이루어진다. 유효성 검증을 통과하면 그대로 업데이트를 진행할 수 있는 반면, 그렇지 않은 경우에는 작업이 중단되고 클라이언트는 오류 응답을 받게 된다. 자바스크립트 함수의 매개변수로 사용자 계정 정보와 업데이트 한 문서가 같이 넘어오므로, 이 값을 이용하여 권한을 결정하면 된다. 가령, 최초 작성자만 문서를 수정할 수 있도록 하고 싶다면 간단히 문서의 author 필드와 현재 사용자 계정을 비교하도록 코드를 작성하기만 하면 된다.
분산 업데이트와 복제
CouchDB는 P2P 기반의 분산 데이터베이스 시스템이기 때문에, 접속이 끊어진 상태에서도 공유 데이터를 접근하거나 수정할 수 있고, 양방향으로 변경 사항을 복제하는 것도 가능하다. CouchDB는 마지막 복제가 일어났던 시점 이후에 변경된 문서만 복제한다. 도중에 복제가 실패하더라도 그냥 마지막으로 복제 전송하던 문서부터 다시 시작하면 된다. 특히 모든 변경이 파일 끝에 추가되는 형태로 이루어지기 때문에 복제에 필요한 디스크 헤드의 움직임도 효율적이다.특정기준을 만족하는 문서만 복제하도록 자바스크립트 함수로 필터링해서 일부분만 복제하는 것도 가능하다. 보통 오프라인 모드로 동작해야 할 때 전체 데이터베이스를 복제하기엔 너무 시간이 오래 걸리니, 실제 사용할 부분만 복제하도록 하는 용도로 쓴다.
분산데이터베이스 시스템에서는 충돌을 탐지하고 관리하는 것도 중요한 문제이다. CouchDB는 충돌을 예외적인 상황이 아닌 일상적인 일로 취급한다. 충돌한 문서가 여러 개여도 상관없다. 어차피 그 중에 최종본이 있을 것이고, 나머지 문서는 Compaction이 진행되면서 물리적으로 삭제되기 전까지는 접근이 가능하다. 충돌 문서도 복제, 보안 적용 등 모두 일반 문서와 똑같이 취급한다.
CouchDB UI
CouchDB는 모든 명령을 REST 스타일의 HTTP 프로토콜을 통해 처리하기 때문에, 데이터베이스 관리 화면 역시 별도의 서버사이드 스크립트를 이용하지 않고 순수하게 HTML과 자바스크립트만을이용해서 구현되어 있다. 아래의 모든 관리 화면의 URL은 배포본의 couchdb/share/www 디렉터리와 연관되어 있다. 만약 소스를 추가하거나 변경하고 싶다면, couchdb/share/Makefile의nobase_dist_localdata_DATA 항목을 수정하고 다시 빌드해야 한다./_utils/couch_tests.html 페이지에서는 테스트가 가능하다. 아직 인큐베이팅 단계라서 그런지 실행할 때마다 테스트 결과가 좀 오락가락 하는 모습을 볼 수 있었다.
/_utils/browse 페이지에서 기본적인 데이터베이스 관리가 가능하다. 테스트를 실행할 때마다 기존 DB를 삭제하고 다시 만들기 때문에, 테스트를 실행한 다음 관리 화면으로 들어가보면 아래와 같이 테스트용 DB가 생성되어 있는 것을 볼 수 있다.
그외 데이터베이스 생성/삭제, 문서 생성/삭제 등은 화면에서 좀 눌러보기만 하면 다 알 수 있기 때문에 더 이상의 설명은 생략한다. 그리고 이후 콘솔 진행에서 혼동할 여지가 있는 것을 하나 일러둔다. CouchDB는 호스트명을 구분해서 데이터를 취급하기 때문에 아래에서 localhost로 쓰던 것을 브라우저에서 IP나 도메인명으로 접근하게 되면 다른 내용을 보게 된다.
CouchDB REST API
테스트페이지에서 자바스크립트 쉘[3]을 띄운 다음 이미 정의되어 있는 CouchDB 자바스크립트 클래스를 써서 인터페이스 하는 모습을 보여주는 것도 가능하지만, HTTP를 좀 더 잘 드러낼 수 있도록 curl을 사용해서 API를 소개하도록 하겠다. 응답은 전부 JSON 포맷으로 출력된다.루트(/)로 접근하면 현재 CouchDB 버전이 출력된다.$ curl http://localhost:5984{"couchdb":"Welcome","version":"0.9.0a-incubating"}
데이터베이스 API
먼저 DB 목록을 알아보자. (테스트를 돌리지 않았다면 []로 표시될 것이다.)$ curl http://localhost:5984/_all_dbs["test_suite_db_b","test_suite_db_a","test_suite_db"]"nchovy"라는 이름의 DB를 만들어보자. 주의할 점은 데이터베이스 이름으로 대문자가 허용되지 않는다는 것이다.$ curl -X PUT http://localhost:5984/nchovy{"ok":true}
만약 이미 존재하는 DB를 생성하려고 하면 아래와 같이 오류 응답이 돌아온다.{"error":"database_already_exists","reason":"Database\"nchovy\" already exists."}
방금만든 데이터베이스의 정보를 얻어오려면, URL에 데이터베이스 이름만 덧대어 단순히 GET 요청을 보내기만 하면 된다.
$ curl http://localhost:5984/nchovy{"db_name":"nchovy","doc_count":0,"doc_del_count":0,"update_seq":0,"compact_running":false,"disk_size":4096}
마지막으로 데이터베이스를 삭제하려는 경우에는 HTTP의 DELETE 메소드를 사용하면 된다.$ curl -X DELETEhttp://localhost:5984/nchovy{"ok":true}
데이터베이스 정보를 볼 때는 GET, 생성은 PUT, 삭제는 DELETE, 이렇게 REST 스타일을 충실히 따르고 있음을 알 수 있다. 이는 문서의 경우에도 마찬가지로 적용된다.
문서 API
먼저 nchovy 데이터베이스에 문서를 하나 만들어보자. 이름을 주고 문서를 생성할 때는 아래와 같이 PUT 메소드를 사용하고, 서버에서 생성한 DocID를 이용하려고 하는 경우에는 POST를 사용한다.curl -X PUT -d "{ \\"Subject\" : \"NCHOVYInternet Storm Center \", \\"Author\" :\"xeraph\", \\"Date\" :\"2008-07-17T17:26:33+09:00\", \\"Tag\" : [\"security2.0\", \"alpha version\"], \\"Body\" : \"NCHOVY InternetStorm Center opened.\"}"http://localhost:5984/nchovy/introduction{"ok":true,"id":"introduction","rev":"1754950543"}
이렇게 문서를 생성한 뒤 조회를 해보면 아래와 같이 _id와 _rev 값이 추가된 것을 볼 수 있는데, 이는 CouchDB에서 예약해서 사용하고 있는 필드이다. 앞으로 밑줄이 앞에 붙은 필드는 예약된 필드라고 생각하면 된다. 응답을 그대로 보기는 어려우므로 포맷을 수정했다.
{"_id":"introduction","_rev":"1754950543","Subject":"NCHOVY InternetStorm Center ",(생략)}
업데이트 할 때도 동일하게 PUT 메소드를 사용하는데, 반드시 _rev 속성을 같이 넘겨주어야만 CouchDB가 어느 버전을 기준으로 업데이트 한 것인지 판단할 수 있게 된다. _rev를 넘기지 않는 경우 실제로는 conflict가 아니더라도 conflict 오류를 보게 된다.
curl -X PUT -d "{ \\"_rev\" :\"1754950543\", \\"Subject\" : \"NCHOVYInternet Storm Center \", \\"Author\" :\"xeraph\", \\"Date\" :\"2008-07-17T17:26:33+09:00\", \\"Tag\" : [\"security2.0\", \"alpha version\"], \\"Body\" : \"NCHOVY InternetStorm Center opened. Visit http://nchovy.kr\"}"http://localhost:5984/nchovy/introduction{"ok":true,"id":"introduction","rev":"4077664153"}
삭제할 때 역시 반드시 쿼리스트링으로 rev까지 붙여야 한다. 삭제라고 하지만 실제로는 문서에 _deleted 필드를 true로 쓰는 것이다. 역시 삭제 또한 최신 버전을 기반으로 하지 않은 경우 conflict 오류를 보게 된다.
$ curl -X DELETEhttp://localhost:5984/nchovy/introduction?rev=4077664153{"ok":true,"id":"introduction","rev":"443171557"}
따라서 이 상태에서 문서를 조회하면 아래와 같이 응답의 reason 속성이 missing으로 뜨지 않고 deleted로 뜨게 된다. 그러므로 문서를 복구하고 싶다면 속성만 제거해주면 된다. 실제 삭제는 Compact Database 명령을 강제로 내리거나 일정 크기 이상 삭제된 것이 쌓여서 자동으로 일괄 삭제가 되는 경우에 이루어진다.
$ curlhttp://localhost:5984/nchovy/introduction{"error":"not_found","reason":"deleted"}
문서에 첨부 파일을 추가하는 것도 가능하다. 위에서 했던 방식대로 하면서 인라인으로 추가하는 것도 가능하고, 별도의 API를 사용해서 첨부하는 것도 가능하다. 첨부는 _attachments 속성으로 전달한다. _attachments에서 추측할 수 있겠지만 여기에 해시로 다시 파일 이름과 데이터 쌍이 들어가게 된다. 파일 데이터는 content_type과 실제 데이터가 data 속성으로 들어간다. data는 BASE64 인코딩을 이용한다. 전송 내용은 아래와 같은 모습을 띈다.{
"_id": "attachment_test",
"_attachments": {
"foo.txt": {
"content_type": "text\/plain",
"data":"VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
}
}}
이제 첨부된 파일을 불러올 수 있다.$ curl http://localhost:5984/nchovy/attachment_test/foo.txtThisis a base64 encoded text
그렇지만 원본 파일을 인코딩 하지 않고 올리는 것이 아무래도 더 편한 방법이다. 아래와 같이 Content-Type 헤더에 MIME 타입을 지정해서 PUT 메소드로 올리면 된다. URL에서 문서 ID 뒷부분에 /파일이름?rev=버전을 붙여준다.
$ curl -X PUT --data-binary @logo.gifhttp://localhost:5984/nchovy/attachment_test/logo.gif?rev=1015{"ok":true,"id":"test","rev":"1236583845"}
뷰 API
뷰는 MapReduce 자바스크립트 함수로 정의된다. 뷰는 임시로 정의되는 것(Ad-Hoc View)과 한 번 정의해놓고 계속 쓸 수 있는 것(Permanent View)으로 나뉘어지는데, 후자는 특별히 설계 문서 (Design Document)로 저장해 두어야 한다. 설계 문서는 문서 ID가 _design/로 시작되어야 하고, map과 reduce 속성으로 뷰 함수를 정의한다. map은 필수적이지만 reduce는 옵션이다.테스트를 위해 먼저 아래의 CVE[4] 문서 목록을 넣어보자. NVD[5]의 CVE XML 스키마를 관계형 데이터베이스에 맞춰서 제대로 정규화 하면 17개나 되는 테이블을 맞닥뜨리게 된다. 이런 경우에는 CouchDB 같은 문서 기반 데이터베이스가 편리하다.
일일이 커맨드라인으로 입력하기는 버거울테니 웹 관리 화면을 이용하는게 좋겠다. 최근에 공개된 오라클, 마이크로소프트, 모질라 재단의 취약점을 몇 개씩 간략하게 뽑아봤다. 처음에 CouchDB가 호스트명을 구분해서 데이터를 저장한다고 했던 것을 상기하기 바란다.
| Document ID | Company | Softwares |
| CVE-2008-0106 | "MS" | ["DE", "SQL Server"] |
| CVE-2008-0107 | "MS" | ["DE", "SQL Server"] |
| CVE-2008-2247 | "MS" | ["Exchange"] |
| CVE-2008-2607 | "Oracle" | ["Database"] |
| CVE-2008-2603 | "Oracle" | ["EM"] |
| CVE-2008-2581 | "Oracle" | ["WebLogic"] |
| CVE-2008-2811 | "Mozilla" | ["Firefox", "ThunderBird"] |
| CVE-2008-2934 | "Mozilla" | ["Firefox"] |
먼저 가장 간단한 뷰를 만들어보자. UI에서 _design/cve 라는 이름의 문서를 새로 생성한 다음, views 필드를 만들고 값은 아래와 같이 채운다.
{ "all":
{ "map": "function(cve) { emit(null, cve) }" }}
views 필드에 all이라는 뷰 함수가 정의되었다. map 함수는 키를 null로 해서 모든 문서를 그대로 내보내기 때문에, 조회 결과는 아래와 같이 출력된다. 조회에 사용되는 URL은 "데이터베이스 이름/_view/설계 문서 이름/뷰 이름"의 형식을 따른다.
$ curl http://localhost:5984/nchovy/_view/cve/all{"total_rows":8,"offset":0,"rows":[{"id":"CVE-2008-0106","key":null,"value":{"_id":"CVE-2008-0106","_rev":"948872442","Company":"MS","Softwares":["DE","SQLServer"]}},{"id":"CVE-2008-0107","key":null,"value":{"_id":"CVE-2008-0107","_rev":"191774303","Company":"MS","Softwares":["DE","SQLServer"]}},{"id":"CVE-2008-2247","key":null,"value":{"_id":"CVE-2008-2247","_rev":"3430513177","Company":"MS","Softwares":["Exchange"]}},{"id":"CVE-2008-2581","key":null,"value":{"_id":"CVE-2008-2581","_rev":"1145726275","Company":"Oracle","Softwares":["WebLogic"]}},{"id":"CVE-2008-2603","key":null,"value":{"_id":"CVE-2008-2603","_rev":"1495661849","Company":"Oracle","Softwares":["EM"]}},{"id":"CVE-2008-2607","key":null,"value":{"_id":"CVE-2008-2607","_rev":"3389120079","Company":"Oracle","Softwares":["Database"]}},{"id":"CVE-2008-2811","key":null,"value":{"_id":"CVE-2008-2811","_rev":"2954206185","Company":"Mozilla","Softwares":["Firefox","ThunderBird"]}},{"id":"CVE-2008-2934","key":null,"value":{"_id":"CVE-2008-2934","_rev":"918845149","Company":"Mozilla","Softwares":["Firefox"]}}]}
하지만 대부분의 경우 목록을 출력하더라도 전체를 한 번에 출력하는 대신 페이징을 이용할 것이다. 쿼리 옵션이 몇 가지 있는데, 이 중에 skip과 count 인수를 이용하면 페이징을 구현할 수 있다.$ curl http://localhost:5984/nchovy/_view/cve/all?skip=3\&count=2{"total_rows":8,"offset":3,"rows":[{"id":"CVE-2008-2581","key":null,"value":{"_id":"CVE-2008-2581","_rev":"1145726275","Company":"Oracle","Softwares":["WebLogic"]}},{"id":"CVE-2008-2603","key":null,"value":{"_id":"CVE-2008-2603","_rev":"1495661849","Company":"Oracle","Softwares":["EM"]}}]}
total_rows가 8이라고 나오는데, 이는 total_rows가 가져온 행 수가 아닌 전체 대상 문서의 수이기 때문이다. 가져온 행 수를 알고 싶은 경우에는 그냥 rows 항목의 length를 쓰면 된다.
나머지 쿼리 옵션은 아래 표를 참고하기 바란다. 다른 DB도 마찬가지이지만 최적의 성능을 얻으려면 startkey나 startkey_docid를 지정해서 범위를 줄이는 것이 좋다. 여기에서 말하는 key는 map 함수에서 emit으로 만들어낸 key를 의미한다. startkey_docid는 메일링리스트를 확인해본 결과 지금은 버그 때문에 제대로 동작하지 않는 상태로 보인다.
| 옵션 | 값 | 기능 |
| key | key값 | 특정 키를 조회한다. |
| startkey | key값 | 지정한 키 이전 값은 버린다. |
| startkey_docid | 문서 ID | 지정한 문서 ID 이전 값은 버린다. |
| endkey | key값 | 지정한 키 이후 값은 버린다. |
| endkey_docid | 문서 ID | 지정한 문서 ID 이후 값은 버린다. |
| update | false | 최신 값이 아니어도 상관없다는 의미로, 더 빠른 조회가 가능하다. |
| descending | true | 내림차순으로 정렬을 뒤집는다. |
이번엔 회사별로, 제품별로 조회할 수 있는 쿼리를 만들고 실행해보자. _design/cve 설계 문서를 열고 views 값을 아래와 같이 변경한다.
{ "all": { "map": "function(cve) { emit(null, cve) }" }, "by_company": { "map": "function(cve) { emit(cve.Company, cve) }" }, "by_product": { "map": "function(cve) { for(i = 0; i <cve.Softwares.length; ++i) emit(cve.Softwares[i], cve) }" }}
이제 Oracle에서 발생한 CVE 목록을 뽑아보자.
$ curl http://localhost:5984/nchovy/_view/cve/by_company?key=\"Oracle\"{"total_rows":8,"offset":5,"rows":[{"id":"CVE-2008-2581","key":"Oracle","value":{"_id":"CVE-2008-2581","_rev":"1145726275","Company":"Oracle","Softwares":["WebLogic"]}},{"id":"CVE-2008-2603","key":"Oracle","value":{"_id":"CVE-2008-2603","_rev":"1495661849","Company":"Oracle","Softwares":["EM"]}},{"id":"CVE-2008-2607","key":"Oracle","value":{"_id":"CVE-2008-2607","_rev":"3389120079","Company":"Oracle","Softwares":["Database"]}}]}
이번엔 Firefox 제품과 관련있는 CVE 목록만 뽑아보자.
$ curl http://localhost:5984/nchovy/_view/cve/by_product?key=\"Firefox\"{"total_rows":11,"offset":5,"rows":[{"id":"CVE-2008-2811","key":"Firefox","value":{"_id":"CVE-2008-2811","_rev":"2954206185","Company":"Mozilla","Softwares":["Firefox","ThunderBird"]}},{"id":"CVE-2008-2934","key":"Firefox","value":{"_id":"CVE-2008-2934","_rev":"918845149","Company":"Mozilla","Softwares":["Firefox"]}}]}
지금까지는 설계 문서에 이미 정의된 Permanent View만 사용했는데, 위에서 언급했던 Ad-Hoc View를 사용하는 것도 가능하다. Ad-Hoc View는 URL을 "/데이터베이스 이름/_temp_view"로 지정하고 POST 메소드를 사용하여 위에서 우리가 views 필드에 넣었던 코드를 그대로 입력하면 된다. 지면이 짧은 관계로 구체적인 실행 결과는 생략한다.
끝맺음
원래 처음에는 ErlyWeb과 CouchDB를 이용해서 간단히 블로그를 만들어볼까 했었다. 그런데 막상 원고를 쓰다보니, 그렇게 하면 흥미는 더 있을지 몰라도 CouchDB의 특성을 충분히 소개할 수 없을 것 같다는 생각이 들어서 기술적인 면과 API를 충분히 전달하는데 주안점을 두었다. CouchDB를 사용해서 실제 응용 프로그램을 만드는 예는, 비록 영문이긴 하지만 ToDo 어플리케이션 예제가 다음 링크에 나와있으니 참고하면 되겠다. (http://jan.prima.de/~jan/plok/archives/108-Programming-CouchDB-with-Javascript.html)
루씬을 이용한 풀텍스트 검색이나, 복제 서버의 설정, 뷰 서버를 별도로 설정하여 사용하는 것까지 상세히 소개할 수 있으면 좋았겠지만, 지면도 모자라고 시간적 여유도 많지 않아 여기에서 마치게 되었다.
그동안 나름대로 많은 언어를 접해봤지만, 얼랭만큼 배우면서 재미있었던 언어는 없었던 것 같다. 그러나 얼랭을 사용하면서 가장 어려웠던 점은 국내외를 불문하고 얼랭에 관련된 문서가 극소수라는 점이었다. 마소를 통하여 미력하나마 얼랭을 소개할 수 있게 되어 기쁘고, 지금은 라이브러리도, 문서도 그리 많지 않지만 점점 얼랭을 사용하는 개발자가 많아져서 얼랭으로 대규모 프로젝트를 해볼 수 있는 날이 오길 기대해본다.
참고자료
Apache Couch 공식 사이트 - http://incubator.apache.org/couchdb/CouchDB Wiki - http://wiki.apache.org/couchdb/
Markov Chains using CouchDB's Group Reduce- http://jchris.mfdz.com/posts/107
[1] http://damienkatz.net/2008/04/couchdb_language_change.html
[2] Slugger - 스프링노트 블로그 프론트엔드 : http://myruby.net/pages/391476
[3] http://www.squarefree.com/shell/ 참고
[4] Common Vulnerabilities and Exposures. 표준화 된 취약점 식별자로 생각하면 된다. 표준화 된 식별자가 없으면 서로 지칭하는 것이 명확하지 않아 혼란스럽게 된다.이것이 잘 이해되지 않으면 같은 바이러스인데도 업체마다 다른 바이러스 이름을 사용하는 상황을 떠올려 보면 된다.
[5] National Vulnerability Database. 미 정부 기관NIST에서 운영.




덧글
Lohengrin 2008/09/17 13:11 # 답글
기고도 하시는군요 ^^
민돌이 2008/09/17 22:34 # 답글
이거 원고료 받긴 받았음?(...)
xeraph 2008/09/17 22:50 #
아직 (..먼 산)
고르엡 2008/09/18 01:36 # 삭제 답글
앗 이것은 양봉렬님이 쓰신 그거?(...)
헐랭이 2009/02/01 07:08 # 답글
굿~!
xeraph 2009/02/01 21:43 #
오래된 글이라 깜짝;;
2011/11/16 18:30 # 삭제 답글
비공개 덧글입니다.