dbt 세만틱 레이어 · 메트릭: MetricFlow로 지표 표준화

세만틱 레이어 — 흩어진 매출 정의가 하나의 metric 정의로 수렴하고, 일관된 SQL이 여러 소비자로 뻗어나간다 SEMANTIC LAYER 흩어진 정의 — 같은 이름, 다른 숫자 단일 진실 공급원 여러 소비자 — 같은 숫자 대시보드 A (마케팅) SUM(total) revenue = 128 대시보드 B (재무) SUM(total − refund − tax) revenue = 96 노트북 C (분석가) SUM(total − refund) revenue = 103 metric: revenue type: simple measure: order_total label: "매출" YAML · PR 리뷰 · CI MetricFlow 조회 시점 SQL 생성 — 조인 · 집계 · 시간 그레인 BI 대시보드 (JDBC) 매출 = 103 노트북 · Python (GraphQL) 매출 = 103 사내 앱 · API (GraphQL) 매출 = 103 = =
대시보드마다 제각각이던 "매출" 정의가 하나의 metric 정의(단일 진실 공급원)로 수렴하고, MetricFlow가 조회 시점에 생성한 일관된 SQL이 BI·노트북·API 모든 소비자에게 같은 숫자를 돌려준다

들어가며

지금까지 다섯 단계에 걸쳐 우리는 dbt로 신뢰할 수 있는 변환 그래프를 세우는 법을 익혔습니다. 모델·ref·소스로 DAG를 그리고, 테스트·문서화로 신뢰를 얹고, 매크로·Jinja로 반복을 없애고, incremental·snapshot으로 규모와 이력을 감당했으며, 5단계 dbt 패키지 · CI에서는 패키지와 Slim CI로 팀 규모의 안전한 배포까지 세웠습니다. 이제 웨어하우스에는 테스트를 통과하고 리뷰를 거친 mart 테이블들이 놓여 있습니다.

그런데 여기서 끝이 아닙니다. mart가 아무리 깨끗해도, 그 위에서 “매출”을 계산하는 SQL은 여전히 대시보드마다 제각각일 수 있습니다. 경영 회의에서 두 팀이 서로 다른 “이번 달 매출”을 들고 오는 사고는 mart의 품질 문제가 아니라 지표 정의가 소비자마다 흩어져 있다는 구조의 문제입니다. 이 글은 dbt-Essential 시리즈의 마지막 6단계(심화)로(dbt Essential Curriculum), dbt 세만틱 레이어(Semantic Layer)MetricFlow로 지표 정의를 코드로 한 번만 내리고 모든 소비자가 그 정의를 공유하게 만드는 — dbt를 변환 도구에서 조직의 지표 단일 진실 공급원으로 끌어올리는 — 여정으로 시리즈를 완주합니다.

📌 이 글에서 다루는 내용

  • 세만틱 레이어의 문제의식: 같은 이름·다른 SQL의 지표 파편화, 차원 조합마다 늘어나는 mart 테이블 폭발, “단일 정의 → 다중 소비”의 가치와 조회 시점 SQL 생성이라는 해법
  • MetricFlow: semantic model 위에 선언하는 entity·dimension·measure, metric 4유형(simple·ratio·derived·cumulative), 지표+차원 질의에서 조인·집계 SQL을 생성하는 원리, mf query로 검증하는 흐름
  • 지표 표준화: JDBC·GraphQL API로 BI·노트북이 같은 정의를 소비, saved query와 export, 지표 변경도 PR·리뷰·테스트를 거치는 거버넌스, “어디까지 mart로 굽고 어디부터 세만틱 레이어에 맡길까”의 실무 판단 기준

한눈에 보기 — 하나의 정의, 여러 소비자

세만틱 레이어의 핵심 아이디어는 한 문장입니다 — 지표 정의는 코드로 한 번만, SQL은 조회 시점에 생성한다. 지표(무엇을 어떻게 집계하나)와 차원(무엇으로 쪼개나)의 정의를 dbt 프로젝트의 YAML로 내려 두면, MetricFlow가 “이 지표를 이 차원으로”라는 질의를 받을 때마다 조인과 집계가 올바른 SQL을 만들어 웨어하우스에 실행합니다. BI 대시보드도, 노트북도, 사내 앱도 각자 SQL을 다시 쓰는 대신 같은 정의에 질의합니다.

