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


Archive»


 
 

쿠버네티스 #4

아키텍쳐


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


쿠버네티스에 대한 개념 이해가 끝났으면, 이제 쿠버네티스가 실제로 어떤 구조로 구현이 되어 있는지 아키텍쳐를 살펴보도록 하자. 아키텍쳐를 이용하면 동작 원리를 이해할 수 있기 때문에, 쿠버네티스의 사용법을 이해하는데 도움이 된다.




<그림. 쿠버네티스 아키텍쳐>

출처 https://kubernetes.io/docs/concepts/architecture/

마스터와 노드

쿠버네티스는 크게 마스터(Master)와 노드(Node) 두 개의 컴포넌트로 분리된다.

마스터는 쿠버네티스의 설정 환경을 저장하고 전체 클러스터를 관리하는 역할을 맏고있고, 노드는 파드나 컨테이너 처럼 쿠버네티스 위에서 동작하는 워크로드를 호스팅하는 역할을 한다.

마스터

쿠버네티스 클러스터 전체를 컨트럴 하는 시스템으로, 크게 다음과 API 서버, 스케쥴러, 컨트롤러 매니져, etcd 로 구성되어 있다.


API 서버

쿠버네티스는 모든 명령과 통신을 API를 통해서 하는데, 그 중심이 되는 서버가 API서버이다.

쿠버네티스의 모든 기능들을 REST API로 제공하고 그에 대한 명령을 처리한다.

Etcd

API 서버가 명령을 주고 받는 서버라면, 쿠버네티스 클러스터의 데이타 베이스 역할이 되는 서버로 설정값이나 클러스터의 상태를 저장하는 서버이다.  etcd라는 분산형 키/밸류 스토어 오픈소스 ()https://github.com/coreos/etcd) 로 쿠버네티스 클러스터의 상태나 설정 정보를 저장한다.

스케쥴러

스케쥴러는 Pod,서비스등 각 리소스들을 적절한 노드에 할당하는 역할을 한다.

컨트롤러 매니져

컨트롤러 매니저는 컨트롤러(Replica controller, Service controller, Volume Controller, Node controller 등)를 생성하고 이를 각 노드에 배포하며 이를 관리하는 역할을 한다.


DNS

그림에는 빠져있는데, 쿠버네티스는 리소스의 엔드포인트(Endpoint)를 DNS로 맵핑하고 관리한다. Pod나 서비스등은 IP를 배정받는데, 동적으로 생성되는 리소스이기 때문에 그 IP 주소가 그때마다 변경이 되기 때문에, 그 리소스에 대한 위치 정보가 필요한데, 이러한 패턴을 Service discovery 패턴이라고 하는데, 쿠버네티스에서는 이를 내부 DNS서버를 두는 방식으로 해결하였다.

새로운 리소스가 생기면, 그 리소스에 대한 IP와 DNS 이름을 등록하여, DNS 이름을 기반으로 리소스에 접근할 수 있도록 한다.

노드

노드는 마스터에 의해 명령을 받고 실제 워크로드를 생성하여 서비스 하는 컴포넌트이다.

노드에는 Kubelet, Kube-proxy,cAdvisor 그리고 컨테이너 런타임이 배포된다.

Kubelet

노드에 배포되는 에이전트로, 마스터의 API서버와 통신을 하면서, 노드가 수행해야 할 명령을 받아서 수행하고, 반대로 노드의 상태등을 마스터로 전달하는 역할을 한다.

Kube-proxy

노드로 들어오거는 네트워크 트래픽을 적절한 컨테이너로 라우팅하고, 로드밸런싱등 노드로 들어오고 나가는 네트워크 트래픽을 프록시하고, 노드와 마스터간의 네트워크 통신을 관리한다.

Container runtime (컨테이너 런타임)

Pod를 통해서 배포된 컨테이너를 실행하는 컨테이너 런타임이다. 컨테이너 런타임은 보통 도커 컨테이너를 생각하기 쉬운데, 도커 이외에도 rkt (보안이 강화된 컨테이너), Hyper container 등 다양한 런타임이 있다.

cAdvisor

cAdvisor는 각 노드에서 기동되는 모니터링 에이전트로, 노드내에서 가동되는 컨테이너들의 상태와 성능등의 정보를 수집하여, 마스터 서버의 API 서버로 전달한다.

이 데이타들은 주로 모니터링을 위해서 사용된다.


전체적인 구조 자체는 복잡하지 않다. 모듈화가 되어 있고, 기능 확장을 위해서 플러그인을 설치할 수 있는 구조로 되어 있다. 예를 들어 나중에 설명하겠지만 모니터링 정보를 저장하는 데이타베이스로는 많이 사용되는 Influx 데이타 베이스 또는 Prometheus 와 같은 데이타 베이스를 선택해서 설치할 수 있고 또는 커스텀 인터페이스 개발을 통해서, 알맞은 저장소를 개발하여 연결이 가능하다.






Google 앱앤진에 node.js 애플리케이션을 배포하기


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

PaaS 서비스란?

PaaS란 Platform as a service의 약자로, 간단하게 설명하면, Linux VM등에 직접 node.js나 기반 환경을 설치하고 디스크와 네트워크 구성, 오토스케일링등의 구성이 필요 없이, 개발자가 작성한 코드만 올려주면, 이 모든 것을 클라우드에서 대행해주는 서비스의 형태이다.

인프라 운영을 위한 별도의 작업이 필요하지 않기 때문에, 숫자가 적은 기업이나 개발에만 집중하고 싶은 기업에는 적절한 모델이라고 볼 수 있다.

근래에 스타트업 비지니스가 발달하고 또한 사용하는 기술 스택들이 복잡해짐에 따라 각각의 기술 스택에 대한 설치나 운영에 대한 인력을 두기보다는 PaaS와 같은 매니지드 서비스를 이용해서 애플리케이션을 개발하는  방식이 스타트업을 중심으로 많이 이루어지고 있다.

구글 클라우드 소개

구글 클라우드는 전세계에 걸쳐 유투브와 구글 검색, 이메일등의 서비스를 하는 구글의 인프라의 경험을 구글 외부의 개발자들에게 개방하기 위해서 개발된 클라우드 서비스이다.

글로벌을 대상으로 서비스를 하던 경험을 기반으로 글로벌 그리고 대용량 서비스에 적절한 특징들을 많이 가지고 있는데, 그 중에서 눈여겨 볼만한것이 네트워크와 빅데이타 서비스이다.

네트워크

구글은 자체 서비스를 위해서 전세계에 걸쳐서 많은 네트워크 망을 가지고 있다. 특히 전세계에 걸쳐 70개 이상의 Pop (Point of Presence)라는 서버들을 보유하고 있는데, 구글 클라우드에서 동작하는 애플리케이션은 클라이언트가 직접 그 데이타 센터의 서버로 인터넷을 통해서 접속하는 것이 아니라, 먼저 가장 가까운 Pop 서버로 연결이 된 후, Pop 서버와 구글 클라우드 데이타 센터간은 구글의 전용 광케이블을 이용해서 접속이 되기 때문에 빠른 네트워크 성능을 보장한다.

예를 들어 한국에서 미국의 서버를 접속하게 되면 일반 인터넷망을 타는 것이 아니라, 가까운 일본에 있는 구글 Pop 서버를 접속한 후에, 구글 광케이블망을 통해서 미국 데이타 센터에 접속하게 된다. 얼마전 7월에 일본-미국을 연결하는 20TB의 구글 클라우드 전용망이 오픈되었기 때문에, 훨씬 더 빠른 접속 속도를 보장받을 수 있다.

이런 특징 때문에, 구글 클라우드를 사용할 경우 여러 국가에 서비스를 제공하는 글로벌 서비스의 경우 이러한 망 가속의 잇점을 볼 수 있다.


또한 내부 네트워크 역시, 1 CPU당 2 GB의 대역폭을 제공한다. (실제 테스트해보면 1.6~1.8 GB정도가 나온다) 최대 8 CPU에 16 GB의 대역폭 까지 지원하기 때문에, 내부 노드간 연산이 많거나 또는 노드간 통신이 많은 클러스터링 솔루션을 사용할때 별도의 비용을 지불하지 않고도 넉넉한 네트워크 대역폭 사용이 가능하다.

빅데이타

구글은 사람들에게 알려진바와 같이 데이타를 기반으로 한 서비스에 강하며 특히 빅데이타를 처리하는 능력이 뛰어난 회사이다. 이러한 빅데이타 처리 기술이 클라우드 서비스로 제공되는데, 알파고와 같은 예측 통계학적인 머신러닝이나 딥러닝 이외에도, 분석 통계학적인 데이타 분석 서비스가 많은데 그중에서 특출난 서비스들로 Pub/Sub, Dataflow, BigQuery 등이 있다.

Pub/Sub은 전달 보장이 가능한 대용량 큐 시스템이다. 오픈소스 Kafka와 같은 서비스라고 보면 되고, 이러한 분산큐를 직접 깔아서 운영하기 어려운 사람들에게 쉽게 대용량 큐 서비스를 제공한다.

Dataflow는 스트리밍 및 배치 분석 시스템인데, Spark Streaming이나 Apache Flink와 같은 스트리밍 프레임웍과 유사하다고 보면된다. 윈도우,트리거,워터마크와 같은 스트리밍 서비스에서 발전된 개념을 이미 내장하고 있다.

마지막으로 BigQuery는 대용량 데이타 저장/분석 시스템으로 SQL과 같은 문법으로 데이타 분석이 가능하며, 데이타 저장 비용은 구글에서도 가장 싸다는 데이타 저장소인 CloudStorage 보다 저렴하며, 1000억개, 4TB의 레코드에 대해서 like 검색을 하는데 소요되는 시간이 불과 30여초로, 빠른 성능을 보장한다. (이 30초 동안에 CPU가 수천개, DISK역시 수천개가 사용된다.)


