품질 테스트: source freshness, accepted values, relationship, custom test
테스트 없는 데이터 모델은 신뢰할 수 없다
dbt 모델이 아무리 잘 설계되어 있어도, "이 데이터가 정말 맞는가?"를 확인하지 않으면 분석가는 숫자를 믿지 못한다. 대시보드를 볼 때마다 "혹시 파이프라인이 멈춘 건 아닐까?", "이 고객 ID가 실제로 존재하는가?"를 의심해야 한다면 그 데이터 플랫폼은 신뢰를 잃은 것이다.
dbt는 SQL 변환과 테스트를 한 곳에서 관리할 수 있게 설계됐다. 테스트를 모델과 같은 프로젝트에 두면 변환 코드가 바뀔 때 테스트도 함께 리뷰된다. 이것이 dbt 테스트의 핵심 가치다.
1. dbt 테스트의 종류
dbt 테스트는 크게 두 범주로 나뉜다.
재사용성: 여러 모델·컬럼에 반복 적용 가능
위치: tests/generic/ 또는 macros/
패키지: dbt-utils, dbt-expectations
재사용성: 특정 모델에만 적용
위치: tests/
복잡한 비즈니스 규칙 검증에 적합
2. 내장 Generic Test 4종
2.1 unique
컬럼 값의 중복 여부를 검사한다. 기본 키나 자연 키에 반드시 설정한다.
# models/marts/finance/_schema.yml
models:
- name: mart_orders
columns:
- name: order_id
data_tests:
- unique
- not_nullunique와 not_null은 항상 함께 쓴다. 둘 중 하나라도 빠지면 기본 키 보장이 무너진다.
2.2 not_null
컬럼에 NULL이 없어야 함을 보장한다. FK 컬럼, 파티션 키, 집계 기준 컬럼에 설정한다.
- name: customer_id
data_tests:
- not_null2.3 accepted_values
컬럼 값이 허용된 목록 안에만 있는지 검사한다. 상태 코드, 등급, 채널 같은 열거형 컬럼에 쓴다.
- name: order_status
data_tests:
- accepted_values:
values: ['pending', 'paid', 'shipped', 'completed', 'cancelled']quote: false 옵션을 쓰면 값을 따옴표 없이 비교한다. 숫자 코드 컬럼이나 Boolean에 유용하다.
- name: is_active
data_tests:
- accepted_values:
values: [true, false]
quote: false2.4 relationships (참조 무결성)
두 테이블 사이의 외래 키 관계를 검사한다. mart_orders.customer_id가 dim_customers에 실제로 존재하는지 확인하는 것이 전형적인 사용 사례다.
- name: customer_id
data_tests:
- relationships:
to: ref('dim_customers')
field: customer_id주의: 소스 테이블에서도 relationships 테스트를 걸 수 있지만, 소스 시스템 간 FK가 깨진 데이터가 흔하다. 처음에는 where 절로 NULL이나 알려진 예외를 필터링하는 것이 현실적이다.
- name: product_id
data_tests:
- relationships:
to: ref('dim_products')
field: product_id
config:
where: "product_id is not null and product_id != '-1'"3. Source Freshness — 소스 데이터 최신성 검사
3.1 왜 필요한가
파이프라인이 멈춰도 dbt run은 성공한다. 기존 테이블은 그대로 있고, 거기서 조인·집계를 하는 모델들도 문제없이 실행된다. 분석가가 대시보드를 열어보고 숫자가 이상하다는 것을 느끼기 전까지는 아무것도 터지지 않는다.
Source Freshness는 이 사각지대를 막는다. 소스 테이블의 마지막 적재 시각을 확인하고, 임계값을 초과하면 경고 또는 오류를 발생시킨다.
3.2 설정 방법
# models/staging/orders/_sources.yml
sources:
- name: raw
database: analytics
schema: raw_data
freshness:
warn_after: {count: 6, period: hour} # 소스 전체 기본값
error_after: {count: 12, period: hour}
loaded_at_field: _etl_loaded_at # 적재 타임스탬프 컬럼
tables:
- name: orders
# 개별 테이블은 소스 기본값 상속
- name: inventory
freshness:
warn_after: {count: 1, period: day} # 테이블별 오버라이드
error_after: {count: 2, period: day}
loaded_at_field: updated_at # 테이블별 오버라이드
- name: audit_log
freshness: null # 이 테이블은 freshness 검사 제외3.3 loaded_at_field 선택 기준
| 컬럼 | 권장 여부 | 이유 |
|---|---|---|
_etl_loaded_at / _loaded_at | 권장 | ELT 도구가 적재 시각을 기록 — 가장 정확 |
updated_at | 주의 | 소스 시스템 갱신 시각. ETL 도달 시각과 다를 수 있음 |
created_at | 비권장 | append-only 테이블에만 유효. 갱신된 행을 놓침 |
dbt는 freshness 검사를 항상 UTC 기준으로 수행한다. 소스 컬럼이 로컬 시각을 저장하면 변환이 필요하다.
3.4 실행 방법
# freshness 검사만 실행
dbt source freshness
# 특정 소스만 검사
dbt source freshness --select source:raw
# 주의: dbt build / dbt test 로는 freshness가 실행되지 않는다3.5 freshness 결과 해석
4. 테스트 설정: severity와 where
4.1 severity — 경고 vs 오류
기본값은 error다. 데이터 품질 문제가 바로 파이프라인을 중단시키지 않아야 하는 경우 warn으로 낮출 수 있다.
- name: email
data_tests:
- not_null:
config:
severity: warn # 실패해도 파이프라인 계속 진행신규 소스 연동 초기에는 warn으로 시작해서 실제 데이터 품질을 파악한 후 error로 올리는 것이 안전하다.
4.2 where — 조건부 검사
특정 행에만 테스트를 적용할 수 있다. NULL을 허용해야 하는 레거시 컬럼이나, 특정 상태에서만 의미 있는 컬럼에 유용하다.
- name: shipped_at
data_tests:
- not_null:
config:
where: "order_status = 'shipped'"5. Custom Generic Test
내장 4종으로 커버되지 않는 비즈니스 규칙은 직접 만들 수 있다.
5.1 정의 방법
tests/generic/ 또는 macros/ 폴더에 test_ 접두어로 시작하는 Jinja 블록을 작성한다.
-- tests/generic/test_amount_is_positive.sql
{% test amount_is_positive(model, column_name) %}
select *
from {{ model }}
where {{ column_name }} < 0
{% endtest %}0 행 반환 = 통과, 1행 이상 반환 = 실패다. YAML에서 컬럼명 없이 모델 전체를 대상으로 하려면 column_name 인자를 생략한다.
# schema.yml에서 사용
- name: amount
data_tests:
- amount_is_positive5.2 파라미터가 있는 Custom Test
-- tests/generic/test_column_sum_equals.sql
{% test column_sum_equals(model, column_name, expected_total) %}
select abs(sum({{ column_name }}) - {{ expected_total }}) as diff
from {{ model }}
having diff > 0.01
{% endtest %} - name: allocated_budget
data_tests:
- column_sum_equals:
expected_total: 10000006. Singular Test — 복잡한 비즈니스 규칙
재사용이 필요 없는 복잡한 규칙은 tests/ 폴더에 SQL 파일로 작성한다. 파일명이 테스트명이 된다.
-- tests/assert_order_revenue_matches_line_items.sql
-- 주문 헤더의 total_amount가 주문 명세 합계와 일치하는지 확인
select
o.order_id,
o.total_amount as header_amount,
sum(li.unit_price * li.quantity) as line_item_total
from {{ ref('mart_orders') }} o
join {{ ref('mart_order_line_items') }} li using (order_id)
group by 1, 2
having abs(header_amount - line_item_total) > 0.01이 쿼리가 행을 반환하면 테스트는 실패한다. Singular Test는 여러 테이블을 함께 검증하거나, 집계 결과를 비교하거나, 복잡한 비즈니스 규칙을 표현할 때 적합하다.
7. 테스트 패키지: dbt-utils와 dbt-expectations
7.1 dbt-utils
dbt Labs가 유지 관리하는 공식 확장 패키지다. 내장 4종 외에 유용한 테스트들이 포함되어 있다.
# packages.yml
packages:
- package: dbt-labs/dbt_utils
version: ">=1.0.0"자주 쓰이는 테스트:
data_tests:
# 두 컬럼의 조합이 유일해야 함
- dbt_utils.unique_combination_of_columns:
combination_of_columns: [order_id, product_id]
# 행 수가 다른 모델보다 적어야 함 (mart가 소스보다 적을 때 정상)
- dbt_utils.fewer_rows_than:
compare_model: ref('stg_orders')
# SQL 표현식이 참이어야 함
- dbt_utils.expression_is_true:
expression: "paid_amount <= total_amount"7.2 dbt-expectations
Great Expectations 스타일의 테스트 62종을 제공한다. 수치 범위, 분포, 패턴 검사에 강하다.
packages:
- package: metaplane/dbt_expectations
version: ">=0.10.0" data_tests:
# 컬럼 값의 범위 검사
- dbt_expectations.expect_column_values_to_be_between:
min_value: 0
max_value: 100
# 문자열 패턴 검사 (이메일 형식)
- dbt_expectations.expect_column_values_to_match_regex:
regex: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}{{BODY}}quot;
# 컬럼의 중앙값이 범위 안에 있어야 함
- dbt_expectations.expect_column_median_to_be_between:
min_value: 50
max_value: 2008. 테스트 전략: 무엇을, 얼마나
모든 컬럼에 모든 테스트를 걸면 실행 시간이 길어지고 테스트 실패가 너무 많아진다. 중요도에 따라 계층화한다.
기본 키: unique + not_null
소스 freshness: 모든 원천 테이블에 warn_after / error_after
FK 컬럼: relationships
열거형: accepted_values
NOT NULL 중요 컬럼
집계 합계 검증: singular test
수치 범위: dbt-expectations
복합 유일성: dbt_utils.unique_combination_of_columns
Elementary Data, Monte Carlo 등 외부 관측 도구로 보완
실전 설정 예시 (우선순위 적용)
models:
- name: mart_orders
data_tests:
# 모델 레벨: 복합 키 유일성
- dbt_utils.unique_combination_of_columns:
combination_of_columns: [order_id, order_date]
columns:
- name: order_id
data_tests:
- unique
- not_null
- name: customer_id
data_tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: order_status
data_tests:
- accepted_values:
values: ['pending', 'paid', 'shipped', 'completed', 'cancelled']
- name: paid_amount
data_tests:
- not_null
- dbt_utils.expression_is_true:
expression: ">= 0"9. 테스트 실행과 CI 통합
# 전체 테스트
dbt test
# 특정 모델 테스트
dbt test --select mart_orders
# 특정 테스트 유형만
dbt test --select test_type:generic
# 빌드 + 테스트 (dbt build = run + test + snapshot)
dbt build --select mart_orders+CI 파이프라인에서는 변경된 모델과 그 하위 모델만 선택해서 테스트한다. 전체 테스트 실행은 시간이 너무 오래 걸린다.
# GitHub Actions에서 PR 대상 변경 모델과 하위 모델만 테스트
dbt build --select state:modified+ --defer --state ./prod_artifacts--defer --state는 변경되지 않은 upstream 모델은 프로덕션 결과물을 재사용하게 한다.
마무리
dbt 테스트는 "데이터가 변환 전제를 만족하는가"를 코드로 표현하는 방법이다. Source Freshness로 적재 지연을 탐지하고, accepted_values와 relationships로 도메인 규칙을 강제하고, custom test로 비즈니스 로직을 검증한다. 중요도에 따라 테스트를 계층화하면 테스트 피로 없이 핵심 품질 보장이 가능하다.
다음 편에서는 이 모델들 간의 lineage를 활용해 변경이 어디까지 영향을 미치는지 분석하고, 다운스트림 대시보드를 보호하는 실전 전략을 다룬다.
References
- dbt Labs, "Add data tests to your DAG": https://docs.getdbt.com/docs/build/data-tests
- dbt Labs, "Writing custom generic data tests": https://docs.getdbt.com/best-practices/writing-custom-generic-tests
- dbt Labs, "freshness reference": https://docs.getdbt.com/reference/resource-properties/freshness
- dbt Labs, "Add sources to your DAG": https://docs.getdbt.com/docs/build/sources
- dbt Labs, "About dbt source command": https://docs.getdbt.com/reference/commands/source
- Paradime, "dbt Source Freshness: Best Practices for Analytics Engineers": https://www.paradime.io/guides/blog-dbt-source-freshness-best-practices
- Datafold, "How to use dbt source freshness tests to detect stale data": https://www.datafold.com/blog/dbt-source-freshness/
- Datacoves, "dbt Testing: A Complete Guide": https://datacoves.com/post/dbt-test-options
- Elementary Data, "dbt tests: How to write fewer and better data tests": https://www.elementary-data.com/post/dbt-tests
- Datafold, "7 dbt testing best practices": https://www.datafold.com/blog/7-dbt-testing-best-practices/