들어가면서

  [1편]에서 매니저로서 피해야 할 길(절망 편)에 대해서 이야기했다. 2편에서는 반대로 마주하고 나아가야 할 길(희망 편)에 대해서 말해보려 한다. 초보 매니저에게 도움이 되는 글이 되었으면 좋겠다.

마주하고 나아가야 할 길

작은 목소리에 귀 기울이기

  매니저가 된 이후 가장 어려웠던 부분은 구성원의 기분과 컨디션을 파악하는 일이었다. 팀원들에게 나의 의견을 강하게 전달하거나 회의에서 구성원과 반대되는 목소리를 내고 난 후에는 하루 종일 회의가 떠오르고 신경 쓰여 편히 쉴 수가 없었다. 구성원의 사기가 꺾이거나 실망하진 않았을지, 더 부드럽게 전달할 수 있는 방법은 없었는지 고민이 고민을 낳아 잠을 이루기 어려운 적도 많았다.(소심함이 문제였을지도...) 매니저로서 의사결정에 확신이 있었더라면 고민이 덜 했을지도 모르겠다. 하지만 나는 주니어 매니저였고 확신에 찬 결정을 할 수 있는 인사이트가 부족했다.
  이렇게 잠 못 드는 상황을 피하기 위해 구성원들에게 늘 반대 의견을 내어주길 요청했지만 사실 구성원들이 강한 목소리를 내는 것은 매니저인 나보다 더욱 어려움이 따르는 일이었다. 그래서 나는 구성원에게 강한 목소리를 내어주는 것을 요구하는 것에 앞서 그들이 내어주고 있는 작은 목소리를 듣는 것에 집중했다. 주니어, 그리고 비전공자로서 가지는 고민, 회사 복지 체계에 대한 개선 의견, 장비나 의자와 같은 소모품에 대한 불편함 등 흘러가듯 오가는 말들에 집중하여 피드백을 드렸다. 모든 요청에 대하여 대하여 해결책을 드릴 수는 없었지만, 작은 목소리에도 확실한 피드백을 드리는 것을 통해 구성원들의 의견 표현이 조금씩 늘어갔다. 작지만 분명한 피드백을 통해 활발한 의사소통을 이어간다면 프로덕트에 집중된 다양하고 강한 목소리도 편하게 낼 수 있지 않을까? 작은 목소리에 귀 기울이는것을 시작으로 구성원의 목소리에 집중하여 잠 못 드는 밤(서로의 상심을 걱정때문에)을 줄여가고 있다.   

동종 업계의 연봉 시세 확인하기

  스타트업처럼 작은 회사는 HR을 위한 전문 부서를 꾸리기 어려울 때가 많다. HR부서가 없는 작은 조직의 경우 연봉협상을 위해 사내 연봉 테이블과 업무성과 평가표에 절대적으로 의존하기보다, 1차 조직장의 리뷰와 회사와 프로덕트의 성과를 바탕으로 연봉협상을 진행하는 경우가 많다. 우리 팀은 여기에 서라운드 리뷰라는 팀 평가기준을 더했지만 정량적인 기준이 되기엔 부족한 면이 있었다. 이런 상황에서 프로덕트의 성장을 위해 함께 달려온 동료와 돈 이야기를 나누고 연봉을 결정해야 하는 것은 초보 매니저에게 여간 어려운 일이 아니었다. (당장 나의 연봉 협상 자리도 어려운걸...)

  더군다나 코로나 이슈로 인해 IT인력시장은 뜨겁게 달궈져 있었고 하루가 멀다 하고 "OO기업 신입 연봉 6천만원!", "이직 시 사이닝 보너스 1억 제공!" 등 자극적인 채용가 홍보 쏟아지며 연봉협상의 난이도를 더욱 높였다. 

  이런 상황에서 구성원의 이탈을 막고, 동기를 부여하면서도 회사까지 수용할 수 있는!! 회사와 구성원 사이에서 최대한의 협상 만족점을 찾아내야 한다는 고민에 빠졌다. 나는 그 고민을 업계 시세에 기반하여 연봉을 협상하는 방법으로 덜어 낼 수 있었다. 헤드헌터로부터 우리 팀원이 제의받은 금액, 초봉 6천이라는 기업의 인재 요구 수준, 이직한 동료가 받는 연봉과 해당 회사의 연봉 밴드, 전 직장 동료들의 재계약 연봉 등 최대한 많은 곳에서 업계 싯가?를 모았고 이를 토대로 구성원과 회사 사이에서 합의점을 찾을 수 있었다. 어렵게 모은 업계 시세정보는 나와 구성원, 그리고 회사에게도 계약 연봉을 가늠할 수 있는 좋은 척도가 되었고 연봉 협상을 조금이나마 가볍게 시작할 수 있는 출발점으로 활용할 수 있었다.

강약약약으로 처세하기 (강자에게 약하고, 약자에게도 약하게)

  회사의 정치적 구조를 생각하지 않고 프로덕트에 몰입할 수 있는 것은 스타트업이 가진 큰 매력점이다. 지나친 정치에서 벗어나기 위해 대기업에서 스타트업으로 이직하는 경우도 많다. 하지만 사람이 셋만 모이면 정치가 시작된다고 하지 않던가. 정도의 차이는 있겠지만, 성장하는 회사라면 구성원 간의 관계 복잡도 또한 올라가기 마련이다. 특히 매니저라는 직책은 협의를 통해 결과물을 만들어내는 것이 주요 업무이고, 정치적 구조안에서 풀어 가야 하는 일이 많기 때문에 매니저로서 협업을 위한 중심을 잡는 것이 중요하다.

  나는 그 중심을 '정정당당함'에서 찾으려 했다. '강자에게는 강하고 약자에게는 귀를 귀울이는 것'을 정정당당한 것으로 생각했고 이에 맞춰 회사에서도 강강약약의 자세로 협업을 진행했다. 특히 강강에 집중했던 나는 상위 결정권자의 의견을 더욱 엄격한 기준으로 판단했고, 팀원들에게 부끄럽지 않은 결정을 해야 한다는 생각에 몰입했다. 하지만 '정정당당'함을 지키기 위한 노력이 협업에 도움이 되거나 프로덕트의 성장에 긍정적인 영향을 미쳤을까? 아니다. 매니저로서 2년이 지난 지금 돌이켜 보면, 나의 강강약약은 프로덕트와 팀원보다 나의 자존심을 더 잘 지켜내는 방법이었다.

  회사는 수익을 내기 위해 협업을 하는 곳이다. 강자에게 강하게 대응한다고 해서 우리의 프로덕트가 눈에 띄게 성장하거나, 우리 팀이 더 편해지지 않는다. 팀과 프로덕트의 성장을 위해서는 강자에게도 약하고 약자에게도 약한 자세를 취해서 최대한 많은 의견을 끌어내고 수렴해서, 발화자의 직책 높낮이에 상관없이 프로덕트만을 위한 결정을 해야 한다. 나보다 직책이 높은 사람의 의견을 따른다고 해서 아부하는 게 아니며, 직책이 낮은 사람의 의견을 반대한다고 해서 군림하는 것이 아님을 늦게 알게 되었다. (정말 집중하고 잘 보이기 위해 노력해야 하는 곳은 유저와 프로덕트였던 것을....) 그때부터 강약약약으로 대응하려 노력하고 있으나 잘 하고있는지 판단하기 위해서는 좀 더 시간이 필요할 것 같다.

개밥 먹기

  개밥 먹기란 우리가 개발하고 있는 프로덕트를 일상적인 생활과 업무에서 사용해보는 것을 뜻하는 IT업계 용어다. 이스포츠 플랫폼을 개발하는 우리 팀의 경우 사내 게임대회를 레벨업지지에서 개최해 본다거나, 유저가 만든 대회에 직접 참가하는 것이 개밥을 먹는 것이다.

  매니저에게 개밥 먹기는 우리가 만들고 있는 것이 무엇인지 제작자의 관점에서 한발 벗어나서 객관적으로 볼 수 있는 좋은 도구이다. 여러 곳에서 쏟아지는 아이디어를 정리하며, 기능을 구현하고 더하는 것에만 집중하다 보면 우리가 만들고 있는 것이 무엇인지 놓칠 때가 있다. 그럴 때면 개밥 먹기를 통해 우리 프로덕트의 현황을 파악하고 해결해야 할 문제점들을 파악할 수 있다.

  그리고 개밥 먹기는 애자일 형태로 개발을 진행하는 조직에게 필수사항으로 업무시간을 들여서라도 진행 하는 것을 추천한다. 무엇을 만들 것인지, 어떻게 만들 것인지, 이번 변경사항이 얼마나 의미있었는지, 어디를 개선해야 할지 등 애자일의 과정 과정마다 좋은 아이디어를 낼 수 있는 원동력을 프로덕트에 대한 이해도로부터 얻을 수 있기 때문이다. 기획자와 디자이너는 물론 개발자와 운영자까지 유저로서 프로덕트를 이해한다면 각자의 파트에서 더욱 의미 있는 역할을 해낼 수 있을 것이라 믿는다. 

투명한 정보 공유

  매니저 업무를 시작하면서 관리해야 하는 정보의 양이 기존보다 두배 이상 증가했다. 실무자일 때보다 더 많은 회의에 참석했고, 더 많은 의사 결정에 참여함으로써 운영진과 타 부서의 정보도 챙겨야 했다. 매니저로서 얻는 정보를 어디까지 구성원에게 공유해야 할까?라는 물음표가 생겼고, 잦은 결정 변경으로 인한 피로감, 무뎌짐, 실패의식을 느꼈던 과거 실무자로서의 경험을 바탕으로 '확정된 것만 공유하자'라는 판단을 했었다. 

  하지만 대표님은 반대로 대부분의 정보(심지어 투자 현황까지)를 전 직원에 공유했고, 나의 우려와는 반대로, 정보를 받아들이는 주체였던 구성원들은 성숙하게 정보를 소화해내고 있었다. 빅픽처의 동료들은 나보다 훨씬 더 강하고 성숙한 사람들이었고, 모든 정보를 투명하게 공유하였을 때 실망하거나 걱정하기에 앞서 그들의 사유를 바탕으로 판단하고 있었다. 정보의 제한을 통해 피할 수 있었던 실망과 아쉬움을 감내해야하는 경우도 있었지만, 투명한 정보 공유로부터 오는 고통은 정보를 차단하고 필터링 하는 것으로부터 오는 부작용보다 건강하고 성장의 바탕이 되는 성장통이었다. 앞선 경험을 통해 나 역시 확정된 정보만 공유하는 것에서 모든 정보를 투명하게 공유하는 것으로 자세를 바꾸었다. 정보를 투명하게 공유함으로써 쌓이는 신뢰도는 우리를 더 단단하게 만들어 주었고 정보 공유를 더 편하게 할 수 있는 선순환구조가 자연스럽게 생겨나 매니저로서 정보를 관리해야 하는 어려움을 줄일 수 있었다.

마치면서

  [1편]은 하면 안 되는 것들에 대하여 적다 보니 편하게 쓸 수 있었나보다. 이번 편은 해야 하는 것들에 대해 적다 보니 소심함과 조심스러움이 더해져 문장들마다 쉼표가 늘어났다. 이제 막 3년 차에 접어드는 초보 매니저의 글로써 나와 비슷한 길에 올라선 분들께 조금이나마 도움이 되는 글이길 바란다. 다음 회고 때는 조금 더 자신감을 뿜뿜할 수 있기를....! 