구글 클라우드 앱앤진

오늘 살펴볼 서비스는 구글 클라우드 중에서도 앱앤진이라는 PaaS 서비스이다. 구글은 처음 클라우드 서비스를 시작할때 PaaS 기반의 서비스를 제공하였고, 내부적으로도 PaaS 서비스를 오랫동안 제공해온 만큼, 진보된 형태의 PaaS 플랫폼을 가지고 있다.

그중에서 이번에는 새롭게 앱앤진 Flex environment 라는 새로운 서비스를 소개 하였다.

Flex environment에는 기존의 Java,PHP뿐 아니라, 요즘 스타트업등에서 많이 사용되는 Python, Ruby on rails 그리고 node.js를 지원한다.

그리고 재미있는 점 중의 하나는 일반적인 PaaS가 VM에 대한 로그인 (telnet 이나 SSH)를 지원하지 않는데 반하여, Flex environment 는 SSH 로그인을 제공함으로써, 고급 사용자들에게 많은 기능과 디버깅에 있어서 편의성을 제공한다.

계정 가입

자아 그러면 이제, 구글 앱앤진 Flex environment를 이용하여, node.js 애플리케이션을 배포해보도록 하자.

먼저 GCP 클라우드를 사용하기 위해서는 구글 계정에 가입한다. 기존에 gmail 계정이 있으면 gmail 계정을 사용하면 된다. http://www.google.com/cloud 로 가서, 좌측 상당에 Try it Free 버튼을 눌러서 구글 클라우드에 가입한다.




다음 콘솔에서 상단의 Google Cloud Platform 을 누르면 좌측에 메뉴가 나타나는데, 메뉴 중에서 “결제" 메뉴를 선택한후 결제 계정 추가를 통해서 개인 신용 카드 정보를 등록한다.


개인 신용 카드 정보를 등록해야 모든 서비스를 제한 없이 사용할 수 있다.  단 Trial의 경우 자동으로 한달간 300$의 비용을 사용할 수 있는 크레딧이 자동으로 등록되니, 이 범위를 넘지 않으면 자동으로 결제가 되는 일이 없으니 크게 걱정할 필요는 없다. (사용자가 Plan을 올려야 실제 카드에서 결재가 된다. 그전에는 사용자의 카드에서 결재가 되지 않으니 걱정하지 마시기를)


프로젝트 생성

계정 생성 및 결제 계정 세팅이 끝났으면 프로젝트를 생성한다.

프로젝트는 VM이나 네트워크 자원, SQL등 클라우드 내의 자원을 묶어서 관리하는 하나의 집합이다. 여러 사람이 하나의 클라우드를 사용할때 이렇게 프로젝트를 별도로 만들어서 별도로 과금을 하거나 각 시스템이나 팀별로 프로젝트를 나눠서 정의하면 관리하기가 용이하다.


화면 우측 상단에서 프로젝트 생성 메뉴를  선택하여 프로젝트를 생성한다.



프로젝트 생성 버튼을 누르면 아래와 같이 프로젝트 명을 입력 받는 창이 나온다. 여기에 프로젝트명을 넣으면 된다.


node.js 애플리케이션 준비

클라우드 프로젝트가 준비되었으면, 이제 구글 클라우드에 배포할 node.js 애플리케이션을 준비해보자.

Node.js 애플리케이션은 Express 프레임웍을 이용한 간단한 웹 애플리케이션을 작성해서 배포하도록 한다.

기본적으로 nodejs와 npm이 설치되어 있다고 가정한다. 이 글에서는 node.js 4.4.4 버전과 npm 2.15.1 버전을 기준으로 작성하였다.

Express generator 설치

Express 프로젝트는 Express 프로젝트 고유의 디렉토리 구조와 의존되는 파일들을 가지고 있다. 그래서 프로젝트를 생성하려면 Express generator가 필요하다. Express generator가 Express 프로젝트에 맞는 프로젝트 구조를 생성해주는데, Express generator를 설치하려면 아래 그림과 같이

% npm install express-generator -g

명령을 사용하면 설치가 된다.



프로젝트 생성

Express generator 설치가 끝났으면 이제, express 프로젝트를 생성해보자.

다음 명령어를 이용하여 helloappengine이라는 Express 프로젝트를 생성한다.

% express --session --ejs --css stylus helloappengine




Express 프로젝트 및 Express 프레임웍에 대해서는 http://bcho.tistory.com/887 http://bcho.tistory.com/888 글을 참고하기 바란다.

코드 수정

생성된 코드에서  ~/routes/index.js 파일을 다음과 같이 수정한다.


var express = require('express');

var router = express.Router();


/* GET home page. */

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

 res.render('index', { title: 'Hello Appengine' });

 console.log('Hello Appengine');

});


module.exports = router;


의존성 모듈 설치와 node.js 런타임 버전 정의

Express 애플리케이션 개발이 다 끝났다. Express 애플리케이션을 실행하려면, Express 프로젝트에 필요한 모듈들을 설치해야 하는데, 의존 모듈들은 ~/package.json에 이미 자동으로 생성되어 있다.

이 파일을 다음과 같이 수정한다.

“engines”라는 항목에 node.js의 버전을 아래와 같이 명기하는데, 이는 구글 앱앤진이 이 애플리케이션을 수행할때 어느 버전의 node.js를 가지고 수행을 할지를 지정한다.


{

 "name": "helloappengine",

 "version": "0.0.0",

 "private": true,

 "scripts": {

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

 },

 "engines":{

    "node":"4.4.4"

 },

 "dependencies": {

   "body-parser": "~1.15.1",

   "cookie-parser": "~1.4.3",

   "debug": "~2.2.0",

   "ejs": "~2.4.1",

   "express": "~4.13.4",

   "morgan": "~1.7.0",

   "serve-favicon": "~2.3.0",

   "stylus": "0.54.5"

 }

}




이 의존성 모듈 설치는 package.json 파일이 있는 디렉토리에서 아래와 같이

% npm install

명령어만 실행해주면 자동으로 설치된다.


테스트

샘플 애플리케이션 개발 및, 이를 실행하기 위한 환경 설정이 모두 끝났다.

그러면 이제 샘플 애플리케이션을 로컬에서 실행해보자.

실행은 ~/ 디렉토리에서 다음과 같은 명령을 실행하면된다.

% node ./bin/www



실행한 후, 결과를 확인하려면 http://localhost:3000 번으로 접속하면 아래와 같은 결과가 나오는 것을 확인할 수 있다.



구글 앱앤진으로 배포 준비

애플리케이션 개발 및 테스트가 끝났으면 이제 구글 앱앤진으로 이 애플리케이션을 배포해보자.

app.yaml 파일 작성

구글 앱앤진 Flex environment 로 애플리케이션을 배포하기 위해서는 애플리케이션에 대한 기본 정보를 app.yaml에 정의해야 한다. 이 파일은 배포할 node.js 애플리케이션이 있는 ~/ 디렉토리에 위치 시킨다.

runtime은 어떤 언어 (node.js , ruby 등)을 지정하고, VM으로 실행할지를 vm:true 로 정의한다.


runtime: nodejs
vm: true


Google Cloud SDK 설치

다음으로 배포를 하려면, gcloud라는 명령을 사용해야 하는데, 이 명령어는 Google Cloud SDK를 설치해야 사용할 수 있다. Google Cloud SDK는 https://cloud.google.com/sdk/downloads 에서 다운 받아서 설치한다.

Google cloud SDK 설정하기

Google cloud SDK 설치가 끝났으면, 혹시 google cloud SDK가 업데이트 되었을 수 있으니, 다음 명령어를 이용하여 최신 SDK로 업데이트 한다. (알파,베타 버전과 같은 신 기능이 들어가면 업데이트가 된다.)


% gcloud component update


라는 명령어를 수행하면 다음과 같이 업데이트 된 내용이 나오면서 업데이트 를 할 것인지 물어보고 “Y”를 선택하면 자동으로 Google Cloud SDK를 업데이트 한다.



Google Cloud SDK 배포가 끝났으면 gcloud 명령을 이용하기 위해서 초기화 작업을 해야 한다. 초기화 작업은 gcloud 명령을 사용했을때, 내 구글 클라우드 프로젝트 중 어느 프로젝트에 명령을 내릴 것인지, 그리고 어떠한 구글 계정을 사용할것인지 그리고 명령을 내릴때 어떤 리전을 디폴트로 해서 명령을 내릴것인지를 정하는 것이다. 사용자에 따라서 여러개의 프로젝트를 가질 수 도 있고, 또한 관리 목적상 여러개의 클라우드 계정을 가질 수 도 있기 때문이다.

초기화 방법은 다음과 같이 명령어를 실행하면 된다

% gcloud init

이 명령을 사용하면, 앞에서 언급한 바와 같이, gcloud 명령을 내릴 때 사용할 계정과, 디폴트 프로젝트 그리고, 리전을 선택하게 된다.


이제 배포를 위한 모든 준비가 끝났다.

구글 앱앤진으로 배포

이제 배포를 해보자. 배포는 매우매우 쉽다.

앞서 작성한 Express 애플리케이션이 있는 ~/ 디렉토리에 가서 다음 명령어를 수행하면 된다.

% gcloud app deploy

명령을 실행하면 다음과 같이 node.js 애플리케이션이 배포 되는 과정을 볼 수 있다.

