🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

고급 파티셔닝 전략 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 14. · 15 Views

고급 파티셔닝 전략 완벽 가이드

데이터 엔지니어링에서 파티셔닝은 성능을 좌우하는 핵심 기술입니다. 이 가이드는 파티션 설계 원칙부터 동적 오버라이트, Z-Order 하이브리드까지 실무에서 바로 써먹을 수 있는 고급 전략을 다룹니다.


목차

  1. 파티션 설계 원칙
  2. 파티션 프루닝 최적화
  3. 동적 파티션 오버라이트
  4. 파티션 재구성 전략
  5. 시간 기반 파티셔닝 패턴
  6. 파티션 vs Z-Order 하이브리드

1. 파티션 설계 원칙

어느 날 김데이터 씨는 회사의 로그 데이터를 분석하는 작업을 맡게 되었습니다. 그런데 쿼리를 실행할 때마다 10분 넘게 걸리는 것이 아닙니까?

선배 박엔지니어 씨가 다가와 물었습니다. "파티션 설계는 어떻게 했나요?"

파티션 설계는 대용량 데이터를 효율적으로 나누어 저장하고 조회하는 전략입니다. 마치 도서관에서 책을 주제별, 저자별로 분류해두는 것과 같습니다.

올바른 파티션 설계는 쿼리 성능을 수십 배 향상시킬 수 있으며, 잘못된 설계는 오히려 성능 저하를 초래할 수 있습니다.

다음 코드를 살펴봅시다.

from pyspark.sql import SparkSession

spark = SparkSession.builder.appName("PartitionDesign").getOrCreate()

# 잘못된 예: 카디널리티가 너무 높은 컬럼으로 파티션
# df.write.partitionBy("user_id").parquet("path")  # 수백만 개 파티션 생성!

# 올바른 예: 적절한 카디널리티의 컬럼 조합
df = spark.read.json("logs/")
df.write \
    .partitionBy("year", "month", "day") \
    .mode("overwrite") \
    .parquet("partitioned_logs/")

# 파티션 개수 확인 (적정 개수: 수백~수천 개)
print(f"파티션 개수: {df.rdd.getNumPartitions()}")

김데이터 씨는 입사 6개월 차 데이터 엔지니어입니다. 오늘도 열심히 데이터 파이프라인을 구축하던 중, 쿼리 실행 시간이 예상보다 훨씬 오래 걸린다는 것을 발견했습니다.

분명히 Spark를 사용하고 있는데 왜 이렇게 느린 걸까요? 선배 박엔지니어 씨가 다가와 코드를 살펴봅니다.

"아, 여기가 문제네요. 파티션 설계를 제대로 하지 않아서 생긴 성능 이슈예요." 그렇다면 파티션 설계란 정확히 무엇일까요?

쉽게 비유하자면, 파티션 설계는 마치 대형 마트에서 상품을 진열하는 전략과 같습니다. 과자를 찾을 때 전체 마트를 뒤지는 것이 아니라 과자 코너로 바로 가면 되는 것처럼, 파티션도 데이터를 논리적인 그룹으로 나누어 필요한 부분만 읽을 수 있게 합니다.

이처럼 파티션 설계도 데이터를 효율적으로 분류하고 접근하는 전략을 담당합니다. 파티션 설계가 없던 시절에는 어땠을까요?

개발자들은 전체 데이터를 스캔해야 했습니다. 1TB의 데이터에서 하루치 로그만 필요해도 전체를 읽어야 했죠.

코드가 간단해 보여도 성능은 끔찍했습니다. 더 큰 문제는 비용이었습니다.

클라우드 환경에서는 데이터를 읽는 만큼 비용이 발생하니까요. 프로젝트가 커질수록 이런 문제는 눈덩이처럼 불어났습니다.

바로 이런 문제를 해결하기 위해 파티셔닝이 등장했습니다. 파티션을 사용하면 파티션 프루닝이 가능해집니다.

필요한 파티션만 읽으니 I/O가 대폭 줄어듭니다. 또한 병렬 처리 효율도 얻을 수 있습니다.

각 파티션을 독립적으로 처리할 수 있으니까요. 무엇보다 비용 절감이라는 큰 이점이 있습니다.

읽는 데이터가 줄면 비용도 줄어들죠. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 잘못된 예시를 보면 user_id로 파티션을 나누고 있습니다. 이 부분이 문제입니다.

사용자가 100만 명이라면 100만 개의 파티션 디렉토리가 생성됩니다. 다음으로 올바른 예시에서는 year, month, day로 파티션을 나눕니다.

이렇게 하면 하루에 하나의 파티션이 생성되고, 1년이면 365개 정도의 적절한 개수가 됩니다. 마지막으로 현재 파티션 개수를 확인하여 설계가 적절한지 검증합니다.

실제 현업에서는 어떻게 활용할까요? 예를 들어 전자상거래 플랫폼의 주문 데이터를 분석한다고 가정해봅시다.

주문 데이터는 주로 날짜별로 조회되므로 order_date로 파티션을 나누면 효과적입니다. 특정 기간의 매출을 분석할 때 해당 날짜의 파티션만 읽으면 되니까요.

많은 기업에서 이런 시간 기반 파티셔닝 패턴을 적극적으로 사용하고 있습니다. 하지만 주의할 점도 있습니다.

초보 데이터 엔지니어들이 흔히 하는 실수 중 하나는 카디널리티가 너무 높은 컬럼으로 파티션을 나누는 것입니다. 이렇게 하면 작은 파일이 수백만 개 생성되어 메타데이터 관리 오버헤드가 발생합니다.

