스토리 홈

인터뷰

피드

뉴스

조회수 3620

Node 서버로 Slack 메신저 자동화하기

Overview백엔드 업무를 하면 데이터 요청과 CS문의를 자주 받습니다. 날짜만 다를 뿐 같은 유형의 문의가 대부분이죠. 결국 반복적인 업무를 효율적으로 처리할 수 있는 방법을 고민했고, 사내 메신저로 사용하는 Slack의 몇 가지 API를 사용하기로 했습니다.1. 알림봇 만들기비즈니스 로직을 만들다 보면 정해진 시간에 맞춰 작업을 해야 하는 경우가 발생합니다. Slack 메신저에 로그온한 상태에서 스케줄러를 이용해 지정한 시간에 Slack 메세지를 전송해보겠습니다.1)Slack API 유저토큰 받기Slack API에 사용할 해당 계정의 토큰을 받아야 합니다. Slack 가입 절차 및 채널 생성은 생략하겠습니다.https://api.slack.com/custom-integrations/legacy-tokens 접속합니다.Legacy tokens 메뉴에서 아래로 스크롤을 내려 토큰 생성버튼을 누릅니다.계정 패스워드를 입력하여 확인하면 토큰을 생성할 수 있습니다.생성된 토큰을 복사하여 저장합니다.2)Node.js를 이용한 알림봇 구현2-1.Node.js 설치Node.js 다운로드 해당 사이트에서 운영체제 환경에 맞는 파일을 다운받아 설치2-2.프로젝트 생성해당 프로젝트 폴더로 이동 후 명령어 실행$ npm init --yes // package.json 파일 생성2-3.Slack 연동2-3-1. slack-node 모듈 설치$ npm install slack-node --save2-3-2. 유저토큰을 이용하여 해당채널에 메세지 전송const Slack = require('slack-node'); // 슬랙 모듈 사용 apiToken = "발급받은 유저토큰"; const slack = new Slack(apiToken); const send = async(message) => { slack.api('chat.postMessage', { username: 'dev-test', // 슬랙에 표시될 봇이름 text:message, channel:'#general' // 전송될 채널 및 유저 }, function(err, response){ console.log(response); }); } send('메세지 내용'); 지정한 채널에 메시지가 발송됩니다. 하지만 이와 같은 방법은 유저 토큰이 공개 코드에 노출되기 때문에 보안이 취약할 수 있습니다. 유저 토큰이 필요 없어도 해당 채널에 URL을 생성하는 WebHooks API를 이용하여 메시지를 전송해보겠습니다.3) Incoming WebHooks APIWebHooks는 유저 토큰 대신 Webhook URL을 생성해 HTTP 통신으로 Slack 메세지를 전송할 수 있습니다. 다양한 메시지 형식을 지원하고 게시할 사용자 이름 및 아이콘 등을 통합적으로 관리할 수 있는 장점을 가지고 있습니다.3-2. Webhook URL 생성하기Slack 해당채널에서 Add an app 클릭검색필터에 WebHooks 검색Incoming WebHooks 추가채널 선택 후 Incoming WebHooks 생성생성된 Webhook URL 복사하여 저장해당채널에 생성되었는지 확인봇이름 및 아이콘등 기본 설정 변경하여 저장curl 사용 예제$ curl -s -d "payload={'text':'메세지 내용'}" "Webhook URL"Webhook URL 사용 중인 모든 메시지는 통합적으로 기본 설정이 변경된 걸 확인할 수 있습니다.다양한 형식의 메세지를 전송해보겠습니다.const Slack = require('slack-node'); // 슬랙 모듈 사용 const webhookUri = "Webhook URL"; // Webhook URL const slack = new Slack(); slack.setWebhook(webhookUri); const send = async(message) => { slack.webhook({ text:"인터넷 검색 포털 사이트", attachments:[ { fallback:"링크주소: ", pretext:"링크주소: ", color:"#00FFFF", fields:[ { title:"알림", value:"해당링크를 클릭하여 검색해 보세요.", short:false } ] } ] }, function(err, response){ console.log(response); }); } 다양한 형태의 메시지를 전송할 수 있습니다.4) Schedule 연동이제 스케줄러를 이용하여 지정한 시간에 메세지를 전송해보겠습니다.4-1. node-schedule 모듈 설치node-schedule는 Node.js 작업 스케줄러 라이브러리입니다.$ npm install node-schedule --savenode-schedule 코드 작성const schedule = require('node-schedule'); // 스케줄러 모듈 사용 // rule-style 사용 var rule = new schedule.RecurrenceRule(); rule.dayOfWeek = new schedule.Range(3,4); rule.hour = 19; rule.minute = 50; schedule.scheduleJob(rule, function(){ console.log('rule 방식'); }); // cron-style 사용 schedule.scheduleJob('50 19 * * *', function(){ console.log('cron-style 방식'); }); 취향에 맞는 스타일로 사용하면 됩니다.5) 지정 시간에 메세지를 전송하는 알림봇을 작성해보겠습니다.const Slack = require('slack-node'); // 슬랙 모듈 사용 const schedule = require('node-schedule'); // 스케줄러 모듈 사용 const webhookUri = "Webhook URL"; // Webhook URL const slack = new Slack(); slack.setWebhook(webhookUri); const send = async(message) => { slack.webhook({ text:message, attachments:[ { fallback:"구글드라이브: ", pretext:"구글드라이브: ", color:"#00FFFF", fields:[ { title:"[알림]", value:"해당링크로 접속하여 작성해 주세요.", short:false } ] } ] }, function(err, response){ console.log(response); }); } schedule.scheduleJob('5 19 * * *', function(){ send('업무보고 보내셨나요?'); }); 업무보고 시간을 미리 알려주는 알림봇2. 대화봇 만들기업무 문서는 주로 구글 독스와 같은 온라인 문서로 관리하고 있습니다. 하지만 매번 구글 드라이브에서 문서를 찾는 건 정말 귀찮은 일입니다. 번거로운 건 딱 질색입니다. Slack API를 이용해 관련된 키워드를 입력하면 링크 주소를 바로 받을 수 있는 대화봇을 만들어 보겠습니다.1) Slack API Bots 토큰 받기Slack API에 사용될 Bots 토큰을 받아야 합니다.https://{App Name}.slack.com/apps 에 접속합니다.Bots 추가Bots Api 토큰을 복사해 저장합니다.설정한 봇이름으로 Apps 영역에 자동으로 추가됩니다.2) 구글독스 대화봇 코드 작성2-1. botkit 모듈 설치$ npm install botkit --save2-2. 코드 작성const botkit = require('botkit'); // 봇 모듈 사용 const Slack = require('slack-node'); // 슬랙 모듈 사용 const controller = botkit.slackbot({ debug: false, log: true }); const botScope = [ 'direct_message', 'direct_mention', 'mention' ]; controller.hears(['업무보고'], botScope, (bot, message) => { bot.reply(message, '업무보고 링크주소'); }); controller.hears(['가이드', 'guide', '튜토리얼'], botScope, (bot, message) => { bot.reply(message, '가이드 링크주소'); }); controller.hears(['api', '명세서'], botScope, (bot, message) => { bot.reply(message, 'api명세서 링크주소'); }); controller.hears(['일정', '일정관리'], botScope, (bot, message) => { bot.reply(message, '일정관리 링크주소'); }); controller.hears(['비품', '비품정리'], botScope, (bot, message) => { bot.reply(message, '비품관리 링크주소'); }); controller.spawn({ token: '발급받은 봇 토큰' }).startRTM(); 지정한 키워드를 입력하면 해당 링크가 수신 됩니다.3) 데이터문의 대화봇 코드 작성데이터 요청 시 결과 데이터를 보내주는 대화봇을 만들어 보겠습니다. 일단 먼저 데이터문의 전용 Bots을 생성합니다.3-1. Python 연동 요청한 데이터는 Mysql 데이터를 조회해서 전송합니다. 그러면 Mysql 을 연동해야겠죠? Node.js에서도 직접 mysql 연결할 수 있지만, 기존 프로젝트가 Python으로 구현되어 있어 Python을 실행해 필요한 데이터를 추출해보겠습니다.3-2. python-shell 모듈 설치Node.js에서 Python 실행가능하도록 모듈을 설치$ npm install python-shell --save3-3. Mysql Sample Table3-4. 회원테이블에 저장된 가입일시 기준으로 몇일전에 가입한 회원을 추출하여 전송하는 코드 작성해 보겠습니다.const botkit = require('botkit'); // 봇 모듈 사용 const Slack = require('slack-node'); // 슬랙 모듈 사용 const ps = require('python-shell'); // 파이썬 쉘 모듈 사용 // 몇일 전 날짜 구하기 function getDaysAgo(dayNo = 0) { let nowDate = new Date(); let tempDate = nowDate.getTime() - (dayNo * 24 * 60 * 60 * 1000); nowDate.setTime(tempDate); let getYear = nowDate.getFullYear(); let getMonth = nowDate.getMonth() + 1; let getDay = nowDate.getDate(); if (getMonth < 10 xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed xss=removed> 3-5. Python 코드 작성 # -*- coding: utf-8 -*- import sys import pymysql // mysql 접속 db = pymysql.connect('hostname', user='', passwd='', db='', charset='utf8') cursor_db = db.cursor() exe_query = "SELECT MEMBER_NAME FROM MEMBER_INFO WHERE MEMBER_REGIST_DETE >= '{}' ORDER BY MEMBER_NO ASC ".format(sys.argv[1]) cursor_db.execute(exe_query) all_rows = cursor_db.fetchall() for idx, row in enumerate(all_rows): print(row[0])     지정한 며칠 전에 가입한 회원 이름이 전송됩니다.   로그도 정상적으로 출력됩니다. 3. Node.js 프로세스 관리를 위한 pm2 모듈 설치 Node.js 는 비동기 I/O를 지원하며 단일 스레드로 동작하는 서버입니다. 비동기식 방식이지만 처리하는 Event Loop는 단일 스레드로 이루어져 있어 처리 작업이 오래 걸리면 전체 서버에 영향을 줍니다. 그래서 pm2를 이용해 프로세스별로 상태를 관리해야 합니다. 1) pm2 모듈 설치$ npm install pm2 -g2) 자주사용하는 pm2 명령어 pm2 list -> 실행중인 프로세스 확인pm2 start {node 파일} -> 시작pm2 stop {id or App name} -> 중지pm2 delete {id or App name} -> 삭제pm2 show {id or App name} -> 상세정보pm2 restart {id or App name} -> 재시작pm2 kill -> pm2 종료pm2 logs {id} -> id 앱의 로그 확인 3) pm2 실행화면$ pm2 start bot.js   프로세스별로 앱 이름, 버전, 상태, cpu 및 memory 사용량이 표시됩니다.$ pm2 show 0   해당 프로세스의 상세 정보를 확인할 수 있습니다. Conclusion 지금까지 Node.js 로 유용한 Slack 메신져 API를 알아봤습니다. 반복적인 업무를 하나씩 줄이다 보면 분명 일의 능률을 높아집니다. 하지만 무분별한 자동화는 서버의 부하를 증가시키기 때문에 꼭 필요한지 확인하고 선택하길 바랍니다. 오늘은 여기까지 글곽정섭 과장 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만  
조회수 1951

Docker, NodeJS, Nginx! 너로 정했다!

