데이터 레이크의 핵심은 "원본은 손대지 않고, 단계별로 정제된 사본을 쌓아 간다"는 것이다. 그 바닥에 S3 가 놓이는 이유는 세 가지다.
용량과 내구성: 사실상 무제한 용량, 99.999999999%(11 9's)로 설계된 내구성.
컴퓨팅과 스토리지의 분리: 데이터는 S3 에 한 벌만 두고 Athena, EMR, Redshift Spectrum, Glue 가 같은 파일을 동시에 읽는다. 엔진을 바꿔도 데이터를 옮길 필요가 없다.
schema-on-read: 먼저 저장하고 읽을 때 스키마를 입힌다. 테이블은 "이 S3 경로를 이런 컬럼으로 해석하라"는 Glue Data Catalog 의 메타데이터일 뿐이다.
💡 관련 이론: 데이터 레이크는 schema-on-read, 데이터웨어하우스는 schema-on-write 다. 레이크는 정형·반정형·비정형 데이터를 원본 그대로 저장하고 쿼리 시점에 스키마를 부여한다. 그래서 적재는 빠르고 유연하지만, 품질 검증이 적재 뒤로 밀린다. 존을 나누는 이유가 여기서 나온다.
key=value 형태를 Hive 스타일 파티셔닝이라 하고, Glue 크롤러와 Athena 의 MSCK REPAIR TABLE 이 이 형태를 파티션으로 인식한다.
-- 파티션 프루닝이 작동: year/month/day 프리픽스만 스캔SELECT region, SUM(amount)FROM ordersWHERE year = 2026 AND month = 6 AND day = 25GROUP BY region;
Athena 가 이 쿼리를 처리하는 순서는 다음과 같다.
테이블 메타데이터를 읽고, Glue Data Catalog 에 GetPartitions 를 호출해 파티션 목록을 가져온다.
WHERE 조건으로 목록을 걸러 낸다(프루닝). 남은 파티션의 S3 위치만 대상이 된다.
남은 위치마다 S3 를 나열(list) 해 파일 목록을 만든다.
파일을 읽는다. 컬럼형 포맷이면 필요한 컬럼, 통계상 걸리는 구간만 읽는다.
이 순서에서 세 가지 사실이 따라 나온다.
파티션 컬럼을 함수로 감싸면 프루닝이 약해진다. 키 값을 그대로 비교하는 조건이 가장 잘 걸러진다. 모든 파티션 키에 등호 조건이 있으면 메타데이터를 직접 집어 올 수 있어 가장 빠르다.
카탈로그에 없는 파티션은 S3 에 파일이 있어도 조회되지 않는다. "어제 데이터가 S3 에는 있는데 결과가 0행"이면 파티션 미등록이 첫 번째 용의자다.
파티션 키 타입은 STRING 이 유리하다. Athena 가 파티션 필터를 Glue 로 내려보내려면 키가 문자열이어야 하고, 파티션이 많을 때 다른 타입은 느려질 수 있다. 날짜처럼 쓰려면 쿼리에서 캐스팅한다.
-- 실행 전에 어떤 파티션을 읽을지 확인EXPLAIN SELECT COUNT(*) FROM orders WHERE dt BETWEEN '2026-06-23' AND '2026-06-25';-- 출력의 dt:string:PARTITION_KEY 줄 아래에 [[2026-06-23], [2026-06-24], [2026-06-25]] 가 나오면 프루닝 성공
가장 흔한 쿼리의 필터에 맞춘다. 대부분 날짜 범위로 조회하면 날짜가 키다. 드문 쿼리를 위해 최적화하지 않는다.
일 단위로 조회하면 시간 단위로 쪼개지 않는다. 가끔 있는 시간대 조회는 타임스탬프 컬럼 필터로 처리한다. 파일 안 데이터를 타임스탬프로 정렬해 두면 컬럼형 포맷의 최소·최대 통계로 필요 없는 구간을 건너뛰어, 시간 단위 파티션에 가까운 효과가 난다.
카디널리티가 높은 값(user_id 등)은 파티션 키로 쓰지 않는다. 파티션마다 레코드가 몇 건뿐인 작은 파일이 쏟아진다. 특정 값 조회가 많으면 버킷팅(같은 값을 같은 파일로 모음)이 맞는 도구다.
날짜는 year=/month=/day= 계층 또는 dt=2026-06-25 단일 키로 둔다. 단일 키가 범위 조건을 쓰기 쉽고 프로젝션 설정도 단순하다.
선택
얻는 것
잃는 것
파티션 없음
관리할 것이 없음
모든 쿼리가 전체 스캔
날짜(일)
날짜 조회의 스캔량 급감, 관리 쉬움
다른 축 필터는 그대로 스캔
날짜 + 리전 등 2~3단
두 축 모두 프루닝
파티션 수 = 각 키 값 수의 곱, 파일이 잘게 쪼개짐
시간 단위·고카디널리티
아주 좁은 조회만 빠름
작은 파일 폭증, 카탈로그·나열 오버헤드
작은 파일 문제(small files problem) — Athena 는 쿼리를 계획할 때 대상 파티션의 위치를 전부 나열하고, 파일마다 요청과 처리 오버헤드를 치른다. 같은 레코드라도 큰 파일 하나가 작은 파일 여러 개보다 빠르다. S3 나열은 한 번에 1,000개까지만 돌려주므로 파티션 하나에 파일이 1,000개를 넘으면 나열도 여러 번이다. 컬럼형 포맷은 파일이 작으면 포맷 자체의 오버헤드가 이점을 덮는다(Parquet 행 그룹의 기본 크기가 128MB 다).
OPTIMIZE ... REWRITE DATA USING BIN_PACK 또는 Glue 자동 컴팩션
Glue 의 groupFiles 는 읽을 때 여러 파일을 한 태스크로 묶는 옵션이다. csv·json·xml 같은 포맷에 적용되고 Parquet·ORC·Avro 입력에는 지원되지 않는다. 입력 파일이 50,000개를 넘으면 Glue 가 자동으로 켠다. 출력 파일을 키우는 것은 별개로, 쓰기 전 파티션 수를 줄여야 한다.
# Glue: 작은 JSON 파일을 약 128MB 단위로 묶어 읽기dyf = glueContext.create_dynamic_frame.from_options( "s3", {"paths": ["s3://acme-datalake-raw-prod/events/"], "recurse": True, "groupFiles": "inPartition", "groupSize": "134217728"}, format="json",)
프로젝션을 켜면 Athena 는 그 테이블의 카탈로그에 등록된 파티션 메타데이터를 무시한다.
규칙이 가리키는 위치에 데이터가 없어도 오류 없이 0행이 나온다. 범위 밖 값을 조회해도 오류가 아니라 0행이다.
빈 파티션이 많은 희소한 테이블에서는 계산된 위치를 전부 S3 에 나열하느라 오히려 느려진다. 프로젝션 대상의 절반 넘게 비어 있으면 일반 파티션이 낫다.
Athena 에서만 쓰인다. 같은 테이블을 Redshift Spectrum, EMR, Athena for Spark 로 읽으면 카탈로그의 일반 파티션 메타데이터를 쓴다. 여러 엔진이 읽는 테이블이면 파티션 등록을 병행해야 한다.
파티션 키에 조건이 없으면 범위 전체를 계산해 나열한다. 값이 매우 많은 키는 범위 대신 injected 타입(쿼리의 등호 조건에서 값을 받음)을 쓴다.
⚠️ 함정: 프루닝과 프로젝션은 다른 것이다. 프루닝은 "조건에 맞는 파티션만 읽는다"이고 파티션 테이블이면 항상 일어난다. 프로젝션은 "파티션 목록을 카탈로그에서 가져오지 않고 계산한다"다. "스캔량을 줄인다"가 정답이면 프루닝, "카탈로그 조회·파티션 등록 부담을 없앤다"가 정답이면 프로젝션이다.
버킷 이름은 전역에서 고유하고 소문자·숫자·하이픈을 쓴다. 환경과 존을 드러낸다: acme-datalake-raw-prod.
프리픽스는 domain/entity/partition 순으로 일관되게 둔다. 테이블마다 서로 겹치지 않는 경로를 준다. 한 테이블의 경로 아래에 다른 테이블 경로가 있으면 MSCK REPAIR TABLE 이 남의 파티션까지 끌어온다.
파티션 키 이름은 소문자로 쓴다. userId= 같은 낙타 표기 경로는 MSCK REPAIR TABLE 이 등록하지 못할 수 있다.
파티션 위치 아래에는 파일을 평평하게 둔다. 하위 디렉터리를 더 만들면 나열이 여러 번 일어난다.
파일명에 타임스탬프나 UUID 를 넣어 충돌을 막는다: orders_20260625_a1b2.parquet.
S3 는 프리픽스당 초당 최소 3,500건의 PUT/COPY/POST/DELETE, 5,500건의 GET/HEAD 를 처리하고 프리픽스 수에는 제한이 없다. 그래서 요청 분산용 해시 프리픽스는 더 이상 필수가 아니고, 의미 있는 계층 구조를 우선한다. 다만 이 확장은 점진적이라 요청이 급증하는 동안에는 503 Slow Down 이 날 수 있다. Athena 쿼리가 SlowDown 으로 실패하면 파일 수가 너무 많거나 같은 데이터에 동시 쿼리가 몰린 것이다.