thumbnailthumbnail

쿼리가 아니라 정의를 고치기로 했습니다: Databricks Metric View 도입기

박현기Data

안녕하세요. 기술본부 데이터인텔리전스셀 박현기입니다. 이 글에서는 Databricks Unity Catalog의 Metric View를 신규 런칭 게임의 데이터 파이프라인에 도입하면서, 대시보드와 분석 에이전트가 같은 지표 정의를 읽게 하기 위해 데이터 구조를 어떻게 나눴는지, 그 과정에서 정한 설계 규칙과 결과를 공유합니다.

#배경: 쿼리마다 다르게 계산되던 지표

게임의 런칭 직후는 데이터 요청이 가장 많고 가장 급한 시기입니다. 게임 서버 DB의 스냅샷, 클라이언트와 서버가 남기는 분석 로그, 메타 정보 테이블처럼 소스가 여럿이고, 어떤 유저를 지표에서 제외할지, 봇이 섞인 게임을 어떻게 다룰지, 어떤 시점의 값을 쓸지 같은 조건도 복잡했습니다. 그런데 짧은 기간에 여러 대시보드가 만들어지고 분석 요청도 쏟아지면서, 이 조건들이 대시보드마다, 그리고 adhoc 분석 쿼리마다 각자 따로 정의되며 쿼리되고 있었습니다. 각 대시보드의 데이터셋은 자기 쿼리 안에서 필요한 집계를 그 안에서 계산했습니다. 빠르게 만들기에는 좋은 방식이었지만 몇 주가 지나자 세 가지 문제가 드러났습니다.

  1. 같은 지표가 데이터셋마다 다른 산출식으로 정의됐습니다. 승률 하나만 놓고 봐도 승부가 결정된 게임을 구분하는 조건이 데이터셋에 따라 달랐고, 비정상 데이터 정제 기준(운영 계정 제외, 비정상 유저 제외)도 어떤 곳은 넓고 어떤 곳은 좁았습니다. 정의를 바꾸면 여러 곳을 고쳐야 했고 한 곳을 빼먹는 순간 차트끼리 숫자가 어긋났습니다.
  2. 집계 전 정제 비용이 대시보드를 열 때마다 반복됐습니다. 게임 로그는 한 이벤트에 여러 참가자와 아이템이 중첩(nested) 필드로 함께 기록되는 경우가 많습니다. 지표를 계산하려면 이 필드를 행으로 펼치고, 서로 다른 소스 데이터를 합치고, 중복을 걸러 내야 합니다. 이 과정이 대시보드의 데이터셋 쿼리때마다 매번 다시 실행되었으므로, 지표 자체 쿼리보다, 지표 앞의 정제 쿼리 비용과 시간이 더 드는 문제도 있었습니다.
  3. 분석 에이전트가 지표를 매번 스스로 정의했습니다. 사내 분석 에이전트에 "지난주 승률 상위 10개 쿠키"를 물으면 정제되지 않은 로그 데이터를 읽어 답을 냈습니다. 그 과정에서 이탈자를 제외할지, 표본 하한을 둘지, 정제를 어느 기준으로 할지 등등 다양한 기준을 에이전트가 판단하여 생성했습니다. 이런 조건을 에이전트가 읽는 가이드 문서(스킬)에 적어 두기는 했습니다. 그런데 지표마다 붙는 조건이 늘어날수록 스킬이 길어졌고, 새 조건이 생기면 스킬을 고쳐야 했고, 그래도 요청마다 붙이는 테이블과 조건이 조금씩 달라지는 것을 다 막지는 못했습니다.

첫 번째와 두 번째 문제는 사람의 시간과 노력으로 어느정도 해결 가능했지만, 세 번째 문제는 기존의 데이터 환경만으로는 해결이 불가능했습니다.

#Metric View란 무엇인가

Metric View는 Databricks Unity Catalog에서 제공하는 의미 계층(semantic layer) 기능입니다. 지표의 정의를 view 객체로 카탈로그에 저장해 두고, 대시보드든 SQL이든 에이전트든 그 view를 조회해 같은 값을 읽게 하는 것이 목적입니다.

