스토리 홈

인터뷰

피드

뉴스

조회수 10020

Estimator: BLE를 사용한 Planning Poker 애플리케이션

1. Planning PokerStyleShare 개발팀에서는 스크럼을 활용하여 일을 진행하고 있습니다.1 스크럼에는 일감의 크기를 추정estimate하는 과정이 있는데요. 구성원들 모두가 일감에 대해 이해하고 일감의 크기가 어느정도인지 함께 논의하여 합의에 이르는 과정입니다. 스프린트 회의에서 일감을 등록한 사람(리포터)이 일감에 대해 설명하고 나서 전체 구성원들이 일감의 크기를 추정하는데, 이 때 사용하는 것이 바로 Planning Poker입니다.Planning Poker는 0.5부터 시작해서 1, 3, 5, 8, 13, 20, … 100과 같이 피보나치 수열로 증가하는 숫자를 가진 카드 덱입니다. 리포터의 설명이 끝난 뒤 스크럼 마스터가 하나, 둘, 셋을 외치면 각자 생각한 일감의 크기에 맞는 카드를 꺼내고, 스크럼 마스터는 구성원들의 추정치가 최대한 가까워지도록 부가설명이나 질문을 유도합니다.2▲ Planning Poker는 이렇게 생겼다. (출처: Control Group 블로그)하지만 개발팀이 커지면서 불편함이 생기기 시작했습니다. 회의에 참여하는 인원이 7-8명씩 되다 보니, 각자가 어떤 카드를 들고있는지 한눈에 보기가 어려워진 것입니다. StyleShare에서 자칭 아이디어 뱅크 역할을 담당하고 있는 저는 획기적인 방법이 필요하다고 생각했고, 굳이 카드를 꺼내들지 않아도 각자가 무슨 카드를 선택했는지를 쉽게 볼 수 있는 애플리케이션을 만들기로 결심했습니다.2. BLE (Bluetooth Low Energy)불편함을 덜기 위한 애플리케이션이므로, 사용자 경험이 굉장히 직관적이고 단순해야 했습니다. N:N 통신이 가능해야하고, 사용자를 귀찮게 하는 페어링Pairing이나 네트워크 접속 과정이 없어야 했습니다. 한마디로, 카드를 꺼내들고 눈으로 확인하는 것보다 더 편한 무언가를 만들어야 했습니다!처음에는 근거리 무선 통신을 위한 기술로 스타벅스에서 사이렌 오더 개발에 사용한 고주파 인식 기술을 생각했습니다.3 각자의 기기에서 선택한 카드에 맞는 소리를 내보내고, 다른 기기에서는 고주파를 읽겠다는 것이었는데요. Soundlly(구 aircast.me)와 같은 상업용 SDK를 쓰지 않는 이상, 사운드 프로그래밍을 한 번도 해본 적 없는 저에게는 데이터가 실린 고주파를 만드는 것부터 소리를 인식해서 데이터를 읽어내는 과정이 마치 화성에서 감자 키우는 이야기처럼 들렸습니다.그러다 문득 생각난 것이 바로 비콘Beacon입니다. 언젠가 소비자가 오프라인 매장에 방문하면 BLE를 이용해서 매장 위치를 파악하는 기술이 있다는 이야기를 들은 적이 있었습니다. 찾아보니 시중에 나와있는 대부분의 모바일 기기에서는 BLE를 위한 최소 조건인 블루투스 4.0을 지원했고, 페어링이나 네트워크 접속 과정도 불필요했습니다. 무엇보다, 화성에서 감자 키우는 것보다는 쉬워보였습니다.3. Swift로 BLE 개발하기그래서 BLE를 사용해서 개발하기로 했습니다. 컨셉은 간단했습니다. 내가 선택한 카드를 브로드캐스팅하고, 다른 사람들이 선택한 카드를 내 모바일 기기에 보여주면 되는 것이었습니다. BLE를 사용하면 정보를 브로드캐스팅할 수 있고, 다른 기기에서 브로드캐스팅하는 정보를 읽을 수 있습니다.BLE에서 데이터를 브로드캐스팅하는 것을 Advertising이라고 합니다. 정보를 advertising하는 주체는 Peripheral이고, advertising되는 정보를 스캔하여 데이터를 읽어들이는 주체는 Central이라고 합니다. Peripheral에서 정보를 advertising할 때에는 특정한 정보를 실어나를 수 있는데요. 이를 Advertising Data Payload라고 합니다. 이 정보에 카드 숫자와 이름을 실어서 전송하면 될 것 같습니다.BLE를 구현하기 위해서, iOS에서는 SDK에 기본적으로 포함돼있는 CoreBluetooth 프레임워크를 사용하면 손쉽게 개발이 가능합니다. CBPeripheralManager 클래스와 CBCentralManager 클래스를 쓰면 되는데요. BLE를 이용하여 제 이름 석자를 advertising하는 코드는 다음과 같습니다.Peripheralimport CoreBluetooth let serviceUUID = CBUUID(string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") let service = CBMutableService(type: serviceUUID, primary: true) /// 1. `CBPeripheralManager`를 초기화하고, self.peripheral = CBPeripheralManager(delegate: self, queue: nil) /// 2. 사용가능한 상태가 되면 특정 UUID를 가진 서비스를 추가한 뒤에 func peripheralManagerDidUpdateState(peripheral: CBPeripheralManager) { if peripheral.state == .PoweredOn { self.peripheral.addService(service) } } /// 3. 원하는 정보를 advertising합니다. func peripheralManager(peripheral: CBPeripheralManager, didAddService service: CBService, error: NSError?) { self.peripheral.startAdvertising([ CBAdvertisementDataLocalNameKey: "전수열", CBAdvertisementDataServiceUUIDsKey: [serviceUUID], ]) } 참고로, UUID는 커맨드라인 명령어를 통해 쉽게 만들 수 있습니다.$ uuidgen XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 마찬가지로, Peripheral에서 advertising하는 정보를 스캔하는 Central 코드는 다음과 같이 작성할 수 있습니다. UUID는 Peripheral에서 advertising에 사용한 UUID와 동일해야합니다.Centralimport CoreBluetooth let serviceUUID = CBUUID(string: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX") let service = CBMutableService(type: serviceUUID, primary: true) /// 1. `CBCentralManager`를 초기화하고, self.central = CBCentralManager(delegate: self, queue: nil) /// 2. 사용가능한 상태가 되면 특정 UUID를 가진 서비스를 스캔합니다. func centralManagerDidUpdateState(central: CBCentralManager) { if central.state == .PoweredOn { // 이미 한 번 스캔된 정보라도 계속 스캔합니다. let options = [CBCentralManagerScanOptionAllowDuplicatesKey: true] self.central.scanForPeripheralsWithServices([serviceUUID], options: options) } } /// 3. Peripheral이 스캔되면 이 메서드가 호출됩니다. func centralManager(central: CBCentralManager, didDiscoverPeripheral peripheral: CBPeripheral, advertisementData: [String : AnyObject], RSSI: NSNumber) { print(advertisementData[CBAdvertisementDataLocalNameKey]) // "전수열" } 스캔을 시작할 때 CBCentralManagerScanOptionAllowDuplicatesKey 옵션을 true로 설정해서 한 번 스캔된 정보라도 중복으로 계속 스캔하도록 합니다.4. 원하는 정보를 실어나르기CBPeripheralManager을 사용하여 advertising을 할 때에는 Advertising Data Payload를 포함시킬 수 있는데, 이 정보 중 개발자가 원하는 값을 넣을 수 있는 곳은 CBAdvertisementDataLocalNameKey밖에 없습니다. 그마저도 길이가 제한돼있기 때문에, 패킷을 효율적으로 사용하기 위해서는 정보를 저장하는 프로토콜을 직접 정의해야 합니다.우선, 카드에 대한 정의는 enum을 사용해서 작성했습니다. 0부터 0xFF까지의 숫자를 가지도록 정의했습니다.public enum Card: Int { case Zero = 0 case Half = 127 case One = 1 case Two = 2 case Three = 3 case Five = 5 case Eight = 8 case Thirteen = 13 case Twenty = 20 case Fourty = 40 case Hundred = 100 case QuestionMark = 0xFD case Coffee = 0xFE case None = 0xFF } 그리고 제가 정의한 패킷의 프로토콜은 다음과 같습니다.영역길이예시설명Version200프로토콜 버전 (00~FF)Channel201BLE 커버리지 내에서 회의하는 팀이 여럿일 수 있으니, 채널로 구분합니다. (00~FF)Card2FE카드의 16진수 값 (00~FF)Name12전수열사용자 이름 (UTF-8 기준 한글 4글자)이렇게 하면 총 18바이트 내에서 필요한 정보를 모두 전송할 수 있습니다. 이제 이 "00", "01", "FE", "전수열" 값을 직렬화해서 CBAdvertisementDataLocalNameKey로 advertising하면 됩니다.Peripheralself.peripheral.startAdvertising([ CBAdvertisementDataLocalNameKey: "0001FE전수열", CBAdvertisementDataServiceUUIDsKey: [serviceUUID], ]) 그리고, Central에서 정보를 스캔할 때에는 이 값을 각 영역의 길이에 맞게 끊어서 읽을 수 있습니다.5. 마치며비록 적은 양의 정보지만, BLE를 사용해서 실시간으로 근거리 통신을 할 수 있게 되었습니다. 이제 남은 것은 카드를 선택할 수 있는 화면과, 다른 사용자가 선택한 카드를 화면에 보여주는 인터페이스입니다. UI 개발은 본 포스트에서 중점적으로 다루고자 하는 주제와는 조금 벗어난 이야기가 될 것 같아, 오픈소스로 공개된 코드로 대신하려고 합니다. 소스코드는 GitHub에서 볼 수 있으며, Estimator는 앱스토어에서 받아보실 수 있습니다.6. 참고 자료BLE(BLUETOOTH LOW ENERGY) 이해하기 - Hard Copy World스타일쉐어의 스크럼이 지나온 길 포스트에 보다 자세히 설명되어 있습니다. ↩구성원들의 추정치에 차이가 난다는 것은 해당 일감에 대해 서로가 이해하고 있는 정도가 다르기 때문입니다. 스크럼 마스터는 구성원들이 일감에 대해 모두 비슷한 생각을 가지도록 커뮤니케이션을 유도해야합니다. ↩http://www.bloter.net/archives/226643 ↩#스타일쉐어 #개발 #개발팀 #개발자 #인사이트
조회수 1366

레진 기술 블로그 - 모두를 위한 설계. 레진 웹 접근성 가이드라인.