flowchart TD
    DEF["metric 정의 (YAML, dbt 프로젝트)<br/>semantic model + metrics<br/>버전 관리 · PR 리뷰 · CI 테스트"]

    subgraph SL["dbt 세만틱 레이어"]
        MF["MetricFlow<br/>entity 그래프에서 조인 경로 해석"]
        GEN["조회 시점 SQL 생성<br/>조인 · 집계 · 시간 그레인"]
    end

    WH[("데이터 웨어하우스<br/>mart 테이블 위에서 실행")]

    BI["BI 대시보드<br/>(JDBC)"]
    NB["노트북 · Python<br/>(GraphQL / SDK)"]
    APP["사내 앱 · API<br/>(GraphQL)"]

    BI -->|"지표 + 차원 질의"| MF
    NB -->|"지표 + 차원 질의"| MF
    APP -->|"지표 + 차원 질의"| MF
    DEF --> MF
    MF --> GEN
    GEN --> WH
    WH -->|"일관된 결과"| BI
    WH -->|"일관된 결과"| NB
    WH -->|"일관된 결과"| APP

이 그림이 이 글의 뼈대입니다. 먼저 이 구조가 필요한지(지표 파편화), 다음으로 어떻게 정의하고 SQL이 생성되는지(MetricFlow), 마지막으로 이 정의를 누가 어떻게 소비하고 지키는지(지표 표준화·거버넌스)를 다룹니다.

세만틱 레이어의 문제의식 — 같은 이름, 다른 SQL

“매출”이 세 개인 회사

지표 파편화는 추상적인 위험이 아니라 어느 조직에나 있는 현실입니다. 같은 fct_orders mart를 보고도, 소비자마다 “매출”을 이렇게 씁니다.

-- 대시보드 A (마케팅): 환불을 빼지 않은 총주문액
SELECT date_trunc('month', ordered_at) AS month,
       SUM(order_total) AS revenue
FROM analytics.fct_orders
GROUP BY 1;

-- 대시보드 B (재무): 환불 제외, 부가세 제외
SELECT date_trunc('month', ordered_at) AS month,
       SUM(order_total - refund_amount - tax_amount) AS revenue
FROM analytics.fct_orders
WHERE status != 'cancelled'
GROUP BY 1;

-- 노트북 C (데이터 분석가): 테스트 주문까지 제외
SELECT date_trunc('month', ordered_at) AS month,
       SUM(order_total - refund_amount) AS revenue
FROM analytics.fct_orders
WHERE status != 'cancelled' AND is_test_order = false
GROUP BY 1;

세 쿼리는 모두 “매출(revenue)”이라는 이름을 달고 있지만 서로 다른 숫자를 냅니다. 어느 것도 틀렸다고 말하기 어렵고 — 각자의 맥락에서는 합리적인 선택입니다 — 바로 그래서 위험합니다. 회의실에서 숫자가 어긋난 뒤에야 발견되고, “어느 쿼리가 진짜인가”를 사람이 고고학처럼 파헤쳐야 합니다. 1~2단계에서 세운 테스트·문서화는 테이블의 품질을 보증하지만, 테이블 위에서 벌어지는 집계의 일관성까지는 보증하지 못합니다.

mart 테이블 폭발 — 조합마다 테이블을 굽는 방식의 한계

파편화를 막는 고전적 처방은 “집계까지 mart로 구워서 내려 주자”였습니다. 그런데 이 방식은 지표 × 차원의 조합 수만큼 테이블을 요구합니다.

revenue_by_day
revenue_by_region
revenue_by_region_month
revenue_by_region_channel_month
active_users_by_day
active_users_by_region_week
...

지표가 M개, 차원이 D개면 잠재 조합은 M × 2^D — 요청이 올 때마다 _by_ 테이블이 하나씩 늘고, 각 테이블은 저마다의 WHERE 절과 미묘하게 다른 집계 로직을 품은 채 관리 밖으로 밀려납니다. 결국 “집계 로직의 중복”이라는 원래 문제가 테이블의 형태로 물질화되었을 뿐입니다. 3단계에서 매크로가 SQL 소스 코드의 중복을 걷어냈다면, 이번에 걷어내야 할 것은 집계 정의의 중복입니다.

해법 — 정의는 한 번, SQL은 조회 시점에

