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


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

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

지난 글에서 IBM Bluemix용 Cloud Foundry 애플리케이션에 대한 로컬 개발 환경 구성을 다루었습니다. 이번 글에서는 이렇게 만들어진 환경에서 필요한 서비스를 하나씩 구현하여 프로토 타입을 만들어 보도록 하겠습니다.

서비스 API 호출 처리 코드 구현

앞서 각각의 서비스는 REST API로 노출하는 형태로 아키텍쳐를 구상했었습니다. 이제 이에 대한 구현 할 차례인데, Node.js 환경에서 REST API를 제공하는 방법은 여러가지가 있습니다.

Node.js 자체에서 제공하는 HTTP server를 이용하여 HTTP Request / Reponse를 처리하는 코드를 만들 수도 있고, 별도로 구성된 Framework을 이용하여 처리 할 수도 있습니다. 보통은 일정한 형태의 틀을 작성한 후 이를 각각의 서비스에 복제한 형태로 구성하는 방법을 선택하지만, 본 글에서는 다양성을 보여주기 위해 서로 다른 형태로 구현해 보도록 하겠습니다.

다만, 서비스 작성에 대해 다음과 같은 공통 부분을 적용하여 향후 DevOps를 염두해 두고자 합니다.

  • Open API 형식을 작성된 API 문서를 기반으로 서비스를 작성한다
  • 작성된 API는 반드시 대응하는 단위 Testcase를 작성한다
  • lint를 적용하여 일관된 expression을 유지한다

이제 하나씩 작성해 보도록 하겠습니다.

사용자 인증 서비스 프로토타입 구현

서비스 API 중 가장 간단한 형태인 사용자 인증 서비스를 먼저 구현해 봅니다.

Swagger Codegen을 이용

앞서 API를 작성할 때 사용했던 Swagger API Editor의 기능 중 작성한 API Spec에 대한 Server 및 Client 코드를 생성 해 주는 기능이 있습니다.

이 기능을 이용해서 사용자 인증 서비스 API를 작성해 보겠습니다. ZIP으로 압축된 파일을 다운로드 한 후 해당 파일의 압축을 해제하면 다음과 같이 apicontrollers로 구성된 형태의 코드를 볼 수 있습니다.

포함된 package.json을 확인 해보면 YAML로 작성했던 spec의 내용이 서비스 정보로 반영된 것을 알 수 있습니다.

{
  "name": "smv-user-auth-service-api",
  "version": "0.0.3",
  "description": "Simple Visitor Management User Authentication Service API",
  "main": "index.js",
  "scripts": {
    "prestart": "npm install",
    "start": "node index.js"
  },
  "keywords": [
    "swagger"
  ],
  "license": "Unlicense",
  "private": true,
  "dependencies": {
    "connect": "^3.2.0",
    "js-yaml": "^3.3.0",
    "swagger-tools": "0.10.1"
  }
}

또한 swagger-tool이라는 모듈이 포함된 것을 알 수 있는데, 아래 index.js 파일을 보면 이 모듈을 이용하여 API 호출 경로 및 해당 API의 처리를 위한 Controller를 초기화 하는 것을 알 수 있습니다.

관련 정보는 GitHub의 swagger-tools 정보를 참고 하시기 바랍니다.

CF 앱 환경으로 통합

smv-userauth는 Bluemix의 node.js 템플릿으로 생성된 것으로 CF앱이므로 cfenv 모듈과 WebUI를 위한 express middleware를 사용한 형태로 구성되어 있습니다. 그러나, swagger-tools는 connect라는 middleware를 사용하므로 구조적인 차이가 있습니다.

따라서, 다음과 같이 Express 관련 모듈을 제거하고 Swagger Editor에서 생성한 코드로 변경합니다.

package.json 파일의 dependencies에서 express를 제거후 connect, js-yaml, swagger-tools를 추가 해 줍니다.

    "dependencies": {
        "cfenv": "1.0.x",
        "connect": "^3.2.0",
        "js-yaml": "^3.3.0",
        "swagger-tools": "0.10.1"
    },

그리고, 애플리케이션 시작 파일인 app.js도 express 관련 코드를 제거후 swagger-tools 코드를 적용합니다.

// cfenv provides access to your Cloud Foundry environment
// for more info, see: https://www.npmjs.com/package/cfenv
var cfenv = require('cfenv');

// get the app environment from Cloud Foundry
var appEnv = cfenv.getAppEnv();

// Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen
var app = require('connect')();
var http = require('http');
var swaggerTools = require('swagger-tools');
var jsyaml = require('js-yaml');
var fs = require('fs');
var serverPort = appEnv.port;
...

코드를 수정했다면 npm install 명령으로 추가된 모듈을 설치한 수 smv-userauth.sh을 실행합니다.