레진엔터테인먼트는 글로벌(한국, 일본, 미국) 서비스를 운영하고 있기에 다양한 사람들의 재능과 욕구에 관심이 있습니다. 우리는 웹 접근성에 관심을 기울여 조금 특별한 욕구를 가진 사람들의 문제를 해결하려고 합니다. 소수의 특별한 욕구는 모두의 욕구와 연결되어 있다고 생각하기 때문입니다.조금 특별한 욕구를 가진 사람WHO는 세계 인구의 15%에 해당하는 사람들이 장애가 있는 것으로 파악하고 있습니다. 그리고 보건복지부 장애인 실태조사에 따르면 후천적 장애 발생률은 90% 수준입니다. 이런 통계에 따르면 한 개인이 일생을 살면서 장애인이 되거나 일시적으로 장애를 체험하게 될 확률은 무려 13.5%나 됩니다.저는 적록 색약입니다. 약한 수준의 장애로 분류할 수 있죠. 채도가 낮은 상태의 적색과 녹색을 쉽게 구별하지 못합니다. 충전 중 적색이었다가 완충이 되면 초록색으로 변하는 LED가 박혀있는 전자제품은 전부 망했으면 개선하면 좋겠어요. 전 세계 남성의 8%가 색약이고, 여성은 0.5%가 색약입니다. 대부분 적록 색약이고 마크 저커버그도 적록 색약입니다. 만화가 이현세 선생님도 적록 색약이고요. 한편 색약인 사람은 빛의 밝고 어두움을 구별하는 능력이 뛰어난 것으로 밝혀져 있어 저격과 관측에 탁월한 능력을 발휘합니다. 숨어있는 저격수 빨리 찾기 게임을 해 보세요. 위장 사진 1, 위장 사진 2, 위장 사진 3. 색약인 사람이 이길 것입니다.전맹 시각장애인은 마우스 포인터와 초점을 볼 수 없으므로 키보드만을 사용해서 웹을 탐색합니다. 키보드와 음성 낭독에 의존하지만, 키보드 기능을 정말 잘 다루죠. 그래서 키보드 접근성 문제를 해결하면 시각장애인뿐만 아니라 키보드를 능숙하게 사용하는 사람들의 사용성이 높아집니다. 소수의 특별한 요구사항을 해결하는 것이 모두를 위한 설계와 연결되어 있습니다.결국, 누구에게나 특별히 다른 측면이 있고 그것을 고려할 때 "모두를 즐겁게 하라!"라는 우리의 좌우명에 한 걸음 더 가까워질 수 있다고 믿습니다.도저히 풀 수 없을 것 같은 숙제웹 접근성을 소개할 때 많이 듣는 질문이 있습니다.장애인이 우리 서비스를 이용해요?매출에 도움이 돼요?시간과 비용이 많이 필요하지 않아요?이 질문에 대한 제 대답은 다음과 같습니다.이용한다면 기쁠 것 같아요.큰 도움은 안 될 거예요.조금은 그렇죠. 하지만 반환이 있어요.레진코믹스와 같이 이미지 기반의 콘텐츠를 서비스하는데 웹 접근성을 준수하려고 노력한다는 것은 무모한 도전에 가깝습니다. 왜냐하면, 현재로서는 전맹 시각장애인 고려가 없고 논의조차 쉽지 않기 때문입니다.하지만 달에 갈 수 없다고 해서 일찌감치 체념할 필요는 없겠지요. 쉬운 문제부터 하나씩 풀어 나아가길 기대합니다. 로켓에 올라탔으니까 금방 갈 수 있지 않을까요?W3C 표준을 우리 언어로W3C에서는 WCAG 2.1이라는 웹 콘텐츠 접근성 지침을 제시하고 있고요. 국내 표준 KWCAG 2.1 또한 있습니다. 국내 표준은 W3C 표준에서 중요도가 높은 항목을 우리 언어로 정리한 것이기 때문에 결국 어떤 지침을 선택해서 따르더라도 괜찮습니다.하지만 표준 문서는 너무 장황하고 전문 용어가 많아 다양한 분야 전문성을 가진 직원들과 함께 보기에는 한계가 있다고 생각했습니다. W3C 표준을 근간으로 하되 비전문가도 15분 정도면 읽고 이해할 수 있을 만큼 정리된 문서가 필요했고 레진 웹 접근성 가이드라인 사내 표준을 제안하고 공개하게 됐습니다.의미를 전달하고 있는 이미지에 대체 텍스트를 제공한다.전경 콘텐츠와 배경은 4.5:1 이상의 명도 대비를 유지한다.화면을 400%까지 확대할 수 있다.키보드만으로 조작할 수 있다.사용할 수 있는 충분한 시간을 제공한다.발작을 유발하는 콘텐츠를 제공하지 않는다.반복되는 콘텐츠 블록을 건너뛸 수 있다.모든 문서의 제목은 고유하고 식별할 수 있다.링크와 버튼 텍스트는 콘텐츠의 목적을 알 수 있다.섹션에는 의미있는 마크업과 헤딩이 있다.문서의 휴먼 랭귀지 속성을 제공한다.문맥 변경은 예측할 수 있다.폼 콘트롤 요소에 설명을 제공한다.실수를 예방하고 정정하는 것을 돕는다.HTML 문법을 준수한다.WCAG 2.1 지침의 1.1.1 항목 예를 들어 볼게요.All non-text content that is presented to the user has a text alternative that serves the equivalent purpose, except for the situations listed below. 사용자에게 제공되는 모든 텍스트 아닌 콘텐츠는 아래 나열된 상황을 제외하고 같은 목적을 수행하는 대체 텍스트를 제공한다.원문 표현보다 아래와 같이 다듬은 표현이 좋다고 보는 것이죠.의미를 전달하고 있는 이미지에 대체 텍스트를 제공한다.물론 사내 지침은 너무 단순하게 표현했기 때문에 지침마다 ‘부연 설명, 관련 예시, 기대 효과, 관련 표준, 평가 도구’ 텍스트와 링크를 간략하게 제공하고 있습니다. 사실상 W3C 표준에 대한 링크 페이지라고 생각해도 괜찮습니다. 사실이 그런걸요.맺음말레진 웹 접근성 가이드라인은 사내 유관 부서 담당자분들께 공유하고 동의를 얻어 사내 지침으로 결정하고 공개할 수 있게 됐습니다. 긍정적으로 검토해 주신 사우님들 감사합니다.레진 웹 접근성 가이드라인은 W3C 표준을 요약한 버전에 불과하므로 누구라도 복제(Fork), 개선 요청(Pull Requests), 문제 제기(Issues)할 수 있습니다."Design for all, amuse everyone!"
조회수 2348

Angular Lazy Loading 모듈 사용하기

Angular는 비동기식 라우팅이 가능합니다. 나중에 사용할 기능들을 NgModule로 분리하여 사용자의 요청이 들어왔을 때 모듈을 불러와 기능을 사용할 수 있고, 이러한 기술을 지연 로딩이라 합니다.프로젝트가 진행되고 기능이 추가될수록 어플리케이션 번들 크기가 커지고, 결국엔 초기 로딩 시간도 길어지게 됩니다. 지연 로딩을 사용하면 초기 로딩 시간을 줄일 수 있습니다. 컴파일 단계에서 나중에 사용할 모듈들을 메인 모듈에서 분리하여 번들을 생성합니다. 그리고 사용자가 기능을 요청할 때 비동기로 스크립트를 불러와 실행합니다. 지연 로딩에 대한 소개와 사용법은 Angular 공식 문서의 Routing & Navigation — Milestom 6: Asynchronous routing 을 참고하시길 바랍니다하지만 지연 로딩을 사용할 때 유의해야할 점이 몇 가지 있습니다.지연 로딩 모듈과 인젝터(Injector)지연 로딩이 완료되었을 때 Angular는 지연 로딩된 모듈을 루트 인젝터(Root Injector)의 자식이 되는 자식 인젝터를 이용하여 초기화하고, 서비스들을 자식 인젝터에 추가합니다. 즉, 인젝터가 분리되기에 지연 로딩된 모듈의 클래스들은 자식 인젝터로의 서비스 주입이 가능하지만 루트 인젝터로 만들어진 클래스들은 불가능합니다.이는 Angular의 독특한 의존성 주입 시스템 때문입니다. Angular의 인젝터는 처음 애플리케이션이 시작되었을 때, 컴포넌트나 다른 서비스에 주입되기 전에 포함된 모든 모듈들의 서비스 제공자들을 블러와 루트 인젝터를 생성합니다. 애플리케이션이 시작되고 나면 인젝터는 서비스들을 생성하고 주입을 시작하고, 새로운 서비스들을 제공자로 추가가 불가능합니다.그러므로 지연 로딩된 서비스들은 이미 생성이 완료된 루트 인젝터로 추가가 불가능합니다. 따라서 Angular는 지연 로딩된 모듈에 대해서 새로운 자식 인젝터를 만들는 전략을 취하게 된 것입니다.자식 인젝터가 새로 만들어지기 때문에 공통된 모듈을 사용할 때 주의하여야 합니다. 예를 들어 다음과 같이 SharedModule 에 CounterService 를 서비스로 추가하고 루트 모듈인 AppModule 과 지연 로딩 모듈인 LazyModule 에 각각 SharedModule 을 import 하였습니다.import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SharedModule } from './shared/shared.module'; import { AppShellComponent } from './app-shell.component'; const APP_ROUTES = [ { path: 'lazy', loadChildren: 'app/lazy/lazy.module#LazyModule' } ]; @NgModule({ imports: [ BrowserModule, SharedModule, RouterModule.forRoot(APP_ROUTES) ], declarations: [ AppShellComponent ], bootstrap: [AppShellComponent] }) export class AppModule { }import { Injectable } from '@angular/core'; @Injectable() export class CounterService { count = 0; increase(): void { this.count++; } decrease(): void { this.count--; } }import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SharedModule } from '../shared/shared.module'; import { SomeLazyComponent } from './some-lazy.component'; const LAZY_ROUTES = [ { path: '', component: SomeLazyComponent } ]; @NgModule({ imports: [ SharedModule, RouterModule.forChild(LAZY_ROUTES) ] }) export class LazyModule { }import { NgModule } from '@angular/core'; @NgModule({ providers: [ CounterService ] }) export class SharedModule { }그리고 루트 모듈의 컴포넌트와 지연 로딩 모듈의 컴포넌트에서 각각 CounterService 를 사용하여 숫자 값을 바꿔봅니다.서로 다른 인젝터에 CounterService 인스턴스가 만들어졌기 때문에 두 컴포넌트에 표시되는 숫자값은 다릅니다. 앞에서 말했듯이 지연 로딩 모듈은 루트 인젝터가 아닌 자식 인젝터를 이용하여 초기화하기 때문입니다.만약, 지연 로딩 모듈에서 제공되는 서비스를 다른 모듈에서 사용하려면 루트 모듈에 포함시켜 줘야 합니다. 다음과 같이 루트 모듈에게만 노출시킬 서비스 제공자들을 따로 빼내어 줄 수 있습니다.import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AccountLoginPageComponent } from './login-page.component'; const ACCOUNT_ROUTES: Routes = [ { path: 'login', component: AccountLoginPageComponent } ]; @NgModule({ imports: [ ... RouterModule.forChild(ACCOUNT_ROUTES) ], decalartions: [ AccountLoginPageComponent ] }) export class AccountLazyModule { }import { ModuleWithProviders, NgModule } from '@angular/core'; import { AccountAuthService } from './auth.service'; @NgModule({ imports: [...] }) export class AccountModule { static forRoot(): ModuleWithProviders { return { ngModule: AccountModule, providers: [ AccountAuthService ] }; } }AccountModule.forRoot() 를 루트 모듈에 import하면 다른 모듈에서도 AccountAuthService 를 사용할 수 있게 됩니다. 물론 이 경우 AccountModule를 지연로딩 모듈로 만들면 루트 모듈에 포함되기 때문에 번들을 나누는 의미가 없어질 수 있으니 AccountLazyModule 을 따로 두어 코드를 분리하였습니다.#타운컴퍼니 #개발 #개발자 #인사이트 #꿀팁
조회수 1390

[Buzzvil People] Andy Kim, Software Engineer

 Buzzvil People에서는 다양한 배경과 성격 그리고 생각을 지닌 버즈빌리언들을 한 분 한 분 소개하는 시간을 갖습니다. 어떻게 버즈빌에 최고의 동료들이 모여 최고의 팀을 만들어가고 있는 지 궁금하시다면, 색색깔 다양한 버즈빌리언들 한분 한분의 이야기가 궁금하시다면, Buzzvil People을 주목해주세요.1. 간단한 자기 소개 부탁드립니다. 안녕하세요. 저는 Andy입니다. 저는 중학교부터 대학교까지 미국에서 7~8년 동안 지내며 학업을 마치고 한국으로 돌아와 버즈빌에 자리를 잡았습니다. 대학 시절에는 제약에서부터 통계, 데이터/컴퓨터 공학까지 전공을 여러 번 바꿨습니다. 개인적으로 새로운 레시피나 방법으로 요리하는 것을 좋아합니다. 개인 시간에는 주로 유튜브에서 요리 콘텐츠를 즐겨보고, 동료나 친구들과 함께 새로운 레시피에 도전해보곤 합니다. 2. 어떻게 버즈빌에 오시게 되셨나요? 대학을 졸업하고 한국으로 돌아온 후, 새로운 소프트웨어 엔지니어링 툴을 적극적으로 사용하는 IT 회사에서 일하고 싶었습니다. 구인 사이트에서 클라우드 기술과 관련된 채용공고를 찾아보다가, 버즈빌을 발견했습니다. 3. 버즈빌에서 어떤 업무를 담당하고 계신가요? 저는 데브옵스(DevOps) 팀에 소속되어 데이터 및 데브옵스 엔지니어로 일하고 있습니다. 제 주 업무는 다음과 같지만 국한되어 있지는 않습니다. – 새로운 데이터 생성 시 Data pipelines 제작 및 유지보수 – 자사에서 사용하는 Redshift 데이터 웨어하우스 유지보수 및 최적화 – 메타베이스(Metabase) 를 이용해 데이터를 다른 직원들이 사용할 수 있도록 가공 4. 스타트업에서 혹은 광고업계에서 일하는 느낌이 어떠세요? 소프트웨어 엔지니어로서 스타트업에서 근무하는 것은 샌드박스 게임을 하는 것과 비슷했습니다. 대부분의 경우, 새로운 기술이나 컨셉을 주저하지 않고 시도해볼 수 있습니다. 업무 사항에서 한발 더 나아가 더 큰 노력을 쏟아붓고자 하면, 사실상 무수히 많은 데이터 자산과 인프라에 접근할 기회도 받습니다. 5. 이것만큼은 버즈빌이 참 좋다! 어떤 게 있으실까요? 무제한으로 제공되는 신선한 커피 원두가 행복한 충격이었습니다. 커피를 내릴 때 3가지 이상의 커피 원두를 마음껏 고를 수 있는데요. 제가 속한 데브옵스 팀에서 커피타임은 매우 중요한 의식 중 하나입니다. 매일 스크럼을 커피타임과 함께 진행하고 있습니다. 돈 한 푼 사용하지 않고 즐길 수 있는 좋은 품질의 커피는 버즈빌에서 일하면서 받을 수 있는 기분 좋은 보너스 중 하나입니다. 6. 개인적인 목표나 꿈이 있으신가요? 있다면, 버즈빌에서의 경험이 어떻게 도움이 된다고 생각하시나요? 최종적으로, 제 꿈은 요식업계에 도전하는 것입니다. 아직 구체적인 계획은 없지만, 편향된 데이터나 해로운 과정의 효과를 최소화해 대중들에게 좋은 음식을 제공하는 서비스를 만들고 싶습니다. 버즈빌에서 데이터 엔지니어로서 일한 경험은 좋은 팀을 만나게 해주고, 유연하면서도 탄탄한 서비스를 만들어, 최종적으로는 사람들의 먹거리에 영향을 주는 것에 도움이 될 것으로 기대하고 있습니다.
