[dbt 101] Day 3: SQL의 한계를 넘어서, Jinja 매크로와 Test
SQL은 강력한 데이터 조작 언어이지만, 프로그래밍 언어로서의 유연성은 부족하다. 반복문을 돌릴 수도 없고, 복잡한 로직을 함수로 묶어 재사용하기도 어렵다. 때문에 데이터 엔지니어들은 비슷한 CASE WHEN 구문을 수십 번 복사/붙여넣기 하곤 했다.
dbt는 Jinja2 템플릿 엔진을 도입하여 이 문제를 해결한다. SQL에 변수, 제어문, 함수(Macro)를 섞어 쓸 수 있게 된 것이다.
또한, “데이터가 정확한가?”라는 영원한 난제를 해결하기 위해 소프트웨어 개발의 테스트 주도 개발(TDD) 개념을 데이터 파이프라인에 도입했다. 오늘은 dbt의 파워 유저가 되기 위한 두 가지 무기, Macro와 Test를 알아본다.
1. SQL에 프로그래밍을 입히다: Jinja
dbt 프로젝트 내의 모든 .sql 파일은 사실 단순한 SQL 파일이 아니라 Jinja 템플릿이다. dbt는 실행 시점에 Jinja 코드를 해석(Compile)하여 순수한 SQL로 변환한 뒤 웨어하우스로 보낸다.
반복문(Loop) 활용하기
예를 들어, 결제 수단별로 매출 합계를 구하는 피벗(Pivot) 테이블을 만든다고 가정해보자. 결제 수단이 늘어날 때마다 CASE WHEN을 추가하는 것은 비효율적이다.
1
2
3
4
5
6
7
8
9
10
11
12
{% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}
select
order_id,
{% for payment_method in payment_methods %}
sum(case when payment_method = '{{ payment_method }}' then amount else 0 end) as {{ payment_method }}_amount,
{% endfor %}
sum(amount) as total_amount
from {{ ref('raw_payments') }}
group by 1
위 코드는 dbt 컴파일러에 의해 아래와 같이 변환된다.
1
2
3
4
5
6
7
8
9
select
order_id,
sum(case when payment_method = 'bank_transfer' then amount else 0 end) as bank_transfer_amount,
sum(case when payment_method = 'credit_card' then amount else 0 end) as credit_card_amount,
sum(case when payment_method = 'gift_card' then amount else 0 end) as gift_card_amount,
sum(amount) as total_amount
from analytics.raw_payments
group by 1
리스트만 관리하면 SQL 쿼리는 자동으로 확장된다.
2. DRY 원칙과 매크로(Macro)
DRY(Don’t Repeat Yourself)는 개발의 기본 원칙이다. SQL에서 자주 쓰이는 로직(예: 원(₩)을 달러($)로 환전, 날짜 포맷 변경 등)을 매크로(Macro)로 정의하면 함수처럼 불러다 쓸 수 있다.
매크로 정의 (macros/cents_to_dollars.sql)
1
2
3
4
5
6
7
select
id as payment_id,
{{ cents_to_dollars('amount') }} as amount_usd, -- 함수처럼 사용
...
from {{ ref('raw_payments') }}
매크로 사용 (models/stg_payments.sql)
1
2
3
4
5
6
select
id as payment_id,
as amount_usd, -- 함수처럼 사용
...
from
이제 환율 계산 로직이 바뀌거나 소수점 처리가 변경되어도, 매크로 파일 하나만 수정하면 프로젝트 전체에 반영된다.
3. 데이터 품질의 방패: Tests
데이터 파이프라인이 실패하는 것보다 더 무서운 것은 “실패하지 않고 쓰레기 데이터를 적재하는 것”이다. dbt는 이를 막기 위해 컴파일/실행 단계에서 데이터 검증(Validation)을 수행한다.
dbt의 테스트는 크게 두 가지로 나뉜다.
Generic Tests (설정 기반)
가장 보편적인 4가지 테스트는 schema.yml 파일에 작성하는 것만으로 적용된다.
unique: 중복 값이 없어야 한다. (Primary Key 검증)not_null: Null 값이 없어야 한다.accepted_values: 특정 값 목록에 포함되어야 한다. (예: status는 ‘placed’, ‘shipped’ 중 하나)relationships: 다른 테이블의 키와 연결되어야 한다. (Foreign Key 검증, 참조 무결성)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# models/schema.yml
version: 2
models:
- name: stg_customers
columns:
- name: customer_id
tests:
- unique
- not_null
- name: status
tests:
- accepted_values:
values: ['active', 'deleted']
dbt test 명령어를 실행하면, dbt는 자동으로 검증 쿼리를 생성해 실행한다. 만약 unique 테스트에서 1건이라도 중복이 발견되면 테스트는 실패(Fail)하고 경고를 보낸다.
Singular Tests (쿼리 기반)
복잡한 비즈니스 로직 검증이 필요할 때 사용한다. tests/ 폴더에 .sql 파일을 만들고 “실패하는 데이터(반환되면 안 되는 데이터)”를 조회하는 쿼리를 작성한다.
1
2
3
4
5
6
7
8
9
-- tests/assert_total_payment_is_positive.sql
-- 결제 금액 합계가 음수인 경우는 데이터 오류다.
select
order_id,
sum(amount) as total_amount
from
group by 1
having total_amount < 0
이 쿼리의 결과가 0건이어야 테스트가 통과(Pass)된다. 결과가 반환되면 데이터에 문제가 있다는 뜻이다.
마치며
Jinja를 이용한 동적 SQL 생성(Macro)과 견고한 데이터 검증(Test)은 dbt를 단순한 SQL 실행기에서 “데이터 엔지니어링 프레임워크”로 격상시킨 핵심 기능이다.
이제 우리는 모델을 만들고, 의존성을 연결하고, 품질까지 검증했다. 하지만 이 모든 과정은 아직 로컬 터미널에서 개발자가 수동으로 실행하고 있다.