따라서 적절한 카디널리티(수백~수천 개)를 가진 컬럼으로 파티션을 나누어야 합니다. 또 다른 중요한 원칙은 쿼리 패턴에 맞춘 파티션 설계입니다.

어떤 컬럼으로 자주 필터링하는지 분석해야 합니다. 만약 국가별, 날짜별로 주로 조회한다면 country와 date로 파티션을 나누는 것이 합리적입니다.

파티션 크기도 중요합니다. 너무 작으면 파일이 많아지고, 너무 크면 병렬 처리 효율이 떨어집니다.

일반적으로 파티션당 128MB~1GB 정도가 적절합니다. 이는 Spark의 기본 블록 사이즈와도 잘 맞습니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 박엔지니어 씨의 설명을 들은 김데이터 씨는 고개를 끄덕였습니다.

"아, 그래서 쿼리가 느렸군요!" 파티션 설계 원칙을 제대로 이해하면 더 빠르고 비용 효율적인 데이터 파이프라인을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 파티션 개수는 수백~수천 개가 적당하며, 수만 개를 넘으면 재설계를 고려하세요

  • 쿼리 패턴을 먼저 분석한 후 가장 자주 필터링되는 컬럼으로 파티션을 구성하세요
  • 파티션당 크기는 128MB~1GB를 목표로 하며, 너무 작은 파일은 compaction으로 병합하세요

2. 파티션 프루닝 최적화

김데이터 씨는 파티션 설계를 개선한 후 쿼리가 빨라질 것으로 기대했습니다. 하지만 여전히 전체 데이터를 읽는 것처럼 느렸습니다.

박엔지니어 씨가 쿼리 실행 계획을 보더니 말했습니다. "파티션 프루닝이 제대로 작동하지 않고 있네요."

파티션 프루닝은 쿼리 조건에 따라 불필요한 파티션을 건너뛰고 필요한 파티션만 읽는 최적화 기법입니다. 마치 백과사전에서 'ㅎ' 항목을 찾을 때 앞부분을 건너뛰고 뒷부분만 펼치는 것과 같습니다.

프루닝이 제대로 작동하려면 쿼리 작성 방법과 파티션 컬럼 사용에 주의해야 합니다.

다음 코드를 살펴봅시다.

from pyspark.sql.functions import col, to_date

# 파티션 프루닝이 작동하지 않는 예
# df.filter(to_date(col("timestamp")) == "2024-01-15")  # timestamp를 변환하면 프루닝 안됨

# 파티션 프루닝이 작동하는 올바른 예
df = spark.read.parquet("partitioned_logs/")

# 파티션 컬럼을 직접 사용
filtered_df = df.filter(
    (col("year") == 2024) &
    (col("month") == 1) &
    (col("day") == 15)
)

# 실행 계획 확인 - PartitionFilters 항목을 체크
filtered_df.explain(True)

김데이터 씨는 파티션을 year, month, day로 깔끔하게 나누었습니다. 이제 쿼리가 빨라질 거라고 확신했습니다.

하지만 막상 실행해보니 여전히 느렸습니다. 뭔가 잘못된 것 같은데 원인을 찾을 수가 없었습니다.

박엔지니어 씨가 실행 계획을 살펴봅니다. "아, 파티션 프루닝이 안 되고 있어요.

쿼리를 어떻게 작성했나요?" 그렇다면 파티션 프루닝이란 정확히 무엇일까요? 쉽게 비유하자면, 파티션 프루닝은 마치 옷장에서 여름옷을 찾을 때 겨울옷 서랍은 아예 열지 않는 것과 같습니다.

여름옷이 필요하다는 것을 미리 알고 있으니 겨울옷 서랍을 뒤질 필요가 없는 거죠. 이처럼 파티션 프루닝도 쿼리 조건을 분석해서 불필요한 파티션을 읽지 않는 최적화 기법입니다.

파티션 프루닝이 작동하지 않으면 어떻게 될까요? Spark는 모든 파티션을 다 읽어야 합니다.

1년치 데이터 중 하루치만 필요해도 365개 파티션을 전부 스캔하게 됩니다. 이는 엄청난 I/O 낭비입니다.

더 큰 문제는 메모리 사용량도 급증한다는 점입니다. 불필요한 데이터까지 메모리에 로드되니 OOM 에러가 발생할 수도 있습니다.

클라우드 환경에서는 비용도 그만큼 증가하게 됩니다. 바로 이런 문제를 해결하기 위해 파티션 프루닝 최적화가 중요합니다.

프루닝이 제대로 작동하면 읽는 데이터양이 대폭 감소합니다. 365개 중 1개만 읽으면 되니까요.

또한 쿼리 실행 시간이 단축됩니다. I/O가 줄면 당연히 빨라지겠죠.

무엇보다 리소스 효율성이라는 큰 이점이 있습니다. CPU, 메모리, 네트워크 모두 절약됩니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 잘못된 예시를 보면 timestamp 컬럼에 to_date 함수를 적용하고 있습니다.

이 부분이 문제입니다. Spark는 함수가 적용된 컬럼은 파티션 프루닝에 사용할 수 없습니다.

다음으로 올바른 예시에서는 파티션 컬럼인 year, month, day를 직접 필터 조건에 사용합니다. 이렇게 하면 Spark가 조건을 분석해서 2024년 1월 15일 파티션만 읽습니다.

마지막으로 explain 메소드로 실행 계획을 출력하여 PartitionFilters 항목을 확인할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 로그 분석 시스템에서 최근 7일간의 에러 로그를 조회한다고 가정해봅시다. 파티션이 날짜별로 나뉘어 있다면 해당 7개 파티션만 읽으면 됩니다.