편집자 주아래와 같이 용어를 표기하기로 저자와 협의함Docker, NodeJS, NginxOverview안녕하세요. 칼 같은 들여쓰기에 희열을 느끼는 브랜디 개발자 강원우입니다! 서버를 운영해본 개발자라면 Fatal 에러, 아웃오브메모리 에러, 또는 전날 흡수한 알코올로 인해 손을 떨다가 한 번쯤 서버를 요단강 너머로 보내봤을 겁니다. 만약 테스트 서버였다면 잠시 마음을 가다듬으면 되지만, 현재 상용 서비스 중인 서버라면 얘기는 달라집니다.님아, 그 강을 건너지 마오!이런 간담이 서늘해지는 경험은 저 하나로 족합니다. 그래서 고군분투했던 지난 날을 되돌아보면서 빠르고 안정적이며, 죽어도 죽지 않는 좀비 같은 서버 구축 방법을 쓰려고 합니다.준비물서비스를 운영할 때 가장 중요하게 여겨야 하는 건 역시 안정성입니다. 이번 글에서는 오래 전부터 개발 세계의 뜨거운 감자였던 Docker와, 단일 스레드와 이벤트 루프로 태생적으로 심플하고 민첩한 NodeJS, 마지막으로 고성능을 목표로 개발된 Nginx를 활용하겠습니다.1. DockerDocker는 컨테이너 기반의 오픈소스 가상화 플랫폼입니다. 대표적으로 LXC(Linux Container)가 있습니다. 화물 컨테이너처럼 어떠한 일련의 기능을 완전히 격리된 소프트웨어 환경에서 작동하게 만드는 기술을 말합니다.OS 가상화와 별반 다를 게 없는 것 같지만 소프트웨어적으로 작동한다는 차이가 있습니다. 다시 말해, 현재 OS의 자원을 그대로 사용하기 때문에 하이퍼 바이저가 가상환경을 위해 가상의 커널을 만드는 오버헤드가 거의 없다는 것이죠.이미지와 속도도 차이를 보입니다. 완벽하게 구성한 세팅을 그대로 이미지화할 수 있고, 해당 이미지는 Docker 위에서 완벽히 동일하게 동작하는 걸 보장합니다. 해당 이미지로 컨테이너를 제작할 땐 1~2초면 새로운 컨테이너가 생겨날 정도로 엄청나게 빠른 속도도 자랑합니다. 1)또한 Docker는 자주 사용되는 다양한 이미지를 퍼블릭 레포지토리에 공유해 사용할 수 있기도 합니다. 양파도 아닌데 특징이 계속 나오죠? 다음 글에서 Docker의 특징을 더 자세히 다루겠습니다.Docker는 리눅스만 지원했었지만, 요즘은 Docker for Windows와 Docker for Mac으로 거의 모든 OS에서 사용할 수 있습니다. 2) Docker 설치 링크는 윈도우와 맥으로 나뉘어져 있습니다. 리눅스는 아래를 참고하세요.curl -fsSL https://get.docker.com/ | sudo sh 2. NodeJSNodeJS는 구글이 구글 크롬에 사용하려고 제작한 V8 오픈소스 자바스크립트 엔진을 기반으로 제작된 자바스크립트 런타임입니다. NodeJS에는 몇 가지 특징이 있습니다.단일 스레드입니다.비동기 방식입니다.이벤트 루프를 사용합니다NPM이라는 끝내주는 동반자가 있습니다.비유하자면 예전엔 낡은 곡괭이로 큰 돌을 캐내려고 수십 명의 인부가 달라 붙었는데, 지금은 육중한 포크래인으로 거대한 돌을 쑥! 뽑아버리는 것과 비슷합니다. 굉장히 효율적이죠. NodeJS는 단일 스레드의 장점을 극대화하려고 이벤트 루프를 통해 모든 처리를 비동기로 수행합니다. 서버 사이드의 묵직한 CPU들이 빠르게 일을 처리하고 이벤트 루프에 등록된 일을 감지해 다음 작업을 빠르게 수행하는 방식입니다.마지막으로 NPM(Node Package Manager)은 NodeJS에서 사용할 수 있는 다양한 모듈을 관리해주는 프로그램입니다. 도커와 상당히 유사합니다. NodeJS에서는 무언가 기능을 만들기 전에 NPM을 먼저 뒤져보라는 말이 있을 정도로 풍부한 모듈 생태계가 구성되어 있습니다. 이는 로깅이나 날짜 계산 등 생각보다 까다로운 것들을 가져다 사용할 수 있게 도와주기 때문에 개발이 빨라집니다. NodeJS 설치링크는 여기를 클릭하세요. 이 글의 예제에서는 NodeJS의 현재시점 LTS인 codename Carbon버젼을 사용합니다!8.x 버젼이 Active LTS 상태입니다.LTS은 Long Term Support의 약자로 가장 오랜기간 지원하는 버전입니다.우선 서비스 구성을 위해 간단한 NodeJS 어플리케이션을 작성해보겠습니다.첫째, packge.json를 작성합시다.{   "name": "nodejs_tutorial_server",   "version": "0.0.0",  "private": true,   "scripts": {     "start": "node nodejs_tutorial_server.js"   },   "description": "NodeJS Tutorial Server",   "author": {     "name": "WonwooKang"   },   "dependencies": {     "express": "^4.16.3",     "uuid": "^3.2.1"   } } nodejs_tutorial_server.js 파일을 메인으로 실행합니다. HTTP Request를 처리하려면 express를 사용해야 하며, 서버를 구분하려면 uuid모듈이 필요합니다.둘째, package.json의 의존 파일들을 설치합시다.npm install npm install 전npm install 후셋째, 간단한 웹 어플리케이션을 작성합시다.var express = require('express'); var app = express(); const port = 3000;  var server = app.listen(port, function () {     console.log("Express server has started on port : "+port);  });  app.get('/', function (req, res) {     res.send('Hello?');  }); 넷째, package.json의 script start 구문을 실행하여 서버를 로드합시다.npm start 3000번 포트로 서버가 시작되었습니다!접속해볼까요?잘 접속됩니다.그런데 수정할 때마다 서버를 매번 다시 띄우면 귀찮을 겁니다. 이럴 땐 nodemon 모듈을 사용합시다. nodemon은 Nodejs의 파일이 수정되는 걸 감지해 자동으로 리로드해주는 편리한 도구입니다.nodemon설치npm install nodemon -g package.json script 변경"scripts": {     "start": "nodemon nodejs_tutorial_server.js"   }, nodemon 실행확인을 위해 약갼의 수정//nodejs_tutorial_server.js 수정 app.get('/', function(req, res) {     res.send('Hello Nodemon');  }); nodemon을 통해 어플리케이션이 실행된 모습파일수정 후 저장했을 때 자동 감지한 모습서버 잘 떴습니다!성공적으로 단 하나의 GET 요청을 처리할 수 있는 심플한 NodeJS 기반 웹 어플리케이션을 완성했습니다. 이제 웹 어플리케이션을 Docker Container위에서 구동해봅시다!3. Docker로 NodeJS Express 서버 구동하기이제 Docker Container위에서 NodeJS서버를 구동할 건데요. 그러려면 우선 Dockerfile을 작성해야 합니다. 물론 Docker의 이미지를 당겨 받고, 컨테이너를 생성하고, 또 컨테이너를 실행해서 Attach하고, 필요한 파일들을 밀어넣는 등 귀찮은 방법도 있습니다. 하지만 개발자에게 이것은 힘든 작업이므로 Dockerfile을 적극 활용합시다. (Dockerfile의 D는 대문자여야 합니다! 꼭이요)Node 도커 이미지에 어플리케이션 파일을 추가해 실행하는 Dockerfile 작성하기FROM node:carbon MAINTAINER Wonwoo Kang [email protected] #app 폴더 만들기 - NodeJS 어플리케이션 폴더 RUN mkdir -p /app #winston 등을 사용할떄엔 log 폴더도 생성 #어플리케이션 폴더를 Workdir로 지정 - 서버가동용 WORKDIR /app #서버 파일 복사 ADD [어플리케이션파일 위치] [컨테이너내부의 어플리케이션 파일위치] #저는 Dockerfile과 서버파일이 같은위치에 있어서 ./입니다 ADD ./ /app #패키지파일들 받기 RUN npm install #배포버젼으로 설정 - 이 설정으로 환경을 나눌 수 있습니다. ENV NODE_ENV=production #서버실행 CMD node nodejs_tutorial_server.js Dockerfile 내용은 node:carbon에서 :carbon이 NodeJS의 이미지 버전 Tag 입니다.Dockerfile을 통해 docker image 빌드하기docker build –tag 레포지토리명: 태그 Dockerfile 경로docker build --tag node_server:0.0.1 [Dockerfile이 위치하는 경로] 호오... 게이지가 마구마구 차오르는군요?build가 완료된 화면입니다. Dockerfile의 내용 순서가 각 Step별로 진행된 것을 알 수 있습니다.빌드 결과 생성된 이미지 확인하기docker images 빌드 명령어에서 입력했던 버전 태그까지 잘 입력된 것을 알 수 있습니다.NodeJS Carbon 이미지를 기반으로 한 node_server 이미지를 제작했습니다. 사이즈는 둘이 합쳐 1Gb가 넘을 것 같지만 실제로는 변경된 부분만 저장됩니다. 그러므로 node_server 이미지의 크기는 6~10Mb 정도입니다.생성된 이미지로 컨테이너 만들기컨테이너 생성 명령어는 아래와 같습니다.docker create --name [서버명] -p [외부 포트:컨테이너 내부포트] [이미지명:버전태그] 주의할 점이 있습니다. 포트번호 바인딩 중 왼쪽은 우리가 접속할 실제 포트이고, 오른쪽은 컨테이너 내부의 NodeJS서버 할당 포트가 된다는 것입니다. 공유기의 포트포워딩 설정과 같습니다.docker create --name NODE_SERVER_0 -p 3000:3000 node_server:0.0.1 알 수 없는 코드가 생성되었습니다. 응?컨테이너 확인하기생성한 컨테이너를 확인해볼까요?docker ps 어.. 없잖아?옵션을 추가합니다.docker ps -a 나타났다!docker ps 명령어는 현재 실행 중(STATUS:Up)인 컨테이너의 목록을 보여줍니다. -a 옵션은 실행하지 않는 모든 컨테이너를 보여줍니다. 위의 이미지에서 node_server:0.0.1이미지로부터 NODE_SERVER_0 이라는 이름으로 2분 전에 생성되었다는 걸 알 수 있습니다. 3)컨테이너 실행하기docker start NODE_SERVER_0 다시 확인하기docker ps 19초 전에 Up상태가 되었다는 걸 알 수 있다.외부 3000번 포트 -> 내부 3000번 포트로 연결되었습니다. 서버도 실행되었고요! 이제 접속해볼까요?내용도 안 바꾸고 새로고침도 빨라서 뜬 건지 잘 모르겠군요. 내용을 수정해서 다시 확인하겠습니다.//nodejs_tutorial_server.js 수정 app.get('/', function (req, res) {     res.send('Hello I\'m In Docker Container Now!');  }); 파일 변경해서 다시 확인하기//버전 태그도 0.0.2로 업해주고 docker build --tag node_server:0.0.2 [Dockerfile위치] 잘 생성되었습니다.//이미지가 잘 생성되었는지 확인하고 docker images 0.0.2가 나타났습니다.//기존 컨테이너를 삭제합니다. -f 옵션은 실행중인 컨테이너도 강제로 삭제하겠다는 뜻입니다.  docker rm -f NODE_SERVER_0 // 잘지워졌나 확인하고  docker ps -a 잘 지워집니다.//0.0.2 버젼 이미지로 컨테이너를 다시 생성합니다.  docker create --name NODE_SERVER_0 -p 3000:3000 node_server:0.0.2   //서버를 실행합니다. docker start NODE_SERVER_0 잘 실행됩니다.이제 다시 접속해봅시다.안녕! 나 지금 Docker 안에 있어!이제 Docker로 여러 개의 서버를 띄우겠습니다. NodeJS는 싱글 스레드이기 때문에 하나의 CPU를 여럿이 나눠 갖는 건 비효율적입니다. 따라서 CPU 숫자에 맞춰서 서버를 띄워보겠습니다.제 맥북엔 CPU가 4개뿐입니다.CPU수에 맞춰 추가로 생성하기추가로 컨테이너를 생성하고, 서버를 실행합니다. 서버 목록도 확인해야겠죠.서버 생성서버 실행서버 목록 확인포트번호는 같은 포트를 쓸 수 없기 때문에 3001, 3002, 3003으로 매핑합니다. 브라우저로 접속해서 확인해보겠습니다.각 포트별 접속 화면미리 만들어둔 이미지 덕분에 서버 3대를 띄우는 데에 5분도 안 걸렸습니다. 하지만 Docker 서버를 여러 개 띄워도 결국 사람의 손이 닿아야 합니다. 따라서 이번에는 NodeJS의 Cluster를 활용해 적은 수의 Docker Container를 이용하면서도 다수의 CPU를 사용하겠습니다. 또 죽은 워커를 다시 살려 서버가 다운되는 것을 막아 안정적인 서비스도 구축해보겠습니다.4. 멀티코어대응 NodeJS Cluster 구성2컨테이너용 NodeJS Cluster서버 어플리케이션 작성하기var cluster = require('cluster'); var os = require('os'); var uuid = require('uuid'); const port = 3000; //키생성 - 서버 확인용 var instance_id = uuid.v4();  /**  * 워커 생성  */ var cpuCount = os.cpus().length; //CPU 수 var workerCount = cpuCount/2; //2개의 컨테이너에 돌릴 예정 CPU수 / 2  //마스터일 경우 if (cluster.isMaster) {     console.log('서버 ID : '+instance_id);     console.log('서버 CPU 수 : ' + cpuCount);     console.log('생성할 워커 수 : ' + workerCount);     console.log(workerCount + '개의 워커가 생성됩니다\n');        //CPU 수 만큼 워커 생성     for (var i = 0; i < workerCount>         console.log("워커 생성 [" + (i + 1) + "/" + workerCount + "]");         var worker = cluster.fork();     }        //워커가 online상태가 되었을때     cluster.on('online', function(worker) {         console.log('워커 온라인 - 워커 ID : [' + worker.process.pid + ']');     });        //워커가 죽었을 경우 다시 살림     cluster.on('exit', function(worker) {         console.log('워커 사망 - 사망한 워커 ID : [' + worker.process.pid + ']');         console.log('다른 워커를 생성합니다.');                 var worker = cluster.fork();     });  //워커일 경우 } else if(cluster.isWorker) {     var express = require('express');     var app = express();     var worker_id = cluster.worker.id;         var server = app.listen(port, function () {         console.log("Express 서버가 " + server.address().port + "번 포트에서 Listen중입니다.");     });        app.get('/', function (req, res) {         res.send('안녕하세요 저는 워커 ['+ cluster.worker.id+'] 입니다.');     });  } CPU 숫자를 받아 CPU 수(4)를 컨테이너 수(2) 로 나눠 워커를 생성하는 NodeJS 클러스터 구성입니다. 이렇게만 해도 운영에는 무리가 없지만 컨테이너 2개의 구분이 안 되서 확인할 수가 없습니다.그러므로 마스터와 워커의 통신을 이용해 마스터의 uuid를 얻겠습니다. (워커와 마스터 간의 데이터 이동은 통신 말고는 메모리DB 등의 데이터 저장소밖에 없습니다)마스터의 아이디를 알아오는 로직이 추가된 어플리케이션 작성var cluster = require('cluster'); var os = require('os'); var uuid = require('uuid'); const port = 3000; //키생성 - 서버 확인용 var instance_id = uuid.v4();  /**  * 워커 생성  */ var cpuCount = os.cpus().length; //CPU 수 var workerCount = cpuCount/2; //2개의 컨테이너에 돌릴 예정 CPU수 / 2  //마스터일 경우 if (cluster.isMaster) {     console.log('서버 ID : '+instance_id);     console.log('서버 CPU 수 : ' + cpuCount);     console.log('생성할 워커 수 : ' + workerCount);     console.log(workerCount + '개의 워커가 생성됩니다\n');         //워커 메시지 리스너     var workerMsgListener = function(msg){                    var worker_id = msg.worker_id;             //마스터 아이디 요청             if (msg.cmd === 'MASTER_ID') {                 cluster.workers[worker_id].send({cmd:'MASTER_ID',master_id: instance_id});            }      }        //CPU 수 만큼 워커 생성     for (var i = 0; i < workerCount>         console.log("워커 생성 [" + (i + 1) + "/" + workerCount + "]");         var worker = cluster.fork();                //워커의 요청메시지 리스너         worker.on('message', workerMsgListener);     }        //워커가 online상태가 되었을때     cluster.on('online', function(worker) {         console.log('워커 온라인 - 워커 ID : [' + worker.process.pid + ']');     });        //워커가 죽었을 경우 다시 살림     cluster.on('exit', function(worker) {         console.log('워커 사망 - 사망한 워커 ID : [' + worker.process.pid + ']');         console.log('다른 워커를 생성합니다.');                 var worker = cluster.fork();         //워커의 요청메시지 리스너         worker.on('message', workerMsgListener);     });  //워커일 경우 } else if(cluster.isWorker) {     var express = require('express');     var app = express();     var worker_id = cluster.worker.id;     var master_id;        var server = app.listen(port, function () {        console.log("Express 서버가 " + server.address().port + "번 포트에서 Listen중입니다.");     });        //마스터에게 master_id 요청     process.send({worker_id: worker_id, cmd:'MASTER_ID'});     process.on('message', function (msg){         if (msg.cmd === 'MASTER_ID') {             master_id = msg.master_id;         }     });        app.get('/', function (req, res) {         res.send('안녕하세요 저는 ['+master_id+']서버의 워커 ['+ cluster.worker.id+'] 입니다.');    });  } Docker Container에 올리기 전 로컬 테스트를 먼저 진행합니다. 서버 구동!두 개의 워커가 실행되었습니다.똑같은 localhost:3000번 접속이지만 워커의 번호가 다릅니다.이제 워커로 CPU 수만큼 워커를 생성할 수 있게 되었습니다. 이제 워커가 어떻게 안정적으로 서비스되는지 테스트하겠습니다. 워커 킬링 테스트하기워커 킬러 로직 작성//워커 킬링 테스트     app.get("/workerKiller", function (req, res) {         cluster.worker.kill();         res.send('워커킬러 호출됨');     }); 실험에 앞서 똑같은 상황 재연 마스터 아이디를 유심히 봐주세요. 워커 킬러를 실행하겠습니다.워커 킬러 호출아래는 호출된 결과입니다. 하나의 워커가 죽자마자 곧장 다른 워커가 태어나(?) 3000번을 Listen하기 시작했습니다. 워커 킬러가 호출된 화면이제 워커 킬러를 여러 번 호출해보겠습니다. CMD+R을 꾸욱 눌러 연속으로 킬링해봤는데 아래 화면처럼 바로 살아납니다.접속해서 현재 워커를 확인합니다.위의 화면처럼 마스터의 UUID가 그대로인데 워커만 교체되었습니다. 준비는 끝났습니다. 이제 Docker를 이용해 2명의 워커를 가진 2개의 NodeJS서버를 실행하고, 4개의 귀여운 CPU를 불살라봅시다! 5. Docker로 NodeJS Cluster 서버 실행하기docker build --tag node_server:0.0.3 /Users/kww/eclipse-workspace/nodejs-for-article docker create --name NODE_SERVER_0 -p 3000:3000 node_server:0.0.3 docker create --name NODE_SERVER_1 -p 3001:3000 node_server:0.0.3 docker start NODE_SERVER_0 docker start NODE_SERVER_1 cluster가 적용된 2개의 컨테이너 start0.0.3번 이미지로 생성된 2개의 컨테이너 서버가 무사히 로드되었습니다. 이제 접속해서 확인해볼까요?cluster가 적용된 2컨테이너 4서버 구동화면WOW! 2개의 URL, 2개의 UUID, 각 2명의 워커까지. 완벽한 2.2.2입니다. 마치 홍진호를 보는 듯한 서버 현황입니다. 이제 워커 킬러로 습격해보겠습니다.워커 킬러 습격 후위의 이미지를 보면 3000번 포트서버에서 13명, 3001번 포트서버에서 22명의 워커가 사망했습니다. UUID를 통해 2개의 서버에서 일정량의 워커가 매우 안정적으로 서버를 지키고 있는 걸 알 수 있었습니다.지금까지 2개의 컨테이너로 4개의 서버를 구성해보았습니다. CPU 숫자와 나눠지는 수에 따라 컨테이너의 수, NodeJS 클러스터 서버의 수를 유동적으로 조정할 수 있습니다. 전에 운영하던 API서버는 16코어 서버였고, 로드벨런서 및 기타 작업용 1코어의 여분을 남기고 15코어 / 3 으로 5개의 워커를 가진 3개의 NodeJS서버를 도커 컨테이너로 운영했었습니다.여기서 문제점이 생깁니다. 우리는 어떤 서비스를 할 때 하나의 도메인을 쓰는데 포트번호가 2개죠? 어떻게 해야 할까요. 여기서 바로 한참을 기다렸던 불곰국의 Nginx가 등장합니다.6. Nginx로 로드밸런싱 하기Nginx은 “더 적은 자원으로 더 빠르게”를 지향합니다. 러시아의 이고르 시쇼브(Игорь Сысоев)는 Apache에서 10,000개의 접속을 동시에 다루기 힘든 걸 해결하려고 Nginx를 개발합니다.Nginx는 NodeJS와 유사하게 싱글 스레드 방식에 이벤트 드리븐 구조 사용하는 오픈소스 HTTP서버로 최근 아파치의 점유율을 상당히 뺏고 있는 서버입니다. 다운로드 링크를 아래에 써두었습니다.Nginx 설치WindowNginx 다운로드Macbrew install nginx Linuxapt-get install nginx or yum install nginx Nginx 설치 성공Nginx 기본 접속 화면서버 조작방법서버 시작 : nginx 서버 중지 : nginx -s stop 서버 재시작 : nginx -r reload (맥에선 이건 안되는듯?) 기본 설정은 8080포트로 되어있습니다. 원하는 포트르 로드벨런싱 설정을 해보겠습니다. Nginx 로드밸런싱 설정아래는 Nginx의 로드밸런싱입니다.#http블럭 내부에 추가     #NodeJS 서버 로드밸런싱     upstream nodejs_server {         #least_conn;         #ip_hash;         server localhost:3000 weight=10 max_fails=3 fail_timeout=10s;         server localhost:3001 weight=10 max_fails=3 fail_timeout=10s;     }        #3333번 포트 NodeJS 서버로 연결     server{         listen               3333;         server_name  localhost;                location / {             proxy_pass http://nodejs_server;         }     } 로드밸런싱이 잘 적용되었는지 확인해보겠습니다. 로드밸런싱 적용 이후모든 브라우저에서 3333번으로 접속했는데 서로 다른 2개의 서버가 번갈아 접속되고, 워커가 가끔 바뀌는 걸 확인할 수 있습니다. 이번엔 로드밸런서로 워커 킬러를 호출하겠습니다.로드밸런싱 포트인 3333번 포트로 여러 번 호출결과 확인Nginx 로드밸런서가 확실하게 작동하는 걸 확인할 수 있었습니다. 위의 이미지에서 서버가 자꾸 바뀌는 모습을 볼 수 있는데, 이는 세션이 유지되지 않기 때문입니다. 실제 서비스에서는 세션의 유지를 위해 ip_hash 옵션이 꼭 필요합니다.ip_hash : 동일한 IP의 접속은 같은 서버로 접속하도록 하는 옵션입니다.  least_conn : 가장 접속이 적은 서버로 접속을 유도하는 옵션으로 ip_hash와 같이쓰입니다. Conclusion자, 고생하셨습니다. 여기까지 Docker와 NodeJS, Nginx를 이용해 관리하기 쉽고, 일부러 죽여도 죽지 않는 안정적인 서비스 환경을 구축해봤습니다. 한 가지 주의할 점이 있습니다. NodeJS의 Cluster는 죽은 워커를 바로 살리는데 싱글스레드여서 그런지 그 속도가 정말 어마어마합니다. 따라서 NodeJS Cluster를 사용할 땐 여러 핸들링에 신중하세요. 모든 promise에 반드시 catch를 달아 핸들링하고, 오류가 날 것 같은 로직엔 반드시 try - catch를 달아 핸들링을 해야 합니다. 그렇지 않으면 다시 살아나는 워커에 의해 서버의 자원이 고갈될 수 있습니다.예전에 16코어 서버를 운영할 땐 서버 자원에 비해 사용자가 적어서..(눈물) 5워커 2개의 서버만 구동하고 여유를 두었습니다. 그리고 서버 패치가 있을 때 3번째 서버를 대기시켰습니다. 앱에서 업데이트가 완료되는 시점에 Docker Container를 바꿔치기 하는 방식으로 Non-Stop서비스를 운영했죠. 혹시 코어가 빵빵한 여유 서버가 있는데 재빠르고 좀비 같은 서비스를 구성해야 한다면 위와 같은 환경 구축을 강력히 추천합니다. 지금까지 긴 글을 읽어주셔서 감사합니다.ps. 글 쓰다 보니 해가 떴네요. 하하.참고1) 가상 머신은 작은 이미지라도 기가바이트 단위의 사이즈와 Load되기까지 상당한 시간이 소요된다.2) 그러나 Windows의 경우, Hiper-v위에 리눅스를 띄워 도커를 구동한다. Mac에서도 가상 머신 위에서 구동된다. 따라서 성능적인 강점은 리눅스에만 적용된다.3) 도커에서는 NAME 속성을 지어주지 않으면 알아서 이름을 지어주는데 romantic한 단어가 많다.글강원우 과장 | R&D 개발2팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발팀 #개발자 #개발환경 #업무환경 #인사이트 #경험공유
조회수 1838

잔디의 새싹 같은 안드로이드 개발자 Gary를 만나다.

