2026년 데이터 분석가를 위한 dbt 완벽 가이드: 모델링, 테스트, 면접 질문

dbt 프로젝트 구조, 머티리얼라이제이션 전략, 데이터 품질 테스트, Jinja 매크로, 그리고 실무 면접에서 자주 등장하는 질문까지 포괄적으로 다룹니다.

dbt data build tool for data analysts modeling testing

데이터 분석가에게 dbt(data build tool)는 2026년 현재 데이터 웨어하우스 변환 작업의 사실상 표준으로 자리잡았습니다. SQL 기반의 모델링, 버전 관리, 자동화된 테스트를 통해 데이터 파이프라인을 소프트웨어 엔지니어링 수준으로 끌어올리는 dbt는 데이터 팀의 필수 도구가 되었습니다. dbt Core v2.0 알파 버전이 공개되면서 성능 개선과 새로운 기능이 추가되었으며, 안정화된 v1.12는 프로덕션 환경에서 널리 사용되고 있습니다. 이 아티클에서는 dbt 프로젝트 구조, 머티리얼라이제이션 전략, 데이터 품질 테스트, 그리고 실무 면접에서 자주 등장하는 질문들을 다룹니다.

dbt는 ELT 파이프라인의 'T'를 담당합니다

전통적인 ETL과 달리, 현대 데이터 스택은 ELT(Extract, Load, Transform) 패턴을 따릅니다. Fivetran이나 Airbyte 같은 도구가 원시 데이터를 데이터 웨어하우스(Snowflake, BigQuery, Redshift)로 로드하면, dbt가 SQL을 사용해 데이터를 변환하고 비즈니스 로직을 적용합니다. 이를 통해 분석가는 Python이나 복잡한 스크립트 없이 순수 SQL만으로 데이터 파이프라인을 구축할 수 있습니다.

dbt 프로젝트 구조: Staging, Intermediate, Marts

dbt 프로젝트는 일반적으로 세 가지 계층으로 구성됩니다. Staging 모델은 원시 소스 데이터를 정제하고 이름을 표준화하는 첫 번째 단계입니다. 컬럼명을 일관되게 변경하고, 데이터 타입을 변환하며, 명백히 잘못된 레코드를 필터링합니다. 모든 staging 모델은 stg_ 접두사를 사용하며, stg_<source>__<entity> 네이밍 컨벤션을 따릅니다.

sql
-- models/staging/stripe/stg_stripe__payments.sql
with source as (
    select * from {{ source('stripe', 'payments') }}
),

renamed as (
    select
        id as payment_id,
        amount / 100.0 as amount_usd,  -- Stripe stores cents
        status as payment_status,
        created::timestamp as created_at,
        customer_id
    from source
    where status != 'failed'  -- Filter invalid records early
)

select * from renamed

Intermediate 모델은 staging과 marts 사이의 중간 계층으로, 복잡한 비즈니스 로직을 캡슐화하지만 최종 사용자에게 직접 노출되지 않습니다. 여러 staging 테이블을 조인하거나, 복잡한 계산을 수행하며, int_ 접두사를 사용합니다.

Marts 모델은 비즈니스 사용자나 BI 도구가 직접 쿼리하는 최종 계층입니다. 특정 비즈니스 도메인(finance, product, marketing)으로 구성되며, fct_(팩트 테이블) 또는 dim_(디멘션 테이블) 접두사를 사용합니다.

sql
-- models/marts/finance/fct_monthly_revenue.sql
with payments as (
    select * from {{ ref('stg_stripe__payments') }}
),

monthly as (
    select
        date_trunc('month', created_at) as revenue_month,
        count(*) as total_transactions,
        sum(amount_usd) as gross_revenue,
        sum(case when payment_status = 'refunded' then amount_usd else 0 end) as refunds
    from payments
    group by 1
)

select
    revenue_month,
    total_transactions,
    gross_revenue,
    refunds,
    gross_revenue - refunds as net_revenue  -- Key business metric
from monthly

이러한 계층 구조는 코드 재사용성을 높이고, 데이터 리니지(lineage)를 명확하게 하며, 각 모델의 책임을 분리합니다. Staging 모델은 단순한 1:1 변환만 수행하고, 복잡한 로직은 intermediate나 marts에서 처리하는 것이 원칙입니다.

