멜로밍 랭킹 - SOOP, 치지직, 씨미 채팅 수집 및 처리 아키텍처
CEO & Fullstack Engineer
라이브 방송의 채팅 한 줄은 화면에 나타났다가 사라지는 짧은 문자열처럼 보입니다. 멜로밍 랭킹에서는 이 채팅을 채널·사용자·방송 단위로 집계해 참여도와 활동 흐름을 보여줍니다. 같은 수집 데이터는 신청곡과 오버레이 같은 실시간 기능에도 연결됩니다.
문제는 이 모든 기능이 같은 속도와 같은 저장 방식을 요구하지 않는다는 데 있습니다. 신청곡은 방송 흐름을 놓치지 않을 만큼 빨라야 하고, 방송 상태와 채널 배정은 장애 뒤에도 정확히 복구되어야 하며, 통계는 많은 데이터를 효율적으로 집계할 수 있어야 합니다.
이 글에서는 SOOP·치지직·씨미에서 라이브 방송을 찾고 채팅을 수집한 뒤, 멜로밍 랭킹의 분석 데이터로 만드는 과정을 다룹니다. 핵심은 서로 다른 플랫폼을 공통 이벤트로 정규화하고, 요구사항에 따라 제어 경로·실시간 경로·분석 경로로 나누는 것입니다.
이 글의 범위는 데이터 수집과 처리 아키텍처입니다. 채널 수를 Worker Pod 수로 바꾸고 노드를 공급하는 방법은 후속 글인 라이브 스트리밍 채팅 수집 시스템을 KEDA와 Karpenter로 확장한 방법에서 별도로 다룹니다.
1. 시작점은 플랫폼별 차이를 격리하는 것
방송 플랫폼마다 라이브 채널을 찾는 API, 채팅 서버에 접속하는 절차, 메시지 형식이 다릅니다. 어떤 플랫폼은 JSON을 사용하고, 어떤 플랫폼은 바이너리 패킷을 해석해야 합니다. 후원과 구독을 표현하는 방식도 서로 다릅니다.
이 차이가 제품 코드까지 퍼지면 신청곡, 오버레이, 통계가 모두 플랫폼 분기를 갖게 됩니다. 새로운 플랫폼을 추가하거나 기존 프로토콜이 바뀔 때 영향 범위도 커집니다.
그래서 수집 경계에서 플랫폼별 Connector가 원본 메시지를 공통 이벤트로 변환합니다.
플랫폼 원본 이벤트
-> Connector 파싱
-> 공통 ChatEvent
- platform
- channelId
- type
- userId
- nickname
- message
- timestamp
- donation metadata
이 경계 뒤의 서비스는 메시지가 어느 플랫폼에서 어떤 패킷으로 들어왔는지 몰라도 됩니다. 신청곡 Dispatcher는 공통 메시지 본문을 보고, 분석 파이프라인은 공통 채널과 사용자 필드를 집계합니다.
여기서 얻은 첫 번째 원칙은 단순합니다.
외부 시스템의 차이는 가장 바깥 경계에서 흡수하고, 내부에는 제품이 이해하는 언어만 전달합니다.
2. 하나의 파이프라인이 아니라 세 개의 경로
전체 흐름을 데이터의 목적에 따라 나누면 다음과 같습니다.
flowchart LR
subgraph Sources["방송 플랫폼"]
P1["SOOP"]
P2["치지직"]
P3["씨미"]
end
D["Discover\n라이브 채널 탐색"]
C["Coordinator\n채널 배정"]
W["Worker Pool\nWebSocket 수집·정규화"]
PG[("PostgreSQL\n상태와 배정")]
R[("Redis Streams\n실시간 이벤트")]
CH[("ClickHouse\n분석 데이터")]
SR["신청곡"]
OV["오버레이"]
AL["실시간 알림"]
RK["랭킹·통계"]
NLP["워드클라우드"]
Sources --> D --> PG
PG --> C --> W
W --> Sources
W --> R
W --> CH
R --> SR
R --> OV
R --> AL
CH --> RK
CH --> NLP
세 저장소는 같은 데이터를 중복해서 담기 위한 구성이 아닙니다. 각각 다른 질문에 답합니다.
| 경로 | 담당 질문 | 선택한 저장소 |
|---|---|---|
| 제어 경로 | 지금 어떤 방송을 누가 수집해야 하는가? | PostgreSQL |
| 실시간 경로 | 방금 들어온 이벤트를 누가 바로 처리해야 하는가? | Redis Streams |
| 분석 경로 | 오랜 기간 쌓인 데이터를 어떻게 빠르게 집계할 것인가? | ClickHouse |
PostgreSQL은 트랜잭션과 제약조건이 필요한 기준 상태를 맡습니다. Redis는 짧은 지연과 독립적인 소비가 필요한 이벤트를 맡습니다. ClickHouse는 대량의 채팅과 시청자 데이터를 컬럼 단위로 압축하고 집계합니다.
이 역할 구분 덕분에 분석 저장이 잠시 느려져도 실시간 신청곡까지 함께 멈추지 않습니다. 반대로 실시간 소비자가 지연되더라도 기준 상태와 분석 데이터가 같은 방식으로 오염되지 않습니다.
3. Pub/Sub만 믿지 않고 상태로 복구하기
Coordinator는 살아 있는 Worker와 각 Worker의 부하를 보고 채널을 배정합니다. 가장 간단한 구현은 Redis Pub/Sub로 “이 채널에 연결하라”는 명령만 보내는 것입니다.
하지만 Pub/Sub 메시지는 구독자가 잠시 연결되지 않은 동안 유실될 수 있습니다. 배포 직후의 Worker나 네트워크가 흔들리는 순간에는 명령이 사라지고, 채널은 아무도 수집하지 않는 상태가 될 수 있습니다.
멜로밍은 명령을 보내기 전에 채널과 Worker의 배정을 PostgreSQL에 기록합니다.
1. PostgreSQL에 channel -> worker 배정 저장
2. Redis Pub/Sub로 빠른 연결 명령 전달
3. Worker가 주기적으로 자신의 배정을 다시 조회
4. 빠진 연결이 있으면 상태를 기준으로 복구
Pub/Sub는 빠른 알림이고 PostgreSQL은 복구 가능한 의도입니다. 알림이 사라져도 최종 상태는 다시 맞춰집니다.
이 패턴은 실시간 시스템에서 자주 유용합니다.
이벤트는 반응 속도를 높이고, 영속 상태는 이벤트 유실 뒤의 복구 기준이 됩니다.
4. 실시간 기능은 Redis Streams로 분기한다
정규화된 채팅은 채널별 Stream과 전체 이벤트 Stream으로 나뉩니다.
- 채널 Stream: 신청곡, 방송 오버레이, 최근 채팅처럼 특정 방송을 따라가는 소비자
- 전체 Stream: 여러 채널을 가로질러 키워드나 운영 이벤트를 찾는 소비자
Redis Streams를 사용하면 여러 소비자가 서로의 진행 상태를 방해하지 않고 같은 이벤트를 읽을 수 있습니다. Consumer Group과 Pending Entry를 활용하면 처리 도중 실패한 이벤트를 다시 가져올 수도 있습니다.
다만 Stream을 무제한 보존할 수는 없습니다. 실시간 기능은 최신 이벤트에 가치가 크기 때문에 길이와 TTL을 제한합니다. 소비자가 지나치게 오래 멈추면 오래된 이벤트가 trim될 수 있다는 사실도 시스템 계약의 일부로 받아들입니다.
모든 메시지를 무기한 지키는 역할은 분석·아카이브 경로가 맡아야 합니다. Redis에 영구 보관 책임까지 주면 메모리 비용과 장애 범위가 함께 커집니다.
5. 분석은 수집 루프에서 분리한다
Worker는 채팅을 받을 때마다 ClickHouse에 한 행씩 쓰지 않습니다. 메모리 버퍼에 모아 일정 크기나 시간이 되면 batch로 저장합니다.
WebSocket message
-> 정규화
-> 실시간 Stream 전달
-> 메모리 batch 적재
-> ClickHouse batch insert
-> 시간·일·채널·사용자 집계
Batch insert는 처리량과 압축 효율을 높이지만 짧은 저장 지연을 만듭니다. 또 프로세스가 예기치 않게 종료되면 아직 flush되지 않은 일부 데이터가 남을 수 있습니다.
여기서 중요한 선택은 실시간 경로와 분석 경로를 하나의 분산 트랜잭션으로 묶지 않은 것입니다. 두 저장소 중 한쪽이 실패했다고 플랫폼 WebSocket 연결까지 끊으면, 분석 정합성을 지키려다 실시간 서비스의 가용성을 잃게 됩니다.
대신 각 경로의 실패를 관측하고 별도로 회복합니다. 실시간 부작용이 큰 신청곡에는 멱등성 키를 적용하고, 분석 데이터는 batch 실패 지표와 장기 아카이브로 보완합니다.
6. Worker 교체에서 공백과 중복 사이 균형 잡기
배포나 자동 확장으로 Worker가 종료되면, 그 Worker가 유지하던 WebSocket 연결을 다른 Worker가 인수해야 합니다.
기존 연결을 먼저 끊고 새 연결을 시작하면 공백이 생깁니다. 새 연결을 먼저 시작하면 두 Worker가 잠시 같은 채널을 수집해 중복이 생깁니다.
멜로밍은 짧은 중첩을 허용하고 중복을 제거하는 쪽을 선택했습니다.
sequenceDiagram
participant Old as 기존 Worker
participant Coord as Coordinator
participant New as 신규 Worker
participant Redis as Redis Streams
Old->>Coord: draining 상태 알림
Coord->>New: 채널 재배정
New->>New: 새 WebSocket 연결
Old->>Redis: 이벤트 발행
New->>Redis: 같은 구간 이벤트 발행
Redis->>Redis: 짧은 dedup window 적용
New->>Coord: 연결 준비 완료
Coord->>Old: 기존 연결 종료
중복 판별에는 플랫폼, 채널과 원본 메시지의 안정적인 식별값을 결합합니다. Redis Lua script 안에서 dedup 확인과 여러 Stream 쓰기를 함께 처리하면, handoff 구간의 경쟁 조건을 줄일 수 있습니다.
여기서도 exactly-once를 주장하지 않습니다. 외부 WebSocket, 프로세스 메모리, 두 저장소를 지나는 전체 경로에서 완전한 exactly-once는 비용이 매우 큽니다.
대신 제품 영향에 맞춰 보장 수준을 달리합니다.
- 오버레이 채팅: 짧은 중복보다 연결 공백을 더 크게 본다.
- 신청곡 생성: 중복 요청이 실제 부작용을 만들므로 별도 멱등성 키를 둔다.
- 통계: 중복 제거 가능한 키와 집계 방식을 사용한다.
7. 수집 데이터가 멜로밍 랭킹이 되는 지점
파이프라인의 가치는 메시지를 많이 저장하는 데 있지 않습니다. 정규화한 방송·채팅 이벤트를 랭킹과 통계로 연결하고, 같은 데이터를 필요한 실시간 기능에서도 재사용할 때 생깁니다.
멜로밍 랭킹
ClickHouse에서는 채널·사용자·시간 단위 집계를 만들고, 방송 세션의 시작·종료와 시청자 수 구간을 결합합니다. 이를 통해 채널별 참여 사용자, 메시지·후원 활동, 방송별 평균·최고 시청자와 시간대별 활동량을 계산할 수 있습니다.
랭킹 조회는 운영 트랜잭션 DB가 아니라 분석 저장소를 사용합니다. 긴 기간을 집계하는 요청이 방송 상태 갱신이나 신청곡 API의 응답 시간을 직접 흔들지 않도록 하기 위한 선택입니다.
신청곡
Dispatcher가 채팅 명령을 해석해 백엔드로 전달합니다. 백엔드는 노래책의 곡과 매칭하고 방송 세션의 신청곡 Queue에 넣습니다. 같은 Stream 이벤트가 재처리돼도 신청곡이 두 번 만들어지지 않도록 메시지 식별자를 멱등성 키로 사용합니다.
오버레이
신청 결과와 현재 Queue 상태를 방송 화면에 실시간으로 전달합니다. 오버레이는 장기 보관보다 지금 이후의 이벤트를 빠르게 반영하는 것이 중요합니다.
워드클라우드
ClickHouse의 채팅 데이터를 형태소 분석해 방송에서 자주 등장한 단어를 계산할 수 있습니다. 랭킹의 수치만으로는 보이지 않는 방송별 대화 주제와 커뮤니티의 언어적 특성을 함께 보여주는 분석입니다.
8. 운영하며 남은 설계 원칙
이 파이프라인에서 얻은 판단 기준을 정리하면 다음과 같습니다.
- 플랫폼 차이는 Connector 경계 안에 가둔다. 제품 서비스가 플랫폼 프로토콜을 직접 알게 하지 않는다.
- 상태, 이벤트, 분석의 저장소를 역할로 나눈다. 한 저장소를 만능으로 만들지 않는다.
- 빠른 명령에는 반드시 복구 가능한 상태를 남긴다. Pub/Sub 유실은 reconcile로 회복한다.
- 실시간과 분석을 하나의 실패 도메인으로 묶지 않는다. 한 경로의 지연이 전체 수집을 멈추게 하지 않는다.
- exactly-once 대신 부작용별 멱등성을 설계한다. 신청곡과 통계는 같은 보장을 요구하지 않는다.
- handoff에서는 공백과 중복 중 무엇이 더 위험한지 먼저 정한다. 멜로밍은 짧은 중복을 감수하고 연결 지속성을 선택했다.
라이브 채팅 시스템은 WebSocket을 많이 연결하는 것만으로 완성되지 않습니다. 무엇을 정확한 상태로 남기고, 무엇을 빠른 이벤트로 흘려보내며, 어느 지점에서 중복과 지연을 허용할지를 정하는 일이 더 중요합니다.
다음 글인 라이브 스트리밍 채팅 수집 시스템을 KEDA와 Karpenter로 확장한 방법에서는 이 구조 위에서 실제 채널 수를 Worker Pod와 노드 용량으로 바꾸는 확장 전략을 다룹니다.