김병규 (bk@bigpi.co)
새로움과 자유로움을 좋아하는 개발자입니다.
프로덕트의 성장은 구성원의 성장으로부터 온다고 믿습니다.  

들어가면서

  실무자가 매니저로 넘어가는 길에 도움이 되는 이정표를 작게나마 더하고 싶었다.

개발자로 7년을 일하다 7명을 팀원을 맡게 되고(자기소개의 글), 2년간 팀과 함께 성장하며 33명이 되기까지 경험을 바탕으로 피해서 돌아가야 할 것과 마주하고 정면 돌파해야 하는 것들을 정리해 보았다.

피하고 돌아가야 할 길

비교형 문장

  우리 팀은 반기마다 서라운드 리뷰(협업했던 동료를 서로 리뷰하고 평가함)를 진행한다. 팀을 리드하는 입장에서 좀처럼 받기 어려운 동료들의 피드백을 들을 수 있는 소중한 시간이다. 서라운드 리뷰를 통해 나는 "프런트엔드와 백엔드를 비교하며 표현하지 말아 달라."라는 피드백을 받았다. 할 일을 계획하는 부서회의 때 "A라는 기능은 트랜잭션 관리 때문에 프런트엔드 개발보다 백엔드 개발이 중요하다. 일정을 더 많이 잡아 달라"라는 표현을 하였고 이에 대한 피드백으로 주신 말씀이었다. 너무나 맞는 피드백이었고 늘 새기며 일하게 되었다. 업무량과 영향범위는 다를 수 있지만  무엇이 더 중요한 일은 없다. 더 중요하다고 생각되더라도 그것은 개인의 의견일 뿐 모두가 동의할 수 있는 사실은 아니다.
  비교하는 표현을 사용하면 부정적으로 표현되는 면이 생기기 마련이다. 매니저가 전체회의에서 하는 말은 영향력이 크기 때문에 비교형 문장은 피하자.

개발 리소스에 등 떠밀리기

  스타트업은 언제나 리소스가 부족하다. FE개발자에게 퍼블리싱 작업은 기본이고 데이터 지표 추적 같은 스크립트 추가는 일상이다. 이런 상황에서 새로운 프로젝트를 목표한 기간 안에 완성하기 위해 가장 쉽게 선택할 수 있는 선택지는 개발자 채용이다. (채용 난이도도 높지만 개발자 출신 뉴비 매니저였던 나는 '그 일정으로는 안됩니다'라고 말하고 실망감을 감내하는 것보다 채용을 통해 일정을 맞추는 방향이 좀 더 쉬웠다.)
  어려운 채용시장에 맞춰 주니어 역량의 구성원을 채용하고 성장을 유도하며 프로젝트를 완료한 뒤 우리 팀은 디자인/기획 리소스에 비해 개발 리소스가 큰 조직이 되었다.(팀 유지비용이 올라가는 것은 덤이었다.) 

이때부터 매니저로서의 나는 '개발자를 놀릴 순 없어!', '개발하지 않으면 재미가 없어져버린다구!'라는 생각을 하며 충분한 고민이 담기지 않은 기획서와 디자인을 요구했고, 유기적으로 돌아가는 팀을 보며 흐뭇해하기까지 했다. 높아진 팀 유지비용을 성과로서 상쇄해야 한다는 조바심이 더해져 상황은 좀처럼 나아질 기미가 없었다.

  조바심이 난 매니저는 어떤 행동이든 할 수 있는 무서운 동물이었다. 의욕이 넘치는 초보 매니저로서 회사를 위해 열심히 달리고 목표를 완수하기 위해 최선을 다했을 뿐인데 나는 예측 불가한 추진체가 되어 있었다. 자연스럽게도 개발 리소스에 등이 떠밀리게 되었고 매니저로서 부족한 결정을 하면서 팀과 프로덕트의 성장에 걸림돌을 만들었다. 

  사용자를 충분히 고려하지 않거나 모두를 만족시키기 위해 복잡한 기능을 설계하고, 유지보수에 사용되는 리소스 예측에 소홀해 매일같이 야근이 이어졌다. 무엇보다 철야하며 구현한 기능들이 유저에게 외면받을 때에는 그 원인을 나에게서 찾지 않고 시장을 탓하곤 했다. 자연스럽고 쉽게 빠져들 수 있는 실수로 인한 등떠밀림의 효과는 강력했고, 그로 인한 결과는 랜덤박스처럼 다방면에서 터져 나왔다. 매니저는 여유롭게 고민하는 시간이 많아야 한다는 조언을 들었을 때 이것은 무슨 소리인가! 했었는데 실수를 거듭하고 난 뒤에야 비싼 값을 치르고 그 의미를 체득하게 되었다. 다시 처음으로 돌아갈 수 있다면 개발 리소스를 늘리기 전에 충분히 고민하고 설계할 수 있는 기획/디자인 리소스 그리고 개발 리소스를 효율적으로 사용할 수 있는 매니저로서의 역량을 먼저 준비할 것이다.

매니징과 실무를 동시에 하기

  개발자에서 초보 매니저로 넘어온 나는 의욕이 넘쳤다. 동료들에게 기술역량이 있는 매니저로서 인정받고 싶었고 코드를 작성하는 실무를 통해 증명할 수 있다고 생각했다. 하지만 실무를 통해 구성원의 신뢰를 얻을 수 있었던 기간은 고작 6개월이었던 것 같다. 성장곡선이 너무나 가팔랐던 동료들은 내가 공유했던 기술 레벨 수준을 각자의 분야에서 빠르게 뛰어넘었고 나는 그들이 챙기지 못하거나 리소스 부족으로 담당자가 없는 업무들을 찾아 빈틈을 채우는 정도로 실무를 이어갔다.

  물론 실무를 이어가며 매니징을 하는데 문제가 없다면 실무에도 도움이 되는 매니저가 될 수 있었다. 하지만 나는 초보 매니저였고, 새로운 업무에 적응하기 위해 모든 리소스를 사용하여도 부족한 상황인 것이 문제였다. 그로 인해 매니저로서 해야 하는 것들을 놓치는 경우가 많았고, 실무에도 온전히 집중할 수 없어 리팩터링이 필요한 코드들이 쌓여갔다.

  매니징과 실무를 동시에 하느라 매일같이 야근하는 매니저, 거기에다 완성도 낮은 코드와 인사이트가 부족한 결정을 내리는 리더와 함께 일하는 동료들은 얼마나 힘이 들지 그때는 미처 생각하지 못했다. 초보 매니저라면 겸업은 피하고 매니저로서의 역할을 먼저 소화해 내는 것이 동료들에게 깊은 신뢰를 얻을 수 있는 길이 아니었을까. 역시나 다시 돌아간다면 매니저로서 역할을 할 수 있도록 온전히 노력을 쏟고 싶다.

K-애자일

스타트업이나 소프트웨어를 개발하는 IT조직에서는 애자일 방법론을 채택하는 경우가 많다.
K-애자일을 논하기 전에 찐 애자일 선언문 먼저 살펴보자.

공정과 도구보다 개인과 상호작용을, 포괄적인 문서보다 작동하는 소프트웨어를
계약 협상보다 고객과의 협력을, 계획을 따르기보다 변화에 대응하기를 가치 있게 여긴다.

Source: YouTube, Making Sense of MVP (Minimum Viable Product), https://youtu.be/0P7nCmln7PM

 

  나는 애자일 선언문을 바탕으로 최소 기능 제품(MVP)을 만드는데 주력하고 유저에 집중하여 완성도를 높여가는(때로는 목적지도 바꾸어가며) 형태로 일하려고 했다(근데 못했다). 지난 2년간 우리 팀은 정말 유저에 집중하고 있었을까? 프로덕트의 완성도를 높여가는데 집중하고 있었을까? 반문해보면 명확히 '아니다'라는 답이 돌아온다. 그러면 내가 추구하고 요구했던 개발 방법론은 무엇이었을까? 그것은 바로 K-애자일이었다 흑ㅠ흑ㅠ

우리가 했던 K-애자일은 이렇게 진행된다.

① 스프린트 결과에 대한 정량적, 정성적 성과를 측정할 수 있는 환경이 없다.

  이전 스프린트의 성과를 측정하지 않고서 다음 스프린트를 계획할 수 있을까? 스케이트보드가 제대로 완성되었는지 알기 어려운 상황에서 핸들을 추가한다면 그것은 킥보드로서 동작할 수 있을 것인가? 거듭되는 스프린트마다 행운이 따라야만 우리는 목적지에 다다를 수 있을 것이다. 

② 결과물(목적지)은 변경할 수 없으며, 개발 완료 시점이 확정되어있다.

  나는 스프린트로 작업해나가는 것을 관철했고 스프린트를 계획하고 종료하는 기준은 배포일이었다. 2주 또는 3주마다 배포일을 지정해두고 배포일에 맞춰 스프린트를 진행했다. 정해진 개발 완료 시점으로 인해 우리 팀은 유저와 기능에 대하여 충분히 고민할 시간은 줄어들고 기획 파트와 개발 파트의 갈등을 야기했다. 기획서 리뷰 시간에 개발자 의견이 더해지면 기획자는 다시 기획서를 업데이트해야 했고, 그만큼 개발할 시간은 줄어들기에 일정을 더 확보해야 했다. (그러나 일정을 추가하지 못하는 것이 K-애자일의 핵심!)

③ 기능 배포 시점을 팀 외부(타 부서, 고객사 등)에서 결정한다.

  앞선 ②번의 상황에서 기능의 퀄리티 높이기 위해서는 일정을 조율하고 다시 첫 스탭부터 수정해나가는 과정을 반복해야 한다. 하지만 기능 배포 시점을 우리가 조정할 수 있는 상황이 아니라면? 스프린트를 진행하는 중간에 꼭 개발해야만 하는 기능이 추가된다면? 무엇보다 그것이 전사 관점의 이슈(이것은 몹시 민감하고 예민하다.)가 반영되어있다면, 개발팀은 일정보다 늦게 완성된 기획서에 발맞춰 개발을 해내야만 한다. 이를 통해 우리는 파트 간 볼맨 소리, 퀄리티의 결핍, 우리가 원했던 건 이게 아닌 데와 같은 다양한 부작용을 얻게 된다.

④ ①번부터 ③번까지 과정을 반복한다.

  어쩌면 워터폴 방식이 어울렸을 우리 조직에 K-애자일이 자리 잡게 된 것은 애자일에 대한 이해도가 부족한 상태에서 애자일을 해야 한다고 큰소리로 주장했던 매니저(나)가 가장 큰 원인이었다. "한국 기업들에서 행해지는 애자일 방법론은 일을 빠르게 많이 시키기 위한 것이 아니냐"는 글이 커뮤니티에 올라올 때마다  "우리는 아니야(응 맞아)"라며 현실을 부정하는 내 모습이 늘 함께했다.

  다행히도 우리 조직은 얼마 전 애자일을 코치와 스크럼 마스터를 전담해주시는 @김영민 님을 만나게 되어 건강한 애자일 문화를 처음부터 다시 키워나가고 나가고 있다. 곧 건강한 애자일 문화에 대한 블로그를 올릴 수 있을 것 같아, 자조 섞인 K-애자일에 대한 설명은 서둘러 마친다.

