Back to Blog
dbtProject StructureSnapshotsSCDStagingMarts

0x05. dbt 프로젝트 구조 & Snapshots - 3계층 컨벤션과 변경 이력 추적

Staging → Intermediate → Marts 3계층 구조와 SCD Type 2를 구현하는 Snapshot 기능을 정리한다.

3계층 프로젝트 구조

dbt Project Layers

dbt 프로젝트가 커지면 모델이 수십, 수백 개로 늘어난다. 규칙 없이 쌓으면 의존성이 스파게티처럼 얽힌다. dbt Labs가 권장하는 Staging → Intermediate → Marts 3계층 구조는 이 문제를 해결한다.

디렉토리 구조

models/
├── staging/
   ├── jaffle_shop/
      ├── _stg_jaffle_shop__models.yml
      ├── _stg_jaffle_shop__sources.yml
      ├── stg_jaffle_shop__orders.sql
      └── stg_jaffle_shop__customers.sql
   └── stripe/
       ├── _stg_stripe__models.yml
       ├── _stg_stripe__sources.yml
       └── stg_stripe__payments.sql
├── intermediate/
   └── finance/
       ├── _int_finance__models.yml
       └── int_orders__pivoted.sql
└── marts/
    ├── finance/
       ├── _finance__models.yml
       ├── fct_orders.sql
       └── dim_customers.sql
    └── marketing/
        ├── _marketing__models.yml
        └── fct_campaigns.sql

Staging — 소스 정리

소스 시스템의 원본 데이터를 표준화하는 첫 단계이다.

-- models/staging/jaffle_shop/stg_jaffle_shop__orders.sql
{{ config(materialized='view') }}

SELECT
    id AS order_id,
    user_id AS customer_id,
    order_date,
    CAST(status AS VARCHAR(20)) AS order_status,
    amount
FROM {{ source('jaffle_shop', 'orders') }}
WHERE _deleted IS FALSE

규칙:

  • 1 소스 테이블 = 1 Staging 모델 (1:1 매핑)
  • 접두사 stg_, 네이밍: stg_{소스}_{테이블}
  • 컬럼 이름 표준화, 타입 캐스팅, 소프트 삭제 필터링 같은 가벼운 정리만
  • 비즈니스 로직 X, JOIN X, 집계 X
  • Materialization: View (항상 최신, 저장 비용 0)

Staging은 소스와 나머지 프로젝트 사이의 방화벽 역할이다. 소스 스키마가 변경되면 Staging만 수정하면 된다.

Intermediate — 비즈니스 로직

여러 Staging 모델을 JOIN하고, 비즈니스 로직을 적용하는 중간 단계이다.

-- models/intermediate/finance/int_orders__pivoted.sql
{{ config(materialized='ephemeral') }}

SELECT
    order_id,
    SUM(CASE WHEN payment_method = 'credit_card' THEN amount END) AS credit_card_amount,
    SUM(CASE WHEN payment_method = 'bank_transfer' THEN amount END) AS bank_transfer_amount,
    SUM(amount) AS total_amount
FROM {{ ref('stg_stripe__payments') }}
GROUP BY order_id

규칙:

  • 접두사 int_
  • 비즈니스 영역별 하위 디렉토리 (finance/, marketing/)
  • 최종 사용자가 직접 쿼리하지 않음
  • Materialization: Ephemeral 또는 View

Marts — 최종 분석 테이블

비즈니스 사용자, BI 도구, ML 파이프라인이 직접 쿼리하는 최종 테이블이다.

-- models/marts/finance/fct_orders.sql
{{ config(materialized='table') }}

SELECT
    o.order_id,
    o.customer_id,
    o.order_date,
    o.order_status,
    p.total_amount,
    p.credit_card_amount,
    c.customer_name,
    c.first_order_date
FROM {{ ref('stg_jaffle_shop__orders') }} o
LEFT JOIN {{ ref('int_orders__pivoted') }} p ON o.order_id = p.order_id
LEFT JOIN {{ ref('dim_customers') }} c ON o.customer_id = c.customer_id

