들어가면서

  지난 데이터 대시보드 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)
새로움과 자유로움을 좋아하는 개발자입니다.
프로덕트의 성장은 구성원의 성장으로부터 온다고 믿습니다.  

목적

Spring + Mybatis로 구성된 서비스를 운영하던 중 사용자가 몰렸을 때 DB에 부담을 덜어주기 위한 방법을 고민했다.
Mybatis 의 ExcutorType 을 Batch로 설정하면 성능이 향상 되었는데 이유를 파악하기 위해 포스트 남긴다.

결론적으로 Batch 는 statement를 재사용 하며 Create, Insert, Update를 벌크로 모아서 처리하고, 
Select를 만나면 기존에 모아놓았던 쿼리들을 수행하기 때문에 C, I, U 작업은 연달아서 할 때 가장 효율적이었다.
트랜잭션과, select문이 끼어드는 위치를 잘 조정하여 사용하면  DB의 부담을 덜 수 있을것으로 보인다. 

Batch Processing? - wiki

일괄 처리(batch processing)란 컴퓨터 프로그램 흐름에 따라 순차적으로 자료를 처리하는 방식을 뜻한다.
일괄 처리 시스템(batch system)이란 일괄처리(batch processing) 방식이 적용된 시스템으로서, 하나의 작업이 끝나기 전까지는 다른 작업을 할 수 없다.

장점 
  • 많은 사용자 사이에서 컴퓨터 자원을 공유할 수 있다.
  • 작업 프로세스의 시간대를 컴퓨터 리소스가 덜 사용되는 시간대로 이동한다.
  • 분 단위의 사용자 응답 대기와 더불어 컴퓨터 리소스의 유휴 사용을 피한다.
  • 전반적인 이용률을 높임으로써 컴퓨터의 비용을 더 잘 상환하도록 도와 준다.

JDBC에서 Batch Processing 

JDBC 2.0는 batch proccessing 은 다음과 같이 제공한다.
1. Prepare statement.
2. Bind parameters.
3. Add to batch.
4. Repeat steps 2 and 3 until interest has been assigned for each account.
5. Execute.

preparedStatement를 사용한 Batch Processing

PreparedStatement stmt = conn.prepareStatement(
    "UPDATE account SET balance = ? WHERE acct_id = ?");"UPDATE account SET balance = ? WHERE acct_id = ?");
int[] rows;

for(int i=0; i<accts.length; i++) {
    accts[i].calculateInterest( );
    stmt.setDouble(1, accts[i].getBalance( ));
    stmt.setLong(2, accts[i].getID( ));
    stmt.addBatch( );
}
rows = stmt.executeBatch( );
  • addBatch() : create, insert, update, deleted 일괄 처리 결과를 관리 할 필요가 없기 때문에 batch에 statement를 추가하는 일만 한다.
  • excuteBatch()가 쿼리들을 수행하고 정수형 배열을 리턴하는데, 각 배열의 요소는 각 statement로 인해 update된 row의 숫자이다. 

batch모드로 수행할 때 auto-commit을 사용하면 각각의 statement 를 수행 후 commit이 발생한다.
그래서 batch 수행 중 에러가 발생하면 에러 발생 전까지 statement는 DB 에 반영되어있고 BatchUpdateException에서 getUpdateCounts()로 성공한 쿼리의 숫자를 얻어 올 수 있다.
실제 배치 과정에서, 각 statement 마다 commit을 하면 배치의 성능이 떨어지게 되는데 이것을 트랜잭션으로 묶어서 관리하려고 해도 성능에 악영향을 미친다. 가장 좋은 방법은 임의의 수 만큼 statement 를 묶어서, 명시적으로 commit 하는 것 같다. 


Mybatis Batch Mode

Mybatis는 SqlSessionFactory에서 SqlSessionTemplate(SqlSession 구현체)을 생성할 때 ExcutorType을 지정해 줄 수 있다.

ExcutorTypeDescription
SIMPLEexecutor does nothing special 
REUSEexecutor reuses prepared statements 
BATCHexecutor reuses statements and batches updates


