[대규모 시스템 설계 기초 2] 4장 분산 메시지 큐

2026. 5. 12. 20:10·Book

대규모 시스템 설계 공부하다가 생긴 궁금증들 

시작하며

《대규모 시스템 설계 기초 2》를 읽으면서 분산 메시지 큐 챕터를 정리하다 보니, 책에 나온 내용보다 오히려

"근데 이건 왜 이렇게 하는 거지?", "이게 그거랑 같은 개념인가?" 싶은 질문들이 계속 쌓였습니다.

이 글은 단순한 요약이 아니라 공부하면서 파생된 궁금증들을 하나씩 짚어보면서 정리한 글입니다.

비슷하게 공부하시는 분들께 도움이 되길 바랍니다.


메시지 큐 vs 이벤트 스트리밍 플랫폼, 뭐가 다른가?

4장 첫 주제 가장 먼저 정리가 필요했던 개념입니다.

항목메시지 큐이벤트 스트리밍 플랫폼
재소비 불가 (소비 후 삭제) 가능 (로그 보관)
소비 형태 1:1 발행-구독
순서 보장 보통 미보장 파티션 내 보장
저장 방식 메모리 기반 디스크 기반
예시 RabbitMQ, SQS Kafka, Pulsar

가장 큰 차이는 두 가지입니다. 한 번 보낸 메시지를 다시 읽을 수 있냐, 그리고 여러 소비자가 구독 형태로 메시지를 처리하냐입니다.

예를 들어 Kafka는 같은 메시지를 분석 서비스, 알림 서비스, 로그 서비스가 각각 독립적으로 읽을 수 있어요. RabbitMQ는 한 번 소비되면 사라집니다.


1단계. 요구사항 정의

기능적 요구사항

  • 텍스트 메시지만 지원
  • 생산된 순서 그대로 유지 (전통적 메시지 큐는 순서 미보장)
  • 메시지 지속성 보장 (디스크 저장, 장애 후에도 복구 가능)
  • 최소 한 번 전달 지원
  • 소비자 설정에 따라 전달 방식 선택 가능

비기능적 요구사항

  • 높은 처리량(대역폭)과 낮은 전송 지연 동시 지원
  • 수평적 확장 가능
  • 장애 내성 (고가용성)

단대단(End-to-End)이란? 책에서 "단대단 지연"이라는 표현이 나오는데, 이건 생산자가 메시지를 보내는 시점부터 소비자가 받는 시점까지의 전체 구간을 의미합니다. 중간에 거치는 네트워크, 브로커 처리, 디스크 쓰기 등을 모두 합친 시간이 짧아야 한다는 뜻이에요.


2단계. 개략적 설계

발행-구독 모델과 토픽

발행-구독 모델의 핵심은 토픽입니다. 생산자가 특정 토픽에 메시지를 발행하면, 그 토픽을 구독하는 소비자 그룹들이 메시지를 받는 구조예요.

데이터 양이 많아지면 서버 한 대로 처리가 불가능하기 때문에, 파티션 샤딩 기법으로 토픽을 여러 파티션으로 분리합니다.

 
 
Topic: orders
  Partition 0 → Broker 1
  Partition 1 → Broker 2
  Partition 2 → Broker 3
  • 같은 키를 가진 메시지 → 항상 같은 파티션 (순서 보장)
  • 키가 없는 메시지 → 무작위 파티션으로 전송
  • 파티션 내 메시지 위치는 오프셋으로 관리

소비자 그룹

같은 그룹 안의 소비자들이 파티션을 나눠서 병렬 처리합니다.

 
 
Topic: orders (Partition 0, 1, 2) 

Group A: C1→P0, C2→P1, C3→P2  ← 각각 다른 파티션 담당
Group B: C4→P0, C5→P1, C6→P2  ← Group A와 독립적으로 동일 메시지 소비

 

"같은 그룹 내 C1, C2가 동일 메시지를 읽을 수 없나요?" 맞습니다. 같은 그룹 안에서는 하나의 파티션을 한 소비자만 담당해요. C1이 P0을 맡으면 C2는 P0을 읽을 수 없습니다. 이게 결국 1:1처럼 보이지만, 같은 메시지를 여러 시스템이 처리해야 한다면 다른 그룹으로 구성하면 됩니다. Group A와 Group B는 서로 독립적으로 같은 메시지를 읽을 수 있거든요.

