블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

node.js에서 Redis 사용하기


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


Redis NoSQL 데이타 베이스의 종류로, mongoDB 처럼 전체 데이타를 영구히 저장하기 보다는 캐쉬처럼 휘발성이나 임시성 데이타를 저장하는데 많이 사용된다.

디스크에 데이타를 주기적으로 저장하기는 하지만, 기능은 백업이나 복구용으로 주로 사용할뿐 데이타는 모두 메모리에 저장되기 때문에, 빠른 접근 속도를 자랑한다.

 

이유 때문에 근래에는 memcached 다음의 캐쉬 솔루션으로 널리 사용되고 있는데, 간단하게 -밸류 (Key-Value)형태의 데이타 저장뿐만 아니라, 다양한 데이타 타입을 지원하기 때문에 응용도가 높고, node.js 호환 모듈이 지원되서 node.js 궁합이 좋다. 여러 node.js 클러스터링 하여 사용할때, node.js 인스턴스간 상태정보를 공유하거나, 세션과 같은 휘발성 정보를 저장하거나 또는 캐쉬등으로 다양하게 사용할 있다.

 

Redis 제공하는 기능으로는 키로 데이타를 저장하고 조회하는 Set/Get 기능이 있으며, 메세지를 전달하기 위한 큐로도 사용할 있다.

 

큐로써의 기능은 하나의 클라이언트가 다른 클라이언트로 메세지를 보내는 1:1 기능뿐 아니라, 하나의 클라이언트가 다수의 클라이언트에게 메세지를 발송하는 발행/배포 (Publish/Subscribe) 기능을 제공한다.




그림 1 RedisPublish/Subscribe의 개념 구조

 

재미있는 것중에 하나는 일반적인 Pub/Sub 시스템의 경우 Subscribe 하는 하나의 Topic에서만 Subscribe하는데 반해서, redis에서는 pattern matching 통해서 다수의 Topic에서 message subscribe 있다.

예를 들어 topic 이름이 music.pop music,classic 이라는 두개의 Topic 있을때, "PSUBSCRIBE music.*"라고 하면 두개의 Topic에서 동시에 message subscribe 있다.

 

자료 구조

 

Redis 가장 기본이 되는 자료 구조를 살펴보자. Redis 다양한 자료 구조를 지원하는데, 지원하는 자료 구조형은 다음과 같다.

1)       String

Key 대해서 문자열을 저장한다. 텍스트 문자열뿐만 아니라 숫자나 최대 512mbyte 까지의 바이너리도 저장할 있다.

 

2)       List

Key 대해서 List 타입을 저장한다. List에는 값들이 들어갈 있으며, INDEX 값을 이용해서 지정된 위치의 값을 넣거나 있고, 또는 push/pop 함수를 이용하여 리스트 앞뒤에 데이타를 넣거나 있다. 일반적인 자료 구조에서 Linked List 같은 자료 구조라고 생각하면 된다.

 

3)       Sets

Set 자료 구조는 집합이라고 생각하면 된다. Key 대해서 Set 저장할 있는데, List 구조와는 다르게 주의할점은 집합이기 때문에 같은 값이 들어갈 없다. 대신 집합의 특성을 이용한 집합 연산, 교집합, 합집합등의 연산이 가능하다.

 

4)       Sorted Set

SortedSet Set 동일하지만, 데이타를 저장할때, value 이외에, score 라는 값을 같이 저장한다. 그리고 score 라는 값에 따라서 데이타를 정렬(소팅)해서 저장한다. 순차성이나 순서가 중요한 데이타를 저장할때 유용하게 저장할 있다.

 

5)       Hashes

마지막 자료형으로는 Hashes 있는데, 해쉬 자료 구조를 생각하면 된다.Key 해쉬 테이블을 저장하는데, 해쉬 테이블에 저장되는 데이타는 (field, value) 형태로 field 해쉬의 키로 저장한다.

키가 있는 데이타를 군집하여 저장하는데 유용하며 데이타의 접근이 매우 빠르다. 순차적이지 않고 비순차적인 랜덤 액세스 데이타에 적절하다.

 

설명한 자료 구조를 Redis 저장되는 형태로 표현하면 다음과 같다.

 



Figure 36 redis의 자료 구조

 

기본적으로 /밸류 (Key/Value) 형태로 데이타가 저장되며, 밸류에 해당하는 데이타 타입은 앞서 언급하 String, List, Sets, SortedSets, Hashes 있다.

 

Redis 대한 설명은 여기서는 자세하게 하지 않는다. 독립적인 제품인 만큼 가지고 있는 기능과 운영에 신경써야할 부분이 많다. Redis 대한 자세한 설명은 http://redis.io 홈페이지를 참고하거나 정경석씨가 이것이 레디스다http://www.yes24.com/24/Goods/11265881?Acode=101 라는 책을 추천한다. 단순히 redis 대한 사용법뿐만 아니라, 레디스의 데이타 모델 설계에 대한 자세한 가이드를 제공하고 있다.

 

Redis 설치하기

개발환경 구성을 위해서 redis 설치해보자.

 

맥의 경우 애플리케이션 설치 유틸리티인 brew 이용하면 간단하게 설치할 있다.

%brew install redis

 

윈도우즈

안타깝게도 redis 공식적으로는 윈도우즈 인스톨을 지원하지 않는다. http://redis.io에서 소스 코드를 다운 받아서 컴파일을 해서 설치를 해야 하는데, 만약에 이것이 번거롭다면, https://github.com/rgl/redis/downloads 에서 다운로드 받아서 설치할 있다. 그렇지만 이경우에는 최신 버전을 지원하지 않는다.

그래서 vagrant 이용하여 우분투 리눅스로 개발환경을 꾸미고 위에 redis 설치하거나 https://redislabs.com/pricing https://www.compose.io  같은 클라우드 redis 환경을 사용하기를 권장한다. ( 클라우드 서비스의 경우 일정 용량까지 무료 또는 일정 기간까지 무료로 서비스를 제공한다.)

 

리눅스

리눅스의 경우 설치가 매우 간단하다. 우분투의 경우 패키지 메니저인 apt-get 이용해서 다음과 같이 설치하면 된다.

%sudo apt-get install redis-server

 

설치가 끝났으면 편하게 redis 사용하기 위해서 redis 클라이언트를 설치해보자.

여러 GUI 클라이언트들이 많지만, 편하게 사용할 있는 redis desktop 설치한다. http://redisdesktop.com/ 에서 다운 받은 후에 간단하게 설치할 있다.

 

이제 환경 구성이 끝났으니, redis 구동하고 제대로 동작하는지 테스트해보자

%redis-server

명령을 이용해서 redis 서버를 구동한다.

 



Figure 37 redis 기동 화면

 

redis desktop 이용해서 localhost 호스트에 Host 주소는 localhost TCP 포트는 6379 새로운 Connection 추가하여 연결한다.

 

 



Figure 38 redis desktop에서 연결을 설정하는 화면

 

연결이 되었으면 redis desktop에서 Console 연다.

 



Figure 39 redis desktop에서 콘솔을 여는 화면

 

Console에서 다음과 같이 명령어를 입력해보자

 

localhost:0>set key1 myvalue

OK

 

localhost:0>set key2 myvalue2

OK

 

localhost:0>get key2

myvalue2

 

localhost:0>

Figure 40 redis desktop에서 간단한 명령을 통해서 redis를 테스트 하는 화면


위의 명령은 key1 myvalue라는 값을 입력하고, key2 myvalue2라는 값을 입력한 후에, key2 입력된 값을 조회하는 명령이다.

 

Redis desktop에서, 디비를 조회해보면, 앞서 입력한 /밸류 값이 저장되어 있는 것을 다음과 같이 확인할 있다.

\


Figure 41 redis에 저장된 데이타를 redis desktop을 이용해서 조회하기

 

node.js에서 redis 접근하기

 

이제 node.js에서 redis 사용하기 위한 준비가 끝났다. 간단한 express API 만들어서 redis 캐쉬로 사용하여 데이타를 저장하고 조회하는 예제를 작성해보자

 

node.js redis 클라이언트는 여러 종류가 있다. http://redis.io/clients#nodejs

가장 널리 쓰는 클라이언트 모듈로는 node-redis https://github.com/NodeRedis/node_redis 있는데, 예제는 node-redis 클라이언트를 기준으로 설명한다.

 

예제는 profile URL에서 사용자 데이타를 JSON/POST 받아서 redis 저장하고, TTL(Time to Leave) 방식의 캐쉬 처럼 10 후에 삭제되도록 하였다.

그리고 GET /profile/{사용자 이름} 으로 redis 저장된 데이타를 조회하도록 하였다.

 

먼저 node-redis 모듈과, json 문서를 처리하기 위해서 JSON 모듈을 사용하기 때문에, 모듈을 설치하자

% npm install redis

% npm install JSON

 

package.json 모듈의 의존성을 다음과 같이 정의한다.

 

 

{

  "name": "RedisCache",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0",

    "redis":"~2.6.0",

    "JSON":"~1.0.0"

  }

}

 

Figure 42 redisJSON 모듈의 의존성이 추가된 package.json

 

다음으로 express 간단한 프로젝트를 만든 후에, app.js 다음과 같은 코드를 추가한다.

 

 

// redis example

var redis = require('redis');

var JSON = require('JSON');

client = redis.createClient(6379,'127.0.0.1');

 

app.use(function(req,res,next){

      req.cache = client;

      next();

})

app.post('/profile',function(req,res,next){

      req.accepts('application/json');

     

      var key = req.body.name;

      var value = JSON.stringify(req.body);

     

      req.cache.set(key,value,function(err,data){

           if(err){

                 console.log(err);

                 res.send("error "+err);

                 return;

           }

           req.cache.expire(key,10);

           res.json(value);

           //console.log(value);

      });

})

app.get('/profile/:name',function(req,res,next){

      var key = req.params.name;

     

      req.cache.get(key,function(err,data){

           if(err){

                 console.log(err);

                 res.send("error "+err);

                 return;

           }

 

           var value = JSON.parse(data);

           res.json(value);

      });

});

 

Figure 43 app.jsredis에 데이타를 쓰고 읽는 부분

 

redis 클라이언트와, JSON 모듈을 로딩한후, createClient 메서드를 이용해서, redis 대한 연결 클라이언트를 생성하자.

 

client = redis.createClient(6379,'127.0.0.1');

 

app.use(function(req,res,next){

      req.cache = client;

      next();

})

 

다음 연결 객체를 express router에서 쉽게 가져다 있도록, 미들웨어를 이용하여 req.cache 객체에 저장하도록 하자.

 

HTTP POST /profile 의해서 사용자 프로파일 데이타를 저장하는 부분을 보면

req.accepts('application/json'); 이용하여 JSON 요청을 받아드리도록 한다.

JSON내의 name 필드를 키로, 하고, JSON 전체를 밸류로 한다. JSON 객체 형태로 redis 저장할 있겠지만 경우 redis에서 조회를 하면 객체형으로 나오기 때문에 운영이 불편하다. 그래서 JSON.stringfy 이용하여 JSON 객체를 문자열로 변환하여 value 객체에 저장하였다.

다음 req.cache.set(key,value,function(err,data) 코드에서 redis 저장하기 위해서 redis 클라이언트를 req 객체에서 조회해온후, set 명령을 이용해서 /밸류 값을 저장한다. 저장이 끝나면 뒤에 인자로 전달된 콜백함수 호출 되는데, 콜백함수에서, req.cache.expire(key,10); 호출하여, 키에 대한 데이타 저장 시간을 10초로 설정한다. (10 후에는 데이타가 삭제된다.) 마지막으로 res.json(value); 이용하여 HTTP 응답에 JSON 문자열을 리턴한다.

 

HTTP GET으로 /profile/{사용자 이름} 요청을 받아서 키가 사용자 이름은 JSON 데이타를 조회하여 리턴하는 코드이다.

app.get('/profile/:name',function(req,res,next) 으로 요청을 받은 , URL에서 name 부분을 읽어서 키값으로 하고,

req.cache.get(key,function(err,data){ 이용하여, 키를 가지고 데이타를 조회한다. 콜백 함수 부분에서, 데이타가 문자열 형태로 리턴되는데, 이를 var value = JSON.parse(data); 이용하여, JSON 객체로 변환한 후에, res.json(value); 통해서 JSON 문자열로 리턴한다.

 

코드 작성이 끝났으면 테스트를 해보자 HTTP JSON/POST REST 호출을 보내야 하기 때문에, 별도의 클라이언트가 필요한데, 클라이언트는 구글 크롬 브라우져의 플러그인인 포스트맨(POSTMAN) 사용하겠다. https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop

 

포스트맨 설치가 끝났으면, 포스트맨에서 HTTP POST/JSON 방식으로 http://localhost:3000/profile 아래와 같이 요청을 보낸다.

 



Figure 44 포스트맨에서 HTTP POSTprofile 데이타를 삽입하는 화면

 

요청을 보낸후 바로 HTTP GET으로 http://localhost:3000/profile/terry 조회를 하면 아래와 같이 앞에서 입력한 데이타가 조회됨을 확인할 있다. 이때 위의 POST 요청을 보낸지 10 내에 조회를 해야 한다. 10초가 지나면 앞서 지정한 expire 의해서 자동으로 삭제가된다.



Figure 45 포스트맨에서 사용자 이름이 terry인 데이타를 조회하는 화면

 

Redisdesktop에서 확인을 해보면 아래와 같이 문자열로 terry 사용자에 대한 데이타가 저장되어 있는 것을 확인할 있다.



Figure 46 redis desktop 에서 입력된 데이타를 확인하는 화면

 

10초후에, 다시 조회를 해보면, terry 키로 가지는 데이타가 삭제된 것을 확인할 있다.

 

지금까지 가장 기본적인 redis 대한 소개와 사용법에 대해서 알아보았다. redis 뒤에 나올 node.js 클러스터링의 HTTP 세션을 저장하는 기능이나, Socket.IO 등에서도 계속해서 사용되는 중요한 솔루션이다. Redis 자체를 다루는 것이 아니라서 자세하게 파고 들어가지는 않았지만, 다소 운영이 까다롭고 특성을 파악해서 설계해야 하는 만큼 반드시 시간을 내서 redis 자체에 대해서 조금 자세하게 살펴보기를 권장한다.

저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

빠르게 훝어 보는 node.js - mongoose ODM 을 이용한 MongoDB 연동


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



 

Mongoosemongodb 기반의 node.jsODM (Object Data Mapping) 프레임웍이다.

이미 앞에서 monk를 이용한 mongodb 연결 방식에 대해서 알아보았는데, mongoose는 무엇인가? 쉽게 생각하면 monk를 자바에서 JDBC 드라이버로 직접 데이타 베이스에 연결하여 SQL을 실행한 거라면, monggoseHibernate/Mybatis와 같이 OR Mapper의 개념이다. 직접 SQL을 실행하는 것이 아니라 node.js의 자바스크립트 객체를 mongodb의 도큐먼트와 맵핑 시켜 주는 개념이다.

이런ODM의 개념은 웹 프로그램에서 주로 사용되는 MVC (Model View Controller)의 개념을 모델 객체를 실제 데이타 베이스 데이타에 맵핑 시킴으로써 쉽게 구현해주게 한다. 또한 자바스크립트가 가지고 있는 데이타에 대한 모호성을 극복하게 해주는데, mongodb는 특성상 데이타에 대한 스키마가 없기 때문에 자유도가 높아서 유연하게 사용이 가능하지만 반대로 RDBMS에서 정의된 스키마나 명시적인 테이블 구조가 없기 때문에, 어떤 컬럼이 어떤 데이타 타입으로 있는지를 알기가 어렵다. 이러한 문제를 보완하기 위해서 mongoose는 스키마라는 개념을 사용한다.

 

앞에 monk로 만든 예제를 mongoose로 변환한 예제를 살펴보도록 하자.

이 예제는 express 4.X 버전을 기준으로 하니 express generator를 통해서 express 프로젝트를 먼저 생성하자.

 

mongoose 설치

프로젝트 생성이 끝났으면 “npm install mongoose” 명령어를 이용하여, mongoose 모듈을 설치하고, 다른 서버에 배포할 것을 염두하여 package.json에 의존성을 추가한다.

{

  "name": "mongooseExpress",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "mongoose": "~4.4.9",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0"

  }

}

 

 

mongoDB 연결

다음으로 app.js에서, mongoose 모듈을 로딩하고, mongoDB에 연결한다.

var app = express();

 

// view engine setup

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'jade');

 

// uncomment after placing your favicon in /public

//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));

app.use(logger('dev'));

app.use(bodyParser.json());

app.use(bodyParser.urlencoded({ extended: false }));

app.use(cookieParser());

app.use(express.static(path.join(__dirname, 'public')));

 

//mongoose configuration

 

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

 

var mongo = require('./routes/mongo.js');

app.use('/', mongo)

 

app.js에서 위와 같은 require를 이용하여 mongoose 모듈을 로딩하고

mongoose.connect명령어를 이용하여, 몽고 DB연결한다. 연결 문자열은 monk와 같게

mongodb://{mongodb ip 주소}:{포트}/{DB}

이다. 위의 예제는 localhost 27017 포트의 mongodbmydbDB에 연결하는 문자열이다.

클러스터된 mongodb 연결 및 timeout등 다양한 설정을 지원하기 때문에, 설정에 대해서는 http://mongoosejs.com/docs/connections.html 를 참고하기 바란다.

 

다음으로, /로 들어오는 요청을 /routes/mongo.js 에서 처리할 수 있도록 app.use / 경로를 mongo 모듈로 맵핑 시킨다.

 

스키마 정의

mongoose에는 스키마라는 개념이 존재하는데, 스키마는 mongoDB에 저장되는 도큐먼트의 데이타 형태를 정의한다. 쉽게 말해서 RDBMS에서 일종의 테이블 정의라고 이해 하면된다.

 

이 예제에서는 HTTP 호출을 처리하는 routes/mongo.js에 스키마를 정의하였다. 아래 코드를 보자.

아래 코드는 routes/mongo.js의 첫 부분이다.

 

var express = require('express');

var router = express.Router();

 

 

var mongoose = require('mongoose');

//define scheme

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String

});

 

 

require 구문을 이용해서 mongoose 모듈을 로드하였고,

mongoose.Schema 명령을 이용해서 userSchema를 정의하였다.

Schema 정의 부분을 보면, userSchemauserid, sex, city 3개의 필드로 정의되고, 각 필드는 String 데이타 타입을 사용한다. 구체적인 스키마 타입에 대해서는 뒤에서 설명한다.

 

데이타 핸들링

그러면 데이타를 읽고,쓰고,삭제하고,수정하는 방법을 알아보자. 앞의 monk 예제와 같이 /insert /list /update /delete /get에 대한 라우팅 메서드를 구현해보자

 

앞의 코드에 이어서 insert 부분을 먼저 살펴보자

             

 

// create model with mongodb collection & scheme

var User = mongoose.model('users',userSchema);

 

router.post('/insert', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

     

      var user = new User({'userid':userid,'sex':sex,'city':city});

      user.save(function(err,silence){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Inserted");

            

         });

});

 

 

가장 중요한 부분은 먼저 앞에서 만든 userSchema를 가지고 모델 객체를 생성해야 한다. 모델 객체 생성 방법은

mongoose.model(‘{mongodb에서 Collection }’, {스키마객체명})

이다. 이 포맷에 따라서 다음과 같이 User라는 모델 객체를 생성하였다.

 

var User = mongoose.model('users',userSchema);

 

User라는 모델 객체는 쉽게 생각하면 mongodbCollection을 대변하는 node.js상의 객체라고 생각하면 된다.

새로운 user 정보를 추가하기 위해서 post 메서드에서 User 모델로 새로운 user 객체를 생성한다. 객체를 생성할때는 JSON을 이용해서 스키마 포맷에 맞춰서 저장하고자 하는 키와 값을 다음과 같이 저장해주면된다.

 