의미 계층은 "승률", "매칭 성공률"처럼 지표에 이름을 붙이고, 해당 지표가 어떤 데이터를 어떻게 집계한 값인지를 한 곳에 정의해 두는 레이어입니다. 소비자는 집계식을 직접 쓰지 않고, 정의된 지표 필드를 select 하면 됩니다. BI 도구의 데이터 모델, dbt의 Semantic Layer, LookML 같은 형태로 오래 쓰여 온 개념으로, 같은 지표를 소비처마다 다시 계산하지 않고 한 번 정의한 것을 모두가 같은 값으로 읽게 하는 것이 목적입니다.

Databricks의 Metric View는 이 개념을 Unity Catalog 안에 구현한 것입니다. 테이블 위에 measure(측정값)와 dimension(차원)을 YAML로 한 번 정의하면, 소비자는 measure() 함수로 지표를 호출하고 group by만 바꿔 어떤 단위로든 같은 정의를 읽을 수 있습니다. 일반 뷰는 만들 때 집계와 그룹 단위가 고정되지만, Metric View는 정의만 고정되고 단위는 조회 시점에 정해집니다.

승률을 예시로 둘 간의 차이를 살펴봅시다. 일반 뷰에서는 집계 단위를 뷰를 만들 때 정해야 합니다.

create view win_rate_daily_by_mode as
select base_date, mode_id,
  count(*) filter (where is_win) * 100.0 / count(*) as win_rate
from battle_play
group by base_date, mode_id

이 뷰는 일별, 모드별 승률만 답할 수 있습니다. 쿠키별 승률이 필요하면 group by가 다른 뷰를 하나 더 만들어야 하고, 월 승률을 내려고 이 뷰의 일별 승률을 avg() 하면 플레이 수가 많은 날과 적은 날이 같은 비중으로 계산되어 틀린 값이 나옵니다.

반면, Metric View는 같은 정의를 dimension과 measure로 나눠 적습니다.

# metric view 의 정의
version: 1.1
source: battle_play
dimensions:
  - name: base_date
    expr: base_date
  - name: mode_id
    expr: mode_id
  - name: cookie_id
    expr: cookie_id
measures:
  - name: win_rate
    expr: count(*) filter (where is_win) * 100.0 / count(*)
-- metric view 의 쿼리 예시
-- 일별, 모드별
select base_date, mode_id, measure(win_rate) as win_rate
from win_rate_metric_view
group by base_date, mode_id

-- 월별
select date_trunc('month', base_date) as base_month, measure(win_rate) as win_rate
from win_rate_metric_view
group by date_trunc('month', base_date)

승률 정의는 YAML 형태로 한 곳에 정의되어 있고, 위 예시에서 보듯이 group by 가 다른 쿼리를 할 수 있습니다. 앞선 일 단위 view 에서는 해당 view 를 사용해 월 단위 쿼리를 할 수 없었지만, Metric View 에서는 월 단위 쿼리도 일별 승률을 평균 내는 것이 아니라 그 달의 row 전체에서 승률을 다시 계산하므로 값이 정확합니다.

앞서 언급했듯 의미 계층 개념은 과거에도 있었으나 대개 BI 도구 내 개념으로 존재했습니다. 그래서 정의가 그 도구 안에서만 통했고, 같은 지표를 다른 도구나 SQL, 노트북에서 보려면 다시 정의해야 했습니다. 반면, Metric View는 BI 도구가 아니라 데이터 카탈로그 (Databricks 의 Unity Catalog)에 위치하므로, 어떤 도구에서든 SQL로 같은 정의를 조회할 수 있고, 누가 이 지표를 볼 수 있는지도 다른 테이블과 같은 방식으로 카탈로그에서 관리합니다. 그래서 대시보드, SQL, 노트북, 외부 BI 도구, 에이전트가 모두 같은 정의를 읽을 수 있습니다. 실제로, Databricks에서도 Metric View 출시 발표에서 아래와 같이 특히 마지막 소비자를 강조하고 있습니다.

