Blog
databrickssnowflakemigrationlakehouseunity-catalog

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

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

Data Dynamics2026년 5월 8일20 min read

이 글은 Airflow + Snowflake 환경에서 운영되던 클라이언트의 데이터 플랫폼을 약 2개월에 걸쳐 Databricks Lakehouse 로 이관한 프로젝트의 실전 기록입니다. 단순히 쿼리를 옮기는 작업이 아니라, 거버넌스·아키텍처·비용 모델 자체를 새로 설계해야 했던 과정을 단계별로 정리합니다.


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원천 시스템에서 들어온 그대로의 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 매핑에서 자주 부딪힌 차이

영역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 였습니다. 처음에는 Databricks 의 SQL Scripting 으로 비슷하게 옮기는 것을 시도했지만, 다음 이유로 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 비교

매일 도는 ETL 잡에 대해 Job cluster 와 Serverless 를 둘 다 측정해봤습니다.

기준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/프로시저를 다시 작성한 것

이 절감된 비용보다 더 큰 가치를 만들었습니다.


참고

— Data Dynamics 엔지니어링 팀