데이터 엔지니어 인터뷰 — Flink

기본 개념

Flink와 Spark Streaming의 근본적인 차이는 무엇인가요

처리 모델이 다릅니다. Spark Structured Streaming은 마이크로배치가 기본이라 작은 배치를 반복 실행합니다. Flink는 레코드 단위 연속 처리가 기본이라 이벤트가 도착하면 곧바로 흘려보냅니다.

그래서 지연 특성이 다릅니다. Flink는 밀리초 단위 지연이 가능하고, Spark는 배치 주기에 묶입니다. 대신 Spark는 배치 처리와 코드·생태계를 공유한다는 이점이 있습니다.

Flink는 배치를 스트림의 특수한 경우(유한한 스트림)로 봅니다. Spark는 반대로 스트림을 배치의 반복으로 봅니다.

JobManager와 TaskManager의 역할을 설명해보세요

JobManager는 잡을 스케줄링하고, 체크포인트를 조율하며, 실패 시 복구를 지휘합니다. TaskManager는 실제 연산을 수행하는 워커이고, 내부에 Task Slot을 두어 병렬 실행 단위를 나눕니다.

슬롯 수가 그 TaskManager가 동시에 맡을 수 있는 파이프라인 수를 정합니다. 잡의 병렬도(parallelism)는 전체 슬롯 수를 넘을 수 없습니다.

연산자 체이닝(operator chaining)은 무엇인가요

인접한 연산자를 같은 스레드에서 실행해 직렬화와 네트워크 전송을 없애는 최적화입니다. map → filter처럼 파티셔닝이 바뀌지 않는 구간이 하나로 묶입니다.

프로파일링할 때는 체인이 묶여 있어 어느 연산자가 느린지 안 보일 수 있습니다. disableChaining()으로 끊어 확인합니다.

시간과 윈도우

이벤트 시간, 처리 시간, 인제스트 시간을 구분해보세요

시간 기준 특성
Event Time 이벤트가 실제 발생한 시각 결과가 재현 가능. 지연·순서 뒤바뀜 처리 필요
Ingestion Time Flink에 들어온 시각 중간
Processing Time 연산자가 처리하는 시각 가장 빠르지만 재실행 시 결과가 달라짐

과거 데이터를 재처리해도 같은 결과가 나와야 한다면 이벤트 시간을 써야 합니다.

워터마크는 무엇이고 어떻게 정하나요

“이 시각 이전의 이벤트는 더 이상 오지 않는다고 간주한다”는 신호입니다. 윈도우를 언제 닫을지 결정합니다.

WatermarkStrategy
    .<Event>forBoundedOutOfOrderness(Duration.ofSeconds(10))
    .withTimestampAssigner((e, ts) -> e.getEventTime());

지연 허용을 크게 잡으면 늦은 데이터를 더 많이 담지만 결과가 그만큼 늦게 나오고 상태도 오래 유지됩니다. 실제 지연 분포를 측정해서 정하는 것이 맞습니다.

워터마크는 여러 입력 중 가장 느린 것을 따릅니다. 파티션 하나가 데이터를 안 보내면 워터마크가 멈춰 윈도우가 영영 닫히지 않습니다. withIdleness()로 유휴 파티션을 제외할 수 있습니다.

윈도우 종류를 설명해보세요

  • Tumbling — 겹치지 않는 고정 크기. 5분마다 집계
  • Sliding — 겹치는 고정 크기. 5분 윈도우를 1분마다 (한 이벤트가 여러 윈도우에 속함)
  • Session — 활동 사이 간격(gap)으로 구분. 사용자 세션 분석
  • Global — 자동으로 닫히지 않음. 커스텀 트리거 필요

Sliding은 겹치는 만큼 상태와 계산이 배로 늘어납니다. 5분/1분이면 이벤트 하나가 5개 윈도우에 들어갑니다.

늦게 도착한 데이터는 어떻게 처리하나요

워터마크가 지난 뒤 온 데이터입니다. 세 가지 선택지가 있습니다.

  1. 버림 (기본)
  2. allowedLateness — 윈도우를 일정 시간 더 열어두고 늦게 온 것을 반영해 재발행
  3. side output — 별도 스트림으로 빼서 따로 처리
stream.keyBy(...)
      .window(TumblingEventTimeWindows.of(Time.minutes(5)))
      .allowedLateness(Time.minutes(1))
      .sideOutputLateData(lateTag)
      .aggregate(...);

집계 결과를 이미 다운스트림에 보냈다면, 재발행이 중복인지 갱신인지 싱크가 구분할 수 있어야 합니다.

상태와 체크포인트

상태(state)의 종류를 설명해보세요