전체 구성요소

 
 
Application Server
  → Producer (버퍼 + 라우팅)
    → Broker 1 (Leader Partition 0) ─┐
    → Broker 2 (Leader Partition 1)  ├── 각자 팔로워에게 복제
    → Broker 3 (Leader Partition 2) ─┘
      → Consumer Group A (서비스 A)
      → Consumer Group B (서비스 B)

별도로
  → ZooKeeper / KRaft (코디네이터, 리더 선출, 메타데이터)

리더와 코디네이터는 다른 역할입니다. 리더 브로커는 특정 파티션의 읽기/쓰기를 담당하고, 코디네이터는 소비자 그룹 관리와 리밸런싱을 담당해요. 브로커 하나가 여러 파티션의 리더를 맡으면서 동시에 특정 소비자 그룹의 코디네이터 역할도 할 수 있지만, 같은 개념은 아닙니다.

파티션 복제 브로커가 별도로 분리된 게 아닙니다. 브로커 1, 2, 3이 각각 어떤 파티션의 리더이면서 동시에 다른 파티션의 팔로워예요. 리더/팔로워는 브로커의 고정 역할이 아니라 파티션 단위 역할입니다.

 
 
Broker 1: Partition 0 리더, Partition 1 팔로워, Partition 2 팔로워
Broker 2: Partition 0 팔로워, Partition 1 리더, Partition 2 팔로워
Broker 3: Partition 0 팔로워, Partition 1 팔로워, Partition 2 리더

3단계. 상세 설계

데이터 저장소 — 왜 DB가 아니라 WAL인가?

읽기/쓰기가 빈번하고, 갱신/삭제는 거의 없는 특성상 일반 DB는 오버헤드가 큽니다.

저장소문제점
RDB 빈번한 읽기/쓰기에 오버헤드, 데이터 모델 부적합
인메모리 용량 한계, 장애 시 유실
WAL 디스크 순차 쓰기로 고성능, 영속성 보장 ✅

WAL + 세그먼트 구조로 저장합니다.

 
 

 

/partition-0/
  00000000.log    ← 실제 메시지 (바이너리)
  00000000.index  ← offset → byte 위치 매핑
  00001000.log
  00001000.index

파일 하나에 계속 쌓으면 OS 파일 핸들링 한계에 부딪히고 오래된 메시지 삭제도 불가능해집니다. 세그먼트로 나누면 오래된 세그먼트를 파일 통째로 삭제할 수 있어요.

실제로 SSD인지 HDD인지, 어떻게 읽고 쓰는지? Kafka는 OS의 sendfile() 시스템 콜과 페이지 캐시를 활용합니다. 클라우드 환경(AWS MSK 등)은 대부분 SSD 기반이에요. 쓰기는 Java의 FileChannel.write()로 순차 append하고, 읽기는 transferTo()(제로카피)를 사용합니다. 제로카피는 디스크→커널 버퍼→네트워크로 유저 공간을 거치지 않아 복사 횟수를 줄여 처리량을 높입니다.

페이지 캐시 활용도 중요한 포인트입니다.

 
 
생산자가 방금 쓴 메시지
→ 소비자가 바로 읽으면 디스크 접근 없이 OS 페이지 캐시에서 서빙
→ 디스크 I/O 최소화

Kafka가 JVM 힙 메모리를 작게 쓰는 이유도 이 때문이에요. 대신 OS 페이지 캐시를 최대한 활용합니다.

메시지 구조

필드설명
topic 메시지가 속한 토픽
partition 파티션 번호
offset 파티션 내 위치
timestamp 생성 시각
key 파티션 결정용
value 실제 데이터 (바이너리)
size 메시지 크기
CRC 무결성 검사

CRC 종류가 여러 가지인데 Kafka는 뭘 쓰나요? Kafka는 CRC32C를 사용합니다. 패리티 검사, 해밍 코드 같은 건 오류 정정(FEC)용이라 목적이 달라요. CRC는 오류 탐지만 하고, 해밍 코드는 탐지+정정까지 해요. 메시지 전송 무결성 확인용으로는 CRC32C가 속도와 신뢰성 균형이 좋습니다.