하지만 쿼리를 잘못 작성하면 수백 개의 파티션을 모두 읽게 됩니다. Netflix, Uber 같은 기업들은 파티션 프루닝을 철저히 활용해서 페타바이트급 데이터를 효율적으로 처리하고 있습니다.

하지만 주의할 점도 있습니다. 초보 엔지니어들이 흔히 하는 실수 중 하나는 파티션 컬럼에 함수를 적용하는 것입니다.

예를 들어 WHERE LOWER(country) = 'kr' 같은 조건을 사용하면 프루닝이 비활성화됩니다. 따라서 파티션 컬럼은 원본 그대로 필터 조건에 사용해야 합니다.

또 다른 실수는 OR 조건을 잘못 사용하는 것입니다. WHERE year = 2024 OR user_id = 123 같은 쿼리는 프루닝이 제대로 작동하지 않을 수 있습니다.

파티션 조건과 비파티션 조건이 OR로 결합되면 Spark는 안전하게 전체 스캔을 선택하기 때문입니다. 프루닝 최적화를 확인하는 가장 좋은 방법은 실행 계획을 분석하는 것입니다.

explain() 메소드를 호출하면 PartitionFilters 항목에 어떤 조건이 프루닝에 사용되는지 나타납니다. 이 항목이 비어있다면 프루닝이 작동하지 않는 것입니다.

성능 모니터링도 중요합니다. Spark UI에서 각 태스크가 읽은 데이터양을 확인할 수 있습니다.

프루닝이 제대로 작동한다면 읽은 데이터양이 예상치와 일치해야 합니다. 만약 전체 데이터를 읽고 있다면 쿼리를 재검토해야 합니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 박엔지니어 씨의 조언대로 쿼리를 수정한 김데이터 씨는 놀라운 변화를 경험했습니다.

"와, 10분 걸리던 쿼리가 10초로 줄었어요!" 파티션 프루닝 최적화를 제대로 이해하면 같은 하드웨어로도 훨씬 빠른 쿼리를 실행할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 쿼리 작성 후 반드시 explain()으로 PartitionFilters를 확인하여 프루닝이 작동하는지 검증하세요

  • 파티션 컬럼에는 절대 함수나 연산을 적용하지 말고 원본 값 그대로 비교하세요
  • Spark UI에서 실제 읽은 데이터양을 모니터링하여 프루닝 효과를 측정하세요

3. 동적 파티션 오버라이트

김데이터 씨는 매일 새로운 데이터를 적재하는 파이프라인을 운영하고 있었습니다. 그런데 실수로 전체 데이터를 날려버린 사고가 발생했습니다.

박엔지니어 씨가 달려와 물었습니다. "동적 파티션 오버라이트 모드를 사용하지 않았나요?"

동적 파티션 오버라이트는 전체 테이블이 아닌 실제로 쓰여진 파티션만 덮어쓰는 안전한 데이터 적재 방식입니다. 마치 사진첩에서 특정 날짜의 사진만 교체하고 다른 날짜는 그대로 두는 것과 같습니다.

이 방식을 사용하면 데이터 손실 위험을 크게 줄일 수 있습니다.

다음 코드를 살펴봅시다.

from pyspark.sql import SparkSession

spark = SparkSession.builder \
    .appName("DynamicPartition") \
    .config("spark.sql.sources.partitionOverwriteMode", "dynamic") \
    .getOrCreate()

# 새로운 데이터 (2024-01-15 하루치만 존재)
new_data = spark.read.json("new_logs/2024-01-15/")

# 정적 모드(기본값): 전체 테이블 삭제 후 새 데이터만 남음 - 위험!
# new_data.write.mode("overwrite").partitionBy("year", "month", "day").parquet("logs/")

# 동적 모드: 2024-01-15 파티션만 덮어쓰고 나머지는 유지 - 안전!
new_data.write \
    .mode("overwrite") \
    .partitionBy("year", "month", "day") \
    .parquet("logs/")

김데이터 씨는 일일 배치 작업을 담당하고 있었습니다. 매일 아침 새로운 로그 데이터를 적재하는 간단한 작업이었습니다.

그런데 어느 날 치명적인 실수를 저질렀습니다. 하루치 데이터를 적재했는데 기존 데이터가 모두 사라진 것입니다.

박엔지니어 씨가 급히 달려왔습니다. "무슨 일이에요?" 김데이터 씨는 당황해서 대답했습니다.

"모르겠어요. 그냥 overwrite 모드로 썼을 뿐인데..." 그렇다면 동적 파티션 오버라이트란 정확히 무엇일까요?

쉽게 비유하자면, 동적 파티션 오버라이트는 마치 달력에서 오늘 날짜만 새로 쓰고 다른 날짜는 건드리지 않는 것과 같습니다. 만약 1월 15일 일정을 수정한다고 해서 1월 전체를 지우지는 않겠죠.

이처럼 동적 파티션 오버라이트도 실제로 데이터를 쓰는 파티션만 교체하고 나머지는 그대로 보존합니다. 동적 파티션 오버라이트가 없던 시절에는 어땠을까요?

기본 overwrite 모드는 정적 오버라이트 방식이었습니다. 이 모드에서는 전체 테이블 디렉토리를 삭제한 후 새 데이터를 씁니다.

하루치 데이터만 쓰더라도 전체가 날아가는 것이죠. 엔지니어들은 이 문제를 해결하기 위해 복잡한 로직을 작성해야 했습니다.

더 큰 문제는 동시성 이슈였습니다. 여러 작업이 동시에 실행되면 데이터 손실이 발생할 수 있었습니다.

