Kim Seogyu
Data Engineering

데이터 엔지니어링 시리즈 #10: 데이터 레이크 vs 웨어하우스 - 레이크하우스 아키텍처

데이터 저장소 아키텍처의 종류와 선택 기준을 배웁니다. Delta Lake의 ACID, Time Travel, Schema Evolution을 심층 분석합니다.

Published 2026년 1월 2일7 min read1,364 words

데이터 엔지니어링 시리즈 #10: 데이터 레이크 vs 웨어하우스 - 레이크하우스 아키텍처

대상 독자: 충분한 경험을 가진 백엔드/풀스택 엔지니어로, PostgreSQL ACID에 익숙하지만 데이터 레이크/웨어하우스는 처음인 분

이 편에서 다루는 것

"S3에 Parquet 올려두면 되는 거 아닌가요?" 라는 질문에서 시작합니다. 왜 Delta Lake 같은 테이블 포맷이 필요한지, 그리고 레이크하우스가 무엇인지 배웁니다.


데이터 저장소의 진화

세대별 변화

flowchart LR
    subgraph Gen1 ["1세대: 데이터 웨어하우스"]
        DW1["온프레미스<br/>Oracle, Teradata"]
        DW2["구조화된 데이터"]
        DW3["SQL 분석"]
    end
    
    subgraph Gen2 ["2세대: 데이터 레이크"]
        DL1["클라우드 스토리지<br/>S3, GCS"]
        DL2["모든 형태의 데이터"]
        DL3["Spark 처리"]
    end
    
    subgraph Gen3 ["3세대: 레이크하우스"]
        LH1["레이크 위에<br/>웨어하우스 기능"]
        LH2["Delta Lake, Iceberg"]
        LH3["ACID + 유연성"]
    end
    
    Gen1 -->|"확장성 한계"| Gen2 -->|"품질 문제"| Gen3

데이터 웨어하우스 (Data Warehouse)

특징

flowchart TB
    subgraph DW ["Data Warehouse"]
        direction TB
        Feature1["✅ 스키마 정의 (Schema-on-Write)"]
        Feature2["✅ ACID 트랜잭션"]
        Feature3["✅ SQL 지원"]
        Feature4["✅ 빠른 쿼리"]
        
        Limit1["❌ 구조화된 데이터만"]
        Limit2["❌ 비용 (저장+컴퓨팅)"]
        Limit3["❌ 벤더 종속"]
    end
    
    Examples["예시:<br/>• Snowflake<br/>• BigQuery<br/>• Redshift"]

PostgreSQL과의 비교

특성PostgreSQL (OLTP)BigQuery (DW)
목적트랜잭션 처리분석 쿼리
스토리지Row-basedColumn-based
스케일수직 확장무한 수평 확장
비용서버 비용쿼리당 비용
쿼리 속도단건 빠름집계 빠름

데이터 레이크 (Data Lake)

특징

flowchart TB
    subgraph DL ["Data Lake"]
        direction TB
        Feature1["✅ 모든 형태의 데이터"]
        Feature2["✅ 저렴한 저장 비용"]
        Feature3["✅ 분리된 저장/컴퓨팅"]
        Feature4["✅ 유연한 처리 (Spark 등)"]
        
        Limit1["❌ ACID 없음"]
        Limit2["❌ 스키마 관리 어려움"]
        Limit3["❌ 데이터 품질 문제"]
    end
    
    Examples["예시:<br/>• S3 + Parquet<br/>• GCS + Avro<br/>• ADLS + JSON"]

데이터 레이크의 문제점

flowchart TB
    subgraph Problems ["레이크의 고질적 문제"]
        P1["동시 쓰기 충돌"]
        P2["부분 실패 → 깨진 데이터"]
        P3["스키마 변경 → 호환성 문제"]
        P4["삭제/수정 어려움"]
        P5["작은 파일 문제"]
    end
    
    Result["결국... 데이터 늪(Data Swamp)"]
    
    Problems --> Result

레이크하우스 (Lakehouse)

두 세계의 통합

flowchart TB
    subgraph Lakehouse ["Lakehouse Architecture"]
        subgraph Top ["웨어하우스 기능"]
            T1["ACID 트랜잭션"]
            T2["스키마 관리"]
            T3["Time Travel"]
            T4["SQL 지원"]
        end
        
        subgraph Middle ["테이블 포맷"]
            M1["Delta Lake"]
            M2["Apache Iceberg"]
            M3["Apache Hudi"]
        end
        
        subgraph Bottom ["오픈 스토리지"]
            B1["S3"]
            B2["GCS"]
            B3["ADLS"]
        end
        
        Top --> Middle --> Bottom
    end

핵심 가치

특성레이크웨어하우스레이크하우스
저장 비용저렴 ✅비쌈저렴 ✅
ACID
오픈 포맷❌ (벤더)
ML 지원제한적
SQL 분석제한적

Medallion Architecture (Bronze/Silver/Gold)