생산자 측 작업 흐름

 
 
Producer
  → 메시지 직렬화 (바이너리)
  → 키 해싱으로 파티션 결정 (hash(key) % partitionCount)
  → 사본 분산 계획 캐시 조회 (어느 브로커가 리더?)
  → 리더 브로커에 직접 전송
  → 버퍼에 모아서 배치로 전송

메시지 키 해싱은 누가 처리하나요? 브로커가 아니라 **생산자(Producer)**가 처리합니다. 생산자 내부 라우팅 로직에서 파티션을 결정한 후 해당 파티션의 리더 브로커로 직접 전송해요. 브로커는 그냥 받아서 저장만 합니다.

버퍼가 어떻게 네트워크를 거칠 필요가 없다는 건가요? 원래 라우팅 계층이 별도 서버로 존재하면 생산자→라우터→브로커 2번 네트워크를 거쳐야 해요. 라우팅 로직을 생산자 내부로 편입시키면 생산자→브로커 1번만 거치면 됩니다. 버퍼는 그 내부에서 메시지를 모아서 한 번에 배치 전송하는 역할이에요. 즉 버퍼는 일괄처리(배치)를 위해 두는 것입니다.

 
 
linger.ms 높임 → 더 많이 모아서 전송 → 처리량 증가, 지연 증가
linger.ms 낮춤 → 바로 전송              → 처리량 감소, 지연 감소

소비자 측 작업 흐름

방식설명단점
푸시 브로커가 소비자에게 밀어넣음 소비자 처리 속도보다 빠르면 부하 폭증
풀 소비자가 직접 요청 메시지 없어도 계속 폴링 → 자원 낭비
롱폴링 메시지 생길 때까지 연결 유지 풀의 자원 낭비 문제 해결 ✅

Kafka는 풀 + 롱폴링 방식을 채택합니다.

컴퓨터 자원을 어떻게 조절하나요? 소비자 쪽에서 max.poll.records, fetch.max.bytes 같은 설정으로 한 번에 가져올 메시지 양을 조절해요. 브로커는 각 소비자 그룹별로 오프셋만 관리하고, 실제 처리 속도 조절은 소비자가 담당합니다. 이게 풀 모델의 핵심 장점인 백프레셔(backpressure) 제어예요.

소비자 재조정 (Rebalancing)

소비자가 연결이 끊기거나 새로 추가되면 파티션을 재배치합니다.

 
 
[재조정 흐름]
1. Group Coordinator가 이상 감지 (하트비트 없음)
2. 모든 소비자에게 JoinGroup 요청
3. 리더 소비자가 파티션 재할당 결정
4. Coordinator가 각 소비자에게 결과 통보
5. onPartitionsRevoked() → onPartitionsAssigned() 콜백

주의할 점은 재조정 중에는 전체 소비가 잠시 중단된다는 거예요. CooperativeStickyAssignor를 사용하면 영향받는 파티션만 점진적으로 재조정해서 이 문제를 최소화할 수 있습니다.

상태 저장소와 메타데이터 저장소

상태 저장소 — 소비자별 파티션 오프셋 정보 저장

  • 지속적 읽기/쓰기 → 데이터 일관성 중요
  • 구버전: ZooKeeper에 저장
  • 현재: __consumer_offsets 토픽 (Kafka 내부)으로 이전

메타데이터 저장소 — 토픽 설정, 파티션 수, 복제 설정 등 저장

  • 변경 빈도 낮고 일관성 중요 → ZooKeeper 또는 KRaft 사용

주키퍼가 계층적이라서 뭐가 좋은 건가요? 파일 시스템처럼 /brokers/ids/1, /topics/orders/partitions/0 형태로 경로 구조를 갖고 있어요. 덕분에 특정 경로에 watch를 걸어 변경사항을 이벤트로 받을 수 있습니다. 브로커가 죽으면 해당 경로가 사라지고, watch 중인 컴포넌트가 즉시 감지하는 방식이에요.

etcd가 Kubernetes에서 쓰는 etcd랑 같은 건가요? 네, 완전히 같은 etcd입니다. 분산 키-값 저장소로 클러스터 상태, 리더 선출, 서비스 탐색 등에 사용돼요. Kubernetes는 클러스터 상태 관리에, Kafka는 브로커 상태/메타데이터 관리에 동일한 etcd를 사용할 수 있습니다.