var user = new User({'userid':userid,'sex':sex,'city':city});

 

그리고 간단하게 user 객체를 save 메서드를 이용해서 저장해주면 mongodb 저장된다.

 

다음은 삭제 부분을 보자

router.post('/delete', function(req, res, next) {

      var userid = req.body.userid;

     

      var user = User.find({'userid':userid});

      user.remove(function(err){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Removed");

            

         });

});

 

userid POST 받은 후에 User.find 명령을 이용하여 ‘userid’필드가 POST에서 전달 받은 userid 필드를 검색하여 삭제하고자 하는 도큐먼트를 찾아낸 후에remove 메서드를 이용해서 삭제한다.

 

다음은 업데이트 부분이다. userid 도큐먼트를 조회해서, 내용을 업데이트 하는 로직이다.

router.post('/update', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

      User.findOne({'userid':userid},function(err,user){

 

           if(err){

               console.log(err);

               res.status(500).send('update error');

               return;

          }

           user.sex = sex;

           user.city = city;

           user.save(function(err,silence){

                  if(err){

                     console.log(err);

                     res.status(500).send('update error');

                     return;

                  }

                  res.status(200).send("Updated");

                 

              });

      });

});

 

 

User.findOne 이용해서 userid 도큐먼트를 조회한 후에, 콜백으로 리턴되는 user 객체의 sex,city 값을 변경한후에

user.save 이용하여 저장하였다.

 

다음은 전체 리스트를 조회하는 부분이다.

router.get('/list', function(req, res, next) {

      User.find({},function(err,docs){

           if(err) console.log('err');

           res.send(docs);

      });

});

 

 find 메서드를 사용하면 되는데, find 메서드의 인자로 검색 조건을 넘기면 된다. 전체 검색이기 때문에 별도의 조건없이 {} 사용하였다.

 

다음은 userid로 도큐먼트를 조회하는 부분이다.

전체리스트와 유사하지만 findOne 메서드를 이용하여 userid에 해당하는 사용자 도큐먼트를 조회하여 출력한다.

router.get('/get', function(req, res, next) {

      db = req.db;

      var userid = req.query.userid

      User.findOne({'userid':userid},function(err,doc){

           if(err) console.log('err');

            res.send(doc);

      });

});

 

module.exports = router;

 

 

route 처리 로직에 요청을 보내는 html 파일을 만들자. 이 파일은 앞서 monk 예제에서 만든 파일과 동일하다.

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

 

<h1> Mongoose Test Example</h1>

<form method='post' action='/insert' name='mongoform' >

      user <input type='text' size='10' name='userid'>

      <input type='submit' value='delete' onclick='this.form.action="/delete"' >

      <input type='button' value='get' onclick='location.href="/get?userid="+document.mongoform.userid.value' >

      <p>

      city <input type='text' size='10' name='city' >

      sex <input type='radio' name='sex' value='male'>male

      <input type='radio' name='sex' value='female'>female

      <p>

      <input type='submit' value='insert' onclick='this.form.action="/insert"' >

      <input type='submit' value='update' onclick='this.form.action="/update"' >

      <input type='button' value='list'  onclick='location.href="/list"' >

     

</form>

</body>

</html>

 

/public/mongoose.html이라는 파일로 저장을 하자.

모든 코드 작성이 끝났다. 이를 실행해보면 다음과 같이 데이타를 넣을 수 있는 입력창이 나온다.

 



Figure 1 mongoose 예제에서 데이타를 입력하는 화면

 

데이타를 입력하고, insert를 이용하여 데이타를 저장한후 다시 get 버튼을 눌러서 데이타를 조회해보면 다음과 같이 useriddavid인 도큐먼트가 조회되는 것을 확인할 수 있다.



Figure 2 mongoose 예제에서 useriddavid인 도큐먼트를 조회한 결과

 

쿼리

 

간단한 삽입,삭제,수정,조회만 앞에서 살펴보았는데, 이외에도 다양한 조회 기능을 제공한다. 자세한 쿼리 사용 방법은 http://mongoosejs.com/docs/documents.html 를 참고하면 된다.

몇 가지 쿼리들을 살펴보도록 하자. 다음 예제를 살펴보자

 

 

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String,

      age : Number

});

 

var User = mongoose.model('users',userSchema);

 

// select city from users where userid='terry'

User.findOne({'userid':'terry'}).select('city').exec(function(err,user){

      console.log("q1");

      console.log(user+"\n");

      return;

});

 

 

// select * from users where city='seoul' order by userid limit 5

User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){

      console.log("q2");

      console.log(users+"\n");

      return;

});

 

// using JSON doc query

// select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q3");

           console.log(users+"\n");

           return;

});

 

//using querybuilder

//select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({})

      .where('city').equals('seoul')

      .where('age').gt(10).lt(29)

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q4");

           console.log(users+"\n");

           return;

});

 

 

코드 첫 부분에서는

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost:27017/mydb');

var userSchema = mongoose.Schema({

      userid: String,

      sex : String,

      city : String,

      age : Number

});

 

var User = mongoose.model('users',userSchema);

 

mongoose 모듈을 로딩하고, mongodb에 연결한다. 다음 스키마를 정의 한다. userid, sex, city를 문자열로 가지고, age라는 필드를 숫자로 갖는다. 스키마 정의가 끝났으면 이 스키마를 이용해서 users 컬렉션에 대해서 User 모델 객체를 생성한다.

 

첫번째 쿼리를 살펴보자

// select city from users where userid='terry'

User.findOne({'userid':'terry'}).select('city').exec(function(err,user){

      console.log("q1");

      console.log(user+"\n");

      return;

});

 

첫번째 쿼리는 useridterry인 도큐먼트를 쿼리해서 그중에서 city 라는 이름의 필드만 리턴하는 쿼리이다.

실행 결과는 다음과 같다.

 

q1

{ city: 'seoul', _id: 56e62f2c1a2762d26afa6053 }

 

두번째 쿼리를 살펴보자

// select * from users where city='seoul' order by userid limit 5

User.find({'city':'seoul'}).sort({'userid':1}).limit(5).exec(function(err,users){

      console.log("q2");

      console.log(users+"\n");

      return;

});

 

다음은 실행결과이다.

 

q2

{ age: 18,

  city: 'seoul',

  sex: 'female',

  userid: 'cath',

  _id: 56e62f351a2762d26afa6054 },{ age: 23,

  city: 'seoul',

  sex: 'female',

  userid: 'stella',

  _id: 56e62f3c1a2762d26afa6055 },{ age: 29,

  city: 'seoul',

  sex: 'male',

  userid: 'terry',

  _id: 56e62f2c1a2762d26afa6053 },{ age: 27,

  city: 'seoul',

  sex: 'female',

  userid: 'yuna',

  _id: 56e62f411a2762d26afa6056 }

 

users 컬렉션에서 city 필드가 seoul인 도큐먼트를 조회한후 결과를 sort({‘userid’:1}) 을 이용하여 userid에 따라 오름차순으로 정렬한 후에, limit(5)를 이용하여 결과중 5개만 리턴한다.

 

세번째 쿼리를 보면

// using JSON doc query

// select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({'city':'seoul', 'age':{$gt:10 , $lt:29}})

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q3");

           console.log(users+"\n");

           return;

});                                 

 

users 컬렉션에서 cityseoul이고, age 10보다 크고, 29보다 작은 도큐먼트를 조회한 후 sort를 이용하여 내림 차순으로 정렬을 한 후, select를 이용하여 useridage 필드만 리턴한다

다음은 쿼리 실행결과이다.

 

q3

{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 }

 

마지막 네번째 쿼리를 보면

//using querybuilder

//select userid,age from users where city='seoul' and age > 10 and age < 29

User.find({})

      .where('city').equals('seoul')

      .where('age').gt(10).lt(29)

      .sort({'age':-1})

      .select('userid age')

      .exec(function(err,users){

           console.log("q4");

           console.log(users+"\n");

           return;

});

 

세번째 쿼리와 내용을 같으나, 검색 조건을 find() 안에서 json으로 정의하지 않고, 밖에 where문으로 빼서 정의했다. 이를 쿼리 빌더라고 하는데, mongoosewhere문에 따라서 자동으로 쿼리를 생성해준다.

.where('city').equals('seoul') city필드가 seoul 도큐먼트를 조회하고

.where('age').gt(10).lt(29) age 10보다 크고, 29보다 작은 도큐먼트를 조회하도록 한다.

다음은 쿼리 수행 결과이다.

q4

{ age: 23, userid: 'stella', _id: 56e62f3c1a2762d26afa6055 },{ age: 18, userid: 'cath', _id: 56e62f351a2762d26afa6054 } 

 


저작자 표시 비영리
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Couchbase Server

#6. Couchbase server 구조

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




이번에는 마지막으로 카우치베이스의 아키텍쳐에 대해서 알아보도록 하자

노드와 클러스터 (Node & Cluster)

노드는 물리적인 서버에서 기동하는 하나의 카우치베이스 인스턴스로, 카우치 베이스는 여러 개의 노드로 이루어진 클러스터로 구성된다


클라이언트 SDK (Client SDK)

프로그래밍 언어별로 카우치베이스에 접근하기 위한 API(SDK)를 제공한다.


vBucket 개념

카우치베이스는 실제데이타와 물리서버간의 맵핑을 vBucket이라는 것을 이용해서 관리한다. 카우치베이스는 키-밸류 스토어이다. 그래서, 각 키가 어디에 저장되어 있는지를 vBucket이라는 단위로 관리 하는데. 키에 대한 해쉬값을 계산한 후에, 각 해쉬값에 따라서 저장되는 vBucket을 맵핑한다음 각 vBucket을 노드에 맵핑한다.

아래는 서버 3대가 있었을 때, vBucket을 맵핑하는 구조에 대한 예제이다.



※ 출처 : http://docs.couchbase.com/couchbase-manual-2.5/cb-admin/#vbuckets

클라이언트 SDK는 이 vBucket와 노드에 대한 맵핑 정보를 클러스터로부터 받아서 관리한다. 즉 키에 대한 물리적인 서버 맵핑 정보를 클라이언트가 SDK를 통해 직접알 수 있기 때문에, 클라이언트가 PROXY등을 거치지 않고 직접 데이터가 저장된 노드로 접근이 가능하다.

CF. mongoDB의 경우 중간에 Proxy를 거쳐서 데이터가 저장된 물리 노드로 접근하게 된다.

만약에 노드가 추가되거나 삭제되었을 때, 물리적으로 데이터가 다른 노드로 다시 분산 배치되고, 새롭게 배치된 데이터에 따라서 vBucket to 노드간의 데이터 맵핑 정보도 업데이트 되는데, 이를 Rebalancing이라고 한다. (Rebalancing에 대한 내용은 뒤에 다시 설명)



http://docs.couchbase.com/couchbase-manual-2.5/images/vbuckets-after.png 


노드의 상세구조

그러면 각 노드는 어떤 형태로 구성이 될까? 아래는 노드의 대략적인 아키텍쳐이다.

카우치베이스의 노드는 아래 그림과 같이 크게 좌측의 Data Manager와 우측의 Cluster Manager로 나뉘어 진다.




Cluster Manager

Cluster Manager는 노드에 대한 상태와 클러스터에 대한 상태, 설정등을 관리하는 부분으로 Erlang/OTP 기반으로 구현되어 있다. 그 상위단에는 Admin Portal을 위한 Web UI가 8091 포트로 제공되고 있고, 같은 포트로 REST API가 함께 제공된다.

카우치베이스는 클라이언트 SDK는 이 8091 포트의 REST API를 통해서, 설정 정보와 앞서 설명한 vBucket 정보를 읽어온다. 여기에는 실제로 데이터에 대한 set/get이나 뷰 쿼리 수행용 포트정보도 포함이 되는데,  아래 Data Manager에서 제공되는 11211 포트나, 8092 포트가 사용된다..

그 외에도 클러스터 노드간의 통신을 위한 4389, 21100 포트등 다수의 포트가 사용되는데, 카우치베이스는 서버-클라이언트, 서버-서버간에 사용하는 포트들이 많기 때문에, 배포 전에 반드시 포트들을 확인하고 방화벽이나 네트워크 설정에 반영해야 한다.

※ http://docs.couchbase.com/couchbase-manual-2.5/cb-admin/#faqs 문서를 보면 배포시 오픈해야 하는 포트들이 설명되어 있다.


Data Manager

Data Manager 부분은 직접 데이터에 접근하는 부분으로 set/get 메서드를 이용하여 데이터를 저장하거나, 뷰에 대한 쿼리를 수행할 때 접근되는 인터페이스이다.

맨 아래단에는 멀티쓰레드 기반의 Persistence 엔진이 있으며, 디스크에 데이터를 저장하거나 읽어드릴때 사용되는 컴포넌트이다. 그 윗단에는 memcached가 있으며, 데이터를 캐슁하는데 사용된다. 또한 이 계층에서 뷰에 대한 쿼리 엔진이 제공된다.

Memcached 위에는 moxi 가 Proxy로 사용된다.


데이터 쓰기와 복제

클라이언트에서 데이터 쓰기가 발생했을 때, 카우치베이스는 어떻게 데이터를 저장할까?

먼저 클라이언트에서 Client SDK를 통해서 쓰기 요청을 하면, Client SDK는 해쉬 알고리즘에 따라데이터의 키 값에 맵핑 되는 vBucket을 찾아내고, 그 vBucket에 맵핑 되는 노드를 찾아서 쓰기 요청을 전달한다.

쓰기 요청은 해당 노드의 Listener로 전달되고, 이 Listener는 들어온 데이터를 로컬의 캐쉬에 쓰고 클러스터의 다른 노드로 복제 요청을 보낸다. 그리고 데이터는 노드의 디스크에 저장된다.




쓰기 과정중에 노드간의 복제가 발생한다.


노드별 메모리 레이아웃

그러면 각 노드별로 메모리 레이아웃은 어떻게 되어 있을까? 카우치베이스의 경우, memcached를 이용하는 만큼 서버의 메모리 공간 계산이 매우 중요하다. 앞서 글들에서도 설명하였지만, 메모리에 대해서 고려할 때, 카우치 베이스는 버킷의 키를 모두 메모리에 로딩해놓고 있다. 최소 메모리 공간은 전체키의 합보다는 최소한 커야 한다.그리고 각 도큐먼트당 60바이트의 메타 정보 저장공간이 필요하다. (키크기 + 60 바이트)*전체레코드수 / 노드수 * 3 (복제본수) 가 노드당 최소 메모리양이다. 최소 메모리란 말 그대로 최소한 돌릴 수 있는 수준을 이야기하는 것인데, 이 경우에는 캐쉬를 전혀 사용하지 못하기 때문에, 이 메모리 용량으로 서버를 운영하면 절대 안된다. (말그대로 아주 최~~소한이다.)


전체 하드웨어 공간에서 OS가 기본적으로 사용하는 용량을 제외 하면, 카우치베이스의 노드가 그 메모리 공간을 활용하는데, 카우치베이스에서는 노드에 할당된 메모리 공간을 버킷별로 다시 할당한다. (버킷을 생성할 때 설정할 수 있음)


리밸런스(Rebalance)

리밸런스 노드가 클러스터에 추가되거나, 장애등의 이유로 삭제되었을 때 데이터를 다시 노드에 분산 배치를 하는 작업이다. 노드간에 데이터 복제가 심하게 일어나기 때문에, 리밸런스는 부하가 적은 시간대에 하도록 권장하고 있다. (관리 콘솔을 보면 리밸런스를 멈추거나 시작할 수 있는 기능이 있다.) 향후에는 리밸런스를 Throttling 하는 기능이 나온다고 하니 기대해볼만하다.

NoSQL의 경우 특정 노드가 장애가 나서 시스템이 장애 나는 케이스보다 보통 노드를 추가/삭제할때 발생하는 이런 리밸런싱에 의해서 부하가 올라가거나 해서 장애가 나오는 케이스가 많기 때문에 특별히 주의를 기울일 필요가 있다.

 

XDCR

XDCR은 데이타 센터간에 카우치베이스 클러스터 데이타 복제를 지원하는 기능이다.

현재 최신 버전은 2.5 버전인데, 2.5 버전에서는 XDCR을 TLS/SSL을 이용해서 복제하기 때문에 자체적으로 보안을 지원한다. 그렇지만 무료 버전인 2.2 (Community Edition)의 경우 TLS/SSL 기반의 복제가 지원되지 않기 때문에, VPN 기반의 네트워크를 터널을 설정하고, VPN 터널을 통해서 XDCR 복제를 하도록 가이드 하고 있다.


Couchbase Gateway & CouchBase Mobile

카우치베이스의 흥미로운 점중의 하나는 모바일 디바이스에 탑재될 수 있는 Couchbase Lite버전을 제공한다는 것이다. iOS,안드로이드 버전을 제공하며, 또는 일반적인 애플리케이션에 사용할 수 있도록 자바버전과 .NET  버전도 제공된다. (모두 Community edition이 제공된다)

이 Couchbase Lite는 Couchbase 서버와 동기화가 가능하다.


다 못한 이야기

개발 관점에서는 GET/SET그리고 뷰 정도로 간편하지만, 운영과 설정에 대해서는 많아도 너무 많다. 대략적인 사용방법 아키텍쳐에 대해서 알아봤는데, 언급하지 못한 부분이 있어서 몇 가지만 언급하고자 한다.

카우치베이스는 다양한 커넥터를 이용하여 다른 솔루션과의 연동을 지원한다.

Elastic Search와 연동을 통하여 데이터에 대한 FTS (Full Text Search : 검색)을 지원할 수 있으며, Hadoop 연동을 통해서 Map & Reduce 기반의 데이터 처리가 가능하다. 또 오픈소스 ETL인 Talend 연동을 통해서 데이터를 다른 시스템으로 연동(복제)가 가능하며, 마지막으로, 데이터 암호화 솔루션인 Gazzang과 연동을 통해서 데이터를 암호화하여 저장할 수 있다.

http://www.couchbase.com/couchbase-server/connectors

 


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Couchbase Server

#4. 뷰(View) 이해하기

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


뷰는 카우치베이스의 아주 강력한 기능중의 하나이다. RDBMS의 뷰의 개념과 유사한 개념으로, 원본 데이터로부터, 필터링을 통하여 원하는 형태의 데이터로 변환하여 보여주는 일종의 읽기 전용 테이블과 유사한 개념으로 보면 된다. 이를 통해서 키-밸류 스토어 기능만 제공하는 일반 NoSQL에 비해서 filtering 뿐만 아니라, Indexing,grouping,ordering과 같은 다양한 기능을 이 뷰를 이용하여 사용할 수 있다.

카우치베이스의 뷰는 원본 데이터에서 자바스크립트로된 맵&리듀스(Map&Reduce) 함수를 통해서 데이터를 정재한 후에, 뷰로 만들어낸다. 간단하게 개념을 잡아보면 다음과 같다.

 


좌측 그림과 같이 이름을 ID로 하고, Country,Role,Age라는 필드를 가지고 있는 값을 VALUE로 저장하고 있다고 하자, 필터링해서 보고 싶은 데이터는 여기서 Country와 Role만을 필터링 하고 싶다. 그래서 맵&리듀스 함수(여기서는 맵 함수만을 사용하였음)를 이용해서 오른쪽과 같은 뷰를 만들어낼 수 있다.


맵&리듀스(Map&Reduce)함수


뷰를 이해하려면 맵&리듀스함수에 대해서 정확하게 이해해야 한다. 맵함수는 버킷에 저장된 모든 데이터에 대해서 맵함수를 실행한다. 뷰를 정의할 때 맵함수는 반드시 정의해야 한다.

function(doc,meta){

 emit(doc.role,doc.country);

}

맵함수는 두개의 인자를 전달 받는다. “doc”는 버킷내의 저장된 개별 데이타로 각 데이터별로 id와 JSON 도큐먼트의 값을 갖는다. “meta” 는 그 데이터에 대한 메타 데이터 (flag,cas 값등)을 리턴한다.