맛있는 인터뷰 : 안드로이드 개발자(Android Developer) Gary 편집자 주잔디와 함께 하는 멤버는 총 35명. 국적, 학력, 경험이 모두 다른 이들이 어떤 스토리를 갖고 잔디에 합류했는지, 무슨 일을 하고 있는지 궁금해하는 분들이 많습니다. 잔디 블로그에서는 이 궁금증을 해결해 드리고자 ‘맛있는 인터뷰’를 통해 ‘잔디’ 멤버들의 이야기를 다루고 있습니다.인터뷰에서 가장 기초적인 질문. 예상하신 자기소개 부탁한다.G : 잔디 개발자 중 제일 어린! 핵심 키워드다. 강조 부탁한다. 가장 어린! 안드로이드 플랫폼 개발을 맡은 Gary다(이하:G). 잔디 입사한 지는 9개월 되었다. 특별한 일 없이 정말 열~! 심히 개발만 했다. 일주일 전부터 식당 선택을 강요(?)했다. 이 식당을 선택한 이유는 무엇인가?G : 음식이 푸짐하게 나오기도 하지만 조명이 좋다. 인터뷰 중 사진을 찍는다고 들었는데 그 사진이 예쁘게 나올 것 같아서 선정했다. 게다가 음식이 정갈하다. 안에 룸도 있어서 회식하기도 좋다. 내 입이 복잡하질 않아서 뭘 먹어도 다 맛있다. 어차피 다 맛있는 거 사진이라도 잘 나와야 한다 싶어서 선택하게 되었다. 잔디에 들어오게 된 계기는?G : 모집 공지가 뜨길래.. 농담이다^^; 전 직장과의 시스템이 다른, 자체 서비스를 하는 회사에 가보고 싶었다. 전 직장에서는 기획, 서버, 디자인, 개발을 각각 다른 회사에서 진행했다. 내가 속한 회사는 개발만 진행했다. 처음부터 끝까지 한 곳에서 서비스하는 회사를 가보고 싶었다. 또한, 기술 스택이 너무 좋았다. 이를 통해 앞으로 나의 실력이 일취월장할 수 있겠다는 생각이 들었다. 9개월 동안 많이 발전한 것 같은가?G : 상당히 많이 발전했다. 어떤 부분에서 발전한 것 같은가?G : 이전 회사에서는 일정에 쫓기다 보니 개발 자체에서 설계라는 게 없었다. 손 가는 데로, 일단 만들고 보자라는 식으로 신기술이고 뭐고 공장처럼 찍어내는 것이 일이었다. 하지만 잔디는 다르다. 2주 단위로 생각을 통해 설계도 해보고 ‘객체지향 설계 5대 원칙’ 등 다양한 사항을 고려해보며 많은 고민 끝에 개발하는 것이 너무 좋다. 이를 통해 개발자로서 더욱 성장한 것이 느껴진다. 이전 회사보다는 일찍 퇴근하는 편인가?G : 이전 회사는 철야, 야근 거의 매일 했다. 입사한 지 9개월 정도 되었는데 이제야 사람답게 사는 것 같다. 그동안은 짐승인 줄 알았다. 일어나면 출근하고 퇴근하면 자고. 9개월 동안 사람답게 살면서 잠도 푹 잤다. 몸이 편해지면 사람 얼굴이 확 산다고 하던데 생긴 건 나아지지 않더라. 옛말도 틀린 말이 있나 보다. (하하) 주말엔 뭐하시는가?G : 집돌이 성향이라 집에 있는 경우가 많다. 밖에 나가면 이웃 주민들과 소통하기도 한다. 지금 사는 곳은 취업한 지 얼마 안 된 사회초년생들에게 국가에서 주는 ‘행복주택’이라는 곳이다. 그곳엔 대부분 사회초년생이라 연령대가 20대 후반에서 30대 초반 정도 된다. 그분들과 소통을 하며 지내고 있다. 나이가 비슷하니 공감대 형성도 되고 행복주택에서 행사 같은 걸 많이 해서 문화생활 영위하는 듯하다. 말 그대로 사람 사는 듯한 느낌이다. 서울 집값이 너무 비싸다. 행복주택은 저렴한 편인가?G : 다른 원룸에 비교하면 저렴한 편이다. 보증금이 들어가지만, 월세는 10만 원 전후다. 서울에서 월세 10만 원 전후라면 저렴한 편 아닌가? 사회 초년생분들께 추천한다. 이웃들도 좋고 문화생활도 하고. 엄청나지 않은가? 신청 후 당첨이 되어야 하지만 당첨되기 쉬운 편이다. 꼭 한번 도전해봐라. 처음 들어왔을 때 잔디는 어땠는가?G : 사실 처음 들어왔을 때 사무실이 시끌벅적하며 소통이 아주 활발할 줄 알았다. 그런데 딱 들어오니 어?! 음? 오?!… 조용하다. 진짜 활발한 곳은 따로 있었다. 목소리가 아닌 손가락으로 얘기하는 곳. 잔디 앱이었다. 사무실 자체는 너무 조용한데 잔디 앱 안에서 매우 활발하다. 아이러니하지만 의사소통은 아주 활발했다. 잔디의 생활 중 가장 마음에 드는 문화는?G : 영어 이름을 사용하며 상호 간에 존중하는 문화다. 30년 동안 한글 이름으로만 살았는데. 회사만 오면 Gary라고 부른다. 첨엔 이게 날 부르는 건지도 잘 몰랐지만 익숙해지니 너무 좋다. 수평적인 관계의 시작이 무엇인지 알게 되었다. 이전 회사와는 다르게 잔디에서는 수평적인 관계여서 의견 개진이 편했다. 의견을 나눌 수 있다는 것이 너무 좋았다. 물론 손끝으로 얘기하지만:D 감정표현도 이모티콘으로 한다. 표정은 무표정이지만 손가락은 웃고 있달까? 내 얼굴은 무표정인데 프랑키 (파랑몬스터 캐릭터)가 웃어준다. 이젠 오프라인으로도 소통이 되었으면 한다. 사무실 밖에선 말이 많으신 분들인데. 사무실만 들어오면 조용하시다. 손으로 말하고 계시니까. (웃음) 회식은 자주 하는가?G : 팀마다 다른 것 같다. 팀 내에서 석 달 치 회식비를 모아서 한 번에 하던가 쪼개서 자주자주 하던가. 어차피 쓰는 돈은 같으니까. 이루고 싶은 꿈이 있다면?G : 백수가 되고 싶다. (비장) 충분한 불로소득이 있는. 소득과 상관없이 내 맘대로 살 수 있는 그런 백수가 되고 싶다. 회사를 가고 싶으면 회사를 가고 사업을 하고 싶으면 사업을 하는 그런 백수(하하). 사실 프로그래머로서 직업을 정했을 때는 최고의 프로그램을 만들어보는 게 꿈이었다. 또한, 안드로이드로 시작했으니 구글에서 종지부를 찍자! 이런 꿈을 꾸었다. 그런데 이전 직장이 너무 힘들었나 보다. 꿈이 변했다. 백수로. 하루는 7개월 정도 만에 칼퇴근하고 집에 갔더니 어머니께서. “너 잘렸냐?”라고 물어보시더라. 그땐 내 회사 생활에 문제가 있구나 싶었다. 백수는 아니지만 일과 삶의 밸런스가 맞는 삶을 살고 싶다. 다음 인터뷰어에게 하고 싶은 질문이 있나?G : 회사 내에 다른 팀원들과 지금 먹고 있는 음식을 같이 먹을 수 있다면 누구랑 같이 먹고 싶은가?#토스랩 #잔디 #JANDI #팀원소개 #인터뷰 #기업문화 #조직문화 #사내문화 #팀원자랑
조회수 1877

파이썬의 개발 “환경”(env) 도구들

