안녕하세요? 이번 글은 Bluemix를 이용하여 간단한 클라우드 애플리케이션을 디자인하고 만들어 가는 과정을 내용으로 연재되고 있습니다. 아래와 같은 순서로 진행되고 있으므로 참고 부탁 드립니다.


  1. 클라우드 환경 이해
  2. 애플리케이션 구상 및 요건 정의
  3. 요건에 대한 Usecase 및 Wireframe 작성
  4. 마이크로 서비스 아키텍쳐 설계
  5. 애플리케이션 서버 환경 준비
  6. 애플리케이션 로컬 개발 환경 준비
  7. 애플리케이션 프로토타입 작성 Part1
  8. 애플리케이션 프로토타입 작성 Part2
  9. 애플리케이션 프로토타입 작성 Part3
  10. 애플리케이션 DevOps 환경 구성

애플리케이션 프로토타입 작성 Part2

지난 글에서 사용자 인증 및 서비스간 공유를 위해 Memory Cache를 활용한 구조를 다루었습니다. 이번 글에서는 이어서 남은 API 서비스에 대해 구성하도록 하겠습니다.

방문 정보 서비스 프로토타입 구현

방문 정보 서비스는 사용자 인증 서비스와 달리 SDK for Node.js Runtime
에서 생성한 코드를 활용하여 작성합니다.

서비스 Handler 환경 구성

사용자 인증 서비스에서에서 처럼 REST 서비스를 위한 Handler 코드를 구성합니다. 사용자 인증 서비스와 유사한 구조로 하기 위해 controllers라는 폴더를 생성하고 다음과 같이 해당 서비스를 노출하는 SMVVisitController.js 파일을 작성합니다.

'use strict';

const BASE_PATH = '/api/smv/v1/visit';

