1. Why Spark?

단일 머신의 한계에서 분산 컴퓨팅까지

  • Haram Lee
  • 2026-08-16
  • studies / Topics / Spark

1.1 The Limits of Single-Machine Processing

  • 초기에는 단일 머신에서 데이터를 읽고 계산하면 충분했음
flowchart TD
  storage["Storage"] --> machine["Single Machine"]
  machine --> cpu["CPU"]
  machine --> memory["Memory"]
  machine --> disk["Disk"]
  • 데이터와 연산량의 증가로 한 머신의 자원이 병목이 되는 상황

    • CPU, memory, disk~
    • 데이터가 메모리보다 크다고 단일 머신에서 처리 자체가 불가능한 것은 아님. 디스크 기반 처리나 데이터 베이스를 사용할 수 있지만, 문제는 데이터가 커질수록 처리 시간이 길어지고 scale-up에도 비용과 물리적 한계가 생긴다는 것
      • 값싼 서버로 scale-out하는 방식을 채택
    • → 새로운 문제
    • 어떻게 데이터를 나눌 것인가?
      어느 머신에 어떤 작업을 보낼 것인가?
      머신이 고장 나면 어떻게 복구할 것인가?
      머신 사이의 데이터를 어떻게 이동할 것인가?
    • → 분산 작업을 자동으로 분할, 실행, 복구할 수 있는 프로그래밍 모델이 필요

1.2 Data Processing Before Spark

  • MapReduce
    • concept: map과 reduce 함수만 정의하면 런타임이 입력 분할, 작업 스케줄링, 장애 처리와 통신까지 담당하도록

    • HDFS → 여러 머신에 데이터를 분할/복제 저장 → MapReduce가 데이터를 분산 처리하는 구조

flowchart TD
  input["Input Data"] --> map1["Map Task 1"]
  input --> map2["Map Task 2"]
  input --> map3["Map Task 3"]
  map1 --> shuffle["Shuffle and Sort"]
  map2 --> shuffle
  map3 --> shuffle
  shuffle --> reduce1["Reduce Task 1"]
  shuffle --> reduce2["Reduce Task 2"]
  reduce1 --> output["Output"]
  reduce2 --> output
- → 세부 구현을 직접 작성하지 않아도 됨

1.3 The Limitations of Hadoop MapReduce

  • MapReduce도 데이터를 한 번 읽어서 변환/결과를 저장하는 단방향 배치 처리에는 효율적
  1. 반복 작업: 머신러닝/그래프 알고리즘 등
flowchart LR
  read1["Data Read"] --> compute1["Iteration 1<br/>Compute"] --> write1["Result Write"]
  write1 --> read2["Result Read"] --> compute2["Iteration 2<br/>Compute"] --> write2["Result Write"]
  write2 --> read3["Result Read"] --> compute3["Iteration 3<br/>Compute"] --> write3["Result Write"]
  • 각 iteration을 별도의 Job으로 구성하면 그 사이의 결과를 HDFS같은 외부 안정적인 저장소에 기록하고 다음 Job에서 읽는 방식
    → 여러 Job이 데이터를 공유하려면 중간 결과를 안정적인 외부 저장소에 물질화하는 방식에 의존
flowchart TD
  hdfs1["HDFS"] -- "read" --> job1["MapReduce Job 1"]
  job1 -- "write" --> hdfs2["HDFS"]
  hdfs2 -- "read" --> job2["MapReduce Job 2"]
  job2 -- "write" --> hdfs3["HDFS"]
  1. 대화형 분석: 사용자가 같은 데이터에 여러 질문을 던져 질의하는 경우

    sql
    SELECT COUNT(*) FROM logs WHERE status = 500;
    
    
    SELECT service, COUNT(*)
    FROM logs
    GROUP BY service;
    
    
    SELECT *
    FROM logs
    WHERE latency > 1000;
  • 각 쿼리가 별도의 MapReduce Job으로 실행되고 원본 데이터를 다시 읽는다면, 사용자는 쿼리마다 긴 시작 시간과 디스크 읽기를 기다려야 함

1.4 Why Distributed Computing?

  • 모두가 아는 세가지
  1. Parallelism
  2. Scalability
  3. Fault Tolerance

장점

  • 병렬 처리
  • 수평 확장
  • 장애 허용