Source: YouTube Product Owner in a Nutshell, https://youtu.be/502ILHjX9EE

마치면서 

  초보 매니저로서 피하고 돌아가야 할 길을 작성하다 보니, 내용이 너무 길어져 버렸다. 아마도 많은 길을 돌아왔기 때문이 아닐까? 매니저로서 역할을 더 잘했다면 피해 갈 수 있었던 길들을 돌아보면서, 어떤 문제든 팀에서 발생하는 문제는 모두 매니저의 책임임을 또 한 번 느낀다.😢 (그렇다면 잘한 것도 모두!? 🤣)

  피해 가야 할 길에 대해서 글을 쓰다 보니 반성문이 되어버렸다. 잘해온 것도 무척?이나 많기에, 반대로 마주하고 지나가야 할 길에 대해서 페이지를 분리해서 공유할 예정이다. 내가 작성한 글이 나와 비슷한 길에 올라서 있는 분들께 조금이나마 도움이 되고 실수를 덜할 수 있는, 혹은 잘못된 길은 피해 갈 수 있는 단서가 될 수 있으면 좋겠다. 

다음 글) 마주하고 지나가야 할 길 

  • 작은 목소리에 귀 기울이기
  • 동종 업계 연봉 시세
  • 강약약약
  • 개밥 먹기
  • 투명한 정보 공유 

김병규 (bk@bigpi.co)
새로움과 자유로움을 좋아하는 개발자입니다.
프로덕트의 성장은 구성원의 성장으로부터 온다고 믿습니다.  

들어가면서

  지난 데이터 대시보드 1편에서 공유한 데이터 대시보드를 고도화하고 당시 아쉬웠던 부분들(고객군 분리, 유저 획득 경로 상세화, 리소스 데이터 추가)을 보완한 내용을 공유한다. 데이터 대시보드 생성 이후 프로덕트를 담당하는 팀원들의  아이디어가 더해지면서 조금 더 실용적인 지표들이 추가되었다. 정답이 없는 질문에 최선의 답을 구하기 위해, 계속해서 데이터와 결과물을 조정해야 하는 작업이다 보니 소심해지고🤭, 집단지성🧠에 기대게 된다. 이 글도 집단지성이라는 호수에 한 방울💧 보탬이 되기를, 그리고 다음 단계를 제안하고 발전할 수 있는 발판이 될 수 있기를 바란다.🙏

대시보드 추가

  기존에 구축했던 데이터 대시보드에는 프로덕트 사용자의 흐름을 전반적으로 살펴보는 AARRR 분석만 존재했다. 동료들에게 AARRR 대시보드를 공유하고 나서, 프로덕트 담당자들이 가지고 있는 개발 스토리에 대한 기능별 대시보드 생성 요청을 받았고 이에 맞춰 3가지 대시보드를 추가했다. 새로운 대시보드를 추가하면서 데이터 구렁텅이에서 방향감각을 잃어 갈 때마다 아래 세 가지 포인트를 기준점으로 다시 정신을 부여잡고 작업을 진행했다. 새롭게 추가한 대시보드들을 소개하며 더불어 대시보드를 통해 개인적으로 얻을 수 있었던 힌트들을 첨언했다.

📌우리가 무엇을 위해 리소스를 투입하는지 목표에 대한 선명도를 높이자.(목표)
📌유저에게 전달한 스토리는 목표를 달성했는지 확인할 수 있어야 한다.(결과 측정)
📌다음 작업에서 우리가 집중해야 할 부분은 무엇인지 힌트를 제공하자.(계획)

  • 유저 랜딩 기능분석 (연관 페이지 : online.gamecoach.pro)
    • 목표
      • 신규 수강생 방문자에게 제품의 핵심 가치를 전달한다.
      • 방문자가 CTA버튼 통해 핵심 Task를 수행하도록 유도한다.
    • 결과 측정
      • 신규 수강생 페이지 참여도 지표 (참여율, 평균 참여 시간, 스크롤 90%, 기타 세부 이벤트)
      • CTA버튼 클릭률 (CTA 클릭 수 / 첫 방문자 수,  CTA 클릭 수 / 세션 수, CTA 클릭 수 / 페이지 방문 수)
    • 특이사항 & 힌트
      • CTA버튼 클릭률을 어떤 기준으로 보아야 할 것인지에 대한 고민이 필요하다. 첫 방문자의 CTA 클릭률이 유저 랜딩 페이지의 목적에 가장 적합하고 퍼센트도 가장 높은 상황이지만, 목표한 클릭률을 달성했는지 등의 성공 판단 기준이 추가된다면 다음 단계를 고민하는데 더욱 도움 될 것으로 보인다.
      • 기존 유저의 CTA 버튼 클릭률은 첫 방문자 대비하여 12 ~ 20% 낮은 상황이다. 재방문자에게는 상대적으로 CTA 버튼이 매력적으로 다가가지 못하고 있다. 재방문에게 맞는 CTA버튼이나 기능 등 리텐션과 연계해서 고민해 본다며 개선 목표를 정할 수 있을 것 같다.