조회수 2987

Python 기반의 웹서비스 개발 환경 구축 방법

안녕하세요. 스포카 크리에이터팀의 프로그래밍 인턴을 맡고 있는 __박종규, 정성재, 고아라__입니다. 저희 세 명은 각각 다른 OS 환경에서 웹서비스를 개발하였는데 이번 포스팅에서는 OS별로 개발 환경을 구축하는 과정을 설명하겠습니다.PythonPython(파이썬)은 비영리의 Python 소프트웨어 재단이 관리하는 개방형, 공동체 기반 개발 모델을 가지고 있습니다. Python은 C파이썬 구현이 사실상의 표준이며 동적 타이핑 범용 프로그래밍 언어로, 펄 및 루비와 자주 비교됩니다. Python은 순수한 프로그래밍 언어의 기능 외에도 다른 언어로 쓰인 모듈들을 연결하는 Glue language로써 자주 이용됩니다. 실제 Python은 많은 상용 응용 프로그램에서 스크립트 언어로 채용되고 있습니다. 또한, 유니코드 문자열을 지원하여 다양한 언어의 문자 처리에도 능합니다. 현대의 Python은 여전히 인터프리터 언어처럼 동작하나 사용자가 모르는 사이에 스스로 Python 소스 코드를 컴파일하여 바이트 코드를 만들어 냄으로써 다음에 수행할 때에는 빠른 속도를 보여줍니다.Windows에서 Python 2.x 설치 방법Linux와 Mac OS에서 Python은 기본적으로 설치되어 있기 때문에 터미널 창에서 Python 명령만으로 쉽게 실행할 수 있지만 Windows에서는 Python을 따로 설치해주어야 합니다.Python 공식 사이트해당 사이트에 들어가서 Quick Links (2.7.3) – Windows Installer 선택하여 python-2.7.3.msi 다운로드 후 실행하여 설치합니다. 특별히 원하는 버전이 있을 때에는 DOWNLOAD – Releases에서 버전별로 설치파일을 다운로드 할 수 있습니다. Python의 기본 설치 경로는 C:\Python27 이며 설치 완료 후, 환경변수를 등록해야 합니다. 환경변수를 등록하는 방법은 다음과 같습니다.내컴퓨터(마우스 오른쪽 버튼 클릭) – 속성(시스템 등록 정보) – 고급 – 환경 변수 클릭시스템 변수 – Path 선택 후 편집 클릭변수 값에 맨 뒤에 C:\Python27;C:\Python27\Scripts; 입력 후, 확인 버튼을 눌러 시스템에 적용변수 값은 각각 ; (세미콜론) 으로 구분Python의 패키지 설치 방법시작 메뉴 – 실행 – CMD 로 커맨드 창을 실행 시킨 후에 Python이 설치된 디렉터리로 이동합니다.cd C:\Python27\Scripts 그 다음 easy_install pip 명령으로 pip를 설치해줍니다. pip는 PyPI(Python Package Index)에 등록된 패키지들을 설치하고 관리할 수 있는 패키지 관리 시스템입니다.$ easy_install pip 다음은 pip 명령의 사용법입니다.pip install packageName 명령 : 최신버전으로 설치pip install packageName==0.x.x 명령 : 0.x.x버전으로 설치패키지 설치 경로 : C:\Python27\Lib\site-packagespip uninstall packageName 명령 : package 제거pip freeze 명령 : 현재 환경에 설치된 package 이름과 버전 목록 PostgreSQLPostgreSQL는 California 대학 Berkeley computer science 학부에서 개발한 POSTGRES, Version 4.2 를 기반으로 한 오브젝트 RDB 관리 시스템(ORDBMS)입니다. 또한, PostgreSQL은 BSD 허가권으로 배포되며 오픈소스 개발자 및 관련 회사들이 개발에 참여하고 있습니다. 따라서 누구라도 사용, 수정, 배포할 수 있으며 목적과 관계없이 무료로 이용할 수 있습니다.각 운영체제 별 PostgreSQL 설치 방법WindowsWindows에서 PostgreSQL을 설치하기 위해 해당 사이트로 접속합니다.PostgreSQL 공식 사이트해당 사이트의 Download탭을 선택하여 Downloads 페이지로 이동합니다. 그 다음 Binary packages 에서 Windows를 선택하여 Windows installers 페이지로 이동합니다. One click installer 에서 Download 선택 후 이 페이지 로 이동하여 해당하는 OS 버전으로 선택하여 다운로드 후 설치합니다.Linux (ubuntu 12.04 LTS)Linux(리눅스)에서 PostgreSQL은 여러 가지 방법으로 설치 할 수 있습니다. 패키지로 설치하는 방법은 터미널 창에서 아래와 같은 명령어를 입력하시면 됩니다.$ sudo apt-get install postgresql-X.X(버전명) 다른방법으로 설치하는 방법은 아래 주소로 들어가시면 자세히 나와 있습니다.PostgreSQL 공식 사이트추가로 PostgreSQL을 편하게 이용하고 싶은 사용자는 pgadmin3이라는 PostgreSQL의 GUI 프로그램을 설치하시면 됩니다. 설치 방법은 터미널 창에서 아래와 같은 명령어를 입력하시면 됩니다.$ sudo apt-get install pgadmin3 Max OS ( MAC OS X Mountain Lion 10.8.2)Mac OS에서 PostgreSQL은 Homebrew를 이용하여 설치하도록 하겠습니다. Homebrew는 Mac OS의 패키지 관리자 프로그램입니다. PostgreSQL은 Homebrew 이외에도 PostgreSQL 다운로드 사이트, Homebrew와 비슷한 기능을 하는 MacPorts 등을 이용해서도 다운받을 수 있습니다. 하지만 PostgreSQL 이외의 다른 프로그램을 설치하기에도 패키지 관리자가 유용하기 때문에 저는 Homebrew를 이용하도록 하겠습니다. 일단, Homebrew 패키지를 컴파일 하기 위해서 xCode가 설치되어 있어야 합니다. 앱스토어에서 xCode를 검색하여 받도록 합니다.xCode 설치를 완료했으면 메뉴에서 __xCode탭 - Preferences - Downloads - Command Line Tools__를 다운받습니다. 그럼 이제 Homebrew 패키지를 컴파일할 수 있게 되었습니다.이제 터미널을 실행시키고 다음 명령을 입력하면 Homebrew가 설치됩니다.$ ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)" Homebrew가 이미 설치되어있다면 업데이트할 사항이 없는지 확인해보고 업데이트를 시켜주세요.$ brew update이제 Homebrew 설치를 완료했으니 간단하게 PostgreSQL을 설치할 수 있습니다.$ brew install postgresql 설치된 버전을 $ psql –ver 명령으로 확인해 보니 PostgreSQL 9.1.4 버전이 설치되어 있었습니다.psycopg2psycopg2는 Python library의 한 종류로써 하는 역할은 Python에서 PostgreSQL를 활용하게 해주는 library입니다. 설치 방법은 터미널 창에서 아래와 같은 명령어를 입력하시면 됩니다.$ pip install psycopg2 * 참고 ubuntu/debian 사용하시는 분 중에서 설치가 안 되시는 분들은 psycopg2를 설치하시기전에 python-dev라는 패키지 파일을 설치하시면 psycopg2를 설치 하실 수 있습니다. 설치 방법은 터미널 창에서 아래와 같은 명령어를 입력하시면 됩니다.$ sudo apt-get install python-dev FlaskFlask는 Python용 Micro Framework이며, Jinja2 template engine과 Werkzerg WSGI toolkit에 의존합니다. Flask에는 기본적으로 많은 환경설정 값들이 존재합니다. 규칙에 따라 템플릿 파일과 CSS, JavaScript, Images 등의 파일들은 templates과 static 이라는 하위 디렉터리에 각각 보관해야 합니다. 기본적으로 Flask는 다른 라이브러리에는 존재하는 Database abstraction layer, Form validation 등의 기능을 포함하지 않지만, 기능을 추가할 수 있는 확장성을 제공합니다. 이미 수많은 라이브러리들이 Database integration, Form validation, Upload handling, Various open authentication technologies등을 제공합니다. SQLAlchemy나 다른 DB Tool을 이용하여 고급 패턴을 구현할 수 있으며, Flask를 이용해보면 다양한 기능을 확장할 수 있다는 것을 알 수 있을 것입니다.Flask 설치 방법Flask를 설치하려면 터미널 창에서 다음 명령어를 입력하면 됩니다. (windows의 경우 cmd창) $ sudo pip install flask SQLAlchemySQLAlchemy는 Oracle, DB2, MySQL, PostgreSQL, SQLite와 같은 관계형 데이터베이스에 강력하고 유연한 인터페이스를 제공하는 Python SQL Toolkit이자 Object Relation Mapper(ORM)입니다. 여기서 ORM은 객체를 관계형 DB 테이블에 매핑해주는 역할을 하는데 SQLAlchemy는 객체를 매핑하기 위해 특정 클래스를 상속받지 않아도 되기 때문에 높은 수준의 라이브러리라고 할 수 있습니다.앞선 단계까지의 설치가 완료되었다면 SQLAlchemy의 설치법 역시 pip 명령을 쓰면 되므로 간단합니다. 윈도우는 커맨드창을 실행시키고, 리눅스와 MAC OS의 경우 터미널을 실행시켜 다음 명령을 입력합니다. 저는 SQLAlchemy 0.7.2 버전을 설치했습니다.$ pip install SQLAlcheymy==0.7.2Permission denied라는 문구가 뜨면 권한이 없는 것이므로 관리자 계정으로 설치를 해주어야 합니다. 관리자 계정은 $ sudo su로 로그인하거나 명령 앞에 sudo를 붙이면 됩니다.$ sudo pip install SQLAlchemy==0.7.2 여기에서 ==0.7.2는 설치할 SQLAlchemy의 버전을 뜻하며 버전을 명시하지 않으면 최신 버전으로 깔립니다.Flask-SQLAlchemyFlask-SQLAlchemy는 SQLAlchemy를 더욱 뒷받침할 수 있는 Flask의 확장으로 SQLAlchemy 0.6 이상의 버전을 필요로합니다. Flask-SQLAlchemy 역시 pip 명령으로 설치할 수 있으며 저는 Flask-SQLAlchemy 0.15 버전을 설치했습니다.$ sudo pip install Flask-SQLAlchemy==0.15 마치며지금까지 Windows, Linux, Mac OS에서 Python, PostgreSQL을 이용한 웹서비스 개발 환경 구축 방법에 대해 알아보았습니다. 이 글을 통해서 처음 접한 사람들도 특정 OS에 구애받지 않고 쉽게 웹서비스 개발 환경을 구축하는 데에 도움이 되었으면 좋겠습니다.출처Python 위키백과pip 위키백과 Flask 홈페이지PostgreSQL plusPostgreSQL 위키백과 Essential SQLAlchemy, O REILLY, 2008Flask-SQLAlchemy 홈페이지#스포카 #개발 #개발팀 #개발자 #파이썬 #Python #개발환경 #업무환경 #꿀팁 #조언 #인사이트
조회수 1418

레진 기술 블로그 - 자바 기반의 백엔드와의 세션 공유를 위한 레일즈 세션 처리 분석