맵함수에서는 이렇게 받은 개별 데이터를 emit이라는 함수로 가공하여 리턴한다. emit 이라는 함수에 필터링등의 로직을 적용할 수 가 있다. emit(인자1,인자2)의 인자1은 뷰의 KEY값을 리턴하고, 인자2는 뷰의 밸류값을 리턴한다.


 emit 함수를 거쳐 맵함수를 끝내면 ID,KEY,VALUE 형식의 데이터 셋이 나오는데, 이를 카우치베이스에서는 “Index”라고 부른다. ID는 원래 데이터에 대한 ID이고, KEY는 뷰의 키값이다. Index는 이 KEY에 따라서 정렬(sorting)된 형태로 리스팅 된다.  그리고 마지막에는 뷰의 값(VALUE)가 저장된다. 이렇게 생성된 Index는 디스크에 저장된다.


저장된 index를 이용해서 리듀스함수(Reduce)를 실행할 수 있는데, 리듀스 함수를 이용해서는 Grouping 기능을 사용할 수 있다. RDBMS의 group by sum이나 group by count와 같은 간단한 기능에서부터 복잡한 계산을 자바스크립트를 이용해서 구현하는 것이 가능하다.



정리하자면 뷰에는 각 버킷내의 개별 데이터를 변환하는 맵함수와, 변환된 개별 데이터를 그룹별로 모아서 처리할 수 있는 리듀스 함수를 갖는다.


Design Document


하나의 버킷에는 여러 개의 뷰를 생성할 수 있다. 아래 그림과 같이 뷰는 Design document라는 단위로 묶이게 되고 각 뷰는 그에 대칭 되는 맵&리듀스 함수를 가지고 있다.

 

 

카우치베이스의 문서를 보면 http://blog.couchbase.com/10-things-developers-should-know-about-couchbase 성능상 무리가 없게 하기 위해서는 하나의 버킷당 4개의 Design Document 정도가 적절하며, 하나의 Design Document당 최대 10개 정도의 뷰를 구현하는 것이 적절하다고 권고하고 있다. (즉 버킷당 최대 40대 정도의 뷰가 적절)


콘솔에서 뷰 만들기


개념을 이해 했으면 이제 간단한 뷰를 만들어보자, 카우치베이스는 웹콘솔에서 뷰생성 및 개발 테스트를 할 수 있도록 지원한다. 카우치베이스 웹콘솔로 이동한후 상단메뉴에서 “Views” 메뉴를 선택한후 아래에 나오는 리스트박스에서 뷰를 생성하고 싶은 버킷을 선택한다. 

다음으로, 아래에 나오는 “Create Development View” 버튼을 선택하면 뷰를 생성할 수 있다.


 

생성 버튼을 누르면 아래와 같이 대화상자가 뜨는데, 첫번째는 Design Document 명이다. 그리고 다음 텍스트 상자가 뷰 이름을 넣는 상자이다.

 


이때, design document를 넣는 텍스트박스에서 Design Document 이름이 “dev_”로 시작하는 것을 볼 수 있는데, Design document는 두 가지 타입이있다. 개발을 위한 개발용 “Development view”와 실제 운영에서 사용할 수 있는 “Production view”가 있다. 이 둘의 차이점은 다음과 같다.

  • Development View : 처음에 뷰를 생성하면 Development View로 생성된다. Design document 이름은 “_design/dev_”로 시작된다. Development View의 경우 버킷의 모든데이타에 대해서 Index를 만드는 것이 아니라 개발 목적이기 때문에 일부의 데이터에 대해서만 Index를 생성한다.
  • Production View : Development View를 이용해서 개발과 테스트를 완료하면 Publish하여 Production View로 전환한다,  Production View는 버킷의 전체 데이터로 Index를 생성한다. 

※ Development View에서 개발 및 테스트가 완료되었으면 반드시 Production View로 배포를 진행해야 한다.

뷰가 생성되었으면 콘솔에서 아래와 같이 뷰의 맵&리듀스 코드를 작성하고 테스트를 해볼 수 있다.



위에 "▼cath”라는 창에 맵&리듀스 코드 작성을 돕기위해서 버킷에 저장된 데이터 중 random으로 출력해서 샘플로 보여준다. “cath”는 샘플로 출력되는 도큐먼트의 ID이다.

아래 “▼View Code” 창 부분에는 왼쪽에는 맵 함수를 오른쪽에는 리듀스 함수를 직접 코딩해볼 수 있다. 그리고 맨아래 부분에는 이 맵&리듀스 함수를 실행했을 때의 결과를 출력해준다.

그 아래 부분에는 Filter Result라는 버튼이 있는데, 이 버튼을 통해서 맵&리듀스에 의해 생성된 결과를 필터링 할 수 있다. 도큐먼트 ID나 KEY의 범위등 지정해서 pagination등에 활용할 수 있다. 자세한 필터에 대한 내용은 향후에 설명하도록 한다.

 





뷰를 이용하여 다양한 기능 구현하기

이러한 뷰를 이용해서 카우치베이스는 다양한 데이터 조작이 가능한데, 대부분의 NoSQL은 키밸류 형태로 RDBMS에서 지원하는 Indexing, Grouping, Sorting 등 강력한 기능을 지원하지 않는 경우가 많다. 카우치베이스에서는 뷰를 이용해서 이러한 기능등을 구현할 수 가 있다.


Indexing 

RDBMS의 Index와 같은 개념으로, 도큐먼트 ID이외에 다른 필드로 검색이 가능하게 해주는 기능이다. NoSQL에서는 이를 Secondary Index라고도 부르는데, Secondary Index를 지원하는 다른 NoSQL의 경우, 제대로 성능이 나도록 설계되어 있는 경우도 있지만, Secondary Index를 기반으로 검색을 할 경우, 분산된 전체 노드를 FULL SCAN해서 (전체 목록을 뒤져서) 검색 결과를 한 노드로 합쳐서 리턴하는 형태로 구현되어 있는 경우가 있다. (Riak의 예전 Secondary Index의 구조) 이 경우, 전체 노드가 O(N)으로 전체 레코드를 검색해야 하기 때문에, 전체적인 성능 저하가 매우 심하다. 앞에서도 언급했지만, 카우치베이스는 이를 Incremental view의 개념을 사용해서, Secondary Index에 해당 하는 필드를 키로 잡은 새로운 물리적인 테이블을 내부적으로 만들기 때문에, Index를 이용한 검색도 타 NoSQL에 비해서 성능이 좋은 편에 속한다.


Extracting & Filtering

필터링은 SQL문장에서 select where라고 보면 된다. 조건에 맞는 특정한 도큐먼트만 select하는 것이 가능하다.

다음과 같은 도규먼트들이 버킷에 저장되어 있다고 할때

{

   "name": "yuna",

   "country": "us",

   "ssn": "140515-1234123",

   "sex": "female"

}

여기서 country="us" 인 것만을 쿼리하고, 전체 필드중 일부 필드(컬럼)만을 가지고 리턴하는 뷰를 만들려고 하면, 다음과 같은 맵 함수를 이용하면 된다.
function (doc, meta) {
  if(doc.country=="us") emit(doc.name,doc.country,doc.ssn);
}
emit에 오는 첫번째 필드가 KEY로 사용된다. 
단 뷰 함수에 의해서 필터링이 된 경우에는 물리적인 Index가 생기기 때문에, country를 Korea,Canada등과 같이 동적으로 변경하는 것이 불가능하다.
이런 동적인 필터링은 카우치베이스의 필터를 이용해서 가능한데, 동적으로 나라에 따라서 필터링을 하고 싶다면, 조건을 적용할 필드를 키로 리턴한 후, 키에 대한 필터링을 하면 되는데, 다음과 같이 맵 함수를 작성한 후에 뷰를 생성하고
function (doc, meta) {
  emit(doc.country,doc.name,doc.ssn);
}
아래와 같이 필터 조건에 key를 "us"로 지정하면 doc.country가 "us" 인 도큐먼트만 쿼리가 된다.
※ 문자열은 반드시 "" 을 사용해서 지정해야 한다.


Sorting
카우치베이스에서 소팅은 뷰의 키값을 기반으로 한다. 소팅의 기준으로 하고자하는 필드를 KEY로 잡으면, 뷰에서는 디폴트로 오름차순으로 키에 따라 정렬을 해준다. 내림 차순으로 정렬을 하기 위해서는 위의 필터에서 descending이라는 옵션을 체크 해주면 된다.

리듀스(Reduce)함수를 이용한 Grouping 연산
Grouping은 RDBMS의 group by와 유사한 기능으로 같은 country인 도큐먼트 수를 카운트하거나, 태어난 해별 사용자 수를 하는 것과 같은 그룹 단위의 연산을 할 수 있다.
먼저 Grouping의 개념을 사용하기 위해서는 Compound Key라는 개념을 이해해야 하는데, 카우치베이스의 Grouping 연산은 이 Compound Key를 기본으로 한다. Compound Key랑 뷰의 키를 [] (배열) 형태로 정의한 경우이다. 예를 들어 키를
["Country","생년","성별"] 과 같은 형태의 배열 형태로 정의하는 것이다. Grouping은 이 Compound Key의 Level 단위 (즉 배열의 첫번째 값으로만 그룹을 만들것인가? 아니면 첫번째,두번째 값을 포함해서 그룹으로 잡을 것인가) 로 처리한다.
실제 예제를 살펴보자. 다음과 같은 JSON 도큐먼트가 있을때,
{
   "name": "yuna",
   "country": "us",
   "ssn": "140515-1234123",
   "sex": "female"
}
앞에서 언급한 형태의 Compound Key를 만드는 함수는 다음과 같다, 
function (doc, meta) {
  var year = doc.ssn.substring(0,2);
  emit([doc.country,year,doc.sex],1);
}
이 맵함수를 이용하면 뷰는 다음과 같아진다.
["canada","69","male"] 1
["korea","75","male"] 1
["us","08","female"] 1
["us","14","female"] 1
["us","75","female"] 1
그러면 국가별로 사용자 수를 통계를 내려면, 리듀스 함수에 "_count" 라고 정의하고
필터에 group 옵션을 선택한후에, group level을 1로 지정한다.

이렇게 하면, Compound Key의 첫번째 필드 (country)를 기반으로 그룹핑을 하고, 리듀스 함수에서 이 그룹단위로 _count 연산을 수행한다.

카우치베이스에는 리듀스함수에서 사용할 수 있는 기본적인 함수를 미리 정의해놨는데, 아래와 같다.

  • _sum : 밸류값을 더한다. (밸류 타입이 Integer라야 함)
  • _count : 그룹별로 묶인 값들의 수를 카운트 한다.
  • _stats : 각종 그룹값을 계산해준다, 위에서 나온 sum뿐만 아니라, sum,count,min(최소값),max(최대값),sumsqr(Sum of square : 제곱합) 값을 출력해준다.
Pagination 
뷰에서 만들어진 데이타는 쿼리시에 pagination을 지원할 수 있다. 필터를 통해서, 뷰의 start/end key값을 정의하거나, start/end 도큐먼트ID를 지정하면 그 범위내의 도큐먼트만 쿼리할 수 있다. (위의 필터 설정 그림 참고) 콘솔에서는 나와 있지 않지만 필터에 추가로 limit라는 옵션을 주면, 최대한 읽어올 수 있는 도큐먼트 수를 정의할 수 있다.
예를 들어 뷰의 키로 1~100까지 100개의 도큐먼트가 있을때, start key=11,end key=50으로 하면 11~50까지의 50개의 레코드가 리턴된다. 여기에 limit=10을 추가하면 11~20까지의 레코드만 리턴되낟. start/end와 limit를 적절하게 사용하면 페이징 기능을 구현할 수 있다. limit를 설정하는 방법은 SDK를 이용하여 뷰를 호출하는 방법에서 알아보도록 한다.

다음번에는 SDK를 이용해서 뷰를 생성하고, 뷰에 대한 쿼리를 수행하는 방법에 대해서 알아보도록 하겠다.









저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Couchbase Server

#2 기본 개념 잡기

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

개념 잡기


카우치베이스 서버 설치가 끝났으면 기본적인 개념을 잡아보도록 하자.


Document 기반의 Key-Value 스토어


카우치 베이스느 키, 밸류 스토어이다 하나의 유니크(Unique)한 키에, 값을 저장하는 방식이다. 저장 되는 값은 JSON 도큐먼트가 저장된다. 키는 최대 250 바이트, (밸류)의 경우에는 카우치베이스 버킷의 경우 20MB, Memcached 방식의 버킷의 경우 1MB 까지 저장이 가능하다.

저장할때, 키와 값뿐만 아니라 메타데이타가 같이 저장되는데, 메타 데이타에는 CAS,TTL,Flag 3가지가 저장된다.

저장할때, 키와 값뿐만 아니라 메타데이타가 같이 저장되는데, 메타 데이타에는 CAS,TTL,Flag 값 3가지가 저장된다.

CAS는 데이타에 대한 일종의 타임 스탬프와 같은 개념으로, 여러 클라이언트가 같이 데이타를 ACCESS 했을때, 일관성(Consistent) 문제가 생기는 것을 해결해줄 수 있다. (자세한 내용은 나중에 설명한다.)

TTL 은 Time To Live 로, 데이타의 유효 시간을 정의한다. TTL을 정의해놓은 데이타는 TTL 시간이 지나면 자동으로 삭제 된다.

FLAG는 카우치베이스 클라이언트에서 사용하는 메타데이타 이다. 

이러한 메타데이타는 하나의 메타데이타 (CAS,TTL,Flag)는  60바이트의 메모리를 차지하게 된다.

카우치베이스 server는 모든 Key와 메타데이타를 메모리에 유지하기 때문에 데이터 모델링 또는 용량을 설계할때, 이 부분을 고려해서 RAM의 사이즈를 결정해야 한다.


버킷(Bucket)


버킷은 일종의 RDBMS의 데이타베이스 같은 공간이다. JSON 도큐먼트들은 이 버킷에 저장된다.

각각의 버킷은 고유의 속성 값을 가지고 있다. 버킷별 사용할 수 있는 메모리양, 옵션으로 버킷별로 접근할 수 있는 TCP 포트를 지정하거나, 접근 비밀번호를 지정할 수 있으며, 버킷에 들어가는 데이타에 대한 복제본의 수등을 정의할 수 있다.

하나의 클러스터에서 버킷은 최대 128개까지 생성할 수 있으나, 보통 성능상 10개를 권장한다.

http://docs.couchbase.com/couchbase-manual-2.0/#setting-maximum-buckets-for-clusters


뷰(View)


카우치베이스의 강력한 기능중의 하나인데, 이 뷰를 이용해서 RDBMS에서 제공되는 Indexing, grouping, sorting등을 가능하게 한다. 이 뷰는 데이타베이스 뷰와 유사한 개념을 갖는데, 카우치베이스의 뷰는 인크리멘탈 뷰 (Incremental view)라는 컨셉을 가지고 있다.

예를 들어 설명해보자. 사용자 정보를 저장하는 버킷에서, 사용자 정보를 저장하는 JSON 도큐먼트 안에 SSN(Social Security Number)라는 이름으로 주민등록 번호를 저장하는 필드가 있고, 이 주민등록 번호 필드에서 80년생 이하만 저장하는 뷰를 만든다고 하자.

데이타가 버킷에 저장될 때마다, 생성된 뷰에 같이 저장되는데, 이때, 뷰코드(View Code)라는 로직을 통해서 뷰에 저장된다. 뷰코드는 자바스크립트로 작성된 코드이다. 뷰코드에서 주민등록 번호가 80년생 이하일 경우에만 뷰에 저장하도록 정의한다.

 


결과적으로 데이타를 저장하거나 업데이트할때, 이 뷰코드가 매번 수행되게 되고 (마치 RDBMS의 트리거 처럼), 뷰코드에 정의된 알고리즘에 따라서 뷰에 데이타를 업데이트하게 된다.

이렇게 데이타가 저장/업데이트될때 마다 뷰를 증분적(Incremental)하게 업데이트 하기 때문에, 인크리멘탈 뷰라고 하며, 실제로 저장된 데이타가 많다고 하더라도 뷰에는 데이타가 업데이트 될때 하나만 추가/수정 되기 때문에 실제로 카우치베이스가 받는 로드는 그리 크지 않기 때문에, Indexing, grouping, sorting등에 사용한다 하더라도 성능상에 큰 문제를 가지지 않고 사용할 수 있는 것이다.


카우치베이스 시작하기


대략적인 개념을 잡았으면, 이제 카우치베이스를 직접 사용해보도록 하자. 


버킷 생성하기


버킷을 생성하기 위해서는 카우치베이스 웹콘솔에서 상단 “Data Buckets”를 선택한후 “Create New Data Bucket”을 선택한다.

 


버킷 생성을 선택하면 아래와 같이 버킷 생성 창이 나오는데, 버킷 이름을 “mybucket”이라고  지정하고,  개발 테스트용으로만 사용할 것이기 때문에, 이 버킷에서 사용할 메모리를 최소 사이즈인 100메가로 설정한후 버킷을 생성한다.

 



데이타 핸들링


버킷이 생성되었으면, 해당 버킷에 직접 데이타를 저장,조회,수정,삭제를 해보도록 하자.

앞에서 생성한 버킷의 우측 부분을 보면 “Documents”라는 버튼이 나온다. 이 버튼을 누르면, 현재 저장되어 있는 도큐먼트들을 보거나 또는 생성,수정,삭제 등이 가능하다.

“Create Document” 버튼을 누른후, 

Document ID에 “user0001”이라고 입력한다.

그리고 아래 그림과 같이 JSON 도큐먼트를 입력하고 Save 버튼을 누른다.


 

이때 주의할점은 카우치베이스에서 JSON 도큐먼트를 정의할때, 문자열은 “로 감싸야 한다. 일반 자바스크립트처럼 ‘로 감쌀 경우 에러가 나온다.

 


조회는 위의 보이는 Documents 메뉴창에서 Lookup Id를 선택하고, 도큐먼트 ID를 넣으면 해당 도큐먼트를 조회할 수 있고, 위의 도큐먼트 좌측에 보이는 “Edit Document”와 “Delete” 버튼을 이용하면, 수정 및 삭제를 할 수 있다.



저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

Couchbase Server

#1 소개 및 설치

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


근래에 여러 NoSQL이 소개되었지만 그중에서 좋은 솔루션인데도 불구하고 그다지 국내에서는 널리 알려지지 않은 카우치베이스에 대해서 소개하고자한다. 모바일 게임중에 유명한 쿠키런의 경우 카우치베이스를 백엔드로 사용하고 있는데, 안정성이나 성능등이 매우 뛰어나고, 사용하기 또한 매우 쉽다. 오늘은 고성능 NoSQL 서버인 카우치베이스(CouchBase) 에 대해서 소개하고자 한다.


소개


예전에 메모리 캐쉬 솔루션인 memcached에 디스크 persistence 기능을 추가하여 membase라는 솔루션이 있었는데, 이 제품에 Apache의 카우치디비(CouchDB)를 기반으로 새롭게 만든 솔루션이 카우치베이스 Server 라는 NoSQL 솔루션이다.

카우치베이스는 mongoDB나, Riak과 같이 JSON document를 직접 저장할 수 있는 Document DB 형태를 가지며, NoSQL의 분산 이론인 CAP theorem에서 CP (Consistency & Partition tolerance) 의 부분에 해당하여 데이타에 대한 일관성과, 노드간의 네트워크 장애시에도 서비스를 제공할 수 있다. 근래에 들어서 600억원의 투자를 유치하는 등 가치를 인정 받고 있는데, mongoDB나 Cassandra에 가려서 그다지 주목을 받지 못하는 것 같아서, 이번 글을 통해서 소개하고자한다.


특장점


카우치 베이스는 다른 NoSQL에 비해서 다음과 같은 추가적인 특징을 더 가지고 있다.


