클라우드 컴퓨팅 & NoSQL/CouchBase

카우치베이스(Couchbase) 서버-#3 node.js를 이용한 카우치베이스 서버 CRUD 구현하기

Terry Cho 2014. 7. 8. 21:29

Couchbase Server

#3. node.js를 이용하여 카우치베이스에 CRUD 구현하기

조대협 http://bcho.tistory.com


SDK를 이용한 프로그래밍


이제, 프로그래밍 언어를 이용해서 실제로 카우치베이스에 데이타를 저장해보자. 


카우치베이스에서 지원하는 데이터 타입


앞서 예제에서 카우치베이스 웹콘솔을 통해서 JSON 도큐먼트를 저장해봤다. 카우치베이스는 JSON 도큐먼트뿐만 아니라, 키-밸류 스토어 형식으로 일반 string(문자열), integer(숫자)도 저장할 수 있다. JSON 도큐먼트 이외의 데이터 형은, 콘솔에서는 테스트가 불가능하고, SDK를 이용해서 프로그램을 작성해서 테스트를 해야 한다.


SDK를 이용한 데이터 접근


카우치베이스의 데이타를 접근하기 위한 메서드들은 다음과 같다. 이해를 돕기 위해서 SQL 문장과 비교해놓은 테이블을 보면

(출처 : http://docs.couchbase.com/couchbase-devguide-2.5/index.html#couchbase-sdks-and-sql-commands)

SQL Command

Couchbase SDK Method

INSERT, to create.

set and add

SELECT, to retrieve/read data.

get, multiple-retrieves, and get-and-touch (get and update expiration).

UPDATE, to modify data.

set with a given key, or add with a new key, or replace with a key, or cas, also known as Check-and-Set. Used to update a value by providing the matching CAS value for the document. There are also methods for incrementing and decrementing numeric values, changing the expiration for a value, as well as pre-pending or appending to stored objects.

DELETE, to remove data.

delete, deletes information associated with a given key.

프로그래밍 언어에 따라 제공되는 SDK에 따라서 다소 차이는 있지만 대부분의 흐름은 다음과 같다.

1) 카우치베이스로 connection 연결

2) 카우치베이스 버킷을 명시하여 client를 생성

다음으로, SDK에서 제공되는 메서드를 이용해서, CRUD 연산을 아래와 같이 수행할 수 있다.


INSERT

  • Upsert : set (key,value) , key 에 해당하는 값을 value로 저장한다. 만약에 key에 이미 값이 저장되어 있으면, value로 업데이트 한다. (upsert)

  • Insert : add (key,value) , key에 해당하는 값을 value로 저장한다. 단, 이미 key에 대한 값이 존재하면 에러를 리턴한다. (insert)

SELECT

데이타를 읽어오는 SELECT는 매우 단순하다. get(key) 명령어를 사용한다.

  • Get : value:=get(key), 키에 해당하는 값을 리턴한다.

  • Multiple get : values[] = get(keys[]), 키들에 해당하는 값들을 리턴한다. 키 리스트를 배열 형태의 인자로 넘긴다.

  • Get & touch : 키에 해당하는 값을 리턴하고, 해당 키에 대한 값의 TTL을 초기 값으로 복구한다. 예를 들어 TTL이 30분인 값이 10분전에 저장되었을때, (현재의 TTL은 20이 남는다.) touch를 하게 되면 TTL 값이 다시 30으로 복귀된다. SDK에 따라 다소 메서드 정의가 다르지만, 일반적으로 get/multiple get시 옵션으로 새로운 TTL expiry time을 주는 방식으로, TTL 값을 복구시킬 수 있다.

UPDATE

  • Replace (key,value) : key에 해당하는 값을 value로 업데이트 한다. 만약에 key에 대한 값이 존재하지 않으면 에러를 리턴한다 (update).

  • Touch(key) : 해당 key 에 대한 TTL 값을 재 설정한다.

  • Check and Set (CAS) : Replace와 같은 업데이트 기능을 한다. 단, CAS 값을 같이 인자로 넘겨서 CAS값이 같을 경우에만 Replace를 하고 다를 경우에는 에러를 리턴한다.
    ※ CAS에 대해서는 뒤에서 다시 자세하게 설명한다.

  • Incr/Decr : 앞서도 잠깐 언급했지만, 카우치베이스에는 밸류를 저장할 때, JSON 도큐먼트뿐만 아니라 integer와 같은 숫자값도 저장이 가능하다. 숫자값을 저장했을 경우에는 incr,decr 명령을 이용하여 값을 더하거나 뺄 수 있다.

  • Append/prepend : 밸류가 JSON 도큐먼트나 integer 타입이 아닌, string 문자열로 저장되었을 경우에, append 명령어를 이용하여 문자열 뒤에 새로운 문자열을 붙이거나, prepend 명령어를 이용하여, 저장된 문자열 앞에 새로운 문자열을 붙일 수 있다.

