3계층 프로젝트 구조
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 Layer | Medallion | Materialization | 역할 |
|---|---|---|---|
| Staging | Bronze | View | 원본 정리 |
| Intermediate | Silver | Ephemeral / View | 비즈니스 로직 |
| Marts | Gold | Table / 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로 기존 값을 덮어쓴다. 주문 상태가 placed → shipped → completed로 변경되면 소스에는 최종 상태만 남는다. 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
전략
| 전략 | 변경 감지 방법 | 조건 |
|---|---|---|
timestamp | updated_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)
이 구조를 따르면 모델이 수백 개로 늘어나도 어디서 무엇을 수정해야 하는지 명확하다.