모바일 앱 로그분석, 어떻게 시작해야 할까?

Firebase와 BigQuery를 이용한 로그분석 시스템 구축하기

Leo Yang
How we build MyRealTrip
12 min readSep 2, 2019

--

대시보드 뒤에 숨어있는 이야기들을 알아봅시다 [image credit]

데이터 파이프라인을 잘 구축한다는 건 어떤 의미일까요? 기술적으로는 어떤 데이터베이스를 이용할지 선택하고, 스트리밍과 배치를 적절히 설계하고, 수집된 데이터의 전처리 프로세스를 만들고… 등등의 수많은 고려사항이 있을텐데요. 개발자가 아닌 분석가 입장에서 ‘데이터 파이프라인을 잘 만든다’는 건 약간 다른 측면의 이야기라고 생각합니다. 저는 다음과 같은 질문을 해 볼 수 있을 것 같네요. 서비스 데이터와 유저 행동 데이터가 잘 구조화되어서 저장되고 있고, 서로 연계해서 보는 데 어려움이 없는가?

면접에서 많이 듣는 말 1위 vs. 입사하고 많이 하는 말 1위

서비스를 이용하는 사용자들이 남기는 로그는 서비스 로그와 행동 로그로 구분됩니다. 서비스 로그는 transaction의 결과를 기록하는 로그입니다. 가입하거나, 예약하거나, 결제하는 등 하나의 transaction이 완료되었을 때 각각의 서비스 로그가 남게 됩니다. 반면 행동 로그는 transaction에 이르기까지 사용자들이 서비스에서 하는 하나하나의 action에 대한 로그를 의미합니다. 특정 상품을 클릭하거나, 검색하거나, 배너를 스와이프하는 등의 action을 예로 들 수 있습니다.

서비스 로그는 기본적인 서비스 운영을 위해서 필수적으로 쌓고 관리해야 하므로 이 데이터를 활용하는 데는 대부분 큰 문제가 없습니다. (모든 변경분을 다 쌓을지, 최종 수정된 내용만 남길지 정도의 고려사항은 있겠지만…)

반면 행동 로그의 경우 생성되는 데이터양도 훨씬 많고, 설계하는 과정에서의 자유도도 높아서 수집이나 활용이 상대적으로 까다로운 편입니다. 당장 볼 수 없다고 해서 서비스에 큰 문제가 생기는 것도 아니고요. 그러다 보니 ‘일단 되는대로 다 쌓자! 근데 어떻게 봐야 할지 모르겠다. 나중에 누군가 보겠지. 뭐…’ 상태로 방치되는 경우가 많습니다.

이 글에서는 마이리얼트립 모바일 앱에서의 행동 로그를 어떤 방식으로 수집해서 보고 있는지를 간단히(?) 소개하려고 합니다. 단, 연동에 대한 기술적인 부분보다는 이벤트 구조를 어떻게 설계하고, 어떤 식으로 적재하고, 어떻게 조회하는지… 분석가 관점에서의 로그 수집과 처리에 대한 이야기를 주로 말씀드릴 예정입니다.

행동 로그 설계하기

행동 로그를 어떻게 설계하느냐에 따라서, 얻을 수 있는 정보의 수준은 완전히 달라집니다.

행동 로그를 보는 가장 단순한 방식은, 발생한 이벤트의 숫자를 count 하는 것입니다. (ex. 가입하기 버튼 클릭 수는 100회입니다.) 하지만 단순 이벤트 숫자 집계만으로는 원하는 수준의 인사이트를 얻기 어렵겠죠. 사실 행동 로그 설계의 핵심은 이벤트의 속성(property)을 어떤 수준으로 함께 남길 것인가? 를 정의하는 부분입니다. 속성에 대해서 약간 더 부연설명을 하자면, 특정 이벤트가 발생했을 때 함께 남길 수 있는 이벤트(혹은 사용자)에 대한 세부정보라고 생각하시면 됩니다. 가령, ‘예약하기’ 버튼을 누르는 이벤트가 있다고 하면, 아래와 같은 것들이 property가 될 수 있겠네요.