정상적으로 실행된다면 다음과 같은 메시지를 볼 수 있습니다.

$ ./smv-userauth.sh 

> NodejsStarterApp@0.0.3 start /Project/DevWorks/smv/projects/smv-userauth
> node app.js

Your server is listening on port 6002 (http://localhost:6002)
Swagger-ui is available on http://localhost:6002/docs

Swagger Router Handler 정의

Try it out! 버튼을 누르고 서버 메시지를 확인 해 보면 Error: Cannot resolve the configured swagger-router handler: loginPOST라는 메시지와 함께 오류가 발생하는 것을 알 수 있습니다. 요청한 API를 처리할 swagger-router Handler가 없다는 메시지인데, api 폴더 아래 있는 swagger.yaml에 정의된 path 정보 중 x-swagger-router-controller 값이 지정되어 있지 않기에 발생하는 부분입니다.

생성된 코드 중 controllers라는 폴더 아래 있는 DefaultController.js, DefaultControllerService.js 파일은 mockup 정보를 제공용으로 이 파일을 참고해서 실제 우리가 필요한 코드를 작성해야 합니다. swagger-router 관련된 정보는 아래 URL을 참고 하기 바랍니다.

이제 Controller 코드를 확인 해 봅니다.

controllers 폴더의 DefaultController.jsDefaultControllerService.js 파일을 참고하여 SMVUserAuthController.js 파일을 아래와 같이 작성합니다.

'use strict';

var url = require('url');

module.exports.authUserinfoGET = function (req, res, next) {
  /**
   * 로그인한 사용자의 정보를 조회
   *
   * returns inline_response_200
   **/
  var args = req.swagger.params;
  var examples = {};
  examples['application/json'] = {
    "role" : "aeiou",
    "serial" : "1234567890",
    "phone" : "+82-2-1234-0000",
    "name" : "John Doe",
    "mobile" : "+82-10-1234-0000",
    "userid" : "CN=John Doe/OU=ACME/O=IBM",
    "email" : "john.doe@acme.ibm.com"
  };
  if (Object.keys(examples).length > 0) {
    res.setHeader('Content-Type', 'application/json');
    res.end(JSON.stringify(examples[Object.keys(examples)[0]] || {}, null, 2));
  } else {
    res.end();
  }
};

module.exports.loginPOST = function (req, res, next) {
  /**
   * 주어진 인증 정보로 로그인
   *
   * email String 사용자 Email
   * passwd String 사용자 비밀번호
   * no response value expected for this operation
   **/
  var args = req.swagger.params;
  res.end();
};

module.exports.logoutGET = function (req, res, next) {
  /**
   * 로그인 상태에서 로그아웃
   *
   * no response value expected for this operation
   **/
  var args = req.swagger.params;
  res.end();
};

그리고, api/swagger.yaml 파일의 x-swagger-router-controller 의 값을 SMVUserAuthController로 업데이트 합니다.

애플리케이션을 다시 시작하면 앞서 발생했던 오류 메시지는 나타나지 않게 됩니다.

Swagger Router Handler 구현

이제 Swagger Router Handler를 구현합니다. SMV 예제에서는 별도의 사용자 인증 서버없이 단순한 정보를 지정하여 사용 하기로 했었으므로 다음과 같이 입력하면 로그인이 성공하도록 loginPOST 함수를 작성합니다.

parameter
email john.doe@acme.ibm.com
passwd passw0rd

API 호출 시 전달되는 parameter는 Form Data이므로 request object의 body 정보에서 확인 할 수 있으나 Swagger Tools를 이용하는 경우 request object의 swagger object라는 정보로도 전달됩니다. 예를 들어 loginPost를 호출한 경우 전달된 req.swagger.params 정보는 다음과 같습니다.

{
    "email": {
        "originalValue": "john.doe@acme.ibm.com", 
        "path": [
            "paths", 
            "/login", 
            "post", 
            "parameters", 
            "0"
        ], 
        "schema": {
            "description": "\uc0ac\uc6a9\uc790 Email", 
            "in": "formData", 
            "name": "email", 
            "required": true, 
            "type": "string"
        }, 
        "value": "john.doe@acme.ibm.com"
    }, 
    "passwd": {
        "originalValue": "passw0rd", 
        "path": [
            "paths", 
            "/login", 
            "post", 
            "parameters", 
            "1"
        ], 
        "schema": {
            "description": "\uc0ac\uc6a9\uc790 \ube44\ubc00\ubc88\ud638", 
            "in": "formData", 
            "name": "passwd", 
            "required": true, 
            "type": "string"
        }, 
        "value": "passw0rd"
    }
}

Controller 구현은 Middleware인 connect의 형식을 따르지만 Node.js의 기본 http를 그대로 사용합니다. 아래 URL에서 해당 정보를 참고 하시기 바랍니다.

Redis를 이용한 정보 공유 구조작성

앞서 소개했던 Adam Wiggins의 클라우드 앱12 요소에서는 프로세스는 Stateless 형태로 구성하며, 특히 Sticky Session을 사용하지 않아야 한다고 강조했었습니다.

그러나, 사용자 입장에서 볼 때는 서버 프로세스가 몇 개가 되든지 서비스가 stateful이거나 stateless이거나 알 필요도 없습니다. 다만 자신은 서버에 로그인을 했고 그 이후 제공하는 기능을 잘 이용하기만 하면 되는 거죠.

그렇다면, 사용자가 로그인 했고 어떤 사용자이며 어떤 권한을 가졌는지 각각 나누어져있는 서비스에서 알 수 있는 방법이 필요합니다. 주어진 환경에 따라서 다양한 방법이 있을 수 있습니다. 보통은 인증 성공 후 해당 서비스 접근에 대한 token을 발급하는데 JWT(JSON Web Token: https://jwt.io/)와 같이 Token에 정보를 담은 후 이를 공유하는 방식을 이용하거나, OAuth2와 같이 별도의 인증 서버를 두고 그 서버가 발급한 token을 사용하기도 합니다.

본 글에서는 범용은 아니고 내부에서 사용하는 것이므로 표준 형태는 아니므로 Bluemix에서 제공하는 Redis 서비스 (Compose for RedisRedis Cloud)를 이용하여 token 발급 기능을 구현하고자 합니다.

애플리케이션 입장에서 Bluemix의 Redis 서비스들의 사용하는 방법은 동일하지만, 제공하는 Plan에 따라 유료 또는 무료로 사용이나 용량과 같은 제한 사항이 있으므로 아래 Bluemix catalog 정보를 참고 하시기 바랍니다.

기본 설계 및 Redis Client 선택

기본 설계는 다음과 같이 정합니다.

  • Login 성공 시 Unique ID인 인증 토큰을 생성한다
  • 생성한 토큰을 기준으로 Redis용 Key를 생성한다
  • Redis의 Expire 기능을 이용하여 정해진 시간 동안만 정보를 유지한다
  • Redis의 Hash Get/Set 기능을 이용하여 서비스간 공유될 정보를 저장 관리한다
  • Logout 시 키 정보를 모두 삭제한다

이제 Node.js용 Redis Client를 설치합니다. https://redis.io/clients#nodejs를 보면 여러가지 Node.js용 Redis Client가 있는 것을 볼 수 있습니다. 본 글에서는 node_redis를 이용합니다.

아래와 같은 npm 명령으로 redis 패키지를 설치하고 package.json 파일도 업데이트합니다.

$ npm install redis --save

Redis Client 초기화

Redis Client는 다양한 option으로 초기화를 할 수 있습니다. host, port, password를 이용할 수도 있고 이 정보가 전부 포함된 url라는 옵션으로 초기화도 가능합니다. 그외 다른 여러가지 옵션이 있으니 https://github.com/NodeRedis/node_redis를 참고하여 필요에 따라 옵션을 결정 하도록 합니다.

앞서, Bluemix에서는 Redis CloudCompose for Redis 두 가지를 제공한다고 했습니다. Redis 서비스를 생성하고 이를 사용할 애플리케이션과 연결(Bind)하면 환경변수 VCAP_SERVICES에 해당 서비스에 대한 신임 정보가 연결됩니다. 따라서, cfenv 모듈에서 필요한 서비스를 선택하고 해당 서비스의 신임정보를 수집하여 Redis Client를 초기화 할 수 있습니다.

다음 코드에서는 Compose for Redis-smv라는 이름으로 생성된 Redis 서비스의 신임정보(Credentials)를 찾고 이를 이용하여 Redis Client 를 생성하는 내용을 보여주고 있습니다. 당연히 Compose for Redis-smv는 이 애플리케이션에 연결(Bind)되어 있어야 합니다.

const REDIS_SERVICE_NAME = 'Compose for Redis-smv';

const appEnv = require('cfenv').getAppEnv();
const redis = require("redis");

const redisCredentials = appEnv.getServiceCreds(REDIS_SERVICE_NAME);

// Initialize 
var redisClientOptions;

if (redisCredentials.uri) {
  redisClientOptions = {

    'url': redisCredentials.uri
  };
} else {
  redisClientOptions = {
    'host': redisCredentials.hostname,
    'port': redisCredentials.port,
    'password': redisCredentials.password
  };
}

const redisClient = redis.createClient(redisClientOptions);

인증 토큰(Auth token) 생성

다음 작업은 로그인이 성공한 사용자에게 발급할 인증 토큰 생성하는 코드의 작성입니다. 인증 토큰은 임의의 값(random)을 사용하기도 하고 필요한 정보를 담아 사용 할 수 있는 일정한 규칙을 가진 형태가 될 수도 있습니다. 또한, 발급된 토큰은 이미 생성되어 있으면 안되며 또한 서로 중복되지 않아야 합니다.

본 글에서는 uuid (https://www.npmjs.com/package/uuid)라는 모듈을 이용하여 인증 토큰을 생성합니다.

다음과 같이 uuid 모듈을 설치합니다.

$ npm install uuid --save

uuid 모듈은 RFC4122의 Version4 (random) 방식 생성을 지원하므로 다음과 같이 인증 토큰 생성 코드를 작성합니다.

var uuidV4 = require('uuid/v4');
var token = uuidV4();

Redis용 Key 생성

인증 토큰을 Redis용 Key로 직접 사용 할 수도 있으나 그 보다는 약간의 변형된 값을 사용 할 수 있도록 다음과 같이 별도로 준비된 함수를 호출하여 Key를 얻도록 합니다. 아래는 인증 토큰의 '-'(dash) 문자를 '_'(underscore) 문자로 바꿔서 Redis 키를 생성하는 내용입니다.

function tokenAsKey(token) {
  return token.replace(/-/gi, '_'); // replace all of '-' to '_'
}

각 코드에서는 이 함수를 이용하여 전달 받은 인증 토큰을 Redis Key로 사용합니다.

Redis의 Expire 기능을 활용한 인증 토큰 생성

Redis의 다양한 기능 중 하나인 EXPIRE 명령(https://redis.io/commands/expire)은 키에 대응하는 정보를 설정된 시간 동안만 유지하고 그 시간 이후에는 삭제합니다. 이를 이용하면 제한된 시간에서만 유효한 인증 토큰을 관리 할 수 있습니다.

가장 먼저 인증 토큰을 생성합니다. 그리고, 이 토큰을 key로 변환합니다.

var token = uuidV4();
var key = tokenAsKey(token);

그 다음 생성된 Key와 HSET 명령(https://redis.io/commands/hset)으로 정보를 생성합니다.

redisClient.hset(key, '_expires_in', EXPIRES_IN_SECS, function(error, result) {
...
});

정보가 생성되면 EXPIRE 명령을 이용하여 주어진 시간까지만 해당 Key에 대한 정보가 유효하도록 설정합니다.

redisClient.expire(key, EXPIRES_IN_SECS, function(error, result) {
    ...
});

이 모든 동작이 성공하면 앞서 생성한 인증 토큰 정보를 사용자 로그인 성공 정보로서 전달하게 됩니다.

  • Redis Key 정보가 유효한지 확인

인증 토큰으로 생성한 Key가 유효한지 확인하는 부분도 필요합니다. 사실 Key가 없다면 데이터를 가져올 수 없어 오류 형태가 되므로 궂이 필요하지 않을 수도 있지만, 앞서 EXPIRE 명령을 이용했기 때문에 TTL 명령을 이용하는 방법으로 해당 Key에 대한 유효성을 알아 볼 수 있습니다.

redisClient.ttl(key, function(error, result) {
    ...
});

Redis의 Hash Get/Set 기능을 이용하여정보를 저장 관리

Key가 준비되었다면 Redis의 HGET 명령(https://redis.io/commands/hget)과 HSET 명령(https://redis.io/commands/hset)을 이용하여 field-value 형태의 정보를 이용 할 수 있습니다.

redisClient.hget(key, field, function(error, result) {
    ...
});
redisClient.hset(key, field, value, function(error, result) {
    ...
});

Redis Key에 대한 정보 삭제

Redis DEL 명령(https://redis.io/commands/del)은 해당하는 Key에 대한 모든 정보를 삭제합니다. 이를 이용하여 사용자 로그아웃 시 아래 코드를 호출하여 정보를 삭제토록 합니다.

redisClient.del(key, function(error, result) {
    ...
});

Redis와 관련된 부분은 다른 서비스에서도 공통으로 사용되어야 하므로 이를 모듈화 한 형태인 SMVAuthTokenHelper로 구성합니다. 코드는 https://github.com/mc500/smv/blob/master/projects/smv-userauth/controllers/SMVAuthTokenHelper.js에서 확인 할 수 있으니 참고하기 바랍니다.

Swagger Router Handler 업데이트

이제 SMVAuthTokenHelper 모듈을 이용하여 SMVUserAuthController 코드를 변경합니다.

'use strict';

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

function extractAuthToken(req) {
  var token = req.headers['x-auth-token'];
  return token;
}

module.exports.userinfoGET = function (req, res, next) {
  /**
   * 로그인한 사용자의 정보를 조회
   *
   * returns inline_response_200
   **/
  var example = {
    "role" : "USER",
    "serial" : "1234567890",
    "phone" : "+82-2-1234-0000",
    "name" : "John Doe",
    "mobile" : "+82-10-1234-0000",
    "userid" : "CN=John Doe/OU=ACME/O=IBM",
    "email" : "john.doe@acme.ibm.com"
  };

  var token = extractAuthToken(req);
  SMVAuthTokenHelper.isValidAuthToken(token, function(valid) {
    if (valid) {
      // 
      SMVAuthTokenHelper.getAuthTokenValue(token, 'userid', function(result){
        if (result && result == example.email) {
          res.setHeader('Content-Type', 'application/json');
          res.end(JSON.stringify(example));
          res.end(); 
        } else {
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        }
      });
    } else {
      res.statusCode = 401;
      res.end('Unauthorized');
    }
  });
};

module.exports.loginPOST = function (req, res, next) {
  // console.log(req.body.email);
  // console.log(req.body.passwd);
  // console.log(req.swagger.params.email.value);
  // console.log(req.swagger.params.passwd.value);
  /**
   * 주어진 인증 정보로 로그인
   *
   * email String 사용자 Email
   * passwd String 사용자 비밀번호
   * no response value expected for this operation
   **/
  var args = req.swagger.params;
  if (!args.email) {
    res.statusCode = 400;
    res.end('email is missing');
    return;
  }
  if (!args.passwd) {
    res.statusCode = 400;
    res.end('passwd is missing');
    return;
  }

  var email = args.email.value;
  var passwd = args.passwd.value
  if (email === 'john.doe@acme.ibm.com' && passwd === 'passw0rd') {
    SMVAuthTokenHelper.generateAuthToken(function(token) {
      // Set the key of user
      SMVAuthTokenHelper.setAuthTokenValue(token, 'userid', email, function(result){
        if (result) {
          res.setHeader('X-AUTH-TOKEN', token);
          res.end('OK'); 
        } else {
          // Error 
          res.statusCode = 500;
          res.end('Internel Server Error');
        }
      });
    });

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

module.exports.logoutGET = function (req, res, next) {
  //console.log(req.headers['x-auth-token']);
  /**
   * 로그인 상태에서 로그아웃
   *
   * no response value expected for this operation
   **/
  var token = extractAuthToken(req);
  SMVAuthTokenHelper.invalidateAuthToken(token, function(valid) {
      if (valid) {
      res.end('OK'); 
    } else {
      res.statusCode = 401;
      res.end('Unauthorized');      
    }
  });
};

Testcase 작성

기능을 위한 코드가 작성 되었다면 원하는 기능이 정상적으로 동작하는지도 반드시 확인해야 합니다. 기본적으로 동작을 하는지 확인하는 것인데, 개발 일정이나 개인의 습관으로 이를 무시하기도 합니다. 그러나, 테스트 케이스는 설계가 잘되었는지 개발 과정에서 사용되며, 이후 개발이 끝난 후 유지 보수를 위한 코드 수정이나 기능 추가 시 개발자가 예상하지 못한 부분까지 확인 할 수 있는 매우 중요한 도구입니다. 향후 DevOps에서도 반드시 필요한 것이므로 작성을 진행합니다.

MochaJS를 이용한 JavaScript 단위 테스트

JavaScript 코드 테스트를 위한 도구는 여러가지가 있겠으나, 본 글에서는 MochaJS를 이용한 테스트 코드를 사용하도록 하겠습니다. MochaJS에 대한 자세한 내용은 https://mochajs.org/를 참고 하기 바랍니다.

먼저 다음 명령으로 mocha 프레임워크를 설치합니다.

$ npm install mocha --save-dev

mocha 모듈이 설치 완료되면 프로젝트 폴더에 test이라는 폴더를 생성합니다.

$ mkdir test

생성된 test 폴더에 SMVAuthTokenHelper용 단위 테스트 파일인 test-authtokenhelper.js를 다음과 같은 모습으로 생성합니다.

const TEST_TIMEOUT = 60*1000; // 60 seconds

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

describe('[SMVAuthTokenHelper Unit Test]', function() {

    var authtoken;

    this.timeout(TEST_TIMEOUT);

    it('AuthToken generation', function (done) {
        SMVAuthTokenHelper.generateAuthToken(function(token) {
            assert(token);

            // keep this. it will be used later testcases
            authtoken = token;

            done();
        });
    });
    
    ...
});

테스트 케이스에 대한 전체 내용은 https://github.com/mc500/smv/blob/master/projects/smv-userauth/test/test-authtokenhelper.js을 참고 하사기 바랍니다.

그리고, package.json 파일의 script에 다음과 같이 test 항목을 추가합니다.

...
"scripts": {
    "start": "node app.js",
    "test": "mocha"
  },
...

이제 생성된 단위 테스트 파일을 테스트를 위해 다음과 같은 명령으로 실행합니다.

$ npm test

그런데, 위 명령으로 실행하면 다음과 같은 오류 메시지가 출력되면서 정상 진행이 되지 않습니다.

$ npm test

> SMVUserAuthService@0.0.3 test /Project/DevWorks/smv/projects/smv-userauth
> mocha

/Project/DevWorks/smv/projects/smv-userauth/controllers/SMVAuthTokenHelper.js:16
if (redisCredentials.uri) {

                    ^

TypeError: Cannot read property 'uri' of null
    at Object.<anonymous> (/Project/DevWorks/smv/projects/smv-userauth/controllers/SMVAuthTokenHelper.js:16:21)
...

SMVAuthTokenHelper 모듈이 사용하는 Redis 서비스에 대한 정보가 없기 때문에 발생하는 오류인데 CF App에서는 연결된 Redis 서비스에 대한 신임 정보를 환경 변수를 통해 얻게 되므로 이를 위해 다음과 같이 로컬 실행 스크립트 파일과 유사한 로컬 테스트 파일인 test.sh을 생성하도록 합니다.

서비스 앱용 스크립트는 Linux나 MacOS의 경우 다음과 같이 작성합니다.

export PORT=6001
export VCAP_SERVICES=$(node vcap-local.js)
npm test

Windows batch 파일의 경우는 다음과 같습니다.

set PORT=6001
FOR /F "delims=" %%i IN ('node vcap-local.js') DO set VCAP_SERVICES=%%i
npm test

이제 test 파일이 정상 실행되면 다음과 같은 실행 결과를 확인 할 수 있습니다.

$ ./test.sh 

> SMVUserAuthService@0.0.3 test /Project/DevWorks/smv/projects/smv-userauth
> mocha



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


  6 passing (2s)

만약 구현 코드를 변경할 때 테스트 케이스의 예상을 벗어난 잘못된 동작을 한다면 다음과 같이 오류가 발생합니다.

$ ./test.sh 

> SMVUserAuthService@0.0.3 test /Project/DevWorks/smv/projects/smv-userauth
> mocha



  [SMVAuthTokenHelper Unit Test]
    1) AuthToken generation
    2) AuthToken validation
    ✓ AuthToken validation negative test (520ms)
    3) AuthToken set value
    4) AuthToken get value
    5) AuthToken invalidate


  1 passing (535ms)
  5 failing

  1) [SMVAuthTokenHelper Unit Test] AuthToken generation:
     AssertionError: undefined == true
      at test/test-authtokenhelper.js:24:13
      at Object.generateAuthToken (controllers/SMVAuthTokenHelper.js:44:3)
      at Context.<anonymous> (test/test-authtokenhelper.js:23:28)

  2) [SMVAuthTokenHelper Unit Test] AuthToken validation:
     AssertionError: undefined == true
      at Context.<anonymous> (test/test-authtokenhelper.js:34:9)

  3) [SMVAuthTokenHelper Unit Test] AuthToken set value:
     AssertionError: undefined == true
      at Context.<anonymous> (test/test-authtokenhelper.js:51:9)

  4) [SMVAuthTokenHelper Unit Test] AuthToken get value:
     AssertionError: undefined == true
      at Context.<anonymous> (test/test-authtokenhelper.js:60:9)

  5) [SMVAuthTokenHelper Unit Test] AuthToken invalidate:
     AssertionError: undefined == true
      at Context.<anonymous> (test/test-authtokenhelper.js:69:9)