레일즈 기반의 프론트엔드(브라우저에서 서버 사이드 렌더링 계층까지)와 자바 기반의 백엔드(내부 API와 그 이후 계층)이 세션을 공유하기 위해 먼저 레일즈의 세션 처리 과정을 분석하고, 레일즈 세션 쿠키를 다루기 위한 자바 소스 코드를 공유합니다.여기저기 자랑하고 다녔으니 아시는 분은 아시다시피 레진은 구글앱엔진을 사용하고 있습니다. 지금이야 Java, Python, Node.js, Go 언어와 Flexible Environment 같은 다양한 선택지가 있지만, 레진이 입주할 당시만 해도 Java 7(subset), Python(subset)을 지원하는 Standard Environment라는 선택지 밖에 없었죠.최근 Saemaeul Undong 기술 부채 탕감의 일환으로 자바7, 스프링3.x, JSP(!) 기반의 백엔드에 포함되어 있던 프론트엔드를 레일즈 기반의 프론트엔드 서버(서버 사이드 렌더링을 담당하는 서버는 프론트일까요? 백엔드일까요?)로 분리하고 있습니다.서로 다른 세계의 존재들 - 자바와 레일즈의 세션을 공유해야하는 상황이 문제의 발단입니다.자바와 레일즈의 세션을 공유하는 여러가지 방법이 있겠지만, 가장 단순하고 효과적인 방법은 쿠키(cookie)라고 판단하고, 세션 encrypt/decrypt와 marshal/unmarshal을 동일한 방식으로 맞추기로 했습니다. (백엔드 API를 완전히 stateless하게 새로 만들면 좋겠지만, 코인은 벌어야 소는 키워야죠)이를 위해 레일즈의 세션 처리 과정을 분석하고 정리했습니다.레일즈의 actionpack의 action_dispatch/middleware/cookie.rb를 보면 EncryptedCookieJar 클래스의 초기화 과정은 다음과 같습니다(digest의 경우 따로 지정안하면 SHA1이 사용되는 듯):class EncryptedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars def initialize(parent_jar) super if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end secret = key_generator.generate_key(request.encrypted_cookie_salt || '') sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end private def parse(name, encrypted_message) debugger deserialize name, @encryptor.decrypt_and_verify(encrypted_message) rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end def commit(options) debugger options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end end key_generator는 EncryptedCookieJar에 포함된 SerializedCookieJars 모듈에 정의되어 있습니다:module SerializedCookieJars # ... def key_generator request.key_generator end end 흠… 좀 더 파보죠. request.key_genrator는 다음과 같습니다:class Request # ... def key_generator get_header Cookies::GENERATOR_KEY end #... end 흠… 좀 더 파야할 듯 ㅠㅠ.Cookies::GENERATOR_KEY는 다음과 같습니다:class Cookies #... GENERATOR_KEY = "action_dispatch.key_generator".freeze end action_dispatch.key_generator는 레일즈의 엔진 모듈에 해당하는 railties의 application.rb에 정의되어 있습니다:def key_generator # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= if secrets.secret_key_base unless secrets.secret_key_base.kind_of?(String) raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`" end key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) ActiveSupport::CachingKeyGenerator.new(key_generator) else ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token) end end # ... def env_config @app_env_config ||= begin validate_secret_key_config! super.merge( # ... "action_dispatch.key_generator" => key_generator, "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest ) end end 너무 깊이 판 느낌적느낌(?)이 있지만, 여기까지 왔으니 좀 더 파보겠습니다.핵심 알고리즘은 activesupport의 key_generator.rb, message_encryptor.rb, message_verifier.rb에 정의되어 있습니다.먼저, key_generator.rb의 핵심은 다음과 같습니다:class KeyGenerator def initialize(secret, options = {}) @secret = secret # The default iterations are higher than required for our key derivation uses # on the off chance someone uses this for password storage @iterations = options[:iterations] || 2**16 end # Returns a derived key suitable for use. The default key_size is chosen # to be compatible with the default settings of ActiveSupport::MessageVerifier. # i.e. OpenSSL::Digest::SHA1#block_length def generate_key(salt, key_size=64) OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) end end 계속해서, message_encryptor.rb의 핵심은 다음과 같습니다:def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end def _encrypt(value) cipher = new_cipher cipher.encrypt cipher.key = @secret # Rely on OpenSSL for the initialization vector iv = cipher.random_iv encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret cipher.iv = iv decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final @serializer.load(decrypted_data) rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end def encrypt_and_sign(value) verifier.generate(_encrypt(value)) end def decrypt_and_verify(value) _decrypt(verifier.verify(value)) end (Hopefully)마지막으로, message_verifier.rb의 핵심은 다음과 같습니다:def initialize(secret, options = {}) raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal end def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? data, digest = signed_message.split("--".freeze) data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end def verified(signed_message) if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} raise end end end def generate(value) data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private def encode(data) ::Base64.strict_encode64(data) end def decode(data) ::Base64.strict_decode64(data) end def generate_digest(data) require 'openssl' unless defined?(OpenSSL) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data) end # ... # encode, decode는 base64사용 이제 레일즈가 쿠키 기반의 세션을 어떻게 처리하는지 조금 눈에 들어옵니다. 그러나 우리의 최종 목표는 레일즈의 내부를 공부하는 것이 아니라, 자바에서 동일한 처리를 하는 것입니다. 모듈 의존성 따위는 가볍게 무시하고 무한복붙(?)을 시전해서, 레일즈의 세션 처리 과정을 눈으로 확인할 수 있도록 재구성했습니다:require 'openssl' require 'base64' require 'concurrent/map' class Object def blank? respond_to?(:empty?) ? !!empty? : !self end def present? !blank? end end class Hash # By default, only instances of Hash itself are extractable. # Subclasses of Hash may implement this method and return # true to declare themselves as extractable. If a Hash # is extractable, Array#extract_options! pops it from # the Array when it is the last element of the Array. def extractable_options? instance_of?(Hash) end end class Array def extract_options! if last.is_a?(Hash) && last.extractable_options? pop else {} end end end module SecurityUtils def secure_compare(a, b) return false unless a.bytesize == b.bytesize l = a.unpack "C#{a.bytesize}" res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res == 0 end module_function :secure_compare end class KeyGenerator def initialize(secret, options = {}) @secret = secret # The default iterations are higher than required for our key derivation uses # on the off chance someone uses this for password storage @iterations = options[:iterations] || 2**16 end def generate_key(salt, key_size=64) OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) end end class CachingKeyGenerator def initialize(key_generator) @key_generator = key_generator @cache_keys = Concurrent::Map.new end # Returns a derived key suitable for use. def generate_key(*args) @cache_keys[args.join] ||= @key_generator.generate_key(*args) end end class MessageVerifier class InvalidSignature < StandardError; end def initialize(secret, options = {}) raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal end def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? data, digest = signed_message.split("--".freeze) data.present? && digest.present? && SecurityUtils.secure_compare(digest, generate_digest(data)) end def verified(signed_message) if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} raise end end end def verify(signed_message) verified(signed_message) || raise(InvalidSignature) end def generate(value) data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private def encode(data) ::Base64.strict_encode64(data) end def decode(data) ::Base64.strict_decode64(data) end def generate_digest(data) require 'openssl' unless defined?(OpenSSL) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data) end end class MessageEncryptor module NullSerializer #:nodoc: def self.load(value) value end def self.dump(value) value end end class InvalidMessage < StandardError; end OpenSSLCipherError = OpenSSL::Cipher::CipherError def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end def encrypt_and_sign(value) verifier.generate(_encrypt(value)) end def decrypt_and_verify(value) _decrypt(verifier.verify(value)) end def _encrypt(value) cipher = new_cipher cipher.encrypt cipher.key = @secret # Rely on OpenSSL for the initialization vector iv = cipher.random_iv encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret cipher.iv = iv decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final @serializer.load(decrypted_data) rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end def new_cipher OpenSSL::Cipher.new(@cipher) end def verifier @verifier end end #key generate encrypted_cookie_salt = 'encrypted cookie' encrypted_signed_cookie_salt = 'signed encrypted cookie' def key_generator secret_key_base = 'db1c366b854c235f98fc3dd356ad6be8dd388f82ad1ddf14dcad9397ddfdb759b4a9fb33385f695f2cc335041eed0fae74eb669c9fb0c40cafdb118d881215a9' key_generator = KeyGenerator.new(secret_key_base, iterations: 1000) CachingKeyGenerator.new(key_generator) end # encrypt secret = key_generator.generate_key(encrypted_cookie_salt || '') sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt || '') encryptor = MessageEncryptor.new(secret, sign_secret, digest: 'SHA1', serializer: MessageEncryptor::NullSerializer) value = "{\"session_id\":\"6022d05887d2ab9c1bad8a87cf8fb949\",\"_csrf_token\":\"OPv/LxbiA5dUjVsbG4EllSS9cca630WOHQcMtPxSQUE=\"}" encrypted_message = encryptor.encrypt_and_sign(value) #encrypted_message = encryptor._encrypt(value) p '-----------encrypted value-------------' p encrypted_message # decrypt encrypted_message = 'bDhIQncxc2k0Rm9QS0VBT0hWc3M4b2xoSnJDdkZNc1B0bGQ2YUhhRXl6SU1oa2c5cTNENWhmR0ZUWC9zN05mamhEYkFJREJLaDQ3SnM3NVNEbFF3ZVdiaFd5YXdlblM5SmZja0R4TE9JbDNmOVlENHhOVFlnamNVS2g1a05LY0FYV3BmUmRPRWtVNUdxYTJVbG5VVUlRPT0tLXd1akRqOU1lTTVneU9LTWszY0I5bFE9PQ==--b0a57266c00e76e0c7d9d855b25d24b242154070' p '-----------decypted value-------------' puts encryptor.decrypt_and_verify encrypted_message p '---------------------------------------' 이 과정을 자바로 구현한 소스는 생략 깃헙에 올려두었습니다. 이 코드를 이용해서 서블릿 세션과 연동하는 방법은 추후 사측(?)과 협의되는 대로 공유할 예정입니다. 물론, 그 전에 쿠키를 공유할 필요가 없어지면(or 공유할 쿠키가 없어지면) 더 좋겠죠 :D
조회수 1041

국내 IT 산업을 주무르는 첫눈 마피아

