1086 words
5 minutes
[DE Design Pattern]04-5. Immutable Dataset과 Proxy 패턴
5. Immutable Dataset && Proxy
Pattern Overview
- 과거 모든 버전을 보존
- 데이터를 한 번만 쓰고(immutable), 소비자에게는 프록시(뷰)를 통해 최신 버전만 노출
- 레이어를 하나 더 두는 것
구현 방식
방식 1: 뷰 기반 (View-based)
- 매번 새 테이블에 쓰고, 뷰가 최신 테이블을 가리키도록 갱신
from datetime import datetime
def proxy_view_based(db, execution_time: datetime, data): # 실행 시점으로 고유한 내부 테이블명 생성 table_suffix = execution_time.strftime('%Y%m%d_%H%M%S') internal_table = f"devices_internal_{table_suffix}"
# 1. 새 테이블에 데이터 적재 (한 번만 쓰기) db.execute(f"CREATE TABLE {internal_table} (type TEXT, full_name TEXT, version TEXT)") db.execute(f"COPY {internal_table} FROM '{data}'")
# 2. 뷰를 최신 테이블로 갱신 db.execute(f"CREATE OR REPLACE VIEW devices AS SELECT * FROM {internal_table}")
# 3. 이전 테이블의 쓰기 권한 제거 (불변성 강제) db.execute(f"REVOKE INSERT, UPDATE, DELETE ON {internal_table} FROM pipeline_user")
# 재실행 시: 다른 timestamp → 다른 테이블명 → 이전 데이터 보존proxy_view_based(db, datetime(2024, 1, 15, 10, 0), "/data/v1.csv")# → devices_internal_20240115_100000
proxy_view_based(db, datetime(2024, 1, 15, 10, 5), "/data/v1.csv")# → devices_internal_20240115_100500 (별도 테이블, 이전 것도 남아있음)방식 2: 매니페스트 기반 (Manifest-based)
- 뷰를 지원하지 않는 저장소(오브젝트 스토어 등)에서 사용다. 매니페스트 파일이 “어떤 경로를 읽어야하는지 표시”
import json
def proxy_manifest_based(storage, execution_time: datetime, data_path: str): # 1. 새 경로에 데이터 쓰기 versioned_path = f"s3://lake/devices/{execution_time.isoformat()}/" storage.copy(data_path, versioned_path)
# 2. 매니페스트 파일 갱신 — 최신 경로만 가리킴 manifest = {"current_version": versioned_path, "updated_at": execution_time.isoformat()} storage.write("s3://lake/devices/_manifest.json", json.dumps(manifest))
# 3. 이전 버전 파일에 WORM 잠금 적용 previous_versions = storage.list("s3://lake/devices/", exclude="_manifest.json") for version_path in previous_versions: if version_path != versioned_path: storage.set_object_lock(version_path) # Write Once Read Many
# 소비자는 매니페스트를 읽어서 최신 경로를 알아냄def read_latest(storage): manifest = json.loads(storage.read("s3://lake/devices/_manifest.json")) return storage.read(manifest["current_version"])방식 3: 버전 기반 (Versioned approach)
Delta Lake, Iceberg, BigQuery처럼 내부적으로 버전을 관리하는 스토어사용
def proxy_versioned(spark, data): # 그냥 overwrite — 이전 버전은 테이블 포맷이 자동 보존 data.write.format("delta").mode("overwrite").save("s3://lake/devices")
# 과거 버전 조회도 가능 # spark.read.format("delta").option("versionAsOf", 3).load("s3://lake/devices")별도의 뷰나 매니페스트 없이, 항상 최신 버전을 기본으로 읽고 필요 시 과거 버전을 조회.
오브젝트 스토어의 WORM(Write Once Read Many)
| 클라우드 | WORM 기능 |
|---|---|
| AWS S3 | Object Lock |
| Azure Blob | Immutability Policies |
| GCP Cloud Storage | Object Holds / Bucket Locks |
Airflow에서 고유 테이블명 생성
from airflow.operators.python import get_current_context
def get_devices_table_name() -> str: """파이프라인 시작 시간으로 고유 테이블명 생성""" context = get_current_context() dag_run = context['dag_run'] table_suffix = dag_run.start_date.strftime('%Y%m%d_%H%M%S_%f') return f"dedp.devices_internal_{table_suffix}"
# 같은 파이프라인 인스턴스 재실행 → 다른 start_date → 다른 테이블# → 이전 테이블은 보존됨 → 불변성 유지주의점
- 불변성 강제 — 오케스트레이션 레벨에서 output을 설정하는 것만으로는 부족.인프라 레벨에서도 쓰기 권한 제거나 WORM 잠금을 적용해야, 실수로라도 이전 테이블이 수정되는 것을 방지할 수 있음
- 매니페스트 생성 분리 — 매니페스트(또는 뷰 갱신)는 데이터 처리 잡과 별도 태스크로 분리
- 버전 기반 방식의 retention 제한 — BigQuery는 7일, Delta Lake은 설정에 따라 다르지만, retention 이후에는 과거 버전이 삭제
Concept
- Proxy 패턴 : 불변 데이터셋 위에 간접 레이어(뷰/매니페스트)를 두어, 소비자에게는 최신 버전만 노출하면서 모든 과거 버전을 보존하는 패턴
- 뷰 기반 구현 : 버전별 테이블을 생성하고, DB 뷰가 항상 최신 테이블을 가리키도록 갱신하는 방식. DW, RDBMS에 적합
- 매니페스트 기반 구현 : 뷰를 지원하지 않는 오브젝트 스토어에서, JSON 매니페스트 파일로 최신 데이터 경로를 관리하는 방식
- 버전 기반 구현 : Delta Lake, Iceberg, BigQuery처럼 내부적으로 버전을 관리하는 스토어에서 overwrite만으로 과거 버전이 자동 보존되는 방식
- WORM (Write Once Read Many) : 오브젝트 스토어에서 한 번 쓴 파일을 수정/삭제할 수 없게 잠그는 메커니즘. 불변성을 인프라 레벨에서 강제
- Writable-once 시맨틱스 : 테이블 생성 후 쓰기 권한을 제거하여, 데이터가 한 번만 기록되도록 보장하는 접근 방식
[DE Design Pattern]04-5. Immutable Dataset과 Proxy 패턴
https://yjinheon.netlify.app/posts/02de/de-design-pattern/04-idempotency/04-05_proxy_pattern/