npm ERR! Test failed.  See above for more details.

Source Code Lint 적용

Microsoft Windows용 애플리케이션 개발 프로그램이었던 Visual Studio를 사용했던 분들은 다른 소스 코드 편집기를 이용하여 코드를 볼 때 Tab Spaces와 Shift Width가 달라 기존 코드를 보기 불편했던 경험을 하셨으리라 생각 합니다. 최근에는, 협업을 통한 개발을 많이 하게 되는데 비단 tab이나 indentation 뿐만 아니라 함수의 선언이나 시작에 대한 스타일 차이로 인해 서로 불편한 상황이 발생하기도 합니다.

소스 코드에 대한 lint는 잘못된 소스 코드 작성으로 인해 발생하는 오류를 미리 확인하고 실행 중 발생 할 수 있는 잠재적인 원인을 줄이는 것을 목적으로 시작되었으나, 요즘과 같이 여러 사람들이 코드를 보고 협업하는 오픈 소스 환경에서는 일관된 코드 스타일을 유지하기 위한 방법으로도 많이 사용되고 있습니다.

JavaScript용 Lint

JavaScript용 Lint는 여러가지가 있습니다. 제일 유명하기도 하지만 그 원조는 Douglas Crockford가 만든 JSLint일 것입니다. 그가 쓴 책인 Javascript the good parts에서 소개된 것인데 JavaScript에 대한 이해를 바탕으로 코드의 질을 높이고 잠재적인 오류를 줄여 보다 효율적인 패턴 및 코딩 방법을 제시합니다. Code가 이와 같은 패턴이나 규칙을 따르고 있는지 검사하는 도구로서 JSLint가 개발 되었다고 합니다.

