DBT and Airflow

3편 · BigQuery 위에서 신뢰할 수 있는 모델 만들기

  • Haram Lee
  • 2026-07-05
  • work / Daangn / data-team-notes

원문: DBT와 Airflow 도입하며 마주한 7가지 문제들 (2024-01-29)

도메인 지식이 있는 구성원이 데이터 엔지니어링을 깊게 몰라도 신뢰할 수 있는 데이터 모델을 만들 수 있도록, DBT와 Airflow 기반의 모델링·실행·테스트 환경을 만든 이야기.

이 글이 당근 데이터 시스템에서 차지하는 위치

앞의 두 글이 데이터를 BigQuery로 가져오는 과정이었다면, 이 글은 BigQuery에 들어온 데이터를 어떻게 믿고 쓸 수 있는 형태로 모델링하는가에 가깝다.

text
Raw / Source Data
  → DBT Base → DBT Dimension / Fact
  → Airflow 로 주기 실행 → 테스트 / 문서화 / 백필
  → 구성원이 사용하는 신뢰 가능한 데이터 모델

당근이 마주한 문제 — 신뢰할 수 있는 정보를 “많이”

신뢰할 수 있는 데이터를 만들려면 도메인 지식데이터 엔지니어링 역량이 둘 다 필요하다. 그런데 도메인 지식은 서비스 팀에 많고, 데이터 엔지니어링 역량은 일부에 집중돼 있다. 그래서 목표는:

도메인 지식이 있는 사람이, 데이터 엔지니어링을 깊게 몰라도, 신뢰할 수 있는 데이터 모델을 만들 수 있게 하자.

원천 데이터는 서비스 동작에 맞춰 저장돼 있어 곧바로 분석에 적합하지 않다. 정리·정의·테스트·문서화 과정이 필요하고, 그 도구로 DBT와 Airflow를 도입했다. 글은 도입하며 마주한 7가지 문제를 다룬다.

해결 구조

① DBT 프로젝트 구조 — Base / Dimension / Fact

사용자 정보를 3계층으로 나눴다.

text
Base       원천 데이터와 거의 1:1. 컬럼 선택 · 타입 캐스팅 · 이름 통일 (DBT의 staging)
Dimension  사용자 · 상품 같은 개체의 속성 (상태, 생성일, 속성값)
Fact       "사용자가 무엇을 했다" (메시지 전송, 화면 진입, 피드 사용 등 행동)

② 모델 저장 방식(materialization)

text
Base       → view         (단순 변환)
Dimension  → table        (JOIN/WHERE, 재사용성)
Fact       → incremental  (계속 쌓이고 큼 → 날짜 파티션 insert_overwrite)

당근 규모에선 DBT의 일반 is_incremental() 패턴으로 전체를 계산하면 비용이 크다. 그래서 Airflow가 target_date를 넘겨 해당 날짜 파티션만 처리하도록 강제했다.

③ DBT ↔ Airflow 연동

DBT는 스케줄러가 아니다. 주기 실행은 Airflow가 맡되, DBT 모델을 Airflow task로 바꾸기 위해 astronomer-cosmos를 썼다.

text
DBT 모델 의존성 파악 → Airflow TaskGroup 렌더링
  → dbt run / dbt test 실행 (tag·path 기준 선택)

DBT의 모델 DAG를 Airflow의 task DAG로 연결한 것.

④ 개발/테스트 프로세스

구성원이 로컬에서 직접 돌리면 환경 구축·권한·가시성·비용·프로덕션 덮어쓰기 위험이 생긴다. 그래서 PR을 열면 PR별 테스트 환경이 만들어지게 했다.

text
DBT 모델 작성 → PR 생성 → PR별 Airflow / dbt docs 배포
  → dry run 으로 스캔량 확인 (기본 1TB 이상이면 실패 → 비용 폭탄 방지)
  → dbt run → dbt test → _test suffix 데이터셋에서 결과 확인

