안녕하세요. 기술본부 데이터인텔리전스셀 박현기입니다. 이 글에서는 Databricks Unity Catalog의 Metric View를 신규 런칭 게임의 데이터 파이프라인에 도입하면서, 대시보드와 분석 에이전트가 같은 지표 정의를 읽게 하기 위해 데이터 구조를 어떻게 나눴는지, 그 과정에서 정한 설계 규칙과 결과를 공유합니다.
#배경: 쿼리마다 다르게 계산되던 지표
게임의 런칭 직후는 데이터 요청이 가장 많고 가장 급한 시기입니다. 게임 서버 DB의 스냅샷, 클라이언트와 서버가 남기는 분석 로그, 메타 정보 테이블처럼 소스가 여럿이고, 어떤 유저를 지표에서 제외할지, 봇이 섞인 게임을 어떻게 다룰지, 어떤 시점의 값을 쓸지 같은 조건도 복잡했습니다. 그런데 짧은 기간에 여러 대시보드가 만들어지고 분석 요청도 쏟아지면서, 이 조건들이 대시보드마다, 그리고 adhoc 분석 쿼리마다 각자 따로 정의되며 쿼리되고 있었습니다. 각 대시보드의 데이터셋은 자기 쿼리 안에서 필요한 집계를 그 안에서 계산했습니다. 빠르게 만들기에는 좋은 방식이었지만 몇 주가 지나자 세 가지 문제가 드러났습니다.
- 같은 지표가 데이터셋마다 다른 산출식으로 정의됐습니다. 승률 하나만 놓고 봐도 승부가 결정된 게임을 구분하는 조건이 데이터셋에 따라 달랐고, 비정상 데이터 정제 기준(운영 계정 제외, 비정상 유저 제외)도 어떤 곳은 넓고 어떤 곳은 좁았습니다. 정의를 바꾸면 여러 곳을 고쳐야 했고 한 곳을 빼먹는 순간 차트끼리 숫자가 어긋났습니다.
- 집계 전 정제 비 용이 대시보드를 열 때마다 반복됐습니다. 게임 로그는 한 이벤트에 여러 참가자와 아이템이 중첩(nested) 필드로 함께 기록되는 경우가 많습니다. 지표를 계산하려면 이 필드를 행으로 펼치고, 서로 다른 소스 데이터를 합치고, 중복을 걸러 내야 합니다. 이 과정이 대시보드의 데이터셋 쿼리때마다 매번 다시 실행되었으므로, 지표 자체 쿼리보다, 지표 앞의 정제 쿼리 비용과 시간이 더 드는 문제도 있었습니다.
- 분석 에이전트가 지표를 매번 스스로 정의했습니다. 사내 분석 에이전트에 "지난주 승률 상위 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에서 매번 계산된다 |