오픈소스 커뮤니티를 통해 JSLint를 여러가지 환경에서 사용할 수 있도록 좀 더 개선한 JSHint도 있고, Nicholas C. Zakas라는 분이 개발 후 오픈소스화 한 ESLint도 있습니다.

ESLint의 경우 플러그인 기능과 개인이 설정한 규칙을 추가 할 수 있어Airbnb JavaScript Style Guide도 ESLint를 기반으로 제시하고 있습니다. 반드시 이 규칙을 따를 필요는 없지만 좋은 예제로서 참고 할 수 있을 듯 합니다.

또한, IDE(통합 개발 환경)에서 lint 기능을 제공하거나 플러그인 형태로 검사 하는 기능을 제공하기도 합니다. 그럴 경우 코드 수정과 동시에 오류 발생 지점 및 원인에 대해 알 수 있으므로 편리합니다만, 본 글에서는 Command Line 환경에서 검사하고 확인하는 것을 목적이므로 IDE에 대한 내용은 다루지 않겠습니다.

본 글에서는 확장성이 높은 ESLint를 활용하도록 합니다.

ESLint 설정

다음과 같이 ESLint 모듈을 설치합니다.

$ npm install eslint --save-dev

ESLint는 lint 실행 중 전체 적용되는 설정 정보를 별도의 .eslintrc.yml라는 파일에 저장해 놓습니다. 또한, eslint --init 명령을 이용하면 기본적인 설정 파일 생성 기능을 제공합니다. .eslintrc.yml 파일은 JavaScript나 JSON 그리고 YAML 세 가지 형식을 지원하므로 초기화 시 적절하게 선택하도록 합니다.