CTA 클릭률 지표를 메인으로하는 유저 랜딩 대시보드

  • 코치 추천 기능분석 (연관 페이지 : online.gamecoach.pro/coach-matching
    • 목표
      • 신규 방문자와 코치님의 1:1 채팅 성사율을 높인다.
    • 결과 측정
      • 생애 첫 채팅과 일반 채팅 성사에 대한 커스텀 이벤트를 생성하여 분자에 대입하고 첫 발문자 수, 세션 시작 수를 분모로 활용하여 목표 결과를 측정할 수 있는 3가지 핵심 지표를 등록했다. 
      • 추천 코치 페이지에서 추천받기 시작부터 추천받기 완료, 추천 코치 카드 선택 등 스탭 별 참여도 지표를 생성하여 유저가 코치를 추천받는 스탭 별 정보를 시각화했다.
    • 특이사항 & 힌트
      • 커스텀 이벤트를 만드는 과정에서 이벤트 등록을 누락한 포인트가 있어서 중요 포인트인 `채팅 수` 값이 상당기간 오염되어 있었다. 확인 즉시 수정하였지만 과거 데이터와 비교가 불가하여 아쉬운 점이 있었다.
      • 추천받기 완료율 | 채팅 성사율 | 수강 결제 완료율 세 가지 지표 간에 강한 연관도가 있을 것으로 보인다. 수강률은 프로덕트 최종 목표에 직결되는 분명한 지표지만 수강률 상승까지 복잡한 단계와 설계가 필요하다. 반면 채팅 성사율 | 추천받기 완료율은 접근 단계가 단순하고, 수강률과 강한 연관이 있기 때문에 수강률 상승을 위한 분할 공략지점으로 활용할 수 있을 것으로 보인다.

수강생과 코치의 채팅 성사율을 메인으로하는 추천 코치 대시보드

  • 코치 찾기 기능분석 (연관 페이지 : online.gamecoach.pro/coach-finder)
    • 목표
      • 수강생이 코치를 검색하는 행동을 분석한다.
    • 결과 측정
      • 수강생이 어떤 코치를 찾는지, 중요하게 보는 이력은 무엇인지 등 유저의 니즈를 파악하는 것은 유저 페르소나를 선정할 때 매우 중요한 부분이다. 코치 찾기 페이지를 오픈 한 뒤 실제 유저가 검색하는 태그와 정렬 등 클릭 위주의 데이터를 수집하여 시각화했다.
    • 특이사항 & 힌트
      • 코치를 검색하기 위해 태그를 클릭하거나 우선순위 정렬을 변경하는 등의 액션이 페이지 뷰 대비 10% 이하로 측정되었다. 코치를 검색하고 정렬하는 기능 자체의 사용률이 낮은 것을 바탕으로 수강생이 코치를 검색하기 이전에 검색이 필요한 환경을 마련하는 것이 우선일지 고민해봐야겠다. 

코치 찾기에서 유저들이 사용하는 정렬기준 정보들

고객군 분리 & 유저 획득 경로 상세화

  대시보드 개선을 위해 유저 타입을 세분화하여 데이터를 필터링할 수 있는 뷰 유저 획득 경로를 상세화하여 주요 유입 포인트를 필터링할 수 있는 뷰를 제공하려 노력했다. 무엇보다 기능 안정화 이후 본격적인 마케팅을 진행할 예정이어서, 신규 유저의 획득 경로 파악이 꼭 필요한 상황이었다.

  먼저 유저 타입을 세분화하기 위해 GA4 연동 스크립트에 userId와 user_properties값을 추가하여 로그인 여부 및 유저 역할(수강생, 코치)을 GA4 보고서의 비교 기준으로 사용할 수 있도록 개선했다. 작업량은 크지 않았은데 레퍼런스가 적고 공유된 자료들은 쇼핑몰 기준이라 온라인 게임코치 서비스 특성에 맞게 커스텀하기에 어려움이 있었다. (결과적으로 GA4 연동 스크립트를 수정하여 대부분 gtag의 'config'기능을 사용하여 세팅했다. 비슷한 작업을 하실 분들을 위해 구글 공식문서를 추천한다.) userId와 user_properties값을 세팅한 이후에는 데이터 비교/필터링 기준으로 로그인 여부와 유저 타입 (코치인지, 학생인지)을 선택할 수 있게 되었고 코치와 유저라는 양 극단에 위치한 유저들의 데이터를 분리하여 확인/비교할 수 있게 되었다.

[개발환경 테스트 지표] 로그인 여부와 user_role을 기준으로 실시간 지표를 비교 할 수 있다.

  두 번째로 유저 획득 경로를 상세화 하기 위해 구글 캠페인 URL 빌더를 적극 활용했다. 구글  캠페인 URL 규격에 맞춰 생성한 URL을 통해 유저가 페이지를 방문하면 URL에 세팅한 referrer(source)와 medium, campaign name 등의 데이터를 얻을 수 있다. 이 기능을 이용하여 direct와 not set으로 뭉쳐져 있던 세션 유입 경로를 각 캠페인, 소스 등으로 세분화하여 확인할 수 있게 되었다. (드디어 direct와 not set의 유입 순위를 떨어뜨렸다. 😂)

획득한 트래픽 차트에서 기존에 최상위었던 referral, direct, not set이 하위로 내려간 모습

  앞서 설정한 두 가지 작업을 바탕으로 데이터 대시보드에서도 필터링을 쉽게 적용할 수 있었다. 이제 모든 대시보드에서 로그인 여부와 캠페인을 기준으로 데이터를 확인할 수 있다. 예를 들면 페이스북 캠페인으로 얻은 유저들의 지표와 틱톡 캠페인으로 얻은 유저들의 지표들을 디테일하게 비교하여 우리가 집중해야 할 매체들을 찾을 수 있게 된 것이다.

새롭게 추가된 데이터 데시보드 필터 옵션

구글 데이터 대시보드에 새로운 데이터 소스 추가

  프로덕트 매니저가 요구하는 지표들을 GA4와 데이터 대시보드가 제공하는 기본적인 기능으로 표현하기 어려울 때가 많다. 이를 극복하기 위해 gtag를 이용하여 커스텀 이벤트를 만들어 대응하고 있지만 부족함을 느끼고 있다. 예를 들어 `게임코치 온라인 유료 포인트를 가지고 있는 유저` 필터 옵션이나 `특정 이벤트를 기준 페이지별 발생 횟수`같은 지표를 구현하기 어렵다. 이를 극복하기 위해 Google BigQuery와 게임코치 온라인 DB를 대시보드의 데이터 소스로 추가하는 작업을 진행했다.

  • Google BigQuery
    • BigQuery는 대규모 데이터 세트에 대해서 쿼리를 신속하게 처리할 수 있는 클라우드 데이터 웨어하우스로 GA4에서 축적한 데이터를 기반으로 SQL과 유사한 구문을 사용하여 조회할 수 있는 기능 제공한다. 기존에 UA 버전에서의 GA에서는 BigQuery와 연결하는 BigQuery connector가 유료버전에만 포함되어 있었는데 GA4버전부터는 무료로 변경되어 손쉽게 사용할 수 있게 되었다. (BigQuery 자체는 유료이지만 무료 플랜으로도 충분히 사용 가능함)
    • Google BigQuery로 GA4 데이터를 들여다보니 모든 데이터는 이벤트 형태로 저장하고 있어서, 이벤트 발생 시 함께 저장한 상세 데이터(이벤트 발생 위치, 유저 식별자, 이벤트 벨류 등)를 기준으로 검색하거나 그룹화할 수 있었다. 덕분에 프로덕트 매니저가 요구하는 다양한 이벤트 기반 지표들(채팅 이벤트가 어떤 위치에서 얼마나 발생하는지 등)을 시각화할 수 있었다. (GA4를 조회하는 빅쿼리를 시작한다면 구글 공식 문서를 강추한다.)
  • 게임코치 온라인 데이터베이스
    • 구글 데이터 대시보드는 MSSQL, MYSQL 등 다양한 데이터베이스를 데이터 소스로 사용할 수 있게 도와주는 커넥터를 지원한다. 또한 데이터 소스들을 연결하는 JOIN 기능을 지원하기 때문에 GA에서 수집한 유저 정보에 기반하여 데이터베이스 정보를 더해 확장하면 강력한 분석 환경을 제공할 수 있다.
      하지만 구글 데이터 대시보드에 우리 게임코치 온라인의 데이터베이스 접근권한을 부여하는 것은 보안상 위험할 뿐만 아니라 법적으로도 검토가 필요한 민감한 사항이기 때문에 연동을 진행하지 않고 내부망에 구성된 별도의 분석 툴을 이용하는 것으로 결정했다. 서비스 DB와 연계가 필요한 지표 요청이 계속되고 있기 때문에 민감한 정보를 조회할 수 없는 DB 엔드포인트를 별도로 준비하여 연결하는 방법을 백엔드 팀장님과 함께 계속 고민하고 있다.

A/B테스트 환경 추가

  최근 12주간 축적해온 개발 내용을 프로덕션에 반영하고 게임코치 메인 페이지는 명확한 CTA 버튼을 노출하고 있다. 페이지의 목표가 명확해지고 예산을 사용하여 마케팅을 진행하려다 보니 A/B테스트에 대한 니즈가 생겼고 해결안으로 Google Optimize를 선택했다. Optimize는 사용하게 된 이유로 몇 가지 장점이 있는데 실제 사용해본 경험을 바탕으로 소개한다. 

  • GA4와의 연동
    • Optimize의 가장 큰 장점은 A/B테스트에서 가장 중요한 각 대안별 효과 측정을 GA4 데이터에 기반하여 자동으로 생성해주는 부분이다. 특히 A/B테스트를 생성할 때 테스트의 목적을 선택할 수 있으며, GA4에서 설정해둔 전환 이벤트를 선택할 수 있고, 부가적인 목표도 차순위 이벤트를 선택할 수 있기 때문에 A/B테스트 목적을 GA4와 일원화하고, 결과를 분석하는 뷰를 목적에 맞게 별도로 생성하는 수고를 덜 수 있다.  
  • 코드 수정이 필요 없는 대안 생성 툴
    • Optimize에서는 A/B테스트를 진행할 대안을 생성하는 툴을 제공한다. 개발자가 아니더라도 클릭만으로 새로운 대안을 추가할 수 있으며 코드 상의 변화도 없기 때문에 배포나 개발팀 도움 없이 대안을 관리할 수 있다. (레이아웃을 변경하거나 노출 순서를 변경하는 등 매우 강력한 기능을 제공하는 것을 보고 감탄만... 🤯)
    • 만약 서비스 프런트엔드를 Vue나 React를 이용하여 SSR형태로 개발했다면, 서버에서 랜더링 한 화면과 프론트에서 랜더링 한 화면이 다르기 때문에 hydration error가 발생할 수 있다. 이 경우 client에서만 렌더링 되도록 예전에 종선 개발자님이 공유한 client-only 컴포넌트와 같은 방법을 적용하면 해결할 수 있다.

Optimize에서 새로운 대안을 수정하는 중 

 

데이터 대시보드를 구축하면서 어려웠던 부분

  프로덕트 매니저가 필요한 데이터와 지표는 자연어 형태로 전달된다. 그리고 이런 요청들은 GA4, BigQuery, 데이터 대시보드, Optimize 등 앞서 소개한 데이터 소스와 툴들이 이해할 수 있는 문맥으로 변환되어야 한다. 데이터 지표를 만드는 과정 초반에는 툴들을 세팅하고 데이터 정합성을 맞추는 것이 어려웠지만, 현재는 매니저 분들의 자연어를 최대한 빈틈없이 기계어로 바꾸는 과정이 가장 어렵고 결과 만족도를 얻기 어려운 상황이다. 지표 생성을 요청 주시는 분들이 데이터 분석 환경과 친해진다면, 그리고 나의 번역 스킬을 높여 간다면 이런 어려움을 극복할 수 있다고 생각한다.(그래서 블로그도 열심히!✍) 울퉁불퉁 어려움 많은 비포장 도로 위에서 프로덕트 매니저와 협업하며 자연어를 기계어?로 번역한 예시를 첨부한다. 💬

  • 자연어 - '1월 15일을 기준으로, 기존 서비스 이용자가 아니었던 사람들의 데이터를 분리해주세요.'
    • '서비스 이용자'라는 기준을 페이지 방문 여부로 결정할 것인지, 전환 이벤트 발생자로 잡을 것인지, 구매를 완료한 수강생으로 잡을 것인지에 대한 논의가 필요하고 데이터 요청자와 콘텍스트를 맞추어야만 결과 지표를 바라보는 시점도 맞출 수 있다.
    • '1월 15일'이라는 기준을 충족하려면 앞서 논의한 '서비스 이용자'의 기준에 날짜 개념을 적용할 수 있어야 한다. GA4에서 날짜 기반으로 필터링 하기 애매한 것들이 있다. 예를 들어 '서비스 방문', '페이지 조회'를 기반으로 필터링하면 1년 전에 서비스를 마지막으로 방문한 유저가 재방문했을 경우 신규 방문자 속성을 가진 '기존' 방문자 이기 때문에 결과 분석 시 이런 예외 케이스에 대한 고려가 필요해진다. 
    • 이런 상황에서 프로젝트 매니저와 '서비스 이용자', '기간'에 대한 논의(개발자의 애원이었을 지도..👩‍💻)를 진행하고, 다른 예시 대시보드 참고하여 GA4 전환 이벤트 중 하나인 '구매자'를 기존 서비스 이용자로 구분자로 활용하고 데이터 대시보드 조회 기간을 수동으로 선택하여 '기존'이라는 조건은 적용하는 것으로 기계어 번역을 마쳤다. (프로젝트 매니저의 요구사항을 깔끔하게 구현하지 못한 것과 불완전할 수 있는 지표로 인해 번역 품질 만족도가 걱정된다. 어렵고 개선이 필요한 부분이다.)

다음 단계

  지금까지 우리 서비스에서 발생하는 데이터를 수집하고, 팀원 모두 간편하게 확인할 수 있는 기반을 다졌다. 지금부터는 '제품 구매'라는 최종 목표로 더 정확하게 나아갈 수 있는 '중간 목표'를 만들어 데이터 대시보드의 실용성을 높이고 싶다. 예를 들면 스트리밍 서비스의 경우 수익이 발생하는 최종 목표가 '도네이션' 또는 '광고 등록'일 때, 최종 목표에 도달한 유저가 공통적으로 가지고 있는 이벤트를 분석하여 '중간 목표'를 '스트리밍 5분 신청'으로 찾아낼 수 있다. 반대로 '스트리밍 5분 지속 시청 여부'를 기준으로 유저를 분리하고 두 집단의 전환율을 비교하여 1차적으로 가설을 증명해 볼 수도 있다. 만약 '스트리밍 5분 지속 시청'이라는 이벤트가 전환율에 중요한 영향을 미친다면 이것을 '중간 목표'로 잡고 '최종 목표'보다 좀 더 명료한 계획과 실행, 증명을 할 수 있을 것이다.

  우리 서비스인 게임을 1:1로 강의하는 게임코치 온라인의 경우 '제품 구매'를 최종 목표로, '코치와 1:1 대화'를 중간 목표로 잡아 볼 수 있다. 하지만 이런 생각을 상상과 가설에 기반하지 않고 데이터에 기반하여 중간 목표 설정이 타당한지, 최종 목표에는 얼마나 영향을 미치는지, 우리의 작업이 중간 목표 달성률을 올렸는지 등을 데이터에 기반하여 판단해야만 한다. 다음 단계로 '중간 지표'를 찾고, 이를 통해 '최종 목표'에 긍정적인 영향을 미칠 수 있는 대시보드 실용성 향상에 집중해 보겠다. 조만간 즐거운 마음으로 데이터 대시보드 3편을 쓸 수 있기를 바라본다.


김병규 (bk@bigpi.co)
새로움과 자유로움을 좋아하는 개발자입니다.
프로덕트의 성장은 구성원의 성장으로부터 온다고 믿습니다. 

들어가면서

  우리는 게임코치온라인(https://online.gamecoach.pro/) 서비스를 애자일 하게 개발하고 있다. 애자일의 가치와 효과를 경험하면서 목적지를 향해 (애자일 코치님의 팀 건강 검진 결과에 따르면🩺) 즐겁게 항해하고 있으며 K-애자일로 변질되지 않도록 부단히 노력하고 있다. 애자일은 함께 일하기 위한 관점으로써 우리가 무엇을-어떻게 만들어야 할지 안내하는 눈이 되어준다. 우리의 시야를 더욱 분명하게 개선하기 위한 노력 중 하나로 데이터 대시보드를 만들고 있으며, 현재까지의 과정을 공유한다.

애자일을 위한 데이터 대시보드?

  스타트업은 모호함을 명확함으로 바꿔가는 과정이라고 생각한다.(by 모호좌👨‍💻)  우리는 끊임없이 무엇을 할 수 있는지, 언제까지 할 수 있는지에 대한 물음에 대답을 해야 하며, 결과물을 통해 어떤 성과를 얻을 수 있을지(또한 무엇을 얻었는지) 예측할 수 있어야 한다. 애자일 하다는 것은 어쩌면 이러한 질문들에 조금 더 정확한 대답을 찾기 위해 노력한다는 것일 수 있겠다. 하지만 완벽한 애자일 방법론이 완벽한 답변을 만들 수는 없다. 앞서 말했듯 애자일은 모호함을 선명하게 보기 위한 관점이기 때문이다. 우리는 선명한 시야를 가지고 서비스에 맞는 정답을 향해 나아가야만 정답에 가까워질 수 있다. 

  애자일을 위한 데이터는 우리 관점의 정확함을 검사할 수 있는 시력검사표 역할을 한다. 그것은 서비스가 현재 위치하는 곳을 제대로 파악하고, 우리가 예측했던 것이 얼마나 정확했는지 혹은 얼마나 틀렸는지를 바라볼 수 있는 검사 지표로써 동작할 것이다. 매번 돌아오는 스프린트의 성과를 측정하지 않으면서, 좋은 서비스로 나아가는 것을 기대한다면 복권 당첨을 기대하는 것과 다를 바 없다. 스케이트 보드의 바퀴가 제대로 굴러가지 않는 걸 모른 채 핸들을 추가한다면 그것은 킥보드로써 역할을 할 수 있을까?

  애자일을 위한 여정에 동참하고, 지속적인 물음에 대답하기 위해 데이터 대시보드를 만들고 있다. 현재까지 구성한 대시보드는 다음과 같은 목표를 가지고 있다.

📌팀의 모든 구성원이 쉽게 확인할 수 있어야 한다.
📌데이터에 기반하여 아이디어를 구현할 수 있는 틀을 제공 해야 한다.
📌기능 추가/변경 시점을 기준으로 우리가 의도한 바를 달성하였는지 확인할 수 있어야 한다.
📌설계자의 질문에 대답할 수 있어야 한다. ex) 특정 기능 사용률, 수정한 기능에서의 구매 전환 변동률 등

기존 데이터 대시보드의 아쉬웠던 부분

  레벨업지지 플랫폼을 개발하기 위해 Google 애널리틱스(이하 GA)와 아파치 제플린(디자인 툴 Zeplin 과는 다름)을 이용한 데이터 대시보드를 이용하고 있었다. 아파치 제플린을 이용하여 데이터베이스와 로그 저장소에 있는 데이터를 상세하게 조회할 수 있고 GA를 통해 웹에서 발생하는 이벤트들을 파악할 수 있지만 애자일 측면에서 몇 가지 아쉬운 부분이 있었다.

📌 대시보드로써 스토리가 부재하다. 

  🔸 데이터 대시보드는 표현하고자 하는 스토리가 분명해야 설득력 있게 데이터를 전달할 수 있다. 하지만 제플린으로 구성한 대시보드는 기능별(유저 사용성이 아닌)로만 대시보드가 분리되어있어 지표들 간의 상호관계가 없다.
  🔸 아파치 제플린으로 구현된 데이터 대시보드는 근엄하고 위압감이 느껴진다. 개발자에게는 각종 테이블에서 뿜어내는 숫자들과 실시간으로 업데이트되는 데이터들이 다정하게 다가갈 수 있지만, 서비스를 운영하고 기획하는 분들에게는 대화를 거부하는 답정너 대시보드처럼 보일 수 있다.   

apache zeppelin 예시화면

📌유저와 서비스 간의 상호작용을 확인하기 어렵고, 특정 시점 기준으로 데이터를 비교하기 어렵다.

  🔸 GA로 유저 이벤트를 수집하고 있었으나 이를 들여다볼 커스텀 보고서가 없었기에 이벤트 발생 빈도, 종류 등 단편적인 정보만 확인하고 있었다. 또한 커스텀 보고서를 이용하더라도 GA를 이용해서 확인할 수 있는 이벤트 상세 정보 (이벤트 벨류, 이벤트 로케이션 등)를 파악하기 어려운 문제점이 있다.

  🔸 제플린으로 표현하는 차트들은 기간을 기준으로 비교하기 어렵다. (GA에서는 최근 2주에 대한 데이터를 조회하면 자동으로 과거 4~3주 차의 데이터와 비교해서 지표 증감률을 표현해준다.) 

새롭게 추가된 데이터 대시보드

  게임코치 온라인 서비스를 개발하는 팀원 전체가 이해하기 쉽고 친근하게 다가갈 수 있는 대시보드 형태가 무엇일까 고민하다 AARRR 해적 지표를 구글 데이터 스튜디오로 구현하는 것을 목표로 첫 번째 대시보드를 만들었다. 우리 서비스의 현재 상황을 파악할 수 있고 개선해야 할 포인트를 드러내서 새로운 아이디어를 유도할 수 있는 지표라고 생각했기 때문이다. 두 번째로 특정 기능에 집중하지 않고, 유저 스토리를 기준으로 데이터를 표현하는 대시보드를 추가했다. 예를 들면 '코치 찾기 스토리' 대시보드의 경우 '유저들은 어떤 추천 태그를 많이 클릭하는가?', '유저들은 어떤 정렬을 많이 적용하는가?', '유저들은 코치 찾기 페이지를 얼마나 탐색하는가?'에 대한 기획자의 질문에 대답할 수 있는 데이터를 중심으로 시각화해서 데이터 가독성을 높이는데 집중한 것이다.

📌AARRR FUNNEL 대시보드

  🔸 서비스를 이용하는 유저의 획득부터 수익, 레퍼럴까지 FUNNEL(깔때기) 형태로 데이터를 표현하여 위에서부터 아래로 데이터를 읽어 갈 수 있도록 구성했다. 데이터를 읽어가는 방향을 제공한다는 점에서, 기존에 사용 중인 GA 보고서와 제플린의 차트와 가장 대비되는 부분이다.

  🔸 대시보드 구성을 위한 데이터 소스로 GA4(새로운 버전의 GA로 빅쿼리를 무료버전에서도 사용할 수 있다.)와 GA3(GA4 이전 버전인 UA 버전), Google BigQuery를 모두 사용하여 GA에 친숙하지 않은 사용자도 배포일 기준으로 방문자나 전환(수익)이 얼마나 발생했는지 쉽게 확인할 수 있다.

  🔸 AARRR 대시보드를 통해서 우리가 집중해야 하는 부분을 파악할 수 있고 "새로운 기능 A는 무엇을 개선하기 위함인가?", "이번에 배포된 기능 B는 우리가 개선하고자 했던 목표를 달성했는가?" 등 팀원들의 질문에 대답할 수 있다. 그리고 데이터에 기반하여 지나간 스프린트를 회고하고 우리가 했던 예측과 대답들이 얼마나 정확했는지 판단할 수 있기 때문에 우리 대답의 정답률을 높이는데 도움이 될 것이다.

게임코치온라인 개발환경 AARRR 대시보드

📌코치 찾기 페이지 대시보드

  🔸 스프린트 리뷰&회고 시간을 통해 AARRR대시보드를 팀에 공유 한 뒤, 서비스 전체 관점이 아닌 에픽이나 유저 스토리 기준의 대시보드의 필요성에 대해서 대화를 나누었다. 
  우리는 에픽이나 스토리의 성과를 측정할 수 있는 지표를 데이터 기반으로 설정하여 무엇이 목표인지 분명히 하고자 했다. 또한 유저 스토리 적용 전부터 부재되어있던 데이터를 미리 쌓아 기능 배포 이전과 이후를 비교할 수 있는 환경을 구축하고자 했다. (스토리 적용 전/후 비교를 통해 에픽의 성공 여부와 개선점을 다시 찾을 수 있기 때문에)
  그래서 에픽을 작성하는 단계에서부터 에픽의 목적과 효과를 데이터에 기반하여 설정하였고, 유저에게 개발 내용이 전달되었을 때 변화를 측정할 수 있도록 데이터 수집 요청 일감을 스토리 개발과 같은 스프린트에 추가하여 데이터 대시보드 구성을 하나의 업무로서 진행하게 되었다.
  🔸 에픽이나 유저 스토리 기반의 데이터 대시보드는 유저의 클릭, 스크롤 상황 등 DB에 저장하지 않는 단순한 유저 상호작용을 모아 메인 데이터로 사용한다. 
그렇기 때문에 GA4, GA3(UA버전)을 데이터 소스로 만들었던 AARRR대시보드와는 달리 GA4로 수집하고 있는 유저 이벤트를 좀 더 상세히 분석해야만 했고 GA4를 연동한 구글 BigQuery를 데이터 소스로 추가하였다. GA4 <-> BigQuery를 이용하면 이벤트의 상세 내용까지 추출할 수 있고, 데이터 요청자의 질문에 더 상세하게 대답할 수 있게 때문에 데이터 대시보드를 구축하는데 큰 도움이 되었다. 

게임코치 온라인 개발환경 코치찾기 대시보드

앞으로 개선이 필요한 부분

  애자일을 위한 데이터 대시보드를 공개하고, 다양한 의견들이 더해지면서 개선이 필요한 부분들을 찾을 수 있었다. 수강 유저와 코칭 유저로 고객군을 분리하는 것, GA 유저 획득 소스가 not set과 direct 부분을 분석하는 것 등 공감되는 부분이 많았다. 의견들을 반영하여 대시보드를 개선하기 위해 다음과 같은 작업을 진행할 계획이다.

📌GA 수집 데이터 상세화

  🔸 고객군 분리: 게임코치 온라인은 코치님과 수강생을 연결하는 서비스를 제공하기 때문에, 고객군을 수강 유저와 코칭 유저로 나눌 수 있다. 두 개의 고객군에게 제공하는 서비스와 집중해야 할 데이터가 다르기 때문에 대시보드도 분리하여 볼 수 있는 기능이 필요하다.

  🔸 유저 획득 들여다보기: GA특성상 많은 유저 획득이 not set이나, direct로 집계된다. AARRR대시보드의 분석 첫 단계로 획득 단계가 구체화될수록 기초를 단단하게 할 수 있을 것이다. 유저 획득을 상세화하고 구분하는 것은 GA를 사용하는 많은 팀들이 같이 고민하는 이슈로 UTM, 뷰저블 등 다양한 방법을 이용하여 not set과 direct를 줄여보려 한다.

📌대시보드 데이터 리소스 추가 & 연결(Join)

  🔸 코칭 서비스의 친구 초대 이벤트를 상세 추적하거나, 프로모션에 투입된 비용 대비 유적 획득 비용을 계산하는 대시보드를 작업하기 위해서는 GA와 서비스 DB, 광고 비용 정보 등 다양한 데이터 리소스를 연결(Join) 해서 보아야 한다. 예를 들어 GA에서 추적한 데이터와 코칭 서비스 DB를 연결하여 대시보드를 구성한다면 구매 유저의 수강 횟수나, 잔여 포인트 현황 등을 함께 볼 수 있어 좀 더 리더블한 대시보드를 만들 수 있을 것이다.

마무리

   우리는 더 좋은 서비스를 만들기 위해 애자일 하게 일하고 있다. 지속적으로 쌓아온 데이터를 분석하여 예측을 하고 유저와 상호작용을 분석하여 예측 결과를 평가한다. 그리고 더 나은 예측을 기대한다. 좋은 서비스를 만드는 것은 서비스를 만드는 사람들의 공통적인 목표일 것이다. (애자일 방법론을 떠나서라도) 팀에서 활용하고 있는 데이터에 스토리를 더하고 더 나은 예측을 위한 노력은 좋은 서비스를 만드는데 도움이 될 것이라 믿는다. 계속해서 발전시켜나갈 데이터 대시보드가 더 좋은 제품을 위한 우리 팀의 노력에 보탬이 될 수 있으면 좋겠다.


김병규 (bk@bigpi.co)
새로움과 자유로움을 좋아하는 개발자입니다.
프로덕트의 성장은 구성원의 성장으로부터 온다고 믿습니다.  

개요

2분 마다 시총 상위 100개 코인에 대한 가격정보를 크롤링하려 한다.
크롤링을 위해서는 Batch Job 과 Scheduler가 필요한데, 어떤 환경으로 구성할까 고민하다 Batch Job은 Spring Batch로 Scheduler는 Jekins로 구성했다.

Spring Batch를 쓴 이유

1. Spring boot를 사용중이라 기존에 만들어둔 Service를 재사용할 수 있다.
2. Transaction, Datasouce 관리가 편하다.
3. 익숙한 환경 (Java, RestTemplate, Error handling 등등)

Jekins를 쓴 이유

1. GUI 환경 Batch Job 관리가 편리하다. (상태확인, 실행 주기 관리, 로그 확인 등등)
2. Jenkins에서 Jar 형태의 Batch Job Application 을 직접 실행할 경우, 로그 확인이 편리

문제점

Spring Batch 를 단순 크롤링 작업으로만 사용하기에는 오버스팩!
오버스팩인것은 맞다. 하지만 구현 속도는 가장 빠르다.

Q Spring Batch 는 Database Table로 Job을 관리하는데 Jekins로 실행한다고?
A Database Table를 이용하지 않고, In - Memory 에서 작동하도록 설정했다. 

Q Batch Application를 이중화 하려면? 
A 이중화가 불가능한 문제다. 다만, 배포를 위한 중단시간이 제로에 가깝고, 잡을 분리하는 등 운영방식으로 해결할 수 있다.

구현

1. 스프링 배치 설정

Spring Batch 의존성 추가 (Spring boot 2.1 기준)

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>

Batch Config, Batch Job 생성 
(InMemoryBatchConfigurer는 따로 생성했다.)

@Configuration
@EnableBatchProcessing
public class BatchJobConfig {

@Autowired
JobBuilderFactory jobBuilderFactory;
@Autowired
StepBuilderFactory stepBuilderFactory;

@Bean
public BatchConfigurer batchConfigurer() {
return new InMemoryBatchConfigurer();
}

@Autowired
private UpdateTop100CoinPriceTasklet updateTop100CoinPriceTasklet;

@Bean
public PlatformTransactionManager transactionManager() {
return new ResourcelessTransactionManager();
}

@Bean
public Job updateTop100CoinPriceJob() {
return jobBuilderFactory.get("updateTop100CoinPrice")
.start(updateTop100CoinPrice())
.build();
}

@Bean
public Step updateTop100CoinPrice() {
return stepBuilderFactory.get("updateTop100CoinPrice")
.tasklet(updateTop100CoinPriceTasklet)
.build();
}
}

Batch Tasklet 생성
Tasklet과 InitializingBean을 구현하면 된고 RepeatStatus를 통해 Tasklet 반복여부를 선택할 수 있다.
Tasklet에서 실행시킬 로직은 기존 Service Bean 주입을 통해 해결할 수 있어서 편

@Component
public class UpdateTop100CoinPriceTasklet implements Tasklet, InitializingBean {

@Autowired
MarketCapService marketCapService;

@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
marketCapService.updateTop100CoinPrice();
return RepeatStatus.FINISHED;
}

@Override
public void afterPropertiesSet() throws Exception {

}
}

