데이터 레이크, 이제 온라인 서비스처럼 써야 할 때
많은 기업이 데이터 레이크에 방대한 데이터를 저장합니다. 하지만 실제로 특정 사용자의 데이터를 빠르게 조회해야 하는 온라인 서비스나 AI 에이전트에게 데이터 레이크는 그다지 친화적이지 않았습니다. Spark 배치 잡이나 Trino 같은 분산 SQL 엔진은 수십 초의 스케줄링과 쿼리 플랜 오버헤드가 발생하기 때문입니다.
Spotify는 이런 문제를 해결하기 위해 RAP(Random Access Parquet) 이라는 독특한 접근법을 제시했습니다. 이 글에서는 RAP의 핵심 아이디어와 이를 통해 데이터 레이크를 온라인 서비스처럼 활용하는 방법을 자세히 살펴보겠습니다.
왜 기존 방식은 느릴까?
Spotify의 사례를 예로 들어보겠습니다. AI 에이전트가 "지난 여름에 내가 뭘 들었지?"라는 질문에 답해야 한다고 가정해 봅시다. 사용자의 청취 기록은 수십억 명의 데이터가 담긴 대용량 파일들로 구성됩니다. 하루에 1,000개의 파일이 생성된다고 하면, 여름 90일 동안 약 90,000개의 Parquet 파일이 생깁니다.
기존 방식으로는 이 중에서 특정 사용자의 데이터를 찾기 위해 파일을 하나씩 열어봐야 합니다. 물론 파티셔닝이나 블룸 필터(Bloom Filter) 같은 기법으로 후보 파일을 줄일 수 있지만, 여전히 파일 내부에서 데이터를 찾는 과정은 복잡합니다. 파일의 footer를 읽고, row group 메타데이터를 파싱하고, 키 컬럼을 스캔하고, 페이지 인덱스를 확인하는 등 의존적인 읽기 체인이 발생하기 때문입니다. 이 체인의 각 단계는 클라우드 스토리지에 대한 별도의 왕복 요청을 필요로 하며, 이는 지연 시간과 비용을 증가시킵니다.
RAP의 핵심 아이디어: 외부 인덱스로 의존적 읽기를 제거
RAP의 핵심은 **외부 인덱스(external index)**입니다. 이 인덱스는 각 키를 특정 파일과 행 번호에 직접 매핑합니다. 주어진 키에 대해 인덱스를 조회하면 정확한 파일과 행을 알 수 있고, 파일 메타데이터를 캐시하여 페이지 위치를 찾은 후 필요한 바이트만 정확하게 읽습니다. 이 과정은 더 이상 의존적인 읽기 체인이 아니라 병렬로 실행 가능한 정밀한 범위 읽기입니다.
인덱스는 multimap 형태로, 하나의 키가 여러 파일과 파티션에 걸쳐 존재할 수 있습니다. 각 엔트리는 키, 파일, 행 번호, 값 개수(선택)로 구성됩니다. 대략적으로 테라바이트 규모의 데이터를 인덱싱하면 기가바이트 규모의 인덱스가 생성됩니다.
이 외부 인덱스는 Parquet의 기본 기능인 PageIndex나 Bloom Filter와는 다릅니다. 그것들은 확률적이고 스캔 범위를 줄여줄 뿐이지만, 외부 인덱스는 결정적입니다. 키가 주어지면 정확한 파일과 행을 반환하므로 스캔 자체를 없앱니다.
핵심 최적화 기법 3가지
RAP는 수정되지 않은 기존 Parquet 파일에서도 동작하지만, 쓰기 시점에 파일을 준비하면 더 큰 효과를 볼 수 있습니다. 최적화는 크게 세 가지 범주로 나뉩니다: 키 데이터 집중, 읽는 바이트 감소, 읽기 작업 수 감소.
1. 키 데이터 집중
- 키 정렬(Sorting by key): 파일 내에서 동일한 키의 모든 행이 연속적으로 위치하도록 정렬합니다. 많은 파이프라인이 이미 정렬된 출력을 생성합니다.
- 해시 버케팅(Hash bucketing): Spark, Scio SMB, Iceberg bucket transform을 사용하면 각 키가 파티션당 하나의 파일에 결정적으로 매핑됩니다.
- 공동 그룹화(Co-grouping): 스키마를 설계하여 각 키가 한 번만 나타나고 값이 반복 또는 중첩 컬럼에 위치하도록 합니다. 예를 들어
SELECT user_id, ARRAY_AGG(STRUCT(timestamp, track_uri, duration_ms)) FROM streams GROUP BY user_id형태로 구성할 수 있습니다. - 더 거친 파티셔닝: 일별 파티셔닝 대신 주별 파티셔닝을 사용하면 키가 걸쳐 있는 파일 수가 줄어듭니다.
2. 읽는 바이트 감소
- 키당 하나의 페이지(One page per key): 키가 변경될 때마다 페이지를 분리하여 작성합니다. 전체 페이지를 읽으면 해당 키의 데이터만 얻을 수 있습니다. 이 경우 페이지 위치를 인덱스에 직접 저장할 수 있습니다.
- ZSTD 프레임 리셋: 키별로 데이터를 별도의 ZSTD 프레임으로 압축합니다. 인덱스는 각 프레임의 오프셋과 크기를 저장합니다. 단, 이 경우 델타 인코딩이나 실행 길이 인코딩을 사용할 수 없습니다.
- 스토리지 정렬(Storage alignment): ZSTD 스키퍼블 프레임을 사용하여 키 사이에 패딩을 추가하고, 읽기가 스토리지 블록 경계를 넘지 않도록 합니다.
3. 읽기 작업 수 감소
- Blob 또는 Variant 단일 컬럼: JSON, Protobuf, Parquet Variant 등 포인트 쿼리에 필요한 필드를 하나의 컬럼으로 저장하면 파일당 한 번의 읽기만 수행합니다.
- 컬럼 인터리빙: 여러 컬럼을 물리적으로 인접하게 배치하고, ZSTD 스키퍼블 프레임으로 컬럼 사이를 연결합니다. RAP 리더는 키에 대한 모든 컬럼을 하나의 연속된 범위 읽기로 가져올 수 있습니다.
- 커버링 인덱스: 인덱스 빌더가 빌드 시점에 모든 행을 방문하므로, 작은 값은 인덱스 엔트리에 직접 호이스팅하여 스토리지 읽기를 완전히 없앨 수 있습니다.
실제 적용 시 주의사항 및 한계
RAP는 매우 강력한 기술이지만, 몇 가지 주의할 점이 있습니다.
- 인덱스 크기: 인덱스가 커지면 별도의 저장 비용이 발생합니다.
- 쓰기 복잡성: 쓰기 시점에 파일을 준비하려면 파이프라인 수정이 필요합니다.
- 호환성: 일부 최적화는 기존 리더와의 호환성 문제를 일으킬 수 있습니다. 예를 들어, 인터리빙된 컬럼을 읽는 일반 리더는 다른 컬럼의 데이터도 읽어야 하므로 I/O가 증가할 수 있습니다.
- 분석 워크로드와의 트레이드오프: 일부 최적화는 분석 쿼리 성능을 저하시킬 수 있습니다. 예를 들어, Blob으로 저장하면 필드별 프루닝이 불가능해집니다.
결론 및 다음 단계
RAP의 가장 큰 장점은 기존 데이터 레이크의 Parquet 파일을 그대로 사용한다는 점입니다. 별도의 복사본이나 ETL이 필요 없습니다. 이는 온라인 서비스에 사용할 수 있는 데이터의 범위를 극적으로 확장합니다. 이제 AI 에이전트는 수개월, 수년 전의 데이터를 포함하여 전체 데이터 레이크를 컨텍스트로 사용할 수 있습니다.
RAP를 실제로 적용해보고 싶다면, 먼저 데이터 레이크의 포인트 쿼리 패턴을 분석하고 어떤 최적화 기법이 가장 큰 효과를 줄지 판단해보세요. 데이터 레이크 인덱싱 원문을 참고하면 더 자세한 기술적 내용을 확인할 수 있습니다. 또한, SageMaker AI 데이터 유출 방지, 3계층 아키텍처로 80% 비용 절감한 실전 가이드와 같은 자료를 통해 데이터 레이크 보안과 비용 최적화에 대한 인사이트도 얻을 수 있습니다.
함께 보면 좋은 글
