- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- Hadoop이 풀려고 했던 문제
- Hadoop의 전체 구성
- HDFS — 분산 파일 시스템
- YARN — 리소스 관리 계층
- MapReduce — 원조 처리 모델
- Hadoop 에코시스템
- 오브젝트 스토리지·클라우드·레이크하우스 시대
- 실무·운영에서 알아둘 점
- 흔한 오해와 함정
- 마치며
- 참고 자료
들어가며
한동안 "빅데이터"라는 단어는 곧 "Hadoop"과 동의어처럼 쓰였습니다. 2010년대 초중반, 수많은 기업이 온프레미스 데이터센터에 수십에서 수백 대의 서버로 구성된 Hadoop 클러스터를 구축했고, 데이터 엔지니어 채용 공고에는 거의 빠짐없이 Hadoop 경험이 요구사항으로 들어가 있었습니다.
그런데 2020년대에 들어서면서 분위기가 달라졌습니다. 클라우드 오브젝트 스토리지(S3, GCS, Azure Blob)가 사실상의 데이터 저장소 표준이 되었고, Spark가 연산 엔진의 주류가 되었으며, Iceberg/Delta/Hudi 같은 테이블 포맷을 기반으로 한 "레이크하우스(Lakehouse)" 아키텍처가 등장했습니다. 그래서 어떤 사람들은 "Hadoop은 죽었다"라고 말하기도 합니다.
이 글은 그 진위를 가리기 위한 글이 아닙니다. 오히려 Hadoop이 어떤 문제를 풀기 위해 어떻게 설계되었는지를 처음부터 다시 짚어보고, 그 설계 결정들이 오늘날 어떤 부분은 여전히 유효하고 어떤 부분은 시대에 뒤처졌는지를 차분하게 정리하는 데 목적이 있습니다. Hadoop을 제대로 이해하면, 그 위에 세워진 현대 데이터 스택의 거의 모든 개념(분산 스토리지, 데이터 지역성, 리소스 스케줄링, 셔플)이 더 선명하게 보이기 때문입니다.
이 글에서 다룰 내용은 다음과 같습니다.
- Hadoop이 등장한 배경과 핵심 설계 철학
- HDFS의 구조: NameNode, DataNode, 블록, 복제(replication)
- HDFS의 읽기/쓰기 경로를 다이어그램으로 추적
- YARN의 구조: ResourceManager, NodeManager, ApplicationMaster
- MapReduce의 동작 원리와 셔플(shuffle) 흐름
- Hive, Spark, HBase 등 에코시스템
- 오브젝트 스토리지/클라우드/레이크하우스 시대에 달라진 Hadoop의 위치
- 실무에서의 운영 포인트와 흔한 함정
참고: 이 글의 기술적 설명은 Apache Hadoop 공식 문서(hadoop.apache.org)를 기준으로 합니다. 버전에 따라 세부 동작이 다를 수 있으므로, 실제 운영 시에는 사용하는 배포판과 버전의 문서를 확인하시길 권합니다.
Hadoop이 풀려고 했던 문제
Hadoop의 기원은 구글이 2003년과 2004년에 발표한 두 편의 논문, 즉 GFS(Google File System)와 MapReduce 논문입니다. 당시 구글은 웹 전체를 크롤링하고 색인하기 위해 어마어마한 양의 데이터를 처리해야 했고, 이를 위해 비싼 고성능 서버 몇 대가 아니라 저렴한 범용 서버(commodity hardware) 수천 대를 묶어서 쓰는 방향을 택했습니다.
이 접근에는 두 가지 큰 전제가 깔려 있었습니다.
- 서버는 언제든 고장 난다. 수천 대를 운영하면 매일 몇 대씩 죽는 것이 정상이다. 따라서 장애는 예외가 아니라 일상이며, 시스템이 장애를 견디도록(fault-tolerant) 설계해야 한다.
- 데이터가 너무 크면 데이터를 연산이 있는 곳으로 옮기는 것보다, 연산을 데이터가 있는 곳으로 옮기는 편이 훨씬 싸다. 이것이 "데이터 지역성(data locality)" 개념입니다.
Hadoop은 이 두 가지 철학을 그대로 오픈소스로 구현한 것입니다. HDFS는 GFS에 대응하고, Hadoop MapReduce는 구글의 MapReduce에 대응합니다. 처음에는 더그 커팅(Doug Cutting)이 Nutch라는 검색 엔진 프로젝트의 일부로 시작했고, 이후 Apache 최상위 프로젝트로 독립했습니다.
핵심 설계 목표를 정리하면 다음과 같습니다.
┌──────────────────────────────────────────────────────────────┐
│ Hadoop 핵심 설계 목표 │
├──────────────────────────────────────────────────────────────┤
│ 1. 확장성(Scalability) │
│ 수평 확장 — 노드를 추가해서 용량/처리량을 늘린다. │
│ │
│ 2. 내결함성(Fault Tolerance) │
│ 장애는 정상 — 복제와 재시도로 데이터/작업을 보호한다. │
│ │
│ 3. 데이터 지역성(Data Locality) │
│ 연산을 데이터 가까이로 — 네트워크 전송 최소화. │
│ │
│ 4. 범용 하드웨어(Commodity Hardware) │
│ 값싼 서버 다수 — 비싼 단일 장비에 의존하지 않는다. │
│ │
│ 5. 처리량 우선(High Throughput, not Low Latency) │
│ 대용량 배치 처리에 최적 — 실시간 응답이 목표가 아니다. │
└──────────────────────────────────────────────────────────────┘
마지막 항목이 특히 중요합니다. Hadoop은 본질적으로 "큰 데이터를 한 번에 순차적으로 쭉 읽어서 배치로 처리"하는 데 최적화되어 있습니다. 작은 파일 수백만 개를 다루거나, 낮은 지연(low latency)으로 단건 조회를 하는 용도로는 처음부터 잘 맞지 않았습니다. 이 특성은 뒤에서 함정을 다룰 때 다시 등장합니다.
Hadoop의 전체 구성
Hadoop은 크게 세 개의 층으로 나눠서 이해하면 편합니다.
┌───────────────────────────────────────────────────────────────┐
│ 처리 계층 (Processing) │
│ MapReduce · Spark · Tez · Hive · Pig · Flink ... │
├───────────────────────────────────────────────────────────────┤
│ 리소스 관리 계층 (Resource Management) │
│ YARN — ResourceManager / NodeManager / ApplicationMaster │
├───────────────────────────────────────────────────────────────┤
│ 스토리지 계층 (Storage) │
│ HDFS — NameNode / DataNode / Block / Replication │
└───────────────────────────────────────────────────────────────┘
- 스토리지 계층은 HDFS가 담당합니다. 데이터를 분산 저장하고 복제로 보호합니다.
- 리소스 관리 계층은 YARN이 담당합니다. CPU/메모리 같은 자원을 여러 애플리케이션에 나눠줍니다.
- 처리 계층에는 다양한 엔진이 올라갑니다. 초기에는 MapReduce뿐이었지만, YARN 도입 이후 Spark, Tez 등 여러 엔진이 같은 클러스터를 공유하게 되었습니다.
이 세 계층의 분리는 Hadoop 2.x에서 YARN이 도입되면서 명확해졌습니다. Hadoop 1.x 시절에는 MapReduce가 스토리지를 제외한 모든 것(스케줄링 + 실행)을 담당했기 때문에, MapReduce가 아닌 다른 처리 방식을 쓰기가 어려웠습니다. YARN은 "리소스 관리"를 "처리 엔진"에서 분리해냄으로써 Hadoop을 단순한 MapReduce 플랫폼이 아니라 범용 분산 처리 운영체제처럼 만들었습니다.
이제 각 계층을 하나씩 깊게 들여다보겠습니다.
HDFS — 분산 파일 시스템
기본 개념: 블록과 복제
HDFS의 가장 근본적인 아이디어는 "큰 파일을 고정 크기의 블록(block)으로 쪼개서, 여러 노드에 분산 저장한다"는 것입니다. 기본 블록 크기는 오랫동안 64MB였다가 최근 배포판에서는 128MB가 일반적입니다(dfs.blocksize로 설정).
일반적인 로컬 파일 시스템의 블록(보통 4KB 수준)과 비교하면 엄청나게 큰데, 이유는 명확합니다. 블록이 작으면 NameNode가 관리해야 할 메타데이터 개수가 폭발하고, 디스크 시크(seek) 비용이 전체 전송 시간에서 차지하는 비중이 커집니다. 블록을 크게 잡으면 한 번 시크한 뒤 길게 순차 읽기를 할 수 있어 처리량이 좋아집니다.
파일: bigfile.log (380 MB), 블록 크기 128 MB
bigfile.log
┌──────────────┬──────────────┬──────────────┐
│ Block A │ Block B │ Block C │
│ 128 MB │ 128 MB │ 124 MB │
└──────────────┴──────────────┴──────────────┘
각 블록은 복제 계수(replication factor, 기본 3)만큼 여러 DataNode에 복제됩니다.
Block A ──▶ DataNode 1, DataNode 3, DataNode 5
Block B ──▶ DataNode 2, DataNode 3, DataNode 6
Block C ──▶ DataNode 1, DataNode 4, DataNode 6
복제 계수가 3이라는 것은, 같은 블록의 사본이 서로 다른 DataNode 3곳에 존재한다는 뜻입니다. 따라서 DataNode 한두 대가 죽어도 데이터는 유실되지 않습니다. 이 "복제를 통한 내결함성"이 HDFS의 핵심입니다.
NameNode와 DataNode
HDFS는 마스터-슬레이브(master-slave) 구조입니다.
┌─────────────────────────────────────────────────────────────┐
│ NameNode (마스터) │
│ - 파일 시스템 네임스페이스(디렉터리/파일 트리) 관리 │
│ - 각 파일이 어떤 블록들로 이루어졌는지 기록 │
│ - 각 블록이 어느 DataNode에 있는지 매핑 보관 │
│ - 메타데이터는 메모리에 상주 (빠른 조회를 위해) │
└─────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│DataNode1│ │DataNode2│ │DataNode3│ │DataNode4│ (슬레이브)
│ 블록 저장│ │ 블록 저장│ │ 블록 저장│ │ 블록 저장│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
- 실제 블록 데이터를 로컬 디스크에 저장
- 주기적으로 NameNode에 하트비트와 블록 리포트 전송
- NameNode는 메타데이터만 관리합니다. 실제 데이터 바이트는 갖고 있지 않습니다. 대신 "어떤 파일이 어떤 블록들로 구성되어 있고, 그 블록들이 어느 DataNode에 있는가"를 전부 알고 있습니다. 이 정보는 빠른 조회를 위해 메모리에 상주합니다.
- DataNode는 실제 블록 데이터를 자신의 로컬 디스크에 저장합니다. 그리고 주기적으로 NameNode에 하트비트(살아 있음 신호)와 블록 리포트(내가 가진 블록 목록)를 보냅니다.
NameNode가 메타데이터를 메모리에 올린다는 점은 중요한 함의를 가집니다. 파일이나 블록이 많아질수록 NameNode 메모리 사용량이 늘어나기 때문에, "작은 파일 수백만 개" 같은 워크로드는 NameNode에 큰 부담을 줍니다. 이것이 바로 악명 높은 "스몰 파일 문제(small files problem)"입니다.
NameNode의 영속성: FsImage와 EditLog
NameNode의 메타데이터는 메모리에 있지만, 재시작 시 복구할 수 있어야 합니다. 이를 위해 두 가지 디스크 구조를 사용합니다.
┌──────────────── NameNode 메타데이터 영속화 ────────────────┐
│ │
│ FsImage : 특정 시점의 파일 시스템 전체 스냅샷 │
│ EditLog : FsImage 이후 발생한 모든 변경 사항의 로그 │
│ │
│ 재시작 시 복구 절차: │
│ 1) FsImage 로드 (기준 스냅샷) │
│ 2) EditLog 재생(replay) (그 이후 변경 반영) │
│ 3) 메모리 상의 최신 메타데이터 완성 │
│ │
└─────────────────────────────────────────────────────────────┘
EditLog가 무한정 커지면 재시작이 느려지므로, 주기적으로 FsImage와 EditLog를 합쳐 새 FsImage를 만드는 작업(checkpointing)이 필요합니다. 이 작업을 예전에는 Secondary NameNode가 담당했고, HA(고가용성) 구성에서는 Standby NameNode가 담당합니다.
여기서 흔한 오해 하나를 짚고 가겠습니다. Secondary NameNode는 NameNode의 백업(대체)이 아닙니다. 이름과 달리 장애 조치(failover)를 수행하지 못하며, 단지 체크포인트를 도와주는 보조 역할일 뿐입니다. 진짜 고가용성은 아래에서 설명하는 HA 구성으로 달성합니다.
NameNode 고가용성(HA)
단일 NameNode 구조에서는 NameNode가 단일 장애점(SPOF, Single Point of Failure)이 됩니다. NameNode가 죽으면 클러스터 전체의 메타데이터에 접근할 수 없으므로 사실상 모든 작업이 멈춥니다. 이를 해결하기 위해 HDFS는 HA 구성을 제공합니다.
┌──────────────┐ 공유 EditLog ┌──────────────┐
│ Active │◀────────────────▶│ Standby │
│ NameNode │ (JournalNodes) │ NameNode │
└──────┬───────┘ └──────┬───────┘
│ │
│ DataNode들은 두 NameNode 모두에게 │
│ 하트비트와 블록 리포트를 전송 │
▼ ▼
┌───────────────────────────────────────────────┐
│ DataNode 1 · DataNode 2 · DataNode 3 · ... │
└───────────────────────────────────────────────┘
Active가 죽으면 → ZKFC + ZooKeeper가 감지 → Standby가 Active로 승격
- Active NameNode가 모든 클라이언트 요청을 처리합니다.
- Standby NameNode는 JournalNode들을 통해 공유되는 EditLog를 계속 재생하면서 Active와 동기화된 상태를 유지합니다.
- ZKFC(ZooKeeper Failover Controller)와 ZooKeeper가 Active의 생존을 감시하다가, 장애 발생 시 Standby를 새 Active로 자동 승격(failover)합니다.
이 과정에서 "스플릿 브레인(split-brain)", 즉 두 NameNode가 동시에 자신이 Active라고 믿는 상황을 막기 위한 펜싱(fencing) 메커니즘이 중요합니다. 잘못 설정하면 데이터 손상으로 이어질 수 있으므로 운영 시 주의가 필요합니다.
HDFS 쓰기 경로
이제 실제로 클라이언트가 HDFS에 파일을 쓸 때 어떤 일이 일어나는지 추적해 봅시다.
[HDFS 쓰기 경로]
Client
│ 1. create() 요청 — "이 경로에 파일 만들래"
▼
NameNode
│ - 권한/존재 여부 확인, 네임스페이스에 파일 항목 생성
│ 2. 첫 블록을 둘 DataNode 목록 반환 (복제 3 → 3개)
│ 예: DN1 ──▶ DN2 ──▶ DN3 (파이프라인)
▼
Client
│ 3. 데이터를 패킷 단위로 DN1에 전송
▼
┌─────────┐ 복제 파이프라인 ┌─────────┐ ┌─────────┐
│ DN1 │ ─── 패킷 전달 ───▶ │ DN2 │ ──────▶ │ DN3 │
│ 디스크↓ │ │ 디스크↓ │ │ 디스크↓ │
└─────────┘ └─────────┘ └─────────┘
│ │
│ 4. ack는 역방향으로: DN3 ──▶ DN2 ──▶ DN1 ──▶ Client │
▼
Client
│ 5. 블록이 다 차면 다음 블록 위해 2~4 반복
│ 6. close() — NameNode에 쓰기 완료 통보
▼
NameNode (메타데이터 최종 확정)
핵심은 복제가 "복제 파이프라인(replication pipeline)" 방식으로 일어난다는 점입니다. 클라이언트는 첫 번째 DataNode에만 데이터를 보내고, 그 DataNode가 두 번째로, 두 번째가 세 번째로 패킷을 흘려보냅니다. 이렇게 하면 클라이언트의 업로드 대역폭이 한 번만 쓰이면서도 3개의 복제본이 만들어집니다.
또 하나 중요한 것이 복제본 배치 정책(replica placement policy)입니다. 기본 정책은 대략 다음과 같습니다.
[기본 복제본 배치 (replication = 3, 랙 인식 기준)]
복제본 1 : 클라이언트가 있는 노드(또는 같은 랙의 임의 노드)
복제본 2 : 복제본 1과 다른 랙의 노드
복제본 3 : 복제본 2와 같은 랙의 다른 노드
목적:
- 같은 랙에 2개 → 랙 내부 네트워크로 빠른 쓰기
- 다른 랙에 1개 → 랙 전체 장애 시에도 데이터 보존
이 "랙 인식(rack awareness)" 정책은 쓰기 성능(같은 랙 안에서는 네트워크가 빠름)과 내구성(랙 전체가 죽어도 살아남음)을 절충한 결과입니다.
HDFS 읽기 경로
읽기는 상대적으로 단순합니다.
[HDFS 읽기 경로]
Client
│ 1. open() — "이 파일 읽을래"
▼
NameNode
│ 2. 파일의 블록 목록 + 각 블록의 DataNode 위치 반환
│ (클라이언트와 가까운 순서로 정렬해서 줌)
▼
Client
│ 3. 각 블록마다 가장 가까운 DataNode에서 직접 읽기
▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ DN (A) │ │ DN (B) │ │ DN (C) │ ← 블록별로 가까운 노드 선택
└─────────┘ └─────────┘ └─────────┘
│
│ 4. 만약 어떤 DataNode가 응답 없으면
│ → 다른 복제본을 가진 DataNode로 재시도
▼
Client (블록들을 이어 붙여 완전한 파일로)
여기서 NameNode는 "어디서 읽어라"라는 위치 정보만 줄 뿐, 실제 데이터는 클라이언트가 DataNode와 직접 주고받습니다. 즉 데이터 트래픽이 NameNode를 거치지 않습니다. 이것이 NameNode가 메타데이터에만 집중하면서도 클러스터 전체 처리량을 높게 유지할 수 있는 비결입니다.
또한 NameNode가 위치를 정렬해 줄 때 "클라이언트와 가까운(같은 노드 → 같은 랙 → 다른 랙)" 순서를 우선하므로, 가능하면 로컬 또는 같은 랙에서 읽게 됩니다. 이것이 읽기에서의 데이터 지역성입니다.
YARN — 리소스 관리 계층
왜 YARN이 필요했는가
Hadoop 1.x에서는 MapReduce가 두 가지 일을 동시에 했습니다. 하나는 클러스터 자원(슬롯)을 관리하고 작업을 스케줄링하는 일(JobTracker), 다른 하나는 실제 맵/리듀스 태스크를 실행하는 일이었습니다. 이 구조에는 문제가 있었습니다.
- JobTracker에 모든 부하가 몰려 확장성에 한계가 있었습니다(대략 4,000 노드 수준에서 병목).
- 클러스터가 오직 MapReduce 작업만 돌릴 수 있었습니다. Spark처럼 다른 방식으로 데이터를 처리하고 싶어도 같은 클러스터를 쓰기 어려웠습니다.
- 자원이 "맵 슬롯"과 "리듀스 슬롯"으로 정적으로 나뉘어, 비효율이 컸습니다.
YARN(Yet Another Resource Negotiator)은 이 "리소스 관리"와 "작업 실행"을 분리했습니다. 그 결과 같은 클러스터 위에서 MapReduce, Spark, Tez, Flink 등 여러 처리 엔진이 자원을 공유하며 공존할 수 있게 되었습니다.
YARN의 구성 요소
┌──────────────────────────────────────────────────────────────┐
│ ResourceManager (RM, 마스터) │
│ - Scheduler: 클러스터 자원을 애플리케이션에 할당 │
│ - ApplicationsManager: AM 시작/재시작 관리 │
└───────────────┬──────────────────────────────────────────────┘
│ 자원 할당 / 상태 보고
┌────────────┼────────────────────────┬───────────────┐
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Node- │ │ Node- │ │ Node- │ │ Node- │
│Manager │ │Manager │ ... │Manager │ │Manager │
│ (NM) │ │ (NM) │ │ (NM) │ │ (NM) │
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
컨테이너 컨테이너 컨테이너 컨테이너
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ App │ │ Task │ │ Task │ │ Task │
│Master│ │ │ │ │ │ │
└──────┘ └──────┘ └──────┘ └──────┘
- ResourceManager(RM)는 클러스터 전체의 마스터입니다. 내부적으로 Scheduler(자원 할당 담당)와 ApplicationsManager(ApplicationMaster 수명 관리 담당)로 나뉩니다.
- NodeManager(NM)는 각 노드에 하나씩 떠서, 그 노드의 자원(CPU/메모리)을 관리하고 컨테이너를 실행/감시합니다. NameNode-DataNode 관계와 비슷하게, RM-NM도 마스터-슬레이브 구조입니다.
- ApplicationMaster(AM)는 애플리케이션(잡)마다 하나씩 뜨는 "그 잡 전용 관리자"입니다. 잡에 필요한 자원을 RM에게 요청하고, 할당받은 컨테이너에서 실제 태스크를 실행/감시합니다.
- Container는 자원(예: 메모리 2GB + vCore 1개)의 묶음입니다. 실제 작업(태스크)은 컨테이너 안에서 실행됩니다.
여기서 ApplicationMaster의 존재가 YARN 설계의 핵심입니다. 클러스터 전체를 관리하는 RM은 잡 하나하나의 세부 사항(태스크 진행 상황, 재시도 등)에 관여하지 않습니다. 그 일은 잡마다 따로 뜨는 AM이 맡습니다. 이렇게 책임을 분산함으로써 RM의 부하를 줄이고 확장성을 확보합니다.
YARN 스케줄링 시퀀스
클라이언트가 잡을 제출했을 때 자원이 어떻게 협상되는지 순서대로 보겠습니다.
[YARN 잡 제출 및 실행 시퀀스]
Client RM NM(여러 노드) AM
│ │ │ │
│ 1. submitApplication │ │
│───────────────────▶│ │ │
│ │ 2. AM 컨테이너 할당 요청 │
│ │──────────────────▶│ │
│ │ │ 3. AM 컨테이너 │
│ │ │ 시작 │
│ │ │──────────────▶│
│ │ 4. AM 등록(register) │
│ │◀──────────────────────────────────│
│ │ 5. 태스크용 컨테이너 요청 │
│ │◀──────────────────────────────────│
│ │ 6. 컨테이너 할당 응답 │
│ │──────────────────────────────────▶│
│ │ │ 7. AM이 NM에 │
│ │ │ 태스크 실행 │
│ │ │ 지시 │
│ │ │◀──────────────│
│ │ │ 8. 태스크 실행 │
│ │ │ (컨테이너 안)│
│ 9. 진행 상황 조회 │ │ │
│◀───────────────────┼───────────────────┼───────────────│
│ │ 10. 완료 후 AM 등록 해제, 자원 반납 │
│ │◀──────────────────────────────────│
▼ ▼ ▼ ▼
흐름을 말로 풀면 이렇습니다.
- 클라이언트가 RM에 애플리케이션을 제출합니다.
- RM이 어느 NM에서 AM을 띄울지 정하고, 그 NM에 AM용 컨테이너를 띄우라고 지시합니다.
- AM이 시작되면 RM에 자신을 등록하고, 잡 수행에 필요한 태스크용 컨테이너를 RM에게 요청합니다.
- RM의 Scheduler가 자원 상황을 보고 컨테이너를 할당합니다.
- AM은 할당받은 컨테이너가 있는 NM들에게 실제 태스크를 실행하라고 지시합니다.
- 태스크가 컨테이너 안에서 돌고, AM이 진행 상황을 추적합니다.
- 잡이 끝나면 AM이 등록을 해제하고 자원을 반납합니다.
스케줄러의 종류
YARN의 Scheduler는 정책에 따라 여러 종류가 있습니다. 대표적으로 다음 세 가지입니다.
| 스케줄러 | 핵심 동작 | 적합한 상황 |
|---|---|---|
| FIFO Scheduler | 제출 순서대로 처리 | 단순 테스트, 단일 사용자 |
| Capacity Scheduler | 큐별로 용량을 미리 보장 | 여러 팀이 최소 자원을 보장받아야 할 때 |
| Fair Scheduler | 실행 중인 잡들에 자원을 공평하게 분배 | 다양한 잡이 자원을 고르게 나눠 쓰길 원할 때 |
대규모 멀티 테넌트 클러스터에서는 보통 Capacity Scheduler나 Fair Scheduler를 씁니다. 팀별/프로젝트별로 큐를 나누고, 각 큐에 최소 보장 용량과 최대 사용 한도를 설정해 자원 다툼을 통제합니다.
MapReduce — 원조 처리 모델
Map과 Reduce
MapReduce는 두 단계의 함수형 연산으로 데이터를 처리합니다.
- Map: 입력을 받아 (키, 값) 쌍의 중간 결과를 만든다.
- Reduce: 같은 키를 가진 값들을 모아서 집계/요약한다.
고전적인 단어 세기(word count) 예시를 보겠습니다.
// Mapper: 한 줄을 받아 각 단어에 대해 (word, 1) 방출
public class WordCountMapper
extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable ONE = new IntWritable(1);
private final Text word = new Text();
@Override
protected void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
String line = value.toString();
for (String token : line.split("\\s+")) {
if (!token.isEmpty()) {
word.set(token);
context.write(word, ONE);
}
}
}
}
// Reducer: 같은 단어에 대한 1들을 모두 더함
public class WordCountReducer
extends Reducer<Text, IntWritable, Text, IntWritable> {
private final IntWritable result = new IntWritable();
@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable v : values) {
sum += v.get();
}
result.set(sum);
context.write(key, result);
}
}
// Driver: 잡 설정과 제출
public class WordCountDriver {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCountDriver.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setCombinerClass(WordCountReducer.class); // 맵 단계 부분 집계
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
셔플과 정렬
MapReduce에서 가장 무겁고, 가장 자주 성능 병목이 되는 단계가 바로 Map과 Reduce 사이의 셔플(shuffle)입니다. Map의 출력을 키 기준으로 재분배해서, 같은 키가 같은 Reducer로 모이게 하는 과정입니다.
[MapReduce 전체 데이터 흐름 (셔플 포함)]
입력 (HDFS 블록들)
┌────────┐ ┌────────┐ ┌────────┐
│ Split1 │ │ Split2 │ │ Split3 │
└───┬────┘ └───┬────┘ └───┬────┘
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Map 1 │ │ Map 2 │ │ Map 3 │ ← 각 Map이 (키,값) 방출
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
│ [파티셔닝 + 정렬 + (선택)컴바이너]
│ │ │
└────┬─────┴─────┬────┘
│ S H U F F L E │ ← 네트워크로 키 기준 재분배
┌────┴─────┐ ┌────┴─────┐
▼ ▼ ▼ ▼
┌────────────────┐ ┌────────────────┐
│ Reduce 1 │ │ Reduce 2 │ ← 같은 키는 같은 Reducer로
│ (키 A,B 담당) │ │ (키 C,D 담당) │
└───────┬────────┘ └───────┬────────┘
▼ ▼
┌────────┐ ┌────────┐
│ 출력 1 │ │ 출력 2 │ ← HDFS에 결과 기록
└────────┘ └────────┘
셔플 단계에서 일어나는 일을 더 세분화하면 다음과 같습니다.
[Map 측 → Reduce 측 셔플 상세]
Map 측:
1. map() 출력이 메모리 버퍼에 쌓임
2. 버퍼가 차면 디스크로 스필(spill) — 이때 파티션별로 정렬
3. 여러 스필 파일을 하나로 머지(merge)
4. (컴바이너 설정 시) 맵 쪽에서 부분 집계해 데이터량 축소
Reduce 측:
5. 각 Map으로부터 자기 파티션 데이터를 가져옴(fetch, 네트워크)
6. 가져온 데이터를 키 기준으로 머지+정렬
7. reduce() 호출 — 같은 키의 값들을 한 번에 처리
셔플은 디스크 I/O와 네트워크 I/O를 모두 발생시키기 때문에 비용이 큽니다. 그래서 컴바이너(combiner)로 맵 단계에서 미리 부분 집계를 해 데이터량을 줄이는 것이 중요합니다. 단, 컴바이너는 교환법칙과 결합법칙이 성립하는 연산(예: 합계, 카운트)에만 안전하게 적용할 수 있습니다. 평균처럼 그렇지 않은 경우에는 잘못된 결과가 나올 수 있으니 주의해야 합니다.
MapReduce의 한계
MapReduce는 견고하지만 느립니다. 가장 큰 이유는 각 단계의 중간 결과를 디스크(HDFS)에 쓴다는 점입니다. 여러 단계로 이루어진 복잡한 파이프라인을 MapReduce로 짜면, 단계마다 디스크에 쓰고 다시 읽는 비용이 누적됩니다. 반복(iteration)이 많은 머신러닝 알고리즘에서는 이 비용이 치명적이었습니다.
이 한계가 바로 Spark가 등장하고 빠르게 주류가 된 배경입니다.
Hadoop 에코시스템
Hadoop은 단일 제품이 아니라 거대한 에코시스템입니다. HDFS와 YARN 위에 다양한 도구들이 쌓여 있습니다.
┌──────────────────────────────────────────────────────────────┐
│ SQL / 질의: Hive, Impala, Presto/Trino │
│ 스크립트: Pig │
│ 처리 엔진: MapReduce, Spark, Tez, Flink │
│ NoSQL: HBase (HDFS 위의 분산 키-값/컬럼 스토어) │
│ 수집: Sqoop(RDB↔HDFS), Flume/Kafka(스트림) │
│ 조정: ZooKeeper (분산 코디네이션) │
│ 워크플로: Oozie, Airflow │
├──────────────────────────────────────────────────────────────┤
│ YARN (리소스 관리) │
├──────────────────────────────────────────────────────────────┤
│ HDFS (분산 스토리지) │
└──────────────────────────────────────────────────────────────┘
특히 자주 언급되는 세 가지를 짚어보겠습니다.
Hive — HDFS 위의 SQL
Hive는 HDFS에 저장된 데이터를 SQL로 질의할 수 있게 해주는 데이터 웨어하우스 도구입니다. 사용자가 SQL(정확히는 HiveQL)을 작성하면, Hive가 이를 내부적으로 MapReduce(또는 Tez/Spark) 잡으로 변환해 실행합니다.
-- 외부 테이블 정의: HDFS 경로의 데이터를 테이블처럼 다룸
CREATE EXTERNAL TABLE access_log (
ip STRING,
ts STRING,
url STRING,
status INT
)
ROW FORMAT DELIMITED FIELDS TERMINATED BY '\t'
STORED AS TEXTFILE
LOCATION '/data/access_log/';
-- 상태 코드별 집계
SELECT status, COUNT(*) AS cnt
FROM access_log
GROUP BY status
ORDER BY cnt DESC;
Hive의 등장은 큰 의미가 있었습니다. Java로 MapReduce를 직접 짤 줄 모르는 수많은 분석가와 데이터 사용자들이 익숙한 SQL로 Hadoop의 데이터에 접근할 수 있게 되었기 때문입니다. 또한 Hive Metastore(테이블 스키마/위치 메타데이터를 관리하는 저장소)는 이후 Spark, Presto/Trino 등 다른 엔진에서도 표준처럼 공유되어, 데이터 카탈로그의 사실상 표준이 되었습니다.
Spark — 인메모리 처리 엔진
Spark는 MapReduce의 디스크 중심 한계를 극복하기 위해 등장했습니다. 핵심 아이디어는 중간 결과를 가능하면 메모리에 유지하고, RDD(이후 DataFrame/Dataset)라는 추상화 위에서 DAG(방향성 비순환 그래프) 형태의 실행 계획을 구성하는 것입니다.
| 항목 | MapReduce | Spark |
|---|---|---|
| 중간 데이터 | 매 단계 디스크에 기록 | 가능하면 메모리에 유지 |
| 프로그래밍 모델 | Map/Reduce 두 단계 강제 | DAG 기반 유연한 연산 |
| 반복 연산 | 매우 비효율 | 적합 (ML 등) |
| 지연 | 높음 | 상대적으로 낮음 |
| 적합 용도 | 단순 대용량 배치 | 복잡한 파이프라인, ML, 인터랙티브 |
중요한 점은 Spark가 Hadoop을 대체한다기보다, Hadoop의 일부(MapReduce)를 대체한다는 것입니다. Spark는 YARN 위에서 실행될 수 있고, HDFS를 데이터 소스로 쓸 수 있습니다. 즉 Spark는 처리 계층의 새 강자이지, 스토리지/리소스 관리 계층까지 통째로 대체하는 것은 아닙니다(물론 클라우드에서는 이 구도가 또 달라집니다. 뒤에서 다룹니다).
HBase — HDFS 위의 NoSQL
HDFS는 큰 파일을 순차적으로 읽고 쓰는 데 최적화되어 있어, 단건 랜덤 읽기/쓰기에는 부적합합니다. HBase는 이 빈틈을 메우는 분산 컬럼 지향 NoSQL 데이터베이스로, 구글의 Bigtable 논문을 본떠 만들어졌습니다. HDFS 위에 올라가지만, 랜덤 액세스와 낮은 지연의 단건 조회를 가능하게 합니다.
[HBase의 위치]
애플리케이션 (낮은 지연 단건 조회 필요)
│
▼
┌──────────┐
│ HBase │ ← 랜덤 읽기/쓰기, 행 단위 접근
└────┬─────┘
▼
┌──────────┐
│ HDFS │ ← 실제 데이터 파일(HFile) 영속 저장
└──────────┘
오브젝트 스토리지·클라우드·레이크하우스 시대
여기까지가 "고전적 Hadoop"의 모습입니다. 이제 2020년대의 변화를 이야기할 차례입니다. 결론부터 말하면, Hadoop의 핵심 아이디어는 살아남았지만 그 구현체(특히 HDFS)의 역할은 클라우드에서 크게 줄었습니다.
가장 큰 변화: 스토리지와 컴퓨트의 분리
고전적 Hadoop의 핵심 전제는 "데이터 지역성"이었습니다. 즉 연산을 데이터가 있는 노드로 보내 네트워크를 아끼는 것입니다. 이를 위해 스토리지(HDFS)와 컴퓨트(MapReduce/YARN)가 같은 물리 노드에 함께 있어야 했습니다(co-location).
그런데 클라우드에서는 이야기가 달라집니다.
- 네트워크가 매우 빨라졌습니다. 데이터센터 내부 대역폭이 충분히 커서, "데이터를 옮기는 비용"이 예전만큼 크지 않습니다.
- 오브젝트 스토리지(S3 등)가 사실상 무한대로 싸고 내구성 높은 저장소를 제공합니다. 가용성/내구성을 클라우드 사업자가 책임집니다.
- 스토리지와 컴퓨트를 분리하면 각각을 독립적으로 확장할 수 있습니다. 데이터는 계속 쌓아두되, 연산이 필요할 때만 컴퓨트를 켜고 끄면 됩니다.
[고전적 Hadoop] [클라우드/레이크하우스]
┌──────────────┐ 컴퓨트(필요할 때만)
│ 노드 = 스토리지 │ ┌──────┐ ┌──────┐ ┌──────┐
│ + 컴퓨트 │ (co-location) │Spark │ │Trino │ │Flink │
│ HDFS + YARN │ └───┬──┘ └───┬──┘ └───┬──┘
└──────────────┘ │ │ │
데이터 지역성 중심 └────────┼────────┘
스토리지·컴퓨트 결합 ▼
┌────────────────┐
│ 오브젝트 스토리지 │
│ S3 / GCS / ... │ (분리)
└────────────────┘
스토리지·컴퓨트 독립 확장
이 분리가 클라우드 데이터 아키텍처의 핵심 변화입니다. 그리고 이 구도에서 HDFS의 자리가 오브젝트 스토리지로 대체되는 경우가 많아졌습니다.
HDFS vs 오브젝트 스토리지
| 항목 | HDFS | 오브젝트 스토리지 (S3 등) |
|---|---|---|
| 운영 주체 | 직접 운영(NameNode/DataNode) | 클라우드 사업자 관리형 |
| 확장 | 노드 추가, NameNode 메모리 제약 | 사실상 무제한 |
| 비용 모델 | 서버/디스크 고정 비용 | 사용량 기반(저장+요청) |
| 일관성 | 강한 일관성 | 강한 일관성(현대 S3 기준) |
| 작은 파일 | NameNode 부담(스몰 파일 문제) | 상대적으로 자유로움 |
| 데이터 지역성 | 있음(같은 노드/랙) | 없음(네트워크 경유) |
| rename/디렉터리 | 빠른 메타 연산 | 비싸거나 원자적이지 않을 수 있음 |
| 적합 환경 | 온프레미스, 고정 워크로드 | 클라우드, 탄력적 워크로드 |
오브젝트 스토리지가 만능은 아닙니다. 가장 유명한 차이는 "디렉터리 이름 변경(rename)"이 HDFS에서는 빠른 메타데이터 연산인 반면, 오브젝트 스토리지에서는 사실상 복사+삭제라서 느리고 원자적이지 않을 수 있다는 점입니다. 많은 데이터 파이프라인이 "임시 디렉터리에 쓰고 마지막에 rename으로 커밋"하는 패턴에 의존하기 때문에, 이 차이는 테이블 포맷의 등장 배경이 됩니다.
테이블 포맷과 레이크하우스
오브젝트 스토리지는 그냥 "파일 덩어리"일 뿐, 테이블이라는 개념을 모릅니다. 트랜잭션, 스키마 변경, 시간 여행(time travel), 동시 쓰기 같은 기능을 주지 않습니다. 이 빈틈을 메우기 위해 등장한 것이 Apache Iceberg, Delta Lake, Apache Hudi 같은 "오픈 테이블 포맷"입니다.
이들은 오브젝트 스토리지 위의 파일들을 "테이블"로 추상화하고, 메타데이터 계층을 통해 ACID 트랜잭션, 스키마 진화, 스냅샷 기반 시간 여행 등을 제공합니다. 그 결과 데이터 레이크의 유연성(아무 데이터나 싸게 저장)과 데이터 웨어하우스의 신뢰성(ACID, 스키마)을 합친 "레이크하우스(Lakehouse)" 아키텍처가 가능해졌습니다.
[레이크하우스 계층 구조]
┌──────────────────────────────────────────────┐
│ 질의/처리: Spark, Trino, Flink, Dremio ... │
├──────────────────────────────────────────────┤
│ 테이블 포맷: Iceberg / Delta / Hudi │
│ - ACID 트랜잭션 - 스키마 진화 - 시간 여행 │
├──────────────────────────────────────────────┤
│ 파일 포맷: Parquet / ORC (컬럼 지향) │
├──────────────────────────────────────────────┤
│ 스토리지: 오브젝트 스토리지(S3/GCS) 또는 HDFS │
└──────────────────────────────────────────────┘
흥미로운 점은 이 스택의 여러 조각이 Hadoop 에코시스템에서 비롯되었다는 사실입니다. 컬럼 지향 파일 포맷인 Parquet와 ORC는 모두 Hadoop 생태계에서 태어났고, 테이블 포맷의 메타데이터 관리는 Hive Metastore의 한계를 극복하려는 시도에서 출발했습니다. 즉 레이크하우스는 Hadoop과 단절된 것이 아니라, Hadoop이 풀던 문제를 클라우드 환경에 맞게 재설계한 결과에 가깝습니다.
Hadoop 시대 vs 레이크하우스 시대
| 관점 | Hadoop 시대 (2010년대) | 레이크하우스 시대 (2020년대) |
|---|---|---|
| 스토리지 | HDFS (직접 운영) | 오브젝트 스토리지 (관리형) |
| 스토리지-컴퓨트 | 결합(co-location) | 분리 |
| 주 연산 엔진 | MapReduce | Spark, Trino, Flink |
| 테이블/트랜잭션 | Hive Metastore | Iceberg/Delta/Hudi |
| 자원 관리 | YARN | Kubernetes(쿠버네티스)도 부상 |
| 확장 단위 | 노드(스토리지+컴퓨트 동시) | 스토리지/컴퓨트 독립 |
| 운영 부담 | 높음(직접 클러스터 운영) | 상대적으로 낮음(관리형) |
여기서 YARN의 자리도 흔들리고 있다는 점을 주목할 만합니다. 클라우드 네이티브 환경에서는 자원 스케줄링을 Kubernetes가 담당하는 경우가 늘고 있습니다. Spark도 YARN뿐 아니라 Kubernetes 위에서 직접 실행될 수 있습니다.
그래서 Hadoop은 죽었는가
"Hadoop이 죽었다"는 말은 절반만 맞습니다. 더 정확히 표현하면 이렇습니다.
- HDFS를 직접 운영하는 온프레미스 클러스터의 신규 구축은 분명히 줄었습니다. 클라우드를 쓴다면 오브젝트 스토리지가 더 합리적인 선택인 경우가 많습니다.
- MapReduce는 사실상 신규 개발에서 거의 쓰이지 않습니다. Spark 등으로 대체되었습니다.
- 하지만 Hadoop이 정립한 개념들(분산 스토리지, 복제, 데이터 지역성, 리소스 스케줄링, 셔플, 컬럼 포맷, 메타스토어)은 현대 데이터 스택의 토대로 그대로 살아 있습니다.
- 또한 데이터 주권/규제, 비용, 보안 등의 이유로 여전히 대규모 온프레미스 Hadoop 클러스터를 운영하는 조직이 적지 않습니다. 이런 환경에서 Hadoop 지식은 여전히 실무적으로 중요합니다.
요컨대 Hadoop은 "특정 제품"으로서는 정점을 지났지만, "아이디어와 어휘"로서는 현대 데이터 엔지니어링의 공용어로 남아 있습니다.
실무·운영에서 알아둘 점
스몰 파일 문제
앞서 언급한 NameNode의 메모리 제약 때문에, HDFS에서는 작은 파일이 많아지는 것을 경계해야 합니다. NameNode는 파일/디렉터리/블록 하나당 일정량의 메모리를 소비하므로, 1KB 파일 1억 개는 1GB 파일 1억 개와 비슷한 메타데이터 부담을 주면서도 처리 효율은 훨씬 나쁩니다.
대표적인 완화책은 다음과 같습니다.
- 작은 파일들을 큰 파일로 합치는 컴팩션(compaction) 잡을 주기적으로 돌리기
- HAR(Hadoop Archive)나 SequenceFile 같은 컨테이너 포맷으로 묶기
- 수집 단계에서 배치 크기를 키워 애초에 작은 파일이 덜 생기게 하기
데이터 포맷 선택
같은 데이터라도 어떤 파일 포맷으로 저장하느냐에 따라 성능과 비용이 크게 달라집니다.
| 포맷 | 특징 | 적합한 경우 |
|---|---|---|
| 텍스트(CSV/JSON) | 사람이 읽기 쉬움, 비효율적 | 임시/디버깅, 외부 연동 |
| Avro | 행 지향, 스키마 진화 우수 | 수집/스트리밍, 전체 행 읽기 |
| Parquet | 컬럼 지향, 압축/스캔 효율 | 분석 질의(특정 컬럼만 읽기) |
| ORC | 컬럼 지향, Hive와 궁합 좋음 | Hive 기반 분석 |
분석 워크로드에서는 컬럼 지향 포맷(Parquet/ORC)이 거의 항상 유리합니다. 질의가 필요한 컬럼만 읽고, 컬럼 단위로 압축이 잘 되며, 술어 푸시다운(predicate pushdown)으로 불필요한 데이터 스캔을 줄일 수 있기 때문입니다.
복제 계수와 비용
복제 계수 3은 안전하지만 저장 비용이 3배입니다. 매우 큰 콜드 데이터(거의 안 읽는 오래된 데이터)에 대해서는 HDFS의 이레이저 코딩(Erasure Coding)을 고려할 수 있습니다. 이레이저 코딩은 복제 대신 패리티를 사용해, 비슷한 내구성을 약 1.5배 정도의 저장 오버헤드로 달성합니다. 다만 인코딩/디코딩 연산 비용이 있고 복구가 느릴 수 있으므로, 자주 읽는 핫 데이터보다는 콜드 데이터에 적합합니다.
리소스 튜닝
YARN 환경에서 잡이 느리거나 자꾸 죽는다면 다음을 점검합니다.
[흔한 점검 포인트]
- 컨테이너 메모리( yarn.scheduler.maximum-allocation-mb )가
태스크에 충분한가? OutOfMemory로 컨테이너가 킬되지 않는가
- 맵/리듀스 태스크 수가 적절한가 (너무 적으면 병렬성 부족,
너무 많으면 스케줄링 오버헤드)
- 데이터 스큐(skew): 특정 키에 값이 몰려 한 Reducer만 오래 도는가
- 셔플량이 과도하지 않은가 (컴바이너/파티셔너로 줄일 수 있는지)
- 큐 설정이 적절한가 (특정 큐가 자원을 독식하는지)
특히 데이터 스큐는 분산 처리에서 가장 흔하고 까다로운 성능 문제입니다. 키 분포가 한쪽으로 치우치면 일부 태스크만 과부하가 걸려 전체 잡이 그 태스크를 기다리게 됩니다. 솔트(salt) 키를 추가하거나 파티셔너를 손보는 식으로 분산을 고르게 만드는 것이 해법입니다.
흔한 오해와 함정
마지막으로 Hadoop을 둘러싸고 자주 보이는 오해들을 정리하겠습니다.
-
"Hadoop = 빠르다"는 오해. Hadoop은 빠른 것이 아니라, 큰 데이터를 처리량 좋게 다루도록 설계되었습니다. 단건 저지연 응답이 목표라면 Hadoop은 잘못된 도구입니다.
-
"Secondary NameNode가 백업이다"라는 오해. Secondary NameNode는 체크포인트 보조이지 장애 조치를 하지 않습니다. 진짜 고가용성은 HA(Active/Standby) 구성으로 얻습니다.
-
"Spark가 Hadoop을 통째로 대체한다"는 오해. Spark는 처리 엔진(MapReduce)을 대체할 뿐, 스토리지(HDFS)나 리소스 관리(YARN)까지 통째로 대체하는 것은 아닙니다. 다만 클라우드에서는 오브젝트 스토리지와 Kubernetes 조합이 이 둘의 역할을 대신하기도 합니다.
-
"오브젝트 스토리지로 옮기면 무조건 좋다"는 오해. rename 비용, 일관성 모델, 메타데이터 연산 특성 등에서 HDFS와 다르므로, 테이블 포맷(Iceberg/Delta/Hudi)과 함께 설계하지 않으면 오히려 함정에 빠질 수 있습니다.
-
"작은 파일은 많아도 괜찮다"는 오해. NameNode 메모리와 처리 효율 모두에 악영향을 줍니다. 컴팩션을 운영 루틴에 포함시켜야 합니다.
-
"복제 3은 항상 정답"이라는 오해. 핫 데이터에는 합리적이지만, 거의 안 읽는 대용량 콜드 데이터에는 이레이저 코딩이 더 경제적일 수 있습니다.
마치며
Hadoop은 "장애가 일상인 값싼 서버 수천 대로 거대한 데이터를 안전하고 효율적으로 처리한다"는 하나의 큰 질문에 대한 답이었습니다. 그 답의 구체적 구현체(HDFS, MapReduce)는 클라우드와 오브젝트 스토리지, 레이크하우스의 등장으로 상당 부분 다른 도구에 자리를 내주었습니다.
하지만 그 답이 정립한 개념들은 여전히 살아 있습니다. 데이터를 쪼개서 분산하고 복제로 보호한다는 발상, 연산을 데이터 가까이로 보낸다는 발상, 리소스 관리와 실행을 분리한다는 발상, 셔플로 키를 재분배한다는 발상은 오늘날 우리가 쓰는 거의 모든 분산 데이터 시스템의 바탕에 깔려 있습니다.
그래서 "Hadoop을 배워야 하느냐"는 질문에 저는 이렇게 답하고 싶습니다. 특정 제품으로서의 Hadoop을 새로 도입할 일은 줄었지만, Hadoop이 가르쳐 준 사고방식을 이해하는 것은 현대 데이터 엔지니어에게 여전히 큰 자산입니다. 레이크하우스든, 클라우드 네이티브 스택이든, 그 뿌리에는 Hadoop이 먼저 부딪히고 풀어낸 문제들이 있기 때문입니다.
참고 자료
- Apache Hadoop 공식 사이트: https://hadoop.apache.org/
- HDFS Architecture (HDFS Design): https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html
- Apache Hadoop YARN: https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html
- MapReduce Tutorial: https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html
- HDFS High Availability: https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSHighAvailabilityWithQJM.html
- Apache Hive: https://hive.apache.org/
- Apache Spark Documentation: https://spark.apache.org/docs/latest/
- Apache HBase Reference Guide: https://hbase.apache.org/book.html
- Apache Iceberg: https://iceberg.apache.org/
- Apache Parquet: https://parquet.apache.org/