배포는 수분이 소요된다. (내부적으로는 Docker 컨테이너를 이용하여 배포가 된다.)

서비스 기동확인

배포가 완료되면 자동으로 서비스가 기동이 된다.

서비스 기동 확인은 http:{프로젝트명}.appspot.com 으로 자동으로 서비스가 뜨게 된다.

해당 URL을 접속해보자. 여기서는 프로젝트명이 “useful-hour-138023”이다.


서비스가 정상적으로 기동 됨을 확인하였다.

모니터링

구글 클라우드 콘솔에 들어가 보면 현재 기동중인 node.js 앱을 확인할 수 있다.

콘솔에서 “App Engine”을 선택하면 다음과 같은 화면이 나온다.



아래 Instances 부분을 보면 Flexible 이라는 이름으로 현재 2개의 인스턴스가 기동되고 있고, 평균 QPS (Query Per Second : 초당 처리량)이 10.898 인것을 확인할 수 있다.


매니지드 서비스이기 때문에, 부하가 늘어나면 자동으로 인스턴스를 추가하고 줄어들면 자동으로 삭제하여 부하에 탄력적으로 대응을 자동으로 해준다.


로그 모니터링은 클라우드 콘솔에서 “Logging”이라는 메뉴를 선택하면 아래와 같은 화면이 나온다.


앱앤진 관련 request log 및 기타 로그들이 다 나오는데, 앞서 샘플코드에서 console.log(“Hello Appengine”) 명령어를 이용하여 Stdout으로 “Hello Appengine”이라는 문자열을 출력하도록 하였기 때문에, 이 문자열이 제대로 출력되었는지를 확인해보자.

App Engine 을 선택하고, 다음 “stdout”을 선택하면 위와 같이 앱앤진의 Stdout으로 출력한 로그들만 볼 수 있는데, 위와 같이 “Hello Appengine” 문자열이 출력된것을 확인할 수 있다.

수정 및 재 배포

그러면 다음으로 애플리케이션을 수정해서 배포해보자. routes/index.js를 다음과 같이 수정해보자


var express = require('express');

var router = express.Router();


/* GET home page. */

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

 res.render('index', { title: 'Hello Appengine is updated' });

 console.log('Hello Appengine is updated');

});


module.exports = router;



코드 수정이 끝났으면 배포를 해보자. 재 배포 역시 간단하게

%gcloud app deploy

명령어를 수행하면 간단하게 배포가 된다.

배포가 완료된 후 URL로 들어가서 확인을 해보면


애플리케이션이 업데이트 된것을 확인할 수 있다.

롤백

앱앤진의 장점 중에 하나가 배포도 쉽지만, 롤백도 매우 쉽게 가능하다는 것이다.

클라우드 콘솔에서 App Engine을 선택하고, 아래와 같이 Versions라는 메뉴를 선택하면 아래와 같은 그림을 볼 수 있다.




현재 두개의 버전이 배포되어 있고, 위의 최신 버전에 Traffic Allocation이 100%로 되어 있는 것을 확인할 수 있다. 이는 새로 배포된 버전으로만 트래픽이 100% 가고 있다는 의미인데, 롤백은 트래픽을 예전 버전으로 라우팅 해주고, 새버전의 서비스를 정지 시키면 된다.


아래 그림과 같이 예전 버전을 선택한 후에, 상단 메뉴에서 “Migrate Traffic”을 선택한다.



그러면 아래와 같이 트래픽이 이전 버전으로 100% 가고 있음을 확인할 수 있다.

그리고 나서 새 버전을 선택한 후 상단의 STOP 버튼을 눌러주면 아래 그림과 같이 새버전의 상태가 Serving 에서 Stopped로 변경된것을 확인할 수 있다.




예전 버전으로 롤백이 잘 되었는지를 확인해보자.

아래 그림처럼 예전 버전으로 롤백이 되었음을 확인할 수 있다.



버전별로 트래픽 분산하기

앱앤진의 장점 중의 하나가 여러 버전을 유지할 수 있고, 버전간에 트래픽을 자유롭게 조절할 수 있다. 예를 들어서 v1으로 90%, v2로 10% 의 트래픽을 보내는 것등이 가능하다.


이렇게 트래픽을 조절하는 것은 크게 2가지 방법으로 활용이 가능한데, 서버단의 A/B 테스팅과 카날리 테스팅이다.

A/B 테스팅은 사용자를 두개 이상의 집단으로 나눈후, 기능 A,B 에 대한 반응을 본 후, 반응이 좋은 기능을 선택하는 방식으로, 모바일 클라이언트단에서 많이 사용되고, 서버단에는 구현이 쉽지 않았는데, 트래픽을 버전별로 나눠서 주는 기능을 사용하면 손쉽게 A/B 테스팅을 수행할 수 있다.


다음으로 카날리 테스트는 옛날에 광부들이 광산에 들어갈때 카나리아 새를 데리고 들어가서 유독가스가 나오면 카나리아 새가 먼저 죽는 것을 보고 위험을 감지하는 방법에서 유래된것으로 서버 배포에서는 전체 사용자를 대상으로 새 버전을 배포하는 것이 아니라 1~2%의 사용자에게 배포해보고 문제가 없으면 전체 사용자에게 배포하는 개념으로, 마찬가지로 v2에 1~2%의 트래픽만 할당하고 나머지 98~99%는 v1에 할당하는 방식으로, 해서 v2에 대한 안정성 검증을 한 후에, v2를 전체 배포 하는데 활용할 수 있다.


버전간 트래픽 비율을 지정하는 방법은 클라우드 콘솔의 앱앤진 메뉴에서 Version을 선택하고 상단 메뉴에서 → 가 두개 겹쳐진 아이콘 (맨 우측)

를 선택하면 아래와 같이 Split traffic 이라는 메뉴가 나온다.

여기서 1개 이상의 Version을 추가한 후에, 각 버전으로 보낼 트래픽의 비중 (%)을 정의한다.






위의 예제에서는 xxx4403 버전으로 30%, xxxx3136 버전으로 70%의 트래픽을 보내도록 하였다.

설정을 완료한 후에 SAVE를 누르고, 다시 Version 부분을 보면, 수분 있다가 아래 그림과 같이 xxxx3136 버전에 70% 트래픽이 xxx4403 버전으로 가게 된다.


부가 기능에 대해서

구글 클라우드에서 앱앤진만 쓸 수 있는 몇가지 좋은 서비스 들이 있는데, 다음과 같다.

매니지드 memcached

앱앤진에서는 구글 매니지드 memcached 서비스를 사용할 수 있다 별도의 설정 없이 바로 사용이 가능하며, 최대 100GB 까지 사용이 가능하다. (100GB 이상도 요청하면 사용이 가능) Shared memcached의 경우에는 별도의 비용없이 일반적인 캐쉬로 사용이 가능하지만, 용량이나 성능등이 보장이 되지 않으니 주의가 필요하다.

https://cloud.google.com/appengine/docs/python/memcache/

보안 스캐닝 (Security Scanning)

매우 유용한 기능중의 하나가 보안 스캐닝 기능이다 앱앤진 메뉴에서 Security Scanning이라는 메뉴를 선택하면 앱앤진으로 개발한 웹 사이트에 대한 보안 스캐닝을 할 수 있는데, 한번만 스캐닝할 수 도 있고 자동 스케쥴 방식으로, 주기적으로 스캐닝 하는 것도 가능하다.


보안 정책이 수시로 업데이트 되기 때문에, 정기적으로 보안 스캔을 하는 것을 권장한다.

텍스트 검색 (Search)

node.js에는 지원하지 않지만, Python,Java,PHP,Go (앱앤진 Standard environment)에서만 지원되는 기능중 하나는 구글 검색 엔진과 같은 검색 엔진을 지원한다. 텍스트 검색 HTML 그리고 GEO_POINT (위/경도 기반 검색)이 가능하다.

https://cloud.google.com/appengine/docs/java/search/?hl=en_US

결론

지금까지 간략하게나마 구글 클라우드에 대한 소개, 앱앤진에 대한 개념 및, 간단한 node.js express 애플리케이션을 작성해서 배포해봤다. 다음글은 앱앤진에 대한 조금 더 구체적인 환경에 대해서 알아보도록 하겠다.


빠르게 훝어 보는 node.js - redis 사용하기 (ioredis 클라이언트 버전)


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


지난 포스팅에서 http://bcho.tistory.com/1098 node.js에서 redis 사용에 있어서 node-redis 클라이언트를 사용했는데, 조금 더 리서치를 해보니, node.js의 redis 클라이언트는 지난번에 포스팅한 node-redis 클라이언트와 ioredis라는 클라이언트가 가장 많이 사용된다. ioredis 클라이언트가 조금 더 최근에 나온 클라이언트인데, https://github.com/luin/ioredis


Bluebird promise 지원, 트렌젝션 지원등 훨씬 더 많은 기능을 제공하고, 사용법이 node-redis와 거의 유사하여 마이그레이션이 어렵지 않다.

아래 코드는 어제 작성 했던 코드를 ioredis 버전으로 변경한것인데, 코드를 보면 변경 내용이 거의 없음을 확인할 수 있다.


mongodb, redis, mysql 지원 모듈을 살펴보다가 느낀건데, 대부분의 모듈들이 Promise를 지원하고, 특히 bluebird를 지원한다는 것이다.

얼마전에 Async framework에 대해서 Async,bluebird, Q등을 고려했는데, 지금까지 인사이트로 봐서는 bluebird를 표준 프레임웍으로 해서 개발하는게 답이 아닐까 한다.


 

// redis example

var Redis = require('ioredis');

var redis = new Redis(6379,'127.0.0.1');

