학습 목표
ETL과 ELT에서 변환이 일어나는 위치를 설명할 수 있다.
warehouse layering (staging · intermediate · marts)으로 역할을 나눌 수 있다.
dbt가 warehouse 안 SQL DAG·테스트를 담당하는 방식을 설명할 수 있다.
orchestration (Airflow·Dagster)과 transform 엔진의 역할 분리를 말할 수 있다.
5·6편 Spark 출력을 warehouse 적재와 연결해 설계할 수 있다.
문제 상황
- Spark에서 join·집계 후 JDBC bulk load — warehouse는 거의 idle, 재처리는 Spark만 다시
- 변환된 테이블만 적재 — 원본이 warehouse에 없어 컬럼 추가·replay 때 소스부터 다시
- Airflow DAG 안에 SQL 200줄 — 의존성·테스트·문서가 스크립트에 묻힘
- dbt와 Airflow 둘 다 쓴다는데 — 누가 schedule·누가 transform인지 겹침
- 5편 gold parquet와 BI Snowflake 테이블 숫자 불일치 — 어느 쪽이 정본인지 없음
- Kafka Connect sink는 있는데 staging·mart 계층 정의가 없음
이번 편은 process 이후 — 데이터를 warehouse에 어떻게 넣고 SQL로 다듬는지다. lakehouse 파일 포맷은 8편.
1. Warehouse loading이 파이프라인에서 하는 일
3편 배치·6편 스트림 모두 process까지는 Spark·Kafka. BI·ML·운영 리포트는 대개 warehouse (또는 mart 테이블)를 읽는다.

| 역할 | loading이 해주는 것 |
|---|---|
| 정본 집약 | 여러 소스를 한 곳에서 join·집계 |
| 소비자 분리 | BI·ML·ops가 동일 mart 참조 — Spark job 직접 의존 감소 |
| 거버넌스 | PII·권한·계보 — warehouse role·masking |
| 재현 | raw 보존 시 정의 변경 후 재실행 |
Kafka / lake raw ──► Spark batch · streaming ──► load path ──► warehouse ──► BI · features
- 4편 Connect warehouse sink — ingest 경로 중 하나
- 5편
gold/user_day_revenue— 파일 mart; warehouse 동기 또는 대체 선택 - 6편 스트림 metric — warehouse 근실시간 테이블 vs 배치 gold 역할 분리
- ETL — transform이 warehouse 밖; ELT — raw load 후 transform이 warehouse 안
2. ETL 흐름
Extract → Transform → Load. 변환이 적재 전 — Spark·Python·전용 ETL 툴.

| 단계 | 설명 | 예 |
|---|---|---|
| Extract | 소스에서 읽기 | JDBC, S3 export, API |
| Transform | 정제·join·PII 마스킹 | PySpark, pandas (소량) |
| Load | 정제된 행만 warehouse | COPY, JDBC append |
| Consume | curated dims/facts | Looker, notebook |
orders (RDS) ──► Spark: dedupe · join users ──► fact_orders (Snowflake)
- 장점 — warehouse CPU·스토리지에 raw 안 실을 때 비용 절감; 민감 데이터 적재 전 제거
- 단점 — 원본 미보존 시 스키마 변경·버그 수정 시 소스 재추출; transform 로직이 Spark에만 있으면 SQL 팀 배제
- 흔한 실무 — 대용량 join·ML feature는 Spark ETL → parquet gold + 요약만 warehouse
- 5편 배치 job이 사실상 ETL의 T — 출력이 lake면 L은 파일, warehouse면 JDBC/connector
# code/etl_load.example.py (발췌) — Spark에서 정제 후 warehouse 적재
curated = (
orders.join(users, "user_id")
.filter(col("status") == "PAID")
.select("order_id", "user_id", "amount", "paid_at")
)
curated.write.format("jdbc").option("dbtable", "analytics.fact_orders").mode("append").save()
3. ELT 흐름
Extract → Load → Transform. raw를 먼저 warehouse(staging)에 넣고 SQL·dbt로 mart 생성.

