ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • DE-Zoomcamp_week7_Stream
    카테고리 없음 2026. 3. 9. 21:51

    DataTalksClub의 Streaming Workshop을 바탕으로 실습한 내용을 정리한 글입니다.

    목차


    1. Streaming 파이프라인이란?

    Streaming 파이프라인은 데이터가 생성되는 즉시(또는 매우 짧은 지연 내에) 처리하는 구조다.  

    Batch처럼 "모아서 한 번에 처리"하지 않고, 이벤트 단위로 계속 흘려보내며 계산/적재/알림을 수행한다.

    핵심 3가지

    • 지연(latency)을 줄인다 — 이벤트 발생 후 수 초~수 분 내에 결과가 나온다.
    • 최신 상태를 빠르게 반영한다 — 대시보드, 알림 등이 즉시 갱신된다.
    • 이벤트 시간(event time) 기준 처리를 설계할 수 있다 — 데이터가 실제로 도착한 시각이 아닌, 발생한 시각 기준으로 정확하게 계산한다.

    2. Batch와 다른 점

    구분 Batch Streaming

    처리 단위 일정 주기(시간/일) 단위로 모아서 처리 이벤트 단위로 즉시 처리
    지연 높음 (분~시간~일) 낮음 (밀리초~초)
    설계 복잡도 상대적으로 단순 순서, 중복, 지연 도착 데이터 등 고려 필요
    강점 최종 집계, 대량 처리 실시간 의사결정, 모니터링
    장애 복구 재실행이 비교적 쉬움 상태(state) + 체크포인트 설계 필요

    한 줄 요약: Batch는 "모아서 한 번에", Streaming은 "오는 족족 바로".


    3. 언제 Streaming을 사용하는가?

    대표적인 Use Case는 다음과 같다.

    Use Case 설명

    CDC (Change Data Capture) DB 변경(INSERT/UPDATE/DELETE)을 실시간으로 다른 시스템에 전달
    실시간 대시보드 분/초 단위 지표 업데이트 (매출, 트래픽, 재고 등)
    이상 탐지 사기 거래, 비정상 트래픽, 장애 징후 탐지
    알림/추천 사용자 행동 기반 즉시 반응 (푸시, 쿠폰 등)
    로그/이벤트 파이프라인 서비스 이벤트를 중앙 수집 후 다중 소비

    4. Kafka

    Kafka는 이벤트 스트리밍 플랫폼이다.  

    Producer가 Topic에 기록하고, Consumer가 읽는다. 메시지는 로그 형태로 저장되고, 오프셋 기반으로 재처리가 가능하다.

    4.1 Kafka 아키텍처

    Producer ──▶ [ Topic (Partition 0) ] ──▶ Consumer Group
                 [ Topic (Partition 1) ]
                 [ Topic (Partition 2) ]
                        │
                      Broker(s)
    
    

    구성 요소별 역할:

    • Producer — 이벤트를 발행한다.
    • Topic — 이벤트 스트림의 논리 단위다.
    • Partition — Topic을 분할한 저장 단위로, 병렬 처리와 확장성을 제공한다.
    • Broker — 데이터를 저장하고 복제하는 서버다.
    • Consumer Group — 여러 Consumer가 파티션을 나눠 읽는 단위다.
    • Offset — Consumer가 어디까지 읽었는지 나타내는 위치 정보다.

    4.2 Redpanda

    Redpanda는 Kafka API 호환 스트리밍 플랫폼이다.  
    C++로 작성되어 JVM 의존성이 없고, 단일 바이너리로 구동된다.

    • Kafka 클라이언트를 코드 변경 없이 그대로 활용할 수 있다.
    • ZooKeeper 같은 외부 의존이 없어 운영이 단순하다.
    • Docker Compose 기반 로컬 실행이 매우 간편하다.

    4.3 Producer / Consumer / Topic이 존재하는 이유

    이 구조의 본질은 결합도 분리(decoupling)다.

    • Producer는 "생성"에만 집중한다.
    • Consumer는 "처리"에만 집중한다.
    • Topic은 둘 사이의 버퍼/로그 역할을 한다.

    Batch 파이프라인과의 차별점:

    관점 Batch Kafka(Streaming)

    처리 단위 파일/테이블 단위 이벤트 단위
    다중 소비 ETL 재실행 필요 같은 이벤트를 여러 Consumer가 독립 소비
    장애 복구 전체 재처리 오프셋 기반 부분 재처리
    속도 차이 흡수 스케줄 의존 Topic이 버퍼 역할 (backpressure 완화)

    5. Flink

    Flink는 상태 기반(stateful) 스트림 처리 엔진이다.  

    정확한 시간 처리(event time), 윈도우, 조인, 체크포인트/복구, Exactly-once 시맨틱을 강하게 지원한다.

    5.1 Flink 아키텍처

    ┌──────────────┐
    
    │  JobManager  │ ← 잡 스케줄링, 체크포인트 관리, 전체 제어
    
    └──────┬───────┘
    
           │
    
    ┌──────▼───────┐   ┌──────────────┐
    
    │ TaskManager  │   │ TaskManager  │ ← 실제 연산 실행 (Operator Chain)
    
    │  (slot × N)  │   │  (slot × N)  │
    
    └──────────────┘   └──────────────┘
    
           │                   │
    
      State Backend + Checkpoint ← 장애 시 상태 복구
    
    

    실습에서는 Docker Compose로 jobmanager, taskmanager 컨테이너를 분리해 실행했다.

    5.2 Flink로 할 수 있는 일

    • 윈도우 집계 — 분/시간 단위 지표 생성
    • 상태 기반 처리 — 사용자 세션, 누적 카운트
    • 스트림 조인 — 클릭 스트림 + 사용자 프로필 등
    • 이상 탐지 — 규칙 기반 실시간 판정
    • 실시간 적재 — Kafka → DB/Postgres 파이프라인

    5.3 Consumer에서 처리하지 않고 Flink를 쓰는 이유

    "단순 처리"는 Consumer 애플리케이션으로도 가능하다.  

    하지만 다음 요구사항이 생기면 Flink가 유리하다.

    | Consumer 직접 처리 | Flink |

    |--------------------|-------|

    | 상태 관리를 직접 구현 | 상태 관리 내장 (RocksDB 등) |

    | 시간/순서 처리를 직접 구현 | Event time + Watermark 내장 |

    | 장애 복구 직접 구현 | Checkpoint 기반 Exactly-once 제공 |

    | 단일 처리 로직 | 복수 연산/복수 싱크를 일관된 잡으로 운영 |

    | 백프레셔 직접 관리 | 백프레셔/재시도 내장 |

    한 줄 요약: Consumer 코드로도 시작할 수 있지만, 파이프라인이 커질수록 처리 엔진이 필요해진다.

    5.4 Flink vs Spark Streaming

    | 구분 | Flink | Spark Structured Streaming |

    |------|-------|---------------------------|

    | 처리 모델 | Native Streaming (진짜 스트림) | Micro-batch 기반 |

    | 지연 | 밀리초~초 수준 | 초~분 수준 (마이크로배치 간격) |

    | 상태 관리 | 강력 (RocksDB, Incremental Checkpoint) | 제한적 |

    | Event time / Watermark | 1급 지원 | 지원하나 Flink만큼 유연하지 않음 |

    | 생태계 연계 | 스트림 특화 | 기존 Spark(ML, SQL, DataFrame) 통합 강점 |

    | 적합한 상황 | 초저지연 + 복잡 상태 처리 | 배치+스트림 통합 운영 |

    결론: "초저지연 + 복잡 상태 처리"면 Flink, "기존 Spark 파이프라인 확장"이면 Spark Streaming이 자주 선택된다.

    5.5 Watermark란 무엇인가

    Watermark는 "이 시점 이전 이벤트는 거의 다 도착했다"는 시간 진행 신호다.

    현실 데이터에서는 네트워크 지연, 클라이언트 오프라인 등의 이유로 이벤트가 늦게 도착할 수 있다.  

    Watermark는 이런 환경에서 윈도우를 언제 닫을지 결정하는 핵심 메커니즘이다.

    이벤트 시간 축 ──────────────────────────────▶
    
    
    
      [이벤트A 09:00:01]  [이벤트B 09:00:03]  [이벤트C 08:59:58 ← 늦게 도착!]
    
                                                  │
    
                                Watermark = 09:00:03 - 5초 = 08:59:58
    
                                → "08:59:58 이전 데이터는 거의 다 왔다"
    
    

    실습 코드에서는 다음과 같이 설정했다:

    WATERMARK FOR event_timestamp AS event_timestamp - INTERVAL '5' SECOND
    
    
    • 5초 이내 늦게 도착한 이벤트는 정상 처리된다.
    • 5초 이상 늦으면 late event로 별도 정책이 필요하다.

    Producer에서 20% 확률로 3~10초 늦은 이벤트를 생성해, 실제로 watermark가 어떻게 동작하는지 확인할 수 있었다.


    6. 실습

    6.1 GitHub Codespaces

    Codespaces를 쓰면 로컬 환경 차이(Java/Python/의존성) 문제를 줄일 수 있다.  

    팀원이 같은 개발 컨테이너 환경에서 재현 가능한 실습을 할 수 있다는 점이 크다.

    • 별도 설치 없이 브라우저에서 바로 개발 환경 진입
    • Docker, Python, Java 등이 미리 구성된 상태로 시작
    • .devcontainer 설정으로 환경 재현성 보장

    6.2 실습에서 Redpanda를 사용한 이유

    이 실습의 목적은 "스트리밍 개념과 Flink 처리"이지, Kafka 운영이 아니다.

    • Redpanda는 기동이 빠르고 설정이 단순하다.
    • Kafka API 호환이므로 kafka-python 등 기존 클라이언트를 그대로 쓸 수 있다.
    • 학습/실습에서 운영 복잡도를 줄여 본질(스트리밍 처리)에 집중할 수 있다.

    6.3 uv add --dev jupyter를 사용한 이유

    --dev는 개발/실험용 의존성을 프로덕션 의존성과 분리하기 위해 사용한다.

    # pyproject.toml
    
    [dependency-groups]
    
    dev = [
    
        "jupyter>=1.1.1",
    
    ]
    
    
    • Jupyter는 실험/탐색용 도구이지, 런타임 필수가 아니다.
    • 프로덕션 환경(예: Flink 컨테이너)에서는 Jupyter가 필요 없다.
    • 배포 이미지 크기, 보안, 재현성 측면에서 분리하는 것이 좋은 습관이다.

    즉, "실행에 필수"와 "개발 편의"를 분리하는 것이다.

    6.4 전체 아키텍처

    ┌──────────────────┐          ┌──────────────┐
    
    │  Producer        │──rides──▶│  Redpanda    │
    
    │  (Python)        │  topic   │  (Kafka API) │
    
    └──────────────────┘          └──────┬───────┘
    
                                         │
    
                            ┌────────────┴────────────┐
    
                            ▼                         ▼
    
                  ┌─────────────────┐       ┌─────────────────────┐
    
                  │ Flink Job:      │       │ Flink Job:           │
    
                  │ pass-through    │       │ 1h tumble aggregation│
    
                  └────────┬────────┘       └──────────┬───────────┘
    
                           ▼                           ▼
    
                  ┌─────────────────┐       ┌─────────────────────────┐
    
                  │ Postgres:       │       │ Postgres:                │
    
                  │ processed_events│       │ processed_events_aggregated│
    
                  └─────────────────┘       └─────────────────────────┘
    
    

    주요 파일 구조:

    | 파일 | 역할 |

    |------|------|

    | docker-compose.yaml | 인프라 (Redpanda, Flink, Postgres) |

    | Dockerfile.flink | Flink 이미지 + 커넥터 JAR 다운로드 |

    | flink-config.yaml | Flink 설정 (메모리, 슬롯 등) |

    | src/producers/producer_realtime.py | 이벤트 생성기 |

    | src/job/pass_through_job.py | 단순 적재 잡 |

    | src/job/aggregation_job.py | 윈도우 집계 잡 |

    6.5 환경 구성

    docker-compose.yaml로 4개 서비스를 함께 띄운다:

    • redpanda — Kafka 호환 메시지 브로커
    • jobmanager — Flink 잡 스케줄러
    • taskmanager — Flink 잡 실행기
    • postgres — 결과 저장소

    Dockerfile.flink에서는 다음 커넥터 JAR을 미리 다운로드한다:

    RUN wget .../flink-json-2.2.0.jar; \
    
        wget .../flink-sql-connector-kafka-4.0.1-2.0.jar; \
    
        wget .../flink-connector-jdbc-core-4.0.0-2.0.jar; \
    
        wget .../flink-connector-jdbc-postgres-4.0.0-2.0.jar; \
    
        wget .../postgresql-42.7.10.jar
    
    

    이렇게 이미지 레벨에서 준비하면 Flink SQL DDL에서 'connector' = 'kafka', 'connector' = 'jdbc'를 바로 쓸 수 있다.

    6.6 Producer: 실시간 + 지연 이벤트 생성

    src/producers/producer_realtime.py는 rides 토픽으로 NYC 택시 이벤트를 계속 보낸다.

    # ~20% 확률로 3~10초 늦은(late) 이벤트를 만든다
    
    if random.random() < 0.2:
    
        delay = random.randint(3, 10)
    
        ride = make_ride(delay_seconds=delay)
    
    
    • 0.5초마다 1건씩 전송
    • 20% 확률로 지연 이벤트 생성 → Watermark 동작을 관찰할 수 있다
    • NYC 택시 존 ID 기반의 현실적인 데이터 구조

    6.7 Pass-through Job: Kafka → Postgres

    src/job/pass_through_job.py는 "스트림 적재 파이프라인의 최소 단위"를 검증하는 단계다.

    INSERT INTO processed_events
    
    SELECT
    
        PULocationID, DOLocationID, trip_distance, total_amount,
    
        TO_TIMESTAMP_LTZ(tpep_pickup_datetime, 3) as pickup_datetime
    
    FROM events
    
    
    • Source: Kafka ('connector' = 'kafka')
    • Sink: Postgres ('connector' = 'jdbc')
    • 변환: epoch milliseconds → timestamp

    6.8 Aggregation Job: 윈도우 집계

    src/job/aggregation_job.py에서는 event time + watermark + 1시간 tumbling window 집계를 수행한다.

    -- Source에 Watermark 정의
    
    event_timestamp AS TO_TIMESTAMP_LTZ(tpep_pickup_datetime, 3),
    
    WATERMARK FOR event_timestamp AS event_timestamp - INTERVAL '5' SECOND
    
    
    
    -- 1시간 단위 집계
    
    SELECT
    
        window_start, PULocationID,
    
        COUNT(*) AS num_trips,
    
        SUM(total_amount) AS total_revenue
    
    FROM TABLE(
    
        TUMBLE(TABLE events, DESCRIPTOR(event_timestamp), INTERVAL '1' HOUR)
    
    )
    
    GROUP BY window_start, PULocationID
    
    

    집계 결과는 (window_start, PULocationID)를 PK로 하는 processed_events_aggregated 테이블에 upsert 형태로 저장된다.

    6.9 실행 흐름

    # 1. 인프라 기동
    
    docker compose up -d --build
    
    
    
    # 2. Producer 실행 (이벤트 생성)
    
    python src/producers/producer_realtime.py
    
    
    
    # 3. Pass-through Job 제출
    
    docker compose exec jobmanager flink run -py /opt/src/job/pass_through_job.py
    
    
    
    # 4. Aggregation Job 제출
    
    docker compose exec jobmanager flink run -py /opt/src/job/aggregation_job.py
    
    

    Flink Web UI는 http://localhost:8081에서 잡 상태를 확인할 수 있다.


    7. 실습 회고

    배운 점

    • 스트리밍에서는 processing time보다 event time + watermark 설계가 더 중요하다.
    • "먼저 pass-through로 엔드투엔드 확인 → 이후 집계 추가" 순서가 디버깅에 유리하다.
    • Flink SQL DDL만으로도 꽤 빠르게 실시간 파이프라인을 만들 수 있다.
    • Redpanda 덕분에 Kafka 운영 부담 없이 스트리밍 개념에 집중할 수 있었다.

    다음에 확장하고 싶은 것

    • CDC 소스 연동 (Debezium)
    • Dead letter topic 도입
    • Window 크기/Watermark 변경에 따른 지연 허용 정책 실험
    • Exactly-once 보장 시나리오와 Checkpoint 튜닝 비교

    보강 정리

    아래 항목은 하단 메모에 적어둔 TODO를 기준으로, 실습 관점에서 바로 설명할 수 있게 정리했다.

    1. uv add --dev jupyter와 --dev의 의미

    uv add --dev jupyter는 jupyter를 "개발용 의존성"으로 추가하는 명령이다.

    • uv add 패키지명: 기본(runtime) 의존성에 추가
    • uv add --dev 패키지명: dev dependency group에 추가

    왜 --dev로 넣는가:

    • Jupyter는 보통 실험/노트북/탐색용 도구다.
    • 서비스 실행(프로덕션 런타임)에는 필수가 아니다.
    • 런타임 이미지 크기, 보안 표면, 설치 시간 최소화에 유리하다.

    실무 기준으로는 다음 구분이 명확하면 좋다.

    • runtime: 애플리케이션이 "실행"되기 위해 반드시 필요한 패키지
    • dev: 개발/테스트/실험/포맷팅/노트북 등 "개발 생산성"을 위한 패키지

    2. dataclass의 기능 설명

    dataclass는 "데이터를 담는 클래스"를 간결하게 정의하기 위한 기능이다.

    핵심 효과:

    • 생성자(__init__) 자동 생성
    • 문자열 표현(__repr__) 자동 생성
    • 비교 메서드(__eq__, 옵션 시 정렬 메서드) 자동 생성
    • 기본값/타입 힌트 기반으로 가독성 높은 모델 정의

    예시:

    from dataclasses import dataclass
    from datetime import datetime
    
    @dataclass
    class RideEvent:
        pu_location_id: int
        do_location_id: int
        trip_distance: float
        total_amount: float
        event_time: datetime
    

    스트리밍 실습에서는 이벤트 스키마를 코드에서 명확히 표현하고, 테스트 데이터 생성 시 필드 누락을 줄이는 데 도움이 된다.

    3. Producer와 Consumer를 나누는 이유

    Producer/Consumer 분리는 "생성"과 "처리"의 관심사를 분리하기 위해 필요하다.

    • Producer: 이벤트를 생성하고 토픽에 기록
    • Consumer: 토픽에서 읽어 가공/적재/알림 수행

    나누면 얻는 장점:

    • 결합도 감소: 생산 시스템 변경이 소비 시스템에 즉시 전파되지 않음
    • 독립 확장: 쓰기 트래픽/읽기 트래픽을 각각 스케일링 가능
    • 장애 격리: Consumer 장애가 Producer까지 바로 전파되지 않음
    • 다중 소비: 동일 이벤트를 여러 Consumer Group이 각자 목적대로 사용

    즉, 토픽이 "내구성 있는 버퍼" 역할을 해서 시스템 전체 복원력과 확장성이 올라간다.

    4. Watermark란?

    Watermark는 event time 기반 처리에서 "여기까지는 데이터가 거의 다 도착했다"고 판단하는 시간 신호다.

    왜 필요한가:

    • 이벤트는 발생 순서대로 도착하지 않을 수 있다.
    • 늦게 도착한 데이터(late event)까지 무한정 기다리면 윈도우를 닫을 수 없다.

    예시 설정:

    WATERMARK FOR event_timestamp AS event_timestamp - INTERVAL '5' SECOND
    

    의미:

    • 최대 5초 지연까지는 정상 윈도우 계산에 포함
    • 5초를 크게 넘긴 이벤트는 late 정책(폐기/별도 저장/재처리) 필요

    5. Flink의 latest-offset 정리

    latest-offset은 Kafka Source 시작 지점을 "현재 시점의 가장 최신 오프셋"으로 두는 방식이다.

    보통 아래처럼 사용한다.

    'scan.startup.mode' = 'latest-offset'
    

    의미와 주의점:

    • 잡 시작 이후 들어오는 신규 이벤트부터 읽는다.
    • 과거 적재 데이터를 재처리하지 않는다.
    • "과거 포함 재처리"가 필요하면 earliest-offset 또는 특정 오프셋/타임스탬프 기반 시작 전략이 필요하다.

    실습 관점에서는 데모를 빠르게 시작할 때 유용하지만, 운영 파이프라인에서는 장애 복구/재처리 전략과 함께 결정해야 한다.

    핵심 요약

    • uv add --dev jupyter: Jupyter는 개발 생산성 도구이므로 runtime과 분리
    • dataclass: 이벤트 구조를 간결하고 안전하게 표현
    • producer/consumer 분리: 결합도 감소, 독립 확장, 장애 격리, 다중 소비
    • watermark: 늦게 도착한 이벤트를 고려하면서 윈도우를 닫기 위한 event time 기준
    • latest-offset: "지금 이후 데이터만" 읽는 시작 전략

    실습 메모

    • 데모/학습에서는 latest-offset이 빠르지만, 검증/리플레이가 필요하면 시작 전략을 바꿔야 한다.
    • Watermark 지연 허용 시간(예: 5초)은 데이터 특성에 맞춰 튜닝해야 정확도와 지연의 균형을 맞출 수 있다.

    댓글

Designed by Tistory.