레이크하우스에서 데이터를 계층화하여 관리하는 표준 패턴입니다. Databricks가 제안하고 현재 업계 표준으로 자리잡았습니다.

출처: Databricks - Medallion Architecture, Armbrust et al., "Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores" (VLDB 2020)

세 레이어 구조

flowchart LR
    subgraph Bronze ["🥉 Bronze Layer"]
        B1["원본 그대로 저장"]
        B2["스키마 변경 보호"]
        B3["감사/재처리 가능"]
    end
    
    subgraph Silver ["🥈 Silver Layer"]
        S1["정제/검증"]
        S2["조인/통합"]
        S3["비즈니스 엔티티"]
    end
    
    subgraph Gold ["🥇 Gold Layer"]
        G1["집계/요약"]
        G2["비즈니스 리포트"]
        G3["ML Features"]
    end
    
    Bronze -->|"정제"| Silver -->|"집계"| Gold

각 레이어의 역할

Layer목적데이터 특성소비자
Bronze원본 보존Raw, 스키마 유연데이터 엔지니어
Silver정제/통합Cleaned, 조인됨데이터 분석가, DS
Gold비즈니스 집계Aggregated, 최적화BI, 경영진

코드 예시

# Bronze: 원본 그대로 저장
raw_events = spark.readStream \
    .format("kafka") \
    .option("kafka.bootstrap.servers", "kafka:9092") \
    .option("subscribe", "user_events") \
    .load()

raw_events.writeStream \
    .format("delta") \
    .option("checkpointLocation", "/checkpoints/bronze") \
    .start("/delta/bronze/events")

# Silver: 정제 및 스키마 적용
bronze_df = spark.read.format("delta").load("/delta/bronze/events")

silver_df = bronze_df \
    .select(from_json(col("value"), schema).alias("data")) \
    .select("data.*") \
    .filter(col("user_id").isNotNull()) \
    .dropDuplicates(["event_id"])

silver_df.write.format("delta").mode("overwrite") \
    .save("/delta/silver/events")

# Gold: 비즈니스 집계
silver_df = spark.read.format("delta").load("/delta/silver/events")

gold_df = silver_df \
    .groupBy("date", "event_type") \
    .agg(
        count("*").alias("event_count"),
        countDistinct("user_id").alias("unique_users")
    )

gold_df.write.format("delta").mode("overwrite") \
    .save("/delta/gold/daily_metrics")

왜 이 패턴인가?

문제Medallion 해결책
원본 데이터 유실Bronze에 원본 보존
스키마 변경 대응Bronze는 스키마 유연, Silver에서 검증
재처리 필요Bronze → Silver → Gold 순서대로 재실행
다양한 소비자 니즈레이어별 최적화된 데이터 제공

Delta Lake 심층 분석

ACID 트랜잭션

flowchart TB
    subgraph WithoutACID ["ACID 없이 (일반 Parquet)"]
        W1["writer 1: 파일 A 쓰기"]
        W2["writer 2: 파일 B 쓰기"]
        W3["동시에 실행"]
        W4["충돌/덮어쓰기 발생 💥"]
        
        W1 --> W3
        W2 --> W3
        W3 --> W4
    end
    
    subgraph WithACID ["Delta Lake (ACID)"]
        D1["writer 1: 트랜잭션 시작"]
        D2["writer 2: 트랜잭션 시작"]
        D3["optimistic concurrency"]
        D4["하나만 커밋 성공<br/>나머지 재시도"]
        
        D1 --> D3
        D2 --> D3
        D3 --> D4
    end

Delta Lake의 방법: 트랜잭션 로그 (_delta_log/)

table/
├── _delta_log/
│   ├── 00000000000000000000.json  # 첫 트랜잭션
│   ├── 00000000000000000001.json  # 두 번째
│   └── 00000000000000000002.json  # 세 번째
├── part-00000.parquet
├── part-00001.parquet
└── part-00002.parquet

Time Travel

flowchart LR
    subgraph History ["버전 히스토리"]
        V0["v0: 초기 데이터"]
        V1["v1: 추가"]
        V2["v2: 수정"]
        V3["v3: 삭제 (현재)"]
        
        V0 --> V1 --> V2 --> V3
    end
    
    Query["어떤 버전이든<br/>쿼리 가능!"]
# 특정 버전으로 읽기
df = spark.read.format("delta") \
    .option("versionAsOf", 2) \
    .load("/delta/users")

# 특정 시점으로 읽기
df = spark.read.format("delta") \
    .option("timestampAsOf", "2024-01-01") \
    .load("/delta/users")

# 히스토리 조회
from delta.tables import DeltaTable

dt = DeltaTable.forPath(spark, "/delta/users")
dt.history().show()

Schema Evolution

flowchart TB
    subgraph Problem ["스키마 변경 문제"]
        P1["기존: id, name, email"]
        P2["새로운: id, name, email, phone"]
        P3["기존 파일은 phone 없음"]
        P4["어떻게 함께 읽지?"]
    end
    
    subgraph Solution ["Delta Lake 해결책"]
        S1["스키마 자동 병합"]
        S2["새 컬럼 NULL 허용"]
        S3["호환성 검사"]
    end