DELETE

  • Remove(key) : 키에 해당하는 데이터를 삭제한다.

Lock 제어

  • Lock/UnLock : 명시적으로 해당 키에 해당하는 데이터에 Lock을 걸거나 풀 수 있다. Lock이 걸려 있을 경우에는 다른 클라이언트에 의해서 해당 데이터에 대해서 접근이 불가능하며, Lock을 풀때에는 Lock을 걸 때 리턴 받은 CAS 값을 가지고 Lock을 풀어야 한다.


db.lock('test-key', function(err, result) {

  if (err) throw err;


  // Do some work


  db.unlock('test-key', {cas: result.cas}, function(err, result) {

    if (err) throw err;


    console.log('Success!');

  });

});



참고 CAS(Check & Set)

CAS는 Check and Set의 약자로, 하나의 도큐먼트에는 CAS의 값이 설정이되어 있고, 도큐먼트의 값(VALUE)가 변경될 때 마다 이 CAS의 값도 변경된다. 이는 일관성을 유지하기 위함인데, 값이 {TOTAL:10}이라는 도큐먼트가 있다고 하자. 애플리케이션에서 이 값을 GET한후에 +1을 해서 다시 저장하는 시나리오가 있을때,


①     클라이언트 A가 GET을 하고+1을 한후에, 다시 SET으로 {TOTAL:11}을 저장하기 전에,

②     클라이언트 B가 GET으로 {TOTAL:10}의 값을 가지고 갔다고 하자.

③     클라이언트 A가 {TOTAL:11}을 저장한후에

④     클라이언트 B도 TOTAL+1을 해서 {TOTAL:11}을 저장하게 되면


위의 시나리오에서는 두개의 클라이언트 A,B가 +1씩 더해서 저장하는 시나리오이기 때문에 값이 12가 되어야 하지만 위와 같이 11이 되는 현상이 발생한다. 일관성이 깨지는 문제인데, 이러한 일관성 문제를 예방 위해서 보통 RDBMS의 경우 lock을 거는 방법을 사용하지만 카우치베이스와 같은 분산 DB의 경우에는 전체 분산 노드 간에 lock을 거는 것이 성능상 문제를 야기할 수 있기 때문에, CAS라는 필드를 사용한다.

위와 같은 경우에 {TOTAL:10}일 때 CAS값이 1이라고 가정하면, GET을 할 때 클라이언트는 {TOTAL:10} value 뿐만 아니라 CAS값 1을 같이 가져온다. 그리고 값을 다시 SET하려하 할 때, 서버에 저장된 도큐먼트의 CAS값과 SET을 하고자 하는 클라이언트가 가지고 있는 CAS 값이 일치해야 SET이 성공한다. 위의 경우 클라이언트 A,B 모두 GET시에는 CAS값을 1을 가지고 갔다고 했으면, 3에 의해서 해당 도큐먼트의 CAS값은 2(다른값)으로 변경이 되었을 것이고, 4에서 클라이언트 B가 이전 CAS값 1을 가지고 SET을 시도하면, 도큐먼트의 CAS값과 다르기 때문에 에러가 발생하게 된다.




Node.JS를 이용한 카우치 베이스 프로그래밍


그러면 Node.JS를 이용해서 카우치베이스의 데이터를 억세스 해보자.

Connection 연결 및 간단한 get/set

아래 코드는 간단하게 카우치베이스에서 Connection을 연결하고, 데이터를 set한 후에, get으로 읽어오는 부분이다.


 

var couchbase = require('couchbase');

 

var bucket = new couchbase.Connection({

    'bucket':'mybucket',

    'host':'127.0.0.1:8091'

},function(err){

    if(err){

        console.error('couchbase server connection error');

        throw err;

    }

})

 

json_document = {

    'name':'terry',

    'country':'korea',

    'role':'architect'

}

// set