In - Memory Batch Configurer 설정
Database Table로 Job을 관리하면 여러모로 신경써야할게 많다. 단순한 목적에 맞게 Memory에서 관리하자. 

public class InMemoryBatchConfigurer implements BatchConfigurer {

private PlatformTransactionManager transactionManager;
private JobRepository jobRepository;
private JobLauncher jobLauncher;
private JobExplorer jobExplorer;

@Override
public PlatformTransactionManager getTransactionManager() {
return transactionManager;
}

@Override
public JobRepository getJobRepository() {
return jobRepository;
}

@Override
public JobLauncher getJobLauncher() {
return jobLauncher;
}

@Override
public JobExplorer getJobExplorer() {
return jobExplorer;
}

@PostConstruct
public void initialize() {
if (this.transactionManager == null) {
this.transactionManager = new ResourcelessTransactionManager();
}
try {
MapJobRepositoryFactoryBean jobRepositoryFactoryBean =
new MapJobRepositoryFactoryBean(this.transactionManager);
jobRepositoryFactoryBean.afterPropertiesSet();
this.jobRepository = jobRepositoryFactoryBean.getObject();

MapJobExplorerFactoryBean jobExplorerFactoryBean =
new MapJobExplorerFactoryBean(jobRepositoryFactoryBean);
jobExplorerFactoryBean.afterPropertiesSet();
this.jobExplorer = jobExplorerFactoryBean.getObject();

SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.afterPropertiesSet();
this.jobLauncher = jobLauncher;
} catch (Exception e) {
throw new BatchConfigurationException(e);
}
}
}