바로 이런 문제를 해결하기 위해 동적 파티션 오버라이트가 등장했습니다. 동적 모드를 사용하면 데이터 안전성이 크게 향상됩니다.

잘못된 조작으로 전체 데이터가 삭제되는 일이 없습니다. 또한 동시 작업 지원도 가능해집니다.

각 작업이 서로 다른 파티션을 업데이트한다면 충돌 없이 실행됩니다. 무엇보다 코드 단순화라는 큰 이점이 있습니다.

파티션별 삭제 로직을 직접 작성할 필요가 없으니까요. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 SparkSession을 생성할 때 중요한 설정을 추가합니다. spark.sql.sources.partitionOverwriteModedynamic으로 설정하는 것이 핵심입니다.

이 설정이 없으면 기본값인 static 모드로 동작합니다. 다음으로 새로운 데이터를 읽어옵니다.

이 데이터는 2024년 1월 15일 하루치만 포함하고 있습니다. 마지막으로 mode를 overwrite로 지정하고 파티션별로 저장합니다.

동적 모드가 활성화되어 있으므로 2024-01-15 파티션만 덮어쓰고 다른 날짜는 그대로 유지됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 전자상거래 플랫폼에서 일일 매출 리포트를 생성한다고 가정해봅시다. 어제 데이터에 오류가 발견되어 재처리해야 하는 상황입니다.

동적 파티션 오버라이트를 사용하면 어제 날짜 파티션만 안전하게 교체할 수 있습니다. 나머지 날짜의 데이터는 전혀 영향을 받지 않습니다.

Airbnb, LinkedIn 같은 기업들은 이런 방식으로 안전한 데이터 파이프라인을 운영하고 있습니다. 하지만 주의할 점도 있습니다.

동적 파티션 오버라이트는 Parquet, ORC 같은 파일 기반 포맷에서만 작동합니다. Hive 테이블도 지원하지만 설정이 추가로 필요합니다.

초보 엔지니어들이 흔히 하는 실수 중 하나는 설정을 세션 레벨이 아닌 쿼리 레벨에서만 지정하는 것입니다. 따라서 SparkSession 생성 시 설정하는 것이 안전합니다.

또 다른 주의점은 Delta Lake와의 차이점입니다. Delta Lake는 자체적으로 트랜잭션을 지원하므로 동적 파티션 오버라이트가 필요하지 않습니다.

Delta Lake를 사용한다면 replaceWhere 옵션을 사용하는 것이 더 효율적입니다. 성능 측면에서도 고려할 점이 있습니다.

동적 모드는 기존 파티션 목록을 먼저 스캔한 후 해당 파티션만 삭제합니다. 파티션 개수가 매우 많다면 이 과정에서 약간의 오버헤드가 발생할 수 있습니다.

하지만 데이터 안전성을 고려하면 충분히 감수할 만한 비용입니다. 실무에서는 모니터링과 검증도 중요합니다.

작업 완료 후 파티션 개수를 확인하여 예상대로 동작했는지 검증해야 합니다. 로그를 분석해서 어떤 파티션이 덮어써졌는지 추적하는 것도 좋은 습관입니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 박엔지니어 씨의 조언으로 동적 파티션 오버라이트를 적용한 후, 김데이터 씨는 안심하고 배치 작업을 운영할 수 있게 되었습니다.

"이제는 실수로 데이터를 날릴 걱정이 없네요!" 동적 파티션 오버라이트를 제대로 이해하면 안전하고 신뢰할 수 있는 데이터 파이프라인을 구축할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - SparkSession 생성 시 partitionOverwriteMode를 dynamic으로 설정하여 전역 적용하세요

  • Delta Lake 사용 시에는 replaceWhere 옵션이 더 효율적이므로 이를 고려하세요
  • 작업 후 파티션 개수와 로그를 확인하여 의도한 파티션만 업데이트되었는지 검증하세요

4. 파티션 재구성 전략

김데이터 씨의 시스템은 2년째 잘 운영되고 있었습니다. 그런데 최근 들어 쿼리가 점점 느려지고 있었습니다.

박엔지니어 씨가 파티션 구조를 살펴보더니 말했습니다. "파티션 재구성이 필요한 시점이네요.

작은 파일이 너무 많아졌어요."

파티션 재구성은 시간이 지나면서 비효율적으로 변한 파티션 구조를 최적화하는 작업입니다. 마치 서랍을 정리하듯이 작은 파일들을 병합하고, 큰 파일은 분할하며, 쿼리 패턴에 맞게 파티션 키를 변경합니다.

적절한 재구성은 성능을 크게 향상시킵니다.

다음 코드를 살펴봅시다.

from pyspark.sql.functions import col

# 1단계: 현재 파티션 상태 분석
df = spark.read.parquet("old_partitioned_data/")
print(f"총 파티션 수: {df.rdd.getNumPartitions()}")

# 2단계: 작은 파일 병합 (Compaction)
df.repartition(200, "year", "month") \
    .write \
    .mode("overwrite") \
    .partitionBy("year", "month") \
    .parquet("compacted_data/")

# 3단계: 파티션 키 변경 (예: day 단위에서 hour 단위로)
df.withColumn("hour", col("timestamp").substr(12, 2)) \
    .write \
    .mode("overwrite") \
    .partitionBy("year", "month", "day", "hour") \
    .parquet("repartitioned_data/")

김데이터 씨는 2년 전에 설계한 데이터 파이프라인을 운영하고 있었습니다. 처음에는 잘 작동했지만 시간이 지날수록 점점 느려졌습니다.