bucket.set('mykey',json_document,function(err,result){

    if(err){

        console.error(err);

    }

    // get

    bucket.get('mykey',function(err,result){

        if(err){

            console.error(err);

        }else {

            console.log(result);

        }

        process.exit(0);

    });

});

 


var couchbase = require('couchbase');

 

var bucket = new couchbase.Connection({

    'bucket':'mybucket',

    'host':'127.0.0.1:8091'

},function(err){


카우치베이스 모듈을 Import한다. 만약에 카우치베이스 모듈이 설치되어 있지 않다면, “npm install couchbase” npm 명령을 이용해서 모듈을 먼저 설치하도록 하자. 카우치베이스 모듈은 C 라이브러리를 포함하고 있기 때문에, GCC나 비주얼 스튜디오와 같은 C 컴파일러가 환경에 설치되어 있어야 한다.

모듈을 import했으면, bucket으로의 연결을 만들어야 한다. Connection에서 bucket 필드에 연결하고자 하는 버킷명과, host 필드에는 연결하고자 하는 호스트명/포트를 명시한다. 이때 호스트명/포트는 array 형태로 정의할 수 있다. 만약에 연결하려는 호스트가 장애가 나서 연결이 되지 않을 경우 자동으로 array 내의 다른 호스트로 연결을 시도한다.

그리고 하나의 Connection을 연결하더라도 내부적으로 한 개가 아니라 여러 개의 Connection이 생기는 것을 확인할 수 있는데, Node.JS의 카우치베이스 SDK는 내부적으로 Connection Pooling 기능을 제공해서 여러 개의 Connection을 연후에, 매번 Connection을 새롭게 만들지 않고, 재사용하도록 하기 때문에 별도의 Connection에 대한 관리를 할필요가 없다.

json_document = {

    'name':'terry',

    'country':'korea',

    'role':'architect'

}

// set

bucket.set('mykey',json_document,function(err,result){

 


카우치베이스에 연결이 되었으면, 버킷에 set 메서드를 이용하여 데이터를 저장한다.메서드의 순서는 bucket.set(키,저장할 데이터,옵션,콜백함수(err,result){..}); 로 정의된다.

옵션 부분은 생략할 수 있다. 

옵션 부분에는 

  • cas : 위에서 설명된 Check and Set 값. 이 값이 이전의 CAS 값과 일치하지 않으면 set 이 실패한다.

  • expiry : TTL값으로, 이 시간이 지나면 자동으로 데이터가 삭제 된다.

  • persist_to integer : integer에 정의된 노드의 숫자만큼 데이터가 복제된후에 쓰기 작업을 끝낸다. 예를 들어 10개의 노드가 있는 카우치베이스 클러스터에서 이 값이 2면, 마스터 노드를 포함하여, 2개의 노드에 쓰기가 완료되면 set 작업이 완료된 것으로 리턴한다.

  • replicate_to integer  : integer에 정의된 원격 노드 (다른 클러스터) 숫자 만큼 복제가 완료되면 set 작업이 완료된것으로 리턴한다.

bucket.get('mykey',function(err,result){

다음으로 set으로 입력한 값을 get을 이용하여 조회 한다. 메서드의 순서는 

bucket.get(키,옵션,콜백함수(err,result){…});

순서로 정의 된다. 키에 대한 밸류값을 콜백함수의 “result”변수를 통해서 리턴한다.

옵션으로는

  • expiry integer : 값을 정할 수 있는데, 이 값을 지정하면 TTL 값이 이 값으로 다시 세팅된다. 앞에서 설명한 Get & Touch를 구현할 때 사용할 수 있다.

간단하게 set/get에 대한 예제를 살펴보았다. Node.JS의 카우치베이스 SDK는 단일 키에 대해서 set/get 뿐만 아니라 여러키에 대한 다중 set/get을 함께 지원한다.

setMulti(kv, options, callback), getMulti(kv, options, callback)

를 이용하면, 동시에 여러 키에 대한 업데이트나, 여러키에 대한 데이터 SELECT를 수행할 수 있다.

작성한 예제를 수행해보면 다음과 같은 결과를 얻을 수 있다.



 

도큐먼트 삭제


도큐먼트에 대한 삭제는 remove(키,옵션,콜백함수(err,result){..}); 를 사용해서 삭제하면된다.

적용할 수 있는 옵션은 set에서 사용한 cas,persist_to,replicate_to를 동일하게 사용할 수 있다.


Integer값과, String 값에 대한 처리


앞에도 설명하였지만 카우치베이스에 값은 JSON 도큐먼트뿐만 아니라, integer, string값도 저장할 수 있고, 이 값에 대해서는 incr/decr과 같은 숫자 전용 메서드와 append/prepend라는 문자열 전용 메서드를 지원한다.

다음은 String 값을 ‘ hello ‘라는 문자열로 세팅한후 append 를 이용해서 문자열 뒤에 ‘ node.js ‘ 라는 문자열을 추가하고 prepend 명령을 이용해 문자열 앞에 ‘ im couchbase ‘ 라는 문자열을 추가한 후에, get 명령을 이용하여 문자열을 가져와서 출력하는 예제이다


 

var couchbase = require('couchbase');

 

var bucket = new couchbase.Connection({

    'bucket':'mybucket',

    'host':'127.0.0.1:8091'

},function(err){

    if(err){

        console.error('couchbase server connection error');

        throw err;

    }

})

 

// set

bucket.set('string',' hello ',function(err,result){

    if(err){

        console.error(err);

    }

    // get

    bucket.get('string',function(err,result){

        if(err){

            console.error(err);

        }else {

            console.log(result);

        }

        bucket.append('string',' node.js',function(err,result){

            if(err){

                console.error(err);

            }else{

                console.log(result);

            }

            bucket.prepend('string',' im couchbase' ,function(err,result) {

                if (err) {

                    console.error(err);

                } else {

                    console.log(result);

                }

                bucket.get('string', function (err, result) {

                    if (err) {

                        console.error(err);

                    } else {

                        console.log(result);

                    }

                });

            });

        });

    });

});

 


위의 예제를 수행하면 결과는 다음과 같다.


 

prepend와 append도 set과 마찬가지로 옵션을 줄 수 있는데, 적용할 수 있는 옵션은 expiry,cas,persist_to,replicate_to 등을 적용할 수 있다.

다음은 integer 값을 저장하고, incr/decr을 이용하여 저장된 값을 증가/삭제하는 예제이다.

/**

 * Created by bw.cho on 2014-07-07.

 */

var couchbase = require('couchbase');

 

var bucket = new couchbase.Connection({

    'bucket':'mybucket',

    'host':'127.0.0.1:8091'

},function(err){

    if(err){

        console.error('couchbase server connection error');

        throw err;

    }

})

 

// set

bucket.set('number',100,function(err,result){

//bucket.incr('number',{initial:100},function(err,result){

    if(err){

        console.error(err);

    }

    // get

    bucket.get('number',function(err,result){

        if(err){

            console.error(err);

        }else {

            console.log(result);

        }

        bucket.incr('number',{offset:10},function(err,result){

            if(err){

                console.error(err);

            }else{

                console.log(result);

            }

            bucket.incr('number',{offset:-1},function(err,result) {

                if (err) {

                    console.error(err);

                } else {

                    console.log(result);

                }

                bucket.get('number', function (err, result) {

                    if (err) {

                        console.error(err);

                    } else {

                        console.log(result);

                    }

                });

            });

        });

    });

});

위의 코드는 먼저 set 명령어를 사용하여 ‘number’라는 키에 대한 값을 숫자 100으로 세팅한후에, 

bucket.set('number',100,function(err,result){

다음으로, incr 명령을 이용해서 이 값을 증가 시킨다. 이때 아래와 같이 옵션에 offset이라는 값을 지정할 수 있는데, off set은 증분값이다. off set 값 만큼 더해지는데, 아래의 경우 10을 설정했기 때문에, +10을 해서 값이 110이된다.

bucket.incr('number',{offset:10},function(err,result){

옵션에서 offset을 설정하지 않으면, 디폴트로 +1이 더해진다.

또는 아래 처럼 offset을 -1과 같이 마이너스 값을 사용하게 되면 그 값을 빼진다.

bucket.incr('number',{offset:-1},function(err,result) {

다음은 위의 소스코드에 대한 실행 결과이다.

 

하나 주의 깊게 봐야 할 것은

bucket.incr('number',{initial:100},function(err,result){

과 같이 incr에서 initial 값을 설정할 수 있는데, 이는 마치 upsert와 같이 동작한다. 만약에 해당 키값으로 정의된 값이 있다면, 기존의 값을 그대로 사용하고 만약에 해당 키로 설정된 값이 없다면 이 initial 값으로 set를 하게 된다.

decr 명령어도 증가가 아니라 값을 감소 시킨다는 점에서 사용법은 동일하다.


그리드형