한 주 동안 데이터 수집의 "어려운 절반"을 다뤘다. Week 2가 "데이터를 어떻게 받아들이는가"의 기초였다면, Week 3는 "그 수집이 실시간이고, 장애가 나고, 운영 DB가 멈출 수 없고, 여러 컴포넌트가 얽혀 있을 때 어떻게 견고하게 만드는가"였다. 오늘은 흩어진 조각을 하나의 그림으로 다시 맞춘다.
핵심 질문 하나로 묶는다. "끝없이 흘러드는 데이터를, 중복·순서 뒤바뀜·장애 속에서, 운영 시스템을 멈추지 않고, 여러 소비자가 각자 활용하도록 어떻게 수집할 것인가." 이번 주의 다섯 날이 이 질문의 다섯 부분에 정확히 대응한다.
[운영 RDS MySQL]
│ (Day3) DMS Full Load + CDC, binlog ROW 필요
▼
[Kinesis Data Streams] ── PartitionKey=userId (Day2 순서 보장)
│
├─(Day4 팬아웃)→ [Firehose] → S3 raw ──(Day4 Batch Layer)
│
▼ (Day1 Speed Layer)
[Managed Flink] Event time + Watermark + Tumbling window
│ 체크포인트로 exactly-once 처리
├─→ OpenSearch (실시간 대시보드)
└─→ 실패 레코드 → (Day2) SQS DLQ / OnFailure destination
이 그림 안에 이번 주 전부가 들어 있다. DMS가 운영 DB의 변경을 무중단으로 캡처(Day3)하고, Kinesis가 순서를 보장하며 받아(Day2), 한쪽은 S3로 적재(배치)하고 한쪽은 Flink로 실시간 집계(Day1)한다 — 전형적인 Lambda 아키텍처(Day4)다. 처리 실패는 DLQ로 격리되고(Day2), 컴포넌트들은 스트림/큐로 느슨하게 결합된다(Day4).
이 파이프라인은 데이터 엔지니어링의 두 가지 불변 원칙을 보여준다. 첫째, 원본 보존(immutable raw) — S3 raw 레이어는 절대 수정하지 않고, 잘못은 재처리로 바로잡는다. 둘째, 단일 진실 공급원(single source of truth)에서의 다중 뷰 — 같은 데이터를 배치 뷰와 실시간 뷰로 각자 재구성한다. 수집 설계의 견고함은 이 두 원칙을 지키는 데서 나온다.
Day2와 Day4에서 따로 본 Kinesis Data Streams, SQS, SNS, EventBridge를 같은 축 위에 올려놓으면 선택이 쉬워진다. 핵심은 "메시지를 읽은 뒤 무슨 일이 일어나는가"다.
판별 축
Kinesis Data Streams
SQS
SNS
EventBridge
모델
로그(스트림)
큐
펍/섭 토픽
이벤트 버스
읽은 뒤 데이터
남는다(보존 기간이 지나야 만료)
컨슈머가 삭제해야 사라진다
저장하지 않고 구독자에게 푸시
저장하지 않고 규칙에 맞는 타깃에 전달
여러 소비자
각자 독립된 위치에서 같은 데이터를 읽는다
한 메시지는 한 컨슈머가 처리
구독자 전원에게 복제
규칙에 매칭된 타깃마다 전달
재생(replay)
보존 기간 안에서 가능
불가(삭제되면 끝)
불가
아카이브를 따로 만들어 둔 경우에만
순서
샤드 내
FIFO는 메시지 그룹 내, Standard는 보장 없음
표준 토픽은 보장 없음
보장 없음
보존
기본 24시간, 최대 365일
기본 4일, 최소 60초~최대 14일
—
—
전달 실패
컨슈머가 같은 위치를 다시 읽는다
가시성 타임아웃 뒤 재노출 → DLQ
재시도 뒤 DLQ(설정 시)
기본 24시간·최대 185회 재시도 뒤 폐기, DLQ 설정 가능
재생이 필요하면 스트림이다. 집계 로직의 버그를 고친 뒤 지난 데이터를 다시 흘려야 하거나(Kappa), 새 컨슈머가 과거 시점부터 읽어야 한다면 큐·토픽으로는 안 된다. Kinesis의 보존 기간을 24시간보다 길게 잡으면 추가 요금이 붙으므로, 더 먼 과거는 S3 원본으로 재처리하는 편이 싸다.
메시지 하나하나의 성공·실패를 따로 관리하려면 큐다. SQS는 메시지 단위로 삭제·재시도·DLQ 격리가 된다. 스트림은 샤드 안에서 순서대로 읽으므로 한 레코드의 실패가 그 뒤 레코드를 막는다.
용량 산정 방식이 다르다. Kinesis 프로비저닝 모드는 샤드당 쓰기 1MB/초 또는 1,000레코드/초, 읽기 2MB/초가 기준이라 샤드 수를 직접 계산해야 한다. 온디맨드 모드는 트래픽에 맞춰 샤드 수를 자동으로 조정한다. SQS Standard는 용량 산정이 사실상 필요 없다.
소비자가 늘면 읽기 한도를 나눠 쓴다. 샤드의 읽기 2MB/초는 GetRecords로 읽는 컨슈머들이 공유한다. 컨슈머가 여럿이고 지연이 문제라면 향상된 팬아웃(등록 컨슈머)으로 컨슈머마다 전용 처리량을 받는다.
⚠️ 함정: "순서 보장 + 정확히 한 번"이라는 단서만 보고 SQS FIFO를 고르면 절반만 맞다. FIFO의 중복 제거는 보내는 쪽의 재전송을 5분 창 안에서 걸러 줄 뿐이다. 컨슈머가 처리 도중 죽어 가시성 타임아웃이 지나면 같은 메시지를 다시 받는다. 처리 결과의 정합성은 여전히 멱등 컨슈머가 책임진다.
시험의 장애 문제는 대부분 "이런 증상이 보인다. 원인과 조치는?" 형태다. 이번 주에 나온 증상을 한 표로 모은다.
증상
가장 흔한 원인
조치
같은 SQS 메시지가 두 번 처리된다
가시성 타임아웃(기본 30초)이 실제 처리 시간보다 짧다
타임아웃을 최대 처리 시간보다 길게 잡거나, 처리 중 ChangeMessageVisibility로 연장한다(첫 수신 시점부터 최대 12시간)
FIFO 큐에서 특정 그룹만 멈춰 있다
같은 MessageGroupId의 앞 메시지가 처리 중이면 뒤 메시지는 나오지 않는다
독약 메시지를 DLQ로 빼고, 그룹 ID를 더 잘게 나눠 병렬성을 확보한다
한참 뒤의 재전송이 중복으로 들어온다
FIFO 중복 제거 창(5분) 밖이다
DynamoDB 조건부 쓰기 기반 멱등성 테이블로 기간을 직접 정한다
Kinesis의 특정 샤드만 쓰기 스로틀이 난다
파티션 키가 쏠린 핫 샤드. 같은 키는 항상 같은 샤드로 간다
키의 카디널리티를 높인다. 샤드를 늘려도 한 키의 트래픽은 나뉘지 않는다
Kinesis→Lambda에서 한 샤드의 처리가 멈춘다
실패한 배치를 레코드가 만료될 때까지 재시도하는 것이 기본 동작이다
MaximumRetryAttempts·MaximumRecordAgeInSeconds로 상한을 두고, BisectBatchOnFunctionError와 실패 대상을 설정한다
Flink millisBehindLatest가 계속 오른다
처리 용량이 유입량을 못 따라간다
병렬성(KPU)을 늘리고, 소스 샤드 수와 핫 키를 함께 점검한다
DMS Full Load는 끝났는데 변경분이 안 온다
원본 로그 설정 누락(binlog ROW / wal_level=logical / supplemental logging)
원본 설정을 켠 뒤 태스크를 다시 시작한다
CDCLatencyTarget만 오른다
대상 쓰기가 병목이다
대상의 적재·커밋 부담과 용량을 본다. 원본 쪽 문제가 아니다
EventBridge 타깃에 이벤트가 드문드문 빠진다
재시도를 소진한 이벤트는 폐기된다
타깃에 DLQ를 달아 실패 이벤트를 받아 둔다
Kinesis→Lambda의 실패 대상을 SQS·SNS로 지정하면 레코드 본문이 아니라 메타데이터(스트림 ARN, 샤드 ID, 시작·끝 시퀀스 번호)만 전달된다. 원본은 그 시퀀스 번호로 스트림에서 다시 읽어야 하므로, 스트림 보존 기간이 지나기 전에 조사해야 한다. S3를 대상으로 지정하면 호출 레코드 전체가 함께 저장된다. SQS의 DLQ가 메시지 자체를 옮겨 담는 것과 다른 점이다.
이번 주에 exactly-once라는 말이 여러 번 나왔지만, 가리키는 범위가 서로 다르다. 보기에서 "어느 계층의 보장인가"를 구분해야 한다.
Flink 체크포인트의 exactly-once는 Flink 내부 상태에 대한 보장이다. 장애가 나면 마지막 체크포인트로 돌아가 소스를 다시 읽으므로, 그 사이에 이미 싱크로 나간 결과는 한 번 더 나갈 수 있다. 싱크가 멱등(같은 키로 덮어쓰기)이거나 트랜잭션을 지원해야 끝에서 끝까지 정합성이 맞는다.
SQS FIFO의 exactly-once는 큐에 중복이 들어오지 않게 하는 것(5분 창)이 핵심이다. 소비 쪽 재전달은 위 함정대로 남는다.
그래서 어느 조합을 쓰든 마지막 방어선은 같다 — 쓰는 쪽을 멱등하게 만든다. 조건부 쓰기, 자연 키 upsert, 파티션 단위 덮어쓰기가 그 도구다.
"단순 변환 후 S3 적재는 Firehose + Lambda"라는 공식에서 Lambda가 항상 필요한 것은 아니다. JSON을 Parquet/ORC로 바꾸는 일은 Firehose의 레코드 형식 변환 기능이 Glue Data Catalog 테이블의 스키마를 참조해 직접 수행한다. 입력이 JSON이 아닐 때(CSV, 로그 텍스트 등) 먼저 JSON으로 바꾸는 단계에서 Lambda 변환이 필요하다. 어느 쪽이든 상태가 없는 변환이므로 Flink를 쓸 이유가 없다는 결론은 같다.