특히 전체 데이터를 스캔하는 쿼리는 견딜 수 없을 정도로 느려졌습니다. 박엔지니어 씨가 스토리지를 조사하더니 한숨을 쉬었습니다.

"여기 보세요. 파티션 하나에 수백 개의 작은 파일이 있어요.

이런 파일이 수만 개 쌓여있네요." 그렇다면 파티션 재구성이란 정확히 무엇일까요? 쉽게 비유하자면, 파티션 재구성은 마치 집안 대청소와 같습니다.

처음에는 깔끔하게 정리했지만 시간이 지나면서 물건이 쌓이고 어지러워집니다. 주기적으로 청소하면서 비슷한 것끼리 모으고, 불필요한 것은 버리고, 새로운 수납 방식을 도입하는 것처럼, 파티션 재구성도 데이터를 효율적으로 다시 배치하는 작업입니다.

파티션 재구성이 필요한 이유는 무엇일까요? 시간이 지나면서 작은 파일 문제가 발생합니다.

Spark나 Hive는 각 파일을 별도 태스크로 처리하므로 파일이 많을수록 오버헤드가 커집니다. 1MB짜리 파일 1000개를 처리하는 것보다 1GB 파일 1개를 처리하는 것이 훨씬 효율적입니다.

또한 쿼리 패턴의 변화도 재구성이 필요한 이유입니다. 처음에는 일 단위 조회가 많았지만 이제는 시간 단위 조회가 많다면 파티션 키를 변경해야 합니다.

바로 이런 문제를 해결하기 위해 파티션 재구성 전략이 필요합니다. 재구성을 통해 쿼리 성능이 향상됩니다.

작은 파일이 병합되면 태스크 오버헤드가 줄어듭니다. 또한 스토리지 효율도 개선됩니다.

압축률이 높아지고 메타데이터가 줄어들죠. 무엇보다 유지보수성이라는 큰 이점이 있습니다.

깔끔한 구조는 문제 진단과 최적화를 쉽게 만듭니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 1단계에서는 현재 파티션 상태를 분석합니다. 총 파티션 수를 확인하는 것이 첫 번째 단계입니다.

다음으로 2단계에서는 작은 파일들을 병합합니다. repartition 메소드로 적절한 개수(200개)로 재분배하면서 year, month 기준으로 파티션을 유지합니다.

이 과정을 컴팩션이라고 부릅니다. 마지막 3단계에서는 파티션 키를 변경합니다.

기존의 day 단위에서 hour 단위로 세분화하여 더 정교한 프루닝을 가능하게 합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 IoT 센서 데이터를 수집하는 시스템이 있다고 가정해봅시다. 초기에는 일 단위로 파티션을 나누었지만, 실시간 모니터링 요구사항이 생겨서 시간 단위 조회가 많아졌습니다.

이럴 때 파티션을 시간 단위로 재구성하면 쿼리 성능이 크게 향상됩니다. Spotify, Netflix 같은 기업들은 정기적으로 파티션 재구성 작업을 수행하여 페타바이트급 데이터를 효율적으로 관리하고 있습니다.

하지만 주의할 점도 있습니다. 파티션 재구성은 비용이 많이 드는 작업입니다.

전체 데이터를 읽고 다시 써야 하므로 많은 리소스를 소비합니다. 따라서 사용량이 적은 시간대에 실행하는 것이 좋습니다.

초보 엔지니어들이 흔히 하는 실수 중 하나는 운영 중인 테이블을 직접 재구성하려는 것입니다. 이렇게 하면 쿼리 실패나 데이터 불일치가 발생할 수 있습니다.

안전한 재구성 프로세스는 다음과 같습니다. 먼저 새로운 위치에 재구성합니다.

원본 데이터는 그대로 두고 새 디렉토리에 최적화된 데이터를 씁니다. 다음으로 검증을 수행합니다.

레코드 수, 합계 등을 비교하여 데이터 무결성을 확인합니다. 마지막으로 원자적 교체를 수행합니다.

Hive 테이블이라면 ALTER TABLE로 파티션을 교체하고, 파일 시스템이라면 심볼릭 링크를 사용합니다. 컴팩션 주기도 중요합니다.

너무 자주 하면 리소스 낭비이고, 너무 늦으면 성능 저하가 심해집니다. 일반적으로 파일 개수가 최적 개수의 2-3배가 되면 컴팩션을 고려합니다.

예를 들어 최적 파일 개수가 10개인데 현재 30개라면 컴팩션이 필요합니다. 파티션 키 변경은 더 신중해야 합니다.

기존 쿼리가 깨질 수 있기 때문입니다. 쿼리 패턴을 철저히 분석한 후 의사결정해야 합니다.

가능하다면 하위 호환성을 유지하는 방향으로 변경하는 것이 좋습니다. 예를 들어 day 파티션을 유지하면서 hour를 추가하는 식입니다.

다시 김데이터 씨의 이야기로 돌아가 봅시다. 박엔지니어 씨의 도움으로 파티션 재구성을 완료한 후, 쿼리 성능이 극적으로 개선되었습니다.

"10분 걸리던 쿼리가 1분으로 줄었어요! 그리고 스토리지 비용도 30% 절감됐습니다." 파티션 재구성 전략을 제대로 이해하면 장기적으로 안정적이고 효율적인 데이터 시스템을 운영할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 파일 개수가 최적 개수의 2-3배가 되면 컴팩션을 고려하고, 사용량 적은 시간대에 실행하세요

  • 재구성 시 원본은 유지하고 새 위치에 작업한 후 검증을 거쳐 원자적으로 교체하세요
  • 파티션 키 변경은 기존 쿼리 영향을 분석한 후 하위 호환성을 고려하여 진행하세요

