일상대화 챗봇 분석 시스템 구축기 1편 - 데이터 파이프라인 편

웹 브라우저 하나만으로 데이터를 분석할 수 있는 비결

이정민 서진형 나선일 | 2020년 10월 07일

핑퐁팀의 제품 파트에서는 일상대화 기술을 총동원해서 친구 같은 챗봇 이루다를 만들고 있습니다. 지금은 베타 테스트를 진행하면서 사람들이 루다와 오랫동안 재밌게 이야기하게 만들 방법을 끊임없이 고민하고 있죠. 루다가 어떤 방향으로 나아가야 할지 정할 때 빼놓을 수 없는 것이 바로 데이터입니다. 이번 시리즈에서는 일상대화 챗봇의 데이터를 분석하는 과정을 공유해 드리려고 합니다.

Table of Contents

  1. 수집할 데이터 결정하기
  2. 데이터 파이프라인 구축
    1. BigQuery: 클라우드 데이터 웨어하우스
    2. Web Application Server
    3. Log Collector
    4. Google Cloud Logging
  3. 데이터 분석 맛보기
    1. 쿼리 작성하기
    2. 복잡한 구조의 자료 다루기
    3. 과금량 최적화 가이드
    4. 저장된 쿼리
    5. GitHub으로 쿼리 관리하기
  4. 마치며
  5. References

수집할 데이터 결정하기

데이터 분석 과정에는 엔지니어와 분석가, 기획자 사이의 긴밀한 협업이 필요합니다. 그중에서 엔지니어는 기획자와 분석가가 원하는 분석 지표를 쉽게 뽑아낼 수 있도록 유연하고 확장성 있는 시스템을 구축하는 역할을 맡습니다. 이를 위해 제일 먼저 할 일은 어떤 데이터를 수집할지 결정하는 것입니다. 최대한 다양한 상황에 대응할 수 있도록 활용 가능성이 높은 자료들을 선별해 보았습니다.

첫 번째는 사용자의 메신저 행동 패턴입니다. 루다는 사용자가 일상에서도 자주 쓰는 페이스북 메신저를 이용해서 소통하는데요, 페이스북 메신저 플랫폼에서 제공하는 API를 이용해서 사용자가 루다의 메시지를 언제 읽고, 답장하고, 반응을 추가하는지와 같은 일련의 행동 패턴을 수집합니다.

두 번째는 자연어 이해(Natural Language Understanding) 정보입니다. NLU 정보에는 사용자와 루다의 대화 텍스트에서 추출할 수 있는 키워드, 대화 주제, 감성 정보, 문장 유사도 등이 포함됩니다. 챗봇 인터페이스의 특성상 루다가 사용자와 상호작용할 방법은 다른 웹/앱 서비스보다 극히 제한되어 있습니다. 그로 인해 분석 과정에서 사용자와 루다가 나눈 대화 텍스트가 중요한 역할을 맡게 됩니다. 따라서 날것의 데이터뿐만 아니라 각종 머신러닝 모델을 이용해 NLU 특징을 추출하고 저장하는 과정을 추가로 마련해야 했습니다.

데이터 파이프라인 구축

데이터 파이프라인 구조도
붉은 실선은 실시간으로 전파되는 경로(hot path), 푸른 실선은 일정 주기마다 한번에 처리하는 경로(cold path)를 의미함.

데이터 웨어하우스는 데이터베이스보다 큰 개념으로, 여러 데이터 소스를 통합적으로 관리함으로써 데이터 기반의 의사결정을 돕는 저장소를 뜻합니다. 루다가 만들어내는 모든 정보는 이 데이터 웨어하우스로 흘러들어가게 되는데 그 과정을 위 그림과 같이 데이터 파이프라인으로 표현할 수 있습니다. 이제 데이터 수집부터 데이터 적재 과정까지 파이프라인을 구성하는 각각의 요소에 대해서 알아보겠습니다. 단, 위 그림의 Data Studio 단계에 해당하는 데이터 시각화 과정부터는 다음 포스트 일상대화 챗봇 분석 시스템 구축기 2편 - 데이터 시각화편에서 다뤄보겠습니다!

BigQuery: 클라우드 데이터 웨어하우스