분석가, 엔지니어, 경영진, 그리고 이제는 AI 에이전트까지 같은 데이터를 서로 다르게 해석하는 일이 잦고, 그 결과 지표가 어긋나고(metric drift) 보고서가 충돌하며 신뢰가 떨어집니다. 에이전트가 데이터 위에서 추론하고 자율적으로 행동하는 시대에는, 흩어진 정의가 혼란을 만드는 데서 그치지 않고 그 혼란을 확장합니다.

출처: Databricks, "Announcing General Availability and Open Sourcing of Unity Catalog Business Semantics" (2026)에서 옮김

저희가 Metric View를 사용하려는 이유도 이 마지막 소비자에 있습니다. 대시보드 성능만이 목적이었다면 데이터 마트로 충분했을 것입니다. 그러나 분석 에이전트는, 대시보드에서 보는 group by 기준보다 더 다양한 기준으로 데이터를 분석할 필요가 있을 때에 데이터 마트보다 더 낮은 레이어의 데이터를 조회할 필요가 있었고, 이 때 에이전트가 정제되지 않은 로그 데이터를 읽어 지표를 스스로 정의할 때 값이 대시보드에 표시되는 값과 어긋나는 문제를 겪고 있었는데요. Databricks가 출시 발표에서 말한 "흩어진 정의가 혼란을 확장한다"는 문장이 저희 환경에서 그대로 재현되고 있었던 것입니다. 따라서 에이전트가 정의를 읽을 수 있는 레이어가 필요했고, 그 레이어는 사용중인 대시보드와 같은 정의를 공유해야 했습니다.

#설계: 정의는 한 레이어에, 정제는 그 아래에

그간 데브시스터즈에서는 Databricks 의 메달리온 아키텍쳐 (Medallion Architecture) 에 따라 Bronze, Silver, Gold 로 구분된 레이어로 데이터 웨어하우스를 관리하고 있었습니다. 그러면 Metric View 는 어디에 위치해야 할까요?

Metric View 는 역할로 보면 Gold와 같은 소비 레이어지만, 집계 단위를 미리 고정하지 않고 정의만 가진다는 점이 다릅니다. 한편, Metric View 의 소스 데이터가 Bronze 레이어일 수도 있겠는데요, 앞서 말씀 드린 것처럼 쿼리 시 nested field 의 처리와 같은 가공 비용을 중복 지불하고 싶지 않았기 때문에, 해당 처리가 다 된 데이터를 Silver 에 두고, Metric View 는 Gold 레이어와 동일하게 Silver 의 데이터를 쿼리하는 형태로 제한적으로 사용하기로 했습니다.

그래서, 우리는 Metric View 를 테이블이 아니라 Silver를 읽는 view 로 두고, Metric View를 Semantic이라는 이름의 레이어로 따로 분류하여, Gold와 나란히 Silver 위에 두었습니다. 최종적으로, 각 레이어의 역할은 우리 데이터 구조에 맞게 다음과 같이 정했습니다.

레이어역할
Bronze원본 그대로. 변경하지 않습니다.
Silver데이터 정합성. 중첩 필드를 펼치고, 서로 다른 소스에 남은 기록을 합치고, 중복을 제거하고, 정제 flag를 붙입니다. measure 로직이 읽는 값은 모두 여기서 만듭니다. 지표는 계산하지 않습니다.
Gold소비 최적화. 집계 단위를 고정해 미리 계산한 물리 테이블입니다. 고유 유저 수나 분위수처럼 미리 집계한 캐시로 만들 수 없는 지표를 고정된 단위로 빠르게 봐야 할 때만 만들고, 그 외의 지표는 Metric View에 둡니다.
Semantic (Metric View)비즈니스 의미. measure 산식과 dimension을 한 곳에서 정의합니다.

#미리 집계한 결과를 읽어 조회 비용을 줄인다