안녕하세요. 스포카 프로그래머 홍민희입니다.파이썬 패키징 생태계에서 개발 환경을 구성하기 위해 널리 쓰이는 virtualenv나 pyvenv, virtualenvwrapper 같은 각종 도구가 왜 필요한지 (또는 자신에게는 큰 도움이 안 되는지) 알려면 그 이전의 파이썬 라이브러리 배포 방식에 대한 이해가 많은 도움이 됩니다. 여기서는 필요한 몇 가지 역사적 사실과 파이썬 패키징 개념 중 현재의 생태계 이해에 필요한 것들을 위주로 정리하고, 최종적으로 각자의 필요에 따라 어떤 도구를 활용하면 될지 지침을 제안합니다.sys.path패키징이고 뭐고 아무것도 없던 90년대 말에는 라이브러리 소스 코드 파일들을 타르볼(tarball)로 압축해서 배포했습니다. 쓰는 사람은 그걸 자신의 애플리케이션 소스 트리 안에 풀어서 사용했습니다.파이썬에는 지금도 sys.path라는 인터프리터 전역적인 상태가 존재합니다. PATH 환경 변수가 실행 바이너리를 찾을 디렉터리 경로들의 목록인 것과 비슷하게, sys.path도 import foo를 하면 foo.py (또는 foo/__init__.py) 파일을 찾을 디렉터리 경로들의 목록을 담습니다. 그리고 기본 동작으로 그 목록의 맨 처음에는 현재 디렉터리(./)가 들어갑니다. 따라서 라이브러리 타르볼을 애플리케이션 소스 트리에 풀어두면 import해서 쓸 수 있습니다.하지만 자신이 작성한 애플리케이션 코드와 남이 작성한 라이브러리 코드를 같은 소스 트리에서 관리하는 것은 여러모로 불편합니다. 따라서 라이브러리는 애플리케이션 소스 트리와는 별도의 디렉터리(예: ../libs/)에 풀어서 관리하고, 애플리케이션 소스 코드 맨 위에 아래와 같이 써두는 패턴이 많았습니다.import sys sys.path.append('../libs') 또는 sys.path를 소스 코드를 건드리지 않고 조작하기 위해 PYTHONPATH 환경 변수를 활용하는 경우가 많았습니다.세기말, 파이썬 1.5를 쓰던 때의 이야기입니다.site-packages새 천 년이 밝았고 파이썬 2.0이 나왔습니다. 표준적인 라이브러리 배포 방식 및 설치 방식이 제안되었고, 표준 라이브러리에 distutils도 들어왔습니다. (지금도 setuptools는 distutils에 의존하고, pip는 setuptools에 의존합니다.) 제안된 방식은 이랬습니다.애플리케이션 코드가 아닌 라이브러리 소스 코드는 모두 /usr/local/lib/pythonX.Y/site-packages/ 디렉터리 안에 둡니다. X.Y는 파이썬 인터프리터 버전이고, 경로는 인터프리터를 빌드할 때 (./configure) 정합니다. 데비안 계열은 site-packages 대신 dist-packages라는 이름으로 바꿔서 빌드하는 등, 파이썬 인터프리터의 설치 방식에 따라 달라집니다. 어떻게 정하든 이를 site-packages 디렉터리라고 부릅니다. 파이썬 인터프리터를 빌드할 때 경로가 결정되므로, 파이썬 인터프리터 별로 각자의 site-packages 디렉터리를 갖게 됩니다. (한 시스템에서 여러 파이썬 버전을 설치했을 때 pip 역시 pip2.7, pip3.6 등과 같이 버전 별로 명령어가 생기는 것도 같은 이유입니다.)기본적으로 sys.path 목록에는 맨 앞에 현재 위치(./), 뒤쪽에는 site-packages 경로가 들어있습니다. import를 하면 현재 위치에서 찾고, 없으면 site-packages를 찾아본다는 뜻입니다.표준 라이브러리의 distutils.core.setup() 함수는 라이브러리 파일들을 시스템의 site-packages 디렉터리에 복사해주는 함수입니다. 라이브러리 타르볼 파일 맨 바깥에는 이 함수를 이용해 라이브러리를 시스템 site-packages에 설치해주는 스크립트를 setup.py라는 파일명으로 포함하는 관례가 있었습니다. pip 같은 게 없던 때에는 라이브러리 타르볼을 받아서 푼 다음 python setup.py install 명령을 실행하는 것이 일반적인 라이브러리 설치법이었습니다. 지금도 pip는 *.whl 파일이 아닌 *.tar.gz/*.zip 파일인 패키지를 설치할 때 내부적으로 python setup.py install 스크립트를 실행합니다.참고로 이때 정립된 파이썬 패키징 표준은 리눅스에서 쓰이는 dpkg나 RPM 같은 일반적인 패키징 방식을 의식하며 만들어졌습니다.1 당시는 도커는 커녕 가상화 자체가 보편적이지 않던 때로, 한 시스템에 여러 애플리케이션을 함께 설치해서 쓰는 멀티테넌시 환경이 일반적이었기 때문입니다.workingenv파이썬으로 작성한 애플리케이션 여럿이 한 시스템에 설치되면 공통으로 의존하는 라이브러리의 버전을 결정하는 게 문제가 됩니다. A 애플리케이션은 foo >= 1.0.0, < 2>에 의존하고 B 애플리케이션은 foo >= 1.5.0에 의존하면 시스템에 설치할 수 있는 foo의 버전은 >= 1.5.0, < 2>으로 한정됩니다. 만약 C 애플리케이션을 설치하려는데 foo > 2.0.0에 의존한다면, A나 C 중 하나는 포기해야 합니다.시스템에 파이썬 애플리케이션을 단 하나만 설치한다 해도, 설치하는데 시스템 관리자 권한이 필요하다는 것도 문제였습니다. 일반적으로 site-packages 디렉터리는 시스템 관리자만 수정할 수 있고 나머지는 읽기만 가능한 /usr 아래 어딘가로 정해졌기 때문입니다. 이를 우회하려고 사용자가 시스템에 설치된 파이썬 인터프리터를 쓰지 않고 직접 파이썬 인터프리터를 빌드해서 사용하는 편법도 쓰였습니다.이런 문제를 해결하기 위해, 애플리케이션·프로젝트마다 별도의 site-packages 디렉터리를 두는 방식이 제안됐습니다. 나중에 virtualenv을 만들게 되는 이안 비킹이 그 전신인 workingenv를 만들어 이 아이디어를 실현했습니다. 현재의 virtualenv 사용 방식은 workingenv에서 만들어진 것입니다.애플리케이션마다 별도의 “환경”(env)을 만듭니다.애플리케이션을 실행하기 전에 우선 그 “환경”을 “활성화”(. bin/activate 또는 Scripts\activate.bat)합니다.workingenv가 만들어주는 활성화 스크립트는 PATH와 PYTHONPATH 환경 변수를 재정의하여 시스템에 설치된 파이썬 인터프리터의 실행 바이너리 디렉터리 및 site-packages 디렉터리를 가리키는 대신, “환경” 내의 bin/ 및 site-packages 디렉터리를 바라보도록 해줍니다. 이안 비킹은 이렇게 분리된 실행 파일들(bin/)과 site-packages 등을 묶어서 “환경”이라고 명명했는데, workingenv 이후로 파이썬 패키징 및 배포 분야에서 이 용어가 정착됩니다.최근에 만들어진 신생 언어의 패키지 관리자는 대부분 파이썬과 달리 애플리케이션·프로젝트마다 별도의 환경을 두고 설치되는 경우가 많습니다. 예를 들어 npm은 -g 옵션을 일부러 켜지 않는 한 현재 디렉터리를 기준으로 ./node_modules 디렉터리에 라이브러리를 설치하게 되어 있고, 별도의 “활성화” 없이도 노드 인터프리터가 해당 경로에서 라이브러리를 찾습니다. 하지만 파이썬의 패키징 표준은 앞서 언급한 것처럼 멀티테넌시 환경이 일반적이었던 시대에 만들어졌고, 또 많은 라이브러리가 실행 파일도 함께 제공하기 때문에2 PYTHONPATH 뿐만 아니라 PATH 환경 변수도 재정의해야 해서 activate 과정이 필요합니다.workingenv는 파이썬 웹 프로그래머 사이에서 빠르게 퍼지기 시작했습니다. 웹 애플리케이션은 정통적인 CLI 및 GUI 애플리케이션과 달리 FHS 표준 같은 것에 크게 구애될 필요가 없었고, 웹 애플리케이션의 배포도 점차 가상화 기술을 통해 완전히 격리된 시스템에 설치되는 식으로 보안 문제에서 많이 자유로워졌기 때문입니다.무엇보다 workingenv는 프로그래머가 여러 프로젝트를 동시에 작업하는 경우 골치 아팠던 라이브러리 버전 충돌 문제를 우회했기 때문에, 배포 도구보다는 개발 도구로 정착되는 면이 컸습니다.virtualenv이안 비킹은 PYTHONPATH를 조작하여 별도의 site-packages 공간을 두는 workingenv의 방식이 복잡하게 패키징된 기존 라이브러리 및 프로젝트에서 호환되지 않는 문제로 골머리를 썩이다, 아예 PYTHONPATH를 이용하지 않는 방식으로 새 도구를 만듭니다.새로운 방식은 아예 파이썬 인터프리터 실행 바이너리를 복사한 뒤, sys.path 기본값에 박힌 시스템 site-packages 경로를 환경 내 site-packages 경로로 바꿔버리는 것이었습니다. 이러한 동작 원리의 차이는 이용자 입장에서 크게 중요한 것은 아닙니다.하여튼 이안 비킹은 virtualenv라는 이름으로 새 도구를 만들었고, workingenv를 빠르게 대체했습니다.virtualenvwrapper앞서 언급한 것처럼, workingenv와 그 후계자인 virtualenv는 저자의 의도와 무관하게 애플리케이션 배포보다는 개발 용도로 더 널리 쓰입니다. 파이썬 프로그래머가 새로운 프로젝트를 시작할 때는 항상 “환경”도 생성합니다. 또 개발을 시작할 때마다 “활성화” 과정도 거칩니다. 너무나 반복적이기 때문에 당연히 이를 자동화하는 도구도 만들어졌습니다. virtualenvwrapper는 바로 그런 목적으로 만들어진 bash/zsh/fish 스크립트 모음입니다.여러 단축 명령을 제공하지만, 핵심 기능은 다음의 두 가지입니다.A라는 프로젝트 작업을 시작할 때마다 cd ~/projects/a; . .venv/bin/activate라고 쳐줘야 했던 것을 workon a 명령으로 줄여줍니다.프로젝트 디렉터리마다 .venv/ 또는 .env/ 등의 이름으로 환경 디렉터리를 생성해두고 버전 관리 시스템에서는 제외되도록 .gitignore 목록에 해당 디렉터리를 넣었어야 했습니다. 예를 들어 ~/projects/a/.venv/, ~/projects/b/.venv/ 같은 식이었습니다.virtualenvwrapper를 쓰면 환경 디렉터리들을 일정한 위치로 모아줍니다. 위치는 기본값이 없으며 virtualenvwrapper 설치할 때 WORKON_HOME 환경 변수를 통해 입맛대로 정할 수 있습니다. 예를 들어 WORKON_HOME을 ~/.virtualenvs/ 디렉터리로 정했다면, 프로젝트별 환경은 ~/.virtualenvs/a/, ~/.virtualenvs/b/ 같은 식으로 저장됩니다.pyvenv파이썬 3.3부터는 virtualenv가 아예 파이썬에 내장됐습니다. 환경을 만드는 명령어는 virtualenv가 아닌 pyvenv로 좀 다르지만, 그 이후의 과정은 같습니다. 파이썬 3만 사용한다면 이제 virtualenv를 따로 설치할 필요가 없어진 것입니다.참고로 아래에서 설명할 pyenv와는 다른 도구입니다. 철자의 “v”에 주의해주세요.pyenv애플리케이션을 개발할 때는 하나의 파이썬 버전을 정하면 되지만, 라이브러리는 여러 파이썬 버전과 호환되어야 합니다. 그러다 보니 라이브러리 개발자는 여러 버전의 파이썬을 시스템에 동시에 설치할 필요가 있습니다. 데드스네이크스 PPA나 데드스네이크스 홈브루 탭 같은 것을 이용해서 설치할 수도 있지만, 보통은 pyenv를 많이 씁니다.pyenv는 동시에 여러 버전의 파이썬을 시스템에 설치해주며, 이렇게 설치된 파이썬은 시스템의 패키지 시스템(데비안·우분투의 APT나 맥OS의 홈브루 등)을 통해 설치되는 것이 아니라, pyenv가 다운로드와 빌드 및 설치를 직접 하여 별도로 관리합니다. 설치된 파이썬들은 PEP 394에 따라 일정한 형식으로 이름지어진 명령어(예: python2.7, python3.6)로 실행할 수 있게 됩니다.또한, 여러 파이썬 버전 중에 하나의 시스템 기본 파이썬 버전도 선택 가능하며, 특정 프로젝트 디렉터리 안에서만 기본 파이썬의 버전이 달라지게 할 수도 있습니다.pyenv-virtualenvpyenv가 여러 파이썬 버전을 동시에 설치해주기는 하지만, 그렇다고 자동으로 site-packages가 프로젝트마다 격리되는 것은 아닙니다. 예를 들어 pyenv로 파이썬 3.6을 설치한 뒤, 파이썬 3.6으로 두 프로젝트를 한 시스템에서 개발할 경우 두 프로젝트는 시스템 site-packages를 함께 쓰게 됩니다.따라서 pyenv를 쓰더라도 virtualenv는 따로 써야 하는데, 따로 사용할 수도 있지만 pyenv-virtualenv를 쓰면 pyenv virtualenv 명령으로 프로젝트에 쓸 파이썬 버전 지정과 가상 환경 생성을 한 번에 할 수 있게 됩니다.비슷하게 pyenv와 virtualenvwrapper를 통합해주는 pyenv-virtualenvwrapper 같은 도구도 있습니다.마치며여러 파이썬 개발 환경 관리 도구를 소개했지만, 여기 있는 모든 도구를 꼭 써야 하는 것도 아니고, 가장 최근에 나온 도구로 하루빨리 갈아타야 하는 것도 아닙니다. 글을 쓴 저 자신도 pyenv 같은 도구가 나온 지 몇 년이나 지났고 주변에서 쓰는 사람이 많음에도 쓰지 않고 있습니다. virtualenvwrapper를 대체하는 Pipenv 같은 실험적인 방식3도 생겨나고 있지만, 어느 쪽이든 동시에 여러 파이썬 프로젝트를 작업하는 사람이 아니라면 굳이 쓸 필요가 없는 도구입니다. 각자의 용도에 따라 필요한 수준의 도구를 이용하면 됩니다. 2017년 10월 현재, 아래의 지침으로 정리할 수 있겠습니다.파이썬 프로그래머가 아니지만, 파이썬 애플리케이션을 설치해서 이용합니다.시스템에서 제공하는 패키지 관리자(APT나 홈브루 등)를 통해 애플리케이션을 설치하세요.파이썬 프로그래머가 아니지만, 파이썬 애플리케이션을 유난히 많이 이용합니다.pipsi를 이용해 파이썬 애플리케이션을 설치하는 것을 권합니다.파이썬 프로그래머이고, 하나의 애플리케이션을 개발합니다.파이썬 3.3 이상을 이용할 경우 pyvenv로 개발 환경을 만들어서 개발하세요. 그 이전의 파이썬 버전을 이용할 경우 virtualenv를 활용하세요.파이썬 프로그래머이고, 여러 애플리케이션을 개발합니다.virtualenvwrapper를 활용하세요.파이썬 프로그래머이고, 여러 애플리케이션을 다양한 파이썬 버전으로 개발합니다.pyenv-virtualenvwrapper를 활용하세요.파이썬 프로그래머이고, 라이브러리를 개발합니다.pyenv와 tox를 활용하세요.파이썬으로 만든 애플리케이션을 distutils를 통해 패키징한 뒤, RPM 기반의 리눅스 배포본 용으로 python setup.py bdist_rpm 명령을 통해 *.rpm 파일을 제공하기도 했습니다. 이를 통해 애플리케이션을 설치할 경우, 각 파일들은 리눅스 FHS 표준과 해당 시스템 설정에 따라 흩어지게 됩니다. ↩예를 들어 파이썬에서 가장 많이 쓰이는 국제화 라이브러리인 바벨은 pybabel 명령어를, 구문 강조 라이브러리인 파이그먼츠는 pygmentize 명령어를, 장고는 django-admin 명령어를 제공합니다. ↩저는 2017년 4월에 한 번 써보았으나, 아직은 실무에서 쓰기에는 이르다는 결론을 내렸습니다. 이에 관한 그때의 제 감상은 별도의 글로 다루었습니다. ↩#스포카 #파이썬 #개발팀 #개발자 #인사이트 #후기 #일지
조회수 5245

"캘린더앱은 돈이 되지 않아요"

지난 2년 내내 투자자 미팅에서 귀에 박히도록 들었던 소리."캘린더앱은 돈이 되지 않아요."맞다. 캘린더앱은 돈이 되지 않는다.지난 몇 년간 다수의 회사들이 출시했던 화제의 캘린더 앱들의 말로를 함께 살펴보자.  1,000만 달러를 투자받은 캘린더앱 - Tempo지평만 열고 2015년에 인수 후 종료.  모두에게 사랑받던 캘린더앱 - SunriseMS가 1억 달러(1천억 원)에 인수를 해 화제가 된 후1년 만에 또 종료(2016년).뭐 바다 건너 이야기는 너무 멀게 느껴질 수 있으니, 국내의 사정을 살펴보자.참고로 아래 4개의 서비스 모두 종료 관련 공식 보도자료를 내지는 않았기에 가볍게 블로그나 커뮤니티를 통해서만 확인이 가능하다(그조차도 없는 서비스는 출시 정보로 대체했다).2015년 9월 다음카카오(현 카카오), 다음캘린더 서비스 종료.2017년 6월, SKT 썸데이 캘린더 종료(2016년 출시, 2017년 종료).2018년 12월, 네이버 타르트 종료.(네이버의 경우 오랫동안 유지 중인 '네이버 캘린더'가 있긴 하지만 사실 신규 '일정 관리 앱'을 실험적으로 출시했었다)위 3개 서비스는 다소 생소할 수 있지만 아래 쏠캘린더는 대부분 한번 정도 들어본 적 있으리라 생각한다.위 서비스들 중 가장 많은 사용자를 확보했던 쏠캘린더도 결국 2016년 가을 종료. (쏠캘린더는 다음과 카카오 합병 전 카카오에서 출시된 서비스라 다음캘린더와 쏠캘린더는 다른 서비스였다)위의 4개, 아니 3개 회사가 캘린더 서비스를 종료하게 된 이유는 각기 다를것이고, 공식 보도자료는 없지만 업계 관계자 및 당사자 분들이 남겨놓은 몇몇 자료들을 통해 소소하게나마 내막을 엿볼 수 있었다.다음캘린더 서비스 개발 비하인드 스토리SKT 모바일앱은 왜 거의 다 '단명'할까 네이버 타르트 - 연구 종료 일지결국 그렇게 국내 현 캘린더 시장은 구글 캘린더, (기존)네이버 캘린더, iOS 기본 캘린더, 삼성 / LG 등 안드로이드 내장 캘린더, 4개 캘린더가 4등분하고 있으며 그 외에도 다양한 커스터마이즈 캘린더와 아웃룩이 작은 포션을 차지하고 있다(물론 어디까지나 국내의 이야기로 나라마다 상황은 다르다).커스터마이즈 캘린더를 쓰는 대부분은 구글 캘린더 또는 iOS 기본 캘린더 서버를 연동해서 사용하기에 사실상 자체 캘린더 서버를 운영하는 기업은 구글과 네이버, 그리고 애플뿐이다. 그런데 또 iOS 캘린더 유저의 상당수는 구글 캘린더를 연동해서 쓰기에 여러모로 얽히고설키고 복잡한 시장이다. 아 원래 하려던 얘기로 돌아와서, 여하튼 카카오와 SKT가 시도하다 접었고 네이버, 구글, 애플이 꽉 잡고 있는 이 시장에,2017년 대학생 5명이 또 하나의 캘린더 기반 서비스를 들고 뛰어들었다.(그렇다. 그 얘기 하나 하려고 이렇게 글이 길어졌다.)이름하야 '받아보는 캘린더 - 린더'. 때는 바야흐로 2017년 1월, 졸업을 앞둔 대학생 5명이 학교 강의실에 모여 창업 아이템을 구상하던 그 시절, 공동창업자 중 한 명이 '일정'을 아이템으로 서비스를 만들어보자고 의견을 던졌다.당시 그는 몇 주 전 교내 '캠퍼스 CEO'라는 창업 수업에서 '일정 관리 및 추천' 기능을 가진 서비스 기획서를 과제로 제출했던 상황이었고 팀의 리더였던 나는 그 제안을 듣고 허탈하게 웃으며 "그런 건 구글이나 네이버가 하는 겁니다"라고 단칼에 거절했다(원래 형 동생이었던 우리 팀은 팀빌딩 시점부터 존댓말을 썼다).비록 나 또한 학생이었지만 다수의 공모전, 해커톤, 회사 근무를 통해 서비스를 출시해본 경험이 있었고 서비스의 기획, 개발, 출시, 마케팅, 운영까지 이어지는 프로세스를 몇 번 정도 겪어본 입장에서 또 하나의 '캘린더' 앱을 출시하는 건 미친짓이라고 생각했다(솔직히 이제와서 말하자면 아직 뭘 몰라서 그냥 하는 말이겠거니 했다).그런데 당시 그가 했던 말 한마디가 우리를 움직였다."그러니까 우리가 해야죠"그의 논리는 이러했다."구글이나 네이버가 할 정도의 아이템이니까 시장이 큰 건 이미 증명이 됐고, 근성과 패기, 실행력으로 그들을 이기면 되는 거 아닙니까? 그게 스타트업 아니에요?"그때 말렸어야 했다.그때 설득되지 말았어야 했다.그때는 몰랐다.'일정'이라는 분야를 기반으로 사업을 기획하고, 운영하고, 확장한다는 것이 이렇게 외롭고 힘든 일이 될 줄은.  앞서 언급한 바와 같이 해외 사례라고는 하나 같이 다 종료된 서비스밖에 없었고 국내 시장은 해외의 그 사례들을 몇 년 후 따라가다 종료되는 수준에서 그쳤다.그래서 우리는 판을 새로 짜기로 했다.우리가 만들고자 한 서비스는 캘린더를 기반으로 하거나, 캘린더처럼 생겼는데, 캘린더 앱은 아니어야 했다.캘린더의 메인 기능인 일정을 '입력'하거나 '수정'하는 기능은 다 빼고, 사이드 기능 중 하나인 '구독'을 핵심으로 뒀다.캘린더도 문제였지만 이미 포화된 앱 시장도 문제였다. 새로운 앱들이 하루에도 수십 개씩 출시된지도 모른 채 사람들의 기억 속에서 잊혀지고 있던 상황이었다.단순히 앱을 통해 돌파구를 찾기보다는, 다양한 판로를 찾아보기로 했다.몇 번의 시행착오를 거쳐,2017년 하반기 즈음 우리가 앞으로 가져가야 할 방향성이 명확해지기 시작했다.카카오, 네이버, SKT 같은 회사의 기라성 같은 업계 선배들이 몇십억을 쓰고도 캘린더 서비스를 종료할 수밖에 없었던 데는 분명 이유가 있었다.우리의 전략은 치밀해야 했고, 2017년 말 아래와 같은 3개년 로드맵을 구상하게 되었다.일정 구독 서비스 린더 - 3개년 로드맵(2017.12)(로드맵에 대한 자세한 내용은 https://brunch.co.kr/@five0203/33 에서 확인할 수 있다)위 로드맵을 바탕으로 지난해 하반기 출시된 모바일앱, 즉 관심 일정 구독 플랫폼:린더의 다운로드 수는 40만, MAU는 18만을 돌파했고 지금도 가파르게 상승하고 있다.  한 달에 린더를 통해 일정을 확인하는 횟수(PV)는 700만 건이 넘었고 린더 내 링크를 통해 웹사이트로 이동하는 전환 횟수는 하루 1만 건을 넘어서고 있다.지난 30일 간 약 10여 건의 광고 및 제휴 문의가 있었고 그중 몇몇은 실행으로 옮겨졌다.린더의 장점은 그동안 광고로만 인식되어오던 이벤트 정보들이 '유용한 정보'로 전달된다는 것이다.누군가에게는 광고인 일정이, 누군가에게는 정보가 될 수 있다는 이유로 린더는 사용자에게 '광고 없는 앱'으로 인식되고 있다.물론 광고의 비중이 올라갈수록 네이티브 광고마저도 거부감을 일으킬 수 있기에, 우리는 일정을 모아 놓치지 않도록 도와주는 최초의 목적을 지속적으로 잊지 말아야 한다.  광고 플랫폼 기업 DMC미디어가 발표한 '2018 DMC리포트 종합 보고서'에 의하면 광고를 의도치 않게 실수로 클릭한 사용자는 28.9%에 그치며, 사용자 10명 중 7명은 노출되는 광고에 관심 및 의도를 가지고 클릭하는 것으로 조사되었습니다.문자, 페이스북, 카톡 플러스 친구 등 기존 채널에 대한 피로도가 높아지고 있는 현시점에서 린더가 경쟁력을 가지게 된 이유는 캘린더 유형의 정보 전달이 현재까지 '유용한 정보'라는 인식이 강하기 때문이라 볼 수 있습니다.위에서 언급한 바와 같이 이미 다양한 유형의 수익모델을 준비 중인 린더이지만 보다 장기적 관점에서 서비스 가치를 보존하기 위해 노력해야만 하며, 서비스 수익화에 대한 사용자의 거부감을 '너무 빠르게' 증가시키지 않아야만 사용자 이탈을 사전에 방지할 수 있습니다.이는 우리가 발생시키고자 하는 수익의 총합이 사용자에게 전달되는 가치의 총합을 섣부르게 넘어서는 안된다는 것을 의미합니다.- 19년 3월 주주서한 중 -아직 우리의 목표 MAU에는 한참 미치지 못한 현 상황에서도 밀려드는 광고 제의를 보며, 팀을 최소한으로 유지하고 서비스 운영 비용을 낮춘다면 향후 서비스의 지속과 생존, 즉 ROI를 맞추어 나가는 것은 어렵지 않을 것 같다는 확신이 생겼다(물론 ROI를 맞추는 것과 BEP를 맞추는 것은 차원이 다른 얘기라 BEP를 달성하신 모든 회사를 진심으로 존경합니다).하지만 성장하지 않고 머무르는 조직은 도태하는 조직이기에, 우리 팀은 앞으로도 여러 무모한 시도를 멈추지 않을 계획이다.  "캘린더앱은 돈이 되지 않아요" 공식적인 투자 라운딩을 3주 전 처음으로 시작하게 됐는데, 작년까지만 해도 귀에 박히게 들리던 이 이야기를 올해는 단 한 번도 듣지 못했다. 애초에 중요한 건 돈이 되는 게 아니었다. 사람들에게 필요한 서비스를 만들고, 그를 통해 새로운 가치를 창출하는 것. 그게 우리가 해야 할 일이었다.다수의 불편함을 소수의 기술력을 통해 해결하며, 그것을 지속&확대하기 위해 수익을 만든다.돈은 수단이지 목적이 아니다.긴 글을 마치기에 앞서 우리의 시작을 잊지 않기 위해, 2017년에 남겼던 감성 페북글 하나와 최근에 진행된 린더의 기업 협업 사례 하나를 남겨본다.2017년 7월(법인설립 1달 후, 기보 대출 받은지 일주일 후), SKT 썸데이 캘린더, 여름 문자 서비스 종료 소회그로부터 약 1년 후인 2018년 10월, SKT NUGU 스피커 x 린더 - 데이터 협업 진행
조회수 1840

스켈티인터뷰 / 스켈터랩스의 잡학다이너마이트 변규홍 님을 만나보세요:)

Editor. 스켈터랩스에서는 배경이 모두 다른 다양한 멤버들이 함께 모여 최고의 머신 인텔리전스 개발을 향해 힘껏 나아가고 있습니다. 스켈터랩스의 식구들, Skeltie를 소개하는 시간을 통해 우리의 일상과 혁신을 만들어가는 과정을 들어보세요! 스켈터랩스의 잡학다이너마이트 변규홍 님을 만나보세요:)PART1. About Skelter Labs사진1. 스켈터랩스의 소프트웨어 엔지니어, 변규홍 님Q. 간단한 자기소개를 부탁한다.A. 이름은 변규홍. 스켈터랩스에서 소프트웨어 엔지니어로 일하며, 컴퓨터에게 열심히 한국어를 가르치고 함께 배우고 있다. 대충 20년 전부터 컴퓨터 공부를 시작해서 컴퓨터 관련된 일이라면 사족을 못쓰는 덕후이기도 하다.Q. 현재 스켈터랩스에서 어떤 업무를 맡고있는가.A. 스켈터랩스의 인공지능 대화 엔진 개발 팀인 헤르메스(Hermes)에서 흔히 ‘챗봇’이라 부르는 인공지능 대화 엔진을 만들고 있다. 우리가 만드는 인공지능 대화 엔진은 ‘챗봇을 만들고자 하는 사람들이 누구나 쉽게 챗봇을 만들도록 돕는 편리한 사용'을 목표로 한다. 때문에 비개발자도 이해하기 쉽도록 효율적이고 간편한 UI와 구조로 개발하고 있다. 거기서 나는 어떻게 하면 컴퓨터가 사람이 하는 말을 더 잘 알아듣고 잘 대답할 수 있는지 연구하고 있다. 어떤 처리를 해야하는지, 언어의 어떤 패턴을 인식하는지 등 ‘자연어 처리(Natural Language Processing,NLP)’ 혹은 자연언어처리라고 불리는 기술 전반에 대한 연구를 진행하고 있다.Q. 자연어 처리라는 부분이 생소하다. 언어의 분석이나 처리에 대한 얘기를 더 해줄 수 있나.A. 챗봇 위주로 설명해 보자. 우리가 한국어 문장을 컴퓨터나 스마트폰에 입력할 때, 특히 채팅할 때는 문장의 변화가 심한 편이다. 띄어쓰기를 실수할 수도 있고 급식체같은 축약어를 사용하기도 한다. 같은 의도를 담은 문장이 아주 다르게 표현되는가 하면, 비슷한 문장이 어순이나 표현 한 두 가지만 바뀌어도 전혀 다른 뜻이 되기도 한다. 이러한 인간의 언어를 컴퓨터가 잘 알아들을 수 있도록 분석하고 처리하는 것이다. 입력된 문장에서 어떤 부분이 명사고 어떤 부분이 동사인지를 찾거나, 문장 속에서 어떤 형태소에 집중해야 하는지 분석한다. 그리고 은행 계좌나 전화번호처럼 규칙에 맞는 숫자가 다양하게 입력될 수 있는 경우를 찾아내기도 한다. 이런 과정을 거쳐 사람이 어떤 의도를 갖고 입력한 문장인지, 어떤 정보가 담겨있는지 식별해낼 수 있다.Q. 들어보니 기술에 대한 지식뿐만 아니라 언어학에 대한 조예가 필요한 분야로 보인다.A. 맞다. 이 분야를 전산학(컴퓨터공학)에서는 ‘자연언어처리’라고 하고 언어학에서는 ‘전산언어학(Computational Linguistics)’ 혹은 ‘계산언어학’이라고 한다. 학제 간 학문으로서의 성격이 강한 분야다. 초창기에는 언어학자들이 찾아낸 인간 언어의 구조, 규칙을 컴퓨터공학자 / 전산학자들이 프로그램으로 구현하는 연구가 많았다. 그러다가 애초의 예상보다 인간의 언어 구조가 훨씬 더 복잡하다는 것을 인식한 이후부터는 인간의 언어에서 규칙성을 찾는 과정도 통계적 방법 등을 통해 컴퓨터의 힘을 빌리게 되었다. 최근에는 요즘 화두인 머신러닝 기법을 적극적으로 적용하면서 연구 트렌드가 조금씩 바뀌고 있다. 다양한 규칙에 따라 문장을 분석하기보다, 빅데이터로 정리된 방대한 언어생활 자료를 컴퓨터 스스로 학습하여 문장 속에서 필요한 정보를 찾아내는 식으로의 전환이랄까. 하지만 여전히 좀 더 좋은 결과물을 내려면 언어학에 대한 지식과 규칙성에서 찾아낸 정보들이 필요한 것도 사실이다. 그래서 스켈터랩스에서는 규칙 기반 기법들과 머신러닝 기법 모두를 하이브리드 형태로 결합하여 대화 엔진을 개발하고 있다.Q. 아무리 다양한 형태로 기법을 결합하여 사용하더라도, 엔지니어가 언어학에 대해 연구하기는 쉽지 않아 보인다. 언어학을 별도로 공부하거나 혹은 언어학에 대한 관심을 이전부터 가지고 있었는지.A. 언어학이라기보다는 사실 나는 대학교에서 문학 동아리 활동을 오랫동안 했다. 자연스럽게 다양한 활동을 통해서 문학에 대한 얘기를 하다 보니 언어에 대한 관심도 꽤 높았던 것 같다. 무엇보다 구글코리아의 번역기 개발팀에서 인턴을 하며, 컴퓨터로 인간의 언어를 다루는 것이 굉장히 흥미롭다고 생각했고 꾸준히 관심을 이어왔다. Q. 구글 코리아 인턴 경험이 규홍님에게 여러모로 지대한 영향을 끼친 것으로 알고 있다. 그 얘기를 듣고 싶다.A. 대학에 처음 입학했을 때, 사실 실망감이 더 컸다. 합리적인 의사소통은 막혀있었고, 당시 학교의 학사제도 개편으로 인해 여러모로 시끄러운 상황이었다. 그러던 차에 마침 학교에 구글코리아에서 캠퍼스 리쿠르팅을 왔는데, 선배 중 한 명이 ‘왜 구글은 한국에서 인턴을 채용하지 않습니까' 라고 꽤나 당돌한 질문을 던졌다. 그렇게 구글 코리아 인턴 채용이 열려 면접 기회를 얻게 되었다. 당시 내 이력서에는 대학교 입학 후의 경력이라고는 연극동아리 공연 이력이 전부였기 때문에 일종의 두려움도 컸다. 하지만 일본어로 된 만화책을 컴퓨터에 넣으면 한국어로 번역된 만화책이 튀어나오게 하고, 컴파일(COMPILE) 사의 게임 중 미처 한국어로 번역되지 못한 게임들을 컴퓨터가 알아서 번역해 즐길 수 있게 하는, 그런 컴퓨터 프로그램을 직접 만들고 싶다는 꿈이 더 컸다. 마침 나의 면접관들도 구글 코리아 번역기 개발팀 분들이었다. 그렇게 구글 코리아 번역기 개발팀 인턴으로 입사하게 되었고, 그때의 경험이 나의 꿈의 실현 가능성에 대한 일종의 확신을 주었다.Q. 스켈터랩스에는 어떻게 입사하게 되었나A. 인턴 할 당시의 구글 코리아 사장이 지금 스켈터랩스 창업자, 조원규 대표님이다. 그리고 구글 코리아 면접관이었던 분이 우리 팀의 테크 리더(Tech Leader)를 맡고 있는 이충식 님이기도 하다. 작년 충식 님으로부터 어려운 문제를 풀어야 하는데 같이 한번 풀어보자는 연락을 받았다. 그 문제가 너무 어려울 것 같아서 답장을 망설이고 있었다. 그러다 이전 직장에 대한 염증과 새로운 일에 대한 호기심 등의 마음으로 충식님을 다시 만나 뵈니, 스켈터랩스에서 내가 어렸을 적 꿈꾸던 챗봇을 만들고 계셨다.Q.  스켈터랩스에서의 업무는 이전에 일했던 혹은 알고 있는 다른 개발자의 업무랑 어떻게 다른가. A. 사실 인공지능을 기반으로 한 스타트업에는 뛰어난 사람들이 많은 것 같다. 그러나 스켈터랩스가 다른 회사와 다른 점은 ‘내 동료가 누구인가'에 대한 인식의 범위가 조금 더 넓다는 점이다. 가령 디자이너는 디자이너끼리, 기획자들을 기획자끼리만 협력하고 부서에 따른 책임이나 업무 범위에 대해서 선을 긋는 문화가 흔히 있지 않나. 어떤 직장들은 수직적인 위계 구조를 강요하고 모든 걸 서류로 보고하게 만들기 때문에 일의 효율이 떨어지기도 한다. 그러나 스켈터랩스는 팀 간에, 직무 간에 서로의 업무 영역을 자로 재듯 규정하지 않고 넘나들며, 좀 더 활발한 소통을 추구한다. 덕분에 ‘하나의 공동체'라는 인식을 자연스럽게 가질 수 있다. 서로와 함께 일한다는 것에 대해 우리 스스로 가지는 자긍심도 대단하다. 사내에는 지인을 신규 입사자로 추천하는 채용 제도가 있는데, 그간 내가 일해왔던 회사 중 우리 회사만큼 열심히 지인들에게 추천하는 회사도 없었다. 사실 내가 일하는 회사가 별로면 친구에게 추천도 못 하지 않겠나. 그만큼 서로 만족하고, 자부심을 가지고 일한다는 것을 방증하는 면모인 것 같다.또한 스켈터랩스는 불필요한 서류 업무를 배제하는 대신, 아주 엄격한 코드 리뷰 시스템을 가지고 있다. 내가 과거에 근무했던 회사들은 많은 경우 상대적으로 지금 당장 작동하는 코드를 만들어 내는 것에 집중했다. 물론 이러한 방식이 때로는 실용적이다. 그러나 기능이 잘 작동되는지만 살피다 보니, 숨겨진 버그(Software Bug)가 남겨지고 이것이 뒤늦게 발견되어 더 큰 문제를 일으키기도 했다. 때로는 버그의 존재를 코드 작성자만이 알고 있기도 했다. 이렇듯 단기간 눈앞의 기능에만 집중하다가 코드의 품질이 저해되는 방식으로 개발이 진행되어 언젠가는 다시 수정해야 하는 일거리가 남겨지는 것을 ‘기술 부채(Technical Debt)’라고 부른다. 스켈터랩스의 코드 리뷰 문화는 사소한 영역까지 기술 부채를 남기지 않는다. 궁극적으로는 짧은 기간 완성도 높은 프로그램을 만들 수 있게 해주는 문화다. 엄격한 코드 리뷰가 가능한 것은 스켈터랩스의 개발자 역량이 높기 때문이기도 하다. 개발자들이 모두 기술에 대한 근본적인 이해와 최신 기술에 대한 섭렵을 두루 갖추었기에 타인이 작성한 코드도 바로 이해할 수 있다. 수준 높은 동료와 함께 일하며 피드백 받고 성장할 수 있다는 것은 회사의 굉장한 강점이라고 생각한다.사진2. 규홍 님과 다른 팀원 간의 코드 리뷰 모습.Q. 코드 리뷰 문화가 유익하기도 하지만, 일종의 압박감도 있을 것 같다. A. 압박감으로 여겨본 적은 없다. 한국 사회에서 개발자의 커리어에 대한 얘기를 나누다 보면 자연스럽게 ‘회사 일을 하다 보니 공부할 시간이 없어서 최신 기술을 알지 못해 뒤처진다.'라는 볼멘소리가 나온다. 그러나 스켈터랩스에서는 개발자 모두가 엄격한 코드리뷰를 거치는 과정에서 자연스럽게 더 나은 성능의 코드, 동료가 더 잘 이해할 수 있는 코드, 예상치 못한 예외 상황을 고려하는 코드를 작성하는 법을 실시간으로 배우게 되고, 때로는 그 과정에서 자연스럽게 코드 리뷰자가 제안하는 최신 기술에 대해 공부하고 습득하며 실력을 늘려나간다. 덕분에 코드 리뷰를 마치고 나면, 다음에 어떻게 해야 개선된 코드를 짤 수 있을지에 더 집중할 수 있고 실제로도 더 나은 코드를 작성할 수 있게 된다.물론 이런 문화가 신규 입사자로서는 다소 답답할 수 있을 것 같다. 나 또한 초반에는 ‘굳이 이런 디테일까지 다 잡아가며 이렇게 리뷰를 남겨야 할까'라는 생각을 해본 적도 있다. 그러나 스켈터랩스와 함께하는 시간이 점점 길어질수록, 꼼꼼한 리뷰로 기술 부채를 최소화하는 것이 팀 전체에도, 나의 성장에도 도움이 된다는 걸 느낀다.Q. 아무리 뛰어난 개발자가 있더라도 코드를 작성하는 사람은 한 명인데, 이를 함께 리뷰하다보면 작성된 코드를 이해하지 못하는 경우가 발생하지는 않나.A. 물론 그럴 수 있다. 때문에 스켈터랩스에서는 코드의 공동 소유, 공동 이해 개념을 깊이 이해하고, 잘 지킬 수 있게 만든다. 나만 이해할 수 있는 코드를 작성하면 장기적으로 다른 개발자들의 수정과 응용이 어려워진다. 그래서 스켈터랩스에서는 각 프로그래밍 언어별로 코딩 스타일 가이드를 준수할 것을 권장하고, 코드 리뷰 이전에도 가이드 준수 여부를 점검하는 도구를 활용하고 있다.Q. 스켈터랩스를 자랑한다면.A. 스켈터랩스는 아직 성장 중인, 그래서 ‘함께 만들어 갈 여지가 많은 회사'다. 나는 개인적으로 대기업부터 창업 초창기 단계의 스타트업까지 다양한 회사를 경험했는데, 이러한 과정에서 구성원 한 명 한 명이 회사의 문화와 기술적 원칙을 만들어가는데 얼마나 큰 영향을 주는지를 느꼈다. 스켈터랩스는 다양한 배경을 가진 개발자와 서로 영감을 주고받으며 함께 성장해가는 곳이다. 개발자 직군의 동료들과 비개발자 직군의 동료들이 끊임없이 소통하며 시행착오와 함께 점점 더 나은 기업문화를 만들어가고 있다. 그리고 실제로 이런 문화가 완성도 높은 프로그램을 만드는 데에 긍정적인 기여를 하고 있고, 현재는 성공 경험을 조금씩 안겨주고 있는 단계다. 역량 있는 인재들과 최신의 기술을 활용하여 새로운 결과물을 창출하는 것에 관심 있는 이들이라면 입사를 추천하고 싶다.#스켈터랩스 #사무실풍경 #업무환경 #사내복지 #기업문화 #개발팀 #팀원인터뷰 #팀원소개 #팀원자랑
조회수 3203

eventlet을 활용한 비동기 I/O 프로그래밍

안녕하세요. 스포카 크리에이터팀 문성원입니다. 현대적인 프로그래밍 환경에서 네트워크는 더는 특정 직군의 개발자만 접하는 분야가 아닙니다. 그런 만큼 대량의 요청을 네트워크를 통해 송수신하는 프로그램이 생각보다 성능이 나오지 않는 경우를 경험하신 분들도 많으실 겁니다. 물론 스포카 개발팀도 예외는 아니었습니다. 그래서 오늘은 저희의 이러한 경험과 그 해결책-eventlet을 통한 비동기 I/O(Asynchronous I/O)-에 대해 소개합니다.Why우선 스포카 개발팀에서 겪었던 문제부터 시작하죠. 얼마 전 페이스북(facebook)의 FQL(Facebook Query Language)를 통해 정보를 수집해서 이를 활용하는 기능을 작성해야 했습니다. 기존의 함수들은 필요할 때마다 FQL을 요청하는 방식이었고 당연히 이건 너무 느렸죠. 그래서 생각한 것이 “하루의 일정 시간마다 대량의 FQL 요청을 보내서 필요한 정보를 미리 갱신시켜놓자.”였습니다. 여기까진 좋았죠. 이때 제가 작성한 코드의 얼개를 살펴보면 대강 이렇습니다.# 페이스북 계정들을 가져와서 반복하면서for account in FacebookAccount.query:    account.update() #FQL을 보내자.view rawgistfile1.py hosted with ❤ by GitHub그런데 문제가 있었습니다. 기존의 FQL을 보내는 FacebookAccount.update()는 FQL요청이 완료될때까지 멈추고 기다립니다. 대부분의 FQL요청이 2, 3초 정도 걸린다고 했을 때 이러한 지연은 매우 치명적입니다. 대안이 필요했고 자연스레 떠오른 것이 서두에 소개한 비동기 I/O(Asynchronous I/O)였습니다.Asynchronous과거 일부 고급 서버 개발자만 알고 있는(혹은 알아야 하는) 기술로 치부되던 ‘비동기(Asynchronous)’란 개념은 2000년대 들어 등장한 Ajax(Asynchronous JavaScript and XML)의 성공 이후 많은 개발자에게 강한 인상을 줬습니다. 사용자는 HTTP 요청이 끝날 때까지 멈추어 있는 하얀 화면으로부터 해방되었고, 다양하고 많은 요청과 응답들이 자연스럽게 서버로 흘러들어 가서 나왔습니다. 개발자들의 이러한 경험과 통찰은 이후 node.js와 같은 플랫폼의 등장에도 많은 영향을 끼쳤습니다.다시 문제로 돌아가죠. 그렇다면 이러한 비동기에 관한 개념은 위의 상황을 어떻게 해결할 수 있을까요? 문제의 원인부터 다시 살펴봅시다. 2, 3초 정도씩 걸리는 FQL 요청이 문제일까요? 물론 요청이 매우 빨리 처리된다면 별도의 처리 없이도 저 코드는 문제없이 동작합니다. 하지만 현실적으로 이런 I/O의 속도를 빠르게 하는데에는 물리적으로 한계가 있습니다. 오히려 여기에서 주목해야 할 점은 ‘2, 3초’ 보다 ‘기다린다’라는 점입니다. FacebookAccount.update() 같은 경우, I/O가 처리되는 동안 CPU는 하던 일을 멈추고 문자 그대로 기다리게 됩니다. 만약 CPU가 멈추지 않고 다른 요청을 보낸다면 어떨까요? 이렇게 말이죠.비동기만으로는 부족하다?이러한 아이디어는 그동안 많은 개발자가 대량의 I/O를 다루는 올바른 방식으로 여겨왔습니다. 하지만 보통 이러한 비동기 I/O를 통한 구현은 동기식 I/O와는 좀 다른 형태를 띠게 됩니다. 이렇게 말이죠.# http://docs.python.org/library/asyncore.html#asyncore-example-basic-http-clientimport asyncore, socketclass HTTPClient(asyncore.dispatcher):    def __init__(self, host, path):        asyncore.dispatcher.__init__(self)        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)        self.connect( (host, 80) )        self.buffer = 'GET %s HTTP/1.0\r\n\r\n' % path    def handle_connect(self):        pass    def handle_close(self):        self.close()    def handle_read(self):        print self.recv(8192)    def writable(self):        return (len(self.buffer) > 0)    def handle_write(self):        sent = self.send(self.buffer)        self.buffer = self.buffer[sent:]client = HTTPClient('www.python.org', '/')asyncore.loop()view rawgistfile1.py hosted with ❤ by GitHub불행하게도, 이 경우 기존에 사용하던 urllib2대신 HTTP 요청을 처리하는 핸들러를 이처럼 재작성 해야합니다. 거기에 FacebookAccount.update()의 호출 방식마저 바뀔 수 있죠. 더군다나 콜백(Callback) 투성이의 코드는 유지보수가 쉬어 보이지도 않습니다. 여러모로 손이 많이 가는 상황이죠.결국, 기존 코드를 최대한 수정하지 않으면서도, 어느 정도 성능은 보장되는 그런 해결책이 필요했습니다. 그런 해결책이 있을까요? 다행히도 그렇습니다.What저희가 해결책으로 택한 eventlet은 Python(정확히는 CPython)에서 코루틴(Coroutine)을 지원하기 위해 만들어진 greenlet을 이용해 작성된 네트워크 관련 라이브러리입니다. 생소한 용어가 갑자기 튀어나와서 놀라셨을지도 모르니 우선 eventlet에 대해 설명하기 전에 앞에 나온 용어들을 찬찬히 한번 살펴보죠.코루틴과 greenlet먼저 코루틴(Coroutine)부터 살펴보죠. 전산학도라면 누구나 그 이름을 한번은 들어봤을 도널드 카누쓰(Donald Knuth)는 자신의 저서 The Art of Computer Programming에서 코루틴을 다음과 같이 설명합니다.Subroutines are special cases of more general program components, called “coroutines.” In contrast to the unsymmetric relationship between a main routine and a subroutine, there is complete symmetry between coroutines, which call on each other.코루틴은 우리가 잘 알고 있는 서브루틴(Subroutine)과 달리 진입점(Entry Point)이 여러 개일 수 있습니다. 쉽게 이야기하면 실행을 멈췄다가(Suspend) 재개(Resume)할 수 있다는 점인데요. 이 특성을 살리면 우리가 익히 아는 스레드(Thread)처럼 쓸 수 있게 됩니다. 다만 스레드와 달리 코루틴은 비선점적(Non-Preemptive)이기때문에 코드의 흐름을 전적으로 사용자가 제어할 수 있습니다.하지만 불행히도 모든 언어에서 이런 코루틴이 지원되진 않습니다. greenlet은 이런 코루틴을 CPython에서 지원하기 위해 작성된 라이브러리입니다.eventlet코루틴을 통해 스레드를 대체할 수 있다는 점에 주목한 사람들은 greenlet을 통해 유용한 네트워크 라이브러리를 만들어냈습니다. eventlet도 그 중 하나죠. 잠시 eventlet의 소갯글을 봅시다.Eventlet is a concurrent networking library for Python that allows you to change how you run your code, not how you write it.위에서 볼 수 있듯이 eventlet은 사용성에 중점을 두었습니다. 기존의 블로킹 I/O 스타일의 프로그래밍에 익숙한 개발자들도 쉽게 비동기 I/O의 장점을 얻을 수 있게끔 하는 게 목적이죠.특히 저희가 주목한 점은 eventlet의 멍키패치 기능입니다. 멍키패치는 본래 동적 언어에서 런타임에 코드를 고쳐서 별도의 파일 변경 없이 본래 소스의 기능을 변경하는 것을 말합니다. eventlet은 eventlet.monkey_patch 메서드를 통해 표준 라이브러리의 I/O 라이브러리를 논블러킹으로 동작하게끔 변경해서 코루틴에 적합하게 만듭니다.How앞서 소개한 eventlet.monkey_patch를 이용하면 실제로 고칠 부분은 정말로 적어집니다. 다음 코드가 eventlet을 이용해 변경한 전부입니다.import eventleteventlet.monkey_patch() #표준 라이브러리를 변환# 여러가지 import를 하고...pool = eventlet.GreenPool()# 페이스북 계정들을 가져와서 반복하면서for account in FacebookAccount.query:    # 코루틴들에게 떠넘기자.    pool.spawn_n(FacebookAccount.update, account)        pool.waitall()view rawgistfile1.py hosted with ❤ by GitHub정말 적죠? 조금만 구체적으로 살펴보죠. 우선 eventlet.monkey_patch는 socket이나 select등의 Python 표준 라이브러리를 eventlet.green 패키지안에 정의된 코루틴 친화적인 모듈들로 바꿔치기 합니다.# from eventlet/pathcer.pydef monkey_patch(**on):    """Globally patches certain system modules to be greenthread-friendly.    The keyword arguments afford some control over which modules are patched.    If no keyword arguments are supplied, all possible modules are patched.    If keywords are set to True, only the specified modules are patched.  E.g.,    ``monkey_patch(socket=True, select=True)`` patches only the select and     socket modules.  Most arguments patch the single module of the same name     (os, time, select).  The exceptions are socket, which also patches the ssl     module if present; and thread, which patches thread, threading, and Queue.    It's safe to call monkey_patch multiple times.    """        accepted_args = set(('os', 'select', 'socket',                          'thread', 'time', 'psycopg', 'MySQLdb'))    default_on = on.pop("all",None)    for k in on.iterkeys():        if k not in accepted_args:            raise TypeError("monkey_patch() got an unexpected "\                                "keyword argument %r" % k)    if default_on is None:        default_on = not (True in on.values())    for modname in accepted_args:        if modname == 'MySQLdb':            # MySQLdb is only on when explicitly patched for the moment            on.setdefault(modname, False)        on.setdefault(modname, default_on)            modules_to_patch = []    patched_thread = False    if on['os'] and not already_patched.get('os'):        modules_to_patch += _green_os_modules()        already_patched['os'] = True    if on['select'] and not already_patched.get('select'):        modules_to_patch += _green_select_modules()        already_patched['select'] = True    if on['socket'] and not already_patched.get('socket'):        modules_to_patch += _green_socket_modules()        already_patched['socket'] = True    if on['thread'] and not already_patched.get('thread'):        patched_thread = True        modules_to_patch += _green_thread_modules()        already_patched['thread'] = True    if on['time'] and not already_patched.get('time'):        modules_to_patch += _green_time_modules()        already_patched['time'] = True    if on.get('MySQLdb') and not already_patched.get('MySQLdb'):        modules_to_patch += _green_MySQLdb()        already_patched['MySQLdb'] = True    if on['psycopg'] and not already_patched.get('psycopg'):        try:            from eventlet.support import psycopg2_patcher            psycopg2_patcher.make_psycopg_green()            already_patched['psycopg'] = True        except ImportError:            # note that if we get an importerror from trying to            # monkeypatch psycopg, we will continually retry it            # whenever monkey_patch is called; this should not be a            # performance problem but it allows is_monkey_patched to            # tell us whether or not we succeeded            pass    imp.acquire_lock()    try:        for name, mod in modules_to_patch:            orig_mod = sys.modules.get(name)            if orig_mod is None:                orig_mod = __import__(name)            for attr_name in mod.__patched__:                patched_attr = getattr(mod, attr_name, None)                if patched_attr is not None:                    setattr(orig_mod, attr_name, patched_attr)        # hacks ahead; this is necessary to prevent a KeyError on program exit        if patched_thread:            _patch_main_thread(sys.modules['threading'])    finally:        imp.release_lock()view rawgistfile1.py hosted with ❤ by GitHub이렇게 바꿔치기된 eventlet.green안의 모듈들은 I/O에 의해 블럭되는 경우 다른 코루틴에 제어권을 넘기는 식으로 지연을 방지합니다.다른 대안들사실 이러한 목적으로 사용되는 라이브러리는 eventlet만 있는 것은 아닙니다. gevent는 eventlet에서 영향을 받았지만, libevent를 기반으로 하여 더욱 나은 성능과 성숙한 인터페이스를 갖추고 있습니다. 저희처럼 libevent의 설치에 제한이 있는 환경이 아니라면 이쪽을 살펴보셔도 좋습니다.만약 이벤트 주도적 프로그래밍(Event-Driven Programming)에 흥미가 있으신 분은 Twisted역시 좋은 대안이 될 수 있습니다.#스포카 #개발 #개발자 #인사이트 #꿀팁
조회수 1086

[Buzzvil Culture] 개발팀의 모바일 스터디 그룹이란?

 버즈빌 개발팀의 모바일 스터디 그룹이란? 모바일 잠금화면 미디어 플랫폼 ‘버즈빌’의 개발팀이 진행하는 모바일 스터디 그룹이란, 모바일이라는 큰 주제를 핵심으로 하여 크고 작은 연관된 기술을 리뷰하고 토의하는 스터디 모임입니다. 2018년 7월에 처음 개설되어 현재까지 매주 진행하고 있으며 특정한 기한 없이 지속적으로 진행할 예정입니다. 모바일이라는 핵심 주제를 고지하기는 했지만 사실상 개발에 관련된 모든 주제가 이야기될 수 있으며, 개발 언어, 특정 라이브러리 및 프레임워크, 개발 관련 툴, Google I/O와 같은 각종 컨퍼런스 등 거의 모든 것이 저희의 관심사입니다. 심지어 한 번은 자주 쓰는 단축키에 대해서도 토의한 적이 있습니다. 어떤 목적을 갖고 만들어졌는가? 개발이라는 일은 특히나 최신 이슈에 민감한 분야인 것 같습니다. 빈번하게 일어나는 OS 업데이트와 그에 따른 이슈 처리, 주요 컨퍼런스 내용에 따른 개발 트렌드 변화, 갑작스레 혜성처럼 등장한 개발 라이브러리… 저희 개발자들은 이러한 이슈에 항상 귀를 기울여야 하며, 그에 대해 생각을 정리할 필요가 있습니다. 또한 이러한 기술 습득은 저희 직원들의 커리어에도 중요한 지표가 될 것은 자명하지요. 그러나 실제 업무에 집중하다 보면 자칫 이러한 이슈에 대해서 멀어지게 되고는 합니다. 숲을 보지 못하고 나무만 보는 꼴이랄까요. 모바일 스터디 그룹은 바로 이러한 점을 해결해보기 위해서 개설됐습니다. 적어도 1주일에 한 번씩은 업무에서 잠시 떨어져 다양한 개발 주제로 생각을 정리해보자는 게 이 스터디의 목적이며, 다재다능한 그룹원들의 참여 아래 훌륭하게 진행되고 있습니다. 어떻게 진행되고 있는가? 우선, 매주 월요일 점심마다 스터디가 진행되고 있습니다. (스터디를 할 경우 회사에서 점심을 제공하고 있어 회사의 모든 스터디 모임이 더욱 활성화되는 것 같습니다.) 스터디 주제는 1주일 전에 그룹원들과 이야기를 통해서 정하고 있고, 주제가 정해지면 자발적으로 주제에 대해 학습하며 자료를 공유합니다. 스터디 당일에는 일정 시간을 개별 학습하는 용도로 사용하고, 그 후에 각자 공부한 내용을 바탕으로 자기 생각을 이야기합니다. 기본적으로 상황에 맞게 자유롭게 진행되기 때문에 꼭 위와 같은 방식을 고수하지는 않습니다. 때로는 특정 주제에 대해서 스터디원이 세미나를 희망하기도 하는데, 이 경우 발표자가 자료를 만들어서 세미나를 진행하기도 합니다. 한 번 했던 주제에 대해서 다수가 흥미를 가질 경우 다음 주에 조금 더 깊이 있는 이야기를 나누거나 실제 실습을 해보는 시간을 갖기도 합니다. 아직 시도하지는 않았지만, 주요 컨퍼런스 영상을 보는 시간으로도 활용할 생각입니다. 어떤 주제를 진행했는가? 모든 주제를 나열할 수는 없지만, 대표적인 사례에 대해서 전달하겠습니다.  RxJava : Reactive 진영의 자바(Java) 라이브러리. 그 내부 원리와 구조 학습 Unit Test : JUnit 4, Mockito, Robolectric의 활용과 실전 예제 학습 Kotlin(코틀린) : 안드로이드(Android)에서의 Kotlin 트렌드 확인. Kotlin의 장단점 분석 MVP / MVVM : 안드로이드(Android) 아키텍쳐로 바라보는 MVP / MVVM의 내용 및 차이 학습  이 외에도 여러 주제에 대해서 지속해서 스터디를 진행했지만, 위 내용은 스터디원이 전체적으로 공감하고 도입 의지를 이끌었다는 점에서 인상적이었던 것 같습니다. 특히 코틀린과 같은 경우는 실험적으로 프로젝트에서 도입을 진행하고 있고, 코드 간결화, Null-Safety 측면에서 큰 장점을 느끼고 있습니다. 이처럼 저희 스터디는 학습하게 된 내용을 단순히 지식으로 놔두지 않고 실제 프로덕션에 도입까지 충분히 진행 할 수 있으며, 반대로 실제 프로덕션에 더 좋은 기술을 도입하기 위해서 다양한 주제를 찾아가고 있습니다.버즈빌의 스터디는 무엇이 다른가? 개인적으로 꽤 많은 스터디에 참여해 봤다고 생각합니다. 다양한 주제는 물론 강의형, 토론형 등 여러 방식으로 진행해본 경험이 있습니다. 그중에는 1년 넘게 유지되면서 다양한 지식을 습득한 모임도 있었고, 몇 번 해보지도 못하고 와해한 안타까운 케이스도 있었습니다. 덕분에 좋은 스터디란 무엇인가에 대해 꽤 고민을 해봤고 어떤 부분이 중요한지 나름대로 생각하고 있는 부분이 있습니다. 그리고 그러한 측면에서 버즈빌의 스터디는 좋은 스터디라고 분명히 말씀드릴 수 있습니다. 그렇다면 구체적으로 어떤 점이 버즈빌의 스터디를 좋게 만드는 것일까요? 그 이유는 다음과 같습니다. 첫째, 버즈빌의 수평적인 문화 버즈빌의 사내 문화는 수평적이고 자율적인 문화로 유명합니다. 소위 고루한 잔소리꾼 문화가 없기 때문에 자신의 의견을 누구나 자유롭게 이야기합니다. 사내문화가 스터디와 무슨 상관이 있냐 하실 수 있지만, 수직적인 조직의 사내 스터디와 비교했을 때 큰 차이를 볼 수 있었습니다. 버즈빌의 스터디에서는 여러 사람이 어떠한 권위에 눈치 보지 않고 자유롭게 자신의 의견을 제시하며, 듣는 이 또한 어느 의견이든 함부로 가늠하지 않고 진지하게 받아들입니다. 이는 단순히 스터디 토론에서만 적용 되는 것이 아니라, 스터디 시스템에 대해서도 불합리하거나 개선하고 싶은 점을 여과 없이 이야기합니다. 그리고 그들의 의견을 피드백하여 시스템이 지속적으로 개선되고 있습니다. 결국은 버즈빌의 수평적인 문화가 스터디 문화 자체도 현실적이고 합리적으로 바꿔나간다고 할 수 있습니다. 둘째, 뛰어난 구성원 스터디에서 구성원은 분명 굉장히 중요한 요소입니다. 구성원의 역량과 열정에 따라서 스터디의 질과 지속력이 결정됩니다. 그런 측면에서 버즈빌은 상당히 축복받은 조직임에 틀림없습니다. 당장 제 옆만 둘러봐도 어디서 이런 분들이 나왔을까 싶을 정도로 뛰어난 역량의 소유자가 많으니까요. 아마 인사팀에서 일을 잘하고 있나 봅니다. 여하튼, 버즈빌에는 다재다능한 인재가 정말 많습니다. 각종 분야에 있어서 상당한 지식을 보유하신 분도 굉장히 많으시고, 무엇보다 개발을 좋아하고 새로운 기술을 배우는 것에 긍정적입니다. 열정이 넘친 나머지 스스로 일정을 잡아서 기술 세미나를 진행하기도 하지요. 이런 분들과 함께 하는 스터디, 안 좋을 수가 없습니다. 셋째, No 강제, No 의무 제가 생각하는 좋은 스터디의 중요한 요소는 지속력입니다. 아무리 좋은 스터디라도 무리한 일정과 과제의 압박이 있다면 지속되기 힘들다고 생각합니다. 단발성으로 집중하여 어떤 지식을 습득하려는 게 아닌 이상은, 결국 얼마나 꾸준히 스터디원이 참여하고 공부를 할 수 있는지가 중요합니다. 그러한 측면에서 볼 때 참가를 강제하고, 어떠한 의무성인 과제를 부여하는 것은 지양해야 합니다. 공부는 스스로의 의지에 의해서 수행되어야 하며, 스터디 시스템에서 이를 강제 해봤자 결국은 보여주기 식의 활동밖에 되지 않습니다. 사람이 어떻게 모든 주제에 항상 열정적으로 공부를 하겠습니까. 그렇기에 스터디라는 시스템보다는 사람이 우선이어야 하며, 공부는 본인의 자유입니다. 위와 같은 요소로 인해 전 결론을 내봅니다. 버즈빌에서 굉장히 좋은 스터디를 하게 되었다고. 결론 버즈빌에서 스터디는 CEO 분들을 비롯하여 많은 구성원이 장려하고 권장하는 부분입니다. 그들은 직원의 역량 강화가 곧 회사 역량의 강화라는 인식을 바로 갖고 있으며, 이를 위해 정책적으로 지원하는 방안을 마련해주고 있습니다. 스터디 제도뿐만 아니라 각 개인이 성장할 수 있도록 동아리 지원, 자기개발비 지원 등은 물론 읽고 싶은 책은 무제한으로 제공 해주고 있습니다. 어쩌면 이러한 사소한 점 하나하나가 버즈빌의 소중한 자산이 아닐까 생각하며, 이만 글을 마무리 짓습니다. 감사합니다.작가소개 Ethan Yoo, Software Engineer (Android) 안녕하세요. 버즈빌에서 안드로이드 부분 개발을 담당하고 있는 Ethan (이든)입니다. 개발이라는 주제로 다양한 곳에 관심사를 갖고 있고, 동료와 함께 개발 이야기를 하는 것을 좋아합니다. 메인 언어는 자바(Java)를 사용하고 있지만, 코틀린(Kotlin) / 파이썬(Python) / 자바스크립트(JavaScript) / 하스켈(Haskell) 등 다양한 언어에 대해 경험이 있습니다. 최근에는 시스템 아키텍쳐에 관심을 갖고 반응형 프로그래밍, 함수형 프로그래밍 등이 안드로이드와 어떤 구조로 표현 될 수 있을지 고민하곤 합니다. 제가 만든 서비스가 세상을 바꿀 수 있기를 희망하고, 이를 위해 버즈빌에서 오늘도 열심히 개발을 하고 있습니다.
조회수 2124

Kubernetes에 EBS 볼륨 붙이기

Kubernetes에서 컨테이너에 Persistent Volume을 붙이는 방법은 몇가지 있다. 여기서는 Kafka 서비스를 예로 삼아 주요 접근방법을 간단히 알아본다.Kubernetes v1.4.0를 기준으로 문서를 작성한다.Static말이 Static이지 수동 마운트를 뜻한다. 기본적으로 관리자가 EBS 볼륨을 만들고특정 Pod에 그 볼륨을 붙이는 작업을 한다. Volumes 문서에 나오는대로 하면 간단하다.apiVersion: v1 kind: Service metadata: name: kafka1 labels: app: kafka1 tier: backend spec: ports: # the port that this service should serve on — port: 9092 name: port targetPort: 9092 protocol: TCP selector: app: kafka1 tier: backend — - apiVersion: extensions/v1beta1 kind: Deployment metadata: name: kafka1 spec: replicas: 1 template: metadata: labels: app: kafka1 tier: backend spec: containers: — name: kafka1 image: wurstmeister/kafka imagePullPolicy: Always volumeMounts: — mountPath: “/kafka” name: kafka1volume ports: — containerPort: 9092 volumes: — name: kafka1volume awsElasticBlockStore: volumeID: vol-688d7099 fsType: ext4여기서 핵심은 다음의 두 줄 뿐이다.awsElasticBlockStore: volumeID: vol-688d7099Dynamic수동으로 볼륨을 붙이는 방법은 간단해서 좋다. 하지만 Autoscaling하는 서비스에 넣기에는 아무래도 무리다. 서비스가 뜰 때 요구사항에 맞는 볼륨을 스스로 만들어 붙이는 방법도 있다. Kubernetes Persistent Volumes를 참고해 작업해본다.우선 Kubernetes 생성할 EBS 볼륨의 사양을 정한다.# storages.yaml apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: name: default1a provisioner: kubernetes.io/aws-ebs parameters: type: gp2 zone: ap-northeast-1a iopsPerGB: “10” — - apiVersion: storage.k8s.io/v1beta1 kind: StorageClass metadata: name: default1c provisioner: kubernetes.io/aws-ebs parameters: type: gp2 zone: ap-northeast-1c iopsPerGB: “10”default1a를 선택하면 ap-northeast-1a Availablity Zone에 기가바이트당 IOPS는 10인 General SSD EBS 볼륨을 생성한다. 이제 다시 Kafka의 돌아가면apiVersion: v1 kind: Service metadata: name: kafka1 labels: app: kafka1 tier: backend spec: ports: # the port that this service should serve on — port: 9092 name: port targetPort: 9092 protocol: TCP selector: app: kafka1 tier: backend — - apiVersion: extensions/v1beta1 kind: Deployment metadata: name: kafka1 spec: replicas: 1 template: metadata: labels: app: kafka1 tier: backend spec: containers: — name: kafka1 image: wurstmeister/kafka imagePullPolicy: Always volumeMounts: — mountPath: “/kafka” name: kafka1volume ports: — containerPort: 9092 volumes: — name: kafka1volume persistentVolumeClaim: claimName: kafka1volumeclaim — - kind: PersistentVolumeClaim apiVersion: v1 metadata: name: kafka1volumeclaim annotations: volume.beta.kubernetes.io/storage-class: “default1a” spec: accessModes: — ReadWriteOnce resources: requests: storage: 300Gi이제 awsElasticBlockStore가 아닌 PersistentVolumeClaim을 통해 볼륨을 할당받는다. kafka1volumeclaim은 default1을 기준으로 스토리지 정책을 정하므로Availablity Zone: ap-northeast-1aIOPS: 기가바이트당 10General SSD300Gi 이상인 스토리지를 원한다는 요구사항을 기술한다. 위의 설정은 이러한 스토리지에 부합하는 EBS 볼륨을 생성하여 kafka1 Pod에 할당한다.분석Dynamic은 Autoscaling에는 적합하나 kubectl delete [service] 또는 kubectl delete [deployment] 등의 명령을 수행하여 서비스를 내렸다가 다시 올린 경우에 기존에 쓰던 볼륨을 마운트하지 않고 새 볼륨을 만드는 문제가 있다. 물론 delete를 하지 않고 서비스를 업데이트만 하는 경우에는 볼륨이 유지되지만 이래선 아무래도 문제의 소지가 많다.그래서 또다른 시나리오를 고민해볼 수는 있다. 짧게 설명하자면관리자가 Volumn Pool을 만들어놓고 Autoscaling 서비스가 이 풀 안에서 볼륨을 할당받게 한다. 이러면 앞서 본 두 가지 방식의 장점을 골고루 흡수할 수 있다.flocker 또는 glushterfs 같은 스토리지 관리 서비스를 활용해도 좋다. 하지만 배보다 배꼽이 큰 것 같은 느낌이 들지도 모르겠다.#데일리 #데일리호텔 #개발 #개발자 #개발팀 #인사이트 #꿀팁
조회수 3597

코인원 개발자는 어떻게 일할까?

코인원의 파이콘 한국 2018 참여기 그 두번째 이야기!코인원의 핵심, 코인원의 자랑 ‘코인원 개발자’들에 대한 이야기를 나눠보려 합니다.지금의 코인원이 있을 수 있는 이유는 바로 더 좋은 프로덕트를 만들어내기 위해 치열하게 고민하는 개발 크루들의 노력이 있었기 때문입니다.코인원은 지난 파이콘 한국 2018 ‘열린공간(Open Speak Talking)’ 세션에 참여했습니다. 이 시간을 통해 그동안 코인원 개발 크루들이 축적한 지식과 경험 그리고 노하우를 공유했어요. 파이콘이 개발자들을 위한 행사인만큼, 코인원의 개발문화와 환경, 채용 원칙에 대한 심도깊은 이야기가 오고갔답니다.그래서 예고했던대로 코인원 개발자들의 ‘Mini Interview’를 통해 개발 크루들은 기술본부를 어떻게 만들어나가고 있는지 자세히 알려드릴게요 :-)'파이콘 한국 2018' 후기 1탄 현장스케치▼코인원X'파이콘 한국 2018' 현장스케치!파이썬 개발자들의 즐거운 축제, ‘파이콘 한국 2018’이 지난 19일 성황리에 막을 내렸습니다. 이번 행사...blog.naver.com지난 8월 19일 진행된 파이콘 열린공간 현장, Open Speak Talking!진우님(CTO)을 집중 심문하고 있는 피플팀 수장 대경님코인원 기술본부는 어떻게 구성되어 있나요?진우님(CTO) : 안녕하세요, 코인원 CTO(Chief Technical Officer, 최고기술책임자) 이진우 입니다. 현재 코인원 기술본부는 총 8개의 팀으로 이루어져 있는데요. Core팀, Web팀, API팀, APP팀, QA팀, SRE팀, 데이터팀, 기술연구팀으로 나뉘어진답니다. 코인원 기술본부는 사용자에게 편리한 서비스를 제공하기 위해 프로덕트를 만들어나가고 있어요. 먼저, 사용자에게 직접적으로 보여지는 화면인 프론트엔드를 책임지는 ‘Web팀과 APP팀’ 그리고 사용자에게 보이지 않지만 시스템의 안쪽에서 수행되는 백엔드를 책임지며 프론트엔드에서 필요로하는 결과를 제공하는 ‘Core팀, API팀, QA팀, SRE팀, 데이터팀, 기술연구팀’이 인터렉티브하게 일하고 있습니다.저희는 암호화폐 거래소 코인원을 더욱 더 잘 구축하고, 개선하고, 안전하게 운영하는 것을 목표로 삼고 있어요. 또한 개발 뿐만 아니라, 지속적인 블록체인 연구를 통해 거래소에 최신 기술 트렌드를 반영할 수 있도록 항상 고민하죠.파이콘의 질문왕, 웹팀의 경화님 :)저 멍때리는거 아니에요, OST에 집중하고 있는거에요. (feat. 새로찬가로찬님)눈감은거아니에요, 뜬거에요. (feat. 킹갓제너럴대현님)코인원 개발자들은 어떻게 일하나요?경화님 (Web developer) : 코인원 개발자들은 서로 어떤 업무를 하고 있는지 눈으로 트래킹 할 수 있고, 투명하게 업무를 공유할 수 있는 개발환경을 만들고 있어요. 저희는 협업툴로 Jira와 Confluence를 사용하고 있습니다. 협업툴로 업무의 효율성을 높이면서 새로운 개발업무에 집중하는데 많은 도움이 되고 있어요. 또한 협업툴 이외에도 새로운 기술 도입에 긍정적이라 자기주도하에 여러가지 기술을 실무에 적용해볼 수 있다는 점이 매력적이죠.새로찬님 (Engineer) : 주어진 요구사항에 맞게 그대로 개발하기 보다는 요구사항의 필요성에 대해 공감하고 더 좋은 방향으로 만들 수 있도록 기획, 디자인, 개발 모든 단계에서 능동적으로 의견을 제시합니다. 기술본부에서는 빠르게 변화하는 시장상황에 맞추기 위해 매일 아침마다 PM, 개발자, 디자이너가 모여 *데일리스크럼을 진행하는데요, 서로의 Task나 Project 진행상황을 공유하고 최고의 프로덕트를 사용자에게 전달하기 위해 노력하고 있어요. *데일리스크럼이란? 정해진 시간에 개발크루들이 모여서 어제 했던 일과 오늘의 할 일 등을 공유하고, 다른 개발 크루들이 이야기하는 업무현황을 들으면서 내가 기여할 수 있는 부분과 이슈를 빠르게 파악할 수 있는 자리에요! 대현님 (Engineer) : 서비스를 만드는데 있어 주도적으로 그리고 적극적으로 개발업무를 수행합니다. 예를 들어, 코인원 Live Service에서 Manual하게 처리하고 있었던 이슈들을 자동화하는 프로젝트가 있었는데요. 수동으로 해결할 수 있는 부분이지만, 이를 서비스 기획자분들과 연계해 Task로 만들어 해결방안을 얻을 수 있었습니다. 또한 여기서 그치는게 아니라, 계속해서 코인원 사용자들을 위해 필요한 기능들을 추가하는 프로젝트에 참여하면서 많은 재미를 느꼈죠.왼쪽부터 파이콘의 나이스걸 (윤정님), 개발자 (희수님), ddddeveloper (선우님)개발본부의 공식포즈, 쁘쁘브브브이 종헌님 ㅇ_ㅇV코인원 개발자가 되면 어떤 점들이 좋나요?희수님 (Engineer) : 이전에 경험하지 못했던 거래소 프로덕트를 만들어볼 수 있다는 점이 정말 좋습니다. 저는 원래 암호화폐와 핀테크에 관심이 많았는데요. 코인원에서 거래소 프로덕트를 직접 만들면서 관심 분야에 대해서도 더 많이 알게 되고, 또 이런 서비스를 개발할 때 중요한 것이 무엇인지 고민해보면서 배워가는 것이 정말 많아요. 제가 코인원 합류 이전에 개발한 프로덕트들과는 성향이 많이 달라서 더 재미를 느끼고 있어요!종헌님 (Web developer) : 함께 일하는 모든 개발자 분들이 더 효율적인 개발 프로세스를 만들기 위해 치열하게 고민하는 것을 보면서 매일 놀라고 있어요. 서비스를 개발하며 느끼는 문제점을 도출하면서 치열하게 토론하고, 그 문제들을 해결하기 위한 아이디어들을 직접 시도해 볼 수 있어서 좋아요. 일을 더 효율적으로, 즐겁게 할 수 있는 환경을 만드는 과정이 코인원만의 애자일을 만들어나가는 것 같아 더 열정적으로 일할 수 있는 것 같습니다.선우님 (Web developer) : 많은 사용자들이 직접 이용하는 서비스를 만드는 개발자로서 다양한 문제 상황을 직면하고 해결해 나가며 보고 배우는게 많습니다. 또 코인원 기술본부는 여느 코인원 조직처럼 언제든 자신의 의견을 자유롭게 이야기하는 분위기가 형성돼있어요. 특히 하나의 프로젝트가 끝나면 회고(Retro)를 진행하는데, 프로젝트의 진행과정, 이슈, 결과물을 공유하면서 저 자신을 성장시켜나갈 수 있는 요소들이 많습니다.안녕하세요, 코인원 신입개발자 (a.k.a CTO) 입니다. (인사성 밝음밝음)“코인원에서는 어떤개발자를 원하나요?진우님 (CTO) : 블록체인을 좋아하고, 개발을 좋아하시는 분이라면 언제든지 환영입니다. 코인원 개발 크루들을 생각했을 때 ‘몰입’이라는 단어가 가장 먼저 떠오르는데요. 모두가 마니아적이고 덕후기질이 있어, 자기주도적으로 업무를 하고 적극적으로 개발할 것들을 찾고 실행합니다.  앞으로 코인원 개발 크루로 합류하실 분들 또한, 적극적으로 아이디어를 내고, 그 아이디어를 실현할 수 있도록 프로젝트를 리딩할 수 있는 분이었으면 해요. 물론 개발하는데 있어서는 누구보다 신중한 자세가 필요하겠죠? 누구보다도 내가 짠 개발코드를 꼼꼼하게 검토하고, 표용력이 있어 좋은 아이디어에는 귀 기울일 줄 아는 분들이셨으면 좋겠습니다. 참고로 Node.js, Python, Spring, C#, AngularJS를 업무에 많이 활용하고 있답니다.개발을 사랑하시는 분! 블록체인을 함께 탐험할 준비가 되신 분! 코인원 개발자 채용에 많은 관심 부탁 드립니다.코인원 개발자 채용에 많은 관심 부탁드립니다 :-)지금까지 미니 인터뷰를 통해 코인원 개발 크루들의 이야기를 들어봤습니다:) 블록체인이라는 새로운 기술 영역에서 매일매일 즐겁게 도전하고 있는 코인원 개발팀에 합류하고 싶은 분들은 현재 코인원 개발자 채용이 진행되고 있으니 지금 바로 확인해주세요!#코인원 #블록체인 #기술기업 #암호화폐 #스타트업인사이트 #기업문화 #조직문화 #팀원소개 #인터뷰
조회수 1755

[인터뷰]미미박스의 TECHNOLOGY를 이끄는 CTO KAY를 만나다

안녕하세요. Ava입니다.여러분에게 더 건강하고, 아름다운 라이프스타일을 제공하기 위해 노력하는 미미박스 뒤에는여러분의 니즈를 만족시키고 안정된 서비스를 제공하기 위한 개발이끊임없이 진행되고 있습니다.오늘은 미미박스 TECHNOLOGY UNIT를이끌고 계신 김종광 CTO(이하 KAY) 님을 소개해드리겠습니다.KAY는 대기업과 IT기업에서 앱 개발과 웹 개발을 진행했던 커리어를 갖고 계신데요. 경력과 전문성 뿐만 아니라 개발자들이 상상할 수 있는 문화를 강조하며 만들어나가고 있습니다.항상 푸근한 아빠 미소로 미미박서의 질문과 제안을 받아주고,열린 리더의 모습을 보여주시는 KAY를 소개합니다.김종광 (Kay) 한국기술교육대학교 전기전자공학 석사전) NC소프트전) SK communicationsUNIT1. KAY를 소개해주세요.Q. 안녕하세요. 항상 아빠 미소를 짓고 계신 KAY~KAY를 소개해주세요.A. 처음에 미미박스에 모바일 앱 개발 총괄로 입사했어요. 지금은 개발 UNIT 전체를 맡고 있고요. 세 가지의 주 업무가 있는데요. 개발 전체 프로젝트를 leading 하고 다른 팀과 연관된 업무에 대해서 지원하는 일과, 새로운 개발자를 충원하는 업무를 하고 있습니다.Q. 미미박스에 입사하시게 된 계기는 무엇인가요? A. 처음에는 지인이 추천해서 미미박스를 알게 되었어요. 그 후에 미미박스에 대해 조사를 해봤죠. 비즈니스 모델, 성장 가능성을 봤을 때 '될 것 같다'는 생각을 가졌어요. 그리고 몇 번 찾아갔었는데 회사 분위기가 활기차고 재밌었어요. 그리고 일하는 사람들 중에 아는 사람들이 4~5명 정도 더 있었어요. 이분들과 다른 구성원들을 보면서 '이 친구들이랑 같이 일하면 즐겁게 일할 수 있겠다'라는 생각이 들었죠.Q. 여러 조직에서 있으셨던 만큼 개발 업무 자체에 대한 이유도 있을 것 같아요. A. 보통 기업에서는 개발자의 역할이 상당히 제한되어있어요. 업무에 대한 의사결정 권한이 거의 없죠. TOP-DOWN 방식으로 내려온 것들을 그냥 해야 하는 경우가 많거든요. 개발자들은 다들 알 거예요. 만들면서 '이거 안될 것 같다.'라는 감이 있는데, 느낌상으로 안될 것 같은 것을 만드니까 의욕이 생기지 않는 경우가 있었어요. 효과성보다는 어떤 서비스를 오픈했다는 것 자체가 실적이 되는 경우가 많거든요. Q. 그런 경우가 있군요. 그렇다면 미미박스 내에서는 개발 업무에 대해 어떤 식으로 의사결정이 이루어지나요?A. 업무에 대한 의사결정은 반반인 것 같아요. 우선 TECHNOLOGY UNIT 내부에서 프로젝트를 진행하는 것이 있어요. '이렇게 하면 회사와 서비스에 도움이 되겠다. 매출도 좋아질 것 같다. 사람들도 좋아할 것 같다'이런 의견을 내고 직접 만들 수 있고요. 서비스를 같이 진행하는 마케팅팀이나 플랫폼 운영팀에서 요청이 들어오면 그 요청에 대해 저희가 납득하고 하면 좋겠다고 생각하는 업무에 대해서 일정을 짜고 진행해요. 각 요소 별로 개발 측면의 논의도 많이 하고 실제 만드는 사람의 의견이 많이 반영됩니다. Q. 직접 만들고 구축하는 사람들의 의견은 정말 중요한 것 같아요.TECHNOLOGY UNIT을 이끌고 있는 UNIT 장님으로서 KAY의 하루 스케줄은 어떻게 되나요?A. 출근 후, 오전에는 집중 개발 업무를 하고 있어요. 제가 플랫폼 개발 팀장도 겸임하고 있거든요. 오후부터는 대부분 팀미팅이나 프로젝트 미팅을 많이 합니다. 프로젝트의 진행사항을 체크하고, 개발이 어려운 부분과 개발하면서 중요하게 생각하는 부분에 대해서 토론하죠. 그리고 새로운 개발 인력들 채용을 위해 면접을 많이 봅니다.Q. TECHNOLOGY UNIT 내부에서 소통과 역량 강화를 위해 주기적으로 여는 세션이 있다고 들었는데 소개해주세요!A. 일주일에 한 번씩 주니어 개발자를 대상으로 스터디를 진행하고 있습니다. 저는 스터디를 leading 하고 멘토 역할을 하고 있고요.이런 시간을 만들게 된 이유는 소통의 장을 만들고 개발자들의 역량을 키우기 위해서입니다.주니어 개발자들이 시니어 개발자들 앞에서 의견을 내는 것에 대해서 소극적인 면이 있어요. 틀릴까 봐 의견을 쉽게 못 내죠. 그래서 주니어 개발자들끼리 모여서 얘기할 공간을 만들어 주는 것이 중요하다고 생각했어요. 서로 의견을 내고 토론하면서 개발에 대한 역량도 쌓고 의견을 내는 훈련도 할 수 있죠.그리고 DATA UNIT의 협조를 받아서 빅데이터 관련 스터디를 진행하고 있어요. 그래서 개발자들 중 관심 있는 사람이들 모여서 빅데이터 관련 LOGIC을 만들어보고, 아이디어를 실현시켜보는 작은 프로젝트를 그룹별로 진행하고 있어요.앞으로는 이런 세션들을 발전시켜 세미나를 열 예정이에요. 그래서 각 개발자들이 적어도 1년에 2번 이상은 주제 발표할 수 있도록 환경을 만들려고 합니다. 개발 업무는 집중도가 높아서 건조해질 위험이 있어요. 집중하다 보면 일에 치여서 자기계발이 어려워질 수 있기 때문에 계속 자기계발하는 분위기를 만들어가려고 합니다. 전사적으로도 그런 분위기가 계속 만들어지면 좋겠어요.Q. 건조하긴요! 제가 보기엔 개발팀들이 가장 활발하고 참여도도 높은 것 같은데요! 열려있는 분들도 많고요.A. 개발팀이 아닌 팀들이랑 많이 소통하라고 조언을 많이 해요. 미미투게더(2개 이상 팀이 함께 회식하면 회식비를 지원해주는 기업문화 제도)를 할 때도 개발팀 내부에서만 하지 말고 무조건 다른 팀들과 함께하라고 하고 있어요. 새로운 아이디어를 나눌 수 있고 인간관계가 힘이 될 때가 많기 때문에 다른 팀들이랑 얘기를 많이 나누는 게 필요하죠. UNIT2. TECHNOLOGY UNIT을 소개해주세요.Q. TECHNOLOGY UNIT을 소개해주세요.A. TECHNOLOGY UNIT에서는 지금 미미박스에서 서비스하는 모든 PRODUCT, 플랫폼, 모바일 앱, PC 웹, 내부 직원들이 쓰는 모든 것들을 개발하고 있습니다. 대부분의 기업에서는 계약직이나 파견직의 고용형태로 진행하는 경우도 있는데, 저희는 모든 구성원이 정직원으로 개발 업무를 하고 있습니다.Q. TECHNOLOGY UNIT의 분위기는 어떤가요?A. 개발자라는 직무를 하는 사람들은 생각 자체가 자유로워야 합니다. 경직되어있으면 좋은 아이디어가 떠오르지 않죠. 그래서 TECHNOLOGY UNIT은 최소한의 규제나 룰을 두고 자유롭게 활동하게 하고 있어요. 특별한 일이 아니면 회의 소집도 지양하고 있어요.다양하게 상상하려면 경직되지 않고, 룰에 집착하지 않는 문화를 만들어야 하기 때문이에요. 그래야 본인의 의견도 편하게 이야기할 수 있죠. 구성원들을 보면 시니어 개발자들은 적응을 잘해요. 주니어 개발자들이 아직 조금 경직되어있긴 해요.지금 신입 공채 2기를 뽑고 있는데요. 보통은 스타트업에서 입사 후 바로 투입될 수 있는 사람을 뽑아요. 하지만 저는 확신이 있어서 저희 미미박스의 DNA를 가지고 처음부터 함께할 수 있는 신입을 뽑고 싶어요. 미미박스의 DNA를 가지고 더 성장하게 되면 저희 개발 조직에 기둥이 될 수 있을 거라고 생각합니다. <채용공고 보러 가기 클릭>Q. KAY와 함께 하는 구성원들이 점점 부러워지네요. 정말 구성원들의 성장에 많은 비중을 두고 여러 계획을 실천하는 것 같아요. 미미박스에는 여성 개발자도 점점 많아지는 것 같은데 재미있는 에피소드 있나요?A. 먼저 여성 개발자들의 비중이 점점 늘어나고 있어요. 우리가 여성 고객을 위한 서비스를 많이 하고 있잖아요. 그 감성을 같이 공유할 수 있는 사람이 많아지면 기술적인 부분뿐 아니라 감성적인 부분에서도 큰 시너지가 나죠. 실제로 웹페이지에 제품 가격이 잘못 올라간 적이 있어요. 저희 남성 개발자들이 그 데이터를 먼저 보는데 '이게 맞는 가격인가' 의심하는 사람이 아무도 없었어요. 그때 여성 개발자분이 '이 제품이 이 가격이 아닐 텐데? 문제를 제기했고 다행히 수정할 수 있었죠. Q. 그래도 남성 개발자들의 화장품 가격에 대한 감이 점점 정확해질 것 같아요. 호호KAY 님이 UNIT을 운영하시면서 가장 보람을 느끼신 적은 언제인가요?A. 고객들이 많이 와서 저희 서비스를 이용해 주실 때 보람을 느낍니다. 저희 UNIT 자체에서도 무언가를 만들어가고 있다는 것을 느끼고, 실제로 좋은 반응을 얻었을 때 기분이 정말 좋아요. Q. 점점 더 많은 분들이 미미박스를 찾아주신다는 게 느껴져요! 앞으로의 목표는 무엇인가요?A. 첫 번째는 글로벌로 플랫폼을 옮기는 것입니다. 저희 내부에서 개발한 플랫폼과 서비스가 점점 확대돼서 미미박스가 해외에 진출할 때마다 플랫폼을 그대로 이동시켜 글로벌화하는 것이 첫 번째 목표고요. 두 번째는 앞으로 온라인을 넘어 오프라인에 대한 서비스도 진행할 예정이에요. 현재 미미박스 플랫폼과 오프라인 요소의 연계성을 찾고 최고의 고객 경험을 만드는 것이죠. 일반적인 O2O 서비스를 넘어 대부분의 고객이 여성이기 때문에 IT 기술 자체가 숨어있고, 알아서 돌아가게 만드는 서비스를 만들 것입니다.미미박스는 뷰티에 대해서 많은 강점과 다양성을 가지고 있기 때문에 이런 것들을 통해 저희만 할 수 있는 서비스를 만들고 싶어요. 마지막은 Data-driven 방식을 더욱 견고히 가져가는 것이에요. 축적되어있는 경험과 데이터를 통해서 고객 맞춤형 서비스에 대한 역량을 강화하는 것이죠. Q. 글로벌 플랫폼, O2O 서비스, Data-driven 앞으로의 TECHNOLOGY UNIT이 만들어낼 것들이 기대돼요. 두근두근. 마지막으로 KAY가 TECHNOLOGY UNIT을 리드하면서 가장 집중하는 3가지가 무엇인지 궁금합니다. A. 가장 중요한 것은 우리 개발자들의 커리어를 관리해주는 것이에요. 이분들이 미미박스에 와서 자기의 역량이 발전하지 않고 정체되다면 제가 역할을 제대로 못했다는 뜻이거든요.그래서 구성원들이 고생을 하든 뭘 하든 해가 갈수록 성장할 수 있도록 관리하는 것에 집중하고 있어요. 두 번째는 우리가 TECH 조직이기 때문에 서비스가 아주 정상적으로 운영되는 것이 목표에요. 단순한 장애를 없애는 것뿐만 아니라 계속 플랫폼이 발전하면서 문제가 없게 만들어야 하죠. 매출, 데이터가 계속 쌓이면서 안정적인 서비스를 만드는 것, 기본적인 것 같지만 가장 중요한 것 같아요. 마지막으로는 Align이에요. 개발팀이 성장할 수 있는 서비스, 개발 역량을 강화시키다며 보면 회사의 목표에 Align 되는 것을 놓칠 수 있어요. 그렇기 때문에 개발자들이 관심 있는 것들과 회사의 목표를 Align시켜서 시너지 효과를 낼 수 있도록 집중하고 있습니다.UNIT2. TECHNOLOGY UNIT으로서 어떤 사람과 일하고 싶나요?Q. TECHNOLOGY UNIT에서 일하기 위하여 갖추어야 할 역량은 어떤 것이 있나요?A. 첫 번째로 성장 가능성을 봅니다. 성장 가능성에는 여러 가지 의미가 있지만 적극적이고, 새로운 지식에 대한 욕구가 항상 강한 사람이어야 합니다. 배우고 싶은 열망, 해보고 싶다는 열망을 가지고 실제 구체적으로 실행해본 경험이 있고, 뭔가를 해본 사람이 성장 가능성이 있는 사람이라고 생각합니다. 제가 면접을 볼 때마다 항상 물어보는 것이 '5년 후 계획, 5년 후 모습은 어떨 것 같아요?'에요. 면접자가 적극적으로 대답하면 '그것을 위해 어떤 실행계획이 있는지' 물어보죠.두 번째로는 스타트업 마인드 FIT이 맞는 것이에요. 저도 미미박스에 처음 왔을 때 힘들었어요. 갖춰져 있는 게 없었거든요. 하나부터 열까지 하려면 뭔가 어디서 걸리는 거예요. 큰 회사는 세팅이 다 되어있는데 말이죠. 그래서 뭔가를 하려면 그 업무뿐 아니라 처음부터 다 찾고 만들어야 해요. 이렇게 만들어가는 걸 좋아하는 사람이 있어요. 준비가 안되어있다고 불평하는 것이 아니라 부족한 환경에서 할 거리가 많은 것을 반기는 사람들. 이런 사람들은 '이것저것 해봐야지~' 신나있어요. 이런 마인드 FIT을 많이 봅니다.Q. 스타트업 마인드 FIT 정말 공간되는 말인 것 같아요. 저도 갖춰져있는 틀에서 무언가를 하는 것보다 이것저것 찾아서 만드는 걸 좋아하거든요! 그런 분들이 많이 오시면 재밌는 일이 많이 벌어질 것 같아요. 우리 미미박스의 비전은 'Beautify the people'인데요. 혹시 취업이나 이직을 준비하는 분들께 이것만은 아름답게 관리하라고 조언하고 싶은 게 있나요?A. 이력서와 경력기술서를 아름답게 해야 해요. 개발자들 중에 '내 역량만 좋으면 되지'라고 생각하시는 분들이 있는데 자신의 커리어 패스를 만드는 것도 중요하거든요. 회사에서 처음에 서류전형을 진행하는 게 많은 내용을 내포하고 있어요. 경력기술서의 내용이 부실하면 회사도 본인도 FIT이 맞는 곳을 찾기가 어려워지죠. 어디서든 인정받는 사람이 되려면 자신의 업무와 역할을 충실하게 표현한 경력기술서를 작성하라고 말씀드리고 싶네요.Q. 정말 실질적인 조언이네요. 누구보다 깊게 고민하고 집중한 일일수록 경력기술서와 이력서를 잘 쓸 수 있고 자신의 경력도 잘 전달할 수 있을 것 같아요.마지막으로 함께 일하고 있는 미미박서분들께도 한마디 해주세요!A. 제가 여기 처음 와서 한 이야기가 있어요. "여기가 제 마지막 회사입니다."그렇게 이야기한 이유는 미미박스의 성장 가능성, 발전 가능성을 보았고 믿음이 있기 때문이죠. 모두가 같이 노력한다면 원하는 것을 이룰 수 있을 거라 생각해요. 다 같이 파이팅!
조회수 1172

애플리케이션 개발부터 배포까지, AWS CodeStar

OverviewAWS CodeStar를 이용하면 애플리케이션의 개발-빌드-배포까지 빠르게 진행할 수 있습니다. CodeStar는 몇 가지 장점을 가지고 있는데요. 오늘은 간단한 Python App Service Tutorial을 통해 CodeStar를 사용하는 방법을 알아보겠습니다. CodeStar의 장점통합된 UI로 한 번에 여러 활동 관리 가능Continuous Delivery 도구 체인을 구성해 신속한 코드 배포 가능소유자, 기여자 및 최종 사용자 추가로 안전한 협업 가능Dashboard를 사용해 전체 개발 프로세스의 진행 상황 추적 가능CodeStar 사용하기1-1. 처음 CodeStar를 실행하면 나오는 화면입니다. ‘Start a Project’를 누르면 프로젝트 템플릿을 선택할 수 있습니다. 1-2. 이것은 아직 지원되지 않는 지역(Region)에서 노출되는 화면입니다. 2-1. ‘Start a Project’를 클릭하면 프로젝트 템플릿을 선택할 수 있습니다. 2-2. Python과 AWS Lambda를 이용해 Web service를 구현해보겠습니다. 3. Project Name을 지정하고 repository를 선택합니다. 여기서는 AWS CodeCommit으로 선택하여 진행해보겠습니다. CodeCommit의 경우 Repository name을 따로 지정할 수도 있습니다. Repository name까지 지정했다면 Next를 클릭합니다. 4. 아래의 화면은 프로젝트의 흐름입니다. CodeCommit에 소스가 저장되고 AWS CodeBuild를 통해서 Build와 Test가 진행됩니다. 그리고 AWS CloudFormation을 통해서 Deploy가 진행되며 Monitoring은 Amazon Cloud Watch를 통해 진행합니다. CodeStar의 경우 IAM 사용자에 AWSCodeStarFullAccess 관리형 정책을 적용합니다.1) 5. Create Project를 클릭하면 프로젝트가 생성되고, CodeStar 유저 설정을 할 수 있습니다. 6-1. 이제 editor를 선택해봅시다. Command line tools, Eclipse, Visual Studio 등을 고를 수 있습니다. 툴은 언제든지 바꿀 수 있으니 여기서는 Eclipse를 이용하여 프로젝트를 진행하겠습니다. 6-2. See Instructions를 클릭하면 Eclipse를 다운로드 받아 설정하는 방법을 볼 수 있습니다. 6-3. 이제 Eclipse를 설치하고 AWS Toolkit for Eclipse를 설치해보겠습니다. Eclipse의 종류는 Eclipse IDE for java EE Developers 에디션을 설치하겠습니다. 다른 버전은 AWS Toolkit 설치할 때 의존성 문제가 발생할 수 있습니다. 7. Eclipse를 설치하고 Eclipse Marketplace에서 AWS Toolkit for Eclipse 2.0를 설치합니다. 8-1. import를 클릭하고 8-2. AWS -> AWS CodeStar Project를 선택합니다. 8-3. 지역(Region)을 선택하면 해당 지역의 CodeStar 프로젝트를 import 할 수 있습니다. 이 때 CodeCommit의 HTTPS Git credentials를 입력해야 합니다. 9. IAM -> Users -> 사용 계정을 선택해 HTTPS Git credentials for AWS CodeCommit에 가면 User Name과 Password를 Generate 할 수 있습니다. (아래 이미지에 민감한 정보는 삭제했습니다.) 10. CodeStar에서 Project를 Eclipse에 import한 모습입니다. buildspec.yml, index.py, README.md, template.yml이 clone 된 것을 확인할 수 있습니다. 11. 브라우저의 Eclipse 설치 설명 화면에서 back을 클릭해 에디터 선택 화면으로 돌아갑니다. 12. 도쿄 지역에 아직 출시되지 않은 Cloud9은 선택을 마치면 자동으로 셋업이 완료됩니다. 그러나 Eclipse는 Skip을 클릭해야 CodeStar Dashboard로 이동할 수 있습니다. 13. CodeStar Dashboard에 진입하였습니다. IDE는 이미 설정이 끝났으므로 I have already done this를 선택합니다. 화면 하단에 파란색 직육면체가 계속 그려지면 deploy가 완료된 상태가 아니므로 조금 기다렸다가 refresh를 해줍니다. 14-1. deploy가 완료되면 위와 같이 Team wiki tile, Application endpoints, Commit history, Continuous deployment, Application activity등이 나타납니다. 14-2. JIRA를 연동해서 사용할 수도 있는데, 그 내용은 다음에 다루겠습니다. ???? 15. 우선 첫 deploy가 완료된 것을 자축하며 Application endpoints를 클릭합니다. 개발자들에게 굉장히 익숙한 “Hello World”가 나옵니다! 간편하게 소스를 deploy 하여 AWS Api-Gateway와 연결했습니다. 이제 각 파일의 용도에 대한 설명과 새로운 method를 추가하는 작업을 진행해보겠습니다. 16. 이미지처럼 sample.py 파일을 추가하고 아래 코드를 추가합니다. import json import datetime def handler(event, context):     data = {         'output': 'Sample! pathParameters test = ' + event["pathParameters"]["test"]     }     return {'statusCode': 200,             'body': json.dumps(data),             'headers': {'Content-Type': 'application/json'}} 17. 그리고 template.yml에는 아래 내용을 추가합니다. — template.yml —  Sample:     Type: AWS::Serverless::Function     Properties:       Handler: sample.handler       Runtime: python3.6       Role:         Fn::ImportValue:           !Join ['-', [!Ref 'ProjectId', !Ref 'AWS::Region', 'LambdaTrustRole']]       Events:         GetEvent:           Type: Api           Properties:             Path: /sample/{test}             Method: get — 18-1. 이제 수정한 내용을 CodeStar에 반영해보겠습니다. 프로젝트에서 오른쪽 클릭을 해 Team -> Commit을 선택하고 Commit합니다. 18-2. 수정한 파일을 Commit하고 Push합니다. 18-3. Dashboard를 보면 Commit history에 Commit 내용이 반영되었습니다. 19-1. Dashboard에 Continuous deployment를 보면 Source -> Build -> Deploy를 통해서 수정한 내용이 반영되는 것을 실시간으로 확인할 수 있습니다. 이 작업은 생각보다 시간이 많이 소요됩니다. Deploy까지 Succeeded로 완료가 되면 새로 만들어진 URL을 클릭합니다. 19-2. 아래와 같이 pathParameters가 정상적으로 출력되는 것을 확인할 수 있습니다. 20. 이어서 새로 만든 API에 단위테스트를 추가해보겠습니다. sample_test.py라는 파일을 만들고 아래 코드를 추가합니다. — sample_test.py — from sample import handler   def test_sample_handler():         event = {         'pathParameters': {             'test': 'testMessage'         }     }         context = {}         expected = {         'body' : '{"output": "Sample! pathParameters test = testMessage"}'         ,'headers': {             'Content-Type': 'application/json'         },         'statusCode': 200     }         assert handler(event, context) == expected  — 21. 그리고 buildspec.yml 파일을 아래와 같이 수정합니다. — buildspec.yml —  version: 0.2 phases:    install:     commands:       - pip install pytest    pre_build:     commands:       - pytest    build:     commands:       - pip install --upgrade awscli       - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.yml artifacts:   type: zip   files:     - template-export.yml  — 22-1. Commit을 진행합니다. 그리고 다시 Source -> Build -> Deploy 를 거쳐서 Succeeded가 되면 Build 부분의 CodeBuild로 들어가서 Build 결과를 확인합니다. 22-2. 맨 마지막에 Build 결과를 클릭하면 Build 상세 내역을 확인하실 수 있습니다. 22-3. Build logs부분을 보면 sample_test.py를 이용한 단위테스트가 정상적으로 진행된 것을 확인할 수 있습니다. Conclusion지금까지 CodeStar를 이용한 간단한 튜토리얼을 진행했습니다. 다음 화에서는 다양한 방법으로 CodeStar를 활용할 수 있는 방법을 소개하겠습니다. CodeStar에 대한 자세한 내용은 여기를 참조하세요. 참고 1) AWS CodeStar 설정글윤석호 이사 | 브랜디 [email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유 #CTO

기업문화 엿볼 때, 더팀스

로그인

/