핑퐁팀은 루다로부터 수집한 데이터를 Google Cloud의 데이터 웨어하우스 제품, BigQuery에 저장합니다. BigQuery 소개 페이지에는 BigQuery의 다양한 특징이 나열되어 있는데, 그 중 루다 데이터 파이프라인을 구축하면서 느낀 주요한 장점을 정리해보면 다음과 같습니다.

  1. 쉽고 간편한 분석. BigQuery는 표준 SQL를 지원하기 때문에 익숙한 SQL 문법으로 쉽게 데이터를 분석할 수 있습니다. 시각화 과정 또한 우리의 친구 구글 스프레드시트와 연동하거나, Google Data Studio로 간편하게 진행할 수 있습니다.
  2. 유연한 데이터 스키마. BigQuery 테이블은 배열(ARRAY, 반복된 데이터)이나 구조체(STRUCT, 중첩된 데이터)를 필드로 가질 수 있습니다. 즉 JSON으로 표현할 수 있는 대부분의 데이터 형식을 다룰 수 있습니다. 더불어 이렇게 비정규화된 데이터를 뛰어난 성능으로 처리합니다.
  3. 저렴한 가격 책정. BigQuery는 TB급의 대용량 데이터 처리 비용을 저렴하게 제공합니다. 사용한 쿼리의 데이터 처리량에 비례하여 가격을 청구하므로 비용을 유연하게 관리할 수 있습니다.

Web Application Server

애플리케이션 서버는 페이스북 메신저 웹훅으로부터 사용자의 메시지를 받아서 루다의 답변을 만들어냅니다. 따라서 대부분의 유의미한 데이터는 이 애플리케이션 서버에서 발생합니다. 애플리케이션 서버를 구성하는 언어나 프레임워크에 대한 제약은 딱히 없고, Apache Log4j 2winston 등의 로깅 라이브러리를 이용해서 JSON 형식의 로그를 작성할 수만 있으면 됩니다.

BigQuery의 장점을 살려 로그 스키마 또한 원하는대로 자유롭게 구성할 수 있습니다. 하지만 이후의 분석에서 문제를 겪지 않으려면 다음 목록을 한번 점검해볼 필요가 있습니다.

다음은 위의 제약사항을 고려하여 실제로 루다 애플리케이션 서버에서 사용하고 있는 로그 스키마 예시입니다.

{
  "level": "INFO",
  "loggerName": "<LOGGER_NAME>",
  "message": {
    "logType": "FACEBOOK_READ",
    "logDescription": "루다가 보낸 메시지를 사용자가 읽었습니다.",
    "user_id": "<USER_ID>",
    "read_at": 0
  },
  "instant": {
    "epochSecond": 0,
    "nanoOfSecond": 0
  }
}

Log Collector

애플리케이션 서버에서 로그를 작성하면, 로그 콜렉터가 이를 수집하여 Google Cloud Logging(구 Stackdriver Logging) 서비스로 스트리밍합니다. 로그 콜렉터는 사이드카 패턴으로 구현해서 애플리케이션 서버와는 독립적으로 작동합니다. 이를 통해 다양한 마이크로서비스에 로그 시스템을 쉽게 통합할 수 있는 기반을 마련했습니다.

로그 콜렉터는 이상 현상을 감지하고 알림 메시지를 보내주는 역할도 수행합니다. 예를 들어, 서버에 오류가 발생하면 오류의 이름과 스택 트레이스 등 디버깅에 도움을 주는 정보를 슬랙의 지정된 채널에 보내줍니다. 이 기능 덕분에 개발자는 루다의 예상치 못한 동작에 신속히 대응할 수 있었습니다.

Google Cloud Logging

Google Cloug Logging 로그 뷰어 웹 UI
간단한 분석 도구를 제공하는 로그 뷰어 페이지.

Google Cloud Logging 서비스는 자체적으로 간단한 로그 뷰어를 제공합니다. 이에 더불어 수집한 로그를 BigQuery 같은 서비스로 전달할 수도 있는데, 이를 로그 라우팅이라고 부릅니다. 앞서 데이터 파이프라인 구조도를 통해 보여드렸듯, 루다 서비스는 두 가지의 로그 싱크에 로그를 라우팅하고 있습니다.

첫 번째는 Google Cloud Storage 버킷입니다. 높은 수준의 데이터 내구성을 보장하기 위해 Cloud Storage에 데이터를 저장합니다. 실시간으로 처리할 필요가 없어 1시간 단위로 누적된 로그를 하나의 파일로 묶어 저장합니다. 이때 생성되는 파일은 줄마다 유효한 JSON 객체가 하나씩 있는 JSONL (Line-delimited JSON) 형식을 따릅니다. 이 형식은 jq와 같은 JSON 처리기를 활용하여 분석하기 용이합니다. Cloud Storage에 데이터를 저장하는 1차 목적은 데이터 보존이지만, 클러스터링 같은 복잡한 알고리즘을 구현하는 등 까다로운 작업이 필요할 때는 BigQuery 대신 원본 데이터를 직접 처리하는 방식이 더 효율적일 수 있습니다. 원본 데이터를 다운로드받을 때는 gsutil 등을 활용하면 됩니다.