⑤ 다른 파이프라인과의 의존성

DBT 모델끼리 의존성은 DBT가 알지만, 참조하는 원천 데이터가 아직 적재 안 됐으면 문제다. 그래서 Airflow의 ExternalTaskSensor로, 원천을 적재하는 DAG가 끝난 뒤 DBT 모델이 실행되게 했다.

text
event_impressed 적재 완료 & db_article 적재 완료
  → user_actively_used_feed (fact) 실행

⑥ 국가별 모델 정의

글로벌 서비스라 국가별 타임존·로직이 다를 수 있는데, 국가마다 프로젝트를 따로 만들면 중복이 크다. 그래서 하나의 DBT 프로젝트 안에서 tag · vars · jinja · macro로 국가별 실행을 분기했다. (예: daily-kr, daily-jp tag → Airflow에서 국가 타임존 DAG가 해당 tag 모델만 실행)

⑦ 백필(Backfill)

일반 DBT 백필은 full refresh인데 당근 규모에선 너무 비싸다. 그래서 모델과 날짜 범위를 입력하면 해당 날짜만 백필하는 별도 Airflow DAG를 만들었다. (예: sampleUser_sent_message, 2023-11-01 ~ 2023-11-30 → 날짜별 dynamic task 생성·실행)

주요 개념 정리

DBT (Data Build Tool) — SQL 기반으로 Warehouse 안에서 데이터를 변환·모델링하는 도구. 단순 SQL 실행이 아니라 의존성 관리 · 테스트 · 문서화 · materialization을 함께 제공.

Airflow — 워크플로 스케줄러. 작업을 DAG로 정의해 순서·주기대로 실행. DBT가 “무엇을 만들지”, Airflow가 “언제 어떤 순서로 실행할지"를 담당.

Base / Dimension / Fact — 원천 정리 계층 / 개체 속성 계층 / 행동·사건 계층. 데이터 모델링의 표준적인 층 나누기.

Materialization — DBT 모델을 view / table / incremental 중 어떤 형태로 저장할지 정하는 설정.

Incremental Model — 전체를 매번 다시 계산하지 않고 필요한 범위(주로 날짜 파티션)만 갱신. Fact처럼 크고 계속 쌓이는 모델에 필수.

Partitioning / Clustering — Partitioning은 데이터를 특정 기준(예: 날짜)으로 나눠 저장, Clustering은 파티션 안에서 자주 필터링되는 컬럼 기준으로 정렬·집약. 둘 다 스캔 비용을 줄인다.

DBT Test — 컬럼 not-null, ID unique 같은 조건을 테스트로 정의해 모델 품질을 지속 검증.

DBT Docs — 모델·컬럼 설명·의존성을 문서화. 사용자가 “이 모델이 무슨 의미이고 어디서 왔는지” 파악.

ExternalTaskSensor — Airflow에서 다른 DAG/task 완료를 기다리는 센서. DBT 모델과 원천 적재 파이프라인 사이 의존성을 챙긴다.

Backfill — 과거 날짜 데이터를 다시 계산·채워 넣는 작업. 로직 변경이나 과거 오적재 시 필요.

온보딩 관점 정리

이 글의 핵심은 DBT를 단순 SQL 실행 도구가 아니라 조직 전체가 신뢰할 수 있는 데이터 모델을 만들기 위한 플랫폼으로 확장한 것이다.

text
DBT      = 데이터 모델 정의 / 테스트 / 문서화 / 의존성
Airflow  = 스케줄링 / 의존성 / 백필 / 실행 관리
BigQuery = 저장소 / 분석 환경

SQL 모델을 작성하는 것 자체가 아니라, **모델 구조 · 실행 주기 · 의존성 · 테스트 · 문서화 · 백필까지 포함한 “운영 가능한 모델링 환경”**을 만드는 것이 데이터 플랫폼의 역할이라는 걸 보여준다.

더 공부할 것