5. 시간 기반 파티셔닝 패턴

김데이터 씨는 새로운 실시간 분석 프로젝트를 시작했습니다. 데이터가 계속 쌓이는 시계열 데이터였습니다.

박엔지니어 씨가 조언했습니다. "시간 기반 파티셔닝을 제대로 설계하지 않으면 나중에 큰 문제가 생길 거예요."

시간 기반 파티셔닝은 시계열 데이터를 시간 단위로 효율적으로 분할하는 가장 보편적인 파티셔닝 패턴입니다. 연, 월, 일, 시간 등의 계층 구조로 데이터를 나누어 조회 성능과 데이터 관리 효율을 극대화합니다.

거의 모든 빅데이터 시스템에서 기본으로 사용되는 핵심 패턴입니다.

다음 코드를 살펴봅시다.

from pyspark.sql.functions import year, month, dayofmonth, hour, col
from datetime import datetime, timedelta

# 타임스탬프 컬럼을 파티션 컬럼으로 변환
df = spark.read.json("streaming_logs/")

partitioned_df = df \
    .withColumn("year", year(col("timestamp"))) \
    .withColumn("month", month(col("timestamp"))) \
    .withColumn("day", dayofmonth(col("timestamp"))) \
    .withColumn("hour", hour(col("timestamp")))

# 계층적 시간 파티션으로 저장
partitioned_df.write \
    .partitionBy("year", "month", "day", "hour") \
    .mode("append") \
    .parquet("time_partitioned_logs/")

# 최근 24시간 데이터만 조회 (효율적인 프루닝)
yesterday = datetime.now() - timedelta(days=1)
recent_df = spark.read.parquet("time_partitioned_logs/") \
    .filter(col("timestamp") >= yesterday)

김데이터 씨는 회사의 새로운 핵심 프로젝트를 맡게 되었습니다. 수백만 사용자의 행동 로그를 실시간으로 수집하고 분석하는 시스템이었습니다.

데이터는 초당 수천 건씩 쌓여가고 있었습니다. 어떻게 파티션을 설계해야 할지 막막했습니다.

박엔지니어 씨가 화이트보드에 그림을 그리며 설명했습니다. "시계열 데이터는 시간 기반 파티셔닝이 답입니다.

이건 거의 산업 표준이에요." 그렇다면 시간 기반 파티셔닝이란 정확히 무엇일까요? 쉽게 비유하자면, 시간 기반 파티셔닝은 마치 일기장을 정리하는 것과 같습니다.

2024년 일기장 안에 1월부터 12월 섹션이 있고, 각 월 안에 1일부터 31일까지 페이지가 있습니다. 특정 날짜의 일기를 찾을 때 연도와 월을 거쳐서 바로 해당 날짜로 갈 수 있죠.

이처럼 시간 기반 파티셔닝도 데이터를 시간의 계층 구조로 체계적으로 분류합니다. 시간 기반 파티셔닝이 왜 이렇게 널리 사용될까요?

대부분의 쿼리가 시간 범위로 필터링되기 때문입니다. "최근 7일간의 매출", "어제의 에러 로그", "지난달 신규 가입자" 같은 쿼리를 생각해보세요.

모두 시간을 기준으로 데이터를 조회합니다. 또한 시간은 자연스러운 카디널리티를 가집니다.

일 단위면 1년에 365개, 시간 단위면 8760개로 적절한 파티션 개수를 유지합니다. 바로 이런 이유로 시간 기반 파티셔닝이 표준이 되었습니다.

시간 파티셔닝을 사용하면 조회 성능이 극대화됩니다. 날짜 범위 쿼리에서 해당 파티션만 읽으면 되니까요.

또한 데이터 라이프사이클 관리가 쉬워집니다. 오래된 파티션은 아카이브하거나 삭제하기만 하면 됩니다.

무엇보다 직관적인 구조라는 큰 이점이 있습니다. 누가 봐도 이해하기 쉽고 관리하기 편합니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 원본 데이터를 읽어옵니다.

이 데이터에는 timestamp 컬럼이 있습니다. 다음으로 중요한 변환 작업을 수행합니다.

timestamp에서 year, month, day, hour를 추출하여 별도 컬럼으로 만듭니다. 이 컬럼들이 파티션 키가 됩니다.

그런 다음 partitionBy로 계층적 파티션 구조를 지정하여 저장합니다. 마지막으로 최근 24시간 데이터를 조회하는 예제를 보여줍니다.

파티션 프루닝이 작동하여 해당 기간의 파티션만 읽게 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 전자상거래 플랫폼의 클릭스트림 데이터를 생각해봅시다. 마케팅 팀은 "어제 특정 상품 페이지를 본 사용자 수"를 자주 조회합니다.

시간 기반 파티셔닝이 되어 있으면 어제 날짜의 파티션만 읽으면 됩니다. 데이터가 수 테라바이트여도 실제로는 수십 기가바이트만 읽게 되죠.

Amazon, Google, Facebook 같은 기업들은 페타바이트급 로그 데이터를 모두 시간 기반 파티셔닝으로 관리하고 있습니다. 하지만 주의할 점도 있습니다.

시간 파티셔닝에서 가장 중요한 결정은 파티션 단위 선택입니다. 일 단위로 할지, 시간 단위로 할지, 아니면 분 단위까지 갈지 결정해야 합니다.

초보 엔지니어들이 흔히 하는 실수 중 하나는 너무 세밀한 단위를 선택하는 것입니다. 분 단위로 파티션을 나누면 1년이면 52만 개 파티션이 생성됩니다.