| 단계 | 설명 | 예 |
|---|---|---|
| Extract | CDC·export | Debezium, Fivetran, Spark copy |
| Load | 가공 최소 staging | raw.orders JSON 그대로 |
| Transform | warehouse SQL | dbt stg_ → int_ → mart_ |
| Test | 품질 게이트 | unique, not_null, 관계 테스트 |
CDC ──► staging.raw_orders ──► dbt: stg_orders → mart_revenue_daily
- 장점 — raw 보존으로 replay·계보 용이; SQL만으로 비분석가도 리뷰; cloud warehouse 탄력 compute
- 단점 — raw 스토리지·반복 스캔 비용; PII를 staging에 두면 권한·마스킹 필수
- 현대 스택 — Snowflake·BigQuery·Redshift + dbt가 ELT의 대표 조합
- Spark 역할 — ELT에서도 heavy 전처리·비정형 파싱은 lake·staging 적재 전에 유지
-- code/dbt_mart_revenue.example.sql (발췌)
with paid as (
select * from {{ ref('stg_orders') }} where status = 'PAID'
)
select
date_trunc('day', paid_at) as revenue_date,
sum(amount) as revenue
from paid
group by 1
4. ETL vs ELT 선택
| ETL | ELT | |
|---|---|---|
| Transform 위치 | warehouse 밖 (Spark 등) | warehouse 안 (SQL/dbt) |
| Raw 보관 | 보통 별도 lake | staging에 적재 |
| 적합 | PII 사전 제거, Spark 이미 표준 | cloud DW, SQL 중심 팀 |
| 재처리 | Spark job 재실행 | dbt full-refresh·incremental |
| 비용 | cluster 고정 | warehouse 쿼리 종량 |
| 신호 | 쪽 |
|---|---|
| 소스 PII를 warehouse에 절대 두면 안 됨 | ETL (또는 ingest 시 마스킹) |
| 수십 TB raw를 DW에 매일 full load | lake + 선택적 ELT (8편) |
| BI가 SQL·dbt로 자급자족 | ELT |
| 복잡 ML feature·비정형 | Spark ETL → feature store / mart |
- 혼합이 일반적 — lake bronze (raw) + Spark silver + warehouse gold marts
- 정본 — metric 하나당 한 테이블; parquet gold vs Snowflake 이중이면 문서화 필수
- 스트림 — 6편 update sink로 staging 근실시간 + 일 배치 dbt merge
5. Warehouse layering
한 덩어리 테이블이 아니라 층으로 나눠 의존성·재사용을 만든다.

| 층 | 역할 | 명명 예 |
|---|---|---|
| Staging | 소스 mirror, 타입 cast 최소 | stg_orders, raw_events |
| Intermediate | join·비즈니스 규칙 공유 | int_orders_enriched |
| Marts | BI·ML 소비용 dims/facts | mart_revenue_daily, dim_users |
staging ──► intermediate ──► marts
▲ ▲ ▲
source shared logic one KPI definition
- staging — 소스와 1:1에 가깝게 — 디버깅·replay
- intermediate — 여러 mart가 쓰는 로직 한 번만 정의
- marts — dashboard·feature가 직접
SELECT— wide·집계 OK - 8편 lakehouse bronze/silver/gold — 개념 대응; 파일 vs 테이블 물리만 다름
- 9편 — schema evolution·품질 테스트가 층마다 다름
6. dbt 개념
dbt (data build tool) — warehouse 안 SELECT를 모델로 관리·의존 그래프·테스트·문서.
| 개념 | 설명 |
|---|---|
| Model | models/ 아래 SQL 파일 — view 또는 table materialize |
| ref() | 모델 간 의존성 — 컴파일 시 DAG |
| source | staging이 읽는 외부 raw 테이블 선언 |
| test | unique·accepted_values·relationships |
| incremental | 대용량 append — merge 키로 증분 |
# code/dbt_project.example.yaml (발췌)
models:
analytics:
staging:
+materialized: view
marts:
+materialized: table
- Jinja —
{{ ref('stg_orders') }}— 환경별 database/schema 치환 - dbt ≠ orchestrator — 실행은
dbt run한 번; 언제 돌릴지는 Airflow 등 - Spark 출력 —
source('lake', 'orders')또는 external table로 staging 연결 - lineage — dbt docs + 9편 계보 도구와 연동
7. Orchestration
언제·어떤 순서로 job을 돌릴지 — 스케줄·의존성·재시도·알림.