# 자동 스키마 병합
df_new.write.format("delta") \
    .mode("append") \
    .option("mergeSchema", "true") \
    .save("/delta/users")

# 스키마 덮어쓰기 (주의!)
df_new.write.format("delta") \
    .mode("overwrite") \
    .option("overwriteSchema", "true") \
    .save("/delta/users")

MERGE (Upsert)

flowchart TB
    subgraph Before ["MERGE 전"]
        Source["Source (새 데이터)"]
        Target["Target (기존 테이블)"]
    end
    
    subgraph Logic ["MERGE 로직"]
        Match["ON 조건으로 매칭"]
        WhenMatched["WHEN MATCHED → UPDATE"]
        WhenNotMatched["WHEN NOT MATCHED → INSERT"]
    end
    
    subgraph After ["MERGE 후"]
        Result["통합된 결과"]
    end
    
    Before --> Logic --> After
from delta.tables import DeltaTable

# 타겟 테이블
target = DeltaTable.forPath(spark, "/delta/users")

# 소스 데이터 (업데이트할 데이터)
source = spark.read.parquet("/staging/users")

# MERGE 실행
target.alias("t").merge(
    source.alias("s"),
    "t.user_id = s.user_id"
).whenMatchedUpdate(
    set={
        "name": "s.name",
        "email": "s.email",
        "updated_at": "current_timestamp()"
    }
).whenNotMatchedInsert(
    values={
        "user_id": "s.user_id",
        "name": "s.name",
        "email": "s.email",
        "created_at": "current_timestamp()"
    }
).execute()

Delta Lake vs Apache Iceberg

비교

flowchart TB
    subgraph Delta ["Delta Lake"]
        D1["✅ Databricks 최적화"]
        D2["✅ Spark 통합 우수"]
        D3["✅ 성숙한 생태계"]
        D4["⚠️ Databricks 외 지원 제한적"]
    end
    
    subgraph Iceberg ["Apache Iceberg"]
        I1["✅ 벤더 중립"]
        I2["✅ 다양한 엔진 지원"]
        I3["✅ Hidden Partitioning"]
        I4["⚠️ 상대적으로 신생"]
    end
특성Delta LakeApache Iceberg
개발사DatabricksNetflix→Apache
Spark 지원최고좋음
Flink 지원제한적좋음
Trino 지원좋음좋음
파티셔닝명시적Hidden (투명)
채택율높음증가 중

선택 가이드

flowchart TB
    Q1{"Databricks<br/>사용?"}
    Q2{"Flink<br/>필요?"}
    Q3{"벤더 중립<br/>중요?"}
    
    Q1 -->|"예"| Delta["Delta Lake"]
    Q1 -->|"아니오"| Q2
    Q2 -->|"예"| Iceberg["Apache Iceberg"]
    Q2 -->|"아니오"| Q3
    Q3 -->|"예"| Iceberg
    Q3 -->|"아니오"| Delta

아키텍처 결정 가이드

언제 무엇을 선택하나?

flowchart TB
    subgraph Decision ["결정 트리"]
        D1{"데이터 크기?"}
        D2{"팀 SQL 역량?"}
        D3{"ML 워크로드?"}
        D4{"예산?"}
        
        D1 -->|"< 100GB"| DW["Data Warehouse<br/>(BigQuery, Snowflake)"]
        D1 -->|">= 100GB"| D2
        D2 -->|"SQL 위주"| DW
        D2 -->|"Python/Spark 혼합"| D3
        D3 -->|"ML 중요"| LH["Lakehouse<br/>(Delta Lake)"]
        D3 -->|"분석 위주"| D4
        D4 -->|"비용 민감"| LH
        D4 -->|"관리 편의"| DW
    end

정리

mindmap
  root((데이터<br/>저장소))
    Data Warehouse
      구조화된 데이터
      ACID
      SQL 최적화
      비용 높음
    Data Lake
      모든 데이터
      저렴
      ACID 없음
      품질 문제
    Lakehouse
      레이크 + ACID
      Delta Lake
      Iceberg
      최신 트렌드
    Medallion
      Bronze: 원본
      Silver: 정제
      Gold: 집계
    Delta Lake
      트랜잭션 로그
      Time Travel
      Schema Evolution
      MERGE

다음 편 예고

11편: 데이터 모델링에서는 분석용 모델링을 다룹니다:

  • Star Schema vs Snowflake Schema
  • Fact Table vs Dimension Table
  • Slowly Changing Dimensions (SCD)

참고 자료

Share

Related Articles

Comments

이 블로그는 제가 알고 있는 것들을 잊지 않기 위해 기록하는 공간입니다.
직접 작성한 글도 있고, AI의 도움을 받아 정리한 글도 있습니다.
정확하지 않은 내용이 있을 수 있으니 참고용으로 봐주세요.

© 2026 Seogyu Kim