세만틱 레이어는 발상을 뒤집습니다. 집계 결과를 미리 구워 두는 대신, “무엇을 어떻게 집계하는가”라는 정의만 코드로 내려 두고, 실제 SQL은 질의가 도착한 시점에 생성합니다.

  • 정의는 dbt 프로젝트 안의 YAML입니다 — 모델·테스트와 같은 저장소에서 버전 관리되고, PR 리뷰와 CI를 거칩니다.
  • 소비자는 SQL이 아니라 의도를 보냅니다 — “revenue를 region과 month로”. 조인·집계·필터가 올바른 SQL을 쓰는 일은 MetricFlow의 몫입니다.
  • 단일 정의 → 다중 소비 — BI, 노트북, 사내 앱이 같은 정의에 질의하므로, 어디서 보든 “매출”은 하나의 숫자입니다. 정의가 바뀌면 모든 소비자가 동시에 새 정의를 따릅니다.

중요한 것은 세만틱 레이어가 미리 계산된 숫자의 저장소가 아니라 SQL 컴파일러라는 감각입니다. 3단계에서 Jinja가 “SQL을 생성하는 SQL”이었다면, MetricFlow는 한 층 위에서 “지표 정의로부터 SQL을 생성하는 엔진”입니다.

MetricFlow — 정의에서 SQL이 나오는 원리

semantic model — 모델 위에 선언하는 의미 계층

MetricFlow는 dbt 세만틱 레이어의 쿼리 생성 엔진입니다. 그 출발점은 semantic model — 기존 dbt 모델(주로 mart) 위에 “이 테이블이 의미상 무엇인가”를 선언하는 계층입니다. semantic model은 세 가지 요소로 구성됩니다.

  • entity: 이 테이블의 식별자이자 조인 키. primary(이 테이블의 그레인을 정의) / foreign(다른 semantic model로 건너가는 다리)
  • dimension: 지표를 쪼개는 축. time(시간 — 그레인 지원) / categorical(범주)
  • measure: 집계의 원료. 어떤 컬럼을 어떤 함수(sum·count_distinct·avg …)로 집계하는가
# models/marts/orders.yml
semantic_models:
  - name: orders
    description: "주문 1건 = 1행. 매출 계열 지표의 원천."
    model: ref('fct_orders')
    defaults:
      agg_time_dimension: ordered_at

    entities:
      - name: order                    # primary: 이 테이블의 그레인
        type: primary
        expr: order_id
      - name: customer                 # foreign: customers로 건너가는 조인 키
        type: foreign
        expr: customer_id

    dimensions:
      - name: ordered_at
        type: time
        type_params:
          time_granularity: day
      - name: is_food_order
        type: categorical

    measures:
      - name: order_total              # 집계의 원료
        agg: sum
      - name: order_count
        agg: count
        expr: 1
      - name: refund_amount
        agg: sum

고객 차원을 다른 semantic model로 분리해 두면, MetricFlow가 필요할 때 알아서 조인합니다.

# models/marts/customers.yml
semantic_models:
  - name: customers
    model: ref('dim_customers')
    entities:
      - name: customer                 # orders의 foreign entity와 이름이 만난다
        type: primary
        expr: customer_id
    dimensions:
      - name: region
        type: categorical
      - name: signup_channel
        type: categorical

여기서 눈여겨볼 것은 조인을 어디에도 쓰지 않았다는 점입니다. orders의 foreign entity customercustomers의 primary entity customer가 이름으로 만나는 순간, MetricFlow는 두 semantic model 사이의 조인 경로를 entity 그래프로 이해합니다. 1단계에서 ref()가 모델 간 의존성 그래프를 세웠듯, 여기서는 entity가 semantic model 간 조인 그래프를 세웁니다.

metric — 4가지 유형

measure가 집계의 원료라면, metric은 소비자에게 노출되는 지표의 공식 정의입니다. 네 유형이 있습니다.

# models/marts/metrics.yml
metrics:
  # 1) simple — measure 하나를 그대로 지표로
  - name: revenue
    label: "매출"
    description: "환불 차감  총주문액. 재무 매출은 net_revenue를 사용할 것."
    type: simple
    type_params:
      measure: order_total

  - name: order_count
    label: "주문 수"
    type: simple
    type_params:
      measure: order_count

  # 2) ratio — 분자/분모 지표의 비율 (조회 차원이 분자·분모에 일관 적용)
  - name: average_order_value
    label: "평균 주문 금액 (AOV)"
    type: ratio
    type_params:
      numerator: revenue
      denominator: order_count

  # 3) derived — 다른 지표들의 수식 조합
  - name: net_revenue
    label: "순매출"
    type: derived
    type_params:
      expr: revenue - refunds
      metrics:
        - name: revenue
        - name: refunds

  # 4) cumulative — 시간 창을 누적하는 지표
  - name: revenue_7d
    label: "최근 7일 매출"
    type: cumulative
    type_params:
      measure: order_total
      window: 7 days

