Rev. 2.73

MongoDB의 ObjectID는 12바이트짜리 BSON타입 UUID를 사용합니다. 4-byte timestamp, 3-byte machine identifier, 2-byte process id, 그리고 3-byte counter로 구성되어 합이 12바이트인 것입니다. 데이터를 쓰기도 전에 ID를 생성하며 ID만으로 타임스탬프를를 뽑아낼 수도 있지요. 그런데 이 ID를 문자로 반환하면 무려 24자 길이의 HEX 스트링이 "50812452a46e98744d000003" 요렇게 튀어나옵니다. 이 ID를 각종 파라메터나 URL에 사용하고 있는데 너무 길어서 좀 예쁘게 줄일 방법이 없나 싶어 찾아봤더니 Base64 인코딩을 이용하여 "UIjDaeXm18RLAAAD" 처럼 반 토막으로 축소할 수 있는 방법이 있어 소개합니다.

module.exports = {
  _encodeBase64ID: function(id) {
    return
      id && new Buffer(id.toString(), 'hex')
        .toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_') || id;
  },
  _decodeBase64ID: function(id) {
    return
      id && new Buffer(id
        .replace(/-/g, '+')
        .replace(/_/g, '/')
        , 'base64').toString('hex') || id;
  },
  ...
}

ExpressJS에서 위 두 함수를 이용하여 ObjectID를 Base64ID로 인코딩/디코딩 할 수 있습니다. 일단은 Helper 모듈로 만들어 가시적으로 노출되는 라우트와 컨트롤러에서만 호출하는 식으로 사용하고 있는데 포스트를 작성하면서 곰곰이 생각해 보니, 익스프레스용 미들웨어로 작성하기에 아주 적합하다는 생각이 드는군요. 라우팅 정보에서 req.params 키를 추출하여 값을 치환해 주면 되는 간단한 일이잖아요. 말이 나온 김에 지금 작성해 보겠습니다.

/*!
 * Middleware - ObjectID parsor:
 * Joon Kyoung(@firejune)
 * Copyright(c) 2012 Spark & Associates Inc.
 * MIT Licensed
 */

exports.objectIDParser = function() {
  var parse = require('url').parse;

  /**
   * Attempt to match the given params to one of the routes.
   */
  function match(req, routes) {
    var params = {}
      , method = req.method
      , captures
      , i = 0;
  
    if ('HEAD' == method) method = 'GET';
    if (routes = routes[method.toLowerCase()]) {
      var pathname = parse(req.url).pathname;
      for (var len = routes.length; i < len; ++i) {
        var route = routes[i]
          , keys = route.keys
          , path = route.regexp
          , params = [];

        if (captures = path.exec(pathname)) {
          for (var j = 1, len = captures.length; j < len; ++j) {
            var key = keys[j - 1]
              , val = typeof captures[j] === 'string'
                ? decodeURIComponent(captures[j])
                : captures[j];

            if (key) {
              params[key.name] = val;
            } else {
              params.push(val);
            }
          }
          return params;
        }
      }
    }
    return params;
  }

  /**
   *  Parse Base64ID to ObjectID in request params,
   *  providing the parsed object as `params.uuid`.
   */
  return function objectIDParser(req, res, next) {
    if (req._body) return next();

    var routes = req.app.routes.routes
      , params = match(req, routes);

    // uuid in params
    if (!params.uuid) return next();      

    // flag as parsed
    req._body = true;

    // current uuid
    if (24 <= params.uuid.length) {
      console.warn('Deprecated parameter type ObjectID is now using base64ID');
    } else {
      // decode to hex
      params.uuid = new Buffer(params.uuid.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('hex');
      Object.defineProperty(req, "params", {value: params, writable: false});
    }

    next();
  }
};

생각처럼 쉬운 일이 아니었습니다. 몇 시간을 삽질했어요. req.url 파싱해서 라우트 룰을 모두 뒤져 찾아내고 거기서 찾은 정규식을 또다시 req.url에 대입하여 req.params 값을 뽑아내야 했어요. req.app.routes에는 이미 req.params 값들이 할당되어 있었지만 업데이트되는 시점은 app.route가 호출되는 가장 마지막이기 때문에 미들웨어에서 찾을 수 있는 정보는 이전 라우트에서 사용했던 거였죠. 또 다른 문제는 req.params 값도 전달이 안 된다는 거에요. 익스프레스가 app.route를 처리되면서 리플레이스해 버립니다.

app.configure('development', function() {
  app.use(express.static(process.cwd() + '/public', {maxAge: 86400000}));
  app.use(middleware.objectIDParser());
  app.use(middleware.octetStream({tempDir: process.cwd() + '/temp/'}));
  app.use(express.bodyParser());
  app.use(express.cookieParser('fire*une'));
  app.use(express.session({secret: 'fire*une'}));
  app.use(express.methodOverride());
  app.use(app.router);
});

일단, 익스프레스에서 라우트를 비교하는 로직을 가져올 필요가 있어서 소스코드를 몽창 뒤져 match라는 함수를 발견하고 땡겨와 약간 수정했습니다. 그리고 아까 배운 Object.defineProperty로 속성을 변경할 수 없게 만들어버렸더니 잘 전달 되더라고요! 단, 주의할 점은 익스프레스가 req.params 속성을 변경 할 수 없는 상태가 돼 버리므로 이 단계에서 완성해야 합니다. 자~ 이제 라우팅 룰에 ":uuid"로 키만 설정하면 미들웨어에 의해 올바른 ObjectID로 자동 치환되어 컨트롤러에 전달됩니다. 아, 왠지 뿌듯하네요! thevelox.com에 바로 적용했습니다.

Comments

Got something to add? You can just leave a comment.

Your Reaction Time!

captcha

avatar