2. Application 실행환경 설정

Application Type을 WEB에서 NONE으로 변경했다. 이제 포트를 찾지 않아도 되어서 여러 Job들을 실행할 수 있다. (프로세스 생성비용은 늘어나지만..)
Spring Batch CommandLineJobRunner 를 사용할까 하다가, 어렵고 과한것 같아서 Application 실행 변수로 Batch Job 명을 전달 받아 실행하는 방식으로 세팅했다. 

@Slf4j
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
@EnableAsync
@EnableScheduling
@EnableCaching
@EnableTransactionManagement(proxyTargetClass = true)
@EnableJpaRepositories(basePackages = "com.happy.life")
public class BatchApplication {

public static void main(String[] args)
throws JobParametersInvalidException, JobExecutionAlreadyRunningException,
JobRestartException, JobInstanceAlreadyCompleteException {

SpringApplication app = new SpringApplication(BatchApplication.class);
app.setWebApplicationType(WebApplicationType.NONE);
ConfigurableApplicationContext ctx = app.run(args);
JobLauncher jobLauncher = ctx.getBean(JobLauncher.class);

if (args.length > 0) {
Job job = ctx.getBean(args[0], Job.class);
jobLauncher.run(job, new JobParameters());
} else {
log.error("배치 잡 이름을 입력해 주세요. EX) {}", "java -jar app.jar updateTop100CoinPriceJob");
}

System.exit(0);
}
}

3. 젠킨스 설정

젠킨스를 설치하고 깃헙 연동하는 과정은 생략한다.

배포하기

젠킨스에서 Freestyle project를 하나 만들어서 Batch Application을 배포하자.
Github에서 소스를 받아와 maven build하는 기본 빌드 이후에 jar를 복사한다.  yes|cp -rf 옵션을 줘서 항상 덮어쓰기 하도록한다.
이렇게 하면 순단없이 아주 빠르게 배포? 한다고 말할 수 있지않을까? 호호호호

실행하기

배포한 jar를 실행한다. Jenkins의 Build periodically값을 조정해서 2분만다 실행하도록 한다.
그리고 Execute shell 에서 Java -jar 옵션으로 배치를 실행한다.

결과 확인

Job에서 뱉어내는 로그들을 잘 기록하면서 수행된 것을 확인할 수 있다. 오예 끝~!


결론

Spring Batch가 소잡는 칼이라 닭을 더 쉽게 잡을 수 있다.

문제

Spring Batch를 사용하기 위해 Maven 의존성을 추가하고, BatchConfig.java를 설정을 완료했다.
그리고 배치를 시작하는 순간 transactionManager bean이 중복선언 되었다는 에러를 마주했다.
배치에서 새로 생성한 transactionManager 때문에, 기존 프로젝트에서 사용하던 기존의 transacrionManager bean을 등록할 수 없다는 이유였다.

@Configuration
@EnableBatchProcessing
public class BatchJobConfig {

@Autowired
JobBuilderFactory jobBuilderFactory;
@Autowired
StepBuilderFactory stepBuilderFactory;

@Bean
public Job updateSomethingJob() {
return jobBuilderFactory.get("updateSomethingJob")
.start(somethingStep1())
.build();
}

@Bean
public Step somethingStep1() {
return stepBuilderFactory.get("updateSomethingJobStep1")
.tasklet((contribution, chunkContext) -> {
log.info("Running UpdateSomethingJobStep1...");
return RepeatStatus.FINISHED;
})
.build();
}
}

APPLICATION FAILED TO START *************************** Description:

The bean 'transactionManager', defined in class path resource [...], could not be registered. A bean with that name has already been defined in class path resource [SimpleBatchConfiguration.class] and overriding is disabled. Action: Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

해결 시도

  1. "아 왜 배치용 transactionManager Bean 이름을 'transactionManager'로 한거야... batchTransactionManager 이런걸로 하면 안되나" 하여 불평.
  2. DefaultBatchConfigur, SimpleBatchConfiguration Bean을 상속받아서 열심히 getTransactionManager()를 열심히 오버라이딩 시도.
  3. 오버라이딩 하며 한창 삽질하다, Error description 에서 BatchConfiguration을 내가 만든게 아니라, SimpleBatchConfiguration.class 만 보던것을 발견. 
  4. 왜 그러지? 왜 왜 bean overriding이 안되지 고민 끝에 Error 내용 중에 Action 문구를 발견함. 

Action: Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

   5. 구글링해보니 Spring Boot 2.1 부터 사고를 방지하기 위하여 Bean Overriding이 기본 비활성화 됨. - Spring Boot 2.1 Release Notes

Bean Overriding

Bean overriding has been disabled by default to prevent a bean being accidentally overridden. If you are relying on overriding, you will need to set spring.main.allow-bean-definition-overriding to true.

  6. Bean Overriding을 활성화 하기 위해 application.yml에 spring.main.allow-bean-definition-overriding: true 옵션 추가.
  7. 파-워 해결.

결론

Spring boot 2.1 부터는 bean definition overriding이 false인것을 기억하자.
분명 이전에도 비슷한 문제를 겪었었는데.... 


R3는 금융 서비스를 위해 설계된 엔터프라이즈 블록체인 플랫폼인 corda를 개발하고 서비스하는 소프트웨어 회사다. R3는 2014년 소규모의 가족 회사로 출발했는데 R3중 R은 CEO인 David Rutter의 성을 뜻하고, 3은 공동 설립자의 수를 뜻한다.

R3는 2014년 동안 암호화폐 회사들이 금융 기관들을 대체하고자 하는 시도를 계속해서 지켜봐왔고, 블록체인 기술이 금융시장에 가져올 혁신을 대응하기 위해 비트코인과 이더리움 등 성공적으로 동작하고 있는 블록체인에 대한 연구를 진행했다. 연구의 목적은 암호화폐 회사들의 공격에 대응하는 것도 있었지만, 블록체인을 금융시장에 어떻게 적용할지, 실무적인 측면에서 바라보고 금융시장에 적용할 수 있는 기존 블록체인 기술에 대한 리뷰와 선택을 진행하는 것도 포함되어 있었다.