두 번째는 앞서 설명해 드린 BigQuery입니다. Cloud Logging은 BigQuery에 로그를 추가할 때 한 줄 씩 스트리밍하는 방식을 사용합니다. 따라서 스트리밍된 데이터는 보통 추가된 지 몇 초 이내에 분석에 직접 활용할 수 있습니다.

데이터 분석 맛보기

핑퐁팀은 데이터 파이프라인을 잘 구축한 덕분에 데이터 분석을 매우 간편하게 수행할 수 있었습니다. 이 절에서는 BigQuery Web UI가 켜진 웹 브라우저 하나만으로 탐색적 데이터 분석(Exploratory Data Analysis)을 하는 모습을 맛보기로 보여드리겠습니다.

일별 활동 사용자 수(Daily Active User)나 사용자 리텐션은 대부분의 서비스에서 중요한 지표라서, 조금만 찾아보면 관련 자료를 쉽게 찾아볼 수 있습니다. 특히 구글 공식 문서에서는 BigQuery를 Firebase와 연동했을 때 사용할 법한 샘플 쿼리도 제공하고 있으니 SQL과 BigQuery에 입문하실 때 같이 참고하면 좋을 듯합니다. 이번 포스트에서는 일상대화 챗봇의 특수성을 고려하여 다음과 같이 가벼운 주제를 선정해 분석을 진행했습니다.

“사람들은 루다와 어떤 음식을 주제로 이야기할까?”

쿼리 작성하기

이 문제를 풀려면, “나 오늘 치킨 먹었어”라는 문장에서 음식 관련 단어 “치킨”을 추출해야 합니다. 이러한 작업을 자연어 처리 분야에서는 개체명 인식(Named Entity Recognition, 이하 NER)이라고 부릅니다. 루다의 애플리케이션 서버는 핑퐁팀의 NER 모델로 추출한 개체(entity) 정보를 같이 로깅합니다. 덕분에 아래와 같이 간단한 SQL문으로 BigQuery에서 쉽게 정보를 추출할 수 있습니다.

SELECT
  jsonPayload.message.pipelineResults.KA_NLU_MODEL.kaFineEntities
FROM pingpongflow.pingpongflow_prod_chat
WHERE jsonPayload.message.logType = "CHAT";

위 SQL 문을 쿼리 편집기 창에 입력하고 실행 버튼을 누르면 몇 초 뒤에 바로 쿼리 결과를 확인할 수 있습니다. 코드 강조 기능(syntax highlighting)을 지원하고 문법 오류도 바로바로 알려주어 별도의 코드 편집기 없이도 간편하게 쿼리를 작성할 수 있습니다.

BigQuery 웹 쿼리 편집기 화면
쿼리 편집기. 오른쪽 하단에서 예상 데이터 처리량(과금량)도 볼 수 있음.

복잡한 구조의 자료 다루기

개체명 추출 쿼리 실행 결과

SQL 문의 질의 결과는 CSV나 JSONL 파일로 내보낼 수도 있고, 위 그림처럼 웹브라우저에서도 실행 결과를 미리 볼 수 있습니다. 눈여겨볼 것은 kaFineEntities 필드의 자료형이 복잡한 구조를 포함하고 있다는 점입니다. 이 필드는 중첩된(nested) 특성을 가져, 문자열이나 숫자 등의 원시 자료형(primitive data type)이 아닌 label, length, startIndex, tokens 네 개의 하위 필드로 구성됩니다. 또한, 이 필드는 반복된(repeated) 특성을 가져, 하나의 행이 동일한 형태의 자료를 여러 개 가질 수도 있습니다. 위 그림의 2번째 행을 JSON 형태로 풀어쓰면 다음과 같은 구조가 됩니다.

{
  "kaFineEntities": [
    {
      "label": "지칭/호칭/직업:1인칭",
      "length": "1.0",
      "startindex": "4.0",
      "tokens": [ "나" ]
    },
    {
      "label": "숫자/시간/단위:기념일/명절",
      "length": "1.0",
      "startindex": "6.0",
      "tokens": [ "생일" ]
    },
    {
      "label": "기타:-",
      "length": "2.0",
      "startindex": "10.0",
      "tokens": [ "깊", "콘" ]
    }
  ]
}