지표에 필터를 조건으로 못 박을 수도 있습니다. “음식 주문 매출”처럼 정의 자체에 조건이 들어가는 지표는, 소비자가 WHERE 절을 기억하는 대신 정의가 조건을 품게 만듭니다.

metrics:
  - name: food_revenue
    label: "음식 주문 매출"
    type: simple
    type_params:
      measure: order_total
    filter: |
      {{ Dimension('order__is_food_order') }} = true

ratioderived의 진가는 차원이 자동으로 일관 적용된다는 데 있습니다. AOV를 region별로 조회하면 분자(revenue)와 분모(order_count)가 같은 region 분해로 계산됩니다 — 분자와 분모를 서로 다른 쿼리에서 따로 구해 붙이다 생기는 고전적 실수가 구조적으로 사라집니다.

SQL 생성의 원리 — 질의에서 조인·집계가 나오기까지

MetricFlow의 SQL 생성 4단계 — 질의 → 조인 경로 해석 → 그레인·집계 배치 → 웨어하우스 실행 질의에서 SQL이 나오기까지 1 질의 metrics: revenue group_by: metric_time__month customer__region 2 조인 경로 해석 orders customers entity: customer entity 그래프 → LEFT JOIN 결정 3 그레인 · 집계 배치 date_trunc('month', ordered_at) SUM(order_total) GROUP BY 1, 2 4 실행 웨어하우스 SQL 소비자는 의도만 보낸다 measure와 dimension이 사는 모델을 잇는다 시간 차원 truncate + 집계 함수 완성된 SQL 실행
MetricFlow의 SQL 생성 4단계 — 질의가 들어오면 entity 그래프에서 조인 경로를 해석하고, 시간 그레인과 집계를 배치해 완성된 SQL을 웨어하우스에 실행한다

이제 소비자가 이렇게 질의한다고 합시다 — “revenue를 월별 × 고객 region별로”. MetricFlow가 SQL을 만드는 과정은 대략 네 단계입니다.

  1. 지표 해석: revenue → simple metric → measure order_total(SUM) → measure가 사는 semantic model은 orders
  2. 조인 경로 해석: customer__region 차원은 customers에 산다 → entity 그래프에서 orders.customer(foreign)customers.customer(primary) 경로 발견 → LEFT JOIN 결정
  3. 시간 그레인 처리: metric_time__monthorders의 기본 시간 차원 ordered_at을 month로 truncate
  4. 집계 배치: 그룹 기준(month, region)으로 SUM을 배치해 최종 SQL 생성

mf query CLI로 이 흐름 전체를 로컬에서 검증할 수 있습니다.

# 정의 정합성 검사 (CI에도 건다)
mf validate-configs

# 정의된 지표 목록 확인
mf list metrics

# 지표 + 차원 질의 — 조인·집계는 MetricFlow의 몫
mf query \
  --metrics revenue \
  --group-by metric_time__month,customer__region \
  --order metric_time__month

# 생성될 SQL을 눈으로 확인
mf query --metrics revenue --group-by metric_time__month,customer__region --explain

--explain이 보여 주는 생성 SQL은 대략 이런 모양입니다(웨어하우스별로 문법은 다릅니다).

-- MetricFlow가 생성한 SQL (개념적 형태)
SELECT
  date_trunc('month', o.ordered_at) AS metric_time__month,
  c.region                          AS customer__region,
  SUM(o.order_total)                AS revenue
FROM analytics.fct_orders o
LEFT JOIN analytics.dim_customers c
  ON o.customer_id = c.customer_id        -- entity 그래프가 해석한 조인
GROUP BY 1, 2;

소비자는 이 SQL을 한 줄도 쓰지 않았습니다. 조인 키를 잘못 짚을 일도, 환불 차감을 빼먹을 일도 없습니다 — 정의가 옳으면 모든 질의가 옳습니다. 그리고 정의가 틀렸다면, 고칠 곳도 한 곳뿐입니다.

