본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 12. · 11 Views
Delta Lake 핵심 개념과 아키텍처 완벽 가이드
Delta Lake의 트랜잭션 로그, Parquet 포맷, 체크포인트 등 핵심 아키텍처를 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어냈습니다. 데이터 엔지니어링의 필수 지식을 술술 읽히는 이북 스타일로 배워보세요.
목차
- 트랜잭션 로그(_delta_log) 이해하기
- Parquet 파일과 Delta 포맷의 관계
- 체크포인트와 로그 압축
- 메타데이터 관리 방식
- Delta Lake 파일 구조 분석
- 버전 관리 메커니즘
1. 트랜잭션 로그( delta log) 이해하기
어느 날 김데이터 씨는 팀 리더로부터 흥미로운 질문을 받았습니다. "우리 데이터 레이크에 동시에 여러 명이 데이터를 쓰면 어떻게 되죠?" 사실 김데이터 씨도 궁금했던 부분입니다.
일반 Parquet 파일은 이런 상황에서 문제가 생기기 쉬운데, Delta Lake는 어떻게 처리할까요?
트랜잭션 로그는 Delta Lake의 핵심 메커니즘으로, 모든 변경 사항을 순차적으로 기록하는 일종의 변경 이력서입니다. 마치 은행 거래 내역처럼 누가 언제 무엇을 바꿨는지 정확하게 추적할 수 있습니다.
이를 통해 데이터의 일관성과 동시성 제어가 가능해집니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
from pyspark.sql import SparkSession
# Spark 세션 생성
spark = SparkSession.builder.appName("DeltaLog").getOrCreate()
# Delta 테이블의 트랜잭션 히스토리 조회
deltaTable = DeltaTable.forPath(spark, "/path/to/delta-table")
# 모든 변경 이력 확인 - _delta_log 디렉토리의 내용을 읽습니다
history = deltaTable.history()
history.show()
# 특정 버전의 데이터 읽기 - 시간 여행 기능
df = spark.read.format("delta").option("versionAsOf", 0).load("/path/to/delta-table")
df.show()
김데이터 씨는 입사 6개월 차 주니어 데이터 엔지니어입니다. 최근 회사에서 기존 Parquet 기반 데이터 레이크를 Delta Lake로 마이그레이션하는 프로젝트를 시작했습니다.
팀 리더는 김데이터 씨에게 Delta Lake의 핵심 아키텍처를 공부해오라고 했습니다. 김데이터 씨가 가장 먼저 마주한 개념이 바로 _delta_log 디렉토리였습니다.
"이게 뭐지?" 처음 보는 폴더 이름에 당황했습니다. 선배 개발자 박빅데이터 씨가 옆에서 설명을 시작했습니다.
"Delta Lake의 가장 중요한 비밀이 바로 저 폴더에 있어요." 그렇다면 트랜잭션 로그란 정확히 무엇일까요? 쉽게 비유하자면, 트랜잭션 로그는 마치 건물의 공사 일지와 같습니다.
건물을 지을 때 누가 언제 어떤 작업을 했는지 날짜별로 기록하듯이, Delta Lake도 데이터에 대한 모든 변경 사항을 시간 순서대로 기록합니다. 1층을 쌓고, 2층을 쌓고, 창문을 달고...
이런 모든 과정이 기록되는 것처럼, 데이터의 추가, 수정, 삭제도 모두 기록됩니다. 일반 Parquet 파일만 사용할 때는 어땠을까요?
여러 사람이 동시에 같은 데이터를 수정하려고 하면 문제가 발생했습니다. A 팀원이 데이터를 업데이트하는 동안 B 팀원도 같은 데이터를 수정하면, 누구의 변경 사항이 최종적으로 반영될지 알 수 없었습니다.
더 큰 문제는 데이터가 손상되거나 일부만 반영되는 경우도 있었다는 점입니다. 또한 "어제 저녁 6시 시점의 데이터를 다시 보고 싶어요"라는 요구사항이 오면 난감했습니다.
일반 파일 시스템에서는 이런 시간 여행이 불가능했기 때문입니다. 바로 이런 문제를 해결하기 위해 Delta Lake의 트랜잭션 로그가 등장했습니다.
트랜잭션 로그를 사용하면 ACID 트랜잭션이 가능해집니다. 데이터베이스에서나 가능했던 원자성, 일관성, 격리성, 지속성을 데이터 레이크에서도 보장받을 수 있습니다.
또한 동시성 제어도 자동으로 처리됩니다. 무엇보다 과거 어느 시점의 데이터든 조회할 수 있다는 큰 이점이 있습니다.
트랜잭션 로그의 내부 구조를 살펴보겠습니다. _delta_log 디렉토리를 열어보면 00000000000000000000.json, 00000000000000000001.json 같은 파일들이 보입니다.
각 파일은 하나의 트랜잭션을 나타냅니다. 파일 이름의 숫자는 버전 번호입니다.
첫 번째 트랜잭션은 0번, 두 번째는 1번, 이런 식으로 증가합니다. 각 JSON 파일 안에는 무엇이 들어있을까요?
add, remove, metaData 같은 액션이 기록됩니다. add는 새로운 파일이 추가되었다는 의미이고, remove는 파일이 삭제되었다는 의미입니다.
metaData는 테이블의 스키마나 설정 정보를 담고 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 DeltaTable.forPath() 메서드로 Delta 테이블 객체를 생성합니다. 이 객체를 통해 트랜잭션 로그에 접근할 수 있습니다.
다음으로 history() 메서드를 호출하면 모든 변경 이력이 DataFrame으로 반환됩니다. 여기에는 버전 번호, 타임스탬프, 작업 내용, 사용자 정보 등이 포함됩니다.
마지막으로 versionAsOf 옵션을 사용하면 특정 버전의 데이터를 읽을 수 있습니다. 이것이 바로 타임 트래블 기능입니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 전자상거래 회사에서 주문 데이터를 관리한다고 가정해봅시다.
실수로 잘못된 배치 작업이 실행되어 오늘 오전 데이터가 망가졌다면, 트랜잭션 로그를 통해 어제 밤 시점으로 되돌릴 수 있습니다. 또한 "지난주 월요일 매출이 정확히 얼마였는지" 감사 목적으로 조회할 때도 유용합니다.
하지만 주의할 점도 있습니다. 트랜잭션 로그 파일은 계속 쌓입니다.
1년 동안 매일 업데이트한다면 365개의 JSON 파일이 생성됩니다. 이렇게 파일이 많아지면 메타데이터 읽기 성능이 저하될 수 있습니다.
따라서 Delta Lake는 주기적으로 체크포인트를 생성하여 이 문제를 해결합니다. 이는 다음 섹션에서 자세히 다루겠습니다.
다시 김데이터 씨의 이야기로 돌아가 봅시다. 박빅데이터 씨의 설명을 들은 김데이터 씨는 감탄했습니다.
"와, 이렇게 동작하는 거였군요!" 트랜잭션 로그를 제대로 이해하면 Delta Lake의 모든 기능이 어떻게 가능한지 알 수 있습니다. 여러분도 실제 프로젝트에서 history() 메서드로 변경 이력을 확인해 보세요.
실전 팁
💡 - **deltaTable.history().limit(10)**으로 최근 10개 변경 이력만 조회하면 성능이 향상됩니다
- DESCRIBE HISTORY SQL 명령어로도 동일한 정보를 조회할 수 있습니다
- 트랜잭션 로그는 절대 수동으로 수정하거나 삭제하면 안 됩니다
2. Parquet 파일과 Delta 포맷의 관계
김데이터 씨가 _delta_log 디렉토리를 이해하고 나니, 또 다른 의문이 생겼습니다. "그런데 실제 데이터는 어디에 저장되나요?" 폴더를 살펴보니 .parquet 확장자를 가진 파일들이 보입니다.
Delta Lake인데 왜 Parquet 파일이 있는 걸까요?
Delta Lake는 실제 데이터를 Parquet 포맷으로 저장합니다. Delta 포맷은 별도의 파일 형식이 아니라, Parquet 파일 위에 트랜잭션 로그를 추가한 것입니다.
마치 일반 노트북에 목차와 색인을 붙여 체계적으로 만든 것과 같습니다. 이를 통해 Parquet의 장점은 유지하면서 ACID 트랜잭션 기능을 추가할 수 있습니다.
다음 코드를 살펴봅시다.
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("ParquetDelta").getOrCreate()
# 일반 DataFrame 생성
data = [("Alice", 25), ("Bob", 30), ("Charlie", 35)]
df = spark.createDataFrame(data, ["name", "age"])
# Parquet로 저장 - 일반 파일 형식
df.write.format("parquet").mode("overwrite").save("/path/to/parquet-table")
# Delta로 저장 - Parquet + 트랜잭션 로그
df.write.format("delta").mode("overwrite").save("/path/to/delta-table")
# 두 폴더를 비교하면 Delta는 _delta_log 디렉토리가 추가되어 있습니다
# 하지만 실제 데이터 파일은 둘 다 .parquet 확장자를 사용합니다
박빅데이터 씨는 김데이터 씨의 질문에 미소를 지었습니다. "좋은 질문이에요.
많은 사람들이 헷갈려하는 부분이죠." 김데이터 씨는 Delta Lake가 완전히 새로운 파일 형식인 줄 알았습니다. 그런데 폴더를 열어보니 .parquet 파일들이 가득했습니다.
"이게 무슨 일이지?" 박빅데이터 씨가 화이트보드를 꺼내들며 설명을 시작했습니다. Delta Lake와 Parquet의 관계는 어떻게 될까요?
쉽게 비유하자면, Delta Lake는 마치 스마트폰 케이스와 같습니다. 스마트폰 자체는 그대로인데, 케이스를 씌워서 보호 기능과 추가 기능을 더한 것입니다.
스마트폰이 Parquet라면, 케이스가 Delta Lake의 트랜잭션 로그입니다. 본질은 같지만 더 안전하고 더 많은 기능을 사용할 수 있게 됩니다.
Parquet만 사용할 때는 어떤 한계가 있었을까요? Parquet는 컬럼 기반 저장 포맷으로 분석 쿼리에 매우 효율적입니다.
압축률도 좋고, 읽기 성능도 훌륭합니다. 하지만 업데이트나 삭제가 불가능했습니다.
데이터를 수정하려면 전체 파일을 다시 써야 했습니다. 또한 스키마 변경도 어려웠습니다.
새로운 컬럼을 추가하거나 기존 컬럼의 타입을 바꾸려면 모든 데이터를 다시 처리해야 했습니다. 대용량 데이터에서는 이것이 큰 부담이었습니다.
Delta Lake는 이 문제를 어떻게 해결했을까요? Delta Lake는 Parquet 파일 자체를 건드리지 않습니다.
대신 트랜잭션 로그에 메타데이터를 기록합니다. "이 Parquet 파일의 이 레코드는 삭제되었음", "저 Parquet 파일이 새로 추가되었음" 같은 정보를 로그에 기록하는 것입니다.
예를 들어 사용자가 DELETE 쿼리를 실행하면 어떻게 될까요? Delta Lake는 실제 Parquet 파일을 수정하지 않습니다.
대신 새로운 Parquet 파일을 생성하여 삭제되지 않은 데이터만 담고, 트랜잭션 로그에 "기존 파일은 remove, 새 파일은 add"라고 기록합니다. 위의 코드를 살펴보겠습니다.
Parquet로 저장할 때는 format("parquet")를 사용합니다. 이렇게 하면 지정한 경로에 .parquet 파일만 생성됩니다.
반면 Delta로 저장할 때는 format("delta")를 사용합니다. 이렇게 하면 .parquet 파일과 함께 _delta_log 디렉토리가 생성됩니다.
중요한 점은 두 경우 모두 실제 데이터는 Parquet 포맷으로 저장된다는 것입니다. Delta Lake는 파일 형식을 바꾸지 않습니다.
단지 트랜잭션 로그를 추가할 뿐입니다. 실제 현업에서는 어떻게 활용할까요?
금융 회사에서 거래 데이터를 관리한다고 가정해봅시다. 기존에는 Parquet로 저장하다가 Delta Lake로 마이그레이션하려고 합니다.
다행히 기존 Parquet 파일을 그대로 둔 채로 Delta 테이블로 변환할 수 있습니다. CONVERT TO DELTA 명령어를 사용하면 기존 Parquet 파일에 트랜잭션 로그만 추가됩니다.
이렇게 하면 데이터를 복사하거나 다시 쓸 필요 없이 Delta Lake의 모든 기능을 바로 사용할 수 있습니다. 시간과 비용을 크게 절약할 수 있는 것입니다.
하지만 주의할 점도 있습니다. Delta Lake를 사용하더라도 개별 Parquet 파일의 크기는 여전히 중요합니다.
너무 작은 파일이 많으면 small files problem이 발생합니다. 반대로 너무 큰 파일은 병렬 처리 효율이 떨어집니다.
일반적으로 100MB~1GB 정도가 적당합니다. 또한 Parquet 파일 자체는 변경 불가능합니다.
Delta Lake가 "수정"이나 "삭제"를 지원한다고 해서 Parquet 파일이 변경되는 것은 아닙니다. 실제로는 새로운 파일을 생성하고 로그에 기록하는 방식입니다.
김데이터 씨는 이제 Delta Lake의 작동 원리를 이해했습니다. "그래서 Delta를 Parquet의 상위 호환이라고 부르는 거군요!" Delta Lake를 사용하면 Parquet의 모든 장점을 유지하면서 트랜잭션 기능까지 얻을 수 있습니다.
여러분의 프로젝트에서도 기존 Parquet 데이터를 Delta로 변환해 보세요.
실전 팁
💡 - **spark.read.format("parquet")**로 Delta 테이블의 기본 Parquet 파일을 직접 읽을 수 있지만, 트랜잭션 로그를 무시하므로 권장하지 않습니다
- Delta 테이블의 Parquet 파일은 수동으로 삭제하면 안 됩니다. VACUUM 명령어를 사용하세요
- Parquet 파일 압축 코덱은 snappy가 기본값이며, 용도에 따라 gzip이나 zstd로 변경할 수 있습니다
3. 체크포인트와 로그 압축
김데이터 씨가 회사 프로덕션 Delta 테이블을 살펴보다가 이상한 파일을 발견했습니다. _delta_log 디렉토리에 .checkpoint.parquet 확장자를 가진 파일들이 있었습니다.
"이건 또 뭐지?" 트랜잭션 로그 JSON 파일은 이해했는데, 체크포인트는 처음 봅니다.
체크포인트는 여러 개의 트랜잭션 로그를 주기적으로 하나의 Parquet 파일로 압축하는 메커니즘입니다. 마치 책의 각 장마다 요약본을 만들어두는 것과 같습니다.
이를 통해 Delta Lake는 수백 개의 JSON 파일을 읽지 않고도 테이블의 현재 상태를 빠르게 파악할 수 있습니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName("Checkpoint").getOrCreate()
# Delta 테이블 로드
deltaTable = DeltaTable.forPath(spark, "/path/to/delta-table")
# 수동으로 체크포인트 생성 (보통은 자동으로 생성됨)
# 기본적으로 10개의 트랜잭션마다 자동 생성됩니다
spark.sql("OPTIMIZE '/path/to/delta-table'")
# 체크포인트 간격 설정 변경
spark.sql("""
ALTER TABLE delta.`/path/to/delta-table`
SET TBLPROPERTIES (delta.checkpointInterval = 20)
""")
# _delta_log 디렉토리의 체크포인트 파일 확인
# 00000000000000000010.checkpoint.parquet 같은 파일이 생성됩니다
박빅데이터 씨는 김데이터 씨가 체크포인트 파일을 발견한 것을 보고 칭찬했습니다. "관찰력이 좋네요.
체크포인트는 Delta Lake의 성능 비밀이에요." 김데이터 씨는 궁금했습니다. JSON 파일로 충분할 것 같은데, 왜 또 다른 형태의 파일이 필요할까요?
박빅데이터 씨가 실제 사례를 들어 설명했습니다. 체크포인트가 필요한 이유는 무엇일까요?
쉽게 비유하자면, 체크포인트는 마치 학기말 성적표와 같습니다. 매일매일의 과제 점수와 출석 기록을 일일이 확인하는 대신, 학기말 성적표 하나만 보면 최종 결과를 알 수 있습니다.
체크포인트도 마찬가지로 수백 개의 트랜잭션을 일일이 읽지 않고, 체크포인트 파일 하나만 읽으면 테이블의 현재 상태를 파악할 수 있습니다. 체크포인트가 없다면 어떤 문제가 발생할까요?
Delta 테이블에 1년 동안 매일 데이터를 업데이트했다고 가정해봅시다. 하루에 한 번씩만 업데이트해도 365개의 트랜잭션 로그 파일이 생성됩니다.
하루에 여러 번 업데이트한다면 수천 개가 될 수도 있습니다. Delta Lake가 테이블을 읽을 때마다 이 모든 JSON 파일을 순차적으로 읽어야 한다면 어떻게 될까요?
메타데이터 읽기 시간이 데이터 읽기 시간보다 더 오래 걸릴 수 있습니다. 이것은 심각한 성능 문제입니다.
체크포인트는 이 문제를 어떻게 해결할까요? Delta Lake는 기본적으로 10개의 트랜잭션마다 자동으로 체크포인트를 생성합니다.
예를 들어 트랜잭션 0번부터 9번까지 실행되면, 10번째 트랜잭션이 끝날 때 체크포인트가 생성됩니다. 이 체크포인트 파일에는 0번부터 9번까지의 모든 변경 사항이 압축되어 저장됩니다.
다음에 테이블을 읽을 때는 어떻게 될까요? Delta Lake는 10개의 JSON 파일을 읽는 대신, 체크포인트 파일 하나만 읽습니다.
그리고 그 이후의 트랜잭션 로그만 추가로 읽으면 됩니다. 읽어야 할 파일 개수가 대폭 줄어드는 것입니다.
위의 코드를 살펴보겠습니다. checkpointInterval 속성을 통해 체크포인트 생성 주기를 조정할 수 있습니다.
기본값은 10이지만, 20이나 50으로 변경할 수도 있습니다. 트랜잭션이 자주 발생하는 테이블은 간격을 늘리고, 가끔 업데이트되는 테이블은 간격을 줄이는 것이 좋습니다.
체크포인트 파일의 이름을 보면 00000000000000000010.checkpoint.parquet 같은 형식입니다. 숫자는 해당 체크포인트가 몇 번째 버전까지의 정보를 담고 있는지 나타냅니다.
실제 현업에서는 어떻게 활용할까요? 스트리밍 데이터 파이프라인에서 Delta Lake를 사용한다고 가정해봅시다.
실시간으로 데이터가 계속 들어와서 초당 여러 번 업데이트가 발생합니다. 하루면 수만 개의 트랜잭션 로그가 쌓일 수 있습니다.
이런 상황에서 체크포인트는 필수입니다. 체크포인트 덕분에 쿼리 시작 시간이 몇 분에서 몇 초로 단축될 수 있습니다.
특히 Structured Streaming 작업에서는 체크포인트가 성능의 핵심입니다. 하지만 주의할 점도 있습니다.
체크포인트 생성 자체도 비용이 듭니다. 모든 트랜잭션 로그를 읽어서 하나의 Parquet 파일로 만드는 작업은 시간이 걸립니다.
따라서 checkpointInterval을 너무 작게 설정하면 오히려 성능이 저하될 수 있습니다. 또한 체크포인트 파일도 삭제되지 않고 계속 쌓입니다.
정기적으로 VACUUM 명령어를 실행하여 오래된 체크포인트를 정리해야 합니다. 기본적으로 7일이 지난 체크포인트는 삭제 대상입니다.
김데이터 씨는 이제 _delta_log 디렉토리의 모든 파일이 어떤 역할을 하는지 이해했습니다. "JSON 파일은 개별 변경 사항, 체크포인트는 누적 상태를 저장하는 거군요!" 박빅데이터 씨가 고개를 끄덕였습니다.
"정확해요. Delta Lake의 이중 메커니즘이죠." 체크포인트를 이해하면 Delta Lake의 성능을 최적화할 수 있습니다.
여러분의 프로젝트에서도 체크포인트 간격을 테이블 특성에 맞게 조정해 보세요.
실전 팁
💡 - 체크포인트 파일은 자동으로 생성되므로 수동 관리가 필요 없습니다
- 매우 큰 테이블에서는 checkpointInterval을 50이나 100으로 늘리면 쓰기 성능이 향상됩니다
- _last_checkpoint 파일은 최신 체크포인트 위치를 캐싱하여 읽기 성능을 더욱 높입니다
4. 메타데이터 관리 방식
김데이터 씨는 이제 Delta Lake의 기본 구조를 이해했습니다. 하지만 팀 리더가 새로운 질문을 던졌습니다.
"테이블에 파티션이 100만 개인데, Delta Lake는 이걸 어떻게 관리하죠?" 김데이터 씨도 궁금해졌습니다. 파일이 수십만 개라면 메타데이터만 해도 엄청난 양일 텐데요.
Delta Lake는 메타데이터를 트랜잭션 로그에 JSON과 Parquet 형식으로 이중 저장하여 관리합니다. 테이블 스키마, 파티션 정보, 파일 통계 등 모든 메타데이터가 버전별로 추적됩니다.
마치 도서관이 책의 목록과 위치를 체계적으로 관리하듯, Delta Lake도 데이터의 모든 정보를 정밀하게 추적합니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
spark = SparkSession.builder.appName("Metadata").getOrCreate()
# 테이블 생성 시 메타데이터 정의
df = spark.range(0, 1000000)
df.withColumn("date", col("id") % 365) \
.write.format("delta") \
.partitionBy("date") \
.option("delta.dataSkippingNumIndexedCols", 32) \
.save("/path/to/partitioned-table")
# 메타데이터 조회 - DESCRIBE DETAIL로 테이블 정보 확인
spark.sql("DESCRIBE DETAIL delta.`/path/to/partitioned-table`").show()
# 스키마 정보 조회
deltaTable = DeltaTable.forPath(spark, "/path/to/partitioned-table")
print(deltaTable.toDF().schema)
# 파일 레벨 통계 확인 (min/max 값)
detail = spark.sql("DESCRIBE DETAIL delta.`/path/to/partitioned-table`")
detail.select("numFiles", "sizeInBytes").show()
박빅데이터 씨는 김데이터 씨를 회의실로 불렀습니다. "메타데이터 관리는 Delta Lake의 핵심 차별점이에요.
이걸 이해하면 왜 Delta가 빠른지 알 수 있어요." 김데이터 씨는 메타데이터가 무엇인지는 알고 있었습니다. 테이블의 스키마, 파티션 구조, 통계 정보 같은 것들이죠.
하지만 Delta Lake가 이것을 어떻게 관리하는지는 몰랐습니다. 박빅데이터 씨가 화이트보드에 그림을 그리기 시작했습니다.
Delta Lake의 메타데이터 관리는 어떻게 다를까요? 쉽게 비유하자면, Delta Lake의 메타데이터는 마치 백과사전의 색인과 같습니다.
책의 본문을 모두 읽지 않고도 색인만 보면 원하는 정보가 몇 페이지에 있는지 바로 알 수 있습니다. 마찬가지로 Delta Lake도 실제 데이터 파일을 열어보지 않고도 메타데이터만으로 어떤 파일에 원하는 데이터가 있는지 파악할 수 있습니다.
전통적인 데이터 레이크에서는 메타데이터 관리가 어려웠습니다. Hive 메타스토어 같은 외부 시스템에 의존했습니다.
문제는 실제 파일과 메타스토어의 정보가 동기화되지 않을 위험이 있다는 점이었습니다. 누군가 파일을 직접 삭제하면 메타스토어는 여전히 그 파일이 존재한다고 믿습니다.
또한 스키마를 변경할 때도 복잡했습니다. 새로운 컬럼을 추가하려면 메타스토어를 업데이트하고, 기존 데이터와 새 데이터의 스키마 차이를 수동으로 처리해야 했습니다.
Delta Lake는 이 문제를 근본적으로 해결했습니다. 모든 메타데이터를 트랜잭션 로그 안에 저장합니다.
테이블을 처음 생성할 때 metaData 액션이 로그에 기록됩니다. 이 액션에는 스키마 정보, 파티션 컬럼, 테이블 속성 등이 포함됩니다.
파일이 추가될 때는 어떻게 될까요? add 액션에 파일 경로뿐만 아니라 파일 레벨 통계도 함께 저장됩니다.
예를 들어 날짜 컬럼의 최솟값과 최댓값이 기록됩니다. 이를 통해 Delta Lake는 쿼리 실행 시 불필요한 파일을 건너뛸 수 있습니다.
이것을 데이터 스키핑이라고 부릅니다. "2024년 1월 데이터만 조회"하는 쿼리가 들어오면, Delta Lake는 메타데이터를 보고 1월 데이터가 없는 파일은 아예 읽지 않습니다.
이것이 성능 향상의 핵심입니다. 위의 코드를 살펴보겠습니다.
partitionBy("date")로 파티션을 지정하면, 이 정보가 메타데이터에 기록됩니다. dataSkippingNumIndexedCols 옵션은 통계를 수집할 컬럼 개수를 지정합니다.
기본값은 32개인데, 필요에 따라 조정할 수 있습니다. DESCRIBE DETAIL 명령어는 테이블의 메타데이터를 보여줍니다.
파일 개수, 총 크기, 파티션 컬럼, 생성 시각 등을 확인할 수 있습니다. 이 모든 정보가 트랜잭션 로그에서 읽혀집니다.
실제 현업에서는 어떻게 활용할까요? 이커머스 회사에서 주문 데이터를 날짜별로 파티셔닝한다고 가정해봅시다.
3년치 데이터가 쌓여서 파일이 수십만 개입니다. "지난주 주문량 분석" 쿼리를 실행하면 어떻게 될까요?
Delta Lake는 메타데이터를 보고 지난주 날짜 범위에 해당하는 파티션만 읽습니다. 나머지 수십만 개 파일은 완전히 무시됩니다.
쿼리가 몇 시간이 아니라 몇 초 만에 끝납니다. 더 나아가 Z-Ordering이나 Liquid Clustering 같은 최적화 기법을 사용하면, 파티션 내에서도 데이터를 재정렬하여 스키핑 효율을 더욱 높일 수 있습니다.
하지만 주의할 점도 있습니다. 메타데이터 통계는 파일이 추가될 때 계산됩니다.
따라서 파일 크기가 너무 크면 통계 수집에 시간이 걸릴 수 있습니다. 또한 너무 많은 컬럼의 통계를 수집하면 메타데이터 자체가 커져서 역효과가 날 수 있습니다.
스키마 진화도 고려해야 합니다. Delta Lake는 Schema Evolution을 지원하지만, 호환되지 않는 변경(예: 타입 변경)은 신중해야 합니다.
기존 데이터와 새 스키마가 충돌할 수 있습니다. 김데이터 씨는 이제 Delta Lake가 왜 빠른지 이해했습니다.
"메타데이터를 활용한 스마트한 최적화군요!" 박빅데이터 씨가 미소 지었습니다. "그렇죠.
메타데이터는 Delta Lake의 두뇌예요." 메타데이터를 잘 관리하면 쿼리 성능을 몇 배에서 몇십 배까지 향상시킬 수 있습니다. 여러분의 프로젝트에서도 파티셔닝과 통계 수집을 전략적으로 설계해 보세요.
실전 팁
💡 - delta.dataSkippingNumIndexedCols를 0으로 설정하면 통계 수집을 비활성화할 수 있습니다
- DESCRIBE HISTORY로 스키마 변경 이력을 추적할 수 있습니다
- 파티션 컬럼은 카디널리티가 너무 높지 않은 것을 선택하세요 (예: 날짜는 좋지만, UUID는 나쁨)
5. Delta Lake 파일 구조 분석
김데이터 씨는 팀 리더로부터 새로운 미션을 받았습니다. "프로덕션 Delta 테이블의 파일 구조를 분석해서 최적화 방안을 제안해 보세요." 김데이터 씨는 터미널을 열고 Delta 테이블 디렉토리를 살펴보기 시작했습니다.
생각보다 복잡한 구조에 당황했지만, 지금까지 배운 지식을 종합하면 이해할 수 있을 것 같습니다.
Delta Lake 테이블의 파일 구조는 데이터 파일(Parquet), 트랜잭션 로그(JSON), 체크포인트(Parquet), CRC 파일로 구성됩니다. 마치 잘 정리된 서류 보관함처럼, 각 파일이 명확한 역할을 가지고 체계적으로 배치되어 있습니다.
이 구조를 이해하면 성능 문제를 진단하고 최적화할 수 있습니다.
다음 코드를 살펴봅시다.
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, current_timestamp
import os
spark = SparkSession.builder.appName("FileStructure").getOrCreate()
# 샘플 Delta 테이블 생성
data_path = "/tmp/delta-file-structure"
df = spark.range(0, 100000).withColumn("timestamp", current_timestamp())
df.write.format("delta").mode("overwrite").save(data_path)
# 파일 구조 확인을 위한 셸 명령어 실행
# 실제로는 다음 명령어들을 터미널에서 실행합니다:
# ls -lah /tmp/delta-file-structure/
# ls -lah /tmp/delta-file-structure/_delta_log/
# Delta 테이블의 파일 목록 확인
files = spark.sql(f"LIST '{data_path}'")
files.show(truncate=False)
# _delta_log 디렉토리의 파일 확인
log_files = spark.sql(f"LIST '{data_path}/_delta_log'")
log_files.show(truncate=False)
# 실제 데이터 파일과 로그 파일의 관계 분석
detail = spark.sql(f"DESCRIBE DETAIL delta.`{data_path}`")
detail.select("numFiles", "sizeInBytes", "minReaderVersion", "minWriterVersion").show()
김데이터 씨는 프로덕션 테이블 디렉토리에 접속했습니다. ls 명령어를 실행하니 수많은 파일이 나열됩니다.
파일 이름만 봐도 패턴이 보이기 시작했습니다. 박빅데이터 씨가 옆에서 지켜보다가 조언을 건넸습니다.
"파일 구조를 이해하는 것이 Delta Lake 마스터의 첫걸음이에요." Delta Lake 디렉토리에는 무엇이 들어있을까요? 쉽게 비유하자면, Delta Lake 디렉토리는 마치 잘 정리된 회사 문서 보관소와 같습니다.
실제 업무 문서(데이터 파일)가 있고, 변경 이력서(_delta_log)가 있고, 정기 요약본(체크포인트)이 있습니다. 각각이 제 역할을 하면서 전체 시스템이 효율적으로 작동합니다.
가장 먼저 눈에 띄는 것은 Parquet 데이터 파일입니다. 파일 이름이 part-00000-xxx-xxx.snappy.parquet 같은 형식입니다.
이 파일들이 실제 데이터를 담고 있습니다. 파일 이름의 UUID 부분은 각 파일을 고유하게 식별하기 위한 것입니다.
다음으로 _delta_log 디렉토리가 보입니다. 이 안에 들어가면 여러 종류의 파일이 있습니다.
첫째, JSON 트랜잭션 로그 파일입니다. 00000000000000000000.json부터 시작하여 순차적으로 번호가 증가합니다.
각 파일은 하나의 트랜잭션을 나타냅니다. 둘째, 체크포인트 파일입니다.
00000000000000000010.checkpoint.parquet처럼 특정 버전마다 생성됩니다. 이 파일은 해당 버전까지의 모든 변경 사항을 압축한 것입니다.
셋째, _last_checkpoint 파일입니다. 이것은 작은 JSON 파일로, 가장 최근 체크포인트의 위치를 캐싱합니다.
Delta Lake가 테이블을 읽을 때 이 파일을 먼저 확인하여 어디서부터 읽어야 할지 빠르게 판단합니다. 넷째, .crc 파일들이 보일 수 있습니다.
이것은 체크섬 파일로, HDFS 같은 파일 시스템에서 데이터 무결성을 확인하기 위해 자동으로 생성됩니다. 파일들 사이의 관계는 어떻게 될까요?
트랜잭션 로그 파일은 데이터 파일을 가리킵니다. 로그를 열어보면 "add" 액션에 Parquet 파일 경로가 포함되어 있습니다.
반대로 데이터 파일은 로그를 모릅니다. 데이터 파일은 그냥 Parquet 포맷일 뿐입니다.
체크포인트 파일은 여러 트랜잭션 로그의 정보를 합친 것입니다. 0번부터 9번까지의 JSON 파일 내용이 10번 체크포인트에 모두 들어있습니다.
위의 코드를 살펴보겠습니다. LIST 명령어로 디렉토리의 파일 목록을 DataFrame으로 가져올 수 있습니다.
이를 통해 파일 크기, 수정 시각 등을 분석할 수 있습니다. DESCRIBE DETAIL은 테이블 레벨의 요약 정보를 제공합니다.
numFiles는 현재 활성 상태인 데이터 파일 개수입니다. 주의할 점은 디렉토리에 있는 모든 Parquet 파일이 활성 상태인 것은 아닙니다.
삭제된 파일도 VACUUM을 실행하기 전까지는 디렉토리에 남아있습니다. 실제 현업에서는 어떻게 활용할까요?
성능 문제가 발생했을 때 파일 구조를 분석하면 원인을 찾을 수 있습니다. 예를 들어 numFiles가 10만 개라면 small files problem입니다.
OPTIMIZE 명령어로 파일을 합쳐야 합니다. 반대로 개별 파일이 10GB씩이라면 병렬 처리 효율이 떨어집니다.
파티셔닝을 조정하거나 데이터를 더 작은 단위로 나눠야 합니다. _delta_log 디렉토리의 JSON 파일이 수천 개라면?
체크포인트가 제대로 생성되고 있는지 확인해야 합니다. checkpointInterval 설정을 점검하세요.
파일 구조를 최적화하는 전략은 무엇일까요? 첫째, 정기적인 OPTIMIZE 실행입니다.
특히 스트리밍 작업이나 자주 업데이트되는 테이블은 주기적으로 파일을 재구성해야 합니다. 둘째, VACUUM으로 오래된 파일 삭제입니다.
기본 retention 기간은 7일인데, 타임 트래블이 필요 없다면 더 짧게 설정할 수 있습니다. 셋째, 파티셔닝 전략 최적화입니다.
파티션별 데이터 크기가 균등하도록 파티션 키를 선택하세요. 하지만 주의할 점도 있습니다.
VACUUM을 실행하면 오래된 버전의 데이터에 접근할 수 없게 됩니다. 타임 트래블이 필요하다면 retention 기간을 길게 유지해야 합니다.
OPTIMIZE는 데이터를 다시 쓰는 작업이므로 비용이 듭니다. 테이블 크기가 크다면 실행 시간과 컴퓨팅 비용을 고려해야 합니다.
김데이터 씨는 프로덕션 테이블을 분석한 결과를 정리했습니다. "파일이 너무 많아서 OPTIMIZE가 필요하고, 체크포인트는 정상 작동하고 있습니다." 팀 리더가 만족스러운 표정을 지었습니다.
"정확한 분석이네요. 바로 최적화 작업을 진행해 보세요." 파일 구조를 이해하면 Delta Lake 테이블의 건강 상태를 진단할 수 있습니다.
여러분도 프로젝트의 테이블을 주기적으로 점검해 보세요.
실전 팁
💡 - spark.sql.files.maxPartitionBytes로 개별 파일의 최대 크기를 조정할 수 있습니다
- _delta_log 디렉토리를 수동으로 수정하면 테이블이 손상될 수 있으니 절대 금지입니다
- S3나 ADLS 같은 클라우드 스토리지에서는 .crc 파일이 생성되지 않을 수 있습니다
6. 버전 관리 메커니즘
김데이터 씨는 이제 Delta Lake의 마지막 퍼즐 조각을 맞출 차례입니다. 팀 리더가 말했습니다.
"Delta Lake의 진짜 파워는 버전 관리예요. Git처럼 데이터의 모든 버전을 추적할 수 있죠." 김데이터 씨는 궁금했습니다.
데이터가 수 테라바이트인데 어떻게 모든 버전을 저장할 수 있을까요?
Delta Lake의 버전 관리는 각 트랜잭션을 순차적인 버전으로 관리하여 시간 여행과 감사 추적을 가능하게 합니다. 마치 Git의 커밋 히스토리처럼, 데이터의 모든 변경 사항이 기록되고 언제든 과거 버전으로 되돌아갈 수 있습니다.
효율적인 메타데이터 관리 덕분에 실제 데이터를 복제하지 않고도 버전 추적이 가능합니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
spark = SparkSession.builder.appName("Versioning").getOrCreate()
# 초기 데이터 생성 - 버전 0
df = spark.range(0, 100).toDF("id")
df.write.format("delta").mode("overwrite").save("/tmp/versioned-table")
# 데이터 추가 - 버전 1
df_new = spark.range(100, 200).toDF("id")
df_new.write.format("delta").mode("append").save("/tmp/versioned-table")
# 데이터 수정 - 버전 2
deltaTable = DeltaTable.forPath(spark, "/tmp/versioned-table")
deltaTable.update(condition=col("id") < 50, set={"id": col("id") + 1000})
# 버전 히스토리 조회
history = deltaTable.history()
history.select("version", "timestamp", "operation", "operationMetrics").show(truncate=False)
# 특정 버전으로 시간 여행 - 버전 0으로 돌아가기
df_v0 = spark.read.format("delta").option("versionAsOf", 0).load("/tmp/versioned-table")
print(f"Version 0 count: {df_v0.count()}")
# 특정 타임스탬프로 시간 여행
df_yesterday = spark.read.format("delta").option("timestampAsOf", "2024-01-01").load("/tmp/versioned-table")
# 버전 복구 - 버전 0으로 롤백
deltaTable.restoreToVersion(0)
박빅데이터 씨는 김데이터 씨에게 마지막 강의를 시작했습니다. "이제 Delta Lake의 핵심 기능, 버전 관리를 배워봅시다." 김데이터 씨는 Git은 익숙했습니다.
코드를 커밋하고, 브랜치를 만들고, 예전 버전으로 되돌리는 것. 하지만 수 테라바이트 데이터에 같은 방식을 적용한다는 것이 신기했습니다.
박빅데이터 씨가 설명을 시작했습니다. Delta Lake의 버전 관리는 어떻게 작동할까요?
쉽게 비유하자면, Delta Lake는 마치 위키피디아의 문서 편집 이력과 같습니다. 위키피디아는 문서가 수정될 때마다 버전을 남깁니다.
누가 언제 무엇을 바꿨는지 모두 기록되고, "이전 버전 보기"를 클릭하면 과거 내용을 볼 수 있습니다. Delta Lake도 똑같이 데이터의 모든 변경을 버전으로 관리합니다.
버전 관리 없이는 어떤 문제가 있었을까요? 전통적인 데이터 레이크에서는 실수로 데이터를 잘못 삭제하거나 수정하면 복구가 불가능했습니다.
백업이 있다면 다행이지만, 백업도 보통 하루 단위로만 있어서 정확한 시점으로 되돌리기 어려웠습니다. 또한 "지난주 월요일 오후 3시 시점의 매출 데이터"를 조회하고 싶어도 방법이 없었습니다.
감사나 규정 준수를 위해 과거 데이터를 정확히 재현해야 할 때 큰 문제였습니다. Delta Lake는 이 문제를 어떻게 해결했을까요?
모든 트랜잭션이 순차적인 버전 번호를 부여받습니다. 테이블을 처음 생성하면 버전 0, 첫 번째 업데이트가 버전 1, 두 번째 업데이트가 버전 2입니다.
이렇게 계속 증가합니다. 중요한 점은 Delta Lake가 데이터를 복제하지 않는다는 것입니다.
변경된 부분만 새로운 Parquet 파일로 저장하고, 변경되지 않은 부분은 기존 파일을 그대로 참조합니다. 트랜잭션 로그에 "이 버전에서는 이 파일들이 활성 상태"라고 기록하는 방식입니다.
예를 들어 1TB 테이블에서 1GB만 수정했다면? 새로 생성되는 것은 1GB의 Parquet 파일뿐입니다.
나머지 999GB는 그대로 재사용됩니다. 이것이 Copy-on-Write 전략입니다.
위의 코드를 살펴보겠습니다. 처음 overwrite로 저장하면 버전 0이 생성됩니다.
append로 데이터를 추가하면 버전 1이 됩니다. update로 데이터를 수정하면 버전 2가 됩니다.
각 작업마다 새로운 트랜잭션 로그 파일이 생성됩니다. history() 메서드는 모든 버전의 이력을 보여줍니다.
버전 번호, 타임스탬프, 수행된 작업, 변경된 행 수 등이 포함됩니다. 이것이 감사 로그의 역할을 합니다.
versionAsOf 옵션을 사용하면 타임 트래블이 가능합니다. 버전 0 시점의 데이터를 읽으면 마치 시간을 되돌린 것처럼 그때의 데이터가 반환됩니다.
실제 파일은 변경되지 않았지만, 트랜잭션 로그를 통해 당시 활성 상태였던 파일만 읽는 것입니다. restoreToVersion() 메서드는 테이블을 과거 버전으로 롤백합니다.
이것은 실제로 새로운 트랜잭션을 생성하여 과거 버전의 파일 구성을 현재 버전으로 만듭니다. 실제 현업에서는 어떻게 활용할까요?
금융 기관에서 거래 데이터를 관리한다고 가정해봅시다. 감사 기관이 "6개월 전 특정 계좌의 거래 내역을 정확히 제출하라"고 요구했습니다.
Delta Lake를 사용하면 타임스탬프를 지정하여 그 시점의 데이터를 정확히 조회할 수 있습니다. 또 다른 사례는 A/B 테스트입니다.
새로운 데이터 변환 로직을 적용하기 전과 후를 비교하고 싶다면, 버전 관리를 통해 동일한 원본 데이터에 대한 두 가지 결과를 비교할 수 있습니다. 머신러닝 모델 학습에도 유용합니다.
"이 모델은 어떤 데이터로 학습했나?"라는 질문에 정확히 답할 수 있습니다. 데이터 버전과 모델 버전을 연결하여 재현 가능한 ML 파이프라인을 만들 수 있습니다.
하지만 주의할 점도 있습니다. 버전이 쌓일수록 스토리지 비용이 증가합니다.
변경된 부분만 저장한다고 해도, 장기적으로는 많은 공간을 차지합니다. 따라서 VACUUM 명령어로 오래된 버전을 정리해야 합니다.
VACUUM을 실행하면 지정한 retention 기간 이전의 파일들이 삭제됩니다. 기본값은 7일입니다.
삭제된 파일에 대한 버전은 더 이상 조회할 수 없습니다. 타임 트래블과 스토리지 비용 사이의 균형을 찾아야 합니다.
또한 restoreToVersion()은 새로운 트랜잭션을 생성한다는 점을 이해해야 합니다. 버전 5로 롤백하면 버전 6이 생성되는데, 이 버전 6의 내용이 버전 5와 같은 것입니다.
버전 히스토리가 지워지는 것은 아닙니다. 버전 충돌은 어떻게 처리될까요?
두 명의 사용자가 동시에 같은 테이블을 수정하려고 하면, 낙관적 동시성 제어가 작동합니다. 먼저 커밋한 사용자가 성공하고, 나중에 커밋한 사용자는 충돌 에러를 받습니다.
그러면 최신 버전을 다시 읽어서 재시도해야 합니다. 이것은 Git의 merge conflict와 유사합니다.
차이점은 Delta Lake는 자동 merge를 시도하지 않는다는 것입니다. 명확한 실패를 통해 데이터 일관성을 보장합니다.
김데이터 씨는 이제 Delta Lake의 모든 핵심 개념을 이해했습니다. "트랜잭션 로그, 메타데이터, 파일 구조, 버전 관리...
모두 연결되어 있군요!" 박빅데이터 씨가 고개를 끄덕였습니다. "정확해요.
이제 여러분은 Delta Lake 전문가예요." 버전 관리를 활용하면 데이터 레이크가 진정한 엔터프라이즈 데이터 플랫폼이 됩니다. 여러분의 프로젝트에도 Delta Lake를 도입하여 안전하고 추적 가능한 데이터 관리를 시작해 보세요.
실전 팁
💡 - VACUUM의 retention을 너무 짧게 설정하면 동시 실행 중인 쿼리가 실패할 수 있으니 주의하세요
- DESCRIBE HISTORY LIMIT 10으로 최근 10개 버전만 조회하면 성능이 향상됩니다
- timestampAsOf는 정확한 타임스탬프 형식을 요구하므로 ISO 8601 형식을 사용하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.