AI 시대의 하드웨어 변화와 새로운 액세스 패턴, 경량 인코딩 연구를 배경으로 Parquet 포맷이 직면한 한계와 발전 방향을 살펴보고, 합의 형성 장치로서의 Parquet 커뮤니티 역할과 기술적·실용적 진화 경로를 논의한다.
지난 몇 년 동안, 컬럼 형식의 새로운 포맷들이 캄브리아기 폭발이라고 부를 만큼 쏟아져 나오며 Parquet의 패권에 도전하고 있다. Lance, Fastlanes, Nimble, Vortex, AnyBlox, F3 (File Format for the Future) 같은 것들이다. 배경에 있는 생각은 이렇다. 맥락이 너무 많이 변했기 때문에, 예전(지난 10년)의 설계로는 앞으로를 버텨낼 수 없다는 것이다.
이게 나에겐 꽤 흥미로웠다. 특히 Parquet의 가장 큰 공헌이 “컬럼 저장소의 표준”을 제공했다는 점을 생각하면 더 그렇다. Parquet은 단순한 파일 포맷이 아니다. ASF가 호스팅하는 오픈 소스 프로젝트로서, 업계의 합의를 만들어내는 장치 역할을 한다. 새로운 포맷을 여섯 개나 만드는 건 상호 운용성(interoperability) 측면에서 도움이 안 된다. 그래서 실제로 무엇이 어떻게 변했는지, 그리고 이 새로운 시대의 요구를 충족하려면 Parquet이 어떻게 적응해야 하는지 조금 더 잘 이해하고 싶어 시간을 들여 살펴보았다. 이 글에서는 그 과정에서 정리한 내용을 공유한다.
좋든 싫든, 우리는 AI가 지배하는 시대에 살고 있다. 앞으로 데이터의 주요 소비 주체가 사람이 아니라 AI가 될 거라는 주장도 있다. 하지만 우리의 데이터 인프라는 상당 부분 전혀 다른 시대를 상정하고 설계되었다.
나는 ASF에서 Parquet PMC의 의장을 맡고 있다. 이 생태계의 일원으로서 내 시각은 당연히 편향되어 있다. 하지만 그렇기 때문에, 우리가 잘한 점과 포맷이 개선되어야 할 지점을 함께 이야기할 수 있는 괜찮은 관점을 가지고 있다고도 생각한다.
Parquet은 2012년에 시작되어, 사실상(de facto) 컬럼 저장소의 표준이 되었다. 커뮤니티를 만들고 표준으로 자리 잡도록 하기 위해 많은 노력을 기울였지만, 사이드 프로젝트로 시작했던 것이 이렇게까지 큰 영향을 미쳤다는 건 지금도 놀랍다.
당시에 우리가 이 프로젝트에 더 투자하기로 마음먹게 한 주된 이유는 단순했다. Parquet라는 이름도 붙기 전의 첫 번째 프로토타입이, 최적화도 거의 안 했는데 저장 공간을 30%나 절약해 줬기 때문이다. 이건 컬럼 표현(columnar representation)의 여러 이점 중 하나일 뿐이다.
우리는 Hadoop 시대를 막 지나고 있었고, 포맷은 Google의 Dremel 논문에서 영감을 받았다. 그 시절에는 Google 논문을 구현하고 있다고 하면 진짜 물건으로 취급받았다.
Parquet은 데이터베이스 연구(C-Store, MonetDB/X100)에 기반해 등장하던 벡터화 쿼리 엔진의 새 세대 사이에서 상호 운용성을 가능하게 했다.
기본적인 트레이드오프는 세 갈래였다.
압축을 더 많이 하면 저장 비용이 줄고 네트워크 전송 시간도 줄어든다. 하지만 압축 해제를 위해 더 많은 CPU 시간이 들 수 있다. 따라서 당시 CPU 특성을 고려했을 때, 어떤 인코딩과 압축 스킴이 가장 달콤한 지점인가가 핵심 질문이 된다.
전송 비용이 가장 싼 데이터는, 여전히 아예 건너뛰는 데이터다. 바로 여기에서 컬럼 레이아웃이 빛을 발한다.
이렇게 하면 다운로드해야 할 데이터 양을 크게 줄일 수 있다. 우리가 OLAP를 작동시키는 방식이 바로 이렇다. 전부 스캔하지 않아도 되도록 적절한 정렬 순서와 파티셔닝 스킴을 고르는 것이다.