일반 뷰는 조회할 때마다 원본 테이블에서 집계를 다시 합니다. Metric View도 처음 출시 때는 정의만 저장하고 결과는 저장하지 않아 기본 동작이 같았습니다. 조회할 때마다 정의를 읽어 집계를 다시 하는 구조라, 같은 결과를 반복해서 여는 대시보드에는 미리 계산해 둔 Gold 테이블이 따로 필요할 것으로 예상했습니다. 그런데 이후 추가된 materialization 기능이 이 걱정을 대부분 해결했습니다. 자주 쓰는 dimension 조합을 지정해 두면 Databricks가 그 조합의 집계 결과를 미리 계산해 저장해 둡니다. 이렇게 저장한 것을 aggregated materialization 이라고 부르겠습니다. 소비자가 그 조합으로 조회하면 정의를 다시 계산하지 않고 저장된 결과를 읽습니다. 대시보드가 쓰는 조합을 여기에 등록하는 것으로 대부분의 대시보드가 충분히 빠르게 그려졌습니다.

다만 이 캐시가 모든 measure에 통하는 것은 아닙니다. 일별 플레이 수를 더하면 월별 플레이 수가 되듯, 미리 집계한 값을 다시 합쳐도 맞는 measure에만 쓸 수 있습니다. 고유 유저 수나 분위수는 일별 값을 더해도 월별 값이 되지 않으므로, 이런 measure는 집계하지 않은 개별 row 전체를 저장한 unaggregated materialization 에서 조회 시점에 다시 계산됩니다. 그래서 조회 기간이 길수록 느려지고, 이런 지표를 고정된 단위로 자주 빠르게 봐야 하는 데이터셋에만 Gold 테이블을 만들었습니다. 이 구분은 규칙 절에서 가산성이라는 이름으로, Gold 테이블을 만드는 기준은 판단 절에서 다시 다룹니다.

#규칙: 가산성과 비가산성 구분하기

Metric View를 설계하면서 가장 주의해야 했던 것은 measure의 가산성(additivity) 이었습니다. 가산 measure는 작은 단위의 값을 더해 큰 단위의 값을 만들어도 맞습니다. 일별 플레이 수를 더하면 월 플레이 수가 됩니다. 비가산 measure는 그렇게 하면 틀립니다. 일별 고유 유저 수를 더하면 여러 날 접속한 유저가 중복되어 월 고유 유저 수보다 커지고, 일별 중앙값을 평균 내도 월 중앙값이 되지 않습니다. 이 구분에 따라 미리 집계해 둘 수 있는지, measure의 comment에 합산 주의를 적어야 하는지, 소비자가 결과를 다시 합쳐도 되는지가 정해졌습니다.

종류지표 예산출식 예합산해도 되는가미리 집계해 둘 수 있는가
가산플레이 수, 승리 수, 대기시간 합count(*), sum(...), count(*) filter (where ...)어느 단위에서 합산해도 안전하다있다. aggregated materialization에 넣는다
비가산고유 유저 수, 대기시간 중앙값count(distinct ...), percentile_approx(...)합산하면 틀린다. 개별 row에서 다시 계산해야 한다없다. unaggregated materialization에서 매번 계산된다

#평균은 합과 건수로 나눠 정의한다

평균은 조회 시점에 계산하는 한 avg()로 정의해도 어떤 단위에서든 정확합니다. 문제는 미리 집계한 결과를 다시 쓸 때입니다. materialization의 aggregated 캐시는 미리 집계한 값을 더 큰 단위로 합쳐서 씁니다. 그런데 평균은 합칠 수 없습니다. 일별 평균 두 개를 평균 내면 표본 수가 많은 날과 적은 날이 같은 비중으로 계산되어 월 평균과 다른 값이 나옵니다. 그래서 avg() measure는 캐시가 적용되지 않고 매번 개별 row에서 계산됩니다. 소비자가 일별 평균을 받아 바깥에서 다시 평균 내는 경우도 같은 문제를 겪습니다.

합과 건수는 각각 합칠 수 있습니다. 그래서 평균을 합 measure와 건수 measure로 나눠 정의하고, 둘을 나눈 조합 measure를 따로 두었습니다. 캐시에서 합과 건수를 큰 단위로 합친 뒤 나누면 정확한 평균이 나오고, 소비자가 어떤 단위로 group by 하든 그 단위에서 다시 나눠집니다.

