[dbt 101] Day 2: 모든 것은 연결된다, ref()와 Lineage
데이터 엔지니어링의 핵심은 순서(Order)와 의존성(Dependency)이다. ‘주문 테이블’이 만들어져야 ‘일별 매출 집계 테이블’을 만들 수 있다. 기존에는 이를 관리하기 위해 Airflow DAG에서 Task 순서를 수동으로 연결하거나, 파일 이름에 01_create_table, 02_update_table 같은 접두어를 붙이는 원시적인 방법을 사용했다.
dbt는 ref() 함수 하나로 이 문제를 완벽하게 해결한다.
하드코딩의 종말
일반적인 SQL 작성 방식을 살펴보자.
1
2
3
4
5
-- bad_practice_model.sql
SELECT
*
FROM analytics.stg_customers -- 테이블 이름을 직접 명시(Hardcoding)
이 방식에는 두 가지 치명적인 문제가 있다.
- 환경 분리의 어려움: 개발(Dev) 환경과 운영(Prod) 환경의 스키마가 다를 경우, 코드를 매번 수정해야 한다.
- 의존성 파악 불가: 실행 도구는 이 SQL이
stg_customers가 만들어진 후에 실행되어야 한다는 사실을 모른다.
The Magic Function: ref()
dbt에서는 테이블 이름을 직접 쓰는 대신 ref() 함수를 사용해야 한다.
1
2
3
4
5
-- good_practice_model.sql
SELECT
*
FROM
Jinja 템플릿 문법인 `` 안에 감싸진 ref 함수는 dbt 컴파일러에게 두 가지 중요한 정보를 전달한다.
- “이 쿼리는
stg_customers모델을 참조하고 있어.” (의존성 정의) - “현재 실행 환경(target)에 맞춰 적절한 스키마.테이블 이름으로 치환해 줘.” (환경 추상화)
Lineage Graph (DAG)의 자동 생성
모든 모델 파일에서 ref()를 사용하면, dbt는 프로젝트 전체를 스캔하여 모델 간의 관계를 파악한다. 개발자가 명시적으로 “A 다음에 B를 실행해”라고 설정할 필요가 없다. dbt가 ref() 관계를 분석하여 자동으로 DAG(Directed Acyclic Graph)를 그리기 때문이다.
이것이 바로 Lineage(데이터 혈통)다.
(Lineage 그래프 예시)
터미널에서 dbt run을 입력하면, dbt는 이 Lineage 그래프의 가장 밑바닥(상위 의존성이 없는 모델)부터 순서대로 병렬 실행한다.
dbt 모델링의 정석: Layering Strategy
ref() 기능을 제대로 활용하기 위해서는 dbt 프로젝트의 구조를 계층화(Layering)하는 것이 중요하다. 업계 표준으로 통용되는 구조는 다음과 같다.
1. Staging Layer (models/staging)
- 역할: Source 데이터(Raw Data)와 1:1로 매핑되는 뷰(View).
- 작업: 컬럼 이름 변경(Renaming), 데이터 타입 캐스팅(Casting). 조인(Join)은 하지 않는다.
- 파일명 규칙:
stg_<source>_<table_name>.sql
2. Intermediate Layer (models/intermediate)
- 역할: 복잡한 로직을 처리하는 중간 단계.
- 작업: 테이블 간의 조인, 복잡한 계산. 최종 사용자에게는 노출되지 않는 경우가 많다.
- 파일명 규칙:
int_<entity>_<verb>.sql
3. Marts Layer (models/marts)
- 역할: 최종 비즈니스 로직이 담긴 테이블. BI 툴이나 분석가가 직접 조회하는 대상이다.
- 작업: 비즈니스 메트릭 계산, 차원 모델링(Star Schema) 구성.
- 파일명 규칙:
fct_orders.sql,dim_customers.sql(Fact/Dimension)
코드 예제: Staging에서 Mart까지
실제 코드가 어떻게 연결되는지 살펴보자.
1. models/staging/stg_customers.sql
1
2
3
4
5
6
select
id as customer_id,
first_name,
last_name
from raw.jaffle_shop.customers
2. models/staging/stg_orders.sql
1
2
3
4
5
6
7
select
id as order_id,
user_id as customer_id,
order_date,
status
from raw.jaffle_shop.orders
3. models/marts/customers.sql (Final Mart)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
with customers as (
select * from
),
orders as (
select * from
),
customer_orders as (
select
customer_id,
min(order_date) as first_order_date,
max(order_date) as most_recent_order_date,
count(order_id) as number_of_orders
from orders
group by 1
),
final as (
select
customers.customer_id,
customers.first_name,
customers.last_name,
customer_orders.first_order_date,
customer_orders.most_recent_order_date,
coalesce(customer_orders.number_of_orders, 0) as number_of_orders
from customers
left join customer_orders using (customer_id)
)
select * from final
위의 customers.sql을 실행하기 위해 개발자는 stg_customers와 stg_orders를 먼저 실행하라고 명령할 필요가 없다. dbt run을 실행하면 dbt는 Lineage를 해석하여 Staging 모델들을 먼저 빌드한 뒤, Mart 모델을 빌드한다.
마치며
ref() 함수는 dbt의 알파이자 오메가다. 이를 통해 우리는 데이터 파이프라인의 모듈화와 자동화를 동시에 달성할 수 있다.