var JSON = require('JSON');

 

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

      req.redis = redis;

      next();

});

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

      req.accepts('application/json');

     

      var key = req.body.name;

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

     

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

           if(err){

                 console.log(err);

                 res.send("error "+err);

                 return;

           }

           req.redis.expire(key,10);

           res.json(value);

           //console.log(value);

      });

});

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

      var key = req.params.name;

     

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

           if(err){

                 console.log(err);

                 res.send("error "+err);

                 return;

           }

 

           var value = JSON.parse(data);

           res.json(value);

      });

});

 

// catch 404 and forward to error handler

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

  var err = new Error('Not Found');

  err.status = 404;

  next(err);

});

 


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 자체에 대해서 조금 자세하게 살펴보기를 권장한다.

 node.js에서 Heapdump를 이용한 메모리 누수 추적하기


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

대부분의 애플리케이션 서버들에서 고질적인 문제점중의 하나가 메모리 누수 현상이다. 비단 애플리케이션 서버에만 해당하는 문제는 아니지만 특히나 동시에 여러개의 요청을 반복적으로 받는 애플리케이션 서버의 경우에는 이 메모리 누수 현상이 훨씬 더 많이 발생한다.

 

node.js 의 경우, 엔진의 근간이 되는 구글 크롬 V8 자바스크립트 엔진 자체가 많은 메모리 누수 버그를 가지고 있지만, 애플리케이션에서 발생하는 메모리 누수는 발생하는 양도 크거니와 더 큰 문제를 야기할 수 있다. 이러한 애플리케이션에서의 메모리 누수를 추적하기 위한 방법을 알아보자

 

node.js의 확장 모듈인 heapdump는 기동중인 node.js 서버의 메모리 스냅샷을 추출할 수 있는 기능을 제공한다. 이 메모리 스냅샷을 힙덤프라고 하는데, 힙덤프에서 어떤 객체들이 메모리를 반복적으로 많이 점유하는 지를 찾아내면 메모리가 누수 되는 지점을 파악할 수 있다.

 

예제를 통해서 살펴보자

먼저 heapdump 모듈을 설치해야 한다. https://www.npmjs.com/package/heapdump

%npm install heapdump

명령어를 이용하여 heapdump 모듈을 설치한다.

 

Express로 간단한 웹 애플리케이션을 생성하자

package.jsonheapdump 모듈에 대한 의존성을 다음과 같이 추가한다.

 

{

  "name": "MemoryLeak",

  "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",

    "heapdump":"~0.3.7"

  }

}

 

Figure 1 package.jsonheapdump 모듈 의존성을 추가

 

다음 express 프로젝트의 app.js에 아래와 같은 코드를 추가한다.

app.use('/', routes); // 기존에 자동으로 생성된 코드

app.use('/users', users); // 기존에 자동으로 생성된 코드

 

var heapdump = require('heapdump');

var memoryLeak = [];

function LeakedObject(){ };

 

app.use('/leak',function(req,res,next){

     

      for(var i=0;i<1000;i++){

           memoryLeak.push(new LeakedObject());

      }

      res.send('making memory leak. Current memory usage :'

                 +(process.memoryUsage().rss / 1024 / 1024) + 'MB');

});

 

app.use('/heapdump',function(req,res,next){

      var filename = '/Users/terry/heapdump' + Date.now() + '.heapsnapshot';

      heapdump.writeSnapshot(filename);

      res.send('Heapdump has been generated in '+filename);

});

 

 

Figure 2 메모리 누수를 유발하는 코드 및 힙덤프를 추출하는 코드 추가

 

/leak URL을 처리하는 부분은 인위적으로 메모리 누수를 만들어낸다. for 루프를 이용하여 LeakedObject 1000개씩 배열에 추가하여 메모리 누수를 유발하고, 화면에 현재 메모리 사용량을 리턴하도록 하였다.

 

다음 /heapdump라는 URL에서는 실제로 heapdump를 생성한다. heapdump.writeSnapshot(파일명) 이라는 메서드를 사용하는데, 이 파일명 위치에 힙덤프를 저장한다.

 

또는 명령어를 사용하지 않더라도 프롬프트 상에서 힘덤프를 생성할 수 있다.

% kill –USR2 {node.js 프로세스 ID}

명령을 이용하면, node.js를 실행한 위치에 heapdumpxxx.heapsnapshot 이라는 이름으로 힙덤프를 생성해준다.

 

코드 작성이 끝났으면 애플리케이션을 실행해보자. http://localhost:3000/leak 로 들어가면 메모리 누수를 유발하고, 현재 사용중인 메모리 양을 볼 수 있다.

 



Figure 3 메모리 누수를 유발하는 URL에 접속

 

많은 메모리 누수를 유발해보기 위해서 ab (Apache benchmark : 아파치 웹서버를 설치하면 같이 설치되는 간단한 부하 테스트 툴이다. http://httpd.apache.org/) 를 이용해서 반복적으로 http://localhost:3000/leak 를 호출해보자

%ab -n 5000 http://localhost:3000/leak

명령어를 이용하면 http://localhost:3000/leak 5000번 호출한다.



Figure 4 아파치 ab툴을 이용하여 메모리 누수를 유도하기 위해서 5000번 부하를 줌

 

다시 http://localhost:3000/leak 를 접속해보면 사용중인 메모리 양이 늘어난것을 볼 수 있다.

 



Figure 5 node.js 의 메모리 사용량이 늘어난것을 확인

 

이제 힙덤프를 추출해보자. http://localhost:3000/heapdump 에 접속하면 자동으로 힙 덤프가 생성된다.

이 힙덤프는 현재 node.js가 사용중인 메모리 양이 클수록 추출하는 속도가 느려진다.

 



Figure 6 힙덤프 추출

 

힙덤프 파일이 추출되었으면, 이 힙덤프를 분석하기 위해서 구글의 크롬 브라우져에서 개발자 도구를 실행해보자

개발자 도구에서 “Profiles” 탭에서 Load 버튼을 눌러서 앞에서 추출한 힙덤프 파일을 로드한다.




Figure 7 크롬 브라우져 개발자 모드에서 힙덤프 파일을 로드

 

힙덤프를 보면, LeakedObject라는 객체가 전체 메모리의 66%, 120MB를 점유함을 확인할 수 있다.

이 객체를 열어보면, 같은 객체가 수도 없이 반복됨을 확인할 수 있다.

 



Figure 8 메모리 누수를 유발한 LeakedObject를 발견

 

이러한 방식으로 어떤 객체들이 메모리를 많이 점유해서 메모리 누수를 유발하는지 찾아낼 수 있다.

예제 소스 코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/MemoryLeak 를 참고하기 바란다.

Heroku 클라우드에 node.js 애플리이션을 배포하기


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



가장 빠르게 이해하는 방법은 직접 해보는 것이다. Heorku 클라우드에 대한 이해를 위해서 express를 이용해 구현된 node.js 애플리케이션을 직접 배포해서 실행해보자

이 예제는 node.js 4.3.1 버전과 express 4.13.4 버전을 기준으로 작성되었다.


Heroku 가입

Heroku사용을 위해서 Heroku계정을 만들자. http://www.heroku.com 에 접속해서 Sign up 메뉴로 들어가면, 간단하게 이름과 이메일 정도의 정보만 넣으면 간단하게 계정을 만들 수 있다.



Figure 1 Heroku 가입 화면


다른 클라우드의 경우 향후 유료 사용을 위해서 회원 가입시 처음부터 신용카드 정보를 요구 하는 경우가 있어서 신용카드가 없는 학생등은 가입이 안되는 경우가 있지만, Heroku의 경우 별다른 부담없이 무료로 계정을 생성해서 사용할 수 있다. 지금부터 진행할 예제는 무료 계정에서 한개의 웹 서버를 기동하여 node.js 애플리케이션을 기동하는 예제로 별도의 비용이 필요없다.


계정이 생성되었으면 Heroku 웹 콘솔에 로그인해보자




Figure 2 Heroku 대쉬 보드


첫 대쉬보드에서는 각 개발플랫폼에서 개발을 시작하는 방법에 대한 가이드를 제공한다.


node.js 애플리케이션 준비


Heroku 계정이 준비되었으면, Heroku에 배포할 node.js 애플리케이션을 준비해보자. 

“Hello Heroku”를 출력하는 간단한 node.js 애플리케이션을 작성하자.

다음과 같이 express generator를 이용해서 node.js 애플리케이션을 생성한다.


% express -–session --ejs --css stylus helloheroku

% cd helloheroku

% npm install


애플리케이션을 설치한 후에, 애플리케이션 디렉토리로 들어가서 npm install 명령을 이용하여, 애플리케이션에서 사용되는 의존성을 가지고 있는 모듈들을 설치한다.


다음으로 /routes/index.js를 다음과 같이 수정한다.


var express = require('express');

var router = express.Router();


/* GET home page. */

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

  res.render('index', { title: 'Heroku' });

  console.log(' hello heroku ');

});


module.exports = router;


Heroku에 배포하는 node.js 애플리케이션을 개발할때, 주의할점 중의 하나가 package.json이다.

보통 로컬 개발환경에 npm 패키지들이 이미 설치 되어 있는 경우 실행이 제대로 되고, 문제가 없기 때문에, package.json에 필요한 모듈에 대한 정의를 빼먹는 경우가 많다. Heroku에 애플리케이션을 배포할때는 의존 패키지들이 설치되어 있지 않기 때문에 이를 설치 해야 하는데,  Heroku는 자동으로 package.json에 정의된 npm 모듈들을 설치해 준다. 반드시 배포전에 package.json에 필요한 모듈들이 제대로 정의되었는지를 확인해보자