규칙:

  • fct_ (팩트 — 이벤트, 트랜잭션), dim_ (디멘션 — 엔티티, 속성) 접두사
  • 비즈니스 영역별 하위 디렉토리
  • Materialization: Table 또는 Incremental

Medallion Architecture와의 대응

dbt LayerMedallionMaterialization역할
StagingBronzeView원본 정리
IntermediateSilverEphemeral / View비즈니스 로직
MartsGoldTable / Incremental분석용 최종 테이블

완전히 1:1은 아니지만 개념적으로 유사하다. Medallion은 데이터 레이크 전체의 계층이고, dbt 레이어는 변환 단계의 계층이다.

dbt_project.yml 설정

models:
  my_project:
    staging:
      +materialized: view
      +schema: staging
    intermediate:
      +materialized: ephemeral
    marts:
      +materialized: table
      +schema: analytics
      finance:
        +tags: ['finance']
      marketing:
        +tags: ['marketing']

폴더 단위로 기본 Materialization과 스키마를 설정한다. 개별 모델에서 config()로 오버라이드할 수 있다.


Snapshots — 변경 이력 추적

소스 시스템의 테이블은 UPDATE로 기존 값을 덮어쓴다. 주문 상태가 placedshippedcompleted로 변경되면 소스에는 최종 상태만 남는다. Snapshot은 SCD Type 2(Slowly Changing Dimension Type 2)를 구현하여 변경 전 값을 보존한다.

설정

# snapshots/orders_snapshot.yml
snapshots:
  - name: orders_snapshot
    relation: source('jaffle_shop', 'orders')
    config:
      unique_key: order_id
      strategy: timestamp
      updated_at: updated_at
      schema: snapshots

전략

전략변경 감지 방법조건
timestampupdated_at 컬럼 비교소스에 타임스탬프 컬럼 필요
check지정 컬럼의 값 비교타임스탬프 없을 때 사용
# check 전략  status, amount 컬럼이 바뀌면 캡처
config:
  strategy: check
  check_cols: ['status', 'amount']

결과

order_id | status    | dbt_valid_from      | dbt_valid_to
---------|-----------|--------------------|-----------------
1001     | placed    | 2026-01-01 10:00   | 2026-01-03 14:00
1001     | shipped   | 2026-01-03 14:00   | 2026-01-05 09:00
1001     | completed | 2026-01-05 09:00   | NULL
  • dbt_valid_from: 이 행이 유효해진 시점
  • dbt_valid_to: 이 행이 만료된 시점. NULL이면 현재 상태

현재 상태 조회

SELECT * FROM {{ ref('orders_snapshot') }}
WHERE dbt_valid_to IS NULL

특정 시점 조회 (Time Travel)

-- 2026-01-04 시점의 주문 상태
SELECT * FROM {{ ref('orders_snapshot') }}
WHERE dbt_valid_from <= '2026-01-04'
  AND (dbt_valid_to > '2026-01-04' OR dbt_valid_to IS NULL)

주의사항

  • 스냅샷 실행 간격 사이의 변경은 캡처되지 않는다. 하루 1회 실행하면 하루 안에 여러 번 바뀌어도 최종 상태만 캡처된다
  • 소스에 updated_at 같은 변경 타임스탬프가 없으면 check 전략을 사용하되, 비교 대상 컬럼을 최소화한다 (성능)
  • 스냅샷 테이블은 절대 --full-refresh 하지 않는다. 전체 이력이 날아간다

정리

dbt 프로젝트의 핵심은 관심사의 분리이다:

  • Staging: 소스와 프로젝트 사이의 방화벽. 1:1 매핑, 가벼운 정리만
  • Intermediate: 비즈니스 로직 적용. 최종 사용자에게 노출하지 않음
  • Marts: 비즈니스 사용자가 직접 쿼리하는 최종 테이블
  • Snapshots: 소스의 UPDATE를 이력으로 보존 (SCD Type 2)

이 구조를 따르면 모델이 수백 개로 늘어나도 어디서 무엇을 수정해야 하는지 명확하다.