measures:
  - name: wait_time_seconds_sum
    expr: sum(case when is_success = 'Y' then wait_time_seconds end)
    comment: 대기시간 합(초) — 성사된 매칭
  - name: wait_sample_count
    expr: count(*) filter (where is_success = 'Y')
    comment: 대기시간 표본 수 — 성사된 매칭
  - name: wait_time_seconds_avg
    expr: measure(wait_time_seconds_sum) / nullif(measure(wait_sample_count), 0)
    comment: 평균 대기시간(초)
  - name: wait_time_seconds_p50
    expr: percentile_approx(case when is_success = 'Y' then wait_time_seconds end, 0.50)
    comment: 대기시간 중앙값(초) — 성사된 매칭 [non-additive]

#점유율은 window measure로

쿠키 픽률처럼 "이 쿠키의 플레이 수 ÷ 모드 전체 플레이 수"인 지표는 분모가 분자보다 넓은 범위를 합친 값입니다. Databricks는 이를 위해 window measure를 제공합니다. 분모 measure의 window 정의에서 쿠키 축을 제외하면, 소비자가 어떤 단위로 group by 하든 분모는 쿠키를 무시한 값으로 계산됩니다.

dimensions:
  - name: mode_id
    expr: mode_id
  - name: cookie_id
    expr: cookie_id
measures:
  - name: mode_total_play_count
    expr: count(*)
    window:
      - order: cookie_id
        range: all
        semiadditive: last
    comment: 모드 전체 플레이 수 — 쿠키를 무시한 분모 [LOD:exclude=cookie_id]
  - name: pick_ratio
    expr: count(*) * 100.0 / nullif(measure(mode_total_play_count), 0)
    comment: 쿠키 픽률(%) — 모드 전체 대비 [LOD:exclude=cookie_id]

다만 분모에서 제외한 dimension(여기서는 쿠키)이 소비 쿼리의 group by에 없으면 분자와 분모가 같아져 에러 없이 조용히 100%가 나오므로, comment에 그 축을 적어 둡니다.

#comment에 무엇을 적는가

우리는 measure comment에 세 가지만 적기로 했습니다. 무엇을 세는지, 이름에 드러나지 않는 모집단 조건, 그리고 합쳐도 되는지를 알려 주는 태그입니다. 산출식은 정의 자체에 이미 있어 다시 적으면 중복이고, comment가 길어지면 정작 봐야 할 경고를 놓치게 되므로 적지 않습니다.

"합쳐도 되는지"를 따로 적는 이유는 아쉽게도 현재의 Metric View가 합치면 안되는 집계를 수행하는 것을 막는 기능이 없기 때문입니다. 정의된 dimension이면 어느 것으로든 group by 할 수 있고, 그 조합이 지표의 의미에 맞는지는 보지 않습니다. 게임 단위로 세는 고유 게임 수를 참가자의 실력 구간으로 group by 하면 한 게임이 여러 구간에 중복 집계되어 합이 전체보다 커지지만, 에러는 나지 않고 그럴듯한 숫자가 나옵니다. dbt의 Semantic Layer는 시간 축으로 합산하면 틀리는 measure를 non_additive_dimension으로 선언해 엔진이 막아 줍니다. Metric View는 현재 YAML 스펙 1.1 기준으로 일반 measure에 그런 선언을 제공하지 않습니다. 그래서 이 제약은 comment에 적어 둡니다. 사람이든 에이전트든 comment를 읽고 쿼리를 쓰는 것은 같지만, 에이전트에게 특히 중요한 정보입니다. 사람은 뜻이 애매하면 만든 사람에게 물어볼 수 있지만, 에이전트는 dimension과 measure의 이름, 타입, comment, 산출식만 보고 바로 쿼리를 씁니다. 산출식만으로는 count(distinct ...)를 월 단위로 합산하면 틀린다는 것을 알 수 없으니, comment에 적혀 있으면 월 범위로 다시 group by 하고 적혀 있지 않으면 스스로 판단해 버립니다.

