본문 바로가기

프로그래밍

dbt + DuckDB로 팀 내 로컬 레이크하우스 모델링 시작 가이드: 초보 실무자를 위한 첫 설정과 예시

반응형

왜 로컬 레이크하우스인가

 

데이터 파이프라인을 바로 클라우드에 올리기 전, 팀 안에서 빠르게 모델을 설계·검증할 공간이 필요합니다. dbt + DuckDB는 노트북 한 대로도 레이크하우스 흐름을 흉내 내며 SQL 모델링과 테스트를 즉시 반복할 수 있게 합니다.

 

복잡한 인프라 없이도 원천 CSV/파케이(Parquet)를 읽고, 변환 모델을 쌓고, 결과를 파일로 저장하는 전 과정을 로컬에서 재현할 수 있습니다. 이는 작은 팀이나 첫 도입 단계에서 비용과 승인 절차를 줄여 초기 설계를 안정적으로 다듬는 데 유리합니다.

 

Why now 관점에서, 현업은 데이터 출처가 늘고 변경 주기도 빨라졌습니다. 클라우드 리소스를 할당받기 전 로컬에서 “원천 -> 변환 -> 검증”을 짧은 주기로 돌리면, 잘못된 스키마나 조인 논리를 초기에 잡아내 운영 리스크를 낮출 수 있습니다.

 

> 안티패턴: 프로덕션 전환을 염두에 두지 않은 로컬 전용 경로·파일명 하드코딩은 피하세요. 개발 단계부터 경로와 설정을 변수화해 배포 전환 비용을 줄이는 편이 안전합니다.

 

dbt와 DuckDB, 각각 무엇이고 어떻게 맞물리나

 

dbt는 SQL로 변환 로직을 모듈화하고, 의존성을 그래프로 관리하는 도구입니다. DuckDB는 파일 기반 컬럼형 엔진으로 노트북·로컬에서 빠르게 SQL을 실행합니다.

 

두 도구가 만나면 흐름은 간단합니다: 원천 파일(CSV/Parquet) -> dbt 모델(SQL 변환) -> DuckDB에서 실행 -> 결과를 테이블·파일로 저장. 팀은 SQL 중심으로 모델을 나누고, 실행은 DuckDB가 맡아 즉시 피드백을 줍니다.

 

현실적인 예를 보겠습니다. 마케팅 CSV와 매출 Parquet를 합쳐 월간 퍼널을 만든다고 할 때, dbt는 소스 선언과 조인 규칙을 버전 관리하고, DuckDB는 로컬 파일을 바로 읽어 조인을 계산합니다.

 

결과는 하나의 DuckDB 데이터베이스 파일이나 Parquet로 남겨 다음 단계 검증에 씁니다.

 

초보자는 다음 체크리스트로 개념을 잡으면 이해가 빠릅니다.
- dbt: 모델은 SQL 파일, 의존성은 ref(), 실행은 run/test
- DuckDB: 파일을 테이블처럼 읽기, 결과를 로컬에 쓰기
- 협업: 모델 이름·폴더로 도메인 구분, 공통 키·시간대 규칙을 yml로 명시

 

> 안티패턴: DuckDB 경로와 임시 폴더를 모델 안에 하드코딩하면 환경 전환이 어렵습니다. profiles.yml이나 환경 변수로 경로를 주입하세요.

 

로컬 최소 설정과 빠른 실행

 

dbt와 DuckDB만 준비하면 로컬에서 바로 흐름을 확인할 수 있습니다. 체크리스트대로 진행하면 첫 모델을 빠르게 실행합니다.

 

- Python 3.9+와 가상환경 준비(프로젝트 별 독립 실행 환경)
- 패키지 설치: dbt-duckdb, duckdb
- dbt init로 프로젝트 생성
- profiles.yml에 DuckDB 연결(type, path) 설정
- 소스 선언과 모델 SQL 작성
- dbt seed/run/test 순서로 실행

 

dbt가 DuckDB 파일에 쿼리하려면 profiles.yml과 모델이 필요합니다. profiles.yml은 연결 설정 파일, 모델은 변환 SQL입니다.

 

# 1) 가상환경과 패키지
python -m venv .venv && source .venv/bin/activate
pip install dbt-duckdb duckdb

# 2) 프로젝트 생성
dbt init local_lake && cd local_lake

# 3) DuckDB 프로필(~/.dbt/profiles.yml)
# local_duckdb:
#   target: dev
#   outputs:
#     dev:
#       type: duckdb        # 엔진 종류
#       path: ./warehouse.duckdb  # 로컬 DB 파일 경로
#       threads: 4

# 4) 소스/모델 생성
mkdir -p models && mkdir -p seeds
echo "id,name,spent\n1,A,100\n2,B,200" > seeds/customers.csv
echo "version: 2\nseeds:\n  - name: customers" > models/schema.yml
echo "select id, upper(name) as name, spent from {{ ref('customers') }}" > models/marts__customers.sql

# 5) 실행
dbt seed
dbt run
dbt test

 

핵심은 profiles.yml의 type=duckdb와 path입니다. path는 로컬 DB 파일 위치이며, 팀에서는 보통 프로젝트 루트 기준 상대경로로 둡니다.

 

> 실무 팁: 절대경로나 사용자 홈 경로 하드코딩은 피하세요. 프로젝트 루트 기준 상대경로와 .env 변수로 분리하면 환경 전환이 수월합니다.

 