ExecutorType.BATCH : 이 실행자는 모든 update(C,I,U,D)구문을 배치처리하고 중간에 select 가 실행될 경우 필요하다면 경계를 표시한다.

여기서 이 경계는 아래 소스코드를 통해 확인 할 수 있다. 


SqlSessionTemplate에서 ExecutorType이 BATCH인 경우에 Create, Insert, Update, Delete 구문은 doUpdate 에서, Select 구문은 doQuery 에서 처리한다.

아래 코드의 59번 줄에서 statement를 재사용하는 로직을 확인 할 수 있고, 61번, 69번 줄에서 statement 재사용 여부에 따라 statementList에 statement를 추가하고 63번, 70번 줄에서 파라미터 추가, 다른 구문일 경우 BatchResult를 추가하는 것을 볼 수 있다.

그리고 doQuery 메소드의 81번줄에서 flushStatements()를 호출 하는데, 이는 그동안 statementList에 쌓아 왔던 statement를  executeBatch() 메소드로 수행한다. 수행 결과는 batchResult에 저장하여 return 하고, 그동안 쌓아왔던 statementList 와 batchResultList 를 초기화한다.

아래 코드처럼 Select문이 실행 될 경우 BATCH 처리를 멈추고 초기화 하기 때문에, BATCH 작업 도중 SELECT 문이 끼어들면 성능이 저하된다. 


org.apache.ibatis.executor.BatchExecutor.class - grepcode

40 public class More ...BatchExecutor extends BaseExecutor {
41
42   public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;
43
44   private final List<Statement> statementList = new ArrayList<Statement>();
45   private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
46   private String currentSql;
47   private MappedStatement currentStatement;
48
49   public More ...BatchExecutor(Configuration configuration, Transaction transaction) {
50     super(configuration, transaction);
51   }
52
53   public int More ...doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
54     final Configuration configuration = ms.getConfiguration();
55     final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
56     final BoundSql boundSql = handler.getBoundSql();
57     final String sql = boundSql.getSql();
58     final Statement stmt;
59     if (sql.equals(currentSql) && ms.equals(currentStatement)) {
60       int last = statementList.size() - 1;
61       stmt = statementList.get(last);
62       BatchResult batchResult = batchResultList.get(last);
63       batchResult.addParameterObject(parameterObject);
64     } else {
65       Connection connection = getConnection(ms.getStatementLog());
66       stmt = handler.prepare(connection);
67       currentSql = sql;
68       currentStatement = ms;
69       statementList.add(stmt);
70       batchResultList.add(new BatchResult(ms, sql, parameterObject));
71     }
72     handler.parameterize(stmt);
73     handler.batch(stmt);
74     return BATCH_UPDATE_RETURN_VALUE;
75   }
76
77   public <E> List<E> More ...doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
78       throws SQLException {
79     Statement stmt = null;
80     try {
81       flushStatements();
82       Configuration configuration = ms.getConfiguration();
83       StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
84       Connection connection = getConnection(ms.getStatementLog());
85       stmt = handler.prepare(connection);
86       handler.parameterize(stmt);
87       return handler.<E>query(stmt, resultHandler);
88     } finally {
89       closeStatement(stmt);
90     }
91   }
92
93   public List<BatchResult> More ...doFlushStatements(boolean isRollback) throws SQLException {
94     try {
95       List<BatchResult> results = new ArrayList<BatchResult>();
96       if (isRollback) {
97         return Collections.emptyList();
98       } else {
99         for (int i = 0, n = statementList.size(); i < n; i++) {
100          Statement stmt = statementList.get(i);
101          BatchResult batchResult = batchResultList.get(i);
102          try {
103            batchResult.setUpdateCounts(stmt.executeBatch());
104            MappedStatement ms = batchResult.getMappedStatement();
105            List<Object> parameterObjects = batchResult.getParameterObjects();
106            KeyGenerator keyGenerator = ms.getKeyGenerator();
107            if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
108              Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
109              jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
110            } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
111              for (Object parameter : parameterObjects) {
112                keyGenerator.processAfter(this, ms, stmt, parameter);
113              }
114            }
115          } catch (BatchUpdateException e) {
116            StringBuffer message = new StringBuffer();
117            message.append(batchResult.getMappedStatement().getId())
118                .append(" (batch index #")
119                .append(i + 1)
120                .append(")")
121                .append(" failed.");
122            if (i > 0) {
123              message.append(" ")
124                  .append(i)
125                  .append(" prior sub executor(s) completed successfully, but will be rolled back.");
126            }
127            throw new BatchExecutorException(message.toString(), e, results, batchResult);
128          }
129          results.add(batchResult);
130        }
131        return results;
132      }
133    } finally {
134      for (Statement stmt : statementList) {
135        closeStatement(stmt);
136      }
137      currentSql = null;
138      statementList.clear();
139      batchResultList.clear();
140    }
141  }
142
143}

 기타, 참조