comment에 붙이는 태그는 두 가지를 사용했습니다. [non-additive]는 집계된 값을 다시 합치면 틀린다는 뜻이고, [LOD:exclude=<dim>]<dim>에 적힌 축을 group by에서 빼면 틀린다는 뜻입니다. 가산 measure와 그것으로 만든 비율에는 태그를 달지 않는데요. 태그가 없다는 것 자체가 "합쳐도 안전하다"는 신호가 되도록 한 것입니다. 같은 태그 설명을 분석 에이전트가 읽는 가이드에도 넣어 두어, Metric View를 만드는 쪽과 조회하는 쪽이 같은 규칙을 보게 했습니다.

#판단: Gold 테이블과 Metric View 중 무엇을 만들 것인가

Metric View를 도입하면 "어차피 집계를 미리 해 두는 것이라면 Gold 테이블과 무엇이 다르고, 어느 쪽을 만들어야 하는가"라는 질문이 자연스럽게 따라옵니다. 저희도 지표마다 이 질문을 받았고, 그때 쓴 기준을 정리하면 다음과 같습니다.

기준Gold 테이블Metric View
조회 속도고정된 단위로 이미 집계되어 있어 가장 빠르다aggregated materialization이 맞으면 비슷하고, 맞지 않으면 unaggregated materialization에서 다시 집계된다
dimension 자유도만들 때 정한 단위로만 볼 수 있다. 새 축이 필요하면 테이블을 새로 만든다정의된 dimension 안에서는 어떤 조합으로도 볼 수 있다
비가산 지표(distinct, 분위수)고정 단위에서만 정확하다. 다른 단위로 다시 집계할 수 없다어떤 단위로 group by 하든 개별 row에서 다시 계산된다
소비처집계 단위가 같은 조회에 적합하다. 단위가 같으면 대시보드 여러 개가 공유할 수 있다대시보드 여러 개, 분석가의 adhoc 쿼리, 에이전트가 단위와 상관없이 같은 정의를 공유할 때 적합하다
지표 추가 비용컬럼을 더하면 그 컬럼을 전 기간 다시 적재해야 하고, 새 단위가 필요하면 테이블을 새로 만든다YAML에 measure를 더하면 다시 적재하지 않고 바로 쓸 수 있다. dimension이 바뀌면 캐시 전체를 다시 계산한다
에이전트와의 관계컬럼이 이미 계산된 값이라 산출식을 에이전트 가이드에 따로 적어 줘야 한다에이전트가 Metric View의 정의(산출식, comment)를 직접 읽으므로 가이드에 산출식을 옮겨 적을 필요가 없다

표에서 보듯 어떤 dimension으로 쪼갤지 미리 정해지지 않은 지표와 단위에 따라 다시 계산돼야 하는 비가산 지표는 Metric View가 맞습니다. 반대로 집계 단위가 고정되어 있고 속도가 가장 중요한 표는 Gold 테이블이 더 단순하고 빠릅니다. 대시보드만 놓고 보면 대부분의 차트는 Gold 테이블로 충분합니다. 저희가 그럼에도 Metric View를 택한 이유는 에이전트 때문입니다. 에이전트는 질문마다 다른 dimension 조합으로 지표를 부르기 때문에, 단위를 고정한 Gold 테이블로는 미리 대비할 수 없었습니다.

Gold 테이블을 만드는 기준도 여기서 나옵니다. 기본은 Silver 위의 Metric View입니다. 속도가 필요한 조회에 비가산 measure가 들어 있어 unaggregated materialization에서 계산해도 충분히 빠르지 않은 경우에만 그 조회를 단위를 고정한 Gold 테이블로 만듭니다. 가산 measure만 쓰는 조회는 aggregated materialization으로 충분하므로 Gold 테이블이 필요하지 않습니다.

#결과: 전환 전후의 비용 비교

Metric View를 도입한 뒤 같은 지표를 서로 다른 정의로 계산하는 일은 줄었습니다. 그렇다면, 로그를 직접 읽던 때보다 비용도 줄었을까요? 앞 절에서 정한 대로 대시보드를 Metric View로 옮겼으니, 옮기기 전과 후를 비교해 보았습니다. 기존 쿼리와 Metric View로 옮긴 쿼리를 같은 파라미터로 1회씩 cold 실행해 compute와 스캔량을 비교했습니다. 가장 무거웠던 대시보드는 데이터셋 합산 결과, 1회 실행 기준 compute 41% 감소, 스캔량 6.7배 감소였습니다. 다만 폭은 데이터셋마다 달랐습니다. 로그를 정제하는 비용이 컸던 곳은 compute가 0.3배까지 줄었고, 원래 가볍던 곳은 0.8배 안팎이었습니다.