Memcached 기반의 Level 2 캐쉬를 내장하여 빠름

카우치베이스는 앞에서도 설명하였듯이 membase를 기반으로 하였기 때문에, memcached를 자체적으로 Level 2 캐쉬로 사용하고 있다. 즉 자체적으로 메모리 캐쉬 기능을 가지고 있기 때문에 성능이 대단히 빠르다. 이번에 카우치베이스 社에서 발표한 자료에 따르면 mongoDB대비 약 6배 이상의 성능을 낸다고 한다. 


(http://info.couchbase.com/2014-Benchmark-Showdown-Results-LP.html)


모바일 디바이스와 Sync

카우치베이스 뿐만 아니라, 카우치디비 계열 DB들은 iPhone이나 Android와 같은 모바일 디바이스에 탑재 할 수 있고, 서버에 설치되 카우치디비 계열들과 Sync가 가능하다. 카우치베이스도 역시, 카우치디비 계열이기 때문에, 모바일 디바이스에 탑재할 수 있고, 서버와 Sync할 수 가 있다.


데이타 센터간 복제 가능

카우치베이스는 XACR(Cross Data Center Replication)이라는 기능을 이용하여, 물리적으로 떨어진 데이타 센터간에 데이타 복제가 가능하다.


Indexing, Grouping ,Ordering,Join 가능

아주 중요한 특징중의 하나인데, 대부분의 NoSQL은 Key/Value Store 형식으로, 개별 필드에 대한 Indexing이나, 필드별로 group by 를 해서 sum,count등을 하는 기능이나, 특정 필드별로 Sorting이 불가능하다. Indexing을 지원하는 경우도 있기는 하지만, 내부적으로 성능상 문제가 있는 경우가 많은데, 카우치베이스의 경우 이러한 성능상 문제를 해결 하면서도 RDBMS들이 지원하는 index, grouping, ordering 기능을 지원할 수 있다. 


확장이 쉬움

보통 분산 구조의 NoSQL의 경우, 노드를 확장하거나 특정 노드가 장애가 났을때의 처리가 어려운데, 카우치베이스는 장애가 손쉽게 장애 처리를 하고,새로운 노드를 추가할때 도 매우 쉽게 노드 추가가 가능하다. 이러한 장점은 운영 관점에서 큰 이점이 된다.


Built in 관리 도구 제공

마지막으로 카우치베이스는 웹 기반의 GUI 관리 도구를 기본으로 제공한다. 많은 NoSQL들이 별도의 관리, 모니터링 도구를 지원하지 않는데 반하여, 기본적으로 강력한 관리 도구를 제공하는 것은 큰 장점이 될 수 있다.


Memcached 프로토콜 지원

캐쉬 솔루션으로 유명한 Memcached 프로토콜을 그대로 지원하기 때문에, 기존의 Memcached 클라이언트를 그대로 사용할 수 있고, 기존에 사용하던 Memcached 인프라를 그대로 대체 할 수 있다.


스키마가 없는 유연한 저장 구조 (Scheme-less)

 스키마가 없는 구조는 카우치베이스뿐만 아니라 대부분의 NoSQL이 갖는 공통적인 특성이다. 스키마가 없기 때문에 하나의 테이블에 컬럼 형식이 다른 데이타를 넣을 수 있다. 즉 하나의 데이타 버켓에 데이타 구조가 다른 JSON 문서들을 넣을 수 있다는 이야기이다.

데이타 타입이 다름에도 불구하고, 공통되는 필드에 대해서, Indexing, grouping 등을 제공할 수 있다. JSON 도큐먼트에, county 라는 앨리먼트가 있는 도큐먼트들을 대상으로 grouping등을 할 수 있다는 이야기이다.

다양한 클라이언트 플랫폼 지원

자바,닷넷,PHP,루비,C,파이썬,node.js 등 다양한 클라이언트 라이브러리를 제공한다. 클라이언트 SDK는 http://www.couchbase.com/communities/all-client-libraries 에서 다운로드 받을 수 있다.


설치하기


카우치베이스를 설치하기 위해서는 www.couchbase.com에서 카우치베이스를 맞는 OS 플랫폼에 따라서 다운로드 받으면 된다. Enterprise Edition과 Community Edition이 있는데, Enterprise Edition은 별도의 상용 라이센스를 구입해야 하며 기술 지원등을 받을 수 있다. Community Edition은 무료로 개발이나 운영 환경에 사용할 수 있으나, 기술 지원등을 받을 수 없고,  Enterprise Edition에 비해서 버전이 낮다.

※ 참고로, 상용과 오픈소스 라이센스 정책을 함께 지원하는 솔루션의 경우에는 버전업이 되면서 라이센스 정책이 갑자기 바뀌는 경우가 많으니, 오픈소스의 경우 다운로드 전에 반드시 라이센스 정책을 확인하기를 바란다. 이 글을 쓰는 현재 Community Edition의 라이센스 정책 기준은 2.2.0을 기준으로 한다. 

여기서는 윈도우즈 환경을 기준으로 설명을 한다. 사이트에서 카우치베이스 server 를 다운로드 받고 실행을 하면 자동으로 설치 위자드가 실행되고, 설치가 진행된다.

 


설치가 다 끝나면, 자동으로 웹페이지가 열리면서, 카우치베이스에 대한 셋업이 시작된다.

 


설정 셋업을 시작하면 기본적인 서버 설정에 대해서 물어보는데

 


데이타 파일을 저장하는 경로와, 호스트명등을 물어본다.그리고 새로운 클러스터를 시작할지, 아니면 기존의 클러스터에 조인할지를 물어보는데, 여기서는 개인 개발 환경을 설정하는 것이기 때문에, “Start a new cluster”로 설정한다. 이때, 카우치베이스가 사용할 메모리 용량을 지정해야 한다. 개인 개발환경이기 때문에, 1G정도로 설정하자. 실제 운영환경에서는 최대한 크게 잡아줘야 한다. 카우치베이스에 저장되는 키와 데이타에 대한 메타 데이타는 모두 메모리로 로딩되기 때문에, 메모리 용량이 충분하지 않으면 제대로된 성능을 발휘할 수 없다.

인스톨이 완료된후에, 카우치베이스 웹 콘솔을 열어 보면, 다음과 같이 인스톨된 카우치베이스 서버의 상태를 볼 수 있다. 

 




저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

빠르게 훝어보는 node.js

#7- mongoose ODM 을 이용한 MongoDB 연동

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


계정본이 http://bcho.tistory.com/1094 올라와 있습니다.


Mongoose ODM을 이용한 MongoDB의 연동

MongooseMongoDB 기반의 nodejs ODM (Object Data Mapping) 프레임웍이다. 앞에서 mongo-native에 대해서 알아봤는데, 그렇다면 mongoose는 무엇인가? 쉽게 생각하면 mongo-native JDBC 드라이브러를 이용한 데이타 베이스 프로그래밍이고, mongoose는 자바의 JPA/Hibernate/MyBatis와 같은 OR Mapper와 같은 개념이다.

mongodb 내의 데이타를 node.js내의 객체로 정의해준다. ODM 개념을 이용하여, MVC 개념들을 조금더 쉽게 구현할 수 있도록 도와 주며, 특히 javascript가 가지고 있는 한계성중인 하나인 모호성을 보완해준다. mongodb json을 저장할때, collection에 들어가는 데이타의 형태(스키마)가 없기 때문에 자유도가 높기는 하지만 반대로 RDBMS에서 정의된 스키마 개념이 없이 때문에 어떤 컬럼이 있는지 코드에서로만은 파악하기가 어려울 수 있다. 이런점을 보완하는 개념이 mongoose의 스키마의 개념인데, 직접 코드를 살펴보자

다음 예제는 HTML에서 이름과 메모를 받아서 DB에 저장하고 조회 하는 예제이다.



<!DOCTYPE html>

<html>

<head>

    <title></title>

</head>

<body>

    <form name="memo" method="post" action="/insert">

        name <input type="text" name="username"/>

        <br>

        message <input type="text" name="memo"/>

        <button type="submit" >Submit</button>

    </form>

</body>

</html>

먼저 실행에 앞서서 npm install mongoose를 이용해서 mongoose 모듈을 설치해야 한다.

다음은 express /app.js 파일이다.

var express = require('express');

var routes = require('./routes');

var user = require('./routes/user');

var http = require('http');

var path = require('path');

 

mongoose를 사용하기 위해서 해당 모듈을 import한다.

var mongoose = require('mongoose');

 

다음으로 Schema를 정의하는데, 이 스키마는 username memo라는 필드를 가지고 있으며 각각의 필드는 String 데이타 타입을 갖는다.

var MemoSchema= mongoose.Schema({username:String,memo:String});

이 스키마를 이용해서 아래와 같이 모델을 정의하는데, 첫번째 인자는 이 모델이 mongodb에 저장될 Collection이름(테이블명)이 되고, 두번째 인자는 이 모델을 정의하는데 사용할 스키마(앞에서 정의한)를 지정한다.

참고

 

mongoose에서는 다양한 데이타 타입을 이용하여 계층화된 스키마를 정의하는 게 가능하다. 아래는 http://mongoosejs.com/docs/schematypes.html 에 정의된 예제 스키마 중의 하나이다.

var schema = new Schema({

  name:    String,

  binary:  Buffer,

  living:  Boolean,

  updated: { type: Date, default: Date.now }

  age:     { type: Number, min: 18, max: 65 }

  mixed:   Schema.Types.Mixed,

  _someId: Schema.Types.ObjectId,

  array:      [],

  ofString:   [String],

  ofNumber:   [Number],

  ofDates:    [Date],

  ofBuffer:   [Buffer],

  ofBoolean:  [Boolean],

  ofMixed:    [Schema.Types.Mixed],

  ofObjectId: [Schema.Types.ObjectId],

  nested: {

    stuff: { type: String, lowercase: true, trim: true }

  }

})

 

 

var Memo = mongoose.model('MemoModel',MemoSchema); // MemoModel : mongodb collection name

다음으로 express를 사용하기 위한 기본 설정을 아래와 같이 하고

var app = express();

 

// all environments

app.set('port', process.env.PORT || 3000);

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

app.use(express.static(path.join(__dirname, 'public')));

 

HTTP/POST로 들어오는 요청을 mongodb에 저장하는 로직을 구현한다.

 

app.post('/insert', function(req,res,err){

    var memo = new Memo({username:req.body.username,memo:req.body.memo});

    memo.save(function(err,silence){

if(err){

            console.err(err);

            throw err;

}

res.send('success');

    });

});

Memo 모델 클래스를 이용해서 memo 객체를 만드는데, username은 앞의 index.html의 폼에서 입력받은 username 값을, memo form에서 입력받은 memo 값으로 memo 객체를 생성한다.

저장하는 방법은 간단하다. memo객체의 save라는 메서드를 호출하고, 비동기 IO이기 때문에, callback 함수를 바인딩 하였다. callback함수에서는 데이타 저장 처리가 끝나면, res.send success 메세지를 출력한다.

다음으로 mongoose mongodb에 연결하고, http server를 기동시켜 보자

mongoose.connect('mongodb://localhost/terrydb',function(err){

    if(err){

        console.log('mongoose connection error :'+err);

        throw err;

    }

    http.createServer(app).listen(app.get('port'), function(){

      console.log('Express server listening on port ' + app.get('port'));

    });

});

mongoose.connect를 이용하여 mongodb에 접속한다. 이때, 접속 URL을 써주면 된다. 여기서는 localhost terrydb를 사용하도록 정의하였다. 여기서 지정한 옵션 이외에도, 포트 #, connection pool 설정등 다양한 옵션을 적용할 수 있다. 자세한 내용은 mongoose 문서 http://mongoosejs.com/docs/connections.html 를 참고하기 바란다.

connect 메서드는 두번째 인자로 callback 함수를 받는데, 이 예제에서는 callback함수에서 http server를 기동하였다. 이는 mongodb가 연결된 다음에 서비스가 가능하기 때문에, mongodb 연결후에 request를 받기 위해서 callback에서 http server를 기동한 것이다.

그러면 실행을 하고 결과를 보자. http://localhost:3000으로 접속하여 위에서 나타난 index.html 폼에 데이타를 넣고, robomongo를 이용해서 그 결과를 살펴보면 아래와 같이 값이 들어간 것을 확인할 수 있다.



자아, mongoose를 이용해서 데이타를 저장하였다. 그러면 추가로 데이타를 조회하는 기능을 구현해 보자

다음은 http://localhost:3000/users/{username}이 들어오면 {username}값의 데이타를 조회해주는 함수이다.

app.get('/users/:username', function(req,res,err){

    var memos = new Memo();

    Memo.findOne({'username':req.params.username},function(err,memo){

        if(err){

            console.err(err);

            throw err;

        }

        console.log(memo);

        res.send(200,memo);

    });

});

app.get('/users/:username' 에서 :username 을 이용하여 URL Parameter username을 받고

Memo 모델의 findOne이라는 메서드를 이용해서 데이타를 가져왔다., findOne query 조건에 부합하는 데이타중에 하나만 리턴하는 함수이다첫번째 인자가 검색 조건인데 여기서는 데이타베이스에서 필드가 username인 필드의 값이 앞에 URL에서 받은 username과 일치하는 레코드를 받도록 하였다.

두 번째 인자는 데이타가 리턴되었을때 수행되는 callback함수 인데, callback 함수 두번째 인자인 memo에 리턴되는 데이타가 저장된다. memo객체로는 JSON 데이타가 리턴되고, JSONres.send(200,memo); 을 이용하여, 리턴하였다. 이 코드를 추가한 후에, 실행해보면 다음과 같은 결과를 얻을 수 있다.



이번에는 memomodels 전체 테이블을 쿼리 해보자 (select * from memomodels)

app.get('/users', function(req,res,err){

    var memos = new Memo();

    Memo.find().select('username').exec(function(err,memos){

        if(err){

            console.err(err);

            throw err;

        }

        console.log(memos);

        res.send(memos);

    });

});

Memo.find()를 하면되는데, 예제에서 .select(‘username’)을 추가하였다. 이 메서드는 select username from memomodels 라고 생각하면 된다. 즉 쿼리해온 값중에서 특정 필드값만을 리턴하도록 하는 것이다.

이 함수를 추가해서 실행해보면 다음과 같은 결과를 얻을 수 있다.



 

아래는 http://mongoosejs.com/docs/queries.html 에서 참고한 샘플인데, where문을 이용한 검색 조건 지정에서 부터, select해 오는 개수, Sorting order, 특정 필드만 가지고 오는 등의 다양한 쿼리 조건을 지정한 예제이다.

Person

.find({ occupation: /host/ })

.where('name.last').equals('Ghost')

.where('age').gt(17).lt(66)

.where('likes').in(['vaporizing', 'talking'])

.limit(10)

.sort('-occupation')

.select('name occupation')

.exec(callback);

 

이외에도 다양한 쿼리 조건을 지정할 수 있으니 자세한 내용은 http://mongoosejs.com/docs/queries.html 를 참고하기 바란다.

 

데이타 validation 하기

mongoose의 유용한 기능중의 다른 하나가 Schema에 대한 validation이다. Schema를 정의할 때, 데이타 타입이나 기타 규칙을 정해놓고, 이 규칙을 벗어나면 에러 처리를 해주게 하는 것인데, 웹 개발등에서는 워낙 일반적인 내용이니 구체적인 개념 설명은 생략하겠다.

Validator를 사용하는 방법은 앞에 구현한 코드 부분에서 Schema를 정의 한 부분을 다음과 같이 변경 한다.

// define validator

function NameAlphabeticValidator(val){

    return val.match("^[a-zA-Z\(\)]+$");

}

function MemoLengthValidator(val){

    if(val.length>10) return null;

    return val;

}

 

// schema definition with validation

var MemoSchema= mongoose.Schema({

                    username:{type:String,validate:NameAlphabeticValidator}

                    ,memo:{type:String,validate:[

                                    {validator:MemoLengthValidator,msg:'memo length should be less than 10'},

                                    {validator:NameAlphabeticValidator,msg:'PATH `{PATH}` should be alphabet only. Current value is `{VALUE}` '}

                                    ]}

    });

var Memo = mongoose.model('MemoModel',MemoSchema); // MemoModel : mongodb collection name

 

먼저 validation rule을 정의해야 하는데, validation rule을 함수로 구현하면 된다.

이를 validator라고 하는데, NameAlphabeticValidator는 들어오는 인자가 영문일 경우에만 PASS하고, 숫자나 특수 문자가 들어오면 오류 처리를 한다. 다음으로 정의한 MemoLengthValidator의 경우에는 문자열의 길이가 10자 이상인 경우에 에러 처리를 한다.

이렇게 정의한 validator를 스키마 정의시 각 데이타 필드에 지정하면 된다. username에는 위와 같이 validator NameAlphabeticValidator를 적용하였다.

다음으로 memo에는 동시에 NameAlphabeticValidator MemoLengthValidator 두 개를 동시에 적용하였는데, 적용할때 msg 인자로 validation이 실패했을 때 리턴해주는 메세지도 함께 지정하였다.

이 메세지 부분에서 NameAlphabeticValidator 에서 발생하는 에러 메세지를 주의 깊게 보면 {PATH} {VALUE} 가 사용된 것을 볼 수 있는데, {PATH}는 이 에러가 발생하는 JSON 필드명을 그리고 {VALUE}는 실제로 입력된 값을 출력한다. 테스트를 해보면



message부분에 특수문자와 숫자를 넣었다. 실행 결과는 아래와 같이 에러 메세지들이 console로 출력되는데, 아래서 보는 바와 같이 message 부분에 {PATH} {VALUE}가 각각 memo Memo-1 값으로 대체 된것을 확인할 수 있다



 

mongoose는 어디에 쓰는게 좋을까?

mongo native가 있고, mongoose가 있는데, 그러면 각각을 어디에 쓰느냐? 이 질문은 JDBC JPA를 언제 쓰느냐? 와 같은 질문과 같지 않을까 싶다.

mongoose를만든 커미터에 따르면 mongodb-native모듈이 mongoose보다 빠르다고 한다. 즉 조금 더 유연한 mongodb에 대한 access가 필요하고 높은 성능을 요구할 경우에는 mongodb-native를 사용하고, 정형화 되고 스키마 정의를 통한 명시성 확보가 필요하며 validation등을 효율적으로 하고자 할때 mongoose를 사용하는 것이 좋다. 실제 프로그램에서는 위의 용도에 맞게 두 프레임웍을 섞어 쓰는 것이 좋다

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

빠르게 훝어보는 node.js

#6- mongo-native 모듈을 이용한 MongoDB 연동

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


아래 글은 오래되서 monk를 이용한 방식으로 새로운 글을 작성하였습니다.

새 글은 node.js 4.x , express 4.x 에서 기동하도록 작성되었습니다.

링크 : 

http://bcho.tistory.com/1080

Persistence 연동

node.js DB NoSQL등의 연동을 지원하는데, 이 역시 철저하게 non-blocking io 방식으로 동작한다. db 연결 socket을 열어서 query를 던져놓고, query 결과가 오면 이벤트를 받아서 callback 함수로 처리하는 순서이다.

그러면 여기서는 몇가지 persistence 연동 방식에 대해서 알아보도록 한다.

MongoDB

먼저 mongodb nosql 데이터베이스중에 가장 많이 사용되는 제품중의 하나이다. Json document를 저장하는 스타일의 document db이며, index grouping과 같은 RDBMS와 유사한 기능까지 지원하기 때문에 사용이 매우 쉽다.

설치 테스트

Mongodb mongodb.org에서 다운로드 받을 수 있다. Mongodb는 무료 tutorial이 잘되어 있는 것이 많은데, https://university.mongodb.com/ 에 가면 언어별 (node.js용도 있다) 튜토리얼이 있으니 참고하기 바란다.

윈도우를 기준으로 다운 받아서 압축을 푼후,  db 디렉토리를 만들어 주어야 하는데, 필요한 곳에 디렉토리를 만든다. 여기서 C:\dev\mongodb-win32-x86_64-2.4.3 \data 에 만들었다.

C:\dev\mongodb-win32-x86_64-2.4.3 에서 다음과 같은 명령어를 이용하여 구동한다.

.\bin\mongod --dbpath C:\dev\mongodb-win32-x86_64-2.4.3\data

인스톨이 끝났으면 간단한 테스트를 해보자, ./bin/mongo.exe를 수행하면 Java Script 기반의 쉘이 수행된다. 이해를 돕기 위해서 하나의 테이블에 Insert , select, delete, update를 수행하는 명령을 SQL 문장과 비교해서 소개한다.

Insert

SQL          : insert into users ("name","city") values("terry","seoul")

Mongo DB     : db.users.insert({_id:"terry",city:"seoul"})

 

Select

SQL          : select * from users where id="terry"

Mongo DB     : db.users.find({_id:"terry"})

 

Update

SQL          : update users set city="busan" where _id="terry"

Mongo DB     : db.users.update( {_id:"terry"}, {$set :{ city:"Busan" } } )

 

Delete

SQL          : delete from users where _id="terry"

Mongo DB     : db.users.remove({_id:"terry"})

 

간단하게 나마, mongodb query에 대해서 설명하였다.

개념적을 약간 더 설명하자면, dbRDBMS db와 같은 개념으로 보면 되고, collection rdbms의 하나의 테이블로 보면 된다. collection에 들어가는 데이터 필드중에서 _id predefined된 필드로 해당 데이터에 대한 primary key로 생각하면 되고, 위와 같이 사용자가 직접 값을 넣어서 입력할 수도 있고 또는 값을 넣지 않으면 mongodb에 의해서 unique한 값으로 자동 지정된다.

조금 더 자세한 쿼리에 대한 설명은 http://docs.mongodb.org/manual/crud/ 를 참고하기 바란다.

GUI 툴로는 robomongo 라는 툴이 있다. http://robomongo.org/



mongodb-native 모듈을 이용하기

node.js에서 mongodb를 연결하는 방법은 여러가지가 있다. 먼저 가장 널리 사용되는 mongo-native 모듈에 대해서 알아보자 https://github.com/mongodb/node-mongodb-native

1) mongo native module 설치하기

mongo native module

% npm install mongo 로 설치가 가능하며, 설치중에 native 모듈을 컴파일 하기 때문에 반드시 컴파일러 (Windows의 경우 Visual C++)이 깔려 있어야 한다.

2) 간단한 쿼리 수행하기

