큐(Queue) 완벽 정리: 자료구조에서 Kafka까지
비동기 처리에 큐가 왜 필요한지부터 SQS와 Kafka의 차이까지, 실무 관점에서 큐를 정리했다.
큐(Queue) 완벽 정리: 자료구조에서 Kafka까지
실무에서 큐를 왜 쓰나
실무에서 큐를 쓰는 이유는 단순하다. 현실의 시스템에서는 요청이 들어오는 속도와 처리하는 속도가 일정하지 않기 때문이다. 큐는 이 불균형을 완충해주는 버퍼 역할을 한다.
핵심 효용 3가지
- 비동기 처리: 사용자에게 즉시 응답하고, 무거운 작업은 뒤에서 천천히 처리. 예) 회원가입 후 환영 이메일 발송
- 부하 분산: 트래픽 스파이크가 발생해도 컨슈머는 자기 페이스대로 처리. 예) 티켓팅 폭주
- 결합도 낮추기: 생산자는 컨슈머의 존재를 몰라도 됨. 컨슈머가 죽어도 생산자는 영향 없음
실무 활용 사례
메시지 큐: 서비스 간 비동기 통신
MSA에서 가장 많이 사용하는 패턴이다. 서비스 A가 서비스 B를 직접 HTTP로 호출하면 B가 죽었을 때 A도 함께 영향을 받는다. 중간에 메시지 큐를 두면 이 의존성이 끊어진다.
대표 시나리오: 주문 시스템
- 사용자가 '주문하기' 버튼 클릭 → 주문 서비스가 주문 정보를 DB에 저장하고 큐에 메시지 발행
- 결제 서비스, 재고 서비스, 알림 서비스, 배송 서비스가 각각 큐를 구독하며 자기 일만 처리
- 알림 서비스가 잠깐 죽어도 주문 자체는 정상 진행됨
대표 솔루션: Apache Kafka, RabbitMQ, AWS SQS, Google Pub/Sub, Redis Streams
작업 큐 (Job Queue): 무거운 백그라운드 작업
API 응답 시간을 빠르게 유지하면서 시간이 오래 걸리는 작업을 처리해야 할 때 사용한다.
- 이미지 / 동영상 인코딩
- PDF, 엑셀 리포트 생성 후 이메일 발송
- 머신러닝 모델 추론 / 배치 학습
- 대량 푸시 알림, SMS, 이메일 발송
- 외부 API 호출이 필요한 작업 (Webhook 재시도 포함)
대표 솔루션: Celery (Python), Sidekiq (Ruby), Bull / BullMQ (Node.js), Spring Batch (Java)
이벤트 스트리밍
- 사용자 클릭스트림 → 추천 / 광고 / 분석 시스템에 동시 전달
- CDC (Change Data Capture): DB 변경 사항을 검색 인덱스, 캐시, 데이터 웨어하우스에 동기화
- 실시간 모니터링 / 이상 탐지 시스템의 입력 소스
웹 서버 내부: Request Queue
Nginx, Tomcat, Node.js 이벤트 루프 모두 들어온 요청을 큐에 담아두고 워커 스레드가 꺼내 처리한다. 'Connection Pool', 'Thread Pool'은 큐로 관리한다.
운영체제 / CPU 스케줄링
프로세스 스케줄러는 실행 가능한 프로세스를 'Ready Queue'에 보관한다. 프린터의 인쇄 작업 큐, 디스크 I/O 요청 큐도 모두 같은 원리이며, BFS도 큐를 사용한다.
큐의 변형들
| 종류 | 특징 | 실무 활용 예시 |
|---|---|---|
| 우선순위 큐 (Priority Queue) | 우선순위가 높은 항목부터 처리. 내부적으로 힙(Heap) 구조 사용 | VIP 고객 요청 우선 처리, 응급 알림, Dijkstra 최단 경로 알고리즘 |
| 덱 (Deque) | 양쪽 끝에서 모두 삽입/삭제 가능 | LRU 캐시 구현, 슬라이딩 윈도우 알고리즘, 브라우저 히스토리 |
| 원형 큐 (Circular Queue) | 고정 크기 배열을 원형으로 사용. 메모리 효율적 | 네트워크 패킷 버퍼, 임베디드 시스템, 스트리밍 버퍼 |
| 지연 큐 (Delay Queue) | 일정 시간 후에야 메시지가 소비 가능해짐 | 결제 후 30분 미입금 시 자동 취소, 예약 알림, 재시도 백오프 |
| Dead Letter Queue (DLQ) | 처리에 실패한 메시지를 별도로 모아두는 큐 | 실패 원인 분석, 수동 재처리, 장애 알림 트리거 |
왜 비동기 처리에 큐를 쓰는가
비동기란?
비동기는 "나중에 처리해도 되는 일" 을 뜻한다. 회원가입 직후 환영 이메일 발송 예시를 보면:
- 동기: 이메일 발송이 끝날 때까지 사용자가 빈 화면을 보며 기다림 (3초)
- 비동기: 일단 "가입 완료!" 응답부터 주고, 이메일은 뒤에서 처리 (0.1초)
그럼 "뒤에서 처리"라는 과정을 큐가 해결해준다.
큐 없이 비동기를 하면 생기는 문제
문제 1. 작업을 어디에 보관할 것인가
비동기로 넘긴 작업은 누군가 처리할 때까지 어딘가에 적혀 있어야 한다. 그냥 메모리 변수에 두면 서버 재시작 시 작업이 증발한다. 큐는 디스크에 영속적으로 저장해준다.
문제 2. 누가 처리할지 어떻게 정하나
워커가 5대 있을 때 같은 메시지를 5대가 동시에 보내면 안 된다. 큐는 "한 메시지는 한 워커만 가져간다"를 보장한다 (락, ack 메커니즘).
문제 3. 처리 속도가 다를 때
회원가입은 초당 1000건이 들어오는데, 이메일 발송은 초당 100건만 가능하다면? 큐가 없으면 워커 과부하 → 다운 → 작업 유실. 큐가 있으면 9000건이 큐에 쌓여 워커가 자기 페이스대로 천천히 소진한다. 이게 버퍼(완충) 역할이다.
문제 4. 실패하면?
이메일 서버가 잠깐 죽었을 때 재시도해야 하는데 작업이 어디 있나? 큐에 있으면 "처리 실패 → 큐에 다시 넣기"가 가능하다.
비동기에 필요한 모든 것을 큐가 해결
| 비동기에 필요한 것 | 큐가 제공하는 것 |
|---|---|
| 작업을 보관할 장소 | 영속적 저장소 |
| 처리할 워커 분배 | 메시지 단일 소비 보장 |
| 속도 차이 흡수 | 버퍼링 |
| 실패 시 재시도 | DLQ, 재처리 메커니즘 |
| 순서 보장 (필요시) | FIFO 보장 |
카프카도 큐인가?
본질적으로는 큐가 맞다. 하지만 "전통적인 메시지 큐"와는 다른, 큐의 진화형이다.
카프카는 "큐"보다 "분산 로그(Log)"
카프카 공식 문서에서도 자기 자신을 "메시지 큐"가 아니라 "분산 이벤트 스트리밍 플랫폼" 또는 "분산 커밋 로그" 라고 부른다.
전통 큐(RabbitMQ, SQS)의 동작
[메시지 A] → 큐에 들어감 → 컨슈머가 가져감 → 큐에서 삭제됨
메시지가 소비되면 사라진다.
카프카의 동작
[메시지 A] → 로그에 추가 → 컨슈머가 읽음 → 로그에 그대로 남음
(컨슈머는 "내가 어디까지 읽었는지"만 기억)
메시지가 읽혀도 사라지지 않는다. 일정 기간(기본 7일) 또는 용량 한도까지 보관된다. 마치 유튜브 영상처럼, 누가 보든 영상은 그대로 있고, 시청자마다 "어디까지 봤는지"만 따로 기록된다.
이 차이가 중요한 이유
1. 여러 컨슈머가 같은 메시지를 읽을 수 있음
[사용자 클릭 이벤트] → Kafka
├─→ 추천 시스템이 읽음
├─→ 광고 시스템이 읽음
├─→ 분석 시스템이 읽음
└─→ 실시간 모니터링이 읽음
2. 시간을 되돌릴 수 있음 (Replay)
- "어제 오후 2시부터 다시 읽어줘" → 가능
- "신규 컨슈머인데 처음부터 다 읽을게" → 가능
3. 압도적인 처리량
전통 큐는 보통 초당 수만 건이 한계지만, 카프카는 초당 수백만 건도 처리한다. 디스크 순차 쓰기는 메모리 랜덤 접근보다도 빠를 수 있다는 원리를 활용한다.
비교 정리
| 항목 | 전통 메시지 큐 (RabbitMQ, SQS) | 카프카 |
|---|---|---|
| 메타포 | 우편함 / 주문 전표 | 녹화 방송 / 로그북 |
| 소비 후 메시지 | 삭제됨 | 그대로 남음 |
| 여러 컨슈머가 같은 메시지 | 별도 설정 필요 | 기본 동작 |
| 과거 메시지 재처리 | 어려움 | 쉬움 |
| 처리량 | 중간 (수만 TPS) | 매우 높음 (수백만 TPS) |
| 메시지 단위 | 작업(Task) 중심 | 이벤트(Event) 중심 |
| 주 용도 | 작업 분산, RPC 대체 | 데이터 파이프라인, 이벤트 스트리밍 |
메시지 큐 vs 카프카: 상태를 누가 관리하나
핵심을 한 단어로 표현하면 상태(state)를 어디가 들고 있느냐다.
전통 메시지 큐: 큐(서버)가 상태를 들고 있음
큐: "다음에 줄 메시지는 #5번이야"
컨슈머: (그냥 받기만 함, 아무것도 기억 안 함)
- 큐가 "어디까지 줬는지"를 기억
- 컨슈머는 멍청해도 됨 (stateless) — 그냥 받아서 처리
- 메시지를 줬는데 컨슈머가 처리 실패하면 → 큐가 "ack 안 왔네, 다시 줄게"
카프카: 컨슈머가 상태를 들고 있음
카프카: (메시지 1, 2, 3, 4, 5, 6, 7, 8, 9, 10... 그냥 쌓아둠)
컨슈머 A: "나 5번까지 읽었어" (offset = 5)
컨슈머 B: "나 8번까지 읽었어" (offset = 8)
컨슈머 C: "나 처음부터 다시 읽을래" (offset = 0)
- 카프카는 그냥 데이터를 쌓아둘 뿐
- 컨슈머가 자기 offset(읽은 위치) 을 직접 관리
- 같은 메시지를 누가 어떻게 읽든 카프카는 신경 안 씀
이 차이가 만드는 시스템 성격
전통 큐 = "큐가 관리하고 컨슈머는 조회만 한다"
- 메시지 전달, 재시도, 순서 보장 다 큐가 해줌
- 큐가 부담을 다 짊어지니까 처리량 한계 있음
- 컨슈머마다 다르게 읽기 어려움
카프카 = "카프카는 넣기만 하고 컨슈머가 관리한다"
- 카프카는 그냥 디스크에 순차 추가만 함 → 빠름
- 컨슈머가 자기 책임 하에 offset 관리 → 자유도 높음
- 여러 컨슈머가 각자 다른 위치에서 읽을 수 있음
비유로 이해하기
- 전통 큐 = 도서관 사서가 책을 한 권씩 건네줌
- 사서가 "다음 사람!" 하고 다음 책을 줌
- 손님은 받기만 하면 됨
- 카프카 = 도서관 책장에 책이 꽂혀 있고 각자 책갈피로 표시
- 책은 그 자리에 쭉 있음
- 손님 A는 5페이지 책갈피, 손님 B는 8페이지 책갈피
- 책갈피를 잃어버리면 손님 본인 책임
카프카도 컨슈머 offset을 카프카 안의 특별한 토픽(
__consumer_offsets)에 저장한다. 그래서 컨슈머가 죽었다 살아나도 복구할 수 있다. 다만 그 offset의 주인은 컨슈머고, 카프카는 보관만 해주는 것이다.
왜 카프카 같은 구조가 필요한가
시나리오: 쇼핑몰의 "주문 완료" 이벤트
"주문이 완료됐다!" 라는 사실을 알아야 하는 곳:
├─ 결제 시스템 → 결제 진행
├─ 재고 시스템 → 재고 차감
├─ 배송 시스템 → 배송 준비
├─ 알림 시스템 → 카톡/SMS 발송
├─ 추천 시스템 → "이 상품 산 사람이 함께 본 상품" 학습
├─ 분석 시스템 → 매출 대시보드 업데이트
├─ 마케팅 시스템 → "구매 감사" 쿠폰 발송 트리거
├─ 회계 시스템 → 세금 계산용 데이터 적재
├─ 데이터 웨어하우스 → BI 분석용 적재
└─ 사기 탐지 시스템 → 이상 패턴 감지
하나의 사실에 10개 시스템이 반응해야 한다.
직접 호출로 풀면: Point-to-Point 안티패턴
주문 서비스가 10개 시스템을 모두 직접 호출하는 방식. 처음엔 그럴듯하지만 지옥이 펼쳐진다.
- 문제 1: 주문 서비스가 모든 시스템을 알아야 함 → 새 시스템 추가될 때마다 코드 수정
- 문제 2: 한 곳이 죽으면 전체가 영향받음 → 추천 시스템이 느리면 주문도 느려짐
- 문제 3: N×M 폭발 → 서비스 5개, 이벤트 10종이면 50개의 연결
카프카로 풀면: 한 번만 발행
주문 서비스 → Kafka에 "주문 완료" 이벤트 발행 (한 번)
│
├─→ 결제 시스템 (자기가 알아서 읽음)
├─→ 재고 시스템 (자기가 알아서 읽음)
├─→ 배송 시스템 (자기가 알아서 읽음)
└─→ ... 새 시스템도 그냥 구독만 추가
주문 서비스는 누가 듣는지 모른다. 그냥 "주문 완료됐다"고 외칠 뿐이다. 이걸 "발행-구독(Pub/Sub)" 패턴이라고 한다.
카프카가 빛나는 순간
1. 시간차 소비 — 같은 데이터를 다른 시점에 읽고 싶을 때
- 결제/배송 같은 실시간 처리 컨슈머: 즉시 읽음
- 분석 시스템: 1시간마다 배치
- 데이터 웨어하우스: 하루에 한 번 새벽
- 사기 탐지 모델: 1주일치 데이터를 한꺼번에 학습용으로
2. 새 시스템을 나중에 붙일 때 — Replay (킬러 기능)
6개월 후, 데이터 사이언스 팀이 새 추천 모델을 만들고 싶음
→ "지난 6개월치 주문 이벤트를 학습 데이터로 쓰고 싶다"
전통 큐: 데이터 다 사라짐
Kafka: 그냥 offset=0부터 다시 읽으면 됨
3. 이벤트를 진실의 원천으로 (Event Sourcing)
전통 방식: DB에 "최종 상태"만 저장
→ 사용자 잔액: 50,000원
이벤트 방식: 모든 변화를 카프카에 기록
→ +100,000 입금
→ -30,000 출금
→ -20,000 출금
→ (현재 잔액은 이걸 다 더한 결과)
이러면 "이 잔액이 왜 이렇게 됐는지"를 시간순으로 추적할 수 있고, 이벤트를 다시 재생해서 상태를 재구성할 수도 있다. 금융권에서 매우 중요하다.
4. 시스템 간 데이터 동기화 (CDC)
상품 DB 변경 → Kafka (Debezium 같은 CDC 도구가 자동으로 흘려보냄)
├─→ Elasticsearch 인덱스 업데이트
├─→ Redis 캐시 무효화
└─→ 추천 시스템 피처 업데이트
정리
| 상황 | 전통 메시지 큐로 충분 | 카프카가 필요 |
|---|---|---|
| 한 작업을 한 워커가 처리 | ✅ | (과한 도구) |
| 여러 시스템이 같은 사실에 반응 | △ (가능하지만 빡셈) | ✅ |
| 과거 데이터 재처리 필요 | ❌ | ✅ |
| 신규 시스템이 과거 데이터부터 받아야 함 | ❌ | ✅ |
| 데이터 파이프라인 (DB → 검색 → 캐시 → 분석) | ❌ | ✅ |
| 초당 수십만~수백만 이벤트 | ❌ | ✅ |
| 이벤트 자체를 진실의 원천으로 | ❌ | ✅ |
SQS vs Kafka 상세 비교
요약
- SQS: "작업을 안전하게 워커에게 분배하는 큐" — Task Queue
- Kafka: "이벤트를 영속 저장하고 여러 소비자가 공유하는 로그" — Event Log
메시지 저장 방식
SQS — 소비되면 사라짐
[메시지 A] → 큐에 들어감
↓
컨슈머가 receive() → "처리 중" 상태 (Visibility Timeout)
↓
컨슈머가 delete() 호출 → 큐에서 영구 삭제
기본 보관 기간 4일(최대 14일), 처리되면 바로 사라진다.
Kafka — 소비돼도 안 사라짐
[메시지 A] → 토픽의 파티션에 추가 (append)
↓
컨슈머 1이 읽음 → 메시지는 그대로 남아있음
컨슈머 2가 읽음 → 여전히 그대로 남아있음
↓
설정된 retention(기본 7일) 후 삭제
컨슈머 모델
SQS — Competing Consumers (경쟁 컨슈머)
┌─→ 워커 1 (메시지 A)
[큐의 메시지들] ─────├─→ 워커 2 (메시지 B)
└─→ 워커 3 (메시지 C)
여러 워커가 같은 큐에서 경쟁적으로 메시지를 가져간다. 같은 메시지를 여러 시스템이 받으려면 SNS와 결합해야 한다.
SNS ─┬─→ SQS 큐 1 → 결제 시스템
├─→ SQS 큐 2 → 알림 시스템
└─→ SQS 큐 3 → 분석 시스템
Kafka — Consumer Groups
┌─→ 컨슈머 그룹 A (결제 서비스)
[Kafka 토픽] ─────────────────├─→ 컨슈머 그룹 B (분석 서비스)
└─→ 컨슈머 그룹 C (알림 서비스)
같은 토픽을 여러 그룹이 독립적으로 소비할 수 있어 자연스럽게 fan-out이 된다.
순서 보장
SQS
- 표준 큐(Standard): 순서 보장 안 함, 처리량 무제한
- FIFO 큐: 엄격한 순서 보장, 초당 3,000 메시지 제한
Kafka
- 파티션 단위로 순서 보장
- 같은 키(예: 주문 ID)를 가진 메시지는 같은 파티션으로 → 순서 보장
- 파티션 간에는 순서 무관
처리량
| 항목 | SQS | Kafka |
|---|---|---|
| 처리량 | 표준 큐: 무제한 / FIFO: 3K TPS | 수백만 TPS |
| 지연시간 | 수십~수백 ms | 수 ms |
운영 부담
SQS — 완전 관리형 (Serverless)
서버 없음, 클러스터 관리 없음, 스케일링 자동, 백업 자동, 패치 자동. 개발자는 메시지 send/receive만 하면 끝이다.
Kafka — 직접 운영해야 함
- 브로커 클러스터 (보통 3대 이상)
- ZooKeeper 또는 KRaft 클러스터
- 토픽/파티션 설계
- 컨슈머 그룹 리밸런싱 이슈 대응
- 디스크/네트워크 모니터링
- 버전 업그레이드, 보안 (TLS, ACL)
매니지드 서비스(AWS MSK, Confluent Cloud)를 써도 SQS보다는 신경 쓸 게 많다.
비용 모델
SQS
- 요청 건수당 과금 (100만 건당 약 $0.40)
- 메시지가 적으면 거의 공짜
- 메시지가 많아질수록 비례 증가
Kafka (MSK 기준)
- 인프라(브로커, 스토리지)에 대한 시간당 과금
- 트래픽 적어도 클러스터 유지 비용 발생 (월 수백 달러부터)
- 트래픽 많을수록 단위당 효율 좋음
작은 회사: SQS가 압도적으로 저렴 대용량 처리: Kafka가 단위당 더 저렴
한눈에 보는 전체 비교
| 항목 | SQS | Kafka |
|---|---|---|
| 본질 | 분산 메시지 큐 | 분산 이벤트 로그 |
| 메시지 보관 | 처리 후 삭제 | 보관 (retention) |
| 컨슈머 모델 | 경쟁 컨슈머 | 컨슈머 그룹 + Pub/Sub |
| 여러 시스템 fan-out | SNS 결합 필요 | 기본 지원 |
| 순서 보장 | FIFO 큐만 (제한적) | 파티션 단위 |
| 처리량 | 표준은 무제한 / FIFO 3K TPS | 수백만 TPS |
| 지연시간 | 수십~수백 ms | 수 ms |
| 재처리(Replay) | 불가 | 가능 |
| 운영 부담 | 거의 없음 (Serverless) | 높음 (또는 매니지드) |
| 비용 모델 | 사용량 기반 (요청당) | 인프라 기반 (시간당) |
| 시작 비용 | $0부터 | 매니지드도 월 수백 달러 |
| 메시지 크기 | 256KB | 기본 1MB |
| AWS 종속성 | AWS 전용 | 어디서나 |
언제 무엇을 쓸까
SQS가 좋은 경우
- 단순한 작업 분산 ("이메일 보내기", "썸네일 생성")
- 트래픽이 일정하지 않고 가끔 폭주
- AWS 생태계 안에 이미 있음
- 운영 인력이 부족한 스타트업
- "한 번 처리하면 끝나는" 작업
예시: 회원가입 환영 이메일, 영수증 PDF 생성, 비동기 결제 처리, 푸시 알림
Kafka가 좋은 경우
- 같은 이벤트를 여러 시스템이 소비
- 데이터 파이프라인 (DB → 검색엔진 → 데이터웨어하우스)
- 이벤트 소싱(Event Sourcing) 아키텍처
- 초당 수만 건 이상의 이벤트
- 과거 데이터 재처리 필요
- 마이크로서비스 간 이벤트 기반 통신
예시: 사용자 클릭 스트림 분석, IoT 센서 데이터 수집, CDC, 실시간 추천 시스템 입력
현실에서는 둘 다 쓴다
큰 회사일수록 둘을 같이 사용한다. 역할이 다르니까.
사용자 클릭 → Kafka (이벤트 로그, 여러 분석 시스템이 공유)
↓
특정 분석 결과
↓
"이메일 발송 필요" → SQS (단순 작업 분산, 워커가 처리)
Kafka는 "무슨 일이 있었는지" 기록하는 데 쓰고, SQS는 "누가 이 일을 처리해줘" 라고 작업 시킬 때 쓴다.
마무리
길게 돌았지만 도구를 고르는 질문은 결국 하나로 줄어든다. 지금 풀려는 게 한 번 처리하면 끝나는 "작업"인가, 여러 시스템이 같은 사실을 각자 소비해야 하는 "이벤트"인가. 작업이면 SQS 같은 전통 큐로 충분하고, 이벤트라서 보관과 재생이 필요해지는 임계점을 넘으면 그때 Kafka를 꺼내면 된다.
이 차이를 머리로만 아는 게 아쉬워서, 결국 Kafka를 직접 만들어보는 쪽으로 넘어갔다. "컨슈머가 상태를 들고 있다"는 게 코드로 어떤 모양인지는 그 글에서 이어진다.