Parquet 파일 구조는 맨 아래에 있는 푸터 메타데이터가 row group을 가리키고, 각 row group은 컬럼 청크(column chunk)를, 컬럼 청크는 다시 페이지(page)들로 나뉘는 형태다. 페이지는 디코딩과 압축 해제의 단위다. Hadoop 시절에는 HDFS 블록 크기에 맞춰 row group 크기를 정했다. 요즘 같은 오브젝트 스토리지 환경에서는, 파일당 하나의 큰 row group을 두는 것이 일반적이다.
페이지 구조 안에는 값들이 있고, 이 값들은 (예를 들어 딕셔너리로) 인코딩된 뒤, 선택적으로 ZStandard 같은 것으로 압축된다. 압축은 선택 사항이다. 인코딩만으로도 충분할 때가 많기 때문이다. 압축률이 잘 나오면, 범용 압축 알고리즘을 추가로 거치는 것보다 그냥 인코딩만 디코딩하는 편이 더 빠르다. 가능하다면, 범용 압축보다 특화된 경량 인코딩이 속도 면에서 훨씬 유리하다.
과거에는 네트워크가 CPU보다 상대적으로 훨씬 느렸기 때문에, CPU를 더 쓰더라도 압축을 더 많이 하는 쪽이 타당했다. 하지만 스토리지-컴퓨트 분리와 오브젝트 스토리지의 부상으로 네트워크·IO 대 CPU 비율이 변했다.