지표 표준화 — 정의를 조직의 자산으로

소비 — BI·노트북·앱이 같은 정의를 쓴다

정의를 세웠으니 이제 소비자를 연결할 차례입니다. dbt 세만틱 레이어는 두 가지 API를 노출합니다.

  • JDBC API (Arrow Flight SQL 기반): BI 도구가 표준 JDBC 드라이버로 접속해, 테이블 대신 지표에 질의합니다. Tableau·Hex·Mode 등이 이 경로로 통합됩니다.
  • GraphQL API: 노트북·사내 앱·커스텀 서비스가 HTTP로 질의합니다. Python SDK도 이 위에 얹혀 있습니다.

JDBC로 들어온 질의는 SQL처럼 생겼지만, FROM 절이 테이블이 아니라 세만틱 레이어 질의 함수라는 점이 다릅니다.

-- BI 도구가 JDBC로 보내는 질의 — 테이블이 아니라 '정의'에 묻는다
select * from {{
  semantic_layer.query(
    metrics=['revenue', 'average_order_value'],
    group_by=[Dimension('metric_time').grain('month'),
              Dimension('customer__region')]
  )
}}

자주 쓰는 질의 조합은 saved query로 이름 붙여 저장하고, 필요하면 export로 웨어하우스에 테이블·뷰로 구체화할 수도 있습니다 — “정의는 세만틱 레이어에, 자주 쓰는 결과는 테이블로”를 한 선언에서 관리하는 장치입니다.

saved_queries:
  - name: weekly_kpi
    description: "주간 경영 리뷰용 표준 KPI 묶음"
    query_params:
      metrics:
        - revenue
        - average_order_value
      group_by:
        - TimeDimension('metric_time', 'week')
        - Dimension('customer__region')
    exports:
      - name: weekly_kpi_table          # dbt build 시 테이블로 구체화
        config:
          export_as: table

거버넌스 — 지표도 PR·리뷰·테스트를 거치는 소프트웨어 자산

세만틱 레이어의 조용한 승리는 기술이 아니라 프로세스에 있습니다. 지표 정의가 dbt 프로젝트의 YAML이 된 순간, 지표는 5단계에서 세운 소프트웨어 규율의 궤도에 그대로 올라탑니다.

  • 변경은 PR로: “매출에서 부가세를 빼자”는 결정이 슬랙 스레드가 아니라 diff로 남습니다. 리뷰어가 승인하고, Git 이력이 “언제, 누가, 왜 바꿨는가”에 답합니다.
  • CI가 정합성을 지킵니다: mf validate-configs를 CI에 걸어 깨진 참조·조인 불능 차원을 병합 전에 잡고, Slim CI의 state 비교는 지표가 딛고 선 모델의 변경까지 함께 검증합니다.
  • 정의가 곧 문서입니다: metric의 label·description이 데이터 카탈로그에 노출되어, “이 지표가 정확히 무엇인가”를 묻는 채널 질문이 정의 문서로 대체됩니다.

2단계에서 테스트·문서가 테이블을 신뢰할 수 있는 자산으로 만들었다면, 세만틱 레이어는 같은 규율로 지표를 신뢰할 수 있는 자산으로 만듭니다.

어디까지 mart로 굽고, 어디부터 세만틱 레이어에 맡길까

책임 경계 — 행을 만드는 일은 mart, 행을 접는 일은 세만틱 레이어 책임의 경계 mart — 행을 만든다 + 행 행 수준 비즈니스 로직 환불 차감 · 상태 정제 · 테스트 주문 제거 무겁고 반복되는 변환 — incremental로 증분 처리 세만틱 레이어 — 행을 접는다 Σ 1행 집계 · 차원 조합 · 지표 공식 SUM · COUNT, group by 축, ratio · derived 시간 그레인 · 누적 창 (cumulative) 원료 테이블 "행을 만드는 일은 mart, 행을 접는 일은 세만틱 레이어"
책임의 경계 — 행 수준 로직과 무거운 변환은 mart가 굽고, 집계·차원 조합·지표 공식은 세만틱 레이어가 조회 시점에 접는다

세만틱 레이어가 생겼다고 mart가 필요 없어지는 것은 아닙니다. 실무의 판단 기준은 “행을 만드는 일은 mart, 행을 접는(집계하는) 일은 세만틱 레이어”입니다.