복제 (Replication)

하드웨어 장애로 데이터가 사라지는 것을 방지하기 위해 복제를 사용합니다.

 
 
Partition 0
  Broker 1: Leader  ← 읽기/쓰기 모두 여기서
  Broker 2: Follower (ISR)
  Broker 3: Follower (ISR)

ack 모드

모드설명신뢰성속도
acks=0 응답 안 기다림 낮음 가장 빠름
acks=1 리더만 저장하면 OK 중간 중간
acks=all ISR 전체 저장해야 OK 높음 느림

ISR(In-Sync Replica)은 리더와 동기화 상태인 복제본 목록입니다. 팔로워가 느리거나 끊기면 ISR에서 제거되고, 리더 장애 시 ISR 중 하나가 새 리더로 선출됩니다.

실제 운영에서는 어떻게 처리하나요? 대부분 acks=all + min.insync.replicas=2 조합을 씁니다. ISR이 2개 이상일 때만 쓰기를 허용해서 리더 장애 시 데이터 손실을 막아요. 결제 같은 민감한 시스템은 여기에 exactly-once까지 추가합니다.

메시지 전달 방식

방식유실중복사용 사례
최대 한 번 (At Most Once) 가능 없음 모니터링, 로그
최소 한 번 (At Least Once) 없음 가능 주문, 알림
정확히 한 번 (Exactly Once) 없음 없음 결제, 재고
 
 
java
// 최소 한 번
process(record);       // 처리 먼저
commitOffset();        // 그 다음 커밋 (장애 시 재처리 발생 가능)

// 정확히 한 번
producer.initTransactions();
producer.beginTransaction();
producer.send(...);
consumer.commitSync();
producer.commitTransaction();  // 전송+커밋 원자적 처리

실무에서는 대부분 최소 한 번 + 멱등성 처리(중복 방지 로직) 조합을 씁니다. 정확히 한 번은 성능 비용과 구현 복잡도가 큽니다.


세그먼트라는 단어, 어디서나 보이는데 같은 개념인가?

공부하다 보면 세그먼트라는 단어가 여러 곳에서 나옵니다. 헷갈릴 수 있어서 정리해봤어요.

맥락세그먼트 의미레벨
CPU 8086 레지스터 16비트 레지스터로 20비트 주소 접근 하드웨어
OS 메모리 관리 Code/Data/Stack/Heap 영역 분리 OS
TCP 네트워크 전송 단위 패킷 네트워크 4계층
Kafka 로그 파일 분할 단위 애플리케이션

전부 **"나눈다"**는 의미에서 같은 단어를 차용한 것이고, 기술적으로 같은 개념은 아닙니다. 세그먼트(Segment)의 영어 뜻 자체가 "조각, 구간"이라 분야마다 가져다 쓰는 거예요.

마찬가지로 오프셋도 맥락마다 다르게 사용됩니다. Kafka에서 오프셋은 메시지의 고유 순번이고, 세그먼트 파일명이 시작 오프셋이라 어느 파일에서 찾아야 할지 바로 알 수 있어 O(1)에 가까운 탐색이 가능합니다.


바이너리 저장 vs 세그먼트, 같은 개념인가?

이것도 헷갈렸던 부분입니다. 같은 단어를 다른 형태로 많이 사용해서 헷갈렸습니다.

바이너리는 저장 포맷, 세그먼트는 파일 관리 전략으로 독립적인 개념입니다.

Kafka는 두 가지를 함께 씁니다. 바이너리로 직렬화한 메시지를 세그먼트 파일에 저장하는 구조예요.

바이너리 직렬화 포맷 비교

포맷특징
Protocol Buffers (protobuf) Google 개발, JSON 대비 3~10배 작음, 스키마 필요
Apache Avro Kafka와 많이 사용, 스키마 함께 저장
MessagePack JSON을 바이너리로 압축한 형태

파일 관리 전략 비교

