TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 21 READS

하스켈로 Parquet 파일 쓰기: DataHaskell의 새 라이터가 던지는 실무적 함의

데이터 과학 생태계에서 Parquet은 사실상의 표준 저장 포맷으로 자리 잡았지만, 정작 하스켈(Haskell) 진영에서는 오랫동안 CSV, JSON, 혹은 ByteString 덤프 정도가 데이터 직렬화의 주된 선택지였다. DataHaskell/Dataframe 프로젝트가 최근 Parquet 라이터를 구현하면서 이 공백을 메우기 시작했다. 사용법 자체는 단순하다. 데이터프레임을 writeParquet 함수에 넘기면 행 그룹(row group)과 페이지(page) 크기에 대한 무난한 기본값으로 파일이 생성되고, 더 세밀한 제어가 필요하면 writeParquetWithOptions를 쓰면 된다. 겉보기의 단순함 뒤에는 포맷 구조와 메모리 관리에 대한 여러 설계 판단이 숨어 있다.

왜 CSV·JSON이 아니라 Parquet인가

CSV와 JSON은 나름의 �임새가 있지만 데이터 양이 커질수록 약점이 뚜렷해진다. 읽고 쓰고 질의하는 속도가 느리고, 각자 만든 자체 포맷은 표준 데이터 과학 도구와의 상호운용성이 떨어진다. Parquet은 단순함을 포기하는 대신 높은 압축률과 효율적인 질의를 얻는다. 핵심은 질의와 무관한 데이터를 최대한 읽지 않도록 만드는 것이다. 장기 보관, 네트워크 전송, 다른 프로그램과의 연동 어느 쪽이든 Parquet의 보편성 덕분에 하스켈 데이터프레임 라이브러리가 이 포맷을 다룰 수 있다는 것은 실질적인 무기가 된다.

Parquet 파일은 여러 개의 행 그룹이 이어지고 파일 끝에 메타데이터가 붙는 구조다. 메타데이터에는 각 컬럼 청크와 페이지의 위치, 통계, 블룸 필터 같은 정보가 담겨 있어, 리더나 질의 계획기가 특정 행 그룹을 읽을지 말지 판단할 수 있다. 각 행 그룹은 동일한 행 수를 가진 컬럼 청크들의 모음이고, 컬럼 청크는 다시 데이터 페이지들의 연속이다. 결국 파일은 컬럼별 페이지가 줄줄이 이어진 형태이며, 메타데이터의 오프셋과 크기 정보로 이를 해석한다. 데이터 페이지 안에는 페이지 메타데이터, nullability와 중첩 구조를 값싸게 인코딩한 정의 레벨(definition level)과 반복 레벨(repetition level), 그리고 실제 인코딩·압축된 데이터가 들어간다. 이번 라이터는 평면 스키마만 겨냥해 정의 레벨을 1까지, 반복 레벨은 0까지만 지원한다.

크기 목표와 행 수 제약이라는 딜레마

설계에서 가장 흥미로운 지점은 pageSize와 rowGroupSize다. 둘 다 바이트 단위의 목표 크기지만, 한 행 그룹 안의 모든 컬럼 청크는 같은 행 수를 담아야 한다. 문제는 데이터 특성과 인코딩, 압축 알고리즘에 따라 컬럼마다 목표 크기에 도달하는 행 수가 제각각이라는 점이다. 개발팀은 이 목표들을 '최선의 노력' 수준으로 간주하고, batchRows 단위 배치로 컬럼을 처리한 뒤 매 배치마다 행 그룹 상태를 점검하는 방식을 택했다. 그 결과 각 행 그룹은 batchRows의 정수 배, 각 페이지는 subBatchRows의 정수 배 행을 담게 된다(마지막 페이지와 마지막 컬럼 청크는 예외). 페이지 단위 서브배칭은 매 쓰기 후의 IORef 부기(book-keeping)를 줄여 상당한 속도 향상을 준다. 다만 개별 항목이 유난히 큰 컬럼이 있으면 목표 크기를 크게 초과할 위험이 있어, 실제 사용자가 이 문제를 겪으면 완화 장치가 필요할 수 있다고 밝히고 있다.

버퍼 크기를 미리 알기 어렵다는 점 때문에 메모리 관리에도 손이 많이 갔다. 라이터는 FFI에 의존하지 않고 원시 메모리를 다루기 위해 MutableByteArray를 사용하고, 페이지 버퍼를 컬럼 청크 버퍼로 혹은 행 그룹을 파일로 흘려보낼 때 Ptr Word8로 변환하려고 pinned ByteArray를 채택했다. pinned 배열은 성장시킬 때 주의가 필요해, Data.Primitive의 grow 대신 새 pinned 배열을 할당하고 기존 것은 GC에 맡긴다. 4KB GHC 블록 안의 pinned 객체 하나가 블록 전체를 살려두는 힙 단편화 우려가 있지만, 버퍼가 대개 4KB보다 훨씬 크고 초기 몇 페이지·첫 행 그룹 이후에는 성장이 드물 것이라는 판단이 깔려 있다.

디스크 쓰기의 함정과 남은 과제

구현 과정에서 저자가 파고든 디스크 쓰기 문제도 실무자에게 참고가 된다. 기본적으로 파일 쓰기는 OS 페이지 캐시에 기록되고, 더티 페이지 비율이 일정 값을 넘으면 커널이 라이트백을 따라잡을 때까지 프로세스를 조절(throttle)한다. 행 그룹을 동기적으로 쓰는 이 라이터에게는 곧 대기 시간이 된다. 그래서 더 적은 시스템 콜로 더 많은 데이터를 쓰도록 충분히 큰 청크가 유리하다는 결론에 이르렀고, dd로 테스트한 끝에 256KiB를 기본 청크 크기로 정했다. 커널을 거치지 않는 O_DIRECT나 멀티스레딩, 충돌 시 데이터 손실 방지는 더 깊은 토끼굴로 남겨두었다.

현재 라이터는 압축과 인코딩의 일부만, 그리고 완전히 단일 스레드로만 동작한다. 컬럼·페이지 통계나 블룸 필터 같은 선택적 메타데이터는 아직 기록하지 않는데, 이는 리더가 질의를 최적화하는 데 유용한 요소들이라 향후 과제로 분류됐다. 또한 모든 버퍼가 메모리에 상주하고 행 그룹 하나는 통째로 메모리에 올라가야 하므로, 특별히 큰 페이지·행 그룹을 다루는 메모리 제약 환경에서는 고갈 위험이 있다. 이를 위해 작은 인메모리 버퍼와 디스크상의 임시 파일을 쓰는 2패스 전략이 계획돼 있다. 정리하면 이번 결과물은 상호운용성 있는 Parquet 쓰기를 하스켈에서 가능하게 만든 실용적 출발점이되, 압축·인코딩 폭, 통계·블룸 필터, 병렬성, 메모리 절약 같은 최적화 기능은 아직 채워야 할 여백으로 남아 있다는 점을 도입 단계에서 분명히 인식하고 접근하는 것이 안전하다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://www.datahaskell.org/blog/2026/09/18/writing-parquet-...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...