소스코드가 준비되었으면 Procfile 이라는 것을 준비해야 한다. 위에서 만든 애플리케이션의 루트”/” 디렉토리에 다음과 같은 내용으로 Procfile을 작성하자.


web: node ./bin/www


Procfile은 애플리케이션을 Heorku에 배포 해서 실행할때, Heroku 클라우드가 맨 처음 실행하는 명령어를 지정하는 파일이다. 보통 애플리케이션을 기동하기 위한 명령어를 기술한다.

앞서 작성한 Procfile을 보면, “web:” 이라고 기술하였는데, 배포하고자 하는 애플리케이션이 웹 애플리케이션임을 정의한다. web 타입의 애플리케이션만 Heroku의 내부 HTTP 로드밸런서에 연결이 되서 웹 요청을 받을 수 있다.

다음으로 “node ./bin/www” 라고 지정하였는데, node.js 애플리케이션을 기동하기 위한 명령어이다. 우리가 작성한 애플리케이션은 express애플리케이션으로 기동 로직이 ./bin/www 에 들어 있다.


추후에 추가로 설명하겠지만, node.js는 모니터링등을 위해서 forever나 pm2와 같은 추가적인 도구를 이용해서 기동을 하는 경우가 있다. 실행을 할때 forever ./bin/www 식으로 실행하는데, Heroku에서 실행하려면 다음과 같이 실행하면 된다.


web: ./node_modules/.bin/forever ./bin/www


이제 배포를 위한 node.js 애플리케이션 준비가 끝났다. Heorku에 배포해보자.


Heroku 툴벳 설치


계정이 준비되고 애플리케이션 개발이 끝났으면, Heroku에 접속해서 배포를 진행해야 한다. Heroku는 툴벳이라는 커멘트라인 인터페이스 (CLI : Command line interface)를 이용해서 사용한다. 

툴벳은 https://toolbelt.heroku.com/ 에서 다운로드 받을 수 있다.




Figure 3 Heroku 툴벳 다운로드 페이지


툴벳을 인스톨했으면 실행해보자. 프롬프트 상에서 

%heroku 

명령어를 수행하면 간단한 사용법이 나온다.

이 툴벳을 통해서 Heroku에 명령을 내리기 위해서는 자신의 계정으로 로그인을 해야 한다. 로그인은 “heroku login”이라는 명령을 사용해서, Heroku 가입시 생성한 계정으로 로그인을 하면 된다.




Figure 4 툴벳에서 Heroku 로그인


Heroku 배포 준비


로그인이 끝났으면, Heroku내에 앱을 생성한다. Heroku의 앱은 애플리케이션 단위로, 이 앱단위로 배포를 하고 운영을 하게 된다. 

앱 생성 방법은 다음과 같다.


%heroku app:create {앱이름}


이 예제에서는 helloherokuterry 라는 이름의 앱을 생성할것이다.

이 앱 이름은 Heroku 클라우드에 걸쳐서 유일한 이름으로 사용을 해야 하기 때문에, 위의 이름으로 앱을 생성하고자 하면 이미 저자가 해당 이름을 사용하고 있기 때문에, 아마도 에러가 날것이다. 다른 적절한 이름을 이용해서 앱을 생성하자


아래는 helloherokuterry 라는 이름의 앱을 생성하고, “heroku apps” 를 이용하여 현재 생성된 앱 리스트를 확인해서 helloherokuterry 라는 앱이 제대로 생성이 되었는지 확인 하는 화면이다.




Figure 5 Heroku에 애플리케이션을 생성하는 화면


만약에 잘못 앱을 생성하였으면 다음 명령어를 이용해서 앱을 삭제할 수 있다.


%heroku app:delete {앱이름}


앱이 생성되었으면, 작성한 애플리케이션을 배포해야 하는데, heroku의 코드 배포는 git 소스코드 관리 시스템을 사용한다.

앱 디렉토리에서 git 리포지토리를 생성한다.

%git init .

다음, 작성한 애플리케이션들을 git에 추가한다.

%git add *

추가된 애플리케이션 코드들을 commit 한다.

%git commit


이제 로컬 git 리파지토리에 애플리케이션 코드들이 저장되었다. 이 코드들을 Heroku에 전송하자.

git에 Heroku 클라우드 상의 git 저장소를 리모트 저장소로 지정해야 한다.

Heroku의 git 저장소를 리모트 저장소로 지정하는 방법은 다음과 같다. 


%heroku git:remote -a {앱이름}


여기서는 애플리케이션의 이름이 helloherokuterry이기 때문에 다음과 같이 리모트 저장소를 추가하겠다.

% heroku git:remote –a helloherokuterry


heroku의 git 리파지토리가 리모트 저장소로 지정되었다.

아래 명령어를 이용해서 로컬에 저장된 코드를 heroku로 올려보자


%git push heroku master


명령어를 실행하면 소스코드를 Heroku에 배포 하는 것을 확인할 수 있다.




Figure 6 git를 이용하여 Heroku에 앱을 배포하는 화면


배포는 단순하게 소스 코드만을 올리는 것이 아니라 올라간 소스코드를 확인해서, package.json에 지정된 의존성 있는 모듈들을 설치 한다. 아울러 환경변수를 세팅한다.

Heroku의 경우 node.js에 대해서 디폴트 환경 변수를 정의하는데

NPM과 node.js에 대한 환경 변수를 다음과 같이 설정한다. 모드를 운영 모드로 설정하는 부분이다.


remote:        NPM_CONFIG_LOGLEVEL=error

remote:        NPM_CONFIG_PRODUCTION=true

remote:        NODE_ENV=production

remote:        NODE_MODULES_CACHE=true


배포에서 git를 사용하였는데, 혹시나 git에 대해 생소한 분들을 위해서 간략하게 배포 흐름을 정리하고 넘어가자.  




Figure 7 git를 이용한 Heroku 배포 개념도


개발환경에서 작성된 소스코드는 git의 로컬 저장소 (Local repository)로 저장되기전에 git add명령어를 이용하여 스테이징 단계 (stage)로 이동된다. 소스를 저장소에 저장하기 전 단계로, 전체 코드 중에서 어떤 파일들을 저장할지를 선택하고 검토하는 일종의 중간단계이다.

스테이징이 끝나면, git commit 명령을 이용하여 로컬 저장소 즉 내 PC에 있는 git 저장소에 소스 코드를 저장한다.

다음 이 코드를 원격에 있는 Heroku의 git 저장소로 전송을 해야 하는데, 이를 푸쉬(push)라고 한다. git push 명령어를 이용해서 실행하며, 코드가 Heroku의 git 저장소에 저장이 되면, 기동을 하기 위한 환경 변수 설정이나 npm 모듈들을 설치 하는 작업을 수행한다.


이제 애플리케이션 실행을 위한 모든 준비가 끝났다. 배포한 애플리케이션을 기동해 보자


서비스 기동 및 모니터링


서버의 기동은 Heroku에서 web dyno수를 0에서 1로 늘려주면 된다.


%heroku ps:scale web=1


서버가 기동된것을 확인하였으면 웹으로 접속해서 애플리케이션이 제대로 동작하고 있음을 확인하자.

Heroku app의 URL은 http://{heroku app 이름}.herokuapp.com 이다.

우리가 만든 예제는 helloherokuterry라는 앱 이름을 사용했기 때문에, https://helloherokuterry.herokuapp.com/ 가 접속 URL이 된다

또는 간단하게 명령 프롬프트 창 내에서 heroku open 이라는 명령어를 사용하면 해당 URL을 브라우져를 열어서 자동으로 열어준다.


%heroku open




Figure 8 Heroku open 명령어를 이용하여 기동중인 웹사이트를 오픈




Figure 9 Hello Heroku 실행 화면


드디어 Heroku에 node.js 애플리케이션을 배포하고 기동시켰다.

애플리케이션의 구동 상태와 로그를 확인해보자

현재 몇개의 dyno 가 구동되고 있는지를 확인하려면 다음 명령어를 실행하면 된다.


%heroku ps




Figure 10 ps 명령어를 이용하여 동작중인 서버를 확인


현재 web 타입의 1개의 dyno 가 실행되고 있음을 확인할 수 있다.

다음은 애플리케이션 로그를 확인해보자


%heroku logs –tail


이 명령어는 node.js에서 나오는 로그를 모니터링 할 수 있게 해준다



Figure 11 node.js의 로그를 확인


위의 로그를 보면 03:11:32에 서버가 기동 되었음을 확인할 수 있고

03:11:34초에 http://helloherokuterry.herokuapp.com/  URL이 호출되었음을 확인할 수 있다.

그리고 그 아래 애플리케이션에서 console.log(‘hello heroku’)로 출력한 로그도 같이 출력됨을 확인할 수 있다.


테스트가 끝났으면 실행중인 서버를 내려보자.

서버를 내리는 방법은 ps:scale 명령어를 이용하여 web dyno수를 1에서 0으로 변경해주면 된다

%heroku ps:scale web=0

명령을 실행하고 ps 명령어를 이용해서 현재 기동중인 dyno 수를 확인해보면, 아무 서버도 기동되지 않음을 확인할 수 있다.



Figure 12 동작중인 dyno를 종료하는 화면


로그에서도 아래와 같이 dyno를 끄는 로그를 확인할 수 있다.



Figure 13 dyno 종료 로그를 확인



Promise를 이용한 node.js에서 콜백헬의 처리


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


