Jinja와 매크로로 SQL을 프로그래밍하기

Jinja와 매크로로 SQL을 프로그래밍하기

SQL만으로는 표현하기 어려운 로직이 프로젝트에 생길 때가 있어요. 예를 들어 똑같은 집계를 결제 수단 개수만큼 반복해서 쓴다거나, 특정 조건일 때만 다른 쿼리를 만들고 싶다면요. dbt에서는 Jinja라는 템플릿 언어를 SQL과 함께 써서 이 문제를 풀어요. Jinja는 그 자체로 프로그래밍 언어는 아니지만, dbt 프로젝트 안에서 SQL의 능력을 크게 확장해 주는 도구예요.

출처: dbt 공식 문서 — Jinja and macros

Jinja로 무엇을 할 수 있을까

Jinja를 쓰면 SQL에서는 평소 하기 어려운 일들을 할 수 있어요.

  • SQL 안에서 제어 구조 사용하기 — 예를 들어 if 문이나 for 루프
  • 운영 배포를 위해 환경 변수 쓰기
  • 현재 타깃에 따라 프로젝트를 다르게 빌드하기
  • 한 쿼리의 결과로 다른 쿼리를 생성하기 — 예를 들어 결제 수단 목록을 뽑아서 수단별 소계 컬럼(pivot)을 만들기
  • 자주 쓰는 SQL 조각을 재사용 가능한 **매크로(macros)**로 추상화하기 — 대부분의 프로그래밍 언어에서 함수와 비슷한 개념이에요

사실 {{ ref() }} 함수를 써 봤다면 이미 Jinja를 쓰고 있는 거예요! Jinja는 모델, analyses, 데이터 테스트, 심지어 후크(hooks)까지 dbt 프로젝트의 모든 SQL에서 쓸 수 있어요.

Jinja를 쓰는 dbt 모델의 예시를 볼게요.

{% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}

select
    order_id,
    {% for payment_method in payment_methods %}
    sum(case when payment_method = '{{payment_method}}' then amount end) as {{payment_method}}_amount,
    {% endfor %}
    sum(amount) as total_amount
from app_data.payments
group by 1

이 쿼리는 컴파일되면 이렇게 돼요.

select
    order_id,
    sum(case when payment_method = 'bank_transfer' then amount end) as bank_transfer_amount,
    sum(case when payment_method = 'credit_card' then amount end) as credit_card_amount,
    sum(case when payment_method = 'gift_card' then amount end) as gift_card_amount,
    sum(amount) as total_amount
from app_data.payments
group by 1

Jinja 구분자 세 가지

Jinja는 언어가 쓰는 구분자, dbt에서는 "컬리(curlies)"라고 부르는 표기로 알아볼 수 있어요.

  • 표현식 {{ ... }}: 문자열을 출력하고 싶을 때 써요. 변수를 참조하거나 매크로를 호출할 때 써요.
  • 문장 {% ... %}: 문자열을 출력하지 않아요. 제어 흐름(예: for 루프, if 문 설정), 변수 설정·수정, 매크로 정의에 써요.
  • 주석 {# ... #}: 주석 안의 텍스트가 실행되거나 출력되지 않게 막아요. SQL 주석인 --는 쓰지 말아요.

dbt 모델에서는 Jinja가 반드시 유효한 쿼리로 컴파일돼야 해요. 컴파일 결과를 확인하려면 dbt Cloud라면 compile 버튼을 눌러 Compiled SQL 패널을 보면 되고, dbt Core라면 커맨드라인에서 dbt compile을 실행한 뒤 target/compiled/{프로젝트명}/ 디렉터리의 컴파일된 SQL 파일을 열면 돼요. 코드 에디터에서 원본과 컴파일 결과를 나란히 열어 두면 좋아요.

매크로 정의하고 사용하기

**매크로(macros)**는 여러 번 재사용할 수 있는 코드 조각이에요. 다른 언어의 "함수"에 해당하고, 여러 모델에서 같은 코드를 반복하게 되면 매우 유용해요. 매크로는 .sql 파일에 정의하며, 보통 macros 디렉터리에 둬요. 매크로 파일에는 하나 이상의 매크로가 들어갈 수 있고, 예시는 이래요.


{% macro cents_to_dollars(column_name, scale=2) %}
    ({{ column_name }} / 100)::numeric(16, {{ scale }})
{% endmacro %}

이 매크로를 쓰는 모델은 이렇게 생길 수 있어요.

select
  id as payment_id,
  {{ cents_to_dollars('amount') }} as amount_usd,
  ...
from app_data.payments

이 모델은 컴파일되면 이렇게 돼요.

select
  id as payment_id,
  (amount / 100)::numeric(16, 2) as amount_usd,
  ...
from app_data.payments

패키지에서 매크로 가져다 쓰기

유용한 매크로 중 상당수는 패키지(packages)로 묶여 공개돼 있어요. 가장 인기 있는 패키지는 dbt-utils예요. 패키지를 프로젝트에 설치하면 패키지 이름을 앞에 붙여 매크로를 호출할 수 있어요.


select
  field_1,
  field_2,
  field_3,
  field_4,
  field_5,
  count(*)
from my_table
{{ dbt_utils.dimensions(5) }}

내 프로젝트 안의 매크로도 패키지 이름을 앞에 붙여 한정(qualify)할 수 있는데, 이 특징은 주로 패키지 작성자에게 유용해요.

dbtonic한 Jinja 사용법

Pythonic이라는 말이 있듯, 잘 짜인 dbt 코드는 dbtonic이라고 불러요. 몇 가지 권장 사항을 볼게요.

가독성을 재사용성보다 우선하기

Jinja의 힘을 배우고 나면 반복되는 줄마다 매크로로 추상화하고 싶어지는 게 자연스러워요. 하지만 Jinja를 쓰면 모델을 다른 사람이 해석하기 더 어려워질 수 있어요. 몇 곳에서 SQL 줄을 반복하더라도 가독성을 우선하는 걸 권장해요. 모든 모델이 매크로로만 만들어져 있다면 한 번쯤 다시 점검해 볼 필요가 있어요.

패키지 매크로 활용하기

매크로를 처음 써 보려고 한다면, dbt-utils에 이미 공개된 매크로가 없는지 먼저 확인해 보세요. 시간을 크게 아낄 수 있어요.

변수는 모델 맨 위에서 정의하기

{% set ... %}는 새 변수를 만들거나 기존 변수를 갱신해요. 변수는 모델 안 곳곳에 하드코딩하기보다 맨 위에 모아서 정의하는 걸 권장해요. 다른 언어에서도 널리 쓰는 관례이고, 두 곳에서 같은 변수를 참조해야 할 때 특히 유용하죠.

-- 🙅 This works, but can be hard to maintain as your code grows
{% for payment_method in ["bank_transfer", "credit_card", "gift_card"] %}
...
{% endfor %}


-- ✅ This is our preferred method of setting variables
{% set payment_methods = ["bank_transfer", "credit_card", "gift_card"] %}

{% for payment_method in payment_methods %}
...
{% endfor %}

더 알아보기