스토리 홈

인터뷰

피드

뉴스

조회수 1423

비전문가를 위한 유전자 정보 기반 산업에 대한 이해

비전문가를 위한 유전자 정보 기반 산업에 대한 이해 "유전자 해독( Genome sequencing)'을 아웃소싱하면 핵심기술은 그 회사에 있는 것 아닌가요? 3billion은 그럼 뭘 하는 건가요?"3billion 시작하고, 기자나 VC 분들에게 아주 빈번하게 받는 질문인데, 아마도 전문가 분들이 아니라면 모든 유전 정보 사업에 대해 가질 수 있는 의문일 수 있겠다 싶어 비전문가 분들이면서 유전자 정보 산업에 대해 궁금해 하시는 분들의 시각에서 답변을 정리해 본다.유전자 분석 과정에 대한 이해 사실 위 질문은 유전자 분석 이라 통칭되는 두리 뭉술한 용어에 대한 오해에서 근원하고 있는 것 같다. 보통 '유전자 분석( Genome analysis)' 이라는 용어를 사용할 때는,  여러 단계의 과정을 포괄해 이야기하는 경우가 많은 것 같다.보통 유전자 분석이라는 용어를 쓸 때  크게 '유전자 해독( Genome sequencing or genome decoding)'과 해독된 Data를 분석해 원하는 목적의 정보를 얻어내는 '유전자 정보 해석( interpretation )' 두 가지를 모두 포괄해 사용하는 경우가 많다.'유전자 해독'은 우리 몸 세포 안의 게놈에 있는 30억 쌍의 DNA 염기 서열을 읽어내는 과정을 의미 한다. DNA는 A,G,T,C 4개의 염기로 구성되어 있다. 따라서 유전자 해독은 이 4개의 염기 A,G,T,C 로 구성된 30억개의 DNA 염기 서열을 읽어내어, 30억개로 이루어진 문자 서열을 얻어내는 과정을 의미한다.2003년 완성된 인간게놈프로젝트가 바로 '유전자 해독'을 목적으로 한 연구 프로젝트 였고, 이를 통해 인류 최초로 30억 쌍의 염기 서열을 처음 부터 끝까지 읽어 낼 수 있었다. 당시 이 한벌의 염기 서열을 해독해 내는데 27억 달러 우리 돈 3조원 가까운 비용이 들었다. 최초의 인간 게놈이 완성된 이래로 유전자 해독 기술은 매 1.8년 마다 2배씩 성능이 개선된다는 반도체 무어의 법칙을 가볍게 능가하는 속도로 매우 빠르게 발전되어, 2014년 말 기준 한 사람의 전체 게놈을 해독하는데 $1000, 우리돈 100만원 수준으로 낮아졌다.유전자 해독 기술이 이렇게 빠르게 발전해, 이제 유전자 정보가 필요한 사람이라면 누구든 유전자 해독을 통해 자신의 유전자 정보를 획득할 수 있는 시대가 펼쳐진 것이다.유전자 해독이 가능해졌으니, 이제 게임 끝! 인가? 아니다.유전자 해독은 30억 쌍의 DNA 염기 서열을 해독한 text 정보일 뿐, 이 정보를 활용해 필요로 하는 다양한 용도로 활용하기 위해서는  '유전자 정보 해석' 기술이 필요하다.백만원 유전자 해독, 십억원 유전자 해석 시대게놈 해독 비용이 낮아져 많은 게놈이 생산되면서, 이를 해석하기 위한비용이 그에 비례해 막대하게 증가하고 있는 상황을 보여주는 그래프( 이미지 출처: https://epilepsygenetics.net/2014/06/27/when-will-we-have-the-1000-epilepsy-genome/ )$1000 게놈 시대가 가시화 되면서, '백만원 게놈 해독, 십억원 게놈 해석( $1000 genome, $1,000,000 genome interpretation)' 시대라는 캐치프레이즈가 등장했다. 유전자 해독 기술이 빠르게 발전해 대중화 될 수 있을 정도로 가격이 낮아져 많은 게놈이 생산되면서,  이들 게놈 정보를 활용해 질병 진단 등에 활용하기 위한 '유전자 해석'에 막대한 비용이 필요한 시대가 펼쳐진 것이다.'유전자 해석( Genome interpretation)'은 앞서 설명한 유전자 해독과정을 통해 생산된 DNA 염기서열 문자 정보로 구성된 Raw data를 필요한 용도로 가공하고 해석해 원하는 목적의 정보를 생산해 내는 과정을 포괄한다.이 과정은 컴퓨터를 통한 대규모 연산 과정을 통해 진행된다. 왜 꼭 그래야만 할까?유전자 해독기를 통해 생산된 raw data는 대규모의 이미지 파일들로, 한 사람의 게놈을 읽어내는데 1Tb(테라바이트)의 정보를 생산해 낸다. 이 이미지 파일을 처리해 생산된 한 사람의 DNA 문자는 30억 쌍에 달한다. Text 파일로 이 문자열을 저장하는데만 3Gb ( 기가 바이트)가 필요하다. 현재 유전자 해독 기술의 특성상 같은 부위를 평균 30배로 읽어내는데 이렇게만 해도 90G 여기에 각 DNA 염기를 읽어내는데 필요한 Quality score 등의 정보를 포함하면 270G로 불어난다. 270Gb 의 정보를 컴퓨터의 도움 없이 처리해 낼 수는 없다. 그래서 해독된 유전자 정보를 활용하기 위해서는 컴퓨터를 통한 정보 처리를 통하지 않을 수 없게 되는 것이다.유전자 해석 과정은 여기에서 부터 시작한다. 여기서 부터는 필요한 목적에 따라 다양한 알고리즘과 분석 방법론을 기반으로 각각의 기술들이 발산한다. 암환자의 유전자 정보로 정밀진단을 하기 위해서는 체세포 변이를 찾아내는 알고리즘을 사용하고, 산전유전진단을 위해서는 산모의 유전자에서 태아의 유전자를 구분해 내는 알고리즘을 사용하고, 최근 이슈가 되고 있는 액체생검을 통한 암조기진단을 위해서는 암세포에서 근원한 변이를 찾아내는 알고리즘을 사용한다.우리 3billion은 유전적 원인에 기반한 희귀질환을 검사할 수 있도록 생식세포 변이( Germ-line mutation)을 판별하고, 다양한 질병들의 유전 특성에 기반해 변이들이 질병을 유발할 것인지, 유발한다면 어느 정도 확률로 이런 가능성이 존재하는지 등을 정밀하게 계산해 낼 수 있는 알고리즘을 개발하고, 여기에는 딥러닝을 포함한 다양한 AI 기술들이 폭넓게 활용된다.즉, 목적에 따라 유전자 해석에 필요한 기술들은 서로 다르다. 그래서, 어떤 목적을 가진 제품을 만드느냐에 따라서, 유전자 정보 기반 회사들은 서로 다른 기술을 고도화 시켜나가는 각자의 길을 걷게 된다.3billion은 희귀 질환을 검사하는데 필요한 다양한 data analysis algorithms/methods, UI/UX 들을 개발하는데 역량을 집중하는데, 여기 활용되는 해석 기법들은 Grail 등의 암 조기 진단을 위한 회사나 Natera 등의 산전유전검사 제품을 만들어내는 회사들이 활용하는 해석 기법과 완전히 다르다.이는 마치, '컴퓨터와 프로그래밍 랭귀지'라는 동일한 자원을 활용해 각 software 회사들이 서로 다른 software를 만든 것과 같다고 할 수 있다. Microsoft 는 운영체제 소프트웨어를, 구글은 검색 엔진 소프트웨어를, 오라클이 DB 소프트웨어를 만든 것 처럼.유전자 산업의 '핵심 기술'자, 그러면 유전자 산업의 핵심 기술은 무엇일까?  유전자를 읽어내는 유전자 해독 기술인가? 아니면 읽어낸 유전자 정보를 해석하는 기술인가? 둘 다 핵심 기술이다. 유전자 해독의 핵심 기술은 유전자 해독기를 만들어 내는 회사에 핵심 기술이 있다. 현재 지구상의 유전자 해독기 시장을 독점하고 있는 illumina 가 가장 경쟁력 있는 유전자 해독 핵심 기술을 가진 곳이라 할 수 있겠다. 그 외에 게놈 해독기를 개발, 사업화 하고 있는 Pacific biosciences,  Oxford nanopore 등이 게놈 해독 핵심 기술을 가지고 있는 회사들이라 할 수 있다.유전자 정보 해석의 핵심 기술은 IT 회사들이 그랬듯, 각 목적에 맞는 software를 만드는 곳들에 핵심 기술이 분산되어 있고, 앞으로 이 분산이 가속화될 것으로 예상된다.  구글이 검색에선 야후, 라이코스, AOL 등 초기 경쟁자들을 물리치고 가장 독보적인 경쟁 우위를 점했고, 마이크로소프트가 윈도우를 기반으로 운영체제 소프트웨어를 독점했듯,  유전자 정보를 기반으로 다양한 문제를 해결하는 산업 분야들이 파생이 될 것이고, 각 영역에서 경쟁 우위를 가지는 회사들이 등장할 것이다.여기서 한가지 강조하고 싶은 것은, 어느 한 도메인의 해석 기술이 좋다고 다른 영역의 문제를 푸는데 필요한 해석 기술이 좋다고 할 수 없다는 점이다. 구글의 검색 엔진은 검색에서 의미가 있지, 운영체제를 만드는데 큰 도움이 안 되듯, 유전자 해석 기술도 마찬가지. Myriad 의 유방암 위험도 검사를 위한 소프트웨어가 산전태아유전 검사를 하는데는 큰 의미가 없다. 마찬가지로 Natera의 산전태아유전 검사를 위한 소프트웨어는 Counsyl의 임신전 태아 유전병 위험도 검사에는 큰 도움이 안 된다.물론, 구글이 Facebook처럼 SNS 를 만들고, Microsoft 가 구글처럼 검색엔진 Bing 을 만들 듯, 다른 회사의 제품들을 만들어 낼 수 있다. 소프트웨어라는 영역이 의례 그러하듯. 하지만, 이미 역사를 통해 우리는 잘 알고 있다. 그런 식으로 타회사의 소프트웨어를 따라 만들어 성공한 전례는 손에 꼽는다는 것을.  구글은 수많은 인재들을 영입해 수많은 소프트웨어 서비스를  출시했지만, 본업인 검색과 이메일 외에는 크롬을 제외하고 성공한 서비스라고 할 만한 것이 없다.  마이크로소프트 역시 수많은 소프트웨어를 만들어 냈지만, 운영체제와 오피스 제품군 이외엔 성공한 제품이 없다.구글은 직접 만든 제품이 아니라, Youtube나 안드로이드 등 좋은 소프트웨어를 만든 회사들을 인수해 막대한 매출을 내는 회사로 키운 경우가 더 많은 듯 하다( 물론 talent acquisition 에 그친 경우가 훨씬 더 많긴 하다만). 마이크로소프트는 그 마저도 신통치 않았지만.기본적으로 '인간 게놈 정보'를 바탕으로, 목적에 맞는 '유전자 해석' 소프트웨어를 주력으로 하는 유전자 정보 산업군의 회사들도 IT 기업들과 같은 전례를 따르며 시장이 성장해 나갈 것이라 생각한다.다만 한가지 변수가 더 있는데, 그게 바로 Data.또하나의 핵심 기술, Data 인간 최고수 바둑 기사들을 무참히 짓밟으며 큰 이슈가 된 AI 기술이 엄청난 주목을 받고 있다. 딥러닝 기술의 발전을 등에 엎은 구글 딥마인드의 알파고가 엄청난  '바둑 실력'을 쌓아, 인간 최고수들을 압도했기 때문이다. 헌데 알파고는 어떻게 '바둑 실력'을 습득한 것일까?알파고는 어떻게 인간 최고수 이세돌을 능가하는 바둑 실력을 학습할 수 있었을까?바로, 인간 최고수들의 바둑 대결을 통해 얻어진 '바둑 대결 기보 Data' 를 통해 바둑 기술의 습득이 가능했던 것이다.  딥마인드 팀이 가진 바둑 대결 기보 Data 에 인간 최고수들 간의 대결 Data는 빠져 있었다면 어땠을까? 절대 인간 최고수들을 능가하는 수준의 실력을 쌓지 못했을 것이다.딥러닝을 포함한 AI 에서 알고리즘도 핵심 기술이지만, 그 보다 더 중요한, 경쟁력을 가르는 핵심은 Data라고 할 수 있다.  특정 문제를 풀기위한 양질의 Data를 누가 더 많이 가지고 있느냐에 따라 AI 성능이 결정된다. 'Garbage in, Garbage out' , 아무리 좋은 성능의 AI 알고리즘을 개발했더라도, AI가 학습할 Data의 질과 양이 받쳐주지 못하면, 그 AI의 수준은 절대 경쟁력을 가질 만한 성능을 내지 못한다.'유전 정보 해석 소프트웨어' 기술에서도 같은 상황이 펼쳐지고 있다. 유전 정보 해석 소프트웨어들 자체가 빅데이터에 기반한 다양한 방법론 , AI 알고리즘 기반으로 하고 있기에, 자연히 양질의 Data 를 기반으로한 소프트웨어가 경쟁력 있는 성능을 확보할 수 있다.액체 생검 기반 '암 조기 진단 키트' 개발을 목표로 하는 Grail, Guardant health, Freenome 등의 회사가 왜 수천억~조단위 투자를 받을까? 투자금을 기반으로 회사를 성장시킬 Plan 이 있어야 하는데, 그 돈을 어디에 쓸 목적으로 막대한 투자를 받았을까?바로 Data 확보다. 암 조기 진단 kit를 개발하기 위해 수십만~수백만명의 게놈 Data를 확보해 암 조기 진단 소프트웨어의 성능을 압도적으로 높이기 위해 엄청난 투자를 받은 것이다.  압도적인 숫자의 양질의 데이터를 확보하고 나면, Data가 진입장벽이 되어 시장을 선도할 수 있게 된다.그런데, 돈만 있으면 양질의 데이터를 확보할 수 있을까? 반은 맞고 반은 틀리다고 본다. 결국, 의미 있는 제품을 만들어, 자연스럽게 고객숫자를 늘려갈 수 있는 곳이 가장 의미 있는 속도로 의미 있는 숫자의 Data를 확보할 수 있을거라고 본다.이를 위해서는 결국 고객의 선택을 받아야 하는데, 여기에는 제품의 품질 외에도, 회사가 추구하는 목적과 가치 등 제품 이외의 요소들도 매우 중요하게 작용하지 않을까 예상한다. 돈벌기 위해 어떤 형태의 Data 사용이든 불사하려고 하는 곳은 아무리 많은 자금을 바탕으로 하더라도 결국 유전자 정보 산업의 Data 경쟁에서 패퇴할 것이라 예상한다.  좋은 제품을 합리적인 가격에 제공하면서, 궁극적으로 질병 치료 등 선한 목적을 추구하는 회사가 유전자 정보 산업의 궁극적으로 가장 큰 경쟁력인 Data 확보 경쟁의 승자가 되지 않을까 예상해 본다.요약 많은 이야기를 했는데, 간단히 요약하면, 유전자 정보 산업에는 '유전자 해독'을 핵심 기술로 하는 회사도 있고, '유전자 해석'을 핵심 기술로 하는 회사도 있다. 유전자 해석을 하는 회사들은 각기 목적하는 바에 해당하는 소프트웨어를 고도화해 나가면서 기술이 차별화 되어 나간다. 유전자 해석을 핵심 기술로 하면서 같은 목적( 예, 액체 생검을 통한 암 조기진단)을 가진 회사들은 어느 곳이 양질의 데이터를 더 많이 가지고 있는가로 궁극적으로는 제품의 차별화, 경쟁력의 차별화가 이루어질 것이다.#3billion #운영 #인사이트 #스타트업 #마인드셋 #시장분석
조회수 3485

100%를 위한 궁극의 1% 현대로템 철차시운전팀을 만나다

현대로템 철차시운전팀은 어떤 업무를 할까요?현대로템 철차시운전팀에 비상등이 켜졌습니다. 2019년, 창사 이래 최대 물량의 완성차 시험을 앞두고 있기 때문입니다. 그럼에도 불구하고 철차시운전팀 가득 기분 좋은 설렘이 넘쳐나는 이유는 무엇일까요? 현대로템 철차시운전팀을 직접 만나 에너지의 근원을 파헤쳐 보았습니다.떨어져 있어도 자주 보지 못해도 우리는 한 팀철차시운전팀 탁월한 팀워크로 뭉쳤습니다“관리직과 기술직을 다 합치면 전체 팀원은 80여 명 가까운데 그중 40%가 주재 및 파견 근무를 하고 있습니다. 그러니 팀 전체가 모이는 일이 정말 드물지요. 그래도 저는 크게 걱정 안 해요. 각자 할 일은 알아서들 잘하고 있을 거고, 얼굴은 자주 못 봐도 우리는 한 팀이니까요.”팀 소개를 핑계로 팀 자랑부터 늘어놓는 유동식 팀장의 말에 지난 10월부터 철차시운전팀의 일원이 되었다는 인턴사원 3인방의 얼굴에도 미소가 번집니다. ‘팀 자랑 이벤트’에 응모하며 임종훈 과장이 전한 메시지가 새삼 떠오르는 순간이었습니다.“현대로템 창사 이래 최대 물량 완성차 시험을 앞두고 국내외에서 불철주야 힘쓰고 있습니다. 특히 인턴사원의 신선한 열정과 팀장님의 부드러운 리더십이 팀 전체에 시너지를 불어넣고 있는 철차시운전팀을 전사에 자랑하고 싶습니다”흐뭇한 눈길을 주고받는 철차시운전팀 사람들에게 임종훈 과장의 메시지를 이야기하자, 유동식 팀장은 “뭘 또 그렇게 자랑을 했노, 부끄럽게”라고 응수했고 박영선 사원은 말없이 엄지를 치켜들었습니다. 대체 어떤 일을 하기에 이들의 얼굴 가득 자부심과 애정이 묻어나는 것인지 더욱 궁금해지지 않을 수 없었습니다.멈춰있던 차량에 첫 숨결을 불어넣다열차 운행의 마지막을 담당한다는 책임과 자부심이 있습니다“설계, 생산, 구매 등 각 팀의 업무를 거쳐 완성된 전동차라도 저희가 전원 버튼을 누른 다음에야 비로소 처음으로 움직이게 됩니다. 그때부터 저희는 차량이 잘 만들어졌는지, 문제없이 동작하는지를 확인해야 하고요. 그러려면 차량 전반에 대해 이해하고 있어야 합니다. 뿐만 아니라 이 모든 과정을 거쳐서 완벽한 품질의 차량을 고객에게 인도하는 것도 저희 몫이죠.”철차시운전팀 업무에 대한 임종훈 과장의 똑 부러진 설명대로 99% 완성된 차량이 현대로템의 이름을 달고 출고되기까지 마지막 1%를 채우는 것이 철차시운전팀의 주된 업무입니다. 스스로는 겸손하게 ‘마지막 1%’라고 했지만, 이 1%는 결코 무시할 수 없는 수치입니다. 공장시험만 편성당 30~50일, 첫 편성 열차는 영업선로에서 다시 3개월, 그 다음 열차들은 20일 정도 시험을 거친 후에야 실제 운행에 들어갈 수 있기 때문입니다. 이 과정 중 각 차량 담당자들은 승객의 안전을 위해 추진, 제동, 도어, 신호등 등 차량 전반을 샅샅이 살펴야 합니다. 특히 영업선로에서 시험을 하는 경우에는 자정 이후부터 새벽까지 업무가 이루어지는 경우가 많습니다. 낮과 밤이 바뀌는 격이니 육체적으로 힘들 수밖에 없는 것이 사실. 유동식 팀장은 이 부분에 대해 특히 애틋한 마음을 전했습니다.“지금 이 시간에도 우리 팀원들이 국내외에서 시운전을 진행하고 있습니다. 2018년의 경우 소사원시, 부산1호선, 터키 예니카프, 브라질 CPTM 등의 성공적인 납품을 이뤄냈죠. 2019년에는 창사 이래 최대 물량을 시험해야 하는 상황이고요. 그래도 걱정보다는 기대가 크다고들 하니 고맙죠. 팀장으로서 저는 이 사람들이 더 신나게 일할 수 있도록 인력 배치 등을 철저히 준비하고 있고요.”공통의 목표를 향해 달리다서로 같은 목표를 향해 달리며 팀 안팎의 시너지를 높이고 있습니다2019년 철차시운전팀은 월평균 140칸, 연간 1천 칸이 넘는 차량을 공장 및 본선에서 시험해 내야 합니다. ‘창사 이래 최대 물량’이니 25년간 현대로템에서 재직하며 23년을 철차시운전팀에서 일해온 유동식 팀장도 새로운 도전을 앞두고 있는 셈입니다.“마지막 공정을 책임져야 하는 만큼, 부담감과 책임감을 느낄 수밖에 없죠. 그래도 시운전 완료 후 고객사에 차량이 인도될 때, 영업 개시 후 승객들이 차량을 이용하는 모습을 볼 때 느끼는 성취감은 말로 표현할 수 없어요.”이렇듯 막중한 임무를 맡고 있는 철차시운전팀을 지켜온 에너지는 무엇일까요? 국내파트 오경석 과장은 ‘공통의 목표’를 첫손에 꼽습니다. 기술직부터 관리직까지 80여 명 가까운 팀원 모두가 ‘납품 기한 내에 고품질의 차량을 고객에게 인도하는 것’이라는 목표를 중심으로 일사불란하게 움직이고 있습니다. 덧붙여 박영선 사원은 남다른 ‘팀워크’를 자랑했습니다.“철차시운전팀은 파트를 막론하고 언제든 서로 도울 준비가 되어 있습니다. 관리직과 기술직으로 직무가 나뉘어 있지만, 주기적으로 풋살 경기를 하고 저녁 식사도 하며 뒤섞여 어울리지요. 팀내에서 탄탄하게 쌓아온 협업 경험은 유관 팀들과 일할 때도 그대로 적용이 되지요.”시운전 중인 차량에 문제가 생길 경우, 일정 조율부터 자재관리까지 각각의 팀들과 소통하며 해결해나가야 하는 것이 철차시운전팀의 숙명이라면 숙명. 그때마다 직무와 파트를 초월해 팀 내 선후배들과 쌓은 협업 경험이 결정적인 역할을 한다는 것입니다.2019년, 무적의 팀워크로 달린다현대로템 철차시운전팀은 올해도 열심히 달립니다인터뷰 후 단체 사진 촬영을 위해 시험 중인 차량 앞으로 모인 철차시운전팀 사이에 유쾌한 옥신각신이 벌어집니다. “무슨 사진이고, 나는 안 찍을란다”하며 슬쩍 빼는 ‘경상도 아재들’과 “아, 선배님. 선배님이 안 찍으시면 어떡합니까. 같이 찍으셔야죠”라며 다정하게 팔을 잡아끄는 후배들 사이의 밀당(?) 끝에 열여섯 명이 겨우 카메라 앞에 섰습니다. 포즈를 잡는 것도, 활짝 웃어야 하는 것도 어색하고 낯설지만 “하나, 둘, 셋!” 구호에 맞춰 같은 곳에 시선을 모은 철차시운전팀 사람들. 앞으로도 이렇게, 철차시운전팀 사람들은 시선을 맞추고 호흡을 맞추며 현대로템의 경쟁력을 높여갈 것입니다.글. 최주연사진. 방문수 ▶ 현대로템 사보 2018년 겨울호에서 원문을 확인할 수 있습니다#현대 #현대그룹 #현대로템 #철차시운전팀 #열차 #전동차 #시운전 #팀워크 #직무소개 #HMG저널 #HMG_Journal #HMG #기업문화 #조직문화 #직무정보 #구성원인터뷰
조회수 886

브랜딩을 다시 생각해보자: 개념원리 브랜딩

브랜딩에 관련된 수많은 얘기가 넘쳐나는 요즘입니다.이미 원론적인 내용은 다양한 전문가님들의 고견들을 통해 섭렵하셨으리라는 전제 아래, 조금 다른 얘기를 해보려고합니다. 전략이나 방향성 등등 브랜딩은 그 자체가 추상적인 개념이기에 원론적인 내용을 빙빙 돌 위험성이높습니다. 하지만 변하지 않는 절대명제는 어떤 방향성이든, 무슨 전략과 계획을 수립하던 누군가는 ‘일’을 해야 한다는 것입니다. 그리고그 시작은 항상 본질에서 비롯되지만, 폭망은 디테일에서 비롯된다는 점이죠.   500만원을 들여서 브랜딩 컨설팅을 몇 개월 내내 받았습니다. 비즈니스모델도 손보고, 마케팅 전략도 일체화 시키고, 막 로고와 슬로건도 재정비하고, 퍼포먼스 브랜딩 전략도 기똥찬 아이디어로 구축했습니다. 막 잘될 것 같아서 만세를 외치고 있는데 정작 폭망의 이유는 단순한 것들에서 비롯됩니다. 엉기 성기 대충 붙인 주소 라벨이나, 전날 술 먹고 퀭한 얼굴로 불친절한 점원의 삐딱한 짝다리 등이 그것이죠.  실무단의 브랜딩은 전문가들의 브랜딩과는 조금 결이 다릅니다. 그들에겐 일이고, 노력이고, 몸을 움직여야 하는 것들입니다. 그래서 저는 이러한 이야기들을 다뤄보고자 합니다. 바로 실무자들의 브랜딩에 대해서 말입니다.  이 글은 브랜딩의 성공을 위한 글이 아닙니다.오히려 대폭망을 예방하는 차원의 디테일한 이야기들에 가까울 것 같습니다. 물론, 그 전에 이번 시간을 통해 브랜딩의 기본적인 개념은 한 번 정리를 해야 할 것 같습니다. 브랜딩이 어떤 식으로 작용하는지 공급자와 소비자 입장에서 나누어 생각해보죠.먼저공급자 입장에서의 브랜딩입니다. 브랜딩. 각인시킨다는 뜻이죠. 알린다는 의미와는 조금 다릅니다. 그 어원도 다르고, 단어자체의 뜻도 다르죠. 물론 어원이 기능을 상징하는 것은 아니지만, 꽤나 의미가 있는 것이니 짚고 넘어갈 필요는 있다고 생각합니다.  일단 ‘알린다.’는 뭔가 정보를 주는 느낌입니다. 이를테면 이런 문장들입니다.우리건 놀라운 기능이 있어. 우리건 화소수가 5천만이야. 우리건 유기농이야. 우리는 사회취약계층에게 일자리를 제공해. 우린 자신을 찾는 교육을 해.이처럼 무엇인가를 ‘가지고 있다.’ 또는 ‘한다’ 등의 정보를 제공합니다. 그러니까 이것은 행위의 문제죠. 이렇게 얘기를 해보겠습니다. 저는 브랜드에 관련된 기획과 디자인을 합니다. 그렇죠. 이것들은 제가 하는 겁니다. 그럼 이것을 하는 저는 어떤 사람일까요? 까칠한사람? 생각 많은 사람? 잘생긴 사람? 네, 모두 맞을 겁니다. (함정이숨어있어!!! -0-!)  제가 하는 일을 알리고 명함을 드리고 제안서를 던지는 것은 ‘정보를 주는 행위’ 입니다. 문제는 누가 이 행위를 하고 있느냐 하는 것이죠. 브랜딩은 이 질문에 대한 해답을 내야 합니다. 당신이 무엇을 하는 지가 아니라, 그러니까 너흰 누군데?  당신이 무엇을 하는 지가 아니라, 그러니까 너흰 누군데?  각인이란 표현을 썼습니다. 각인. 새긴다는 뜻이죠. 원래는 가축이나,벽돌, 목판, 또는 살에다가 새겼던 것입니다. 불로 지져서. 아프게. 물론 꼭 노예와 전쟁포로를 구별하기 위함만은 아니었습니다. 목조건물과 선박이 많았던 옛날옛적에는 인두로 까맣게 태워서 고유의 문장을 만들곤 했으니까요. 나무나 벽돌, 가축에 불로 각인시키는 것도 Brand의 행위 중 하나였죠. 이것은 현재의 브랜딩 개념과는 조금 다른 단순한 식별과 책임소재, 품질에 대한 보증을 나타나는 일종의 표시행위와 같다고 할 수 있어요. 이 행위는 이미 기원전 수 천년 전, 인류문명의 발단과 함께 발생하기 시작했습니다. 이 후 산업의 발전과 다양한 경제체제의 발달, 문화와 종교의 발전과 기업와 온라인매체의 등장으로 그 정의가 다양하게 바뀌긴 했습니다만, 브랜딩이 가진 고유의 가치는 변치 않고 항상 내포되어 있습니다. '표기'의 기능이죠. 그럼 여기서 질문. 그럼 브랜딩은 단순히 로고 만드는 거예요? 불로 새겨서 간판 만들듯이? 아닙니다. 그런 얘기를 할 거면 글을쓰지 않았을 겁니다. 아마 간판을 만든 이유는 이랬을 겁니다.  13세기중반 무렵 옆 집 말발굽(편자) 장인이 어느 순간 무쇠로만든 것을 만들어서 팔기 시작했을 겁니다. 13세기 이전에는 청동등을 이용해서 편자를 만들었는데, 녹이 쉽게 슬고 성형이 어려워서 무쇠로 만든 편자가 유행하기 시작했죠. 그래서나도 질 수 없으니 무쇠편자를 만들기 시작했습니다. 그런데 녀석이 간판에 ‘원조 말발굽’ 이라고 써 붙이고 자기 이름도 막 써 붙인 겁니다. 녀석이 원조긴 하지만, 그렇다고 내 손님들을 다 뺏길 순 없으니, 뭔가 다른 점을 어필하고 싶었을 겁니다. 고민하던 편자집 사장은간판에 이렇게 써 붙입니다. ‘말사랑 편잣집’. 그리곤 5살때부터 말을 타고 놀았던 프로교감러의 특기를 살려 ‘내 말이 말같지 않을 때.’ 라는 캐치프라이즈를 써 붙이곤 말의 종합검진 서비스까지 함께 진행하기 시작했습니다. 내 말이 소중해서 매일 쓰다듬어주던 마주들은 종합검진 서비스까지 받으면서 나에겐 무뚝뚝하지만 내 말에겐 자상한이 츤데레 영감에게 빠져들기 시작했을 겁니다.  그리고 사람들은 저 말사랑 편잣집을 애용하기 시작했습니다. 그리고 소문을 냈겠죠. 거기 어디야? 라고 사촌 에넬슨도 물어보고, 사돈의 팔촌인 에릭도 물어 봤을 겁니다. 설명을 해줘야 하는데 ‘거기 시장골목 안쪽으로 들어가서 45걸음을 걸어간 후 옆에 과일가게 맞은편 골목 안쪽 어쩌고……’라며 주구장창 말할 순 없었겠죠. 뭔가 신호가 필요했던 것입니다. 이 때 이미지나 이름이 있다면 쉽게 말할 수 있었을 겁니다. ‘하트안에 말 그려진 곳으로 가’‘말사랑말발굽이라고 써진 곳을 찾아봐’ 라고 말이죠. 간판과 로고, 심볼의 존재 목적은 이것입니다. 무엇인가를 인식하고 식별하는 역할이죠. 우리가 좀 착각하고 있는 것은 로고가 겁나 예쁘면 우리가 브랜딩 된다는 생각들입니다. 비주얼 브랜딩의 측면에서 비주얼은 당신이 이미 하고 있는 행동을 상징화시키는 도구일 뿐입니다. 그 예쁜 이미지가 당신의 행위나 가치관을 상징하지 못한다면 공허해지는 것이죠. 물론 위는 가상의 예지만, 중요한점을 시사하고 있습니다. 행위가 먼저이고, 인식은 그 후입니다. 각인은 그 인식의 반복 또는 섬광기억을 통해 형성되는 것 이고요. 이게공급자 입장에서의 브랜딩입니다. 행위가 먼저이고, 인식은 그 후입니다.소비자입장에서의 브랜딩은 오히려 조금 다른 개념입니다.  공급자 입장에선 인식을 시켜야 하고 그걸 반복시켜야 하는데, 소비자에게 그걸 직접 어필할 순 없습니다. 길가가던 사람에게 로고를 들이밀고 외워주세요!! 라고 외치지 않는 이상 말이죠.  소비자 관점에서 브랜딩이란 자신도 모르는 사이에 서서히 무의식에 쌓여가는 느낌이 더 강합니다. 게다가 우리는 의식적인 기억에 대한 허무함을 잘 알고 있습니다. 기억해야지!! 라고 다짐한 것들을 내일이면 죄다 까먹는다는 사실을 지난 12년+대학생활의 중간/기말 고사를 통해서 충분히 깨달았을 테니까요.  대신 어디 빵집의 딸기 케익이 겁나 맛있었다는 사실은 아주 오래도록 기억할 수 있죠. 그렇다고 딸기 케익이 생존과 연관이 있는 것은 아닙니다. 물론 뇌 입장에선 딸기 케이크야 말로 내 삶의 원동력이라고 인식했을순 있겠습니다만, 기존의 생존용 기억의 우선저장 메커니즘과는 조금 결을 달리합니다. 물론 인간은 옛 본성을 대부분 간직하고 있기에, 생존에 필요한 정보를 우선적으로 저장합니다. 하지만, 이제 인간은 길가다가 사자에게 물려 죽을 걱정은 하지 않아도 되는 세상에 살고 있습니다.  대신 정보들이 겁나 많으니 그것을 취사선택 해야 할 필요가 생겼습니다. 버려야 하는 것들을 일일이 검증해서 골라내는 것은 뇌 입장에선 귀찮은 일이죠. 인간의 기억메커니즘은 ‘선호도나 긍정적인 경험’을 우선적으로 받아들이고 나머지는 ‘모르니까 안 해’ 카테고리에 던져버리기 시작했습니다. 물론 짱 싫은 것들은 따로 분류를 해놓았겠죠. 그것은 짱 싫으니까요. 이를테면 저에게 브로콜리 같은 것 말입니다. 그러니 이러한 관점에서 보자면 소비자에게 브랜딩이란 ‘자신이 경험한 것’ 그 자체입니다. ‘경험을 사고 판다.’ 라는 것이 마케팅이나 브랜딩의 기본 명제가 된 것도 무리가 아닙니다.  하지만 이 ‘긍정적인 경험’ 이란 것의 기준은 무엇일까요. 물론 대다수에게 행복한 경험들이 존재합니다. 사랑이나, 이타심, 따뜻한 것(마음이 아니고, 진짜 그냥 따뜻한 것), 맛있는 것, 고양이와 강아지 등이 그것이죠. 대부분 이러한 것들은 인간의 본능에 접점을 두고 있는 것 들입니다.  그러나 대부분의 인간은 ‘취향’을 지니고 있고, 이 때문에 수많은 변수와 갈래들이 생겨나기 시작합니다. 허나 70억 인구가 모두 다른 취향을 지니고 있느냐 하면 또 그것은 아닙니다. 물론 사소한 취향까지 경우의 수를 따져보면 모두 다를 순 있겠지만 대부분 ‘나만의 취향’ 이라고 생각하는 것들은 이미 함께 하는 다른 사람들이 존재하기 마련입니다. 코딱지를 파서 책상 밑에 붙이던 것도 나만 하는 줄 알았겠지만, 이미 이 자리를 지나간 선배님들의 역사 속 분비물들을 손 끝으로 느꼈을 때의 소름처럼 말이죠. 원피스나 나루토도 명확하게 그 파가 갈립니다. ‘순대에 된장을 찍냐 초장을 찍냐’도 그렇죠. 자박한 된장찌개나, 시원한 된장찌개도 그 취향을 공유하는 사람들이 꽤나 있습니다. 꽃 향을 좋아하는 사람과 시원한 향을 좋아하는 사람도 나뉘죠. 이처럼 취향이란 것은 개인의 것이기도 하지만, 카테고리화가 가능한 수준의 것 들입니다. 덕분에 소비자심리학에선 소비자들의 행동패턴과 취향을 분류하여 데이터화 시킬 수 있었죠. 이러한 혼돈 속의 질서, 그러니까 ‘심리적프랙탈’ 덕분에 인간은 공감대를 나누고 사회라는 것을 이루며 살아갈 수 있게 되었습니다.  문제는 대부분의 기업들이 인간이 지닌 이 취향과 경험을 혼돈한다는 것입니다. 취향은 말그대로 취향일 뿐입니다. 그러나 경험이란 것은 좀 더 다양한 요소의 결합이죠. 소비자들은 대부분의 객관적 정보에 대해선 호/불호를 가를 수 있습니다. 그러나 그저 노출되는 것에 대해선 그 경계를 구분 지을 수 없어서 애매한 정보로 남겨놓기 마련이죠. 그리고그것을 호기심과 경계의 시선으로 바라봅니다. 처음엔 경험도 꺼리게 되죠. 하지만 그 경험에서 어떠한 좋은 요소를 발견했다면, 얼른 ‘좋은 쪽’으로 남겨놓으려고 합니다. 뇌 입장에선 불투명한 것보다 섣부른 판단이 더 합리적이고 편하거든요. 무엇이 좋은 경험을제공하는 요소일지는 취향과는 별개로 굉장히 다양한 디테일들이 결정합니다. 취향은 그 시발점을 제공하지만, 결과물을 보장하진 않습니다. 예를 하나 들어볼까요.뇌 입장에선 불투명한 것보다 섣부른 판단이 더 합리적이고 편하거든요.  원피스 카페가 오픈했습니다. 원피스 팬들은 막 원피스 레어 피규어와 메리 호 인테리어를 보는 것만으로도 취향 저격당해서 심장을 움켜쥐었을 것입니다. 그러나 그것만이 경험의 모든 것이었을까요? 아닙니다. 입구에서 카페주문, 음식의맛, 애기들이 얼마나 뛰어다니고 시끄러운지, 좁은 공간과, 화장실의 청결도 등…… 다양한 행위들의 합을 통해서 경험의 총평을 내립니다. 물론 취향저격이란 것은 어느 정도의 마이너스요소를 방어해주는 +5방어력의 쉴드 아이템 이지만 무적은 아니죠. 그 마이너스점수가 인내심을 초과하는 순간, 소비자는 그곳을 ‘싫어!’ 로 분류합니다. 재미있는 것은 그곳이 싫다고 해서 원피스가 싫어지는 것은 아니란 점입니다. 이것이 취향과 경험의 차이입니다. 소비자 입장에서의 브랜딩이란 것은 내 취향을 저격하는 것이 아니라, 내 시간과 비용을 만족으로 채울 수 있는 경험을 의미합니다. 정리해 보자면 공급자는 행위를 하는 것이고, 소비자는 그 행위를 통해 만족스런 경험을 얻는 것입니다. 여기서의 경험이란 내가 직접 하는 행동 이외에도 앱 하단에 미친 듯 떠오르는 광고창에 x가 눌러지지 않아서 막 광고링크로 넘어가 버리거나, 카페에 와이파이가 잘 안 잡혀서 곤혹스러웠다든가, 불량상품의 교환이 1달씩걸린다든가, 고객센터 상담원님이 한숨 쉬면서 상담할 때 등의 다양한 간접/환경적 경험도 포함합니다.  경험을 제공해야하는 공급자 입장에선 당연히 세세한 부분의 매뉴얼이나 기획, 운영, 제작 측면에 대한 고민이있을 수 밖에 없죠. 지금까지는 브랜딩의 나름대로의 정의와 역할에 대해 알아보았습니다. 다음 글에선 이러한 브랜딩이 실무단으로 넘어갈 때 어떤 일들이 발생하는 지 살펴보도록 하겠습니다.
조회수 792

아래로부터 스타트, 업(up)

저는 스타트업 경험이 없어서 그런지 핀다에 들어온 일주일 간은 서로서로 oo 님이라고 부른다는 것에 익숙하지 않았습니다. 대표님이 인턴을 부를 때에도, 인턴이 그 누구를 부를 때에도 직책이 아닌 oo 님이라고 지칭하며 존대를 하는 문화는 갓 제대한 군인에게는 오히려 부담스럽게 느껴졌습니다. 익숙하지 않았기에 더욱 의문이 들었습니다. 때론 상명하복 방식이 효율적이라고 생각하지만 스타트업에서는 왜 서로의 의견을 존중해야 하며 그렇기에 존대를 하는 것일까라는 의문이 들었습니다. 핀다에서 약 두 달간 일하며 드디어 이 물음에 대한 답을 찾은 것 같아 여러분과 공유하고 싶습니다.   #1. 스타트업에서 일하는 것은 힘듭니다. 회사의 Front 와 Back을 넘나들며 경영, 인사, 재무, 기획 때로는 개발까지 넘나들며 모든 일을  처리해야 하기 때문일까요. 불확실한 미래, 많은 업무량, 현재로서는 만족할 수 없는 월급 등 생각 없이 무작정 덤벼들기에는 포기해야 할 것들이 많습니다. 하지만 창업이라는 힘든 길을 찾아서 걷는 사람들에게는 한 가지 공통된 이유가 있습니다. 바로 회사가 이 세상에서 이루고자 하는 비전, 그 꿈을 믿고 실현시키기 위하여, 현실에서 그 꿈을 이루어 세상에 조금이라도 보탬이 되고자, 많은 것들을 포기하고도 밤을 설쳐가며 일을 하고 있습니다.   Finda, For your better Financial Decision. 당신의 더 나은 금융 선택을 위하여, 핀다.IMF 외환위기, 2009년 금융위기를 겪으며, 또 그 사태에 대해 공부하면서 대한민국의 금융 기반 시설 자체가 선진국들에 비해 매우 부족하다는 생각이 들었습니다. 저는 투자은행, 국제 금융기관을 거쳐 경험을 쌓은 뒤 금융법까지 공부하여 대한민국의 금융 시스템 발전에 조그마한 도움을 주겠다는 꿈을 가지고 있습니다. 어떻게 보면 그 큰 꿈의 목표 중 하나가 개개인의 금융에 대한 접근성, 이해성을 높이는 것이라고 생각했습니다. 개인의 금융 선택을 편리하게 만들어 사람들의 삶의 질을 높이겠다는 핀다의 비전, 저는 핀다의 그러한 클래시한 비전에 동참하고자 핀다와 함께하게 되었습니다.스타트업에서의 꿈과 현실, 그리고 고민모든 것들이 계획한 대로, 처음 꿈꾸던 대로 이루어진다면 얼마나 좋을까요. 역시나 꿈을 이뤄나가는 데에 있어서 항상 이상적으로 모든 것이 해결되는 것은 아니었습니다.  “Of course your opinion MATTERS.”#2. 비전 차원의 고민저는 두 달간  회사의 재무 계획 (financial projection)을 세우는 일을 담당하였습니다. 회사의 사업과 비용을 사용하는 모든 활동에 대한 분석을 하다 보니 의문이 들었습니다. 우리가 하려고 하는 사업과 우리가 사용하는 모든 돈은 과연 우리의 비전을 위한 것인가. 제휴사들을 위한 페이지를 만든다는 것은 과연 누구를 위한 일인 것일까. 페이스북에서 광고를 하는 것이, 타 검색 사이트에서 핀다를 노출시켜 방문자 수를 높이는 것은 얼마나 의미가 있는 것인지, 사업 초기 단계에 어떻게 해서라도 제휴사, 방문자를 늘리는 것도 중요하지만 이것이 과연 고객을 위한 일 일지, 이렇게 하는 것이 비전에서 멀어지는 것이 아닌지 걱정도 들더군요. 고객의 입장에서 항상 고민하는 핀다(Finda)새로운 사업영역을 확장하는 것에서부터, 제휴사와 협약을 맺을 때, 사이트상 노출 우선순위를 정할 때,  또는 이메일을 작성하는 가장 기본적인 회사의 업무 프로세스까지 핀다는 항상 핀다의 가치관에 따른 결정을 내리고자 노력합니다. 모든 결정과 판단이 비전과 직접적 연관이 있는 것은 아니지만 항상 비전 차원의 논의를 한다는 것이 핀다의 멋이 아닐까요. 저 또한 비록 인턴으로 일하였지만 제가 처리하는 모든 업무에 저의 결정권, 그리고 재량을 가지고 핀다의 비전을 업무 곳곳에 녹여낼 수 있었습니다. 그렇기에 인턴사원의 발언, 업무 하나하나 존중받는 아래로부터 시작하는 핀다의 문화는 매력적이었습니다. 바로 그런 점에서 핀다는 비전을 함께하는 자, 구성원 모두를 존중하기에 서로를 존대하는 것이라고 생각합니다. 때로는 그 과정이 비생산적일 수도, 결과적으로 더 돌아가는 길이기에 느릴 수도 있지만 단순히 설립자의 비전을 위해 직원들이 지시사항을 따르는 회사가 되는 것을 핀다는 거부 합니다. 꿈과 현실 사이... 금년의 투자유치 계획을 달성하는 것, 제휴사를 늘리는 것, 회사의 기업가치를 높이는 것 모두 중요한 일입니다. 하지만 그보다 중요한 것은 대표부터 인턴까지 회사의 일원 모두가 하나의 꿈을 이루기 위하여 나아가고 있다는 것, 그리고 그것을 항상 고민하며 반성하고 있다는 사실이 핀다의 클래시함입니다.  금융 선택의 길잡이가 되겠다는 장기적 계획을 가지고 꾸준히 노력한다면 언젠가 핀다를 알아주는 분들이 생길 것이라는 확신이 있습니다. 조급하지 않게 언젠가는 올 기회를 기다리며 오늘의 화려함보다는 기업의 진실됨으로 평가받고 싶고, 그렇기에 고객 중심의 서비스를 만들기 위하여 항상 노력하고 있습니다. 진정한 기업의 가치란 이러한 비전에서 나오는 것이라 믿습니다. #3. 핀다는 항상 미래를 고민하고 있습니다.한 번은 신규 사업/카테고리 론칭을 위해 900개 정도 웹상에 흩어져있는 데이터를 엑셀 파일로 한 곳에 모으는 작업을 수기로 진행한 적이 있었습니다. 단순 반복적인 작업에 심신이 피폐해져, 핀다에 조인하게 된 이유 + 열정적으로 회사의 비전에 동참하겠다는 초기의 모습과는 멀어져 있는 제 자신을 발견하게 되더군요. 이때마다 동료들의 도움이 컸습니다. 멋진 비전으로 뭉쳐져 있고 열정에 차있는 동료들을 볼 때면 제 자신을 반성하며 초심에 대해 생각하고, 회사의 거시적 방향성에 대해 다시 한번 생각을 해보게 되는 시간이 되었습니다.  때로는 일에 치여, 때로는 현실과의 타협 속에 초심을 잊어버릴 때면 핀다의 일원 모두는 항상 목표를 다시 가다듬으며 더욱 큰 꿈을 위해, 클래시 한 기업이 되기 위하여 노력하고 있고 그러한 진정한 기업가치를 높이기 위하여 노력하고 있습니다.비록 저는 이제 다시 학업을 마치기 위해 타국에 홀로 돌아와 있지만 핀다의 멋진 비전에, 열정적인 사람들과 잠시나마 함께했다는 추억은 잊지 못할 것 같습니다. 기업으로서의 격, 클래시함을 추구하는 핀다, 항상 옆에서 지켜보며 응원하겠습니다.You Stay Classy, Finda!RA from Finda최민 드림.   #핀다 #스타트업일상 #RA #기업문화 #조직문화
조회수 1281

꼰대가 되지 않는 법

 요즘에 "아재"라는 말이 유행하면서 많은 20대 후반 ~ 30대 청년들이 고생을 하고 있어요. 저를 포함해서 말이죠. 좌중의 분위기를 냉각시키는 엄청나게 썰렁한 농담(특히 언어유희에 관련된 농담)을 아재 개그라고 칭하기도 하죠. 여기서 아재라는 말이 아저씨의 줄임말인 것은 모두들 알고 있을 거예요. 우리나라에서 아저씨 = 나이가 좀 먹은 사람을 뜻하고 나이를 먹은 사람은 대체로 보수적이고 모험을 싫어하며 불금에 약속이 점점 없어지고 주말엔 집에서 배를 긁으며 예능 방송이나 미드 등을 시청하는 그런 사람으로 생각하기 마련입니다. 안 그런 아재들도 많지만, 대부분의 아재들은 이렇게 틀에 박힌 생활을 하다 보면 역시 생각이 굳기 마련이죠. 자신의 생활양식에 익숙해지게 되고 어느 순간 자신의 생각과 다른 사고방식을 마주치는 순간 그것을 자신의 잣대에 비추어 바꾸려고 하거나 외면하게 되는 이른바 "꼰대 기질"을 보이는 사람을 "꼰대"라고 우리는 지칭합니다.(위에서 아재 얘기를 꺼냈다고 해서 아재 = 꼰대라는 말은 절대 아닙니다) 여기서 우리가 한 가지 인정해야 할 사실은, 바로 나이가 먹을수록 꼰대가 될 가능성이 높다라는 것입니다. 경험이 많아지면서 새로운 것을 맞닥뜨릴 기회가 사라지는 것은 당연하고 살아가면서 부딪히는 모든 케이스에 자신의 과거 경험을 비추어 익숙한 방식대로 일을 해치우기 때문이지요. 어떻게 보면 한 살 한 살 나이를 먹을수록 자신의 인생의 해답지를 하나씩 작성하여 완성하는 것이라고 볼 수 있겠습니다. 해답지만 보면 어떤 케이스의 문제든지 바로바로 풀 수 있겠지요. 이 글을 클릭하여 읽고 있는 여러분은 꼰대가 아니거나, 아주 약한 꼰대 기질을 가졌을 가능성이 높습니다. 보통의 꼰대들은 자신이 꼰대라고 생각하지 않기 때문에 이 글을 절대 클릭하지 않죠. 그러므로 꼰대가 되지 않으려고 노력하는 당신은 정말 대단한 사람임에 틀림없어요. 제가 항상 우리 팀원들에게 얘기하는 문장이 있습니다.더 좋은 방법이 있다면 주저하지 않고 적용해보자이것이 바로 꼰대가 되지 않을 수 있는 첫 번째 방법이자 마지막 방법입니다. 더 풀어서 설명하자면 모든 일에 있어서 나뿐만 아니라 모두가 좋을 수 있는 방법을 찾기를 항상 노력하며, 방법을 찾았을 때 시행착오를 두려워하지 않고 바꾸기를 주저하지 않는 것입니다. 제가 감명 깊게 읽은 책중에 하나인 "미움받을 용기"에서 "생활양식"이라는 단어가 등장합니다. 사람은 각자의 생활양식이 있고 그 생활양식을 바꾸면 나 자신이 바뀔 수 있고, 생활양식을 바꾸는 것은 손을 뒤집는 것만큼이나 굉장히 쉽지만, 용기가 부족하여 우리는 생활양식을 바꾸기를 거부한다라는 내용인데요. 제가 정말 좋아하는 내용입니다. 우리는 지금까지 살면서 익숙해졌던 생활양식대로 살아가고 있습니다. 그래서 어떤 상황에서 어떻게 대처해야 하는지 경험으로 잘 알고 있지요. 그러나 생활양식을 바꾸게 되면 어떤 상황에서 어떻게 대응해야 하는지에 대한 데이터베이스가 부족하여 버벅거리기 마련입니다. 우리는 그 시행착오를 두려워하여 바뀌지 못하고 항상 내 방식만을 고집하게 되는데요, 그럴 때 바로 꼰대 기질이 발휘되는 것입니다. 이는 꼰대가 되지 않는 가장 좋은 방법일 뿐만 아니라 나와 내 주변 사람의 발전을 함께 도모하는 길이기도 합니다. 부수적으로 도움이 되는 이야기를 몇 가지 해드리자면,정당한 이유를 댈 수 없다면 다음 기회를 준비하라 이것은 예를 한번 들어봅시다. 여러분에게 사춘기, 질풍노도의 시기를 겪고 있는 자녀가 있다고 가정해봅시다. 아이가 이야기합니다. "한 달에 용돈 100만 원을 줘"라고요. 여기서 왜 여러분의 자녀가 100만 원을 받을 수 없는지 합리적인 이유를 들어 얘기하기는 쉬울 것입니다. 만약 합리적인 이유를 댈 수 없다면 솔직해지는 것도 방법이겠지요, "얘야, 우리 집엔 그만큼의 여유가 없단다". 여기서 조심해야 할 점은 바로"무슨 학생이 100만 원이 필요해. (쓸데없는 소리하지 말고 가서 공부나 해)"라고 말하는 것입니다.(만약 그래 왔다면 여러분의 자녀에게 미안하다고 사과하기를 권장합니다) 무슨 학생이 100만 원이 필요해라는 문장 속에는 이런 뜻을 내포하고 있습니다.나도 옛날에 학생 이어 봐서 아는데 따내는 절대 100만 원이 필요한 일이 없었어. 애들이랑 떡볶이나 사 먹으려면 5만 원이면 될 것 같은데. 무슨 소리하는 거야. 이제 좀 명확해지나요? 자녀가 100만 원이 필요하다는 얘기를 꺼냈을 때는 우선 이유부터 물어보도록 노력해야 합니다. 가장 좋은 방법은 한 달간에 예산을 직접 짜서 아이에게 있어 그만큼의 돈을 받아야 하는 정당한 이유를 만들도록 하는 것이지요. 100만 원이 필요한 아이는 그나마 핸들링하기 쉬운 편입니다. 만약에 노란색으로 염색하고 싶다는 아이, 주말에 3시간 정도 이성친구와 시간을 가지고 싶다는 아이와 같은 어려운 과제가 남아있습니다. 여러분의 자녀가 100만 원은 필요 없고 노란색으로 염색하고 싶다는 얘기를 했다고 칩시다. 어떻게 반응할 건가요? 이제는 무슨 학생이 염색이야라며 이야기하지 않으시겠죠? 또 생각해볼까요? 9시에 출근해서 5시에 퇴근하는 9 to 5로 출근하고 싶다는 당신의 부하직원, 어떻게 설득하실 건가요? 합리적이고 정당한 이유를 댈 수 없다면 한 발짝 물러서서 다시 한번 생각해보는 것이 좋습니다. 겉으로 보기에는 절대 용납할 수 없는 일이지만 또 다른 이점이 있을 수도 있으니까요. 예를 들어 염색하겠다는 아이의 경우 가까운 친구들과 유행을 즐기며 교우관계가 좋아질 수 있으며, 이성친구와 주말에 시간을 보내고 싶다는 아이는 그 이성친구 때문에 더 좋은 성적을 낼 수도 있을 것이며, 9 to 5를 주장하는 당신의 부하직원은 유치원에서 아이를 데리고 집에 돌아와 200%의 미친듯한 퍼포먼스로 일에 집중할 수도 있으니까요. 다시 말해 서로가 합리적이고 정당한 이유를 가지고 행동한다는 것은 서로를 동등한 존재로 인정한다는 것이며 그것은 바로 서로가 꼰대에서 벗어날 수 있는 가장 좋은 방법입니다. 이 예를 보면 감정적인 사람보다는 합리적인 사람이 더 꼰대가 되지 않을 가능성이 높은 것 같군요.(꼭 그렇다는 것만은 아니지만요) 저도 꼰대가 되지 않으려고 오늘도 미친 듯이 발버둥 치고 있답니다. 여러분 저를 도와주세요!#비주얼캠프 #인사이트 #경험공유 #조언 
조회수 950

Sublime Text 3에서 Gist 연동하기

블로그 같은 곳에 작성한 코드를 올리기 위해 매번 구현된 코드를 복사 붙여넣기 하여 하나하나 gist에 업로드하기는 엄청 귀찮은 일이다. 그래서 알아보았더니 서브라임 텍스트에서는 플러그인을 통해 서브라임 자체에서 바로 gist에 업로드 할 수 있었다. 엄청 간단하게 연동할 수 있음.Package Control 설치일단 서브라임 텍스트 플러그인을 설치하기 위해 Package Control을 설치해야 함.1. Sublime Text Package Control 코드 복사하기2. 서브라임 텍스트를 실행하여 control+`으로 콘솔 열기3. 복사한 내용을 붙여넣고 엔터를 눌러 설치플러그인 설치이번엔 방금 설치한 패키지 컨트롤을 이용하여 gist 플러그인을 설치해야 함.1. 서브라임에서 command+shift+p로 Command Palette 열기2. Package Control: Install Package를 선택3. gist 검색 후 설치github와 연동하기1. github에서 Settings>Personal access tokens에서 Generate new token 버튼 클릭2. Token description에 내가 알아볼 수 있게 설명을 입력한 후 Select scopes에서 gist 선택 후 Generate token 버튼을 클릭하여 생성3. 생성된 토큰을 복사4. 서브라임에서 Preferences>Package Settings>Gist>Settings-User 선택5. 열린 창에서 아래와 같이 token에 복사해놓은 키를 붙여넣고 include_users에 내 깃헙 아이디 입력 후 저장사용서브라임 텍스트에서 코드 작성 후 command+k, command+p를 누르면 하단에 gist 순서대로 설명 입력하고 엔터 제목을 입력하고 엔터를 누르면 자동으로 업로드됨.#트레바리 #개발자 #안드로이드 #앱개발 #Sublime #백엔드 #인사이트 #경험공유
조회수 1663

네이버 신디케이션 — Rails

블로그에 새 글이 올라올 때, naver에 사이트 등록을 한다. 네이버 신디케이션 API를 이용하면 자동으로 등록된다.Wordpress에는 네이버 신디케이션 plugin이 존재한다. Rails gem을 찾아보니 애석하게도 없었다. 직접 만들면서 알게 되었다. 딱히 gem을 만들 만한 일도 아니더라.네이버 신디케이션을 이용하려면 우선 네이버 웹마스터 도구를 이용해야 한다. 해당 url이 자기 것이라는 인증과정만 거치면 바로 사용할 수 있다.작동방법은 대강 이렇다.네이버 신디케이션 API를 이용해서, 새로운 글이 생성되었음을 알린다. (혹은 글이 지워졌음을)네이버 크롤링 봇, Yeti가 와서 크롤링 해간다.API를 이용할 때 미리 약속된 format으로 만들어야 되는데, ATOM feed와 구조가 거의 같다. 다만 네이버가 정한 룰 때문에 (꼭 이름/저자/업데이트날짜 이런 순서를 지켜야 한다.)Rails에서 제공하는 atom_feed helper를 그대로 이용할 수 없다. 그러나 format만 살짝 바꾸면 되기 때문에 atom_feed helper를 이용해서, feed를 만드는 방법을 알려주는 Railscast가 늘 그렇듯 엄청 도움이 된다.(요즘 새로운 episode가 안올라오고 있는데… 힘내시라는 의미에서 예전에 유료결제 해드렸다)atom_feed helper의 코드를 그대로 가져와서 formating만 바꾼 naver_atom_feed helper를 만들었다. 별다른 건 없고, feed option 초기화 부분과 제일 마지막에 나와야 되는 link 부분을 주석처리한게 전부다.module NaverSyndicationHelper def naver_atom_feed(options = {}, █) ... feed_opts = {} //feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'} ... xml.feed(feed_opts) do xml.id... // xml.link... // xml.link... yield ActionView::Helpers::AtomFeedHelper::AtomFeedBuilder.new(xml, self, options) end end end새로만든 naver_atom_feed helper를 이용해서, feed부분만 완성한 code이다.naver_atom_feed({xmlns: "http://webmastertool.naver.com", id: 'http://ikeaapart.com'}) do |feed| feed.title "이케아아파트" feed.author do |autor| autor.name("이케아아파트") end feed.updated Link.maximum(:updated_at) feed.link(:rel => 'site', :href => (request.protocol + request.host_with_port), :title => '이케아아파트')이제 entry쪽을 만들어야 되는데, 네이버가 지정한 순서에 맞아야지만 신디케이션 서버에 전달할 수 있다. 정말 이상한 형식이다. 아무튼 그래서 Rails에서 제공하는 entry method를 사용하지 못한다. 이번엔 AtomFeedBuilder class에 naver_entry method를 만들었다.#config/initializers/feed_entry_extentions.rbmodule ActionView module Helpers module AtomFeedHelper class AtomFeedBuilder def naver_entry(record, options = {}) @xml.entry do @xml.id... # if options[:published]... # @xml.published(...) # end # if options[:updated]... # @xml.updated(...) # end # @xml.link(..) ...이번에도 순서 때문에 주석처리 한 것 밖에 없다. naver_entry method를 이용해서 완성된 코드가 아래 코드이다.# views/links/show.atom.buildernaver_atom_feed({xmlns: "http://webmastertool.naver.com", id: 'http://ikeaapart.com'}) do |feed| feed.title "이케아아파트" feed.author do |autor| autor.name("이케아아파트") end feed.updated Link.maximum(:updated_at) feed.link(:rel => 'site', ...) feed.naver_entry(@link, {id: link_url(@link)}) do |entry| entry.title(@link.title) entry.author do |author| author.name("이케아아파트") end entry.updated(@link.updated_at.xmlschema) entry.published(@link.created_at.xmlschema) entry.link(:rel => 'via', :href => (request.protocol + request.host_with_port)) entry.content(@link.contents) end end이제 새 글이 만들어 질 때, 이 atom 파일 주소를 네이버 신디케이션 API로 보내주면 된다. 참고로 Rails에서는 어떤 view파일을 사용할지 알아서 해주니, controller에 따로 ‘response_to’ 를 이용해서 format을 나눠줄 필요는 없고, 이름만 잘 맞춰주면 된다. (위 파일명은 show.atom.builder 이다)네이버 신디케이션 API에 핑을 보내는 code이다. 네이버가 지정해 놓은 header를 설정해 줘야 되고, 신디케이션 인증 토큰을 받아서 header에 넣어줘야 된다. 신디케이션 토큰은 네이버 웹마스터 페이지에서 볼 수 있다.require 'net/http' ... header = {"User-Agent"=>"request", "Host"=>"apis.naver.com", "Progma"=>"no-cache", "Content-type"=>"application/x-www-form-urlencoded", "Accept"=>"*/*", "Authorization"=>"Bearer " + ENV["NAVER_SYNDICATION_TOKEN"]} uri = URI.parse('https://apis.naver.com/crawl/nsyndi/v2') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true args = {ping_url: link_url(link_id, format: "atom")} uri.query = URI.encode_www_form(args)request = Net::HTTP::Post.new(uri.request_uri, header) http.request(request)네이버 신디케이션 페이지에서 핑이 제대로 도달하는지 바로 확인해 볼 수 있다.#티엘엑스 #TLX #BA #BusinessAnalyst #비즈니스애널리스트 #꿀팁 #인사이트 #조언
조회수 4141

파이썬 코딩 컨벤션

스포카 개발팀 문성원입니다. 저희는 (익히 아시다시피) 서버를 개발하는데 파이썬(Python)을 사용하고 있는데, 오늘은 이러한 파이썬 코드를 작성할 때 기준이 되는 코딩 컨벤션(Coding Convention)에 대해서 알아보겠습니다.Coding Convention코딩 컨벤션이란 개념에 대해 생소하신 분들도 계실 테니 이를 먼저 알아보죠. 코딩 컨벤션은 프로그램 코드를 작성할 때 사용되는 일종의 기준입니다. 이를테면 들여쓰기(Indentation)는 공백으로 할거냐 탭으로 할거냐. 부터 var a = 3; 과 같은 코드에서 a와 =를 붙이느냐 마느냐라던지를 정해주는 것이죠. 알고 계시는 것처럼 이러한 차이는 특별히 실행 결과의 영향을 주지 않습니다. 다르게 이야기하자면 “실행 결과에 별 차이가 없는 선택지들”이기 때문에 일관성이 있는 기준을 두어 통일하자는 것이지요.그렇다면 왜 이런 선택지를 통일해야 할까요? 불행히도 우리가 작성한 코드는 많은 사람들이 보게 됩니다. 같이 일하는 동료, 이바지하고 있는 프로젝트의 리뷰어, 심지어 내일의 자기 자신까지도 말이죠. 그런데 이런 많은 사람들이 우리가 코드를 작성할 때 했던 선택지를 일일이 추론해서 이해하는 건 굉장히 피곤하고 짜증 나는 일입니다. 그래서 우리는 사소한 것부터 일종의 규칙을 정해서 이런 짜증과 불편함을 줄이려는 겁니다. 또한, 일반적으로 좋은 기준에는 훌륭한 프로그래머들의 좋은 습관이 배어있기 때문에 더 나은 품질의 코드를 작성하는 데에도 많은 도움이 됩니다.이런 코딩 컨벤션은 극단적으로 이야기하면 프로젝트마다 하나씩 존재한다고 볼 수도 있지만, 일반적으로 그 언어문화를 공유하는 공동체에서 인정하는 컨벤션은 대부분 통일되어 있습니다. 파이썬은 지금부터 살펴볼 PEP 8이 대표적입니다.PEP?PEP(Python Enhance Proposal)이란 이름대로 본디 파이썬을 개선하기 위한 개선 제안서를 뜻합니다. 이러한 제안서는 새로운 기능이나 구현을 제안하는 Standard Track, (구현을 포함하지 않는) 파이썬의 디자인 이슈나 일반적인 지침, 혹은 커뮤니티에의 정보를 제안하는 Informational, 그리고 파이썬 개발 과정의 개선을 제안하는 Process의 3가지로 구분할 수 있습니다. (좀 더 자세한 사항은 PEP에 대해 다루고 있는 PEP인 PEP 1을 참고하세요.) 파이썬은 언어의 컨벤션을 이러한 제안서(Process)로 나타내고 있는데 이것이 바로 PEP 8입니다.Laplace’s Box기본적으로 가이드라인이니만큼 규칙만 빽빽할 것 같지만, PEP 8는 서두부터 예외를 언급한 섹션이 있습니다.A style guide is about consistency. Consistency with this style guide is important. Consistency within a project is more important. Consistency within one module or function is most important.스타일 가이드는 일관성(consistency)에 관한 것입니다. 이 스타일 가이드의 일관성은 중요하죠. 하지만 프로젝트의 일관성은 더욱 중요하며, 하나의 모듈이나 함수의 일관성은 더더욱 중요합니다.But most importantly: know when to be inconsistent – sometimes the style guide just doesn’t apply. When in doubt, use your best judgment. Look at other examples and decide what looks best. And don’t hesitate to ask!하지만 가장 중요한 건 언제 이것을 어길지 아는 것입니다. – 때때로 스타일 가이드는 적용되지 않습니다. 의심이 들 때는 여러분의 최선의 판단을 따르세요. 다른 예제를 보고 어느 게 제일 나은지 골라야 합니다. 질문을 주저하지 마세요!Two good reasons to break a particular rule:When applying the rule would make the code less readable, even for someone who is used to reading code that follows the rules.To be consistent with surrounding code that also breaks it (maybe for historic reasons) – although this is also an opportunity to clean up someone else’s mess (in true XP style).다음은 규칙들을 어기는 2가지 좋은 예외 사항입니다.규칙을 적용한 코드가 (규칙을 숙지한 사람 눈에도) 읽기 어려운 경우일관성을 지키려고 한 수정이 다른 규칙을 어기는 경우(아마도 역사적인 이유겠죠.)아직 아무것도 안나왔는데 좀 이르다구요?It’s all about common sense예외 규정을 보여주며 시작하는 PEP 8이지만 얼개는 그리 복잡하지도 않고 크게 난해하지도 않습니다. 여기서는 대표적인 몇 가지만 추려서 소개하겠습니다.Code lay-out들여쓰기는 공백 4칸을 권장합니다.한 줄은 최대 79자까지최상위(top-level) 함수와 클래스 정의는 2줄씩 띄어 씁니다.클래스 내의 메소드 정의는 1줄씩 띄어 씁니다.Whitespace in Expressions and Statements다음과 같은 곳의 불필요한 공백은 피합니다.대괄호([])와 소괄호(())안쉼표(,), 쌍점(:)과 쌍반점(;) 앞키워드 인자(keyword argument)와 인자의 기본값(default parameter value)의 = 는 붙여 씁니다.Comments코드와 모순되는 주석은 없느니만 못합니다. 항상 코드에 따라 갱신해야 합니다.불필요한 주석은 달지 마세요.한 줄 주석은 신중히 다세요.문서화 문자열(Docstring)에 대한 컨벤션은 PEP 257을 참고하세요.Naming Conventions변수명에서 _(밑줄)은 위치에 따라 다음과 같은 의미가 있습니다._single_leading_underscore: 내부적으로 사용되는 변수를 일컫습니다.single_trailing_underscore_: 파이썬 기본 키워드와 충돌을 피하려고 사용합니다.__double_leading_underscore: 클래스 속성으로 사용되면 그 이름을 변경합니다. (ex. FooBar에 정의된 __boo는 _FooBar__boo로 바뀝니다.)__double_leading_and_trailing_underscore__: 마술(magic)을 부리는 용도로 사용되거나 사용자가 조정할 수 있는 네임스페이스 안의 속성을 뜻합니다. 이런 이름을 새로 만들지 마시고 오직 문서대로만 사용하세요.소문자 L, 대문자 O, 대문자 I는 변수명으로 사용하지 마세요. 어떤 폰트에서는 가독성이 굉장히 안 좋습니다.모듈(Module) 명은 짧은 소문자로 구성되며 필요하다면 밑줄로 나눕니다.모듈은 파이썬 파일(.py)에 대응하기 때문에 파일 시스템의 영향을 받으니 주의하세요.C/C++ 확장 모듈은 밑줄로 시작합니다.클래스 명은 카멜케이스(CamelCase)로 작성합니다.내부적으로 쓰이면 밑줄을 앞에 붙입니다.예외(Exception)는 실제로 에러인 경우엔 “Error”를 뒤에 붙입니다.함수명은 소문자로 구성하되 필요하면 밑줄로 나눕니다.대소문자 혼용은 이미 흔하게 사용되는 부분에 대해서만 하위호환을 위해 허용합니다.인스턴스 메소드의 첫 번째 인자는 언제나 self입니다.클래스 메소드의 첫 번째 인자는 언제나 cls입니다.메소드명은 함수명과 같으나 비공개(non-public) 메소드, 혹은 변수면 밑줄을 앞에 붙입니다.서브 클래스(sub-class)의 이름충돌을 막기 위해서는 밑줄 2개를 앞에 붙입니다.상수(Constant)는 모듈 단위에서만 정의하며 모두 대문자에 필요하다면 밑줄로 나눕니다.Programming Recommendations코드는 될 수 있으면 어떤 구현(PyPy, Jython, IronPython등)에서도 불이익이 없게끔 작성되어야 합니다.None을 비교할때는 is나 is not만 사용합니다.클래스 기반의 예외를 사용하세요.모듈이나 패키지에 자기 도메인에 특화된(domain-specific)한 기반 예외 클래스(base exception class)를 빌트인(built-in)된 예외를 서브클래싱해 정의하는게 좋습니다. 이 때 클래스는 항상 문서화 문자열을 포함해야 합니다.class MessageError(Exception): """Base class for errors in the email package."""raise ValueError('message')가 (예전에 쓰이던) raise ValueError, 'message'보다 낫습니다.예외를 except:로 잡기보단 명확히 예외를 명시합니다.(ex. except ImportError:try: 블록의 코드는 필요한 것만 최소한으로 작성합니다.string 모듈보다는 string 메소드를 사용합니다. 메소드는 모듈보다 더 빠르고, 유니코드 문자열에 대해 같은 API를 공유합니다.접두사나 접미사를 검사할 때는 startswith()와 endwith()를 사용합니다.객체의 타입을 비교할 때는 isinstance()를 사용합니다.빈 시퀀스(문자열, 리스트(list), 튜플(tuple))는 조건문에서 거짓(false)입니다.불린형(boolean)의 값을 조건문에서 ==를 통해 비교하지 마세요.Give me a reason하지만 몇몇 규칙은 그 자체만으론 명확한 이유를 찾기 어려운 것도 있습니다. 가령 예를 들면 이런 규칙이 있습니다.More than one space around an assignment (or other) operator to align it with another.Yes:x = 1 y = 2 long_variable = 3No:x = 1 y = 2 long_variable = 3보통 저런 식으로 공백을 통해 =를 맞추는 건 보기에도 좋아 보입니다. 하지만 변수가 추가되는 경우에는 어떨까요. 변수가 추가 될때마다 공백을 유지하기 위해 불필요한 변경이 생깁니다. 이는 소스를 병합(merge)할 때 혼란을 일으키기 쉽습니다.언뜻 보면 잘 이해가 안 가는 규칙은 이런 것도 있습니다.Imports should usually be on separate lines, e.g.:Yes: import os import sys No: import sys, os굳이 한 줄씩 내려쓰면 길어지기만 하고 보기 안 좋지 않을까요? 하지만 이 역시 대부분의 변경 추적 도구가 행 기반임을 고려하면 그렇지 않습니다.#스포카 #개발 #파이썬 #개발자 #Python #컨벤션 #이벤트참여 #이벤트후기 #후기
조회수 816

[인공지능 in IT] '머신 비전', 내 눈에 걸리기만 해봐

50~60년대 국내 상황은 말로 표현하기 힘들다. 당시 강대국들은 전쟁 직후 한국이 다시 정상적으로 복귀하는 것은 불가능하다고 여길 정도였으니, 여러 모로 살아남기 힘든 환경이었던 것만은 분명하다. 하지만, 뭐든지 열심히 노력하는 특유의 국민성을 바탕으로 한걸음씩 내딛기 시작했고, 1988년 서울 올림픽까지 개최할 정도로 경제 성장을 이뤘다. 당시 필자가 태어난 것은 아니었지만, 여러 자료나 부모님 세대의 말씀을 조합하면, 이 같은 성장의 중심에는 제조업의 부흥이 있었기 때문이다.제조업은 국가 실물 경제의 근간이라고 할 정도로 중요한 역할을 담당한다. 단단한 제조업 생태계가 창출해 내는 부가가치를 바탕으로 서비스업이 발전한다면, 산업의 경쟁력을 잃지 않으면서 지속적인 성장을 이뤄낼 수 있는데 큰 보탬이 된다. 최근에는 인공지능과 같은 고도의 기술이 널리 퍼져 제조업의 중요성을 더욱 부각하고 있다. 전통적인 기계 산업 기술은 과학기술을 지탱하는 뿌리의 역할을 하고, 인공지능이나 데이터의 확장 등 탄탄한 제조업 중심의 주력 산업과 융합해 폭발적으로 성과를 낼 수 있다. 결국, 아무리 새로운 기술이 등장한다 해도, 제조업과는 떼려야 뗄 수 없는 관계인 셈이다.인공지능은 제조업에서 매우 유용하게 쓰이고 있다. 그 중에서 공장 자동화에 큰 역할을 하고 있는 '머신 비전(Machine Vision)'에 대해서 이야기를 해보자. 머신 비전은 사물인식, 얼굴인식, 이미지 캡션, 문자 인식 등 여러 형태로 적용되며, 최근 들어 딥 러닝을 통해 더욱 강력해지고 있다. 특히, 비전을 활용해 불량품을 검출하는 'Defect Detection'은 제조업에서 큰 역할을 할 수 있다. 대다수의 공장에서 제품 생산 마지막 공정은 '품질보증(Quality Assurance, QA)'이다. QA를 통해서 생산한 제품 혹은 부품에 문제가 없는지 확인한 후, 구매자에게 좋은 품질의 제품만을 제공해야 하기 때문에 매우 중요하다.실제로 대량생산라인을 보유하고 있는 제조업 기반 기업은 QA에 막대한 비용을 소모하고 있다. 때문에 유심히 확인하지 않거나, 몇몇 샘플들만 체크하고, 심지어 QA를 생략하는 경우도 있다. 결국 피해는 고스란히 최종 구매자에게 이어진다. 예를 들어, 새로 장만한 스마트폰이나 자동차 부품에 흠집이 있는 경우, 최종 구매자가 겪어야 할 불편함은 작지 않다. 또한, 고객 충성도 하락까지 이어질 수 있어 기업은 사전에 방지해야 한다.불량품 검출이 이루어지는 프로세스를 간단하게 알아보자. 스켈터랩스의 정수익 책임 PM의 도움을 받아 이미지로 구성했다.< 불량품 검출 프로세스, 출처: 스켈터랩스 >먼저 부품 생산 과정 중 불량을 탐지하기 위해서는 광학 기기를 사용해 사진을 찍어야 한다. 그리고 촬영된 사진을 이용해 머신 비전으로 탐지하는 것이다. 하지만, 머신 비전이 적용되었다고 해서 바로 족집게처럼 불량품을 검출해낼 수 있는 것도 아니다. 이미 많은 이들이 알고 있지만, 딥 러닝은 수많은 데이터셋을 바탕으로 선행한 학습 전제가 필요하다. 결함으로 판명된 부품들에 대한 데이터를 수집하고, 학습해 '이 부품은 이런 형태의 손상이 있으니 불량이다'라고 판단하는 방식이다. 인식하고, 학습하고, 검출하는 단계를 계속해서 반복하며 기계가 점점 '똑똑해진다'라고 할 수 있다.이어서 스켈터랩스의 사례를 참고해보자. 내부에서 개발하고 있는 불량품 검출 서비스는 크게 세가지 부분으로 구성된다. 파란색 네모 안에 있는 이름은 가제다.< 스켈터랩스의 머신 비전 불량품 검출 서비스 >하나씩 살펴보면, 'Dulok'은 실제로 현장에서 촬영되는 이미지를 모니터링하거나, 이를 클라우드에 업로드하는 '모니터링 모듈'이며, 'Ewok'은 웹상으로 부품 정보에 대해 'curation', 'labeling', 추론 결과를 확인할 수 있도록 하는 '애플리케이션'이다. 마지막으로 'Gorax'는 '학습을 통해 부품의 결함을 검출하는 모델'이다. 이 부분은 실제 서비스에서 단순히 딥 러닝을 통한 추론 외에도 다른 피쳐들이 제공되어야 한다.기존에는 사람이 이미지 상에서 결함에 대한 정의를 하나하나 내리고, 결함의 특징을 수동으로 설정해야 했다. 때문에 반도체나 LCD처럼 표면 형태가 정형화되어 있는 분야에서만 머신 비전 기술을 활용할 수 있었다. 반대로 섬유나 천연가죽 등 표면 형태가 비정형화된 분야에서는 결함 특징 값을 수동으로 설정하기 어려워 육안검사에 의존해야만 했다.그러나 점차 '머신 비전' 기술이 발전하면서 적용되는 영역은 계속 늘어나고 있다. 이는 품질을 높이는 결과로 이어져, 결과적으로는 최종 소비자들이 혜택을 받는다. 이처럼 인공지능 기술은 향후 지속적으로 발전을 거듭해 제조업의 일자리를 뺏는 것이 아닌, 함께 공생하는 생태계를 구축하는데 도움될 것이라 생각한다.이호진, 스켈터랩스 마케팅 매니저조원규 전 구글코리아 R&D총괄 사장을 주축으로 구글, 삼성, 카이스트 AI 랩 출신들로 구성된 인공지능 기술 기업 스켈터랩스에서 마케팅을 담당하고 있다#스켈터랩스 #기업문화 #인사이트 #경험공유 #조직문화 #인공지능기업 #기술기업
조회수 1287

일본 스타트업 적응기#7   「동료」

언제부터였을까'가족같은 분위기의 회사' 는절대 피해야할 구인광고가 되었다.'분위기  가족같은 회사'는 곧'분위기가  족같은 회사'라는 웃지 못할 유머가 있듯이 말이다.그렇지만 본 적 있는가? TED에서 봤던 인상적인 강의 중에 하나"빌 그로스(Bill Gross): 스타트업 성공의 가장 중요한 요소"를 보면(링크확인)<기업을 성공으로 이끄는 주요한 5가지 요소>아이디어, 팀, 비즈니스 모델, 자금, 타이밍 중 첫 번째를 Timing, 두 번째를 Team로 꼽고 있다.Timing과 관련해서는 개인이 회사에 합류를 결정하는 시점이면 몰라도, 이미 전개되고 있는 비즈니스에 함께하는 입장에서는 영향을 끼치기가 쉽지 않다. 어쩌면 이것은 리더의 안목과 운의 작용에 의해 만들어나가는 것인지도 모르니 말이다.하지만 Team - 사내 문화, 분위기, 동료관계와 관련해서는 특히나 스타트업이라면 개개인의 영향력이 클 수밖에 없다. 따라서 내가 속한 조직을 성공으로 이끌고자할 때 각자에 자리에서 맡은 일을 잘하는 것도 중요하지만, 이에 바탕이되는 행복한 동료 관계에도 늘 관심을 기울여야한다고 생각한다.한국에서는 많은 사람들이 '가족같은 회사가 되는 것'을 경계해서 일까언제부턴가 돈이 많거나, 아이디어가 남다르거나, 비즈니스 모델이 훌륭한 기업은 성공할 것이라 믿으면서, 상대적으로 Team - 사내 문화, 분위기, 동료관계 등의 중요성은 점점 소홀하게 생각하고 있는지도 모른다.근래에 한국 비즈니스 전개로 인해 출장이 잦았다. 다소 숨 가쁘게 진행된 일정에 기진맥진하여 돌아오면 동료이 있다. 그리고 그들을 보면 마음이 편안해진다.'그래 내가 열심히 해서 우리 동료들 다 같이 한국 가서 맛있는 음식 한번 먹어야지!' 라는 마음으로 일을 하고 있다.동료가 이런 말을 한 적이 있다.'난 지금 우리가 하는 서비스가 예상 못한 상황을 만나 쫄딱 망하더라도, 지금 함께하는 동료들과 같이라면 언젠간 꼭 목표를 성취하는 날이 올 거라 믿는다'4월 FULLER 동료들과 다같이 하나미(벚꽃놀이)에서이런 동료애를 바탕으로 한다면 뭘 해도 할 수 있지 않을까?Fuller 의 대표 서비스 App Ape가 오랜 준비기간을 걸쳐 드디어 한국에 진출하였습니다!신뢰성 있는 모바일 앱 데이터를 기반으로 한 다양한 인사이트와 정보를 전해드리겠습니다.- 페이스북 페이지 : https://www.facebook.com/appape.korea/ - App Ape Lab : http://lab.appa.pe/ko/index.html많은 관심을 부탁드립니다!#Fuller #일본 #스타트업 #해외취업 #스타트업합류 #일상 #인사이트 #팀원 #동료
조회수 4688

안드로이드 앱의 Persistent data를 제대로 암호화해 보자! (2/2)

들어가기1부에서는, KeyStore 를 사용해 Shared Preferences 를 암호화 하는 법에 대해 알아봤습니다. 그리고 이 글에서는 Room을 사용한 Database 를 암호화 하는 방법에 대해 설명합니다.2018년 현재, 안드로이드 자체에서 데이터베이스를 암호화하는 기능을 제공해 주진 않습니다. 따라서 오픈 소스 프로젝트인 SQLCipher, SafeRoom 의 사용법 위주로 설명할 예정입니다. 또한 KeyStore 에 대칭키를 생성하는 기능은 API Level 23 이후에서만 가능하며, SQLCipher 가 Android KeyStore 를 지원하지 않고 있습니다.이로 인해 1부에서 소개한 키 암호화 메커니즘으로 보호한 별도의 키를 디스크 어딘가에 저장해 두고, 필요할 때만 복호화 해서 쓴 다음 복호화된 내용을 지우는 방식으로 구현해야 합니다. 하지만 이런 방식으로 사용하는 키는 메모리에 순간적으로 남기 때문에 좋은 공격 표면(Attack surface) 이 됩니다. 그 이유도 함께 다뤄 보겠습니다.SqlCipher team 에서 하루라도 빨리 현재의 char[] 형식의 passphrase 를 입력받는 대신, JCA 를 사용해 암호화하는 데이터베이스를 구현하길 기대해 봅시다.SqlCipher1부에서 보여드렸다시피 internal storage 에 저장한 데이터는 결코 안전하지 않습니다. 파일 DB 인 Sqlite 데이터는 포맷을 모르면 어차피 볼 수 없을테니 조금 다르지 않을까요? 그렇지 않다는 것을 다음 예에서 보여드리겠습니다. 루팅한 디바이스에서 adb pull명령으로 sqlite3 데이터베이스를 추출 후 내용을 열어보면 다음과 같습니다.$ hexdump -vC secure_database.sqlite3 00000000  53 51 4c 69 74 65 20 66  6f 72 6d 61 74 20 33 00  |SQLite format 3.| 00000010  10 00 02 02 00 40 20 20  00 00 00 02 00 00 00 04  |.....@  ........| 00000020  00 00 00 00 00 00 00 00  00 00 00 04 00 00 00 04  |................| 00000030  00 00 00 00 00 00 00 04  00 00 00 01 00 00 00 00  |................| 00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................| 00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 02  |................| 00000060  00 2e 01 5a 0d 0f 95 00  02 0e a9 00 0e a9 0f c9  |...Z............| 00000070  0e 6f 0e 6f 00 00 00 00  00 00 00 00 00 00 00 00  |.o.o............| ... 00000d30  00 00 00 00 00 82 37 03  07 17 57 57 01 83 4d 74  |......7...WW..Mt| 00000d40  61 62 6c 65 73 71 6c 69  74 65 62 72 6f 77 73 65  |ablesqlitebrowse| 00000d50  72 5f 72 65 6e 61 6d 65  5f 63 6f 6c 75 6d 6e 5f  |r_rename_column_| 00000d60  6e 65 77 5f 74 61 62 6c  65 73 71 6c 69 74 65 62  |new_tablesqliteb| 00000d70  72 6f 77 73 65 72 5f 72  65 6e 61 6d 65 5f 63 6f  |rowser_rename_co| 00000d80  6c 75 6d 6e 5f 6e 65 77  5f 74 61 62 6c 65 05 43  |lumn_new_table.C| 00000d90  52 45 41 54 45 20 54 41  42 4c 45 20 60 73 71 6c  |REATE TABLE `sql| 00000da0  69 74 65 62 72 6f 77 73  65 72 5f 72 65 6e 61 6d  |itebrowser_renam| 00000db0  65 5f 63 6f 6c 75 6d 6e  5f 6e 65 77 5f 74 61 62  |e_column_new_tab| 00000dc0  6c 65 60 20 00 00 00 00  00 00 00 00 00 00 00 09  |le` ............| ... [리스트 1] Internal storage 에 저장된 SQLite3 database 를 dump 한 결과.역시 기대했던대로 데이터가 하나도 암호화되어 있지 않은 것을 확인할 수 있습니다. 그렇다면 가장 간단한 방법은 SQLiteDatabase클래스를 확장하는 일일 텐데요, 문제는 이 클래스가 final 로 상속 불가능하게 되어 있단 점입니다. 이 때문에 암호화된 SQLiteDatabase 구현체는 이 클래스 및 이 클래스에 강하게 결합되어 있는 SQLiteOpenHelper 를 온전히 쓸 수 없다는 문제가 있습니다. 즉, 바닥부터 새로 만들어야 하는 상황인데요, 다행히도 Zetetic 사에서 만든 SQLCipher for Android 는 이 문제를 모두 해결해 주는 고마운 오픈 소스 프로젝트입니다.SqlCipher 의 사용법은 기존의 SQLiteDatabase 에 의존하던 로직들의 import namespace 만 바꿔주면 되도록 구현되어 있어 마이그레이션 비용도 거의 들지 않습니다.// 안드로이드에서 제공해 주는 SQLiteDatabase 클래스명 import android.database.sqlite.SQLiteDatabase; // SqlCipher 에서 제공해 주는 SQLiteDatabase 클래스명 import net.sqlcipher.database.SQLiteDatabase; // 프로그램 시작시 native library 를 로드해줘야 한다. class MyApplication extends android.app.Application {    @Override public void onCreate() {        super.onCreate();        net.sqlcipher.database.SQLiteDatabase.loadLibs(this);    } } [리스트 2] android SQLiteDatabase 에서 SqlCipher SQLiteDatabase 로 마이그레이션 하기물론 두 클래스는 전혀 타입 호환되지 않지만, net.sqlcipher.database.SQLiteDatabase 의 모든 메소드 및 field의 signature 가 기본 android.database.sqlite.SQLiteDatabase 와 같기 때문에 이런 변경이 가능합니다. SqlCipher 개발팀의 수고에 박수를 보냅니다.RoomRoom 은 SQL 을 객체로 매핑해 주는 도구입니다. Room 을 이용해 데이터베이스를 열 때는 보통 아래와 같은 코드를 사용합니다.object Singletons {    val db: DataSource by lazy {        Room.databaseBuilder(appContext, DataSource::class.java, "secure_database")            .build()    } } abstract class DataSource: RoomDatabase() {    abstract fun userProfileDao(): UserProfileDao } // 클라이언트 코드에서 아래와 같이 호출 val userProfile: UserProfile = Singletons.db.userProfileDao().findUserByUid(userId) [리스트 3] Room database 의 정의 및 활용Sqlite 의 기본 동작은 파일 데이터베이스에 단순 Read 및 Write 만 합니다. 따라서 데이터베이스 접근시 암호화/복호화 동작을 하는 callback 을 주입해야 데이터베이스를 암호화 할 수 있습니다. 그리고 RoomDatabase.Builder 클래스는 데이터베이스를 열때 우리가 주입한 일을 할 수 있는 hook method(openHelperFactory) 를 제공해 주고 있습니다. 다음 코드를 살펴봅시다.class RoomDatabase.Builder {    class Builder {        /**        * Sets the database factory. If not set, it defaults to {@link FrameworkSQLiteOpenHelperFactory}.        */        @NonNull        public Builder openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory)    } } interface SupportSQLiteOpenHelper {    /**     * Create and/or open a database that will be used for reading and writing.     */    SupportSQLiteDatabase getWritableDatabase();    /**     * Create and/or open a database. This will be the same object returned by {@link #getWritableDatabase}.     */    SupportSQLiteDatabase getReadableDatabase();    /**     * Factory class to create instances of {@link SupportSQLiteOpenHelper} using {@link Configuration}.     */    interface Factory {        /**         * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration.         */        SupportSQLiteOpenHelper create(Configuration configuration);    } } [리스트 4] Room builder 의 SupportSQLiteOpenHelper 주입 메소드 및 SupportSQLiteOpenHelper.Factory 인터페이스 정의설명을 최대한 간소하게 하기 위해 관심가질 필요 없는 코드 및 코멘트는 모두 제외했습니다. 아무튼 SupportSQLiteOpenHelper 구현체를 주입하면 뭔가 데이터베이스 작업 이전에 우리의 로직을 실행할 수 있을 것 같습니다.사실 이 인터페이스의 핵심은 바로 getWritableDatabase(), getReadableDatabase() 구현입니다. javadoc 에도 있지만 두 메소드로 반환하는 인스턴스는 같아야 하며 또한 암호화를 지원해야 한다는 것을 알 수 있습니다.결국 우리 목표는 Room 과 데이터베이스 암호화 로직을 연결해 주는 SupportSQLiteDatabase 구현체를 만드는 것임을 알 수 있습니다. 이 인터페이스는 규모가 제법 크기 때문에 이게 만만한 일이 아님을 직감하실 수 있을 겁니다.saferoom 도입으로 SupportSQLiteDatabase 인터페이스 구현체 사용하기앞서 살펴봤듯 SupportSQLiteDatabase 구현에는 상당한 노력이 필요하단 것을 알 수 있습니다. 그런데 고맙게도 saferoom 이라는 오픈 소스 프로젝트가 우리의 귀찮음을 잘 해결해 주고 있습니다. saferoom 의 SupportSQLiteOpenHelper 구현체를 간단히 살펴보면 아래와 같습니다./** * SupportSQLiteOpenHelper.Factory implementation, for use with Room  * and similar libraries, that supports SQLCipher for Android.  */ public class SafeHelperFactory implements SupportSQLiteOpenHelper.Factory {    private final char[] passphrase;    public SafeHelperFactory(final char[] passphrase) {        this.passphrase = passphrase;    }    @Override    public SupportSQLiteOpenHelper create(final SupportSQLiteOpenHelper.Configuration configuration) {        return(new com.commonsware.cwac.saferoom.Helper(configuration.context,            configuration.name, configuration.version, configuration.callback,            this.passphrase));    }    /**     * NOTE: this implementation zeros out the passphrase after opening the database     */    @Override    public SupportSQLiteDatabase getWritableDatabase() {        SupportSQLiteDatabase result = delegate.getWritableSupportDatabase(passphrase);        for (int i = 0; i < passphrase>            passphrase[i] = (char) 0;        }        return(result);    }    /**     * NOTE: this implementation delegates to getWritableDatabase(), to ensure that we only need the passphrase once     */    @Override    public SupportSQLiteDatabase getReadableDatabase() {        return getWritableDatabase();    } } /**  * SupportSQLiteOpenHelper implementation that works with SQLCipher for Android  */ class Helper implements SupportSQLiteOpenHelper {    final OpenHelper delegate;    Helper(Context context, String name, int version, SupportSQLiteOpenHelper.Callback callback, char[] passphrase) {        net.sqlcipher.database.SQLiteDatabase.loadLibs(context);        this.delegate = createDelegate(context, name, version, callback);        this.passphrase = passphrase;    }    abstract static class OpenHelper extends net.sqlcipher.database.SQLiteOpenHelper {        SupportSQLiteDatabase getWritableSupportDatabase(char[] passphrase) {            SQLiteDatabase db = super.getWritableDatabase(passphrase); return getWrappedDb(db);        }    } } [리스트 5] Saferoom 의 SupportSQLiteOpenHelper 구현체.소스 코드를 보면 SQLiteDatabase 의 원래 요구사항을 만족하지 못하는 구현 부분도 보입니다만, 그래도 이 정도면 수고를 꽤 크게 덜 수 있어 훌륭합니다.그리고 로직을 잘 보면 데이터베이스를 연 직후 암호로 넘겨준 char[] 배열을 초기화 하는 코드가 있다는 점입니다. 이것이 바로 이 문서의 서두에서 말했던 attack surface 를 최소화 하기 위한 구현입니다. 이 글의 주제에서 벗어난 내용이기에 여기서는 다루지 않습니다만, 궁금하신 분들은 부록 1: in-memory attack 맛보기에서 확인하실 수 있습니다.SqlCipher + SafeRoom + Room 구현 및 코드 설명이상으로 데이터베이스 암호화 전략에 대해 살펴봤습니다. 이 장에서는 실제로 연동하는 방법에 대해 다룹니다.불행히도 2018년 현재 SqlCipher 는 Android KeyStore 를 지원하지 않고 있습니다. 그리고 인스턴스 생성에 쓸 비밀번호로 CharArray 가 필요한데, 이 값은 한번 정해지면 불변해야 합니다. 여기 사용할 키를 KeyStore 에 저장하면 문제를 깔끔하게 해결할 수 있을 것 같습니다. 하지만 1부에서 살펴봤듯이 하드웨어로 구현된 Android KeyStore 밖으로는 키가 절대로 노출되지 않는다고 합니다. 이 문제를 어떻게 해결해야 할까요?먼저, SqlCipher 에 사용하기 위해 KeyStore 로 생성한 AES256 키의 내용을 한번 살펴봅시다.val secretKey = with(KeyGenerator.getInstance("AES", "AndroidKeyStore"), {    init(KeyGenParameterSpec.Builder(alias,             KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)        .setKeySize(256)        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)        .build())    generateKey() }) val keyInfo = with(KeyFactory.getInstance(privKey.getAlgorithm(), "AndroidKeyStore"), {    factory.getKeySpec(privKey, KeyInfo::class.java) }) println("Key algorithm : " + secretKey.algorithm) println("Key format : " + secretKey.format) println("Encoded key size: " + secretKey.encoded?.size) println("Hardware-backed : " + keyInfo.isInsideSecureHardware) // 실행 결과 Key algorithm : AES Key format : null Encoded key size: null Hardware-backed : true [리스트 6] AndroidKeyStore 에 저장한 Key 는 어플리케이션에서 직접 쓸 수 없다.저희가 보유중인 개발 시료 Nexus 5 에서 실행한 결과 위와 같이 나타났습니다. secretKey.encoded 의 값이 메모리에 있다면 이 값을 SqlCipher 생성자에 넘겨줄 수 있겠지만 값이 null 이네요. 보안 측면에서는 다행일 지 모르지만 우리 구현에서는 쓸 수 없으니 문제입니다. 그래서 별 수 없이 임의로 키를 만들고(AndroidAesHelper#generateRandomKey()), 1부에서 소개했던 AndroidRsaCipherHelper 를 이용해 암호화한 값을 Shared Preferences에 저장하는 식으로 구현해 봅시다.val settingsPrefs = appContext.getSharedPreferences("app_settings", Context.MODE_PRIVATE) val settings = SecureSharedPreferences.of(settingsPrefs) val dbPass = with(settings, {    /*     * String.toCharArray() 같은 함수를 쓰면 로직이 좀더 간단해지지만, JVM 에서의 String은     * Immutable 하기 때문에 GC 이전에는 지울 방법이 없으므로 attack surface 가 더 오랫동안     * 노출되는 부작용이 있다. 따라서 key의 plaintext 는 가급적 String 형태로 저장하면 안된다.     */    var savedDbPass = getString("DB_PASSPHRASE", "")    if (savedDbPass.isEmpty()) {        // KeyStore 에 저장해도 SqlCipher 가 써먹질 못하니 그냥 1회용 키 생성 용도로만 활용한다.        val secretKey = AndroidAesCipherHelper.generateRandomKey(256)        // String 생성자 사용: 이 문자열은 heap 에 저장된다.        savedDbPass = String(Base64.encode(secretKey, Base64.DEFAULT))        putString("DB_PASSPHRASE",  AndroidRsaCipherHelper.encrypt(savedDbPass))        // 메모리 내에 plaintext 형태로 존재하는 attack surface 를 소멸시켜 준다.        secretKey.fill(0, 0, secretKey.size - 1)    } else {        // decrypt 메소드 내부에서 String 생성자 사용하므로 base64 인코딩된 plaintext 키는 heap 에 저장된다.        savedDbPass = AndroidRsaCipherHelper.decrypt(savedDbPass)    }    val dbPassBytes = Base64.decode(savedDbPass, Base64.DEFAULT)    /*     * SqlCipher 내부에서는 이 char[] 배열이 UTF-8 인코딩이라고 가정하고 있다.     * 그리고 UTF-8 인코딩에서는 byte range 의 char 는 1 바이트니까,     * 아래 변환을 거치더라도 키 길이는 32 byte(256 bit)가 유지된다.     *     * UTF-8 인코딩에서는 32 글자 != 32 바이트가 아님에 항상 유의해야 한다!     */    CharArray(dbPassBytes.size, { i -> dbPassBytes[i].toChar() }) }) [리스트 7] 암호화한 SqlCipher 용 passphrase 를 사용하는 방법.위 코드를 사용해 char[] 타입의 값 dbPass 를 얻을 수 있습니다. 리스트 7을 이용해 얻은 dbPass를 아래 코드에 사용하면 SqlCipher - SafeRoom - Room 의 연동이 끝납니다.val dataSource = Room.databaseBuilder(_instance, DataSource::class.java, "secure_database") .openHelperFactory(SafeHelperFactory(dbPass))                .build() // 메모리 내에 plaintext 형태로 존재하는 attack surface 를 소멸시켜 준다. dbPass.fill('0', 0, dbPass.size - 1) [리스트 8] SqlCipher - SafeRoom - Room 연동하기위 코드에서 볼 수 있듯, 임의로 저장한 키를 Base64 인코딩으로 변환, 그리고 그것을 다시 CharArray 로 변환하는 과정에서 key 가 메모리에 존재해야 하는 순간이 있습니다. 이 구간을 바로 공격 표면(attack surface) 이라고 합니다.JVM 단에서 넘겨주는 Passphrase 를 SqlCipher 내부에서 native 로 어떻게 처리하고 있는지는 SqlCipher SQLiteDatabase 구현및 SqlCipher crypto 구현 에서 확인할 수 있습니다.결과 확인하기SafeHelperFactory 를 주입한 Room database 파일을 추출 후 hexdump 로 확인해 보겠습니다.hwan@ubuntu:~$ hexdump -vC secure_database.sqlite3 00000000  8c 0d 04 07 03 02 11 eb  a4 18 33 4f 93 e8 ed d2  |..........3O....| 00000010  e9 01 21 d7 49 df 25 9a  f4 1d c7 1e ff 2d b0 13  |..!.I.%......-..| 00000020  fc 17 9b 4b b2 1c a3 1d  7d 1d 69 76 b1 ea ec e8  |...K....}.iv....| 00000030  1f 50 e4 c4 6c 50 e6 82  58 27 b9 fe 85 21 27 99  |.P..lP..X'...!'.| 00000040  ec 54 53 ba 32 c6 59 09  b4 30 65 39 a0 75 3e c4  |.TS.2.Y..0e9.u>.| 00000050  b8 f7 ea 47 14 df c4 f0  7c be 9f 62 26 49 1c b2  |...G....|..b&I..| 00000060  0f 63 00 7a 09 7e 33 e0  43 2b eb ea 80 21 bb 5d  |.c.z.~3.C+...!.]| 00000070  5c 04 ff 57 a3 a3 7f c2  19 42 b9 67 6c e3 d5 c8  |\..W.....B.gl...| ... 00000d30  c1 f3 93 1f 4e 5b 6a 70  39 c2 e9 2c 3e 8f 7e ff  |....N[jp9..,>.~.| 00000d40  73 3a 9a 39 0d 8a 1a 3e  6b d4 5b de 1f 6d c4 b8  |s:.9...>k.[..m..| 00000d50  fb 62 3e 21 09 0a 31 20  37 5d 8d 0a 39 6d 35 31  |.b>!..1 7]..9m51| 00000d60  26 d6 b0 22 41 7e 6c 54  7d 77 22 ba 1b f3 cf 5a  |&.."A~lT}w"....Z| 00000d70  e5 47 97 76 f0 89 e5 98  b3 37 3c 8d 43 af 0e b9  |.G.v.....7<.C...| 00000d80  18 74 fd f5 2a 41 d8 b1  d9 70 32 0b 5c 93 4b 0d  |.t..*A...p2.\.K.| 00000d90  bc 60 4c 25 9a ec 53 23  90 60 b2 52 a8 a1 b1 87  |.`L%..S#.`.R....| 00000da0  f3 3e 03 3e ac 0a 75 a0  61 d8 bd 07 b8 5a 48 66  |.>.>..u.a....ZHf| 00000db0  57 85 13 ac 04 26 55 30  34 46 57 bf 8b 42 c6 2d  |W....&U04FW..B.-| 00000dc0  9e 82 a2 df 77 bb b3 2e  96 43 70 23 23 03 df 1d  |....w....Cp##...| ... [리스트 9] Internal storage 에 저장된 SQLite3 database 를 dump 한 결과. 리스트 1과 비교해 보자.이로서 오픈 소스의 힘을 빌려 우리 앱의 데이터베이스를 비교적 간편하게 암호화 할 수 있음을 알 수 있습니다.맺으며이로서 Persistent data 암호화에 대한 설명을 마칩니다. Android KeyStore 가 API Level 23 이상의 기기에서만 100% 동작한다는 점은 2018년 현재까지는 큰 단점입니다. 하지만 사소한 데이터라 하더라도 보안의 중요성은 날로 강조되고 있습니다. 따라서 빠르던 늦던 고객 데이터 암호화에 투자해야 할 순간이 다가온다는 점은 변하지 않습니다.언젠가는 적용해야 할 고객 데이터 보호의 순간에, 이 글이 여러분의 앱의 보안에 조금이나마 도움이 된다면 좋겠습니다.부록 1: in-memory attack 맛보기앞서 계속 반복해서 설명드렸던 메모리 내의 attack surface 를 찾아내는 방법을 간단히 설명해 보겠습니다. 잘 지키려면 잘 공격하는 법을 알아야 하므로 알아두면 좋지 않을까요? 그리고 일반적인 앱 개발과는 다소 동떨어진 이 장의 내용이 이해되지 않으신다면 한줄요약한 메모리 내부의 값도 때로는 안전하지 않을 수 있다 는 한마디만 기억해 두시면 됩니다. 모든 데모는 LG Nexus 5(Hammerhead), 시스템 버전 6.0.1(M) 에서 실행한 결과며 시스템마다 약간의 차이는 있을 수 있습니다.마켓에 출시한 앱들은 debuggable:false 가 설정된 상태이므로 힙 덤프를 바로 뜰 수는 없습니다. 그런데 어떻게 in-memory attack 이 가능할까요? 다음 리스트는 디버그 불가능한 앱의 힙 덤프를 시도할 때 보안 정책 위반 오류가 발생함을 보여줍니다.hwan@ubuntu:~$ adb shell ps | grep "com.securecompany.secureapp" USER PID PPID VSIZE RSS WCHAN PC NAME u0_a431   25755 208   1700384 100888 sys_epoll_ 00000000 S   com.securecompany.secureapp hwan@ubuntu:~$ adb shell am dumpheap 25755 "/data/local/tmp/com.securecompany.secureapp.heap" java.lang.SecurityException: Process not debuggable: ProcessRecord{b6f96fc 25755:com.securecompany.secureapp/u0_a431}     at android.os.Parcel.readException(Parcel.java:1620)     at android.os.Parcel.readException(Parcel.java:1573)     at android.app.ActivityManagerProxy.dumpHeap(ActivityManagerNative.java:4922)     at com.android.commands.am.Am.runDumpHeap(Am.java:1248)     at com.android.commands.am.Am.onRun(Am.java:377)     at com.android.internal.os.BaseCommand.run(BaseCommand.java:47)     at com.android.commands.am.Am.main(Am.java:100)     at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)     at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:251) [리스트 10] debuggable=false 설정된 앱의 힙 덤프 시도시 발생하는 예외(SecurityException)SuperUser 는 가능할까요? SuperUser 권한으로 앱을 강제로 디버그 가능한 상태로 시작해 보도록 하겠습니다.hwan@ubuntu:~$ adb shell 32|shell@hammerhead:/ $ su 1|root@hammerhead:/ \# am start -D -n "com.securecompany.secureapp/MainActivity" && exit Starting: Intent { cmp=com.securecompany.secureapp/MainActivity } hwan@ubuntu:~$ \# adb shell ps | grep "com.securecompany.secureapp" USER PID PPID VSIZE RSS WCHAN PC NAME u0_a431   27482 211   1700384 100888 sys_epoll_ 00000000 S   com.securecompany.secureapp hwan@ubuntu:~$ adb forward tcp:12345 jdwp:27482 hwan@ubuntu:~$ netstat -an | grep 12345                                                           tcp4       0      0  127.0.0.1.12345         *.*                    LISTEN     hwan@ubuntu:~$ jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=12345 java.net.SocketException: Connection reset     at java.net.SocketInputStream.read(SocketInputStream.java:210)     at java.net.SocketInputStream.read(SocketInputStream.java:141)     at com.sun.tools.jdi.SocketTransportService.handshake(SocketTransportService.java:130)     at com.sun.tools.jdi.SocketTransportService.attach(SocketTransportService.java:232)     at com.sun.tools.jdi.GenericAttachingConnector.attach(GenericAttachingConnector.java:116)     at com.sun.tools.jdi.SocketAttachingConnector.attach(SocketAttachingConnector.java:90)     at com.sun.tools.example.debug.tty.VMConnection.attachTarget(VMConnection.java:519)     at com.sun.tools.example.debug.tty.VMConnection.open(VMConnection.java:328)     at com.sun.tools.example.debug.tty.Env.init(Env.java:63)     at com.sun.tools.example.debug.tty.TTY.main(TTY.java:1082) Fatal error:  Unable to attach to target VM. [리스트 12] SuperUser 권한으로도도 Java 디버거를 붙일 수 없다.다행히도 debuggable=false 로 릴리즈한 앱은 자바 디버거(jdb)를 붙일 수 없으니 프로그램 실행을 매우 정밀하게 제어할 수는 없다는 것을 알 수 있습니다(debuggable=true 설정된 앱에 위 과정을 실행하면 어떤 일이 벌어지는지 직접 확인해 보세요!).하지만 안드로이드의 앱은 ‘linux process’ 에서 실행되므로 SuperUser 권한으로 process 메모리 전체 dump를 뜨는 것은 막을 수 없습니다. 정공법으로는 /proc/PID/maps 의 내용을 분석하면 됩니다만 제가 안드로이드를 깊게 알고 있는 것은 아니라, 어느 영역이 dalvik heap 인지를 알아낼 수 없었습니다. 이 때문에 프로세스 메모리를 통째로 떠서 내용을 헤집어보는 방식으로 공격해 보겠습니다. 여담입니다만, 데모를 위해 공격한 앱은 dumpsys 명령으로 확인해보니 약 6MiB 의 Java heap 을 쓰고 있는데요, 이 크기를 줄이면 줄일 수록 공격이 더욱 수월할 겁니다.아래 데모에서는 안드로이드 기기용(arm-linux-gnueabi)으로 컴파일한 gdb 를 미리 설치한 결과를 보여드리고 있습니다. 참고로 여기 보이는 [heap] 은 아쉽지만 native heap 이므로 우리 공격 목표는 아닙니다.1|root@hammerhead:/ \# cd /proc/27482 1|root@hammerhead:/proc/27482 \# cat maps 12c00000-12e07000 rw-p 00000000 00:04 8519       /dev/ashmem/dalvik-main space (deleted) ... b7712000-b771f000 rw-p 00000000 00:00 0 [heap] bee86000-beea7000 rw-p 00000000 00:00 0 [stack] ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors] 1|root@hammerhead:/proc/27482 \# ifconfig wlan0     Link encap:Ethernet          inet addr:192.168.12.117          inet6 addr: fe80::8e3a:e3ff:fe5f:64c9/64 1|root@hammerhead:/proc/27482 \# gdbserver –attach :12345 27482 Attached; pid = 27482 Listening on port 12345 [리스트 13] SuperUser 권한으로 gdbserver 실행.hwan@ubuntu:~$ adb forward tcp:23456 tcp:12345 hwan@ubuntu:~$ netstat -an | grep 23456 tcp4       0      0  127.0.0.1.23456         *.*                    LISTEN     [리스트 14] 로컬 포트 23456 으로 원격 포트 12345 를 연결하는 과정.이제 모든 준비가 끝났습니다. 개발 기기에서 gdb로 원격 프로세스에 접근한 뒤, 메모리를 덤프해 봅시다.hwan@ubuntu:~$ ./gdb (gdb) target remote 192.168.12.117:12345 Remote debugging using 192.168.12.117:12345 0xb6f92834 in ?? () (gdb) dump memory /tmp/com.securecompany.secureapp.heap 0x12c00000 0xb771f000 (gdb) [리스트 15] gdb 로 메모리를 덤프하는 과정.덤프한 힙 덤프 파일 속에 있을지도 모르는 문자열을 검색해 봅시다. 그 전에 잠시, 데이터베이스에 사용할 키를 어떻게 처리했었나 되새겨 볼까요? if (savedDbPass.isEmpty()) {        // ...        // String 생성자 사용: 이 문자열은 heap 에 저장된다.        savedDbPass = String(Base64.encode(secretKey, Base64.DEFAULT))    } else {        // decrypt 메소드 내부에서 String 생성자 사용하므로 base64 인코딩된 plaintext 키는 heap 에 저장된다.        savedDbPass = AndroidRsaCipherHelper.decrypt(savedDbPass)    } [리스트 16] Base64 인코딩을 처리하기 위한 임시 String 생성 과정.우리 로직은 256 비트의 키를 Base64 변환해서 디스크에 저장합니다. 그리고 256비트의 byte array 를 base64 변환한 결과는 (4 * (256 / 3)) / 8 = 42.66 바이트 -> 4의 배수여야 하므로 44바이트입니다. 약 1.34 바이트의 pad 를 맞추기 위해 문자열의 끝에 =가 최소 1글자 이상은 있을 겁니다. 한번 찾아봅시다.hwan@ubuntu:~$ strings /tmp/com.securecompany.secureapp.heap ... /masterkey ... user_0/.masterkey em_s 1337 ... [리스트 17] strings 명령을 사용한 힙 덤프 파일내의 문자열 검색의외로 = 나 == 로 끝나는 문자열이 발견되지 않습니다. 하지만 안심하기는 이릅니다. 이건 단순히 (공격자의 입장에서) 운이 나빠서 발견되지 않은 것일 뿐입니다. 우리가 원하는 어떤 ‘순간’ 에 힙 덤프 명령을 내리지 않았기 때문에 그렇습니다. 우리의 구현은 attack surface 를 매우 짧은 시간동안만 메모리에 노출하기 때문에 이 순간이 짧으면 짧을 수록, 디바이스의 성능이 좋으면 좋을 수록 순간을 잡아내기가 더욱 어려워집니다. 즉, 이 문서에서 보여드린 방식으로 CharArray 의 내용을 아주 짧은 시간 동안만 사용하고 지워버리면 내용을 탈취하기 굉장히 어렵습니다. 하지만 안심하기는 이릅니다. nano-time 단위로 앱을 실행할 수 있는 환경을 가진 국가급 공격자는 여전히 있기 때문입니다.그리고 이 방법은 루팅하지 않은 기기에서는 절대 재현이 불가능하므로 루팅되지 않은 환경일 경우에만 실행 가능하도록 한다던가 하는 방식까지 더한다면 공격자가 더욱 우리 앱을 뚫기 힘들 겁니다.여담입니다만 독자 여러분들 중 GameGuardian 처럼 다른 게임의 메모리값을 마구 바꾸는 앱이 어떻게 동작하나 궁금하신 분들도 있을 겁니다. 그런 류의 앱들도 바로, 이 장에서 설명했던 방식으로 동작합니다.장황했던 이 장의 내용을 한줄로 요약하면 Android KeyStore 로 보호하지 않은 키는 많은 수고를 들이면 뚫을 수 있다고 할 수 있습니다.부록 2: SQLite database 의 UPDATE / DELETE 구현 특성SQLite3 의 구현특성상, UPDATE / DELETE 시에 이전 레코드의 값이 남아있는 경우가 있습니다. 암호화 했으니 좀더 안전하다곤 하지만 찌거기 값을 굳이 남겨둬서 공격자에게 더 많은 힌트를 제공할 필요도 없습니다.이 문서는 암호화 구현에만 초점을 맞췄기 때문에 상세하게 다루진 않습니다만, LINE Tech blog에 소개된 True delete 는 이 문제를 해결하기 위한 방법을 제시하고 있으므로 그 문서도 한번 읽어보시길 권합니다.더 보기SQLCipherSafeRoomAndroid SQLite3 True delete - by LINE tech blogDifference between java.util.Random and java.security.SecureRandomAttack surface on security measuresAOSP: DebuggingRootbeer: Simple to use root checking Android library#하이퍼커넥트 #개발 #개발자 #안드로이드 #앱개발 #모바일 #PersistentData #인사이트 #개발후기
조회수 1080

레진 기술 블로그 - AWS Auto Scalinging Group 을 이용한 배포

레진코믹스의 서버 시스템은 잘 알려진대로 Google AppEngine에서 서비스되고 있지만, 이런저런 이유로 인해 최근에는 일부 컴포넌트가 Amazon Web Service에서 서비스되고 있습니다. AWS 에 새로운 시스템을 셋업하면서, 기존에 사용하던 PaaS인 GAE에서는 전혀 고민할 필요 없었던, 배포시스템에 대한 고민이 필요했습니다. 좋은 배포전략과 시스템은 안정적으로 서비스를 개발하고 운영하는데 있어서 필수적이죠.초기에는 Beanstalk을 이용한 운영에서, Fabric 을 이용한 배포 등의 시행착오 과정을 거쳤으나, 현재는 (스케일링을 위해 어차피 사용할 수밖에 없는) Auto Scaling Group을 이용해서 Blue-green deployment로 운영 중입니다. ASG는 여러 특징 덕분에 배포에도 유용하게 사용할 수 있습니다.ASG를 이용한 가장 간단한 배포는, Instance termination policy 를 응용할 수 있습니다. 기본적으로 ASG가 어떤 인스턴스를 종료할지는 AWS Documentation 에 정리되어 있으며, 추가적으로 다음과 같은 방식을 선택할 수 있습니다.OldestInstanceNewestInstanceOldestLaunchConfigurationClosestToNextInstanceHour여기서 주목할 건 OldestInstance 입니다. ASG가 항상 최신 버전의 어플리케이션으로 스케일아웃되게 구성되어 있다면, 단순히 인스턴스의 수를 두배로 늘린 뒤 Termination policy 를 OldestInstance 로 바꾸고 원래대로 돌리면 구버전 인스턴스들부터 종료되면서 배포가 끝납니다. 그러나 이 경우, 배포 직후 모니터링 과정에서 문제가 발생할 경우 기존의 인스턴스들이 이미 종료된 상태이기 때문에 롤백을 위해서는 (인스턴스를 다시 생성하면서) 배포를 다시 한번 해야 하는 반큼 빠른 롤백이 어렵습니다.Auto scaling lifecycle 을 이용하면, 이를 해결하기 위한 다른 방법도 있습니다. Lifecycle 은 다음과 같은 상태 변화를 가집니다.기본적으로,ASG의 인스턴스는 InService 상태로 진입하면서 (설정이 되어 있다면) ELB에 추가됩니다.ASG의 인스턴스는 InService 상태에서 빠져나오면서 (설정이 되어 있다면) ELB에서 제거됩니다.이를 이용하면, 다음과 같은 시나리오로 배포를 할 수 있습니다.똑같은 ASG 두 개를 구성(Group B / Group G)하고, 그 중 하나의 그룹으로만 서비스를 운영합니다.Group B가 라이브 중이면 Group G의 인스턴스는 0개입니다.새로운 버전을 배포한다면, Group G의 인스턴스 숫자를 Group B와 동일하게 맞춰줍니다.Group G가 InService로 들어가고 ELB healthy 상태가 되면, Group B의 인스턴스를 전부 Standby로 전환합니다.롤백이 필요하면 Standby 상태인 Group B를 InService 로 전환하고 Group G의 인스턴스를 종료하거나 Standby로 전환합니다.문제가 없다면 Standby 상태인 Group B의 인스턴스를 종료합니다.이제 훨씬 빠르고 안전하게 배포 및 롤백이 가능합니다. 물론 실제로는 생각보다 손이 많이 가는 관계로(특히 PaaS인 GAE에 비하면), 이를 한번에 해주는 스크립트를 작성해서 사용중입니다. 대략 간략하게는 다음과 같습니다. 실제 사용중인 스크립트에는 dry run 등의 잡다한 기능이 많이 들어가 있어서 걷어낸 pseudo code 입니다. 스크립트는 사내 PyPI 저장소를 통해 공유해서 사용 중입니다.def deploy(prefix, image_name, image_version): '''Deploy specified Docker image name and version into Auto Scaling Group''' asg_names = get_asg_names_from_tag(prefix, 'docker:image:name', image_name) groups = get_auto_scaling_groups(asg_names) # Find deployment target set future_set = set(map(lambda g: g['AutoScalingGroupName'].split('-')[-1], filter(lambda g: not g['DesiredCapacity'], groups))) if len(future_set) != 1: raise ValueError('Cannot specify target auto scaling group') future_set = next(iter(future_set)) if future_set == 'green': current_set = 'blue' elif future_set == 'blue': current_set = 'green' else: raise ValueError('Set name shoud be green or blue') # Deploy to future group future_groups = filter(lambda g: g['AutoScalingGroupName'].endswith(future_set), groups) for group in future_groups: asg_client.create_or_update_tags(Tags=[ { 'ResourceId': group['AutoScalingGroupName'], 'ResourceType': 'auto-scaling-group', 'PropagateAtLaunch': True, 'Key': 'docker:image:version', 'Value': image_version, } ]) # Set capacity, scaling policy, scheduled actions same as current group set_desired_capacity_from(current_set, group) move_scheduled_actions_from(current_set, group) move_scaling_policies(current_set, group) # Await ELB healthy of instances in group await_elb_healthy(future_groups) # Entering standby for current group for group in filter(lambda g: g['AutoScalingGroupName'].endswith(current_set), groups): asg_client.enter_standby( AutoScalingGroupName=group['AutoScalingGroupName'], InstanceIds=list(map(lambda i: i['InstanceId'], group['Instances'])), ShouldDecrementDesiredCapacity=True ) def rollback(prefix, image_name, image_version): '''Rollback standby Auto Scaling Group to service''' asg_names = get_asg_names_from_tag(prefix, 'docker:image:name', image_name) groups = get_auto_scaling_groups(asg_names) def filter_group_by_instance_state(groups, state): return filter( lambda g: len(filter(lambda i: i['LifecycleState'] == state, g['Instances'])) == g['DesiredCapacity'] and g['DesiredCapacity'], groups ) standby_groups = filter_group_by_instance_state(groups, 'Standby') inservice_groups = filter_group_by_instance_state(groups, 'InService') # Entering in-service for standby group for group in standby_groups: asg_client.exit_standby( AutoScalingGroupName=group['AutoScalingGroupName'], InstanceIds=list(map(lambda i: i['InstanceId'], group['Instances'])) ) # Await ELB healthy of instances in standby group await_elb_healthy(standby_groups) # Terminate instances to rollback for group in inservice_groups: asg_client.set_desired_capacity(AutoScalingGroupName=group['AutoScalingGroupName'], DesiredCapacity=0) current_set = group['AutoScalingGroupName'].split('-')[-1] move_scheduled_actions_from(current_set, group) move_scaling_policies(current_set, group) 몇 가지 더…Standby 로 돌리는 것 이외에 Detached 상태로 바꾸는 것도 방법입니다만, 인스턴스가 ASG에서 제거될 경우, 자신이 소속된 ASG를 알려주는 값인 aws:autoscaling:groupName 태그가 제거되므로 인스턴스나 ASG가 많아질 경우 번거롭습니다.cloud-init 를 어느 정도 최적화해두고 ELB healthcheck 를 좀 더 민감하게 설정하면, ELB 에 투입될 때까지 걸리는 시간을 상당히 줄일 수 있긴 하므로, 단일 ASG로 배포를 하더라도 롤백에 걸리는 시간을 줄일 수 있습니다. 저희는 scaleout 시작부터 ELB에서 healthy 로 찍힐 때까지 70초 가량 걸리는데, 그럼에도 불구하고 아래의 이유 때문에 현재의 방식으로 운영중입니다.같은 방식으로 단일 ASG로 배포를 할 수도 있지만, 배포중에 혹은 롤백 중에 scaleout이 돌면서 구버전 혹은 롤백 버전의 인스턴스가 투입되어버리면 매우 귀찮아집니다. 이를 방지하기 위해서라도 (Blue-green 방식의) ASG 두 개를 운영하는게 안전합니다.같은 이유로, 배포 대상의 버전을 S3나 github 등에 기록하는 대신 ASG의 태그에 버전을 써 두고 cloud-init 의 user-data에서 그 버전으로 어플리케이션을 띄우게 구성해 두었습니다. 이 경우 인스턴스의 태그만 확인해도 현재 어떤 버전이 서비스되고 있는지 확인할 수 있다는 장점도 있습니다.다만 ASG의 태그에 Tag on instance 를 체크해 두더라도, cloud-init 안에서 이를 조회하는 경우는 주의해야 합니다. ASG의 태그가 인스턴스로 복사되는 시점은 명확하지 않습니다. 스크립트 실행 중에 인스턴스에는 ASG의 태그가 있을 수도, 없을 수도 있습니다.굳이 인스턴스의 Lifecycle 을 Standby / InService 로 전환하지 않고도 ELB 를 두 개 운영하고 route 53 에서의 CNAME/ALIAS swap 도 방법이지만, DNS TTL은 아무리 짧아도 60초는 걸리고, JVM처럼 골치아픈 동작 사례도 있는만큼 선택하지 않았습니다.물론 이 방법이 최선은 절대 아니며(심지어 배포할때마다 돈이 들어갑니다!), 현재는 자원의 활용 등 다른 측면에서의 고민 때문에 새로운 구성을 고민하고 있습니다. 이건 언젠가 나중에 다시 공유하겠습니다. :)

기업문화 엿볼 때, 더팀스

로그인

/