분산 메시지 큐
- 4.1 문제 이해 및 설계 범위 확정
- 4.2 개략적 설계안 제시
- 4.3 상세 설계
- 4.4 마무리
분산 메시지 큐는 카프카(Apache Kafka)를 모델로 한 대규모 메시지 큐 시스템 설계인데, 전통적인 메시지 큐(RabbitMQ 등)와 이벤트 스트리밍 플랫폼의 차이까지 다뤄. 분산 시스템의 뼈대가 되는 컴포넌트라 이해해두면 다른 장에서도 계속 써먹지.
면접관이 "분산 메시지 큐를 설계해 보세요"라고 하면, 먼저 어떤 종류의 메시지 큐인지 확인해야 해.
전통적 메시지 큐 — RabbitMQ 같은 거야. 메시지가 소비(consume)되면 삭제돼. 일대일(point-to-point) 전달이 기본이지.
이벤트 스트리밍 플랫폼 — 카프카 같은 거야. 메시지가 소비돼도 바로 삭제되지 않아. 같은 메시지를 여러 컨슈머가 독립적으로 읽을 수 있고, 과거 메시지를 다시 읽을 수도 있지.
이 장에서 설계하는 건 카프카 스타일의 이벤트 스트리밍 플랫폼이야. 핵심 요구사항은 이래.
- 프로듀서가 메시지를 **토픽(topic)**에 보낸다
- 컨슈머가 토픽을 **구독(subscribe)**해서 메시지를 받는다
- 메시지는 순서가 보장되어야 한다 (같은 파티션 내에서)
- 메시지는 **반복 소비(re-consume)**가 가능해야 한다
- 높은 처리량, 낮은 지연시간, 높은 가용성
토픽(Topic) — 메시지의 논리적 분류야. "주문 이벤트", "결제 이벤트" 같은 카테고리.
파티션(Partition) — 토픽을 물리적으로 분할한 단위야. 하나의 토픽이 여러 파티션으로 나뉘고, 각 파티션은 순서가 보장되는 메시지의 연속이지. 파티션이 많을수록 병렬 처리가 가능해.
프로듀서(Producer) — 메시지를 토픽에 보내는 쪽이야. 메시지에 **키(key)**를 붙여서 같은 키의 메시지가 같은 파티션으로 가도록 할 수 있어.
컨슈머(Consumer) — 토픽에서 메시지를 읽는 쪽이지.
컨슈머 그룹(Consumer Group) — 같은 토픽을 구독하는 컨슈머들의 논리적 그룹이야. 그룹 내에서 각 파티션은 하나의 컨슈머에게만 할당돼. 같은 그룹 내 컨슈머끼리는 메시지를 나눠 먹고, 다른 그룹은 같은 메시지를 각각 받지.
오프셋(Offset) — 파티션 내에서 각 메시지의 위치를 나타내는 숫자야. 컨슈머는 자신이 어디까지 읽었는지를 오프셋으로 추적하지.
아키텍처 구성요소를 보면:
- 프로듀서 → 메시지를 브로커로 전송
- 브로커(Broker) → 메시지를 저장하고 컨슈머에게 전달하는 서버
- 코디네이터(Coordinator) → 컨슈머 그룹 관리, 파티션 할당 등 메타데이터 관리 (주키퍼 역할)
- 메타데이터 저장소 → 토픽, 파티션, 컨슈머 그룹 정보 등
"메시지 큐인데 디스크에 저장한다고? 느리지 않나?" 카프카의 핵심 통찰이 여기 있어.
디스크 I/O가 느린 건 랜덤 접근(random access) 때문이야. **순차 접근(sequential access)**은 메모리에 버금갈 정도로 빠르거든. 카프카는 메시지를 **로그 파일에 순차적으로 추가(append)**만 하니까 디스크의 순차 쓰기 성능을 그대로 활용하지.
여기에 OS의 페이지 캐시를 적극 활용해. OS가 디스크 데이터를 메모리에 캐싱하니까, 최근에 쓴 데이터를 읽을 때 디스크를 안 찍고 메모리에서 바로 가져와. JVM 힙 메모리를 따로 쓰지 않으니 GC 부담도 없지.
제로 카피(Zero Copy) 기법도 써. 보통 디스크 → OS 버퍼 → 애플리케이션 버퍼 → 소켓 버퍼 → 네트워크로 4단계 복사가 일어나는데, 제로 카피를 쓰면 OS 버퍼에서 바로 네트워크로 보내지. CPU 복사가 거의 없어져.
각 파티션은 디스크에 세그먼트(segment) 파일들로 저장돼. 하나의 세그먼트가 너무 커지면 새 세그먼트를 만들지. 각 세그먼트에는 데이터 파일과 오프셋 인덱스 파일이 있어.
오프셋 인덱스는 "오프셋 N번의 메시지는 데이터 파일의 어느 위치에 있다"를 알려줘. 모든 오프셋을 다 기록하는 건 아니고, 일정 간격으로 기록하는 **희소 인덱스(sparse index)**야. 원하는 오프셋을 찾을 때 이진 탐색으로 가까운 인덱스 엔트리를 찾고, 거기서부터 순차 스캔하지.
프로듀서 흐름을 보면:
- 프로듀서가 토픽과 메시지를 지정해서 전송
- 라우팅 레이어가 메시지 키를 해싱해서 어느 파티션으로 보낼지 결정 (키가 없으면 라운드로빈)
- 해당 파티션의 리더 브로커에게 전송
- 리더가 메시지를 로그에 추가하고, **복제본(replica)**에 전파
- 충분한 수의 복제본이 확인(ack)하면 프로듀서에게 성공 응답
ack 설정이 성능과 내구성의 트레이드오프를 결정해.
ack=0: 확인 안 기다림 → 가장 빠르지만 메시지 유실 가능ack=1: 리더만 확인 → 리더가 죽으면 유실 가능ack=all: 모든 ISR(In-Sync Replica)이 확인 → 가장 안전하지만 지연 증가
컨슈머 흐름은:
- 컨슈머가 컨슈머 그룹에 가입
- 코디네이터가 파티션을 컨슈머에게 할당(assignment)
- 컨슈머가 할당된 파티션에서 오프셋 기준으로 메시지를 가져옴(pull)
- 메시지 처리 후 오프셋을 커밋(commit) — "여기까지 읽었다"
오프셋 커밋이 중요한 이유 — 컨슈머가 죽었다가 다시 살아나면 마지막 커밋된 오프셋부터 다시 읽기 시작하거든. 커밋을 너무 빨리 하면 메시지 유실, 너무 늦게 하면 중복 소비가 발생할 수 있어.
컨슈머 그룹에 컨슈머가 추가되거나 빠지면 파티션 할당을 재조정해야 해. 이걸 **리밸런싱(rebalancing)**이라고 하지.
코디네이터가 컨슈머들의 하트비트를 모니터링하다가 하트비트가 끊기면 해당 컨슈머가 죽은 것으로 판단하고 리밸런싱을 트리거해. 리밸런싱 동안에는 메시지 소비가 일시 중단될 수 있으니 빈번한 리밸런싱은 피해야 하지.
각 파티션은 여러 브로커에 **복제(replication)**돼. 하나가 리더, 나머지가 팔로워야. 리더에게 쓰기가 발생하면 팔로워가 가져가서 동기화.
ISR(In-Sync Replicas) — 리더와 싱크가 맞는 복제본들의 집합이야. 리더가 죽으면 ISR 중 하나가 새 리더가 돼. ISR에서 빠진 복제본은 리더가 될 수 없으니 데이터 유실을 방지하지.
분산 메시지 큐는 현대 분산 시스템의 접착제 같은 존재야. 서비스 간 비동기 통신, 이벤트 소싱, 로그 집계, 스트림 처리 등 거의 모든 곳에서 쓰이지. 이 장에서 다룬 파티셔닝, 복제, 오프셋 관리 같은 개념은 6장(광고 클릭 이벤트 집계), 12장(전자 지갑) 등에서도 반복적으로 등장해.
정리
4장 읽고 기억할 거 세 가지:
- 카프카 스타일의 메시지 큐는 디스크 순차 쓰기 + 페이지 캐시 + 제로 카피로 높은 처리량을 달성해. "디스크 = 느리다"는 선입견을 깨는 설계지.
- 파티션이 병렬성의 단위이고, 같은 컨슈머 그룹 내에서 한 파티션은 한 컨슈머에게만 할당돼. 파티션 수가 최대 병렬 소비 수를 결정하지.
- ack 설정이 처리량과 내구성의 트레이드오프를 결정해.
ack=all이면 안전하지만 느리고,ack=0이면 빠르지만 유실 가능. 비즈니스 요구에 맞게 선택해야 하지.