머티리얼라이제이션 전략: View, Table, Incremental, Ephemeral

dbt는 네 가지 머티리얼라이제이션 방식을 제공하며, 각 모델의 크기와 쿼리 패턴에 따라 적절한 전략을 선택해야 합니다.

| 타입 | 특징 | 사용 사례 | 성능 | |------|------|-----------|------| | view | 쿼리 시점에 실행 | Staging 모델, 작은 변환 | 빠른 빌드, 느린 쿼리 | | table | 전체 재생성 (DROP + CREATE) | Marts, 자주 쿼리되는 모델 | 느린 빌드, 빠른 쿼리 | | incremental | 신규/변경 데이터만 추가 | 대용량 팩트 테이블, 이벤트 로그 | 빠른 빌드, 빠른 쿼리 | | ephemeral | CTE로만 존재 (DB에 생성 안 됨) | 중간 계산, 임시 변환 | 가장 빠른 빌드 |

Incremental 모델은 dbt의 핵심 기능 중 하나로, 수백만 건의 로우를 가진 테이블을 효율적으로 관리할 수 있게 해줍니다. 첫 실행 시에는 전체 테이블을 생성하고, 이후 실행에서는 마지막 실행 이후의 신규 데이터만 처리합니다.

sql
-- models/marts/product/fct_page_views.sql
{{ config(
    materialized='incremental',
    unique_key='page_view_id',
    incremental_strategy='merge'
) }}

with events as (
    select
        event_id as page_view_id,
        user_id,
        page_url,
        session_id,
        event_timestamp
    from {{ ref('stg_snowplow__events') }}
    where event_type = 'page_view'

    {% if is_incremental() %}
        -- Only process new events since last run
        and event_timestamp > (select max(event_timestamp) from {{ this }})
    {% endif %}
)

select * from events

is_incremental() 매크로는 현재 모델이 이미 존재하는지 확인하고, {{ this }}는 현재 빌드 중인 테이블을 참조합니다. unique_key를 지정하면 중복 레코드를 자동으로 처리하며, incremental_strategy는 데이터 웨어하우스별로 다른 전략(merge, delete+insert, append)을 사용할 수 있습니다.

Staging 모델은 일반적으로 view로 설정하고, marts는 table 또는 incremental로 설정하는 것이 일반적입니다. 소규모 프로젝트에서는 모든 것을 view로 시작하고, 쿼리 성능이 문제가 될 때 table로 전환하는 것이 좋습니다.

데이터 품질 테스트: Schema Tests와 Data Tests

dbt는 데이터 파이프라인에 자동화된 테스트를 통합하여, 코드 변경이나 소스 데이터 문제로 인한 데이터 품질 저하를 사전에 방지합니다. Schema tests는 YAML 파일에 선언적으로 정의하며, unique, not_null, accepted_values, relationships 네 가지 기본 테스트를 제공합니다.

yaml
# models/staging/stripe/_stripe__models.yml
version: 2

models:
  - name: stg_stripe__payments
    description: "Cleaned payment records from Stripe"
    columns:
      - name: payment_id
        description: "Unique payment identifier"
        tests:
          - unique
          - not_null
      - name: payment_status
        tests:
          - accepted_values:
              values: ['succeeded', 'pending', 'refunded']
      - name: amount_usd
        tests:
          - not_null
          - dbt_utils.expression_is_true:
              expression: ">= 0"  # No negative payments

dbt_utils 패키지는 추가적인 테스트를 제공하며, expression_is_true는 커스텀 조건을 검증할 수 있게 해줍니다. relationships 테스트는 외래 키 제약을 검증하여, 참조 무결성을 보장합니다.

Data tests(또는 singular tests)는 SQL 파일로 작성하며, 복잡한 비즈니스 로직을 검증할 수 있습니다. 테스트는 문제가 있는 레코드를 반환하는 쿼리로 작성하며, 결과가 0개면 테스트 통과입니다.

sql
-- tests/assert_revenue_not_negative.sql
-- This test fails if any month has negative net revenue
select
    revenue_month,
    net_revenue