R3는 2014부터 2015년까지 수차례의 컨퍼런스를 진행하면서 블록체인과 분산원장에 대한 차이점을 분명히 하는 최초의 논문인 CaaS(Consensus-as-a-Service)를 발표했고, FX 결제를 초함한 구체적인 금융 유즈케이스를 포함한 협의를 진행했다. 또한 다양한 은행과 블로체인 기업, 금융기관들이 함께하는 컨소시엄을 구성하게 되면서 2015년 11월 말 42개의 금융 기관이 참여하는 DLG(Distributed Ledger Group)를 발표하며 R3라는 이름을 대체하게 되었다.

IBM에서 하이퍼레저를 개발하던 Richard Brown은 여러차례의 컨퍼런스와 대화를 통해 기존의 블록체인 플랫폼으로는 실 금융시장에 적용할 수 없다는 판단을 내렸고, 영국 버클레이 은행의 아키택트였던 James Carlyle, 비트코인 초기 개발자 Mike Hearn 등을 영입하여 Corda라는 새로운 금융 특화 블록체인 플랫폼을 개발하였고 2016년 1분기 말에 Corda라는 새로운 시스템을 구축하였다. 2016년 4월 5일 Richard는 Corda가 무엇인지, 설계 목표가 무엇진지, Corda는 블록체인이나 Cryptocurrency가 아니라 분산 원장이라는는 점에 대해 처음으로 공개적으로 설명했다. (Corda의 블록체인에 대한 기술적 정의는 다음편에 다루는 것으로...) 

기존의 비트코인, 이더리움, 리플과 같은 블록체인 플랫폼을 실 금융시장에 적용할 수 없었던 이유에 대하여 백종찬님의 글을 인용하였다.

1. 데이터 프라이버시

기존의 블록체인 구조는 네트워크 참여자가 모든 거래 내역을 보관하고 열람하는 구조다. 즉 A와 B간의 거래를 C가 검증해야한다. 고객 또는 자산의 기밀성을 보호해야 하는 금융산업에서 이와 같은 공개적인 데이터 검증 구조는 적용될 수 없다.

2. 확장성

모든 네트워크 참여자가 거래에 대한 합의를 도출하고 검증을 하게 되면 참여자가 많아질수록 거래 처리의 속도가 느려진다. 업계에서 주로 "초당 거래수"라고 얘기하는데, 빠른 속도가 생경인 긍융거래에서 이러한 확장성의 한계는 큰 문제다.

3. 법률적 책임

스마트계약의 경우 기존의 블록체인은 법률적 책임의 소재가 불분명하다. 컴퓨터 코드에 의한 비가역적인 실행은 가능하지만 실제로 그것이 법률적 강제성을 가지는 것은 아니다.

4. 개연적 결제 완결성

기존의 블록체인, 특히 비트코인이나 이더리움과 같은 퍼블릭 블록체인의 경우, 결제의 완결성을 법률 또 기술적으로 100% 보장하지 않는다. 항상 네트워크의 포크 또는 블록 재종에 등의 가능성이 존재한다. 은행거래의 경우 결제의 완결성은 중앙은행이 법적으로 보장하기 때문에 법률적인 특면에서도 문제가 있다.

2017년 R3 컨소시엄은 공식적으로 두 그룹으로 나누었다. 한 그룹은 Corda 플랫폼 구축에 중점을 두고 있으며, 다른 하나는 컨소시엄 멤버에게 적절한 서비스를 제공하는데 중점을 두는 연구 그룹이다. 연구팀은 40개가 넘는 프로젝트를 진행중이며 20개 이상의 프로젝트를 완료했다. 그들이 진행하는 프로젝트들은 모두 이더리움, 리블, 패브릭, 코다, 캐나다 중앙은행, 바클레이즈 은행 등 블록체인에 국한되지 않은, 다양한 플랫폼과 관련되어 있다. 

R3 컨소시엄의 프로젝트 중 하나인 제네시스 프로젝트는 42개 금융사들이 블록체인 위에서 기업어음을 보내는 프로젝트를 진행했고 그 이후에도 60개가 넘는 다양한 프로젝트에 은행들이 자발적으로 참여하여 매우 빠른 발전을 이루었다. 또한 국내 사례로 2017년 5월 국내 은행 중 국민,신한,우리,하나,기업은행 등 5개 시중은행이 R와 함께 고객확인 정보를 블록체인으로 저장,관리하는 프로젝트를 성공한 바 있다. 다만 금융사들의 고객정보를 공유하는 행위 등 법적으로 금지되어 있는 문제가 있기 때문에 상용화까지 처리해야할 이슈가 있을것으로 보인다.

지금까지 R3와 Corda의 탄생 배경과 발전 과정을 알아보았다. 다음 글에서는 Corda의 기술적 바탕을 좀 더 살펴보고 기존의 블록체인과 다른점은 무엇인지, 엔터프라이즈 플랫폼으로서 하이퍼레져와 차이점은 무엇인지 등 기술적인 부분을 좀 더 살펴 보고 Corda에 대한 이해도를 높이고자 한다.

참조

A brief history of R3 – the Distributed Ledger Group 

THE CORDA WAY OF THINKING

R3, 넌 도데체 누구냐!  

개요

결혼을 하게 되어 모바일 청첩장을 직접 만들기로 했다.

기획자가 확실하신 분이라 그분의 니즈를 만족시키기 위한 템플릿을 찾을 수 없었다.

기획자님은 선과 원이 만나 우리가 된다는 것을 강조하길 원하셨고 

나는 스크롤을 내리면 화면의 구성을 변경할 수 있는 parallax scrolling라는 기술로 개발하기로 했다.

Parallax scrolling를 위한 라이브러리는 skrollr.js 를 사용했다.


개발

최대한 단순화 하기 위해서 index.html 안에 css와 js 모두 인라인으로 개발했다.

jquery, skrollr 두개의 라이브러리를 사용했고, 이미지는 상대경로로 설정해서, 사용하고자 하는 분은 이미지만 바꾸면 된다.

나는 AWS S3에 public 저장소를 하나 생성하고, FrontCloud CDN 을 연결해서 하객들에게 공유했다.

사용자 환경은 모바일에 최적화 했고, PC화면에서 보아도 깨지지 않도록 살짝 설정해 두었다.

청첩장을 직접 만드시는 분들께 도움이 될 수 있길 바라며 포스팅한다.


데모

깃헙

스크린샷



개요

호가창 무결성을 보장하고 트랜잭션을 쉽게 관리하기위해서  RDB를 이용한 구현을 찾아봤다.

하지만 RDB를 이용한 구현은 수평적으로 확장이 어렵기 때문에 IO를 담당할 하나의 MASTER DB에 의존성이 매우 높다.

또한 물리적 성능 향상을 위해서는 더 좋은 장비를 구매해야하고 단일장비의 성능엔 한계가 존재한다. (물리적으로나 비용적으로나)

그래서 호가 매칭 알고리즘에서 IO가 많이 발생하는 사용자 주문 생성, 주문 매칭 단계는 수평적 확장이  쉬운 Redis를 사용하여 처리하고

매칭된 주문 트랜잭션 생성 및 처리는 Mysql에서 관리하는 방향으로 설계해 보았다.


Redis 살펴보기

  1. 레디스는 메모리 기반 저장소라서 클러스터 서버 전체 다운시 데이터가 날아간다??
    1. Redis가 Memory 기반 저장소라서 캐시 전용 저장소로 오해하는 부분이 많은데, Redis도 옵션에 따라 텍스트 형태의 파일로 데이터를 저장할 수 있다.
      때문에 클러스터가 전체 다운되어도 파일을 통해 데이터를 복구할 수 있다.
  2. 트랜잭션 관리가 어렵다??
    1. 레디스는 싱글 쓰레드 기반이라 트랜잭션 관리가 쉬운편이다.
      또한 Lua Script 를 사용하거나 트랜잭션을 지원하는 명령어 (MULTI) 를 통해서 트랜잭션을 보장할 수 있다.
    2. 오히려 싱글 쓰레드라 대량 작업시 블럭되는 점을 조심해야 한다. (keys 명령어나, 대용량 삭제와 같은 작업을 조심해야한다.)
  3. 대용량 데이터 저장이 어렵다??
    1. 하나의 Key 당 최대 512MB까지 저장가능하다. 적지않은 사이즈지만 히스토리 정보까지 모두 메모리에 올려두기엔 부담스러운게 사실이다.
      그래서 IO 관련 데이터들만 redis에 저장하고 거래/히스토리 데이터는 RDB에서 관리하는 방향으로 설계해야한다.
  4. 클러스터링을 지원하여 가용성이 높다?
    1. Redis를 사용하는 가장 큰 이유이기도하지만 가장 조심해야할 부분이기도 하다.
      클러스터 구성에 따라 다르지만 보통 레디스 클러스터는 Master - Slave 구조로 구성된다.
      Master가 다운될 시 Slave가 자동으로 Master로 승격되는데 이때 승격될 Slave 에 데이터가 비어있으면 모든 클러스터의 데이터가 비워지게 된다.
      우리는 Redis 클러스터를 직접 구현하지않고 AWS 를 사용할거라 다행이다 ^^

설계

    필요한 자료구조

    1. 주문 상황판
      (buy/sell 주문의 order price 별로 order quantity 를 가지는 Sorted Set<score, orderSum>)
    2. 주문 리스트
      (Map<typeAndCoinAndPrice, List<order>> buyList, Map<typeAndCoinAndPrice, List<order>> sellList ) 
    3. 내 주문
      (내 주문 상태를 관리하고 목록을 확인할 수 있는 Map<memberNo, Map<orderId, order>>)
    4. 매칭된 두개의 Order 를 저장/처리할 Transaction RDB 테이블

