oauth2-restapi-server: 하이브리드 앱(Hybrid App)으로 다양한 플랫폼 빠르게 지원하기

지난 포스팅에서 소개했던 oauth2-restapi-server 의 샘플 앱은 브라우저 상에서 url 로 실행가능한 앱이다. 이번 포스팅에서는 동일한 기능을 하지만 브라우저 없이 독립적으로 실행가능한 oauth2-restapi-server 의 하이브리드 샘플 앱을 소개하려고 한다.

뭐, 하이브리드 앱 (Hybrid App) ?


모바일 단말 (스마트폰, 태블릿 등)의 네이티브 앱은 모바일 플랫폼 별로 다른 언어로 개발되는데 대표적으로 iOS 경우 Objective-c 로, Android 경우 Java 로 만들어진다. 그리고 모바일 앱으로 서비스를 런칭하기 위해 iOS, Android 용 둘 다 개발하는 것이 일반적이다. 이 경우 똑같은 기능을 하는 앱을 몇가지 언어로 만들어야 하니 최초 개발, 확장 및 유지보수를 생각했을 때 회사 그리고 개발자들에겐 참으로 번거로운 일이 아닐 수 없다. 하나의 언어로 앱을 개발하여 여러 모바일 플랫폼에서 사용할 수 있게 할 수는 없을까.  ‘크로스 플랫폼’을 이용하면 가능하다. 컴파일을 해야하는 네이티브 언어 쪽의 대표적인 ‘크로스 플랫폼’ 은 Xamarin (C# 으로 앱개발) 이고, 웹 언어 쪽에선 Cordova (Phonegap 의 오픈소스 버전) 이다. 여기에선 웹에 초점을 맞추고 있기 때문에 Cordova 만 간략히 언급하도록 하겠다. Cordova 의 공식 사이트에선 다음과 같이 Cordova 를 정의하고 있다.

Apache Cordova is a platform for building native mobile applications using html, css, and javascript

즉, Cordova 로 만든 앱의 컨텐츠는 html, css, javascript  같은 웹 언어로 만들어진다. 모바일 플랫폼 별로 웹언어를 실행해주기 위한 네이티브 레벨의 WebView 컴포넌트들이 제공되고 있다. iOS는 UIWebView 클래스를, Android는 WebView 클래스를 제공하고 있다. 네이티브 레벨에서 이들 컴포넌트를 이용해서 html, css, javascript 로 구성된 컨텐츠를 화면에 표시할 수 있다. Cordova 의 경우 이러한 WebView 컴포넌트를 포함하는 네이티브 소스 코드를 자동으로 생성해주고, 패키징해준다. 그래서 Cordova를 native wrapper 라고 부르기도 한다. Cordova 앱 개발자는 네이티브 레벨은 생각할 필요가 없고, 단지 WebView 컴포넌트 위에서 로딩 및 렌더링될 html, css, javascript 컨텐츠를 만들기만 하면 된다. 또Cordova 는 native wrapper 역할 뿐만 아니라 카메라, 센서 등 디바이스의 자원에 접근하기 위한 javascript 형태의 web api 를 제공한다. 이 web api 은  iOS, Android, window phone 등 지원되는 모바일 플랫폼 별로 다르지 않기 때문에 Cordova app 을 하나 만들면 변경없이 여러 모바일 플랫폼에 사용할 수 있다. 이처럼 Cordova 로 만든 앱은 컨텐츠는 웹으로, 패키징/설치/실행은 네이티브로 하고 있기 때문에 하이브리드 (hybrid) 앱이라 부른다. 내가 생각하기에 하이브리드의 앱의 단점으로는 실행 성능이 순수 네이티브 앱에 비해 느리다는 점이다. 왜냐하면 iOS, Android 에서의 하이브리드 앱은  순수 네이티브 앱과 다르게 Webkit 웹엔진으로 렌더링 및 플랫폼 리소스 접근하고, Javascript 엔진으로 Javascript 코드 해석하고 실행하는 등 추가적인 작업을 하기 떄문이다.  또 다른 단점으로는 네이티브 레벨에서 지원되는 tab bar, navigation bar 같은 UX 관련 컴포넌트들을 사용할 수 없다는 점도 있겠다.  하이브리드 앱으로 서비스를 하던 링크드인페이스북이 순수 네이티브 앱으로 바꾼 이유도 방금 언급한 두가지 단점과 관련이 있다. 그렇다고 하이브리드 앱을 쓰지 않을 건가. 시간이 흐르면서 이러한 단점들은 사라지지 않을까 생각한다. 이유는 속도 측면에선 하드웨어 성능과 웹엔진과 javascript 엔진 자체의 성능이 꾸준히 높아지고 있고, UX 측면에선 네이티브 UX 컴포넌트를 html, css, javascript 로 구현하여 cordova 와 integration 시킨 오픈소스들이 생겨나고 있기 때문이다 (으흑, 정말 고마울 따름 흑흑;;). 대표적인 것으로는 ioniconsen-ui 가 있다. 네이티브 앱을 쓸건가 하이브리드 앱을 쓸건가는 서비스의 성격과 회사/개발자가 처한 상황에 맞게 선택해야 할 부분이지만 나처럼 1인 개발을 하면서 아이디어를 구현하여 빨리 시장에 내놓으려고 한다면 하이브리드 앱이 적절하지 않을까 생각한다.

브라우저 기반 앱을 하이브리드 앱으로!


지난 포스팅에서 소개했던 oauth2-restapi-server 의 브라우저 기반 샘플 앱을 cordova 를 사용해 하이브리드 앱으로 변경한 과정을 설명하고자 한다. 기존의 브라우저 기반 샘플 앱의 소스 코드는 서버에 위치해 있지만, UI와 로직의 실제 실행은 서버가 아닌 서버에 접속한 디바이스에서 이루어진다. 이것은 중요한데 이유는 하이브리드 앱을 만들기 위해서 앱 소스코드를 서버가 아닌 로컬로 가져오더라도 oauth2-restapi-server 는 전혀 변경할 필요가 없기 때문이다. RESTful API로 서버와 데이터를 주고 받을 수 있도록 미리 샘플 앱을 만들어놓은 덕분이다. 브라우저 기반 샘플 앱을 하이브리드 앱으로 변환하기 위해서 다음과 같은 작업이 필요하다.

  • cordova 설치
  • cordova 프로젝트 생성
  • cordova plugin인 InAppBrowser 설치
  • server 상에 있던 app 소스를 “<project>/www/” 으로 복사
  • bower.json 및 .bowerrc 를 “<project>/” 에 생성
  • app 소스에 cordova.js import 및 초기화
  • rest api 의 url 을 절대경로로 바꾸기
  • ‘InAppBrowser’를 이용해 access token 받아오기

각 작업에 대해 하나씩 아래에서 설명하도록 하겠다.

cordova 설치, cordova project 생성, cordova plugin 설치


cordova 는 node.js 로 만들어져 있으며, 아래와 같은 순서로 진행하였다.

$ sudo npm install -g cordova
$ cordova create hybrid com.example.hybrid MyHybrid
$ cd hybrid
$ cordova platform add ios
$ cordova plugin add org.apache.cordova.inappbrowser

cordova 커맨드라인 명령어 대한 자세한 설명은 아래 링크를 참고 바란다.

http://cordova.apache.org/docs/en/3.5.0/guide_cli_index.md.html

bower, ‘cordova.js’  import, rest api 절대경로로 변경


브라우저 기반 샘플 앱은 angular.js, bootstrap 등 외부 javascript, css 라이브러리를 bower 툴을 이용해 하위 디렉토리에 다운로드 받아서 사용했었는데, 로딩 성능을 위해서 하이브리드 앱에서도 bower 를 이용해 미리 로컬에 그런 라이브러리들을 다운로드하여 앱에 패키징되게 하였다.  bower 를 사용하기 위해서 두개의 파일 bower.json 과 .bowerrc 를 만든다. bower.json 에는 다운로드하려는 javascript, css 와 버전을 지정하고, .bowerrc 에는 다운로드될 위치를 지정한다. 그리고  아래의 명령어를 실행하면  bower.json 에 지정된 라이브러리들이 지정된 위치에 다운로드 된다.

$ bower install

그리고 변환된 하이브리드 앱이 cordova의 device api를 사용하지는 않지만, cordova의 plugin인 InAppBrowser 를 사용하기 때문에 cordova 관련 javascript 오브젝트들을 생성하여 초기화하는 cordova.js 파일을 하이브리드 앱의 시작페이지인 index.html 안에서 import 하였다. 다른 html 에는 cordova.js 를 import 하지 않았는데 이유는 샘플 앱이 Single page app (SPA)이기 때문이다. 아래처럼 하이브리드 앱의 시작 페이지에서 한번 cordova.js 를 로딩하는것으로 충분하다. [gist f597ddf76e59f169f62d] 그리고 angular.js 가 초기화되면서 cordova의 device api 를 호출하는 등 cordova 관련 작업을 수행할 수도 있기 때문에 하이브리드 샘플의 angular.js 의 초기화는 cordova 초기화가 완료되어 cordova 오브젝트를 사용할 수 있는 시점인 ‘deviceready’ 이벤트 발생 이후로 하였다. 만약 하이브리드 앱이 시작될 때 cordova 의 오브젝트를 전혀 사용하지 않는다면, <html ng-app=’myApp’> 또는 <body ng-app=’myApp> 처럼 시작페이지가 로딩될 때 자동으로 angular를 초기화하도록 해도 된다. 아래 angular.bootstrap() 은 명시적으로 angular 를 초기화하도록 요청하는 함수이다. [gist 55d93626673a022a9454] 그리고 서버 상에 앱 소스가 존재할 경우 oauth2-restapi-server 와 동일한 위치에 있었기 때문에, 앱 소스에서 angular.js의 $resource 에 상대 경로 url을 넣어줘도 AJAX 요청이 성공했다. 하지만 하이브리드 앱의 경우 $resource 를 수행하는 파일이 로컬에 있기 때문에 oauth2-restapi-server의 절대경로 넣어주지 않으면 안된다. 아래 prefix 변수에 할당된 url 은 내가 띄워놓은 oauth2-restapi-server 의 url 이다. [gist 864891177879dccb68c3]

InAppBrowser 로 타사 소셜 앱의 access token 받아오기


타사 소셜앱 계정으로 샘플 앱에 signup 또는 login 하기 위해서는 샘플 앱이 타사 소셜앱 서버가 발행해준 access token 을 받아와야 한다 (소셜 앱 서버가 발행한 access token 을  oauth2-restapi-server 와 샘플 앱이 어떻게 활용하는지에 대해서는 이전 포스팅의 내용을 참고바란다). 브라우저 기반의 앱으로 동작할 땐 아래처럼 same origin 관계를 활용하여 새롭게 열린 탭 또는 윈도우에서 window.opener.authState() 함수를 호출하면서 샘플 앱에게 타사 소셜앱 서버가 발행해준 access token 을 넘겨주었다. 아래는 oauth2-restapi-server 가 타사 소셜앱과 access token 교환을 마친후 샘플 앱의 열려있는 탭 또는 윈도우에 보내는 html 내용이다. ouath2-restapi-server 는 이 파일을 보내기 전에'<%= %>’ 로 된 부분에 적절한 값을 미리 넣는다. 아래 data.token 에는 실제로 타사 소셜앱 서버로부터 받은 access token 이 들어가 있다. [gist 06ea8386432e760b6961] 그러나 하이브리드 앱에선 서버의 페이지 내에서 window.opener 로 값을 전달하는 위의 솔루션이 동작 하지 않는다. 이유는 same origin 이 아니기 때문에 Webkit 엔진에서 서로의 window 오브젝트간 DOM 접근을 하지 못하게 막는다. 앱이 띄운 새 윈도우(또는 탭) 에 로딩된 서버 페이지의 origin은 나의 경우 https://ec2-54-199-141-31.ap-northeast-1.compute.amazonaws.com:3443 이다. 그리고 window.opener 에 해당하는 앱 자체의 origin은 없다 (원래 로컬 파일은 origin 이 없다). 그럼 어떻게 하이브리드 앱이 타사 소셜 앱의 access token 을 받게 할 수 있을까. cordova의 plugin인 InAppBrowser 를 이용하면 가능하다. 기본 아이디어는 다음과 같다.

  1. window.open()의 두번째 인자에 ‘_blank’ 를 지정해서 새로운 윈도우를 띄운다
    • 이 새 윈도우에는 타사 소셜앱 서버의 공식 authorization page가 처음 로딩되고,
    • oauth2-restapi-server 의callback url (사전에 해당 소셜앱 서버에 등록된 url) 에 code 및 access token 값이 붙어서 리다이렉션 된다.
    • 그 다음, oauth2-restapi-server 는 타사 access token 값을 포함하는 html 을 하이브리드 앱이 띄워놓은 이 윈도우에게 전달한다.
  2. 하이브리드 앱은 oauth2-restapi-server 가 보내는 html (with access token) 의 url 을 모니터링한다.
    • <child window>.addEventListener(‘loadstop’, callback) 를 이용해 페이지 로딩이 완료후 callback 함수에서 특정 url 체크를 한다
    • url 이 매치된다면, callback 함수 내에서 <child window>.executeScript(code, callback) 를 이용해 새 윈도우 페이지 내 정의된 특정 함수를 호출한다.
    • 새 윈도우 페이지 내 해당 함수가 정상적으로 호출되면 리턴값으로써 유효한 값이 들어있는 type, data 변수들을 보낸다.
    • 정상적으로 <child window>.executeScript(code, callback)가 수행되었다면 지정된 callback함수가 호출되는데 callback함수 안에서 명시적으로 <child  window>.close() 를 호출하여 열린 윈도우를 닫는다.

위의 아이디어를 구현하기 위해서 먼저 기존에 서버가 보내던 html 에 getResult() 라는 함수를 추가했다. 당연히 아래 소스는 oauth2-restapi-server 소스 디렉토리에 위치한다. 이 함수는 executeScript의 첫번째 인자 값으로 사용된다. [gist 190a648f0830a6348ff9] 아래 소스는 angular.js 에 등록한 service 들이 정의된 소스파일이다. 재사용을 위해서 InAppBrowser를 다루는 부분을 하나의 service 로 구현했다. ref 변수는 window.open 으로 생성된 child window object 이다. 여기서 하나 주의해야 할 것은, 이 child window object 가 일반적인 window object 가 아니라는 것이다. window.open 하는 순간 InAppBrowser 플러그인이 특수한 이벤트와 함수들을 window object 에 붙이고, 반대로 몇몇 표준 함수들 및 변수들은 제거한다. InAppBrowser 플러그인을 설치했다면, 새롭게 window.open으로 리턴된 child window object 를 다룰땐 InAppBrowser  플러그인이 제공하는 기능들만이 사용하는 것이 좋을 것 같다.  getResult() 함수는 아래  ref.executeScript()의 첫번째 인자로 들어간다. ‘loadstop’ 이벤트 발생시 체크하는 url 은 /auth/login/<social service name>/callback/success과 /auth/login/<social service name>/callback/failure 두 가지 이다. 한가지 주의해야 할 점은, 실제로 위의 과정을 통해 타사 소셜 앱의 access token 갖게 되는 executeScript()에 지정된 콜백 함수가 angular.js 컨텍스트 밖에서 실행되고 있다는 것이다. 이 때문에 해당 콜백 함수에서 $rootScope 이나 $scope 의 자식 오브젝트 값을 변경하거나 함수를 호출하려는 경우에는 $apply() 를 이용해서 명시적으로 angular.js 컨텍스트 들어가서 $rootScope 이나 $scope 에 접근하게 해야 한다. 하이브리드 샘플 앱의 경우 executeScipt() 에 지정된 콜백 함수 내에서 $rootScope 의 커스텀 이벤트를 발생시켜야 해서 ‘$rootScope.$apply(function () { … }) ‘ 같이 $apply 사용하게 했다. [gist e66e97dcea9aa5b9e472] 사용자가 소셜앱 계정으로 로그인을 하려거나 로그인 이후 다른 소셜앱 계정을 연결(connect) 하려고 할 때 각각의 angular controller 에서 아래 $scope 함수들이 호출된다. 두 함수 모두 하는 일은 동일하며 window.open 의 첫 인자에 넣어주는 url 만 다르다. url 을 다르게 하는 이유는 oauth2-restapi-server 가 이 url 을 보고 로그인을 위한 것인지 연결을 위한 것인지 구분하도록 하기 위함이다. [gist 0a5fe47ba846101ed953] 아래는 iOS 플랫폼으로 동작하는 디바이스인 iPhone 에서 하이브리드 앱을 실행하여 타사 소셜앱 계정으로 로그인 수행 전/후를 캡처한 스크린 샷이다. 앱의 기능이나 UI 측면에선 지난 포스팅에서 소개한 브라우저 기반 샘플 앱과 다른 점이 없다. 이렇게 cordova 로 만든 이 하이브리드 앱은 Android 및 Window Phone 에서도 그대로 재사용될 수 있다.

하이브리드 샘플 앱 소스 코드


변경한 하이브리드 샘플 앱 소스코드는 아래 github 에서 확인할 수 있다.

 

마무리하며..


이번 포스팅에선 하이브리드 앱에 대해 설명하고, oauth2-restapi-server 와 함께 동작하는 브라우저 기반 앱을 cordova를 사용해 하이브리드 앱으로 변환하기 위해서 필요한 작업들을 살펴보았다. 이미 살펴본바대로 앱 소스를 서버에서 로컬로 가져올 때 발생하는 문제들만 해결해준다면, 하이브리드 앱에서는 기본적으로 브라우저 상에서 동작하던 html, js, css 를 그대로 사용할 수 있다.  oauth 인증과 access token 기반으로 동작하는 하이브리드 앱을 준비하는 개발자들에게 작은 도움이 되었으면 하는 바램으로 글을 마무리 한다.

Tagged with: , , , , , , , , , , , ,
Posted in Technical Note

oauth2-restapi-server: 모바일 앱을 OAuth2.0으로 좀 더 안전하게!

개발중인 앱을 좀 더 안전하게 서비스하기 위해서 만든 백엔드 서버 프로토타입인 oauth2-restapi-server을 공유하고자 한다. oauth2-restapi-server 은 모두 node.js 위에서 동작하며 OAuth2.0 스펙대로 동작하는 Authorization server와 Authorization server가 발행한 access token으로 리소스 접근을 관리하는 매우 기본적인 RESTful API server로 이루어져 있다.

https://github.com/vinebrancho/oauth2-restapi-server

 당연한 얘기지만, 앱이 사용자의 개인정보와 리소스를 안전하게 보호되는 것을 보장해주지 않는다면 그 앱은 사용자로부터 외면받게 될 것이다. 그래서 앱 개발 시 사용자 인증(Authentication)과 리소스에 대한 권한부여(Authorization)는 반드시 필요하고 제공되어야 한다. 앱 개발시 인증과 권한 부여하는 방법에 관해 OAuth, OpenID, SAML 등 몇가지 스펙이 있는데, 이중에서 가장 많이 사용되고 twitter, facebook, google등 메이저급 업체들이 사용하는 스펙이 OAuth 이다. OAuth는 현재 2.0 까지 나왔으며, 최종 버전(final version)은 RFC 6749 (http://tools.ietf.org/html/rfc6749) 이다. OAuth 2.0은 인증 및 권한부여에 관한 기본적인 흐름(flow)뿐만 아니라 implementor가 자기의 목적에 맞게 확장할 수 있도록 해준다. 이 때문에 OAuth 2.0은 프로토콜인 동시에 프레임워크라고 부르기도 한다. OAuth 2.0 으로 서버와 클라이언트 간 인증을 마치면 서버는 권한부여의 결과로써 access token을 발행해주게 되는데, 클라이언트는 이 access token을 이용해서 서버로 리소스 접근을 요청할 수 있다. 서버는 access token 기반으로 리소스 접근을 허용할지 말지를 결정하고, 결과 데이터를 클라이언트에게 보내줄 수 있다. 서버는 access token 기반으로 클라이언트를 인식하고 서비스하기 때문에, 세션(session)이나 쿠키(cookie)를 이용해 클라이언트의 상태정보를 유지할 필요가 없다. 이것은 REST(Representational State Transfer) 디자인 컨셉의 주요한 요구사항인 ‘stateless’ 특성을 만족시켜준다. 그렇다. OAuth2.0의 access token을 이용해서 서버는 RESTful API를 클라이언트에게 제공할 수 있다. 실제로 OAuth2.0 을 제공하는twitter, facebook, google, yahoo, linkedin등 메이저급 앱의 서버들은 access token 기반의 RESTful API를 제공하고 있다. OAuth2.0에 대한 내용은 스펙문서 자체를 참고하길 바라며, 지금부터는 구체적으로 아래 내용들에 대해 살펴보고자 한다.

  • 앱스토어에 올릴 1st party client 에 OAuth2.0의 어떤 권한부여 타입을 적용할 것인가?
  • 인증 및 권한부여를 하는 authorization server는 어떻게 구현 하지?
  • 발행된 access token 기반으로 동작하는 RESTful API server는 어떻게 구현 하지?
  • oauth2-restapi-server 소스코드

잠깐, 문득 용어 정리가 필요할 듯 하다. 관점에 따라 앱은 다음과 같이 구성된다.

  • 사용자 관점에서 프론트엔드(front-end)와 백엔드(back-end)
  • 시스템 관점에서 클라이언트(client)와 서버(server)

앱 개발시 프론트엔드와 클라이언트, 그리고 백엔드와 서버는 동일한 의미로 사용된다. 클라이언트는 1st party client와 3rd party client 로 나눠진다. 1st party client 는 업체가 서비스를 위해 제공하는 공식 클라이언트이며 직접 사용자의 id와 password를 입력받을 수 있다. 그리고 3rd party client는 서비스에 접근하는 외부 클라이언트이며, 보안상의 이유로 해당 서비스 접근을 위해서 사용자로부터 직접적으로 id와 password 입력을 받지 못한다 (id, password 정보가 다른 앱에 저장될 위험이 있으니깐 말이다). OAuth2.0에서 서버는 authorization server와 resource server로 나눠진다. authorization server는 클라이언트들을 인증하고 access token을 발행해주는 서버이다. resource server는 클라이언트의 API 요청을 access token 기반으로 처리하는 서버이며 흔히 얘기하는 API server 이다. 자세한 것은 OAuth 2.0 스펙을 참고하기 바란다. OAuth를 이야기할 때는 클라이언트와 서버라는 용어를 사용하며, 본 글에서도 이 용어들을 사용할 것이다. 

앱스토어에 올릴 1st party client 에 OAuth2.0의 어떤 인증 타입을 적용할 것인가?


위에서도 언급했듯이  1st party client 는 특정 서비스를 위한 공식 클라이언트이다. 이 때문에 1st party client 는 등록된 사용자로부터 직접 id 와 password를 입력받아서 Authorization server 를 통해 사용자 인증을 하는데 그것들을 쓸 수 있다. 이러한 특성을 고려했을 때, 클라이언트 및 사용자 인증에 가장 적합한 인증 타입은 ‘Resource Owner Password Credentials’ 임을 찾을 수 있었다.

OAuth2.0 스펙에선 아래 4가지 타입의 인증 방식을 소개하고 있다.

  • Authorization Code
  • Implicit
  • Resource Owner Password Credentials
  • Client Credentials

간략하게 핵심 내용을 설명하자면, Authorization Code와 Implicit 타입은 OAuth2 스펙을 보면 credential client 즉, 서비스에 접근하는 외부 앱을 위한 타입이다. Authorization Code는 자신만의 백엔드 서버를 갖고 동작하는 앱일 경우에, Implicit는 백엔드 서버 없이 순수 클라이언트 사이드에서만 동작하는 앱일 경우에 사용한다. 이들 외부 앱은 보안상의 이유로 사용자의 id와 password를 직접 입력 받지 못하기 때문에, access token을 얻기 위해서는 먼저 해당 서비스로 이동하여 사용자 확인(로그인) 및 허락을 받아야 한다. 이를 위해 외부 앱은 새로운 window 또는 webview 를 생성해서 해당 서비스의 Authorization server url에 client id와 callback url을 붙여서 리다이렉션 시킨다. Authroization server는 client id와 callback url을 검사하여 요청한 client가 developer site를 통해 등록된 정상적인 client인지 확인하는데 사용한다. 그리고 리다이렉션된 페이지에서 사용자가 id, password 로 로그인(Authentication)을 하고 해당 외부 앱이 자신의 데이터를 엑세스하도록 허용하면 (보통 ‘Allow’ 해당하는 버튼을 클릭함), Authorization server는 해당 외부 앱의 callback url에 인증타입에 따라 code 또는 access token을 붙여서 리다이렉션 시킨다. ‘Authorization Code’ 타입일 경우 callback url에 code가 붙으며, 외부 앱은 이 code 값과 해당 서비스에서 발급해준 자신의 client id와 secret 을 Authorization server로 다시 보내어 access token을 발급 받는다. 그리고 ‘Implicit’ 타입일 경우 바로 callback url에 access token정보가 fragment로 붙어서 넘어가는데 ‘Authorization Code’ 타입보다 간소화된 타입이라 보면 된다. client 는 사용자의 단말 또는 PC 에 설치되기 때문에 하드 코딩된 client id 값이 노출될 수도 있다. 그래서 client id 값만으로 access token을 받아오는 ‘Implicit’ 타입은 보안상 약간 위험할 수 있다. 그러나 ‘Authorization Code’ 의 경우, Authroization server와 통신하는 주체가 client가 아니라 백엔드 서버이고 client id와 secret이 백엔드서버에 저장되어 사용되기 때문에 노출될 여지가 없기 때문에 좀 더 보안상 좋다고 할 수 있다. 아무튼 이 두 타입의 인증방식은 용도상 외부 앱이 내 서비스에 접근하기 위한 것으로써 1st party client 인증용으로는 적절하지 않다. 그리고 마지막의 ‘Client Credentials’ 타입은 어떤 사용자든 상관없이 클라이언트 그 자체만을 인증하기 위한 방식으로써 사용자별로 인증하여 access token을 발행해야 하는 1st party client 인증용으로는 역시 적절하지 않다.

마지막으로 남은 ‘Resource Owner Password Credentials’. 이 타입은 아래의 OAuth2.0 스펙의 설명을 보면 1st party client 으로 적절해 보인다. 아 처음 스펙을 읽을 땐 왜 이렇게 이 문장이 ‘1st party client 를 위한 것이다!’ 라고 느껴지지 않았는지 모르겠다. 그냥 속시원하게 ‘1st party client 을 위해 적절하다’ 라고 명시해놓았으면 좋았을 것을 ㅠㅠ;

The resource owner password credentials grant type is suitable in cases where the resource owner has a trust relationship with the client, such as the device operating system or a highly privileged application (4.3)

1st party client 는 자신의 Authozation server 에 인증 요청을 하는 것이므로, 어떤 페이지로의 리다이렉션 없이 직접 사용자의 id, password를 입력받고 자기의 client id와 secret을 Authorization server로 인증 및 access token을 요청한다. 이 경우, 1st party client 는 사용자 단말에 설치되어 있어서 하드 코딩된 client id 와 secret 이 노출되는 것이 위험할 수도 있으나, 이 타입의 경우 사용자의 id, password 를 함께 보내야만 인증 가능하므로 사용자 id, password 만 단말 어딘가에 저장하지 않는다면 안전하다고 할 수 있겠다. 그래서 OAuth2.0 스펙에도 access token을 얻은후에 client는 반드시 사용자의 credentials 즉, id와 password를 제거해라고 언급되어 있다.

The client MUST discard the credentials once an access token has been obtained. (4.3.1)

결론은 1st party client 에는 ‘Resource Owner Password Credentials’ 타입을 적용하자! 이다.

 

인증 및 권한부여를 하는 authorization server는 어떻게 구현 하지?


우선 1st party client 만 배포하는 나로써는 Authorization server 구현시 ‘Resource Owner Password Credentials’ 타입만 고려하면 되지만, 혹시나 나중에 외부 앱이 나의 서비스에 접근해야 하는 요구사항이 생길 수도 있으니 미리 다른 인증 타입도 고려해서 만들면 낫지 않을까 생각했다. 그리고 어차피 node.js 로 백엔드를 구성하고 있기 때문에 Authorization server 역시 node.js 로 구현했다.

1.  유용한 node.js 사용하기

  • oauth2orize
  • https
  • passport-http
  • passport-oauth2-public-client

oauth2orize 는 OAuth2.0 스펙의 핵심 flow를 node.js 형태로 구현해놓은 모듈이다. 요청된 인증 타입(grant_type)에 따라 개발자가 등록해놓은 callback function을 호출해주고, 해당 function이 리턴한 결과를 적절히 http response로 만들어서 client에게 응답해주는 모듈이다. 나는 이 모듈을 이용해서 oauth2-restapi-server 의 뼈대를 만들 수 있었다. oauth2-restapi-server가 express 모듈을 사용하고 있기 때문에 아래에서 설명할 Authorization API 가 호출될 때,  oauth2orize의 grant, exchange, errorHandler가 실행되도록 oauth2orize 기능들을 모든 Authorization API에 express의 middleware로 지정하였다.

https 는 TLS (Transport Layer Security) 기반으로 서버가 동작할 수 있도록 해주는 모듈로써 client와 Authorization server 간 주고 받는 패킷에 적용하기 위해서 사용했다. 나는 Authorization server에 self signed certificate를 만들어서 client와 주고 받는 패킷을 encryption / decryption 하도록 만들었는데 실제 product 시점엔 공인된 CA로부터 발급받은 certificate 를 사용해야 하지 않을까 싶다. 그리고 http 로 요청이 들어와도 https 로 리다이렉션이 되도록, 리다이렉션 기능을 모든 API 에 express의 middleware로 지정하였다.

passport-http 는 Authorization server가 client id 와 secret으로 client 검증을 해야 하는 경우에 사용한다. ‘Authorization Code’ 타입의 client가 code로 access token 을 요청하는 경우, ‘Resource Owner Password Credentials’ 타입의 client가 access token을 요청하는 경우, (잘 쓰이지는 않지만) ‘Client Credentials’ 타입의 client가 access token을 요청한 경우가 해당된다. 이 모듈은 http header의 ‘Authorization: Basic’ 뒤에 base64로 인코딩된 스트링을 파싱하여 client id와 secret을 개발자가 등록한 callback function으로 넘겨준다. client id와 secret이 유효한지 여부를 검사하는건 개발자의 몫이다.

passport-oauth2-public-client 는 Authorization server가 client id 만으로 client 검증을 하는 경우에 사용한다. ‘Authorization Code’ 타입의 client가 access token 요청 전 code 자체를 얻기 위해서 요청하는 경우, ‘Implicit’ 타입의 클라이언트가 access token을 요청하는 경우가 해당된다. 이 모듈은 http body의 ‘client_id’ 값을 개발자가 등록한 callback function으로 넘겨준다. client id가 유효한지 여부를 검사하는 건 개발자의 몫이다.

2. Authorization을 위한 API 설정

1st party 든 3rd party 든 모든 client들은 인증 및 access token을 요청하기 위해서 url 형태의 API를 통해서 접근한다. 아래의 API로 4가지 인증 타입의 요청을 모두 받아들일 수 있다.

  1. /oauth2/authorize
  2. /oauth2/authorize/decision
  3. /oauth2/token

1, 2 번 API는 ‘Authorization Code’ 와 ‘Implicit’ 타입의 인증방식에서만 사용된다. 즉, 3rd party client 가 사용하게 되며 2번 API의 응답 결과가 해당 client가 등록될 때 지정한 callback url 뒤에 붙어서 넘어가게 된다. 응답 결과는 위에서 설명한대로 인증 타입에 따라 code또는 access token 둘 중 하나이다. 3번 API는 ‘Authorization Code’, ‘Resource Owner Password Credentials’, ‘Client Credentials’ 타입의 인증방식에서 사용된다. API 요청시 client는 OAuth2.0 스펙에서 요구하는 header와 body를 만들어서 요청해야 하며, authorization server는 이 값들을 파싱하여 client 검증 또는 사용자 검증을 하는데 사용해야 한다. API 사용법에 대한 자세한 설명은 oauth2-restapi-server github 페이지를 참고하길 바란다.

authorization의 의미대로 권한부여를 제대로 하려면 client 별로 사용자별로 접근할 수 있는 리소스를 제한하는 기능이 있어야 한다. 이를 위해 OAuth2.0 스펙에서는 ‘scope’ 이라는 필드가 있는데 제대로 authorization server를 만드려면 리소스에 대한 scope를 정의하고 http body의 ‘scope’ 필드를 활용할 필요가 있다. 여기서는 주제를 벗어나므로 자세한 내용은 생략한다. oauth2-restapi-server 는 아직 ‘scope’ 필드를 활용하고 있지 않다. 그래서 발행되는 모든 access token 은 API server 내 특정 사용자의 모든 자원에 대해 read/write 가능하다.

3. 필수 database 설정

OAuth2.0 스펙대로 동작하려면 기본적으로 아래와 같은 데이터를 Authorization server가 갖고 있어야 한다.

  • 등록된 client들의 id 와 secret 정보 (보통 developer site 에서 각 client 개발자가 등록함, 난 테스트를 위해 디폴트로 하나씩 등록함)
  • 등록된 사용자들의 id 와 password 정보
  • access token을 얻기 위해 발행된 code 정보 (‘Authorization Code’ 타입을 위해서만 사용)
  • 발행된 access token 정보

나는 mongoose 모듈을 이용해 authorization server가 mongo db 에 이러한 정보들은 저장하고 검색하고 삭제하도록 구성하였다.

4. 테스트를 위한 4가지 인증 타입별 client 정보를 등록

authorization server는 모든 요청에 대해 client 검증을 하기 때문에 타입별로 client 정보를 등록할 필요가 있다. 등록을 위한 developer site 가 있다면 편리하겠지만 여력이 없어서 단순히 authorization server가 런칭될 때, 디폴트로 client 들이 등록되도록 하였다. 이 때 등록되는 정보들은 다음과 같다.

  • client name *
  • client id *
  • client secret *
  • token refresh 여부 *
  • callback url

‘ * ‘ 붙은 정보는 mandatory 이며, callback url은 ‘Authorization Code’와 ‘Implicit’ 타입에서만 사용된다.

5. 인증 및 access token 발행 테스트

각 타입별로 client 를 만들어서 확인해보면 가장 좋겠지만, Authorization server에게 요청/응답 받는 건 ‘postman’ 같은 http client로도 충분하다. 나는 크롬의 extension인 ‘postman’을 사용해서 위의 4가지 타입의 client들을 API로 검증할 수 있었다. 1st party client를 위한  ‘Resource Owner Password Credentials’ 타입에 대해서는 특별히 angular.js 기반의 client를 만들었다.

6. access token 저장 및 소셜 앱을 위해 확장한 기능

angular.js 기반의 1st party client는 local account로 로그인한 직후, ‘Resource Owner  Password Credentials’ 타입으로 access token을 요청하고 받아온다. 그리고 해당 access token은 local storage에 보관하여 client 종료후 다시 실행시 local storage에서 읽혀 다시 사용된다 (만약 명시적으로 authorization server에 API로 logout을 요청하면 authorization server는 해당 client와 사용자를 위해서 생성했던 access token을 제거하고, client 역시 local storage에 저장된 access token을 제거한다). 개인적으로 고민했던 부분은 access token을 받아온 후 client가 이것을 어떻게 보관하느냐 하는 부분이었다. 현재로써 결론은 client 가 access token을 저장할 필요가 있으니, Android/iOS 플랫폼에서 app isolation을 보장해준다는 가정하에 client가 살아있는 동안 영구적으로 보관하기 위해  local storage를 선택했다.

이 angular.js client는 local account 로 signup 및 login을 지원하며, 외부 social account 로 연결(connect)이 가능하다.  소셜 앱을 개발한다면 사용자들의 편의를 위해서 local account 를 생성하는 것 없이 1st party client 가 twitter, facebook 같은 외부 social account 인증만으로도 signup을 해줄 필요가 생긴다. 이 경우에 사용자의 social account로 1st party client를 로그인 한 후 RESTful API로 내부 리소스를 접근하려면 access token이 필요하다. local account 가 아닌데 ‘Resource Owner Password Credentials’ 타입을 어떻게 이용하지? 고민을 하다 생각해낸 아이디어는 1st party client가 social account로 login 하게 되면 결과값으로 twitter 등 해당 social 서버가 발행해준 access token을 client로 보내주고, client는 http body의 ‘username’과 ‘password’ 필드에 각각 해당 서비스 이름과 해당 서비스의 서버에서 발행해준 access token을 넣어주도록 하는 것이다. 그럼 authorization server는 ‘username’이 local account의 사용자 id아닌 서비스 이름일 경우 password에 있는 access token 값으로 내부 database를 검색하여 등록된 social account인지 체크 후, RESTful API 사용을 위해서 access token을 발행해주는 것이다. 테스트 결과 잘 동작하였으며 외부 social account을 지원하는 앱을 만드는 경우라면 참고해도 될 것 같다.

 

 발행된 access token 기반으로 동작하는 RESTful API server는 어떻게 구현 하지?


client가 Authorization server를 통해 access token을 얻었다면, 그 access token을 갖고 특정 리소스에 접근하기 위해서 API를 호출할 것이다. 이 때 API server는 오직 access token 하나만으로 해당 요청을 받아야 할지 말지를 결정하고 적절히 그 요청을 처리해야 한다. OAuth2.0에서는 client로부터의 API 요청시 authorization server와 API server 중 누가 access token의 유효성을 검사해야 하는지 정해놓지 않았다. 그래서 implementor가 성능, 보안을 고려해서 자신의 상황에 맞게 선택해야 한다. 나의 경우 API server가 직접 access token 을 검사하도록 만들었다. API server 역시 node.js 로 구현하였고 내용은 다음과 같다.

1. 유용한 node.js 모듈 사용하기

  • https
  • passport-http-bearer

API server 에 역시 안전하게 client와 패킷 교환을 해야하기 때문에 https 를 적용했다.

passport-http-bearer 는 http header에 있는 ‘Authorization Bearer:’ 뒤에 있는 스트링인 access token을 개발자가 등록한 callback function으로 넘겨준다. access token 이 유효한지 여부를 검사하는 건 개발자의 몫이다. API server는 자신에게 들어온 API 요청을 처리하기 전에 전달받은 access token이 유효한지 검증하는데 이 모듈을 사용한다.

2. 리소스 접근을 위한 API 설정

개발하는 앱에 따라 리소스를 정의하고 그 리소스에 접근할 RESTful API를 정의할 필요가 있다. RESTful API는 보통 다음과 같은 특징들을 제공한다.

  • Uniform Interface (hyper media를 이용, http의 헤더, 바디, url을 통해 인터페이스를 정의)
  • Stateless (session을 사용하지 않는다. 그래서 서버의 물리적 확장성 높임)
  • Cacheable (동일한 요청은 캐쉬데이터로 전달)
  • Client-Server (client 코드가 서버 독립적으로 작성가능, 서버 또한 client 독립적으로 제작 가능)
  • Layered System (client 상태를 유지하지 않기 때문에 중간 서버들을 추가하여 layer서버구조 가능)
  • Code on Demand (optional한 특징이며, cilent에서 실행할 코드를 미리 생성하여 전달)

RESTful API를 정의하려고 한다면 우선 아래의 자료들을 읽어보는 것이 좋을 듯하다.

 

oauth2-restapi-server 소스코드


아래 github 주소로 실행가능한 소스코드를 올려두었다. OAuth2.0 authorization server나 access token기반 API server를 이해하고 설계하는데 entry point로 도움이 되었으면 한다. 현재 oauth2-restapi-server 는 authorization server와 API server가 하나의 process 로 실행된다. 서버 아키텍처에 따라 이 두 server는 분리될 수도 있기 때문에 최대한 코드 레벨에서 서로 디펜던시를 갖지 않도록 만들었다.

github: https://github.com/vinebrancho/oauth2-restapi-server

 

마치며..


안전하게 앱과 사용자를 인증하는 방법으로 OAuth2.0를 살펴보았고, 실제로 본인이 node.js 로 구현한 authorization server와 API server를 토대로 몇가지 주요한 내용을 살펴보았다. OAuth2.0은 많은 논의 거쳐 final version이 나왔고 현재 여러 서비스 업체들이 사용하고 있기 때문에 나름(?) 믿고 적용할만한 프로토콜이다. 앱 인증과 RESTful API를 고려중인 개발자들에게 작은 도움이 되었으면 하는 바램으로 글을 마무리한다.

 

Posted in Technical Note

내 앱에서 social network 계정을 oauth로 인증하기

현재 많이 사용되는 social network 몇개를 내 앱에서 oauth로 인증하기 위하여 먼저 node.js로 프로토타입을 위한 앱을 만들어봤다. 3rd 앱에서 oauth로 다른 social network의 사용자를 인증하는 것을 보통  social login 또는 social authentication 이라고 부른다. 통계를 보면, social login을 위해 가장 많이 사용되는 계정들은  facebook, google+, yahoo, twitter 순이다.

The landscape of social login: Facebook makes a comeback

신규 앱에서는 사용자 인증관련해 social login을 많이 사용하는 추세인데 이것은 사용자에게 편리함을 준다. 왜냐하면 별도의 가입절차 없이, 이미 등록된 다른 social network의 계정으로 신규 앱에 로그인을 할 수 있게 해주기 떄문이다.  뿐만 아니라 oauth 인증 결과로 받은 access token을 이용해 신규 앱은 허용된 범위에서 해당 social network 제공자에게 어떤 자원에 대해 read/write를 요청할 수도 있다. 예를 들면 사용자가 word press 에 twitter 계정을 연결(connect)시켜서 write권한이 있는 access token을 얻게한 다음, 나중에 word press에 포스팅했을 때 자동으로 자신의 twitter 계정에 tweet이 남겨지도록 할 수 있다.

social login은 주로 OAuth 또는 OpenID 방식으로 구현이 된다.  현재 주요 social network 제공자들은 OAuth 방식을  채택하고 있기 때문에, 3rd 앱들은 OAuth로 social login 기능을 구현하고 있다. OAuth스펙은 OAuth1.0, OAuth1.0A를 거쳐 현재 OAuth2 까지 나온 상태이다.  OAuth2가 기존 OAuth의 단점을 커버하고 있기 때문에 OAuth2를 많이 사용하는 추세이다.

나는 node.js 로 social-logins 앱을 만들어보았는데 이 때 사용한 주요 node.js 모듈은 다음과 같다. 나의 social-logins 앱은 위에서 얘기한 3rd 앱이 된다.

  • express 4.0 모듈과 passport 모듈
  • social network 별 passport strategy 모듈
  • mongodb 와 mongoose 모듈

social-logins 앱이 제공하는 기능은 다음과 같다.

  • email, password 기반의 로컬 계정 생성 (sign up) , 로컬 계정 로그인 (log in, or sign in)
  • social network 계정으로 로그인
  • 프로필 페이지에서 로컬 계정 정보 표시
  • social network 계정 연결(connect), 해제 (disconnect) 제공
  • 로그인 또는 연결된 social network 계정 기본정보를 프로필에 표시

로컬 계정과 더불어 기본으로 지원되는 social network 계정은 다음과 같다.

  • Twitter (OAuth1.0A)
  • Facebook (OAuth2)
  • Google+ (OAuth2)
  • Yahoo! (OAuth2)
  • LinkedIn (OAuth2)
  • GitHub (OAuth2)

social-logins 앱 소스 코드는 나의 github 저장소에 올려두었다.

https://github.com/vinebrancho/oauth2-restapi-server/tree/node_server_render_ver

node.js로 social login 구현시 OAuth를 핸들링을 하는 passport strategy 모듈이 이미 만들어져 있다면, 해당 social network 계정을 붙이는 작업은 쉽게 가능하다. 하지만 passport strategy 모듈이 없다면 OAuth 처리를 위해 해당 social network의 개발자사이트에 명시된 스펙을 꼼꼼히 체크하면서 개발해야 한다. 위에 나열한 것들은 각각의 passport strategy 모듈이 존재하기 때문에 social login을 구현하는데 많은 시간이 들지 않았다.

UI를 가지는  front-end 영역은 브라우저의 페이지, 또는 iOS, Android 네이티브 앱, 또는 webview를 가진 네이티브 앱인 hybrid 앱  형태가 될 수도 있다. 나의 social-logins 앱은 기능 검증이 목적이어서 fron-end는 단순히 브라우저의 페이지로 제작했다(추후 angular.js와 bootstrap 기반으로 리팩토링 예정). 아래는 social-logins 앱의 스크린샷이다. (난 UI 에는 정말 소질이 없나보다 -_-;;)

social-logins 앱 로그인

Image

social-logins 앱의 로컬 계정 생성

Image

로컬 계정으로 로그인 후 프로필 페이지

Image

로그인 화면에서 트위터 계정으로 로그인하여 생성된 새로운 프로필

Image

하나의 유저에 여러 social network 계정을 연결(connect)한 후.  (브라우저 사이즈상 스크롤하여 나눠서 스샷했음, 아래 사진들은 같은 페이지에 있음)

스크린샷 2014-04-20 오전 12.47.17

Image

마치며..

사용자의 편의를 위해서, 새로운 앱을 만들 때 social-login 기능은 이제 필수인거 같다. 그리고 많은 사용자를 갖고 있는 social network 들은 이미 OAuth 기반의 사용자 인증을 위한 RESTful API를 제공하고 있고 검증해본 결과 잘 동작한다. 물론 node.js로 앱을 만들면 이미 RESTful API 처리를 해주는 것들이 모듈로 개발되어 있는 상태라 기능 OAuth 검증이 매우 쉽다. 앱 개발자들은 로컬 계정 뿐만 아니라 social network 계정도 지원하여 사용자가 좀 더 편리하게 앱을 이용할 수 있도록 하면 좋을 것 같다.

 

< References >

http://oauth.net/2/

– https://github.com/jaredhanson/passport

– https://github.com/jaredhanson/passport/wiki/Strategies

– http://scotch.io/tutorials/javascript/easy-node-authentication-linking-all-accounts-together

Posted in Technical Note

node.js로 javascript 기반 cpu-intensive 작업 가능할까

node.js (이하 node)는 공식적으로 event loop 이 수행되는 main thread 에서만 v8엔진 기반의 javascript 실행환경을 제공한다. node 는 자신의 프로세스 내/외부에서 발생하는 모든 request를 main thread를 통해 처리한다. 이 때문에 main thread 를 잠깐 멈추게 하는 cpu-intensive javascript 코드를 수행해야 하는 usecase는 node에서 사용하기엔 무리가 있다. 이것이 node의 제약이기도 하다. 그러나 node로 꼭 그러한 작업를 수행해야 한다면 어떻게 해야 할까. 언제든 제약 또는 단점으로 여겨지는 문제를 푸는 일은 재미있는 일이다. node에서 cpu intensive 작업을 수행하면서도 node가 멈추지 않고 동작하게끔 할 수 있는 몇가지 방법을 정리해보려 한다. 

* 이글에서 말하는 cpu intensive 작업은 cpu를 연속적으로 사용하는 javascript 코드로써 코드 수행이 끝날때까지 자신을 호출한 caller에게 결과를 리턴하지 않는 작업을 의미한다. 

1. child_process 모듈

node 프로세스를 fork하여 새로운 node 프로세스를 생성하는 것이다. 즉, 자식 node 프로세스를 만드는 것이다. 아래의 코드처럼 child_process를 fork하면서 child process가 수행해야 할 javascript 파일을 지정할 수 있다.
[gist 981c240f2fa24b7d9dfc]

 child process는 지정된 javascript 코드를 수행하면서 수행한 결과를 메시지를 통해 parent process에게 전달해줄 수 있다. 물론 parent process가 이 메시지를 받기 위해서 listen하고 있어야 한다. child process를 fork해서 cpu-intensive 작업을 수행하도록 하면 원래 node 프로세스는 계속해서 event loop을 수행하며 다른 요청들을 처리할 수 있다. 그러나 child_process 를 사용시 고려해야 할 것이 있다. 그것은 child 프로세스가 또 하나의 node 프로세스라는 것이다. 이것은 child 프로세스가 부모처럼 node를 위한 각종 초기화 작업을 수행한 후 지정된 javascript 코드를 수행시킨다는 의미이다. node 초기화 작업은 크게 다음과 같다.

 – v8 초기화 및 Isolate, Context 생성
 – uv event loop 생성 및 non-blocking 작업을 위한 thread pool 생성 (현재 4개)
 – ‘process’와 ‘module’ js object 생성, 각종 builtin 모듈 로딩

음, cpu-intensive 작업을 위해 이런 비용을 들이면서 프로세스를 생성하는 것은 그렇게 나이스해 보이지 않는다.  child_process 모듈은 쉘 명령어를 수행해서 표준출력으로 결과를 parent process에게 보내는 용도가 더 적합하지 않을까 싶다. 그러나 node 프로세스 생성 비용이 큰 문제가 되지 않는 시나리오라면 사용해도 괜찮을 것 같다. 

2. cluster 모듈

cluster 모듈은 내부적으로는 child_process 모듈의 fork 메소드를 사용해서 구현하기 때문에 위에서 언급한 node 프로세스 비용이 고스란히 발생한다. 무엇보다 cluster 모듈은 cpu core를 모두 활용함으로써 동시 발생하는 외부 요청들을 더욱 많이 처리하여 scalability를 높이기 위해 만들어졌다. 그러므로 cpu intensive 작업을 처리하는데 있어서는 cluster 모듈 사용은 적절하지 않은 것 같다. 
[gist df60c43becedbfe94b51]

3. threads-a-gogo 모듈

cpu intensive 작업을 위해서 별도의 프로세스를 생성하는 것보다는 이미 실행중인 node 프로세스안에 새로운 thread를 생성하여 javascript 실행환경을 만들어주는 것이 비용과 반응성 측면에서 더 나아보인다. v8의 javascript 실행환경을 위해서는 반드시 v8::Isolate과 v8::Context가 필요하다. 새롭게 생성된 thread는 이미 만들어진 v8::Isolate 를 쓸 수도 있고, 아니면 새롭게 만들어서 쓸 수도 있다. main thread가 다른 thread와 javascript 오브젝트들을 공유하려면 javascript 실행환경이 같아야 한다. 이말은 같은 v8::Isolate를 사용해야 한다는 것인데, 이렇게 되면 동시에 두 thread가 javascript 코드를 수행할 수 없다. 왜냐하면 v8이 하나의 v8::Isolate은 자신을 lock을 하고 있는 하나의 thread에 의해서만 사용되도록 제한하고 있기 때문이다. main thread의 javascript 수행이 멈추지 않도록 하려면 새로운 thread에 새로운 v8::Isolate과 v8::Context를 생성해줘야 한다. main thread는 콜백이나 메시지 전달을 통해 결과를 받을 수 있어야 할 것이다. 찾아보니 방금 말한 것을 구현한 node 모듈이 있었다. 
threads-a-gogo 모듈이다. (이름이 아주 독특한데 왜 gogo라고 했을까 a는 뭐지-_-;ㅋ). threads-a-gogo의 목적자체가 cpu-intensive 작업을 main thread가 아닌 별도의 thread에서 처리하도록 하는 것이다. 별도로 생성된 thread는 작업이 완료되면 main thread에게 콜백 또는 메시지로 결과를 알려줄 수 있다. 아래는 별도의 thread를 생성하고 계산작업을 시킨후 그 결과를 main thread 내 콜백으로 호출하는 예이다. 
[gist 2d12240c4fd64d8c2eab]

threads-a-gogo가 생성하는 thread는 pthread이며 main thread의 v8::Isolate를 사용하지 않고 별도의 v8::Isolate과 v8::Context을 생성하여 사용한다. 즉, main thread가 생성한 어떤 v8 object도 접근할 수 없을뿐만 아니라 다른 thread과도 어떤 v8 Object도 공유할 수 없다는 의미이다. 생성된 thread는 독립적으로 javascript 코드 또는 파일을 수행하고 main thread에게 결과만 전달한다. 아래 코드는 thread-a-gogo의 c++ addon에서pthread_create를 호출할때 start_routine 인자로써 전달하는 함수이다.  
[gist b14b66d0cd11ba6ce64c]

4. webworker-threads 모듈

HTML5의 Web Worker 스펙을 구현한 node 모듈이다. 서버 사이드에서 web worker를 쓸 수 있게 했다는 것만으로도 좋은 시도라 생각한다. 바로 위에서 소개한 threads-a-gogo 모듈을 이용해서 thread 처리를 하고 있다. 아래 소스는 threads-a-gogo의 예제를 web worker버전으로 바꿔본 것이다.
[gist 12008430e591d3209699] 

목적 자체가 threads-a-gogo와 같고 thread 내부 동작도 동일하나 개발자들에게 노출된 javascript API만 다르다. 둘 중 맘에 드는 걸 선택해서 쓰면 되겠으나, Web Worker 스펙은 W3C에 의해 만들어졌고 클라이언트 사이드에서 많이 사용되고 있어 레퍼런스도 많으니 이왕이면 webworker-threads 모듈을 사용하는게 좋을 듯하다.

5. JXCore (forked from node.js)

JXCore는 node.js 소스코드를 fork하여 multithreading 기능을 추가한 프로젝트이다. 프로젝트가 시작된 이유는 node가 제 아무리 I/O중심의 요청들을 받아서 빠르게 처리한다 하더라도 동시에 수많은 요청이 한꺼번에 들어오면 반응성이 떨어지는 현상이 있는데 이를 해결하기 위해서라고 한다. node.js는 cluster 모듈을 이용해서 node 프로세스를 여러개 생성해서 반응성을 높이려고 하는데 JXCore는 하나의 node 프로세스 안에 추가적인 thread들을 생성하여 각각 v8::Isolate, v8::Context를 만들고 각각 uv기반의 event loop을 갖도록 하여 반응성을 높인다. 일반적으로 thread가 process에 비해 cpu나 메모리 측면에서 생성비용이 적게 때문에 JXCore가 cluster에 비해 효과를 보는 부분이 있을 거라고 예상할 수 있다. 실제로 벤치마크 결과(레퍼런스 참고)를 보면 node.js의 cluster보다 좋은 성능을 내고 있음을 알수 있다. JXCore가 node.js에 multithreading 기능을 넣은 건 기존 cluster의 반응성 문제를 해결하기 위해서였으나, 이 multihreading 기능을 이용해서 cpu-intensive 작업도 할 수 있도록 지원한다. 아래의 코드에서 보듯이 생성된 JXCore의 thread pool에 task를 add하면 JXCore가 내부적으로 thread를 선택하여 해당 task를 수행시킨다. main thread는 등록한 callback 또는 메시지를 통해 결과를 받을 수 있다. 
[gist 63d008f5b38d9020a5fa]

thread를 이용해서 동시 요청에 대한 반응성을 끌어올린 부분과 cpu intensive 작업을 병렬로 처리하게 한 부분은 주목할만하지만, 굳이 node.js를 fork해서 독립적으로 개발할 필요가 있었을까 싶다. 물론 node.js의 core를 수정해야 했기에 그렇다고는 하지만 node.js가 앞으로 계속 버전업이 될텐데 그에 따라 계속 호환성 유지를 해줄지 모르겠다. 현재까지는 node.js의 버전 (0.11)과 100% 호환되기 때문에 기존 node module과 app이 동작하는데 문제가 없다. JXCore는 현재 오픈 소스도 아니고 중간에 독자적인 길을 걷게 될 수도 있기 때문에 JXCore 기반으로 node 코드를 짜는건 node 개발자들에겐 약간 risk가 있게 느껴지지 않을까 싶다. 

마무리하며..

node에서 제약으로 여겨지는 cpu intensive 작업을 병렬적으로 수행하여 해결하는 몇가지 방법을 간단히 살펴보았다. cpu intensive 작업을 수행하는 상황과 시나리오가 다양하고 다를 것이기 때문에 어떤 방법이 좋다라고 결론 짓기는 어렵다. 상황에 맞게 선택해서 쓰도록 하자. 

 

< refereces >

– http://nodejs.org/api/child_process.html

– http://nodejs.org/api/cluster.html

– https://github.com/xk/node-threads-a-gogo

– https://github.com/audreyt/node-webworker-threads

– http://jxcore.com/docs/jxcore-feature-multithreading.html

– http://flippinawesome.org/2014/03/03/jxcore-a-node-js-distribution-with-multi-threading/

– http://jxcore.com/nodejx-vs-vert-x-vs-node-js-cluster/

Posted in Technical Note

Node.js 내가 쓰기로 선택한 이유.

개인 프로젝트에서 RESTful API를 구현하기 위해서 어떤 서버사이드 플랫폼을 사용할까 고민하다가 node.js 를 선택했다. 2000년초반, PC 웹이 한창 성장하고 있을 때만 해도 서버사이드 플랫폼으로는 asp, jsp, php 정도였던 걸로 기억한다. 그 때 취미삼아 웹사이트를 만들어본 경험이 있는데 고민했던 게 위 3가지 정도였다^^; 그러나 지금은 선택할 수 있는 서버 사이드 플랫폼이 상당히 많아졌다. 언어별로 대표적인 플랫폼을 나열해보았다.

  • Javascript : node.js
  • Ruby : Ruby on Rails (+ EventMachine for event loop)
  • Pyhon : Django (+ Twisted for event loop), Pyramid, Bottle, Nimble
  • Java : Play, Spring, Vert.x
  • PHP : cakePHP, Code igniter
  • Scala : Play
  • Erlang : inets, misultin

서버사이드 개발자들에겐 익숙한 것들이겠지만, 이번에 서버사이드 플랫폼을 들여다보기 시작한 나에게는 대부분이 낯설었다. 위의 모든 플랫폼에 경험이 있다면 각각 장, 단점을 기반으로 내 서비스에 적합한 것을 쏙 하나 집어 낼텐데, 나는 그렇지 못하기 때문에 내 나름의 기준에 맞는 플랫폼을 몇개 추려내고 그 중 하나를 선택하기로 했다.

내가 선택하는 서버사이드 플랫폼은 다음을 만족해야 했다.

  • 시간이 많지 않기 때문에 당장 쓸수 있는 언어 기반이어야 한다.
  • 소셜 서비스 성격상 수많은 사용자의 동시 접속과 요청을 빠르게 처리할 수 있어야 한다.
  • 이를 위해 제약된 하드웨어 인프라에서 확장성이 좋아야 한다.
  • 개발 시간을 단축하기 위해서 유틸리티 성격의 확장 모듈들도 많아야 한다.
  • 문서화 및 개발자 커뮤니티가 활성화되어 있어야 한다.

이렇게 기준을 정하고 나니 node.js와 django 두개로 압축할 수 있었다. 대부분은 1번 기준으로 제외되었다 -_-;; 그리고 twitter나 github가 Ruby on Rails(RoR)를 사용하고 있어서 한번 써보고 싶기는 했으나 Ruby 언어를 익혀야 하니 일단은 제외시켰다. 자, 그럼 node.js와 django 중 어떤 걸 할까. 2009년에 개발자 Ryan Dahl에 의해 공개된 후 실리콘밸리 개발자들을 열광하게 만들면서 급성장하게 된 node.js. Yahoo, VMWare, Microsoft, LinkedIn 같은 쟁쟁한 회사들이 쓰고 있는 node.js. 현재 5만개가 넘는 모듈을 갖고 있는 node.js. 직접 경험해보고 싶었다. 얼마나 좋길래. 그래서 node.js를 개인 프로젝트의 서버사이드 플랫폼으로 쓰기로 결정했다.

지금부터는 node.js의 기술적인 특징에 대해 몇가지 나누고자 한다. 먼저 node.js는 공식사이트에서 다음과 같이 소개되고 있다.

Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

– nodejs.org

이 문장 안에 사실 node.js에 대한 주요 특징들이 다 포함되어 있는 거 같다. node.js는 javascript 코드를 실행시키고, event driven 및 non-blocking I/O 모델을 사용한다. 그 결과 확장성 있고 데이터 중심의 실시간 네트워크 어플리케이션 개발을 가능하게 해준다라고 한다. 추상적으로 이해될수도 있는 부분인데 특징을 하나씩 살펴 좀 구체적으로 이해를 해보자.

Javascript 언어 실행

node.js를 만든 Ryan Dahl은 개발 언어로 javascript를 선택했다. 히스토리를 보면 C, Rua를 고려하다가 몇개월 뒤에 javascript가 적합하다고 판단하였다고 한다[1]. 아마도 javascript 언어가 가진 특징 때문이지 않을까 싶다. javascript 는 언어 자체적으로 event driven 모델을 이미 지원한다[2]. 이 때문에 클라이언트 사이드인 웹페이지에서 버튼을 하나 클릭하면 바로 화면에 어떤 작업이 수행된다. 서버 사이드로 예를 들면 http 요청이 한개 들어오면 바로 연결을 맺으면서 어떤 작업을 수행시키게 될 것이다. 잠시 뒤에서 좀 더 다루겠다. 또한 javascript는 내부 함수로써 외부 함수의 변수에 접근할 수 있는 closures란 기능을 지원한다[3]. 콜백함수를 파라미터로 지정하는 event driven 모델에서 매우 유용한 기능이다. 보통 함수는 자신의 scope에 지정된 변수만 접근 가능하기 때문에 콜백 함수로 사용된 어떤 함수가 외부의 변수에 접근하려면 콜백 함수를 호출하는 쪽에서 반드시 사용할 해당 변수를 지정해줘야 한다. closure 덕분에 javascript 코딩시 이런 것을 신경쓰지 않아도 된다.

node.js는 CommonJS 라는 스펙을 따르고 있다[4]. CommonJS는 특정 코드가 아니라, 웹브라우저 외 서버사이드나 독립적인 애플리케이션으로 사용하기 위해 필요한 기능을 정의한 스펙이다. node.js 로 개발하다 보면, module, exports 그리고 require 같이 브라우저용 자바스크립트 언어에는 없는 지시어들을 보게 될 텐데 그러한 것들은 CommonJS 인한 것으로 보면 된다.

node.js 이전에도 helma, appjet, jaxer 등 javascript 기반 서버사이드 플랫폼이 있었으나 활성화가 되지 못했다. 이유가 궁금하여 구글링을 해보니 node.js 가 성공한 이유는 javascript 언어에 대한 인식 변화, javascript engine의 성능 향상 그리고 Blocking I/O을 제거했기 때문이라는 분석글을 봤다[5]. 반대로 이전에 서버사이드 플랫폼들은 이 세가지를 갖고 있지 못했기 때문에 활성화가 되지 않았다는 건데 설득력이 있다. 첫째, 웹이 성장하면서 javascript 언어의 가치도 함께 올라갔고 그만큼 개발자들도 늘어났다. 이제 더이상 javascript 언어를 부정적으로 보거나 사라질 언어로 보질 않는다.둘째, 구글이 javascript 코드를 동적으로 컴파일하여 기계어로 바꾸는 V8 엔진을 개발하면서 javascript 실행 성능이 크게 좋아졌다. 그리고 V8이 컴파일된 기계어 코드를 캐슁(caching)하기 때문, 동일한 로직을 많이 수행할 수 밖에 없는 서버사이드에서는 더욱 효과적일 것이다.셋째, Ryan dahl이 unix/linux의 시스템 레벨에서 blocking I/O 처리되던 라이브러리들을 node.js에서 사용될 때 non-blocking I/O로 동작하도록 바꾸면서 서버 확장성의 문제를 해결한 것이다. non-blocking I/O에 대해서는 잠시 뒤에서 다루겠다.  아무튼 이 3박자가 잘 들어맞아 현재의 node.js 가 많은 개발자와 회사들로부터 사랑을 받는게 아닌가 싶다.

Single thread event-driven 그리고 Non-blocking I/O 모델

event driven은 event 기반, 즉 특정 사건이 발생할 때 작업이 수행되는것을 의미한다. UI를 가지면서 유저와 상호작용해야 하는 프로그램에선 유저가 언제 터치 같은 입력(즉, event)을 줄지 모르기 때문에 event driven 모델 사용이 일반적이다. javascript 언어가 event-driven 모델기반이기 때문에, node.js 는 javascript를 채택함으로써 개발자들이 서버사이드의 모든 작업을 event-driven 모델로 개발하도록 한다.실제로 node.js 가 기본으로 제공하는 javascript API 원형들을 보면 파라미터로써 callback을 입력하도록 요구하고 있다[6].

전통적인 서버사이드 개발 모델은 적절한 표현일지 모르겠지만 thread driven 모델이었다. 즉,  서버에 요청이 들어오면 서버는 이를 처리하기 위해 매번 별도의 thread를 생성했었다. apache같은 웹서버가 이런 모델인데, 이유는 POSIX 기반 함수들이 blocking I/O 방식으로 file을 엑세스한다라는 사실에서 찾을 수 있다. blocking I/O 방식은 filesystem, database 리소스 접근시 결과가 리턴될 때까지 코드 수행을 멈추고 다른 작업이 수행되지 못하게 하기 때문에, 하나의 thread로는 추가적으로 들어오는 클라이언트 요청을 처리할 수 없다. thread driven 모델은 클라이언트 별 thread로 인해 메모리 사용량이 증가하는 문제와 컨텍스트 스위칭 비용이 발생하는 문제가 있고 이는 클라이언트 수가 증가할수록 각각 확장성과 반응성을 떨어뜨리게 한다. Ryan Dahl이 2009년 JSConf에서 apache vs. nginex(event driven모델 서버중 하나) 성능 벤치마크 자료를 통해 수많은 클라이언트가 동시 요청시 event driven 모델이 초당 더 많은 요청을 받을 수 있고 메모리를 훨씬 덜 사용한다고 소개하기도 했다[7]. thread driven 모델에서 확장성과 반응성을 올리려면 서버 하드웨어 스펙을 올리던지 여러대의 서버를 운영하여 적절히 로드밸런싱이 되도록 해야 한다. 물론 그에 따른 경제적 비용이 만만치 않겠지만 말이다.

node.js는 event loop과 non-blocking I/O를 통해 전통적인 서버 사이드 모델에서의 이러한 문제들을 해결한다. node.js 서버(하나의 프로세스)는 한개의 event-loop thread(보통 main thread)로 동작한다. v8과 그 위에서 동작하는 서버사이드 javascript 코드는 모두 event loop thread에서 동작한다. 그리고 이 thread가 모든 클라이언트 요청을 받고 처리한다. node.js는 javascript 언어에서 기본적으로 제공되지 않는 filesystem, network (tcp/udp, http/https)등 시스템 리소스에 서버사이드 개발자가 접근하도록 하기 위해서 v8의 interface[8]에 맞게 c++로 구현(<node.js>/src/*.cc) 및 javascript로 바인딩하여 콜백 파라미터를 가진 javascript API set을 제공(<node.js>/lib/*.js) 한다[9]. blocking I/O 작업을 해야하는 리소스에 대해서는 thread pool을 이용해 node.js가 event loop thread가 아닌 별도의 thread에 그 일을 위임한다. blocking I/O 작업을 하는 thread의 작업이 끝나면, event loop thread가 이를 인지하고 v8을 통해 해당 리소스에 매핑되어 있는 javascript 콜백을 바로 호출한다. 즉, event loop thread에서 수행되는 javascript 컨텍스트에선 시스템 리소스 접근이 non-blocking I/O 처럼 동작하는 것이다. 아래 그림은 이런 flow를 잘 설명해주고 있다.

<source : https://www.udemy.com/blog/learn-node-js/>

기본적으로 node.js는 단일 event loop을 가진 thread로 동작하기 때문에 클라이언트 요청 증가에 대해 thread driven 모델보다는 좀 더 좋은 확장성(scalability)을 보인다. 그리고 별도의 thread에서 blocking I/O 작업이 처리되는 동안 event loop thread에선 다른 작업을 수행할 수 있으므로 동시에 발생하는 클라이언트 요청에 대해 좋은 반응성(responsiblity)을 보여줄 수 있다. 하지만 thread driven 대비 동시접속에 대한 요청당 처리시간이 빠르다고는 하지만 동시접속수가 많아질수록 반응성은 떨어질 수 밖에 없기 때문에 node.js 서버를 사용하더라도 여러개의 node.js를 띄워서 로드밸런싱을 해줘야 하지 않을까 싶다[10].

node.js를 들여다볼수록 node.js의 핵심은 blocking I/O로 동작하던 filesystem, network 기능들을 non-blocking I/O로 동작하도록 바꾼 것이란 생각이 든다. node.js는 javascript와 low level system 사이의 glue 역할을 하고 있는데 non-blocking I/O가 없었다면 지금의 node.js도 없지 않았을까. 시간이 될때 이부분을 소스레벨에서 hacking 해봐야겠다 ^^

node.js로 추천하지 않는 usecase

node.js는 한개의 event loop thread로 서버로의 모든 요청을 처리하기 때문에, 특정 요청을 처리하는 javascript 콜백에서 연산등의 cpu intensive한 작업을 오랫동안 한다면 그 시간만큼 다른 요청들은 대기할 수밖에 없다. 물론 cluster 모듈을 이용해서 멀티코어에서 코어 갯수만큼 동일한 node.js 프로세스를 띄워서 다른 코어 위에 동작하는 node.js 프로세스로 요청을 분산되도록 할 수 있으나, 같은 tcp port 로 들어오는 클라이언트 요청을 여러 코어로 분산되도록 하는건 node.js 가 아닌 커널레벨의 정책에 따른 것이라 어떻게 동작할지 예측하기가 어려울 것 같다. (node.js의 현재 공식 stable version 0.10.26 까지는 커널이 몇몇 특정 worker 프로세스로만 요청을 보내는 문제가 발생하여, 0.11 버전 때부터 로드밸런스를 커널에게 맡기지 않고 node.js가 직접하기 위해서 cluster master 프로세스가 직접 모든 요청을 받고 자기가 생성한 worker 프로세스들에게 round robin (디폴트 정책)으로 분산을 해주는 패치가 적용됐다[11][12]. stable version 0.12부터 해당 내용이 적용될 것이다.). 더군다가 클라이언트 요청이 몇 천, 몇만 건으로 들어오면 이런 류의 cpu-intensive 한 서비스는 제대로 처리해주기 어려우므로 node.js에 적합하지 않다. 그러나 굳이 꼭 node.js를 써야 한다면.. cpu intensive 작업을 c++ native 모듈을 만들되 node.js의 non-blocking 인터페이스 (uv_*함수들)나 v8의 멀티쓰레딩 인터페이스를 활용하여 v8에 binding한다면 별도의 쓰레드로 cpu intensive한 작업을 수행시키는 것이므로 괜찮지 않을까 싶다. node.js로 흔히 사용되는 usecase는 CRUD 작업을 주로 하는 RESTful 서비스나 notification push 같은 real time web application에 유용하다고 한다. realtime 지원은 node.js의 socket.io 모듈을 이용하면 쉽게 구현이 가능하다. 다음은 node.js의 usecase 관련 글이다.

http://nodeguide.com/convincing_the_boss.html

http://www.toptal.com/nodejs/why-the-hell-would-i-use-node-js

node.js에서 많이 사용되는 주요 모듈

node.js 서버 개발을 시작한지 며칠되지 않아서 다양한 모듈을 사용해보질 못했다. 하지만 현재 express 모듈로 RESTful API처리하는 부분을 코딩해보니 헉,, 그 편리함에 놀라고 있다. 모듈 개발자들에게 고마운 마음이;; ㅎㅎ. 계속 개발을 하다보면, 내 삶을 더욱 편하게 만들어줄 여러 모듈들이 있을거라 생각하는데 혹시나 없다면 직접 만들어서 npm 모듈로도 공개해봐야겠다. 참고로 현재 공개된 npm 모듈은 5만개이상이다.

https://www.npmjs.org

https://nodejsmodules.org/tags/build

http://www.queness.com/post/16219/29-nodejs-frameworks-for-fast-javascript-development

 

node.js 튜토리얼

node.js의 API는 공식사이트의 documentation에 잘 정리되어 있다. 그 외 아주 기본적인 내용들은 아래 사이트를 참고하면 될 것 같다.

http://www.nodebeginner.org/index-kr.html

http://howtonode.org/

http://pismute.github.io/nodeguide.com/beginner.html

http://nodeschool.io/

node.js 적용 사례에 대한 글

http://nodejs.org/industry/

https://github.com/joyent/node/wiki/Projects,-Applications,-and-Companies-Using-Node

http://readme.skplanet.com/wp-content/uploads/Tech_Planet_2013_baek.pdf

https://queue.acm.org/detail.cfm?id=2567673

Exclusive: How LinkedIn used Node.js and HTML5 to build a better, faster app

마치며…

단말용 tizen의 C++기반 web runtime을 개발하면서 주로 테스트 용도로만 javascript 언어를 사용했었다. 그 덕분에 node.js 로 javascript 코딩을 하는데 언어적인 거부감이나 어려움은 없다. 그리고 초반부에 서버사이드 플랫폼을 선정하는 내 나름의 기준들을 node.js 가 잘 만족시켜주는 것 같다. 에효 드디어 정했다. (ps. 혹시나 제가 쓴 글 중에 잘못된 부분이 있다면 주저말고 지적해주세요.)

< References >

[1] http://www.theregister.co.uk/2011/03/01/the_rise_and_rise_of_node_dot_js/?page=3
[2] http://www.launchacademy.com/codecabulary/learn-javascript/event-driven-asynchronous-callbacks
[3] http://javascriptissexy.com/understand-javascript-closures-with-ease/
[4] http://en.wikipedia.org/wiki/CommonJS
[5] http://programming.oreilly.com/2011/06/node-javascript-success.html
[6] http://nodejs.org/api/addons.html
[7] http://www.scribd.com/doc/23801896/Node-js-JSConf-2009
[8] https://developers.google.com/v8/embed
[9] http://nodejs.org/api/
[10] http://zgadzaj.com/benchmarking-nodejs-basic-performance-tests-against-apache-php 
[11] http://strongloop.com/strongblog/whats-new-in-node-js-v0-12-cluster-round-robin-load-balancing/
[12] https://github.com/joyent/node/commit/e72cd41?__utma=108008000.708719925.1395624487.1395790930.1395792961.3&__utmb=108008000.2.10.1395792961&__utmc=108008000&__utmx=-&__utmz=108008000.1395792961.3.3.utmcsr=google%7Cutmccn=(organic)%7Cutmcmd=organic%7Cutmctr=(not%20provided)&__utmv=-&__utmk=101010433

 

Tagged with: , , , , ,
Posted in Technical Note

AWS, 클라우드 서비스로 백엔드 서버 준비

소셜 네트워크 어플을 개발할 때, 필연적으로 사용자들의 데이터를 처리를 위해서 백엔드 서버(backend server)를 사용하게 된다. 요즘 백엔드 서버는 RESTful API 형태로 요청을 받고 사용자 어플에게 응답한다. 나 역시 백엔드 서버가 필요했기에 간단하게 서버 구성을 시도해보았다.

백엔드 서버를 돌리기(running) 위해서는 크게 하드웨어 서버 장비와 이 장비 위에서 수행될 소프트웨어 서버 솔루션이 필요하다. 요즘은 많은 경우 비싼 서버 장비를 구매하여 IDC 센터에서 돌리지 않고, Amazon Web Service(이하 AWS), Joyent, goole AppEngine 같은 클라우드 서비스를 이용해서 간접적으로 서버를 돌리는 것 같다. 장비를 직접 구매하는 것에 비해 초기 자본이 거의 발생하지 않기 때문에 대부분의 스타트업은 클라우드 서비스를 선택하는 추세이고, 서버 스케일링이나 로드밸런싱, 하둡기반 분산처리가 안정적으로 이뤄지기 때문에 기존 업체들도 클라우드 서비스로 이동을 하고 있다. AWS 대표적인 사례는 아래와 같다.

http://aws.amazon.com/ko/solutions/case-studies/

클라우드 서비스 시장에서는 AWS가 대략 25%로 1위이다. 구글이나 IBM 등 경쟁 업체들에 비해 일찍 시작한 이유도 있겠지만 단지 그 이유때문이겠는가. 클라우드 서비스의 질이나 사용성이 다른것들에 비해 편리하거나 다른 무언가가 있지 않을까 늘 궁금했었다. 그래서 이번에 클라우드 서비스를 선택할 때 주저 없이 AWS를 사용해보기로 결심했다. AWS에는 아래와 같이 상당히 많은 하위 서비스들이 존재한다. 와우..

Image

이 중 Amazon EC2 가이드를 따라 했더니 내 서버가 금방 만들어졌다.

http://docs.aws.amazon.com/gettingstarted/latest/computebasics-linux/getting-started.html

  • AWS 계정 생성,
  • Auto Scaling 커맨트라인 툴 설치,
  • Debian linux AMI로 가상서버 인스턴스 생성,
  • 서버 시큐리티 그룹 생성,
  • custom AMI 생성,
  • Load Balancer 생성,
  • Auto Scaling 으로 서버 생성/종료하기

Image

가상화 기술을 이용해서 AWS가 내가 있는 동북 아시아 쪽에 위치한 서버장비에서 가상서버 인스턴스를 한개 생성해주었다. 아마존 서버 인프라는 전세계 8개 지역(Region)에 흩어져 있고, 각 지역은 몇개의 가용존(Availability Zone)을 가지고 있다.  가용존에 실제 아마존 서버들이 위치해 있다. 한국이 있는 동북 아시아의 지역(Region)은 도쿄가 본부이고(에잇!), 가용존은 2개가 있다.

AMI (Amazon Machine Image) 는 가상서버 인스턴스가 실행될 때 바라보는 운영체제 이미지인데 아마존이 제공하는 것 외에도 AWS Marketplace 에 수많은 AMI가 올라와있다. AWS를 사용하는 개인 또는 기업, 커뮤니티들이 각자의 용도에 맞게 서버 설정을 해놓은 것들을 AMI 로 만들어서 공유가 되고 있다. 가령 설치형 WordPress AMI 이나 다음 포스팅 주제인 node.js + mongo db AMI 등을 들수있다. 오픈소스 기반의 AMI는 대부분 무료이다. 나는 debian7 wheezy AMI로 서버 인스턴스를 생성했다. 서버 인스턴스마다 public DNS와 IP가 할당되지만 기본적으로 인스턴스를 재부팅하면 이 값들은 바뀐다. 그래서 서버 인스턴스가 고정 IP를 받으려면 먼저 Elastic IP 서비스로 요청해야 한다.

Auto Scaling과 Load Balancer 는 각각 서버 인스턴스 갯수를 조절하고 트래픽을 분산시키는 서비스를 의미한다. 소셜 네트워크 어플의 특성상 갑자기 많은 유저들이 접속을 할 수가 있는데, 그런 트래픽을 빠르게 처리할 방법이 필요하다. 클라우드 환경에서는 AMI같은 이미지를 이용해 가상 서버 인스턴스를 쉽게 생성하거나 반대로 제거할 수 있기 때문에 트래픽 상황에 맞게 인스턴스 갯수를 줄였다 늘였다 할 수 있다. 이것이 중요한 이유는 사용한 만큼 내는 AWS의 가격정책 때문이다. 트래픽이 없는데도 서버 인스턴스를 많이 생성해놓는다면 불필요하게 요금이 청구될 수 있다. 사용자는 aws 커맨드를 이용해서 Auto Scaling 그룹과 정책을 지정할 수 있으며 이후엔 아마존 클라우드 환경이 이 설정에 따라 auto scaling을 수행하게 된다. 딱 필요한 만큼 서버 인스턴스를 돌리자.

ssh를 이용해서 각 서버 인스턴스에 접속할 수 있으며 sudo 그룹의 유저 또는 루트로 로그인이 되니 각종 패키지를 설치/삭제할수 있고 서버 환경을 자신의 서비스에 맞게 설정할 수 있다. 테스트를 위해서 apache2, php5, php5-mysql extension, mysql을 설치한 후 WordPress 최신 버전을 설치해봤다. 뭐 예상대로 잘 나왔다.

Image

소셜 네트워크 어플을 준비하는 단계에서는 서버 인스턴스 하나로 충분히 기능 검증이 가능하기에 Auto Scaling과 Load Balancer는 어플서비스 런칭 직전으로 미루어도 될 것 같다. 클라우드 서비스를 이용하기 때문에 개발 초기에 트래픽을 예측하고 미리 서버 및 네트워크 환경을 구성해야 하는 머리 아픈 상황을 피할 수 있다. 참으로 스타트업이 활성화되기에 좋은 환경이다.

직접 AWS의 일부 서비스를 써보니 사용성과 속도면에서 매우 만족스러웠다. 많은 스타트업과 기업들이 이용하는 이유를 어느 정도 이해할 것 같다. 당분간은 내 AWS 서버 인스턴스에 많이 접속할 듯.

AWS 관련하여 유용한 자료 몇개를 링크한다.

http://docs.aws.amazon.com/gettingstarted/latest/computebasics-linux/getting-started.html

http://opentutorials.org/course/608/3004

http://bcho.tistory.com/543

Tagged with: , , , , , , , ,
Posted in Technical Note

iOS 어플 개발에 대한 몇가지 생각

ios_xcode

애플 개발자 프로그램에 등록한 기념으로 iOS 어플 개발에 대해 몇가지 생각을 나누고자 한다.

1. Objective-C

Objective-c로 개발됐던 NeXT를 기반으로 탄생한 OS X 및 iOS는 objective c 기반의 툴과 프레임워크 (또는 라이브러리)를 갖고 있다. 그렇다보니 이들 운영체제에서 동작하는 어플을 개발할 때 기본적으로 objective-c 라는 언어를 사용한다. 물론 크로스 플랫폼 툴 예를 들면, Xamarin을 이용해 C#을 쓰거나 Apache Cordova를 이용해 HTML5로도 iOS 어플을 개발할 수는 있다. 그렇지만 최근 몇년간 objective c의 사용률이 계속 증가하는 걸 보면 아직까지는 objective c로 어플 개발하는 것이 대세인 것 같다.

objective c는 언어 이름에서도 알수 있듯이  객체지향적 요소가 혼합된 c 언어라고 볼 수 있지 않을까 싶다. 필자가 objective c를 개인적으로 익혀보니 c언어와 c++,  java, python 같은 객체지향 언어를 사용할 줄 아는 개발자라면 objective c를 익히는데 그리 큰 시간이 들지 않을 것이다. 클래스 표기 방법이나, 객체 생성 삭제, 메소드 호출 방법 등 문법적 차이만 있을뿐 기본적으로 다른 객체지향 언어와 용도는 같다.

물론 objective c 만의 독특한 기능들도 있는데 유효하지 않은 객체에 접근할 때 0을 리턴해주는 nil 객체라던지 (c,c++에선 segfault로 프로그램이 종료-_-;) 기존 클래스를 변경하거나 상속하지 않고도 해당 클래스에 메소드를 추가해서 쓸 수 있는 카테고리(Category) 기능이 필자에겐 인상적이었다. 카테고리는 개발 초기보다 릴리즈 이후 기능 추가를 해야 할 경우에 유용하지 않을까 생각된다.

objective c 를 위한 자료는 구글링을 해보면 정말 많이 찾을 수 있지만, 필자가 본 것중 괜찮은 자료 몇개를 공유한다.

http://www.otierney.net/objective-c.html.ko

http://cocoadevcentral.com/d/learn_objectivec

2. xcode 그리고 cocoa touch storyboard

많은 개발자들이 칭찬하는 애플의 xcode ide. 내가 써보기 전에는 공감할 수 없었다. 필자는 소프트웨어를 개발하면서 SDK나 IDE 같이 UI 가진 툴을 거의 쓰지 않았다. 필자는 리눅스 기반에서 코딩을 해온지라 커맨드라인 툴들인 vi, ctag, gnu 툴체인, gdb 정도만 있으면 개발, 디버깅엔 문제가 없었다. 무엇보다 마우스를 옮기며 클릭하는 것에 비해 움직임이 적었기 때문에 내가 생각하는 바를 좀 더 빨리 수행할 수 있었기에 커맨드라인 환경을 더 선호했었다. 필자가 이런 배경을 가지고 있었던터라 xcode 이용해 코딩하는거에 약간의 거부감이 있었으나 며칠을 사용해보고 나서 생각이 달라졌다. xcode. 정말 잘 만든 ide 이다!

내가 제대로된 어플을 개발해본적은 없지만, xcode 상에서 직관적으로 UI를 만들고 로직을 붙일 수 있었다. storyboard 를 이용하면 버튼, 라벨, 네비게이션 바 등 iOS7 위젯들을 쉽게 추가할 수 있다. storyboard는 단순히 스케치북이라 생각하면 될 것 같다. 생각하는 어떤 UI가 있다면 storyboard 상에서 드래그 앤 드랍으로 덕지덕지(?) 붙인 후 시뮬레이터로 구동시켜서 어떤 느낌을 주는지 확인해볼 수 있다. 물론 사용자와 인터랙션을 해야 하는 시나리오, 가령 버튼을 클릭하면 배경화면이 바뀌는 등의 일을 하려면 storyboard와 소스코드를 연동해야만 가능하다.

xcode에서 project 하나를 만드면 xcode 가 자동으로 기본 소스 파일을 생성하면서 템플릿 코드들을 추가해준다. 템플릿 코드에는 어플의 라이프 사이클 관련 이벤트 핸들러들이 들어 있으며 템플릿 타입(게임 등)에 따라 추가적인 템플릿 파일을 생성해준다. 특정 이벤트 발생시 수행해야 할 작업이 있다면 iOS 어플 개발자는 이들 핸들러를 구현해주어야 한다. 템플릿이 생성해주는 것은 정말 기본적인 몇가지 밖에 되지 않기 때문에 개발자가 자신의 어플을 만들면서 각종 위젯과 이에 따른 이벤트 처리들을 직접 추가 및 구현해주어야 한다.

storyboard에 추가한 위젯 (버튼, 텍스트 입력 등)와 소스코드를 연결할때 핵심 부분을 간략히 정리하자면 다음과 같다. 우선 소스코드에 storyboard상에 붙여놓은 해당 위젯 타입으로 멤버 변수를 선언해주어야 한다. 이 때 storyboard 상의 위젯을 가리킨다는 의미로 변수형 앞에 IBOutlet 지시어를 꼭 넣어주어야 한다. 그리고 storyboard 내 Outlets 리스트에서 해당 IBOutlet 변수를 선택하여 연결하고 싶은 위젯에 Ctrl + 드래그앤드랍를 해주면 storyboard 상 위젯이 소스코드 상의 멤버 변수라는 것을 xcode(정확히는 Interface Builder)에게 알려주게 된다. 이후 부터는 소스코드 상에서 해당 위젯의 속성을 변경하거나 해당 위젯이 제공하는 특정 이벤트 핸들러들을 구현하여 처리해줄 수 있다.

반대로 특정 위젯 위에서의 사용자의 터치나 제스처가 발생했을 때 무언가 처리를 해주어야 한다면, 소스코드에서 IBAction 지시자를 넣어 메소드를 생성하고 이를 storyboard 내 Recieved Actions 리스트에서 해당 IBAction 메소드를 선택하여 연결하고 싶은 위젯에 Ctrl + 드래그앤드랍를 해주면 된다. 이 때 받을 이벤트나 제스처를 선택해야 할 수 있도록 팝업이 뜨고 그 중 하나를 선택하면 된다. 이를 통해 개발자는 사용자의 터치나 제스처 입력에 대해 처리할 수 있다.

그리고 뜬금없지만.. xcode 내 editor 는 자동 완성 기능이 아주 좋은 것 같다. vi에서는 ctag와 연동해 별도 설정을 해야만 했던 기능인데 xcode에서는 디폴트로 동작한다.

3. 개발 참고 문서

objective c와 xcode 를 이용해서 iOS 어플을 어떻게 만드는지 경험해보고 싶다면, 아래 애플의 개발자 문서를 추천한다. 필자는 인터넷에 떠도는 여러 튜토리얼을 읽어보았는데 애플 문서 만큼 iOS를 개발하는데 필요한 여러 내용들을 잘 정리한 건 없었던 것 같다.

https://developer.apple.com/library/ios/referencelibrary/GettingStarted/RoadMapiOS/index.html#//apple_ref/doc/uid/TP40011343

그 외 아래 튜토리얼도 지루하지 않고 꽤나 괜찮았다.

http://www.raywenderlich.com/tutorials

본격적으로 iOS 어플 개발을 하게 된다면 API 등 참고해야 할 내용들이 아주 많아질텐데 애플 개발자 사이트에 문서화가 너무나도 잘 되어 있어서 다행이다.

https://developer.apple.com/library/ios/navigation/

마지막으로 개발자 포럼 사이트이다. stackoverflow 에서도 왠만한 답은 얻을 수 있겠지만 여기 커뮤니티도 꽤나 크니 활용할만하다.

https://devforums.apple.com

4. 3rd party 개발자 그리고 철학

나는 타이젠 플랫폼 구체적으로 web framework 을 개발하던 사람으로서 외부에 공개되는 기능을 정의하고 개발할 때 3rd party 개발자 입장에서 생각하면서 만드려고 노력했었다. 3rd party 개발자라면 이런 기능이 필요할까. API의 원형은 어떤게 더 효율적일까. 문서는 충분히 만들어졌나. 개발자들이 이것을 이용해 내가 의도한대로 사용해줄까. 이 기능 때문에 개발자가 몰려들까. 이런 류의 질문을 스스로 던지곤 했었다. 타이젠 에코시스템이 아직 자리 잡지 않은 상황이고 개발자 풀이 적은 상황에서 정답을 얘기해줄 수 있는 사람이 사실 주변에 없었다. 나 역시 3rd party 개발자로서의 경험이 없기 때문에 스스로 던지는 질문에 늘 예측만 할 뿐이었다.

며칠간 iOS 개발을 위해 환경을 설정하고 기본 기술을 익히고 사용해보면서, 아 이걸로 제대로 된 어플 하나 만들 수 있겠다 하는 생각이 들었다. iOS의 심플한 UX를 추구하면서 사람들에게 도움이 되는 무언가를 나눌 수 있다는 사실에 의욕이 생긴다. 3rd party 개발자 입장이 되보니 플랫폼에서 중요한게 무엇인지 하나씩 알아가게 되는 것 같다. 에코시스템, 개발환경, 개발 문서 그리고 커뮤니티 모두 중요하지만, 개발자들의 참여를 자발적으로 이끌어낼 수 있는 철학이 무엇보다 중요하지 않을까 싶다.

자, 그럼 이제부터 시작해보자.

Posted in Technical Note