업계에서 페이팔 마피아(PayPal mafia)에 대한 이야기를 듣는 건 어렵지 않다. 전세계 IT 산업의 핵심인 실리콘밸리를 장악하고 있는 페이팔 마피아를 탄생시킨 회사는 컨피니티(Confinity)라는 회사다. 이 회사의 이름은 낯설지 몰라도 이 회사가 개발한 페이팔(PayPal)이라는 전자 송금 서비스의 이름은 낯설지 않을 것이다. 1998년 시작된 컨피니티는 페이팔을 만들어 2002년 상장한 뒤 얼마 지나지 않아 이베이(eBay)에 매각된 닷컴 버블 폭풍의 몇 안되는 생존자 중 하나였다. 페이팔 마피아란 페이팔의 초기 임직원들을 일컫는 말로, 이들은 이후에 또 다른 혁신적인 회사들(Tesla Motors, LinkedIn, Palantir Technologies, SpaceX, YouTube, Yelp, Yammer 외 다수)을 계속 만들어 내고 있다. 이들의 모습이 마치 사회 모든 곳에 얽혀 있는 마피아 조직을 연상시킨다는 것에 비유해, 이들을 페이팔 마피아라고 부르기 시작했다. 심지어 포춘지(Fortune)에서는 2007년 이들을 한데 모아 마피아 같은 옷을 입히고 재미있는 사진을 남기기도 했다. 출처 : Fortune Magazine, 2007년 11월페이팔 마피아들의 성공은 절대 우연이 아니라고 생각한다. 이들이 닷컴 버블의 위기를 이겨낼 수 있었던 밑바닥에는 페이팔이라는 회사가 가진 확고한 고유의 문화(culture)가 존재했다. 자신들이 만들어 낸 본연의 문화가 몸에 배인 초기 구성원들이 새로운 회사를 시작하고, 또 다시 새로운 그들만의 확고한 문화를 만들어 가면서 또 다른 페이팔이 만들어 질 가능성이 높아졌다고 할 수 있다. 또한 각자가 가진 강력한 인적 네트워크를 통해 서로 도움을 주고 받으며 새로운 회사를 만들고 함께 성장할 수 있도록 하는 노력을 아끼지 않았다. 실리콘밸리에 또 다른 닷컴 버블이 터진다 하더라도 이들 페이팔 마피아의 생존 가능성은 의심할 여지가 없을 것이다. 정부 시스템이 무너져도 마피아 조직의 견고함은 무너지지 않는 영화 같은 이야기가 실리콘밸리에서 실제로 일어나고 있는 것이다.출처 : jamesaltucher.com한국에서는 이런 사례가 없는지 늘 궁금했다. 얼마 전 알토스벤처스의 한킴 대표님 페이스북에서 ‘네오위즈 마피아'에 대한 이야기를 본적이 있다. 그런데 나는 이 이야기를 읽으면서 네오위즈보다 주목해야 할 회사는 첫눈이 아닌가 하는 생각이 들었다. 네오위즈, 넥슨, NHN 같은 회사보다 훨씬 더 마피아의 밀집도가 높은, 미국의 페이팔을 연상시킬만한 작은 조직이었기 때문이다.첫눈은 2005년 6월 네오위즈에서 분사하며 설립되었던 검색 회사다. 가장 큰 특징은 자사의 내부 DB를 통해 검색 결과를 제공하는 경쟁사들과 차별화하여 인터넷 전체를 검색 대상으로 삼는다는 점이었다. 이후 설립 1년 만에 NHN에 350억원 규모로 매각되었고, NHN의 당시 보도자료에 따르면 임직원은 총 55명이었다. ‘첫눈 마피아’에 대해 정리하려고 마음 먹고 그간 나왔던 첫눈에 대한 기사들을 많이 읽어 봤다. 간간이 첫눈을 페이팔 마피아에 비유했던 기사들도 있었다. 또 마침 우리 회사 렌딧의 홍보 담당인 꼬날이 첫눈의 홍보 담당이었던 덕분에 생생한 이야기를 직접 들을 수 있었다. 정리해 본 첫눈 마피아들은 아래와 같다 (존칭 생략):장병규 : 지난 9월 25일 4차산업혁명위원장에 임명되어 국가적 화제의 인물로 떠오른 장병규 블루홀스튜디오 의장이 첫눈의 창업자. 2005년 6월 네오위즈에서 검색엔진 개발팀 인력들을 이끌고 분사해 첫눈을 창업했다. 2006년 6월 첫눈 설립 1년 만에 NHN 과 350억원 규모로 매각. 이렇게 NHN에 들어간 첫눈의 인재들이 주축이 되어 개발한 서비스가 라인(LINE)이다. 장병규 대표는 첫눈의 NHN 인수 후 초기 기업에 전문적으로 투자하는 벤처캐피털 본엔젤스벤처파트너스를 설립해 우수한 창업자와 스타트업을 발굴하고 지원하는 일에 힘써왔다. 2007년에는 게임개발사인 블루홀스튜디오를 창업해 현재까지 의장직을 맡고 있다.신중호 : 신중호 라인 CGO(Chief Global Officer)는 첫눈의 CTO였다. 2005년에 첫눈 창업 시 장병규 대표와 함께 네오위즈에서 나왔다. 2006년 NHN 인수합병 시 NHN에 합류, NHN 재팬의 검색서비스를 책임지고 일본에 건너가 있던 중 라인을 개발했다. 일본과 동남아시아 여러나라에서 현지화에 성공, 2016년 7월 라인의 나스닥 상장을 견인했다. 최근에는 WAVE, Clova 등 네이버의 AI 사업을 총괄하며 미래를 이끌어 가고 있다.이상호 : SK텔레콤 AI사업단장 역시 첫눈 출신이다. 자연언어처리와 음성합성, 음성검색 분야의 국내 최고 권위자로 알려졌다. 첫눈에 합류하기 전에는 LG전자 연구원을 거쳐 서울산업기술대학 교수를 지냈다. 당시 이상호 교수를 첫눈에 영입하기 위해 장병규 대표가 오랜 시간 공을 들인 것으로 알려졌다. 이상호 단장 역시 첫눈의 NHN 인수합병으로 NHN에 합류한 후 음성 검색 서비스 등 검색 개발에 집중하다, 2011년 다이알로이드라는 음성 인식 개발사를 창업했다. 국내 최고의 음성 검색 전문가 4인으로 구성되었던 다이알로이드는 2012년 9월 국내 최초로 음성으로 문자를 전송하는 앱 ‘다이알로이드'를 선보였다 (관련 내용 : http://limwonki.com/536). 이상호 대표는 2012년 말 다음이 다이알로이드를 인수하며 다음을 거쳐 카카오에서 음성 검색 연구를 지속했으며, 이후 SK플래닛에 입사, CTO 로 기술을 책임지다 2017년 4월부터 SKT AI 사업단장을 맡아 누구 (NUGU) 등 AI 부문 사업을 총괄하고 있다.노정석 : 잘 알려지지 않은 사실이지만 노정석 리얼리티 리플렉션 대표 역시 첫눈 출신이다. 첫눈 창업 초기 약 4개월 간 글로벌 사업 담당으로 일하다, 2005년 9월에 블로그 개발사인 태터앤컴퍼니를 창업했다. 노정석 대표는 1996년 카이스트-포항공대 해킹 전쟁의 주인공으로 유명하다. 이후 1997년 선배들과 창업한 보안회사 인젠이 2002년 코스닥 상장에 성공. 2005년에 설립한 태터앤컴퍼니는 2007년 9월 구글이 인수하며 국내 스타트업으로는 드물은 글로벌 M&A 성공 사례로 기록 되었다. 이후 구글에서 2년여 간 프로덕트 매니저로 일하다 2010년에 설립한 파이브락스(설립 시 사명은 아블라컴퍼니)가 2014년 8월 다시 미국의 모바일 광고 플랫폼 회사인 탭조이에 매각되며, 국내 스타트업에서 드물은 글로벌 M&A 성공 사례를 다시 남긴 주인공이 되었다. 2015년 5번째 회사인 리얼리티 리플렉션을 창업해 국내 대표적인 ‘연쇄 창업가'로 불리운다. 창업과 더불어 엔젤투자자로서 좋은 창업가들을 발굴하고 후배 창업가들과 함께 호흡하는 것을 좋아한다. 티몬, 비트파인더, 미미박스, 다이알로이드, 다노, 다이알로이드(다음이 인수), 파프리카랩(일본 그리 인수), 울트라캡숑(카카오 인수) 등에 투자했다. 나 역시 2011년 미국에서 창업했던 두번째 회사인 스타일세즈 창업 때 노정석 대표의 엔젤투자를 받았다.그 외 2011년 모바일 메신저 ‘틱톡'을 개발해 카카오톡의 강력한 경쟁자로 부상했던 매드스마트의 김창하 대표 역시 첫눈 개발자 출신이다. 김창하 대표는 2012년 매드스마트를 SK플래닛에 매각하며 SK플래닛에서 일하다, 현재는 라인에 합류해 신중호 CGO와 함께 일하고 있는 것으로 알려져 있다. 라인의 박의빈 CTO 역시 첫눈 출신으로 오랜 기간 신중호 CGO 와 함께 하고 있는 핵심 인물이다. 천재 개발자로 유명한 보이저엑스 남세동 대표 역시 장병규 대표와 오랫동안 함께 한 개발자다. 19살에 네오위즈 인턴으로 시작해서 세계 최초의 웹기반 채팅 서비스인 ‘세이클럽 채팅' 개발을 주도하였고, 이후 첫눈을 거쳐 라인에서 일했다. 카카오의 AI 사업을 총괄하고 있는 김병학 부사장 역시 첫눈 출신이다.    대기업과 스타트업을 오가며 활약 중인 첫눈의 인재들도 있다. 이은정 라인플러스 이사는 베인앤컴퍼니 등에서 컨설턴트로 일하던 중 장병규 대표에게 스카우트되어 첫눈에 입사. 첫눈이 NHN에 인수된 후 현대카드, GS홈쇼핑, 삼성카드 등 대기업에서 승승장구 하던 중 2014년 라인플러스에 입사해 글로벌 사업의 중추 역할을 하며 다시 신중호 CGO와 일하고 있다. 엘지생활건강에서 모바일 플랫폼 혁신을 주도하고 있는 권도혁 상무 역시 첫눈 출신이다. 첫눈 합류 전 베인앤컴퍼니와 NHN에서 경력을 쌓았던 권도혁 상무는, 첫눈이 NHN에 인수된 후 다시 전직장 NHN에 합류하지 않고 스타트업인 큐박스에 합류했다. 이후 2011년 ‘울트라캡숑' 이라는 재미있는 이름의 스타트업을 창업, 대학생들의 익명 커뮤니티인 ‘클래스메이트', 소셜미디어 서비스 ‘너 말고 니 친구', ‘마티니', 다이어트 앱 ‘다이어터' 등을 개발해 서비스 하던 중 2014년 카카오에 매각하고 엘지생활건강에 합류했다.   스타트업 홍보를 열심히 하고 있는 우리 회사 꼬날도 첫눈 출신이다. 첫눈 (NHN 매각) - 태터앤컴퍼니 (구글 매각) - 엔써즈 (KT 매각, 이후 닐슨에 재매각됨) - 파이브락스 (탭조이 매각) 등 성공적인 엑시트로 평가되는 스타트업에 연이어 렌딧에 합류. 스타트업 홍보의 미다스 손으로 불리는 국내 유일무이한 이력의 홍보전문가다.첫눈이 NHN에 인수되면서 첫눈의 새로운 검색 정책과 혁신적인 서비스에 기대를 많이 했던 초기 사용자들이 '첫눈이 녹아 버렸다'며 아쉬워했었다고 한다. 하지만 첫눈은 매각 당시 보도했던대로 NHN과 함께 글로벌 서비스 개발에 힘썼고, 메신저 서비스 라인을 만들어 글로벌 시장 진출에 성공했다. 또한 다양한 첫눈 마피아들이 여전히 창업 전선에서 맹활약 중이다. 내가 창업에 뛰어든지 이제 만 12년이 되었고 세번째 회사인 P2P금융 렌딧을 시작한지 2년 반이 지났다. 렌딧은 기술 혁신을 통한 금융 서비스의 효율화라는 미션을 갖고 시작되었다. 혁신적인 서비스를 통해 우리 삶에 긍정적인 영향을 주는 것만큼이나 내게 강한 동기가 되는 것은 함께 일하는 사람들과의 동반 성장이다. 렌딧의 성장 뿐만 아니라 우리 고유의 문화가 몸에 배인 렌딧맨들이 미래에 또 다른 곳에서 새로운 혁신을 만들어 낼 수 있는 강력한 렌딧 마피아가 형성되기를 기대해본다.지난 5월27일, 렌딧의 SeriesB 투자가 확정되던 날 모든 렌딧맨과 함께
조회수 1440

[인공지능 in IT] 서로 다른 우리, 대화할 수 있을까?

설연휴 동안 그간 못 봤던 밀린 TV 프로그램들을 맘껏 즐기며 여유로운 시간을 보냈다. 그 중에서도 여러 분야의 전문가를 초빙해 특강을 해주는 tvN의 '어쩌다 어른'을 보기 시작했다. 몇 년 전 언어인문학을 주제로 한 조승연 작가님편을 보니 새삼 현재 대한민국이 처한 현실을 피부로 느끼게 되더라.< tvN>강연에서 가장 심도있게 다룬 부분은 대한민국 영어교육의 현실이다. 초등학교부터 영어 수업을 듣고, 심지어 말도 제대로 떼기 전인 유아기부터 영어를 주입시키는 것이 어느새 자연스러운 일이 되어버렸다. 하지만, 10년, 20년 이상 영어 교육을 받았는데도 막상 영어로 문서 작업을 하거나, 외국인이 길을 물어보면 식은땀을 흘리는 이유는 무엇일까? 어째서 한국에서는 영어를 제대로 하려는 노력보다, 영어를 아는 노력을 하고 있는 것일까?재미있는 사실은 우리만 영어를 배우려고 애먹는 것이 아니다. 미국인이 한국어를 배울 때에도 비슷한 현상을 겪는다. 강연 중 'FSI(The Foreign Service Institute)'에서 미국인들이 다른 나라 언어를 얼마나 공부해야 소통할 수 있는지에 대한 연구 자료를 공개했다. 언어별 Level 1부터 Level 5까지 다섯 가지 난이도로 구분 되어있고, 이에 따른 총 필요시간으로 구성되어 있는 연구에서, 한국어는 일본어, 중국어와 함께 소통하기 까지 총 2,200시간을 공부해야 하는 Level 5군에 속해 있었다.즉, 전세계 7,000여 개가 넘는 언어 중 한국어는 영어와 문장구조가 완전히 다르기 때문에, 24시간 내내 공부해도 90일 넘게 공부해야 한다는 것. 이렇듯 모국어가 아닌 다른 언어를 배우기 위해서는 어마어마한 시간과 노력이 필요하다. 만약, 단순히 언어를 알기 위해 배우는 것보다, 소통하기 위해 배운다면 흔히들 말하는 'ROI(Return on Investment)'를 더 높일 수 있자 않을까.출처: 동아일보소통을 위한 언어 학습은 비단 사람에게만 해당되는 것이 아니다. 기계와 사람의 소통 역시 요즘과 같은 인공지능 시대에서는 빼놓을 수 없는 부분이다. 몇 년 전부터 업계에서는 '챗봇(Chatbot)' 열풍이 불고있다. 챗봇은 대화(Chat)와 로봇(Robot) 두 단어를 합친 신조어로서, 각종 앱이나 웹을 기반으로 문자를 통해 사용자의 의도를 파악해 대화할 수 있는 인공지능 기계다. 여기에는 '자연어 처리(Natural Language Processing, NLP)', '자연어 이해(Natural Language Understanding, NLU)', '머신러닝(Machine Learning)' 등 수많은 기술이 접목되어 발전 중이다. 현재 챗봇은 나날이 진화하며, 텍스트를 텍스트로만 처리하는 것을 넘어, '음성으로 변환(Text-To-Speech, TTS)'시키거나, '음성을 텍스트로 변환(Speech-To-Text)'시키는 등 다양성에 있어 점점 넓은 범위에 적용되고 있는 추세다.< 출처: Understanding Natural Language Understanding, Bill MacCartney >글로벌 챗봇 시장은 매년 큰 폭으로 성장하고 있는 추세이며, 여러 사업 분야에 걸쳐 사용되고 있다. 북미의 시장조사기관 'Credence Research' 조사에 따르면, 글로벌 챗봇 시장은 2015년부터 2023년까지 연평균 35% 성장할 것으로 예상된다. IT솔루션 기업 'MindBowser'가 조사한 결과, 95%의 기업이 챗봇 활용성에 대해 긍정적인 반응을 보였으며, 고객응대(93%)부터, 마케팅(61%), 상품 주문(47%), 소셜 미디어(32%) 등 사업 분야에서 활용되는 용도 역시 다양한 것으로 밝혀졌다.챗봇은 어떠한 프로세스를 통해 실제로 작동하는지 살펴보기 위해서 사내 엔지니어의 도움을 받았다. 스켈터랩스에서 대화형 인공지능 프로젝트팀에 있는 정태형 엔지니어가 메신저를 통한 간단한 시범 사례를 스크린샷으로 찍어 보여주었다.< 인공지능 메신저 사례, 출처: 스켈터랩스 >여행지를 자동으로 추천해주는 엡에 적용할 수 있는 챗봇과의 대화다. 사용자가 "여행을 가고 싶다"고 말하자 자동으로 '카이트봇'이 반응하고, 여행 기간과 테마를 물어본다. 여기서 사용자가 "여행 기간"을 말하자 챗봇은 자동으로 '3월'과 '7일'을 인식, 이전 질문에서 대답하지 않은 테마에 대해 질문한다. 이렇게 사용자와 챗봇 사이에서 대화를 자연스럽게 주고 받을 수 있는 것은 대화의 구성 요소 중 '의도(Intent)', '개체(Entity)', '맥락(Context)'이 중요한 역할을 한다. 이를 간단히 살펴보도록 하자.의도(Intent)는 사용자가 어떠한 의도로 대화를 하는지를 의미한다. 위 스크린샷의 경우, 여행을 가는 것'이 의도라 할 수 있다. 예를 들어, "여행 가고 싶어"가 아닌 "여행 가볼까?"로 입력하더라도 - 미리 여행을 가는 것에 대한 자연어 기반 패턴이 'Intent Classifier'에 입력되어 있는 상태라면 - 이를 '사용자가 여행을 가고 싶구나'라는 의도로 이해할 수 있는 것이다.개체(Entity)는 사용자의 의도 중에서 실체가 될 수 있는 변수를 말한다. 개체는 사용자가 입력한 문장에서 특정한 변수가 달라질 때 사용된다. 위 스크린샷의 경우, '3월 3일', '해변', '일주일' 등과 같이 주로 명사 형태로 구성된 문장에 들어가는 구성 요소를 말한다.문맥(Context)은 이전 대화를 자연스럽게 이어갈 수 있도록 처리할 수 있는 기능이다. 예를 들어, 사용자가 챗봇에게 "가수 빅뱅의 프로필을 검색해달라"고 요청했다. 그리고 빅뱅의 노래를 듣기 위해 "거짓말 틀어 줘"라고 명령하면, 기존에 빅뱅이라는 가수에 대해 대화하고 있던 문맥을 인식해 God의 거짓말이 아닌 빅뱅의 거짓말을 재생하는 것이다.이 외에도 챗봇에는 '말뭉치(Utterance)', '시나리오(Scenario)', '슬롯채우기(Slot Filling)' 등 다양한 구성요소를 통해 대화를 이어나갈 수 있다. 물론, 아직 100% 인간과 대화하는 기술까지 이르지는 못했다. 하지만, 우문현답하지 않고 사용자 의도를 정확하게 파악하는 수준에 이르러 생활에 편의성을 제공하고 있다.한국어의 경우 언어의 난이도 때문에 국내 기업은 물론 많은 글로벌 IT 기업도 아직 완벽한 수준에 도달하지 못했다. '잘 한다'라는 말만 하더라도 '훌륭하게 하다', '만족할 만하다', '자주 하다' 등의 긍정적인 표현이 있는가 하면, '잘 하는 짓이다' 등의 부정적인 표현인 경우도 흔하기 때문이다. 결국 챗봇도 기계이기 때문에, 여러가지 문장과 상황을 학습시켜 한국어 성능을 향상시켜야만 한다.다시 '어쩌다 어른'으로 돌아가보자. 강연을 마무리할 즈음 조승연 작가는 이렇게 말한다."영어도 결국 언어의 한 종류, 영어를 쓰는 사람들도 우리와 같은 사람, 우리처럼 희로애락을 느끼는 인간입니다. 기계와 얘기하기 위해 법칙에 맞춰 말해야 하는 것이 아니라 그 사람과 감정을 통하게 해주는 어떤 도구입니다."여전히 우리는 챗봇이라는 기계와 소통한다기 보다, 일방적으로 질문을 던지고, 챗봇은 미리 입력되어 있는 규칙 안에서만 답한다. 학습을 통해 수많은 데이터가 축적된다 하더라도, 아직까지 언어를 통해 전달되는 인간의 감정을 완벽히 이해하기에는 부족한 것이 사실이다. 과연 기계가 '법칙'에 맞춰서 말해야 하는 것 이상을 넘어서는 순간이 올까? 우리는 그 순간을 찾아 지금도 노력하고 있는지 모른다.이호진, 스켈터랩스 마케팅 매니저조원규 전 구글코리아 R&D총괄 사장을 주축으로 구글, 삼성, 카이스트 AI 랩 출신들로 구성된 인공지능 기술 기업 스켈터랩스에서 마케팅을 담당하고 있다#스켈터랩스 #기업문화 #인사이트 #경험공유 #조직문화 #인공지능기업 #기술기업
조회수 1563

스타일쉐어 커머스 시스템 리빌딩 회고 1

스타일쉐어 스토어 소개스타일쉐어 스토어(이하 커머스)는 2016년 4월 출시되어 지금까지 나날이 성장하고 있습니다. 작년 초, 커머스 시스템을 리빌딩하기로 했고 현재까지 진행 중입니다. 어떤 이유로 리빌딩을 하는지, 어떤 고민을 했는지, 어떻게 진행하고 있는지 몇 자 적어보려 합니다. 이 글은 문제 인식, 목표, 계획에 대한 내용입니다.리빌딩을 결정한 이유커머스 프로젝트를 시작할 당시 커머스 시스템을 경험해본 개발자가 없었습니다. 이 상황에서 새로이 구현하기엔 위험 부담이 크다는 판단을 했습니다. 때문에 커머스 솔루션을 도입했고, 적은 비용으로 커머스 기능을 출시할 수 있었습니다. 거래량이 점점 늘어나면서 솔루션이 감당할 수 있는 한계치를 벗어나자, 예상치 못했던 기술적인 이슈가 발생했습니다. 사내 MD팀, CS팀은 물론, 입점사들과 유저들에게까지 불편한 경험을 주고 있었습니다. 개발팀은 솔루션 유지 보수와 운영 이슈에 집중했지만, 끝이 없는 문제들에 점점 지쳐갔습니다. 개발팀의 퍼포먼스는 저하되고 있었고, 새로운 기능 개발에 집중하지 못하는 상황까지 발생했습니다. 이 시점에서도 거래량과 매출은 꾸준히 늘어났으며, 더 늦기 전에 리빌딩을 진행해야겠다고 판단했습니다.리빌딩의 목표당장 눈앞에 생겨나는 문제들로, 서비스가 해결하고자 하는 본질적인 문제들에 집중하지 못하는 상황입니다. 궁극적으로 이 상황을 개선하고 싶었습니다. 그리고 선택의 기로에 섰을 때 좋은 기준을 제시하기 위해, 언제 끝날지 모르는 리빌딩이 산으로 가는 걸 막기 위해 목표를 몇 가지 세웠습니다.유지보수 및 운영 이슈에 소모되는 개발 리소스 최소화커머스 시스템과 연계되는 기능들을 공격적으로 개발할 수 있도록 함개발 리소스 대비 높은 퍼포먼스를 낼 수 있어야 함튼튼한 커머스 시스템목표 1. 유지보수 및 운영 이슈에 들이는 개발 리소스 최소화기존 솔루션의 큰 레거시는 소모될 개발 리소스의 양과 일정을 예측하기 어렵게 만들었고, 개발자에게도 큰 스트레스를 안겨줬습니다. 서비스의 성장을 방해하는 큰 걸림돌 중 하나이며, 개발팀의 움직임을 느리게 만드는 주된 원인이었습니다. 리빌딩을 완료하더라도 유지보수와 운영 이슈는 끝이 없을 테지만, 더 이상 같은 문제를 반복하고 싶지 않았습니다. 효율적인 개발 리소스 운용을 위해선 가장 신경 써야 하는 부분이라고 생각됩니다.목표 2. 커머스 시스템과 연계되는 기능들을 공격적으로 개발할 수 있도록 함기존 솔루션 레거시가 너무 복잡하여 기능을 추가하거나 개선하기 어려워 반려한 요구사항이 많았습니다. 매력적인 요구사항에도 조심스럽게 대응했습니다. 서비스 성장을 위해 다양한 시도를 해야 하는 상황인데, 기술적인 이유로 진행이 까다롭다고 말하는 게 항상 아쉬웠습니다. 개발팀에서도 좋은 기능을 공격적으로 구현하고 싶으나, 실제로도 작업하기 까다로워 항상 답답했습니다. 어떤 방법이던 괜찮으니 지금보다 훨씬 더 공격적인 기능 구현으로 서비스 성장에 좋은 영향을 주고 싶었습니다.목표 3. 개발 리소스 소모 대비 높은 퍼포먼스를 낼 수 있어야 함스타일쉐어 팀은 기존 서비스 운영과 동시에 항상 새로운 무언가를 찾으려 합니다. 개발팀은 이 움직임에 맞춰 개발 리소스를 효율적으로 운용해야 합니다. 하지만 개발 리소스는 한정적이며, 다양항 상황에 따라 유동적으로 변해 예측하기 어렵습니다. 이런 상황에서 커머스 관련 작업 시, 언제나 평균 이상의 높은 퍼포먼스를 내고 싶었습니다.목표 4. 튼튼한 커머스 시스템커머스 시스템의 장애는 매출에 직접적인 악영향을 줍니다. 어떤 작업을 하더라도 커머스 시스템은 잘 운영되어야 합니다. 높은 가용성은 개발팀의 숙명이며, 공격적인 기능 개발에도 높은 가용성을 유지하려면 더욱 신경 써야 합니다.모두 꿈에 가까운 목표들입니다. 이 목표들에 조금이라도 더 가까이 다가갈 수 있도록 노력한다면 보다 좋은 결과를 얻을 수 있을 거라 생각했습니다.현실 반영과 계획당연하게도 현실적인 부분을 생각해야 했습니다. 이미 예정된 작업들이 많아 리빌딩에 필요한 개발 리소스를 확보하기 어려웠고, 개발 성공 여부 또한 불확실해 팀원들을 설득하기 어려웠습니다. 유지보수와 운영 이슈는 끝이 없었고, 회사 방향에 따라 추가 기능 개발과 개선 작업을 진행해야 했습니다. 이 상황을 고려해 리빌딩을 성공적으로 진행할 수 있도록 계획을 세워나갔습니다.개발 리소스에 여유가 생길 때까지 서브 프로젝트로 진행기존 커머스 시스템과 리빌딩된 시스템을 동시에 운영할 수 있어야 함적절한 단위로 서비스를 나눠 안전하고 효율적으로 개발 진행계획 1. 개발 리소스에 여유가 생길 때까지 서브 프로젝트로 진행다들 리빌딩이 필요하다고 느꼈지만, 선뜻 진행하기엔 큰 부담이었습니다. 시간이 흘러 여유가 생길 때 리빌딩을 진행해도 되지만, 그땐 너무 늦을 것 같았습니다. 최대한 빨리 작업을 시작하고 싶었고, 그러기 위해선 서브 프로젝트 수준으로 진행하는 게 제일 빠른 방법이라 생각했습니다.계획 2. 기존 커머스 시스템과 연동되어 동시에 운영할 수 있도록 함리빌딩의 완료 시점을 예측하기 어려웠습니다. 때문에 기존 커머스 시스템을 운영하며 리빌딩을 진행해야만 했습니다. 그리고 리빌딩된 시스템은 점진적으로 기존 커머스 시스템을 교체해 나가야 하므로, 두 시스템이 서로 연동되어야 했습니다. 또한 리빌딩된 시스템이 잘 동작할지에 대한 확신이 없어, 언제나 후퇴 계획을 세워야 했습니다. 이런 이유로 기존 커머스 시스템의 DB와 스키마를 그대로 사용하고, 두 시스템의 로직이 서로에게 문제가 되지 않도록 조심스럽게 개발하기로 했습니다.계획 3. 적절한 단위로 서비스를 나눠 안전하고 효율적으로 개발 진행커져가는 개발팀과 복잡해져 가는 커머스 시스템을 생각하면 요즘 자주 들리는 마이크로 서비스 구조(이하 MSA)를 도입해도 되지 않을까 싶었습니다. 솔직히 이런저런 이유보다, 재미 때문에 도입해보고 싶었습니다. 처음 도입하는 구조라 조심스러웠지만, 적절한 단위로 서비스를 나눈다면 충분히 좋은 효율을 낼 수 있을 것이라 생각되었습니다. 재미와 만족은 덤이고요. 서비스를 나누는 기준은 2가지로 잡았습니다.개발 안정성/개발 속도 둘 중 어느 것에 집중해야 하는가?서비스를 사용하는 주체는 누구인가?—개발 안정성/개발 속도 둘 중 어느 것에 집중해야 하는가?개발 안정성과 개발 속도에 대해 생각한 이유는 시스템 안정성과 작업 퍼포먼스 모두 잡고 싶은 마음 때문이었습니다. MSA는 서비스 별로 다른 언어를 사용해도 괜찮다는 장점이 있습니다. 덕분에 상황 별로 각기 다른 언어를 사용할 수 있고, 이를 통해 개발 안정성 혹은 개발 속도에 잘 집중할 수 있을 것이라 기대했습니다. 나중에 기술 스택을 설명하면서 얘기를 하겠지만, 개발 안정성을 추구해야 하는 부분은 java8로, 개발 속도를 추구해야 하는 부분은 node나 python으로 구현됩니다.서비스를 사용하는 주체는 누구인가?서비스를 어떤 기준으로 나눌지 고민이 많았습니다. 처음엔 상품 관리, 주문 관리, 출고 관리 등 DB 테이블에 가까운 기준으로 나누려고 했으나, 서버 운영이 큰 부담으로 다가올 것 같다는 의견 때문에 다른 기준을 세워야 했습니다. 서비스를 사용하는 주체 별로 나누면 어떨까라는 말이 나왔고, 이 기준이라면 큰 부담이 없을 것 같았습니다. 서비스를 사용하는 주체는 유저, 관리자, 입점사, 다른 서비스 이 4가지라고 정의했고, 이에 따라 서비스를 구성했습니다.—앞서 말한 두 기준으로 나눴을 때에 대한 예시입니다.유저 커머스 서비스 (node 혹은 python)관리자 커머스 서비스 (node 혹은 python)입점사 커머스 서비스 (node 혹은 python)커머스 핵심 기능 서비스 (java8)위 두 기준 말고도 서비스 운영에 큰 부담이 없다면 상황에 맞춰 다른 기준을 세워 서비스를 나누기도 했습니다.이 고민이 끝난 후, 현실적으로 실행 가능한 계획이 나온 것 같아 만족스러웠습니다. 이 계획을 토대로 아키텍처를 구성하고 개발을 진행했습니다.마무리어떤 문제 때문에 리빌딩을 결정했고, 어떤 목표와 계획을 세웠는지 주저리주저리 적어봤습니다. 앞에서 말했듯이 리빌딩은 아직 진행 중입니다. 현재 정식 일감으로 진행되고 있고, 상황에 맞게 계획을 다시 세워 조금씩 목표에 다가가고 있습니다. 처음엔 3부로 계획했지만, 아직 리빌딩 중이란 걸 까먹고 있었나 봅니다. 이 프로젝트가 완료되기 전까진 회고록으로써 글을 계속 쓸 것 같습니다. 앞으로 풀어야 할 문제들이 많으니 하소연할 것들도 많을 거고요. 아무쪼록 다음 편에선 시스템 구성도와 기술 스택에 대해서 한번 이야기해보려 합니다.스타일쉐어 개발팀의 고민과 생각들이 부디 도움이 되었길 빕니다. :)#스타일쉐어 #개발팀 #리빌딩 #인사이트 #조언
조회수 1701