이는 너무 많습니다. 일반적인 가이드라인은 다음과 같습니다.

배치 처리 시스템은 일 단위나 시간 단위가 적절합니다. 실시간 분석 시스템은 시간 단위나 분 단위를 고려할 수 있습니다.

하지만 분 단위는 정말 필요한 경우에만 사용해야 합니다. 대부분의 경우 일 단위나 시간 단위면 충분합니다.

또 다른 고려사항은 타임존입니다. UTC로 저장할지, 로컬 타임존으로 저장할지 결정해야 합니다.

글로벌 서비스라면 UTC로 통일하는 것이 권장됩니다. 타임존 변환은 쿼리 시점에 하는 것이 안전합니다.

데이터 보존 정책과의 통합도 중요합니다. "90일 이전 데이터는 삭제" 같은 정책이 있다면 파티션 단위로 쉽게 구현할 수 있습니다.

단순히 오래된 파티션 디렉토리를 삭제하면 되니까요. 이것이 시간 파티셔닝의 큰 장점 중 하나입니다.

스트리밍 데이터와의 결합도 고려해야 합니다. Structured Streaming이나 Flink를 사용한다면 마이크로배치 단위로 파티션에 append 됩니다.

이 경우 작은 파일 문제가 발생할 수 있으므로 주기적인 컴팩션이 필요합니다. 다시 김데이터 씨의 이야기로 돌아가 봅시다.

박엔지니어 씨의 조언을 따라 시간 기반 파티셔닝을 구현한 후, 시스템이 안정적으로 운영되기 시작했습니다. "쿼리도 빠르고, 오래된 데이터 삭제도 간단하네요.

왜 모두가 이 패턴을 쓰는지 이제 알겠어요!" 시간 기반 파티셔닝 패턴을 제대로 이해하면 시계열 데이터 시스템을 효율적으로 설계할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 일반적으로 일 단위나 시간 단위가 최적이며, 분 단위는 정말 필요한 경우에만 사용하세요

  • 글로벌 서비스는 UTC로 저장하고 타임존 변환은 쿼리 시점에 수행하세요
  • 데이터 보존 정책과 통합하여 오래된 파티션을 주기적으로 삭제하는 자동화를 구축하세요

6. 파티션 vs Z-Order 하이브리드

김데이터 씨는 Delta Lake를 처음 접했습니다. 문서를 읽다가 Z-Ordering이라는 개념을 발견했습니다.

박엔지니어 씨에게 물었습니다. "Z-Order를 쓰면 파티션은 필요 없나요?" 박엔지니어 씨가 웃으며 대답했습니다.

"둘을 함께 쓰는 하이브리드 전략이 가장 강력합니다."

파티션과 Z-Order 하이브리드는 Delta Lake에서 파티션의 물리적 분할과 Z-Order의 지능적 정렬을 결합하여 최상의 쿼리 성능을 달성하는 고급 전략입니다. 마치 책을 장르별로 나누고(파티션) 각 장르 내에서 알파벳순으로 정렬하는(Z-Order) 것처럼 다차원 최적화를 제공합니다.

다음 코드를 살펴봅시다.

from delta.tables import DeltaTable

# 1단계: 파티션으로 대략적 분할 (카디널리티 낮은 컬럼)
df = spark.read.json("user_events/")

df.write \
    .format("delta") \
    .partitionBy("date") \
    .mode("overwrite") \
    .save("/delta/events")

# 2단계: 각 파티션 내에서 Z-Order 최적화 (카디널리티 높은 컬럼)
deltaTable = DeltaTable.forPath(spark, "/delta/events")

deltaTable.optimize() \
    .where("date >= '2024-01-01'") \
    .executeZOrderBy("user_id", "event_type")

# 하이브리드 쿼리: 파티션 프루닝 + 데이터 스키핑
result = spark.read.format("delta").load("/delta/events") \
    .filter("date = '2024-01-15' AND user_id = 12345")

김데이터 씨는 회사가 Delta Lake로 전환하면서 새로운 개념들을 공부하고 있었습니다. 특히 Z-Ordering이라는 기능이 흥미로웠습니다.

문서에서는 "다차원 데이터 정렬로 쿼리 성능을 향상시킨다"고 설명했습니다. 그렇다면 파티션은 이제 필요 없는 걸까요?

박엔지니어 씨가 화이트보드에 도표를 그리며 설명했습니다. "파티션과 Z-Order는 경쟁 관계가 아니라 상호 보완 관계예요.

함께 쓰면 시너지가 엄청납니다." 그렇다면 파티션과 Z-Order 하이브리드란 정확히 무엇일까요? 쉽게 비유하자면, 이 전략은 마치 대형 백화점의 상품 진열 시스템과 같습니다.

먼저 층별로 카테고리를 나눕니다(파티션). 1층은 화장품, 2층은 의류, 3층은 가전제품처럼요.

그다음 각 층 내에서는 브랜드별, 가격대별로 세밀하게 정렬합니다(Z-Order). 고객이 "2층의 나이키 30만원대 신발"을 찾을 때 2층으로 바로 가서(파티션 프루닝) 나이키 섹션의 30만원대 구역을 찾습니다(데이터 스키핑).

이처럼 하이브리드 전략도 거시적 분할과 미시적 최적화를 결합합니다. 왜 파티션만으로는 부족할까요?

파티션은 물리적 디렉토리 분할입니다. 날짜로 파티션을 나누면 각 날짜가 별도 디렉토리가 됩니다.