중첩, 반복된 필드를 직접 다루기에는 형태가 너무 복잡하기 때문에 우리에게 친숙한 1차원 배열로 바꾸는 평탄화(flatten) 작업이 필요합니다. ARRAY_TO_STRING()을 비롯한 각종 배열 함수 또는 UNNEST 구문을 이용하여 복잡한 자료 구조를 단순화할 수 있습니다. 이외에도 STARTS_WITH() 같은 문자열 함수, COUNT() 같은 집계 함수, GROUP BYORDER BY 문을 적재적소에 활용하면 아래와 같은 쿼리를 완성할 수 있습니다. SQL 구문과 함수에 대한 깊은 설명은 본 포스트의 범위를 벗어나므로 생략하겠습니다.

최종 쿼리 및 실행 결과
사람들이 자주 언급한 음식의 종류와 명칭을 언급 횟수에 대한 내림차순으로 정렬한 결과.

결과를 보면 많은 사람에게 사랑받는 치킨과 피자 얘기도 자주 나오고, 글 작성 시점인 추석에 맞추어 송편 얘기도 자주 보이네요! 이런 정보를 활용해서 사용자가 좋아하는 주제에 루다가 보다 적극적으로 반응하도록 시나리오나 이벤트를 설계할 수 있습니다.

과금량 최적화 가이드

글 작성 시점을 기준으로 BigQuery는 데이터 처리량 1TB 당 5.00~6.00 USD를 청구합니다. (리전에 따라 비용이 다릅니다.) 앞서 시연한 1만건 상당의 NER 정보를 처리한 “자주 이야기된 음식” 쿼리는 청구 금액이 약 0.05원(10MB 기준)으로 아주 저렴합니다. 다만 서비스에 데이터가 누적될수록 처리량도 증가하므로 쿼리 비용이 (문자 그대로) 기하급수적으로 증가한다는 사실을 조심해야 합니다. 따라서 성능뿐만 아니라 비용 절약을 위해서도 쿼리를 잘 설계해야 합니다. 다음은 구글의 공식 문서를 참조하여 과금량을 최적화하는 방법을 요약한 것입니다.

  1. 최소한의 필드만 가져오기. BigQuery는 데이터를 처리할 때 테이블에 있는 모든 열을 조회하지 않고, 쿼리 분석에 필요한 열만을 읽어 옵니다. 따라서 “자주 이야기된 음식” 쿼리에서는 주제와 무관한 필드(대화 시각, 사용자 정보, 다른 NLU 정보 등)가 아무리 무거워도 데이터 처리량에 전혀 영향을 주지 않습니다. 달리 말하면 불필요한 필드를 대량 참조할 수 있는 SELECT * 같은 구문은 최대한 사용을 자제해야 합니다.
  2. timestamp 기간 필터 활용하기. BigQuery의 테이블은 데이터 수집 시간을 기준으로 파티션을 나누는 옵션을 제공합니다. Cloud Logging을 이용하여 BigQuery 테이블을 구축한 경우에는 자동으로 timestamp 필드를 기준으로 테이블이 파티셔닝됩니다. 따라서 쿼리에 WHERE timestamp BETWEEN '2020-09-01' AND '2020-09-30' 같이 기간 필터를 넣을 경우 데이터 처리량을 줄일 수 있습니다.
  3. LIMIT 절은 효과가 없음. 쿼리된 테이블의 행 수를 제한하는 LIMIT 절을 이용해서도 데이터 처리량을 줄일 수 있다고 오해할 수 있습니다. 하지만 BigQuery에서는 LIMIT 절을 적용하더라도 테이블에서 읽는 전체 데이터 양이 줄어들지는 않습니다.

저장된 쿼리

BigQuery 웹 저장된 쿼리 페이지
핑퐁팀에서 실제로 사용하는 사용자 관련 쿼리 모음

BigQuery를 활용하여 데이터를 분석하기 위해서는 SQL을 잘 다뤄야 합니다. 프로그래밍 언어를 능숙하게 다룰 필요는 없어 진입장벽이 그렇게 높지는 않지만, 여전히 개발 경험이 없는 직군에게는 쉽지 않은 도전일 수 있습니다. BigQuery에서는 저장된 쿼리 기능이 있어 개발자나 데이터 분석가가 작성한 SQL 문을 프로젝트 구성원들에게 쉽게 공유할 수 있습니다. 이를 통해 SQL 경험이 부족한 기획자도 간단한 웹 UI 조작을 통해서 쉽게 원하는 데이터를 추출할 수도 있습니다.