비용

  • 네트워크 통신
  • 작업 조율
  • 데이터 직렬화
  • Shuffle
  • Data Skew

1.5 How Spark Approaches Distributed Processing

  • 2009년 개발 시작해서 2010년 공개
  • 여러 연산에서 반복적으로 사용하는 working set을 어떻게 효율적으로 유지할 것인가?

RDD는 다음 정보를 함께 표현함.

  • 여러 머신에 파티셔닝된 불변 데이터 집합
  • 데이터를 만드는 연산 관계
  • 손실된 파티션을 복구하는 정보
  1. Partitioning: 데이터를 파티션으로 나눔
  • 각 파티션은 서로 다른 Executor에서 병렬로 처리할 수 있음

예를 들어 하나의 RDD가 Partition 0부터 Partition 3까지 네 개의 파티션으로 나뉘면, 각 파티션을 서로 다른 Executor에서 동시에 처리할 수 있음.

  1. DAG: 여러 Transformation의 의존 관계를 DAG로 표현
flowchart TD
  read["Read"] --> filter["Filter"]
  filter --> map["Map"]
  map --> join["Join"]
  other["Read another dataset"] --> join
  join --> aggregate["Aggregate"]
  aggregate --> write["Write"]
  1. Lazy Execution: 연산을 지연 실행
  • 바로 실행하지 않고 연산 관계만 기록 / 실제 action이 호출될 때 실제 작업을 실행
python
result = (
    data
    .filter(...)
    .map(...)
)
  1. 필요한 데이터를 재사용
  • 같은 데이터를 여러 Action이나 반복 연산에서 사용한다면 계산된 파티션을 Executor 메모리에 보관할 수 있음
flowchart TD
  source["Source"] --> transform["Expensive Transformation"]
  transform --> cached["Cached RDD"]
  cached --> analysisA["Analysis A"]
  cached --> analysisB["Analysis B"]
  cached --> analysisC["Analysis C"]

1.6 Why Spark Is Fast

① 계산을 병렬화한다

  • 데이터를 여러 파티션으로 나누고 여러 CPU core에서 병렬처리
text
Partition 0 → Task 0 → Executor A
Partition 1 → Task 1 → Executor B
Partition 2 → Task 2 → Executor C

② 불필요한 디스크 I/O와 재계산을 줄인다

  • 반복해서 사용하는 데이터는 캐싱
text
MapReduce-style

Read → Compute → Write
Read → Compute → Write
Read → Compute → Write
text
Spark-style

Read → Compute → Cache
                 ├→ Compute
                 ├→ Compute
                 └→ Compute

③ 여러 연산을 하나의 실행 흐름으로 연결한다

  • map → filter → map처럼 Shuffle이 필요 없는 연산은 한 Task 안에서 이어서 처리하기: Pipelining
  • 현대 Spark에서는 실행 계획도 최적화한다
    • 오늘날 주로 사용하는 DataFrame과 Spark SQL은 데이터 스키마와 연산 구조를 알고 있음
    • Spark SQL은 필요한 컬럼만 읽는 컬럼 가지치기, 조인 전략 선택, 통계 기반 최적화, 실행 중 계획을 조정하는 AQE 등을 제공
    • 캐싱, 파티션 조정, 조인 전략 선택, 런타임 통계를 사용하는 AQE 등
flowchart TD
  user["사용자가 작성한 연산"] --> logical["Logical Plan"]
  logical --> optimized["Optimized Plan"]
  optimized --> physical["Physical Plan"]
  physical --> execution["Distributed Execution"]

Spark가 항상 빠른 것은 아님. Executor 시작, Task 스케줄링, 직렬화, 네트워크 통신에는 고정 오버헤드가 발생함.

Spark가 잘 맞는 경우

  • 데이터가 한 머신의 처리 범위를 크게 넘어감
  • 여러 머신에서 병렬 처리할 수 있음
  • 반복해서 재사용할 데이터가 있음
  • 복잡한 조인·집계·ETL이 필요함

느려질 수 있는 경우

  • 데이터가 매우 작음
  • Shuffle이 지나치게 큼
  • 특정 Key에 데이터가 몰림
  • 파티션 수가 부적절함
  • Driver로 데이터를 전부 collect()
Discussion