초기 설정 체크와 선택 기준 요약

 

초기 설정에서 자주 막히는 건 경로와 스키마입니다. 로컬 전용 값 하드코딩은 이관 때 큰 비용이 됩니다.

 

- 파일 경로: 절대경로 대신 프로젝트 루트 기준 상대경로 사용
- 대상 스키마: DuckDB의 schema 대신 dbt의 target_name으로 결과 경로 분리
- 시간 컬럼: 문자열 날짜는 즉시 변환해 파티션·증분 기준 확정
- 시드/소스: seed와 external 소스 혼용 금지, 소스는 catalog.yml에 명시

 

DuckDB와 Parquet 중 무엇을 쓸지 먼저 정하세요. 빠른 실험은 .duckdb, 팀 공유나 다른 엔진 연동은 Parquet이 맞습니다.

 

- .duckdb: 단일 파일, 쿼리 빠름, 로컬 탐색 쉬움
- Parquet: 컬럼형 압축, 엔진 간 호환, 버전 관리 유리
- 혼용: 중간 산출은 .duckdb, 최종 산출은 Parquet

 

증분 모델은 고유키와 변경 기준이 핵심입니다. updated_at이 없으면 해시 키로 중복을 막고, 삭제는 별도 테이블로 관리하세요.

 

> 안티패턴: 매번 전체 리빌드는 데이터가 늘수록 시간 급증. 초기에 증분 기준과 날짜 범위 파라미터를 마련하세요.

 

테스트와 문서는 최소로 시작해도 충분합니다. 스키마 테스트는 not_null, unique, accepted_values 3가지만 두고, 모델 설명에 소스 주기와 갱신 시간을 남기면 클라우드 전환 판단이 빨라집니다.

 

지금 할 일과 다음 단계 정리

 

첫 목표는 로컬에서 “원천 파일 -> dbt 모델 -> DuckDB 결과” 흐름을 끝까지 통과시키는 것입니다. 작게 시작해 빠르게 검증하고, 경로·스키마 규칙을 초기에 고정하세요.

 

바로 실행하려면 아래 순서를 체크하세요. 한 번 통과하면 팀 규칙(폴더, 스키마, 파일 포맷)을 리드미에 남겨 재현성을 확보합니다.

 

- Python 가상환경 구성과 dbt-duckdb 설치
- dbt init 후 profiles.yml에 DuckDB 파일 경로 설정
- 소스 선언(catalog.yml)과 첫 모델(SQL) 작성
- dbt seed/run/test 실행과 결과(.duckdb 또는 Parquet) 확인

 

증분이 필요한 모델은 고유키와 변경 기준 컬럼을 먼저 정하고, 실험 산출은 .duckdb, 공유 산출은 Parquet로 분리해 저장합니다. 작은 예시를 끝내면, 월간 배치 기준과 폴더 네이밍을 팀 합의로 문서화하세요.

 

> 실무 팁: 로컬 전용 절대경로, 날짜가 박힌 파일명 하드코딩은 이관 시 장애 포인트입니다. 프로젝트 루트 기준 상대경로와 매크로를 기본값으로 통일하세요.

 

최소 템플릿 요약

 

로컬에서 바로 실행할 수 있는 3단계(소스 → 정리 → 집계) 템플릿입니다. 흐름을 고정하면 디버깅과 확장이 쉬워집니다.

 

먼저 CSV/Parquet 같은 원본 파일을 소스로 선언합니다. 상대경로는 프로젝트 루트 기준이 안전합니다.

 

version: 2
sources:
  - name: raw
    tables:
      - name: marketing
        external:
          location: "data/marketing.csv"
          file_format: "csv"
          options: { header: true, delim: "," }
      - name: sales
        external:
          location: "data/sales.parquet"
          file_format: "parquet"

 

source()는 원본의 이름·형식을 고정해, 이후 모델에서 같은 참조를 보장합니다.

 

중간 정리(staging)에서는 날짜·키 형식을 먼저 통일합니다. 정규화는 조인 오류를 줄입니다.

 

-- models/stg_sales.sql
with base as (
  select
    order_id,
    customer_id,
    cast(order_date as date) as order_date,
    amount
  from {{ source('raw','sales') }}
)
select * from base

 

stg에서 타입을 확정해 두면 비교나 증분 처리 로직이 안정됩니다.

 

마케팅과 매출을 월 단위로 합쳐 간단한 퍼널을 만듭니다. 퍼널은 단계별 전환을 보는 표입니다.

 

-- models/fct_monthly_funnel.sql
with mkt as (
  select date_trunc('month', cast(event_date as date)) as month, user_id
  from {{ source('raw','marketing') }}
),
ord as (
  select date_trunc('month', order_date) as month, customer_id
  from {{ ref('stg_sales') }}
)
select
  mkt.month,
  count(distinct mkt.user_id) as touched_users,
  count(distinct ord.customer_id) as ordered_users
from mkt
left join ord using(month)
group by 1

 

ref()는 모델 의존성을 연결해 실행 순서와 캐싱을 자동으로 관리합니다.

 

> 실무 팁
> - 파일 경로는 env_var로 분리해 로컬/CI 전환을 쉽게 하세요.
> - 날짜 변환은 한 모델만 맡기고, 다른 단계에서는 재가공을 피하세요.

반응형