IT 서비스 모니터링 제대로 잘하기

모니터링은 IT 운영의 핵심입니다. 장비의 활성화 상태에서 애플리케이션의 변화와 성능 이슈까지 언제나 실시간으로 인지와 대응이 가능해야 합니다. 서비스를 운영에 장애를 없앨 수는 없지만 좋은 모니터링 전략을 가지고 있다면 빠른 예방과 대응을 통해 고객이 불편함을 느끼지 못하게 할 수는 있습니다.  IT 운영에서의 비지니스 목표IT 서비스 모니터링 전략을 만들기 전에 우리는 우선 목표를 선정해야 합니다. 빠른 예방과 대응은 좋은 모니터링 전략의 기본 목표일 뿐입니다. 우리는 모니터링을 통해 아래와 같은 비지니스 목표를 이루어야 합니다. 브랜드 이미지 향상매출증대비지니스 개선비지니스 목표를 위한 모니터링그리고 이런 비지니스 목표를 위해서는 아래와 같은 일들을 모니터링을 통해 수행할 수 있어야 합니다. 안정적인 서비스 운영 (브랜드 이미지 향상, 매출증대)빠른 장애 대응 (브랜드 이미지 향상, 매출증대)장애 예방 (브랜드 이미지 향상, 매출증대)사용자 분석 (비지니스 개선)사용성 분석 (비니지스 개선)서비스 성능 개선 (브랜드 이미지 향상, 매출증대)현대 IT 서비스는 물리서버와 클라우드가 혼재되어 있는 인프라스트럭처 환경과 다양한 플랫폼에서 개발된 애플리케이션들이 작게 구성되어 있는 복잡한 구성을 가지고 있습니다. 뿐만아니라 서비스의 구성 또한 전 세계에 분산되어 있는 상황에서 우리는 효율적인 모니터링 전략을 만들어서 IT 서비스를 운영해야 합니다.비지니스 목표를 위한 모니터링 전략이런 체계적이고 효율적인 IT 서비스 모니터링 전략을 만들기 위해서는 아래와 같은 것들을 고려해야 합니다.1. 통합 모니터링 체계를 구축하세요.  인프라스트럭처와 애플리케이션을 모두 모니터링하여 전체 그림을 얻어야 합니다. 전체적인 그림을 모든 운영자들이 알수 있어야 체계적인 IT 서비스 운영이 가능합니다.2. 기준을 넘어서는 성능 변화가 생기면 알수 있도록 경고를 설정해야 합니다. CPU 부하율, 메모리 사용률, 누적 트랜잭션 등 다양한 상황에 대한 기준 값을 선정하고 이에 대한 알림을 받을 수 있어야 합니다. 초기 이슈 확인은 고객이 영향을 받기 저너에 문제를 해결할 수 있게 해 줍니다. 3. 사용자 관점에서 모니터링 해야 합니다. 예를 들어 TPS의 평균값만으로 서비스의 안정성을 판단해서는 안됩니다. 사용자 개개별 현황을 파악 할 수 있어야 합니다. 기업의 브랜드는 서비스 사용에 불편을 겪는 1%의 고객을 통해 내려갈 수 있습니다.4. 메트릭을 비지니스 목표와 맞출 수 있어야 합니다. 현재 서비스에 접속한 사용자 현황을 알 수 있어야 합니다. 예를 들면 동시 접속자 수를 기반으로 현재 서비스의 성능을 설명할 수 있어야 합니다. 5. 애플리케이션에서 특히 데이터베이스의 성능을 평가할 수 있어야 합니다. 많은 이슈들이 데이터베이스에서 발생합니다. 6. 애플리케이션의 코드 성능을 분석할 수 있어야 합니다. 많은 프로젝트에서 오픈소스 또는 서드파티 솔루션들이 사용되고 있습니다. 여기서 발생하는 문제들은 심각한 장애 상황을 유발할 수 있습니다.7. 모든 서비스를 분석 할 수 있어야 합니다. 몇몇 페이지가 아니라 전체 페이지를 분석 할 수 있어야 합니다. 우리는 항상 효율적인 IT 모니터링 전략을 재평가하고 새로 구축해야 합니다. 모니터링 전략을 만드는 것은 쉬운 일이 아닙니다. 하지만 모니터링 전략을 만드는 데 시간을 투자하는 것은 안정적으로 서비스를 운영하는데 있어서 매우 가치있는 일입니다. #와탭랩스 #개발자 #개발팀 #인사이트 #경험공유 #일지
조회수 1023