이벤트의 속성을 남기지 않았다면, 예약하기 버튼을 클릭한 숫자 정도만 확인이 가능합니다. 하지만 이벤트의 property를 함께 기록했다면 어떤 상품을 클릭했는지, 그 상품의 가격이 얼마였는지, 어떤 화면에서 클릭했는지…와 같은 훨씬 더 자세한 정보를 얻을 수 있습니다. 만약 사용자 property까지 함께 기록했다면? 해당 이벤트를 한 사용자가 어떤 특성이 있는지도 확인할 수 있습니다. 즉 하나의 이벤트가 발생했을 때 훨씬 더 입체적으로 정보를 얻을 수 있게 됩니다. 아래 표를 보면 property를 남기는 수준에 따라서 얻을 수 있는 인사이트의 수준이 크게 차이 남을 확인하실 수 있습니다.

BigQuery에 로그 적재하기

마이리얼트립은 Firebase-BigQuery 로 이어지는 파이프라인을 이용해서 앱 로그를 남기고 있습니다. BigQuery는 구글이 제공하는 대용량 데이터베이스인데요. TB 단위의 스캔에도 전혀 무리 없는 빠른 속도, 매우 저렴한 저장 비용, 무제한에 가까운 용량, 관리의 편의성, Firebase 프레임웍과의 연동… 등 앱 로그를 쌓는 용도로 사용하기에 아주 좋은 선택입니다. BigQuery에 대한 자세한 소개, 그리고 Firebase를 BigQuery와 연동하기 위한 설정에 대한 디테일한 내용은 아래 링크를 참고하시길 바랍니다.

BigQuery에 쌓인 이벤트 로그는 아래와 같은 포맷의 스키마를 갖습니다. event에 딸린 key가 있고(위에서 설명한 event property 명칭에 해당합니다), 해당 key에 매핑된 value (event property의 구체적인 값에 해당합니다)가 있습니다. 특이한 점은 value가 자료형에 따라 구분되어 있다는 건데요. string, integer, float, double 각 자료형에 따라서 컬럼이 구분되어 있고, 추후 조회를 할 때도 자료형에 따라 정확한 컬럼을 지정해야 값을 확인할 수 있습니다. 참고로, 아래는 event property를 예로 들었지만, user property도 적재되는 방식은 동일합니다.

Scheme 예시
event 안에 N개의 key가 있고, 각 key에는 매핑되는 value가 있습니다 (4개 type 중 하나)

또 한가지 주목해야 하는 부분은 하나의 이벤트에 복수 개의 property가 존재하는 경우, row 안에 row가 내재한 구조로 쌓인다는 점인데요 (BigQuery에서는 이런 유형의 컬럼을 Record type으로 분류합니다. 일명 nested 구조…) 구글은 이걸 장점이라고 홍보하던데 -_-; 사실 분석하는 입장에서는 이런 nested 구조 때문에 쿼리 작성의 난이도가 굉장히 높습니다ㅠㅜ 간단히 설명해 드리면, record type의 데이터를 조회할 때는 항상 unnest 한 다음에 쿼리를 해야 한다는 건데요. 이 부분은 뒤에서 자세히 설명해 드리도록 하겠습니다.

(저도 nested 구조에 익숙하지 않아서 처음에는 엄청 고생하다가, 이 글을 다섯 번쯤 정독하고 나서야 경건한 마음으로 콘솔에 쿼리 작성을 할 수 있었습니다…)

BigQuery에서 로그 조회하기

unnest를 활용하여 기본적인 쿼리를 작성하는 방법은 아래와 같습니다. from 문 뒤에 unnest를 이용해서 특정 record type을 임시로 펼치고, 그 컬럼을 불러와서 정보를 조회합니다.

검색어 입력 방법(search_type)에 따라 count 하는 단순한(!) 쿼리

위 케이스는 하나의 property를 사용하는 일반적인 상황에서 쓸 수 있는데요. 만약, 특정 record type에 있는 property 여러 개를 동시에 사용하는 쿼리를 짜고 싶다면 어떻게 해야 할까요? 가령 ‘특정 검색어’에 대해서만 ‘search_type’을 구분해서 보고 싶다면? 좀 번거롭긴 하지만 사용하려고 하는 property 개수만큼 해당 record를 unnest하면, 복수 property를 활용하는 쿼리문을 작성할 수 있습니다.