이제 다음과 같은 명령으로 설정 정보를 초기화 해 봅니다.

$ ./node_modules/.bin/eslint --init
? How would you like to configure ESLint? Answer questions about your style
? Are you using ECMAScript 6 features? Yes
? Are you using ES6 modules? No
? Where will your code run? Node
? Do you use JSX? No
? What style of indentation do you use? Spaces
? What quotes do you use for strings? Single
? What line endings do you use? Unix
? Do you require semicolons? Yes
? What format do you want your config file to be in? YAML
Successfully created .eslintrc.yml file in /Project/DevWorks/smv/projects/smv-userauth

위와 같은 문답 형식으로 만들어진 파일은 YAML 형식인 .eslintrc.yml 파일로 생성되었으며 내용은 아래와 같습니다.

env:
  es6: true
  node: true
extends: 'eslint:recommended'
rules:
  indent:
    - error
    - 4
  linebreak-style:
    - error
    - unix
  quotes:
    - error
    - single
  semi:
    - error
    - always

기본적으로 ESLint에 내장된 eslint:recommended 규칙을 확장하고 있으며 적용된 규칙에 대한 상세 내용은 https://eslint.org/docs/rules/에서 check되어 있는 규칙을 참고 하시기 바랍니다.

이제 이 상태에서 ./node_modules/.bin/eslint app.js 명령으로 ESLint를 실행하여 작성된 코드를 검사합니다.