하지만 파티션 내부에서는 여전히 전체 스캔이 필요합니다. 예를 들어 "2024-01-15일의 user_id=12345 데이터"를 찾을 때 해당 날짜 파티션은 찾지만, 그 안에서는 모든 파일을 읽어야 합니다.

또한 카디널리티가 높은 컬럼을 파티션으로 쓸 수 없습니다. user_id가 백만 개면 백만 개 디렉토리가 생기니까요.

바로 이런 문제를 해결하기 위해 Z-Order가 등장했습니다. Z-Order는 파일 내에서 다차원 데이터를 지능적으로 정렬합니다.

user_id와 event_type 같은 여러 컬럼을 동시에 고려하여 관련된 데이터를 가까이 배치합니다. 그리고 Delta Lake는 각 파일의 최소/최대값 통계를 메타데이터에 저장합니다.

쿼리 시점에 이 통계를 보고 필요 없는 파일을 건너뛰는 것을 데이터 스키핑이라고 합니다. 하이브리드 전략을 사용하면 두 기법의 장점을 모두 얻습니다.

먼저 파티션으로 대략적 분할을 수행합니다. 카디널리티가 낮고 쿼리에서 항상 사용되는 컬럼을 선택합니다.

날짜가 대표적입니다. 다음으로 Z-Order로 세밀한 최적화를 수행합니다.

각 파티션 내에서 카디널리티가 높은 컬럼들을 Z-Order로 정렬합니다. user_id, product_id 같은 컬럼이 적절합니다.

결과적으로 파티션 프루닝과 데이터 스키핑이 동시에 작동하여 극적인 성능 향상을 얻습니다. 위의 코드를 한 줄씩 살펴보겠습니다.

먼저 1단계에서는 전통적인 파티셔닝을 수행합니다. date 컬럼으로 파티션을 나누어 Delta 테이블을 생성합니다.

이것만으로도 날짜 기반 쿼리는 빨라집니다. 다음으로 2단계에서 Z-Order 최적화를 실행합니다.

optimize().executeZOrderBy()를 호출하여 각 파티션 내의 데이터를 user_id와 event_type 기준으로 재정렬합니다. 마지막으로 하이브리드 효과를 보는 쿼리를 실행합니다.

date 조건으로 파티션 프루닝이 작동하고, user_id 조건으로 데이터 스키핑이 작동하여 극소수 파일만 읽게 됩니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 온라인 게임 회사의 플레이 로그를 생각해봅시다. 수억 명의 유저가 생성하는 로그는 엄청난 양입니다.

날짜로 파티션을 나누고, 각 파티션 내에서 user_id와 game_id로 Z-Order를 적용합니다. "2024-01-15일에 user_id=999가 game_id=777을 플레이한 기록"을 조회할 때 날짜 파티션 하나만 접근하고, 그 안에서도 극소수 파일만 읽게 됩니다.

Databricks, Uber, LinkedIn 같은 기업들은 이런 하이브리드 전략으로 페타바이트급 데이터를 밀리초 단위로 조회하고 있습니다. 하지만 주의할 점도 있습니다.

Z-Order 최적화는 비용이 많이 드는 작업입니다. 데이터를 읽고 재정렬한 후 다시 씁니다.

따라서 너무 자주 실행하면 리소스 낭비입니다. 일반적으로 데이터가 충분히 쌓인 후 주기적으로 실행하는 것이 좋습니다.

예를 들어 매일 새 데이터가 들어온다면 주 1회 정도 Z-Order를 실행하는 것이 적절합니다. Z-Order 컬럼 선택도 중요합니다.

쿼리에서 자주 사용되는 컬럼을 선택해야 합니다. 하지만 너무 많은 컬럼을 지정하면 효과가 떨어집니다.

일반적으로 2-4개 컬럼이 최적입니다. 가장 선택도가 높은 컬럼부터 지정하는 것이 좋습니다.

파티션 컬럼과 Z-Order 컬럼의 역할 분담도 명확해야 합니다. 카디널리티가 낮고 쿼리에서 거의 항상 사용되는 컬럼은 파티션으로 선택합니다.

날짜, 국가, 리전 같은 컬럼이 대표적입니다. 반면 카디널리티가 높고 선택적으로 사용되는 컬럼은 Z-Order로 지정합니다.

user_id, product_id, session_id 같은 컬럼이 적절합니다. 성능 측정도 필수입니다.

Z-Order 적용 전후로 쿼리 성능을 비교해야 합니다. Delta Lake의 데이터 스키핑 통계를 확인하면 얼마나 많은 파일을 건너뛰었는지 알 수 있습니다.

이 수치가 높을수록 Z-Order가 효과적이라는 뜻입니다. 다시 김데이터 씨의 이야기로 돌아가 봅시다.

하이브리드 전략을 적용한 후 쿼리 성능이 놀랍도록 향상되었습니다. "파티션만 썼을 때보다 10배는 더 빨라진 것 같아요!

그리고 파티션 개수도 적절하게 유지되네요." 파티션과 Z-Order 하이브리드 전략을 제대로 이해하면 Delta Lake의 진정한 힘을 발휘할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 파티션은 카디널리티 낮은 컬럼(날짜, 국가), Z-Order는 높은 컬럼(user_id, product_id)으로 역할 분담하세요

  • Z-Order 최적화는 주기적으로(주 1회 또는 월 1회) 실행하되, 데이터가 충분히 쌓인 후에 수행하세요
  • 데이터 스키핑 통계를 모니터링하여 Z-Order 효과를 측정하고 컬럼 선택을 조정하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Spark#Partitioning#Delta Lake#Z-Order#Performance Optimization#Data Engineering,Big Data,Delta Lake,Spark

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.