Keyed State는 키별로 격리된 상태입니다(ValueState, ListState, MapState, ReducingState). keyBy 이후에만 쓸 수 있고, 같은 키의 이벤트는 항상 같은 태스크로 갑니다.

Operator State는 키와 무관하게 연산자 인스턴스가 갖는 상태입니다. Kafka 소스의 오프셋 같은 것이 여기 해당합니다.

체크포인트와 세이브포인트의 차이는 무엇인가요

  체크포인트 세이브포인트
목적 장애 복구 (자동) 의도적 중단·재시작 (수동)
주기 설정된 간격마다 사람이 트리거
수명 보통 최신 것만 유지 명시적으로 지울 때까지
용도 실패 시 자동 재개 버전 업그레이드, 병렬도 변경, 코드 배포

잡 코드를 바꿔 배포할 때는 세이브포인트를 떠서 중단하고, 새 코드로 그 세이브포인트에서 재개합니다. 상태 스키마가 호환되지 않으면 복원에 실패하므로, 상태를 쓰는 연산자에는 UID를 고정해 두는 것이 중요합니다.

stream.keyBy(...).process(...).uid("order-state-processor");

UID를 안 주면 자동 생성되는데, 코드가 조금만 바뀌어도 값이 달라져 세이브포인트에서 상태를 못 찾습니다.

exactly-once는 어떻게 달성하나요

체크포인트 알고리즘(Chandy-Lamport 변형)으로 스트림에 배리어를 흘려보내 일관된 스냅샷을 만듭니다. 실패하면 마지막 스냅샷으로 되돌리고 소스 오프셋도 함께 되감습니다.

여기까지는 Flink 내부 상태에 대한 보장입니다. 싱크까지 exactly-once가 되려면 싱크가 트랜잭션을 지원하고 2PC에 참여해야 합니다(TwoPhaseCommitSinkFunction). Kafka 싱크는 트랜잭션을 지원하고, 파일 싱크는 커밋 시점에 파일을 이동하는 방식으로 구현합니다.

싱크가 트랜잭션을 지원하지 않으면 at-least-once가 한계이고, 멱등 쓰기로 보완해야 합니다.

상태 백엔드는 어떤 것을 고르나요

  • HashMapStateBackend — 상태를 JVM 힙에 둠. 빠르지만 힙 크기가 상한
  • EmbeddedRocksDBStateBackend — 상태를 로컬 디스크의 RocksDB에 둠. 힙보다 훨씬 큰 상태를 다룰 수 있고 증분 체크포인트 가능. 직렬화 비용으로 느림

상태가 GB 단위를 넘어가면 RocksDB가 사실상 유일한 선택입니다. 증분 체크포인트를 켜면 변경분만 저장해 체크포인트 시간이 크게 줄어듭니다.

운영

백프레셔(backpressure)는 무엇이고 어떻게 진단하나요

다운스트림이 처리 속도를 못 따라가면 업스트림으로 압력이 전파되어 소스까지 느려지는 현상입니다. 버퍼가 무한정 쌓여 터지는 대신 자연스럽게 속도를 맞추는 메커니즘입니다.

Flink Web UI의 Backpressure 탭에서 어느 연산자가 원인인지 볼 수 있습니다. 압력을 받는 연산자가 아니라 그 아래에서 막고 있는 연산자가 진짜 원인입니다.

흔한 원인은 외부 시스템 호출(DB, API) 지연, skew로 인한 특정 서브태스크 과부하, 직렬화 비용입니다.

데이터 skew는 어떻게 다루나요

keyBy의 키 분포가 치우치면 특정 서브태스크만 바쁩니다. 접근은 Spark와 비슷합니다.

  • 키에 임의 접미사를 붙여 분산한 뒤 2단계 집계
  • 사전 집계(local aggregation)로 네트워크로 보내는 양을 줄임
  • 병렬도를 키의 카디널리티에 맞춰 조정

잡 재시작 전략에는 무엇이 있나요

fixed-delay(고정 횟수·간격 재시도), failure-rate(일정 시간 내 실패율 초과 시 중단), exponential-delay, none이 있습니다.

무한 재시도로 두면 근본 원인(잘못된 데이터, 권한 문제)이 있을 때 조용히 계속 실패합니다. 실패율 기반으로 두고 알림을 거는 편이 낫습니다.

집계·조인 위주의 스트림 처리를 선언적으로 표현할 때 유용합니다. Table API/SQL은 배치와 스트림에서 같은 쿼리를 쓸 수 있다는 점이 큰 장점입니다.

다만 복잡한 상태 로직이나 세밀한 제어가 필요하면 DataStream API로 내려가야 합니다. 그리고 SQL로 짠 잡은 계획이 바뀌면 상태 호환성이 깨질 수 있어 업그레이드가 까다롭습니다.