A Short History of Data Systems
앞 세대의 어떤 아픔이 다음 세대를 낳았나
- Haram Lee
- 2026-07-24
- studies / Topics
데이터 기술은 이름이 너무 많다. Hadoop, Spark, BigQuery, Kafka, Flink, Iceberg, Arrow를 하나씩 따로 외우면 금방 뒤섞인다.
이 글은 각 기술을 앞 세대의 어떤 아픔이 다음 세대를 낳았는지의 순서로 읽는다. 그렇게 읽으면 대부분의 기술이 하나의 줄로 이어진다.
~2003 단일 DB의 시대 "한 머신을 더 키우자" (스케일업)
2003-04 구글 논문 GFS + MapReduce, "싼 머신 여러 대로" (스케일아웃)
2006~ Hadoop 시대 HDFS + MapReduce, 이후 YARN 분리
2010~ Spark 시대 메모리 + DAG + 고수준 API로 MapReduce 대체
2011~ 스트리밍 Kafka → Flink, "배치 주기만큼 늦는" 문제
2012~ 클라우드 웨어하우스 BigQuery, Snowflake, 저장과 계산의 분리
2017~ 레이크하우스(진행 중) Iceberg, Delta, 레이크에 테이블 개념 다시 얹기출발점: 왜 분산이 필요해졌고, 왜 어려운가
2000년대 초까지 데이터가 커지면 답은 더 큰 머신, 즉 스케일업이었다. 그런데 웹 규모에서는 아무리 큰 머신도 부족했고, 가격은 성능에 비례하지 않고 훨씬 가파르게 올랐다. 구글의 선택이 판을 바꿨다. 고장 나는 싼 머신 수천 대를 묶어 쓰자는 스케일아웃이다.
이 선택은 지금까지 모든 데이터 기술이 씨름하는 두 가지 근본 문제를 낳았다.
- 부분 실패(partial failure): 머신 한 대의 연간 고장률이 1%라면, 1,000대 클러스터에서 고장은 일상이다. 실패는 예외가 아니라 기본 전제가 된다. 복제, 재시도, lineage 재계산 같은 장치는 모두 여기서 나온다.
- 네트워크는 느리다. 메모리, 디스크, 네트워크 순으로 느려진다. 그래서 “데이터를 계산으로 보내지 말고 계산을 데이터로 보내자”(locality), “머신 간 데이터 이동(셔플)을 최소화하자"가 모든 엔진의 강박이 됐다.
분산 처리의 문제는 결국 둘로 나뉜다. 저장(잃어버리지 않기)과 계산(나눠서 처리하기)이다. 이후의 기술은 모두 이 둘 중 하나에 답한다.
Hadoop: 최초의 대중적인 답
Hadoop은 구글 논문(GFS, MapReduce)의 오픈소스 구현이다. 한 단어처럼 부르지만 실은 세 부품으로 이뤄져 있다.
┌─────────────────────────────────────┐
│ MapReduce ← 계산 프레임워크 │ 어떻게 나눠서 처리하나
├─────────────────────────────────────┤
│ YARN ← 자원 관리자 │ 누가 어느 머신의 CPU와 메모리를 쓰나
├─────────────────────────────────────┤
│ HDFS ← 분산 파일시스템 │ 데이터를 어디에 어떻게 저장하나
└─────────────────────────────────────┘HDFS: 분산 저장
HDFS는 큰 파일을 블록(기본 128MB)으로 잘라 여러 머신에 세 벌씩 복제해 둔다.
NameNode 한 대가 “어떤 파일의 몇 번 블록이 어느 머신에 있다"는 목차만 관리하고, 여러 대의 DataNode가 실제 블록을 디스크에 보관한다. 머신이 죽어도 복제본 두 벌이 남고, NameNode가 부족한 복제본을 다시 채운다.
중요한 부산물이 하나 있다. 시스템이 데이터가 어느 머신에 있는지 안다는 것이다. 그래서 계산을 데이터 쪽으로 보낼 수 있다. Spark 스케줄러가 locality를 따지는 문화가 여기서 시작됐다.
MapReduce: 계산, 그리고 셔플의 원조
MapReduce의 아이디어는 어떤 집계든 “각자 처리(map), 키별로 모으기(shuffle), 집계(reduce)“로 표현할 수 있다는 것이다.
입력 블록 map (각 머신에서) shuffle (키별로 모음) reduce
"a b a" → (a,1)(b,1)(a,1) → a: [1,1,1] → (a, 3)
"a c" → (a,1)(c,1) → b: [1] → (b, 1)
c: [1] → (c, 1)map은 자기 머신의 블록만 처리하므로 통신 없이 완전히 병렬로 돈다. shuffle은 같은 키를 같은 머신으로 모으는 재배치이고, 모든 머신이 모든 머신과 통신하는 유일한 구간이라 비싸다. 내결함성은 각 단계의 결과를 디스크에 쓰고 죽은 task만 다시 실행하는 방식으로 얻는다.
MapReduce의 한계 두 가지가 곧 다음 세대의 존재 이유가 된다.
- 단계마다 디스크를 거친다. join 하나에 MapReduce를 세 번 돌리면 중간 결과를 세 번 HDFS에 썼다가 읽는다. 같은 데이터를 수십 번 반복해서 읽는 머신러닝 알고리즘에는 치명적이다.
- 표현력이 부족하다. 모든 로직을 map과 reduce 두 함수에 욱여넣어야 해서 join 하나에 자바 코드가 수백 줄 필요했다.
YARN: 자원 관리를 떼어내다
MapReduce v1은 계산과 자원 관리가 한 덩어리라 클러스터에서 MapReduce만 돌 수 있었다. Hadoop 2.0(2013)은 자원 배분을 계산 프레임워크에서 떼어내 YARN으로 독립시켰다. 그 뒤로 같은 클러스터에서 MapReduce도 Spark도 돌 수 있게 됐다.
ResourceManager가 클러스터 전체의 CPU와 메모리 장부를 들고 컨테이너 요청을 배정하고, 머신마다 있는 NodeManager가 컨테이너를 띄우고 감시한다. 앱마다 하나씩 뜨는 ApplicationMaster가 앱을 대표해 자원을 협상하는데, Spark on YARN에서는 driver가 이 역할을 맡는다. Spark에게 YARN은 executor를 띄울 자리를 내주는 쪽일 뿐이고, 자리가 잡히면 task 분배는 driver가 직접 한다.
Spark: 디스크 대신 메모리, 두 함수 대신 그래프
MapReduce의 두 한계를 정면으로 겨냥한 것이 Spark다(UC Berkeley, 2010).
- 메모리 유지: 중간 결과를 매번 디스크에 쓰지 않고 가능하면 메모리에 둔다. 실패에 대비하는 방법도 복제가 아니라 lineage다. 계산 그래프를 기록해 두었다가 잃어버린 부분만 다시 계산한다. 데이터를 백업하는 대신 레시피를 백업하는 셈이다.
- DAG: map과 reduce 두 단계를 강제하지 않고, filter, join, groupBy 같은 임의의 연산 그래프를 받아 stage로 쪼갠다. 셔플 개념과 map, reduce task라는 이름은 MapReduce에서 그대로 물려받았다. Spark 코드의
ShuffleMapTask가 그 흔적이다. - 고수준 API: 자바 수백 줄이
df.groupBy("k").count()한 줄이 된다.
그리고 중요한 방향 전환이 있었다. SQL의 귀환이다. 초기 Spark는 RDD라는 함수형 API가 중심이었지만, 결국 DataFrame과 Spark SQL이 주류가 됐다. 선언형이어야 엔진이 대신 최적화해 줄 수 있기 때문이다. “어떻게"를 사람이 쓰면 최적화할 여지가 없고, “무엇을"만 쓰면 엔진이 필터를 앞당기고 조인 순서를 바꾼다. Hive가 MapReduce 위에 SQL을 얹은 것도 같은 이야기다.
Spark의 내부 구조는 Spark 시리즈에서 따로 다룬다.
클라우드: 저장과 계산의 분리
Hadoop 모델의 전제는 “데이터가 계산할 머신들 안에 있다"는 것이었다. 클라우드가 이 전제를 깼다.
S3, GCS 같은 object storage는 사실상 무한한 용량을 관리 부담 없이 훨씬 싸게 제공한다. 대신 데이터가 어느 머신에 있는지를 아는 locality 이점은 포기해야 한다. 네트워크가 빨라지면서 이 손해를 감당할 수 있게 됐다.
그 결과 저장과 계산의 분리가 표준이 됐다. 데이터는 항상 object storage에 있고, 계산 클러스터는 일할 때만 떴다가 일이 끝나면 사라진다. 계산 중간에 클러스터가 죽어도 데이터는 안전하므로, 언제 회수될지 모르는 spot 인스턴스도 쓸 수 있게 됐다.
BigQuery, Snowflake, Redshift 같은 클라우드 웨어하우스는 “클러스터 관리 자체를 하고 싶지 않다"는 요구에 대한 답이다. SQL만 던지면 저장, 계산, 최적화를 통째로 관리형으로 처리해 준다. 한 세대 전에는 DB에 다 넣을 수 없어서 Hadoop을 만들었는데, 저장과 계산을 분리한 구조 덕분에 웨어하우스가 빅데이터 규모를 다시 삼켰다.
자원 관리도 YARN에서 Kubernetes로 세대가 바뀌고 있다.
여기서 하나의 패턴이 보인다. 직접 운영하는 Hadoop 클러스터에서 관리형 클러스터(EMR, Dataproc)로, 그리고 클러스터라는 개념 자체가 숨은 웨어하우스로 옮겨 간다. 관리형이 먹는다. 관리형 전송이나 관리형 쿼리로 해결되는 일이라면 굳이 Spark 잡을 직접 띄울 이유가 없다.
스트리밍과 CDC: 배치 주기만큼 늦는 문제
배치는 아무리 잘해도 “한 시간 전” 또는 “하루 전” 데이터다. 이 지연을 없애려는 흐름이 스트리밍이다.
Kafka: 데이터의 중앙 배관
Kafka(2011, LinkedIn)는 이벤트를 도착 순서대로 쌓아 두는 분산 로그다. DB처럼 현재 상태를 저장하지 않고, 일어난 일의 순서 있는 기록을 저장한다.
본질은 생산자와 소비자의 분리다. 쓰는 쪽은 누가 읽는지 모르고, 읽는 쪽은 각자의 offset을 들고 자기 속도로 읽는다. 소비자가 죽어도 로그는 남아 있으니 이어서 읽으면 된다.
하나의 topic은 여러 partition으로 나뉘어 병렬로 처리된다. 같은 키는 같은 partition으로 가므로 키 단위 순서가 보장된다. Spark 셔플의 파티셔닝과 같은 아이디어가 저장 계층에 적용된 것이다.
Kafka Connect는 DB와 Kafka를 잇는 코드를 매번 짜지 않도록 만든 커넥터 프레임워크다. 설정만으로 source(DB에서 Kafka로)와 sink(Kafka에서 저장소로)를 붙인다. GCP의 Pub/Sub은 같은 배관의 관리형 버전이다. 관리형이 먹는다는 패턴이 배관에도 적용된 셈이다.
CDC: 운영 DB를 건드리지 않고 따라가기
CDC(Change Data Capture)는 DB의 변경 로그(MySQL binlog, MongoDB oplog나 Change Stream)를 실시간으로 흘려보내는 기법이다.
주기적인 전체 덤프는 무겁고 느리며, SELECT로 긁어 오면 운영 DB에 부하를 준다. 그런데 DB는 어차피 복제를 위해 모든 변경을 로그로 쓰고 있다. 그 로그를 읽으면 운영 부하 없이 모든 변경을 실시간으로 얻을 수 있다. 웨어하우스를 운영 DB의 replica처럼 만드는 기법이다.
Debezium이 사실상의 표준 구현체다. Kafka Connect의 source 커넥터로 돌면서 binlog를 Kafka 이벤트로 바꾼다.
CDC에서 풀어야 하는 어려움은 대체로 정해져 있다.
- 스냅샷과 로그의 이음새: 초기 전체 복사가 끝난 뒤 로그를 어디서부터 이어 읽을 것인가
- 스키마 변경 전파
- 삭제 처리(tombstone)
- 최종 저장소에서 merge하기: 같은 키의 마지막 상태를 만드는 일
아키텍처도 한 번 흔들렸다. 한때는 배치와 스트리밍을 이중으로 두는 람다 아키텍처가 유행했지만, 같은 로직을 두 번 짜는 고통 때문에 스트리밍 중심으로 통합하는 카파 아키텍처 쪽으로 기울었다.
처리 엔진: Flink, Spark, Beam
Flink는 이벤트가 오는 즉시 하나씩 처리하는 스트리밍 네이티브 엔진이다. 상태(state)와 체크포인트가 1급 개념이다. Spark는 배치 출신이라 스트리밍도 마이크로배치로 처리하므로, 아주 낮은 지연이 필요하면 Flink가 강하다. 두 엔진은 출발 방향이 반대다. Spark는 배치에서 스트리밍으로, Flink는 스트리밍에서 배치로 넓혀 왔다.
Beam은 배치와 스트리밍을 한 API로 쓰는 프로그래밍 모델이고, Dataflow는 그것을 돌려 주는 GCP의 관리형 러너다. 엔진 관리를 하고 싶지 않다는 요구의 스트리밍 버전이다.
Flink의 상태와 체크포인트는 Flink 시리즈에서 자세히 다룬다.
레이크하우스와 포맷 전쟁
Table format: 레이크에 테이블의 기본기를 돌려주기
Iceberg, Delta, Hudi는 데이터 레이크에 대한 반성에서 출발한다. S3에 Parquet 파일을 쌓기만 하면 싸고 유연하지만, 테이블로서의 기본기가 없다.
- 쓰다 만 파일이 보인다. 트랜잭션이 없기 때문이다.
- 행 하나를 고치려면 파일을 통째로 다시 써야 하고, 두 잡이 동시에 쓰면 서로 덮어쓴다.
- “이 테이블의 어제 상태"를 볼 수 없다.
- 파티션 디렉터리 구조(
dt=2026-07-27/)가 곧 스키마라서 바꾸기 어렵다.
Table format의 답은 파일들 위에 메타데이터 층을 얹는 것이다. “이 테이블의 v42는 이 파일 목록이다"라는 스냅샷을 관리하고, 커밋은 스냅샷 포인터를 새것으로 원자적으로 바꾸는 일이 된다. 그러면 다음을 얻는다.
- 트랜잭션: 읽는 쪽은 항상 완결된 스냅샷만 본다.
- time travel: v41을 그대로 읽으면 어제 상태가 된다.
- 스키마 진화: 컬럼 추가나 이름 변경이 메타데이터 수정으로 끝난다.
- 삭제와 수정: delete 파일을 얹고 읽을 때 합친다.
Hive metastore가 하던 테이블 목차 역할이 진화한 형태이고, 웨어하우스의 편함과 레이크의 개방성을 합친 것이 레이크하우스라는 구도다. Netflix에서 시작해 중립 표준을 지향한 Iceberg가 사실상 우세하고, Delta는 Databricks 진영의 포맷이다.
반대로 BigQuery 같은 웨어하우스는 트랜잭션, time travel, 스키마 진화를 이미 관리형으로 제공한다. 데이터가 웨어하우스 안에 있다면 별도의 table format이 필요 없고, Iceberg가 필요해지는 시점은 웨어하우스 밖의 파일에 테이블 수준 관리가 필요해질 때다.
Arrow: 메모리 위의 공용 컬럼 포맷
Parquet가 디스크 위의 컬럼 포맷이라면, Arrow는 메모리 위의 컬럼 포맷 표준이다.
엔진끼리 데이터를 주고받을 때마다 직렬화와 역직렬화를 반복하는 것은 분산 시스템의 숨은 비용이었다. Spark에서 pandas로 넘기는 것만 해도 JVM 객체를 바이트로 바꾸고 다시 Python 객체로 바꾸는 왕복이 일어난다. Arrow는 메모리에 데이터를 놓는 방식을 표준화해서, 같은 바이트를 변환 없이(zero-copy) 여러 엔진이 그대로 읽게 만든다.
Spark의 UnsafeRow와 문제의식이 같다. 둘 다 JVM 객체를 피하려 한다. 차이는 UnsafeRow가 Spark 내부용 행 지향 포맷이고, Arrow는 엔진 간 교환용 컬럼 지향 공용 표준이라는 점이다.
Arrow는 의외로 여러 곳에서 만난다. pyspark의 toPandas() 가속, BigQuery Storage Read API의 응답 형식, Spark Connect의 결과 전송, DuckDB와 Polars의 기본 포맷이 모두 Arrow다. 직접 만질 일은 적지만, 요즘 도구들이 서로 빨리 통하는 이유가 이것이다.
단일 머신의 반격
머신이 좋아지면서 웬만한 데이터는 다시 노트북 한 대에서 처리할 수 있게 됐다. DuckDB와 Polars가 그 흐름이다. “그거 정말 분산이 필요한 데이터인가?“라는 질문이 다시 유효해졌다. 스케일아웃으로 시작한 역사가 한 바퀴 돈 지점이다.
컬럼 지향 분석 엔진이 단일 노드와 분산 사이에서 어떤 선택을 하는지는 ClickHouse 시리즈에서 볼 수 있다.
역사에서 반복되는 패턴
- SQL은 항상 돌아온다. MapReduce 위에 Hive, RDD 위에 Spark SQL, 레이크 위에 Trino와 BigQuery가 얹혔다. 선언형이어야 엔진이 최적화할 수 있고, 사람들은 결국 SQL을 원한다.
- 저장과 계산은 점점 분리된다. HDFS에서는 한 몸이었고, object storage와 일회성 클러스터에서는 분리됐고, 웨어하우스에서는 계산이 아예 숨었다.
- 개념은 살아남고 구현은 바뀐다. 블록과 파티션, locality, 셔플, 컨테이너 자원 협상은 이름만 바꾸며 반복된다. 하나를 제대로 이해하면 다음 기술이 쉬워지는 이유다.
- 관리형이 먹는다. 운영 부담이 기술 선택을 결정한다. 관리형이 있으면 그것을 쓰고, 비어 있는 칸만 직접 만든다.
미니 사전
- Parquet, ORC: 컬럼 지향 파일 포맷. 필요한 컬럼만 읽을 수 있고 압축에 유리해 분석용 표준이 됐다.
- Avro, Protobuf: 스키마가 있는 직렬화 포맷. Kafka 이벤트와 RPC에 흔하다. 행 하나를 안전하게 주고받는 용도이고, Parquet는 수백만 행을 효율적으로 저장하는 용도다.
- Hive, metastore: HDFS 위 SQL의 원조. 본체는 밀려났지만 metastore(테이블 스키마와 위치의 목차)는 표준으로 살아남았다.
- Trino(구 Presto): 저장 계층 없이 여러 소스에 SQL을 날리는 분산 쿼리 엔진.
- dbt: 웨어하우스 안에서 SQL로 하는 변환(ELT의 T)을 버전, 테스트, 의존성까지 코드처럼 관리하는 도구. 적재 후 가공이 표준이 되면서 떴다.
- ZooKeeper: 분산 시스템의 합의와 조정(리더 선출 등)을 맡는 서비스. Kafka와 HBase가 썼고, 요즘은 각자 내장하는 추세다.