판단 기준 mart로 굽는다 세만틱 레이어에 맡긴다
로직의 성격 행 수준 비즈니스 로직 — 환불 차감, 상태 정제, 테스트 주문 제거 집계와 차원 조합 — SUM/COUNT, group by의 축
변환 비용 무겁고 반복되는 변환 (incremental로 증분 처리) 조회 시점 계산이 감당되는 가벼운 집계
소비 패턴 조회 빈도가 매우 높고 지연에 민감한 고정 화면 (또는 export로 구체화) 차원 조합이 자주 바뀌는 탐색적 분석·셀프서비스
정의의 성격 한 테이블 안에서 완결되는 파생 컬럼 여러 지표를 엮는 공식 (ratio·derived), 시간 창(cumulative)

바꿔 말해 4단계까지 배운 것들(incremental·snapshot·매크로)은 좋은 원료 테이블을 만드는 기술이고, 세만틱 레이어는 그 원료 위에서 집계의 일관성을 보증하는 계층입니다. fct_ordersnet_order_total 같은 행 수준 파생 컬럼을 굽는 것은 여전히 mart의 일이지만, 그것을 월별·지역별로 어떻게 접을지는 이제 _by_ 테이블이 아니라 정의가 담당합니다. 조회 성능이 발목을 잡는 조합만 export(또는 소비층 캐시)로 구체화하면, “유연성은 정의로, 성능은 선택적 구체화로”라는 균형이 잡힙니다.

정리

  • 파편화는 구조의 문제다: “매출”이 대시보드마다 다른 것은 사람의 실수가 아니라, 지표 정의가 소비자마다 흩어져 있는 구조의 필연입니다. 조합마다 mart를 굽는 처방은 중복을 테이블의 형태로 물질화할 뿐입니다.
  • 정의는 한 번, SQL은 조회 시점에: 세만틱 레이어는 미리 계산된 숫자의 저장소가 아니라 SQL 컴파일러입니다. semantic model(entity·dimension·measure)과 metric(simple·ratio·derived·cumulative)을 YAML로 선언하면, MetricFlow가 entity 그래프에서 조인 경로를 해석해 질의마다 올바른 SQL을 생성합니다.
  • 정의가 옳으면 모든 질의가 옳다: ratio·derived 지표는 차원이 분자·분모에 일관 적용되고, 필터는 정의가 품습니다. mf validate-configsmf query --explain으로 정의와 생성 SQL을 검증합니다.
  • 지표도 소프트웨어 자산이다: JDBC·GraphQL API로 BI·노트북·앱이 같은 정의를 소비하고, 지표 변경은 PR·리뷰·CI를 거쳐 diff와 이력으로 남습니다. 경계 원칙은 하나 — 행을 만드는 일은 mart, 행을 접는 일은 세만틱 레이어.

그리고 이것으로 dbt-Essential 시리즈 6단계 완주입니다. 우리는 모델링 기초에서 출발했습니다 — SELECT 하나가 모델이 되고 ref()가 의존성 그래프를 세우는 원리를 그리고(1단계), 테스트·문서화·lineage로 그 그래프에 “이 숫자를 믿어도 되는가”라고 물을 수 있는 신뢰를 얹었습니다(2단계). 그 위에 재사용과 규모를 쌓았습니다 — 매크로·Jinja로 수십 모델에 흩어진 반복을 한 곳으로 모으고(3단계), incremental·snapshot으로 대용량의 비용과 변화의 이력을 감당했습니다(4단계). 마지막으로 협업과 표준화로 넓혔습니다 — 패키지·Slim CI로 팀 규모의 안전한 배포를 세우고(5단계), 오늘 세만틱 레이어·MetricFlow로 조직의 지표까지 코드의 규율 아래 두었습니다(6단계). “웨어하우스 안의 SQL을 소프트웨어처럼 다룬다”는 dbt의 단순한 발상이, 모델에서 테스트로, 배포에서 지표로 확장되는 전 과정을 완주한 것입니다. 이제 여러분은 dbt 프로젝트의 그래프를 읽고, 신뢰할 수 있는 변환 파이프라인을 설계·운영하며, 팀의 배포와 조직의 지표 표준까지 책임질 수 있는 애널리틱스 엔지니어링의 손을 갖췄습니다. 완주를 축하합니다 — 커리큘럼의 체크박스를 모두 채우러 갑시다.

다음 학습 (Next Learning)