ESLint로 소스 코드 검사

ESLint를 이용하여 smv-userauthapp.js의 소스 코드를 확인 했더니 아래와 같은 오류 메시지를 출력합니다.

$ ./node_modules/.bin/eslint app.js

/Project/DevWorks/smv/projects/smv-userauth/app.js
  24:3  error  Expected indentation of 4 spaces but found 2  indent
  25:3  error  Expected indentation of 4 spaces but found 2  indent
  26:3  error  Expected indentation of 4 spaces but found 2  indent
  59:3  error  Expected indentation of 4 spaces but found 2  indent
  60:5  error  Expected indentation of 6 spaces but found 4  indent
  61:5  error  Expected indentation of 6 spaces but found 4  indent
  62:5  error  Missing semicolon                             semi
  65:3  error  Expected indentation of 4 spaces but found 2  indent
  68:3  error  Expected indentation of 4 spaces but found 2  indent
  71:3  error  Expected indentation of 4 spaces but found 2  indent
  74:3  error  Expected indentation of 4 spaces but found 2  indent
  77:3  error  Expected indentation of 4 spaces but found 2  indent
  78:5  error  Expected indentation of 6 spaces but found 4  indent
  78:5  error  Unexpected console statement                  no-console
  79:5  error  Expected indentation of 6 spaces but found 4  indent
  79:5  error  Unexpected console statement                  no-console