용어

SqlSessionTemplate - http://www.mybatis.org/spring/ko/sqlsession.html
SqlSessionTemplate은 마이바티스 스프링 연동모듈의 핵심이다.
SqlSessionTemplate은 SqlSession을 구현하고 코드에서 SqlSession를 대체하는 역할을 한다.
SqlSessionTemplate 은 쓰레드에 안전하고 여러개의 DAO나 매퍼에서 공유할수 있다.
getMapper()에 의해 리턴된 매퍼가 가진 메서드를 포함해서 SQL을 처리하는 마이바티스 메서드를 호출할때 SqlSessionTemplate은 SqlSession이 현재의 스프링 트랜잭션에서 사용될수 있도록 보장한다.
추가적으로 SqlSessionTemplate은 필요한 시점에 세션을 닫고, 커밋하거나 롤백하는 것을 포함한 세션의 생명주기를 관리한다.
또한 마이바티스 예외를 스프링의 DataAccessException로 변환하는 작업또한 처리한다.
SqlSessionTemplate은 마이바티스의 디폴트 구현체인 DefaultSqlSession 대신 항상 사용된다.
왜냐하면 템플릿은 스프링 트랜잭션의 일부처럼 사용될 수 있고 여러개 주입된 매퍼 클래스에 의해 사용되도록 쓰레드에 안전하다.
동일한 애플리케이션에서 두개의 클래스간의 전환은 데이터 무결성 이슈를 야기할수 있다.

참조

Batch Processing - https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%84_%EC%B2%98%EB%A6%AC

Batch Processing(JDBC) - https://www.safaribooksonline.com/library/view/database-programming-with/1565926161/ch04s02.html

Batch Processing(JDBC) - http://www.tutorialspoint.com/jdbc/jdbc-batch-processing.htm

Mybatis3 - http://www.mybatis.org/mybatis-3/ko/index.html

Grep Code(org.apache.ibatis.executor.BatchExecutor.class) - http://grepcode.com/file/repo1.maven.org/maven2/org.mybatis/mybatis/3.2.7/org/apache/ibatis/executor/BatchExecutor.java#BatchExecutor

Flyway?

데이터베이스 형상관리 툴.

로컬에서 변경한 데이터베이스의 스키마나 데이터를 운영 데이터베이스에 반영하는 것을 누락하는것을 막기 위해 사용한다.

또한 개발 DB와 운영 DB의 스키마를 비교하거나, 운영 DB에 수작업을 가하는 노가다와 위험성을 줄이기 위해 사용한다.

생성한 형상을 새로운 DB에 적용하면 그게 마이그레이션이다.



 

동작방식

Flyway 가 연결된 데이터베이스에 자동으로 SCHEMA_VERSION 이라는 메타데이터 테이블을 생성한다.

Flyway 는 사용자가 정의한 sql의 파일명을 자동으로 스캔하여, SCHEMA_VERSION 에 버전 정보를 남기는 동시에,

데이터베이스에 변경내용을 적용한다.

SCHEMA_VERSION 테이블에는 다음과 같은 정보를 담고 있다.

 


명령어 설명

init - SCHEMA_VERSION 을 baseline 과 함께 생성한다. 테이블이 이미 생성되어 있으면 수행되지 않는다.

migrate - 스키마정보를 리얼DB에 마이그레이션한다.