설치가 끝났으면 간단하게 select를 해서 내용을 json으로 리턴하는 코드를 만들어보자. Select를 할것이기 때문에 미리 mongo에 값을 넣어보자

% mongo.exe 를 실행한 후에 다음 쿼리를 수행한다.

db.users.insert({_id:'terry',city:'seoul'});

db.users.insert({_id:'cath',city:'suwon'});

 

제대로 입력이 되었는지 select를 해본다.



아래는 mongodb 도구인 robomongo를 이용해서 데이터가 들어가 있는 것을 확인한 화면이다.



자아 그러면 이제 코드를 구현해보자

var express = require('express');

var routes = require('./routes');

var http = require('http');

var path = require('path');

var app = express();

var MongoClient = require('mongodb').MongoClient

var Server = require('mongodb').Server;

먼저 위와 같이 MongoClient Server 클래스를 생성한다

// all environments

app.set('port', process.env.PORT || 3000);

app.set('views', path.join(__dirname, 'views'));

app.set('view engine', 'ejs');

app.use(express.logger('dev'));

app.use(express.json());

app.use(express.urlencoded());

app.use(express.methodOverride());

app.use(app.router);

app.use(express.static(path.join(__dirname, 'public')));

 

var mongoclient = new MongoClient(new Server('localhost',27017,{'native_parser':true}));

var db = mongoclient.db('test');

다음으로 mongoclient를 생성한다. 일종의 Connection 설정이라고 보면 되는데, 설정만 여기서 하는 것이지 실제 연결은 open을 호출해야 된다. 다음으로 client연결을 이용해서 사용할 db를 선택한다. 여기서는 ‘test’ db를 사용하였다.

 

app.get('/', function(req,res) {

   db.collection('users').findOne({},function(err,doc){

       if(err) throw err;

       res.send(doc);

   });

});

Db 객체를 얻었으면, 쿼리를 수행할 수 있는데, collection(‘collection.즉 테이블명’)으로 collection을 선택한후, findOne()메서드를 이용하여 하나의 row select를 하였다. 비동기 호출이기 때문에 query 수행이 끝나면, function(err,doc) callback 함수를 호출하는데, 위에서는 에러가 났을때는 err throw하고 에러가 없을 경우에는 리턴받은 json document response return하도록 하였다. 여기 까지 끝났으면, 실제 mongodb를 연결해보자 연결은 mongoclient.open을 하면 되는데, 연결이 완료되면 open안에 정의된 callback method를 호출한다.

아래코드를 보면 이 callback 메서드 안에서 httpServer를 띄운 것을 볼 수 있는데, http server가 기동되서 mongodb를 사용하기 때문에 mongoclient를 먼저 띄우기 위해서 httpserver callback안에 넣었다.

 

mongoclient.open(function(err, mongoclient) {

    if(err) throw err;

    console.log('mongo client connected');

    http.createServer(app).listen(app.get('port'), function(){

        console.log('Express server listening on port ' + app.get('port'));

    });

 

});

 

코드 구현이 끝났으면 실행해보자. 다음과 같이 레코드가 JSON으로 리턴됨을 확인할 수 있다.



이 코드에서 보면 mongoclient를 하나만 생성하였다. 그렇다면 내부적으로 물리적인 connection이 하나만 생길까? Mongoclient는 내부적으로 connection pooling을 이용한다. 그래서 별도의 설정을 해주지 않아도 내부적으로 여러 개의 connection을 여는데, default 5개의 connection을 열도록 되어 있고, connection의 수는 open 옵션에서 조정할 수 있다.

var mongoclient = new MongoClient(new Server('localhost',27017,{'native_parser':true,'poolSize':8,'maxPoolSize':10}));

 

위의 코드는 max connection 10개로 하고, 초기에 poolSize 8개로 지정한 경우이다. 실제로 기동해보면 8개의 connection이 생성되었음을 확인할 수 있다.

3) Insert, update and delete

Select는 해봤는데, 그러면 insert,update,delete는 어떻게 할까? mongodb query와 매우 유사하다.먼저 Insert를 보자

db.collection('users').insert({city:'suji'},function(err,doc){

       console.log('inserted '+doc[0]._id+':'+doc[0].city);

   });

위와 같이 collection을 선택한다음에, insert메서드를 이용해서 json document를 넣으면 된다. 위에서는 callback function을 지정한 예인데, callback function은 생략할 수 도 있다. 위의 코드를 잘 보면 앞에 예제와는 다른게 _id값을 입력하지 않은 것을 볼 수 있다. 이 경우 _id값은 자동으로 mongodb가 생성하여 unique한 값을 넣게 된다.



그리고 insert된 값은 callback의 두번째 인자에서 array 형태로 넘겨 받게 된다.

또는

db.collection('users').insert([{city:'suji'},{city:'busan'}],function(err,doc){

          

와 같은 배열 형태를 사용하면, 하나의 insert문장으로 여러 개의 document를 동시에 insert할 수 있다.( batch insert가 가능하다)

삭제는 insert와 유사하게 remove 메서드를 이용하면 된다.

db.collection('users').remove({city:'busan'},function(err,doc){});

위의 예제는 city 필드가 busan인 것을 삭제한 것인데, _id 필드 이외에는 index로 지정이 되어 있지 않기 때문에 index를 지정하지 않은 필드로 삭제등을 했을 경우 table full scan이 발생할 수 있으니 주의하도록 해야 한다. (한마디로 느려진다는 이야기)

다음으로 수정은 collection.update 메서드를 이용하면 된다.

db.collection('users').update({_id:'terry'},{$set:{'sex':'male'}},function(err,doc){

       throw err;

   });

_id terry document‘sex’ 필드를 ‘male’로 변경하는 쿼리이다. 이미 ‘sex’필드가 있으면 그 내용을 ‘male’로 바꾸고, 없으면 새롭게 ‘sex’필드를 만든다.앞에 $set을 줬기 때문에 ‘sex’필드만 내용을 바꾸는데,

db.collection('users').update({_id:'terry'},{'sex':'male'},function(err,doc){

       throw err;

   });

$set을 빼버리게 되면 _id=’terry’ document의 내용을 {‘sex’:’male’}로 바꿔 버린다. (필드만 추가하는게 아니다.)

4) Search

앞에서는 findOne만 해서 하나의 record query 하는 예제 였는데, 여러 개의 record를 받고 싶으면 find 메서드를 사용해서 검색 조건을 주고 .toArray를 호출하면 인자로 넘어가는 callback함수의 docs 인자에 쿼리 결과를 배열로 리턴해준다.

db.collection('users').find({city:'suji'}).toArray(function(err,docs) {

       if (err) throw err;

       res.send(docs);

       for (i = 0; i < docs.length; i++) {

           console.log(docs[i].city);

       }

   });

여기서는 아주 기본적인 API만을 다뤘기 때문에 자세한 API들은 http://mongodb.github.io/node-mongodb-native/api-generated 를 참고하기 바란다

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

전준식 이사님의 Infinispan 강의 내용 요약 정리


In Memory Data Grid - Infinispan(JBoss Data Grid) Webinar from Opennaru on Vimeo.


[Cosistent hashing]

- Hash Ring 형태로 저장

- 서버가 늘어나고 줄어드는 것에 대해서 대응이 가능한 구조

1번은 0번부터

2번은 27번부터

3번은 50번부터의 해쉬 값을 저장함.


장애 대응

예를 들어, 2번이 죽으면 1번이 0~49번까지 보관함

복제는, 다른 서버에다가도 값을 복제 해놓음.


이 알고리즘을 이용하면, memcached 등을 이용해서도 고가용 서버를 만들 수 있음


Segment(Replica,Virtual node)

- Hash ring에 가상의 노드를 만들어서, 특정 서버에 값이 몰리는 현상을 방지함

Inifinispan은 32bit int를 key로 사용하는데 통계적으로 100~200개정도의 Virtual Node를 사용하면 데이타의 몰림 현상이 일반적으로 해결됨

즉 예를 들어서, 서버가 4대면, segment를 50으로 잡으면 노드가 200개 정도로 ㅓㄹ정됨.


451 group reseach

-NewSQL : RDBMS의 기능을 가지면서, 분산 확장 개념을 가지는 DB - VoltDB


[Data Grid 기본 개념]

- in memory storage

- networked memory

- ★ pacemakers to database

- provide simple KV storage

- Linear scalability and eslasticity. (distributed)


사용 용도

- DB Cache, Session Clustering

※ EAS6 (JBoss)의 경우, Infinispan을 default로 탑재되서 session clustering에 사용됨


[Infinispan]

- Apache 2.0 license / Java와 Scala 언어로 이용하여 구현됨 (핵심 부분은 Scala 구현체)

- 두가지 제품이 있음

  1) Library 모드 - 임베딩 가능한 형태. 상세한 API 지원 가능하며, 고성능 지원 가능. Java만 지원 (Comment : HazelCast와 비슷한 개념). 

  2) Client/Server 모드 - 독립 서버. 다양한 언어 지원이 가능함. 

     : memcached,Hot Rod (Infinispan native protocol), REST 프로토콜을 사용함

     : 고급 기능은 사용 불가. MR,JTA Tx등. 

  대부분의 경우 Library 모드를 사용함.

- Python,Java,.NET 등에서 사용 가능.

- 주요 특징

  1) JTA TX 지원

  2) CacheStore (Disk,DB) Persistence 지원

  3) DisributedExecutors 지원 (일종의 MAP을 분산노드에서 실행하는 개념과 유사)

  4) M 지원

  5) FTS (Lucene API) 지원

  6) GridFileSystem - 메모리상의 파일 시스템 제공 (메모리를 기반으로 파일시시틈을 지원하는)

  

- 분산 tolpology

1) Local Storage - Local Cache 용도로 사용. 노드간 복제 않함

 Write through, Write behind

 Eviction

 Expiration (Time)

 Transaction

2) Replication Cache  - 모든 node가 같은 값을 가짐

 서버 개수만큼 복제해야 하기 때문에, 성능 문제가 있을 수 있다. 10대 서버 이하일 경우에만 사용 권장

3) Distributed Cache - 데이타를 분산 저장함. 장애 대응을 위해서 백업본은 다른 노드에도 저장함. 현재 노드에 데이타가 없으면 다른 노드에서 긁어옴. (매번) 

 GET시에는 최대 1회의 remote 호출 (현재 서버에 값이 없으면)이 발생할 수 있음

4) L1 + Disribution - 매번 다른 서버의 데이타를 가지고 오는 것을 줄이기 위해서 L1 캐쉬를 잡아놓고, 다른 서버에서 오는 값을 L1에 저장함

5) Invalidation Cache - 각 노드가 같은 데이타를 저장. 단 데이타 삭제가 발생하면, 다른 노드에도 데이타 삭제를 요청함. 2)번과 비슷

 데이타베이스와 같은 영속적인 스토어가 있는 경우에만 사용(??)

--> Topology에 따라서 성능이 다름.


- Evition and Expiration

Policy - None,FIFO,LRU,Unordered(무작위),LIRS (Low Inter reference recency Set)

예) <eviction maxEntries=1000 strategy=LRU wakeUpInterval=500>

500ms 마다 eviction을 LRU 정책으로 하고, 1000개의 레코드만 유지하도록 한다.


cf. Eviction : 엔트리의 수가 넘었을때 삭제, Expiration : 일정 시간이 지나면 삭제

Expriation의 예 - Session Time out 등

예) <expiration wakeupinternal=500 lifespan=60000 maxIdle=10000>

생성된지, 60,000 ms 가 되거나

사용된지 10,000ms 되면 삭제

500ms 마다 expiration check 수행


- DataGrid Store

1) 캐쉬에서 넘쳐나는 것을 저장하거나

2) 리스타트 되었을 때 캐쉬를 다시 복구 하는 용도(Redis와 비슷하네)

 DB나 파일,클라우드 스토리지(S3,Swift등) Remote Store(다른 데이타 그리드), Cassandra등에 저장가능


CacheStore에는 writethrough(Sync), write behind(Async) 로 설정 가능


- 코드 예제

아래는 Client/Server mode (Hot Rod 프로토콜을 사요아는 경우)

RemoteCacheManager manager = new RemoteCacheManager("ip:port,ip:port,ip:port,....")

Cache <String,String> cache = manager.getCache("cachename")

cache.put (key,value)

cache.get (key)

cache.remove (key)


대부분의 DataGrid는 사용 방식이 거의 비슷함. 거의 다 put,get,remove


- 부가 기능

1. Data Grid Disributed Executor : 분산 실행 기능. 데이타가 저장되어 있는 인스턴스에서 실행하여 결과를 반환. 거의 디스크 access나 네트워크 액세스를 줄이고 실행

Callable의 call(),setEnvironment()를 implement

Callable을 구현한후, executor에 submit을 하면 됨.

Callable은 각 값이 저장된 Cache 노드에 분산되서 실행된후에, List 형태로 그 값을 반환함. 


2. MR 기능 제공

 Hadoop 의 분산 병령 처리와 동일


3. DataGrid Query

 Apache Lucene (FTS)를 이용. Index를 만들어놓고, Hibernate Search API를 이용하여 검색함

 Index를 어디에 저장할것인가가. 디자인 이슈 (분산캐쉬에 넣어도 되고, 디스크에 넣어도 되고, 다향한 configuration이 있음)


4. 기타 확장 API

org.infinispan.cache 는 java / ConcurrentMap을 상속

 putAsync,getAsync과 같은 추가 API를 지원함

 @Annotation을 지원 - Cache에 listener등을 걸거나 하는 것등이 가능함

 JTA를 지원하며, 명시적으로  lock을 걸 수 있음

 Batch Put을 지원

 Custom Interceptor 지원 - lock 걸렸을때, put할때, get 할때 등등

 Listener 지원(이벤트가 발생했을때, listener를 실행해줌)

 Queue 형태의 자료구조는 시스템적으로는 지원하지 않음 (Redis는 지원). -> K/V 형태로 해서 Linked List형태로 구현이 가능

※ interceptor와 listener 차이가 모지? 비슷해 보이는데.

근데. 내부적으로 JGroup을 사용하네.


- 성능 측정 (Radar Gun 프로젝트 - Infinispan의 프로젝트로 DataGrid들에 대한 성능 비교 프레임웍)

Coherence와 비교했을때, inifinispan이 더 빠르네??


- Azul Zing (JVM) - Stop the world 현상이 적음. -Xmx40g 사용 가능

http://www.azulsystems.com/products/zing/whatisit



[추가내용 - Infinispan vs Hazelcast]

Infinispan 
전체적으로 캐쉬의 개념이 강함
- K/V 형식으로 get/set 만 제공
- TTL 기능 있음.
- Local / Server 방식으로 클러스터 구성 가능함
- size(), values(), keySet(), entrSet()등 Full Scan 관련 API는 Concurrency 문제와, Performance 문제를 유발함
- JTA 기반 Tx 지원
- Index 개념 있음
- Query 기능
  * Search/Filtering 지원 - Hibernate Search framework과 Apache Lucene으로 구현됨 (Full Scan 이 아니라 Index와 Lucene을 사용하니 단순 full scan 방식은 아니여서 쓸만할듯)
  * Sorting 지원
  * Pagenation 지원
- Map & reduce 지원
- Locking ?
- Eviction - LRU,TTL, Custom LRU 지원
- Executor 개념 지원
  
HazelCast
- 데이타 타입 : Distributed java.util.{Queue, Set, List, Map}
- Queue/Topic 개념 지원 ★
- Locking
  * global lock 지원
- Eviction - LRU,LFU,TTL 지원
- Query는 지원하긴 하지만, 전체 노드에 분산 수행되기 때문에 효율적이지 않음 (Riak과 비슷하네.)
- Index 지원함. 
- Trasaction 지원
- Executor 개념 지원 
- WAN Replication 지원


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

CouchDB Overview


어제 SSAG 에서 정명훈 이사가 강의한 CouchDB에 대한 내용 정리


CouchDB 일반 특징

CouchDB Apache 프로젝트로 MongoDB와 같은 Document DB의 형태를 띄며, NOSQL CAP이론중 AP 에 해당 한다. (장애에 매우 강하다.) 단 Consistency는 Eventual Consistency를 제공한다. (버전으로 하는 방식), Eventual Consistency 모델이기 때문에 Locking을 사용하지 않는다. (Optimistic Lock)


유사 프로젝트

유사 프로젝트로 CouchBase와 BigCouch, Cloudant등이 있다.

CouchBase는 memcached와 CouchDB를 합쳐놓은 제품으로, 앞단에 캐쉬가 있어서 성능이 매우 빠르다. BigCouch는 CouchDB가 single instance만 지원하는데 반해서, 클러스터링 기능을 가지고 있다. Cloudant는 이 BigCouch를 기반한 제품으로 DBaaS 형식의 서비스로, DB 서비스를 제공하고, 운영을 대행해준다. 향후 BigCouch와 CouchDB가 합쳐져서 클러스터링 기능을 제공할 예정이다.

그 외에도 mobile에 탑재 가능한 버전의 CouchDB들이 있다.


Secondary Index

재미있는 특징중의 하나가 Sencondary Index를 지원하는 데 이는 내부적인 Map & Reduce를 통해서 구현되는데, 2nd index search시마다 MR이 도는 것이 아니라, 2nd index를 만들어 놓은 다음에 이를 View와 같은 형태로 저장해놓고, 이 index 내에서 검색을 한다. 또한 새로운 데이타가 추가되거나 변경이 되더라도 전체 index를 rebuild하는 것이 아니라, incremental 방식으로 변경된 부분만 재 생성한다.