function newVisit(req, res) {

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function getVisit(req, res) {
  var id = req.params.id;

  console.log(`visit id ${id}`);
  
  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function updateVisit(req, res) {
  var id = req.params.id;
  
  console.log(`visit id ${id}`);

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function deleteVisit(req, res) {
  var id = req.params.id;
  
  console.log(`visit id ${id}`);

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

function searchVisits(req, res) {
  // TODO
  res.statusCode = 200;
  res.end('OK');
}

module.exports = function(app, options) {
  app.post(BASE_PATH, newVisit);
  app.get(BASE_PATH + '/:id', getVisit);
  app.put(BASE_PATH + '/:id', updateVisit);
  app.delete(BASE_PATH + '/:id', deleteVisit);

  app.get(BASE_PATH + '/search', searchVisits);
};

그리고 app.js에서 SMVVisitorController 모듈을 로딩하고 초기화하는 코드를 아래와 같이 추가합니다.

...
// Expose the SMVVisitController
var SMVVisitController = require('./controllers/SMVVisitController');
SMVVisitController(app);
...

사용자 인증 공통 모듈 적용

앞서 사용자 인증 서비스에서 구성한 공통 모듈인 SMVAuthTokenHelper.js를 복사합니다. NPM package로 등록되어 있다면 이를 이용할 수도 있겠으나 각각의 구분된 프로젝트에서 사용하는 것이므로 이 부분은 직접 복제를 하도록 합니다. 그리고, SMVVisitController.js에 사용자 인증 필터를 아래와 같이 추가합니다.

const AUTH_TOKEN_KEY = 'X-AUTH-TOKEN';

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

function extractAuthToken(req) {
  var token = req.headers[AUTH_TOKEN_KEY] || req.headers[AUTH_TOKEN_KEY.toLowerCase()];
  if (!token) {
    console.error(`${AUTH_TOKEN_KEY} is not in the header as key`);
  }
  return token;
}

function authenticatedFilter(req, res, next) {
  var token = extractAuthToken(req);
  SMVAuthTokenHelper.isValidAuthToken(token, function(valid) {
    if (valid) {
      // go next
      return next();
    }

    res.statusCode = 401;
    res.end('Unauthorized');
  });
}

...

module.exports = function(app, options) {
  app.post(BASE_PATH, authenticatedFilter, newVisit);
  app.get(BASE_PATH + '/:id', authenticatedFilter, getVisit);
  app.put(BASE_PATH + '/:id', authenticatedFilter, updateVisit);
  app.delete(BASE_PATH + '/:id', authenticatedFilter, deleteVisit);

  app.get(BASE_PATH + '/search', authenticatedFilter, searchVisits);
};

테스트 환경 구성

사용자 인증 서비스에서와 같이 MochaJS를 이용한 JavaScript 단위 테스트를 구성합니다.

$ npm install mocha --save-dev

기존 사용자 인증 서비스에서 사용하던 test-authtokenhelper.jstest 폴더에 복사하고 정상 동작하는지 확인 해 볼 수 있습니다.

사용지 인증 서비스와 달리 실제 노출되는 REST API 서비스를 기반으로한 테스트 케이스를 만들어 보겠습니다.

먼저 request 모듈을 다음 명령으로 설치합니다.

$ npm install request --save-dev

그리고 test/test-smv-visit.js 파일을 다음과 같은 내용으로 작성합니다.

#!/usr/local/bin/node

/* eslint-env mocha */

var appEnv = require('cfenv').getAppEnv();

const BASE_PATH = appEnv.url+'/api/smv/v1/visit';
const TEST_TIMEOUT = 60*1000; // 60 seconds

var assert = require('assert'),
  SMVAuthTokenHelper = require('../controllers/SMVAuthTokenHelper'),
  request = require('request');

describe('[SMVVisit API Test]', function() {
  var authtoken,
    testid = 1234;

  before(function () {
    //console.log('>>>>>>>>>>>>>>> before');
  });

  after(function () {
    //console.log('after <<<<<<<<<<<<<<<<');
  });

  this.timeout(TEST_TIMEOUT);

  describe('POST /', function() {
    it('returns status code 200', function(done) {
      request.post(BASE_PATH, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('GET /{id}', function() {
    it('returns status code 200', function(done) {
      request.get(`${BASE_PATH}/${testid}`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('PUT /{id}', function() {
    it('returns status code 200', function(done) {
      request.put(`${BASE_PATH}/${testid}`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('DELETE /{id}', function() {
    it('returns status code 200', function(done) {
      request.delete(`${BASE_PATH}/${testid}`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

  describe('GET /search', function() {
    it('returns status code 200', function(done) {
      request.get(`${BASE_PATH}/search`, function(error, response, body) {

        assert.equal(200, response.statusCode);
        done();
      });
    });
  });
});

이 상태에서 mocha test를 실행하면 다음과 같은 결과가 나타납니다.

$ ./test.sh 

> SMVVisitService@0.0.3 test /Project/DevWorks/smv/projects/smv-visit
> mocha



  [SMVAuthTokenHelper Unit Test]
    ✓ AuthToken generation (712ms)
    ✓ AuthToken validation (176ms)
    ✓ AuthToken validation negative test (175ms)
    ✓ AuthToken set value (175ms)
    ✓ AuthToken get value (175ms)
    ✓ AuthToken invalidate (349ms)

  [SMVVisit API Test]
    POST /
      1) returns status code 200
    GET /{id}
      2) returns status code 200
    PUT /{id}
      3) returns status code 200
    DELETE /{id}
      4) returns status code 200
    GET /search
      5) returns status code 200


  6 passing (3s)
  5 failing

  1) [SMVVisit API Test] POST / returns status code 200:

      Uncaught AssertionError: 200 == 401
      + expected - actual

      -200
      +401
      
      at Request._callback (test/test-smv-visit.js:32:16)
      at Request.self.callback (node_modules/request/request.js:188:22)
      at Request.<anonymous> (node_modules/request/request.js:1171:10)
      at IncomingMessage.<anonymous> (node_modules/request/request.js:1091:12)
      at endReadableNT (_stream_readable.js:974:12)
      at _combinedTickCallback (internal/process/next_tick.js:80:11)
      at process._tickCallback (internal/process/next_tick.js:104:9)
...

앞서 작성한 authenticatedFilter 함수에서 사용자 인증 토큰이 없어 401 Unauthorized가 발생하는 상황을 알 수 있습니다. 사용자 인증 토큰 발급을 위해 테스트 케이스에서 로그인 API를 처리하고 이를 header를 통해 전달하는 코드를 작성합니다.

const AUTH_TOKEN_KEY = 'X-AUTH-TOKEN';
const BASE_PATH = appEnv.url+'/api/smv/v1/visit';
const TEST_TIMEOUT = 60*1000; // 60 seconds
const SMV_USERAUTH_BASE_URL = process.env['SMV_USERAUTH_BASE_URL'];

...

function buildAuthHeaders(authtoken) {
  var authheaders = {};
  authheaders[AUTH_TOKEN_KEY] = authtoken;
  return authheaders;
}

...

describe('[SMVVisit API Test]', function() {
  var authtoken,
    testid;

  this.timeout(TEST_TIMEOUT);

  before(function (done) {
    // Login
    request.post({
      url: SMV_USERAUTH_BASE_URL+'/api/smv/v1/auth/login',
      form: {
        email: 'john.doe@acme.ibm.com',
        passwd: 'passw0rd'
      }
    }, function(error, response, body) {
      authtoken = extractAuthToken(response);
      assert(authtoken);
      done();
    });
  });

...

  describe('GET /{id}', function() {
    it('returns status code 401', function(done) {
      request.get(`${BASE_PATH}/${testid}`, function(error, response, body) {
        assert.equal(401, response.statusCode);
        done();
      });
    });

    it('returns status code 200', function(done) {
      request.get({
        url: `${BASE_PATH}/${testid}`,
        headers: buildAuthHeaders(authtoken)
      }, function(error, response, body) {
        assert.equal(200, response.statusCode);
        done();
      });
    });
  });

...

API에 대한 기본적인 테스트 케이스를 완성하면 이를 기반으로 API 상세를 구현 할 수 있습니다.

SMVVisitController.js에서 방문 정보를 생성하는 newVisit에 대한 함수를 구현합니다. 기본형으로 아래와 같은 모습인데 여기에 기능을 하나 추가해 보도록 합니다.

function newVisit(req, res) {

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

방문자 정보 서비스는 영구 데이터 저장을 위해 Cloudant 서비스를 연결하는 것으로 설계 했었습니다. UI 서비스에서 연결했던 Cloudant 서비스를 그대로 사용할 수 있으므로 Bluemix 대시보드에서 해당 서비스를 연결합니다.

기존 항목 연결을 선택하여 연결 할 서비스를 선택합니다.

서비스 연결이 완료되면 local 실행을 위해 서비스 신임 정보를 vcal-loca.json 파일로 저장합니다.

방문자 정보 저장을 위한 서비스가 Cloudant 코드이므로 이를 위한 모듈을 설치합니다.

$ npm install cloudant --save

그리고, cloudant를 이용한 정보 저장 관리를 위해 SMVCloudantHelper.js를 작성합니다. 이 모듈은 SMV UI 앱의 app.js에 포함된 cloudant 정보 초기화 코드를 활용합니다.

'use strict';

const CLOUDANT_SERVICE_NAME = 'smv-ui-app-cloudantNoSQLDB';
const CLOUDANT_SMV_DATABASE = 'smv';

const appEnv = require('cfenv').getAppEnv();
var dbCredentials = appEnv.getServiceCreds(CLOUDANT_SERVICE_NAME);

// Initialize 
const cloudant = require('cloudant')(dbCredentials.url);

function createDatabase(dbName) {
  // check if DB exists if not create
  cloudant.db.create(dbName, function(err, res) {
    if (err) {
      console.log('Could not create new db: ' + dbName + ', it might already exist.');
    }
  });
}

// ----------------------------------------------------------------------------

module.exports = {
  'cloudant': cloudant,
  'initDB': function (dbName) {
    var name = dbName || CLOUDANT_SMV_DATABASE;

    // check if DB exists if not create
    createDatabase(name);

    return cloudant.use(name);
  }
};

app.js를 참고하여 초기화 코드를 작성 했지만 향후 필요에 따라서 다양한 옵션을 선택 할 수 있습니다. cloudant 모듈에 대한 자세한 정보는 https://www.npmjs.com/package/cloudant를 참고하시기 바랍니다.

이렇게 만든 SMVCloudantHelper.js에 대한 테스트 코드를 다음과 같이 생성하고 간단한 테스트 코드를 작성합니다. 파일 이름은 test-cloudanthelper.js 입니다.

#!/usr/local/bin/node

/* eslint-env mocha */

const TEST_TIMEOUT = 60*1000; // 60 seconds

var assert = require('assert'),
  SMVCloudantHelper = require('../controllers/SMVCloudantHelper');

describe('[SMVCloudantHelper Unit Test]', function() {
  before(function () {
    //console.log('>>>>>>>>>>>>>>> before');
  });

  after(function () {
    //console.log('after <<<<<<<<<<<<<<<<');
  });

  this.timeout(TEST_TIMEOUT);

  describe('smv database test', function() {
    var mydb = SMVCloudantHelper.initDB('smv'),
      docid = 'test',
      doctype = 'test',
      jsondoc,
      lastrev;

    const json = {
      _id: docid,
      type: doctype,
      text: 'hello'
    };

    it('CREATE', function (done) {
      assert(mydb);
      mydb.insert(json, function(err, doc) {
        if (err) {
          done(err);
        } else {
          assert(doc.id == docid);
          done();
        }
      });
    });

    it('READ', function (done) {
      assert(mydb);
      mydb.get(docid, function(err, doc) {
        if (err) {
          done(err);
        } else {
          jsondoc = Object.assign({}, doc);
          lastrev = doc._rev;
          assert(doc._id == docid);
          assert(doc.type == doctype);
          done();
        }
      });
    });

    it('UPDATE', function (done) {
      assert(mydb);
      assert(jsondoc);
      jsondoc.text = 'world';
      mydb.insert(jsondoc, function(err, doc) {
        if (err) {
          done(err);
        } else {
          lastrev = doc.rev;
          assert(lastrev);
          assert(doc.id == docid);
          done();
        }
      });
    });

    it('DELETE', function (done) {
      assert(mydb);
      assert(docid);
      assert(lastrev);
      mydb.destroy(docid, lastrev, function(err, doc) {
        if (err) {
          done(err);
        } else {
          assert(doc.id == docid);
          done();
        }
      });
    });
  });
});

다시 newVisit 함수로 되돌아 와서 newVisit 함수가 전달 받은 정보를 확인하면 request에서 정보를 얻는 과정을 거쳐야 합니다. 특히 newVisit과 같은 POST 메소드를 처리하는 경우 body의 데이터를 읽어야 하는데, body-parser 모듈은 이와 같인 기능을 자동으로 처리해 주므로 이를 활용하도록 합니다.

다음과 같은 명령으로 body-parser 모듈을 설치합니다.

$ npm install body-parser --save

그리고, app.js에서 다음과 같이 body-parser 모듈을 사용합니다.

...
var app = express();

var bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({
    extended: true
}));
app.use(bodyParser.json());
...

newVisit 함수가 호출되면 req.body object를 통해 전달 받은 정보를 확인 할 수 있습니다.

function newVisit(req, res) {
  console.log(req.body);

  // TODO
  res.statusCode = 200;
  res.end('OK');
}

newVisit이 넘겨 받은 샘플 데이터는 다음과 같습니다.

{ id: 10103023023010,
  date: '2017-04-28 12:10:00 GMT+09:00',
  updated: 1493349000000,
  visitor: 
   { name: 'Jane Doe',
     title: 'Dr.',
     contact: '+82-1-1234-0678',
     email: 'jane.doe@customer.com',
     nationality: 'South Korea',
     company: 'Customers Inc' },
  escort: 
   { id: 'CN=John Doe/OU=ACME/O=IBM',
     name: 'John Doe',
     email: 'john.doe@acme.ibm.com',
     dept: 'Client Innovation Lab' },
  agreement: 
   { templateid: '238a8a7d77f7bf',
     agreement: 'User agreement blabla',
     date: 1493349000000,
     signature: 'data:image/jpeg;base64,R0lGODlhDwAOAOYAAAAAAP' },
  badge: { id: 12392391239, type: 'VISITOR', number: 1, returnYN: 'YES' } }

Cloudant DB 구성

이제 Cloudant에 필요한 데이터를 저장하고 이를 호출하도록 구성해야 합니다. 그러면 먼저 Cloudant에 대한 몇 가지 특징을 알고 있어야 합니다.

  • 저장되는 데이터는 JSON 형식이다
  • 저장되는 데이터는 문서로 취급한다
  • 문서는 최초 생성 시 중복되지 않는 ID로 구분한다
  • 정보가 갱신되면 문서 ID는 변하지 않으나, 문서 revisioin이 변경된다.
  • 검색을 위한 KEY를 지정하여 View를 통해 정보를 접근한다
  • 관계형 DB와 달리 참조가 없다

사실 마지막 참조가 없다는에 대한 부분이 기존 관계형DB와 가장 다른점입니다. 예를 들어 RDB는 특정 ID를 갖는 정보가 저장되어 있는 테이블과, 이를 Foreign Key로 가진 테이블과 join해서 필요한 정보를 가져 올 수 있으며 그렇기에 하나의 테이블의 정보를 변경해도 해당 정보를 가져오는 시점에 변경된 내용이 반영된 결과를 얻게 됩니다. Cloudant의 경우 저장되는 데이터는 문서 단위입니다. 하나의 고유한 ID를 갖는 문서에 데이터가 포함되고 변경됩니다. 그러나, 테이블에서처럼 column이나 데이터 타입에 대한 제한이 없으므로 상당히 유연한 형태가 됩니다.

따라서, Cloudant 사용시에는 문서의 종류를 구분하고 그 문서가 가지는 데이터 영역이 얼마나 되는지를 먼저 결정해야 합니다. 방문자 API를 예를 든다면 visitorescort 에 대한 부분입니다. 본 글에서는 하나로 합치는 방법을 선택하며 필요에 따라 view를 정의하는 것으로 진행 합니다.

물론 하나의 문서에 여러개의 데이터가 저장되는 경우 반복되는 내용이 들어갈 수 있습니다. 중복되는 경우가 상당히 많다고 판단되고, 정보 조회를 위해 추가 API를 호출하는 것이 더 낫다고 판단되면 분리된 형태로 구성할 수도 있습니다만, 본 글에서는 그렇지 않기 때문에 하나로 합쳐서 생성합니다.

방문 정보의 id는 Cloudant에서 자동으로 생성되도록하며 나머지 데이터를 모두 저장하는 형식으로 처리하도록 합니다. 또한, 임시 방문증 정보와 같은 내용과 구분하기 위해 type이라는 이름으로 문서 종류를 구분하도록 합니다.

이제 Bluemix 애플리케이션 대시보드를 통해 Cloudant 관리 화면으로 들어가볼 차례입니다.

애플리케이션에 연결되어 있는 서비스 중 Cloudant 서비스를 선택하면 Cloudant 서비스로 연결에 필요한 링크를 볼 수 있습니다.

화면 오른쪽의 LAUNCH 버튼을 클릭하면 별도로 구성된 Cloudant Web Dashboard 화면으로 자동으로 전환됩니다.

대시보드에서 현재 생성된 database를 볼 수 있는데 smv-ui 앱에서 사용하는 my_sample_db와 smv-visit에서 사용하는 smv가 생성된 것을 볼 수 있습니다. 만약 smv가 보이지 않는 경우 SMVCloudantHelper.js 모듈일 실행되어 database를 생성한 적이 없는 경우입니다. 그런 경우 애플리케이션을 실행하거나 아니면 단위 테스트를 실행하거나, Cloudant 대시보드의 오른쪽 상단의 Create Database를 클릭하여 새로운 데이터베이스를 생성 하는 방법으로 새롭게 데이터 베이스를 작성 할 수 있습니다.

서비스 Handler 구현

앞서 구성해 놓은 API Handler에 대해 Cloudant를 이용하여 구현합니다. 본 글에서는 Cloudant 사용에 대한 간략한 설명만 하고 있으므로, Cloudant Query에 대한 상세 부분은 https://docs.cloudant.com/cloudant_query.html나 Bluemix의 https://console.ng.bluemix.net/docs/services/Cloudant/api/cloudant_query.html에서 확인 하실 수 있습니다.

Document 생성

Cloudant에서는 document를 생성 할 때 id를 지정하지 않으면 자동으로 생성됩니다. smv-visit API를 통해 전달되는 id를 cloudant의 id 정보로 활용 할 수 있도록 해야 합니다. 또한, Cloudant와 API를 통해 처리되는 데이터가 동일하지 않기 때문에 이에 대한 처리 코드를 작성해야 합니다.

다음은 API로 부터 데이터를 넘겨 받아 cloudant document를 생성하는 코드입니다.

function newVisit(req, res) {
  var json = req.body;
  if (json.hasOwnProperty('id')) {
    json['_id'] = String(json.id);
    json['id'] = undefined;
  }
  json['type'] = 'VISIT';
  json['date'] = new Date(json.date).getTime();
  json['updated'] = new Date().getTime();

  mydb.insert(json, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      res.json(cleanseVisitObject(Object.assign(json, doc)));
      res.end();
    }
  });
}

cleanseVisitObject 함수는 저장된 API에 대한 정보를 smv-visit API에 맞춰 변경하는 코드로 다음과 같습니다.

function cleanseVisitObject(visit) {
  // 
  var id = visit._id || visit.id;
  var rev = visit._rev || visit.rev;
  var type = visit.type;
  
  var json = Object.assign(visit, {
    id: id
  });

  json['_id'] = undefined;
  json['_rev'] = undefined;
  json['type'] = undefined;
  json['date'] = new Date(visit.date).toString();

  return json;
}

Document 읽기

Read에 대한 부분은 다음과 같이 구현됩니다.

function getVisit(req, res) {
  var docid = req.params.id;

  mydb.get(docid, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      res.json(cleanseVisitObject(doc));
      res.end();
    }
  });
}

Document 갱신

smv-visit API에서는 수정을 원하는 부분만 입력하고 해당 부분에 대한 내용을 업데이트 하는 형태로 구성했습니다만, Cloudant의 경우 일부분에 대한 변경에 대한 일지라도 전체 document의 내용이 반영되어 있어야 합니다. 따라서, id에 대응하는 document를 먼저 읽은 후 변경이 필요한 부분을 확인하고 이를 적용한 최종 document를 만들어 업데이트 하는 과정을 거치게 됩니다.

function updateVisit(req, res) {
  var docid = req.params.id;
  
  mydb.get(docid, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      var jsondoc = Object.assign({}, doc);
      //console.log(`key:${key}`);
      // Select field to update
      for (var idx in UPDATABLE_PROPS) {
        var key = UPDATABLE_PROPS[idx];
        if (req.body.hasOwnProperty(key)) {
          jsondoc[key] = Object.assign(jsondoc[key], req.body[key]);
        }
      }
      jsondoc['updated'] = new Date().getTime();

      mydb.insert(jsondoc, function(err, doc) {
        if (err) {
          console.error(err);
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        } else {
          // new jsondoc is saved well
          res.json(cleanseVisitObject(jsondoc));
          res.end();
        }
      });
    }
  });
}

Document 삭제

cloudant document를 삭제하기 위해서는 id 뿐만 아니라 최신 저장된 revision id가 있어야 합니다. 따라서, update에서와 같이 id에 대한 document를 읽은 후 해당 document에 포함된 rev 정보를 참고하여 document를 삭제합니다.

function deleteVisit(req, res) {
  var docid = req.params.id;

  mydb.get(docid, function(err, doc) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {
      // Delete
      mydb.destroy(doc._id, doc._rev, function(err, doc) {
        if (err) {
          console.error(err);
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        } else {
          res.end('OK');
        }
      });
    }
  });
 }

Document 조회

사실 조회에 대한 부분이 제일 까다롭습니다. RDB의 SQL과 같은 형태의 Query를 제공하지 않기 때문에 약간 혼란스러울 수 있습니다만, 기본적인 동작은 다음과 같이 find 명령으로 조건에 맞는 document를 찾습니다.

function searchVisits(req, res) {
  var date = req.query.date;
  var type = req.query.type;
  var keyword = req.query.keyword;
  var page = Number(req.query.page);
  var size = Number(req.query.size);
  
...

  var selector = {
    '$and':[
      {'type': 'VISIT'}
    ]
  };

...

  mydb.find({
    selector: selector,
    limit: limit,
    skip: skip
  }, function(err, result) {
    if (err) {
      console.error(err);
      // Error 
      res.statusCode = 500;
      res.end('Internel Server Error');
    } else {

...

    }
  }

selector에 필요한 조건을 입력하면 그에 따른 document 리스트를 얻게 됩니다. smv-visitor API에서는 page 기능을 위해 resulttotal 이란 property를 지정하여 결과를 제공하도록 했는데, cloudant API에서 제공하는 limit, skip 옵션을 이용하여 pagingation 정보를 구성 했습니다.

물론 현재 검색 조건으로 얻을 수 있는 전체 문서 갯수에 대한 정보는 제공하지 않으므로 갯수를 조회하여 결과를 얻는 부분도 추가했습니다. search의 경우 cloudant API가 두 번 호출되게 되는 셈입니다. 대신 전체 정보를 얻을 필요 없이 최소한의 정보만 얻기 위해 fields라는 옵션으로 구성합니다.

...
      mydb.find({
        selector: selector,
        fields: ['type'],
        use_index: queryIndex
      }, function(err, totalInfo) {
        console.log(`total count : ${totalInfo.docs.length}`);
        resObject.total['total'] = totalInfo.docs.length;

        if (err) {
          console.error(err);
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        } else {
...
        }
      });
...

그리고, 한가지 주의 할 부분은 API 호출 시 해당 조건에 대한 전체 document 갯수를 page에 대한 total size를 알 수 없는 상태에서 조회하는 page 정보가 최대 page 보다 크게 입력될 수 있습니다. 이 부분에 대한 방어 코드를 작성하여 페이지 정보를 알 수 없는 상태를 만들지 않도록 주의 합니다. 따라서, API 구성 시 total size를 먼저 얻은 후 page size를 계산해서 최대 page index 정보를 얻는 코드가 처리되어야 합니다.

이 부분에 대한 자세한 내용은 Github https://github.com/mc500/smv/blob/master/projects/smv-visit/controllers/SMVVisitController.js 코드를 참고하시기 바랍니다.

지금까지 애플리케이션 서비스 중 방문자 정보 서비스에 대한 부분을 프로토 타입으로 만들어 보았습니다. 다음 글에서는 남은 API 서비스들에 대한 프로토타입을 구성하는 내용으로 진행 하겠습니다.


  1. 클라우드 환경 이해
  2. 애플리케이션 구상 및 요건 정의
  3. 요건에 대한 Usecase 및 Wireframe 작성
  4. 마이크로 서비스 아키텍쳐 설계
  5. 애플리케이션 서버 환경 준비
  6. 애플리케이션 로컬 개발 환경 준비
  7. 애플리케이션 프로토타입 작성 Part1
  8. 애플리케이션 프로토타입 작성 Part2
  9. 애플리케이션 프로토타입 작성 Part3
  10. 애플리케이션 DevOps 환경 구성

참고

토론 참가

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다