에이전트에서는 어땠을까요? 사내 분석 에이전트가 참고하는 도메인 지식을 Metric View 위주로 다시 구성한 뒤, 같은 질문 세트로 전과 후를 비교했습니다. 답을 내는 데 쓴 토큰과 비용은 약 절반으로, 답까지 거친 턴 수는 30% 줄었습니다.

앞에서 말한 "승률 상위 10개 쿠키" 질문이 대표적입니다. 로그를 직접 읽던 때는 에이전트가 먼저 어떤 테이블을 쓸지 찾고, 정제 기준과 승리 판정 조건을 스스로 정해 긴 쿼리를 짠 뒤, 결과가 미심쩍어 한 번 더 확인했습니다. Metric View를 쓴 뒤에는 정의된 measure를 한 번 호출하는 것으로 끝났습니다. 두 답 모두 상위 쿠키의 순서는 맞았지만, 로그를 직접 읽은 답은 정제 기준과 판정 조건을 에이전트가 골랐으므로 승률 숫자가 대시보드에 표시되는 값과 조금씩 달랐던 반면, Metric View 를 사용하면서부터는 대시보드와 같은 값을 산출할 수 있었습니다.

#보완: Metric View를 운영하면서 배운 것

배포한 뒤 석 달 동안 Metric View를 고치면서 알게 된 것 중, Metric View의 동작 방식과 관련된 것만 정리합니다.

#소비자가 항상 거는 필터가 캐시에 없으면 캐시는 쓰이지 않는다

대시보드 데이터셋은 거의 전부 정상 유저 flag를 where로 걸고 있었습니다. 그런데 aggregated materialization의 dimension 목록에 그 flag가 빠져 있었습니다. Databricks의 query rewrite는 group by뿐 아니라 where에 쓰인 dimension도 materialization이 가지고 있어야 그것을 후보로 삼습니다. explain extended로 확인하니 미리 집계해 둔 캐시가 적용되지 않고 전부 unaggregated materialization으로 가고 있었습니다. 소비자가 항상 거는 필터 dimension을 aggregated에 포함하는 규칙을 추가했습니다.

#dimension이 바뀌면 캐시 전체가 다시 만들어진다

materialization은 정의가 바뀌면 전 기간을 다시 계산합니다. measure만 고치면 다시 계산하지 않지만, dimension을 더하거나 바꾸면 전 기간을 다시 계산해 dimension 하나를 추가하는 비용이 생각보다 큽니다. 일반 마트처럼 날짜별로 다시 적재하는 개념이 없기 때문입니다. 그래서 source가 전 기간을 다시 계산할 수 있는 쿼리인지가 설계 조건이 되었고, dimension을 추가할 때는 그 축이 정말 필요한지와 전체 재계산 비용을 먼저 따지게 됐습니다.

#비가산 지표는 개별 row의 캐시가 있어야 조회할 수 있다

distinct 유저 수와 분위수는 어떤 단위로 group by 하든 개별 row에서 다시 계산해야 하므로 집계된 캐시로는 커버할 수 없습니다. 그래서 개별 row 전체를 저장하는 unaggregated materialization이 항상 필요하고, 이것을 빼고 aggregated만 두는 구성은 성립하지 않습니다. 비가산 measure를 하나라도 부른 쿼리는 전체가 unaggregated materialization에서 계산됩니다. 그래서 대시보드에서는 자주 보는 비가산 지표를 가산 지표와 다른 차트로 분리했습니다. 대시보드는 어떤 measure를 어떤 조합으로 부를지 미리 정해져 있어 이렇게 나눌 수 있지만, 에이전트는 질문마다 부르는 measure가 달라 같은 방법을 쓸 수 없었습니다.