Good Bye, Parse

한 번도 안 써 본 개발자는 있어도 한 번만 써 본 개발자는 없을 듯한 Parse(parse.com)가 오늘 아침 충격적인 공지 메일을 보내왔는데, 1년후 서비스 종료한다는 것이었다. Parse는 모바일에 특화된 DB 플랫폼을 시작으로, 어드민툴, 푸시, 애널리틱스, 로그인, 크래쉬리포트 등으로 서비스를 확장하며 모바일 개발자들의 백엔드 구축을 편하게 만들어주었는데 2013년 페이스북에 인수되면서 안정성에 대한 신뢰가 커지고, 가격 정책이 변경되어 상당히 저렴해지면서 개발자들의 더욱 큰 호응을 얻었다. 1초에 30개의 요청까진 무료였으니 1개월로 환산하면 7700만 요청이 무료이고, 이는 10만 사용자 이상도 무난하게 대응할 수 있는 숫자이다. 물론 사용자와 요청 수가 늘어나면 월 100~200달러씩 비용을 추가하면 되고 무엇보다 서버 확장에 신경쓰지 않아도 됐다. 푸시 발송은 100만대의 기기까지 발송이 무제한 무료였고 크래쉬리포트와 유저 리텐션 분석도 심플하고 쓸만했는데 무료였다. 역시 무료인 2TB 트래픽은 상용 CDN을 사용하면 수십만원의 비용이 든다.복잡한 쿼리를 작성하기 어렵고 서버사이드 코드를 디버깅하기 쉽지 않아 서버 로직이 복잡한 서비스에는 적합하지 않았지만 계속 보완된다는 느낌이 있고 무료이니 불편해도 감수하고 쓸만 했어서, 이외에는 단점을 찾기가 어려울 정도였다. 무료티어가 워낙 후해서 돈을 내고 쓰는 개발자가 얼마나 될까 싶다는 점이 오히려 불안요소였다. Parse 그리고 페이스북 입장에서 수익성이 어떻게 확보될까 의구심이 들었던 것이다.  수익성 측면에서 무료로 사용하던 앱들의 트래픽이 늘어나 과금티어로 넘어가야 하는데 그러려면 100만 다운로드 이상의 중대형 서비스로 성장해야 하고, 그 정도 되면 그 스타트업도 자체 백엔드 엔지니어가 확보되고 백엔드 로직도 복잡해져 Parse와 궁합이 잘 안 맞게 되는 것이다. 수익성이 아니라면 잠재적인 가치로써 Parse를 사용하는 스타트업과 페이스북의 연결고리를 찾아 유저 베이스를 활용하던지 광고 매체로써 활용하던지 방안이 있어야 하는데 페이스북 자체 패밀리들(페이스북, 메신저, 인스타그램, 와츠앱)의 매체력과 광고수익이 워낙 크고 견고하게 성장하는 중인데다 페이스북 애널리틱스와 외부 광고 서비스가 자리잡으면 Parse는 더더욱 투자가치가 떨어진다. 당장 수익성이 없고, 잠재적인 가치도 잘 모르겠고, 똑똑한 내부 엔지니어들이 묶여 있고, 무료로 쓰는 개발자들은 매일 뭐 만들어달라 뭐 고쳐달라 요청을 하니 '우리가 이걸 왜 붙들고 있어야 하지' 내부 논의가 계속 있어왔을 것 같은데, 그래도 작년 가을까지는 페이스북 코리아에서 Parse 관련 세미나와 컨퍼런스도 열고 미국에서 F8이 열린 시점에는 IoT와 React로 확장하는 등 좀더 적극적으로 활용하려는 듯 했다. 작년 겨울부터 분위기가 이상해졌다. 우선 작년 말 크래쉬리포트 서비스 종료. Parse의 서비스의 일부이긴 했지만 다른 대안과 비교해서 확실한 우위가 있던 서비스여서 '수익성'의 이유 외에는 정리할 이유가 없어보였다. 그 동안 문제 삼지 않았던 '수익성'이 이제는 문제가 된건가? 그러면 사실 Parse 전체 서비스도 접을 수 있다는 생각이 들었다. 그리고 이어서 나온 '오픈소스화'와 'Heroku 연동'. 오픈소스는 좋은 의미 또는 서비스의 발전을 위해서라고 볼 수도 있는데 Heroku 연동은 굳이 왜? Parse 입장에서나 Parse를 쓰는 개발자 입장에서나 백엔드를 Parse 클라우드 코드 대신 Heroku로 이동할 이유가 없어 보였다. Parse를 접게 되어 마이그레이션 해야 한다면 모를까...??? 가 현실이 되었다.나도 작년까지는 서버비 한 푼 내지 않고 대부분의 서비스를(수십개) Parse에 올렸었고 주변에나 Parse 세미나에서도 Parse의 장점에 대해 이야기하곤 했는데... 수익성 없는 앱들을 정리하면서 Parse에 대한 의존도가 낮아지긴 했지만 지금 제작하고 있는 것들부터 마이그레이션 해야 하고, 추천했던 지인들에게는 마이그레이션 전략을 컨설팅해야 할 판이다. 서비스 종료라는 극단적인 선택을 하기 전에 무료 티어를 좁혀서 수익화를 한 상태로 좀더 오래 버텨줬으면 어땠을까 하는 아쉬움이 든다. 구글앱스도 수익화를 하면서 유지하듯이. 광고 외의 B2B 비즈니스에서 페이스북에 대한 신뢰가 많이 하락하지 않을까 싶다. 마이그레이션 비용은 상당하다. 우선 Parse의 슬로건이 백엔드 작업 없이 백엔드를 이용할 수 있는 것이었기 때문에 이제 백엔드를 직접 구성하던지 Parse가 제공하던 서비스들의 대체제들을 각각 찾아 옮겨야 하고, 초기부터 그런 대안으로 구축한 것보다 이전하는 비용이 훨씬 큰 것은 당연하다. 클라이언트 엔지니어가 Parse로 구축했다가 아직까지 백엔드 엔지니어가 없는 서비스들은 그냥 정리될 가능성도 크다. 사실 백엔드 서비스들의 이용료 자체는 많이 낮아진 상태이기 때문에 여기서 말하는 마이그레이션 비용은 대부분 외주/컨설팅이나 백엔드 개발자 채용 등의 엔지니어링 비용이다. 나도 대안들을 이제 막 찾기 시작했지만 이전할 수 있는 대안들을 정리하면서 마무리해본다. Parse DB: http://firebase.com/ (구글이 인수했다. 데자뷰의 느낌이 들 수도 있지만 그래도 믿어보자)Parse Push: http://valuepotion.com/ (푸시 & 분석& 마케팅툴 - 카카오 국내 자회사의 글로벌 서비스)https://www.pushwoosh.com/ (urbanairship보다 저렴하다고)Parse Crash Reporting: http://crashlytics.com (트위터 인수, 과거에 유료였으나 무료로 전환됨)Parse Cloud Code:http://heroku.com/ (Parse의 Heroku 연동과 함께 살펴보자)https://github.com/ParsePlatform/parse-server (Parse에서 제공한 서버 오픈소스. 이를 활용한 마이그레이션 서비스가 나올 듯 싶다)
조회수 899