✖ 16 problems (16 errors, 0 warnings)

app.js만 실행했을 뿐인데 꽤 많은 오류가 나오는 것을 볼 수 있습니다만, 대부분이 indentation(들여쓰기)으로 app.js가 사용하는 들여쓰기 크기가 2이기 때문에 이런 결과를 가져온 것입니다. .eslintrc.yml 의 내용 중 들여쓰기에 대한 부분을 다음과 같이 4에서 2로 변경합니다.

...
rules:
  indent:
    - error
    - 2
...
$ ./node_modules/.bin/eslint app.js

/Project/DevWorks/smv/projects/smv-userauth/app.js
  62:5  error  Missing semicolon             semi
  78:5  error  Unexpected console statement  no-console
  79:5  error  Unexpected console statement  no-console

✖ 3 problems (3 errors, 0 warnings)

처음보다 많이 줄어든 것을 알 수 있습니다. 62:5 error Missing semicolon는 62번째 줄에 세미콜론이 누락된 것을 말하므로 해당 라인에 세미콜론을 추가해 주면 오류가 사라집니다. 마찬가지로 78, 79번째 라인에 예상치못한 console 구문이 있다는 메시지를 볼 수 있습니다. 이 부분은 no-console 규칙으로 인해 발생하는 것인데, Bluemix의 로그 메시지는 항상 console을 통한 표준 메시지 출력을 하는므로 .eslintrc.yml에서 해당 규칙을 비활성화합니다.