| 도구 | 역할 |
|---|---|
| Airflow | DAG task — ingest → dbt → export |
| Dagster | asset 단위 데이터 의존성 |
| dbt Cloud / CI | PR마다 test — 배포 게이트 |
06:00 ingest_raw_orders
06:20 dbt run --select staging+
06:45 dbt test
07:00 refresh BI extract (optional)
- idempotency — 같은 logical date 재실행해도 중복 없게 — 10편 신뢰성
- sensor — upstream 파일·partition 도착 후 dbt trigger
- 실패 — staging까지는 성공·mart 실패 → 부분 재실행 (
dbt run --select mart_revenue+) - 5편 Spark batch — orchestrator task 하나; ELT면 load task 뒤 dbt
# code/pipeline.dag.example.yaml (발췌)
schedule: "0 6 * * *"
tasks:
- id: extract_orders
operator: spark_submit
conf: s3://jobs/raw_orders_daily.json
- id: dbt_staging
operator: dbt
depends_on: [extract_orders]
command: run --select staging+
- id: dbt_marts
operator: dbt
depends_on: [dbt_staging]
command: run --select marts+
- id: dbt_test
operator: dbt
depends_on: [dbt_marts]
command: test
8. 적재 설계 체크리스트
| # | 질문 |
|---|---|
| 1 | ETL vs ELT — transform 위치가 팀·비용·PII와 맞는가 |
| 2 | raw가 어디에 얼마나 보존되는가 |
| 3 | staging / intermediate / marts 명명·소유가 정해졌는가 |
| 4 | KPI 정본 테이블이 하나인가 — lake gold vs warehouse |
| 5 | dbt test·orchestrator retry가 SLA를 커버하는가 |
| 6 | Spark·스트림·dbt 경계 — 누가 어떤 변환을 소유하는가 |
- Incremental — 일 partition·merge 키 — full scan 회피
- Backfill — 날짜 범위 dbt vars 또는 Spark dt 루프 — 10편
- 권한 — staging PII role 분리; mart는 masked view
- 8편 — Delta/Iceberg로 lake와 warehouse 경계 흐림
정리
- Warehouse loading — process 이후 BI·ML이 쓰는 정본 층
- ETL — Spark 밖에서 정제 후 load; ELT — raw load 후 SQL/dbt
- Layering — staging → intermediate → marts — 재사용·디버깅
- dbt — warehouse SQL DAG·test·docs — orchestrator와 분리
- Orchestration — 스케줄·의존성; 5·6편 Spark job과 같은 DAG 안에서 연결
다음에 다룰 것
- Lakehouse 개요
- data lake vs warehouse, Delta·Iceberg, object storage ACID
해당 내용은 dbt documentation (What is dbt), Kleppmann — Designing Data-Intensive Applications (Ch 10) 를 기반으로 합니다.
'Data & AI' 카테고리의 다른 글
| Lakehouse 개요 (0) | 2026.07.18 |
|---|---|
| Spark Structured Streaming (0) | 2026.07.13 |
| Spark 배치 처리 (0) | 2026.07.09 |
| Kafka 기초 (0) | 2026.07.05 |
| Batch vs Stream (0) | 2026.07.03 |