property를 하나 더 추가해서 보려면? 좀 더 귀찮아집니다

다음으로, parameter 안에 있는 value를 꺼내서 select나 group by의 기준으로 쓰고 싶다면? 아래와 같이 subquery를 이용하면 됩니다. name, key, value가 좀 헷갈릴 수 있는데, 자꾸 사용하다 보면 어쨌든 익숙해지긴 하더군요.

사실 이쯤되면 과거의 내가 짠 쿼리랑 싸우는 경우가 왕왕 생깁니다…

이런 식으로 이벤트와 사용자의 property를 잘 정의해서 BigQuery에 쌓아두면 사용자의 행동 로그를 굉장히 자세한 레벨에서 분석할 수 있습니다. 기본적인 노출이나 클릭 이벤트 집계는 물론이고, 주요 페이지에 대한 퍼널 분석이라던지, 핵심 기능에 대한 사용성 확인, 신규 피쳐에 대한 A/B 테스트 성과 확인 등이 모두 가능합니다. user property를 잘 남겼다면 특정 행동을 한 유저 리스트를 추출하거나, 적합한 프로모션을 위한 세밀한 타겟팅도 할 수 있습니다.

다만 한가지 문제가 있는데요. nested 되어서 쌓이는 DB 구조 때문에 여러 가지 복잡한 조건이 포함된 쿼리를 작성하는 게 굉장히 어렵습니다. ㅠㅜ 기본적인 SQL 문법을 알고 있는 사람이라도 하더라도, 쿼리를 작성할 때 아래 사항을 챙기느라 꽤 시행착오를 겪어야 합니다.

  • 기본적으로 unnest가 항상 필요
  • unnest하는 과정에서 불필요하게 생성되는 많은 row로 인해서 쿼리 스캔 비용이 늘어남
  • event, parameter.key, parameter.value가 매번 헷갈림
  • value의 타입이 string, integer, float, double 중 어느 것인지를 쿼리할때마다 정확히 지정해야 함
  • parameter.value를 꺼내 쓰려면 subquery 등 굉장히 복잡한 문법이 필요함

사실 정확히 말하면… 쿼리 작성이 어려운 게 문제라기보다는, 쿼리 작성이 어려워지면서 로그를 점점 소극적으로 보게 된다는 문제가 생깁니다. 물론 빈번하게 활용되는 쿼리들은 초기에 일괄 세팅해서 편하게 볼 수 있도록 했지만, 이후 추가로 분석할만한 다양한 주제가 생겼을 때 쿼리 작성이 까다로워서 진행 자체가 늦어지거나, 세세한 데이터를 충분히 보지 못하는 경우가 종종 발생했거든요.

분석이 편한 로그 테이블 만들기

불편했지만 어찌어찌 적응하면서 지내던 저와 달리, 마이리얼트립 그로스팀에서 바른생활과 톤앤매너를 담당하고 있으며 2019년 4월의 MRTer (a.k.a 우수사원)로 선정된 안성환 님은 이 문제를 꼭 해결하고 싶어했습니다. 몇 번의 시행착오를 거치긴 했지만, 결과적으로 성환님은 굉장히 깔끔한 방법으로 로그 테이블 전처리에 성공했는데요. 짧게 소개해 드리겠습니다.

nested 된 테이블 예시

위와 같이 nested 된 테이블을 ‘사람이 보기 편한’ 형태로 flatten 하려면 어떻게 해야 할까요? 약간 복잡한 subquery를 사용해야 하지만, key를 기준으로 unnest 시키면서 value의 type을 하나하나 잘 지정해주면 그리 어렵진 않습니다.

성환님이 만든 보석같은 쿼리…

이렇게 flatten을 하면, 아래와 같이 매우 예쁜(!) 테이블이 나오게 됩니다.

unnest 후, 필요한 key-value만 깔끔하게 남긴 테이블