테이블과 뷰 사이의 관계도
테이블과 뷰 사이의 개념을 표현하는 관계도. 화살표는 뷰가 해당 테이블(또는 뷰)에서 파생했음을 뜻함.

현재 루다의 애플리케이션 서버에서 발생한 모든 로그는 하나의 테이블에 저장됩니다. 이러한 구조는 비정규화된 대용량 데이터를 뛰어난 성능으로 처리하는 BigQuery의 장점을 살리고, 스키마 변경에도 쉽게 대응할 수 있습니다. 하지만 다양한 주제의 자료가 하나의 테이블에 저장되어 분석 쿼리가 장황해지는 문제가 발생하게 됩니다. 특히 사용자의 인구통계학적 정보(나이나 성별 등) 같은 범용적인 정보가 필요할 때마다 긴 쿼리를 새로 작성해야한다면 번거로울 수밖에 없습니다.

이러한 문제를 해결하기 위해 BigQuery의 뷰를 사용합니다. 뷰는 SELECT 문으로 정의하는 가상 테이블입니다. 내부적으로는 서브쿼리처럼 동작하지만 뷰를 사용할 때는 일반 테이블에서 쿼리하듯이 FROM <view_name> 절을 호출하면 됩니다. 뷰를 사용하면 복잡한 쿼리의 가독성을 개선할 수 있습니다.

GitHub으로 쿼리 관리하기

웹 UI를 이용하면 간편하게 뷰를 만들고 수정할 수 있습니다. 하지만 웹 UI에서 직접 뷰를 관리하게 되면 뷰의 과거 수정 기록을 확인할 수 없습니다. 특히 루다 서비스의 경우 분석에 필요한 데이터의 종류가 시시각각 바뀔 수 있어서, 철저한 버전 관리의 필요성이 대두되었습니다. 핑퐁팀은 이 문제를 GitHub Actions를 도입하여 해결했습니다.

뷰와 SQL 파일을 연동하는 구조도
GitHub Actions를 이용한 뷰의 버전 관리

하나의 뷰는 한 줄의 SQL 문으로 구성됩니다. 따라서 GitHub 저장소에 뷰에 대응하는 *.sql 파일을 작성하여 버전을 관리합니다. 해당 파일의 변경 사항이 리뷰어의 승인을 받아 master 브랜치에 반영되면, GitHub Actions가 *.sql 파일을 토대로 뷰를 갱신하는 스크립트를 작동시킵니다. 이 동기화 스크립트는 Python의 BigQuery 라이브러리를 활용해 만들었습니다. 이처럼 GitHub과의 통합을 통해서 쿼리 변천사를 추적할 수 있었을 뿐만 아니라 이슈 트래커와 풀 리퀘스트를 활용하여 기획자, 데이터 분석가, 개발자가 같이 협업하는 GitHub flow도 구축해냈습니다.

마치며

이번 글에서는 데이터 웨어하우스 BigQuery를 활용해서 데이터 파이프라인을 구성하는 방법을 소개해드렸습니다. 사용자로부터 발생한 데이터가 어떤 경로를 거쳐 수집되고, 저장되는지에 중점을 두었죠. 하지만 수집된 데이터에 가치를 부여하기 위해서는 분석시각화 과정 또한 필수적입니다. 잘 시각화된 분석 결과는 제품 기획자에게 좋은 통찰을 줄 뿐만 아니라, 쭉쭉 성장하는 핵심 성과 지표(Key Performance Indicator)를 보면서 팀 전체의 사기를 돋우는 데에도 긍정적인 영향을 줍니다.

일상대화 챗봇 분석 시스템 구축기 다음 편에서는 Google Data Studio를 이용한 데이터 시각화 과정을 소개해 드리겠습니다. 많은 기대 부탁드립니다!

References

미리보기 이미지 사진의 원 저작자는 Ross Mathieson이며 CC-BY SA 2.0으로 배포됩니다.

핑퐁팀이 직접 전해주는
AI에 관한 소식을 받아보세요

능력있는 현업 개발자, 기획자, 디자이너가
지금 핑퐁팀에서 하고 있는 일, 세상에 벌어지고 있는 흥미로운 일들을 알려드립니다.