from {{ ref('fct_monthly_revenue') }}
where net_revenue < 0  -- Should never happen

dbt test 명령어는 모든 테스트를 실행하고, CI/CD 파이프라인에 통합하여 프로덕션 배포 전에 데이터 품질을 검증할 수 있습니다. 테스트 실패는 슬랙이나 이메일로 알림을 받을 수 있으며, 데이터 팀이 문제를 즉시 인지하고 대응할 수 있게 합니다.

Data Analytics 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

Jinja와 매크로: 재사용 가능한 SQL 로직

dbt는 Jinja 템플릿 엔진을 사용하여 SQL에 프로그래밍 로직을 추가합니다. 변수, 조건문, 반복문을 사용할 수 있으며, **매크로(macro)**를 통해 재사용 가능한 SQL 함수를 정의할 수 있습니다.

sql
-- macros/cents_to_dollars.sql
{% macro cents_to_dollars(column_name) %}
    ({{ column_name }} / 100.0)::numeric(12, 2)
{% endmacro %}

매크로를 정의하면 모든 모델에서 {{ cents_to_dollars('column_name') }} 형태로 호출할 수 있습니다. 이는 DRY(Don't Repeat Yourself) 원칙을 따르며, 비즈니스 로직 변경 시 한 곳만 수정하면 됩니다.

sql
-- Usage in any model
select
    payment_id,
    {{ cents_to_dollars('amount_cents') }} as amount_usd,
    {{ cents_to_dollars('tax_cents') }} as tax_usd
from {{ source('stripe', 'payments') }}

Jinja의 강력한 기능 중 하나는 환경 변수를 사용한 동적 쿼리 생성입니다. {% if target.name == 'prod' %}를 사용해 개발 환경과 프로덕션 환경에서 다른 로직을 실행할 수 있으며, {{ var('start_date') }}로 외부 변수를 주입할 수 있습니다.

dbt의 내장 매크로인 ref()source()는 데이터 리니지를 추적하고, 모델 간 의존성을 자동으로 관리합니다. ref('model_name')은 다른 dbt 모델을 참조하고, source('source_name', 'table_name')은 원시 소스 테이블을 참조합니다. 이를 통해 dbt는 DAG(Directed Acyclic Graph)를 생성하여 올바른 순서로 모델을 빌드합니다.

dbt 면접 질문: 실무에서 자주 묻는 5가지

Q1: ref()source()의 차이점은 무엇이며, 각각 언제 사용해야 합니까?

source()는 dbt 외부에서 관리되는 원시 데이터 테이블을 참조할 때 사용합니다. 일반적으로 ETL 도구(Fivetran, Airbyte)가 로드한 테이블이며, models/<source_name>/_<source_name>__sources.yml에 정의됩니다. ref()는 dbt가 관리하는 다른 모델을 참조하며, dbt가 자동으로 의존성 순서를 결정하고 빌드합니다.

핵심 차이는 데이터 리니지와 freshness 체크입니다. source()는 소스 데이터의 최신성을 검증할 수 있는 freshness 테스트를 지원하지만, ref()는 dbt 모델 간 참조이므로 freshness가 의미 없습니다. Staging 모델은 항상 source()를 사용하고, 그 이후 모든 계층은 ref()를 사용하는 것이 원칙입니다.

Q2: Incremental 모델의 작동 원리와 unique_key의 역할을 설명하세요.

Incremental 모델은 첫 실행 시 전체 테이블을 생성(full refresh)하고, 이후 실행에서는 is_incremental() 조건 내부의 필터링을 통해 신규 또는 변경된 데이터만 처리합니다. 일반적으로 타임스탬프 컬럼을 기준으로 where created_at > (select max(created_at) from {{ this }})와 같은 조건을 사용합니다.

unique_key는 중복 레코드 처리 방식을 결정합니다. 지정하지 않으면 단순히 새 로우를 append하지만, unique_key='id'로 지정하면 동일한 키를 가진 기존 레코드를 업데이트(merge 전략) 또는 삭제 후 재삽입(delete+insert 전략)합니다. 이는 늦게 도착하는 데이터(late-arriving data)나 업데이트된 레코드를 올바르게 처리하는 데 필수적입니다.

성능 최적화를 위해 파티셔닝된 테이블의 경우 partitions 설정을 사용하고, Snowflake에서는 merge 전략이, BigQuery에서는 insert_overwrite 전략이 더 효율적일 수 있습니다.

Q3: SCD(Slowly Changing Dimension) Type 2를 dbt로 구현하는 방법은?

SCD Type 2는 차원 테이블의 변경 이력을 모두 보관하는 패턴으로, valid_from, valid_to, is_current 컬럼을 사용합니다. dbt의 snapshot 기능을 사용하면 선언적으로 SCD Type 2를 구현할 수 있습니다.

snapshots/ 디렉토리에 스냅샷 정의 파일을 생성하고, timestamp 또는 check 전략을 선택합니다. Timestamp 전략은 소스 테이블의 updated_at 컬럼을 사용하여 변경을 감지하고, check 전략은 지정한 컬럼들의 값을 비교하여 변경을 감지합니다.

dbt snapshot 명령어를 실행하면 dbt는 현재 소스 데이터와 스냅샷 테이블을 비교하여, 변경된 레코드는 dbt_valid_to를 업데이트하고 새 버전을 삽입합니다. 이를 통해 특정 시점의 데이터를 쿼리하거나, 변경 이력을 분석할 수 있습니다.

Q4: Staging, Intermediate, Marts 계층 구조의 목적과 각 계층에서 수행해야 할 작업은?

Staging은 소스 데이터의 1:1 표현으로, 원시 테이블과 동일한 그레인(grain)을 유지합니다. 수행 작업은 컬럼명 표준화, 데이터 타입 캐스팅, 명백한 불량 레코드 필터링입니다. 비즈니스 로직이나 조인은 금지되며, 항상 view로 머티리얼라이즈합니다.

Intermediate는 재사용 가능한 로직 모듈로, staging 모델들을 조인하거나 복잡한 계산을 수행하지만 최종 사용자에게 노출되지 않습니다. 예를 들어 int_customers_with_lifetime_value는 주문 데이터와 고객 데이터를 조인하여 LTV를 계산하지만, 여러 marts 모델에서 재사용됩니다.

Marts는 비즈니스 도메인별로 구성된 최종 계층으로, BI 도구나 애널리스트가 직접 쿼리합니다. marts/finance/, marts/product/, marts/marketing/처럼 도메인으로 폴더를 나누고, 각 도메인의 핵심 메트릭과 디멘션을 제공합니다. Table이나 incremental로 머티리얼라이즈하여 쿼리 성능을 최적화합니다.

Q5: Schema tests와 data tests(singular tests)의 차이점과 각각의 사용 사례는?

Schema tests는 YAML 파일에 선언적으로 정의하며, 컬럼 레벨의 제약 조건을 검증합니다. unique, not_null, accepted_values, relationships는 가장 일반적인 테스트이며, dbt_utils나 dbt_expectations 패키지를 통해 확장할 수 있습니다. 각 컬럼에 대해 독립적으로 실행되며, 테스트 실패 시 어떤 컬럼이 문제인지 즉시 식별할 수 있습니다.

Data tests는 SQL 파일로 작성하며, 복잡한 다중 컬럼 제약이나 비즈니스 룰을 검증합니다. 예를 들어 "총 매출이 환불보다 항상 커야 한다", "활성 구독이 있는 고객은 결제 수단이 있어야 한다" 같은 룰은 data test로만 표현 가능합니다. 테스트는 위반 레코드를 반환하는 SELECT 쿼리로 작성하며, 결과가 비어있으면 통과입니다.

실무에서는 간단한 제약은 schema tests로, 복잡한 비즈니스 룰은 data tests로 구현합니다. 모든 primary key는 uniquenot_null 테스트를 추가하고, 외래 키는 relationships 테스트로 참조 무결성을 보장하는 것이 베스트 프랙티스입니다.

프로덕션 환경의 dbt 베스트 프랙티스

실무 환경에서 dbt를 안정적으로 운영하려면 몇 가지 핵심 원칙을 따라야 합니다.

1. Source freshness 검증: 소스 데이터가 예상 시간 내에 업데이트되는지 확인합니다. ETL 파이프라인 지연이나 장애를 조기에 감지할 수 있습니다.

yaml
# models/staging/stripe/_stripe__sources.yml
version: 2

sources:
  - name: stripe
    database: raw
    schema: stripe
    freshness:
      warn_after: {count: 12, period: hour}
      error_after: {count: 24, period: hour}
    loaded_at_field: _fivetran_synced
    tables:
      - name: payments
      - name: customers

dbt source freshness 명령어를 스케줄러에서 주기적으로 실행하고, 실패 시 알림을 받도록 설정합니다. warn_after는 경고만 발생시키고, error_after는 빌드를 실패시킵니다.

2. CI/CD 파이프라인 통합: Pull request마다 dbt build --select state:modified+ 명령어로 변경된 모델과 하위 의존성만 빌드하고 테스트합니다. dbt Cloud의 Slim CI 기능이나 GitHub Actions를 사용할 수 있습니다.

3. 환경 분리: profiles.yml에서 dev, staging, prod 환경을 분리하고, 각각 다른 스키마와 데이터베이스를 사용합니다. 개발자는 dev 환경에서 자유롭게 실험하고, prod는 승인된 코드만 배포합니다.

4. 문서화 자동화: dbt docs generate && dbt docs serve 명령어로 데이터 카탈로그를 생성하고, 내부 포털에 호스팅합니다. 모든 모델과 컬럼에 description을 추가하여, 비기술 팀원도 데이터를 이해할 수 있게 합니다.

5. 성능 모니터링: dbt Cloud의 실행 로그나 dbt run --log-level debug 출력을 분석하여, 느린 모델을 식별하고 최적화합니다. Incremental 전환, 파티셔닝, 인덱싱을 통해 대용량 테이블의 빌드 시간을 단축합니다.

데이터 엔지니어링 직무에서는 Airflow와 dbt를 통합하여 오케스트레이션하는 경우가 많으며, 데이터 애널리틱스 직무에서는 dbt로 데이터를 모델링하고 Tableau나 Looker로 시각화합니다. SQL 윈도우 함수와 CTE를 능숙하게 다루는 것은 dbt 모델 작성의 기초이며, 고급 SQL 면접 준비를 통해 복잡한 분석 쿼리를 최적화하는 능력을 키울 수 있습니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

결론: dbt로 데이터 파이프라인을 소프트웨어처럼 관리하기

dbt는 데이터 분석가가 SQL만으로 엔터프라이즈급 데이터 파이프라인을 구축할 수 있게 해주는 강력한 도구입니다. 2026년 현재 대부분의 데이터 팀은 dbt를 핵심 인프라로 채택했으며, 면접에서도 dbt 경험을 필수 또는 우대 요건으로 요구하는 경우가 많아졌습니다.

핵심 요약:

  • 계층화된 모델링: Staging(정제) → Intermediate(로직 모듈) → Marts(비즈니스 메트릭) 구조로 코드 재사용성과 유지보수성을 높입니다.
  • 적절한 머티리얼라이제이션: Staging은 view, marts는 table/incremental로 설정하여 빌드 시간과 쿼리 성능의 균형을 맞춥니다.
  • 자동화된 테스트: Schema tests와 data tests를 통해 데이터 품질을 보장하고, 프로덕션 장애를 사전에 방지합니다.
  • Jinja와 매크로: 재사용 가능한 SQL 로직을 작성하고, DRY 원칙을 따릅니다.
  • 프로덕션 베스트 프랙티스: Source freshness 체크, CI/CD 통합, 환경 분리, 문서화를 통해 안정적인 데이터 파이프라인을 운영합니다.

dbt의 핵심은 단순히 SQL 변환 도구가 아니라, 데이터 파이프라인에 소프트웨어 엔지니어링의 베스트 프랙티스(버전 관리, 테스트, 문서화, 모듈화)를 적용하는 철학입니다. dbt를 마스터하면 데이터 분석가로서의 가치를 크게 높일 수 있으며, 데이터 엔지니어링 역할로의 커리어 전환도 가능해집니다.

연습을 시작하세요!

면접 시뮬레이터와 기술 테스트로 지식을 테스트하세요.

태그

#dbt
#data-analytics
#sql
#data-modeling
#interview

공유

관련 기사