Apache CouchDB는 Erlang 기반의 문서 지향 오픈 소스 데이터베이스이다. CouchDB는 각 문서가 자립적으로 존재하는 스키마 없는 데이터베이스로 ID와 개정 번호 이외의 특정 필드는 불필요하다. 데이터베이스를 쿼리하는 것에서부터 데이터베이스에서 데이터를 생성하고 변경하는 것에 이르기까지 모든 조치는 REST 기반 API를 통해 수행된다. CouchDB는 많은 애플리케이션에서 관계형 데이터베이스를 대체할 수 있는 유용한 도구가 될 수 있으며 특히 구조화하기 어려운 데이터를 포함하고 있는 애플리케이션에서는 더 그렇다. 이 기사에서는 Clojure를 사용하여 기본 CouchDB 조작을 수행하고 뷰와 데이터베이스 복제를 사용하여 CouchDB를 쿼리하는 과정을 살펴본다. 또한, 예제 코드를 통해, 상위 레벨에서는 Clutch API를 사용하고, 하위 레벨에서는 더 기본적인 HTTP 라이브러리인 clj-http를 사용하여 Clojure에서 CouchDB REST API를 액세스하는 방법을 설명한다.
이 기사의 예제 코드는 CouchDB 1.0.1, Clojure 1.2.0, Clutch 0.2.4 및 clj-http 0.1.2로 작성되었다. Leiningen 빌드 도구는 예제 코드의 종속 항목을 다운로드하고 설정하는 데 사용되었다. 예제는 코드를 Clojure REPL에서 실행한다는 관점에서 작성되었다.
시작하기 전에 CouchDB를 설치했는지 확인한다(설치 정보는 참고자료 참조). 다양한 운영 체제에서 사전에 패키지된 2진 파일을 사용할 수 있으며
사용 중인 운영 체제에는 기본적으로 이러한 2진 파일이 포함되어 있다. 코드를 실행할 수 있게 환경을 설정하려면 먼저 Leiningen을 설치한다(다운로드 링크는
참고자료 참조). 그런 다음 lein new couchdb-from-clojure 명령을 사용하여 Leiningen 프로젝트를 새로 작성한다. 목록 1과 같은 형태가 되도록
Clutch와 clj-http를 project.clj 파일에 추가한다.
목록 1. Clojure project.clj를 사용하는 CouchDB
(defproject couchdb-with-clojure "1.0.0-SNAPSHOT"
:description "CouchDB from Clojure Examples"
:dependencies [[org.clojure/clojure "1.2.0"]
[org.clojure/clojure-contrib "1.2.0"]
[com.ashafa/clutch "0.2.4"]
[clj-http "0.1.2"]])
|
다음은 lein deps를 실행하여 필요한 JAR 파일을 다운로드한다. 원하는 어떠한 환경에서도 REPL을 통해 이 코드를 실행할 수 있다. Leiningen에서
lein repl을 사용하거나 선택한 IDE를 사용하여 이 파일을 실행할 수 있다. REPL 프롬프트가 표시되면 목록 2에 있는 REPL 세션에 표시된 명령문을 입력하여
이 기사에서 사용할 네임스페이스를 삽입한다.
목록 2. clj-http, contrib.json 및 Clutch 요구
user> (require ['com.ashafa.clutch :as 'clutch]) nil user> (require ['clj-http.client :as 'client]) nil user> (require ['clojure.contrib.json :as 'json]) nil user> (def movies-db "http://localhost:5984/movies") #'user/movies-db |
목록 2에 있는 마지막 명령문에서는 이 기사의 예제에서 사용한 CouchDB 데이터베이스를 액세스할 때 사용할 URL을 정의한다. 로컬에 설치된 CouchDB의 기본 URL은 http://localhost:5984이다. CouchDB의 사본이 다르게 구성된 경우에는 필요에 따라 REPL 세션에서 포트 번호를 변경한다.
CouchDB 데이터는 관계형 데이터베이스와는 상당히 다른 자립형 JSON(JavaScript Object Notation) 문서 형식으로 구조화되어 있다. 영화 데이터베이스를 생각해 보자. 영화를 관계형 데이터베이스로 모델링하려면 영화에 적합한 정보(예: 제목 및 개봉 날짜)를 저장할 테이블이 필요하다. 영화에는 배우, 감독, 제작자 등과 같은 정보가 있지만, 이 영화 테이블에는 이러한 정보를 삽입하지 않는다. 그 대신, 배우 테이블이나 더 일반적인 정보를 담고 있는 영화 참여자 테이블을 작성한 후, 영화 테이블에서 배우 테이블에 있는 행을 참조한다. 구조도 너무 단순하다. 그러나 결합 테이블을 사용하여 다대다 관계를 설정해야 하고 해당 배우가 특정 영화에 적합한지 판별하려면 여러 번 결합을 해야 한다. 이 프로세스를 정규화라고 하며 이 프로세스에서는 데이터를 재구조화하여 중복을 제한한다. 관계형 데이터베이스는 이러한 방식으로 데이터를 처리할 수 있게 조정된다.
CouchDB 영화 데이터베이스에는 특정 영화의 모든 정보가 하나의 문서에 포함된다. 이렇게 하면 더 정규화된 구조와 비교했을 때 일부가 중복될 수 있다. 예를 들면, 배우가 역할을 맡은 각 영화의 정보가 있는 문서마다 배우의 이름이 표시된다. 목록 3에는 CouchDB에 저장되어 있거나 CouchDB에서 추출한 영화 문서의 내용이 표시되어 있다.
목록 3. 영화 데이터에 해당하는 예제 JSON 문서
{"movie-title":"Psycho",
"director":"Alfred Hitchcock",
"runtime":109,
"year-released":1960,
"studio":"Shamley Productions"
"actors":["Anthony Perkins" "Vera Miles" "John Gavin" "Janet Leigh"]}
|
JSON 형식에서는 오브젝트, 배열 및 리터럴을 구분한다. 중괄호({})는 오브젝트를 나타내고 대괄호([])는 배열을 나타낸다. 리터럴은 "Psycho"와 같은
문자열과 1960과 같은 정수이다. 이 형식은 Clojure의 지속적 데이터 구조와 잘 일치한다. Clojure에서는 중괄호를 맵핑에 사용하고 대괄호를 벡터에 사용하며
리터럴도 Javascript와 동일한 구문으로 되어 있다. 표 1에는 JSON 데이터 유형과 이 유형과 일치하는 Clojure 유형이 표시되어 있다.
표 1. JSON 데이터 유형과 일치하는 Clojure 데이터 유형
| 데이터 유형 | JSON 예제 | Clojure 예제 | 설명 |
|---|---|---|---|
| Number | 1 | 1 | 정수와 실수를 표현하는 유형 |
| String | "Example String" | "Example String" | 문자열을 표현하는 유형 |
| Boolean | true/false | true/false | 부울 유형 |
| Array | [1, 2, 3, 4] | [1 2 3 4] | JSON 배열, Clojure 벡터 |
| Object | {"key1" : "value1",
"key2" : "value2"} | {:key1 "value1"
:key2 "value2"}
| JSON 오브젝트, Clojure 맵 |
목록 4에는 CouchDB의 JSON 문서와 이 문서와 일치하는 Clojure 맵 표현이 있다.
목록 4. JSON 오브젝트와 Clojure 맵 비교
;;JSON object for Psycho
{"_id":"Psycho"
"Director":"Alfred Hitchcock",
"runtime":109,
"year-released":1960,
"studio":"Shamley Productions"}
;;Clojure map for Psycho
{:_id "Psycho"
:director "Alfred Hitchcock"
:runtime 109
:year-released 1960
:studio "Shamley Productions"}
|
clojure.contrib.json 라이브러리를 이용하면 Clojure로 JSON을 쉽게 처리할 수 있다. 이 라이브러리는 Clojure의 데이터 구조를 가져와서 JSON 문자열로
변환하거나 그 반대로 동작을 수행한다. 또한, JSON 오브젝트의 맵 항목에 있는 키를 Clojure 키워드로 변환한다. 이 키는 Clojure에서 더
관용적으로 처리된다. 앞으로 살펴볼 HTTP 기반 예제에서는 clojure.contrib.json을 사용한다. Clutch API는 이 라이브러리를 기반으로 하므로
이 API를 사용하면 사용자가 JSON 스펙을 신경 쓰지 않아도 된다.
영화 Psycho에 관한 모든 정보를 CouchDB로 요청할 수 있다고 가정한다. 이렇게 하려면 목록 3과 같은 문서를 저장해야 한다.
CouchDB는 데이터베이스를 많이 유지할 수 있으므로 첫 번째 단계에서는 데이터베이스를 작성한 다음, 문서를 추가한다. 목록 5에서는 movies-db URL에서
데이터베이스를 새로 작성한 후에 문서를 작성한다.
목록 5. Clutch를 사용하는 CouchDB 문서 작성
user> (clutch/create-database movies-db)
{:ok true...}
user> (clutch/with-db movies-db
(clutch/create-document {:director "Alfred Hitchcock"
:runtime 109
:year-released 1960
:studio "Shamley Productions"}
"Psycho"))
{:_id "Psycho" ... }
|
목록 5에는 CouchDB 문서를 작성할 때 API 배후에서 Clutch가 하고 있는 역할 중 일부가 표시되어 있다. create-document로 전달되는 마지막 인수는
문서의 ID이다.
목록 6에는 clj-http를 사용하는 동일한 코드가 표시되어 있다.
목록 6. clj-http를 사용하는 CouchDB 문서 작성
user> (client/put movies-db) ;; Create Database
{:status 201 ... :body "{\"ok\":true}\n"}
user> (->> {:director "Alfred Hitchcock"
:runtime 109
:year-released 1960
:studio "Shamley Productions"}
json/json-str
(hash-map :body)
(client/put (str movies-db "/Psycho")))
{:status 201... :body "{\"ok\":true,
\"id\":\"Psycho\",\"rev\":\"1-ba6b110617a1a8920903b648f208a8fac\"}\n"}
|
CouchDB에서는 같은 데이터베이스를 두 번 작성할 수 없다. 목록 5 및 목록 6에 있는 예제를 모두 실행할 경우에는 데이터베이스를 다시 작성하기 전에
(client/delete movies-db)를 사용하여 해당 데이터베이스를 삭제한다.
목록 6에서는 영화 정보가 있는 해시 맵을 작성한 후, json/json-str을 사용하여 영화 정보를 Clojure 해시 맵에서 JSON 문서로 변환한다. 그러면 clj-http가
요청의 본문으로 인식하는 또 다른 해시 맵 안에 이 문서가 삽입된다. 결국 이 코드는 문서를 저장할 CouchDB를 대상으로 PUT 요청을 실행한다. 문서를 PUT할 때
사용한 URL은 영화 데이터베이스 URL이며 그 다음에는 CouchDB 문서의 ID(이 경우에는 Psycho)가 따라온다.
문서가 제대로 유지되었는지 확인하려면 프로그램 방식으로 CouchDB에서 문서를 검색하여 해당 문서를 조사한다. 목록 7에는 Clutch를 사용하여 문서를 검색한 후, 이 문서를 JSON 응답에서 Clojure 맵으로 변환하는 방법이 표시되어 있다.
목록 7. Clutch를 사용하여 CouchDB에서 문서 검색
user> (clutch/with-db movies-db
(clutch/get-document "Psycho"))
{:_id "Psycho",
:_rev "1-a6b110617a1a8920903b648f208a8fac",
:director "Alfred Hitchcock",
:runtime 109,
:year-released 1960,
:studio "Shamley Productions"}
|
목록 8에는 clj-http를 사용하는 비슷한 예제가 표시되어 있다.
목록 8. clj-http를 사용하여 CouchDB에서 문서 검색
user> (-> (str movies-db "/Psycho")
client/get
:body
json/read-json)
{:_id "Psycho",
:_rev "1-a6b110617a1a8920903b648f208a8fac",
:director "Alfred Hitchcock",
:runtime 109,
:year-released 1960,
:studio "Shamley Productions"}
|
목록 8에 있는 clj-http 코드는 응답의 본문을 가져와서 이 본문을 대상으로 json/read-json을 호출하여 본문을 JSON 응답에서 Clojure 맵으로 변환한다.
또한, 여러 가지 방법을 통해 코드 외부에서 수행되는 작업을 쉽게 확인할 수 있다. 한 가지 방법은 코드에서 사용하는 것과 같은 REST URL을 브라우저에
입력하거나 cURL과 같은 도구를 사용하는 것이다. 앞서 사용한 GET URL(http://localhost:5984/movies/Psycho)을 입력한다.
가장 쉬운 방법은 CouchDB의 Futon 애플리케이션을 사용하는 것이다(참고자료 참조). 이 애플리케이션은 CouchDB와 함께 제공되며 http://localhost:5984/_utils URL을 통해 확인할 수 있다. (CouchDB를 설치할 때 기본 포트를 사용하도록 구성하지 않은 경우에는 이 URL의 포트 번호를 올바른 포트 번호로 대체한다. Futon을 사용하여 문서와 뷰를 작성하고 복제할 수도 있다.
다음은 문서를 CouchDB에 추가하는 과정을 조금 더 심도 있게 살펴본다.
CouchDB 문서 작성 섹션에서는 영화 Psycho에 해당하는 레코드를 작성했다. 적절한 문서 ID를 선택하는 것이 중요하다는 점을 설명하기 위해 데이터베이스에 영화를 새로 추가하는 과정을 생각해 보자. Psycho는 1998년에 다시 영화화되었으므로 목록 9에서와 같이 새 버전을 추가한다.
목록 9. 충돌하는 ID를 사용하여 문서 추가
user> (clutch/with-db movies-db
(clutch/create-document {:director "Gus Van Sant"
:runtime 105
:year-released 1998
:studio "Universal Pictures"}
"Psycho"))
;;409 Conflict
|
목록 9에 있는 코드에서는 데이터베이스에 이미 존재하는 ID를 사용하려고 했기 때문에 이 코드를 실행하면 오류가 발생한다. 모든 CouchDB 문서는 ID에 따라 저장되고 각 문서 ID는 고유하다. 이 경우에는 영화 제목이 고유한 것처럼 보였을 수도 있지만 그렇지 않다. 영화 제목을 기반으로 ID를 설정했기 때문에 충돌이 발생했다. (이 경우에는 하나의 데이터베이스에서만 충돌이 발생한다. 그러나 분산 환경에서는 이러한 문제점이 훨씬 더 많이 발생할 수 있다. 이러한 복제 문제점을 간단하지만 더욱 깊이 있게 다룰 것이다.) 따라서 문서 ID를 지정하는 방법을 다시 검토해 보아야 한다. UUID(Universal Unique Identifier)와 같이 고유성이 보장되는 것을 ID로 사용하는 것이 좋다. Clutch를 사용하는 경우에는 사용자가 ID를 지정하지 않으면 ID가 자동으로 생성된다.
목록 10에는 고유 ID에 대한 필요성 때문에 코드를 변경한 새 문서가 표시되어 있다.
목록 10. 더 나은 영화 ID 선택(Clutch 예제)
user> (clutch/with-db movies-db
(clutch/create-document {:movie-title "Psycho"
:director "Gus Van Sant"
:runtime 105
:year-released 1998
:studio "Universal Pictures"}))
{:_id "d6993381eb5ede34fded2f018b9f10b0",
:_rev "1-29ff788958134c2023d9be94a9231528",
:movie-title "Psycho",
:director "Gus Van Sant",
:runtime 105,
:year-released 1998,
:studio "Universal Pictures"}
|
목록 10에 있는 문서에서는 원래의 ID, Psycho를 movie-title로 바꾸고 ID 키 쌍을 제거했다. 이 문서에는 필드가 두 개 추가되었다. 하나는
_rev인데, 이 필드는 나중에 살펴본다. 다른 하나는 _id이며, 이 값은 목록 9에서의 문제점을 피하기 위해 자동으로 생성한 고유 값이다. 이 값은 자동으로 생성된
것이기 때문에 독자의 _id와 _rev 값은 목록 10에 있는 값과는 다르다.
목록 11은 비슷한 clj-http 코드이다.
목록 11. 더 나은 영화 ID 선택(clj-http 예제)
user> (->> {:movie-title "Psycho"
:director "Gus Van Sant"
:runtime 105
:year-released 1998
:studio "Universal Pictures"}
json/json-str
(hash-map :body)
(client/put (str movies-db "/" (java.util.UUID/randomUUID)))
:body
json/read-json)
{:ok true,
:id "f043a641-045b-4316-83f5-67c8f9bb99c3",
:rev "1-29ff788958134c2023d9be94a9231528"}
|
목록 11에 있는 문서의 대부분은 이전의 clj-http 코드와 동일하지만 ID가 생성되었다는 점이 다르다. 목록 11에서는 JVM에서 생성된 UUID를
사용한다. 목록 11에 있는 문서를 CouchDB로 POST하면 CouchDB는 자동으로 UUID를 생성한 후, 자체 UUID 생성 전략을 사용하여 이 UUID를
해당 문서에 추가한다. CouchDB에서 생성된 UUID를 http://localhost:5984/_uuids URL에서 가져올 수 있다.
문서를 올바르게 작성했는지 검증하려면 모든 문서 ID로 구성된 목록을 데이터베이스에서 검색한다. 목록 12에서는 Clutch를 사용하여 이러한 작업을 수행한다.
목록 12. Clutch를 사용하여 데이터베이스 문서 ID 가져오기
user> (clutch/with-db movies-db
(->> (clutch/get-all-documents-meta)
:rows
(map :id)))
("d6993381eb5ede34fded2f018b9f10b0" "Psycho")
|
목록 13에서는 clj-http를 사용하여 ID 목록을 검색한다.
목록 13. clj-http를 사용하여 데이터베이스 문서 ID 가져오기
user> (->> (str movies-db "/_all_docs"
client/get
:body
json/read-json
:rows
(map :id))
("d6993381eb5ede34fded2f018b9f10b0" "Psycho")
|
목록 12와 목록 13에서는 호출을 통해 CoutchDB에게 영화 데이터베이스에 있는 모든 문서의 메타데이터를 요청하고 자동으로 생성된 ID 하나와
또 다른 이름 Psycho를 리턴한다. 일관성을 유지하기 위해 목록 14에서는 Psycho 문서를 삭제한 다음, 이 문서를 생성된 ID와 함께 다시 추가한다.
목록 14. 이전 키가 있는 문서를 삭제한 후, 다시 추가
(clutch/with-db movies-db
(let [original-psycho (clutch/get-document "Psycho")]
(clutch/delete-document original-psycho)
(-> original-psycho
(assoc :movie-title (:_id original-psycho))
(dissoc :_id)
clutch/create-document)))
{:_id "84bbfce1b0e4cf6c9aa2f4196909f39d", :movie-title "Psycho"...}
|
목록 14에서는 현재의 Psycho 문서를 검색하여 CouchDB에서 이 문서를 삭제한 후, 현재의 ID 값과 함께 movie-title 키를 새로 추가하고 맵에서 ID를
제거하여 문서를 다시 작성한다. ID가 포함되어 있는 경우에는 Clutch가 이 ID를 사용하여 문서를 작성하므로 이 ID를 제거해야 한다.
약간 차이가 있긴 해도 문서를 업데이트하는 과정은 문서를 삽입하는 과정과 거의 동일하다. 문서를 작성하면 개정 번호가 자동으로 주어진다. 목록 15에는 새로 작성된 영화에 대한 결과물이 표시되어 있다.
목록 15. 개정 번호가 있는 예제 문서
user> (clutch/with-db movies-db
(clutch/create-document {:movie-title "Rear Window"
:director "Alfred Hitchcock",
:runtime 112,
:year-released 1955,
:studio "Paramount Pictures"}))
{:_id "1f91c6a2e1af23fa89ca640e889bbdb6",
:_rev "1-43386b891e9ad538de0d16fcb66aff5e",
:movie-title "Rear Window"...}
|
개정 번호는 목록 15에 있는 _rev 맵 항목이다. 이 개정 번호는 사실상 문서의 MD5 해시이며, CouchDB에 의해 자동으로 추가된다. 문서가 변경될 때마다 이 해시가
변경된다. CouchDB 문서를 업데이트할 때는 이 개정 번호가 언제나 있어야 사용자가 변경하는 문서의 어떤 버전이 업데이트 중인지 CouchDB가 알 수 있다. 목록 16에서는
Rear Window 영화 문서를 가져와서 변경한 다음, 문서를 업데이트하여 이 영화에 다른 제목을 추가한다.
목록 16. 문서 업데이트
user> (clutch/with-db movies-db
(-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
(clutch/update-document {:alternate-titles ["La ventana indiscreta"]})))
=> {:alternate-titles ["La ventana indiscreta"]
:_id "1f91c6a2e1af23fa89ca640e889bbdb6",
:_rev "2-6601a377a55d733c0bd111539801edc8",
:movie-title "Rear Window"...}
|
목록 16에서는 UUID를 사용하여 문서를 쿼리하므로 목록 12(또는 목록 13)를 이용하여 UUID를 가져온 다음, 이 UUID를 목록 16에서 사용한다.
목록 16에 있는 update-document 호출은 인수를 두 개 전달 받는다. 하나는 원래의 문서이고 다른 하나는 업데이트된 문서가 CouchDB에 저장되기 전에,
원래의 문서에 병합되는 해시 맵이다. update-document 함수는 사실상 다중 메소드로
update-in과 같은 중첩 구조 업데이트 및 키/값 쌍 병합과 같은 맵 조작에 필요한 다양한 표준 Clojure 접근 방식을 반영하고 있다. 또한, 이 함수는
필요한 변경 작업이 이미 수행되었지만, 개정 번호와 ID는 변경되지 않고 남아 있는 하나의 맵을 인수로 받는다.
동시성 관점에서 보면 목록 16의 접근 방식은 다소 유용하다고 할 수 있다. 이제 목록 17에 있는 코드를 생각해 보자.
목록 17. 충돌이 발생하는 업데이트
user> (clutch/with-db movies-db
(let [client1-rw (clutch/get-document
"1f91c6a2e1af23fa89ca640e889bbdb6")
client2-rw (clutch/get-document
"1f91c6a2e1af23fa89ca640e889bbdb6")]
(clutch/update-document client1-rw
#(conj % "Fenêtre sur cour")
[:alternate-titles])
(clutch/update-document client2-rw
#(conj % "Arka pencere")
[:alternate-titles])))
;; 409 Conflict Error
|
여기에서는 문서를 두 번 검색하며 처음에는 문제 없이 업데이트가 진행된다. 두 번째 업데이트는 "409 오류"가 발생하며 실패한다. 409 오류는 자원의 현재 상태로 인해 충돌이 발생하여 조작이 완료될 수 없다는 점을 호출자에게 중계하기 위해 애플리케이션에서 사용하는 HTTP 충돌 오류 코드이다. 문서가 검색되었을 때, 이러한 문서에는 올바른 개정 ID가 있으므로 첫 번째 업데이트는 제대로 수행되지만, 그 다음에는 두 번째 업데이트 프로그램이 인식할 수 없는 위치에 문서의 새 버전이 있게 되어 두 번째 업데이트는 실패한다. CouchDB에서는 개정 번호가 유효하지 않은 경우에는 문서를 업데이트할 수 없다. 두 번째 업데이트 프로그램은 무엇을 할 수 있을까? 불행히도 두 번째 업데이트 프로그램이 할 수 있는 역할은 목적에 따라 달라진다. 이러한 오류가 발생하지 않도록 하려면 언제나 업데이트를 수행하기 바로 전에 문서를 검색하는 것이다. 목록 17에 있는 코드를 목록 18과 같이 수정하면 업데이트가 제대로 작동한다.
목록 18. 충돌하지 않는 두 개의 클라이언트 업데이트
(clutch/with-db movies-db
(-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
(clutch/update-document #(conj % "Fenêtre sur cour")
[:alternate-titles]))
(-> (clutch/get-document "1f91c6a2e1af23fa89ca640e889bbdb6")
(clutch/update-document #(conj % "Arka pencere")
[:alternate-titles])))
{:movie-title "Rear Window",
:alternate-titles ["La ventana indiscreta"
"Fenêtre sur cour"
"Arka pencere"]
...}
|
이렇게 하면 직면한 문제점을 해결할 수 있지만 그래도 여전히 문제점이 발생할 수 있다. 이러한 시나리오를 처리할 수 있는 코드를 작성해야 한다. 해결책은 요구사항에 따라 문서를 다시 검색하여 새 버전과 재병합하는 것이다. 또 다른 경우, 예를 들어, 사용자가 더 이상 구입할 수 없는 품목을 구입하려고 시도하는 경우에는 사용자에게 오류를 리턴한다.
결국 CouchDB에는 문서를 변경한다는 개념만 있지, 문서의 일부를 변경한다는 개념은 없다. 이는 문서를 변경하면 문서의 해시가 새로 생성되기 때문이다. 이 해시는 CouchDB가 개정 ID를 생성하기 위해 사용한다. 단순히 추가만 하는 변경, 문서의 일부 삭제 및 문서 수정 작업은 동일하게 처리되고 따라서 이러한 작업이 수행될 때마다 개정 번호가 새로 생성된다. 이러한 개념은 데이터베이스를 복제하는 경우에도 중요하다.
CouchDB는 관계형 데이터베이스처럼 SQL을 사용하여 쿼리를 수행하지는 않는다. 기본적으로 데이터 검색은 뷰라고 하는 MapReduce 스타일 코드를 통해 이루어진다. 다양한 언어 중에서 선택하여 뷰를 작성할 수 있다. (기본 언어는 Javascript이다. 다음 예제는 Javascript용으로 작성되었지만, 특정 MapReduce 코드는 다르다.)
이 기사에서는 Clutch와 함께 포함된 Clojure 뷰 서버를 통해 Clojure를 뷰 언어로 사용한다. Clojure 뷰 서버를 사용하려면 CouchDB의 사본에 이 뷰 서버를 설치해야 한다. (Clutch 웹 사이트에 있는 설치 정보를 가리키는 링크는 참고자료를 확인한다.) 뷰 서버는 클라이언트 코드에 추가되는 것이 아니라 해당 서버에 추가된다.
이 기사에서는 먼저 뷰를 작성하여 실행한 다음, 세부적인 내용을 살펴볼 것이다. 먼저, 쿼리할 데이터베이스에 항목이 많이 있도록 목록 19와 같이 문서를 몇 개 더 추가한다.
목록 19. Clutch를 사용하여 문서를 대량으로 추가
user> (clutch/with-db movies-db
(clutch/bulk-update
[{:movie-title "The Godfather"
:director "Francis Ford Coppola"
:runtime 175
:year-released 1972
:studio "Paramount"}
{:movie-title "The Godfather II"
:director "Francis Ford Coppola"
:runtime 200
:year-released 1974
:studio "Paramount"}
{:movie-title "The Godfather III"
:director "Francis Ford Coppola"
:runtime 162
:year-released 1990
:studio "Paramount"}]))
|
목록 19에서는 CouchDB의 대량 업데이트 기능을 사용한다. 대량 업데이트는 새로 작성된 문서를 대상으로 수행되며 기존의 여러 가지 문서를 업데이트한다.
목록 20에 있는 코드에서는 데이터베이스에 있는 모든 영화의 런타임을 표시하는 임시 뷰를 작성하기 위해 모든 문서를 쿼리한다.
목록 20. 임시 뷰 예제
user> (clutch/with-db movies-db
(clutch/ad-hoc-view
(clutch/with-clj-view-server
{:map (fn [doc] (when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
(:runtime doc)]]))})))
{:total_rows 6,
:rows [{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value 105}
{:id "84bbfce1b0e4cf6c9aa2f4196909f39d",
:key "Psycho",
:value 109}
...]}
|
각 영화에서 영화 제목이 관련 런타임과 함께 리턴된다. 또한, movie-title과 runtime이 존재하는지도 확인한다. 이렇게 하는 이유는
이 코드는 각 문서(새로 작성된 문서 포함)를 대상으로 실행되기 때문이다. CouchDB는 정의된 스키마를 사용하지 않으므로 동일한 필드가 모든 문서에
있을 필요는 없다. 쿼리할 필드가 모든 문서에 존재하지 않는 상황이 발생하지 않도록 뷰를 보호하는 것이 좋다.
목록 20에 있는 함수는 여러 벡터 중 하나의 벡터를 리턴한다. 이는 각 문서에서 많은 맵 항목이 0으로 생성될 수 있고 각 맵 항목이 벡터로 표현되기 때문이다. 내부 벡터의
첫 번째 항목은 키(이 경우에는 movie-title)이고 두 번째 항목은 값(이 경우에는 실행 시간을 나타내는 정수)이다.
이 뷰는 앞서 작성한 문서와 비슷한 결과를 출력한다. 이 경우에는 맵 대신 영화 이름과 런타임 값이 출력되고 있지만 개념은 모두 동일하다. 필요한 경우에는
영화의 값을 맵핑할 수 있다. 또한, 출력이 문서와 비슷하다고 하더라도 동일한 고유성 요구사항이 적용되는 것은 아니다. 뷰에서는 key(내부 벡터의 첫 번째 항목)로
출력되는 모든 것이 내부적으로는 key를 출력한 문서의 ID와 쌍을 이룬다. 목록 20의 뷰 출력에서 ID를 확인할 수 있다. 예상대로 데이터베이스에 있는
모든 영화의 런타임이 출력에 표시된다.
지금 살펴본 접근 방식은 뷰를 실행하는 데 약간 문제점이 있다. 첫 번째 이 뷰는 개발용으로 작성된 임시 뷰이다. 마지막으로 실행되고 나서 문서가 변경되지 않은 경우에도 뷰를 실행할 때마다 데이터베이스에 있는 각 문서가 다시 조사된다. "각 문서를 다시 확인"하여 각 문서를 함수에 전달되고 그 결과를 출력 맵에 추가한다. 두 번째, Clojure 코드를 통해서만 쿼리를 실행하도록 제한된다.
이러한 문제점을 모두 수정하려면 목록 21과 같이 뷰를 계속 유지해야 한다.
목록 21. Clutch를 사용하여 CouchDB 저장
user> (clutch/with-db movies-db
(clutch/save-view "movies" "runtimes"
(clutch/with-clj-view-server
{:map (fn [doc] (when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
(:runtime doc)]]))})))
{:_id "_design/movies",
:language "clojure",
:views {"runtimes" ...}}
user> (clutch/with-db movies-db
(clutch/get-view "movies" "runtimes"))
{:total_rows 6,
:rows [{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value 105}
{:id "84bbfce1b0e4cf6c9aa2f4196909f39d",
:key "Psycho",
:value 109}
...]}
|
뷰를 계속 유지하여 쿼리를 처음 실행했을 때, 그 쿼리 결과를 저장하고 문서가 변경될 때만 업데이트한다. 그리고 모든 사용자가 Clojure, 웹 브라우저 및 기타 언어를 사용하여 쿼리를 실행하도록 할 수 있다. 목록 21에 있는 코드는 목록 20과 동일한 결과를 리턴하지만, 캐싱 면에서는 더 우수하며 다른 언어나 Futon 또는 브라우저를 통해 재사용할 수 있다.
뷰를 사용하면 수동으로 데이터를 쿼리할 때에 비해 명확한 성능상의 이점을 얻을 수 있다. 이 기사의 앞 부분에서는 문서의 키만으로 데이터베이스를 쿼리했으며
문서 목록을 가져오거나 사전에 키를 파악하여(데이터베이스에서 영화 제목을 키로 사용한 경우) 키를 확인했다. 찾고자 하는 특정 문서의 ID를 알고 있는
경우에는 쿼리가 신속하게 수행된다. 그러나 일반적인 경우와 같이 ID를 모르고 있는 경우에는 쿼리가 느리게 수행된다. 영화 데이터베이스에는 문서가 단지 몇 개만
들어 있기 때문에 임시 뷰도 쿼리 결과를 신속하게 리턴한다. 수천이나 수백 개의 문서가 있는 데이터베이스에서는 모든 문서를 대상으로
map 함수를 실행하면 시간이 많이 소비된다. 따라서 이러한 뷰가 유용하려면 뷰의 결과를 반드시 저장해야 한다.
Clutch를 사용하여 뷰를 저장하기 위해 목록 21에서는 값을 처리하는 함수와 더불어
map의 단일 키/값 쌍이 있는 Clojure 맵과 두 개의 문자열을 전달하는 save-view 함수를 사용했다. Clutch를 사용하면 뷰 문서를 저장하는 평범한 작업을 신경 쓰지
않아도 된다. 이러한 뷰는 사실상 일반 CouchDB 문서로 저장되지만 특별한 이름을 갖고 있다.
목록 22는 clj-http를 사용하여 뷰 문서를 작성하는 예제이다.
목록 22. clj-http를 사용하여 뷰 저장
user> (->> {:language "clojure"
:views {:runtimes
{:map "(fn [doc]
(when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
(:runtime doc)]]))"}}}
json/json-str
(hash-map :body)
(client/put (str movies-db "/_design/movies/")))
{:status 201
...
:body "{\"ok\":true...}\n"}
user> (-> (str movies-db "/_design/movies/_view/runtimes")
client/get
:body
json/read-json
:rows)
[{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value 105}
...]
|
목록 22에 있는 코드에서 몇 가지 흥미로운 점을 발견할 수 있다. 첫째, 이 코드는 이제까지 CouchDB에 저장한 그 밖의 모든 것과 똑같은
CouchDB 문서이다. 다른 점은 이 코드가 특별히 이름이 지정된 문서(이 경우에는 _design/movies)에 저장된다는 사실이다. CouchDB 디자인 문서는
뷰가 포함되어 있는 CouchDB 문서이다. 이 문서의 이름은 _design으로 시작된다. 디자인 문서에는 language 특성(이 경우에는 Clojure)과
디자인 문서에서 찾을 수 있는 특정 뷰의 맵이 포함되어 있는 views 특성이 있다. 목록 21에서 Clutch를 사용하여 save-view를 호출하면
첫 번째 매개변수 두 개를 사용하여 뷰의 이름과 디자인 문서를 정의한다. 맵의 이 뷰 섹션은 관련된 많은 쿼리를 수용하기 위한 것이다. 런타임 뷰에는
Clutch API를 사용하여 정의한 원래의 함수와 비슷한 형태의 뷰와 연관된 맵이 있다. 이제 특별히 이름이 지정된 CouchDB 문서를 작성하는 과정을 통해 뷰가 작성되었다. 목록 22의
두 번째 파트에서는 특수 URL을 사용하여 뷰의 결과를 가져온다.
이전 예제에서는 뷰에서 리턴한 모든 결과를 검색하는 방법을 설명했다. 이 방법은 유용하지만 원하는 것이 아닐 수도 있다. 영화 제목을 데이터베이스의 키로 선택한 원래의 이유를 생각해 보자. 영화 제목이 고유하지 않은 경우에도 영화 제목으로 영화 데이터베이스를 쿼리하고자 하는 것이 타당한 것처럼 보일 수 있다. 이러한 목표를 염두에 두면 영화 제목을 키로 사용하는 완전한 영화 문서를 리턴하는 뷰를 작성할 수 있다. 이번에는 하나의 숫자를 리턴하는 대신 완전한 문서를 리턴한다는 점을 제외하면 이 뷰는 앞서 살펴본 코드와 비슷하다. 목록 23에는 뷰를 작성하여 영화 제목으로 뷰를 쿼리하는 코드가 표시되어 있다.
목록 23. Clutch를 사용하여 영화 제목으로 쿼리하기
user> (clutch/with-db movies-db
(clutch/save-view "movies" "by_title"
(clutch/with-clj-view-server
{:map (fn [doc] (when (and (:movie-title doc)
(:runtime doc))
[[(:movie-title doc)
doc]]))})))
user> (clutch/with-db movies-db
(:rows (clutch/get-view "movies" "by_title" {:key "Psycho"})))
[{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value {:_id "d6993381eb5ede34fded2f018b9f10b0",
:movie-title "Psycho",
:director "Gus Van Sant",
...}
...}]
|
특정 영화를 쿼리하는 것과 모든 영화를 쿼리하는 것 간의 유일한 차이점은 쿼리 매개변수에 있다. 목록 23에 있는 Clutch 코드에서는 쿼리 매개변수 맵을 사용한다. 이렇게 하면 목록 24에 있는 clj-http 쿼리에서와 같이 쿼리 문자열에 URL을 사용할 수 있다.
목록 24. clj-http를 사용하여 영화 제목으로 쿼리하기
user> (->> "\"Psycho\""
java.net.URLEncoder/encode
(str movies-db "/_design/movies/_view/by_title?key=")
client/get
:body
json/read-json
:rows)
[{:id "d6993381eb5ede34fded2f018b9f10b0",
:key "Psycho",
:value {...}
...}]
|
CouchDB에는 오름차순 정렬, 내림차순 정렬, 키 범위 및 제한과 같은 다양한 쿼리 옵션이 있다. 또한, 키가 목록이나 맵과 같은 다른 JSON 구조가 될 수도 있다. CouchDB 뷰에 대한 자세한 정보는 참고자료를 참조한다.
map 함수만을 사용하여 뷰를 통해 데이터를 쿼리해도 대부분의 개발자가 필요로 하는 것을 처리할 수 있을 것이다. 그러나 때로는 정보를 수집하고자 할 수도 있다. 평균, 합계와 같은 데이터 요약 유형은 map 함수만으로는 처리할 수 없다. CouchDB는 이러한 용도로 사용할 수 있는 reduce 함수를 제공한다. 목록 25에는 특정 스튜디오의 데이터베이스에 있는 총 영화 수를 표시하는 뷰를 작성하는 예제가 표시되어 있다.
목록 25. reduce 함수를 사용하는 뷰
user> (clutch/with-db movies-db
(clutch/save-view "movies" "studio"
(clutch/with-clj-view-server
{:map (fn [doc] (when (:studio doc)
[[(:studio doc) 1]]))
:reduce (fn [keys vals rereduce]
(if rereduce
(apply + vals)
(count vals)))})))
user> (clutch/with-db movies-db
(clutch/get-view "movies" "studio"))
{:rows [{:key nil, :value 6}]}
user> (clutch/with-db movies-db
(clutch/get-view "movies" "studio" {:key "Paramount"}))
{:rows [{:key nil, :value 3}]}
|
목록 25에 있는 reduce 함수는 인수를 세 개 받는다.
- 첫 번째 인수인
keys는map함수에서 작성된 키의 목록이다. 키 목록은 목록 25에서 출력된studio로만 구성되는 것이 아니라 이 studio와 ID로 구성된다. - 두 번째 인수인
vals는 이 함수로 전달된 키 값으로 구성된 목록이다. 이 경우에는 이 인수가 출력된 일련의 키 값이 된다. - 세 번째 인수인
rereduce는 이reduce함수가map함수에서 얻은 원시 결과와 수집 정보를 조작하는지 여부와 관계가 있다.
이 reduce 함수를 이해하려면 CouchDB에서 이러한 결과를 저장하는 방식을 다소 알아야 한다. reduce 함수의 호출 결과는 B-Tree에 저장되며
B-Tree에서는 데이터가 루트에 근접할수록 요약 레벨이 높아진다. studio 뷰를 처음 호출하면 6이 리턴된다. 이 값은 이 트리의 루트에서 가져온 뷰이다. 이 시점에서
키를 출력하면 데이터베이스에 있는 각 문서의 [studio doc-id] 쌍을 확인할 수 있다. 루트에서 아래쪽으로 트리를 더 내려감에 따라
다른 요약(루트에 있는 요약보다 레벨이 더 낮은 요약)을 출력할 수 있다. 목록 25에 있는 두 번째 뷰 호출에서는 "Paramount" 스튜디오 영화를 요청한다. 이렇게 하면
로그 시간으로 트리를 순회하여 루트 노드의 스튜디오 영화 요약에 가장 근접한 것을 찾는다. 이 구조는 성능을 높이는 데 목적이 있다. 데이터를 변경하거나
값을 계산해야 하는 경우에도 반드시 모든 계산을 다시 실행하지 않아도 요약을 사용할 수 있다. 이 구조도 rereduce 매개변수의 기반이 된다. 계산 중인 노드의
하위 노드가 이미 계산된 경우(그리고 이 예제에서는 개별 값 1이 아닌 경우)에는 rereduce 매개변수가 true가 된다.
CouchDB에서는 인스턴스를 많이 확장하여 클러스터에 있는 다양한 노드 간에 점진적으로 복제할 수 있다. 이러한 작업은 모두 CouchDB의 복제 기능을 기반으로 수행되며 이러한 기능은 분산된 CouchDB 데이터베이스를 확장하는 작업 이외의 분야에서도 유용하다. CouchDB에서 데이터를 복제하는 과정은 API 관점에서 1단계 프로세스로 진행된다. 복제는 로컬 데이터베이스나 원격 데이터베이스 또는 이런 것들이 조합된 형태로 수행될 수 있다. CouchDB를 이용하면 이제까지 사용했던 것과 동일한 REST 인터페이스를 사용하여 데이터베이스 레벨에서 데이터를 쉽게 복제할 수 있다. 여기에서는 이 기능을 적용하여 CouchDB를 확장하는 방법 보다는 CouchDB의 복제 기능과 이 기능을 프로그램 방식으로 사용하는 방법을 집중적으로 살펴볼 것이다. (CouchDB를 확장하는 방법에 대한 자세한 정보는 참고자료를 확인한다.)
CouchDB에서는 기존 데이터베이스(로컬 또는 원격 데이터베이스) 간에 복제가 이루어질 수 있다. 복제를 하기 전에 이러한 두 데이터베이스 간에 요구되는 계통적 요구사항은 없다. 복제는 Futon 애플리케이션이나 프로그램 방식으로 수행된다. 성능에 일부 문제점이 있어서 CouchDB 데이터베이스의 두 번째 인스턴스를 추가하여 수요를 맞추어야 한다고 가정하자. 테스트를 수월하게 하려면 목록 26과 같이 또 다른 로컬 데이터베이스로 데이터를 복제하면 된다.
목록 26. Clutch 복제 예제
user> (def moviesb-db "http://localhost:5984/movies-b")
#'user/moviesb-db
user> (clutch/create-database moviesb-db)
{:ok true,...}
user> (clutch/replicate-database "movies" "movies-b")
{:ok true...}
user> (let [movie-ids (clutch/with-db movies-db
(->> (clutch/get-all-documents-meta)
:rows
(map :id)))
movie-b-ids (clutch/with-db moviesb-db
(->> (clutch/get-all-documents-meta)
:rows
(map :id)))]
(= movie-ids movie-b-ids))
true
|
목록 26의 코드에서는 비어 있는 데이터베이스(movies-b)를 새로 작성한 다음, movies 데이터베이스를 이 데이터베이스로 복제한다. 이렇게 하는 이유는
이 두 가지 데이터베이스의 이름이 같아서는 안 되기 때문이다. 그런 다음, 모든 문서 ID를 가져와서 이것들이 모두 동일한지 확인한다. 단지 복제 작업만 수행한
것이므로 모든 문서 ID가 동일해야 한다. 목록 27에는 clj-http를 사용하는 동일한 코드가 표시되어 있다.
목록 27. clj-http 복제 예제
user> (def moviesb-db "http://localhost:5984/movies-b")
#'user/moviesb-db
user> (client/put moviesb-db)
{:ok true,...}
user> (->> {:source "movies" :target "movies-b"}
json/json-str
(hash-map :body)
(client/post "http://localhost:5984/_replicate"))
{:ok true...}
user> (let [movie-ids (->> (str movies-db "/_all_docs")
client/get
:body
json/read-json
:rows
(map :id))
movie-b-ids (->> (str moviesb-db "/_all_docs")
client/get
:body
json/read-json
:rows
(map :id))]
(= movie-ids movie-b-ids))
true
|
목록 27에서는 소스와 대상이 포함된 JSON 문서를 게시한다. 여기서부터는 CouchDB가 JSON 문서를 처리한다. 데이터가 두 개이면 장점과 단점이 동시에 존재한다. 두 번째 데이터베이스를 추가했기 때문에 CouchDB는 요청을 더 신속하게 처리할 수 있다. 그러나 데이터베이스가 변경되면 어떻게 될까? 예를 들어, 목록 28과 같이 각 데이터베이스에 영화를 새로 추가한 후, 데이터베이스 간에 복제를 수행해 본다.
목록 28. 두 개의 데이터베이스에 문서를 새로 추가
user> (clutch/with-db movies-db
(clutch/create-document
{:movie-title "Vertigo"
:director "Alfred Hitchcock",
:runtime 128,
:year-released 1958,
:studio "Paramount Pictures"}))
{:_id "728b2293180e0be566cea3f3127b6cf3"...}
user> (clutch/with-db moviesb-db
(clutch/create-document
{:movie-title "North by Northwest"
:director "Alfred Hitchcock",
:runtime 131,
:year-released 1959,
:studio "MGM"}))
{:_id "386d0400e336e54933a47aec656289c4"...}
(clutch/replicate-database "movies" "movies-b")
(clutch/replicate-database "movies-b" "movies")
|
목록 28의 첫 번째 명령문 두 개를 제외하면 데이터베이스 두 개는 서로 다르다. movies 데이터베이스에는 movies-b에는 없는 Vertigo 문서가 있고
movies-b에는 movies에는 없는 North by Northwest 문서가 있다. 이러한 문서 두 개는 생성된 ID가 서로 다를 뿐만 아니라 나타내는 영화도 서로 다르다. 문서가
서로 다르기 때문에 아무런 문제 없이 복제가 완료된다. 복제는 한 방향으로만 진행된다는 점에 유의한다. movies에서 movies-b로 복제가 수행되면
movie-b에 있는 문서는 아무 것도 이동되지 않는다. 따라서 North by Northwest 문서의 사본을 얻으려면 마찬가지로 movies-b에서 movies로 복제를
수행해야 한다. 새 문서가 데이터베이스 두 개에 모두 존재하는지 확인하기 위해 목록 27에 있는 코드를 다시 사용하여 데이터베이스 두 개의 문서 목록을 비교한다.
방금 설명한 시나리오는 기본적인 시나리오이다. 여기서는 새 문서만 다루고 있지 충돌에 대해서는 다루고 있지 않다. 또 다른 기본 시나리오는 소스에서 업데이트되었지만 대상에서는 업데이트되지 않은 문서를 복제하는 경우이다. 서로 다른 두 개의 데이터베이스에서 문서를 수정한 다음, 복제하면 어떻게 될까? 이러한 시나리오에서는 복제 과정에서 오류가 발생할 수 있다. 이러한 문제점을 해결할 수 있는 가장 손쉬운 방법은 업데이트 과정이 충돌하지 않도록 데이터베이스를 설계하는 것이다. 문서 지향 데이터베이스 설계에서는 가능한 문서를 독립적으로 작성하는 것이 좋다. 새 정보를 새 문서에 삽입되도록 하여 문서를 업데이트하는 작업이 필요하지 않도록 데이터베이스를 설계할 수 있을까? 이렇게 하는 것이 언제나 가능한 것은 아니지만 이렇게 할 수만 있으면 복제를 훨씬 더 수월하게 수행할 수 있다. 목록 29에 있는 예제에서는 각 데이터베이스에 있는 문서에 새 키를 추가한다. 하나는 재상영된 해에 해당하고 다른 하나는 영화의 사운드 믹싱 정보에 해당한다.
목록 29. 복제 충돌
user> (clutch/with-db movies-db
(-> (clutch/get-document "386d0400e336e54933a47aec656289c4")
(clutch/update-document {:re-released 1996})))
{:re-released 1996
:movie-title "North by Northwest"...}
user> (clutch/with-db moviesb-db
(-> (clutch/get-document "386d0400e336e54933a47aec656289c4")
(clutch/update-document {:sound-mix "Mono"})))
{:sound-mix "Mono"
:movie-title "North by Northwest"...}
user> (clutch/replicate-database "movies" "movies-b")
{:ok true...}
|
목록 30에 있는 결과를 확인할 때까지는 모든 것이 적절하게 복제된 것처럼 보인다.
목록 30. 충돌이 발생한 복제 확인
user> (clutch/with-db moviesb-db
(keys (clutch/get-document "386d0400e336e54933a47aec656289c4")))
(:movie-title :director :_conflicts :_rev :language
:runtime :studio :_id :sound-mix :year-released)
|
re-released 키가 없는 것으로 보아 movies 데이터베이스가 업데이트되면서 이 키가 사라진 것처럼 보인다. movies가 movies-b로 복제될 때, 충돌이
발생한 것이다. 개정 번호가 서로 다른 동일한 문서를 복제하려고 했기 때문에 충돌이 발생한 것이다. CouchDB에서 이와 같은 충돌이 발생해도 정보는
손실되지 않는다. 그 대신 CouchDB에서는 Clutch를 사용하여 검색할 수 있는 문서와 연관된 충돌 레코드를 목록 31과 같이 새로 작성한다.
목록 31. 충돌 확인(Clutch 예제)
user> (clutch/with-db moviesb-db
(clutch/get-document "386d0400e336e54933a47aec656289c4" {:conflicts true}))
{:movie-title "North by Northwest",
...
:_conflicts ["2-ac7e4d143dff32f7be437de99a659ba1"]
...}
|
목록 32에는 clj-http를 사용하는 동일한 코드가 표시되어 있다.
목록 32. 충돌 확인(clj-http 예제)
user> (-> (str moviesb-db "/386d0400e336e54933a47aec656289c4?conflicts=true")
client/get
:body
json/read-json)
{:movie-title "North by Northwest"
...
:_conflicts ["2-ac7e4d143dff32f7be437de99a659ba1"]
...}
|
목록 31과 목록 32에는 충돌이 일어났으며 충돌이 문서의 특정 개정 번호(이 경우에는 2-ac7e4d143dff32f7be437de99a659ba1)와 관련이 있다고
표시되어 있다. 충돌이 발생한 특정 개정 번호를 가져와서 이 개정 번호를 직접 업데이트할 수 있다. 목록 33에서는 Clutch를 사용하여 이 작업을 수행한다.
목록 33. 충돌한 문서 가져오기(Clutch)
user> (clutch/with-db moviesb-db
(keys (clutch/get-document "386d0400e336e54933a47aec656289c4"
{:rev "2-ac7e4d143dff32f7be437de99a659ba1"}
#{:rev})))
(:_id :_rev :re-released :movie-title :director
:runtime :year-released :studio)
|
목록 34에는 clj-http를 사용하는 동일한 코드가 표시되어 있다.
목록 34. 충돌한 문서 가져오기(clj-http)
user> (-> (str moviesb-db
"/386d0400e336e54933a47aec656289c4"
"?rev=2-ac7e4d143dff32f7be437de99a659ba1")
client/get
:body
json/read-json
keys)
(:re-released :movie-title :director :_rev :language
:runtime :studio :_id :year-released)
|
keys 목록에는 sound-mix 키/값 쌍이 포함되어 있지 않지만 re-released 키/값 쌍은 포함되어 있다. 이러한 충돌을 해결하는 부담은
개발자가 지게 된다. 또한, CouchDB 문서 업데이트 섹션에서 사용한 것과 같은 규칙이 여기에도 적용된다. 문서를 조금이라도 변경하면
개정 ID가 새로 생성되기 때문에 충돌이 일어날 수 있다. 문서의 다른 영역을 변경해도 충돌이 자동으로 해결되지는 않는다. 이러한 문제점을 해결하는 단계는
다음과 같다.
- 현재의 문서를 읽는다.
- 충돌한 이전 버전을 읽는다.
- 특정 도메인에 적합한 병합 논리를 적용한다.
- 문서를 병합된 새 버전으로 업데이트한다.
- 충돌하는 문서 버전을 제거한다.
5단계에는 문서가 어떻게 검색되었는지를 반영하는 추가 revision 매개변수가 있지만 이 단계는 다른 문서를 삭제하는 단계와 동일하다.
복제 과정에서 발생하는 충돌의 핵심은 오류를 처리하는 과정이 도메인에 따라 다르다는 데 있다. 이 예제에서는 업데이트를 서로 병합하고 title 필드를 교대로 결합한다. 이러한 논리에는 많은 대안들이 있다. 언제나 가장 최근에 수행된 업데이트를 취하는 또 다른 방법도 있다. 이 방법은 영화 수익과 같은 문서의 경우에 합당하다. 영화의 가장 최근의 수익 정보만 필요할 수도 있기 때문이다. 다른 경우에는 언제나 첫 번째 업데이트가 수행되기를 원할 수도 있다.
간단한 JSON 문서 형식과 REST API 및 Clutch의 멋진 Clojure 지원 기능을 이용하면 Clojure를 사용하여 CouchDB를 액세스하는 매력적인 작업을 수행할 수 있다. Clojure로 CouchDB 뷰를 작성할 수 있게 되면 애플리케이션에서 지원할 언어가 하나 더 적어지고 코드의 연속성이 증가한다. CouchDB의 REST 기초를 확실하게 이해하면서 Clutch에서 제공하는 추상화를 이용하면 CouchDB 기반 애플리케이션을 신속하게 개발하고 유지보수할 수 있다.
교육
-
CouchDB - The Definitive Guide(J. Chris Anderson, Jan Lehnardt 및 Noah Slater, O'Reilly Media, 2010년 1월): 언어와 무관한 CouchDB 정보를 확인하려면
이 책을 탐구하자. (온라인 버전은 무료로 사용 가능하다.) CouchDB를 클러스터링하는 방법에 대한 자세한 정보는 Chapter 19를 참고한다.
-
"Introducing clj-http, a Clojure HTTP Client"(Mark McGranaghan, 2010년 8월): 이 기사에는 clj-http가 소개되어 있다.
-
CouchDB Wiki: CouchDB 설치, Futon 및 HTTP View API 관련 섹션이 있는 Apache CouchDB 사이트를 방문해 보자.
-
Clojure: 온라인 Clojure 문서를 확인해 보자.
-
"The Clojure programming language"(Michael Galpin, developerWorks, 2009년 9월): Clojure를 시작하여 Clojure의 구문 중 일부를 배우고 Eclipse용 Clojure 플러그인을
활용해 보자.
-
"Java development 2.0: REST up with CouchDB and Groovy's RESTClient"(Andrew Glover, developerWorks, 2009년 11월): CouchDB의 기본사항을 자세히 배우고
Groovy로 CouchDB 작업을 수행하는 방법을 찾아보자.
-
"Java technical podcast series"(Andrew Glover, developerWorks): Java 및 관련 기술에 대한 이 기술적인 팟캐스트 시리즈에는
Stuart Halloway와의 Clojure 관련 인터뷰와 CouchOne의 Aaron Miller 및 Nitin Borwankar와의 인터뷰가 포함되어 있다.
-
Clutch on Github: 뷰 서버 설정 지시사항과 몇 가지 사용법 관련 예제는 READ를 참조한다.
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술: Java 프로그래밍의 모든 특성을 다루고 있는 수백 편의 기사를 찾아보자.
제품 및 기술 얻기
-
CouchDB: CouchDB를 얻자.
-
Clojure: Clojure를 다운로드하자.
-
clj-http on Github: clj-http를 다운로드하자.
-
Clutch: Clutch를 다운로드하자.
-
Leiningen: Clojure용 Leiningen 빌드 도구를 다운로드하자.
토론
- developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.

Ryan Senior는 Revelytix의 수석 엔지니어로 Clojure를 사용하여 시맨틱 웹 소프트웨어를 개발하고 있다. 이전에는 제조, 금융 및 헬스케어를 포함한 다양한 분야에서 Java 개발자로 일했다. Urbana-Champaign에 있는 일리노이대학교에서 전산학 석사학위를 받았고 서부 일리노이대학교에서 전산학 학사학위를 받았다. Ryan은 Strange Loop 코어 팀의 구성원이기도 하다. 그의 트위터 주소는 @objcmdo이며 블로그는 Object Commando이다.