데이터 엔지니어 인터뷰 — dbt

기본 개념

dbt는 무엇을 하는 도구인가요

T(Transform)만 담당하는 도구입니다. 데이터를 추출하거나 적재하지 않습니다. 이미 웨어하우스에 들어와 있는 원본을 SQL로 변환해 분석용 테이블·뷰를 만드는 일을 합니다.

핵심은 SQL SELECT문 하나가 모델 하나가 되고, dbt가 그것을 CREATE TABLE AS / CREATE VIEW AS로 감싸 실행한다는 점입니다. 개발자는 DDL을 쓰지 않고 “이 데이터가 무엇인지”만 정의합니다.

여기에 소프트웨어 개발 관행을 얹습니다 — 버전 관리, 의존성 그래프, 테스트, 문서, 환경 분리.

ETL과 ELT의 차이는 무엇인가요

ETL은 변환을 웨어하우스 밖에서 하고 결과만 적재합니다. ELT는 원본을 먼저 적재하고 변환을 웨어하우스 안에서 합니다.

ELT가 주류가 된 이유는 웨어하우스의 연산 능력이 저렴하고 강력해졌기 때문입니다. 원본이 그대로 남아 있어 로직이 바뀌면 다시 만들면 된다는 점도 큽니다. dbt는 ELT의 T를 맡습니다.

ref()와 source()는 왜 중요한가요

테이블 이름을 하드코딩하는 대신 함수로 참조합니다.

-- models/marts/daily_orders.sql
SELECT
    order_date,
    COUNT(*) AS order_count,
    SUM(amount) AS revenue
FROM {{ ref('stg_orders') }}
GROUP BY 1

이렇게 쓰면 세 가지를 얻습니다.

  1. 의존성 그래프가 자동으로 만들어집니다. dbt가 ref를 파싱해 실행 순서를 결정합니다
  2. 환경 분리가 됩니다. 개발 환경에서는 dev_schema.stg_orders, 운영에서는 prod_schema.stg_orders로 컴파일됩니다
  3. 리니지가 공짜로 생깁니다. 어느 모델이 무엇에 의존하는지 문서에 그대로 나옵니다

source()는 dbt가 만들지 않은 원본 테이블을 가리킬 때 씁니다. 이걸 구분해 두면 “여기서부터가 우리 책임”이라는 경계가 명확해집니다.

# models/sources.yml
sources:
  - name: raw
    tables:
      - name: orders
        freshness:
          warn_after: {count: 12, period: hour}
          error_after: {count: 24, period: hour}
        loaded_at_field: ingested_at

머티리얼라이제이션

머티리얼라이제이션 종류를 설명해보세요

모델을 웨어하우스에 어떻게 구현할지 정하는 설정입니다.

타입 동작 적합한 경우
view 뷰 생성 가볍고 자주 안 쓰이는 변환. 저장 비용 0
table 매번 전체 재생성 조회가 잦고 데이터가 크지 않을 때
incremental 새 데이터만 추가·갱신 대용량 팩트 테이블
ephemeral CTE로 인라인, 객체 미생성 중간 단계 정리용

view는 조회할 때마다 원본을 스캔하므로 비용 절감 효과가 없습니다. BigQuery처럼 스캔량 과금인 환경에서 무거운 뷰를 대시보드가 자주 조회하면 요금이 크게 나옵니다.

증분 모델은 어떻게 동작하나요

{{ config(
    materialized='incremental',
    unique_key='order_id',
    incremental_strategy='merge'
) }}

SELECT * FROM {{ source('raw', 'orders') }}

{% if is_incremental() %}
  WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}

is_incremental()이미 테이블이 존재하고 full-refresh가 아닐 때만 참입니다. 첫 실행에서는 전체를 만들고, 이후에는 WHERE 절이 붙어 새 데이터만 처리합니다.

{{ this }}는 현재 모델 자신을 가리킵니다.

