요약
데이터 레이크(Parquet/Iceberg)는 원래 컬럼 스캔 전용으로 설계되었다. 수억 행을 집계하는 분석 쿼리에는 탁월하지만, 특정 사용자 ID나 엔티티 키 하나를 1초 이내에 꺼내야 하는 포인트 쿼리에는 근본적으로 맞지 않는 구조다.
Spotify는 이 문제를 해결하기 위해 Random Access Parquet(RAP)를 설계했다. 핵심 아이디어는 단순하다: 데이터 자체를 복제하지 않고, 외부 인덱스가 "이 키는 이 파일의 이 바이트 오프셋에 있다"는 매핑만 저장한다. 같은 Iceberg 테이블이 분석·ML·AI 에이전트·온라인 서비스 네 가지 접근 패턴을 동시에 서빙할 수 있게 된다.
- 공개 시점: 2026-07 (Spotify Engineering Blog)
- 원본: https://engineering.atspotify.com/2026/07/random-access-parquet/
배경: 데이터 레이크가 포인트 쿼리에 약한 이유
Parquet 파일 구조의 근본 한계
Parquet는 행 그룹(Row Group) 단위로 데이터를 나누고, 각 컬럼을 따로 저장한다. 파일 하나에 보통 50만~100만 행이 들어가며, 특정 키 하나를 찾으려면:
- 파일 푸터에서 Row Group 메타데이터 읽기
- 컬럼별 min/max 통계로 해당 Row Group 후보군 좁히기
- 후보 Row Group 전체 다운로드 후 필터 적용
후보 Row Group 하나만 읽어도 수십 MB를 다운로드해야 한다. S3 레이턴시까지 더하면 단일 키 조회에 수 초가 걸린다.
일반적인 우회책과 그 비용
| 우회책 | 방식 | 문제 |
|---|---|---|
| Redis / DynamoDB 복제 | 별도 KV 스토어에 복제 | 데이터 이중화, 정합성 관리 부담 |
| 소형 파케이 파일 분할 | 키 범위별로 파일 잘게 나눔 | 파일 수 폭발, S3 list 비용 급증 |
| 인메모리 캐시 | 자주 쓰는 엔티티만 캐시 | 콜드 엔티티 SLA 불충족 |
| DuckDB/Polars 전체 로드 | 파일 전체를 메모리에 올림 | 메모리 한계, 테이블 크기 제약 |
Spotify 규모(수천 개 테이블, 각각 수억 행)에서는 어떤 우회책도 운영 부담 없이 유지되지 않았다.
RAP 아키텍처: 외부 인덱스 접근법
핵심 원칙: 인덱스는 포인터만 저장한다
RAP 인덱스의 각 항목은 (lookup_key) → (parquet_file_uri, row_group_id, row_offset_in_group)의 세 값이다. 실제 데이터는 원본 Parquet 파일에 그대로 있다. 인덱스는 포인터만 관리하기 때문에:
- 인덱스 크기가 원본 데이터 크기와 무관하게 컴팩트하게 유지된다
- 원본 테이블의 스키마나 접근 패턴이 바뀌어도 인덱스 재빌드만 하면 된다
- 인덱스 자체도 Parquet 포맷으로 저장되어 Iceberg 커밋에 연동된다
포인트 쿼리 흐름:
- 인덱스에서 키로
(file_uri, row_group_id, row_offset)조회 — 인덱스 파일은 수십 MB, 메모리에 올릴 수 있음 - S3 Range GET으로 해당 Row Group의 해당 행만 수 KB 다운로드
- 행 반환 — 전체 과정 목표 레이턴시 100ms 이내
전체 Row Group 다운로드 없이 필요한 행만 바이트 수준으로 접근하는 것이 핵심이다.
인덱스 빌드 전략 세 가지
전략 1: 해시 버킷팅 (Hash Bucketing)
키를 해시해서 버킷 번호를 결정하고, 같은 버킷의 키들을 같은 인덱스 파일에 모은다. 버킷 번호가 파일을 결정하기 때문에 인덱스 파일 자체를 전부 읽지 않아도 어느 파일을 봐야 할지 계산으로 알 수 있다.
bucket = hash(lookup_key) % NUM_BUCKETS
index_file = s3://index-bucket/table/bucket={bucket}/part-00000.parquet장점: 인덱스 조회가 한 번의 파일 접근으로 끝남 단점: NUM_BUCKETS 변경 시 전체 인덱스 재빌드 필요
전략 2: 코-그루핑 (Co-grouping)
원본 데이터를 Iceberg에 쓸 때 같은 키의 모든 행을 같은 Parquet 파일에 몰아넣는다. 키 기준으로 파티셔닝해서 쓰는 방식이다.
장점: 파일 하나를 보면 그 키의 모든 이력/버전을 얻을 수 있음 (타임시리즈 ML 피처에 유리) 단점: 쓰기 파이프라인에 키 기준 정렬이 필요, 데이터 쏠림 위험
전략 3: 정렬 기반 (Sort-based)
Parquet 파일 내 Row Group을 키 기준으로 정렬해서 쓴다. 인덱스 없이도 이진 탐색으로 Row Group을 좁힐 수 있지만, RAP는 여기에 인덱스를 더해 이진 탐색 자체도 건너뛴다.
장점: 범위 쿼리(range scan)와 포인트 쿼리를 동시에 지원 단점: 정렬 컬럼 외의 키로 조회 시 효과 없음
실제 운영에서는 세 전략을 조합한다. 기본 사용자 ID 조회는 해시 버킷팅, ML 피처 타임시리즈는 코-그루핑, 시간 범위 스캔이 함께 필요한 테이블은 정렬 기반을 선택한다.
인덱스 빌더: Iceberg 커밋에 연동되는 append-only 구조
Iceberg는 새 데이터가 추가될 때마다 새 스냅샷을 만든다. RAP 인덱스 빌더는 이 Iceberg 커밋 이벤트를 구독해서 추가된 Parquet 파일에 해당하는 인덱스 Fragment만 새로 생성한다.
새 Iceberg 스냅샷 감지
→ 신규 Parquet 파일 목록 추출
→ 각 파일 스캔: (key, file_uri, row_group_id, offset) 추출
→ 인덱스 Fragment Parquet 파일 생성 및 S3 업로드
→ 인덱스 메타데이터 업데이트 (Iceberg 커밋에 연동)이 구조의 장점:
- 증분 빌드: 전체 테이블을 다시 스캔하지 않는다. 새 파일에 대한 Fragment만 추가한다
- 원자성: Iceberg 커밋 단위와 인덱스 Fragment가 1:1로 대응된다. 인덱스와 데이터의 정합성을 Iceberg 스냅샷 ID로 보장한다
- 이전 버전 호환: 인덱스가 없는 구 파티션은 기존 스캔 방식으로 폴백한다
AI 에이전트 시대의 포인트 쿼리 급증
왜 지금 이 문제가 중요해졌는가
2025~2026년에 접어들면서 Spotify 같은 대형 플랫폼은 AI 에이전트 파이프라인에서 포인트 쿼리 폭발을 경험하기 시작했다.
분석 쿼리는 "사용자 1억 명의 청취 패턴을 집계해줘" 같은 형태다. 이건 Parquet가 잘 하는 일이다. 하지만 에이전트 추론 루프는 다르다: "사용자 A의 최근 10개 청취 이력 → A가 좋아한 아티스트의 팬 유사도 → 상위 5명 사용자 각각의 플레이리스트 → 각 플레이리스트의 최신 트랙 메타데이터 → …"
이런 체인에서 각 단계는 1개~수십 개 키의 포인트 조회다. 에이전트가 추론 중에 수백 번의 개별 엔티티 조회를 날리는 패턴이 일반화되고 있다.
기존 온라인 서빙 스택과의 차이
| 기준 | Redis/DynamoDB 복제 | RAP 외부 인덱스 |
|---|---|---|
| 데이터 신선도 | 복제 파이프라인 지연 | Iceberg 커밋 즉시 |
| 스키마 관리 | 2개 시스템 이중 관리 | 단일 Iceberg 스키마 |
| 쿼리 유연성 | KV 조회만 | 컬럼 프로젝션 자유 |
| 비용 | KV 스토어 상시 유지 비용 | S3 + 인덱스 파일만 |
| 데이터 복제 | 필수 | 없음 (포인터만) |
| 콜드 엔티티 SLA | 동일 | 동일 |
에이전트 파이프라인이 요구하는 "최신 데이터 + 다양한 컬럼 접근 + 낮은 레이턴시" 조합에서 RAP가 Redis 복제보다 운영 부담이 작다.
Iceberg와의 통합
RAP는 Iceberg의 기존 메타데이터 구조를 활용한다:
- Iceberg Table Properties: 테이블마다 어떤 컬럼을 인덱스 키로 쓸지, 어떤 버킷 전략을 쓸지 설정
- Iceberg Custom Metadata: 각 스냅샷에 해당 인덱스 Fragment의 S3 URI를 첨부
- Iceberg REST Catalog: 인덱스 메타데이터를 Catalog를 통해 조회 — 별도 인덱스 관리 서비스 불필요
Spark/Trino 같은 기존 쿼리 엔진은 인덱스를 인식하지 못해도 Iceberg 테이블을 그대로 읽는다. RAP 클라이언트만 인덱스를 활용한 포인트 쿼리 경로를 택한다. 기존 배치 파이프라인과 분석 쿼리는 영향 없다.
운영 시 고려 사항
인덱스 재빌드 비용
컬럼 추가, 버킷 수 변경, 키 컬럼 변경 시 전체 또는 부분 인덱스 재빌드가 필요하다. Spotify는 Iceberg Time Travel을 활용해 특정 스냅샷부터 다시 빌드하는 방식을 쓴다.
재빌드 트리거:
- 버킷 수 변경 (
NUM_BUCKETS수정) - 인덱스 키 컬럼 변경
- 인덱스 Fragment 오염 감지 (체크섬 불일치)
인덱스 Fragment 관리
인덱스 Fragment는 Iceberg 스냅샷 lifecycle을 따른다. expire_snapshots를 실행하면 해당 스냅샷의 Fragment도 함께 만료된다. 오래된 인덱스 파일을 별도로 정리할 필요가 없다.
레이턴시 목표와 캐싱
S3 Cold Read 기준 단일 포인트 쿼리 목표: 100ms 이내. 반복 접근이 잦은 인덱스 파일은 로컬 캐시(예: Alluxio, Caffeine)를 통해 10ms 이내로 내릴 수 있다.
인덱스 파일 크기가 작으면(수 MB~수십 MB) 조회 서비스 인스턴스 메모리에 전체 인덱스를 로드하는 것도 가능하다.
동시성과 정합성
Iceberg의 낙관적 잠금(optimistic concurrency control)을 사용하므로 여러 빌더가 동시에 Fragment를 생성해도 충돌이 자동 해소된다. 동일 Parquet 파일에 대해 중복 Fragment가 생성되면 메타데이터 업데이트 단계에서 감지되어 하나만 커밋된다.
체크리스트
- [ ] 포인트 쿼리가 필요한 테이블 목록을 Iceberg 메타데이터에서 식별했는가
- [ ] 인덱스 키 컬럼의 카디널리티와 분포(핫키 위험)를 사전에 측정했는가
- [ ] 버킷 전략(해시/코-그루핑/정렬)을 접근 패턴에 맞춰 선택했는가
- [ ] 인덱스 빌더가 Iceberg 커밋 이벤트를 구독하도록 파이프라인을 구성했는가
- [ ] 인덱스 Fragment와 Iceberg 스냅샷 expire 정책을 연동했는가
- [ ] RAP 클라이언트의 폴백 경로(인덱스 없으면 풀 스캔)를 검증했는가
- [ ] 레이턴시 SLA(100ms 이내)를 달성하기 위해 캐싱 레이어를 설계했는가
- [ ] 기존 Spark/Trino 파이프라인이 RAP 도입 후에도 정상 동작함을 확인했는가
References
- Spotify Engineering Blog, "Random Access Parquet" (2026-07): https://engineering.atspotify.com/2026/07/random-access-parquet/
- InfoQ, "Spotify Introduces Random Access Parquet for Low-Latency Point Queries on Data Lakes" (2026-08)
- Apache Iceberg 공식 문서 — Table Properties, Custom Metadata: https://iceberg.apache.org/docs/latest/configuration/
- Apache Parquet 파일 포맷 스펙 — Row Group, Page, Statistics: https://parquet.apache.org/docs/file-format/
- amdatalakehouse.substack.com, "Weekly: Spotify RAP, Iceberg v3, and Point Query Patterns" (2026-08)