[Buzzvil People] Jin Yoon, Product Manager

 Buzzvil People에서는 다양한 배경과 성격 그리고 생각을 지닌 버즈빌리언들을 한 분 한 분 소개하는 시간을 갖습니다. 어떻게 버즈빌에 최고의 동료들이 모여 최고의 팀을 만들어가고 있는 지 궁금하시다면, 색색깔 다양한 버즈빌리언들 한분 한분의 이야기가 궁금하시다면, Buzzvil People을 주목해주세요.1. 간단한 자기 소개 부탁드립니다. 안녕하세요. 버즈빌의 여러 Product 중 하나인 버즈스크린(BuzzScreen)을 담당하고 있는 Product Manager, Jin 입니다. 요즘에는 사무실에서 알파카 or 라마를 닮았다는 흉흉한 소문이 퍼지면서 이름 대신 불리기도 합니다. 첫 사회생활은 Oil & Gas industry의 한국 대기업에서 시작했습니다. 쉽게 얘기하면 세계 곳곳 석유가 묻혀있는 곳에 그 석유를 캐내고 정제하는 공장을 지어주는일이죠. 몇억 불에 달하는 프로젝트 전반을 관리하는 Project Management가 저의 role이었습니다. 그 후에는 모바일광고, pet food ecommerce, 음식 배달 등 한국/미국의 작은 스타트업에서 일하다가 버즈빌에 조인하게 됐습니다.  2. 어떻게 버즈빌에 오시게 되셨나요? 가장 보수적인 industry의 가장 한국적인 대기업이었던 첫 회사를 그만두고 MBA를 하면서 크게 3가지에 초점을 맞춰 진로를 찾았습니다.  빠르게 변화하는 industry 나의 transferable skill을 사용할 수 있는 position 조금 더 자유로운 분위기에서 일할 수 있는 환경  찾다보니 그 industry는 IT였고, Project Management 에서 나름 배웠던 skillset을 사용할 수 있는 포지션은 여러 가지가 있었지만, Product Manager가 가장 가깝다고 생각했습니다. 자유로운 분위기는 미국에 있는 여러 tech giant 들, 그게 아니라면 스타트업이라는 생각이 확고했고요. 그렇게 들어간 곳이 LA에 있는 작은 스타트업이었습니다. 총 4명 정도의 작은 회사였기 때문에 1년여간 일하면서 마케팅, 기획 등 여러 가지 일들을 배울 수 있었고 개발적인 부분도 일부 배울 수 있었습니다. 하지만 tech 회사라고 하기에는 개발인력도 많이 부족했고, 조금 더 배울 수 있는 곳을 찾다 보니 버즈빌에도 지원하게 되었습니다. 버즈빌에 오기로 결정하게 된 가장 큰 이유는 버즈빌이 인터뷰를 진행하는 방식이였습니다. 3차례의 인터뷰를 보면서 굉장히 재미있었거든요.  PM면접은 1, 2차 두 번 다 과제가 있었고, 타이트한 데드라인에 맞춰 준비하면서 긴장도 많이 하고 엄청난 부담감을 갖고 인터뷰에 들어갔는데… 하지만 막상 인터뷰에서는 제가 해온 과제를 평가받는 게 아니라 “이 문제를 조금 더 잘 풀기 위해서 어떻게 할 수 있을까?”를 같이 머리를 맞대고 자유롭게 얘기하면서 고민하다가 시간이 가더라고요. CEO, CPO와 보는 인터뷰가 이런 거라면 “일할때도 내 생각을 자유롭게 얘기하면서 같이 일할 수 있겠구나” 라는 느낌을 강하게 받아서 조인하기로 결정했습니다. Interviewer로 참석했던 Jay 와 Young이 보여준 “만담” 도 한 몫했습니다.  3. 버즈빌에서 어떤 업무를 담당하고 계신가요? 버즈스크린이라는 Product의 Product Manager 역할을 하고 있습니다. 간단하게 얘기해서 supply side인 파트너사들과 유저의 니즈, 시장의 상황 등을 반영하여 로드맵을 짜고, 그 로드맵에 맞춰 프로덕트를 발전시키고 개선하는 역할이라고 할 수 있겠네요.  특히 버즈스크린은 SDK 상품이다 보니 파트너사와 interaction이 많은 편입니다. 파트너사와 정기적인 미팅을 통해 개선점을 발굴하고 필요한 기능들을 제품에 녹여내기도 합니다. 하지만 한국뿐만 아니라 외국의 여러 파트너사도 하나의 공통된 Product를 사용하기 때문에 너도, 나도 원하는걸 다 세세하게 전부 들어줄 수 없습니다. 그렇게 되면 결국 더는 관리 할 수 없는 Product이 될수 있기 때문이죠. 무엇이 정말 Product의 발전을 위해 필요한것인지, 어떻게 하면 Product의 sustainability를 해치지않고 유저와 파트너사들을 만족시킬 수 있는지 생각을 많이 해야 하는 포지션인 것 같습니다. 또 내부적으로는 Business의 호흡과 Development의 호흡을 조절하는 역할을 담당해야 합니다. 현재 상황을 놓고 생각해봤을 때 어느 한쪽이 너무 빠르거나 느리게 달려간다고 생각할때는 속도를 조절하고, 이에 맞춰 counterpart의 기대치를 조정하는 역할을 해야합니다. 이를 통해 개발자들이 쫓기지 않고 개발할 수 있는 환경을 마련해주어야 하고 사업 담당자들이 파트너사에 적절하게 대응할 수 있는 환경도 마련해주어야 하고요. 결국 각 분야에서 전문성을 가진 사람들이 자신들의 역량을 가장 잘 발휘할 수 있도록 그 일에만 집중할 수 있게 만드는 일을 하고 있다고 (혹은 해야 한다고..) 생각합니다. 4. 스타트업에서 혹은 광고업계에서 일하는 느낌이 어떠세요? 스타트업에서 일하는 건 정말 힘든일인 것 같아요. 하지만 힘든 만큼 나름 재미도 있고 보람도 느끼면서 일하고 있어요. “힘들다”는 사실이 큰 장점이 될 수도 있는 곳이 스타트업인것 같습니다. 대기업에서 일했던 경험과 비교해보면 스타트업은 확실히 프로세스가 덜 갖춰져 있습니다. 그러다 보니 프로세스에서 보완될 수 있는 부분들에까지 리소스가 들어간다는 점, 회사에서 이탈하는 한명 한명의 빈자리가 상대적으로 크다는점은 단점이라고 할 수 있을 것 같네요. 하지만 바꿔서 생각해보면, 정해진 프로세스가 없다 보니 자유도가 높고, 일의 진행속도도 빠릅니다. 부서 간에 scope of work를 놓고 논쟁하지 않고, 모두 달려들어 일을 끝낼 수 있는 가장 빠른 방법을 찾아 끝내고, 그 과정에서 내가 할 수 있는 일을 스스로 찾아서 할 수 있는 것도 굉장히 흥미롭습니다. 또한 회사 구조적으로도 이것저것 새로운 시도들을 하는 것도 재미있습니다. 대기업에 있을 때는… 이미 다 채색까지 완성된 그림이 있고 그 위에다가 계속해서 정해진 같은 색으로 조금씩 점을 찍고 있는 느낌이 들었다면, 스타트업에서는 그야말로 스케치만 되어있는 도화지에 그림을 그리는 느낌이 듭니다. (물론 이건 스타트업에서 일하는 느낌이 아니라 버즈빌에서 일하는 느낌일 수도…) 누가 그리느냐에 따라 초등학생의 낙서가 될 수도 있고, 유명한 화가의 명작이 될 수도 있겠지만요. 그 과정은 정말 정말 힘들지만, 회사의 성장에 기여한다는 보람도 느낄 수 있고, 나도 성장할 수 있는 환경이라고 할 수 있겠네요.  욕심 없이 편안하게 주어진 일만 하면서 살고 싶은 분들에게는 스타트업에서 일하는 게 정말 지옥 같고 힘든 일이 될 것 같네요. (지극히 개인적인 의견입니다.) 5. 이것만큼은 버즈빌이 참 좋다! 어떤 게 있으실까요? 버즈빌은 그야말로 인사가 만사다 라는 말에 딱 들어맞는 회사입니다. 이 사람들과는 어떤 일을 해도 성공할 수 있겠다는 생각을 하게 하는 분들만 모여있는 것 같아요. 제가 힘들 때마다 Steve가 항상 “지금은 공기처럼 당연해서 크게 느껴지지 않겠지만 지금처럼 좋은 사람들과 함께 일할 수 있는 환경은 드물다”라고 하시는 데 공감하지 않을 수 없습니다.  특히 제가 입사한 지 한 달이 채 안 되었을 때 외부적인 요인으로 회사가 힘든 상황에 놓인 적이 있었는데, 각자 할 수 있는 분야에서 최고의 능력을 발휘해서 위기를 넘기는 모습은 짧은 기간에 버즈빌리언들의 뛰어난 개개인의 역량을 느낄 수 있었던 좋은 기회였던 것 같습니다. 업무 외적으로도 좋은 사람들과 일하고 있다는 것을 실감하고 있습니다. 점심시간마다 (낮잠을 포기하고) 탁구를 치거나 게임을 할 때마다 제 부족한 탁구/게임 실력을 걱정해주기도 하고, 실력 향상을 위한 진심 어린!! 조언도 아끼지 않습니다. 6. 개인적인 목표나 꿈이 있으신가요? 있다면, 버즈빌에서의 경험이 어떻게 도움이 된다고 생각하시나요? 한마디로 얘기하자면 최고의 2인자가 되는게 꿈입니다. 다른 사람들 앞에 나서지도 않고 조명도 받지 않지만 “이 사람과 함께라면 어떤일도 다 성공할 수 있어” 라는 생각이 들게끔 만드는 사람이 되는 것..이라고나 할까요.. 어릴때는 막연하게 “다른 사람들을 돕는일을 하고 싶다” 라는 생각을 갖고 살았던것 같아요. 평범한 학창시절을 보내고, 대학에 가고, 취업을 하면서 마음 한켠으로 치워두게된.. 그냥 그정도의 생각이었죠. 처음 다니던 회사를 그만두고 나는 평생 어떤 일을 하면서 살아야할까 라는 원론적인 고민을 하게 되었고, 그때 이 생각을 다시 한번 바라보게 된것같아요. 그러다가 기회가 닿아 MBA에 가게 되고 지금까지 만나보지 못했던 사람들을 만나면서 한때는 막연했던 이 생각을 조금 더 구체화시킬 수 있었습니다.  최고의 2인자가 되는 첫번째 step으로.. 우선 주변에 아이디어만 있고 실행으로 옮기고싶은데 어떻게 할 수 있는지를 몰라서 헤매는 친구들에게 작게나마 도움이 되고 싶습니다. 엔젤 투자자나 인큐베이터보다 조금 더 깊게 사업에 참여하고 실질적인 업무를 도와주며 같이 일하고 문제를 해결하면서 그 친구들의 아이디어를 실현하는데 일조하고 싶어요. 지금 버즈빌에서 지금 하고 있는 일이 이와 크게 다른 것 같지 않습니다. PM으로써 하나의 프로덕트를 기획하고 만들고 운영하는 게 결국은 하나의 작은 사업을 시작하는것이라고 생각합니다. 프로덕트를 만드는 과정에서 필요한 일들을 챙기고 처리하고 또 그 과정에서 고통스러워하고 즐거워하다보면, 아이디어를 구체화 시키면서 필요한 일들을 직/간접적으로 경험할 수 있겠죠. 그렇게 저를 잘 단련시키다보면 결국 제가 이루고자 하는 꿈에 다가갈수 있지 않을까요. *버즈빌의 채용공고(전문연구요원 포함)를 확인하고 싶으면 아래 버튼을 눌러주세요!

기업문화 엿볼 때, 더팀스

로그인

/