앞의 글(http://bcho.tistory.com/1083) 에서 async 프레임웍을 이용한 콜백헬을 처리 하는 방법에 대해서 알아보았다.

async 프레임웍 이외에, 콜백헬을 해결할 수 있는 프레임웍으로 promise가 있다.

Promise는 원래 콜백헬을 해결하기 위한 프레임웍이 아니라, 프로그래밍 패턴중의 하나로 지연 응답을 통해서 동시성을 제어 하기 위한 목적으로 만들어졌다. 자바스크립트에서는 JqueryDeferred, CommonJS에 구현되어 있고, ECMAScript5 표준에 포함되서 크롬,파이어폭스,인터넷익스플로러 9 버전등에 포함되어 있다.

구현체가 많아서 설치해야 한다.

node.js는 크롬의 자바스크립트 엔진을 기반으로 하기 때문에, promise가 내장되어 있다.

 

프로미스의 개념

 

asyncfunction이라는 비동기 함수가 있다고 가정하자. 이 함수는 param1,param2를 인자로 받아서 비동기로 처리하는 함수이다. promise 패턴에서는 이 asyncfunction을 호출하면, promise라는 것을 리턴한다. promise란 미래 결과에 대한 약속이다. 그리고 promise의 결과가 성공인지 실패인지에 따라서 이를 핸들링하기 위한 로직을 정의해놓는다. asyncfunction이 처리를 끝내고 결과를 리턴하면 promise에 의해 정의된 로직에 따라 결과값을 처리한다.

약간 말이 복잡한데, 이를 풀어서 설명해보면 다음과 같다.

 

·         프로그램      : asyncfunction에게 “param1param2로 처리해줘라고 부탁한다.

·         asyncfunction : “알았어 처리해줄께, 대신 시간이 걸리니 바로 답은 줄 수 없고, 나중에 답을 줄게. 이게 그 약속(promise)라고 하고, 약속(promise) 객체를 리턴한다.

·         프로그램      : ‘언제 끝날지 모르는 작업이구나그러면 이렇게 해줘. 작업이 성공하면 결과 처리 로직을 실행하게 하고, 만약에 실패하면 에러 처리 로직을 처리하게 하자. 이 내용을 니가 준 약속(promise)에 추가로 적어 넣을께

·         asyncfunction : 실행이 성공적으로 종료되었어. 아 아까 준 약속에 성공시에 처리하는 로직이 정의되어 있군. “결과처리로직를 실행하자

 

이런 내용이 어떻게 코드로 구체화 되는지를 살펴보자

 

var promise = asyncfunction(param1,param2);

promise.then(function(result){

      //결과처리로직

},function(err){

      //에러처리로직

}

 

Figure 1 promise를 이용한 비동기 호출 처리 예제

 

var promise = asyncfunction(param1,param2);

첫번째 코드에서 asyncfunction은 앞서 언급한것과 같이 비동기 함수이다. asyncfunction이 호출되고 나서 결과 값이 아니라, 나중에 결과를 주겠다는 약속(promise) 객체를 리턴한다.

 

다음으로 비동기 함수의 처리가 끝났을때 성공과 실패의 경우 어떻게 처리를 할지를 비동기 함수가 리턴한 약속(promise)에 기술해놓는다.이를 위해서 then이라는 키워드를 사용하는데 다음과 같은 포맷을 사용한다.

 

promise.then(결과처리함수(결과값) ,에러처리 함수(err) )

Figure 2 promise.then의 문법

 

비동기 함수 실행이 끝나면, then에 정의된 첫번째 함수 결과처리함수를 실행하여 비동기 함수 실행 결과를 처리한다.

이때 결과처리함수는 비동기 함수가 처리한 내용에 대한 결과 값을 인자로 갖는다.

만약에 에러가 발생하였을 경우에는 then에 두번째 인자로 정의된 에러처리함수를 실행하여 에러를 처리한다. “에러처리함수는 에러의 내용을 err이라는 인자를 통해서 받는다.

 

앞의 예제에서 2~6줄은 then을 이용하여, 첫번째 인자로 결과처리로직을 가지는 함수를 정의하고, 두번째 인자로에러처리로직을 갖는 함수를 정의했다.

 

promise.then(function(result){

      //결과처리로직

},function(err){

      //에러처리로직

}

Figure 3 프로미스에 결과 및 에러 처리를 지정하는 방법

 

 첫번째 결과처리로직을 갖는 함수는 비동기 함수가 리턴해준 결과값인 “result”를 인자로 받고, 두번째 에러처리로직을 갖는 함수는 에러내용을 “err”라는 인자로 받는다.

 

그렇다면 약속(promise)를 리턴하는 비동기 함수는 어떤 형태로 정의되어야 할까?

promise를 지원하는 비동기 함수는 아래와 같은 형태와 같다. 리턴시에 new Promise를 이용하여 promise 객체를 만들어서 리턴하는데, 이때 두가지 인자를 받는다. resolved reject인데, 성공적으로 실행이 되었으면 이 resolved함수 를 호출하고 이때 인자로 결과값을 넣어서 넘긴다. 반대로 실패했을 경우에는 인자로 받은 rejected 함수를 호출하되 호출 인자로 에러 내용을 담고 있는 err을 넣어서 넘긴다.

 

function asyncfunction(param1,param2){

     

      return new Promise(resolved,rejected){

           if(성공하였는가?){

                 // 성공하였을 경우

                 resolved ("결과");

           }else{

                 rejected(Error(err));

           }

      }

}

Figure 4 프로미스 지원 비동기 함수 정의 방법


프로미스 예제

 

그러면 위의 개념에 따라 실제로 작동하는 코드를 작성하자

 

var Promise = require('promise');

 

var asyncfunction = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       resolved('hello'+param);

                 },2000);

      });

         

}

 

var promise = asyncfunction(' terry ');

promise.then(console.log,console.err); // 여기가 비동기 결과에 대한 콜백함

 

Figure 5 간단한 프로미스 함수 및 사용 예제

 

promise를 사용하기 위해서는 promise 모듈을 require 이용하여 불러들인다.

다음으로 asyncfunction을 정의하고 리턴값으로 Promise객체를 리턴한다. Promise 객체 안에서는 처리할 비지니스 로직이 정의되어 있다. 위의 예제에서는  setTimeout을 이용하여 2초를 기다리도록 하였고, 2초후에 콜백함수에서 resolved 함수를 호출하여 promise를 종료하도록 하였다.

 

다음은 이 promise를 리턴하는 비동기 함수를 실제로 호출하고, 이 비동기 함수에 대해서 성공 및 실패에 대한 처리 함수를 then으로 정의한 부분이다.

var promise = asyncfunction(' terry ');

promise.then(console.log,console.err); // 여기가 비동기 결과에 대한 콜백함


then을 이용하여, 성공시 console.log 함수를 호출하도록 하였고, 실패시에는 console.err를 통해서 에러 메시지를 출력하도록 하였다.

 

프로미스 체이닝 (promise chainning)

여러개의 비동기 함수를 순차적으로 실행하는 방법에 대해서 알아보자.

async 프레임웍의 waterfall과 같은 흐름 제어이다.

다음은 asyncfunction1,2,3를 순차적으로 실행하고, 앞 비동기 함수의 결과를 뒤에 따라오는 비동기 함수의 입력값으로 받아서 처리하는 예제이다.


 

var Promise = require('promise');

 

var asyncfunction1 = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       fullfilled('result 1:'+param);

                 },1000);

      });

}

var asyncfunction2 = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       fullfilled('result 2:'+param);

                 },1000);

      });

}

var asyncfunction3 = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       fullfilled('result 3:'+param);

                 },1000);

      });

}

 

var promise = asyncfunction1(' terry ');

promise

.then(asyncfunction2)

.then(asyncfunction3)

.then(console.log);

 

Figure 6 프로미스 태스크 체이닝 예제

 

promise를 리턴하는 3개의 비동기 함수를 정의하였고, 첫번째 함수로 promise를 만든다음. 실행을 하였다. 다음 then을 이용하여, 다음번에 실행해야하는 비동기 함수 asyncfunction2, asyncfunction3를 순차적으로 정의하였고, 마지막의 최종 결과를 출력하기 위해서 최종 then console.log를 지정하여, 결과값을 출력하도록 하였다.

 

result 3:result 2:result 1: terry

 

하나의 예제를 더 살펴보자

다음 예제는 파일을 읽어서 읽은 내용을 다른 파일에 쓰는 내용이다.

 

 

var Promise = require('promise');

 

var fs = require('fs');

var src = '/tmp/myfile.txt';

var des = '/tmp/myfile_promise2.txt';

 

var fread = Promise.denodeify(fs.readFile);

var fwrite = Promise.denodeify(fs.writeFile);

 

fread(src,'utf-8')

.then(function(text){

           console.log('Read done');

           console.log(text);

           return fwrite(des,text); // 체이닝을 하려면 return 해줘야 .

      })

.then(function(){           

           console.log('Write done');

      })

.catch(function(reason){               

           console.log('Read or Write file error');

           console.log(reason);

});

 

console.log('Promise example');

 

Figure 7 프로미스를 이용해서 파일을 읽어서 다른 파일에 쓰는 예제

 

이 코드에서 먼저 주의 깊게 봐야 하는 부분은 denodeify 부분이다.


var fread = Promise.denodeify(fs.readFile);

var fwrite = Promise.denodeify(fs.writeFile);

 

node.js의 비동기 함수들은 프로미스패턴을 지원하지 않는 경우가 많다. 그래서 프로미스 패턴을 지원하지 않는 일반 함수들을 프로미스를 지원할 수 있는 형태로 변경을 해야 하는데, 이 변경을 해주는 함수가 Promise.denodeify이다.

프로미스화가 끝났으면 이 함수를 프로미스를 사용해서 호출할 수 있다.

 