물론 이 index를 rebuild하는 과정에서 서버가 부하를 받아서 성능이 다소 떨어질 수 있으나, 초당 수천건 정도의 index 변경은 수용할 수 있는 수준이라고 한다.


Replication

CouchDB의 강력한 기능중의 하나가 복제 기능인데, 단방향, 양방향 복제, 1:N, M:N 그리고 필터나 룰 기반의 복제(서브셋만 복제하는 기능)도 가능하다. 1회성으로 복제하는 시나리오와 지속적으로 복제하는 시나리오도 가능하면, 네트워크에서 끊어졌다가 나중에 붙어도 복제 및 Sync만 하는 것도 가능하다.

무엇보다 CouchDB는 모바일 버전이 있기 때문에, 이를 이용하면 쉽게 단말과 서버간의 Sync 시나리오를 구현할 수 있다. 오픈 소스로 파일이나 이미지를 서버와 디바이스간에 복제하는 프로젝트가 있다.


Interface

인터페이스는 HTTP REST만 지원한다. 아쉬운 부분인데, 성능이 높은 Protocol Buffer등을 향후 지원했으면 한다.


보안

HTTPS와 OAuth 인증을 제공하며, HTTP Basic Auth도 지원한다. User Identity source (LDAP)등의 연동에 대해서는 자세하게 설명되지 않았는데, 앞단에 reverse proxy를 둬서 인증하는 방식으로 구현이 가능하리라 본다.


대략적인 아키텍쳐

기본적인 메카니즘이 append only 구조로, 일정 이상의 데이타가 쌓이면 내부적으로 자동 Compaction을 한다.

Erlang으로 구현되어 있으며, 그 위에 javascript를 제공하여, MR등은 javascript를 사용하여 구현한다.



저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

redis Introduction


Intro
Redis는 "REmote DIctionary System"의 약자로 메모리 기반의 Key/Value Store 이다.
Cassandra나 HBase와 같이 NoSQL DBMS로 분류되기도 하고, memcached와 같은 In memory 솔루션으로 분리되기도 한다.
성능은 memcached에 버금가면서 다양한 데이타 구조체를 지원함으로써 Message Queue, Shared memory, Remote Dictionary 용도로도 사용될 수 있으며, 이런 이유로 인스탄트그램, 네이버 재팬의 LINE 메신져 서비스, StackOverflow,Blizzard,digg 등 여러 소셜 서비스에 널리 사용되고 있다.
BSD 라이센스 기반의 오픈 소스이며 최근 VMWare에 인수되어 계속해서 업그레이드가 되고 있다.
16,000 라인정도의 C 코드로 작성되었으며, 클라이언트 SDK로는
Action Script,C,C#,C++,Clojure,Erlang,Java,Node.js,Objective-C,Perl,PHP,Python,Smalltalk,Tcl등 대부분의 언어를 지원한다. (참고 : http://www.redis.io/clients )

이번 글에서는 Redis란 무엇인지, 그리고 대략적인 내부 구조에 대해서 살펴보도록 한다.

1. Key/Value Store
Redis는 기본적으로 Key/Value Store이다. 특정 키 값에 값을 저장하는 구조로 되어 있고 기본적인 PUT/GET Operation을 지원한다.

단, 이 모든 데이타는 메모리에 저장되고, 이로 인하여 매우 빠른 write/read 속도를 보장한다. 그래서 전체 저장 가능한 데이타 용량은 물리적인 메모리 크기를 넘어설 수 있다. (물론 OS의 disk swapping 영역등을 사용하여 확장은 가능하겠지만 성능이 급격하게 떨어지기 때문에 의미가 없다.)
데이타 억세스는 메모리에서 일어나지만 server restart 와 같이 서버가 내려갔다가 올라오는 상황에 데이타를 저장을 보장하기 위해서 Disk를 persistence store로 사용한다.

2. 다양한 데이타 타입
단순한 메모리 기반의 Key/Value Store라면 이미 memcached가 있지 않은가? 그렇다면 어떤 차이가 있길래 redis가 유행하는 것일까?
redis가 Key/Value Store이기는 하지만 저장되는 Value가 단순한 Object가 아니라 자료구조를 갖기 때문에 큰 차이를 보인다.
redis가 지원하는 데이타 형은 크게 아래와 같이 5가지가 있다.

1) String
일반적인 문자열로 최대 512mbyte 길이 까지 지원한다.
Text 문자열 뿐만 아니라 Integer와 같은 숫자나 JPEG같은 Binary File까지 저장할 수 있다.

2) Set
set은 string의 집합이다. 여러개의 값을 하나의 Value 내에 넣을 수 있다고 생각하면 되며 블로그 포스트의 태깅(Tag)등에 사용될 수 있다.
재미있는 점은 set간의 연산을 지원하는데, 집합인 만큼 교집합, 합집합, 차이(Differences)를 매우 빠른 시간내에 추출할 수 있다.

3) Sorted Set
set 에 "score" 라는 필드가 추가된 데이타 형으로 score는 일종의 "가중치" 정도로 생각하면 된다.
sorted set에서 데이타는 오름 차순으로 내부 정렬되며, 정렬이 되어 있는 만큼 score 값 범위에 따른 쿼리(range query), top rank에 따른 query 등이 가능하다.


4) Hashes
hash는 value내에 field/string value 쌍으로 이루어진 테이블을 저장하는 데이타 구조체이다.
RDBMS에서 PK 1개와 string 필드 하나로 이루어진 테이블이라고 이해하면 된다.


5) List
list는 string들의 집합으로 저장되는 데이타 형태는 set과 유사하지만, 일종의 양방향 Linked List라고 생각하면 된다. List 앞과 뒤에서 PUSH/POP 연산을 이용해서 데이타를 넣거나 뺄 수 있고, 지정된 INDEX 값을 이용하여 지정된 위치에 데이타를 넣거나 뺄 수 있다. 


6) 데이타 구조체 정리
지금까지 간략하게 redis가 지원하는 데이타 구조체들에 대해서 살펴보았다.
redis의 데이타 구조체의 특징을 다시 요약하자면
  • Value가 일반적인 string 뿐만 아니라, set,list,hash와 같은 집합형 데이타 구조를 지원한다.
  • 저장된 데이타에 대한 연산이나 추가 작업 가능하다. (합집합,교집합,RANGE QUERY 등)
  • set은 일종의 집합, sorted set은 오름차순으로 정렬된 집합, hash는 키 기반의 테이블, list는 일종의 링크드 리스트 와 같은 특성을 지니고 있다.
이러한 집합형 데이타 구조 (set,list,hash)등은 redis에서 하나의 키당 총 2^32개의 데이타를 이론적으로 저장할 수 있으나, 최적의 성능을 낼 수 있는 것은 일반적으로 1,000~5,000개 사이로 알려져 있다.

데이타 구조에 따른 저장 구조를 정리해서 하나의 그림에 도식화해보면 다음과 같다.



3. Persistence
앞서도 언급하였듯이, redis는 데이타를 disk에 저장할 수 있다. memcached의 경우 메모리에만 데이타를 저장하기 때문에 서버가 shutdown 된후에 데이타가 유실 되지만, redis는 서버가 shutdown된 후 restart되더라도, disk에 저장해놓은 데이타를 다시 읽어서 메모리에 Loading하기 때문에 데이타 유실되지 않는다.
redis에서는 데이타를 저장하는 방법이 snapshotting 방식과 AOF (Append on file) 두가지가 있다.

1) snapshotting (RDB) 방식
순간적으로 메모리에 있는 내용을 DISK에 전체를 옮겨 담는 방식이다.
SAVE와 BGSAVE 두가지 방식이 있는데,
SAVE는 blocking 방식으로 순간적으로 redis의 모든 동작을 정지시키고, 그때의 snapshot을 disk에 저장한다.
BGSAVE는 non-blocking 방식으로 별도의 process를 띄운후, 명령어 수행 당시의 메모리 snaopshot을 disk에 저장하며, 저장 순간에 redis는 동작을 멈추지 않고 정상적으로 동작한다.
  • 장점 : 메모리의 snapshot을 그대로 뜬 것이기 때문에, 서버 restart시 snapshot만 load하면 되므로 restart 시간이 빠르다.
  • 단점 : snapshot을 추출하는데 시간이 오래 걸리며, snapshot 추출된후 서버가 down되면 snapshot 추출 이후 데이타는 유실된다.
    (백업 시점의 데이타만 유지된다는 이야기)
2) AOF 방식
AOF(Append On File) 방식은 redis의 모든 write/update 연산 자체를 모두 log 파일에 기록하는 형태이다. 서버가 재 시작될때 기록된  write/update operation을 순차적으로 재 실행하여 데이타를 복구한다. operation 이 발생할때 마다 매번 기록하기 때문에, RDB 방식과는 달리 특정 시점이 아니라 항상 현재 시점까지의 로그를 기록할 수 있으며, 기본적으로 non-blocking call이다.
  • 장점 : Log file에 대해서 append만 하기 때문에, log write 속도가 빠르며, 어느 시점에 server가 down되더라도 데이타 유실이 발생하지 않는다.
  • 단점 : 모든 write/update operation에 대해서 log를 남기기 때문에 로그 데이타 양이 RDB 방식에 비해서 과대하게 크며, 복구시 저장된 write/update operation을 다시 replay 하기 때문에 restart속도가 느리다.
3) 권장 사항
RDB와 AOF 방식의 장단점을 상쇠하기 위해서 두가지 방식을 혼용해서 사용하는 것이 바람직한데
주기적으로 snapshot으로 백업하고, 다음 snapshot까지의 저장을 AOF 방식으로 수행한다.
이렇게 하면 서버가 restart될 때 백업된 snapshot을 reload하고, 소량의 AOF 로그만 replay하면 되기 때문에, restart 시간을 절약하고 데이타의 유실을 방지할 수 있다.


4. Pub/Sub Model
redis는 JMS나 IBM MQ 같은 메세징에 활용할 수 있는데, 1:1 형태의 Queue 뿐만 아니라 1:N 형태의 Publish/Subscribe 메세징도 지원한다.(Publish/Subscribe 구조에서 사용되는 Queue를 일반적으로 Topic이라고 한다.)
하나의 Client가 메세지를 Publish하면, 이 Topic에 연결되어 있는 다수의 클라이언트가 메세지를 받을 수 있는 구조이다. (※ Publish/Subscribe 형태의 messaging 에 대해서는 http://en.wikipedia.org/wiki/Pub/sub  를 참고하기 바란다.)


재미있는 것중에 하나는 일반적인 Pub/Sub 시스템의 경우 Subscribe 하는 하나의 Topic에서만 Subscribe하는데 반해서, redis에서는 pattern matching을 통해서 다수의 Topic에서 message 를 subscribe할 수 있다.
예를 들어 topic 이름이 music.pop music,classic 이라는 두개의 Topic이 있을때, "PSUBSCRIBE music.*"라고 하면 두개의 Topic에서 동시에 message를 subscribe할 수 있다.

5. Replication Topology
redis는 NoSQL 계열의 Key/Store Storage인데 반해서 횡적 확장성을 지원하지 않는다.
쉽게 말해서 2.4.15 현재 버전 기준으로는 클러스터링 기능이 없다. (향후 지원 예정)
그래서 확장성(scalability)과 성능에 제약사항이 있는데, 다행이도 Master/Slave 구조의 Replication(복제)를 지원하기 때문에 성능 부분에 있어서는 어느정도 커버가 가능하다.

Master/Slave replication
Master/Slave Replication이란, redis의 master node에 write된 내용을 복제를 통해서 slave node에 복제 하는 것을 정의한다.
1개의 master node는 n개의 slave node를 가질 수 있으며, 각 slave node도 그에 대한 slave node를 또 가질 수 있다.


이 master/slave 간의 복제는 Non-blocking 상태로 이루어진다. 즉 master node에서 write나 query 연산을 하고 있을 때도 background로 slave node에 데이타를 복사하고 있다는 이야기고, 이는 master/slave node간의 데이타 불일치성을 유발할 수 있다는 이야기이기도 하다.
master node에 write한 데이타가 slave node에 복제중이라면 slave node에서 데이타를 조회할 경우 이전의 데이타가 조회될 수 있다.

Query Off Loading을 통한 성능 향상
그러면 이 master/slave replication을 통해서 무엇을 할 수 있냐? 성능을 높일 수 있다. 동시접속자수나 처리 속도를 늘릴 수 있다. (데이타 저장 용량은 늘릴 수 없다.) 이를 위해서 Query Off Loading이라는 기법을 사용하는데
Query Off Loading은 master node는 write only, slave node는 read only 로 사용하는 방법이다.
단지 redis에서만 사용하는 기법이 아니라, Oracle,MySQL과 같은 RDBMS에서도 많이 사용하는 아키텍쳐 패턴이다.
대부분의 DB 트렌젝션은 웹시스템의 경우 write가 10~20%, read가 70~90% 선이기 때문에, read 트렌젝션을 분산 시킨다면, 처리 시간과 속도를 비약적으로 증가 시킬 수 있다. 특히 redis의 경우 value에 대한 여러가지 연산(합집합,교집합,Range Query)등을 수행하기 때문에, 단순 PUT/GET만 하는 NoSQL이나 memcached에 비해서 read에 사용되는 resource의 양이 상대적으로 높기 때문에 redis의 성능을 높이기 위해서 효과적인 방법이다.

Sharding 을 통한 용량 확장
redis가 클러스터링을 통한 확장성을 제공하지 않는다면, 데이타의 용량이 늘어나면 어떤 방법으로 redis를 확장해야 할까?
일반적으로 Sharding이라는 아키텍쳐를 이용한다. Sharding은 Query Off loading과 마친가지로, redis 뿐만 아니라 일반적인 RDBMS나 다른 NoSQL에서도 많이 사용하는 아키텍쳐로 내용 자체는 간단하다.
여러개의 redis 서버를 구성한 후에, 데이타를 일정 구역별로 나눠서 저장하는 것이다. 예를 들어 숫자를 key로 하는 데이타가 있을때 아래와 그림과 같이 redis 서버별로 저장하는 key 대역폭을 정해놓은 후에, 나눠서 저장한다.
데이타 분산에 대한 통제권은 client가 가지며 client에서 애플리케이션 로직으로 처리한다.

현재 버전 2.4.15에서는 Clustering을 지원하지 않아서 Sharding을 사용할 수 밖에 없지만 2012년 내에 Clustering기능이 포함된다고 하니, 확장성에 대해서 기대해볼만하다. redis가 지원할 clustering 아키텍쳐는 ( http://redis.io/presentation/Redis_Cluster.pdf ) 를 참고하기 바란다.

6. Expriation
redis는 데이타에 대해서 생명주기를 정해서 일정 시간이 지나면 자동으로 삭제되게 할 수 있다.
redis가 expire된 데이타를 삭제 하는 정책은 내부적으로 Active와 Passive 두 가지 방법을 사용한다.
Active 방식은 Client가 expired된 데이타에 접근하려고 했을 때, 그때 체크해서 지우는 방법이 있고
Passive 방식은 주기적으로 key들을 random으로 100개만 (전부가 아니라) 스캔해서 지우는 방식이 이다.
expired time이 지난 후 클라이언트에 의해서 접근 되지 않은 데이타는 Active 방식으로 인해서 지워지지 않고 Passive 방식으로 지워져야 하는데, 이 경우 Passive 방식의 경우 전체 데이타를 scan하는 것이 아니기 때문에, redis에는 항상 expired 되었으나 지워지지 않는 garbage 데이타가 존재할 수 있는 원인이 된다.

7. Redis 설치(윈도우즈)
https://github.com/rgl/redis/downloads 에서 최신 버전 다운로드 받은후
redis-server.exe를 실행

클라이언트는 redis-cli.exe를 실행
아래는 테스트 스크립트
    % cd src
    % ./redis-cli
    redis> ping
    PONG
    redis> set foo bar
    OK
    redis> get foo
    "bar"
    redis> incr mycounter
    (integer) 1
    redis> incr mycounter
    (integer) 2
    redis> 


참고 자료


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

MongoDB를 구성할때 보면, 가장 많이 권장 받는 부분 중의 하나가, 메모리량과 디스크 성능이다.

메모리 크기가 아주 sensitive한 요인이 되는데, 어떤 부분이 문제가 되는지 내부 저장 구조를 살펴 봄으로써 이해를 돕고자 한다.


저장 구조

mongodb는 기본적으로 memory mapped file (OS에서 제공되는 mmap을 사용) 을 사용한다. mongodb는 데이타를 write할때, 논리적으로 memory 공간에 write하고, 일정 주기에 따라서, 이 메모리 block들을 주기적으로 disk로 write하는데, 이 디스크 writing 작업은 OS에 의해서 이루어 진다.


OS에 의해서 제공되는 Virtual Memory를 사용하게 되는데, Pysical Memory 양이 작더라도 Virtual Memory는 훨씬 큰 공간을 가질 수 있다. Virtual Memory는 page라는 블럭 단위로 나뉘어 지고, 이 Block들은 Disk의 block에 mapping이되고, 이 block들의 집합이 하나의 데이타 파일이 된다. (아래 그림 참조)




참고 http://www.polyspot.com/en/blog/2012/understanding-mongodb-storage/


메모리에 저장되는 내용

메모리에 저장되는 내용은 실제 데이타 블록과, Index 자체가 저장이 된다. mongodb에서 index를 남용하지 말라는 이야기가 있는데, 이는 index를 생성 및 업데이트 하는 데 자원이 들어갈뿐더러, index가 메모리에 상주하고 있어야 제대로 된 성능을 낼 수 있기 때문이기도 하다.


만약에 Physical Memory에 해당 데이타 블록이 없다면, page fault가 발생하게 되고, disk에서 그 데이타 블록을 loading하게 된다. 물론 그 데이타 블록을 loading하기 위해서는 다른 데이타 블록을 disk에 써야 한다.

즉, page fault가 발생하면, page를 memory와 disk 사이에 switching하는 현상이 일어나기 때문에, disk io가 발생하고, 성능 저하를 유발하게 된다.


즉 메모리 용량을 최대한 크게 해서 이 page fault를 예방하라는 이야기이다.

그러나, page fault가 안 발생할 수 는 없고, (1TB 저장하려고, 메모리를 진짜 1TB를 저장할 수 없는 노릇이니...). page fault를 줄이는 전략으로 접근 하는 것이 옳은 방법인데..


page fault시 disk로 write되는 데이타는 LRU 로직에 의해서 결정이 된다. 그래서, 자주 안쓰는 데이타가 disk로 out되는데, 일반적인 application에서 자주 쓰는 데이타 (최근 데이타)의 비율은 그리 크지 않다. 예를 들어 게시판이나 블로그만을 생각해보더라도, 앞의 1~10 페이지 정도가 많이 보게 되지 뒤의 데이타를 잘 안보게 된다. 


이렇게 자주 억세스 되는 데이타를 Hot Data라고 하는데,이 Hot Data 들이 집중되서 메모리에 올라가도록 key 설계를 하는 것이 핵심이다.

쓸떼 없이 전체 데이타를 scan하는 등의 작업을 하게 되면, 100% page fault가 발생하기 때문에, table scan등이 필요한 시나리오는 별도의 index table(summary table)을 만들어서 사용하는 등의 전략이 필요하다.


note

Physical memory < Virtual Memory (=Physical memory + Swapping) < mmap = total file size < disk size



Do I need to configure swap space?

Always configure systems to have swap space. Without swap, your system may not be reliant in some situations with extreme memory constraints, memory leaks, or multiple programs using the same memory. Think of the swap space as something like a steam release valve that allows the system to release extra pressure without affecting the overall functioning of the system.

Nevertheless, systems running MongoDB do not need swap for routine operation. Database files are memory-mapped and should constitute most of your MongoDB memory use. Therefore, it is unlikely that mongod will ever use any swap space in normal operation. The operating system will release memory from the memory mapped files without needing swap and MongoDB can write data to the data files without needing the swap system.


http://docs.mongodb.org/manual/faq/diagnostics/#faq-memory



저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

초간단 Mongo DB Quick Start Guide

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

 

Mongo DB NoSQL 중에서 가장 널리 사용되는 인기있는 제품이다. 사용과 운영이 다른 시스템에 비해서 매우 쉽고, JSON 기반의 Document를 제공하기 때문에, 데이타 구조에 대한 이해 및 사용이 쉽다. 또한 index search와 같은 feature를 제공하고 있고, 근래에는 Text Search GridFS를 이용한 파일 저장 그리고, 위치 정보 저장 및 쿼리등 다양한 기능을 제공하고 있다.

 

Mongo DB 10gen이라는 회사에서 개발되어서, 현재 오픈소스로와 상용 버전으로 공급되고 있다.

이 글에서는 Mongo DB에 대한 이해를 돕기 위해서 간단한 설치에서 부터, 자바 기반의 프로그래밍 샘플까지 소개하도록 한다.

 

1.설치

1) 다운로드 하기

mongodbhttp://www.mongodb.org/downloads 에서 다운로드 받을 수 있다. 여기서는 Windows 64-bit Production Release 버전을 기준으로 한다.

zip 파일을 다운로드 받은 후에 C:\dev\mongodb-win32-x86_64-2.4.3 디렉토리에 압축을 풀었다.

2) Mongodb 구동