아래는 원문 밖에서 직접 찾아본 답변. 온보딩용 감을 잡는 수준이고, 실제 구현은 팀 코드로 확인 필요.

DBT ref / source / macro / test는 각각 어떻게 쓰일까? ref('model')은 다른 dbt 모델을 참조하는데, 이걸 통해 dbt가 의존성 그래프(DAG)를 만들고 실행 순서를 결정하며 환경별 스키마로 자동 치환한다. source('name','table')은 dbt 외부의 원천 테이블을 선언·참조하는 것으로 freshness 체크·문서화가 가능하다. macro는 Jinja 함수로 반복 SQL 로직(국가별 분기, 공통 변환 등)을 재사용한다. test는 데이터 assertion으로, 기본 test(unique/not_null/accepted_values/relationships)와 custom SQL test가 있고 dbt test로 실행해 실패 시 파이프라인을 fail시킨다.

DBT incremental model은 BigQuery에서 어떻게 동작할까? 첫 실행은 전체 테이블을 만들고, 이후엔 is_incremental() 블록 조건에 맞는 새 데이터만 처리한다. BigQuery 전략은 merge(PK로 MERGE문)와 insert_overwrite(파티션 단위 교체)가 있는데, 대용량은 **insert_overwrite + partition_by(날짜)**가 유리하다 — 대상 파티션만 통째로 replace하므로 MERGE보다 저렴하고 멱등적이다. 당근처럼 target_date를 넘겨 해당 파티션만 덮어쓰면 재실행해도 결과가 같고(idempotent) full scan을 피한다. 단 스키마 변경 대응(on_schema_change)과 늦게 도착한 데이터를 위한 lookback window를 신경 써야 한다.

Airflow ExternalTaskSensor는 어떤 경우에 문제가 될 수 있을까? 기본 동작이 같은 logical date의 task를 기다리는 것이라, 두 DAG의 schedule/execution_date가 안 맞으면 영원히 대기한다(→ execution_delta/execution_date_fn으로 맞춰야 함). 업스트림이 그날 안 돌거나(스킵) poke 방식으로 대기하면 worker slot을 점유하므로 timeout·mode='reschedule'·failed_states 설정이 필요하다. 또 업스트림 DAG/task_id를 문자열로 참조하기 때문에 이름이 바뀌면 조용히 깨진다 — 그래서 최근엔 Airflow **Datasets(data-aware scheduling)**로 전환하는 흐름이 있다.

DBT 모델이 많아질수록 DAG 복잡도는 어떻게 관리할까? 레이어링(staging/intermediate/marts)과 디렉토리 컨벤션으로 그래프를 정리하고, tags/selectors로 부분 실행(daily·hourly·국가별)을 나눠 하나의 거대 DAG 대신 논리 단위로 쪼갠다. Cosmos가 dbt DAG를 Airflow TaskGroup으로 렌더하면 세밀하지만 task가 폭증할 수 있어, 규모가 커지면 모델 그룹 단위로 묶거나 dbt build를 통째 태스크로 돌리기도 한다. lineage(dbt docs·exposures)와 모델 소유권·contract로 거버넌스를 잡는다.

Base / Dimension / Fact 이후 Mart나 Semantic Layer는 어떤 역할을 할까? Mart는 Dimension/Fact를 특정 도메인·용도에 맞게 조인·집계한 소비자용 최종 테이블이다(예: 마케팅 mart, 특정 대시보드용) — 분석가가 바로 쓰는 층. Semantic Layer는 지표(metric) 정의를 중앙화해서, “활성 사용자” 같은 지표가 BI 도구마다 다르게 계산되는 걸 막는다(dbt Semantic Layer/MetricFlow가 metric을 쿼리 타임에 SQL로 변환). 정리하면 Base/Dim/Fact = 재사용 모델, Mart = 용도별 조합, Semantic Layer = 지표 정의의 단일 진실이다.

Discussion