clean - flyway로 생성한 스키마를 모두 삭제한다고 하지만, 해당 데이터 베이스의 모든 테이블을 삭제한다. 

info - DB에 적용된 스키마 정보와, 로컬에 pending 되어있는 변경 정보를 보여준다.

validate - DB에 적용된 스키마 정보와, 로컬의 변경점을 비교하여 보여준다.

repair - 마이그레이션 실패한 내역을 수정한다 (삭제, 교체)

baseline - flyway로 형상 버전관리를 시작 할 baseline 을 설정한다.

Command-line tool 로 예제 실행

(1) 설치

http://flywaydb.org/getstarted/firststeps/commandline.html

.

(2) 설정 

설치 폴더 -> conf -> flyway.con 파일 수정 (C:\flyway\conf\flyway.conf)

flyway.url=jdbc:mysql://localhost:3306/테이블명
flyway.user=아이디
flyway.password=비번

.

(3) 마이그레이션 sql문 생성 

설치 폴더 -> sql (C:\flyway\sql) 에 파일 생성.

init을 이용하여 SCHEMA_VERSION 테이블을 생성하면 V1 로 생성되기 때문에 파일명을 V2 로 생성함.

V2__Create_person_table.sql 

create table PERSON ( 
    ID int not null, 
    NAME varchar(100) not null 
);create table PERSON ( 
    ID int not null, 
    NAME varchar(100) not null 
);                                                                            

V2.1__Insert_person.sql

insert into PERSON (ID, NAME) values (1, 'Axel'); 
insert into PERSON (ID, NAME) values (2, 'Mr. Foo'); 
insert into PERSON (ID, NAME) values (3, 'Ms. Bar');

파일명은 V 와 숫자로 버전명을 지정하고 under_bar 두개로 시작되어야한다. 이미지참조.


  

.

(4) flyway init 명령문 수행

flyway init 없이 migrate를 수행하면, 자동으로 schema_version이 생성된다.

flyway init 을 수행하면 결과 화면과 같이 baseline 이 V1 로 입력되어있는것을 확인 할 수 있다.

명령문 수행화면

데이터베이스 적용 결과

(5) flyway migrate 명령문 수행

flyway가 sql 파일들을 스캐닝하여 수행한다.

V2 와 V2.1 의 sql 파일이 버전 순서대로 수행된 것을 확인 할 수 있다.

명령문 수행화면

데이터베이스 적용결과

.

(6) 마이그레이션 sql문 추가 생성

V2.2__Insert_another_person.sql 

insert into PERSON (ID, NAME) values (5, 'CLEAN!!!!');

.

(7) flyway validate 명령문 수행

migrate 없이, flyway validate 를 수행하면 새로 추가한 쿼리문에 실제 DB에 적용되어 있지않기 때문에,

fail이 떨어진다.

명령문 수행화면

.

(8) flyway info 명령문 수행

flyway info 로  내용을 확인해 보면 2.2 버전의 파일이 pending 상태인것을 알 수 있다.

명령문 수행화면

.

(9) flyway clean 명령문 수행

flyway clean 을 수행하면 y/n 질문도 없이 데이터베이스의 모든 테이블들이 드랍된다.

flyway로 작업된 테이블만 드랍된다는 말이 있는데, 아니다 모두 드랍된다.

명령문 수행화면

생각

전체적으로 사용하기 쉽고 대부분의 툴과 연동이 가능한 것이 장점.

데이터 베이스의 스키마 변경 이력을 확인 할 수 있다는 점이 가장 좋았음.

각자 로컬 데이터 베이스를 이용하여 개발하지 않으면 형상관리와 마이그레이션의 의미가 약해짐.

개발 DB 를 따로 운영함으로서, 작업중 스키마 변경에 따른 이슈가 발생 할 수 있지만,

스키마 이력관리를 위해, Flyway 를 적용하는 것은 오버스팩이라고 생각함. 

데이터베이스의 스키마를 변경하면 이력을 남기는 워크벤치쪽 플러그인이 있으면 좋겠다고 생각함.

참조

http://flywaydb.org/

+ Recent posts