fread(src,'utf-8')

.then(function(text){

           console.log('Read done');

           console.log(text);

           return fwrite(des,text); // 체이닝을 하려면 return 해줘야 .

      })

 


fread를 수행한 후에, then에서 return시 다음 비동기 함수인 fwrite를 수행한다. 이렇게 하면 task들을 체이닝할 수 있다.


프로미스 에러 핸들링

프로미스에서 에러를 핸들링하는 방법에 대해서 알아보자. 앞의 예제에서 then 중간에 catch라는 구문을 사용했는데, catch가 에러핸들러이다.

아래 코드를 보자 아래 코드는 비동기 함수에서 인위적으로 에러를 발생시켜서 처리 하는 코드이다.


 

var Promise = require('promise');

 

var asyncfunction = function(param){

      return new Promise(function(fullfilled,rejected){

           setTimeout(

                 function(){

                       rejected(Error('this is err '+param));

                 },2000);

      });

         

}

 

asyncfunction(' terry ')

.then(console.log,console.error);

 

asyncfunction('cath')

.then(console.log)

.catch(console.error);

 

Figure 8 프로미스에서 에러처리를 하는 예제

 

asyncfunction내의 프로미스에서 setTimeout으로 2초가 지나면, rejected를 이용하여 에러를 리턴하였다.

첫번째 asyncfunction호출에서는 then에 두개의 인자를 넘겼는데, 두번째 console.error가 에러 핸들러이다. 그래서 에러를 console.error로 출력하게 된다.

두번째 asyncfunction 호출에서는 다른 문법의 에러 핸들링을 사용했는데, then에 두개의 인자를 넘기는 대신, catch를 이용해서 에러 핸들러를 정의하였다.

이 예제를 실행하면 다음과 같은 결과를 얻게 된다.

 

[Error: this is err  terry ]

[Error: this is err cath]

Figure 9 프로미스에서 에러처리를 하는 예제 실행 결과

 

만약에 여러개의 태스크가 연결된 비동기 함수 체인을 호출할때 에러 처리는 어떻게 될까? 아래 코드를 보자.

asyncfunction1,2,3,4,5 가 정의되어 있고, 2 4에서 에러를 발생 시키도록 하였다.

그리고 3번과 5번 뒤에 catch를 넣어서 에러 처리를 하도록 하였는데, 그러면 에러 처리 흐름은 어떻게 될까?


var Promise = require('promise');

 

var asyncfunction1 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func1');

                       resolved('func 1 success:'+param+'\n');

                 },500);

      });

}

var asyncfunction2 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func2');

                       rejected(new Error('func 2 error:'+param+'\n'));

                 },500);

      });

}

var asyncfunction3 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func3');

                       resolved('func 3 success:'+param+'\n');

                 },500);

      });

}

var asyncfunction4 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func4');

                       rejected(Error('func 4 error:'+param+'\n'));

                 },500);

      });

}

var asyncfunction5 = function(param){

      return new Promise(function(resolved,rejected){

           setTimeout(

                 function(){

                       console.log('func5');

                       resolved('func 5 success:'+param+'\n');

                 },500);

      });

}

 

var promise = asyncfunction1(' terry ');

promise

.then(asyncfunction2)

.then(asyncfunction3)

.catch(console.error) // errorhandler1

.then(asyncfunction4)

.then(asyncfunction5)

.catch(console.error)  // errorhandler2

.then(console.log);

 

 

Figure 10 프로미스 태스크 체인에서 에러 처리를 하는 예제

 

3,5번 뒤에 붙은 catch는 어느 비동기 함수들의 에러를 처리할까? 다음 그림을 보자


Figure 11 프로미스 태스크 체인에서 에러 처리를 하는 예제의 에러 처리 흐름

 

1,2,3 번 뒤에 catch를 정의 했기 때문에, 1,2,3번을 수행하던중 에러가 발생하면 수행을 멈추고 첫번째 에러핸들러인 //errorhandler1으로 가서 에러를 처리한다. 여기서 중요한 점은 에러처리 후에, 다시 원래 제어 흐름으로 복귀한다는 것이다. 흐름을 끝내지 않고, 다음 에러핸들러에 의해서 통제 되는 4,5번을 수행한다. 4,5번의 에러는 4,5번 호출 뒤에 붙어 있는 catch //errorhandler2에 의해서 처리 된다. 마찬가지로 //errorhandler2에 의해서 실행이 된후에 맨 마지막 비동기 함수인 console.log를 실행하게 된다.

 

앞에서 2,4번에 에러를 냈으니 실제 흐름이 어떻게 되는지 확인해보자



Figure 12 프로미스 태스크 체인에서 에러 처리를 하는 예제의 실제 수행 흐름


asyncfunction 1,2가 실행되고 에러를 만나서 첫번째 catch에 의해서 에러 처리가 되고, 에러 처리 후 4번이 실행된후 에러를 만나서 두번째 catch가 실행이 되고 마지막에 정의된 console.log가 실행이 된다.

이 흐름을 그림으로 표현해보면 다음과 같다.

 

실행 결과는 다음과 같다.

 

func1

func2

[Error: func 2 error:func 1 success: terry

 

]

func4

[Error: func 4 error:undefined

]

undefined

 

Figure 13 프로미스 태스크 체인에서 에러 처리를 하는 예제를 실행한 결과 


프로미스 지원 프레임웍

지금까지 promise 모듈을 이용하여 promise 패턴을 이용한 비동기 패턴 처리를 알아보았다. 앞에서 살펴보았듯이 쉽게 콜백헬을 해결할 수 있다. async waterfall 흐름 제어와 동일한 흐름 제어 부분만 살펴보았지만, promise 역시, asyncseries, parallel등과 같은 다양한 흐름 제어 알고리즘을 지원한다.

 이 글에서는 promise 모듈을 사용하였지만, 이 프로미스 패턴을 지원하는 모듈은 이외에도 Q (https://github.com/kriskowal/q) , bluebird (http://bluebirdjs.com/docs/getting-started.html) 등 다양한 프레임웍이 있다.

근래에는 성능이나 기능 확장성이 좋은 bluebird가 많이 사용되고 있으니, 실제 운영 코드를 작성하기 위해서는 다른 프로미스 프레임웍도 검토하기 바란다.

 

참고

https://davidwalsh.name/promises

https://github.com/stackp/promisejs 예제가 좋음

http://programmingsummaries.tistory.com/325 정리가 잘되어 있음 추천.

monk 모듈을 이용한 mongoDB 연결


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


mongoDB 기반의 개발을 하기 위해서 mongoDB를 설치한다. https://www.mongodb.org/ 에서 OS에 맞는 설치 파일을 다운로드 받아서 설치한다.

설치가 된 디렉토리에 들어가서 설치디렉토리 아래 ‘./data’ 라는 디렉토리를 만든다. 이 디렉토리는 mongoDB의 데이타가 저장될 디렉토리이다.

 

mongoDB를 구동해보자.

% ./bin/mongod --dbpath ./data



Figure 1 mongoDB 구동화면


구동이 끝났으면 mongoDB에 접속할 클라이언트가 필요하다. DB에 접속해서 데이타를 보고 쿼리를 수행할 수 있는 클라이언트가 필요한데, 여러 도구가 있지만 많이 사용되는 도구로는 roboMongo라는 클라이언트가 있다.

https://robomongo.org/download 에서 다운로드 받을 수 있다. OS에 맞는 설치 파일을 다운로드 받아서 설치 후 실행한다.

 

설치 후에, Create Connection에서, 로컬호스트에 설치된 mongoDB를 연결하기 위해서 연결 정보를 기술하고, 연결을 만든다





Figure 2 robomongo에서 localhost에 있는 mongodb 연결 추가

 

주소는 localhost, 포트는 디폴트 포트로 27017를 넣으면 된다.

 

환경이 준비가 되었으면 간단한 테스트를 해보자. 테스트 전에 기본적인 개념을 숙지할 필요가 있는데, mongoDBNoSQL 계열중에서도 도큐먼트DB (Document DB)에 속한다. 기존 RDBMS에서 하나의 행이 데이타를 표현했다면, mogoDB는 하나의 JSON 파일이 하나의 데이타를 표현한다. JSON을 도큐먼트라고 하기 때문에, 도큐먼트 DB라고 한다.

 

제일 상위 개념은 DB의 개념에 대해서 알아보자, DB는 여러개의 테이블(컬렉션)을 저장하는 단위이다.

Robomongo에서 mydb라는 이름으로 DB 를 생성해보자



Figure 3 robomongo에서 새로운  DB를 추가 하는 화면

 

다음으로 생성된 DB안에, 컬렉션을 생성한다. 컬렉션은 RDBMS의 단일 테이블과 같은 개념이다.

Robomongo에서 다음과 같이 ‘users’라는 이름의 컬렉션을 생성한다



Figure 4 robomongo에서 컬렉션(Collection) 생성

 

users 컬렉션에는 userid를 키로 해서, sex(성별), city(도시) 명을 입력할 예정인데, userid가 키이기 때문에, userid를 통한 검색이나 소팅등이 발생한다. 그래서 userid를 인덱스로 지정한다.

인덱스 지정 방법은 createIndex 명령을 이용한다. 다음과 같이 robomongo에서 createIndex 명령을 이용하여 인덱스를 생성한다.



Figure 5 users 컬렉션에서 userid를 인덱스로 지정

 

mongoDB는 디폴트로, 각 컬렉션마다 “_id”라는 필드를 가지고 있다. 이 필드는 컬렉션 안의 데이타에 대한 키 값인데, 12 바이트의 문자열로 이루어져 있고 ObjectId라는 포맷으로 시간-머신이름,프로세스ID,증가값형태로 이루어지는 것이 일반적이다.

_id 필드에 userid를 저장하지 않고 별도로 인덱스를 만들어가면서 까지 userid 필드를 별도로 사용하는 것은 mongoDBNoSQL의 특성상 여러개의 머신에 데이타를 나눠서 저장한다. 그래서 데이타가 여러 머신에 골고루 분산되는 것이 중요한데, 애플리케이션상의 특정 의미를 가지고 있는 필드를 사용하게 되면 데이타가 특정 머신에 쏠리는 현상이 발생할 수 있다.

예를 들어서, 주민번호를 _id로 사용했다면, 데이타가 골고루 분산될것 같지만, 해당 서비스가 10~20대에만 인기있는 서비스라면, 10~20대 데이타를 저장하는 머신에만 데이타가 몰리게 되고, 10세이하나, 20세 이상의 데이타를 저장하는 노드에는 데이타가 적게 저장된다.

이런 이유등으로 mongoDB를 지원하는 node.js 드라이버에서는 _id 값을 사용할때, 앞에서 언급한 ObjectId 포맷을 따르지 않으면 에러를 내도록 설계되어 있다. 우리가 앞으로 살펴볼 mongoosemonk의 경우에도 마찬가지이다.

 

이제 데이타를 집어넣기 위한 테이블(컬렉션) 생성이 완료되었다.

다음 컬렉션 에 대한 CRUD (Create, Read, Update, Delete) 를 알아보자

SQL 문장과 비교하여, mongoDB에서 CRUD 에 대해서 알아보면 다음과 같다.

CRUD

SQL

MongoDB

Create

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

db.users.insert({userid:"terry",city:"seoul"})

Read

select * from users where id="terry"

db.users.find({userid:"terry"})

Update

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

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

Delete

delete from users where _id="terry"

db.users.remove({userid:"terry"})

Figure 6 SQL문장과 mongoDB 쿼리 문장 비교


mongoDB에서 쿼리는 위와 같이 db.{Collection }.{명령어} 형태로 정의된다.

roboMongo에서 insert 쿼리를 수행하여 데이타를 삽입해보자



Figure 7 mongoDB에서 users 컬렉션에 데이타 추가

 

다음으로 삽입한 데이타를 find 명령을 이용해 조회해보자



Figure 8 mongoDB에서 추가된 데이타에 대한 확인

 

mongoDB에 대한 구조나 자세한 사용 방법에 대해서는 여기서는 설명하지 않는다.

http://www.tutorialspoint.com/mongodb/ mongoDB에 대한 전체적인 개념과 주요 쿼리들이 간략하게 설명되어 있으니 이 문서를 참고하거나, 자세한 내용은 https://docs.mongodb.org/manual/ 를 참고하기 바란다.

https://university.mongodb.com/ 에 가면 mongodb.com에서 운영하는 온라인 강의를 들을 수 있다. (무료인 과정도 있으니 필요하면 참고하기 바란다.)

 

mongoDBnode.js에서 호출하는 방법은 여러가지가 있으나 대표적인 두가지를 소개한다.

첫번째 방식은 mongoDB 드라이버를 이용하여 직접 mongoDB 쿼리를 사용하는 방식이고, 두번째 방식은 ODM (Object Document Mapper)를 이용하는 방식이다. ODM 방식은 자바나 다른 프로그래밍 언어의 ORM (Object Relational Mapping)과 유사하게 직접 쿼리를 사용하는 것이 아니라 맵퍼를 이용하여 프로그램상의 객체를 데이타와 맵핑 시키는 방식이다. 뒷부분에서 직접 코드를 보면 이해가 빠를 것이다.

 

Monk를 이용한 연결

첫번째로 mongoDB 네이티브 쿼리를 수행하는 방법에 대해서 소개한다. monk라는 node.jsmongoDB 클라이언트를 이용할 것이다.

monk 모듈을 이용하기 위해서 아래와 같이 package.jsonmonk에 대한 의존성을 추가한다.


{

  "name": "mongoDBexpress",

  "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",

    "monk":"~1.0.1"

  }

}

 