증분 모델에서 자주 나는 실수는 무엇인가요

늦게 도착한 데이터를 놓치는 것입니다. MAX(updated_at)보다 이전 시각을 가진 레코드가 나중에 들어오면 영영 반영되지 않습니다.

lookback 윈도우를 두는 것이 일반적인 해법입니다.

{% if is_incremental() %}
  WHERE updated_at > (SELECT DATEADD(day, -3, MAX(updated_at)) FROM {{ this }})
{% endif %}

3일치를 겹쳐 다시 읽되, unique_key로 MERGE하므로 중복이 생기지 않습니다.

또 하나는 삭제를 못 잡는 것입니다. 소스에서 행이 사라져도 증분 로직은 알 수 없습니다. CDC로 삭제 이벤트를 받거나, 주기적으로 full-refresh를 돌려야 합니다.

incremental_strategy에는 어떤 것들이 있나요

  • append — 그냥 추가. 중복 처리 없음. 로그성 데이터
  • mergeunique_key 기준 upsert. 가장 일반적
  • delete+insert — 대상 키를 지우고 삽입
  • insert_overwrite — 파티션 단위로 통째 교체. BigQuery·Spark에서 효율적

insert_overwrite는 파티션이 명확할 때 가장 깔끔합니다. 해당 파티션을 통째로 다시 쓰므로 멱등성이 자연스럽게 보장됩니다.

테스트와 문서

dbt의 테스트 종류를 설명해보세요

일반 테스트(generic test)는 YAML로 선언합니다.

models:
  - name: stg_orders
    columns:
      - name: order_id
        tests:
          - unique
          - not_null
      - name: status
        tests:
          - accepted_values:
              values: ['PAID', 'PENDING', 'REFUNDED']
      - name: customer_id
        tests:
          - relationships:
              to: ref('dim_customers')
              field: customer_id

기본 제공되는 네 가지가 unique, not_null, accepted_values, relationships입니다.

싱글러 테스트(singular test)는 SQL 파일로 씁니다. 행이 반환되면 실패입니다.

-- tests/assert_revenue_positive.sql
SELECT order_id, amount
FROM {{ ref('fct_orders') }}
WHERE amount < 0

테스트 실패를 어떻게 다뤄야 하나요

전부 에러로 두면 파이프라인이 자주 멈춥니다. 심각도를 나눕니다.

tests:
  - unique:
      config:
        severity: error        # 차단
  - not_null:
      config:
        severity: warn         # 경고만
        error_if: ">100"       # 100건 넘으면 에러로 승격

그리고 dbt build를 쓰는 것이 중요합니다. dbt run + dbt test는 모델을 전부 만든 뒤 테스트하지만, dbt build는 모델마다 실행 직후 테스트해서 실패한 모델의 다운스트림을 중단시킵니다.

문서와 리니지는 어떻게 만들어지나요

dbt docs generate
dbt docs serve

모델·컬럼 설명은 YAML에 쓰고, 의존성 그래프는 ref에서 자동으로 만들어집니다. 문서가 코드와 같은 저장소에 있어 리뷰 대상이 된다는 점이 별도 카탈로그 도구와 다른 지점입니다.

persist_docs 설정을 켜면 컬럼 설명이 웨어하우스 메타데이터에도 반영됩니다.

스냅샷과 매크로

스냅샷(snapshot)은 무엇인가요

SCD Type 2를 구현하는 기능입니다. 소스가 현재 상태만 갖고 있을 때, dbt가 주기적으로 확인해 변경 이력을 쌓습니다.

{% snapshot customers_snapshot %}
{{ config(
    target_schema='snapshots',
    unique_key='customer_id',
    strategy='timestamp',
    updated_at='updated_at'
) }}
SELECT * FROM {{ source('raw', 'customers') }}
{% endsnapshot %}