동작 시나리오

  1. 주문생성 (Create)  - (주문 생성 전 주문이 필요로하는 코인/현금만큼 사용자의 잔고를 freeze 해야한다.)
    1. 새로운 주문이 들어오면 시퀀스 시작. 
    2. (redis 트랜잭션 시작)
      '주문 상황판'을보고 매칭할 주문 리스트 진입점을 찾는다. (매칭되는 진입점이 없으면 v 단계로 이동)
      1. 주문 타입이 Buy면 주문 가격이하의 Sell List중 가장 저렴한 리스트를 찾는다.
      2. 주문 타입이 Sell이면 주문 가격 이상의 Buy List중 가장 비싼 리스트를 찾는
    3. 매칭되는 리스트가 있으면 LPOP으로 order를 꺼내오고, '주문 상황판'과 '내 주문'에서 해당 order를 제거한다. 
    4. 요청 들어온 주문과 매칭된 주문의 거래량을 비교하여 분기 시퀀스를 따른다. 
      1. IF (RequestOrder.quantity > MatchedOrder.quantity)면  두개의 주문을 갖는 Transaction 객체를 생성하고,
        RequestOrder.quantity 를 차감한다. 
        처리된 MatchedOrder를 상황판과 사용자별 주문에 반영한다.
        (redis 트랜잭션 커밋 후 다시 ii 단계부터 시작)
      2. ELSE IF (RequestOrder.quantity <= MatchedOrder.quantity) Matched Order을 RequestOrder.quantity 기준으로 분리한 후
        Transaction 객체를 추가하고, 분리된 여유분의 Order를  다시 주문리스트로 LPUSH한다. (RequestOrder 의 주문량은 0이됨)
        스플릿되어 처리된 MatchedOrder를 상황판과 사용자별 주문에 반영한다.
        (redis 트랜잭션 커밋후 vi 단계로 이동) 
    5. 매칭되는 리스트가 없으면 Request Order의 가격 리스트에 RPUSH 하고 상황판과 내 주문에 반영한다. 
      (redis 트랜잭션 커밋)
    6. 생성된 Transaction 객체들을 처리한다.
      1. 주문 밸리데이션 (잔고, 요청내용 2중 검증)
      2. 사용자별 잔고 증감 및 수수료 취득 
  2. 주문 관리 (Read)
    1. 고객별 My 주문 리스트는 사용자 별 '내 주문' 모델을 통해 관리한다.
    2. 전체 주문량은 '주문 상환판' 모델을 통해 관리한다.
    3. 체결내역은 Transaction 테이블을 통해 관리한다.
  3. 주문 수정 (Update)
    1. 주문량/주문가 수정의 경우
      (redis 트랜잭션 시작)
      대상 주문을 리스트에서 POP 하여 값 수정 후 RPUSH 
      주문 상황판, 내 주문도 업데이트
      (redis 트랜잭션 종료)
  4. 주문 취소(Delete)
    1. 주문 취소의 경우
      (redis 트랜잭션 시작)
      대상 주문을 리스트에서 POP
      주문 상황판, 내 주문도 업데이트
      (redis 트랜잭션 종료)

예외 처리

  1. 모든 주문은 요청 전에 검증(잔고확인, 소유자 확인, 권한 확인)을 통과해야한다.
  2. 검증된 주문이라도 트랜잭션 처리 전에 한번 더 검증한다.
  3. 주문처리 우선순위는 가격 > 시간이다.
  4. 트랜잭션 생성을 위해 주문을 LPOP 하여 거래량 차감 후 LPUSH 하는사이 시간 우선순위를 위배할 수 있다.   



개요

암호화폐 거래소를 구축하기 위해 주문(호가) 매칭 알고리즘을 찾아보던 중 좋은 글이 있어 번역과 사족을 달았다.

David Veksler 라는 소프트웨어 아키택트의 글이고, 설계한 암호화페 거래소를 중국 시장에 런칭까지 했다고 한다. 

원문과 더불어 중국 시장 거래소 런칭에 관련된 글도 있으니 같이 보면 좋겠다. 

번역은 녹색으로 표현하고 사족은 검은색으로 달았다.

결론

아주 단순한 구조의 설계지만 꼼꼼히 살펴볼수록 튼튼해 보인다.

Buy/Sell 주문을 하나의 테이블에 넣기 때문에 RDB 저장소를 사용하여도 괜찮은 IO를 보장할 것같다. (사실 IO 성능이 아닌 확장성 문제 때문이긴 하지만 하나의 마스터DB와 슬레이브들로 어느정도 서비스 가능한 TPS를 보장해 줄 수 있을듯)

아래에 사족을 달았듯이 Order 의 Status 검증을 주문 요청 전에 진행한다면 트래픽을 분산시켜 성능 높일 수 있을것 같다. 

테이블 구조도에서 Type 값들을 모두 정규화 했는데 반정규화해서 사용해도 무방해 보이고 (미약하나마 성능향상위해) OrderBook 과 Transaction 이 가지는 관계, 남은 거래량을 처리하는 방법이 단순하지만 명시적이라 좋은 방법이라 생각한다.

또한 매칭 모듈 시작 트리거를 고객이 주문한 시점으로만 제한하고있어서 개인적으로 모호했던 부분을 분명히 할 수 있어서 좋았다.

본문

비트코인 거래소 프로젝트 파트 2: 주문 매칭 알고리즘

요약

통화 거래소는 구매자와 판매자가 자신이 가진 통화를 서로 다른 통화 유형으로 교환 할 수 있는 시스템이다.  

본문

주문 매칭 모듈은 구매/판매 주문을 매칭하고 거래를 만들어 거래의 흐름을 기록하고 고객들의 잔고를 업데이트 한다.

트리거: 한명의 고객이 거래량과 가격을 입력하고 주문버튼(Buy 또는 Sell)을  클릭 한다.

본문에서는 액션으로 적혀있지만 트리거라고 이해하면 더 좋을것 같다. 이제부터 나오는 1~12번까지의 과정은 고객의 주문 트리거를 통해 실행된다.

1. 웹사이트는 고객의 Order를 Pending 상태로 OrderBook 이라는 테이블에 저장한다. 우선 주문을 처리하기 위해 쌓는다(filed).

그런 다음 오더 매칭 서비스는 Pending된 Order를 순차적으로 훑어본다.

[public int PlaceBuyBid(int customerId, decimal quantityOfBTC, decimal pricePerBTC, DateTime ?expirationDate = null) ,

public int PlaceSellOffer(int customerId, decimal quantityOfBTC, decimal pricePerBTC, DateTime ?expirationDate = null)]

2. 이 때 주문이 취소되었거나 만료되었는지를 확인하여 Order의 상태를 재검증 한다.

3. order 마다 자금 검증을 하고, 고객은 order 가 필요로 하는 자금을 가지고 있어야만 해당 order의 상태룰 active로 변경한다.

그렇지 않을 경우엔 suspended 되는데 고객이 이후에 자금을 입금하면 다시 활성화 한다.

검증을 통과하면 

a: order 의 상태가 Active로 변경된다.

b: 비용 지불이 필요한 order면(sell order를 뜻하는 듯) Frozen Balance를 더한다. (고객이  자신이 가진 자산보다 더 많은 주문생성을 못하게 하기 위함) - 하지만 이 기능은 나중에 제거했다 - 우리는 Available balance보다 더 많은 주문 생성을 가능하게 했고 거래 전에 검증하는 쪽으로 방향을 바꾸었다. 

Order suspended 는 그냥 pending 상태로 둔다는 말 같다. 앞서 말한 트리거가 발동할때마다 잔고를 확인해서 pending 또는 active로 처리한다. 잔고 확인하는 부분이 특이한데, 잔고 확인없이 주문을 생성해줄 경우 고객입장에서 주문생성을 빠르게 진행할 수 있지만 어뷰징이 걱정되고, 그냥 주문 요청시 잔고 체크하여 걸러 내는게 더 좋을것 같다. David는 이걸로도 부족했는지 order 검증시에도 잔액 검증을 빼고 거래 시점에 검증하는 쪽으로 바꿨다고한다.

4. 주문 매칭 시스템은 buy와 sell order 를 매칭하기 위해 동작한다. 주문을 매칭하기위해 모든 Acitve 상태인 Order의 가격을 검색한다.

* 만약 트리거 주문이 buy 이면 가격이 작거나 같은 주문을 찾는다.

* sell 이면 가격이 크거나 같은 주문을 찾는다.

* 시장가의 sell 이면 가장 비싼 주문을, buy 만 가장 싼 주문을 찾는다.

Order 인입시 검증을 해서 바로 Active 상태로 들여온 다음 1, 2, 3 과정을 생략하고 바로 4번으로 하는건 어떨까?

pending -> active 대신 active -> pending 으로 상태변경 과정을 바꾸고 외부 배치 작업으로 order 의 pending -> active, expired 작 업을 진행하는것도 좋을것 같다. 

우리는 buy order를 위해서 오름차순으로 매칭하고 sell order를 위해서 내림차순으로 매칭한다. 그럼 다음 가격이 매칭되면 시간순으로 정렬한다.

[ISpecification IsMatchingOrderQuery(decimal price, int orderTypeId, int wantAssetTypeId,int offerAssetTypeId, bool? isMarketOrder)]

5. 우리는 상위 3개의 매치들을 메모리로 불러온다. 3개까지 가져오는 이유는 매칭된 주문들이 추후 검증에서 실패할 수 있기 때문이다.

메모리로 불러온다는 말은 단순히 DB에서 시스템상으로 SELECT 해온다는 말 같다.

6. 우리는 order 와 match를 비교한다. 이것은 C#은 이용한 이중 검증이다. - Order가 이전 데이터베이스 쿼리 결과와 일치해야한다.

order는 다음 조건은 만족해야한다. ($/BTC, order types[buy/sell], non-two market orders, matching prices)  

     [OrderComparisonResult CompareOrders(Order firstOrder, Order secondOrder)]

쿼리로 매칭하는 주문들을 불러왔지만 시스템 상에서 다시 한번 검증한다는 말인듯.

7. 만약 order 비교가 성공하면 매칭됨을 기록(record)하는 transaction 을 만든다.

 [Transaction GetTransactionForTwoOrders(OrderComparisonResult comparisonResult)]

      * A_Order 는 buy order 

* B_Order 는 sell order

(A와 B를 사용하는 이유는 아직 어떤 주문이 Buy/Sell 이 될지 확실하지 않기 때문이다.)

Buy/Sell 두개의 오더를 가지는 하나의 Transaction 생성

Transaction 에 들어간 Order Status도 변경하는게 좋지 않을까?

8. 만약 주문간에 주문량이 정확히 일치하지 않으면 주문량이 남은 쪽 주문을 쪼개서 새로운 주문을 생성한다.

9.  [ActivateTakeProfitAndStopLossOrders(Order order)]  를 실행한다. 

TODO - 여기서 바로 시작하는게 아니라 스케쥴링한다. 자세한 내용은 다른 포스트 참고

무슨내용인지 궁금한데 연결된 포스트가 없다 ㅠㅠ

10. 주문이 매칭된 거래 (Transaction)을 처리하고 결과를 기록(record)한다.  

[public Order ProcessTransaction(Transaction transaction)] (note: this module is a database transaction)

a: 거래와 쪼개진 주문을 DB에 추가한다.

b: 거래에 포함된 두 주문 모두에게 

* 인출 금액을 차감한다. (고객의 달러나 BTC 계좌에서)

* 신용자산을 더한다. (고객의 달러나 BTC 계좌에서))

* 수수료 계좌에 수수료를 기록(record)한다.

* 동결 자산을 녹인다.

* 변경점들을 저장한다.

11. 쪼개진 주문의 남은 거래량이 0이 될때까지 대상으로 과정을 반복하거나, 매칭되는 주문이 없을때까지 반복 한 뒤 해당 주문을 active 상태로 유지한다.

foreach (OrderProcessResultModel n in ProcessOrder(splitOrder.OrderId)){
    yield return n;
}

거래량이 남았을경우 처리가 궁금했는데 단순하게 푼것 같다.

필요한 거래량만큼 여러 주문을 불러와서 트랜잭션 진행하는것보다 직관적인 방법이지만

시간 복잡도 N이 추가된다. 안정성이 중요하다면 이정도는 감수하는것도 좋을듯..

12. 만약 남은 거개량이 0이되면 status를 Completed 로 변경한다.


'BLOCKCHAIN' 카테고리의 다른 글

R3와 Corda의 탄생배경  (899) 2019.01.13
중앙 거래소 호가 매칭 알고리즘 설계  (933) 2018.10.24

+ Recent posts