한눈에 보기
- 문제: 기술적 지표 계산을 dbt SQL로 옮길 수 있는가. 옮길 수 있다면 실버 레이어 전체를 선언적 SQL로 관리하고 싶었다.
- 원인: EMA 계열 지표는 이전 출력이 다음 입력이 되는 재귀 계산이라 SQL 윈도우 함수로 표현할 수 없다.
- 선택: 지표 계산(실버)은 Python/Polars에 남기고, dbt는 그 결과 위의 분석 마트(Gold)로 재배치했다.
- 결과: dbt-SQL 실버는 부분 지표만 계산하는 반쪽으로 남아 폐기됐다. dbt는 자기가 잘하는 레이어를 찾았다.
- 핵심 교훈: 도구가 쓸모없는 게 아니라 틀린 레이어에 있었던 것이다.
실험: 지표를 SQL로 옮겨보기
실버 레이어(지표가 계산된 데이터)는 처음부터 Python으로 만들고 있었다. dbt를 도입하면서 자연스러운 질문이 생겼다 — 이 계산을 dbt 모델로 옮기면, 지표 정의가 SQL로 선언되고 테스트와 문서가 붙는 관리 가능한 자산이 되지 않을까.
일부는 실제로 됐다. SMA, Bollinger Band, RSI, OBV, CCI처럼 윈도우 함수로 표현 가능한 지표는 dbt SQL로 구현했고, 삼성전자 최근 5거래일에 대해 8개 지표가 기존 Python 구현과 소수점까지 완전히 일치하는 것을 검증했다(2026-07-12). 워밍업 규칙(min_samples=N)도 row_number() >= N 조건으로 재현됐다.
문제는 나머지 절반이었다.
왜 안 되는가: 두 번의 확인
이 블로그에서 지표 수식을 자세히 다루지는 않는다. 필요한 건 한 줄이다. EMA는 EMA_t = α·P_t + (1-α)·EMA_{t-1} — 자신의 직전 출력이 다음 계산의 입력이 되는 지수평활 재귀다. SQL 윈도우 함수(avg, lag, sum, …)는 입력 행들에 대한 함수이지, 자기 출력에 대한 함수가 아니다. MACD와 블루라인 등 EMA를 내장한 지표 전부가 같은 이유로 막힌다.
“그럼 재귀 CTE는?”까지 검토했다. Trino의 WITH RECURSIVE는 experimental이고 기본 재귀 깊이가 짧아, 수천 일 길이의 시계열을 운영 환경에서 계산하는 용도로는 부적합하다고 판단했다. 이 결론은 “Trino SQL 재귀 CTE로 EMA/MACD를 production 구현하지 않는다”는 non-goal로 설계 문서에 명문화해서, 나중에 같은 검토를 반복하지 않게 막아뒀다.
재귀만이 문제가 아니었다. 빅브라더(big_brother) 같은 커스텀 패턴 지표는 전체 시계열을 절차적으로 훑어야 해서 단일 SQL 식으로도, Polars의 단일 Expr로도 표현되지 않는다. 재귀성과 절차성 — SQL 실버가 넘을 수 없는 벽이 두 개였다.
반쪽 실버의 폐기
그 결과 dbt-SQL 실버는 애매한 위치가 됐다. SMA/RSI 부분집합만 계산하는 테이블 — Python 실버는 어차피 전체 지표(60컬럼)를 계산해야 하므로, SQL 실버는 그 부분집합을 한 번 더 계산하는 존재였다. 게다가 이 테이블을 읽는 소비자가 아무도 없다는 것도 확인됐다.
기술적 불가능(재귀/패턴 지표)과 제품상 무소비(소비자 0), 두 근거가 겹쳐 dbt-SQL 실버는 폐기했다. 최종 구조에서 실버는 Python/Polars가 계산해 Iceberg 테이블로 적재한다(19.65M행, 6,541심볼, 60컬럼 — 적재는 Polars→staging→Trino MERGE 경로다).
참고로 Python 실버 쪽은 지표가 늘 때마다 파이프라인 코드를 고치지 않도록 플러그인 레지스트리 구조(데코레이터로 지표 등록, 자동 발견)로 정리해 뒀다. “새 지표 추가 = 파일 하나 추가”가 목표였고, 새 지표마다 과거 데이터를 재작성해야 하는가 하는 질문에는 JSON 스키마리스 저장 가설까지 PoC로 검증했다가 기각한 일도 있었다 — JSON도 결국 모든 행에 키를 채워야 해서 재작성 비용이 고정 컬럼과 같았다. 해법은 포맷이 아니라 파일 분할 구조였다.
dbt의 재배치: 실버가 아니라 골드
여기서 끝났다면 “dbt 도입 실패기”가 됐겠지만, 결론은 반대였다. 폐기 결정 문서에 남긴 문장 그대로 — “dbt는 쓸모없어진 게 아니라 잘못된 레이어에 있었다.”
| 레이어 | 담당 | 이유 |
|---|---|---|
| Silver (지표 계산) | Python/Polars → Iceberg | EMA 재귀·패턴 지표의 절차성은 SQL 불가 |
| Gold (분석 마트) | dbt-SQL (Trino) | 스크리닝·랭킹·시장폭 집계 = window·GROUP BY·filter, SQL의 본령 |
지표가 이미 계산돼 Iceberg에 앉아 있는 상태에서, 그 위의 “오늘 조건을 만족하는 종목”, “섹터별 상승 종목 비율”, “백테스트 피처 테이블” 같은 변환은 순수하게 선언적이다. 재귀도 절차도 없다. 여기가 dbt가 테스트·문서·lineage와 함께 실력을 발휘하는 자리다. 골드 마트 설계는 현재 진행 중이다.
정리
계산의 성질이 도구를 결정했다. 행 간 재귀가 있는 계산은 벡터화된 Python으로, 행 집합에 대한 선언적 변환은 SQL로. 이 경계를 무시하고 한 도구로 밀어붙였다면 — SQL 쪽으로는 experimental 재귀 CTE 위에 프로덕션을 올렸을 것이고, Python 쪽으로는 dbt가 공짜로 주는 테스트·문서·lineage를 직접 재구현하고 있었을 것이다.
“이 도구로 할 수 있는가”보다 좋은 질문은 “이 계산의 성질에 맞는 도구인가”였다.
관련 글: 이 실험이 촉발한 레이크하우스 전환은 사라지는 산출물 - dbt와 Trino Iceberg 레이크하우스, 실버를 받아먹는 실시간 설계는 실시간은 관심종목만 - Spark 없는 람다 아키텍처.