실행은 도메인별로, 문맥은 한 레포에
요약
본 글은 중앙 집중식 DW Airflow 구조의 문제점을 지적하고, 이를 도메인별 독립 파이프라인으로 분리하는 과정을 공유합니다. Strangler Fig 패턴을 적용하여 기존 시스템에 영향을 주지 않으면서 점진적으로 새로운 플랫폼(뉴타운)을 구축했습니다.
핵심 포인트
- 중앙 집중식 Airflow는 배치가 얽히고 유지보수가 어려움.
- Strangler Fig 패턴으로 안전하게 마이그레이션 진행 가능.
- dbt 기반 선언적 파이프라인으로 코드 복잡도와 실행 시간이 크게 개선됨.
들어가며
안녕하세요, 토스 Data Warehouse 팀의 Data Analytics Engineer 김문수입니다.
이 글에서는 데이터 웨어하우스(DW) 마트를 만드는 중앙 DW Airflow를 멈추지 않고, 도메인별 DW Airflow와 dbt 기반의 선언적 파이프라인으로 옮겨 온 과정을 공유하려고 합니다. 새 플랫폼은 팀 안에서 이를 ‘뉴타운’이라고 부르고 있습니다.
얼마 전 코딩 에이전트에게 "이 테이블 어디서 만들어져?"라고 물었습니다. 한참 코드를 뒤지던 에이전트의 답은 이랬습니다. "적재 주체는 코드에 없어 확인할 수 없습니다." 맞는 말입니다. 정말로 코드에 없었기 때문이죠.
이 글은 그 답을 코드에 적어 넣기까지의 이야기입니다. 그 과정에서 토스 광고 도메인의 DAG 코드는 2,794줄에서 100줄이 되었고, 테이블 순서를 손으로 잇던 >> 180줄은 0줄이 되었습니다. 파이프라인 시간도 4시간에서 2시간 30분으로 줄었습니다.
DW Airflow 하나에 DAG 400여 개
토스에는 용도와 팀에 따라 운영하는 Airflow가 여럿 있습니다. 이 글에서 다루는 건 그중 DW 마트를 만드는 DW Airflow입니다. DW 마트에는 광고, 쇼핑, 결제, 금융 등 도메인 팀이 각자 관리하는 도메인 마트와, 중앙 DW팀이 관리하는 마트가 있습니다. 중앙 DW팀은 전사 공통 마트뿐 아니라, 어느 도메인에도 딱 떨어지지 않아 주인이 애매한 마트까지 맡고 있습니다. 그런데 이 마트 파이프라인이 도메인과 상관없이 전부 DW Airflow 클러스터 하나에서 돌고 있었습니다. DAG 파일로 세면 400개가 넘습니다.
한 클러스터에 모여 있다 보니 문제가 몇 가지 있었습니다. 한 도메인의 배치가 몰리면 다른 도메인의 배치까지 밀렸고, 파이프라인 하나를 고치려면 다른 도메인이 어떻게 돌고 있는지까지 알아야 했습니다. 비슷한 로직도 표준 없이 여기저기 복사돼 있었습니다.
매일 겪는 불편은 조금 더 구체적이었습니다. 테이블 하나를 이해하려면 다섯 군데를 열어야 했습니다.
- 어느 DAG에서 어떤 순서로 도는지: DAG 파일
- 어떻게 만드는지: SQL 파일
- 품질 규칙: 별도 설정 파일이나 품질 관리 시스템
- 테이블과 컬럼 설명: 사내 데이터 카탈로그
- 읽는 테이블이 어디서 만들어지는지: 원천 DB에서 적재한 테이블이면 적재 메타 관리 시스템, 다른 마트면 영향도 시스템 등 여러 사내 시스템
고쳐 쓰지 않고 옆에 새로 짓기로 했습니다
처음에는 기존 레포를 리팩토링하는 방법도 생각했습니다. 그런데 문제의 뿌리가 "클러스터 하나에 다 모여 있다"는 구조 자체라서, 코드를 아무리 정리해도 이 부분은 해결이 안 됩니다. 그렇다고 전사 배치를 한 번에 새 플랫폼으로 옮기는 건 너무 위험합니다. 문제가 생기면 대응하기가 꽤 까다로워요.
돌이킬 수 있으면서 간편한 방식을 원했습니다. 그래서 기존 시스템 옆에 새 시스템을 짓고, 기능을 하나씩 옮긴 뒤 옮긴 부분만 기존 시스템에서 걷어내는 방식을 택했습니다.
이걸 부르는 이름이 있을 것 같아 찾아보니 Strangler Fig 방식이라고 하더라고요. 교살 무화과나무가 숙주 나무를 감싸며 자라다가 결국 기존 나무를 대체하는 자연현상에서 유래했다고 합니다. (이름이 무서운 나무네요.) 아무튼 기존 파이프라인은 마지막 순간까지 그대로 돌고, 옮기다 문제가 생기면 언제든 되돌릴 수 있습니다. 옮기는 단위를 도메인으로 잡았기 때문에, 언제, 얼마나 옮길지는 도메인 팀이 정할 수 있었습니다.
같은 데이터를 하루에 두 번 만들었습니다
플랫폼이 어느정도 만들어졌을 때, 적당한 마이그레이션 대상을 찾고 있었는데요. 마침 찾은 이 마트 DAG은 주인이 따로 없고, 마지막 변경이 1년이 넘었고, TASK의 수도 적당했습니다.
7월 말에 이 마트를 옮겨 둔 도메인의 main DAG에 13시 대기 sensor와 이 마트의 트리거를 추가했습니다. 기존 파이프라인은 평소처럼 새벽에 돌고, 뉴타운은 같은 날 13시에 같은 데이터를 한 번 더 만듭니다. 같은 데이터를 하루에 두 번 만드는 건 낭비 같지만, 가장 싼 보험이었습니다. 이렇게 3주 동안 두 결과를 비교했습니다.
8월 20일에 검증이 끝났다고 보고, 13시 대기를 빼서 새벽에 돌게 하는 PR을 머지했습니다. 그런데 같은 날 저녁 revert했습니다. 문제가 생긴 건 아니고, 기존 파이프라인 쪽 준비가 더 필요했기 때문입니다. 기존 DW Airflow에는 테이블간의 audit 기반으로 만든 영향도를 보고 앞 단계를 기다리는 sensor가 있어서, 이 마트를 뉴타운 쪽 결과로 바라보게 바꾸는 작업을 따로 해야 했습니다.
그래서 다음 주로 미뤘습니다. revert 커밋 메시지는 ‘다음주에 하는걸로’ 한 줄이었고, 실제로 8월 24일에 다시 머지했습니다. 그동안 기존 파이프라인은 계속 새벽에 돌고 있었으니 데이터를 쓰는 쪽에서는 아무 일도 없었습니다. 한 번에 옮기는 방식이었다면 이 일정 조정은 꽤 큰 롤백 작업이 됐을 겁니다. 광고 도메인을 옮길 때도 같은 방식으로 두 파이프라인을 나란히 돌리며 결과를 비교한 뒤 넘겼습니다.
반대로 하지 않기로 한 것도 있습니다. Spark 잡은 dbt로 바꾸지 않았고, Jenkins 잡도 당장 없애지 않고 이어 쓰되 새로 만드는 것만 막았습니다. 다 할 수는 있는 일이지만, 전부 새 방식으로 바꾸려고 하면 이주가 늦어지고 두 플랫폼을 같이 운영하는 기간만 길어집니다. 같은 이유로 기존의 모델 레이어 구성 방식을 바꾸는 것도 이관 이후에 하는 걸 권장했어요.
도메인별 클러스터와 dbt
새 플랫폼에서는 DW Airflow를 도메인별로 나눴습니다. 도메인마다 자기 DW Airflow 클러스터를 갖는 구조로, 지금은 ads, foundation, finops, fmd, dw-core까지 5개 클러스터가 있습니다. 다만 기존 파이프라인을 대규모로 옮겨 온 건 아직 광고(ads) 한 곳이고, 나머지는 새로 만드는 마트나 옮기기 위한 준비 위주로 쓰이고 있습니다. 도메인 팀이 자기 파이프라인을 책임지려면, 다른 도메인의 배포나 장애에 흔들리지 않는 실행 환경이 있어야 한다고 봤습니다. 코드는 레포 하나에 두고, 공통 모듈은 플랫폼팀이, 도메인 디렉토리는 각 도메인 팀이 관리합니다. 물론 공통 모듈에 기여하는 건 누구나 할 수 있습니다.
변환 로직은 Impala에 직접 날리던 쿼리에서 dbt 모델로 옮겼습니다. dbt는 SELECT 문만 적으면 테이블 생성과 적재를 대신 해 주고, 모델 사이의 의존 관계를 그래프로 관리해 주는 도구입니다. dbt 모델을 Airflow task로 펼치는 데는 흔히 쓰는 Cosmos를 썼고, 모델 하나가 task 하나가 됩니다. dbt만으로는 Spark 잡이나 다른 클러스터의 테이블을 기다리는 sensor를 표현할 수 없어서, 선언만 해 두면 이런 task와 의존성을 자동으로 만들어 주는 wrapper를 따로 만들었습니다. 이 wrapper가 실제로 무엇을 읽고 무엇을 만드는지는 아래 ‘나머지는 yml에’ 문단에서 보여 드리겠습니다.
클러스터를 나누니 새로운 문제도 생겼습니다. 클러스터 하나일 때는 손쉽게 해결됐던 DAG 간 의존이, 클러스터를 건너는 의존이 된 것입니다. 이건 두 가지로 풀었습니다. 도메인마다 스케줄을 가진 main DAG를 하나만 두고 나머지 DAG는 main이 trigger하게 해서, 모두 같은 날짜를 공유하게 했습니다. 기존 DW Airflow에서도 쓰던 패턴이라 팀원들이 익숙하게 받아들일 수 있었습니다. 그리고 다른 곳에서 만드는 테이블은 만들어지는 방식에 따라 기다리는 방법을 나눴습니다. 원천 DB에서 적재하는 테이블은 적재 상태가 적재 시스템의 백엔드 서버에 메타로 기록되고 있어서, ‘오늘 적재됐는가’를 그 기록에서 직접 확인할 수 있었습니다. 기존 DW Airflow에서 만드는 마트는 그 테이블을 만드는 Airflow task가 끝났는지를 봅니다.
SQL에는 로직만 남겼습니다
기존 SQL과 옮긴 뒤의 SQL을 간단히 비교하면 이렇습니다. (예시를 위해 단순화했습니다.)
insert overwrite 같은 적재 구문은 사라지고, 읽는 테이블 이름이 ref()로 바뀝니다. 비즈니스 로직은 그대로입니다. 대신 이 ref()가 곧 실행 순서가 됩니다. Cosmos가 ref 관계를 읽어서 task 순서를 이어 주기 때문에, 모델을 새로 추가해도 DAG 파일은 손대지 않습니다. 광고 도메인의 >> 180줄이 0줄이 된 이유가 이것입니다. SQL 파일 수는 거의 그대로인데 DAG 코드가 2,794줄에서 100줄로 줄어든 것도 같은 이유입니다.
나머지는 yml에
다섯 군데에 흩어져 있던 나머지 정보는 모델 옆 yml 파일로 모았습니다. 설명, 품질 테스트, 지표 정의는 모델 yml에 적습니다. 읽는 테이블이 어디서 만들어지는지, 아직 dbt로 옮기지 않은 Spark나 Jenkins 잡이 무엇을 읽는지는 source yml에 적습니다.
여기서 meta.producer는 dbt 기본 기능이 아니라 저희가 추가한 선언입니다. dbt의 meta 필드는 커스텀 설정값을 넣을 수 있는데요, 이 값을 이용해서 앞에서 말한 wrapper가 테이블이 만들어지는 방식에 맞는 sensor를 붙여 줍니다.
Spark 잡도 같은 방식입니다. 기존 환경은 대부분 Impala 쿼리로 돌지만, Spark를 써야 하는 경우가 있어서 반드시 지원해야 했습니다. source인데 task를 만든다는 게 이상하게 들릴 수 있습니다만, dbt 프로젝트 입장에서 Spark 잡의 결과 테이블은 분명 source가 맞습니다. 하지만 Airflow에서는 그 Spark 잡을 task로 돌려야 합니다. 그렇다고 Spark 잡을 위한 yml을 한 벌 더 관리하기는 싫었습니다. 그래서 source에 "이 테이블은 이 Spark 잡으로 만들어진다"는 레시피를 같이 적어 두고, wrapper가 그 레시피대로 task를 만들어 dbt 모델 앞에 이어 주게 했습니다. 광고 도메인에서만 source 200여 개가 이렇게 연결되어 있습니다.
원천 DB에서 적재하는 테이블은 source yml도 사람이 손으로 관리하지 않습니다. 새 적재 테이블이 추가되면 source 선언이 자동으로 추가되고, 원천 테이블의 컬럼이 바뀌면 사내 메타 시스템의 정보를 받아 source yml의 컬럼과 설명도 자동으로 고쳐집니다. 삭제된 컬럼을 쓰는 모델이 있으면 바로 반영하지 않고, 영향받는 모델을 확인할 수 있게 draft PR로 올려 줍니다.
의존 관계가 코드로 남으니 검증도 쉬워졌습니다. PR을 올리면 CI가 dbt compile로 없는 모델 참조를 잡고, DAG를 실제로 import해 보고, 컴파일된 SELECT를 LIMIT 0으로 돌려 컬럼이 맞는지 확인합니다. 클러스터마다 UI에서 넣던 Airflow connection과 pool도 코드로 옮겼습니다. 코드에 적힌 값이 각 클러스터에 들어가고, 급할 때는 UI에서 덮어쓸 수 있지만 코드와 달라지면(drift) 검사해서 알려 줍니다.
그리고 한동안 멈췄습니다
솔직히 말씀드리면, 이 프로젝트가 처음부터 쭉 달린 건 아닙니다. 2025년 여름 두 달 동안 뼈대를 세우고, 다른 우선순위에 밀려 반년 가까이 거의 손을 대지 못했어요. 본격적으로 옮기기 시작한 건 2026년 3월부터입니다. 그래서 제목의 9개월은 실제로 작업한 기간이에요.
처음 시작할 때 기대한 건 편의와 강건성이었습니다. task 의존성 연결을 손으로 하지 않고, 깨진 참조를 배포 전에 잡고, 도메인끼리 장애가 번지지 않게 하는 것이었습니다. 그런데 다시 시작해 보니 그 사이 코딩 에이전트가 일상 도구가 되어 있었고, 가장 큰 수확은 전혀 다른 곳에서 나왔습니다.
에이전트에게 물어봤습니다
이제 첫 문단에서 꺼낸 이야기로 돌아가 보겠습니다.
기존 레포에서 에이전트는 SQL 파일을 grep하고 operator 수백 개와 >> 줄을 한참 따라가다가 "적재 주체는 코드에 없어 확인할 수 없습니다"라고 하더라고요. 사람한테도 어려운 구조는 에이전트한테도 어려웠습니다.
뉴타운에서는 같은 질문에 source yml 한 줄을 읽고 바로 답합니다. 사람이 편하자고 정해진 자리에 정해진 형식으로 적어 둔 것이, 그대로 에이전트가 읽기 좋은 문맥이 된 것입니다.
레포의 문서 구성도 에이전트가 읽기 쉽게 바꿨습니다. 물론 사람이 읽기에도 좋으면 더 좋고요. 앞에서 이야기한 yml뿐 아니라 레포에 대한 설명과 운영 지식도 레포 안의 문서로 모아서, 에이전트가 필요한 문서를 바로 찾을 수 있게 했습니다. 시작은 OpenAI의 harness engineering 글이었습니다. 사람이 직접 작성한 코드 없이 에이전트만으로 제품을 만든 이야기인데, 거기서 영감을 많이 받았습니다. 그 뒤로 몇 가지 구조를 시험해 봤고, 지금은 Diátaxis와 OKF(Open Knowledge Format) 형식으로 정리하고 있습니다.
저희만 이렇게 느낀 건 아니었습니다. Anthropic 데이터팀이 공개한 사례(How Anthropic enables self-service data analytics with Claude, 2026-06-03)를 보면, 구조화된 문맥(스킬) 없이는 분석 질문 정확도가 21%에 그쳤고 스킬을 붙이자 95% 이상이 나왔다고 합니다. 문맥을 따로 관리하지 않았더니 한 달 만에 약 95%에서 약 65%로 떨어졌다는 이야기도 있습니다. 지금은 데이터 모델 PR의 약 90%가 같은 diff에서 스킬도 함께 고친다고 합니다. SQL과 yml을 같은 PR에서 고치는 저희 방식과 거의 같은 모양이라 반가웠습니다.
저희도 9월에 5시간 정도 진행한 팀 해커톤에서 작게 확인해 봤습니다. 앞에서 옮긴 그 마트를 대상으로, 모델 yml에 지표 정의를 더해 사내 데이터 챗봇의 지식베이스에 싣고 전후를 비교했습니다.
항목
문항 수
싣기 전
실은 후
맞는 테이블을 고른 비율 (지표 정의를 보지 않은 사람이 쓴 질문)
8
25%
75%
설명형 질문 정답률
9
22%
78%
숫자를 계산해야 하는 질문은 조금 다르게 풀렸습니다. 지표 정의를 지식베이스에 싣기만 했을 때는 SQL 결과값 정답률이 46%에서 움직이지 않았습니다. 봇이 계산식은 찾았는데 쿼리를 끝까지 실행하지 않았기 때문입니다. 그래서 같은 정의를 시맨틱 레이어로 옮기고, 봇이 MetricFlow로 만든 SQL을 그대로 실행하게 했더니 82%(11문항 중 9개)가 나왔습니다. 두 측정은 문항 구성이 달라서 그대로 비교할 수는 없고, 질문 수가 적어 실행마다 7%p 정도 흔들린다는 점도 감안해야 합니다. 그래도 방향은 분명했습니다. 문맥은 무엇을 찾을지를, 시맨틱 레이어는 어떻게 계산할지를 풀어 줬습니다.
시맨틱 레이어는 지표의 계산식과 필터, 조인 키를 모델 yml에 한 번 적어 두면 MetricFlow가 SQL을 만들어 주는 구조라, 사람이 뽑든 챗봇이 뽑든 같은 숫자가 나옵니다. Anthropic 사례도 같은 방향입니다. 에이전트가 시맨틱 레이어를 가장 먼저 쓰도록 스킬로 강제하고 있습니다. 반대로 LLM이 원천 테이블과 쿼리 로그를 보고 지표 정의를 자동으로 만들게 한 시도는, 그럴듯해 보이지만 없애려던 모호함을 그대로 담아서 사람이 다듬은 작은 레이어보다 오히려 평가 결과가 나빴다고 합니다. 그래서 문서는 모델로 만들되 정의는 사람이 소유하라고 권합니다.
많은 일들이 있었습니다.
옮기면서 부딪힌 문제도 많았습니다.
- SQL에 Jinja 문법이 섞여서 처음엔 낯설고, 쿼리를 복사해서 Impala에서 바로 돌릴 수 없습니다. 이런 작업을 돕는 편집기 확장 도구가 Impala 지원이 약해서, 팀원이 직접 만들어 쓰고 있습니다.
- 모델마다 task를 만들다 보니 광고 DAG 하나에 task가 1,300개가 넘고, task마다 dbt가 뜨면서 스키마를 훑는 비용이 생겼습니다.
- 사내 Impala 환경의 제약 때문에 dbt Impala 어댑터를 포크해서 고쳐 써야 했습니다. MetricFlow도 Impala를 지원하지 않아서 Impala용으로 직접 구현해 쓰고 있습니다.
- 기존 파이프라인은 날짜 변수를 다양하게 쓰고 있었는데, Airflow의 logical_date와 다시한번 씨름하며 싸우게 되었습니다.
- source 자동 동기화는 원천 DB에서 적재하는 테이블만 대상이라, 다른 마트나 Spark 결과 테이블의 컬럼이 바뀌었을 때 이를 쓰는 모델을 검증하는 흐름은 아직 다듬는 중입니다.
그래도 한 번 겪은 문제의 해법은 공통 모듈과 변환 가이드에 쌓아 두어서, 다음 도메인은 같은 문제를 다시 겪지 않아도 됩니다.
마치며
9월 말 기준으로 도메인 클러스터 5개, dbt 모델 983개가 뉴타운에서 돌고 있습니다. 옮기는 동안 기존 배치가 뉴타운 때문에 멈춘 날은 없었습니다.
아직 기존 DW Airflow에는 옮기지 않은 파이프라인이 많이 남아 있습니다. 언제, 얼마나 옮길지는 도메인 팀이 정하니, 어떤 파이프라인은 꽤 오래 그 자리에 남아 있을 수도 있습니다. 솔직히 영원히 남는 것도 있을 것 같습니다. 그래도 두 플랫폼이 나란히 도는 데 문제가 없게 만들어 두었으니, 그것도 괜찮다고 생각합니다. 이 글은 뉴타운 이야기 세 편 중 첫 편으로, 왜 옮겼고 어떻게 옮겼는지 전체 그림을 소개했습니다. 다음 글에서는 사람 손을 덜 타는 파이프라인 운영을 다룹니다. 원천 테이블이 바뀌면 source yml이 알아서 따라가고, 적재가 끝났는지 sensor가 알아서 확인하고, Airflow 설정이 클러스터마다 어긋나지 않게 만든 이야기입니다. 마지막 글에서는 지표 정의를 시맨틱 레이어로 옮겨, 사람이 뽑든 챗봇이 뽑든 같은 숫자가 나오게 만든 이야기를 해 보겠습니다.
실행은 도메인별로 나누고, 문맥은 한 레포에 모았습니다. 처음에는 사람이 편하자고 시작한 일이었는데, 돌아보니 에이전트도 일하기 좋은 구조가 되어 있었습니다. 레포를 정리하는 일이 이렇게까지 중요해질 줄은 저희도 몰랐습니다.
긴 글 읽어주셔서 감사합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 RSS: Toss Tech Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기