자, 그럼 이제 이 쿼리를 기반으로 UDF(User Defined Function)를 만들고, 기존 테이블에다가 이 함수를 적용한 결과를 새 테이블로 저장하면, 놀랍도록 깔끔한 데이터셋을 얻을 수 있습니다. (이 과정을 자동화하는 pipeline은 데이터플랫폼 팀의 도움을 많이 받았습니다) 아래에 적혀진 것처럼, paramValueByKey 라는 마법의(!) 함수를 통해 nested 된 테이블의 flatten 작업을 깔끔하게 진행하고 새로운 테이블에 저장할 수 있었습니다.

Flatten을 위한 UDF 예시

이런 식으로 테이블이 깔끔하게 정리되고 나면, 똑같은 데이터를 추출하는데 필요한 쿼리도 굉장히 심플해집니다. 위에서 잠깐 언급했던 검색어별 검색 방법을 확인하는 쿼리(복수의 property를 활용했던 사례)를 예로 들어보면, before 대비 after 쿼리가 엄청나게 간결해진 것을 확인하실 수 있습니다. 저 정도면 사내 SQL 교육을 수료한 멤버들이 직접 궁금한 정보를 찾아보는데 전혀 무리가 없는 테이블 구조라고 할 수 있습니다. (데이터가 흐르는 조직!)

훨씬 더 간결해진 쿼리

이처럼 심플한 구조로 로그 데이터를 저장하게 되면 여러 장점이 있습니다. 무엇보다 쿼리를 작성하는 과정이 간편해지면서 훨씬 더 다양한 데이터를 적극적으로 살펴보게 되었고요. unnest 과정에서 생기는 불필요한 row가 사라지면서, 쿼리 비용도 훨씬 절감할 수 있었습니다. (BigQuery의 경우, 스캔하는 데이터 양 기준으로 쿼리에 대한 과금을 합니다) 기존 쿼리를 수정하거나 개선하는 경우에도, 분석하는 시간이 크게 단축되어서 생산성이 크게 향상되었습니다.

마무리

이상으로, 마이리얼트립에서 어떤 식으로 행동 로그를 정의하고, 수집하고, 조회하는지… 에 대해서 간략하게 소개해 드렸습니다. 로그 분석은 초반에 설계를 잘하는 것만큼이나 QA를 꼼꼼하게 하고, 이후 서비스가 업데이트될 때마다 꾸준히 잘 챙기는 게 중요한데요. (전체를 갈아엎고 새로 싹 만드는 일의 난이도가 10이라면, 이후 변경사항을 꾸준히 잘 챙기는 일의 난이도는 100쯤 되는 것 같네요…)

로깅 작업을 꼼꼼하게 잘 챙기는 개발자는 있지만 (제가 N개 회사를 다녀봤는데, 마이리얼트립 개발자분들은 진짜 잘 챙겨주십니다…ㅎㅎ), 그것과 별개로 이걸 좋아하는(?) 개발자는 없다고 생각합니다. 엄청난 꼼꼼함과 완벽함이 요구되는 무한 반복 업무거든요 -_-;; (애초에 사람이 할 일이 아닌 것 같기도…)

개인적으로 마이리얼트립 개발자분들이 로깅을 꼼꼼하게 잘 챙겨주시는 큰 이유는 실제로 앱 로그 데이터가 여기저기서 잘 활용되고 있고, 같이 일하는 멤버들 사이에서 굉장히 중요한 데이터로 인식되고 있다는 점 때문이 아닐까 싶습니다. 마이리얼트립에서는 A/B테스트를 하거나, 새로운 기능을 출시했을 때, 서비스에 이상 징후가 있을 때 굉장히 열심히 행동 로그를 살펴보는 편이거든요. 누가 언제 볼지 모르는 데이터를 일단 쌓는 것과, 모두가 굉장히 열심히 보고 있는 데이터를 쌓는 건… 동기부여의 차원이 전혀 다를 테니까요!

이렇게 열심히 데이터를 보면서 재미있는 문제를 해결하는 곳. 굉장히 좋은 동료들과 함께 할 수 있는 곳. 마이리얼트립에서 함께 할 동료들을 찾고 있습니다.

많은 포지션이 열려있고, 아직 늦지 않았어요!

https://career.myrealtrip.com/

--

--