Figure 9 monk 모듈에 대한 의존성이 추가된 package.json

 

app.js에서 express가 기동할때, monk를 이용해서 mongoDB에 연결하도록 한다.

var monk = require('monk');

var db = monk('mongodb://localhost:27017/mydb');

 

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

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

    req.db = db;

    next();

});

app.use('/', mongo);

Figure 10 monk를 이용하여 app.js에서 mongoDB 연결하기

 

mongoDB에 연결하기 위한 연결 문자열은 'mongodb://localhost:27017/mydb' mongo://{mongoDB 주소}:{mongoDB 포트}/{연결하고자 하는 DB} 으로 이 예제에서는 mongoDB 연결을 간단하게 IP,포트,DB명만 사용했지만, 여러개의 인스턴스가 클러스터링 되어 있을 경우, 여러 mongoDB로 연결을 할 수 있는 설정이나, Connection Pool과 같은 설정, SSL과 같은 보안 설정등 부가적인 설정이 많으니, 반드시 운영환경에 맞는 설정으로 변경하기를 바란다. 설정 방법은 http://mongodb.github.io/node-mongodb-native/2.1/reference/connecting/connection-settings/ 문서를 참고하자.

 

이때 주의깊게 살펴봐야 하는 부분이 app.use를 이용해서 미들웨어를 추가하였는데, req.dbmongodb 연결을 넘기는 것을 볼 수 있다. 미들웨어로 추가가 되었기 때문에 매번 HTTP 요청이 올때 마다 req 객체에는 db라는 변수로 mongodb 연결을 저장해서 넘기게 되는데, 이는 HTTP 요청을 처리하는 것이 router에서 처리하는 것이 일반적이기 때문에, routerdb 연결을 넘기기 위함이다. 아래 데이타를 삽입하는 라우터 코드를 보자

 

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

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

     

      db = req.db;

      db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc){

             if(err){

                console.log(err);

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

                return;

             }

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

            

         });

});

Figure 11 /routes/mongo.js 에서 데이타를 삽입하는 코드


req 객체에서 폼 필드를 읽어서 userid,sex,city등을 읽어내고, 앞의 app.js 에서 추가한 미들웨어에서 넘겨준 db 객체를 받아서 db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc) 수행하여 데이타를 insert 하였다.

 

다음은 userid필드가 HTTP 폼에서 넘어오는 userid 일치하는 레코드를 지우는 코드 예제이다. Insert 부분과 크게 다르지 않고 remove 함수를 이용하여 삭제 하였다.


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

      var userid = req.body.userid;

     

      db = req.db;

      db.get('users').remove({'userid':userid},function(err,doc){

             if(err){

                console.log(err);

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

                return;

             }

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

            

         });

});

Figure 12 /routes/mongo.js 에서 데이타를 삭제하는 코드

 

다음은 데이타를 수정하는 부분이다. Update 함수를 이용하여 데이타를 수정하는데,

db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

와 같이 ‘userid’userid 인 필드의 데이타를 },{'userid':userid,'sex':sex,'city':city} 대치한다.

 

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

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

      db = req.db;

      db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

      //db.get('users').update({'userid':userid},{$set:{'sex':'BUSAN'}},function(err,doc){

             if(err){

                console.log(err);

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

                return;

             }

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

            

         });

});

Figure 13 /routes/mongo.js 에서 데이타를 수정하는 코드


전체 레코드를 대치하는게 아니라 특정 필드만 수정하고자 하면, $set: 쿼리를 이용하여, 수정하고자하는 필드만 아래와 같이 수정할 수 있다.

db.collection('users').updateOne({_id:userid},{$set:{'sex':'BUSAN'}},function(err,doc){

 

마지막으로 데이타를 조회하는 부분이다. /list URL은 전체 리스트를 리턴하는 코드이고, /get ?userid= 쿼리 스트링으로 정의되는 사용자 ID에 대한 레코드만을 조회해서 리턴한다.

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

      db = req.db;

      db.get('users').find({},function(err,doc){

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

           res.send(doc);

      });

});

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

      db = req.db;

      var userid = req.query.userid

      db.get('users').findOne({'userid':userid},function(err,doc){

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

           res.send(doc);

      });

});

Figure 14 /routes/mongo.js 에서 데이타를 조회하는 코드

 

이제 /routes/mongo.js 의 모든 코드 작업이 완료되었다. 이 코드를 호출하기 위한 HTML 폼을 작성하자.

 

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

 

<h1> Native MongoDB 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>

Figure 15 /public/monksample.html

 

node.js를 실행하고 http://localhost:3000/monksample.html 을 실행해보자



Figure 16 http://localhost:3000/monksample.html 실행 결과

 

아래 insert 버튼을 누르면, 채워진 필드로 새로운 레코드를 생성하고, update 버튼은 user 필드에 있는 사용자 이름으로된 데이타를 업데이트 한다. list 버튼은 컬렉션에서 전체 데이타를 조회해서 출력하고, delete 버튼은 user 필드에 있는 사용자 이름으로된 레코드를 삭제한다. get 버튼은 user 필드에 있는 사용자 이름으로 데이타를 조회하여 리턴한다.

다음은 list로 전체 데이타를 조회하는 화면이다.

 


Figure 17 /list를 수행하여 mongoDB에 저장된 전체 데이타를 조회하는 화면


이 코드의 전체 소스코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongoDBexpress 에 있으니 필요하면 참고하기 바란다


다음 글에서는  node.js의 mongoDB ODM 프레임웍인 mongoose 이용한 접근 방법에 대해서 알아보기로 한다.