2010년 전후와 오늘을 비교해, 고급 서버를 기준으로 보면, 코어 수는 평균 16배, SIMD 폭은 4배 정도 늘어났다. 활용할 수 있는 병렬 처리 능력이 엄청나게 증가한 것이다.
GPU도 마찬가지다. 오늘날 GPU는 15년 전보다 스레드(에 해당하는 단위)가 약 40배 많다. GPU는 전통적인 스레드라기보다는 SIMD에 더 가까운데, 같은 명령을 여러 데이터에 동시에 적용하는 방식으로 동작한다는 점을 떠올리면 된다.
이게 첫 번째 변화다. 처리에 사용할 수 있는 병렬성이 엄청나게 늘어났다는 것.
두 번째 변화는 데이터에 접근하는 방식이다.
벡터 스토어에서 임베딩을 질의할 때는 문서를 빠르게 찾아야 한다. 예를 들어 챗봇에서 RAG를 구현한다고 해 보자. 벡터 스토어 질의는 답변을 생성하기 전에 거쳐야 하는 긴 파이프라인의 한 단계일 뿐이다. 이 맥락에서는 밀리초 단위의 지연도 다 합쳐진다. 레이턴시를 가능한 한 줄여야 한다.
이 벡터 스토어들 역시 컬럼 저장소를 사용한다. 하지만 우리가 처음에 최적화했던 순차 스캔뿐만 아니라, 매우 빠른 랜덤 액세스도 필요하다. 거기에 더해, “AI의 속도”로 데이터를 처리하려면, 스캔 시 더 높은 처리량이 필요하고, 가능하다면 AI가 있는 곳(GPU)에서 직접 처리해야 할 수도 있다.
이 요구사항에 맞추기 위해 Parquet에서 개선할 수 있는 부분이 여럿 있다.
이런 변화는 사람들로 하여금 데이터 인코딩에 대한 새로운 접근법을 탐구하게 만들었다. 새로운 연구 논문들이 등장했고, 현대 하드웨어의 병렬성을 훨씬 더 잘 활용하는 **경량 인코딩(특정 타입에 특화된, 잘 압축되면서도 매우 빠른 인코딩)**이 제안되었다. 예: BtrBlocks, ALP(부동소수점), FastLanes(정수), FSST(문자열).
최근에는 현 상태를 흔드는 새로운 파일 포맷들도 쏟아져 나왔다.
이제 우리에게 주어진 문제는, 이들 중 어떤 것들이 생태계에서 광범위한 채택에 성공할 것인가를 가늠하는 일이다. 내가 서 있는 위치에서 보면, 부족한 부분을 Parquet에 기여하고 그 위에 시스템을 쌓는 편이 더 쉬워 보인다.
이 포맷들은 해당 기법들이 실제로 잘 작동한다는 걸 보여준다. 새로운 접근법을 적용하면 훨씬 더 좋은 성능을 얻을 수 있다. 커뮤니티 차원에서는, 이 신호를 잘 읽고 우리가 가진 것을 진화시켜야 한다.
이 새로운 포맷들의 설계를 들여다보면, 근본적으로는 똑같은 컬럼 레이아웃을 사용한다.
물론 차이는 있다. 메타데이터 레이아웃에 더 신경을 쓰기도 하고, 자신들의 유스케이스에 더 잘 맞는 인코딩을 선택하기도 한다. 하지만 전체적인 캡슐화(encapsulation) 포맷은 크게 다르지 않다. 여전히 PAX를 따르고, row group과 페이지 같은 다단계 구조를 가지며(이름만 다를 뿐), 다양한 레벨에서 스킵을 가능하게 하는 메타데이터를 두는 식이다.
일부 포맷이 제공하는 테이블 변경, 버저닝 같은 기능(Lance는 테이블 포맷이기도 하다)은 Parquet보다는 Iceberg에 영향을 주고 있다.
그렇다면 Parquet은 무엇일까?
Parquet을 파일 포맷이라고 생각하기 쉽다. 하지만 Parquet은 무엇보다 먼저 오픈 소스 프로젝트다. 합의를 만들어 내는 장치다. 수많은 오픈 소스 프로젝트와 벤더들을 끌어당기는 구심점이다.
흔히 인용되는 속담이 있다. “빨리 가고 싶으면 혼자 가고, 멀리 가고 싶으면 함께 가라.”
Parquet은 커뮤니티를 끌고 가야 하기 때문에 느리게 움직인다. 이게 어려운 부분이다. 커뮤니티를 만들고 합의를 이뤄내는 일은 시간이 오래 걸린다. 하지만 그 대가로, 생태계 전체의 넓은 채택을 얻는다.
연구 논문들이 특정 Parquet 구현을 **기준선(baseline)**으로 삼아 자신들의 결과를 평가할 때면, 나는 약간 메타적인 느낌을 받는다. Parquet의 인코딩들 역시 예전 연구 논문들을 기반으로 하고 있다. Parquet은 움직이는 타깃이다. 지금까지 우리가 연구에서 흡수해 온 것들의 집합일 뿐이다. 당신의 접근법이 훨씬 낫다면, 언젠가는 프로젝트 안으로 들어와 새로운 기준선이 될 가능성이 크다.
우리는 거인의 어깨 위에 서 있다. 인코딩을 연구하는 분들이 다음에 우리가 어디로 가야 할지 길을 보여 주는 어려운 일을 한다. 우리는 그 연구들이 산업계에서 채택되도록 합의를 만들어내는 역할을 맡는다. 모든 것을 다 받아들일 수는 없고, 복잡성을 최소화하기 위해 균형을 잡아야 한다는 점이 우리의 트레이드오프다.
보다 큰 변화가 Parquet 안으로 들어오는 방식을 보여 주는 예로, 약 1년 전쯤의 일을 들어보자. 당시 엔지니어들이 Spark 안에 있던 Variant 타입의 중립적인 집을 찾자는 제안을 처음 했다. Variant는 JSON의 바이너리 표현과 비슷하다. 필드 이름을 한 컬럼에, 값을 다른 컬럼에 분리해서 저장한다. 필요하다면 일부 필드만 따로 컬럼으로 “찢어서(shred)” 둘 수도 있다. 필드 개수를 알 수 없거나, 매우 많은 희소(sparse) 필드가 있는 데이터에 유용하다.
당시 큰 질문은 이것이었다. 이 새로운 타입을 Spark, Arrow, Iceberg, Parquet 중 어디에 정의해야 가장 말이 되는가였다. 관련 논의를 보면 알겠지만, 이들(그리고 그 외 여러 프로젝트들) 모두가 이 타입을 쓰게 될 것이 자명했다.
우리는 이를 Parquet에 두기로 합의했다. 그리고 나서 커뮤니티로서 스펙에 대한 합의를 마무리했다. 모두가 같은 페이지에 있는지 확인해야 했다. 몇 가지를 바꾸고, 하나하나 동의를 확인한 뒤, 생태계 전반에 구현을 진행했다. (Gang, Aihua, Gene, Micah, Andrew, Ryan B, Ryan J, Yufei, Jiaying, Martin, Aditya, Matt, Antoine, Daniel, Russell 등 많은 분들께 감사한다.)
커뮤니티는 여러 시스템(오픈 소스든 아니든)에서 다중 구현을 만들어 내고, 상호 호환성 테스트를 함께 진행해, 호환 가능한 시스템을 만들고 있는지 확인했다. Databricks, Snowflake, Google, Tabular, Datadog, CMU, InfluxData, Dremio, Columnar 등(혹시 빠뜨린 분이 있다면 알려 주시면 여기 추가하겠다)에서 온 개인들이 참여했다.
이제 우리는 한 시스템에서 Variant를 쓰면, 다른 시스템에서 정상적으로 읽힌다는 것을 안다. Databricks에서 Snowflake, BigQuery로, DataFusion에서 DuckDB, Spark로. (그리고 Dremio, InfluxDB 등등) 놀랄 일이 없다.
이것이 합의 형성 장치가 작동하는 방식이다.
넓은 범위의 OLAP 스캔을 최적화하는 일과 랜덤 액세스를 최적화하는 일이 서로 상충하는 것처럼 느껴질 수 있다. 하지만 여기에는 좋은 소식이 있다. 더 많은 병렬화를 가능하게 하고, 더 빠른 디코딩을 가능하게 하는 인코딩의 특성이, 동시에 더 빠른 랜덤 액세스를 가능하게 하는 특성이기도 하다는 점이다.
요령은 단 하나다. 모든 데이터 의존성을 제거하는 것.