C:\dev\mongodb-win32-x86_64-2.4.3 에서 다음과 같은 명령어를 이용하여 구동한다.

.\bin\mongod --dbpath C:\dev\mongodb-win32-x86_64-2.4.3\data

3) 콘솔 확인

구동후에, http://localhost:28017 을 접속하면 아래와 같이 mongodb의 기본 관리 화면을 접속할 수 있다.



2.간단한 테스팅

인스톨이 끝났으면 간단한 테스트를 해보자, ./bin/mongo.exe를 수행하면 Java Script 기반의 쉘이 수행된다. 이해를 돕기 위해서 하나의 테이블에 Insert , select, delete, update를 수행하는 명령을 SQL 문장과 비교해서 소개한다.

 

Insert

SQL              : insert into users ("name","city") values("terry","seoul")

Mongo DB     : db.users.insert({_id:"terry",city:"seoul"})

 

Select

SQL              : select * from users where id="terry"

Mongo DB     : db.users.find({_id:"terry"})

 

Update

SQL              : update users set city="busan" where _id="terry"

Mongo DB     : db.users.update( {_id:"terry"}, {$set :{ city:"Busan" } } )

 

Delete

SQL              : delete from users where _id="terry"

Mongo DB     : db.users.remove({_id:"terry"})

 

간단하게 나마, mongodb query에 대해서 설명하였다.

조금 더 자세한 쿼리에 대한 설명은 http://docs.mongodb.org/manual/crud/ 를 참고하기 바란다.

 

3. 자바로 간단한 프로그램 작성하기

지금까지 설치와 간단한 쿼리 수행을 해봤다. 그러면 이번에는 다음 단계로 간단한 자바 클라이언트를 구현해보자, RDBMS를 이용하기 위해서 JDBC 드라이버를 사용한다면, mongodb는 전용 드라이버 mongo-java-driver 를 제공한다.

이 예제에서는 별도로 설치하기 보다는 아래와 같이 maven 빌드 스크립트에 dependency를 추가하여, 빌드시에 자동으로 라이브러리가 로딩 되도록 한다.

 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>

 

  <groupId>mongodb</groupId>

  <artifactId>simplemongo</artifactId>

  <version>0.0.1-SNAPSHOT</version>

  <packaging>jar</packaging>

 

  <name>simplemongo</name>

  <url>http://maven.apache.org</url>

 

  <properties>

    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

  </properties>

 

  <dependencies>

        <dependency>

               <groupId>org.mongodb</groupId>

               <artifactId>mongo-java-driver</artifactId>

               <version>2.10.1</version>

        </dependency>

  </dependencies>

    <build>

        <plugins>

               <plugin>

                       <groupId>org.apache.maven.plugins</groupId>

                       <artifactId>maven-compiler-plugin</artifactId>

                       <version>2.3.1</version>

                       <configuration>

                              <source>1.6</source>

                              <target>1.6</target>

                       </configuration>

               </plugin>

                <plugin>

                       <groupId>org.apache.maven.plugins</groupId>

                       <artifactId>maven-eclipse-plugin</artifactId>

                       <configuration>

                              <downloadSources>true</downloadSources>

                              <downloadJavadocs>true</downloadJavadocs>

                       </configuration>

               </plugin>

 

        </plugins>

  </build>

</project>

 

 

다음은 localhost에 있는 mongodb instance에 하나의 document(RDBMS로 치면 row) insert하는 클라이언트 프로그램 예제이다.

package com.terry;

import com.mongodb.BasicDBObject;

import com.mongodb.DB;

import com.mongodb.DBCollection;

import com.mongodb.MongoClient;

import com.mongodb.ServerAddress;

 

public class SimpleMongo {

       

        MongoClient mongoClient = null;

        DB db=null;

       

        public void mongoTest(String ip,int port,String dbname) throws Exception{

              

               mongoClient = new MongoClient(new ServerAddress(ip,port));

               db = mongoClient.getDB(dbname);

              

               DBCollection userTable = db.getCollection("userTable");

               BasicDBObject doc = new BasicDBObject("name", "MongoDB").

                append("type", "database").

                append("count", 1).

                append("info", new BasicDBObject("x", 203).append("y", 102));

 

               userTable.insert(doc);

        }

       

        public static void main(String args[]) throws Exception{

               SimpleMongo testRunner = new SimpleMongo();

               testRunner.mongoTest("localhost", 27017, "testdb");

        }

}

 

간단한 예제이기 때문에 몇가지만 설명을 하면

com.mongodb.DB mySQL DB등과 같이, RDBMSDB의 개념을 표현한다. 그래서 mongoDB에 연결한후에, db getDB를 통해서 연결한다.

mongoDB에서 테이블은 collection이라는 개념으로 존재한다. Table insert를 하기 위해서는 getCollection 메서드를 사용해서 해당 테이블 객체를 가지고 온후, Json Object를 만들어서 Table (Collection) insert를 하면 된다.

 

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

NoSQL 인기 순위

클라우드 컴퓨팅 & NoSQL/NoSQL 일반 | 2012.09.07 22:30 | Posted by 조대협

미국의 NoSQL 인기 순위를 분석해보니, mongodb가 앞도적인 1위, 2위권은 cassandra,hbase 그리고 다음이 redis 맨 아래로 riak,couchdb 등이 있다.

아무래도 기능이 편리한 mongodb 가 단연 인기고, 난이도는 있지만 확장성에 우위가 있는 cassandra,hbase가 그 뒤를 따른다. 


분석 방법은 indeed.com 이나 monster.com의 구인 광고중, 해당 기술별 구인 광고를 분석하였다.


<indeed.com 분석 결과>


  • mongodb 276
  • cassandra 149
  • hbase 146
  • redis 91
  • coherence 53
  • couchdb 40
  • riak 24

<monster.com 구직 분포>

저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

NoSQL 데이타 모델링 #2

Facebook Server Side Architecture Group
http://www.facebook.com/groups/serverside
조대협

NoSQL 데이타 모델링 패턴

NoSQL 데이타 모델링 패턴[1]Key/Value 저장 구조에 Put/Get 밖에 없는 단순한 DBMS에 대해서 다양한 형태의 Query를 지원하기 위한 테이블을 디자인하기 위한 가이드 이다.

특히 RDBMS에는 있는

“ Order by를 이용한 Sorting, group by를 이용한 Grouping, Join등을 이용한 개체간의 relationship 정의, 그리고 Index 기능

들을 데이타를 쿼리하는데, 상당히 유용한 기능인데, NoSQL은 이러한 기능들을 가지고 있지 않기 때문에, NoSQL 내에서 데이타 모델링을 통해서 이러한 기능들을 구현하는 방법에 대한 가이드를 제공한다.


1. 기본적인 데이타 모델링 패턴

NoSQL의 데이타 모델링을 할때, 가장 기본적으로 많이 사용되는 패턴들이다.


1)    Denormalization

Denormalization은 같은 데이타를 중복해서 저장하는 방식이다.
Denormalization
을 하면, 테이블간의 JOIN을 없앨 수 있다. NoSQL에서는 JOIN을 지원하지 않기 때문에, 두 테이블을 조인해서 데이타를 가지고 오는 로직을 구현하려면, 각 테이블에서 데이타를 각각 가져와서 애플리케이션에서 합쳐야 하기 때문에, 2번의 IO가 발생한다.

Denormalization을 적용하여, 하나의 테이블에 JOIN될 데이타를 중복 저장하게 되면 1번의 IO로도 데이타를 가지고 올 수 있다.

예를 들어서 사용자 정보를 저장하는 User 테이블과, 도시 이름을 저장하는 City 테이블이 있다고 하자,



이런 테이블 구조에서 사용자별로 이름,나이,성별,우편번호를 출력하는 쿼리는 다음과 같다.

select u.name,u.age,u.sex,c.zipcode from user u,city c where u.city = c.city

이 테이블 구조를 그대로 해서, NoSQL에서 구현하면

select $city,name,age,sex from user where userid=”사용자ID”

select zipcode from city where city=$city

식으로 구현해야 하는데, User 테이블에서 city값을 읽어온 후에, City 테이블에서 앞서 읽어온그래city값을 키로 해서 다시 zip code를 찾아와야 한다. 두번 쿼리를 발생 시킨다. 단순한 예이지만, Join을 해야 하는 테이블 수가 많을때는 NoSQL로의 IO수가 훨씬 더 늘어난다. IO 수를 줄이려면, 아예 처음부터 Join이 되어 있는 테이블을 하나 더 만들어 버리면 된다.



이런식으로 새로운 테이블을 추가하면 된다.

Denormalization을 이용하여 중복을 허용하였을 경우에 장단점은 다음과 같다.

장점은

Ÿ   성능 향상 – Join을 위해서 몇번씩 쿼리를 하지 않아도 되기 때문에, 당연히 쿼리 성능이 올라간다.

Ÿ   쿼리 로직의 복잡도가 낮아짐 – Join에 대한 로직이 필요 없이, 한번에 데이타를 가지고 오기 때문에, 쿼리 로직이 단순해 진다.

   반대로 다음과 같은 단점을 불러온다.

Ÿ   데이타 일관성 문제 발생 가능 – age,sex,zip code등을 insert하거나 update하였을때, User, City 테이블뿐만 아니라 UserZipCode 테이블도 업데이트 해줘야 한다. 만약에 같은 User 테이블을 업데이트 하다가 에러가 났을 경우, UserZipCode 테이블은 업데이트가 안된 상태이면, 양쪽 테이블에 데이타 불 일치성이 발생할 수 있다.

Ÿ   스토리지 용량 증가 : 데이타를 중복해서 저장하는 만큼 스토리지 용량이 증가 된다.

사실 보면 단점도 있지만, NoSQL의 경우 Join이 어렵고, 애플리케이션 적으로 구현을 한다고 해도 성능이 안나오거나 구현이 어려운 경우가 많기 때문에, Denormalization을 통한 중복은 상당히 효과적인 모델링 패턴이 된다.


2)    Aggregation

드롭박스나, 슈가싱크처럼 파일을 저장하는 개인 스토리지 서비스가 있다고 가정하자. 이 서비스는 파일에 대한 정보를 저장하고, 음악,동영상,사진 등의 파일의 종류에 따라서 추가적인 메타 정보를 저장한다. ERD를 기준으로 개체를 표현해보면 다음과 같다.



NoSQL의 재미있는 특성중의 하나가 Scheme-less 또는 Soft Scheme라는 특성인데, RDBMS의 경우 테이블에 데이타를 넣을때, 반드시 테이블의 구조에 맞도록 넣어야 한다. 컬럼수와 이름도 지켜야 하고, 각 컬럼에 해당 하는 데이타 타입도 준수해야 한다 또한 모든 ROW는 같은 컬럼을 가져야 한다반면 NoSQL의 경우에는 Key만 똑같다면, Row는 제멋대로라도 상관없다. 꼭 같은 컬럼을 가질 필요도 없고, 데이타 타입도 각기 틀려야 한다. Value에 저장되는 Row 데이타들은 한줄의 Row 구조를 가지기는 하지만, 전체 테이블에 대해서 그 Row 구조가 일치할 필요가 없다. 이를 데이타가 구조는 가지고 있지만, 구조가 각각 틀리는 것을 허용한다고 해서 Soft-Scheme 또는 Schemeless 라고 한다.

이 특성을 이용하면, 위의 데이타 모델을 하나의 테이블로 합칠 수 있다



1:N과 같은 복잡한 엔터티들의 관계를 손쉽게 하나의 테이블로 바꿀 수 있고, 이는 결과적으로 Join의 수를 줄여서 Query 성능을 높이는 방법이 된다.


3)    Application Side Join

NoSQL Join이 거의 불가능하다. (지원하는 제품도 있지만). 그래서 Denormalization이나 Aggregation 그리고 앞으로 설명할 데이타 모델링 패턴을 사용하는 것인데, 그래도 어쩔 수 없이 Join을 수행해야할 경우가 있다. 이 때는 NoSQL의 쿼리로는 불가능하고, NoSQL을 사용하는 클라이언트 Application단에서 로직으로 처리해줘야 한다.

예를 들어서 TABLE 1,2 두개의 테이블이 있고, TABLE 1의 컬럼중 하나가 Foreign Key로써 TABLE 2의 데이타를 가르키고 있다면, Application Join을 하려면, Table 1에서 Primary Key로 한 Row를 읽어온 후에, Table 2를 가르키는 컬럼의 값을 Key로 해서, 다시 Table 2에서 쿼리를 해온다.



Application Side Join Join이 필요한 테이블 수 만큼 NoSQL로의 Request/Response IO가 발생하는 만큼 다소 부담 스럽기는 하지만, 반대로 Denormalization등에 비해서는 스토리지 사용량을 절약할 수 있다.


참고 : Map & reduce 기능을 이용한 Server Side Join

Application Side Join이 있다면, 반대로 Server Side Join 기능도 있다. NoSQL에는 Join 기능이 없다고 했는데, Server Side Join은 무엇인가?


Riak이나 MongoDB와 같은 일부 NoSQL DB들은 RDBMS stored procedure [2]를 지원한다. Map & Reduce [3]라는 이름으로 이 기능을 지원한다. 즉 위에서 Application에서 수행한 로직을 NoSQL 서버들 내에서 수행해서 리턴하는 방식이다. 이 로직들은 NoSQL에서 지원하는 언어를 통해서 구현된후, NoSQL 엔진위에서 수행된다.




이렇게 하면, Application에서 NoSQL로의 호출이 Map & Reduce function 한번만 호출하면 되기 때문에, 네트워크 IO를 줄여서 성능을 향상 시킬 수 있다. 반면, Join에 대한 로직이 NoSQL 서버 쪽에서 수행되기 때문에, 반대로 NoSQL에 대한 부담이 가중된다.


2. 확장된 데이타 모델링 패턴

1)    Atomic aggregation

NoSQL에서 고민해야할 것 중의 하나의 두 개이상의 테이블을 업데이트 할때, 트렌젝션에 관리에 대한 문제이다. 그림과 같이 두개의 테이블을 업데이트 하는 시나리오에서, 1번 테이블을 업데이트 한 후에, 애플리케이션 로직이나 NoSQL의 장애로 인해서 2번 테이블이 업데이트가 되지 않는 문제가 발생할 수 있다.



이 경우 데이타의 일관성 문제를 야기하는데, 이에 대한 해결 방안으로 생각할 수 있는 것은, TABLE 1,2를 하나의 테이블로 합쳐 버리는 것이다.



하나의 테이블에 대해서는 NoSQL atomic operation(원자성)을 보장하기 때문에, 트렌젝션을 보장 받을 수 있다. 구현 패턴상으로는 Aggregation과 동일한데, Aggregation1:Nrelationship aggregate해서 join을 없애기 위함이고, Atomic Aggregation은 트렌젝션 보장을 통한 데이타 불일치성을 해결하기 위함이다.

예를 들어 사용자 정보가 UserAddress,UserProfile,UserId라는 이름의 3개의 테이블로 분산이 되어 있을때, 사용자를 생성하는 경우, 3개의 테이블에 Insert를 해줘야 한다. 그러나 테이블이 3개로 분산되어 있기 때문에, 앞에서 언급한 장애시 트렌젝션에 대한 보장이 되지 않는다. 이를 해결하기 위해서 atomic aggregation 패턴을 적용하여, 3개의 테이블을 User 테이블의 필드로 집어 넣게 되면, 하나의 테이블이기 때문에, 장애나 에러로 인한 트렌젝션 불일치 문제를 방지할 수 있다.



2)    Index Table

NoSQL RDBMS 처럼 Index가 없기 때문에, Key 이외의 필드를 이용하여 Search를 하면, 전체 Table Full Scan 하거나 아니면 Key이외의 필드에 대해서는 아예 Search가 불가능하다. 이 문제를 해결하기 위해서 Index를 위한 별도의 Index Table을 만들어서 사용할 수 있는데, 상당히 사용 빈도가 많은 방법이다.

예를 들어, 파일 시스템을 NoSQL에 저장한다고 했을때, 파일 테이블은 다음과 같은 구조를 갔는다.



여기서 특정 디렉토리에 있는 파일만을 리스트업 하고 싶다면, 별도의 Index Table을 다음과 같이 만들면 된다.



Cassandra Riak과 같은 일부 NoSQL에는 제품적으로 secondary Index라는 이름으로 Key 이외의 필드를 Index로 지정하는 기능을 가지고 있는데, 이 기능들은 아직까지 성숙되지 못해서, 여기서 설명하는 Index Table을 사용하는 것보다 성능이 나오지 않는다. 만약에 NoSQL에 있는 Secondary Index 기능을 사용할 예정이라면, 반드시 이 Index Table 패턴과 성능을 비교해보기를 추천한다. 당연히 Secondary Index를 이용하면 구현은 조금 편리해질 수 있지만, 성능 차이가 많이 날 수 있다.


3)    Composite Key

이 글을 읽으면서 눈치가 빠른 사람이라면, Key를 정의하는데, “:” deliminator를 이용하여 복합키를 사용하는 것을 눈치 챘을 것이다. NoSQL에서는 이 Key를 어떻게 정의하느냐가 매우 중요하다. 특히 Ordered KV Store 의 경우에는 이를 이용하여 order by와 같은 sorting 기능이나 grouping을 구현할 수 있다. Composite Key는 하나 이상의 필드를 deliminator를 이용하여 구분지어 사용하는 방법으로 RDBMS의 복합키 (Composite primary key)와 같은 개념이라고 생각하면 된다. 단지 RDBMS의 경우에는 여러개의 컬럼을 묶어서 PK로 지정하지만 NoSQL은 한컬럼에 deliminator를 이용하여 여러개의 키를 묶어서 넣는다.

아래의 예제를 보자, PC의 디렉토리를 Ordered KV Store에 저장한다고 하자.
 windows
하위 디렉토리를 가지고 올때, “windows:etc” 부터 쿼리를 해서, 다음 row를 반복적으로 쿼리해서, key windows로 시작하지 않을때 까지 읽어오면, windows 디렉토리의 하위 디렉토리를 모두 가지고 올 수 있다.



노드가 추가,삭제되더라도, 내부적으로 sorting이 되기 때문에, 문제없이 사용이 가능하다.

