List
지난 글에서는 dbt의 7대 핵심 구성요소를 살펴보며, 각각이 데이터 파이프라인 속에서 어떤 역할을 하는지 정리했습니다.
데이터는 단순히 변환만 잘 된다고 끝나는 게 아니라, 항상 신뢰할 수 있어야 비로소 활용 가치가 생깁니다. 이를 위해 dbt는 다양한 테스트 기능을 제공하고 있으며, 이를 통해 데이터가 올바른 형태와 규칙을 지키고 있는지 자동으로 검증할 수 있습니다.
이번 글에서는 dbt의 테스트 기능을 하나씩 살펴보며, 실제 비즈니스 시나리오에서 어떻게 활용할 수 있는지 구체적으로 다뤄보겠습니다.
dbt에서의 테스트
데이터는 잘 돌아갈 때는 조용하지만, 어긋나는 순간 해당 데이터를 사용하는 다양한 곳에서 문제가 발생합니다. 스키마가 바뀌거나, 새 값이 유입되거나, 조인 키가 끊기는 문제 등 다양한 문제들은 빨리 발견할수록 해결하는 비용이 줄어듭니다. dbt의 테스트는 규칙을 코드로 고정하게 해줍니다.
•
Generic Tests: YAML에 선언만 하면 되는 기본 검증 (unique, not_null, accepted_values, relationships 등)
•
Singular Tests: SQL로 직접 쓰는 맞춤 검증 (비즈니스 규칙)
Generic Tests — 선언형 기본 검증
dbt는 YAML 선언만으로 기본적인 데이터 무결성 검증을 자동 실행해 줍니다.
가장 자주 쓰는 4종 세트:
•
unique : 중복 금지
•
not_null : NULL 금지
•
accepted_values : 허용된 값만
•
relationships : 참조 무결성(FK 유사)
각 테스트는 선언만 하면 dbt가 SQL을 생성·실행합니다.
1) unique / not_null — 기본 무결성
# models/core/customers.yml
version: 2 # dbt 메타데이터 버전. 관례적으로 2를 사용.
models:
- name: customers # 테스트를 걸 대상 모델명(=테이블/뷰 이름)
description: "고객 마스터 테이블" # 문서화에도 활용됨(dbt docs)
columns:
- name: customer_id # 검증 대상 컬럼
description: "고유 고객 식별자"
tests: # 여기 아래에 제약(=테스트)을 선언형으로 적는다
- not_null # NULL 허용 안 함
- unique # 중복 허용 안 함
# (선택) 실패를 경고로만 처리하려면
# - not_null:
# severity: warn # 기본은 error. warn으로 낮출 수 있음
# - unique:
# severity: warn
YAML
복사
예시 설명
•
customers.customer_id에 NULL이나 중복이 있으면 테스트 실패로 기록
•
DB의 PK 제약과 유사한 논리 제약을 YAML로 명시
•
배치/증분 업데이트 도중 유입된 이상치(중복 키, 누락 키)를 빠르게 조기 감지 가능
2) accepted_values — 허용된 값만 통과
# models/core/orders.yml
version: 2
models:
- name: orders
description: "주문 테이블"
columns:
- name: status
description: "주문 상태 (문자열)"
tests:
- accepted_values:
# values: 허용할 값 목록
values: ['new', 'shipped', 'cancelled']
# quote: 값들을 SQL에서 작은따옴표로 감쌀지 여부
# - 문자열이면 true (또는 생략) -> 'new', 'shipped' 같은 리터럴 생성
# - 숫자/불리언이면 false -> 1, 2, 3 처럼 따옴표 없이 생성
quote: true
- name: status_code
description: "주문 상태 코드 (정수)"
tests:
- accepted_values:
values: [1, 2, 3] # 숫자라면 보통 quote: false (또는 생략) 권장
quote: false # 숫자/불리언은 false로 해야 타입 미스매치 방지
# (선택) 최근 데이터만 검사하고 싶다면 where 조건을 추가할 수 있음
# where: "order_date >= current_date - interval '30' day"
YAML
복사
예시 설명
•
dbt가 내부적으로 status in ('new','shipped','cancelled') 같은 SQL을 생성
•
숫자/불리언 컬럼은 따옴표를 쓰면 타입 불일치가 나거나 인덱스를 못 타는 경우가 있으니 quote: false가 안전
3) relationships — 참조 무결성(FK 유사)
# models/core/orders.yml
version: 2
models:
- name: orders
description: "주문 테이블"
columns:
- name: customer_id
description: "주문 고객 ID"
tests:
- relationships:
# to: 어떤 테이블(모델)로 참조를 확인할지
to: ref('customers') # ref()는 dbt가 의존성과 실제 오브젝트 이름을 자동 해석
# field: 상대 테이블의 어떤 컬럼과 매칭할지
field: customer_id
# (선택) orders 쪽에서 필터를 걸어 검사 대상을 좁힐 수도 있음
# where: "is_test_data = false"
YAML
복사
예시 설명
•
orders.customer_id가 반드시 customers.customer_id에 존재해야 테스트가 통과
•
DB 외래키(FK)가 없어도 dbt에서 논리적 참조 무결성을 보장할 수 있음
•
소프트 삭제(soft delete)나 테스트 데이터는 where로 제외하는 방식이 좋음
Singular Tests — 비즈니스 규칙을 SQL로
Generic Test로는 표현이 애매한 규칙(“최근 7일 주문은 반드시 고객이 있어야 한다”, “금액은 0보다 커야 한다”)은 Singular Test로 직접 SQL을 씁니다. 쿼리 결과에 1행이라도 나오면 실패로 판단합니다.
예시 1) “주문 금액은 양수여야 한다”
-- tests/t_orders_amount_positive.sql
-- 목적: 주문 금액(amount)은 항상 0보다 커야 한다.
-- 규칙 위반(금액 <= 0)인 행을 모두 출력하면 테스트는 "실패"로 처리된다.
{{ config(
severity='error' -- 기본값은 error. 필요하면 'warn'으로 낮춰 운영 차단 없이 경고만 남길 수 있음
#, where="1=1" -- (선택) 공통 필터가 있다면 여기에 조건을 걸어 재사용 가능
) }}
select
o.order_id, -- 문제된 주문 ID
o.amount, -- 0 이하 금액
o.order_time
from {{ ref('orders') }} as o -- 테스트 대상 모델. ref()를 써야 의존성/이름 해석이 안전함
where
coalesce(o.amount, 0) <= 0 -- NULL도 0으로 취급해 실패로 잡음
SQL
복사
예시 설명
•
orders 테이블에서 금액이 0 이하인 모든 행을 반환
•
한 행이라도 나오면 dbt가 테스트를 실패로 판단
예시 2) “최근 N일 주문은 반드시 고객이 있어야 한다”
-- tests/t_recent_orders_have_customer.sql
-- 목적: 최근 N일 동안의 주문은 반드시 customers에 존재하는 고객이어야 한다.
-- (증분 적재나 조인 순서 문제로 orphan 주문이 생기는 것을 방지)
{% set recent_days = var('recent_days', 7) %} -- 기본 7일. 실행 시 --vars '{"recent_days": 14}' 로 오버라이드 가능.
{{ config(
severity='error' -- 실패 시 파이프라인 차단. 필요하면 'warn'
) }}
select
o.order_id,
o.customer_id,
o.order_time
from {{ ref('orders') }} as o
left join {{ ref('customers') }} as c
on o.customer_id = c.customer_id
where
-- (예시는 SQLite). 엔진별 날짜 함수 차이를 주의
date(o.order_time) >= date('now', '-' || {{ recent_days }} || ' day')
-- 고객이 없으면 "고아 주문" → 테스트 실패 대상
and c.customer_id is null
SQL
복사
예시 설명
•
최근 recent_days일 안에 발생한 주문 중 customers에 고객 키가 없는 주문을 전부 리턴
•
1행이라도 나오면 실패고, 원인(증분 적재 순서, 외래키 누락)을 빠르게 확인 가능
•
파라미터화: var('recent_days', 7)로 기간을 외부에서 바꿔가며 테스트 가능.
◦
dbt test --select t_recent_orders_have_customer --vars '{"recent_days": 3}'
•
엔진별 날짜 함수를 고려해서 작성
실행·선택·심각도 — 운영 관점의 기본기
실행과 선택
dbt test # 전체 테스트 실행
dbt test --select orders # 특정 모델에 연결된 테스트만 실행
dbt test --select source:raw.* # 특정 소스에 연결된 테스트만 실행
dbt test --select tag:data_quality # 태그 기반 선택 실행
Bash
복사
•
전체 실행은 배치 검증 시 유용하지만, 개발·CI 상황에서는 너무 무거움
•
선택 실행(-select)을 활용하면 영향 범위가 좁은 부분만 빠르게 검증 가능
•
태그(tag)를 활용하면 중요 규칙만 골라 주기적으로 모니터링하는 식의 운영 가능
심각도(severity)와 실패 행 저장
# models/staging/stg_orders.yml
version: 2
models:
- name: stg_orders
columns:
- name: status
tests:
- accepted_values:
values: ['PENDING', 'PAID', 'CANCELLED']
severity: warn # 실패해도 파이프라인 중단은 아님
YAML
복사
# dbt_project.yml
tests:
+store_failures: true # 실패한 행을 별도 테이블에 저장 (지원 어댑터 한정)
YAML
복사
•
severity
◦
error → 실패 시 빌드를 멈추고 문제를 즉시 차단
◦
warn → 실패는 기록되지만 빌드 중단은 없음 (감시용)
•
store_failures
◦
실패한 행을 남겨두면 디버깅 속도가 크게 빨라짐
•
운영에서는 치명적인 규칙은 error, 추적성 규칙은 warn으로 구분하는 전략이 필요
계층별 테스트 전략
계층 | 검증 요소 | 예시 |
Sources | 원천 데이터 스키마·무결성 | not_null / unique / accepted_values |
Staging | 변환·표준화 결과 | not_null / accepted_values / 값 범위 |
Core | 비즈니스 규칙·참조 무결성 | relationships / 금액·날짜 규칙 (singular) |
Mart | 리포트 직전 sanity check | 레코드 수 범위, NULL 비율, 임계치 경보 |
운영 원칙
•
넓게: Source/Staging 계층에는 범용 검증을 폭넓게 배치
•
깊게: Core/Mart 계층에는 비즈니스 규칙 기반의 정밀 검증
•
유연하게: 규칙의 변경 가능성이 크다면 tag를 달아 선택 실행/경보 수준을 조절
실무 시나리오별 적용
1) 신규 상태값 감지 (화이트리스트 기반)
# models/staging/stg_orders.yml
version: 2
models:
- name: stg_orders
columns:
- name: status
tests:
- accepted_values:
values: ['PENDING', 'PAID', 'CANCELLED']
severity: warn
YAML
복사
•
효과
◦
새 상태값이 유입되면 즉시 경고 발생
•
대응
◦
정상적인 신규 값이면 화이트리스트를 업데이트
◦
의도치 않은 값이면 업스트림 버그로 추적
2) 주문-고객 참조 무결성
# models/core/orders.yml
version: 2
models:
- name: orders
columns:
- name: customer_id
tests:
- relationships:
to: "{{ ref('customers') }}"
field: customer_id
YAML
복사
•
효과
◦
고객 테이블에 존재하지 않는 키가 들어오면 실패
•
대응
◦
최근 적재분만 실패 → 증분 적재 순서/지연 문제 가능성
◦
과거까지 실패 → 키 정책/매핑 로직 오류 가능성
3) 금액·환불 규칙 (커스텀 테스트)
-- tests/t_refund_amount_negative_only.sql
-- 환불 건은 반드시 금액이 음수여야 한다
select *
from {{ ref('orders') }}
where status = 'REFUND'
and amount >= 0
SQL
복사
•
효과
◦
단순 SQL로 비즈니스 규칙을 강제
•
대응
◦
결제/회계 파이프라인과 협의해 부호 정책을 일치
테스트 실패 시 대응 절차
1.
유형 분류
•
스키마 불일치, 값 범위, 참조 무결성, 비즈니스 규칙 중 어디에서 발생했는지
2.
영향 범위 확인
•
최근 적재분만? 특정 소스만? 전 기간 전체?
3.
원인 구분
•
업스트림(원천) 문제인지, 우리 모델 로직 문제인지
4.
임시 완화
•
운영 차질이 크면 severity: warn으로 낮추고 원인 해결
5.
영구 수정
•
accepted_values 보강, 키 매핑 재설계, 로직 보완 등
6.
재발 방지
•
실패 행을 저장해 원인을 추적하고, 테스트를 보강
마무리
이번 글에서는 dbt의 Generic·Singular 테스트, 그리고 운영 전략을 중심으로 데이터 품질을 코드로 보장하는 방법을 살펴보았습니다. 품질 검증을 자동화된 규칙으로 끌어올려, 데이터 파이프라인의 신뢰성을 일관되게 유지하는 데 있습니다.
다음 글에서는 dbt에서 가장 강력한 무기 중 하나인 Jinja와 매크로를 다룰 예정입니다. 단순 반복을 줄이고, 동적 SQL을 효율적으로 작성하며, 팀 차원의 표준화된 로직을 공유할 수 있는 방법을 알아보겠습니다.