방식대표 사례특징
단일 파일 Redis AOF 단순하지만 무한정 커짐
롤링 파일 Logback 날짜/크기 기준 교체
세그먼트 Kafka offset 기반 탐색 최적화
스냅샷 Redis RDB 특정 시점 전체 상태 저장
WAL PostgreSQL 장애 복구용
LSM Tree Cassandra 쓰기 최적화

모니터링은 풀 모델인가, 푸시 모델인가?

책 내용과 연결되는 내용이라 같이 정리했습니다.

  • Prometheus → 풀(Pull) 모델. 주기적으로 각 서비스의 /metrics 엔드포인트를 직접 scrape
  • Loki → 푸시(Push) 모델. Promtail 같은 에이전트가 로그를 Loki로 밀어넣음
  • Grafana → 둘 다 아님. 시각화 도구로 Prometheus/Loki에서 데이터를 조회해서 표시

OpenTelemetry(OTel)는 둘 다 지원합니다.

 
 
서비스 → (push) → OTel Collector → (pull) → Prometheus
                                 → (push) → Loki
                                 → (push) → Jaeger (트레이싱)

풀 모델이 부하 제어에 유리한 이유는 수집기가 scrape 간격을 조절할 수 있기 때문입니다. OTel Collector가 중간에서 버퍼링/배치 처리도 해줘서 부하를 완화합니다.


배운 점

공부하면서 느낀 건, 개념을 단순히 외우는 것보다 "왜 이렇게 설계했을까"를 따라가는 게 훨씬 기억에 오래 남는다는 점입니다.

세그먼트를 왜 쓰는지, 버퍼가 왜 필요한지, 풀/푸시 중 왜 풀을 선택했는지를 이해하면 비슷한 상황에서 스스로 판단할 수 있게 됩니다.


마치며

분산 메시지 큐는 처음에는 개념이 많아서 복잡하게 느껴지지만, 전체 흐름을 한 번 잡고 나면 각 컴포넌트가 왜 그 자리에 있는지가 보이기 시작합니다.

이 글이 같은 책을 공부하시는 분들께 도움이 됐으면 좋겠습니다. 잘못된 내용이나 추가할 내용이 있으면 편하게 댓글로 알려주세요!


참고

  • 《대규모 시스템 설계 기초 2》 4장 분산 메시지 큐
  • Apache Kafka Documentation
  • Kafka KRaft Mode

'Book' 카테고리의 다른 글

[대규모 시스템 설계2] 5장 지표 모니터링 및 경보 시스템  (0) 2026.06.03
[대규모 시스템 설계 기초] 4장 처리율 제한 장치의 설계  (0) 2025.05.06
[대규모 시스템 설계 기초] 3장 시스템 설계 면접 공략법  (0) 2025.04.23
[대규모 시스템 설계 기초] 2장 개략적인 규모 추정  (0) 2025.04.11
[대규모 시스템 설계 기초] 1장 사용자 수에 따른 규모 확장성  (0) 2025.04.06
'Book' 카테고리의 다른 글
  • [대규모 시스템 설계2] 5장 지표 모니터링 및 경보 시스템
  • [대규모 시스템 설계 기초] 4장 처리율 제한 장치의 설계
  • [대규모 시스템 설계 기초] 3장 시스템 설계 면접 공략법
  • [대규모 시스템 설계 기초] 2장 개략적인 규모 추정
Ry-
Ry-
  • Ry-
    developer_Ryu
    Ry-
  • 전체
    오늘
    어제
    • 분류 전체보기 (96)
      • AI (14)
      • DB (3)
      • Book (6)
      • Development Environment (2)
        • Mac (2)
      • Frontend (8)
        • Flutter (6)
        • React (1)
        • React-Native (1)
      • Backend (19)
        • Spring (14)
        • Java (1)
        • Node.js (4)
      • Computer Science (27)
        • PS (20)
        • CS (7)
      • Network (12)
      • 이것저것 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      java
      spring
      오블완
      종만북
      스프링
      flutter
      mongodb
      chatgpt api
      database
      redis
      aop
      mysql
      Kakao
      BOGGLE
      몽고디비
      백준
      티스토리챌린지
      구글 로그인
      최대 힙
      Baekjoon
      node.js
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    Ry-
    [대규모 시스템 설계 기초 2] 4장 분산 메시지 큐
    상단으로

    티스토리툴바