#필터를 measure에 고정하면 소비자가 선택할 수 없다

대기시간 measure에 "성사된 매칭 중 봇이 없는 매칭만"이라는 조건을 고정했습니다. 그러자 봇 매칭 비중이 높은 시기에 봇을 포함한 대기시간을 볼 수 없었습니다. 그렇다고 모든 조건을 dimension으로 빼면 지표 정의가 흩어지므로, 지표 정의에 속한 조건(성사된 매칭만)은 measure 안에 남기고 소비자가 상황에 따라 바꿔 쓸 조건(봇 포함 여부)은 dimension으로 노출하기로 했습니다.

#남은 과제

Metric View 도입을 마치고 나서 보니 measure가 너무 많아졌습니다. 비슷한 이름의 measure가 여럿이고, 어느 것을 써야 하는지 정의를 열어 봐야 알 수 있는 것도 있습니다. 전환 초기에 저희는 대시보드에 있는 숫자를 모두 Metric View에서 바로 꺼낼 수 있어야 한다고 보고, 기존 대시보드의 쿼리 결과를 기준으로 삼았습니다. 결과에 있는 컬럼은 전부 dimension이나 measure가 되어야 하고, 전환 전과 후의 값이 컬럼 단위로 일치하는 것을 완료 조건으로 두었습니다. 빠르게 옮기고 바로 검증할 수 있는 방식이었지만, 그 결과 Metric View의 형태가 대시보드 결과의 형태를 그대로 따라갔습니다. 승률 하나가 있으면 "봇을 제외한 승률", "특정 모드의 승률", "1위 기준 승률"처럼 조건이 하나 붙을 때마다 measure가 하나씩 생겼습니다. 두 measure를 나눈 지수처럼 다른 measure에서 파생되는 것도 전부 measure가 됐습니다.

이런 파생 지표들은 Metric View 밖에 두는 게 좋았을 것 같습니다. 조건이 붙은 승률은 승리 수와 플레이 수에 필터를 걸면 되기 때문인데요. 그래서 Metric View에는 다른 지표의 재료가 되는 기본 measure와 단위에 따라 다시 계산해야 하는 비가산 measure만 남기는 방향으로 정리하려고 합니다.

또 하나, 이 작업에서 정한 규칙들(가산성 판정, comment 태그, materialization 배치, 검증 절차)을 사람이 매번 기억하기는 어렵습니다. 그래서 Metric View를 만들고 고치는 절차 자체를 코딩 에이전트의 스킬로 옮기는 작업을 진행하고 있습니다. 위 규칙을 위반한 YAML을 기계적으로 잡아내고, 지표 정의의 의도처럼 사람이 판단해야 하는 항목은 명시적으로 리뷰를 요청하게 하는 것이 목표입니다. 규칙이 문서에만 있지 않고 스킬이 자동으로 검사하게 되면, 이 글의 구조를 다른 게임에 적용할 때도 같은 품질을 기대할 수 있지 않을까 합니다.

#마치며

지표 정의가 대시보드 쿼리에도, 분석 쿼리에도, 에이전트가 읽는 스킬 문서에도 따로 적혀 있으면, 정의가 조금씩 달라지는 것을 막을 수 없습니다. 그래서 저희는 지표 정의를 Metric View에 모아 두고, 대시보드와 에이전트가 모두 그 정의를 그대로 쓰게 했는데요. 대시보드에서 보는 숫자와 에이전트가 답하는 숫자가 달라서 어느 쪽이 맞는지 확인하는 일이 눈에 띄게 줄었습니다. 정의를 모으는 것이 목적이었지만, 정제와 집계를 매번 반복하지 않게 되면서 조회 비용도 함께 줄일 수 있었습니다.

같은 지표인데 조회하는 곳마다 다른 값이 나오는 문제를 겪고 있으시다면, 각 쿼리를 고치기 전에 지표 정의를 한 곳에 모을 방법부터 검토해 보시길 권합니다. 감사합니다.

deco cookie

데브시스터즈는 최고의 인재를 찾고 있습니다.

자세한 내용은 채용 사이트를 확인해 주세요!