데이터 파이프라인 · 파티션과 파일 크기 · 이론
파티션 키가 뒤에 오는 모든 조회를 정한다
한 줄 요약
파티션 키를 무엇으로 잡느냐가 앞으로의 모든 조회 비용을 정하고, 잘게 쪼갤수록 작은 파일이 쌓이며, 잘못 잡았을 때 되돌리는 값은 전체를 다시 쓰는 비용이다.
왜 이게 필요했나
같은 행을 넣었는데 어떤 조회는 0.2초에 끝나고 어떤 조회는 40초가 걸린다. 쿼리는 똑같고 자료도 똑같다. 다른 것은 파일이 디스크에 어떻게 놓여 있는가 하나다.
파티션은 값마다 디렉터리를 나눠 두는 것이다. day=2026-01-03/ 아래에 그날 자료만 있으면, 그날만 보는 조회는 나머지 디렉터리를 아예 열지 않는다. 열지 않은 파일은 비용이 0 이다. 이것이 파티션이 주는 거의 유일한, 그리고 아주 큰 이득이다.
문제는 그 이득이 키에 걸린 조건에만 생긴다는 것이다. 디렉터리 이름에 없는 칸으로 거르면 잘라 낼 것이 없어 전부 열어 봐야 한다. 그래서 키를 고르는 일은 자료의 성질이 아니라 앞으로 들어올 조회의 모양을 고르는 일이다.
어떻게 동작하나
"그러면 자주 쓰는 칸을 다 키로 넣으면 되겠네" 가 첫 번째 함정이다. 날짜와 지역과 유입 경로를 모두 키로 잡으면 파티션 수는 세 값의 곱이 된다. 하루 300건짜리 자료가 갑자기 160개 디렉터리로 흩어지고, 파일 하나가 두세 줄짜리가 된다.
이것이 작은 파일 문제다. 손해는 세 군데에서 난다. 파일 하나를 여는 데 드는 고정 비용이 내용을 읽는 비용보다 커지고, 목록을 관리하는 쪽(매니페스트든 메타스토어든)의 항목 수가 폭발하고, 압축과 인코딩이 제대로 먹지 않는다.
파일 하나가 읽기 단위와 어떻게 맞물리는가를 보면 왜 작은 파일이 손해인지 또렷해진다. Parquet 의 [개념 문서](https://parquet.apache.org/docs/concepts/)는 행 그룹(row group) 을 "자료를 행 방향으로 논리적으로 나눈 것" 으로 정의하고, 열 덩이(column chunk) 를 "특정 행 그룹 안에 있고 파일 안에서 연속되어 있음이 보장되는 한 칼럼의 자료" 로 정의한다. 연속되어 있다는 말이 핵심이다 — 한 번의 큰 순차 읽기로 가져올 수 있다는 뜻이다. 페이지(page) 는 그 아래 단위이고 "압축과 인코딩의 관점에서 더 쪼갤 수 없는 단위" 다.
[Parquet 설정 문서](https://parquet.apache.org/docs/file-format/configurations/)는 행 그룹을 512MB 에서 1GB 로 크게 잡기를 권한다. 큰 순차 입출력과 큰 열 덩이를 얻기 위해서이고, 행 그룹 하나가 블록 하나에 들어가도록 맞추는 것을 이상적인 배치로 든다. 페이지는 8KB 를 권하는데, 작을수록 한 행만 찾아 읽는 일이 쉬워지기 때문이다.
여기서 결론이 나온다. 파일이 행 그룹보다 훨씬 작으면 그 구조가 아무 일도 하지 못한다. 2KB 짜리 파일에 512MB 짜리 행 그룹을 담을 수는 없다. 읽는 쪽은 파일마다 꼬리의 메타데이터를 읽고 열기를 반복할 뿐이다. 그래서 파일 크기는 취향이 아니라 읽기 단위와의 맞물림이다.
이 실습은 Parquet 를 쓰지 않는다. 실습 이미지에 pyarrow 가 없고 파드는 런타임 설치를 할 수 없다. 그래서 파티션 디렉터리와 매니페스트와 JSON Lines 로 같은 구조를 손으로 만든다. 행 그룹이라는 이름만 없을 뿐, 파일 하나가 읽기 단위와 맞물린다는 이야기는 그대로다.
묶기와 그 도중의 독자
작은 파일이 쌓이면 묶기(compaction) 를 돌린다. 한 파티션 안의 작은 파일들을 목표 크기에 가깝게 이어 붙여 큰 파일로 바꾸는 일이다. 어렵지 않다. 어려운 것은 묶는 도중에 읽는 사람이 무엇을 보는가다.
새 파일을 쓰는 동안 옛 파일도 그대로 있다. 이때 디렉터리를 훑어 파일을 모으는 독자는 옛 파일과 새 파일을 모두 집어 같은 줄을 두 번 센다. 합계가 정확히 두 배가 되는 사고가 여기서 난다.
막는 방법은 하나다. 읽는 쪽이 디렉터리가 아니라 목록(매니페스트)을 본다. 묶기는 새 파일을 다 쓴 다음 목록을 한 번에 바꿔 달고, 그다음에야 옛 파일을 지운다. 목록 교체가 원자적이면 독자는 옛 목록 전체 아니면 새 목록 전체를 본다. 그 사이는 없다.
새 파일 이름에도 규칙이 있다. 옛 이름과 겹치면 안 된다. 겹치면 읽고 있는 파일을 덮어쓰게 되어 그 파티션이 통째로 빈다.
현장에서 만나는 모습
첫째, 키를 바꾸는 값은 전체를 다시 쓰는 것이다. 파티션 키는 디렉터리 구조 자체이므로 바꾸려면 모든 행을 읽어 모든 파일을 새로 써야 한다. 그래서 키를 고르는 회의는 한 번 더 할 가치가 있다.
둘째, 시간 키는 거의 언제나 들어간다. 조회의 대부분이 기간으로 들어오고, 오래된 자료를 통째로 지우는 일도 디렉터리 단위가 되기 때문이다.
셋째, 값이 많은 칸은 키로 쓰지 않는다. 사용자 아이디나 주문 번호를 키로 잡으면 파티션이 행 수만큼 생긴다. 이 실수는 자료가 적을 때는 아무 증상이 없다가 몇 달 뒤에 드러난다.
넷째, 평균 파일 크기는 거짓말을 한다. 큰 파일 하나와 작은 파일 수백 개가 섞이면 평균은 멀쩡해 보인다. 중앙값과 임곗값 미만 파일의 개수를 함께 봐야 실제 모양이 보인다.
실무에서 진짜 중요한 것
- 키는 조회의 모양에서 고른다. 자료의 칸 목록에서 고르지 않는다.
- 키를 더할 때마다 파티션 수는 곱해진다. 세 개면 곱이 세 번이다.
- 평균이 아니라 중앙값과 작은 파일 개수를 본다.
- 읽는 쪽은 목록을 본다. 디렉터리를 훑는 독자는 묶기 도중에 두 배를 본다.
- 묶을 때 새 이름은 옛 이름과 겹치지 않게 한다.
다음 실습에서 할 것
주문 원본을 만들고 도구 pq.py 를 한 단계씩 키운다. 날짜 하나로 쪼갠 레이크와 날짜·지역·경로로 잘게 쪼갠 레이크를 각각 만들어 파일 수와 크기 분포를 견주고, 파티션 키에 걸린 조건과 걸리지 않은 조건이 여는 파일 수가 어떻게 달라지는지 잰다. 그다음 묶기를 돌리고, 묶는 도중에 죽여 매니페스트로 읽는 쪽과 디렉터리를 훑는 쪽이 각각 무엇을 보는지 확인한다. 마지막으로 키를 바꿔 다시 쓰고 그 비용을 적는다. 채점기는 매번 다른 원본과 목표 크기로 여러분의 도구를 실제로 돌려 답을 대조한다.