, Key값을 선택할때 주의해야할 사항은 특정 서버로의 몰림 현상을 들 수 있다. NoSQL은 특성상, N개의 서버로 구성된 클러스터이다. 그리고 데이타는 Key를 기준으로 N개의 서버에 나눠서 저장이 된다. 예를 들어 Key Range A~Z 26개라고 가정하고, 클러스터의 서버 수가 26대라고 하면, 각 서버는 Key의 시작으로 사용되는 알파벳 키들의 데이타를 저장한다. 1번은 A로 시작되는 Key 데이타들, 2번은 B, 3번은 C로 등등.

Key 값을 “City:User Name”으로 했다고 가정하자, 우리나라에서는 5000만 인구중, 1/5 1000만이 서울에 살고 있다. 그래서 데이타중 약 20%의 키는 “Seoul:xxx”로 시작할 것이고 26대의 서버중에서 S로 시작하는 키를 저장하는 한대의 서버는 20%의 부하를 처리하게 될것이다. 서버가 26대이니까는 각 서버는 1/26 ( 3.8%)의 부하를 처리해야 하는데, 이건 예상치 보다 5배 이상 많은 로드를 처리하기 때문에 성능 저하를 유발할 수 있다. 이런 이유로, Key를 선정할 때는 전체 서버에 걸쳐서 부하가 골고루 분산될 수 있는 Key를 선정하는 것이 좋다.


4)    Inverted Search Index

검색엔진에서 많이 사용하는 방법인데, 검색엔진은 사이트의 모든 페이지를 검색 로봇이 검색해서 문서내의 단어들을 색인하여 URL에 맵핑해서 저장해놓는다.



검색은 단어를 키로 검색이 되기 때문에, 위의 테이블 구조에서는 value에 검색 키워드들이 들어가 있기 때문에, 효과적인 검색을 할 수 없다. 이 검색 키워드를 키로 해서 URLvalue로 하는 테이블을 다시 만들어 보면, 아래와 같은 식으로 표현되고, 검색 키워드로 검색을 하면 빠르게, 검색 키워드를 가지고 있는 URL을 찾아낼 수 있다.



이렇게 value의 내용을 key로 하고, key의 내용을 반대로 value로 하는 패턴을 inverted search index라고 한다.


5)    Enumerable Keys

NoSQL 솔루션에 따라서, RDBMS Sequence와 같은 기능을 제공하는 것들이 있다. 이 기능들은, 키에 대해서 자동으로 카운터를 올려주는 기능을 가지고 있다. (예를 들어 첫번째 키는 1, 두번째 키는 2,다음은 3,4,5 식으로 순차적으로 연속된 키를 부여해주는 기능).

이 기능은 데이타에 대한 traverse 기능을 제공한다. , 100번 키를 가지고 왔는데, 이 앞뒤의 값을 알 수 있다. Sequential한 키를 사용했기 때문에, 당연히 앞의 키값은 99, 다음 키값은 101로 해서 값을 가지고 올 수 있다.


3. 계층 데이타 구조에 대한 모델링 패턴

NoSQL들은 데이타 모델이 KV,Ordered KV,Document들 여러가지가 있기는 하지만, 기본적으로 row,column을 가지고 있는 테이블 구조 저장구조를 갖는다. 애플리케이션 개발중에는 이런 테이블 구조뿐만 아니라 Tree와 같은 계층형 데이타 구조를 저장해야 할 경우가 있는데, 테이블 구조의 저장 구조를 갖는 NoSQL의 경우 이러한 계층형 구조를 저장하는 것이 쉬운일은 아니다.

RDBMS의 경우에도 이런 계층형구조를 저장하기 위해서 많은 고민을 했는데, RDBMS 솔루션에서 기능적으로 자체 지원할 수 도 있고, 데이타 모델링을 통해서도 이러한 계층형 구조를 저장할 수 있다.

여기서 소개하는 NoSQL에서 계층형 구조를 저장하는 기법은 RDBMS에서 사용하는 기법들을 많이 참고하였다. 추가적인 기법은 RDBMS Tree구조 저장 기법을 참고하기 바란다.


1)    Tree Aggregation

Tree 구조 자체를 하나의 Value에 저장하는 방식이다. JSON이나 XML 등을 이용하여, 트리 구조를 정의 하고, Value에 저장하는 방식이다.

Tree 자체가 크지 않고, 변경이 많이 없는 경우에는 사용하기 좋다. “계층형 게시판의 답글 Tree 구조등을 저장하기에 용이하다.


2)    Adjacent Lists

Adjacent List 구조는, 전통적인 자료 구조에서 사용하는 Linked List와 같은 자료 구조형을 사용하여, Tree의 노드에 parent node에 대한 포인터와 child node들에 대한 포인터를 저장하는 방식이다.

Tree의 내용을 검색하려면, root 노드에서 부터 child node child node 포인트를 이용하여, 값을 가져오는 방식이다. (이를 Tree traversing 이라고 한다.)

특정 노드만 알면, 해당 노드의 상위, 하위 노드를 자유롭게 traversing할 수 있어서 traversing에는 장점을 가지고 있지만, 반대로, 하나의 노드를 이동할 때마다, 포인터를 이용해서 매번 쿼리를 해와야 하기 때문에, Tree의 크기가 크다면 NoSQL로의 IO가 엄청나게 많이 발생한다. (트리의 노드 수가 N이면, N번 쿼리를 해야 한다)

아래 그림을 보자, 파일 디렉토리를 저장하는 자료 구조를 만든다고 했을때, Directory라는 테이블을 정의하고, 이 테이블에는 directory 명과, 해당 directory의 상하위 디렉토리 이름을 저장하도록 한다.

이를 바탕으로 테이블에 저장된 데이타를 보면 아래와 같다.



구현이 쉬운 편이긴 하지만 Tree 구조 traverse에 많은 IO를 유발하기 때문에, Tree구조를 저장하거나 잦은 read가 있을때는 권장하지 않는다.

     RDBMS 의 경우에는 이런 Tree 구조 traversing을 지원하기 위해서 recursive 쿼리에 대해서 native recursive function을 지원함으로써, tree 구조 저장을 지원하는 경우도 있다.


3)    Materialized Path

Materialized Path Tree 구조를 테이블에 저장할때, root에서 부터 현재 노드까지의 전체 경로를 key로 저장하는 방법이다.

이 방법은 구현에 드는 노력에 비해서 매우 효율적인 저장 방식이다. 특히 Key에 대한 Search를 할때, Regular Expression을 사용할 수 있으면, 특정 노드의 하위 트리등을 쿼리해 오는 기능등 다양한 쿼리가 가능하다. 일반적은 KV Ordered KV에서는 적용하기는 힘들지만, MongoDB와 같은 Document DB Regular Expression을 지원하기 때문에, 효과적으로 사용할 수 있다.



4)    Nested Sets

Netsted Set의 기본원리는 Node가 포함하는 모든 Child Node에 대한 범위 정보를 가지고 있다. 먼저 예를 보고 설명하자. Node는 배열이나 리스트에 Sorting된 형태로 저장되어 있다.

Node는 자신이 포함하는 모든 Sub Tree (Child Node)들이 포함된 start end index를 저장한다.

아래 A는 전체 Sub Tree를 포함하기 때문에 2~9번에는 A Sub Tree의 내용이 된다.
C
노드의 경우 자신이 포함하는 모든 Child Node 4~9에 포함되어 있기 때문에, Index 4~9로 저장한다.



각 노드만 안다면, Sub Tree start,end index만 있으면 쭈욱 읽어오면 되기 때문에 매우 빠른 성능을 보장할 수 있다. 단 이 데이타 구조 역시 update에 취약하다. update가 발생하였을 경우, Index를 다시 재배열해야 하기 때문에 이에 대한 로드가 매우 크다.

위의 예제에서 B 노드 아래에 J 노드를 추가해보자, J 노드의 index 3이 되어야 하고, 3 Index부터는 모두 +1 씩 더해져야 하며, Child Node의 값 역시 모두 변화 되어야 한다.



트리 생성 후에, 변화가 없는 대규모 트리등을 저장하는데 유용하게 사용할 수 있다

.

결론

지금까지 몇가지 NoSQL 적용시 사용할 수 있는 데이타 모델링 패턴에 대해서 살펴보았다. 기본 패턴은 대부분의 NoSQL 솔루션에 적용할 수 있으며, 확장된 모델링 패턴의 경우에는 NoSQL이 지원하는 기능이나 데이타 구조 (KV, Ordered KV,Document etc)에 따라서 적용할 수 있다.

NoSQL을 이용한 시스템을 개발할때는

1.     데이타 모델링이 80% 이상이다. 선정한 NoSQL과 애플리케이션의 특성에 맞는 데이타 모델링에 집중하자.

2.     NoSQL은 어떤 솔루션이 좋다, 나쁘다가 없다. 어떤 솔루션이 특성이 어떻다는 것만 있기 때문에 반드시 데이타 모델과 내부 아키텍쳐 두 가지를 파악한 후에, 애플리케이션의 특성에 맞는 NoSQL을 선정해야 한다.

3.     하나의 NoSQL로 전체 데이타를 저장하려고 하지 마라. NoSQL은 데이타 구조가 매우 다눈하지만, 애플리케이션들은 하나의 단순한 데이타 구조로 저장할 수 없는 데이타가 반드시 존재한다. RDBMS와 혼용하거나, 다른 NoSQL과 혼용하거나 성능면에서는 캐슁 솔루션과 혼용하는 것을 반드시 고려 해야한다.

NoSQL은 놀라온 성능과 확장성을 제공하지만, 많은 연구와 노력이 필요하다. Oracle과 같은 데이타베이스를 사용할때도, 전문 DBA를 두고, 튜닝에 시간을 두고, 데이타 모델을 가꾸고, 튜닝을 하지 않는가? NoSQL이 오픈소스를 중심으로 사용되고 있지만, 오픈 소스라서 쉬운게 아니다. 그만큼 많은 투자와 연구와 노력이 필요한 만큼, 신중하게 검토하고 도입을 결정하기를 바란다.



[2] RDBMS에서 프로그래밍 언어를 이용해서 구현된 사용자 정의 데이타 Query 함수. 예를Oracle 들어 RDBMS의 경우에는 PL/SQL이라는 프로그래밍 언어를 이용하여, 데이타 베이스에 대한 비지니스 로직을 구현해서 저장해놓고, 클라이언트에서 이 함수를 호출해서 사용하게 할 수 있다.

[3] Map & Reduce Input 데이타를 여러 조각으로 쪼갠 후, 여러대의 서버에서 각 데이타 조각을 처리한후(Map), 그 결과를 모아서 (Reduce) 하나의 결과를 내는 분산 처리 아키텍쳐 이다.




링크 

NoSQL 데이타 모델 및 데이타 모델링 절차 : http://bcho.tistory.com/665

참고 자료 http://www.pearltrees.com/#/N-u=1_752336&N-p=53126547&N-play=1&N-fa=5793524&N-s=1_5827086&N-f=1_5827086


저작자 표시
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

NoSQL 데이타 모델링 #1

Facebook Server Side Architecture Group
http://www.facebook.com/groups/serverside
조대협

빅데이타,클라우드,NoSQL은 요즘 기술적인 화두중에 하나이다. 그중에서도 NoSQL은 많은 사람이 관심을 갖고 있음에도 불구하고, 기존의 RDBMS 데이타 모델링 관점에서 접근을 하기 때문에, 많은 문제를 유발한다. NoSQL은 데이타 베이스이기도 하지만 RDBMS와는 전혀 다른 성격을 가지고 있고, 접근 방식도 틀리다.

특히 테이블 구조를 정의 하는 데이타 모델에 따라서 NoSQL의 성능은 하늘과 땅차이만큼 차이가 난다. 이 글에서는 NoSQL의 데이타 모델링 기법에 대해서 소개하고자 한다.
※ 깨지는 그림은 클릭해서 봐주세요.

NoSQL 데이타 모델

데이타 모델링에 앞서서 NoSQL이 어떤 구조로 데이타를 저장하는지를 먼저 이해할 필요가 있다. NoSQL의 데이타 모델링 패턴은 아래와 같이 크게 3가지 패턴정도로 구분된다.

1.     Key/Value Store

가장 기본적인 패턴으로, 대부분의 NoSQL은 다른 데이타 모델을 지원하더라도, 기본적으로 Key/Value의 개념을 지원한다. Key/Value Store, Unique Key에 하나의 Value를 가지고 있는 형태를 이야기 한다. Put(Key,Value), Value := get(Key) 형태의 API로 접근한다.


Value String이나 Integer와 같은 Primitive 타입이 될 수 도 있지만, 이정도로는 우리가 일반적으로 사용하는 테이블 형태의 데이타를 저장할 수 없기 때문에, 조금 더 확장된 개념을 사용하는데, Column Family라는 개념을 사용한다. Key 안에 (Column,Value) 조합으로 된 여러개의 필드를 갖는데, 이를 Column Family라고 한다.


예를 들어, 사용자 프로필을 저장하는 시나리오가 있을 때, 사용자의 이름을 KEY로 한다면, 성별,주소,나이들은 각각의 Column이 될 수 있다. Key 필드는 RDBMS에서 Primary Key, Column 필드들은 RDBMS의 일반 데이타 필드로 이해하면 된다. 프로그램 언어와 비교해서 생각한다면, Key/Value Store Map 데이타 구조와 유사하다.
Oracle Coherence
Redis와 같은 NoSQL이 이 데이타 모델을 기본 모델로 사용한다.

2.     Ordered Key/Value Store

Key/Value Store의 확장된 형태로 Key/Value Store와 데이타 저장 방식은 동일하나, 데이타가 내부적으로 Key를 순서로 Sorting되서 저장된다.



Sorting이 별거 아닌것 같지만, NoSQL 관점에서는 대단히 중요한 기능을 제공하게 된다. 뒤에 데이타 모델링 기법에서도 다루겠지만, NoSQL RDBMS Order By와 같은 기능을 제공하지 않기 때문에 결과값을 업데이트 날짜등으로 소팅해서 보여주는 것은 이 Ordered Key/Value Store가 절대적으로 유리하다.

대표적인 제품으로는 Apache Hbase, Cassandra 등이 있다.

3.     Document Key/Value Store

Key/Value Store의 확장된 형태로, 기본적으로는  Key/Value Store이다. Key에 해당하는 Value 필드에 데이타를 저장하는 구조는 같으나, 저장되는 Value의 데이타 타입이 Document 라는 타입을 사용하는데, Document 타입은 MS-WORD와 같은 문서를 이야기 하는것이 아니라, XML,JSON,YAML과 같이 구조화된 데이타 타입으로, 복잡한 계층 구조를 표현할 수 있다.



아울러, Document Store 기반의 NoSQL은 제품에 따라 다르기는 하지만 대부분 추가적인 기능 (Sorting,Join,Grouping)등의 기능을 제공한다.

대표적인 제품으로는 MongoDB,CouchDB,Riak 등이 있다.


그리고 여기서는 구체적으로 다루지 않지만 Graph Tree구조와 같은 데이타 구조를 저장하기 위해서 최적화된 Neo4J등의 NoSQL이 있다. 만약에 테이블 구조의 데이타가 아니라 Graph 구조의 데이타를 저장하고자 한다면 Neo4J를 한번 체크해보기 바란다.


RDBMS NoSQL의 차이

NoSQL DBMS라고 생각해서 RDBMS와 같은, 또는 떨어지지만 유사한 기능을 제공할것이라고 생각하면 큰 오산이다. NoSQL은 데이타를 저장한다. 그리고 Key에 대한 Put/Get만 지원한다. RDBMS로 치자면

Put : Insert into TABLE VALUES(KEY,value1,value2,…,valuen)

Get : Select * from TABLE where KEY=”key”

만 딱 지원한다. 물론 제품에 따라서 기능에 대한 지원 범위는 다르기는 하지만, 공통적으로 고민해야 하는 기능은

Ÿ   Sorting (SQL Order By)

Ÿ   Join (RDBMS에서 두개의 Table Foreign Key를 이용하여 join)

Ÿ   Grouping (SQL문의 group by)

Ÿ   Range Query (where key>”start” and key<”end” 와 같이 일정 범위내의 내용을 쿼리해오는 기능)

Ÿ   Index (RDBMS Index를 지정하여 select query 성능을 높이는 기능)

이다. RDBMS에서는 너무나도 익숙하게 사용했던 기능들이기 때문에, 막상 이 기능들을 빼고 데이타를 다루고자 하면 매우불편하다. 여기서는 이러한 기능들을 “NoSQL 데이타 모델링 패턴소개를 통해서 NoSQL에서 어떻게 구현할 수 있는지 알아볼 것 이다.

NoSQL 데이타 모델링 시작하기

NoSQL 데이타 모델링이란, NoSQL에 저장할 데이타들의 구조, 즉 테이블 설계를 하는 것을 정의한다. NoSQL DBMS이기는 하지만, 우리가 지금까지 익숙하게 사용해왔던, RDBMS와는 그 특성이 매우 다르기 때문에 접근 방법을 바꿔야 한다.

NoSQL RDBMS의 데이타 모델링 차이

NoSQL을 사용해서 데이타 모델링을 하려면 근본적인 사상 2가지를 바꿔야 한다.

1)     개체 모델 지향에서 쿼리 결과 지향 모델링

RDBMS의 모델링 기법은, 저장하고자하는 도메인 모델을 먼저 분석한 후에, 개체간의 관계(relationship)를 식별하고, 테이블을 추출해내고, 테이블을 이용하여 쿼리를 구현하여 결과를 뽑아내는 방식이다.

NoSQL의 경우에는 이 접근 방법을 역순으로 진행해야 한다.

RDBMS가 도메인 모델 à [테이블 à 쿼리] 순서로 진행을 했다면, NoSQL은 도메인 모델 à [쿼리 결과 à 테이블] 순서로 테이블을 디자인해야 한다. RDBMS의 경우 여러가지 최적화된 기능으로 테이블을 가지고 자유롭게 쿼리 결과를 뽑아낼 수 있지만, NoSQL의 경우 복잡한 쿼리 기능이 없기 때문에, 반대로 도메인 모델에서 어떤 쿼리 결과가 필요한지를 정의한후에, 이 쿼리 결과를 얻기 위한 데이타 저장 모델을 역순으로 디자인해야 한다.

2)     정규화(Normalization)에서 비정규화(Denormalization)

RDBMS 모델링에서는 데이타의 일관성과 도메인 모델과의 일치성을 위해서 데이타 모델을 정규화한다. 그중에서도 같은 데이타가 두개 이상의 테이블에 중복되게 저장하는 것을 제거 하는데, NoSQL은 반대의 접근 방법이 필요하다. 쿼리의 효율성을 위해서 데이타를 정규화하지 않고, 의도적으로 중복된 데이타를 저장하는 등의 비정규화된 데이타 모델 설계 방식으로 접근해야 한다.

NoSQL 데이타 모델링 절차

그러면, RDBMS NoSQL의 두가지 결정적인 차이를 인식하고, NoSQL 기반의 데이타 모델링 절차를 살펴보자.

1.     도메인 모델 파악

먼저 저장하고자하는 도메인을 파악한다. 어떤 데이타 개체가 있는지 개체간의 관계는 어떻게 되는지등을 분석하고 ERD를 그려서 도식화 한다. RDBMS의 모델링 접근 방법이고, NoSQL에서는 이렇게 하지 않고 바로 애플리케이션 관점으로 접근하는 경우도 많은데, 도메인 모델 분석 없이 필자의 경우에는 이런 방식에는 반대이다. NoSQL도 데이타베이스이고 저장할 데이타에 대한 명확한 이해 없이는 제대로된 데이타 모델이 나올 수 없다.

다음은 간단한 블로그 시스템의 데이타 도메인 모델이다. 이 블로그 시스템은

Ÿ   사용자 ID 기반으로 블로그의 분류(Category)를 가지고 있고,

Ÿ   분류별로 글을 작성할 수 있으며,

Ÿ   글에 파일을 첨부할 수 있고,

Ÿ   댓글을 달 수 있는 블로그이다.

Ÿ   이번 예제에서는 검색이나 페이징 기능은 제외한다. (단순화