Snowflake에서 Databricks로의 마이그레이션 — 2개월간의 실전 기록
Airflow + Snowflake 기반 데이터 플랫폼을 Databricks Lakehouse 로 이관하면서 마주친 문제들과 해결 과정, 그리고 거버넌스·비용·운영 관점에서 얻은 교훈을 정리합니다.
데이터 플랫폼 이전(마이그레이션)은 이삿짐 옮기는 일처럼 단순하지 않습니다. 단순히 박스를 들어 새 집에 내려놓는 것이 아니라, 새 공간에 맞게 가구 배치를 다시 설계하고 생활 방식도 바꿔야 하는 작업이거든요.
이 글은 Airflow + Snowflake 환경에서 운영되던 클라이언트의 데이터 플랫폼을 약 2개월에 걸쳐 Databricks Lakehouse로 이관한 프로젝트의 실전 기록입니다. 단순히 쿼리를 옮기는 작업이 아니라, 거버넌스·아키텍처·비용 모델 자체를 새로 설계해야 했던 과정을 단계별로 정리합니다.
이 글에서 배우는 것
- Snowflake → Databricks 마이그레이션의 실제 동기와 목표 설정 방법
- Unity Catalog와 Medallion Architecture로 거버넌스를 재설계하는 방법
- Bronze 계층에서 COPY INTO와 Auto Loader를 어떻게 나눠 쓰는지
- Silver/Gold SQL 변환 시 자주 마주치는 차이점과 해결 패턴
- 비용 최적화를 위한 클러스터 선택 전략과 실제 절감 사례
1. 프로젝트 개요
출발점
먼저 이 마이그레이션의 출발점을 정확히 파악해봅시다. 어디서 어디로 가는 여정인지 알아야 중간에 길을 잃지 않습니다.
- 기존 스택: Airflow (오케스트레이션) + Snowflake (DW) + 다양한 BI/ML 도구
- 대상 스택: Databricks Lakehouse (Unity Catalog + Delta Lake + Workflows)
- 기간: 약 2개월
- 인력: 데이터 엔지니어 2~3명
마이그레이션의 동기
클라이언트가 Snowflake를 떠나기로 결정한 배경은 단순한 비용 문제가 아니었습니다. "그냥 비싸서"라는 이유 하나로 이 규모의 마이그레이션을 시작하면 절대 완주하지 못합니다. 실제로는 다음의 복합적인 이유가 있었습니다.
- 데이터 엔지니어링 / ML / BI 가 분산되어 있어, 같은 데이터에 대해 도구마다 다른 사본이 존재했습니다.
- 세분화된 권한 관리가 어렵고, 부서·도메인 단위의 데이터 정책 적용이 일관되지 않았습니다.
- 컴퓨팅 비용이 상승 추세였고, ML 워크로드를 위한 별도의 GPU/Spark 인프라까지 필요했습니다.
- 엔지니어링 팀이 Spark 기반 처리에 대한 기술적 통제권을 더 확보하고 싶어했습니다.
따라서 이 마이그레이션의 목표는 "Snowflake를 Databricks로 바꾼다"가 아니라 "분산된 데이터 자산을 단일 Lakehouse로 통합하면서 거버넌스를 재설계한다" 였습니다.
한 문장으로: 마이그레이션의 본질은 도구를 바꾸는 것이 아니라 데이터 자산을 다시 설계하는 기회입니다.
2. 거버넌스 구조 먼저 — Unity Catalog 와 Medallion
Unity Catalog 부터 시작
집을 이사할 때 짐을 먼저 옮기고 나서 가구 배치를 고민하면 동선이 엉망이 됩니다. 데이터 이전도 마찬가지입니다. 가장 먼저 한 일은 코드 한 줄을 옮기기 전에 Unity Catalog의 카탈로그 / 스키마 / 그룹 구조를 설계하는 것이었습니다.
이전 환경에서는 Snowflake의 Database/Schema 명명 규칙이 부서마다 다르고, 권한도 개별 유저 단위로 흩어져 있었습니다. 이를 그대로 이전하면 같은 혼란이 Databricks에서도 반복될 게 뻔했습니다. 그래서 먼저 다음 원칙을 세웠습니다.
- 카탈로그는 환경 단위(
prod,stg,dev) - 스키마는 비즈니스 도메인 단위(
sales,marketing,finance등) - 권한은 개인이 아닌 그룹(Account-level group) 에 부여
- 그룹은 IdP (예: Azure AD / Okta) 의 그룹과 SCIM 으로 동기화
-- 카탈로그 / 스키마 생성 예시
CREATE CATALOG IF NOT EXISTS prod;
CREATE SCHEMA IF NOT EXISTS prod.sales;
-- 그룹 단위 권한 부여
GRANT USE CATALOG ON CATALOG prod TO `data-platform-readers`;
GRANT USE SCHEMA, SELECT ON SCHEMA prod.sales TO `sales-analysts`;Medallion Architecture (Bronze / Silver / Gold)
스키마 안의 테이블을 Medallion Architecture에 따라 세 계층으로 나눴습니다. 원석(Bronze)을 제련해서 은(Silver)으로 만들고, 최종 제품(Gold)으로 완성하는 흐름을 생각하면 이해하기 쉽습니다.
| 계층 | 역할 | 데이터 형태 |
|---|---|---|
| Bronze | 원천 시스템에서 들어온 그대로의 raw 데이터 | append-only, 스키마 변동 허용 |
| Silver | 정제·표준화된 비즈니스 엔터티 | 타입 통일, PII 마스킹, 중복 제거 |
| Gold | 분석·BI 용 집계 / 비즈니스 메트릭 | dashboard·ML feature 직접 소비 |
이 계층 분리 덕분에 마이그레이션을 계층별로 끊어서 진행할 수 있었고, "어떤 테이블이 어디까지 옮겨졌는가"를 추적하기도 훨씬 쉬웠습니다.
3. Bronze 계층 — 처음엔 COPY INTO, 결국 Auto Loader
처음엔 가장 단순해 보이는 선택부터
수백 개의 Snowflake 원본 테이블을 Databricks로 가져와야 했습니다. 처음에는 "일단 가장 직관적인 방법부터 써보자"는 방식으로 시작했습니다. 이 선택이 결국 맞지 않았지만, 그 과정에서 중요한 교훈을 얻었습니다.
-- Snowflake 에서 외부 스토리지 (S3 / ADLS) 로 unload
COPY INTO @ext_stage/sales/
FROM raw.sales
FILE_FORMAT = (TYPE = PARQUET)
HEADER = TRUE;Snowflake 에서 객체 스토리지로 COPY INTO 로 unload 한 뒤, Databricks 측에서도 동일하게 COPY INTO 로 적재하는 시나리오였습니다.
-- Databricks 측 적재
COPY INTO prod.bronze.sales
FROM 's3://landing/sales/'
FILEFORMAT = PARQUET;부딪힌 문제 — 타입 미스매치와 운영 부담
이 방식은 **초기 풀로드(full load)**에는 그럭저럭 작동했습니다. 하지만 현실 운영에 들어서자 두 가지 큰 문제가 수면 위로 떠올랐습니다.
- 타입 미스매치: Snowflake 의
NUMBER(38,0),VARIANT,TIMESTAMP_TZ등이 Parquet 으로 unload 되는 과정에서 의도치 않은 타입으로 떨어졌습니다. Databricks 쪽에서 다시 캐스팅하는 코드가 테이블마다 누적되었습니다. - 증분 처리에 부적합:
COPY INTO는 멱등성을 일부 보장하지만, 새로 도착한 파일만 안전하게 처리하려면 별도의 상태 관리가 필요했습니다. 수백 개 테이블에 대해 이 코드를 직접 작성하는 비용이 너무 컸습니다.
결정 — full load 는 COPY INTO, 증분은 Auto Loader
두 가지 문제를 동시에 해결하려다 둘 다 어설프게 되는 것보다, 도구를 용도에 맞게 나눠 쓰는 쪽을 택했습니다.
결국 다음과 같이 역할을 나눴습니다.
- 최초 풀로드 :
COPY INTO(한 번만 실행되고 끝나므로 단순함이 장점) - 증분 적재 : Auto Loader (
cloudFiles)
Auto Loader 를 도입하면서 얻은 가장 큰 이점은 스키마 추론과 진화(schema evolution) 였습니다.
df = (
spark.readStream
.format("cloudFiles")
.option("cloudFiles.format", "parquet")
.option("cloudFiles.schemaLocation", "/Volumes/prod/_schemas/sales")
.option("cloudFiles.schemaEvolutionMode", "addNewColumns")
.load("s3://landing/sales/")
)
(
df.writeStream
.option("checkpointLocation", "/Volumes/prod/_checkpoints/sales")
.trigger(availableNow=True)
.toTable("prod.bronze.sales")
)수백 개 테이블에 동일한 패턴을 적용해야 했으므로, 위 코드를 테이블 메타데이터 기반의 제너레이터로 감싸 한 번에 생성·운영했습니다. 결과적으로 테이블당 작성해야 할 코드가 거의 사라졌고, 신규 컬럼이 원천에 추가되어도 Bronze 가 자동으로 따라가게 되었습니다.
교훈: full load 와 incremental load 는 서로 다른 도구를 쓰는 편이 단순합니다. 하나의 도구로 두 가지를 모두 해결하려고 하면 어느 쪽도 깔끔하지 않게 됩니다.
4. Silver / Gold 계층 — 레거시 SQL 과 프로시저 변환
자동 변환 도구는 결국 포기
"자동화 도구가 있으면 빠르지 않나요?" 많이들 하는 질문입니다. Silver / Gold 계층에는 수년간 누적된 Snowflake SQL, 뷰, JavaScript 기반의 stored procedure가 있었습니다. 처음에는 BladeBridge 같은 자동 변환 솔루션을 도입할 계획이었으나, 다음 이유로 수동 변환으로 전환했습니다.
- 라이선스 비용이 프로젝트 예산을 크게 초과
- 자동 변환 결과의 검증 비용이 결국 수동 변환과 큰 차이가 없을 정도로 컸음
- 변환 과정 자체가 레거시 로직을 정리할 절호의 기회 였음
Snowflake → Databricks SQL 매핑에서 자주 부딪힌 차이
"SQL은 표준이니까 그냥 복사하면 되겠지?"라는 생각은 현실에서 무너집니다. 아래 표는 실제로 가장 많이 막혔던 차이점들입니다. 미리 파악해두면 시행착오가 크게 줄어듭니다.
| 영역 | Snowflake | Databricks (Spark SQL) |
|---|---|---|
| Semi-structured | VARIANT, OBJECT, ARRAY | STRUCT, MAP, ARRAY + from_json |
| 타임존 | TIMESTAMP_TZ | TIMESTAMP (UTC 권장) |
| Merge 동작 | MERGE (row-level) | MERGE INTO (Delta, 동일 의미) |
| 시점 조회 | AT (TIMESTAMP => ...) | TIMESTAMP AS OF / VERSION AS OF |
| Generated column | DEFAULT ... AS ... | GENERATED ALWAYS AS (...) |
| Stored procedure | JavaScript / SQL Scripting | Python notebook + Workflows |
특히 VARIANT 가 광범위하게 사용된 테이블은, Databricks 로 옮기면서 JSON 의 어떤 키를 실제로 쓰는가 를 분석해서 일부는 컬럼으로 평탄화하고, 나머지는 STRING (또는 VARIANT 타입) 으로 보관하는 식으로 정리했습니다. 이 작업만으로도 다운스트림 쿼리 비용이 눈에 띄게 줄었습니다.
Stored procedure 는 SQL 로 강제로 변환하지 않음
가장 많이 고민한 부분이 JavaScript 기반 stored procedure였습니다. "SQL로 만들어놓은 걸 SQL로 옮기는 게 당연하지 않나?"라는 생각을 버리고, PySpark + Workflow 조합으로 재구현하는 방향으로 바꿨습니다. 그 이유는 다음과 같습니다.
- 분기·반복·예외 처리 로직이 SQL 보다 Python 에서 훨씬 명료
- 단위 테스트가 가능 (pytest 로 검증 가능)
- 실행 단위가 명확해져서 Workflow 의 task 로 그대로 매핑됨
# 기존 Snowflake stored procedure 의 핵심 로직을 PySpark 로 재구현한 예시
from pyspark.sql import functions as F
def build_daily_sales(spark, run_date: str) -> None:
src = (
spark.table("prod.silver.orders")
.where(F.col("order_date") == F.lit(run_date))
)
agg = (
src.groupBy("store_id", "product_id")
.agg(
F.sum("amount").alias("gmv"),
F.countDistinct("order_id").alias("orders"),
)
.withColumn("run_date", F.lit(run_date))
)
(
agg.write
.format("delta")
.mode("overwrite")
.option("replaceWhere", f"run_date = '{run_date}'")
.saveAsTable("prod.gold.daily_sales")
)replaceWhere 를 활용해서 멱등 재실행이 가능하게 만든 것이 운영상 큰 차이를 만들었습니다. 같은 날짜의 잡을 몇 번을 다시 돌려도 결과가 동일합니다.
5. 비용 모델의 재설계
Snowflake 와 Databricks 는 비용 사고방식의 차이
Snowflake의 비용 모델은 단순합니다 — "warehouse 크기를 올리면 빠르고, 멈추면 돈이 안 나간다". Databricks는 다릅니다. 클러스터 / 잡 / 데이터 / 스토리지의 차원이 분리되어 있어, 같은 사고방식으로 접근하면 비용이 예상치 못한 곳에서 새어 나옵니다.
다음 원칙을 세웠습니다.
- interactive (개발/탐색) → All-purpose cluster 또는 SQL Warehouse (Serverless)
- scheduled (배치) → Job cluster (잡 시작 시 생성, 종료 시 자동 해제)
- ad-hoc 분석 → Serverless SQL Warehouse + auto-stop
Job Cluster vs Serverless 비교
"그냥 Serverless 쓰면 편하지 않나요?" 라고 생각하기 쉽지만, 실제로 매일 도는 ETL 잡에 대해 둘 다 측정해봤더니 상황에 따라 결과가 달랐습니다.
| 기준 | Job Cluster | Serverless |
|---|---|---|
| 시작 시간 | 1~3분 | 수 초 |
| 단가 | 낮음 | 높음 |
| 짧고 빈번한 잡 | 시작 시간 비중이 큼 | 유리 |
| 길고 무거운 잡 | 유리 | 단가가 누적되어 불리 |
결론적으로 잡의 평균 실행 시간 / 빈도 에 따라 둘을 섞어 썼습니다. 모든 잡을 한쪽으로 몰아넣지 않은 것이 비용 절감의 핵심이었습니다.
클러스터 메트릭으로 인스턴스 타입을 다시 골랐다
초기에는 "기본값으로 일단 시작" 했지만, 이후 Databricks 의 cluster metrics (CPU / Memory / GC / Shuffle) 를 보면서 인스턴스 타입을 재조정했습니다.
- 메모리 압박이 큰 잡 → memory-optimized
- CPU 만 빠듯한 잡 → compute-optimized
- 셔플이 많은 잡 → 디스크 IO 가 좋은 시리즈
이 단계만으로도 일부 잡은 30~50% 비용 절감이 났습니다.
6. Lakehouse Federation 으로 외부 시스템과 연결
"모든 데이터를 Databricks로 다 옮겨야 하나요?" — 꼭 그렇지는 않았습니다. 일부 OLTP 시스템(예: Oracle, PostgreSQL)은 운영팀이 계속 소유했고, 분석 측면에서는 읽기만 가능하면 충분했습니다.
이런 시스템에는 Lakehouse Federation (현 Lakeflow Connect) 을 사용해서 물리적인 데이터 이동 없이 연결했습니다.
-- 외부 Oracle 을 카탈로그로 마운트
CREATE CONNECTION oracle_prod TYPE oracle
OPTIONS (
host '...', port '1521', user '...', password '...'
);
CREATE FOREIGN CATALOG oracle_prod
USING CONNECTION oracle_prod
OPTIONS (database 'ORCL');
-- 그 다음부터는 평범한 카탈로그처럼 SELECT 가능
SELECT *
FROM oracle_prod.app.customer c
JOIN prod.silver.orders o
ON o.customer_id = c.id;원천에 부담을 주지 않을 만한 가벼운 조회·조인에만 한정해 사용한다면, 별도의 ETL 을 만들지 않고도 Lakehouse 안에서 통합 뷰를 제공할 수 있습니다.
7. 자주 겪은 이슈들
마이그레이션 중반 이후 반복적으로 발견된 안티패턴을 정리합니다. 여러분이 처음 Databricks에 옮겨온다면 거의 동일한 지점에서 막히게 됩니다. 미리 알아두면 삽질을 크게 줄일 수 있습니다.
7.1. job 의존성을 너무 잘게 쪼개지 마라
Airflow 시절의 습관 때문에, 모든 단계를 별개의 Databricks job 으로 만든 경우가 많았습니다. job 간 의존성이 늘어날수록 다음 문제가 생깁니다.
- job 마다 클러스터 시작 비용이 누적됨
- 실패 지점이 많아져 운영 모니터링이 복잡해짐
기준: 같은 클러스터에서 순차로 실행되어도 무방한 단계라면, 별도 job 이 아니라 한 Workflow 안의 task 로 묶는 것이 좋습니다.
7.2. display, collect, toPandas 의 함정
개발 노트북에서는 결과를 확인하려고 display(df) 나 df.collect() 를 자주 쓰지만, 이를 그대로 운영 잡에 두고 가는 사례가 많았습니다.
collect/toPandas는 모든 데이터를 드라이버로 모음 → OOM 의 단골 원인display는 인터랙티브 컨텍스트 외부에서는 의미 없는 비용을 발생시킴
운영 코드에서는 모두 제거하거나 LIMIT 을 동반한 show 로 바꿔야 합니다.
7.3. pandas 단일 노드 처리
기존 Python 코드를 그대로 옮겨온 경우, 핵심 변환을 pandas 로 수행하고 있는 부분이 자주 발견됐습니다. 데이터가 작을 때는 문제가 없지만, 그대로 두면 어느 시점에 반드시 터집니다.
- 가능한 한 PySpark DataFrame API 로 재작성
- 그게 어렵다면 pandas API on Spark (
pyspark.pandas) 로 우회
7.4. 보안이 유지되어야할 값을 노트북에 그대로 넣는 일
마이그레이션 초기에 가장 흔한 보안 실수가 노트북 셀에 비밀번호를 그대로 적어두는 것이었습니다. 반드시 dbutils.secrets 와 secret scope 를 거쳐 접근해야 합니다.
jdbc_password = dbutils.secrets.get(scope="prod", key="oracle_password")8. 권장 팀 구성
이 정도 규모(원천 수백 테이블, 2개월 일정) 에서 추천하는 팀 구성은 다음과 같습니다.
| 역할 | 인원 | 책임 |
|---|---|---|
| 클라우드 인프라 엔지니어 | 1명 | 네트워크, IAM, 스토리지, Databricks workspace 설정 |
| PySpark 숙련 데이터 엔지니어 | 1~2명 | Silver/Gold 계층 변환, 잡 작성 |
| Databricks 경험자 | 1명 이상 | Unity Catalog 설계, 클러스터 정책, 비용 가드레일 |
| (선택) 분석/BI 담당 | 1명 | Gold 계층 검증, 대시보드 마이그레이션 |
특히 Databricks 경험자가 한 명도 없는 팀이 단독으로 시작하는 것은 권장하지 않습니다. 초기에 잡혀버린 카탈로그/권한/클러스터 정책의 잘못된 결정은 나중에 되돌리는 비용이 큽니다.
9. 마이그레이션 이후의 변화
2개월의 작업이 끝나고 운영에 들어간 후 실제로 어떤 변화가 있었는지 수치로 확인해봤습니다.
| 영역 | 변화 |
|---|---|
| 인프라 비용 | 이전 대비 약 25% 수준 |
| 워크플로우 통합 | 데이터 엔지니어링 / ML / BI 가 동일 플랫폼에서 동작 |
| 거버넌스 | Unity Catalog 기반의 일관된 그룹·도메인 정책 |
| 데이터 품질 | 중복 데이터 제거 + 파이프라인 로직 정비로 결과 일관성 향상 |
가장 중요한 교훈
이 프로젝트에서 얻은 가장 큰 교훈은 **"마이그레이션은 시스템 이전이 아니라 데이터 자산을 다시 설계할 기회"**라는 점이었습니다. 단순히 Snowflake의 테이블을 Databricks의 테이블로 1:1 옮겼다면, 비용은 어느 정도 줄었을지 몰라도 거버넌스·품질 측면의 문제는 그대로 따라왔을 것입니다.
옮기는 김에 해야 할 세 가지:
- 누가 봐도 더 이상 사용되지 않는 테이블을 과감히 폐기하고,
- 같은 데이터를 가리키는 여러 사본을 하나의 Silver 테이블로 통합하고,
- 비효율적인 SQL/프로시저를 다시 작성한 것
이 세 가지가 단순 플랫폼 전환으로 절감된 비용보다 더 큰 가치를 만들었습니다.
마치며 — 핵심 요약
- 마이그레이션의 출발점은 "왜 이전하는가"를 명확히 하는 것입니다. 비용만이 이유라면 중간에 흔들리기 쉽습니다.
- Unity Catalog 구조(카탈로그·스키마·그룹)를 코드 한 줄 옮기기 전에 설계해두세요. 나중에 바꾸면 비용이 두 배입니다.
- 풀로드는
COPY INTO, 증분 적재는 Auto Loader — 하나의 도구로 둘 다 해결하려 하면 어느 쪽도 깔끔하지 않습니다. - JavaScript stored procedure는 SQL로 옮기지 말고 PySpark + Workflow로 재구현하세요. 테스트 가능성과 운영 명확성이 훨씬 좋아집니다.
- Databricks 비용은 잡의 길이와 빈도에 따라 Job Cluster와 Serverless를 섞어 써야 최적화됩니다.
- Databricks 경험자가 팀 안에 없다면 초기 아키텍처 결정을 외부 전문가와 함께 검토하는 것을 강력히 권합니다 — 초기 실수를 되돌리는 비용이 큽니다.
참고
— Data Dynamics 엔지니어링 팀