...
rules:
  no-console:
    - off
...

이제 다음과 같이 다른 파일들에 대해 검사해 보도록 하겠습니다. 대상이 폴더인 경우 해당 폴더와 하위 폴더에 있는 확장자가 js인 파일을 모두 검사합니다.

$ ./node_modules/.bin/eslint app.js controllers

꽤 많이 나오기도 하고 여러가지가 있어 몇 가지만 정리하도록 하겠습니다.

메시지 규칙 조치 사항
'url' is assigned a value but never used no-unused-vars url 변수를 삭제하거나 필요한 경우 comment 처리
Expected indentation of 4 spaces but found 2 indent 들여쓰기를 4로 조정
Strings must use singlequote quotes 문자열을 작은 따옴표로 작성
Missing semicolon semi 세미콜론 추가

만약 특정 파일이 전체 규칙에서 벗어나야하는 경우 대상 파일에 ESLint 설정 정보를 추가할 수도 있습니다. 다음은 사용하지 않은 변수에 대한 내용을 warning으로 보여주는 규칙입니다.

/*eslint no-unused-vars: "warn" */

...

no-unused-vars의 경우 다음과 같이 현재는 사용하지 않지만 나중에라도 사용 할 수 있는 변수가 선언되어 있는 부분에서 오류가 발생하는 것을 볼 수 있습니다.

module.exports.loginPOST = function (req, res, next) {

...

이를 위해 no-unused-vars 규칙은 다음과 같은 옵션을 주고 있습니다.

  • vars 옵션은 alllocal 두가지가 있는데 기본 값으로 all입니다.
  • varsIgnorePattern 옵션은 정규식 패턴을 지정하여 해당 이름인 경우 무시도록 합니다.
  • args 옵션은 after-used, all, none 세가지 옵션을 제공하는데 기본 값은 after-used인데 제일 마지막 argument가 사용되었는지 확이는 옵션입니다.

우리는 모든 argument에 대한 부분은 무시하도록 다음과 같은 옵션을 추가합니다.

/*eslint no-unused-vars: ["warn", {"args": "none"}] */

...

아니면 .eslintrc.yml 파일에 다음과 같은 규칙을 추가합니다.

...
rules:
  no-unused-vars:
    - warn
    - args: none
...

또한, 앞서 작성해 놓았던 Testcase에 대서도 마찬가지로 ESLint를 적용 할 수 있습니다. 그러나 실행 환경의 차이가 있기 때문에 testcase 파일에 다음과 같이 환경 정보를 추가해 주어야 no-undef 에러가 발생하지 않습니다.

/* eslint-env mocha */

그리고, ESLint의 대상이 어느정도 정해졌다면 ESLint를 package.json의 script 항목에 등록하여 실행 할 수 있습니다. 다음과 같이 pckage.json 파일에 ESLint 호출 코드를 추가합니다.

...
  "scripts": {
    "start": "node app.js",
    "test": "mocha",
    "lint": "./node_modules/.bin/eslint app.js controllers test"
  },
...

그리고 명령창에서는 다음과 같이 npm run lint 명령을 실행하면 scripts에 정의한 명령이 실행됩니다.

$ npm run lint

> SMVUserAuthService@0.0.3 lint /Project/DevWorks/smv/projects/smv-userauth
> eslint app.js controllers test

전반적인 ESLint의 기능 및 규칙에 대한 상세 부분은 http://eslint.org/docs/rules/를 참고하시기 바랍니다.

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


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

참고

토론 참가

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