전략이 둘입니다.

  • timestampupdated_at 컬럼이 바뀌면 변경으로 판단. 소스가 이 컬럼을 신뢰성 있게 갱신해야 함
  • check — 지정한 컬럼들의 값을 비교. updated_at이 없을 때

dbt_valid_from / dbt_valid_to 컬럼이 자동으로 붙어 유효 구간을 표현합니다.

스냅샷은 실행 시점에만 변경을 잡습니다. 하루 한 번 돌리면 그 사이 여러 번 바뀐 것은 마지막 상태만 남습니다.

매크로는 언제 쓰나요

Jinja로 작성하는 재사용 함수입니다. 반복되는 SQL 패턴을 묶습니다.

-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name, precision=2) %}
    round({{ column_name }} / 100.0, {{ precision }})
{% endmacro %}
SELECT {{ cents_to_dollars('amount_cents') }} AS amount FROM {{ ref('stg_orders') }}

다만 과하게 쓰면 SQL을 읽을 수 없게 됩니다. dbt 프로젝트가 매크로 미로가 되는 것은 흔한 실패 패턴입니다. dbt compile로 실제 생성되는 SQL을 확인하는 습관이 필요합니다.

구조와 운영

프로젝트 레이어를 어떻게 나누나요

일반적으로 세 계층을 씁니다.

계층 역할 머티리얼라이제이션
staging 소스 1:1 대응. 이름·타입 정리, 가벼운 정제 view
intermediate 재사용되는 중간 변환, 조인 view / ephemeral
marts 비즈니스 단위 팩트·디멘전 table / incremental

staging에서는 조인하지 않는 것이 원칙입니다. 소스와 1:1로 두면 원본 변경에 대응하기 쉽습니다.

실행 대상을 선택하는 방법은 무엇인가요

dbt build --select stg_orders              # 이 모델만
dbt build --select stg_orders+             # 이 모델과 모든 다운스트림
dbt build --select +fct_orders             # 이 모델과 모든 업스트림
dbt build --select tag:daily               # 태그로
dbt build --select state:modified+         # 변경된 모델과 다운스트림

state:modified+가 CI에서 특히 유용합니다. 이전 실행의 manifest와 비교해 바뀐 모델과 그 영향 범위만 돌리므로, PR 검증 시간이 크게 줄어듭니다.

CI/CD는 어떻게 구성하나요

  1. PR이 열리면 임시 스키마에 변경된 모델만 빌드(state:modified+)
  2. 테스트 실행
  3. 성공하면 머지, 임시 스키마 정리
  4. 머지 후 운영 스키마에 반영

핵심은 운영 데이터를 건드리지 않고 검증하는 것입니다. dbt의 target 설정으로 환경별 스키마를 분리합니다.

dbt로 해결되지 않는 것은 무엇인가요

  • 추출·적재 — Fivetran, Airbyte, 자체 파이프라인이 필요합니다
  • 스케줄링 — dbt Cloud를 쓰지 않으면 Airflow 같은 오케스트레이터가 필요합니다
  • 스트리밍 — 배치 변환 도구입니다
  • 웨어하우스 밖의 처리 — 모든 것이 SQL로 웨어하우스에서 실행됩니다

그리고 비용이 웨어하우스로 넘어간다는 점을 알아야 합니다. 무거운 모델을 자주 돌리면 그대로 요금이 됩니다. 증분 모델과 적절한 실행 주기가 중요한 이유입니다.

dbt 프로젝트가 커지면 어떤 문제가 생기나요

  • 실행 시간 증가 — 전체 빌드가 몇 시간이 됩니다. 태그와 선택자로 나눠 돌려야 합니다
  • 모델 수 폭발 — 비슷한 모델이 중복 생성됩니다. 레이어 규칙과 리뷰가 필요합니다
  • 테스트 실행 비용 — 테스트도 쿼리라 웨어하우스 비용이 듭니다
  • 매크로 복잡도 — 추상화가 과해져 디버깅이 어려워집니다

관련 글