빅데이터와 데이터 분석
한 대로는 감당이 안 되는 데이터를 여러 대로 나눠 처리하는 원리부터, Spark가 작업을 스테이지와 태스크로 쪼개 돌리는 방식, 셔플과 쏠림 같은 성능 함정, 스트리밍의 시간 개념, 대용량 데이터의 전처리와 보관, 분석 엔진과 근사 집계, 질문에서 공유까지의 분석 흐름을 숫자로 계산해 가며 정리한다.
목차 (10)
ETL·ELT, 레이크하우스, Parquet 인코딩, 테이블 포맷, 오케스트레이션, 차원 모델링, 데이터 품질, 분석가를 위한 통계는 데이터 엔지니어링 노트에 있다. 이 노트는 그 위에서 큰 데이터를 실제로 계산하고, 다듬고, 쌓아 두고, 읽어 내는 쪽을 다룬다.
빅데이터란: 한 대에 안 들어갈 때
빅데이터는 정해진 크기가 아니라 한 대로는 감당이 안 되는 상태다. 막히는 곳은 셋이다. 디스크에 다 들어가지 않거나(저장), 계산 도중의 데이터가 메모리를 넘거나(메모리), 한 대로 읽고 계산하는 데 너무 오래 걸린다(시간). 흔히 말하는 3V도 같은 이야기다. 양이 많고(volume), 빠르게 쏟아지고(velocity), 표·로그·JSON·이미지처럼 모양이 제각각이다(variety).
시간을 계산해 보자. 하드 디스크 한 개가 초당 200MB를 순서대로 읽는다고 하면 1TB는 5,000초, 약 83분이다. 같은 데이터를 100대에 나눠 두고 동시에 읽으면 50초다. 분산 처리의 출발점이 이 산수다. 다만 요즘 기계 한 대는 생각보다 세다. 초당 3GB를 읽는 NVMe SSD 한 개면 같은 1TB가 약 6분이고, 서버 한 대에 메모리 수백 GB와 코어 수십 개가 흔하다.
- 먼저 작은 도구로 되는지 본다: pandas는 데이터를 통째로 메모리에 올리고 중간 결과마다 사본을 만들기 쉬워, 데이터 크기의 몇 배 메모리가 든다. 수 GB를 넘으면 Polars나 DuckDB를 먼저 쓴다. 둘 다 열 지향·벡터화·멀티코어로 돌고, 필요한 열만 읽으며, 메모리를 넘는 중간 결과는 디스크로 넘겨 가며 한 대에서 수십~수백 GB를 처리한다.
- 여러 대는 마지막 수단: 데이터가 TB를 넘거나, 이미 객체 저장소에 쌓여 있어 옮기는 비용이 크거나, 매일 도는 파이프라인이 한 대의 시간 안에 끝나지 않을 때 Spark·Trino·클라우드 웨어하우스로 간다. 분산은 셔플, 직렬화, 클러스터 운영이라는 세금을 매번 낸다.
분산 처리의 원리: 나누고, 계산을 데이터 쪽으로
분산 처리의 뼈대는 두 가지다. 데이터를 파티션으로 나눠 여러 기계에 두고, 데이터를 계산하는 곳으로 끌어오는 대신 계산(코드)을 데이터가 있는 기계로 보낸다. 코드는 몇 KB지만 데이터는 TB이기 때문이다. Google의 MapReduce(Dean·Ghemawat, 2004)는 이 생각을 세 단계로 굳혔다.
- 맵: 입력 분할마다 태스크 하나가 레코드를 (키, 값) 쌍으로 바꾼다. 그림의 세 줄에서 쌍 8개가 나왔다. 서로 기다리지 않으니 기계 수만큼 빨라진다.
- 셔플: 키의 해시로 받을 리듀서를 정해, 같은 키를 한곳에 모은다. 모든 맵이 모든 리듀서에 보낼 수 있어 디스크와 네트워크를 가장 많이 쓰는 단계다. 맵 쪽에서 미리 더해 두는 컴바이너를 두면 보낼 양이 준다.
- 리듀스: 키마다 모인 값을 합친다. 결과는 data 3, map 2, spark 3다.
데이터를 나눠 두는 쪽은 HDFS가 맡았다. 큰 파일을 흔히 128MB 블록으로 잘라 여러 데이터노드에 흩고, 블록마다 보통 세 벌을 다른 기계(그중 하나는 다른 랙)에 복제한다. 어느 블록이 어디 있는지는 네임노드가 안다. 스케줄러는 블록이 있는 기계에 맵 태스크를 우선 배정한다(데이터 지역성).
기계 수천 대에서는 고장이 일상이다. MapReduce는 중간 결과를 디스크에 쓰고, 기계가 죽으면 그 기계가 맡았던 태스크만 다른 기계에서 다시 돌린다. 유독 느린 태스크는 다른 기계에서 같은 일을 한 번 더 돌려 먼저 끝난 쪽을 쓴다(백업 태스크, 추측 실행). 대가는 속도다. 단계마다 디스크를 거치니, 같은 데이터를 수십 번 도는 반복 계산(머신러닝, 그래프)에는 느렸다. Spark는 여기서 출발했다.
Spark의 구조
Spark는 RDD 논문(Zaharia 등, NSDI 2012)에서 시작했다. 중간 결과를 메모리에 두고, 잃어버린 파티션은 디스크 사본이 아니라 계보(lineage, 이 파티션을 만든 변환의 기록)를 따라 다시 계산한다. 2026년 10월 기준 공식 문서는 4.2 버전이다.
- 드라이버: 사용자 프로그램이 도는 프로세스다. 코드를 실행 계획으로 바꾸고, 작업을 태스크로 쪼개 익스큐터에 배정하고, 결과를 모은다.
- 익스큐터: 작업 노드에서 도는 프로세스다. 코어 수만큼 태스크를 동시에 돌리고, 캐시한 데이터를 메모리에 들고 있는다.
- 클러스터 매니저: 문서가 꼽는 것은 Spark 자체의 Standalone, Hadoop YARN, Kubernetes다. 익스큐터를 띄울 자원을 나눠 준다.
- RDD에서 DataFrame으로: RDD는 타입 없는 객체 묶음이라 Spark가 안을 들여다보지 못한다. DataFrame(그리고 Scala·Java의 타입 있는 Dataset)은 열 이름과 타입이 있는 표라, 엔진이 계획을 최적화할 수 있다. 지금은 DataFrame과 Spark SQL이 기본이고 둘은 같은 엔진을 탄다.
Spark의 연산은 두 종류다. 변환(transformation: filter, select, groupBy, join)은 새 DataFrame을 돌려줄 뿐 아무것도 계산하지 않고 계획에 적어 둔다. 액션(action: count, collect, show, write)을 부르는 순간 쌓인 계획 전체를 최적화해 실행한다. 이것이 지연 평가다. 끝까지 봐야 필요한 열과 행만 읽는 계획을 짤 수 있다.
from pyspark.sql import SparkSession
from pyspark.sql.functions import col
spark = SparkSession.builder.appName("orders").getOrCreate()
# 변환: 여기까지는 계획만 쌓인다
orders = spark.read.parquet("s3://lake/orders/")
paid = orders.filter(col("status") == "paid")
by_country = paid.groupBy("country").count()
# 최적화된 물리 계획을 본다
by_country.explain()
# 액션: 이제야 읽고 계산한다
by_country.show()액션이 불리면 드라이버는 계획을 DAG로 만들고, 그것을 스테이지로, 스테이지를 태스크로 쪼갠다. 기준은 의존의 종류다. filter·select처럼 자식 파티션 하나가 부모 파티션 하나에만 기대는 좁은 의존은 한 태스크 안에서 줄줄이 이어 돌린다. groupBy·join처럼 자식 파티션 하나가 여러 부모 파티션의 데이터를 받아야 하는 넓은 의존은 셔플을 부르고, 그 자리가 스테이지 경계가 된다. 태스크는 파티션 하나를 맡는 일의 단위다.
변환만 적었다. filter·groupBy는 계획에 기록될 뿐 아직 아무 데이터도 읽지 않는다(지연 평가).
- Catalyst 옵티마이저: Spark SQL 논문(Armbrust 등, 2015)이 소개한 옵티마이저다. 논리 계획을 규칙으로 고쳐 쓰고, 물리 계획을 고를 때는 통계도 본다. 조건을 데이터 소스 쪽으로 내려보내(predicate pushdown) Parquet의 행 그룹 통계로 읽지 않을 덩어리를 건너뛰게 하고, 질의에 안 나오는 열은 아예 읽지 않는다(열 가지치기).
explain()의PushedFilters와ReadSchema에서 확인할 수 있다. - Tungsten: 실행 쪽 최적화 묶음이다. JVM 객체 대신 바이너리 행 형식을 직접 관리하고, 여러 연산자를 한 함수로 합쳐 코드를 생성한다(whole-stage code generation).
- AQE(adaptive query execution): 계획을 처음에 한 번만 정하지 않고, 셔플이 끝난 뒤 실제 통계를 보고 다시 짠다. 문서에 따르면 Spark 3.2.0부터 기본으로 켜져 있고, 셔플 뒤 작은 파티션 합치기, 소트 머지 조인을 브로드캐스트 조인으로 바꾸기, 쏠린 조인 파티션 쪼개기를 한다.
셔플과 성능
Spark 작업이 느릴 때 범인은 대개 셔플이다. 셔플 데이터는 직렬화돼 디스크에 쓰이고 네트워크를 건너간다. 성능 조정은 결국 셔플을 줄이거나, 고르게 하거나, 없애는 일이다.
- 파티션 수: 파일을 읽을 때는 파티션 하나가 기본 128MB(
spark.sql.files.maxPartitionBytes), 셔플 뒤 파티션 수는 기본 200(spark.sql.shuffle.partitions)이다. 너무 적으면 태스크 하나가 메모리를 넘쳐 디스크로 흘리고(spill), 너무 많으면 태스크 배정 비용과 작은 출력 파일이 늘어난다. AQE의 파티션 합치기가 뒤쪽 문제를 상당 부분 덜어 준다. - coalesce와 repartition: 둘 다 파티션 수를 바꾸지만 비용이 다르다. RDD 가이드에 따르면
repartition은 늘 모든 데이터를 네트워크로 셔플해 고르게 다시 나누고,coalesce는 파티션 수를 줄이는 쪽으로만 쓰며 크게 걸러 낸 뒤 쓰기 좋다. 파일 수를 줄이려 할 때 먼저 coalesce를 떠올리되, 줄인 파티션이 한쪽으로 쏠리면 repartition을 쓴다. - 미리 줄여서 보낸다: 키별 합계·평균처럼 집계가 목적이면 RDD의
groupByKey보다reduceByKey·aggregateByKey가 훨씬 빠르다고 같은 가이드가 적는다. 앞의 컴바이너처럼 맵 쪽에서 먼저 줄인 값만 셔플하기 때문이다. - collect는 드라이버로 모은다:
collect()는 데이터 전체를 드라이버 한 대로 가져오므로 큰 결과에서는 드라이버 메모리가 넘친다. 몇 행만 보려면take()나show()를 쓰고, 결과가 크면 파일로 쓴다.
데이터 쏠림(skew)은 키 하나에 행이 몰리는 일이다. 해시 파티셔닝은 같은 키를 반드시 한 파티션에 보내므로, 쏠린 키를 맡은 태스크 하나만 오래 돈다. 스테이지는 가장 늦은 태스크가 끝나야 끝난다. 행 1억 개 중 4천만 개가 한 키(예: 비로그인 사용자 ID가 빈 값)이고, 태스크 20개가 각자 초당 100만 행을 처리한다면, 그 키를 맡은 태스크가 43초를 도는 동안 나머지는 3초 만에 끝나고 기다린다.
소금 치기(salting)는 쏠린 키 뒤에 0~N−1의 무작위 꼬리를 붙여 키를 N개로 쪼갠다. 1차로 (키, 꼬리)별로 집계하고, 꼬리를 떼고 2차로 한 번 더 집계한다. 10갈래로 쪼개면 스테이지가 7초로 준다. 셔플이 한 번 늘지만 가장 긴 태스크가 줄어든 효과가 훨씬 크다. 조인 쪽 쏠림은 AQE가 쏠린 파티션을 자동으로 쪼개 주고, 빈 값·기본값처럼 의미 없는 키는 조인 전에 걸러 내는 것이 가장 싸다.
조인은 셔플을 가장 많이 부르는 연산이다. 한쪽 표가 작으면 셔플 자체를 없앨 수 있다.
- 브로드캐스트 조인: 200GB 주문 표와 8MB 상품 표를 익스큐터 50개에서 조인하면, 상품 표 사본 400MB만 옮기면 된다. Spark는 통계상 크기가
spark.sql.autoBroadcastJoinThreshold(기본 10MB) 이하인 표를 자동으로 브로드캐스트한다. 작은 표가 드라이버와 익스큐터 메모리에 다 들어가야 하므로 너무 크게 올리면 메모리 부족으로 죽는다. - 셔플(소트 머지) 조인: 둘 다 크면 양쪽을 조인 키로 다시 나눠 같은 키끼리 모으고, 파티션마다 정렬해 병합한다. 이 경우 약 200GB가 네트워크를 지난다. 같은 키로 자주 조인하는 큰 표는 미리 키로 나눠 저장해 두면(버킷팅) 이 셔플을 줄일 수 있다.
- 캐시와 퍼시스트: 같은 DataFrame을 여러 번 쓸 때만
cache()·persist()한다. 지연 평가 때문에 액션마다 처음부터 다시 계산하기 때문이다. RDD의 기본 저장 수준은MEMORY_ONLY이고, 메모리가 모자라면MEMORY_AND_DISK같은 수준을 고른다. 한 번만 쓰는 데이터를 캐시하면 메모리만 빼앗고, 다 쓴 뒤에는unpersist()로 놓아 준다. - 작은 파일 문제: 100GB를 100KB 파일로 쓰면 1,000,000개, 128MB 파일로 쓰면 782개다. 파일 하나를 열고 메타데이터를 읽는 데 10ms가 든다고 하면, 100개를 동시에 열어도 앞쪽은 여는 데만 100초, 뒤쪽은 0.08초다. 셔플 파티션이나 스트리밍 배치마다 파일을 하나씩 쓰면 이렇게 된다. 쓰기 전에
repartition·coalesce로 파일 수를 맞추고, 쌓인 뒤에는 합친다(보관 절의 컴팩션).
스트리밍: 끝나지 않는 데이터
배치는 끝이 있는 데이터를 처리하고, 스트리밍은 계속 들어오는 데이터를 처리한다. 처리 주기를 어떻게 고르는지와 CDC는 데이터 엔지니어링 › 배치, 스트리밍, CDC에, 정확히 한 번 처리와 상태 관리의 속은 시스템 설계 › 배치와 스트림 처리에 있다. 여기서는 대용량 처리 엔진이 스트림을 어떻게 다루는지만 짚는다.
- Kafka를 로그로: Kafka의 토픽은 파티션된 추가 전용 로그다. 소비자는 자기가 어디까지 읽었는지(오프셋)만 기억하고, 로그는 보존 기간 동안 남는다. 그래서 처리 코드에 버그가 있으면 오프셋을 되감아 다시 처리할 수 있다. 스트림 처리의 입력과 출력을 잇는 표준 배관이 됐다.
- Spark Structured Streaming: 스트림을 끝없이 행이 덧붙는 표로 보고, 배치와 같은 DataFrame 코드를 쓴다. 문서에 따르면 기본은 작은 배치를 연달아 돌리는 마이크로배치로 종단 지연이 100ms 수준까지 내려가고 정확히 한 번을 보장한다. 1ms 수준을 노리는 연속 처리 모드는 최소 한 번 보장이다.
- Flink: Flink는 이벤트를 하나씩 연산자 사이로 흘려 보내는 구조다. 연산자마다 상태를 들고 있고, 주기적으로 모든 연산자의 상태를 한꺼번에 찍는 체크포인트로 장애에서 복구한다. 지연이 낮고 상태가 큰 작업에 강하다.
| Spark Structured Streaming | Flink | |
|---|---|---|
| 처리 단위 | 작은 배치를 연달아(기본). 배치 코드와 같은 DataFrame API | 이벤트를 하나씩 연산자 사이로 흘린다 |
| 지연 | 문서 기준 마이크로배치 100ms 수준, 연속 처리 모드 1ms 수준 | 배치를 기다리지 않아 지연이 낮다 |
| 장애 복구 | 체크포인트와 선행 기록 로그(WAL)에 처리 중인 오프셋 범위를 남기고, 다시 읽을 수 있는 원천과 멱등 싱크로 정확히 한 번 | 모든 연산자 상태를 함께 찍는 체크포인트(Chandy–Lamport 계열)와 스트림 다시 읽기로 정확히 한 번 |
| 상태 | 스트리밍 집계·이벤트 시간 윈도·조인을 배치와 같은 DataFrame API로 쓴다 | 키마다 상태를 내장 키-값 저장소처럼 두고, 상태 백엔드로 저장 방식을 고른다 |
| 고르는 기준 | 이미 Spark로 배치를 돌리고, 초 단위 지연이면 충분할 때 | 지연이 짧아야 하거나 상태가 크고 오래 사는 이벤트 처리일 때 |
표의 장애 복구·상태 칸은 Structured Streaming 가이드와 Flink의 상태 있는 스트림 처리 문서를 따랐다. 둘 다 정확히 한 번을 말하지만 그 보장은 엔진 안의 상태와 지원되는 싱크까지다. 밖으로 나가는 부작용은 시스템 설계 › 배치와 스트림 처리의 정확히 한 번 절에서 다룬다.
스트리밍의 어려움은 시간이다. 이벤트가 일어난 이벤트 시간과 시스템에 도착한 처리 시간이 다르고, 모바일 앱이 오프라인이었다가 한꺼번에 보내면 순서도 뒤섞인다. “10시~10시 5분 사이 주문 수” 같은 윈도 집계를 이벤트 시간으로 하려면, 언제 그 윈도를 닫고 결과를 낼지 정해야 한다. 그 기준이 워터마크다. “이 시각 이전 이벤트는 이제 거의 다 왔다”는 선언으로, 흔히 지금까지 본 가장 늦은 이벤트 시각에서 허용 지연을 뺀다.
그림의 여섯 번째 이벤트(시각 2분)가 도착했을 때 워터마크는 이미 7분이었다. 허용 지연을 늘리면 늦은 이벤트를 더 받아 주지만, 윈도를 오래 열어 두니 결과가 늦고 상태 메모리가 커진다. 정확성·지연·비용 사이의 이 저울질을 하나의 모델로 정리한 것이 Dataflow 모델(Akidau 등, 2015)이다.
전처리: 데이터 정리
원천 데이터는 거의 언제나 지저분하다. 분석이나 학습에 넣기 전에 다듬는 일을 전처리라 한다. 규칙 자체는 작은 표와 같지만, 대용량에서는 눈으로 훑어볼 수 없으니 규칙을 코드로 적고 전후 숫자(행 수, 결측 비율, 고유 값 수)를 기록하는 것이 중요하다. 들어올 때 검사하는 데이터 품질 규칙은 데이터 품질과 데이터 계약에서 다룬다.
원본: 나이·금액이 글자이고, 도시 표기가 제각각이며, id 2가 두 번 들어 있다.
- 스키마 확인과 타입 변환: CSV·JSON의 스키마 추론은 앞부분만 보고 타입을 정하거나, 섞여 있으면 전부 문자열로 둔다. 큰 데이터는 스키마를 명시해 읽고, 변환에 실패한 값은 조용히 null이 되므로 변환 전후 null 수를 비교한다.
- 결측값: 세 가지 길이 있다. 행을 지우면 간단하지만, 결측이 무작위가 아니면(소득을 안 적은 사람이 특정 집단에 몰리면) 남은 데이터가 치우친다. 중앙값·최빈값 등으로 대체하면 행을 지키지만 분산이 줄어든다. 그래서 대체할 때도 “대체함” 같은 표시 열을 남겨, 결측이었다는 사실 자체를 정보로 쓴다. pandas와 Spark 모두
fillna·dropna를 둔다. - 중복 제거: 기본 키가 있으면 키 기준으로 지운다(Spark의
dropDuplicates(["id"])). 같은 키의 행이 서로 다르면 어느 쪽을 남길지(가장 최근 갱신 등) 규칙을 정한다. 키가 없는 근사 중복(“서울시 강남구”와 “서울 강남구”)은 정규화한 뒤 비교하고, 규모가 크면 MinHash 같은 근사 유사도로 후보만 먼저 추린다. - 이상값: IQR 규칙은 Q1 − 1.5·IQR과 Q3 + 1.5·IQR 밖을 후보로 본다. 그림의 금액 네 개에서 Q1은 9,125, Q3는 309,000이고, 1,200,000은 위쪽 경계 밖이다. z 점수(평균에서 표준편차 몇 개만큼 떨어졌나)로 보면 같은 값이 1.50에 그친다. 표본이 작으면 이상값이 평균과 표준편차를 함께 끌어올려 스스로를 숨기기 때문이다. 대용량에서는 정확한 분위수 대신
approxQuantile같은 근사 분위수를 쓴다. 이상값은 지우기 전에 입력 오류인지, 진짜 큰 값인지 먼저 확인한다. - 정규화·표준화: 최소-최대 정규화는 값을 0~1로 옮긴다(나이 27~35에서 31은 0.5). 표준화는 평균 0, 표준편차 1로 옮긴다. 거리·경사 하강을 쓰는 모델은 열의 단위가 다르면 큰 단위 열에 끌려가므로 필요하고, 트리 모델은 대개 필요 없다. 이상값이 있으면 최소-최대 정규화가 나머지 값을 한쪽으로 몰아 버린다.
- 범주형 인코딩: 원-핫 인코딩은 범주마다 0/1 열을 만든다. 범주가 수만 개면 열이 폭발하므로 드문 범주를 “기타”로 묶거나 해시로 열 수를 고정한다. 범주를 그 범주의 목표값 평균으로 바꾸는 타깃 인코딩은 강력하지만, 자기 행의 정답이 입력에 섞여 들어가 과적합하기 쉽다. 교차 폴드로 다른 부분의 평균만 쓰게 해야 한다.
- 날짜와 시간대: 저장은 UTC 타임스탬프로 하고, 보여 줄 때 지역 시간으로 바꾼다. “하루”를 어느 시간대 기준으로 자르는지 정하지 않으면 한국 기준 일 매출과 UTC 기준 일 매출이 9시간어치 어긋난다. 서머타임이 있는 지역은 하루가 23시간이나 25시간인 날도 있다.
- 조인 키 정리: 한쪽은 정수, 한쪽은 문자열이거나, 앞자리 0(“007”과 7), 대소문자, 공백이 다르면 조인이 조용히 비어 버린다. 조인 전후에 행 수를 세어, 줄었으면 키 불일치를, 늘었으면 한쪽 키가 유일하지 않은지(1:N 폭발)를 의심한다.
- 샘플링: 탐색과 규칙 시험은 표본으로 빠르게 한다. 드문 집단이 중요하면 집단마다 따로 뽑는 층화 추출을 써야 표본에서 사라지지 않는다. 표본으로 정한 규칙은 전체에 돌린 뒤 숫자를 다시 확인한다.
가장 흔한 실수는 데이터 누수다. 결측 대체에 쓰는 중앙값, 표준화의 평균과 표준편차, 타깃 인코딩의 범주 평균을 전체 데이터로 구한 뒤 학습·테스트로 나누면, 테스트 세트의 정보가 학습에 새어 들어가 성능이 실제보다 좋게 나온다. 통계는 학습 세트에서만 구하고 그 값을 검증·테스트 세트에 그대로 적용한다. 시간 순서가 있는 데이터는 무작위가 아니라 시간으로 자른다. 나누는 방법은 머신러닝의 원리 › 데이터 나누기와 교차 검증에, 함정의 예는 scikit-learn 문서에 있다.
보관: 저장소, 형식, 파티셔닝, 수명 주기
처음 하둡 시절에는 계산 노드의 디스크가 곧 저장소(HDFS)였다. 지금은 대부분 객체 저장소(Amazon S3, Google Cloud Storage, Azure Data Lake Storage)에 둔다. 저장과 계산이 분리되니, 데이터는 한 벌만 싸게 두고 계산 클러스터는 필요할 때만 띄웠다가 끈다. 대신 데이터는 늘 네트워크로 읽고, 목록 조회가 느리며 “폴더 이름 바꾸기” 같은 원자적 연산이 없다. 이 빈틈을 메우는 것이 열린 테이블 포맷이다. 객체 저장소 자체의 원리는 저장장치 › 종류에, 관리형 서비스는 클라우드 노트에 있다.
| 형식 | 배치 | 스키마 | 주로 쓰는 곳 |
|---|---|---|---|
| CSV | 행, 글자 | 없음(추론) | 사람·외부와 주고받기. 타입이 없고 따옴표·줄바꿈 처리가 제각각이다. |
| JSON Lines | 행, 글자 | 없음, 중첩 가능 | 로그·API 원본. 키 이름이 행마다 반복돼 크다. |
| Avro | 행, 바이너리 | 파일에 포함, 진화 규칙 있음 | Kafka 메시지, 한 행씩 쓰는 적재. 읽는 쪽 스키마와 쓰는 쪽 스키마를 맞춰 열 추가·기본값을 처리한다. |
| Parquet | 열, 바이너리 | 파일에 포함 | 분석 표의 기본. 열마다 인코딩·압축·통계가 붙는다. |
| ORC | 열, 바이너리 | 파일에 포함 | Hive 생태계에서 출발한 열 지향 형식. 구조와 쓰임은 Parquet와 비슷하다. |
열 지향 파일이 왜 작고 빠른지(행 그룹, 사전·RLE 인코딩, min/max 건너뛰기)는 데이터 엔지니어링 › 열 지향 파일: Parquet에서 계산해 두었다. 인코딩 뒤에 거는 압축 코덱은 속도와 압축률을 맞바꾼다. Snappy는 빠르지만 덜 줄이고(Spark가 Parquet를 쓸 때의 기본값), zstd는 레벨로 둘 사이를 고를 수 있어 저장 비용이 중요할 때 흔히 고르며, gzip은 많이 줄이지만 느리다. 주의할 점이 하나 있다. Parquet 안의 압축은 페이지 단위라 파일을 나눠 읽을 수 있지만, 통째로 gzip한 CSV는 처음부터 풀어야 해서 파일 하나를 태스크 하나가 다 읽는다.
파티셔닝은 자주 거르는 열(대개 날짜)의 값으로 폴더를 나눠 쓰는 것이다. 질의 조건이 파티션 열에 걸리면 엔진은 파일을 열기 전에 폴더 이름만 보고 읽을 곳을 고른다(파티션 가지치기).
- 얼마나 줄어드나: 통째로 읽으면 7.3TB, 날짜 파티션으로 140GB, 열 서른 개 중 세 개만 읽으면 14GB다. 스캔한 바이트로 요금을 매기는 엔진에서는 이 차이가 곧 비용이다.
- 너무 잘게 나누지 않기: 사용자 ID처럼 값이 수백만 개인 열로 파티션하면 폴더마다 작은 파일이 생겨 앞 절의 작은 파일 문제가 된다. 파티션 하나가 적어도 수백 MB~수 GB가 되게 고른다. 값이 많은 열은 대신 버킷팅(키의 해시로 정해진 개수의 파일에 나눠 담기)이나 파일 안 정렬로 건너뛰기를 돕는다. Iceberg는 날짜를 파생해 파티션하는 숨은 파티셔닝으로 질의가 파티션 열을 몰라도 되게 한다.
- 컴팩션: 스트리밍 적재나 잦은 작은 쓰기로 쌓인 파일은 주기적으로 큰 파일로 합친다. Iceberg는 Spark의
rewriteDataFiles로, Delta Lake는OPTIMIZE로 한다. 합치면서 자주 거르는 열로 정렬해 두면 min/max 건너뛰기도 좋아진다.
데이터는 나이가 들수록 덜 읽힌다. 그래서 나이에 따라 더 싼 저장 계층으로 옮기는 수명 주기 규칙을 둔다.
- 계층: S3를 예로 들면 Standard → Standard-IA(드문 접근) → Glacier Instant Retrieval → Glacier Flexible Retrieval·Deep Archive 순으로 저장 단가는 내려가고, 꺼낼 때의 요금과 시간, 최소 보관 기간은 늘어난다. 가장 깊은 계층은 복원에 몇 시간이 걸린다. 접근 패턴을 모르면 접근 빈도를 보고 알아서 옮기는 Intelligent-Tiering을 쓴다. 가격과 기간은 자주 바뀌므로 공식 페이지에서 확인한다.
- 원본은 남기되 싸게: 정제 층이 망가졌을 때 다시 만들 수 있도록 원본(raw)은 오래 두는 편이지만, 차가운 계층으로 내린다. 자주 읽는 집계 표는 따뜻한 계층에 둔다.
- 보존 기간과 삭제 요구: 개인 정보는 “필요한 동안만” 두는 것이 원칙이고, 법이나 사용자의 요청으로 특정 사람의 행을 지워야 할 때가 있다. 불변 파일로 이뤄진 레이크에서 행 하나를 지우려면 그 행이 든 파일을 다시 쓰거나 삭제 벡터를 남겨야 하고, 옛 스냅숏이 그 파일을 가리키는 동안은 실제로 사라지지 않는다. 그래서 스냅숏 만료와 정리까지 해야 삭제가 끝난다. 개인 식별 정보를 별도 표로 떼어 두고 분석 표에는 가명 ID만 두면, 지울 곳이 작은 표 하나로 준다.
분석 엔진과 SQL
분석가는 대개 Spark 코드가 아니라 SQL로 묻는다. 파일 위에 표를 얹어 SQL로 읽는 SQL-on-files 엔진이 여럿이다. Hive는 SQL을 MapReduce 작업으로 바꿔 돌리던 원조이고, 그 메타스토어(표 이름 → 파일 위치)는 지금도 여러 엔진이 카탈로그로 쓴다. Trino(Presto에서 갈라져 나온 프로젝트)는 상주하는 여러 대가 질의를 나눠 메모리에서 흘려 처리하는 대화형 엔진으로, 커넥터로 여러 원천을 한 질의에서 조인한다. Spark SQL은 긴 배치와 ETL에 강하다. 클라우드 웨어하우스(BigQuery, Snowflake, Amazon Redshift, Databricks SQL)는 같은 일을 관리형으로 해 주며, 대부분 저장과 계산을 분리하고 레이크의 열린 표를 외부 표로 읽을 수 있다.
이 엔진들이 TB를 몇 초에 훑는 이유는 비슷하다. 필요한 열만 읽고(열 지향), 압축된 채로 비교하며, 값을 하나씩이 아니라 수천 개 묶음으로 처리해 CPU 캐시와 SIMD를 잘 쓴다(벡터화 실행). 여기에 조건 내려보내기와 파티션 가지치기로 읽을 양 자체를 줄이고, 여러 대가 나눠 읽는다(MPP). 열 지향 저장과 벡터화의 속은 데이터베이스 › 분석과 검색에 있다.
큰 데이터에서 정확한 답이 비싼 질문이 있다. 고유 사용자 수(COUNT(DISTINCT user_id))를 정확히 세려면 본 ID를 모두 기억해야 한다. 사용자 1억 명의 8바이트 ID면 800MB이고, 여러 대에서 세려면 ID를 셔플해 모아야 한다. HyperLogLog는 ID의 해시에서 앞쪽에 0이 몇 개 이어지는지를 레지스터 m개에 나눠 최댓값만 기억한다. 0이 k개 이어진 해시를 봤다면 대략 2k개쯤 봤으리라는 어림이다. 표준 오차는 약 1.04/√m이라, 레지스터 16,384개(각 6비트, 12KB)로 오차 약 0.81%다. 레지스터끼리 최댓값을 취하면 합칠 수 있어서, 일별 스케치를 저장해 두면 월간 고유 사용자를 원본 없이 낸다.
- 쓰는 법: Spark SQL은
approx_count_distinct, Trino는approx_distinct, BigQuery는APPROX_COUNT_DISTINCT를 둔다. 대시보드의 고유 방문자처럼 1% 안쪽 오차가 괜찮은 곳에 쓰고, 정산처럼 정확해야 하는 곳에는 쓰지 않는다. - Count-Min 스케치: “어떤 검색어가 몇 번 나왔나” 같은 빈도를 고정된 크기의 카운터 표로 어림한다. 해시 함수마다 한 줄씩 카운터를 올리고, 읽을 때는 줄들 중 최솟값을 쓴다(늘 실제보다 크거나 같다). 오차를 전체 건수의 0.1% 이내로, 그 보장이 깨질 확률을 1%로 잡으면 너비 2,719 × 깊이 5의 카운터면 된다.
윈도 함수는 행을 줄이지 않고 이웃 행을 보며 계산한다. 누적 합, 사용자별 최근 주문 고르기, 직전 값과의 차이 같은 분석 대부분이 GROUP BY와 자기 조인 대신 이것으로 풀린다.
SELECT
user_id,
order_date,
amount,
-- 사용자별 누적 구매액
SUM(amount) OVER (PARTITION BY user_id ORDER BY order_date) AS running_total,
-- 사용자별 최근 주문이 1
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_date DESC) AS recency,
-- 직전 주문과의 간격(일)
DATEDIFF(order_date, LAG(order_date) OVER (PARTITION BY user_id ORDER BY order_date)) AS gap_days
FROM orders분산 엔진에서 PARTITION BY는 그 키로 셔플을 부른다. 쏠린 키가 있으면 윈도 함수도 앞 절의 쏠림을 그대로 겪는다. DATEDIFF처럼 날짜 함수는 엔진마다 이름과 인자 순서가 다르니 문서를 확인한다.
분석의 흐름: 질문에서 공유까지
도구가 커져도 분석의 순서는 같다. 많은 데이터를 빨리 돌릴 수 있게 되면 오히려 질문 없이 숫자를 뽑는 일이 쉬워지므로, 순서를 지키는 것이 더 중요해진다.
- 질문 정하기: 이 분석으로 무엇을 결정하는지부터 적는다. 그다음 지표를 정확히 정의한다. “활성 사용자”의 분모는 누구이고, 기간은 어디서 어디까지이며, 하루는 어느 시간대로 자르는가. 같은 이름의 지표가 팀마다 다르게 계산되는 것이 흔한 사고다.
- 탐색(EDA): 집계하기 전에 데이터를 본다. 열마다 분포(히스토그램, 백분위), 결측 비율, 고유 값 수, 최솟값·최댓값을 확인하고, 열 사이의 상관을 훑는다. 예상과 다른 값은 대개 데이터 문제이거나 가장 흥미로운 발견이다. 요약 통계 읽는 법은 확률과 통계 › 기술 통계와 분석가를 위한 통계 기초에 있다.
- 집계·시각화: 그룹별로 나눠 비교한다. 비율은 늘 분모와 함께 보이고, 작은 그룹의 극단 값은 표본이 작아서일 수 있음을 적는다. 차트는 질문에 맞게 고른다. 시간 추이는 선, 그룹 비교는 막대, 분포는 히스토그램이나 상자 그림이다.
- 해석: 두 지표가 함께 움직여도 하나가 다른 하나를 일으킨다는 뜻은 아니다. 숨은 제3의 변수가 둘을 함께 움직이거나, 집단을 합치면 방향이 뒤집히기도 한다(상관과 인과: 심슨의 역설). 살아남은 것만 보이는 데이터(해지한 고객이 빠진 만족도 조사)는 선택 편향을 의심한다. 원인을 말하고 싶으면 실험(A/B 테스트)이 가장 확실하다.
- 공유: 반복해서 볼 지표는 BI 대시보드로, 한 번의 깊은 분석은 노트북이나 보고서로 남긴다. 어느 쪽이든 질의, 데이터 시점, 지표 정의를 함께 남겨 다른 사람이 같은 숫자를 다시 낼 수 있게 한다. 대시보드는 지표 정의를 한곳(시맨틱 층, dbt 모델)에 두고 끌어 써야 숫자가 화면마다 어긋나지 않는다.
핵심 자료
요지는 직접 정리한 한 줄이다.
| 자료 | 종류 | 한 줄 요지 |
|---|---|---|
| Apache Spark 문서 | 공식 문서 | 클러스터 구조, RDD·DataFrame·SQL 가이드, 성능 조정(AQE·조인 전략)과 Structured Streaming을 다룬다. |
| MapReduce (Dean·Ghemawat, OSDI 2004) | 논문 | 맵·셔플·리듀스 모델과, 실패한 태스크를 다시 돌려 장애를 견디는 실행 방식을 보였다. |
| Resilient Distributed Datasets (Zaharia 등, NSDI 2012) | 논문 | Spark의 바탕. 데이터를 메모리에 두고, 잃은 파티션은 계보를 따라 다시 계산한다. |
| Spark SQL (Armbrust 등, SIGMOD 2015) | 논문 | DataFrame API와 Catalyst 옵티마이저의 규칙 기반 계획 변환을 설명한다. |
| The Dataflow Model (Akidau 등, VLDB 2015) | 논문 | 이벤트 시간·윈도·워터마크·트리거로 배치와 스트림을 한 모델로 묶었다. |
| DuckDB 문서 | 공식 문서 | 내장형 열 지향 SQL 엔진. Parquet·CSV를 파일째로 질의한다. |
| Polars 사용자 가이드 | 공식 문서 | Rust로 만든 데이터프레임. 지연(lazy) API와 스트리밍 실행을 설명한다. |
| Apache Kafka 문서 | 공식 문서 | 파티션된 추가 전용 로그, 소비자 그룹과 오프셋, 보존 설정을 다룬다. |
| Apache Flink 문서 | 공식 문서 | 이벤트 단위 스트림 처리, 상태와 체크포인트, 이벤트 시간과 워터마크를 다룬다. |
| HyperLogLog (Flajolet 등, 2007) | 논문 | 레지스터 m개로 고유 값 수를 표준 오차 약 1.04/√m로 어림하는 알고리즘이다. |
| scikit-learn: 흔한 함정 | 공식 문서 | 전처리 통계를 학습 세트에서만 구해야 하는 이유(데이터 누수)를 예제로 보인다. |
- Apache Spark 문서
- MapReduce (Dean·Ghemawat, OSDI 2004)
- Resilient Distributed Datasets (Zaharia 등, NSDI 2012)
- Spark SQL (Armbrust 등, SIGMOD 2015)
- The Dataflow Model (Akidau 등, VLDB 2015)
- DuckDB 문서
- Polars 사용자 가이드
- Apache Kafka 문서
- Apache Flink 문서
- HyperLogLog (Flajolet 등, 2007)
- scikit-learn: 흔한 함정
- Spark 클러스터 모드 개요
- Spark RDD 프로그래밍 가이드
- Spark SQL 성능 조정
- Spark Structured Streaming 가이드
- Spark Parquet 데이터 소스
- HDFS 아키텍처
- Flink — 이벤트 시간과 워터마크
- Flink — 상태 있는 스트림 처리(체크포인트·상태 백엔드)
- Trino 문서
- Apache Avro 문서
- Apache ORC
- Iceberg — 테이블 유지 관리(파일 합치기)
- Amazon S3 스토리지 클래스
- pandas — 결측 데이터 다루기