본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 12. · 13 Views
Delta Lake 테이블 최적화 완벽 가이드
Delta Lake 테이블을 효율적으로 관리하고 성능을 극대화하는 방법을 배웁니다. Small File Problem 해결부터 자동 최적화 설정까지, 실무에서 바로 적용할 수 있는 최적화 기법을 다룹니다.
목차
- Small File Problem 이해와 해결
- OPTIMIZE 명령어로 파일 병합
- VACUUM으로 오래된 파일 정리
- Auto Compaction 설정
- 데이터 스킵을 위한 통계 관리
WHERE orderDate = '2025-01-10'- 특정 날짜의 주문 조회- 테이블 속성 튜닝
1. Small File Problem 이해와 해결
어느 날 김데이터 씨가 Delta Lake 테이블 쿼리를 실행했는데, 생각보다 너무 느린 성능에 당황했습니다. 데이터는 얼마 안 되는데 왜 이렇게 오래 걸리는 걸까요?
선배 박레이크 씨가 테이블을 살펴보더니 "파일이 너무 작고 많네요. Small File Problem이에요."라고 말했습니다.
Small File Problem은 Delta Lake에서 가장 흔한 성능 저하 원인입니다. 마치 편의점에서 물건 하나씩 천 번 사는 것보다 마트에서 한 번에 사는 게 효율적이듯, 작은 파일 수천 개보다 적당한 크기의 파일 몇 개가 훨씬 빠릅니다.
스트리밍이나 자주 업데이트되는 테이블에서 특히 심각합니다.
다음 코드를 살펴봅시다.
from pyspark.sql import SparkSession
from delta.tables import DeltaTable
# Delta 테이블 생성 및 데이터 삽입
spark = SparkSession.builder.appName("DeltaOptimize").getOrCreate()
# 작은 배치로 여러 번 삽입 (Small File Problem 발생)
for i in range(100):
df = spark.range(i * 1000, (i + 1) * 1000)
df.write.format("delta").mode("append").save("/tmp/delta/events")
# 파일 개수 확인 - 100개 이상의 작은 파일들이 생성됨
deltaTable = DeltaTable.forPath(spark, "/tmp/delta/events")
print(f"파일 개수: {len(deltaTable.detail().select('numFiles').collect()[0][0])}")
김데이터 씨는 데이터 엔지니어링팀에 입사한 지 2개월 된 주니어입니다. 오늘도 열심히 실시간 스트리밍 파이프라인을 모니터링하던 중, 대시보드 쿼리가 점점 느려지는 것을 발견했습니다.
처음엔 1초도 안 걸리던 쿼리가 이제는 30초가 넘게 걸립니다. 선배 박레이크 씨가 다가와 상황을 살펴봅니다.
"테이블 상세 정보 좀 볼까요?" 박레이크 씨가 DESCRIBE DETAIL 명령어를 실행하자, 놀라운 사실이 드러났습니다. 파일 개수가 무려 5,000개가 넘었습니다.
그렇다면 Small File Problem이란 정확히 무엇일까요? 쉽게 비유하자면, Small File Problem은 마치 도서관에서 책을 찾는 것과 같습니다.
백과사전이 한 권으로 되어 있으면 펼쳐서 바로 찾으면 되지만, 같은 내용이 100쪽짜리 소책자 100권으로 나뉘어 있다면 어떨까요? 사서는 어느 책에 원하는 내용이 있는지 일일이 확인해야 합니다.
Delta Lake도 마찬가지로 파일마다 메타데이터를 읽고 처리해야 하므로 파일이 많을수록 느려집니다. Small File Problem은 왜 발생할까요?
스트리밍 작업에서 가장 자주 발생합니다. Structured Streaming은 기본적으로 마이크로 배치마다 새로운 파일을 생성합니다.
1분마다 배치가 실행된다면, 하루에 1,440개의 파일이 생성됩니다. 일주일이면 1만 개가 넘어갑니다.
자주 업데이트되는 테이블도 문제입니다. Delta Lake는 업데이트나 삭제 시 기존 파일을 수정하지 않고 새 파일을 생성합니다.
이것이 ACID 트랜잭션을 보장하는 핵심 메커니즘이지만, 부작용으로 작은 파일이 계속 늘어납니다. 파티션이 과도하게 나뉜 경우도 원인입니다.
날짜별로 파티셔닝된 테이블에 시간별로 데이터를 삽입하면, 각 파티션마다 작은 파일들이 생깁니다. Small File Problem이 왜 심각한 문제일까요?
첫째, 메타데이터 오버헤드가 커집니다. Spark는 쿼리 시작 전에 모든 파일의 메타데이터를 읽어야 합니다.
파일이 1,000개면 1,000번의 메타데이터 읽기가 발생합니다. 클라우드 스토리지에서는 각 읽기마다 네트워크 레이턴시가 추가됩니다.
둘째, 태스크 스케줄링 비효율이 생깁니다. Spark는 기본적으로 파일 하나를 하나의 태스크로 처리합니다.
파일이 많으면 태스크도 많아지고, 태스크 스케줄링 자체에 시간이 걸립니다. 셋째, I/O 효율성이 떨어집니다.
큰 파일 하나를 읽는 것이 작은 파일 여러 개를 읽는 것보다 훨씬 빠릅니다. 디스크나 네트워크의 시퀀셜 읽기는 랜덤 읽기보다 몇 배나 빠르기 때문입니다.
위의 코드를 살펴보겠습니다. 반복문으로 100번 삽입하면서 매번 append 모드로 저장합니다.
각 삽입마다 새로운 Parquet 파일이 생성되므로, 최소 100개의 파일이 만들어집니다. 실제로는 파티션 수에 따라 더 많을 수 있습니다.
deltaTable.detail()을 실행하면 테이블의 메타데이터를 확인할 수 있습니다. numFiles 컬럼이 실제 파일 개수를 보여줍니다.
이 예제에서는 100개 이상이 나올 것입니다. 실제 현업에서는 어떻게 나타날까요?
예를 들어 IoT 센서 데이터를 수집하는 시스템을 운영한다고 가정해봅시다. 매초 센서 데이터가 들어오고, 10초마다 배치로 저장합니다.
하루면 8,640개의 파일이 생성됩니다. 한 달이면 약 26만 개입니다.
이 상태에서 "지난주 평균 온도는?"이라는 간단한 쿼리도 26만 개의 파일 메타데이터를 읽어야 하므로 느려집니다. Small File Problem을 어떻게 감지할까요?
첫 번째 신호는 쿼리 성능 저하입니다. 데이터 양은 늘지 않았는데 쿼리가 점점 느려진다면 의심해야 합니다.
두 번째는 Spark UI를 확인하는 것입니다. 태스크 개수가 수천, 수만 개로 늘어나면 파일이 너무 많다는 증거입니다.
세 번째는 직접 확인하는 것입니다. DESCRIBE DETAIL 명령어로 numFiles를 보거나, 스토리지에서 실제 파일 개수를 세어볼 수 있습니다.
다시 김데이터 씨의 이야기로 돌아가 봅시다. 박레이크 씨의 설명을 들은 김데이터 씨는 고개를 끄덕였습니다.
"그래서 쿼리가 느려졌군요! 그럼 어떻게 해결하죠?" 박레이크 씨가 미소를 지으며 말했습니다.
"바로 다음 카드에서 배울 OPTIMIZE 명령어를 사용하면 됩니다." Small File Problem을 이해하는 것이 Delta Lake 최적화의 첫걸음입니다. 문제를 인식해야 해결할 수 있으니까요.
실전 팁
💡 - DESCRIBE DETAIL 명령어로 주기적으로 파일 개수를 모니터링하세요
- 파일 개수가 데이터 크기(GB)의 10배를 넘으면 최적화가 필요합니다
- Spark UI에서 태스크 개수가 비정상적으로 많다면 Small File Problem을 의심하세요
2. OPTIMIZE 명령어로 파일 병합
김데이터 씨는 Small File Problem을 발견하고 박레이크 씨에게 해결 방법을 물었습니다. "걱정 마세요.
Delta Lake에는 OPTIMIZE 라는 마법 같은 명령어가 있어요. 단 한 줄로 모든 파일을 깔끔하게 병합할 수 있죠."
OPTIMIZE 명령어는 작은 파일들을 큰 파일로 병합하는 Delta Lake의 핵심 기능입니다. 마치 흩어진 레고 블록을 정리해서 큰 세트로 만드는 것처럼, 수천 개의 작은 Parquet 파일을 적절한 크기의 파일로 재구성합니다.
쿼리 성능이 극적으로 개선됩니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
# 기본 OPTIMIZE - 전체 테이블 최적화
deltaTable = DeltaTable.forPath(spark, "/tmp/delta/events")
deltaTable.optimize().executeCompaction()
# 특정 파티션만 최적화 (WHERE 조건 사용)
deltaTable.optimize().where("date >= '2025-01-01'").executeCompaction()
# Z-ORDER 최적화 - 특정 컬럼으로 데이터 정렬
deltaTable.optimize().executeZOrderBy("userId", "timestamp")
# SQL 문법으로도 가능
spark.sql("OPTIMIZE delta.`/tmp/delta/events`")
spark.sql("OPTIMIZE delta.`/tmp/delta/events` WHERE date = '2025-01-10'")
spark.sql("OPTIMIZE delta.`/tmp/delta/events` ZORDER BY (userId, timestamp)")
박레이크 씨가 터미널을 열고 명령어 하나를 입력했습니다. OPTIMIZE delta.events 엔터.
몇 분 후, 마법처럼 파일 개수가 5,000개에서 50개로 줄어들었습니다. 김데이터 씨는 놀라서 눈이 휘둥그레졌습니다.
그렇다면 OPTIMIZE는 어떻게 작동할까요? 쉽게 비유하자면, OPTIMIZE는 마치 정리정돈 전문가와 같습니다.
방 안에 작은 상자 100개가 흩어져 있다면, 내용물을 꺼내서 큰 상자 10개에 깔끔하게 정리합니다. 공간도 절약되고 찾기도 쉬워집니다.
Delta Lake도 마찬가지로 작은 Parquet 파일들의 데이터를 읽어서 큰 파일로 다시 쓰는 작업을 수행합니다. OPTIMIZE가 없던 시절에는 어땠을까요?
개발자들은 직접 repartition이나 coalesce로 파일을 병합해야 했습니다. 전체 테이블을 읽고, 파티션을 조정하고, 다시 써야 했습니다.
코드도 복잡하고 실수하기 쉬웠습니다. 더 큰 문제는 원본 파일 삭제 타이밍이었습니다.
잘못하면 데이터 유실이나 쿼리 실패가 발생할 수 있었습니다. 바로 이런 문제를 해결하기 위해 OPTIMIZE가 등장했습니다.
OPTIMIZE를 사용하면 안전하게 파일을 병합할 수 있습니다. Delta Lake의 트랜잭션 로그 덕분에 OPTIMIZE 중에도 다른 쿼리나 쓰기 작업이 정상적으로 동작합니다.
또한 자동으로 최적 크기를 계산합니다. 기본적으로 1GB 정도의 파일로 병합하는데, 이는 Spark에서 가장 효율적인 크기입니다.
무엇보다 사용이 간단합니다. 한 줄이면 끝입니다.
위의 코드를 단계별로 살펴보겠습니다. 첫 번째 예제는 가장 기본적인 형태입니다.
optimize().executeCompaction()을 호출하면 전체 테이블의 모든 파일을 병합합니다. 파티션된 테이블이라면 각 파티션별로 병합이 일어납니다.
두 번째 예제는 WHERE 조건을 사용합니다. 전체 테이블이 아닌 특정 파티션만 최적화하고 싶을 때 유용합니다.
예를 들어 어제 데이터만 최적화하거나, 특정 지역 데이터만 최적화할 수 있습니다. 이렇게 하면 시간과 리소스를 절약할 수 있습니다.
세 번째 예제는 Z-ORDER라는 고급 기법입니다. 단순히 파일을 병합하는 것을 넘어서, 특정 컬럼 기준으로 데이터를 정렬합니다.
이후 해당 컬럼으로 필터링하는 쿼리가 더욱 빨라집니다. 이것은 다음다음 카드에서 자세히 배우겠습니다.
네 번째부터는 SQL 문법입니다. Python API와 동일한 기능이지만, SQL에 익숙한 분들에게는 더 편할 수 있습니다.
실제 현업에서는 어떻게 활용할까요? 쇼핑몰의 주문 데이터 테이블을 관리한다고 가정해봅시다.
실시간으로 주문이 들어와서 1분마다 Delta 테이블에 저장됩니다. 하루가 지나면 1,440개의 작은 파일이 생깁니다.
매일 새벽 2시에 OPTIMIZE를 실행하도록 스케줄링하면, 매일 아침 깔끔하게 정리된 테이블로 시작할 수 있습니다. 대형 이커머스 기업들은 실제로 이런 패턴을 사용합니다.
Airflow나 Databricks Jobs로 매일 또는 매시간 OPTIMIZE를 실행합니다. 특히 파티션별로 분할 실행하는 것이 좋습니다.
최근 파티션만 최적화하면 시간이 훨씬 단축됩니다. 하지만 주의할 점도 있습니다.
첫째, OPTIMIZE는 비용이 드는 작업입니다. 데이터를 읽고 다시 쓰므로 컴퓨팅 리소스를 사용합니다.
너무 자주 실행하면 클러스터 비용이 늘어납니다. 반대로 너무 드물게 실행하면 Small File Problem이 누적됩니다.
적절한 주기를 찾는 것이 중요합니다. 둘째, OPTIMIZE 후에도 원본 파일은 남아있습니다.
Delta Lake는 기본적으로 30일 동안 이전 버전을 유지합니다. 타임 트래블이나 롤백을 위해서죠.
스토리지 비용이 걱정된다면 다음 카드에서 배울 VACUUM으로 정리해야 합니다. 셋째, OPTIMIZE 중에는 쓰기 작업이 느려질 수 있습니다.
같은 파티션에 동시에 쓰기가 발생하면 락 충돌이 생길 수 있기 때문입니다. 따라서 쓰기가 적은 시간대에 실행하는 것이 좋습니다.
얼마나 자주 OPTIMIZE를 실행해야 할까요? 스트리밍 테이블이라면 매일 또는 매시간이 적당합니다.
파일이 빠르게 늘어나기 때문입니다. 배치 테이블이라면 주 1-2회로도 충분할 수 있습니다.
업데이트 빈도가 낮다면 파일도 천천히 늘어납니다. 읽기 전용 테이블이라면 처음 한 번만 실행하면 됩니다.
더 이상 파일이 늘어나지 않으니까요. 다시 김데이터 씨의 이야기로 돌아가 봅시다.
OPTIMIZE를 실행한 후 대시보드 쿼리를 다시 실행해 보니, 30초 걸리던 쿼리가 3초로 단축되었습니다. 10배나 빨라진 것입니다.
김데이터 씨는 감탄했습니다. "이렇게 간단한 명령어 하나로 이런 차이가 나다니!" 박레이크 씨가 웃으며 말했습니다.
"Delta Lake의 진짜 매력은 이런 강력한 기능을 쉽게 사용할 수 있다는 거예요. 하지만 여기서 끝이 아닙니다.
OPTIMIZE 후에는 VACUUM으로 오래된 파일을 정리해야 해요." OPTIMIZE는 Delta Lake 운영의 필수 도구입니다. 정기적으로 실행해서 테이블을 건강하게 유지하세요.
실전 팁
💡 - 스트리밍 테이블은 매일, 배치 테이블은 주 1-2회 OPTIMIZE를 권장합니다
- WHERE 조건으로 최근 파티션만 최적화하면 시간과 비용을 절약할 수 있습니다
- OPTIMIZE 후에는 반드시 VACUUM으로 스토리지를 정리하세요
3. VACUUM으로 오래된 파일 정리
김데이터 씨가 OPTIMIZE 후 스토리지 사용량을 확인했는데, 생각보다 줄지 않았습니다. "왜 파일은 병합됐는데 용량은 그대로죠?" 박레이크 씨가 설명했습니다.
"OPTIMIZE는 새 파일을 만들 뿐 옛날 파일은 안 지워요. VACUUM으로 청소해야죠."
VACUUM은 더 이상 필요 없는 오래된 파일을 삭제하는 명령어입니다. 마치 컴퓨터의 휴지통 비우기처럼, Delta Lake의 불필요한 파일을 영구 삭제해서 스토리지 비용을 절약합니다.
기본적으로 7일 이상 된 파일만 삭제하므로 안전합니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
# 기본 VACUUM - 7일 이상 된 파일 삭제
deltaTable = DeltaTable.forPath(spark, "/tmp/delta/events")
deltaTable.vacuum()
# 보관 기간 커스터마이징 (24시간)
deltaTable.vacuum(24)
# SQL 문법
spark.sql("VACUUM delta.`/tmp/delta/events`")
spark.sql("VACUUM delta.`/tmp/delta/events` RETAIN 168 HOURS")
# DRY RUN - 삭제될 파일 미리 확인 (실제 삭제 안 함)
spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "false")
spark.sql("VACUUM delta.`/tmp/delta/events` DRY RUN")
박레이크 씨가 스토리지 브라우저를 열어서 보여주었습니다. Delta 테이블 디렉토리에는 수많은 Parquet 파일이 있었습니다.
그중 절반 이상이 이미 사용되지 않는 오래된 파일들이었습니다. "이것들이 다 스토리지 비용을 잡아먹고 있어요." 그렇다면 VACUUM은 왜 필요할까요?
쉽게 비유하자면, VACUUM은 마치 옷장 정리와 같습니다. 계절이 바뀌면 여름옷을 꺼내고 겨울옷을 넣습니다.
하지만 옛날 옷을 버리지 않으면 옷장이 점점 꽉 찹니다. 언젠가는 기부하거나 버려야 합니다.
Delta Lake도 마찬가지로 업데이트, 삭제, OPTIMIZE 때마다 새 파일이 생기고 옛 파일은 남아있습니다. 주기적으로 청소하지 않으면 스토리지가 계속 늘어납니다.
Delta Lake는 왜 바로 삭제하지 않고 남겨둘까요? 첫 번째 이유는 타임 트래블입니다.
Delta Lake의 강력한 기능 중 하나는 과거 버전의 데이터를 읽을 수 있다는 것입니다. VERSION AS OF 10 같은 문법으로 10번째 버전을 조회할 수 있습니다.
파일을 바로 삭제하면 이 기능을 쓸 수 없습니다. 두 번째 이유는 안전성입니다.
장시간 실행되는 쿼리가 있을 수 있습니다. 쿼리가 시작할 때의 스냅샷을 읽고 있는데, 파일이 중간에 삭제되면 에러가 발생합니다.
충분한 유예 기간을 두고 삭제해야 안전합니다. 세 번째 이유는 롤백입니다.
잘못된 업데이트나 삭제를 실행했다면, 이전 버전으로 복구할 수 있어야 합니다. 파일이 남아있어야 롤백이 가능합니다.
그래서 Delta Lake는 기본적으로 7일 동안 파일을 보관합니다. 7일이 지난 후에야 VACUUM으로 안전하게 삭제할 수 있습니다.
위의 코드를 살펴보겠습니다. 첫 번째 예제는 가장 기본 형태입니다.
vacuum()을 인자 없이 호출하면 기본값인 7일(168시간)이 적용됩니다. 7일 이전의 오래된 파일들만 삭제됩니다.
두 번째 예제는 보관 기간을 커스터마이징합니다. vacuum(24)는 24시간 이상 된 파일을 삭제합니다.
하지만 주의하세요. 7일보다 짧게 설정하면 타임 트래블이나 장시간 쿼리에 문제가 생길 수 있습니다.
Delta Lake는 기본적으로 7일 미만 설정을 막습니다. 세 번째, 네 번째는 SQL 문법입니다.
RETAIN 168 HOURS는 168시간(7일)을 보관한다는 의미입니다. 다섯 번째 예제는 DRY RUN 모드입니다.
실제로 삭제하지 않고, 어떤 파일들이 삭제될지만 미리 보여줍니다. 처음 VACUUM을 실행할 때는 DRY RUN으로 확인하는 것이 안전합니다.
혹시 모를 실수를 방지할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?
금융권의 거래 내역 테이블을 운영한다고 가정해봅시다. 규정상 90일 동안은 모든 버전을 보관해야 합니다.
감사 추적이 필요하기 때문입니다. 이런 경우 vacuum(90 * 24)로 설정해서 90일치 데이터를 보관합니다.
반대로 개발 환경이나 테스트 테이블이라면 스토리지 비용을 아끼기 위해 24시간으로 설정할 수도 있습니다. 물론 이 경우 타임 트래블이 제한됩니다.
많은 기업들이 OPTIMIZE와 VACUUM을 함께 스케줄링합니다. 예를 들어 매일 새벽 2시에 OPTIMIZE를 실행하고, 3시에 VACUUM을 실행합니다.
이렇게 하면 파일은 깔끔하게 병합되고, 오래된 파일은 자동으로 정리됩니다. 하지만 주의할 점이 있습니다.
첫째, VACUUM은 되돌릴 수 없습니다. 일단 파일이 삭제되면 복구할 방법이 없습니다.
따라서 처음에는 반드시 DRY RUN으로 확인하세요. 둘째, 보관 기간을 너무 짧게 설정하면 위험합니다.
장시간 실행되는 쿼리나 배치 작업이 있다면, 중간에 파일이 삭제되어 실패할 수 있습니다. 운영 환경에서는 최소 7일, 가능하면 14일 이상을 권장합니다.
셋째, VACUUM도 비용이 듭니다. 삭제할 파일 목록을 만들기 위해 메타데이터를 읽어야 합니다.
파일이 수백만 개라면 시간이 오래 걸릴 수 있습니다. 하지만 한 번 실행하면 스토리지 비용이 크게 줄어들므로 충분히 가치가 있습니다.
VACUUM을 실행하면 얼마나 절약될까요? 실제 사례를 보면, OPTIMIZE를 자주 했지만 VACUUM을 안 한 테이블의 경우 실제 데이터의 3-5배 용량을 차지하는 경우가 많습니다.
VACUUM 한 번으로 60-80% 스토리지를 줄일 수 있습니다. S3나 ADLS 같은 클라우드 스토리지는 GB당 비용이 발생하므로, 대용량 테이블에서는 월 수백만 원을 절약할 수 있습니다.
다시 김데이터 씨의 이야기로 돌아가 봅시다. VACUUM을 실행한 후 스토리지 사용량을 확인해 보니, 500GB에서 150GB로 줄어들었습니다.
70% 감소입니다. 김데이터 씨는 놀라워했습니다.
"이렇게 많은 용량이 낭비되고 있었다니!" 박레이크 씨가 고개를 끄덕였습니다. "그래서 OPTIMIZE와 VACUUM을 세트로 기억해야 해요.
OPTIMIZE로 성능을 개선하고, VACUUM으로 비용을 절약하는 거죠." VACUUM은 Delta Lake 운영 비용 관리의 핵심입니다. 정기적으로 실행해서 스토리지를 깔끔하게 유지하세요.
실전 팁
💡 - 처음 실행할 때는 반드시 DRY RUN으로 확인하세요
- 운영 환경에서는 최소 7일, 가능하면 14일 이상 보관을 권장합니다
- OPTIMIZE 직후 VACUUM을 실행하면 최대 효과를 얻을 수 있습니다
4. Auto Compaction 설정
김데이터 씨가 매일 OPTIMIZE를 실행하는 스케줄러를 만들고 있는데, 박레이크 씨가 더 좋은 방법을 알려주었습니다. "매번 수동으로 할 필요 없어요.
Auto Compaction을 켜면 자동으로 최적화해줍니다."
Auto Compaction은 쓰기 작업 시 자동으로 작은 파일들을 병합하는 기능입니다. 마치 스마트폰의 자동 정리 기능처럼, 백그라운드에서 알아서 파일을 최적화합니다.
Databricks에서 제공하는 강력한 기능으로, 스트리밍 테이블에 특히 유용합니다.
다음 코드를 살펴봅시다.
# Auto Compaction 활성화 (테이블 속성)
spark.sql("""
ALTER TABLE delta.`/tmp/delta/events`
SET TBLPROPERTIES (
'delta.autoOptimize.optimizeWrite' = 'true',
'delta.autoOptimize.autoCompact' = 'true'
)
""")
# 새 테이블 생성 시 처음부터 활성화
df.write.format("delta") \
.option("delta.autoOptimize.optimizeWrite", "true") \
.option("delta.autoOptimize.autoCompact", "true") \
.save("/tmp/delta/events_optimized")
# 현재 설정 확인
spark.sql("DESCRIBE DETAIL delta.`/tmp/delta/events`").select("properties").show(truncate=False)
김데이터 씨는 Airflow DAG를 작성하고 있었습니다. 매일 새벽 2시에 OPTIMIZE를 실행하는 스케줄러를 만들려고 했습니다.
코드를 거의 다 작성했을 때, 박레이크 씨가 다가왔습니다. "이렇게 복잡하게 할 필요 없어요.
Delta Lake에는 자동 최적화 기능이 있거든요." 그렇다면 Auto Compaction은 무엇일까요? 쉽게 비유하자면, Auto Compaction은 마치 로봇 청소기와 같습니다.
매번 빗자루로 직접 청소할 필요 없이, 로봇 청소기가 알아서 정해진 시간에 청소합니다. Delta Lake도 마찬가지로 데이터를 쓸 때마다 백그라운드에서 자동으로 파일을 정리합니다.
Auto Compaction은 사실 두 가지 기능으로 구성됩니다. Optimized Writes는 쓰기 작업 중에 파일 크기를 최적화합니다.
일반적으로 Spark는 파티션 개수만큼 파일을 생성합니다. 파티션이 200개면 200개 파일이 나옵니다.
Optimized Writes는 쓰기 전에 자동으로 repartition을 수행해서 적절한 크기의 파일을 만듭니다. 보통 128MB 정도입니다.
Auto Compaction은 쓰기 작업 후에 백그라운드로 OPTIMIZE를 실행합니다. 작은 파일이 여러 개 생겼다면, 쓰기가 끝난 직후 자동으로 병합합니다.
사용자는 아무것도 할 필요가 없습니다. 이 두 기능을 함께 사용하면 Small File Problem이 거의 발생하지 않습니다.
Auto Compaction이 없던 시절에는 어땠을까요? 개발자들은 직접 스케줄러를 만들어야 했습니다.
Airflow, Databricks Jobs, Cron 등으로 매일 또는 매시간 OPTIMIZE를 실행했습니다. 스케줄러 코드를 작성하고, 모니터링하고, 실패 시 알림을 받고, 재실행 로직을 만들어야 했습니다.
복잡하고 관리 포인트가 늘어났습니다. Auto Compaction을 사용하면 이 모든 것이 자동화됩니다.
위의 코드를 살펴보겠습니다. 첫 번째 예제는 기존 테이블에 Auto Compaction을 활성화하는 방법입니다.
ALTER TABLE로 테이블 속성을 변경합니다. delta.autoOptimize.optimizeWrite는 Optimized Writes를, delta.autoOptimize.autoCompact는 Auto Compaction을 활성화합니다.
두 번째 예제는 새 테이블을 만들 때 처음부터 활성화하는 방법입니다. option()으로 같은 속성을 설정합니다.
새로 만드는 테이블이라면 이렇게 처음부터 켜는 것이 좋습니다. 세 번째 예제는 현재 설정을 확인하는 방법입니다.
DESCRIBE DETAIL의 properties 컬럼에 모든 테이블 속성이 표시됩니다. 실제 현업에서는 어떻게 활용할까요?
실시간 로그 수집 시스템을 운영한다고 가정해봅시다. Kafka에서 데이터를 읽어서 Delta 테이블에 쓰는 Structured Streaming 작업입니다.
1분마다 마이크로 배치가 실행되고, 매번 작은 파일이 생깁니다. Auto Compaction을 켜면, 쓰기마다 자동으로 파일이 병합됩니다.
별도의 OPTIMIZE 스케줄러 없이도 테이블이 항상 깔끔하게 유지됩니다. 운영 부담이 크게 줄어듭니다.
실제로 많은 기업들이 스트리밍 테이블에는 무조건 Auto Compaction을 켭니다. 특히 고빈도 업데이트 테이블이나 CDC(Change Data Capture) 테이블에서 효과가 큽니다.
하지만 주의할 점도 있습니다. 첫째, 쓰기 성능이 약간 느려질 수 있습니다.
Optimized Writes는 repartition을 수행하므로 약간의 오버헤드가 있습니다. Auto Compaction도 백그라운드 작업이지만 리소스를 사용합니다.
쓰기 지연 시간(latency)에 민감한 애플리케이션이라면 신중히 고려해야 합니다. 둘째, 모든 환경에서 지원하는 것은 아닙니다.
Auto Compaction은 Databricks에서 제공하는 기능입니다. 오픈소스 Delta Lake나 다른 플랫폼에서는 사용하지 못할 수 있습니다.
사용 전에 환경을 확인하세요. 셋째, 완벽하지는 않습니다.
Auto Compaction이 모든 상황을 커버하지는 못합니다. 매우 큰 테이블이나 복잡한 파티션 구조에서는 여전히 수동 OPTIMIZE가 필요할 수 있습니다.
Auto Compaction은 보조 수단이지 완전한 대체재는 아닙니다. 언제 Auto Compaction을 켜야 할까요?
스트리밍 테이블이라면 무조건 켜는 것을 추천합니다. 파일이 빠르게 늘어나는 환경에서 가장 효과적입니다.
자주 업데이트되는 배치 테이블도 좋은 후보입니다. 하루에 여러 번 업데이트가 일어난다면 Auto Compaction이 유용합니다.
읽기 전용 테이블이나 한 번만 쓰는 테이블에는 불필요합니다. 추가 오버헤드만 발생합니다.
다시 김데이터 씨의 이야기로 돌아가 봅시다. Auto Compaction을 활성화한 후, 스트리밍 작업을 며칠 동안 모니터링했습니다.
파일 개수가 항상 적정 수준으로 유지되었습니다. 별도로 OPTIMIZE를 실행하지 않아도 되었습니다.
김데이터 씨는 감탄했습니다. "이제 스케줄러 관리 부담이 없어졌네요!" 박레이크 씨가 웃으며 말했습니다.
"맞아요. 하지만 여기서 끝이 아닙니다.
성능을 더 끌어올리려면 Data Skipping을 이해해야 해요." Auto Compaction은 Delta Lake 운영 자동화의 핵심입니다. 조건이 맞다면 적극적으로 활용하세요.
실전 팁
💡 - 스트리밍 테이블이나 고빈도 업데이트 테이블에서 가장 효과적입니다
- Databricks 환경에서만 사용 가능하므로 플랫폼을 확인하세요
- Auto Compaction을 켜도 가끔은 수동 OPTIMIZE로 추가 최적화가 필요할 수 있습니다
5. 데이터 스킵을 위한 통계 관리
김데이터 씨가 OPTIMIZE를 했는데도 특정 쿼리가 여전히 느렸습니다. 박레이크 씨가 쿼리를 보더니 말했습니다.
"WHERE userId = 123 같은 필터 쿼리는 Data Skipping을 활용해야 해요. Z-ORDER로 통계를 최적화하면 훨씬 빠릅니다."
Data Skipping은 불필요한 파일을 읽지 않고 건너뛰는 Delta Lake의 핵심 최적화 기법입니다. 각 파일의 최솟값, 최댓값 통계를 활용해서 필터 조건에 맞지 않는 파일은 아예 스캔하지 않습니다.
Z-ORDER로 특정 컬럼 기준으로 데이터를 정렬하면 효과가 극대화됩니다.
다음 코드를 살펴봅시다.
from delta.tables import DeltaTable
# 기본 OPTIMIZE - Data Skipping 통계는 자동 생성됨
deltaTable = DeltaTable.forPath(spark, "/tmp/delta/events")
deltaTable.optimize().executeCompaction()
# Z-ORDER로 특정 컬럼 기준 최적화
deltaTable.optimize().executeZOrderBy("userId")
# 여러 컬럼으로 Z-ORDER (최대 4개 권장)
deltaTable.optimize().executeZOrderBy("userId", "eventDate")
# SQL 문법
spark.sql("OPTIMIZE delta.`/tmp/delta/events` ZORDER BY (userId, eventDate)")
# 통계 정보 확인
spark.sql("DESCRIBE DETAIL delta.`/tmp/delta/events`").select("numFiles", "sizeInBytes").show()
김데이터 씨는 고객 행동 분석 대시보드를 만들고 있었습니다. 사용자 ID로 필터링하는 쿼리가 많았는데, OPTIMIZE를 했는데도 여전히 느렸습니다.
100GB 테이블에서 특정 사용자 한 명의 데이터만 찾는데 1분이 넘게 걸렸습니다. 박레이크 씨가 Spark UI를 열어서 보여주었습니다.
"보세요. 100개 파일을 모두 스캔하고 있어요.
하지만 실제로 필요한 데이터는 5개 파일에만 있습니다. 95개 파일은 쓸데없이 읽는 거예요." 그렇다면 Data Skipping은 어떻게 작동할까요?
쉽게 비유하자면, Data Skipping은 마치 도서관의 색인 카드와 같습니다. 책을 하나하나 펼쳐보지 않고, 색인 카드만 보고 "이 책에는 내가 찾는 내용이 없네" 하고 건너뛸 수 있습니다.
Delta Lake도 마찬가지로 각 파일의 통계 정보(최솟값, 최댓값)를 보관합니다. 쿼리 시 이 통계를 먼저 확인해서, 조건에 맞지 않는 파일은 아예 읽지 않습니다.
예를 들어 WHERE userId = 123 쿼리를 실행한다고 가정해봅시다. 파일 A의 userId 범위가 1-100이면, 123은 이 파일에 없다는 것을 확신할 수 있습니다.
파일 A는 건너뜁니다. 파일 B의 userId 범위가 100-200이면, 123이 있을 가능성이 있습니다.
파일 B는 읽습니다. 이렇게 통계만으로 대부분의 파일을 스킵할 수 있습니다.
읽는 데이터 양이 줄어들면 쿼리가 빨라집니다. 하지만 데이터가 랜덤하게 섞여 있으면 Data Skipping 효과가 떨어집니다.
파일마다 userId가 1부터 1000까지 골고루 섞여 있다면, 모든 파일의 범위가 1-1000으로 동일합니다. 어느 파일도 스킵할 수 없습니다.
이런 경우 Z-ORDER가 필요합니다. Z-ORDER는 무엇일까요?
Z-ORDER는 특정 컬럼 기준으로 데이터를 재정렬하는 고급 최적화 기법입니다. 비슷한 값끼리 모아서 저장하므로, 각 파일의 값 범위가 좁아집니다.
범위가 좁으면 Data Skipping 효과가 극대화됩니다. 다시 userId 예제로 돌아가 봅시다.
Z-ORDER를 실행하면, userId 1-100은 파일 A에, 101-200은 파일 B에 모입니다. 이제 WHERE userId = 123 쿼리는 파일 B만 읽으면 됩니다.
나머지 파일은 모두 스킵됩니다. 위의 코드를 살펴보겠습니다.
첫 번째 예제는 일반 OPTIMIZE입니다. 이것만으로도 Delta Lake는 자동으로 통계를 생성합니다.
Data Skipping은 기본으로 작동합니다. 하지만 데이터가 섞여 있으면 효과가 제한적입니다.
두 번째 예제는 Z-ORDER를 사용합니다. executeZOrderBy("userId")는 userId 컬럼 기준으로 데이터를 정렬해서 병합합니다.
userId로 자주 필터링한다면 큰 성능 향상을 얻을 수 있습니다. 세 번째 예제는 여러 컬럼으로 Z-ORDER를 수행합니다.
userId와 eventDate 모두 자주 사용된다면 두 컬럼을 함께 지정합니다. 하지만 너무 많은 컬럼을 지정하면 효과가 떨어집니다.
최대 3-4개를 권장합니다. 실제 현업에서는 어떻게 활용할까요?
이커머스 주문 테이블을 예로 들어봅시다. 가장 자주 사용되는 쿼리 패턴이 두 가지입니다.
2. WHERE orderDate = '2025-01-10' - 특정 날짜의 주문 조회
실전 팁
💡 - 가장 자주 필터링되는 컬럼 2-3개를 Z-ORDER하세요
- Z-ORDER는 비용이 크므로 주 1회 또는 월 1회로 충분합니다
- 파티션 컬럼은 Z-ORDER에서 제외하세요 (이미 분리되어 있으므로)
6. 테이블 속성 튜닝
김데이터 씨가 박레이크 씨에게 물었습니다. "최적화는 다 배운 것 같은데, 더 할 게 있나요?" 박레이크 씨가 웃으며 말했습니다.
"테이블 속성을 튜닝하면 성능과 비용을 더 개선할 수 있어요. 마지막 퍼즐 조각이죠."
테이블 속성 튜닝은 Delta Lake 테이블의 세부 동작을 제어하는 고급 최적화 기법입니다. 파일 크기, 로그 보관 기간, 통계 수집 범위 등을 조정해서 워크로드에 맞게 최적화할 수 있습니다.
마치 자동차 엔진을 세밀하게 튜닝하는 것과 같습니다.
다음 코드를 살펴봅시다.
# 주요 테이블 속성 설정
spark.sql("""
ALTER TABLE delta.`/tmp/delta/events`
SET TBLPROPERTIES (
'delta.targetFileSize' = '134217728', -- 128MB (기본 1GB)
'delta.tuneFileSizesForRewrites' = 'true', -- 재작성 시 크기 조정
'delta.deletedFileRetentionDuration' = 'interval 7 days', -- VACUUM 보관 기간
'delta.logRetentionDuration' = 'interval 30 days', -- 트랜잭션 로그 보관
'delta.enableChangeDataFeed' = 'true', -- CDC 활성화
'delta.columnMapping.mode' = 'name', -- 컬럼명 매핑
'delta.checkpoint.writeStatsAsJson' = 'false', -- 체크포인트 최적화
'delta.checkpoint.writeStatsAsStruct' = 'true'
)
""")
# 현재 속성 확인
spark.sql("SHOW TBLPROPERTIES delta.`/tmp/delta/events`").show(truncate=False)
김데이터 씨는 지금까지 배운 최적화 기법들을 모두 적용했습니다. OPTIMIZE, VACUUM, Auto Compaction, Z-ORDER까지.
성능은 크게 개선되었지만, 박레이크 씨는 "아직 할 수 있는 게 더 있다"고 말했습니다. "테이블 속성이라는 게 있어요.
Delta Lake의 숨겨진 설정들이죠. 이걸 잘 조정하면 성능과 비용을 더 최적화할 수 있습니다." 그렇다면 테이블 속성은 무엇일까요?
쉽게 비유하자면, 테이블 속성은 마치 스마트폰의 고급 설정 메뉴와 같습니다. 일반 사용자는 기본 설정만으로도 충분하지만, 전문가는 배터리 최적화, 네트워크 설정, 앱 권한 등을 세밀하게 조정해서 성능을 끌어올립니다.
Delta Lake도 마찬가지로 다양한 내부 동작을 제어할 수 있는 속성들을 제공합니다. 위의 코드에서 주요 속성들을 하나씩 살펴보겠습니다.
delta.targetFileSize는 OPTIMIZE 시 목표 파일 크기입니다. 기본값은 1GB인데, 작은 테이블이나 자주 업데이트되는 테이블에서는 128MB나 256MB로 줄이는 것이 좋습니다.
파일이 너무 크면 업데이트 시 전체 파일을 다시 써야 하므로 비효율적입니다. delta.tuneFileSizesForRewrites는 재작성(업데이트/삭제) 시 파일 크기를 자동 조정하는 옵션입니다.
true로 설정하면 자주 변경되는 파티션의 파일 크기를 자동으로 줄여서 재작성 비용을 낮춥니다. delta.deletedFileRetentionDuration은 VACUUM의 기본 보관 기간입니다.
7일로 설정하면 vacuum() 호출 시 자동으로 7일이 적용됩니다. 매번 인자를 전달할 필요가 없습니다.
delta.logRetentionDuration은 트랜잭션 로그 보관 기간입니다. Delta Lake는 모든 변경 사항을 로그로 기록합니다.
기본 30일이지만, 오래된 버전을 조회할 필요가 없다면 7일로 줄여서 메타데이터 크기를 줄일 수 있습니다. delta.enableChangeDataFeed는 CDC(Change Data Capture) 기능입니다.
true로 설정하면 변경된 행(삽입/업데이트/삭제)만 추출할 수 있습니다. 증분 ETL 파이프라인에서 유용합니다.
delta.columnMapping.mode는 컬럼명 변경을 지원하는 기능입니다. name으로 설정하면 컬럼명을 자유롭게 변경하거나 재정렬할 수 있습니다.
스키마 진화가 자주 일어나는 테이블에 유용합니다. delta.checkpoint.writeStatsAsStruct는 체크포인트 최적화입니다.
체크포인트는 트랜잭션 로그의 스냅샷인데, 통계를 구조체 형태로 저장하면 읽기 성능이 개선됩니다. 실제 현업에서는 어떤 속성을 주로 조정할까요?
스트리밍 테이블의 경우 targetFileSize를 128MB로 줄입니다. 빠르게 쌓이는 작은 파일들을 효율적으로 관리할 수 있습니다.
자주 업데이트되는 테이블의 경우 tuneFileSizesForRewrites를 켜고, targetFileSize를 256MB 정도로 설정합니다. 업데이트 비용이 줄어듭니다.
CDC 파이프라인의 경우 enableChangeDataFeed를 켭니다. 변경 데이터만 추출해서 다운스트림 시스템으로 전달할 수 있습니다.
개발/테스트 환경에서는 deletedFileRetentionDuration과 logRetentionDuration을 짧게 설정해서 스토리지 비용을 절약합니다. 하지만 주의할 점이 있습니다.
첫째, 모든 속성을 변경할 필요는 없습니다. 기본값은 대부분의 경우에 잘 작동하도록 설계되어 있습니다.
성능 문제가 있거나 특수한 요구사항이 있을 때만 조정하세요. 둘째, 일부 속성은 변경할 수 없습니다.
테이블을 만들 때만 설정 가능한 속성들이 있습니다. 변경하려면 새 테이블을 만들고 데이터를 복사해야 합니다.
셋째, 잘못된 설정은 역효과를 낼 수 있습니다. 예를 들어 targetFileSize를 너무 작게 설정하면 파일이 너무 많아져서 Small File Problem이 다시 발생합니다.
너무 크게 설정하면 업데이트가 느려집니다. 워크로드를 이해하고 적절한 값을 찾아야 합니다.
어떻게 최적값을 찾을까요? 첫 번째는 모니터링입니다.
Spark UI, 쿼리 로그, 메트릭을 지속적으로 관찰하세요. 병목 지점을 찾으면 관련 속성을 조정합니다.
두 번째는 실험입니다. 개발 환경에서 여러 값을 테스트해 보세요.
128MB, 256MB, 512MB로 각각 OPTIMIZE를 실행하고 쿼리 성능을 측정합니다. 세 번째는 벤치마크입니다.
대표적인 쿼리 몇 개를 선정해서 설정 변경 전후를 비교합니다. 객관적인 수치로 개선 여부를 확인할 수 있습니다.
실제 사례를 보면, 한 회사는 targetFileSize를 1GB에서 256MB로 줄였더니 업데이트 성능이 3배 개선되었습니다. 다른 회사는 enableChangeDataFeed를 켜서 CDC 파이프라인 처리 시간을 절반으로 줄였습니다.
다시 김데이터 씨의 이야기로 돌아가 봅시다. 박레이크 씨의 조언에 따라 몇 가지 속성을 조정했습니다.
스트리밍 테이블의 targetFileSize를 128MB로, CDC 테이블의 enableChangeDataFeed를 켰습니다. 며칠 후 결과를 보니, 쿼리 성능은 유지되면서 스토리지 비용이 20% 줄어들었습니다.
김데이터 씨는 만족스러워했습니다. "이제 정말 완벽하게 최적화된 것 같아요!" 박레이크 씨가 어깨를 두드리며 말했습니다.
"잘했어요. 하지만 최적화는 일회성이 아니에요.
데이터와 워크로드가 변하면 다시 조정해야 합니다. 지속적인 모니터링과 튜닝이 중요합니다." 김데이터 씨는 고개를 끄덕였습니다.
이제 Delta Lake 최적화의 모든 것을 배웠습니다. Small File Problem 이해부터 시작해서, OPTIMIZE로 병합하고, VACUUM으로 정리하고, Auto Compaction으로 자동화하고, Z-ORDER로 쿼리 성능을 극대화하고, 마지막으로 테이블 속성까지 튜닝하는 전체 과정을 마스터했습니다.
테이블 속성 튜닝은 Delta Lake 최적화의 마지막 퍼즐입니다. 워크로드를 이해하고 적절히 조정하세요.
실전 팁
💡 - 기본값으로 시작하고, 문제가 있을 때만 속성을 변경하세요
- targetFileSize는 워크로드에 따라 128MB-1GB 사이에서 조정하세요
- 변경 후에는 반드시 성능을 측정해서 개선 여부를 확인하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (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의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.