Blog
databrickssnowflakemigrationlakehouseunity-catalog

Snowflake에서 Databricks로의 마이그레이션 — 2개월간의 실전 기록

Airflow + Snowflake 기반 데이터 플랫폼을 Databricks Lakehouse 로 이관하면서 마주친 문제들과 해결 과정, 그리고 거버넌스·비용·운영 관점에서 얻은 교훈을 정리합니다.

Data Dynamics2026年5月8日25 min read
This post is not yet translated. The original Korean version is shown below.

데이터 플랫폼 이전(마이그레이션)은 이삿짐 옮기는 일처럼 단순하지 않습니다. 단순히 박스를 들어 새 집에 내려놓는 것이 아니라, 새 공간에 맞게 가구 배치를 다시 설계하고 생활 방식도 바꿔야 하는 작업이거든요.

이 글은 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)**에는 그럭저럭 작동했습니다. 하지만 현실 운영에 들어서자 두 가지 큰 문제가 수면 위로 떠올랐습니다.

  1. 타입 미스매치: Snowflake 의 NUMBER(38,0), VARIANT, TIMESTAMP_TZ 등이 Parquet 으로 unload 되는 과정에서 의도치 않은 타입으로 떨어졌습니다. Databricks 쪽에서 다시 캐스팅하는 코드가 테이블마다 누적되었습니다.
  2. 증분 처리에 부적합: 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은 표준이니까 그냥 복사하면 되겠지?"라는 생각은 현실에서 무너집니다. 아래 표는 실제로 가장 많이 막혔던 차이점들입니다. 미리 파악해두면 시행착오가 크게 줄어듭니다.

영역SnowflakeDatabricks (Spark SQL)
Semi-structuredVARIANT, OBJECT, ARRAYSTRUCT, MAP, ARRAY + from_json
타임존TIMESTAMP_TZTIMESTAMP (UTC 권장)
Merge 동작MERGE (row-level)MERGE INTO (Delta, 동일 의미)
시점 조회AT (TIMESTAMP => ...)TIMESTAMP AS OF / VERSION AS OF
Generated columnDEFAULT ... AS ...GENERATED ALWAYS AS (...)
Stored procedureJavaScript / SQL ScriptingPython 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 ClusterServerless
시작 시간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 엔지니어링 팀