다음 값을 디코딩할 때, 이전 값을 디코딩한 결과에 의존하지 않도록 해야 한다. 그렇지 않으면, 병렬로 디코딩할 수 없고, n번째 값을 읽기 위해 앞의 모든 값을 디코딩해야만 한다.
Parquet의 몇몇 인코딩은 데이터 의존성이 매우 강하다. 예를 들어, 연속된 값들 사이의 변화량이 작을 때 흔히 사용하는 기법이 **델타 인코딩(delta encoding)**이다. 연속된 값들 간의 차이를 계산하면 작은 정수들이 나오고, 이를 작은 비트 폭으로 패킹해 저장할 수 있다. 하지만 이렇게 저장한 값을 디코딩하려면, 이 차이값들을 순서대로 누적 합해야 한다. 이건 제대로 병렬화할 수 없고, 어떤 값을 읽으려면 그 앞의 모든 값을 디코딩해야 한다.
게다가 페이지는 기본적으로 범용 압축 알고리즘(zstd 같은)으로 압축된다. 이 또한 페이지 안에서 읽고자 하는 데이터 앞에 있는 데이터를 모두 압축 해제해야 한다.
이런 점들은 병렬성을 제한함으로써 디코딩 처리량을 제한하고, 랜덤 읽기 시 읽고자 하는 데이터에 도달하기 전에 디코딩해야 하는 데이터 양을 늘려 레이턴시를 키운다. 또한, 인코딩된 형태 위에서 직접 표현식을 계산하는 것도 불가능하게 만든다.
푸터 메타데이터는 Thrift로 표현된다. Protobuf와 마찬가지로, 부분만 디코딩하려 해도 전체 데이터 구조를 훑어야 하므로, 스키마가 매우 넓을 때 필요한 컬럼의 메타데이터로 곧장 건너뛰기 어렵다. FlatBuffer는 당시 존재하지 않았지만, 지금은 이를 해결하기 위한 제안이 진행 중이다(Alkis에게 감사한다). 이 제안은 Thrift 대신 FlatBuffer를 사용하는 새 Parquet 푸터를 추가하는 것으로, 뒤로 호환 가능하며, 특히 폭넓은 스키마에서 선택적 메타데이터 디코딩을 훨씬 쉽게 해준다.
제안되고 있는 새로운 인코딩들은 모두 데이터 의존성을 제거하고 SIMD를 활용하는 데 초점을 맞춘다.
**ALP (Adaptive Lossless Floating Point)**는 부동소수점 값을 효율적으로 정수로 바꿔 저장한다. 우리가 저장하는 부동소수점 값들 중 상당수는 사실상 **소수(decimal)**이며, 이를 정수로 인코딩할 좋은 방법들이 있다. 문제는 소수 부동소수점 숫자가 2진수로는 정확히 표현되지 않는 경우가 많다는 점이다. 이 논문은 정밀도를 잃지 않으면서 SIMD를 활용해 이를 매우 빠르게 처리하는 방법을 제시한다.
FastLanes는 정수 인코딩을 위해, SIMD(single instruction, multiple data)와 분기 예측(branch prediction)을 최적화한다. 현대 프로세서는 전 명령이 끝나기도 전에 다음 명령을 미리 실행한다. 분기 예측이 빗나갈 때마다 사이클을 잃는다. 핵심 기법은 디코딩 로직 안 어디에도 분기가 없도록 만드는 것이다.
FSST는 문자열 부분집합에 대한 딕셔너리 인코딩이다. 자주 등장하는 부분 문자열들의 효율적인 딕셔너리를 만들어 압축한다. 장점은 두 가지다.
BtrBlocks는 **경량 인코딩을 중첩(layering)**하는 방식을 이야기한다. (Zstandard 같은 범용 압축이 아니라, 타입별로 특화된 매우 빠른 인코딩들을 여러 층으로 겹치는 것)
먼저, 오늘 당장 쓸 수 있으면서도 새로운 유스케이스에 도움이 될 만한 실용적인 튜닝 포인트들이 있다.
row group이 많으면 오브젝트 스토리지 요청이 많아진다:
값 하나를 읽기 위해 디코딩해야 할 값이 너무 많다:
블록 압축 때문에 페이지 전체를 압축 해제해야 한다:
어떤 인코딩은 랜덤 액세스를 지원하지 않는다:
컬럼이 백만 개면 메타데이터가 거대해지는 건 맞다. 하지만 정말로 백만 개 컬럼을 단일 파일에 다 넣어야 할까?
모든 컬럼이 밀집(dense, 대부분 값이 정의되고 null이 거의 없음)되어 있다면, 어떤 방식의 컬럼 레이아웃을 쓰더라도 아주 폭넓은 스키마를 효율적으로 저장하는 건 원천적으로 어렵다. 주어진 파티션 크기 안에서 너무 많은 작은 컬럼들로 쪼개게 되고, 그만큼 비효율이 생긴다.
현실적으로는, 이 넓은 스키마의 일부 컬럼만 밀집되어 있고 나머지는 희소한 경우가 많다. 그런 상황이라면, 희소 컬럼들을 Variant 타입으로 합쳐 저장하는 방식을 고려해야 한다. 이렇게 하면 컬럼 레이아웃에 잘 맞지 않는 희소 데이터를 하나의 컬럼에 모아 저장할 수 있고, 그 밖의 밀집 데이터는 기존 컬럼 레이아웃을 유지하는 하이브리드 구조를 만들 수 있다. 너무 많은 작은 컬럼들 때문에 성능이 떨어지는 것을 피할 수 있다.
또 메타데이터는 꼭 파일에서 직접 읽을 필요도 없다. Parquet 파일 위에 쿼리 엔진을 구축하고 있다면, 플래닝 단계에서 더 빠르게 질의할 수 있는 다양한 메타데이터 저장 방식을 쓸 수 있다. 예를 들어 외부 Parquet 인덱스 같은 것들이다.
내 생각에, 유일한 본질적 과제는 이 새로운 인코딩들을 Parquet에 통합하는 일이다. 당연히, 데이터를 인코딩하는 올바른 방식을 서로 합의해야 한다.
그 밖의 것들—인덱스, 테이블 추상화, 메타데이터에 대한 인덱스 기반 접근—는 그 위에 쌓을 수 있다.
다른 개선들은 Iceberg와 Arrow에 들어갈 것이다. 예를 들어 일부 컬럼을 다른 파일에 저장한다든가, 늦은 머티리얼라이제이션(late materialization)을 허용한다든가 하는 것들이다.
기존 프로젝트에 모든 것을 기여해야 하느냐, 아니면 완전히 새로운 스택을 만들어야 하느냐 같은 이분법은 존재하지 않는다. 우리는 모듈식으로 합성 가능한 시스템을 만들 수 있다. 이게 바로 상호 운용성을 가능하게 하는 표준 포맷에 협력하는 이유다.
이 방향으로 나아가면서, 우리는 OLAP 세계와 벡터 데이터베이스의 요구 사이의 격차를 점점 줄여 나갈 것이다. 그 과정에서, 그렇지 않았다면 생태계와의 호환성을 잃고 하나의 사일로로 고립되었을 시스템들 사이의 벽을 허물게 될 것이다.
이미 앞으로의 길은 어느 정도 보이고 있다. OLAP과 AI 워크로드를 동시에 지원하는 새로운 인코딩, 메타데이터 개선, 실용적인 튜닝, 그리고 진화를 준비한 커뮤니티 말이다.
Parquet은 계속해서 연구에서 나온 최신 성과를 받아들일 것이다. 이미 ALP와 FSST를 병렬성과 랜덤 액세스를 지원하는 더 나은 인코딩으로 통합하고, 푸터를 Thrift에서 FlatBuffer로 옮겨 메타데이터 오버헤드를 줄이고 메타데이터에 대한 랜덤 액세스를 쉽게 만드는 작업이 진행 중이다. 커뮤니티 안에서 이 변화가 어떻게 이뤄져야 할지에 대한 공동의 합의가 쌓이는 만큼, 이런 노력들은 계속해서 진전될 것이다.
Parquet에 기여하는 일이 너무 어렵게 느껴진다면, 한 번 시도해 보라고 권하고 싶다. 최악의 경우, “생각보다 훨씬 나았다”는 사실에 pleasantly surprised될 것이다. 광범위한 채택을 이루는 데에는 수년간의 커뮤니티 빌딩이 필요하다. Parquet, Arrow, Iceberg에는 이미 번성하는 커뮤니티가 있다. 합의를 만드는 일은 시간이 걸리지만, 그만한 가치가 있다. 생태계 전체에 큰 추진력을 만들어 내기 때문이다.
이것이 바로 근본적으로 합의를 만드는 것에 관한 오픈 소스 프로젝트의 힘이다.
그리고 이것이 우리가 서 있는 지점이다. AI 시대에 맞게 컬럼 저장소를, 한 번에 하나의 인코딩씩, 적응시켜 가는 중이다.
Parquet에 기여하는 데 관심이 있거나, 이 글에서 언급한 제안들에 대해 논의하고 싶다면, 메일링 리스트와 격주 Parquet sync에 참여해 달라. 이 모든 것은 커뮤니티 덕분에 가능하다.