[dbt] Semantic Layer 지표 정의 구조와 마트 테이블 방식 비교
1. 개요
시맨틱 레이어(Semantic Layer)는 데이터의 물리적 구조와 비즈니스 의미를 분리하고 지표와 데이터 간 관계를 일관된 기준으로 정의하는 계층이다. 시맨틱 레이어를 통해 사용자는 복잡한 테이블 구조나 SQL을 직접 이해하지 않아도 공통으로 정의된 비즈니스 지표를 조회하고 활용할 수 있다.
dbt Semantic Layer는 이 계층을 dbt 프로젝트 안의 YAML로 정의하고 MetricFlow라는 엔진이 정의를 읽어 SQL을 만들어 데이터 플랫폼에서 실행하는 구조다. 이 글은 dbt v1.12 이상에서 쓰는 최신 YAML 스펙을 기준으로 구성 요소와 정의 단위를 정리하고 같은 질문을 마트 테이블로 답할 때와 비교한다.
본문의 테이블명과 컬럼명은 예시 값이다. 생성 SQL은 MetricFlow의 실제 출력이 아니라 조인과 집계 논리를 보여주기 위해 줄여 쓴 것이고, 실제 출력은 서브쿼리로 감싸져 더 길다. 공식 문서는 2026년 10월에 확인한 내용이다.
2. 구성 요소와 요청 경로
dbt Semantic Layer는 오픈소스 엔진과 유료 플랫폼 서비스로 나뉜다. MetricFlow는 Apache 2.0 라이선스의 오픈소스 엔진으로, YAML 정의를 읽어 SQL을 생성한다. 로컬 환경에 설치하면 정의 검증, SQL 생성, CLI 조회까지 할 수 있다. BI 도구나 외부 애플리케이션이 API로 지표를 조회하는 기능과 캐시, exports는 dbt 플랫폼의 Starter 이상 플랜에서 제공한다.
dbt 플랫폼에서 제공하거나 플랫폼 연결이 필요한 기능은 다음과 같다.
- Semantic Layer API: JDBC와 GraphQL로 외부 도구가 지표를 조회한다.
- 공식 연동: Tableau, Google Sheets, Excel 등 BI 도구와 스프레드시트에서 지표를 선택해 쓴다.
- 결과 캐시와 exports: 자주 쓰는 조회를 미리 계산해 두거나 데이터 플랫폼에 테이블로 기록한다.
- dbt MCP 서버: self-hosted와 remote 방식이 있다. Semantic Layer 지표 조회 도구를 사용할 수 있는 범위는 연결한 dbt 플랫폼의 플랜과 API 권한에 따라 달라진다.
3. 정의 단위
semantic model은 dbt 모델 하나에 의미를 붙인 것이다. 최신 스펙에서는 모델 YAML 안에 semantic_model: enabled: true를 두고 컬럼마다 entity 또는 dimension을 선언하며, 지표는 같은 파일의 metrics: 목록에 둔다. 구 스펙의 최상위 semantic_models: 블록과 measures는 폐지 수순이다.
entity는 조인 키다. 타입은 네 가지다.
primary: 행마다 하나씩 있고 null이 없는 키unique: 유일하지만 null을 허용하는 키foreign: 다른 semantic model을 가리키는 키natural: SCD Type 2처럼 같은 키가 시간에 따라 여러 행에 걸치는 경우
dimension은 지표를 자르거나 필터링하는 기준이며 categorical과 time 두 타입이 있다. time dimension에는 컬럼 수준에서 granularity를 지정하고 모델 수준의 agg_time_dimension이 그 모델에 정의된 지표의 기본 시간축이 된다.
metric은 다섯 타입이다.
| 타입 | 정의 | 예 |
|---|---|---|
simple |
컬럼 식 하나를 집계한다 (sum, count, count_distinct, average, percentile 등) | 매출 = sum(amount) |
ratio |
지표 ÷ 지표 | 객단가 = 매출 / 주문수 |
derived |
지표끼리의 산식. 기간 오프셋 지정 가능 | 전주 대비 매출 증감 |
cumulative |
누적 또는 윈도우 집계 | 월 누적 매출, 최근 30일 활성 사용자 |
conversion |
기준 이벤트 뒤 일정 기간 안에 전환 이벤트가 일어난 비율 | 가입 후 결제 전환율 |
주문 fact 테이블과 고객 dimension 테이블을 최신 스펙으로 정의하면 다음과 같다.
models:
- name: fct_orders
semantic_model:
enabled: true
agg_time_dimension: ordered_at
columns:
- name: order_id
entity:
type: primary
name: order
- name: customer_id
entity:
type: foreign
name: customer
- name: ordered_at
granularity: day
dimension:
type: time
- name: order_status
dimension:
type: categorical
metrics:
- name: revenue
type: simple
agg: sum
expr: amount
- name: order_count
type: simple
agg: count
expr: 1
- name: dim_customers
semantic_model:
enabled: true
agg_time_dimension: first_ordered_at
columns:
- name: customer_id
entity:
type: primary
name: customer
- name: first_ordered_at
granularity: day
dimension:
type: time
- name: region
dimension:
type: categorical
metrics:
- name: avg_order_value
type: ratio
numerator: revenue
denominator: order_count
두 semantic model을 잇는 것은 entity 이름이다. fct_orders.customer_id는 foreign, dim_customers.customer_id는 primary로 선언됐고 둘 다 이름이 customer라서 MetricFlow는 이 둘 사이에 조인 경로가 있다고 본다. 조인 조건은 따로 적지 않는다.
customer라는 entity 이름으로 이어지고, 다른 지표를 참조하는 지표는 최상위 metrics에 둔다.공식 문서는 이 관계를 semantic graph라고 부른다. dbt DAG가 모델 사이의 실행 의존성을 담는 것과 달리 semantic graph는 지표와 기준 사이의 사용 가능한 조합을 담는다. MetricFlow는 entity 타입으로 조인 가능한 경로를 정하고 행이 불어나는 fan-out 조인과 chasm 조인은 만들지 않는다고 문서에 설명돼 있다. primary나 unique entity가 없는 테이블(이벤트 로그 등)의 dimension은 그 테이블 안의 지표에서만 쓸 수 있다.
4. 조회 명령과 생성 SQL
dimension은 엔티티__디멘전 형식으로 부른다. customer__region은 customer 엔티티를 따라 도달하는 dim_customers의 region이고, order__order_status는 fct_orders 자체의 상태다. 시간축은 metric_time이며 metric_time__month처럼 grain을 붙이면 컬럼의 granularity(day)보다 큰 단위로 올린다.
로컬 환경에 MetricFlow를 직접 설치한 경우 mf 명령을 쓴다.
mf query --metrics revenue,avg_order_value --group-by metric_time__month,customer__region
dbt 플랫폼 CLI에서는 dbt sl 명령을 쓰고 필터는 --where에 dimension 참조 문법으로 적는다.
dbt sl query --metrics revenue --group-by metric_time__month --where "{{ Dimension('customer__region') }} = 'KR'"
mf query에 --explain, dbt sl query에 --compile을 붙이면 실행하지 않고 생성 SQL만 본다. 정의 검증은 mf validate-configs 또는 dbt sl validate다.
5. 마트 테이블 방식과의 비교
dbt 모델만 쓰는 경우 질문 하나에 마트 모델 하나를 만든다. 월별 매출은 mart_revenue_by_month, 지역별 매출은 mart_revenue_by_region 같은 식이다. Semantic Layer는 새 테이블을 만들지 않고 3장의 정의로 요청 시점에 SQL을 생성한다. 아래에서 지표와 기준을 바꿔 가며 두 방식이 실행하는 SQL과 누적되는 마트 수를 비교할 수 있다. 기준은 여러 개를 함께 고를 수 있다.
-- models/marts/mart_revenue_by_month.sql
select
date_trunc('month', o.ordered_at) as month,
sum(o.amount) as revenue
from fct_orders o
group by 1mf query --metrics revenue \
--group-by metric_time__monthselect
date_trunc('month', o.ordered_at) as metric_time__month,
sum(o.amount) as revenue
from fct_orders o
group by 1date_trunc('month', o.ordered_at)주문일을 월 단위로 잘라 묶는다agg_time_dimension(ordered_at, day)을 __month로 올린다sum(o.amount)주문 금액을 모두 더한다revenue 지표 · simple · agg: sum · expr: amountfrom fct_orders o주문 1건이 1행인 fact 테이블에서 시작한다지표가 정의된 semantic model이 출발점이다group by 1고른 기준 조합마다 한 행으로 묶는다--group-by 목록을 그대로 쓴다- 월은 주문일(ordered_at) 기준이다. 결제일 기준이 필요하면 agg_time_dimension이 다른 지표를 따로 둔다.
두 방식이 실행하는 SQL 자체는 거의 같다. 차이는 두 가지다. 마트 방식은 사람이 SQL을 쓰고 질문마다 테이블을 추가하며, Semantic Layer는 엔진이 SQL을 생성하고 테이블을 추가하지 않는다.
고객 지역별을 고르면 양쪽 모두 dim_customers 조인이 붙는다. 마트 방식에서는 작성자가 조인 조건을 쓰고, Semantic Layer에서는 customer 엔티티의 foreign·primary 관계를 보고 MetricFlow가 붙인다. 주문 상태별은 fct_orders 안의 dimension이라 조인 없이 바로 쓴다.
마트 수는 지표 수와 기준 조합 수의 곱으로 늘어난다. 위 예시는 지표 3개, 기준 3개라 조합이 3 × 2³ = 24종이고 기준이 하나 늘 때마다 조합은 두 배가 된다. 실무에서는 보통 자주 묻는 조합만 마트로 두고 나머지는 그때마다 SQL을 쓴다. 더할 수 있는 지표(매출, 주문수)는 더 잘게 자른 마트를 다시 집계해 상위 수준 값을 얻을 수 있어 실제 마트 수는 조합 수보다 적다. 비율 지표는 그렇게 되지 않는데, 6장에서 다룬다.
6. 비율 지표와 정의 변경 범위
객단가처럼 비율로 정의된 지표는 그룹별 값을 다시 평균 내도 상위 수준의 값이 나오지 않는다.
| 지역 | 매출 | 주문수 | 객단가 |
|---|---|---|---|
| A | 1,000 | 10 | 100.0 |
| B | 300 | 1 | 300.0 |
| 전체 | 1,300 | 11 | 118.2 |
두 지역 객단가의 평균은 200이지만 전체 객단가는 118.2다. 마트 방식에서도 마트에 객단가 대신 매출과 주문수를 저장해 두고 조회할 때 각각 합산한 뒤 나누면 맞는 값이 나온다. 다만 이 규칙은 마트를 만드는 사람과 마트를 읽는 도구가 각자 지켜야 한다. 마트에 객단가 컬럼이 있으면 BI 도구가 그 컬럼을 평균 내는 것을 막을 방법이 없고, 분자와 분모만 저장했다면 나누는 식을 조회하는 쪽마다 다시 써야 한다. Semantic Layer에서는 avg_order_value가 ratio 타입이라는 사실이 정의에 들어 있어서, 어떤 기준으로 조회하든 MetricFlow가 분자와 분모를 각각 집계한 뒤 나눈다. 조회하는 쪽은 나누는 식을 모른 채 지표 이름만 부른다.
정의가 바뀔 때도 고칠 범위가 다르다. 취소 주문을 매출에서 빼기로 했다면 마트 방식에서는 매출 계산식이 들어간 마트를 전부 찾아 고쳐야 한다. 5장 위젯의 매출 계산식 사본 수가 이 범위다. Semantic Layer에서는 revenue 지표의 expr 한 곳을 고치면 revenue를 참조하는 avg_order_value와 모든 조회에 반영된다.
-- mart_revenue_by_month.sql
-- mart_revenue_by_region.sql
-- mart_avg_order_value_by_*.sql
-- 매출 계산식이 있는 마트 전부
sum(case when order_status
<> 'cancelled'
then amount else 0 end)
as revenuemetrics:
- name: revenue
type: simple
agg: sum
expr: >
case when order_status
<> 'cancelled'
then amount else 0 endrevenue 지표의 expr 한 곳에서 반영된다.7. 도입 시 확인 사항
공식 문서 기준으로 확인한 제약이다.
- YAML 스펙 변경: v1.12부터 최상위
semantic_models:,measures,type_params가 구 스펙이 됐다. 블로그와 예제 대부분이 구 스펙이라 섞이지 않게 해야 하고 기존 정의는dbt-autofix deprecations --semantic-layer로 옮긴다. 설치한 패키지가 구 스펙 정의를 담고 있으면 패키지 쪽도 갱신돼야 파싱 오류가 나지 않는다. - dbt Mesh 프로젝트 간 참조: 최신 스펙에서는 아직 지원하지 않고 구 스펙에서만 된다.
- 지원 데이터 플랫폼: 현재 공식 문서의 목록은 Snowflake, BigQuery, Databricks, Redshift, Postgres, Trino(Starburst Galaxy 포함)다. Microsoft Fabric은 지원하지 않는다. 버전과 실행 환경에 따라 지원 범위가 달라질 수 있으므로 도입 전에 다시 확인한다.
expr에는 순수 SQL만 쓴다. MetricFlow는 dbt 내장 Jinja 함수와 패키지 매크로를 아직 지원하지 않는다.- 플랫폼 의존: Semantic Layer API, 공식 BI 연동, 캐시와 exports는 dbt 플랫폼 Starter 이상 기능이다. MCP 서버는 self-hosted와 remote 방식이 있지만 Semantic Layer 관련 도구의 사용 범위는 플랜과 권한에 따라 달라진다. 로컬 MetricFlow만 사용하면 CLI 조회까지 가능하다.
- v1.12부터는 dbt YAML 대신 Apache Ossie 문서로 semantic model을 정의하는 선택지도 있다.
8. 정리
- dbt Semantic Layer는 entity, dimension, metric을 모델 YAML에 선언하고 MetricFlow가 그 정의로 SQL을 생성하는 구조다. SQL 생성 엔진은 Apache 2.0 오픈소스고 API, 연동, 캐시는 플랫폼 기능이다.
- 두 semantic model은 entity 이름이 같으면 조인 경로가 생기고 조인 조건은 따로 적지 않는다.
- 같은 질문에 대해 마트 방식과 생성되는 SQL은 거의 같다. 마트 방식은 지표와 기준 조합마다 테이블이 늘고 Semantic Layer는 테이블을 추가하지 않는다.
- 비율 지표는 그룹별 값을 다시 평균 내면 틀린다. 마트 방식에서는 분자와 분모를 따로 저장하고 조회 시점에 나누는 규칙을 사람이 지켜야 하고, Semantic Layer는 ratio 타입 정의를 보고 엔진이 매번 그렇게 계산한다. 정의 변경도 지표 한 곳에서 반영된다.
- v1.12 스펙 변경, 프로젝트 간 참조 미지원, 지원 플랫폼 목록, Jinja 미지원은 도입 전에 확인해야 한다.
Leave a comment