Redis - 인메모리 데이터 저장소의 모든 것
TL;DR
- Redis - 인메모리 데이터 저장소의 모든 것의 핵심 개념을 빠르게 파악할 수 있다.
- 등장 배경과 채택 이유를 통해 왜 필요한지 맥락을 이해할 수 있다.
- 주요 특징과 상세 내용을 통해 실무 적용 포인트를 확인할 수 있다.
1. 개념
Redis - 인메모리 데이터 저장소의 모든 것의 핵심 정의와 문제 공간을 간단히 정리한다.
2. 배경
이 주제가 등장한 기술적·조직적 배경과 기존 접근의 한계를 설명한다.
3. 이유
왜 지금 이 방식을 채택해야 하는지, 기대 효과와 트레이드오프를 함께 정리한다.
4. 특징
핵심 동작 방식, 장단점, 적용 시 주의점을 빠르게 훑을 수 있도록 요약한다.
5. 상세 내용
Redis - 인메모리 데이터 저장소의 모든 것
작성일: 2026-02-26 카테고리: Backend / Database / Cache 포함 내용: Redis, In-Memory Database, Cache, Persistence, RDB, AOF, Pub/Sub, Sentinel, Cluster, Session, 캐싱 전략, Sorted Set, Valkey
1. Redis란? (완전 기초부터)
1.1 Redis = Remote Dictionary Server
┌─────────────────────────────────────────────────────────────────┐
│ Redis란 무엇인가? │
│ │
│ Redis = Remote Dictionary Server │
│ = 원격 사전 서버 │
│ = "데이터를 메모리에 저장하는 초고속 저장소" │
│ │
│ 만든 사람: │
│ ├── Salvatore Sanfilippo (별명: antirez) │
│ ├── 이탈리아 개발자 │
│ └── 2009년에 첫 릴리스 │
│ │
│ 공식 정의: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "In-Memory Data Structure Store" │ │
│ │ = 인메모리 자료구조 저장소 │ │
│ │ │ │
│ │ 풀어서 설명하면: │ │
│ │ ├── In-Memory: 데이터를 RAM(메모리)에 저장 │ │
│ │ ├── Data Structure: 다양한 자료구조 지원 │ │
│ │ │ (문자열, 리스트, 집합, 해시, 정렬 집합 등) │ │
│ │ └── Store: 데이터를 저장하고 조회하는 저장소 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 핵심 특징: │
│ ├── 싱글 스레드 기반 (명령어 하나씩 순서대로 처리) │
│ ├── 초당 10만~20만 건 처리 가능 │
│ ├── 밀리초(ms)가 아닌 마이크로초(μs) 단위 응답 │
│ └── C언어로 작성되어 매우 가벼움 │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 초보자를 위한 비유
┌─────────────────────────────────────────────────────────────────┐
│ Redis를 일상생활로 이해하기 │
│ │
│ 메모리(RAM)와 디스크의 차이: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ RAM = 책상 위 메모장 │ │
│ │ (빠르지만 전원 끄면 사라짐) │ │
│ │ │ │
│ │ 디스크 = 서랍 속 노트 │ │
│ │ (느리지만 전원 꺼도 남음) │ │
│ │ │ │
│ │ Redis = 책상 위 메모장 │ │
│ │ + 주기적으로 서랍에 백업하는 시스템 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 다른 DB와 비교: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MySQL/PostgreSQL │ │
│ │ = 도서관 서고 │ │
│ │ (정리 잘 되어있고 영구 보존) │ │
│ │ (찾으려면 서고까지 걸어가야 함 → 느림) │ │
│ │ │ │
│ │ Redis │ │
│ │ = 책상 위 즐겨찾기 목록 │ │
│ │ (자주 보는 것만 빠르게 접근) │ │
│ │ (공간이 제한적 → 모든 것을 올릴 수 없음) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 속도 차이를 체감으로 이해하기: │
│ ├── 디스크에서 읽기: 사무실까지 걸어가서 서류 가져오기 │
│ ├── 메모리에서 읽기: 눈 앞 메모장에서 바로 읽기 │
│ └── 차이: 약 10,000배 (만 배!) │
│ │
└─────────────────────────────────────────────────────────────────┘
1.3 “Redis는 DB인가?”
┌─────────────────────────────────────────────────────────────────┐
│ Redis는 DB냐 아니냐? │
│ │
│ 답: DB도 맞고, 캐시도 맞고, 메시지 브로커도 맞음! │
│ │
│ Redis의 세 가지 얼굴: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 데이터베이스(DB)로서의 Redis │ │
│ │ └── RDB/AOF로 데이터를 영구 저장 가능 │ │
│ │ │ │
│ │ 2. 캐시(Cache)로서의 Redis │ │
│ │ └── 자주 조회하는 데이터를 메모리에 임시 저장 │ │
│ │ │ │
│ │ 3. 메시지 브로커로서의 Redis │ │
│ │ └── Pub/Sub, Stream으로 실시간 메시지 전달 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 전통적 DB(MySQL)와 Redis의 목적 차이: │
│ ├── MySQL: 모든 데이터를 영구히 저장하는 것이 목적 │
│ │ └── 안전성이 최우선, 속도는 그 다음 │
│ ├── Redis: 빠른 접근이 목적, 영속성은 선택사항 │
│ │ └── 속도가 최우선, 안전성은 선택 │
│ └── 둘은 대체 관계가 아니라 보완 관계! │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MySQL = 은행 금고 │ │
│ │ (안전하지만 매번 금고실 가야 해서 느림) │ │
│ │ │ │
│ │ Redis = 지갑 │ │
│ │ (빠르지만 들어가는 양이 제한적) │ │
│ │ │ │
│ │ → 큰 돈은 금고에, 자주 쓸 돈만 지갑에 │ │
│ │ → 중요한 데이터는 MySQL에, 자주 볼 데이터는 Redis에 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
2. Redis의 탄생 배경
2.1 전통적 DB의 한계
┌─────────────────────────────────────────────────────────────────┐
│ 전통적 데이터베이스의 한계 │
│ │
│ 핵심 문제: 디스크 I/O 병목 (Disk I/O Bottleneck) │
│ │
│ 속도 비교: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 디스크 읽기: 약 1ms (1 밀리초) │ │
│ │ 메모리 읽기: 약 0.0001ms (100 나노초) │ │
│ │ │ │
│ │ 차이: 약 10,000배! │ │
│ │ │ │
│ │ 비유로 이해: │ │
│ │ ├── 메모리 읽기 = 1초 걸리는 일이라면 │ │
│ │ └── 디스크 읽기 = 약 2.8시간 걸리는 일 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 트래픽 폭증 시 문제: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 평소: 100명 접속 → DB 여유있게 처리 │ │
│ │ 이벤트: 10,000명 동시 접속 → DB가 먼저 죽음! │ │
│ │ │ │
│ │ 사용자 → 웹서버 → DB (여기서 병목!) │ │
│ │ ↑ 웹서버는 확장 쉬움 (서버 추가) │ │
│ │ ↑ DB는 확장 어려움 (디스크 한계) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: 고속도로 병목 │
│ ├── 고속도로(네트워크): 차선 넓혀서 확장 가능 │
│ ├── 톨게이트(디스크 I/O): 여기서 차가 막힘! │
│ └── 해결: 하이패스(메모리 캐시)로 톨게이트 우회 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.2 Memcached의 등장과 한계 (2003)
┌─────────────────────────────────────────────────────────────────┐
│ Memcached - Redis의 선배 (2003) │
│ │
│ 만든 사람: Brad Fitzpatrick │
│ 배경: LiveJournal (초기 SNS) 서비스 운영 │
│ 목적: DB 부하를 줄이기 위한 인메모리 캐시 │
│ │
│ Memcached의 특징: │
│ ├── 최초의 대중적 인메모리 캐시 시스템 │
│ ├── key-value 방식 (키로 값을 저장/조회) │
│ ├── 매우 빠른 읽기/쓰기 │
│ └── Facebook, Twitter 등 대규모 서비스에서 사용 │
│ │
│ Memcached의 한계: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. key-value만 지원 │ │
│ │ └── 문자열 값만 저장 가능 │ │
│ │ └── 리스트, 집합 같은 자료구조 없음 │ │
│ │ │ │
│ │ 2. 영속성(Persistence) 없음 │ │
│ │ └── 재시작하면 모든 데이터 소멸 │ │
│ │ └── 디스크에 저장하는 기능 자체가 없음 │ │
│ │ │ │
│ │ 3. 복제(Replication) 없음 │ │
│ │ └── 서버 하나 죽으면 그 데이터 소멸 │ │
│ │ └── 고가용성(HA) 구성 불가 │ │
│ │ │ │
│ │ 4. 단순 문자열만 │ │
│ │ └── 숫자도 문자열로 저장 → 서버에서 변환 필요 │ │
│ │ └── 복잡한 데이터 구조 표현 불가 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 결론: 단순 캐시로는 훌륭하지만, 그 이상은 불가능 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.3 Salvatore Sanfilippo의 동기 (2009)
┌─────────────────────────────────────────────────────────────────┐
│ antirez는 왜 Redis를 만들었나? │
│ │
│ 배경 상황: │
│ ├── Salvatore Sanfilippo (antirez) │
│ ├── LLOOGG.com 이라는 실시간 웹 분석 서비스를 운영 │
│ └── 웹사이트 방문자를 실시간으로 추적하고 분석하는 서비스 │
│ │
│ 겪은 문제들: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. MySQL로 실시간 로그 분석이 너무 느렸음 │ │
│ │ └── 매 초마다 수천 건의 로그를 DB에 쓰고 읽어야 함 │ │
│ │ └── MySQL은 이런 실시간 처리에 부적합 │ │
│ │ │ │
│ │ 2. Memcached는 자료구조가 없어서 불편 │ │
│ │ └── "최근 방문자 10명"을 보여주려면 리스트가 필요 │ │
│ │ └── "방문 횟수 순위"를 보여주려면 정렬 집합이 필요 │ │
│ │ └── Memcached는 문자열만 지원 → 직접 구현해야 함 │ │
│ │ │ │
│ │ 3. 원하는 것: │ │
│ │ ├── 메모리 속도 (Memcached처럼 빠르게) │ │
│ │ ├── 다양한 자료구조 (리스트, 집합, 정렬된 집합...) │ │
│ │ └── 선택적 영속성 (필요하면 디스크에도 저장) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 그래서 직접 만들었다! │
│ ├── C언어로 작성 (최대 성능) │
│ ├── 싱글 스레드 이벤트 루프 (단순하고 안정적) │
│ ├── 다양한 자료구조 내장 │
│ └── 2009년, Redis 첫 릴리스 │
│ │
└─────────────────────────────────────────────────────────────────┘
2.4 Redis 성장 타임라인
┌─────────────────────────────────────────────────────────────────┐
│ Redis의 성장 역사 (2009~현재) │
│ │
│ 2009 ── Redis 첫 릴리스 (antirez 개인 프로젝트) │
│ │ └── 기본 자료구조: String, List, Set, Hash │
│ │ │
│ 2010 ── VMware 스폰서십 │
│ │ └── antirez가 풀타임으로 Redis 개발에 집중 │
│ │ │
│ 2013 ── Redis Sentinel 출시 │
│ │ └── 고가용성(HA) 지원: 마스터 장애 시 자동 전환 │
│ │ └── 모니터링, 알림, 자동 페일오버 │
│ │ │
│ 2015 ── Redis Cluster 출시 │
│ │ └── 수평 확장(Horizontal Scaling) 지원 │
│ │ └── 데이터를 여러 노드에 분산 저장 │
│ │ └── 16,384개의 해시 슬롯으로 데이터 분배 │
│ │ │
│ 2018 ── Redis Streams 출시 │
│ │ └── 이벤트 스트리밍 기능 (Kafka 경량 대체) │
│ │ └── 컨슈머 그룹 지원 │
│ │ │
│ 2020 ── Redis Labs → Redis Inc. 사명 변경 │
│ │ └── 상업화 본격화 │
│ │ │
│ 2024 ── 라이선스 변경 사건! │
│ │ ├── BSD(자유 라이선스) → SSPL(제한적 라이선스) │
│ │ ├── 클라우드 업체의 무임승차 방지 목적 │
│ │ └── 커뮤니티 반발 → Valkey 포크 탄생 │
│ │ │
│ 2024 ── Valkey 등장 │
│ ├── Linux Foundation이 관리하는 Redis 포크 │
│ ├── AWS, Google, Oracle 등이 지원 │
│ └── BSD 라이선스 유지 (완전한 오픈소스) │
│ │
│ 교훈: 오픈소스 라이선스 변경은 커뮤니티 분열을 초래할 수 있음 │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 전원이 꺼지면? - 영속성(Persistence)
3.0 핵심 질문
┌─────────────────────────────────────────────────────────────────┐
│ "껐다가 켜지면 무슨 일이 발생해?" │
│ │
│ 이것이 Redis에서 가장 자주 나오는 질문이다. │
│ │
│ Redis는 메모리에 데이터를 저장한다. │
│ 메모리는 전원이 꺼지면 내용이 사라진다. │
│ 그러면... Redis 데이터는 전부 날아가는 건가? │
│ │
│ 답: 설정에 따라 다르다! │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설정 없음 → 전부 날아감 (순수 캐시 모드) │ │
│ │ RDB 설정 → 마지막 스냅샷까지 복구 │ │
│ │ AOF 설정 → 거의 모든 데이터 복구 (최대 1초 유실) │ │
│ │ RDB + AOF 혼합 → 가장 안전한 복구 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
3.1 아무 설정도 안 했을 때
┌─────────────────────────────────────────────────────────────────┐
│ 영속성 설정 없음 = 순수 캐시 모드 │
│ │
│ 동작: │
│ ├── 메모리에만 데이터 저장 │
│ ├── 서버 재시작 시 모든 데이터 소멸 │
│ └── 빈 상태로 시작 │
│ │
│ 이게 괜찮은 경우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Redis를 순수 캐시로만 사용할 때 │ │
│ │ │ │
│ │ 왜 괜찮냐면: │ │
│ │ ├── 원본 데이터는 MySQL/PostgreSQL에 있음 │ │
│ │ ├── Redis에는 "복사본"만 올려놓은 것 │ │
│ │ ├── Redis가 죽어도 원본은 안전 │ │
│ │ ├── Redis 재시작 후 다시 캐시가 채워짐 │ │
│ │ └── 잠깐 느려질 뿐 데이터 유실은 없음 │ │
│ │ │ │
│ │ 비유: 포스트잇이 떨어져도 원본 문서는 있으니까 OK │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 이게 문제인 경우: │
│ ├── Redis에만 있는 데이터 (세션, 장바구니 등) │
│ ├── 재시작하면 모든 사용자 로그아웃 │
│ └── 장바구니 내역 전부 소멸 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 RDB (Redis Database) - 스냅샷 방식
┌─────────────────────────────────────────────────────────────────┐
│ RDB = 스냅샷 (사진 찍기) │
│ │
│ 비유: │
│ ├── 게임의 "세이브 포인트" │
│ └── 특정 시점에 전체 상태를 사진 찍듯 저장 │
│ │
│ 동작 원리: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 설정된 조건을 만족하면 자동 저장 │ │
│ │ 예: save 900 1 │ │
│ │ = "900초(15분) 내에 1번 이상 변경이 있으면 저장" │ │
│ │ │ │
│ │ 2. Redis가 fork()로 자식 프로세스를 생성 │ │
│ │ └── 부모: 계속 클라이언트 요청 처리 │ │
│ │ └── 자식: 메모리 내용을 dump.rdb 파일로 저장 │ │
│ │ │ │
│ │ 3. COW (Copy-On-Write) 메커니즘 활용 │ │
│ │ └── fork 시 메모리를 복사하지 않음 │ │
│ │ └── 변경이 발생한 페이지만 복사 │ │
│ │ └── 메모리 효율적! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ RDB 저장 과정 시각화: │
│ │
│ 시간 ──────────────────────────────────────► │
│ │ │
│ │ [데이터 변경] [데이터 변경] [스냅샷!] │
│ │ ↓ ↓ ↓ │
│ │ 메모리에만 메모리에만 dump.rdb에 │
│ │ 반영 반영 전체 저장 │
│ │ │
│ │ ← 이 구간의 데이터는 스냅샷 전이면 유실 가능 → │
│ │
│ 설정 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ save 900 1 # 15분 내 1번 변경 시 저장 │ │
│ │ save 300 10 # 5분 내 10번 변경 시 저장 │ │
│ │ save 60 10000 # 1분 내 10,000번 변경 시 저장 │ │
│ │ │ │
│ │ → 변경이 많을수록 자주 저장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 빠른 복구 (바이너리 파일 통째로 로드) │
│ ├── 작은 파일 크기 (압축된 바이너리) │
│ ├── 백업 편리 (파일 하나만 복사하면 됨) │
│ └── 성능 영향 적음 (자식 프로세스가 처리) │
│ │
│ 단점: │
│ ├── 마지막 스냅샷 이후 데이터 유실 가능! │
│ │ └── 예: 5분마다 스냅샷 → 최대 5분치 데이터 유실 │
│ ├── fork() 시 메모리 사용량 일시적 증가 │
│ └── 데이터가 클수록 스냅샷 시간 증가 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.3 AOF (Append Only File) - 로그 기록 방식
┌─────────────────────────────────────────────────────────────────┐
│ AOF = 모든 명령을 기록하는 가계부 │
│ │
│ 비유: │
│ ├── 가계부처럼 모든 거래를 순서대로 기록 │
│ ├── 은행 거래 내역서처럼 모든 변경사항을 시간순 저장 │
│ └── RDB가 "사진"이라면 AOF는 "일기" │
│ │
│ 동작 원리: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 모든 쓰기 명령을 파일에 순서대로 추가(append) │ │
│ │ │ │
│ │ SET name "kim" → 파일에 기록 │ │
│ │ INCR counter → 파일에 기록 │ │
│ │ LPUSH list "hello" → 파일에 기록 │ │
│ │ DEL temp → 파일에 기록 │ │
│ │ │ │
│ │ 복구 시: 파일의 명령을 처음부터 끝까지 재실행 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 설정: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ appendonly yes # AOF 활성화 │ │
│ │ │ │
│ │ appendfsync 옵션: │ │
│ │ ├── always : 매 명령마다 디스크에 기록 │ │
│ │ │ (가장 안전, 가장 느림) │ │
│ │ │ 데이터 유실: 0 │ │
│ │ │ │ │
│ │ ├── everysec : 매 1초마다 디스크에 기록 (★ 권장) │ │
│ │ │ (안전과 성능의 균형) │ │
│ │ │ 최대 데이터 유실: 1초 │ │
│ │ │ │ │
│ │ └── no : OS가 알아서 기록 (보통 30초) │ │
│ │ (가장 빠름, 위험) │ │
│ │ 최대 데이터 유실: ~30초 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ AOF Rewrite (자동 압축): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: 시간이 지나면 AOF 파일이 계속 커짐 │ │
│ │ │ │
│ │ 예: 카운터를 100번 증가시켰다면 │ │
│ │ 기존 AOF: │ │
│ │ INCR counter (1번째) │ │
│ │ INCR counter (2번째) │ │
│ │ ... (100줄) │ │
│ │ INCR counter (100번째) │ │
│ │ │ │
│ │ Rewrite 후: │ │
│ │ SET counter 100 (1줄로 압축!) │ │
│ │ │ │
│ │ → 최종 결과만 남기고 중간 과정을 제거 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 데이터 유실 최소화 (최대 1초, everysec 기준) │
│ ├── 사람이 읽을 수 있는 텍스트 형식 │
│ ├── 잘못된 명령 제거 후 복구 가능 │
│ └── append만 하므로 쓰기 성능 좋음 │
│ │
│ 단점: │
│ ├── RDB보다 파일이 큼 │
│ ├── 복구 시간이 RDB보다 느림 (명령을 하나씩 재실행) │
│ └── 쓰기 부하가 높으면 성능 영향 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.4 RDB + AOF 혼합 (Redis 4.0+, 권장)
┌─────────────────────────────────────────────────────────────────┐
│ RDB + AOF 혼합 모드 (★ 가장 권장) │
│ │
│ Redis 4.0부터 지원하는 최적의 영속성 전략 │
│ │
│ 원리: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ AOF 파일 안에 RDB 스냅샷을 앞부분에 넣고, │ │
│ │ 그 이후 발생한 명령만 AOF 방식으로 추가 기록 │ │
│ │ │ │
│ │ AOF 파일 구조: │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ [RDB 스냅샷 데이터] (바이너리) │ ← 빠른 로드 │ │
│ │ │ ─────────────────────────────── │ │ │
│ │ │ SET key1 "val1" │ │ │
│ │ │ INCR counter │ ← 추가 명령들 │ │
│ │ │ LPUSH list "item" │ │ │
│ │ └──────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 장점: │
│ ├── 빠른 복구 (앞부분 RDB를 한 번에 로드) │
│ ├── 세밀한 데이터 보존 (뒷부분 AOF로 추가분 재생) │
│ └── 두 방식의 장점만 조합! │
│ │
│ 설정: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ appendonly yes │ │
│ │ aof-use-rdb-preamble yes # 혼합 모드 활성화 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ├── RDB만 = 매일 사진 한 장 (중간에 뭔 일 있었는지 모름) │
│ ├── AOF만 = 매 순간 일기 (처음부터 다 읽어야 해서 느림) │
│ └── 혼합 = 사진 + 사진 이후의 일기 (빠르고 정확) │
│ │
└─────────────────────────────────────────────────────────────────┘
3.5 재시작 시 복구 과정
┌─────────────────────────────────────────────────────────────────┐
│ Redis 재시작 시 데이터 복구 과정 │
│ │
│ Redis 서버가 재시작되면 어떤 순서로 데이터를 복구할까? │
│ │
│ 복구 판단 플로우: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Redis 재시작 │ │
│ │ │ │ │
│ │ ├── AOF 활성화되어 있나? │ │
│ │ │ │ │ │
│ │ │ ├── YES → AOF 파일로 복구 (더 정확하므로) │ │
│ │ │ │ ├── 혼합 모드 → RDB 부분 로드 후 │ │
│ │ │ │ │ AOF 명령 재생 │ │
│ │ │ │ └── 순수 AOF → 명령 처음부터 재생 │ │
│ │ │ │ │ │
│ │ │ └── NO → RDB 파일 있나? │ │
│ │ │ │ │ │
│ │ │ ├── YES → dump.rdb 로드 (빠름) │ │
│ │ │ └── NO → 빈 상태로 시작 │ │
│ │ │ │ │
│ │ └── 둘 다 없으면 → 빈 상태로 시작 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 AOF를 우선하나? │
│ ├── AOF가 RDB보다 더 최신 데이터를 가지고 있을 확률이 높음 │
│ ├── RDB: 마지막 스냅샷 시점 (수 분 전일 수 있음) │
│ └── AOF: 마지막 fsync 시점 (1초 전일 수 있음) │
│ │
│ 복구 시간 비교: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 방식 │ 10GB 데이터 복구 시간 (대략) │ │
│ │ ─────────────┼────────────────────────────────── │ │
│ │ RDB │ 10~20초 (바이너리 로드) │ │
│ │ AOF (순수) │ 수 분 (명령 하나씩 재실행) │ │
│ │ AOF (혼합) │ 10~20초 + 추가분 재생 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 실무 권장: │
│ ├── 캐시 전용 → 영속성 끄기 (가장 빠름) │
│ ├── 일반 용도 → RDB + AOF 혼합 모드 (★ 권장) │
│ └── 절대 유실 불가 → AOF always + 복제(Replication) 구성 │
│ │
└─────────────────────────────────────────────────────────────────┘
4. Redis의 자료구조 - 단순 캐시가 아닌 이유
┌─────────────────────────────────────────────────────────────────┐
│ Redis가 "자료구조 저장소"인 이유 │
│ │
│ Memcached: key-value만 (문자열만 저장 가능) │
│ Redis: 다양한 자료구조를 네이티브로 지원! │
│ │
│ 이것이 Redis를 "단순 캐시"가 아닌 │
│ "인메모리 자료구조 저장소"로 만드는 핵심 차이점 │
│ │
│ 지원하는 자료구조: │
│ ├── String : 문자열 (가장 기본) │
│ ├── List : 순서 있는 목록 │
│ ├── Set : 중복 없는 집합 │
│ ├── Sorted Set: 점수로 정렬된 집합 (★ 킬러 기능) │
│ ├── Hash : 필드-값 쌍의 객체 │
│ ├── Stream : 이벤트 로그 스트림 │
│ ├── HyperLogLog: 근사 카운팅 │
│ ├── Bitmap : 비트 단위 연산 │
│ └── Geospatial: 위치 기반 검색 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.1 String (문자열) - 가장 기본
┌─────────────────────────────────────────────────────────────────┐
│ String - 가장 기본 자료구조 │
│ │
│ 가장 단순하면서도 가장 많이 쓰이는 타입 │
│ │
│ 기본 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SET name "김철수" # 값 저장 │ │
│ │ GET name # 값 조회 → "김철수" │ │
│ │ │ │
│ │ SET counter 10 # 숫자도 문자열로 저장 │ │
│ │ INCR counter # 원자적 1 증가 → 11 │ │
│ │ INCRBY counter 5 # 원자적 5 증가 → 16 │ │
│ │ DECR counter # 원자적 1 감소 → 15 │ │
│ │ │ │
│ │ SETNX key "value" # key 없을 때만 저장 │ │
│ │ SETEX key 60 "value" # 60초 후 자동 삭제 │ │
│ │ MSET k1 "v1" k2 "v2" # 여러 키 동시 저장 │ │
│ │ MGET k1 k2 # 여러 키 동시 조회 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 특징: │
│ ├── 최대 크기: 512MB │
│ ├── 바이너리 안전 (이미지, 직렬화된 객체도 저장 가능) │
│ └── INCR/DECR은 원자적(atomic) → 동시성 문제 없음 │
│ │
│ "원자적"이란? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 여러 클라이언트가 동시에 INCR counter 해도 │ │
│ │ 값이 꼬이지 않음 (Redis가 하나씩 순서대로 처리) │ │
│ │ │ │
│ │ MySQL에서는 SELECT → 계산 → UPDATE 하는 사이에 │ │
│ │ 다른 요청이 끼어들 수 있음 (락 필요) │ │
│ │ Redis는 싱글 스레드라 자동으로 안전 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주요 용도: │
│ ├── 캐싱: DB 조회 결과를 임시 저장 │
│ ├── 카운터: 조회수, 좋아요 수, API 호출 횟수 │
│ └── 세션 토큰: 로그인 토큰 저장 및 만료 관리 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.2 List (리스트) - 순서가 있는 목록
┌─────────────────────────────────────────────────────────────────┐
│ List - 순서가 있는 목록 │
│ │
│ 비유: 양쪽에서 넣고 뺄 수 있는 줄 서기 │
│ (왼쪽에서도 넣고 오른쪽에서도 넣을 수 있는 대기열) │
│ │
│ 기본 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ LPUSH mylist "a" # 왼쪽(앞)에 추가: [a] │ │
│ │ LPUSH mylist "b" # 왼쪽에 추가: [b, a] │ │
│ │ RPUSH mylist "c" # 오른쪽(뒤)에 추가: [b, a, c] │ │
│ │ │ │
│ │ LPOP mylist # 왼쪽에서 꺼냄: "b" → [a, c] │ │
│ │ RPOP mylist # 오른쪽에서 꺼냄: "c" → [a] │ │
│ │ │ │
│ │ LRANGE mylist 0 -1 # 전체 조회: ["a"] │ │
│ │ LRANGE mylist 0 2 # 0~2번 인덱스 조회 │ │
│ │ LLEN mylist # 리스트 길이 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 시각화: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ LPUSH ← [b] [a] [c] → RPUSH │ │
│ │ LPOP ← [b] [a] [c] → RPOP │ │
│ │ │ │
│ │ 왼쪽(L)에서 넣고 오른쪽(R)에서 빼면 = 큐(Queue) │ │
│ │ 왼쪽(L)에서 넣고 왼쪽(L)에서 빼면 = 스택(Stack) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주요 용도: │
│ ├── 메시지 큐: 작업을 순서대로 처리 │
│ ├── 최근 활동 목록: "최근 본 상품 10개" │
│ ├── 타임라인: SNS 피드, 채팅 메시지 목록 │
│ └── 작업 대기열: 백그라운드 작업 관리 │
│ │
│ 실전 예시 - 최근 본 상품: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 사용자가 상품을 볼 때마다 │ │
│ │ LPUSH recent:user123 "상품A" │ │
│ │ LTRIM recent:user123 0 9 # 최근 10개만 유지 │ │
│ │ │ │
│ │ # 최근 본 상품 조회 │ │
│ │ LRANGE recent:user123 0 9 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.3 Set (집합) - 중복 없는 모임
┌─────────────────────────────────────────────────────────────────┐
│ Set - 중복 없는 집합 │
│ │
│ 비유: 출석부 (같은 이름이 두 번 적히지 않음) │
│ │
│ 기본 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SADD myset "a" # 추가: {a} │ │
│ │ SADD myset "b" "c" # 추가: {a, b, c} │ │
│ │ SADD myset "a" # 중복! 무시됨: {a, b, c} │ │
│ │ │ │
│ │ SMEMBERS myset # 전체 조회: {a, b, c} │ │
│ │ SISMEMBER myset "a" # 포함 여부: 1 (true) │ │
│ │ SCARD myset # 원소 개수: 3 │ │
│ │ SREM myset "b" # 삭제: {a, c} │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 집합 연산 (★ Set의 진짜 힘): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SADD set1 "a" "b" "c" │ │
│ │ SADD set2 "b" "c" "d" │ │
│ │ │ │
│ │ SINTER set1 set2 # 교집합: {b, c} │ │
│ │ SUNION set1 set2 # 합집합: {a, b, c, d} │ │
│ │ SDIFF set1 set2 # 차집합: {a} (set1에만 있는 것) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주요 용도: │
│ ├── 태그 시스템: 게시물에 달린 태그 관리 │
│ ├── 고유 방문자: 오늘 방문한 사용자 ID 기록 (중복 자동 제거) │
│ ├── 공통 친구 계산: SINTER 으로 두 사용자의 공통 친구 찾기 │
│ └── 추천 시스템: "A를 좋아하는 사람이 좋아하는 다른 것" │
│ │
│ 실전 예시 - 공통 친구: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SADD friends:kim "이영희" "박민수" "정대리" │ │
│ │ SADD friends:lee "박민수" "정대리" "최사원" │ │
│ │ │ │
│ │ SINTER friends:kim friends:lee │ │
│ │ → {"박민수", "정대리"} # 공통 친구! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.4 Sorted Set (정렬된 집합) - Redis의 킬러 기능
┌─────────────────────────────────────────────────────────────────┐
│ Sorted Set - Redis의 킬러 기능 (★) │
│ │
│ 비유: 자동으로 점수순 정렬되는 성적표 │
│ (새 점수를 넣으면 자동으로 순위가 재배치됨) │
│ │
│ Set + Score = Sorted Set │
│ ├── Set처럼 중복 없음 │
│ ├── 각 원소에 점수(score)가 붙음 │
│ └── 점수 기준으로 자동 정렬 │
│ │
│ 기본 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ZADD leaderboard 100 "kim" # kim: 100점 │ │
│ │ ZADD leaderboard 200 "lee" # lee: 200점 │ │
│ │ ZADD leaderboard 150 "park" # park: 150점 │ │
│ │ │ │
│ │ # 점수 낮은 순 (오름차순) │ │
│ │ ZRANGE leaderboard 0 -1 WITHSCORES │ │
│ │ → kim(100), park(150), lee(200) │ │
│ │ │ │
│ │ # 점수 높은 순 (내림차순) ← 리더보드에 주로 사용 │ │
│ │ ZREVRANGE leaderboard 0 -1 WITHSCORES │ │
│ │ → lee(200), park(150), kim(100) │ │
│ │ │ │
│ │ ZRANK leaderboard "park" # 순위 조회: 1 (0부터) │ │
│ │ ZREVRANK leaderboard "park" # 역순위: 1 │ │
│ │ ZSCORE leaderboard "kim" # 점수 조회: 100 │ │
│ │ ZINCRBY leaderboard 50 "kim" # 점수 증가: 150 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 "킬러 기능"인가? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 시간복잡도: O(log N) │ │
│ │ │ │
│ │ 이게 무슨 뜻이냐면: │ │
│ │ ├── 데이터 100만 건: log2(1,000,000) ≈ 20번 연산 │ │
│ │ ├── 데이터 1억 건: log2(100,000,000) ≈ 27번 연산 │ │
│ │ ├── 사실상 데이터 양에 관계없이 즉시 응답! │ │
│ │ └── 100만 건 리더보드 조회: 약 0.1ms │ │
│ │ │ │
│ │ MySQL로 같은 일을 하면? │ │
│ │ SELECT * FROM scores ORDER BY score DESC LIMIT 10 │ │
│ │ → 인덱스 있어도 수~수십 ms │ │
│ │ → 인덱스 없으면 수 초! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주요 용도: │
│ ├── 리더보드/순위표: 게임 랭킹, 점수판 │
│ ├── 범위 검색: 특정 점수 구간의 데이터 조회 │
│ ├── 우선순위 큐: 점수가 우선순위인 작업 대기열 │
│ └── 타임라인 정렬: 타임스탬프를 점수로 사용 │
│ │
│ 내부 구조: Skip List │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Sorted Set은 내부적으로 Skip List를 사용 │ │
│ │ │ │
│ │ Level 3: [1] ──────────────────────── [9] │ │
│ │ Level 2: [1] ──────── [5] ──────── [9] │ │
│ │ Level 1: [1] → [3] → [5] → [7] → [9] │ │
│ │ │ │
│ │ → 상위 레벨에서 큰 보폭으로 건너뛰며 탐색 │ │
│ │ → 이진 검색처럼 O(log N) 달성 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.5 Hash (해시) - 객체 저장
┌─────────────────────────────────────────────────────────────────┐
│ Hash - 객체(Object) 저장 │
│ │
│ 비유: 이름표에 여러 정보를 적는 것 │
│ (이름, 나이, 직업을 한 장에 적기) │
│ │
│ Hash = field-value 쌍의 모음 (Python dict, JS object와 유사) │
│ │
│ 기본 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 사용자 정보 저장 │ │
│ │ HSET user:1001 name "김철수" │ │
│ │ HSET user:1001 age 28 │ │
│ │ HSET user:1001 job "개발자" │ │
│ │ │ │
│ │ # 한 번에 여러 필드 저장 │ │
│ │ HSET user:1001 name "김철수" age 28 job "개발자" │ │
│ │ │ │
│ │ HGET user:1001 name # "김철수" │ │
│ │ HGETALL user:1001 # 모든 필드-값 조회 │ │
│ │ HDEL user:1001 job # 특정 필드 삭제 │ │
│ │ HEXISTS user:1001 age # 필드 존재 여부 │ │
│ │ HINCRBY user:1001 age 1 # 숫자 필드 증가 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 시각화: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ key: "user:1001" │ │
│ │ ┌────────────┬────────────┐ │ │
│ │ │ field │ value │ │ │
│ │ ├────────────┼────────────┤ │ │
│ │ │ name │ "김철수" │ │ │
│ │ │ age │ 28 │ │ │
│ │ │ job │ "개발자" │ │ │
│ │ └────────────┴────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 String 대신 Hash를 쓰나? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ String 방식: │ │
│ │ SET user:1001 '{"name":"김철수","age":28}' │ │
│ │ → 나이만 바꾸려면 전체를 읽고 → 파싱 → 수정 → 저장 │ │
│ │ │ │
│ │ Hash 방식: │ │
│ │ HINCRBY user:1001 age 1 │ │
│ │ → 나이 필드만 직접 수정! (네트워크, CPU 모두 절약) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주요 용도: │
│ ├── 사용자 프로필: 이름, 이메일, 설정 등 │
│ ├── 상품 정보: 이름, 가격, 재고 등 │
│ ├── 설정 저장: 앱 설정값들을 필드별로 관리 │
│ └── 세션 데이터: 사용자 세션의 여러 속성 저장 │
│ │
└─────────────────────────────────────────────────────────────────┘
4.6 기타 자료구조
┌─────────────────────────────────────────────────────────────────┐
│ Redis의 특수 자료구조들 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 1. Stream (스트림) - 이벤트 로그 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ Kafka의 경량 대체재 │
│ ├── 이벤트를 시간순으로 저장 │
│ ├── 컨슈머 그룹 지원 (여러 소비자가 나눠서 처리) │
│ ├── 영속성 있음 (Pub/Sub과 차이점!) │
│ └── 용도: 주문 이벤트, 로그 수집, 작업 큐 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ XADD mystream * name "주문" item "노트북" │ │
│ │ XADD mystream * name "결제" amount 1500000 │ │
│ │ XRANGE mystream - + # 전체 스트림 조회 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 2. HyperLogLog - 근사 카운팅 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ "정확한 숫자는 필요 없고, 대략 몇 명인지만 알면 될 때" │
│ ├── 12KB의 고정 메모리로 수억 개의 고유값 개수를 근사 │
│ ├── 오차율: 약 0.81% │
│ ├── 용도: 일일 방문자 수(UV), 검색어 카운팅 │
│ └── 비유: 콘서트장에 "약 5만 명" 정도라고 추정하는 것 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PFADD visitors "user1" "user2" "user3" │ │
│ │ PFADD visitors "user1" # 중복은 카운트 안 됨 │ │
│ │ PFCOUNT visitors # → 3 (근사값) │ │
│ │ │ │
│ │ 1억 명의 고유 방문자를 추적해도 단 12KB! │ │
│ │ (Set으로 하면 수 GB 필요) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 3. Bitmap - 비트 단위 연산 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ 각 비트를 on/off로 사용하여 극도로 메모리 효율적 │
│ ├── 1비트 = 1개의 상태 (있다/없다) │
│ ├── 용도: 출석 체크, 일별 활성 사용자, 기능 플래그 │
│ └── 비유: 출석부에서 동그라미(O) 치는 것 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 2024년 1월 1일 출석 체크 (비트 0 = 1일, 1 = 2일...) │ │
│ │ SETBIT attendance:user1 0 1 # 1일 출석 │ │
│ │ SETBIT attendance:user1 1 0 # 2일 결석 │ │
│ │ SETBIT attendance:user1 2 1 # 3일 출석 │ │
│ │ │ │
│ │ BITCOUNT attendance:user1 # 출석 일수: 2 │ │
│ │ │ │
│ │ 365일 출석 데이터 = 단 46바이트! │ │
│ │ (사용자 100만 명 × 365일 = 약 44MB) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 4. Geospatial - 위치 기반 검색 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ "내 주변 3km 음식점 찾기" 같은 위치 기반 기능 │
│ ├── 경도/위도로 위치 저장 │
│ ├── 반경 내 검색, 거리 계산 │
│ └── 내부적으로 Sorted Set 사용 (Geohash를 score로) │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ GEOADD restaurants 126.9780 37.5665 "맛집A" │ │
│ │ GEOADD restaurants 126.9850 37.5700 "맛집B" │ │
│ │ │ │
│ │ # 현재 위치에서 3km 이내 음식점 │ │
│ │ GEOSEARCH restaurants FROMLONLAT 126.98 37.57 │ │
│ │ BYRADIUS 3 km ASC │ │
│ │ │ │
│ │ # 두 지점 사이 거리 │ │
│ │ GEODIST restaurants "맛집A" "맛집B" km │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5. 무엇을 대체하고 언제 효과적인가?
5.1 세션 관리 (서버 메모리/DB 세션 대체)
┌─────────────────────────────────────────────────────────────────┐
│ 세션 관리 - Redis의 대표 사용 사례 │
│ │
│ 문제: 서버가 여러 대일 때 세션 관리 │
│ │
│ 기존 방식과 문제점: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 방식 1: 서버 메모리에 세션 저장 │ │
│ │ │ │
│ │ 사용자 → [로드밸런서] → 서버A (세션 있음) │ │
│ │ → 서버B (세션 없음!) ← 문제! │ │
│ │ │ │
│ │ → 서버 여러 대면 세션 불일치 발생! │ │
│ │ │ │
│ │ 해결 시도: │ │
│ │ ├── Sticky Session (고정 세션) │ │
│ │ │ └── 같은 사용자 → 같은 서버로 고정 │ │
│ │ │ └── 문제: 부하 분산 제한, 서버 죽으면 세션 소멸 │ │
│ │ │ │ │
│ │ └── DB 세션 (MySQL에 세션 저장) │ │
│ │ └── 매 요청마다 DB 접근 → 느림! │ │
│ │ └── DB 부하 증가 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Redis 세션의 해결: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 사용자 → [로드밸런서] → 서버A ──┐ │ │
│ │ → 서버B ──┤ │ │
│ │ → 서버C ──┤ │ │
│ │ ▼ │ │
│ │ [ Redis ] │ │
│ │ (세션 중앙 저장소) │ │
│ │ │ │
│ │ 모든 서버가 같은 Redis에서 세션 조회 │ │
│ │ → 어느 서버로 요청이 가도 동일한 세션! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 속도 비교: │
│ ├── DB 세션 (MySQL): 약 10ms │
│ ├── Redis 세션: 약 0.1ms │
│ └── 차이: 약 100배 빠름! │
│ │
│ 구현 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 로그인 시 세션 생성 (30분 만료) │ │
│ │ HSET session:abc123 user_id 1001 │ │
│ │ HSET session:abc123 username "김철수" │ │
│ │ HSET session:abc123 role "admin" │ │
│ │ EXPIRE session:abc123 1800 # 30분 = 1800초 │ │
│ │ │ │
│ │ # 매 요청 시 세션 확인 │ │
│ │ HGETALL session:abc123 │ │
│ │ EXPIRE session:abc123 1800 # 접근 시 만료 갱신 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.2 캐싱 전략 (DB 부하 감소)
┌─────────────────────────────────────────────────────────────────┐
│ 캐싱 전략 - DB 부하를 획기적으로 줄이기 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 전략 1: Cache-Aside (Lazy Loading) - 가장 일반적 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 요청 → Redis에 있나? (캐시 히트) │ │
│ │ │ │ │
│ │ ├── YES → Redis에서 바로 반환 (빠름!) │ │
│ │ │ │ │
│ │ └── NO → DB에서 조회 (캐시 미스) │ │
│ │ → 결과를 Redis에 저장 │ │
│ │ → 결과를 클라이언트에 반환 │ │
│ │ → 다음 번엔 Redis에서 바로 반환! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: 처음엔 서고(DB)에서 책을 가져오고, │
│ 책상(Redis)에 올려놓으면 다음에는 바로 읽음 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 전략 2: Write-Through - DB 쓰기 시 캐시도 동시 업데이트 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 데이터 변경 시: │ │
│ │ 1. DB에 저장 │ │
│ │ 2. Redis에도 동시에 저장 │ │
│ │ │ │
│ │ 장점: 캐시가 항상 최신 │ │
│ │ 단점: 쓰기가 느려짐 (두 곳에 써야 하니까) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 전략 3: Write-Behind (Write-Back) - 캐시 먼저, DB는 나중에 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 데이터 변경 시: │ │
│ │ 1. Redis에 먼저 저장 (빠르게 응답) │ │
│ │ 2. 나중에 비동기로 DB에 반영 │ │
│ │ │ │
│ │ 장점: 쓰기가 매우 빠름 │ │
│ │ 단점: Redis 장애 시 데이터 유실 위험 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────── │
│ TTL (Time To Live) - 자동 만료 설정 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ SET product:1001 '{"name":"노트북"}' EX 3600 │ │
│ │ → 3600초(1시간) 후 자동 삭제 │ │
│ │ │ │
│ │ 왜 필요한가? │ │
│ │ ├── 메모리는 유한하므로 오래된 캐시를 정리해야 함 │ │
│ │ ├── DB의 원본이 바뀌었을 수 있음 (캐시 일관성) │ │
│ │ └── 적절한 TTL이 캐싱의 핵심! │ │
│ │ │ │
│ │ TTL 가이드: │ │
│ │ ├── 자주 바뀌는 데이터: 30초 ~ 5분 │ │
│ │ ├── 보통 데이터: 1시간 ~ 1일 │ │
│ │ └── 거의 안 바뀌는 데이터: 1일 ~ 1주 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ──────────────────────────────────────────────────────────── │
│ Cache Stampede (캐시 쏠림 현상) 방지 │
│ ──────────────────────────────────────────────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제 상황: │ │
│ │ 인기 상품의 캐시가 만료됨 │ │
│ │ → 동시에 1000명이 조회 │ │
│ │ → 1000번 모두 DB에 쿼리 (캐시 미스 폭주!) │ │
│ │ → DB 폭사! │ │
│ │ │ │
│ │ 해결 방법: │ │
│ │ ├── 락(Lock): 첫 요청만 DB 조회, 나머지는 대기 │ │
│ │ ├── 미리 갱신: TTL 만료 전에 미리 캐시 갱신 │ │
│ │ └── 랜덤 TTL: 만료 시간에 랜덤 값을 더해 분산 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 실시간 리더보드 (RDB 복잡한 쿼리 대체)
┌─────────────────────────────────────────────────────────────────┐
│ 실시간 리더보드 - Sorted Set의 위력 │
│ │
│ MySQL vs Redis 리더보드 비교: │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MySQL 방식: │ │
│ │ SELECT username, score │ │
│ │ FROM game_scores │ │
│ │ ORDER BY score DESC │ │
│ │ LIMIT 10; │ │
│ │ │ │
│ │ → 데이터 적으면 OK │ │
│ │ → 100만 건이면? 수백 ms ~ 수 초 소요! │ │
│ │ → 실시간 갱신이면? 매번 이 쿼리 실행? DB 부하 폭발 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Redis 방식: │ │
│ │ ZREVRANGE leaderboard 0 9 WITHSCORES │ │
│ │ │ │
│ │ → 100만 건도 약 0.1ms! │ │
│ │ → 점수 업데이트도 O(log N)으로 즉시 반영 │ │
│ │ → 실시간 순위 변동을 즉시 보여줄 수 있음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 실전 리더보드 구현: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 점수 등록/업데이트 │ │
│ │ ZADD leaderboard 2500 "player:kim" │ │
│ │ ZADD leaderboard 3200 "player:lee" │ │
│ │ ZADD leaderboard 1800 "player:park" │ │
│ │ │ │
│ │ # 상위 10명 조회 │ │
│ │ ZREVRANGE leaderboard 0 9 WITHSCORES │ │
│ │ │ │
│ │ # 특정 플레이어 순위 확인 │ │
│ │ ZREVRANK leaderboard "player:kim" → 1 (2등) │ │
│ │ │ │
│ │ # 점수 증가 (게임 중 실시간) │ │
│ │ ZINCRBY leaderboard 500 "player:kim" │ │
│ │ │ │
│ │ # "내 주변 순위" (나 기준 위아래 5명) │ │
│ │ ZREVRANGE leaderboard 등수-5 등수+5 WITHSCORES │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.4 Rate Limiting (API 호출 제한)
┌─────────────────────────────────────────────────────────────────┐
│ Rate Limiting - API 호출 횟수 제한 │
│ │
│ "1분에 최대 100번까지만 API를 호출할 수 있도록 제한" │
│ │
│ 왜 필요한가? │
│ ├── 서버 과부하 방지 │
│ ├── 악의적 사용(DDoS) 차단 │
│ ├── 공정한 자원 배분 │
│ └── API 과금 기준 │
│ │
│ 방법 1: INCR + EXPIRE (고정 윈도우) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 사용자가 API 호출할 때마다 │ │
│ │ key = "rate:user123:2024-01-15T14:30" │ │
│ │ │ │
│ │ count = INCR key # 카운터 +1 │ │
│ │ if count == 1: │ │
│ │ EXPIRE key 60 # 첫 호출이면 60초 만료 설정│ │
│ │ │ │
│ │ if count > 100: │ │
│ │ return "429 Too Many Requests" # 거부! │ │
│ │ else: │ │
│ │ return "200 OK" # 허용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 방법 2: Sorted Set (슬라이딩 윈도우, 더 정밀) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 현재 타임스탬프를 점수로 사용 │ │
│ │ now = 현재시간(밀리초) │ │
│ │ │ │
│ │ # 1분 전보다 오래된 기록 삭제 │ │
│ │ ZREMRANGEBYSCORE rate:user123 0 (now - 60000) │ │
│ │ │ │
│ │ # 현재 요청 기록 │ │
│ │ ZADD rate:user123 now now │ │
│ │ │ │
│ │ # 1분 내 요청 수 확인 │ │
│ │ count = ZCARD rate:user123 │ │
│ │ │ │
│ │ if count > 100: 거부! │ │
│ │ │ │
│ │ 장점: 경계 시점 문제 없음 (진짜 "최근 1분" 기준) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.5 Pub/Sub (실시간 메시징)
┌─────────────────────────────────────────────────────────────────┐
│ Pub/Sub - 실시간 메시지 발행/구독 │
│ │
│ Pub/Sub = Publish(발행) + Subscribe(구독) │
│ 비유: 라디오 방송 (방송국이 송출하면 듣고 있는 사람만 들음) │
│ │
│ 동작 방식: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Publisher ──PUBLISH──► [채널: chat] ──► Subscriber A │ │
│ │ (발행자) (Redis) Subscriber B │ │
│ │ Subscriber C │ │
│ │ │ │
│ │ 발행자가 메시지를 보내면 │ │
│ │ 해당 채널을 구독 중인 모든 구독자에게 전달 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 명령어: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 구독자 (터미널 A) │ │
│ │ SUBSCRIBE chat │ │
│ │ → 대기 중... "chat" 채널의 메시지를 기다림 │ │
│ │ │ │
│ │ # 발행자 (터미널 B) │ │
│ │ PUBLISH chat "안녕하세요!" │ │
│ │ → 구독자 A에게 "안녕하세요!" 전달됨 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 용도: │
│ ├── 채팅 시스템: 실시간 메시지 전달 │
│ ├── 실시간 알림: 주문 상태 변경 알림 │
│ ├── 이벤트 브로드캐스트: 설정 변경을 모든 서버에 전파 │
│ └── 실시간 대시보드: 데이터 변경을 즉시 화면에 반영 │
│ │
│ Pub/Sub vs Kafka 비교: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Redis Pub/Sub: │ │
│ │ ├── 메시지 영속성 없음! (수신자 없으면 유실) │ │
│ │ ├── 매우 가벼움, 설정 간단 │ │
│ │ └── 적합: 실시간 알림, 채팅 (유실 허용) │ │
│ │ │ │
│ │ Kafka: │ │
│ │ ├── 메시지 영속성 있음 (디스크에 저장) │ │
│ │ ├── 무거움, 설정 복잡 │ │
│ │ └── 적합: 주문 처리, 로그 (유실 불가) │ │
│ │ │ │
│ │ Redis Stream: │ │
│ │ ├── Pub/Sub + 영속성을 합친 것 │ │
│ │ └── Kafka의 경량 대안! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.6 분산 락 (Distributed Lock)
┌─────────────────────────────────────────────────────────────────┐
│ 분산 락 - 여러 서버 간 동시성 제어 │
│ │
│ 문제: 서버가 여러 대일 때 같은 자원에 동시 접근 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 예: 재고 1개 남은 상품에 2명이 동시 주문 │ │
│ │ │ │
│ │ 서버A: 재고 확인(1개) → 주문 처리 → 재고 0 │ │
│ │ 서버B: 재고 확인(1개) → 주문 처리 → 재고 -1 ← 문제! │ │
│ │ │ │
│ │ → 두 서버가 동시에 "재고 1개"를 봤기 때문! │ │
│ │ → 한 명만 주문 성공해야 하는데 둘 다 성공함 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: 화장실 잠금장치 │
│ ├── 하나의 잠금장치(락)로 한 명만 사용 가능 │
│ ├── 사용 끝나면 잠금 해제 → 다음 사람 사용 │
│ └── Redis가 모든 서버가 공유하는 "잠금장치" 역할 │
│ │
│ 기본 구현: SETNX + TTL │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # SETNX = SET if Not eXists (없을 때만 설정) │ │
│ │ │ │
│ │ # 락 획득 시도 │ │
│ │ SET lock:order:1001 "server-A" NX EX 10 │ │
│ │ # NX: 키가 없을 때만 설정 │ │
│ │ # EX 10: 10초 후 자동 해제 (안전장치) │ │
│ │ │ │
│ │ 결과: │ │
│ │ ├── OK → 락 획득 성공! 작업 수행 │ │
│ │ └── nil → 이미 다른 서버가 락 보유 → 대기 또는 실패 │ │
│ │ │ │
│ │ # 작업 완료 후 락 해제 │ │
│ │ DEL lock:order:1001 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ TTL이 중요한 이유: │
│ ├── 락을 획득한 서버가 장애로 죽으면? │
│ ├── 락이 영원히 풀리지 않음 → 데드락! │
│ └── TTL로 자동 해제되므로 데드락 방지 │
│ │
│ Redlock 알고리즘 (더 안전한 분산 락): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Redis 1대만으로는 부족할 때 (그 1대가 죽으면?) │ │
│ │ │ │
│ │ Redlock: │ │
│ │ ├── N개의 독립적인 Redis 인스턴스 사용 (보통 5개) │ │
│ │ ├── 과반수(N/2+1)에서 락 획득 성공해야 유효 │ │
│ │ ├── 1-2개 Redis가 죽어도 락이 유지됨 │ │
│ │ └── Martin Kleppmann의 비판도 있음 (완벽하진 않음) │ │
│ │ │ │
│ │ [Redis1] [Redis2] [Redis3] [Redis4] [Redis5] │ │
│ │ OK OK OK FAIL FAIL │ │
│ │ └── 3/5 성공 = 과반수 → 락 획득 성공! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6. Redis vs 전통적 DB 비교
6.1 상세 비교표
┌─────────────────────────────────────────────────────────────────┐
│ Redis vs MySQL/PostgreSQL 상세 비교 │
│ │
│ ┌──────────────┬──────────────────┬──────────────────────┐ │
│ │ 비교 │ Redis │ MySQL/PostgreSQL │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 저장 위치 │ 메모리 (RAM) │ 디스크 (SSD/HDD) │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 속도 │ ~0.1ms │ ~1-10ms │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 데이터 크기 │ RAM 크기 제한 │ 사실상 무제한 │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 영속성 │ 선택적(RDB/AOF) │ 기본 보장 │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 자료구조 │ List,Set,Hash... │ 테이블/행/열 │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 쿼리 │ 단순 명령어 │ SQL(복잡한 조회) │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 트랜잭션 │ MULTI/EXEC │ ACID 완전 지원 │ │
│ │ │ (제한적) │ │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 관계형 │ 지원 안 함 │ JOIN, FK 지원 │ │
│ ├──────────────┼──────────────────┼──────────────────────┤ │
│ │ 주 용도 │ 캐시, 세션, │ 영구 저장, │ │
│ │ │ 실시간 처리 │ 복잡한 조회 │ │
│ └──────────────┴──────────────────┴──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
6.2 각 항목 상세 설명
┌─────────────────────────────────────────────────────────────────┐
│ 각 비교 항목 상세 설명 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 1. 저장 위치 │
│ ──────────────────────────────────────────────────────────── │
│ ├── Redis: RAM에 저장 → 빠르지만 비쌈 │
│ │ └── RAM 1GB 가격 ≈ SSD 100GB 가격 │
│ └── MySQL: 디스크에 저장 → 느리지만 저렴 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 2. 속도 │
│ ──────────────────────────────────────────────────────────── │
│ ├── Redis: 0.1ms (마이크로초 단위 응답) │
│ │ └── 단순 GET/SET은 수십 마이크로초 │
│ └── MySQL: 1~10ms (밀리초 단위 응답) │
│ └── 인덱스 없는 복잡한 쿼리는 수 초도 가능 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 3. 데이터 크기 │
│ ──────────────────────────────────────────────────────────── │
│ ├── Redis: RAM 크기에 제한 │
│ │ └── 서버 RAM이 64GB면 실사용 가능량은 약 50GB 이하 │
│ │ └── 데이터가 100GB면? Cluster로 여러 대 분산 │
│ └── MySQL: 디스크만 있으면 수 TB도 가능 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 4. 트랜잭션 │
│ ──────────────────────────────────────────────────────────── │
│ ├── MySQL: ACID 완전 지원 │
│ │ ├── Atomicity: 전부 성공하거나 전부 실패 │
│ │ ├── Consistency: 항상 일관된 상태 │
│ │ ├── Isolation: 트랜잭션 간 격리 │
│ │ └── Durability: 커밋 후 영구 보존 │
│ │ │
│ └── Redis: MULTI/EXEC (제한적) │
│ ├── 명령들을 모아서 한 번에 실행은 가능 │
│ ├── 하지만 중간에 롤백 불가! │
│ └── Lua 스크립트로 복잡한 원자적 연산 가능 │
│ │
│ ──────────────────────────────────────────────────────────── │
│ 5. 관계형 (JOIN, Foreign Key) │
│ ──────────────────────────────────────────────────────────── │
│ ├── MySQL: "주문과 고객을 JOIN해서 보여줘" → 자연스러움 │
│ └── Redis: JOIN 개념 없음 → 앱에서 직접 조합해야 함 │
│ └── 복잡한 관계 데이터에는 부적합 │
│ │
└─────────────────────────────────────────────────────────────────┘
6.3 핵심 메시지: 대체가 아닌 보완!
┌─────────────────────────────────────────────────────────────────┐
│ Redis는 DB를 "대체"가 아닌 "보완"하는 것! │
│ │
│ 비유로 완벽히 이해하기: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ MySQL = 은행 금고 │ │
│ │ ├── 안전하고 체계적 │ │
│ │ ├── 모든 거래 기록이 정확하게 보존됨 │ │
│ │ └── 하지만 매번 금고실까지 가야 해서 느림 │ │
│ │ │ │
│ │ Redis = 지갑 │ │
│ │ ├── 자주 쓰는 돈만 넣어 다님 │ │
│ │ ├── 필요할 때 바로 꺼내 쓸 수 있음 │ │
│ │ └── 하지만 잃어버리면 그 돈은 사라짐 │ │
│ │ │ │
│ │ → 둘 다 필요! │ │
│ │ → 금고만 쓰면 매번 느리고 │ │
│ │ → 지갑만 쓰면 위험 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 올바른 아키텍처: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 사용자 요청 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ [애플리케이션 서버] │ │
│ │ │ │ │ │
│ │ │ ▼ │ │
│ │ │ ┌─────────┐ │ │
│ │ │ │ Redis │ ← 빠른 조회: 캐시, 세션, 리더보드 │ │
│ │ │ └─────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ MySQL/PgSQL │ ← 영구 저장: 사용자, 주문, 결제 │ │
│ │ └──────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 언제 Redis를 쓰고, 언제 MySQL을 쓸까? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Redis가 적합한 경우: │ │
│ │ ├── 자주 조회되는 데이터 캐싱 │ │
│ │ ├── 세션/토큰 관리 │ │
│ │ ├── 실시간 순위/카운터 │ │
│ │ ├── Rate Limiting │ │
│ │ └── 임시 데이터 (TTL로 자동 만료) │ │
│ │ │ │
│ │ MySQL이 적합한 경우: │ │
│ │ ├── 사용자 정보 (영구 보존 필요) │ │
│ │ ├── 주문/결제 기록 (정확성, 무결성 필수) │ │
│ │ ├── 복잡한 관계 데이터 (JOIN 필요) │ │
│ │ ├── 복잡한 검색/필터링 (SQL의 강점) │ │
│ │ └── 감사 로그 (변경 이력 추적) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 한 줄 요약: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "Redis는 MySQL의 적이 아니라 최고의 파트너이다" │ │
│ │ Redis를 도입하면 MySQL의 부하가 줄어서 │ │
│ │ MySQL도 더 잘 동작한다. 둘 다 쓰자! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
7. 고가용성과 확장
7.1 Redis Sentinel (감시자)
┌─────────────────────────────────────────────────────────────────┐
│ Redis Sentinel - 자동 장애 복구 │
│ │
│ Master-Replica 구조란? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Master (마스터): 쓰기(Write) 담당 │ │
│ │ Replica (복제본): 읽기(Read) + 백업 담당 │ │
│ │ │ │
│ │ [클라이언트] ─── 쓰기 ──→ [Master] │ │
│ │ [클라이언트] ─── 읽기 ──→ [Replica 1] │ │
│ │ [클라이언트] ─── 읽기 ──→ [Replica 2] │ │
│ │ │ │
│ │ Master → Replica로 데이터 자동 복제 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Sentinel(감시자)의 역할: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Sentinel은 Master를 계속 감시하다가 │ │
│ │ Master가 죽으면 → Replica를 Master로 승격! │ │
│ │ │ │
│ │ [Sentinel 1] ──감시──→ [Master] ← 여기가 죽으면? │ │
│ │ [Sentinel 2] ──감시──→ [Master] │ │
│ │ [Sentinel 3] ──감시──→ [Master] │ │
│ │ │ │ │
│ │ └── 3개 중 2개 이상이 "Master 죽었다"에 합의 │ │
│ │ → Replica를 새 Master로 승격! (Failover) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 경비원(Sentinel)이 사장님(Master)을 감시 │ │
│ │ → 사장님이 쓰러지면? │ │
│ │ → 경비원들이 회의: "사장님 진짜 쓰러진 거 맞지?" │ │
│ │ → 과반수 동의하면 부사장(Replica)을 사장으로 승격! │ │
│ │ │ │
│ │ 왜 과반수 합의가 필요한가? │ │
│ │ → 네트워크 일시 장애로 "거짓 알람" 방지 │ │
│ │ → 최소 3개 Sentinel 권장 (2개면 합의 불가 상황 발생) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 클라이언트는 어떻게 Master를 찾나? │
│ ├── 클라이언트가 Sentinel에게 물어봄: "현재 Master 누구야?" │
│ ├── Sentinel이 현재 Master의 주소를 알려줌 │
│ └── Master가 바뀌면 Sentinel이 클라이언트에 알림 │
│ │
└─────────────────────────────────────────────────────────────────┘
7.2 Redis Cluster (수평 확장)
┌─────────────────────────────────────────────────────────────────┐
│ Redis Cluster - 데이터를 분산 저장 │
│ │
│ 왜 Cluster가 필요한가? │
│ ├── 데이터가 한 서버 메모리에 못 들어갈 만큼 많을 때 │
│ ├── 하나의 서버로는 처리량이 부족할 때 │
│ └── 여러 서버에 데이터를 나눠 저장 (수평 확장) │
│ │
│ 데이터 분산 원리: Hash Slot │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 총 16,384개의 Hash Slot으로 나눔 │ │
│ │ │ │
│ │ 키가 어디에 저장되는가? │ │
│ │ CRC16(key) % 16384 = 슬롯 번호 │ │
│ │ → 해당 슬롯을 담당하는 노드에 저장! │ │
│ │ │ │
│ │ 예시 (3개 노드): │ │
│ │ ├── 노드A: 슬롯 0 ~ 5,460 (약 1/3) │ │
│ │ ├── 노드B: 슬롯 5,461 ~ 10,922 (약 1/3) │ │
│ │ └── 노드C: 슬롯 10,923 ~ 16,383 (약 1/3) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 도서관의 책을 여러 건물에 분산 보관: │ │
│ │ ├── 가~나로 시작하는 책 → 1호관 │ │
│ │ ├── 다~라로 시작하는 책 → 2호관 │ │
│ │ └── 마~하로 시작하는 책 → 3호관 │ │
│ │ │ │
│ │ 책을 찾으려면? │ │
│ │ → 제목 첫 글자만 보면 어느 건물인지 바로 알 수 있음! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 각 노드에 Primary + Replica 배치: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 노드A(Primary) ──복제──→ 노드D(Replica of A) │ │
│ │ 노드B(Primary) ──복제──→ 노드E(Replica of B) │ │
│ │ 노드C(Primary) ──복제──→ 노드F(Replica of C) │ │
│ │ │ │
│ │ 노드B가 죽으면? → 노드E가 새로운 Primary로 승격! │ │
│ │ │ │
│ │ 최소 6개 노드 권장 (3 Primary + 3 Replica) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Sentinel vs Cluster: │
│ ├── Sentinel: 같은 데이터를 복제 (고가용성만) │
│ └── Cluster: 데이터를 나눠 저장 (확장성 + 고가용성) │
│ │
└─────────────────────────────────────────────────────────────────┘
7.3 메모리 부족 시 - Eviction Policy
┌─────────────────────────────────────────────────────────────────┐
│ 메모리가 꽉 차면? - Eviction Policy │
│ │
│ 상황: Redis가 사용할 수 있는 메모리가 한계에 도달! │
│ → maxmemory 설정으로 최대 메모리 크기 지정 │
│ → 초과하면 "어떤 데이터를 삭제할까?"를 정책으로 결정 │
│ │
│ Eviction 정책 종류: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 정책 설명 │ │
│ │ ─────────────── ───────────────────────────────────── │ │
│ │ noeviction 에러 반환 (쓰기 거부, 데이터 보존) │ │
│ │ allkeys-lru 가장 오래 안 쓴 키 삭제 (캐시에 추천) │ │
│ │ volatile-lru TTL 설정된 키 중 오래 안 쓴 것 삭제 │ │
│ │ allkeys-random 랜덤 삭제 │ │
│ │ volatile-random TTL 설정된 키 중 랜덤 삭제 │ │
│ │ volatile-ttl TTL이 가장 짧은 키 먼저 삭제 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 냉장고가 꽉 찼을 때 정리하는 방법: │ │
│ │ │ │
│ │ noeviction = 더 이상 안 넣겠다! (거부) │ │
│ │ allkeys-lru = 가장 오래 안 먹은 것부터 버림 │ │
│ │ volatile-lru = 유통기한 있는 것 중 오래된 것 버림 │ │
│ │ allkeys-random = 아무거나 눈 감고 버림 │ │
│ │ volatile-ttl = 유통기한 제일 짧은 것부터 버림 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 실무 권장: │
│ ├── 캐시 서버: allkeys-lru (가장 일반적) │
│ ├── 세션 서버: volatile-lru 또는 volatile-ttl │
│ └── 영속 데이터: noeviction (삭제 방지, 에러로 대응) │
│ │
│ 설정 예시: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # redis.conf │ │
│ │ maxmemory 2gb │ │
│ │ maxmemory-policy allkeys-lru │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8. Spring Boot에서 Redis 사용 (실전)
8.1 의존성 추가
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot Redis 의존성 설정 │
│ │
│ build.gradle.kts: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ dependencies { │ │
│ │ // Redis 기본 연동 │ │
│ │ implementation( │ │
│ │ "org.springframework.boot:" + │ │
│ │ "spring-boot-starter-data-redis" │ │
│ │ ) │ │
│ │ │ │
│ │ // 세션을 Redis에 저장하려면 추가 │ │
│ │ implementation( │ │
│ │ "org.springframework.session:" + │ │
│ │ "spring-session-data-redis" │ │
│ │ ) │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ starter-data-redis에 포함된 것들: │
│ ├── Lettuce 클라이언트 (기본, 비동기 지원) │
│ ├── Spring Data Redis (RedisTemplate 등) │
│ └── Connection Pool 지원 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.2 설정
┌─────────────────────────────────────────────────────────────────┐
│ application.yml 설정 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # application.yml │ │
│ │ spring: │ │
│ │ data: │ │
│ │ redis: │ │
│ │ host: localhost │ │
│ │ port: 6379 │ │
│ │ password: mypassword │ │
│ │ lettuce: │ │
│ │ pool: │ │
│ │ max-active: 8 # 최대 커넥션 수 │ │
│ │ max-idle: 8 # 최대 유휴 커넥션 │ │
│ │ min-idle: 2 # 최소 유휴 커넥션 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 설정 항목 설명: │
│ ├── host: Redis 서버 주소 (운영 시 실제 서버 IP) │
│ ├── port: 기본 포트 6379 │
│ ├── password: Redis 서버 비밀번호 │
│ └── lettuce.pool: 커넥션 풀 설정 (DB 커넥션 풀과 같은 개념) │
│ │
└─────────────────────────────────────────────────────────────────┘
8.3 RedisTemplate 사용
┌─────────────────────────────────────────────────────────────────┐
│ RedisTemplate - Redis를 직접 조작하기 │
│ │
│ RedisTemplate은 Redis 명령어를 Kotlin/Java에서 │
│ 직접 사용할 수 있게 해주는 도구이다. │
│ │
│ 1단계: RedisConfig 설정 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Configuration │ │
│ │ class RedisConfig { │ │
│ │ │ │
│ │ @Bean │ │
│ │ fun redisTemplate( │ │
│ │ factory: RedisConnectionFactory │ │
│ │ ): RedisTemplate<String, Any> { │ │
│ │ return RedisTemplate<String, Any>().apply { │ │
│ │ connectionFactory = factory │ │
│ │ // 키는 문자열로 직렬화 │ │
│ │ keySerializer = │ │
│ │ StringRedisSerializer() │ │
│ │ // 값은 JSON으로 직렬화 │ │
│ │ valueSerializer = │ │
│ │ GenericJackson2JsonRedisSerializer() │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2단계: Service에서 사용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Service │ │
│ │ class UserCacheService( │ │
│ │ private val redisTemplate: │ │
│ │ RedisTemplate<String, Any> │ │
│ │ ) { │ │
│ │ // 사용자 정보를 Redis에 캐싱 (30분 만료) │ │
│ │ fun cacheUser(user: User) { │ │
│ │ redisTemplate.opsForValue() │ │
│ │ .set( │ │
│ │ "user:${user.id}", │ │
│ │ user, │ │
│ │ Duration.ofMinutes(30) │ │
│ │ ) │ │
│ │ } │ │
│ │ │ │
│ │ // Redis에서 사용자 정보 조회 │ │
│ │ fun getCachedUser(id: Long): User? { │ │
│ │ return redisTemplate.opsForValue() │ │
│ │ .get("user:$id") as? User │ │
│ │ } │ │
│ │ │ │
│ │ // 캐시 삭제 │ │
│ │ fun evictUser(id: Long) { │ │
│ │ redisTemplate.delete("user:$id") │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ opsForXxx() 메서드: │
│ ├── opsForValue(): String 타입 조작 (GET, SET) │
│ ├── opsForList(): List 타입 조작 (LPUSH, RPUSH) │
│ ├── opsForSet(): Set 타입 조작 (SADD, SMEMBERS) │
│ ├── opsForZSet(): Sorted Set 조작 (ZADD, ZRANGE) │
│ └── opsForHash(): Hash 타입 조작 (HSET, HGET) │
│ │
└─────────────────────────────────────────────────────────────────┘
8.4 @Cacheable 어노테이션 (가장 간단)
┌─────────────────────────────────────────────────────────────────┐
│ @Cacheable - 어노테이션 하나로 캐싱 완료! │
│ │
│ Spring Cache + Redis = 코드 변경 최소화로 캐싱 적용 │
│ │
│ 1단계: 캐싱 활성화 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @EnableCaching │ │
│ │ @Configuration │ │
│ │ class CacheConfig │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 2단계: Service에서 어노테이션 사용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Service │ │
│ │ class ProductService( │ │
│ │ private val productRepository: │ │
│ │ ProductRepository │ │
│ │ ) { │ │
│ │ @Cacheable( │ │
│ │ value = ["products"], │ │
│ │ key = "#id" │ │
│ │ ) │ │
│ │ fun findById(id: Long): Product { │ │
│ │ // 처음 호출: DB 조회 → Redis에 캐싱 │ │
│ │ // 두 번째 호출: Redis에서 바로 반환! │ │
│ │ return productRepository.findById(id) │ │
│ │ .orElseThrow { │ │
│ │ NotFoundException( │ │
│ │ "Product $id not found" │ │
│ │ ) │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ @CacheEvict( │ │
│ │ value = ["products"], │ │
│ │ key = "#id" │ │
│ │ ) │ │
│ │ fun update( │ │
│ │ id: Long, │ │
│ │ request: UpdateRequest │ │
│ │ ): Product { │ │
│ │ // 업데이트 시 캐시 삭제 │ │
│ │ // → 다음 조회 시 새 데이터 캐싱 │ │
│ │ val product = findById(id) │ │
│ │ product.update(request) │ │
│ │ return productRepository.save(product) │ │
│ │ } │ │
│ │ │ │
│ │ @CacheEvict( │ │
│ │ value = ["products"], │ │
│ │ allEntries = true │ │
│ │ ) │ │
│ │ fun clearAllCache() { │ │
│ │ // 전체 캐시 삭제 │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 어노테이션 정리: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Cacheable = 캐시에 있으면 캐시 반환, 없으면 실행 │ │
│ │ @CacheEvict = 캐시 삭제 (데이터 변경 시) │ │
│ │ @CachePut = 항상 실행 + 결과를 캐시에 갱신 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @Cacheable = "메모장에 있으면 그거 보고, 없으면 찾아" │ │
│ │ @CacheEvict = "메모장에서 이거 지워" (낡은 정보 삭제) │ │
│ │ @CachePut = "항상 새로 찾아서 메모장도 업데이트" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
8.5 세션 관리 (spring-session-data-redis)
┌─────────────────────────────────────────────────────────────────┐
│ Redis 세션 관리 - 서버 재시작해도 세션 유지! │
│ │
│ 설정 한 줄이면 끝: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ @EnableRedisHttpSession( │ │
│ │ maxInactiveIntervalInSeconds = 1800 // 30분 │ │
│ │ ) │ │
│ │ @Configuration │ │
│ │ class SessionConfig │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 이것만으로 해결되는 문제들: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Before (세션 in 서버 메모리): │ │
│ │ ├── 서버 재시작 → 세션 전부 날아감 (재로그인!) │ │
│ │ ├── 서버 2대 운영 → 서버A 세션이 서버B에 없음 │ │
│ │ └── 스케일 아웃 → Sticky Session 필요 (귀찮음) │ │
│ │ │ │
│ │ After (세션 in Redis): │ │
│ │ ├── 서버 재시작 → 세션 그대로 유지! │ │
│ │ ├── 서버 N대 운영 → 모두 같은 Redis에서 세션 조회 │ │
│ │ └── 스케일 아웃 → 자유롭게 서버 추가/삭제 가능! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 동작 원리: │
│ ├── HTTP 요청의 세션 ID(쿠키)로 Redis에서 세션 데이터 조회 │
│ ├── 세션 저장/수정도 자동으로 Redis에 반영 │
│ └── TTL로 세션 만료 자동 관리 │
│ │
└─────────────────────────────────────────────────────────────────┘
9. 주의사항과 안티패턴
9.1 KEYS * 명령 금지!
┌─────────────────────────────────────────────────────────────────┐
│ KEYS * 명령 - 프로덕션에서 절대 금지! │
│ │
│ KEYS * 가 위험한 이유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ KEYS * = 모든 키를 순회 (O(N)) │ │
│ │ │ │
│ │ 키가 100개 → 괜찮음 │ │
│ │ 키가 10,000개 → 좀 느려짐 │ │
│ │ 키가 100만 개 → Redis가 수 초간 멈춤! │ │
│ │ │ │
│ │ 왜 멈추나? │ │
│ │ → Redis는 단일 스레드! │ │
│ │ → KEYS * 실행하는 동안 다른 명령어 처리 불가 │ │
│ │ → 전체 서비스 장애로 이어질 수 있음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 대안: SCAN 명령 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # KEYS * 대신 SCAN 사용 │ │
│ │ SCAN 0 MATCH "user:*" COUNT 100 │ │
│ │ │ │
│ │ → 한 번에 100개씩 점진적으로 순회 │ │
│ │ → 사이사이에 다른 명령어 처리 가능 │ │
│ │ → 서비스에 영향 없음! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: 도서관에서 모든 책 목록을 한 번에 뽑기(KEYS) vs │
│ 한 서가씩 천천히 확인하기(SCAN) │
│ │
└─────────────────────────────────────────────────────────────────┘
9.2 Big Key 문제
┌─────────────────────────────────────────────────────────────────┐
│ Big Key - 하나의 거대한 키 │
│ │
│ Big Key란? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 하나의 키에 수 MB ~ 수 GB 데이터가 저장된 상태 │ │
│ │ │ │
│ │ 예시: │ │
│ │ ├── String 값이 10MB │ │
│ │ ├── List에 원소가 100만 개 │ │
│ │ ├── Set에 멤버가 50만 개 │ │
│ │ └── Hash에 필드가 100만 개 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 문제인가? │
│ ├── 읽기 시: 큰 데이터를 한 번에 전송 → 네트워크 지연 │
│ ├── 삭제 시: DEL 명령이 오래 걸려 Redis 블로킹 │
│ └── 만료 시: TTL 만료로 삭제될 때도 블로킹 발생 │
│ │
│ 대안: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 데이터를 쪼개서 여러 키로 분산: │ │
│ │ │ │
│ │ Before: user:1:followers → Set(100만 명) │ │
│ │ │ │
│ │ After: user:1:followers:1 → Set(1만 명) │ │
│ │ user:1:followers:2 → Set(1만 명) │ │
│ │ ... │ │
│ │ user:1:followers:100 → Set(1만 명) │ │
│ │ │ │
│ │ 삭제할 때는 UNLINK (비동기 삭제) 사용 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.3 Hot Key 문제
┌─────────────────────────────────────────────────────────────────┐
│ Hot Key - 특정 키에 요청이 집중 │
│ │
│ Hot Key란? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 특정 키에 요청이 극단적으로 몰리는 현상 │ │
│ │ │ │
│ │ 예시: │ │
│ │ ├── 인기 연예인의 프로필 페이지 │ │
│ │ ├── 실시간 검색어 1위 관련 데이터 │ │
│ │ ├── 한정판 상품 재고 수량 │ │
│ │ └── 초당 수만 건의 GET 요청이 하나의 키에 집중 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Cluster 환경에서 특히 문제: │
│ ├── 특정 키 → 특정 노드에만 요청 집중 │
│ ├── 나머지 노드는 한가한데 한 노드만 과부하 │
│ └── Cluster의 분산 효과가 무의미해짐 │
│ │
│ 대안: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. 키 복제: 같은 데이터를 여러 키로 분산 │ │
│ │ hot:data:1, hot:data:2, hot:data:3 │ │
│ │ → 랜덤으로 접근하여 부하 분산 │ │
│ │ │ │
│ │ 2. 로컬 캐시 병용: │ │
│ │ 앱 서버 메모리에도 짧은 TTL로 캐싱 │ │
│ │ → Redis 요청 자체를 줄임 │ │
│ │ │ │
│ │ 3. 읽기 복제본(Replica) 활용: │ │
│ │ 읽기 요청을 여러 Replica로 분산 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
9.4 메모리 계획 필수
┌─────────────────────────────────────────────────────────────────┐
│ 메모리 계획 - RAM이 생명줄! │
│ │
│ Redis는 메모리 기반 → RAM 부족 = 재앙 │
│ │
│ 주의해야 할 점들: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. OOM Kill 위험 │ │
│ │ Redis가 OS 메모리를 다 쓰면 │ │
│ │ → Linux OOM Killer가 Redis를 강제 종료! │ │
│ │ → 데이터 유실 가능! │ │
│ │ │ │
│ │ 2. Fork 시 메모리 2배 필요 │ │
│ │ RDB 저장, AOF Rewrite 시 fork() 호출 │ │
│ │ → Copy-On-Write(COW) 오버헤드 │ │
│ │ → 최악의 경우 메모리 사용량 2배! │ │
│ │ │ │
│ │ 3. 메모리 단편화 (Fragmentation) │ │
│ │ 실제 데이터보다 OS가 할당한 메모리가 더 큼 │ │
│ │ → INFO memory로 mem_fragmentation_ratio 확인 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 실무 권장: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ # 반드시 maxmemory 설정! │ │
│ │ maxmemory 2gb │ │
│ │ maxmemory-policy allkeys-lru │ │
│ │ │ │
│ │ # 실제 데이터의 2배 이상 RAM 확보 │ │
│ │ # 데이터 1GB → 서버 RAM 최소 3GB 이상 권장 │ │
│ │ # (fork COW + OS + 여유분) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: 집 크기(RAM)에 맞게 가구(데이터)를 들여놓아야 한다. │
│ 가구가 너무 많으면 집이 무너짐(OOM Kill)! │
│ │
└─────────────────────────────────────────────────────────────────┘
9.5 단일 스레드 이해
┌─────────────────────────────────────────────────────────────────┐
│ 단일 스레드인데 왜 빠른가? │
│ │
│ Redis의 핵심: 메인 처리가 단일 스레드 │
│ │
│ "스레드 하나인데 어떻게 초당 10만 건을 처리해?" │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 비밀: I/O Multiplexing (epoll/kqueue) │ │
│ │ │ │
│ │ 비유: 라면 가게 요리사 1명 │ │
│ │ ├── 요리사 1명이 냄비 100개를 동시에 관리 │ │
│ │ ├── 냄비마다 물 끓는 시간이 다름 │ │
│ │ ├── 물이 끓은 냄비부터 처리 (이벤트 기반) │ │
│ │ └── 한 냄비 앞에서 멍하니 기다리지 않음! │ │
│ │ │ │
│ │ 기술적으로: │ │
│ │ ├── 네트워크 I/O를 논블로킹으로 처리 │ │
│ │ ├── epoll(Linux)/kqueue(macOS)로 이벤트 감지 │ │
│ │ └── 준비된 요청만 골라서 처리 → 대기 시간 없음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 단일 스레드의 장점: │
│ ├── 락(Lock)이 불필요 → 오버헤드 없음 │
│ ├── 컨텍스트 스위칭 없음 → CPU 낭비 없음 │
│ ├── 코드가 단순 → 버그가 적음 │
│ └── 명령어가 원자적(Atomic) → 동시성 문제 없음 │
│ │
│ 주의할 점: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ CPU 바운드 작업은 피해야 함! │ │
│ │ ├── 무거운 Lua 스크립트 → Redis 전체가 멈춤 │ │
│ │ ├── KEYS * → 전체 순회로 블로킹 │ │
│ │ └── 큰 데이터 SORT → CPU 소모 │ │
│ │ │ │
│ │ Redis 6.0+: │ │
│ │ ├── I/O 스레드 도입 (네트워크 읽기/쓰기만 멀티스레드) │ │
│ │ └── 명령 실행은 여전히 단일 스레드 (안정성 유지) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10. Redis 최신 변화와 Valkey
10.1 Redis 7.0+ 주요 기능
┌─────────────────────────────────────────────────────────────────┐
│ Redis 7.0+ 주요 새 기능 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Redis Functions │ │
│ │ ├── Lua 스크립팅의 발전 버전 │ │
│ │ ├── 서버에 함수를 등록해두고 호출 │ │
│ │ └── EVAL보다 관리가 편하고 재사용 가능 │ │
│ │ │ │
│ │ 2. ACL (Access Control List) │ │
│ │ ├── 사용자별로 접근 권한 제한 │ │
│ │ ├── 특정 사용자는 GET만 가능 (읽기 전용) │ │
│ │ ├── 특정 키 패턴만 접근 허용 │ │
│ │ └── 운영 환경에서 보안 강화 │ │
│ │ │ │
│ │ 3. Client-side Caching │ │
│ │ ├── 클라이언트(앱)가 로컬에도 캐시 │ │
│ │ ├── Redis까지 네트워크 왕복 자체를 제거! │ │
│ │ ├── Redis가 데이터 변경 시 클라이언트에 알림 │ │
│ │ └── "캐시의 캐시"로 극한의 성능 │ │
│ │ │ │
│ │ 4. Sharded Pub/Sub │ │
│ │ ├── 기존 Pub/Sub: Cluster에서 모든 노드에 전파 │ │
│ │ ├── Sharded Pub/Sub: 해당 슬롯 노드에만 전파 │ │
│ │ └── Cluster 환경에서 Pub/Sub 효율 대폭 개선 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
10.2 라이선스 변경과 Valkey 포크 (2024)
┌─────────────────────────────────────────────────────────────────┐
│ Redis 라이선스 변경과 Valkey의 탄생 (2024) │
│ │
│ 무슨 일이 있었나? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2024년 3월: Redis가 라이선스를 바꿈! │ │
│ │ │ │
│ │ Before: BSD 라이선스 (완전 자유 오픈소스) │ │
│ │ After: SSPL + RSALv2 (상업적 사용 제한) │ │
│ │ │ │
│ │ SSPL = 클라우드 서비스로 제공하려면 │ │
│ │ 전체 인프라 코드를 공개해야 함 │ │
│ │ → 사실상 AWS, Google 등이 Redis를 │ │
│ │ 서비스로 팔기 어렵게 만든 것 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 왜 라이선스를 바꿨나? │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ AWS ElastiCache, Google Memorystore 등이 │ │
│ │ Redis를 그대로 가져다 서비스로 팔아서 큰 수익을 냄 │ │
│ │ │ │
│ │ 하지만 Redis 프로젝트에 기여는 거의 없음 │ │
│ │ → Redis Inc. 입장: "우리가 만든 걸 가져가서 │ │
│ │ 돈만 버는데 우리한테 돌아오는 건 없다!" │ │
│ │ → 라이선스를 바꿔서 이런 무임승차를 막겠다! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 그 결과: Valkey의 탄생 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Linux Foundation이 "Valkey"라는 이름으로 Redis를 포크 │ │
│ │ │ │
│ │ Valkey의 특징: │ │
│ │ ├── BSD 라이선스 유지 (진정한 오픈소스) │ │
│ │ ├── Redis 7.2.4에서 포크 │ │
│ │ ├── AWS, Google, Oracle, Ericsson 등이 지원 │ │
│ │ └── Redis와 호환되는 API (거의 드롭인 교체 가능) │ │
│ │ │ │
│ │ 현재 상황: │ │
│ │ ├── Redis: 상업적 사용에 제한 있음 │ │
│ │ └── Valkey: 완전한 오픈소스로 자유롭게 사용 가능 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 비유: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 레시피를 무료로 공개했더니 │ │
│ │ 대형 프랜차이즈만 그 레시피로 돈을 벌었다. │ │
│ │ │ │
│ │ 원작자가 화가 나서 레시피에 조건을 붙였다: │ │
│ │ "이 레시피로 장사하려면 주방 설계도도 공개해!" │ │
│ │ │ │
│ │ 그러자 요리사들이 조건 붙기 전 버전의 레시피로 │ │
│ │ 새 브랜드(Valkey)를 시작했다. │ │
│ │ │ │
│ │ "레시피는 여전히 무료! 누구나 쓸 수 있어요!" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11. 정리
┌─────────────────────────────────────────────────────────────────┐
│ Redis 전체 내용 정리 │
│ │
│ Redis = 인메모리 데이터 저장소 (Remote Dictionary Server) │
│ ├── 2009년 Salvatore Sanfilippo(antirez)가 개발 │
│ ├── Memcached의 한계를 넘어 다양한 자료구조 지원 │
│ └── 전통적 DB를 대체하지 않고 보완하는 존재 │
│ │
│ 영속성 (전원 꺼지면?): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설정 없음: 모든 데이터 유실 │ │
│ │ RDB: 스냅샷 (세이브 포인트처럼 주기적 저장) │ │
│ │ AOF: 모든 명령 기록 (가계부처럼) │ │
│ │ RDB + AOF: 실무 권장 (빠른 복구 + 최소 유실) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 자료구조 (단순 캐시가 아닌 이유): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ String: 캐싱, 카운터 │ │
│ │ List: 메시지 큐, 타임라인 │ │
│ │ Set: 고유값 집합, 교집합/합집합 │ │
│ │ Sorted Set: 리더보드, 순위 (킬러 기능) │ │
│ │ Hash: 객체 저장 │ │
│ │ Stream: 이벤트 스트리밍 │ │
│ │ HyperLogLog: 대량 고유 카운팅 │ │
│ │ Bitmap: 비트 단위 플래그 │ │
│ │ Geospatial: 위치 기반 검색 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 효과적인 활용: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 세션 관리: 다중 서버 환경에서 세션 공유 │ │
│ │ 캐싱: DB 부하 감소 (100배 빠른 응답) │ │
│ │ 리더보드: 100만 건도 0.1ms 조회 │ │
│ │ Rate Limit: API 호출 제한 │ │
│ │ Pub/Sub: 실시간 메시징 │ │
│ │ 분산 락: 동시성 제어 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 고가용성과 확장: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Sentinel: Master 감시 → 장애 시 자동 Failover │ │
│ │ Cluster: 16,384 Hash Slot으로 데이터 분산 저장 │ │
│ │ Eviction: 메모리 부족 시 데이터 삭제 정책 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Spring Boot 연동: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ RedisTemplate: 직접 Redis 명령어 사용 │ │
│ │ @Cacheable: 어노테이션으로 간편 캐싱 │ │
│ │ @CacheEvict: 캐시 삭제 │ │
│ │ Session: spring-session-data-redis │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 주의사항: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ KEYS * 금지: SCAN 사용 (점진적 순회) │ │
│ │ Big Key 방지: 데이터를 쪼개서 분산 │ │
│ │ Hot Key 방지: 키 복제 + 로컬 캐시 병용 │ │
│ │ 메모리 계획: maxmemory 반드시 설정 │ │
│ │ 단일 스레드: CPU 바운드 작업 피하기 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 최신 동향: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Redis 7.0+: Functions, ACL, Client-side Caching │ │
│ │ 2024년: 라이선스 변경 (BSD → SSPL) │ │
│ │ Valkey: Linux Foundation의 오픈소스 포크 (BSD 유지) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 한 줄 요약: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ "자주 쓰는 데이터는 Redis에, 영구 보관은 DB에. │ │
│ │ 둘을 같이 쓰면 빠르면서도 안전하다!" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11. Pub/Sub과 메시징 시스템 완전 가이드
11.1 용어 사전 (Terminology Dictionary)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 핵심 용어 사전 │
│ │
│ 모든 용어의 풀네임, 어원, 왜 그렇게 부르는지 완전 정리 │
│ │
│ ═══════════════════════════════════════════════════════════════ │
│ │
│ ■ Pub/Sub (Publish/Subscribe) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 풀네임: Publish/Subscribe Messaging Pattern │ │
│ │ 어원: 신문·잡지 "출판/구독" 모델에서 유래 │ │
│ │ │ │
│ │ 현실 세계 비유: │ │
│ │ ├── Publisher = 신문사 (기사를 발행) │ │
│ │ ├── Subscriber = 구독자 (관심 있는 신문을 구독) │ │
│ │ ├── Topic/Channel = 신문 섹션 (스포츠, 경제, IT) │ │
│ │ └── 신문사는 구독자가 누군지 모름 (느슨한 결합) │ │
│ │ │ │
│ │ 학술적 최초 등장: │ │
│ │ ├── 1982: DEC SDDB에서 비공식 최초 구현 │ │
│ │ ├── 1987: ACM SOSP '87 학술 문서화 │ │
│ │ │ Kenneth Birman & Thomas Joseph │ │
│ │ │ "Exploiting Virtual Synchrony in │ │
│ │ │ Distributed Systems" (Isis Toolkit) │ │
│ │ └── Isis Toolkit의 "news" 서브시스템이 원형 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Message Broker (메시지 브로커) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 어원: 14세기 영어 "broceur" (소규모 상인) │ │
│ │ 고대 프랑스어에서 "포도주 통을 따다" │ │
│ │ 비유: 금융 중개인 (증권 브로커처럼 메시지를 중개) │ │
│ │ │ │
│ │ 역할: │ │
│ │ ├── 생산자와 소비자 사이에서 메시지를 중개 │ │
│ │ ├── 메시지 라우팅, 변환, 저장 │ │
│ │ └── 예: RabbitMQ, ActiveMQ, Redis (제한적) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Message Queue vs Message Bus vs Event Bus │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Message Queue (메시지 큐): │ │
│ │ ├── 어원: FIFO "줄서기" 대기열 │ │
│ │ ├── 패턴: 1:1 Point-to-Point │ │
│ │ ├── 하나의 Consumer만 메시지를 소비 │ │
│ │ └── 예: SQS, RabbitMQ Queue │ │
│ │ │ │
│ │ Message Bus (메시지 버스): │ │
│ │ ├── 어원: 컴퓨터 하드웨어 "버스"에서 유래 │ │
│ │ │ (여러 장치가 공유하는 통신 경로) │ │
│ │ ├── 패턴: 1:N Broadcast │ │
│ │ └── 모든 참여자에게 메시지 전달 │ │
│ │ │ │
│ │ Event Bus (이벤트 버스): │ │
│ │ ├── Message Bus와 유사하나 "사건(Event)" 초점 │ │
│ │ ├── 과거형으로 명명 (OrderCreated, UserSignedUp) │ │
│ │ └── 예: Spring ApplicationEventPublisher │ │
│ │ │ │
│ │ Message Queue Message/Event Bus │ │
│ │ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Producer │ │ Publisher│ │ │
│ │ └────┬─────┘ └────┬─────┘ │ │
│ │ │ 1:1 │ 1:N │ │
│ │ ┌────▼─────┐ ┌───▼──────┐ │ │
│ │ │ Queue │ │ Bus │ │ │
│ │ └────┬─────┘ └┬───┬───┬─┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌────▼─────┐ ┌───▼┐ ┌▼─┐ ┌▼──┐ │ │
│ │ │ Consumer │ │ S1 │ │S2│ │S3 │ │ │
│ │ └──────────┘ └────┘ └──┘ └───┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Topic vs Channel vs Subject vs Queue │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 왜 각 시스템이 다른 용어를 쓰는가? │ │
│ │ │ │
│ │ Topic (토픽): │ │
│ │ ├── 사용: JMS(1990s말), Kafka, AMQP │ │
│ │ ├── 비유: 신문의 "섹션" (스포츠면, 경제면) │ │
│ │ └── "주제"별로 메시지를 분류 │ │
│ │ │ │
│ │ Channel (채널): │ │
│ │ ├── 사용: Redis, NATS, Phoenix │ │
│ │ ├── 비유: TV 방송 "채널" │ │
│ │ └── "방송 채널"을 돌리듯 관심 채널 선택 │ │
│ │ │ │
│ │ Subject (서브젝트): │ │
│ │ ├── 사용: NATS │ │
│ │ ├── 비유: 우편물의 "제목" │ │
│ │ ├── 계층적 점(.) 구분 (orders.us.new) │ │
│ │ └── 와일드카드: * (단일), > (하위 전체) │ │
│ │ │ │
│ │ Queue (큐): │ │
│ │ ├── 사용: IBM MQ, RabbitMQ, SQS │ │
│ │ ├── 최초의 메시징 용어 "대기열" │ │
│ │ └── FIFO 줄서기의 직관적 표현 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Producer/Publisher vs Consumer/Subscriber │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Producer / Consumer: │ │
│ │ ├── 어원: 공장 생산라인 은유 │ │
│ │ ├── "생산자가 만들고, 소비자가 가져간다" │ │
│ │ ├── 사용: Kafka (Pull 모델이라 Consumer가 가져감) │ │
│ │ └── Consumer가 능동적 → "Smart Consumer" 철학 │ │
│ │ │ │
│ │ Publisher / Subscriber: │ │
│ │ ├── 어원: 출판/구독 은유 │ │
│ │ ├── "출판사가 발행하고, 구독자에게 배달된다" │ │
│ │ ├── 사용: JMS, MQTT, AMQP, Redis │ │
│ │ └── Broker가 Push → "Smart Broker" 철학 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 메시지 전달 보장 (Delivery Guarantees) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ At-most-once (최대 1회): │ │
│ │ ├── "보내고 잊어버리기" (Fire-and-Forget) │ │
│ │ ├── 유실 허용, 중복 불허 │ │
│ │ ├── 가장 빠름 (ACK 없음) │ │
│ │ └── 예: Redis Pub/Sub, NATS Core, UDP │ │
│ │ │ │
│ │ At-least-once (최소 1회): │ │
│ │ ├── "확인될 때까지 계속 보내기" │ │
│ │ ├── 유실 불허, 중복 허용 │ │
│ │ ├── ACK + 재전송 메커니즘 │ │
│ │ └── 예: Kafka(기본), RabbitMQ, SQS, Redis Streams │ │
│ │ │ │
│ │ Exactly-once (정확히 1회): │ │
│ │ ├── 이론적으로 불가능! (Two Generals Problem) │ │
│ │ ├── 실용적 근사치만 구현 가능 │ │
│ │ ├── Idempotent Operation + Deduplication 조합 │ │
│ │ └── 예: Kafka Transactions, Google Cloud Pub/Sub │ │
│ │ │ │
│ │ 전달 보장 스펙트럼: │ │
│ │ ┌──────────┬──────────────┬───────────────┐ │ │
│ │ │At-most-1 │ At-least-1 │ Exactly-once │ │ │
│ │ ├──────────┼──────────────┼───────────────┤ │ │
│ │ │ 가장빠름 │ 중간 │ 가장느림 │ │ │
│ │ │ 유실가능 │ 중복가능 │ 근사치만가능 │ │ │
│ │ │ 단순구현 │ ACK필요 │ 트랜잭션필요 │ │ │
│ │ └──────────┴──────────────┴───────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Consumer Group (컨슈머 그룹) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 어원: Kafka의 핵심 설계 개념 │ │
│ │ 비유: 회사의 팀/부서 │ │
│ │ │ │
│ │ 동작 방식: │ │
│ │ ├── 여러 Consumer가 동일한 group.id를 공유 │ │
│ │ ├── 파티션을 그룹 내 Consumer끼리 분담 │ │
│ │ ├── 같은 그룹 내에서는 하나만 메시지를 받음 │ │
│ │ └── 다른 그룹은 동일 메시지를 각각 받음 │ │
│ │ │ │
│ │ Topic: orders │ │
│ │ ┌─────┬─────┬─────┬─────┐ │ │
│ │ │ P0 │ P1 │ P2 │ P3 │ (4 Partitions) │ │
│ │ └──┬──┴──┬──┴──┬──┴──┬──┘ │ │
│ │ │ │ │ │ │ │
│ │ Group-A: ┌──┐ ┌──┐ (P0,P1→C1 / P2,P3→C2) │ │
│ │ │C1│ │C2│ │ │
│ │ └──┘ └──┘ │ │
│ │ Group-B: ┌──┐ (P0~P3→C3, 혼자서 전부) │ │
│ │ │C3│ │ │
│ │ └──┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Dead Letter Queue (DLQ, 데드 레터 큐) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 어원: 1825년 미국 우체국 "Dead Letter Office" │ │
│ │ ├── 배달도 불가능하고 반송도 불가능한 편지 │ │
│ │ ├── 수취인 불명, 주소 불명인 편지를 모아두는 부서 │ │
│ │ └── 실제로 미국 우체국에 이 부서가 존재했음 │ │
│ │ │ │
│ │ 소프트웨어에서: │ │
│ │ ├── 처리 실패한 메시지를 별도 큐로 라우팅 │ │
│ │ ├── 재시도 횟수 초과, 파싱 실패, 비즈니스 로직 오류 │ │
│ │ ├── 나중에 수동 검토하거나 재처리 │ │
│ │ └── 메시지 유실 방지의 마지막 안전망 │ │
│ │ │ │
│ │ 정상 Queue Dead Letter Queue │ │
│ │ ┌────────┐ ┌────────────┐ │ │
│ │ │ msg1 ──┼─ 성공 │ │ │ │
│ │ │ msg2 ──┼─ 실패 ─┼─→ msg2 │ │ │
│ │ │ msg3 ──┼─ 성공 │ msg5 │ ← 수동 검토 대상 │ │
│ │ │ msg4 ──┼─ 성공 │ msg7 │ │ │
│ │ │ msg5 ──┼─ 실패 ─┼─→ │ │ │
│ │ └────────┘ └────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Backpressure (백프레셔) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 어원: 19세기 증기기관의 배기구 "역압력" │ │
│ │ ├── 자동차 배기관이 좁으면 엔진에 역압력 발생 │ │
│ │ ├── 배기가스가 빠져나가지 못하면 엔진 성능 저하 │ │
│ │ └── 출구가 막히면 입구도 느려지는 물리 현상 │ │
│ │ │ │
│ │ 소프트웨어에서: │ │
│ │ ├── Consumer가 Producer에게 "속도 늦춰달라" 신호 │ │
│ │ ├── 처리 속도 < 생산 속도일 때 발생 │ │
│ │ ├── 대응: 버퍼링, 드롭, 속도 조절 │ │
│ │ └── Reactive Streams (2013, Netflix/Pivotal/Twitter) │ │
│ │ │ │
│ │ Producer ──(빠름)──→ Buffer ──(느림)──→ Consumer │ │
│ │ │ │ │
│ │ [버퍼 가득!] │ │
│ │ │ │ │
│ │ Producer ←──(느려!)──── Backpressure 신호 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Fan-out / Fan-in (팬아웃 / 팬인) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 어원: 1950-60년대 디지털 논리 회로 용어 │ │
│ │ ├── Fan-out: 게이트 출력이 구동할 수 있는 입력 수 │ │
│ │ │ TTL: fan-out 10, CMOS: fan-out 50+ │ │
│ │ ├── Fan-in: 게이트가 받을 수 있는 입력 수 │ │
│ │ └── 전자공학 → 소프트웨어로 의미 확장 │ │
│ │ │ │
│ │ 소프트웨어에서: │ │
│ │ Fan-out: 하나의 메시지 → 여러 수신자에게 분배 │ │
│ │ Fan-in: 여러 소스의 결과 → 하나로 취합 │ │
│ │ │ │
│ │ Fan-out: Fan-in: │ │
│ │ ┌→ B A ─┐ │ │
│ │ A ───┼→ C B ─┼──→ D │ │
│ │ └→ D C ─┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 프로토콜/시스템 이름의 유래 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Kafka: │ │
│ │ ├── Jay Kreps 명명 (LinkedIn, 2010년 개발) │ │
│ │ ├── 작가 Franz Kafka에서 유래 │ │
│ │ ├── "쓰기(writing) 최적화 시스템이라 작가 이름" │ │
│ │ └── 2011년 오픈소스 공개 │ │
│ │ │ │
│ │ AMQP (Advanced Message Queuing Protocol): │ │
│ │ ├── JPMorgan Chase의 John O'Hara가 2003년 고안 │ │
│ │ ├── 금융기관 독점 메시징의 상호운용성 문제 해결 │ │
│ │ ├── 2012년 OASIS 표준, 2014년 ISO/IEC 인증 │ │
│ │ └── 프로토콜 수준 표준화 (와이어 포맷 정의) │ │
│ │ │ │
│ │ MQTT (Message Queuing Telemetry Transport): │ │
│ │ ├── IBM Andy Stanford-Clark + Eurotech Arlen Nipper │ │
│ │ ├── 1999년 발명 │ │
│ │ ├── 중동 사우디 송유관 위성 SCADA 모니터링용 │ │
│ │ ├── 저대역폭 위성 통신에 최적화 │ │
│ │ └── 최소 헤더 단 2바이트! (극도로 가벼움) │ │
│ │ │ │
│ │ STOMP (Simple Text Oriented Message Protocol): │ │
│ │ ├── 2000년대 초반 등장 │ │
│ │ ├── 스크립팅 언어에서 ActiveMQ 접속용 │ │
│ │ └── HTTP처럼 텍스트 기반 (디버깅 쉬움) │ │
│ │ │ │
│ │ NATS (Neural Autonomic Transport System): │ │
│ │ ├── Derek Collison이 2010년 개발 │ │
│ │ ├── 인간 신경계(Neural)에서 영감 │ │
│ │ ├── Autonomic = 자율신경계처럼 자동 관리 │ │
│ │ └── Cloud Foundry 메시징 컨트롤 플레인으로 시작 │ │
│ │ │ │
│ │ ZeroMQ: │ │
│ │ ├── "Zero = broker, latency, admin, cost 모두 Zero" │ │
│ │ ├── iMatix Pieter Hintjens + Martin Sustrik (2007) │ │
│ │ ├── AMQP 워킹그룹 탈퇴 후 집중 개발 │ │
│ │ └── 브로커 없는 메시징 라이브러리 │ │
│ │ │ │
│ │ Redis Streams: │ │
│ │ ├── antirez: "append-only log 데이터 구조" │ │
│ │ ├── "Stream" = 끊임없이 흘러가는 데이터의 흐름 │ │
│ │ ├── Kafka의 로그 개념에서 영감 │ │
│ │ └── Redis 5.0 (2018년)에 도입 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.2 등장 배경과 역사 (Why & History)
┌─────────────────────────────────────────────────────────────────┐
│ 왜 메시징 시스템이 필요한가? │
│ │
│ ■ 동기식 통신의 4가지 한계 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 시간적 결합 (Temporal Coupling): │ │
│ │ ├── 호출자와 수신자가 동시에 활성 상태여야 함 │ │
│ │ └── 한쪽 다운 → 전체 실패 │ │
│ │ │ │
│ │ 2. 공간적 결합 (Spatial Coupling): │ │
│ │ ├── 호출자가 수신자의 주소(IP, URL)를 알아야 함 │ │
│ │ └── 서비스 위치 변경 → 모든 호출자 수정 필요 │ │
│ │ │ │
│ │ 3. 확장성 병목 (Scalability Bottleneck): │ │
│ │ ├── 동기 호출은 요청당 스레드/커넥션 점유 │ │
│ │ └── 대량 트래픽 시 연쇄 장애 (Cascading Failure) │ │
│ │ │ │
│ │ 4. 동기화 대기 (Synchronization Wait): │ │
│ │ ├── 응답 올 때까지 블로킹 │ │
│ │ └── 느린 서비스 하나가 전체 체인을 느리게 만듦 │ │
│ │ │ │
│ │ 동기식: │ │
│ │ A ──요청──→ B ──요청──→ C │ │
│ │ A ←──응답── B ←──응답── C │ │
│ │ (C가 느리면 A도 느림, C가 죽으면 A도 실패) │ │
│ │ │ │
│ │ 비동기 메시징: │ │
│ │ A ──메시지──→ [Broker] ──메시지──→ B │ │
│ │ A ──메시지──→ [Broker] ──메시지──→ C │ │
│ │ (B, C 독립 동작. 하나 죽어도 나머지 영향 없음) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Redis가 Pub/Sub을 내장한 이유 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ antirez의 동기: │ │
│ │ ├── "out of context 기능이지만 추가하기로 했다" │ │
│ │ ├── 캐시 무효화 브로드캐스트가 핵심 용도 │ │
│ │ │ (한 서버에서 캐시 갱신 → 다른 서버에 알림) │ │
│ │ ├── 구현: 약 150줄의 코드 (매우 가벼움) │ │
│ │ └── Redis 2.0 (2010년)에 포함 │ │
│ │ │ │
│ │ Redis Pub/Sub의 핵심 한계 (7가지): │ │
│ │ ├── 1. 영속성 없음 (메시지가 메모리에만 존재) │ │
│ │ ├── 2. Fire-and-Forget (보내면 잊어버림) │ │
│ │ ├── 3. 연결 끊김 처리 없음 (끊기면 메시지 소실) │ │
│ │ ├── 4. ACK 메커니즘 없음 (처리 확인 불가) │ │
│ │ ├── 5. 느린 구독자가 메모리 압박 유발 │ │
│ │ ├── 6. 메시지 재생(Replay) 불가 │ │
│ │ └── 7. Consumer Group 미지원 │ │
│ │ │ │
│ │ → 이 한계를 극복하기 위해 Redis Streams 탄생 (2018) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 학술적 기원: 3가지 Decoupling (Eugster 2003) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 논문: "The Many Faces of Publish/Subscribe" │ │
│ │ 저자: Patrick Eugster et al. (2003) │ │
│ │ │ │
│ │ Pub/Sub의 본질 = 3가지 결합 해제: │ │
│ │ │ │
│ │ 1. Space Decoupling (공간 분리): │ │
│ │ Publisher와 Subscriber가 서로의 identity를 모름 │ │
│ │ → IP 주소, 프로세스 ID 등을 알 필요 없음 │ │
│ │ │ │
│ │ 2. Time Decoupling (시간 분리): │ │
│ │ 동시에 활성 상태일 필요 없음 │ │
│ │ → Subscriber 오프라인이어도 메시지 보존 가능 │ │
│ │ (단, Redis Pub/Sub은 이것을 지원하지 않음!) │ │
│ │ │ │
│ │ 3. Synchronization Decoupling (동기화 분리): │ │
│ │ Publisher가 Subscriber의 응답을 기다리지 않음 │ │
│ │ → 비동기적으로 독립 실행 │ │
│ │ │ │
│ │ 구독 방식 3가지 분류: │ │
│ │ ├── Topic-based: 이름 기반 (가장 일반적, 표현력 낮음) │ │
│ │ ├── Content-based: 내용 기반 (표현력 높음, 비용 높음) │ │
│ │ └── Type-based: 타입 기반 (중간 수준) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ CAP 정리와 메시징 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 각 메시징 시스템의 CAP 선택: │ │
│ │ │ │
│ │ ┌──────────────┬──────────────────────────────┐ │ │
│ │ │ 시스템 │ CAP 선택 │ │ │
│ │ ├──────────────┼──────────────────────────────┤ │ │
│ │ │ Kafka │ 기본 AP, min.insync.replicas │ │ │
│ │ │ │ 설정으로 CP 이동 가능 │ │ │
│ │ │ RabbitMQ │ 기본 CP, Quorum Queue로 │ │ │
│ │ │ │ majority ack │ │ │
│ │ │ NATS Core │ AP (가용성 우선) │ │ │
│ │ │ Pulsar │ CP 지향 (BookKeeper 복제) │ │ │
│ │ └──────────────┴──────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Exactly-once의 이론적 불가능성 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Two Generals Problem (1975): │ │
│ │ ├── 신뢰할 수 없는 채널에서 완벽한 합의 불가능 │ │
│ │ ├── ACK도 유실 가능 → 무한 확인 연쇄 │ │
│ │ └── "ACK의 ACK의 ACK..." 끝이 없음 │ │
│ │ │ │
│ │ 장군A ──"공격!"──→ 장군B │ │
│ │ 장군A ←──"알겠다"── 장군B (이 ACK가 유실되면?) │ │
│ │ 장군A ──"ACK받았다"→ 장군B (이것도 유실되면?) │ │
│ │ ... 무한 반복 ... │ │
│ │ │ │
│ │ FLP Impossibility (1985): │ │
│ │ ├── Fischer, Lynch, Paterson 증명 │ │
│ │ ├── 비동기 분산환경에서 단 1개 프로세스가 │ │
│ │ │ crash 가능하면 결정론적 합의 불가능 │ │
│ │ └── Raft, Paxos 등은 확률적/시간제한 기반 우회 │ │
│ │ │ │
│ │ 실용적 대안: │ │
│ │ ├── 1. Idempotent Operations (같은 연산 N번 = 1번) │ │
│ │ ├── 2. Deduplication (메시지 ID로 중복 제거) │ │
│ │ ├── 3. Transactional Outbox (DB 트랜잭션 활용) │ │
│ │ └── 4. At-least-once + Consumer Idempotency │ │
│ │ (가장 현실적이고 널리 사용되는 방식) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Jay Kreps "The Log" (2013) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 핵심 개념: Log = 시간순 append-only 레코드 시퀀스 │ │
│ │ │ │
│ │ Log-Table Duality (로그-테이블 이중성): │ │
│ │ ├── Log를 재생하면 임의 시점의 Table을 재구성 가능 │ │
│ │ ├── Table의 변경 이력 = Log │ │
│ │ └── 이벤트 소싱의 이론적 토대 │ │
│ │ │ │
│ │ State Machine Replication: │ │
│ │ ├── 동일 초기 상태 + 동일 입력 순서 = 동일 출력 │ │
│ │ └── Log를 다른 시스템에 전파 → 동일 상태 복제 │ │
│ │ │ │
│ │ Log: [e1][e2][e3][e4][e5][e6][e7]... │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ 시점3: [e1][e2][e3] → Table 상태 A │ │
│ │ 시점7: [e1][e2][e3][e4][e5][e6][e7] → Table 상태 B │ │
│ │ │ │
│ │ → Kafka의 핵심 설계 철학 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.3 진화 타임라인 (Evolution Timeline)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 진화 타임라인 (1964 ~ 현재) │
│ │
│ ■ 이론적 토대기 (1964~1985) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1964 │ Unix Pipe 개념 (Doug McIlroy 메모) │ │
│ │ │ "프로그램을 정원 호스처럼 연결하자" │ │
│ │ │ │ │
│ │ 1973 │ Unix Pipes 구현 │ │
│ │ │ cmd1 | cmd2 | cmd3 (최초의 스트리밍 패턴) │ │
│ │ │ │ │
│ │ 1975 │ Two Generals Problem 증명 │ │
│ │ │ (신뢰할 수 없는 채널에서 합의 불가능) │ │
│ │ │ │ │
│ │ 1979 │ Carnegie Mellon Accent 시스템 │ │
│ │ │ (초기 IPC 메시지 패싱) │ │
│ │ │ │ │
│ │ 1982 │ DEC SDDB (최초 비공식 Pub/Sub 구현) │ │
│ │ │ │ │
│ │ 1983 │ System V IPC │ │
│ │ │ (Message Queues, Semaphores, Shared Memory) │ │
│ │ │ │ │
│ │ 1985 │ FLP Impossibility 증명 │ │
│ │ │ (분산 합의의 근본적 한계) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 학술/상용화기 (1987~2003) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1987 │ ACM SOSP Birman/Joseph 논문 │ │
│ │ │ (Pub/Sub 최초 학술 문서화, Isis Toolkit) │ │
│ │ │ │ │
│ │ 1990s│ TIBCO Rendezvous │ │
│ │ │ (금융기관 실시간 가격 피드에 혁명) │ │
│ │ │ │ │
│ │ 1993 │ IBM MQSeries 1.1.1 │ │
│ │ │ (최초 상업용 메시지 큐 플랫폼) │ │
│ │ │ │ │
│ │ 1997 │ Microsoft MSMQ │ │
│ │ │ (Windows 생태계 메시지 큐) │ │
│ │ │ │ │
│ │ 1999 │ MQTT 발명 │ │
│ │ │ (사우디 송유관 위성 SCADA 모니터링용) │ │
│ │ │ │ │
│ │ 2001 │ JMS 1.0.2b (Java Message Service) │ │
│ │ │ (Java 메시징 표준 API) │ │
│ │ │ │ │
│ │ 2003 │ AMQP 시작 (JPMorgan Chase) │ │
│ │ │ (금융기관 메시징 상호운용성 해결 목표) │ │
│ │ │ │ │
│ │ 2003 │ Eugster "Many Faces of Pub/Sub" 논문 │ │
│ │ │ (3가지 Decoupling 이론 체계화) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 폭발적 성장기 (2004~2015) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 2004 │ AMQP Working Group 결성 │ │
│ │ │ │ │
│ │ 2006 │ Amazon SQS GA │ │
│ │ │ (AWS 최초 서비스 중 하나!) │ │
│ │ │ │ │
│ │ 2007 │ RabbitMQ 1.0 (LShift + CohesiveFT, Erlang) │ │
│ │ │ ZeroMQ 시작 (Hintjens + Sustrik) │ │
│ │ │ │ │
│ │ 2009 │ Redis 최초 릴리스 (antirez) │ │
│ │ │ │ │
│ │ 2010 │ Redis 2.0 + Pub/Sub 내장 │ │
│ │ │ NATS 시작 (Cloud Foundry) │ │
│ │ │ Kafka 개발 시작 (LinkedIn) │ │
│ │ │ │ │
│ │ 2011 │ Kafka 오픈소스 공개 │ │
│ │ │ (Log-structured 메시징 혁명의 시작) │ │
│ │ │ │ │
│ │ 2012 │ Amazon Kinesis │ │
│ │ │ │ │
│ │ 2013 │ Jay Kreps "The Log" 블로그 포스트 │ │
│ │ │ (Log-Table Duality 개념 대중화) │ │
│ │ │ │ │
│ │ 2014 │ Kafka 0.8 Replication (프로덕션 사용 본격화) │ │
│ │ │ │ │
│ │ 2015 │ Google Cloud Pub/Sub │ │
│ │ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 성숙/차세대기 (2016~현재) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 2016 │ Apache Pulsar (Yahoo → ASF) │ │
│ │ │ (Multi-layer, Multi-tenancy 네이티브) │ │
│ │ │ │ │
│ │ 2017 │ Kafka Connect & Streams 성숙 │ │
│ │ │ (단순 메시징 → 스트림 플랫폼 진화) │ │
│ │ │ │ │
│ │ 2018 │ Redis 5.0 + Redis Streams │ │
│ │ │ (Pub/Sub 한계 극복 → Consumer Group 지원) │ │
│ │ │ │ │
│ │ 2019 │ Redpanda 등장 (C++ Kafka-compatible) │ │
│ │ │ NATS JetStream (영속성 추가) │ │
│ │ │ │ │
│ │ 2022 │ Kafka KRaft GA (3.3) │ │
│ │ │ (ZooKeeper 의존성 제거!) │ │
│ │ │ │ │
│ │ 2023 │ WarpStream 등장 (S3-native, Diskless Kafka) │ │
│ │ │ │ │
│ │ 2024 │ WarpStream → Confluent 인수 ($220M) │ │
│ │ │ Kafka 4.0 (KRaft 필수, ZK 완전 제거) │ │
│ │ │ Redis 라이선스 변경 (BSD → SSPL) │ │
│ │ │ Valkey 포크 (Linux Foundation, BSD 유지) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 시각적 타임라인: │
│ │
│ 1973 1993 2007 2011 2018 2024 │
│ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ Pipes IBM MQ RabbitMQ Kafka Streams KRaft │
│ │ │ │ │ │ │ │
│ ─┼─────────┼─────────┼─────┼─────┼───────┼──→ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ │ 1985 │ 1999 2006│ 2010│ 2016│ 2023 │
│ │ FLP │ MQTT SQS │ NATS│Pulsar│ Warp │
│ │ │ │ │ │ Stream │
│ │ 1997 2009 │ 2019 │
│ │ MSMQ Redis │ Redpanda │
│ │ 2.0 2013 │
│ │ "The Log" │
│ │
└─────────────────────────────────────────────────────────────────┘
11.4 Redis Pub/Sub 상세 (Deep Dive)
┌─────────────────────────────────────────────────────────────────┐
│ Redis Pub/Sub 깊이 파헤치기 │
│ │
│ ■ 내부 동작 원리 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 핵심: Fire-and-Forget (메모리 기반, 영속성 없음) │ │
│ │ │ │
│ │ 동작 흐름: │ │
│ │ 1. SUBSCRIBE channel → Redis가 구독자 목록에 추가 │ │
│ │ 2. PUBLISH channel msg → Redis 이벤트 루프가 처리 │ │
│ │ 3. 채널에 연결된 모든 구독자에게 즉시 전달 │ │
│ │ 4. 전달 완료 → 메시지 즉시 소멸 (저장하지 않음) │ │
│ │ │ │
│ │ Publisher Redis Subscribers │ │
│ │ ┌──────┐ ┌──────────────┐ ┌──────┐ │ │
│ │ │ App1 │──→ │ │──→ │ Sub1 │ │ │
│ │ └──────┘ │ Event Loop │──→ │ Sub2 │ │ │
│ │ │ │──→ │ Sub3 │ │ │
│ │ │ 채널:구독자 │ └──────┘ │ │
│ │ │ 매핑 테이블 │ │ │
│ │ └──────────────┘ │ │
│ │ │ │ │
│ │ 메시지 전달 후 │ │
│ │ 즉시 메모리에서 │ │
│ │ 제거 (저장 안 함!) │ │
│ │ │ │
│ │ PUBLISH 반환값 = 메시지를 받은 구독자 수 │ │
│ │ (0이면 아무도 안 받음 → 메시지 소실!) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 정확한 한계점 7가지 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 메시지 영속성 없음: │ │
│ │ ├── 메시지가 디스크에 저장되지 않음 │ │
│ │ ├── Redis 재시작 → 모든 구독 관계 초기화 │ │
│ │ └── RDB/AOF와 무관 (Pub/Sub은 영속화 대상 아님) │ │
│ │ │ │
│ │ 2. 오프라인 구독자 메시지 소실: │ │
│ │ ├── 구독자가 연결되어 있지 않으면 메시지 받지 못함 │ │
│ │ └── 재연결 후에도 놓친 메시지 복구 불가 │ │
│ │ │ │
│ │ 3. ACK 메커니즘 없음: │ │
│ │ ├── Redis는 구독자가 메시지를 처리했는지 모름 │ │
│ │ └── 처리 실패 시 재전송 메커니즘 없음 │ │
│ │ │ │
│ │ 4. At-most-once만 지원: │ │
│ │ ├── 최대 1회 전달 (0회도 가능) │ │
│ │ └── At-least-once, Exactly-once 구현 불가 │ │
│ │ │ │
│ │ 5. 느린 구독자 문제: │ │
│ │ ├── 구독자가 느리면 Redis 출력 버퍼에 메시지 적체 │ │
│ │ ├── client-output-buffer-limit pubsub 설정 초과 시 │ │
│ │ └── 해당 구독자 연결 강제 종료 │ │
│ │ │ │
│ │ 6. 메시지 재생(Replay) 불가: │ │
│ │ ├── 과거 메시지를 다시 읽을 수 없음 │ │
│ │ └── 디버깅, 새 구독자 초기화 등에 불리 │ │
│ │ │ │
│ │ 7. Consumer Group 미지원: │ │
│ │ ├── 모든 구독자가 모든 메시지를 받음 (Fan-out only) │ │
│ │ ├── 경쟁 소비자(Competing Consumer) 패턴 불가 │ │
│ │ └── 부하 분산이 불가능 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 패턴 구독 (PSUBSCRIBE) 성능 주의사항 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ SUBSCRIBE channel → 정확히 일치하는 채널만 │ │
│ │ PSUBSCRIBE pattern → 패턴 매칭 (glob-style) │ │
│ │ │ │
│ │ 예시: │ │
│ │ PSUBSCRIBE news.* → news.sports, news.tech 등 │ │
│ │ PSUBSCRIBE orders.*.kr → orders.123.kr 등 │ │
│ │ │ │
│ │ ⚠ 성능 주의: │ │
│ │ ├── 모든 패턴을 순회하여 매칭 O(N*M) │ │
│ │ │ N = 패턴 수, M = 채널에 해당하는 패턴 검사 │ │
│ │ ├── 패턴이 많아질수록 PUBLISH 성능 저하 │ │
│ │ └── SUBSCRIBE + PSUBSCRIBE 동시 사용 시 │ │
│ │ 동일 메시지를 2회 수신할 수 있음! │ │
│ │ │ │
│ │ 예: SUBSCRIBE news.sports │ │
│ │ PSUBSCRIBE news.* │ │
│ │ → news.sports 메시지 2번 수신! │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Redis 7.0+ Sharded Pub/Sub (Cluster 환경) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 기존 Pub/Sub의 Cluster 문제: │ │
│ │ ├── PUBLISH → 모든 노드에 브로드캐스트 │ │
│ │ ├── 구독자가 없는 노드에도 전달 → 네트워크 낭비 │ │
│ │ └── 노드가 많을수록 오버헤드 선형 증가 │ │
│ │ │ │
│ │ Sharded Pub/Sub 해결: │ │
│ │ ├── SSUBSCRIBE / SPUBLISH 명령어 │ │
│ │ ├── 채널 이름을 Hash Slot에 매핑 │ │
│ │ ├── 해당 Slot을 소유한 노드에서만 처리 │ │
│ │ └── 네트워크 트래픽 대폭 감소 │ │
│ │ │ │
│ │ 기존: Node1 ──→ Node2 ──→ Node3 ──→ Node4 │ │
│ │ (모든 노드에 브로드캐스트) │ │
│ │ │ │
│ │ Sharded: Channel "orders" → Hash Slot 12345 │ │
│ │ → Node2만 담당 (나머지 노드 관여 안 함) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 메시지 유실 대응: Dual-write 패턴 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 문제: Pub/Sub만으로는 메시지 유실 방지 불가 │ │
│ │ 해결: PUBLISH와 동시에 백업 저장 │ │
│ │ │ │
│ │ 패턴 1: PUBLISH + RPUSH (List 백업) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ // 발행 시 │ │ │
│ │ │ MULTI │ │ │
│ │ │ PUBLISH channel:orders message │ │ │
│ │ │ RPUSH backup:orders message │ │ │
│ │ │ EXEC │ │ │
│ │ │ │ │ │
│ │ │ // 구독자 재연결 시 │ │ │
│ │ │ LRANGE backup:orders 0 -1 → 놓친 메시지 복구│ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 패턴 2: PUBLISH + XADD (Stream 백업, 더 권장) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ MULTI │ │ │
│ │ │ PUBLISH channel:orders message │ │ │
│ │ │ XADD stream:orders * data message │ │ │
│ │ │ EXEC │ │ │
│ │ │ │ │ │
│ │ │ → 실시간은 Pub/Sub, 복구는 Stream에서 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 패턴 3: 외부 시퀀스 번호 │ │
│ │ ├── 메시지에 순차 번호 부여 │ │
│ │ ├── 구독자가 마지막 처리 번호 기록 │ │
│ │ └── 재연결 시 빠진 번호 요청 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.5 Redis Streams 상세 (Pub/Sub의 진화)
┌─────────────────────────────────────────────────────────────────┐
│ Redis Streams 완전 가이드 │
│ │
│ ■ Pub/Sub vs Streams 근본적 차이 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
│ │ │ 특성 │ Pub/Sub │ Streams │ │ │
│ │ ├──────────────┼──────────────┼──────────────┤ │ │
│ │ │ 저장 방식 │ 저장 안 함 │ Append-only │ │ │
│ │ │ │ │ Log │ │ │
│ │ │ 전달 보장 │ At-most-once │ At-least- │ │ │
│ │ │ │ │ once │ │ │
│ │ │ Consumer │ 미지원 │ 지원 │ │ │
│ │ │ Group │ │ (Kafka유사) │ │ │
│ │ │ ACK │ 없음 │ XACK │ │ │
│ │ │ Replay │ 불가 │ 가능 │ │ │
│ │ │ 오프라인 │ 메시지 소실 │ 메시지 보존 │ │ │
│ │ │ 구독자 │ │ │ │ │
│ │ │ 메모리 관리 │ 없음(즉시 │ MAXLEN/ │ │ │
│ │ │ │ 제거) │ MINID │ │ │
│ │ │ 사용 사례 │ 실시간 알림 │ 이벤트 로그 │ │ │
│ │ │ │ 캐시 무효화 │ 작업 큐 │ │ │
│ │ └──────────────┴──────────────┴──────────────┘ │ │
│ │ │ │
│ │ Pub/Sub: Publisher ──→ [즉시 전달, 즉시 소멸] │ │
│ │ Streams: Producer ──→ [저장 → 읽기 → ACK → 완료] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Consumer Group 동작 방식 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Stream: mystream │ │
│ │ ┌─────┬─────┬─────┬─────┬─────┬─────┐ │ │
│ │ │ e1 │ e2 │ e3 │ e4 │ e5 │ e6 │ (시간순) │ │
│ │ └──┬──┴──┬──┴──┬──┴──┬──┴──┬──┴──┬──┘ │ │
│ │ │ │ │ │ │ │ │ │
│ │ Group "orders-processing": │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ last-delivered-id: e4 │ │ │
│ │ │ │ │ │
│ │ │ Consumer-A: e1(ACK), e3(ACK) │ │ │
│ │ │ Consumer-B: e2(ACK), e4(pending...) │ │ │
│ │ │ │ │ │
│ │ │ PEL (Pending Entries List): │ │ │
│ │ │ ┌────┬────────────┬──────────┬─────────┐ │ │ │
│ │ │ │ ID │ Consumer │ 전달시각 │ 전달횟수│ │ │ │
│ │ │ ├────┼────────────┼──────────┼─────────┤ │ │ │
│ │ │ │ e4 │ Consumer-B │ 10:30:05 │ 1 │ │ │ │
│ │ │ └────┴────────────┴──────────┴─────────┘ │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 흐름: │ │
│ │ 1. XREADGROUP → 새 메시지를 Consumer에게 분배 │ │
│ │ 2. Consumer 처리 후 XACK → PEL에서 제거 │ │
│ │ 3. XACK 안 하면 → PEL에 남아서 재처리 대상 │ │
│ │ 4. XPENDING → PEL 조회 (미처리 메시지 확인) │ │
│ │ 5. XCLAIM → 다른 Consumer가 미처리 메시지 인수 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 핵심 명령어 상세 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ XADD (메시지 추가): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XADD mystream * field1 value1 field2 value2 │ │ │
│ │ │ │ │ └── 키-값 쌍 (여러 필드) │ │ │
│ │ │ │ └── * = 자동 ID 생성 │ │ │
│ │ │ └── Stream 이름 │ │ │
│ │ │ │ │ │
│ │ │ ID 형식: <밀리초타임스탬프>-<시퀀스번호> │ │ │
│ │ │ 예: 1679012345678-0 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ XREAD (단순 읽기, Consumer Group 없이): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XREAD COUNT 10 BLOCK 5000 STREAMS mystream $ │ │ │
│ │ │ │ │ │ │ │ │
│ │ │ │ │ $ = 새 메시지만 대기 │ │ │ │
│ │ │ │ └── 5초 블로킹 대기 │ │ │ │
│ │ │ └── 최대 10개 읽기 │ │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ XREADGROUP (Consumer Group으로 읽기): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XREADGROUP GROUP mygroup consumer1 │ │ │
│ │ │ COUNT 10 BLOCK 5000 │ │ │
│ │ │ STREAMS mystream > │ │ │
│ │ │ │ │ │ │
│ │ │ > = 아직 전달되지 않은 새 메시지만 │ │ │
│ │ │ 0 = 내 PEL에 있는 pending 메시지 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ XACK (처리 완료 확인): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XACK mystream mygroup 1679012345678-0 │ │ │
│ │ │ → PEL에서 해당 메시지 제거 │ │ │
│ │ │ → "이 메시지 정상 처리 완료!" 선언 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ XPENDING (미처리 메시지 조회): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XPENDING mystream mygroup │ │ │
│ │ │ → PEL 요약 (총 개수, 최소ID, 최대ID, │ │ │
│ │ │ Consumer별 pending 수) │ │ │
│ │ │ │ │ │
│ │ │ XPENDING mystream mygroup - + 10 │ │ │
│ │ │ → 상세 목록 (ID, Consumer, idle시간, 전달수) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ XCLAIM (메시지 소유권 이전): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XCLAIM mystream mygroup consumer2 │ │ │
│ │ │ 300000 1679012345678-0 │ │ │
│ │ │ │ │ │ │
│ │ │ └── 300초(5분) 이상 idle인 경우만 │ │ │
│ │ │ │ │ │
│ │ │ XAUTOCLAIM (Redis 6.2+): │ │ │
│ │ │ XAUTOCLAIM mystream mygroup consumer2 │ │ │
│ │ │ 300000 0-0 │ │ │
│ │ │ → 5분 이상 pending인 메시지 자동 claim │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ MAXLEN / MINID 메모리 관리 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ MAXLEN (최대 항목 수 제한): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XADD mystream MAXLEN 1000 * data hello │ │ │
│ │ │ → 정확히 1000개 유지 (성능 비용 있음) │ │ │
│ │ │ │ │ │
│ │ │ XADD mystream MAXLEN ~ 1000 * data hello │ │ │
│ │ │ → 대략 1000개 유지 (근사치, 더 빠름!) │ │ │
│ │ │ → ~ (tilde) = 근사 트리밍 (권장!) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ MINID (최소 ID 기준 제거): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ XADD mystream MINID ~ 1679000000000-0 │ │ │
│ │ │ → 지정된 ID보다 오래된 항목 제거 │ │ │
│ │ │ → 시간 기반 보존 정책에 유용 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ⚠ XDEL 안티패턴: │ │
│ │ ├── XDEL은 항목을 삭제하지만 메모리를 반환하지 않음 │ │
│ │ ├── "Swiss cheese" 메모리 단편화 발생 │ │
│ │ ├── Radix tree 노드가 비어도 메모리 유지 │ │
│ │ └── 권장: MAXLEN/MINID로 trim (XDEL 대신) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Kafka와의 유사점/차이점 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 유사점: │ │
│ │ ├── Append-only log 구조 │ │
│ │ ├── Consumer Group으로 부하 분산 │ │
│ │ ├── Offset(ID) 기반 메시지 추적 │ │
│ │ └── 메시지 재생(Replay) 가능 │ │
│ │ │ │
│ │ 차이점: │ │
│ │ ┌──────────────┬────────────────┬────────────────┐ │ │
│ │ │ 항목 │ Redis Streams │ Kafka │ │ │
│ │ ├──────────────┼────────────────┼────────────────┤ │ │
│ │ │ 확장성 │ 단일 노드/ │ 수백 브로커 │ │ │
│ │ │ │ Redis Cluster │ 확장 가능 │ │ │
│ │ │ 파티션 │ 없음 │ 토픽당 N개 │ │ │
│ │ │ 저장 │ 메모리 (+AOF) │ 디스크 │ │ │
│ │ │ 처리량 │ 수만/s │ 수백만/s │ │ │
│ │ │ 복잡도 │ 매우 낮음 │ 높음 │ │ │
│ │ │ 적합 규모 │ 소~중규모 │ 대규모 │ │ │
│ │ │ 의존성 │ Redis만 필요 │ Broker+KRaft │ │ │
│ │ └──────────────┴────────────────┴────────────────┘ │ │
│ │ │ │
│ │ 판단 기준: │ │
│ │ ├── 이미 Redis 사용 중 + 소규모 → Redis Streams │ │
│ │ ├── 대용량 + 높은 내구성 필요 → Kafka │ │
│ │ └── Redis Streams = "가벼운 Kafka"로 시작하기 좋음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.6 대안 시스템 완전 비교 (All Alternatives)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 대안 완전 가이드 │
│ │
│ ■ Apache Kafka │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: "Smart Consumer, Dumb Broker" │ │
│ │ 핵심: Log-structured, Sequential I/O, Pull 모델 │ │
│ │ │ │
│ │ 아키텍처: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Producer ──→ Broker Cluster ──→ Consumer │ │ │
│ │ │ ┌─────────────┐ │ │ │
│ │ │ │ Topic │ │ │ │
│ │ │ │ ├─ Part.0 ──┼─→ CG-A: C1 │ │ │
│ │ │ │ ├─ Part.1 ──┼─→ CG-A: C2 │ │ │
│ │ │ │ └─ Part.2 ──┼─→ CG-A: C3 │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ KRaft │ CG-B: C4 │ │ │
│ │ │ │ (메타데이터)│ (별도 offset) │ │ │
│ │ │ └─────────────┘ │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 핵심 개념: │ │
│ │ ├── Broker: 메시지 저장/서빙 서버 │ │
│ │ ├── Topic: 메시지 카테고리 (논리적 구분) │ │
│ │ ├── Partition: 순서 보장 불변 레코드 시퀀스 │ │
│ │ │ (병렬 처리의 단위, 파티션 내에서만 순서 보장) │ │
│ │ ├── Offset: 파티션 내 메시지 위치 (Consumer가 관리) │ │
│ │ └── Consumer Group: 파티션을 분담하는 Consumer 집합 │ │
│ │ │ │
│ │ 성능 비결: │ │
│ │ ├── O(1) append (Sequential I/O) │ │
│ │ ├── Page Cache 활용 (double buffering 방지) │ │
│ │ ├── sendfile() zero-copy (커널 직접 전송) │ │
│ │ ├── Lock-free 구조 (쓰기 잠금 없음) │ │
│ │ └── Batch 전송 (네트워크 왕복 최소화) │ │
│ │ │ │
│ │ KRaft (Kafka 3.3+ GA, 4.0 필수): │ │
│ │ ├── ZooKeeper 의존성 완전 제거 │ │
│ │ ├── Raft 기반 쿼럼 컨트롤러 │ │
│ │ ├── __cluster_metadata 내부 토픽으로 메타데이터 관리 │ │
│ │ └── 운영 복잡도 대폭 감소 │ │
│ │ │ │
│ │ 강점: 대용량 처리, 메시지 보존, 리플레이, 에코시스템 │ │
│ │ 약점: 운영 복잡도, 소규모 오버킬, 초기 학습 곡선 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ RabbitMQ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: "Smart Broker, Dumb Consumer" │ │
│ │ 핵심: AMQP, Exchange-Queue-Binding, Push 모델 │ │
│ │ │ │
│ │ 아키텍처: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Producer │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ │ │ │
│ │ │ Exchange (4종) │ │ │
│ │ │ ├── Direct: routing_key 정확히 일치 │ │ │
│ │ │ ├── Fanout: 모든 바인딩된 큐에 복사 │ │ │
│ │ │ ├── Topic: 패턴 매칭 (*.orders.#) │ │ │
│ │ │ └── Headers: 헤더 속성 기반 라우팅 │ │ │
│ │ │ │ │ │ │
│ │ │ ▼ (Binding + Routing Key) │ │ │
│ │ │ Queue ──→ Consumer │ │ │
│ │ │ Queue ──→ Consumer │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ACK 방식: │ │
│ │ ├── Auto ACK: 전달 즉시 제거 (unsafe, 유실 위험) │ │
│ │ ├── Manual ACK: basic.ack (처리 확인 후 제거) │ │
│ │ └── basic.nack / basic.reject (거부 → DLX로 이동) │ │
│ │ │ │
│ │ Kafka와의 핵심 철학 차이: │ │
│ │ ┌──────────────┬──────────────┬──────────────┐ │ │
│ │ │ │ RabbitMQ │ Kafka │ │ │
│ │ ├──────────────┼──────────────┼──────────────┤ │ │
│ │ │ 브로커 역할 │ Smart Broker │ Dumb Broker │ │ │
│ │ │ │ (라우팅, │ (저장만, │ │ │
│ │ │ │ 필터링) │ 서빙) │ │ │
│ │ │ Consumer │ Push │ Pull │ │ │
│ │ │ 메시지 보존 │ 소비 후 삭제 │ 보존 기간 │ │ │
│ │ │ │ │ 동안 유지 │ │ │
│ │ │ 라우팅 │ 복잡한 라우팅│ 토픽 기반 │ │ │
│ │ │ │ (Exchange) │ (단순) │ │ │
│ │ └──────────────┴──────────────┴──────────────┘ │ │
│ │ │ │
│ │ 강점: 유연한 라우팅, 다양한 프로토콜, 관리 UI │ │
│ │ 약점: 대용량 처리 한계, 메시지 보존 어려움 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Apache Pulsar │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: Multi-layer Architecture, Multi-tenancy │ │
│ │ 핵심: Broker(stateless) + BookKeeper(storage) 분리 │ │
│ │ │ │
│ │ 아키텍처: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Client │ │ │
│ │ │ ▼ │ │ │
│ │ │ Broker (Stateless, 수평 확장) │ │ │
│ │ │ ▼ │ │ │
│ │ │ BookKeeper (Storage, 분리된 계층) │ │ │
│ │ │ ▼ │ │ │
│ │ │ ZooKeeper (Metadata) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 토픽 계층 구조: │ │
│ │ persistent://tenant/namespace/topic │ │
│ │ ├── tenant: 조직/팀 (Multi-tenancy) │ │
│ │ ├── namespace: 정책 그룹 (보존, ACL 등) │ │
│ │ └── topic: 실제 토픽 │ │
│ │ │ │
│ │ 강점: │ │
│ │ ├── Multi-tenancy 네이티브 지원 │ │
│ │ ├── Geo-replication 네이티브 (다중 리전 복제) │ │
│ │ ├── Broker와 Storage 독립 확장 │ │
│ │ └── 매우 높은 처리량 (벤치마크 2.6M+ msgs/s) │ │
│ │ │ │
│ │ 약점: │ │
│ │ ├── 운영 복잡도 최고 (Broker+BK+ZK 3개 시스템) │ │
│ │ ├── Kafka 대비 커뮤니티/생태계 작음 │ │
│ │ └── 학습 곡선 가파름 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ NATS / JetStream │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: 극도의 단순성, "Always On" 메시징 │ │
│ │ 핵심: Go 단일 바이너리, 최소 설정 │ │
│ │ │ │
│ │ Core NATS: │ │
│ │ ├── Fire-and-forget (Redis Pub/Sub과 유사) │ │
│ │ ├── At-most-once 전달 │ │
│ │ ├── Subject 와일드카드: * (단일), > (하위 전체) │ │
│ │ │ 예: orders.* → orders.kr, orders.us │ │
│ │ │ 예: orders.> → orders.kr.seoul 등 하위 전체 │ │
│ │ └── Queue Groups (경쟁 소비자 패턴) │ │
│ │ │ │
│ │ JetStream (영속성 계층): │ │
│ │ ├── Core NATS 위에 영속성 추가 │ │
│ │ ├── At-least-once / Exactly-once 지원 │ │
│ │ ├── Raft 기반 복제 │ │
│ │ ├── 시간적 디커플링 (오프라인 Consumer 지원) │ │
│ │ └── Key-Value Store, Object Store 기능 포함 │ │
│ │ │ │
│ │ 강점: 극도의 단순성, 낮은 지연시간, 쉬운 운영 │ │
│ │ 약점: Kafka 수준의 대용량 처리 어려움, 생태계 작음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Amazon SQS / SNS / Kinesis / EventBridge │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ SQS (Simple Queue Service): │ │
│ │ ├── Pull 기반 P2P 큐 │ │
│ │ ├── Standard: at-least-once (순서 보장 없음) │ │
│ │ ├── FIFO: exactly-once (순서 보장, 초당 3,000건) │ │
│ │ └── 완전 관리형 (서버리스) │ │
│ │ │ │
│ │ SNS (Simple Notification Service): │ │
│ │ ├── Push 기반 Fan-out (1:N) │ │
│ │ ├── 최대 12,500,000 구독자 지원 │ │
│ │ ├── 프로토콜: HTTP, Email, SMS, SQS, Lambda │ │
│ │ └── SNS → SQS 조합 = Fan-out + 영속성 │ │
│ │ │ │
│ │ Kinesis: │ │
│ │ ├── Shard 기반 실시간 스트리밍 │ │
│ │ ├── 최대 365일 보존 │ │
│ │ ├── Kafka와 유사하지만 AWS 관리형 │ │
│ │ └── Shard당 1MB/s 쓰기, 2MB/s 읽기 │ │
│ │ │ │
│ │ EventBridge: │ │
│ │ ├── 이벤트 버스 (이벤트 드리븐 아키텍처) │ │
│ │ ├── 콘텐츠 기반 필터링 (100+ 규칙) │ │
│ │ ├── SaaS 통합 (Shopify, Zendesk 등) │ │
│ │ └── Schema Registry 내장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Google Cloud Pub/Sub │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: 글로벌 스케일, 관리형 메시징 │ │
│ │ │ │
│ │ 특징: │ │
│ │ ├── 전 세계 Google 데이터센터에 분산 │ │
│ │ ├── Spanner/Bigtable 파생 스토리지 │ │
│ │ ├── 자동 확장 (프로비저닝 불필요) │ │
│ │ ├── Exactly-once: 동일 Region 내에서만 보장 │ │
│ │ └── 7일 기본 메시지 보존 │ │
│ │ │ │
│ │ 강점: 완전 관리형, 글로벌 분산, 자동 확장 │ │
│ │ 약점: 벤더 종속, 세밀한 튜닝 어려움 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ ZeroMQ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: "Zero = broker, latency, admin, cost" │ │
│ │ 핵심: 메시징 라이브러리 (브로커가 아님!) │ │
│ │ │ │
│ │ 패턴: │ │
│ │ ├── PUB/SUB: 발행/구독 │ │
│ │ ├── PUSH/PULL: 작업 분배 │ │
│ │ ├── REQ/REP: 요청/응답 │ │
│ │ └── DEALER/ROUTER: 비동기 요청/응답 │ │
│ │ │ │
│ │ 강점: 최저 지연시간, 브로커 없음, 임베딩 가능 │ │
│ │ 약점: 영속성 없음, 직접 장애 처리, 운영 도구 없음 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Redpanda │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: Kafka API 호환 + C++ 고성능 │ │
│ │ 핵심: Thread-per-core (Seastar 프레임워크) │ │
│ │ │ │
│ │ 특징: │ │
│ │ ├── Kafka API 100% 호환 (클라이언트 교체 없이 전환) │ │
│ │ ├── C++: GC Pause 없음 │ │
│ │ ├── 공식 주장: 10x 낮은 레이턴시, 1/3 노드 수 │ │
│ │ ├── 단일 바이너리 (JVM, ZK 불필요) │ │
│ │ └── 내장 Schema Registry, HTTP Proxy │ │
│ │ │ │
│ │ ⚠ 주의: │ │
│ │ ├── 장기 연속 부하 시 성능 저하 보고 (독립 검증) │ │
│ │ └── Kafka 대비 커뮤니티/플러그인 생태계 작음 │ │
│ │ │ │
│ │ 강점: 낮은 레이턴시, 쉬운 운영, Kafka 호환 │ │
│ │ 약점: 장기 부하 성능, 생태계 크기, BSL 라이선스 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ WarpStream │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 설계 철학: S3-native, Diskless Kafka │ │
│ │ 핵심: Stateless Agent + 오브젝트 스토리지 │ │
│ │ │ │
│ │ 전통 Kafka: │ │
│ │ Agent ──→ Broker(디스크) ──→ 복제 ──→ Consumer │ │
│ │ (Cross-AZ 네트워크 비용 발생!) │ │
│ │ │ │
│ │ WarpStream: │ │
│ │ Agent ──→ S3 (같은 AZ) ──→ Consumer │ │
│ │ (Cross-AZ 네트워크 비용 근본 제거!) │ │
│ │ │ │
│ │ 특징: │ │
│ │ ├── Stateless Agent (로컬 디스크 불필요) │ │
│ │ ├── Cross-AZ 네트워크 비용 80%+ 절감 │ │
│ │ ├── BYOC (Bring Your Own Cloud) 모델 │ │
│ │ └── 2024년 Confluent에 $220M 인수 │ │
│ │ │ │
│ │ 강점: 비용 절감, 운영 단순, Kafka 호환 │ │
│ │ 약점: 레이턴시 증가 (S3 경유), 새로운 기술 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.7 종합 비교표 (Comparison Matrix)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 종합 비교표 │
│ │
│ ■ 핵심 특성 비교 (10개 시스템) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────┬────────┬──────┬──────┬──────┬──────┐ │ │
│ │ │ 시스템 │영속성 │순서 │전달 │리플레│CG │ │ │
│ │ │ │ │보장 │보장 │이 │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │Redis │ X │ X │At- │ X │ X │ │ │
│ │ │Pub/Sub │ │ │most-1│ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │Redis │ O │ O │At- │ O │ O │ │ │
│ │ │Streams │(메모리)│ │least1│ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │Kafka │ O │파티션│All 3 │ O │ O │ │ │
│ │ │ │(디스크)│내 │ │ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │RabbitMQ │ O │큐 내 │At- │ X │경쟁 │ │ │
│ │ │ │(디스크)│ │least1│ │소비자│ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │Pulsar │ O │파티션│All 3 │ O │ O │ │ │
│ │ │ │(BK) │내 │ │ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │NATS Core │ X │ X │At- │ X │Queue │ │ │
│ │ │ │ │ │most-1│ │Group │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │NATS │ O │ O │At- │ O │ O │ │ │
│ │ │JetStream │(Raft) │ │least1│ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │SQS │ O │FIFO만│At- │ X │ X │ │ │
│ │ │ │ │ │least1│ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │Google │ O │ X │At- │ O │ O │ │ │
│ │ │Pub/Sub │ │ │least1│(30d) │ │ │ │
│ │ ├──────────┼────────┼──────┼──────┼──────┼──────┤ │ │
│ │ │ZeroMQ │ X │ X │At- │ X │ X │ │ │
│ │ │ │ │ │most-1│ │ │ │ │
│ │ └──────────┴────────┴──────┴──────┴──────┴──────┘ │ │
│ │ │ │
│ │ ┌──────────┬────────┬──────┬──────────┬──────────┐ │ │
│ │ │ 시스템 │프로토콜│확장성│운영복잡도│라이선스 │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │Redis │RESP │중간 │낮음 │SSPL/ │ │ │
│ │ │Pub/Sub │ │ │ │Valkey BSD│ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │Redis │RESP │중간 │낮음 │SSPL/ │ │ │
│ │ │Streams │ │ │ │Valkey BSD│ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │Kafka │자체 │매우 │높음 │Apache 2.0│ │ │
│ │ │ │바이너리│높음 │ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │RabbitMQ │AMQP │높음 │중간 │MPL 2.0 │ │ │
│ │ │ │STOMP등 │ │ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │Pulsar │자체 │매우 │매우높음 │Apache 2.0│ │ │
│ │ │ │바이너리│높음 │ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │NATS │자체 │높음 │낮음 │Apache 2.0│ │ │
│ │ │ │텍스트 │ │ │ │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │SQS │HTTPS │무한 │없음 │관리형 │ │ │
│ │ │ │ │(AWS) │(서버리스)│ │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │Google │gRPC │무한 │없음 │관리형 │ │ │
│ │ │Pub/Sub │HTTPS │ │(서버리스)│ │ │ │
│ │ ├──────────┼────────┼──────┼──────────┼──────────┤ │ │
│ │ │ZeroMQ │자체 │제한적│최저 │MPL 2.0 │ │ │
│ │ │ │(소켓) │ │(라이브러리)│ │ │ │
│ │ └──────────┴────────┴──────┴──────────┴──────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ■ 성능 벤치마크 데이터 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ StreamNative 2023 벤치마크: │ │
│ │ ├── Pulsar: 2,600,000 msgs/s │ │
│ │ ├── NATS: 160,000 msgs/s │ │
│ │ └── RabbitMQ: 48,000 msgs/s │ │
│ │ │ │
│ │ DevOps.dev 단일 노드 벤치마크: │ │
│ │ ├── RabbitMQ: ~2,667 RPS │ │
│ │ ├── Kafka: ~2,369 RPS │ │
│ │ └── Redis: ~2,097 RPS │ │
│ │ (단일 노드에서는 비슷, 클러스터에서 Kafka 압도) │ │
│ │ │ │
│ │ Redpanda 공식 주장: │ │
│ │ ├── Kafka 대비 10x 낮은 p99 레이턴시 │ │
│ │ └── 독립 검증: 장기 연속 부하 시 성능 저하 보고 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 운영 복잡도 스펙트럼 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 낮음 ◄──────────────────────────────────────► 높음 │ │
│ │ │ │
│ │ ZeroMQ Redis NATS Redis Rabbit Redpanda │ │
│ │ (라이브 Pub/Sub Streams MQ │ │
│ │ 러리) │ │
│ │ ─┼────────┼────────┼──────┼────────┼────────┼── │ │
│ │ │ │
│ │ Kafka Pulsar │ │
│ │ ────┼──────┼── │ │
│ │ │ │
│ │ ※ SQS/Google Pub/Sub = 운영 복잡도 0 (서버리스) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 처리량 스펙트럼 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 낮음 ◄──────────────────────────────────────► 높음 │ │
│ │ │ │
│ │ ZeroMQ RabbitMQ NATS Redpanda Pulsar │ │
│ │ (단일 ~48K/s JetStream /Kafka 2.6M+/s │ │
│ │ 프로세스 ~160K/s 수 M/s │ │
│ │ 한계) │ │
│ │ ─┼───────┼──────────┼──────────┼───────────┼── │ │
│ │ │ │
│ │ ※ 클러스터 확장 시 처리량 (단일 노드와 다름) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.8 시나리오별 최적 선택 (Best Choice by Scenario)
┌─────────────────────────────────────────────────────────────────┐
│ 12가지 시나리오별 최적 메시징 시스템 선택 │
│ │
│ ■ 시나리오 1: 실시간 채팅 (메시지 유실 허용) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Redis Pub/Sub │ │
│ │ 이유: 초저지연, 간단한 구현, 유실돼도 다음 메시지 있음 │ │
│ │ 대안: NATS Core │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 2: 주문 처리 시스템 (유실 불가) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: RabbitMQ │ │
│ │ 이유: ACK/NACK, DLQ, 복잡한 라우팅, 재시도 내장 │ │
│ │ 대안: Kafka (대량 주문 시) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 3: IoT 센서 데이터 수집 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: MQTT (수집) + Kafka (파이프라인) │ │
│ │ 이유: MQTT 2바이트 헤더 (저대역), Kafka 대용량 처리 │ │
│ │ 대안: NATS (경량 대안) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 4: 마이크로서비스 이벤트 드리븐 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: RabbitMQ │ │
│ │ 이유: 유연한 Exchange 라우팅, 서비스별 큐 분리 │ │
│ │ 대안: Kafka (대규모), NATS JetStream (간편) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 5: 로그/이벤트 파이프라인 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Kafka │ │
│ │ 이유: 대용량 처리, 장기 보존, 리플레이, 에코시스템 │ │
│ │ 대안: Redpanda (운영 간편), Kinesis (AWS) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 6: 캐시 무효화 브로드캐스트 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Redis Pub/Sub │ │
│ │ 이유: 이미 캐시용 Redis 사용 중, 추가 인프라 불필요 │ │
│ │ 대안: Redis 7.0 Sharded Pub/Sub (Cluster 환경) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 7: 비동기 작업 큐 (이메일, 이미지 처리) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: RabbitMQ │ │
│ │ 이유: ACK, prefetch 조절, DLQ, 우선순위 큐 │ │
│ │ 대안: Redis Streams (가벼운 작업 큐) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 8: 실시간 스트림 처리 (분석, 집계) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Kafka + Kafka Streams / Flink │ │
│ │ 이유: 리플레이, 파티션 병렬 처리, 스트림 처리 생태계 │ │
│ │ 대안: Pulsar + Functions │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 9: 글로벌 분산 시스템 (다중 리전) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Apache Pulsar │ │
│ │ 이유: Geo-replication 네이티브, Multi-tenancy │ │
│ │ 대안: Google Cloud Pub/Sub, Confluent Cloud │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 10: 스타트업 MVP (빠른 시작) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Redis Streams │ │
│ │ 이유: 이미 Redis 사용 중, 추가 인프라 없음, 간단한 API │ │
│ │ 대안: SQS (AWS 사용 시), NATS (경량) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 11: 대규모 이벤트 소싱 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: Kafka │ │
│ │ 이유: 무기한 보존, 리플레이, Log-Table Duality │ │
│ │ 대안: Pulsar (multi-tenancy 필요 시) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 시나리오 12: 모바일 푸시 알림 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 추천: AWS SNS │ │
│ │ 이유: APNS/FCM 직접 통합, 수백만 디바이스, 관리형 │ │
│ │ 대안: Google Cloud Pub/Sub + Firebase │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Redis Pub/Sub을 쓰면 안 되는 경우 (구체적 실패 사례) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 오프라인 컨슈머가 존재하는 경우: │ │
│ │ ├── 구독자가 잠시라도 연결 해제되면 메시지 소실 │ │
│ │ └── 재연결 후 복구 메커니즘 없음 │ │
│ │ │ │
│ │ 2. TCP 연결 끊김 감지 실패 (알려진 버그): │ │
│ │ ├── 네트워크 불안정 시 연결 상태 감지 지연 │ │
│ │ └── Redis는 "연결됨"으로 인지하나 실제로는 끊김 │ │
│ │ │ │
│ │ 3. 수평 확장 시 모든 인스턴스 중복 수신: │ │
│ │ ├── App 서버 3대 → 동일 메시지 3번 처리 │ │
│ │ └── Consumer Group 없어서 부하 분산 불가 │ │
│ │ │ │
│ │ 4. 구독자 증가 시 선형 성능 저하: │ │
│ │ ├── 1개 PUBLISH → N개 구독자에게 각각 복사 전달 │ │
│ │ └── 구독자 1만+ → 눈에 띄는 지연 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Kafka가 오버킬인 경우 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ├── 일 수천 건 메시지 (Kafka 클러스터 유지 비용 낭비) │ │
│ │ ├── 서비스 2-3개 간 단순 이벤트 │ │
│ │ ├── 스타트업 초기 (운영 인력/지식 부족) │ │
│ │ └── 밀리초 이하 초저지연이 최우선 │ │
│ │ │ │
│ │ → 이럴 때는 Redis Streams, NATS, RabbitMQ 권장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 의사결정 트리 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 메시지 유실 허용? │ │
│ │ ├── YES → Redis Pub/Sub / NATS Core │ │
│ │ │ │ │
│ │ └── NO → 처리량 1M+ msgs/s 필요? │ │
│ │ ├── YES → Kafka / Pulsar / Redpanda │ │
│ │ │ │ │
│ │ └── NO → 복잡한 라우팅 필요? │ │
│ │ ├── YES → RabbitMQ │ │
│ │ │ │ │
│ │ └── NO → 인프라 관리 최소화? │ │
│ │ ├── YES → SQS / SNS / │ │
│ │ │ Google Pub/Sub │ │
│ │ │ │ │
│ │ └── NO → Redis 이미 사용? │ │
│ │ ├── YES → Redis │ │
│ │ │ Streams │ │
│ │ │ │ │
│ │ └── NO → NATS │ │
│ │ JetStream │ │
│ │ / Kafka │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.9 베스트 프랙티스 (Best Practices)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 베스트 프랙티스 │
│ │
│ ■ Redis Pub/Sub 베스트 프랙티스 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 적합한 사용 사례: │ │
│ │ ├── 실시간 스포츠 스코어 알림 │ │
│ │ ├── 소셜 피드 실시간 업데이트 │ │
│ │ ├── 채팅 메시지 브로드캐스트 │ │
│ │ ├── 캐시 무효화 (최적 사용 사례!) │ │
│ │ └── 사용자 프레즌스 (온라인/오프라인 상태) │ │
│ │ │ │
│ │ 유실 대응 전략: │ │
│ │ ├── Dual-write: PUBLISH + 백업 저장 병행 │ │
│ │ │ (RPUSH, XADD 등으로 백업) │ │
│ │ └── 외부 시퀀스 번호 부여 → 빠진 번호 감지 │ │
│ │ │ │
│ │ 구독자 수 권장: │ │
│ │ ├── 수백까지: 문제 없음 │ │
│ │ ├── 수천: 주의 필요 (출력 버퍼 모니터링) │ │
│ │ └── 1만+: 병목 발생 가능 (다른 솔루션 검토) │ │
│ │ │ │
│ │ PSUBSCRIBE 주의: │ │
│ │ ├── 패턴 수 최소화 (성능 O(N*M)) │ │
│ │ ├── SUBSCRIBE와 PSUBSCRIBE 혼용 시 2회 수신 주의 │ │
│ │ └── 가능하면 정확한 채널명 SUBSCRIBE 사용 │ │
│ │ │ │
│ │ Redis 7.0+ Cluster 환경: │ │
│ │ ├── SSUBSCRIBE/SPUBLISH 사용 (Sharded Pub/Sub) │ │
│ │ └── 기존 SUBSCRIBE/PUBLISH 대비 네트워크 효율 향상 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Redis Streams 베스트 프랙티스 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Consumer Group 설계: │ │
│ │ ├── 책임별 분리: order-processor, notification-sender │ │
│ │ ├── Consumer 수 = 애플리케이션 인스턴스 수 │ │
│ │ └── Consumer 이름: {서비스}-{인스턴스ID} │ │
│ │ │ │
│ │ XACK 규칙: │ │
│ │ ├── 처리 성공 후 반드시 XACK! │ │
│ │ ├── 미ACK → PEL에 남아서 메모리 소비 │ │
│ │ └── 예외 발생 시에도 finally에서 XACK 또는 재시도 │ │
│ │ │ │
│ │ 미처리 메시지 관리: │ │
│ │ ├── XAUTOCLAIM (Redis 6.2+) 활용 │ │
│ │ │ 5분 이상 pending → 자동으로 다른 Consumer에 할당 │ │
│ │ ├── XPENDING으로 주기적 모니터링 │ │
│ │ └── DLQ 패턴: times-delivered > MAX_RETRIES │ │
│ │ → 별도 DLQ stream으로 이동 │ │
│ │ │ │
│ │ 메모리 관리: │ │
│ │ ├── MAXLEN ~ 사용 (근사 트리밍, 성능 좋음) │ │
│ │ ├── MINID ~ 사용 (시간 기반 보존 정책) │ │
│ │ ├── XDEL 피하기! (Swiss cheese 메모리 단편화) │ │
│ │ └── MEMORY USAGE key로 주기적 크기 확인 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Kafka 베스트 프랙티스 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 파티션 설계: │ │
│ │ ├── 소규모: 3-6개 │ │
│ │ ├── 중간: 10-30개 │ │
│ │ ├── 최대 Consumer 수의 1.5~2배로 설정 │ │
│ │ └── 파티션은 늘리기 쉽지만 줄이기 어려움! │ │
│ │ │ │
│ │ Consumer Group: │ │
│ │ ├── CooperativeStickyAssignor (Kafka 2.4+) 사용 │ │
│ │ │ (리밸런싱 시 전체 정지 방지) │ │
│ │ ├── enable-auto-commit: false (수동 커밋 권장) │ │
│ │ └── Consumer 수 <= 파티션 수 (초과 시 유휴 Consumer) │ │
│ │ │ │
│ │ Idempotent Producer: │ │
│ │ ├── enable.idempotence=true │ │
│ │ ├── acks=all │ │
│ │ └── 중복 전송 방지 (PID + Sequence Number) │ │
│ │ │ │
│ │ 토픽 네이밍 컨벤션: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ <도메인>.<엔티티>.<이벤트타입>.<버전> │ │ │
│ │ │ │ │ │
│ │ │ 예시: │ │ │
│ │ │ ├── order.payment.completed.v1 │ │ │
│ │ │ ├── user.account.created.v2 │ │ │
│ │ │ └── inventory.stock.updated.v1 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 보존 기간: │ │
│ │ ├── 실시간 처리: 3-7일 │ │
│ │ ├── 감사/규정 준수: 30-90일 │ │
│ │ └── 이벤트 소싱: 무기한 (retention.ms=-1) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ RabbitMQ 베스트 프랙티스 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Exchange 활용: │ │
│ │ ├── 직접 Queue에 메시지 보내지 말 것! (안티패턴) │ │
│ │ ├── Exchange를 통한 라우팅이 핵심 장점 │ │
│ │ └── 향후 라우팅 변경 시 Producer 수정 불필요 │ │
│ │ │ │
│ │ Prefetch (QoS): │ │
│ │ ├── 기본값: 10-50 (워크로드에 따라 조절) │ │
│ │ ├── 공식: 왕복 지연 시간 / 메시지 처리 시간 │ │
│ │ ├── 너무 낮으면: Consumer 유휴 시간 증가 │ │
│ │ └── 너무 높으면: 메모리 소비 + 불균등 분배 │ │
│ │ │ │
│ │ DLX (Dead Letter Exchange): │ │
│ │ ├── 트리거 조건: │ │
│ │ │ ├── basic.reject / basic.nack (requeue=false) │ │
│ │ │ ├── TTL 만료 │ │
│ │ │ └── Queue 길이 초과 (x-max-length) │ │
│ │ └── 모든 Queue에 DLX 설정 권장 (메시지 유실 방지) │ │
│ │ │ │
│ │ Lazy Queue / Quorum Queue: │ │
│ │ ├── Lazy Queue: 디스크 직접 저장 (대용량, 메모리 절약) │ │
│ │ ├── RabbitMQ 3.12+: Quorum Queue 권장 │ │
│ │ │ (Raft 기반 복제, 데이터 안전성 향상) │ │
│ │ └── Classic Queue는 단계적 폐지 예정 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 공통 베스트 프랙티스 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 1. 스키마 관리: │ │
│ │ ├── Schema Registry + Avro 또는 Protobuf 사용 │ │
│ │ ├── 하위 호환성(Backward Compatibility) 유지 │ │
│ │ └── JSON은 편하지만 스키마 검증 없음 (주의) │ │
│ │ │ │
│ │ 2. Idempotent Consumer (멱등성 소비자): │ │
│ │ ├── DB Unique Constraint로 중복 확인 │ │
│ │ ├── Redis SET NX로 메시지 ID 중복 체크 │ │
│ │ └── "동일 메시지 N번 처리해도 결과 동일" │ │
│ │ │ │
│ │ 3. Circuit Breaker: │ │
│ │ ├── Resilience4j + Consumer 일시정지 │ │
│ │ ├── 외부 서비스 장애 시 메시지 소비 중단 │ │
│ │ └── 장애 복구 후 자동 재개 │ │
│ │ │ │
│ │ 4. 모니터링 핵심 메트릭: │ │
│ │ ├── consumer_lag: 가장 중요! (처리 지연 정도) │ │
│ │ ├── records_error_rate: 처리 실패율 │ │
│ │ ├── under_replicated_partitions (Kafka): 복제 부족 │ │
│ │ ├── pending_count (Streams): 미처리 메시지 수 │ │
│ │ └── queue_depth (RabbitMQ): 큐 적체량 │ │
│ │ │ │
│ │ 5. 메시지 크기 가이드: │ │
│ │ ├── Kafka: <1MB (max.message.bytes) │ │
│ │ ├── RabbitMQ: <128KB 권장 │ │
│ │ ├── Redis: <64KB 권장 │ │
│ │ └── 대용량 → Claim-Check 패턴 사용 │ │
│ │ │ │
│ │ Claim-Check 패턴: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ Producer │ │ │
│ │ │ ├── 1. 대용량 데이터 → S3/DB에 저장 │ │ │
│ │ │ └── 2. 참조 URL/ID만 메시지로 전송 │ │ │
│ │ │ │ │ │
│ │ │ Consumer │ │ │
│ │ │ ├── 1. 메시지에서 참조 URL/ID 추출 │ │ │
│ │ │ └── 2. S3/DB에서 실제 데이터 조회 │ │ │
│ │ │ │ │ │
│ │ │ 메시지: {"file_ref": "s3://bucket/obj123"} │ │ │
│ │ │ (10MB 파일 대신 50바이트 참조만 전송!) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.10 안티패턴 (Anti-patterns)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 9가지 안티패턴 │
│ │
│ ■ 안티패턴 1: Redis Pub/Sub을 메시지 큐로 사용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: Pub/Sub을 작업 큐처럼 사용 → 메시지 유실 발생 │ │
│ │ 증상: 서버 재시작 후 처리되지 않은 작업 소실 │ │
│ │ 구독자 없으면 메시지가 그냥 사라짐 │ │
│ │ 해결: Redis Streams 또는 RabbitMQ로 교체 │ │
│ │ 유실 허용 불가 → 절대 Pub/Sub 사용 금지 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 2: Kafka를 단순 작업 큐로 사용 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: 파티션 기반이라 개별 메시지 ACK 불가 │ │
│ │ 증상: 특정 메시지 실패 시 전체 파티션 오프셋 정체 │ │
│ │ 재시도 로직 복잡, 메시지 순서 꼬임 │ │
│ │ 해결: 작업 큐 → RabbitMQ (개별 ACK, DLQ, 우선순위) │ │
│ │ Kafka는 "스트리밍 파이프라인"에 적합 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 3: 대용량 페이로드 직접 전송 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: 10MB 이미지를 메시지 본문에 포함 │ │
│ │ 증상: 브로커 메모리 폭발, 네트워크 대역폭 소진 │ │
│ │ Consumer 처리 지연, 전체 시스템 성능 저하 │ │
│ │ 해결: Claim-Check 패턴 사용 │ │
│ │ 메시지에는 참조(URL/ID)만 포함 │ │
│ │ 실제 데이터는 S3/DB에 별도 저장 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 4: Consumer Lag 모니터링 누락 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: Consumer가 뒤처지는 것을 감지하지 못함 │ │
│ │ 증상: 데이터 처리 지연이 수 시간~수일까지 누적 │ │
│ │ 디스크 가득 참, 메모리 부족 (Streams) │ │
│ │ 장애 인지 시점에 이미 복구 불가 │ │
│ │ 해결: consumer_lag 메트릭 반드시 모니터링 │ │
│ │ Kafka: Burrow, 자체 메트릭 │ │
│ │ Streams: XPENDING 주기적 확인 │ │
│ │ 알림 임계값: lag > 1000 → 경고, > 10000 → 심각 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 5: 단일 토픽에 모든 메시지 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: 모든 이벤트를 하나의 토픽/채널에 발행 │ │
│ │ 증상: Consumer가 불필요한 메시지까지 처리 │ │
│ │ 필터링 비용 증가, 파티셔닝 비효율 │ │
│ │ 토픽 관련 설정 변경이 모든 이벤트에 영향 │ │
│ │ 해결: 이벤트 타입별 토픽 분리 │ │
│ │ 도메인.엔티티.이벤트 형식 네이밍 │ │
│ │ 관련 이벤트만 같은 토픽에 그룹핑 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 6: 재시도 로직 없는 Consumer │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: 처리 실패 시 메시지를 그냥 버림 │ │
│ │ 증상: 일시적 장애(DB 타임아웃 등)에도 메시지 영구 유실 │ │
│ │ 데이터 정합성 깨짐, 운영 이슈 빈발 │ │
│ │ 해결: 지수 백오프 재시도 (1s, 2s, 4s, 8s...) │ │
│ │ 최대 재시도 횟수 설정 (3-5회) │ │
│ │ 최대 재시도 초과 → DLQ로 이동 │ │
│ │ DLQ 메시지 주기적 검토/재처리 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 7: Schema 관리 없이 구조 변경 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: 메시지 형식을 무계획으로 변경 │ │
│ │ 증상: 구버전 Consumer가 새 형식 파싱 실패 │ │
│ │ 런타임 역직렬화 에러 대량 발생 │ │
│ │ 어떤 Consumer가 어떤 버전인지 파악 불가 │ │
│ │ 해결: Schema Registry 도입 (Confluent, Apicurio) │ │
│ │ Avro/Protobuf (스키마 진화 지원) │ │
│ │ 항상 하위 호환성 유지 (필드 추가만, 삭제 불가) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 8: Pub/Sub에서 메시지 순서 의존 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: Redis Pub/Sub이 메시지 순서를 보장한다고 가정 │ │
│ │ 증상: 단일 Publisher → 단일 Subscriber는 순서 유지 │ │
│ │ 다중 Publisher 시 순서 보장 없음 │ │
│ │ 네트워크 지연으로 도착 순서 변경 가능 │ │
│ │ 해결: 순서 의존 로직 → Kafka (파티션 내 순서 보장) │ │
│ │ 또는 Redis Streams (단일 스트림 내 순서 보장) │ │
│ │ 메시지에 타임스탬프/시퀀스 포함하여 Consumer 정렬 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 안티패턴 9: Exactly-once 맹신 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 문제: "Exactly-once 지원" 문구만 보고 중복 처리 안 함 │ │
│ │ 증상: 네트워크 파티션, 장애 복구 시 중복 메시지 발생 │ │
│ │ 결제 2회 처리, 재고 2회 차감 등 심각한 문제 │ │
│ │ 해결: Exactly-once는 이론적으로 불가능 │ │
│ │ (Two Generals Problem, FLP Impossibility) │ │
│ │ 항상 Consumer Idempotency 구현! │ │
│ │ ├── DB Unique Constraint │ │
│ │ ├── Redis SET NX (메시지 ID 중복 체크) │ │
│ │ └── Idempotent Key 설계 (주문ID+상태 조합) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.11 마이그레이션 가이드 (Migration Guide)
┌─────────────────────────────────────────────────────────────────┐
│ 메시징 시스템 마이그레이션 가이드 │
│ │
│ ■ Redis Pub/Sub → Redis Streams │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 난이도: ★★☆☆☆ (동일 Redis, API만 변경) │ │
│ │ │ │
│ │ 단계별 전환: │ │
│ │ │ │
│ │ Phase 1: Dual-write 시작 │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ // Producer 수정 │ │ │
│ │ │ MULTI │ │ │
│ │ │ PUBLISH channel:orders message // 기존 │ │ │
│ │ │ XADD stream:orders * data message // 신규 │ │ │
│ │ │ EXEC │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Phase 2: Consumer 전환 │ │
│ │ ├── 기존 SUBSCRIBE → XREADGROUP으로 하나씩 전환 │ │
│ │ ├── Consumer Group 생성: XGROUP CREATE stream:orders │ │
│ │ │ mygroup $ MKSTREAM │ │
│ │ └── 전환된 Consumer 정상 동작 확인 │ │
│ │ │ │
│ │ Phase 3: PUBLISH 제거 │ │
│ │ ├── 모든 Consumer 전환 완료 확인 │ │
│ │ ├── PUBLISH 코드 제거 │ │
│ │ └── XADD만 유지 │ │
│ │ │ │
│ │ 주의: │ │
│ │ ├── Pub/Sub: Push 모델 → Streams: Pull 모델 │ │
│ │ ├── Consumer 로직을 Polling 방식으로 변경 필요 │ │
│ │ └── XREADGROUP BLOCK으로 Push 유사 경험 가능 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Redis Pub/Sub → Kafka │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 난이도: ★★★★☆ (인프라 + 패러다임 전환) │ │
│ │ │ │
│ │ 핵심 변경 사항: │ │
│ │ ├── Push → Pull 모델 전환 │ │
│ │ ├── Channel → Topic + Partition 설계 │ │
│ │ ├── 구독 기반 → Consumer Group + Offset 관리 │ │
│ │ └── 메시지 직렬화 형식 결정 (JSON/Avro/Protobuf) │ │
│ │ │ │
│ │ 단계: │ │
│ │ 1. Kafka 클러스터 구축 및 토픽 설계 │ │
│ │ 2. Producer에서 Dual-write (PUBLISH + Kafka produce) │ │
│ │ 3. 신규 Consumer부터 Kafka에서 소비 │ │
│ │ 4. 기존 Consumer를 하나씩 Kafka로 전환 │ │
│ │ 5. 모든 전환 완료 → PUBLISH 제거 │ │
│ │ │ │
│ │ 주의: │ │
│ │ ├── Kafka는 파티션 내에서만 순서 보장 │ │
│ │ ├── Partition Key 설계가 매우 중요 │ │
│ │ └── Consumer Group 리밸런싱 시 일시적 지연 발생 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ RabbitMQ → Kafka │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 난이도: ★★★★★ (철학 자체가 다름) │ │
│ │ │ │
│ │ 핵심 패러다임 차이: │ │
│ │ ├── Smart Broker → Smart Consumer │ │
│ │ ├── Exchange+Routing → Topic+Partition │ │
│ │ ├── 메시지 소비 후 삭제 → 보존 기간 동안 유지 │ │
│ │ └── Push 모델 → Pull 모델 │ │
│ │ │ │
│ │ 매핑 전략: │ │
│ │ ├── Exchange → Kafka Topic │ │
│ │ ├── Queue → Consumer Group │ │
│ │ ├── Routing Key → Partition Key │ │
│ │ └── DLX → DLT(Dead Letter Topic) + @DltHandler │ │
│ │ │ │
│ │ 전환 방법: │ │
│ │ ├── 옵션 A: Dual-write 점진적 전환 │ │
│ │ │ 1. 신규 서비스부터 Kafka 사용 │ │
│ │ │ 2. 기존 서비스 하나씩 전환 │ │
│ │ │ 3. 모든 전환 완료 → RabbitMQ 제거 │ │
│ │ │ │ │
│ │ └── 옵션 B: Kafka Connect 브리지 │ │
│ │ RabbitMQ Source Connector로 기존 메시지를 │ │
│ │ Kafka에 미러링하면서 점진적 전환 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Zero-downtime 전략 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Blue/Green 전환: │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Phase 1: Blue(기존) 100% / Green(신규) 0% │ │ │
│ │ │ Phase 2: Blue 100% / Green 0% (Green 테스트) │ │ │
│ │ │ Phase 3: Blue 0% / Green 100% (즉시 전환) │ │ │
│ │ │ Phase 4: Blue 제거 / Green 100% │ │ │
│ │ │ │ │ │
│ │ │ 장점: 빠른 롤백 (Blue로 복귀) │ │ │
│ │ │ 단점: 두 시스템 동시 운영 비용 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Canary 전환 (점진적): │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ Phase 1: 기존 100% (신규 준비) │ │ │
│ │ │ Phase 2: 기존 90% / 신규 10% (카나리 테스트) │ │ │
│ │ │ Phase 3: 기존 70% / 신규 30% (확대) │ │ │
│ │ │ Phase 4: 기존 0% / 신규 100% (완료) │ │ │
│ │ │ │ │ │
│ │ │ 장점: 위험 최소화 (문제 시 10%만 영향) │ │ │
│ │ │ 단점: 전환 기간 길어짐 │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 공통 주의사항: │ │
│ │ ├── Dual-write 기간 동안 양쪽 모두 모니터링 필수 │ │
│ │ ├── Consumer Idempotency 반드시 구현 (중복 처리 방지) │ │
│ │ ├── 전환 전 충분한 부하 테스트 수행 │ │
│ │ └── 롤백 계획 반드시 수립 (전환 실패 대비) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
11.12 Spring Boot/Java 통합 (Integration Examples)
┌─────────────────────────────────────────────────────────────────┐
│ Spring Boot 메시징 통합 코드 예시 │
│ │
│ ■ Spring Data Redis Pub/Sub │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ // 1. 설정 (Configuration) │ │
│ │ @Configuration │ │
│ │ public class RedisPubSubConfig { │ │
│ │ │ │
│ │ @Bean │ │
│ │ RedisMessageListenerContainer container( │ │
│ │ RedisConnectionFactory factory, │ │
│ │ MessageListenerAdapter adapter) { │ │
│ │ │ │
│ │ RedisMessageListenerContainer container = │ │
│ │ new RedisMessageListenerContainer(); │ │
│ │ container.setConnectionFactory(factory); │ │
│ │ container.addMessageListener(adapter, │ │
│ │ new ChannelTopic("orders")); │ │
│ │ return container; │ │
│ │ } │ │
│ │ │ │
│ │ @Bean │ │
│ │ MessageListenerAdapter adapter( │ │
│ │ OrderSubscriber subscriber) { │ │
│ │ return new MessageListenerAdapter( │ │
│ │ subscriber, "onMessage"); │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 2. 구독자 (Subscriber) │ │
│ │ @Component │ │
│ │ public class OrderSubscriber { │ │
│ │ public void onMessage(String message, │ │
│ │ String channel) { │ │
│ │ log.info("Channel: {}, Msg: {}", │ │
│ │ channel, message); │ │
│ │ // 비즈니스 로직 처리 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 3. 발행 (Publisher) │ │
│ │ @Service │ │
│ │ @RequiredArgsConstructor │ │
│ │ public class OrderPublisher { │ │
│ │ private final StringRedisTemplate redisTemplate; │ │
│ │ │ │
│ │ public void publish(String message) { │ │
│ │ redisTemplate.convertAndSend("orders", message); │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Spring Data Redis Streams │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ // 1. Producer │ │
│ │ @Service │ │
│ │ @RequiredArgsConstructor │ │
│ │ public class OrderStreamProducer { │ │
│ │ private final StringRedisTemplate redisTemplate; │ │
│ │ │ │
│ │ public RecordId send(OrderEvent event) { │ │
│ │ StringRecord record = StreamRecords.string( │ │
│ │ Map.of( │ │
│ │ "orderId", event.getOrderId(), │ │
│ │ "status", event.getStatus(), │ │
│ │ "amount", event.getAmount().toString() │ │
│ │ ) │ │
│ │ ).withStreamKey("stream:orders"); │ │
│ │ │ │
│ │ return redisTemplate.opsForStream() │ │
│ │ .add(record); │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 2. Consumer (Consumer Group) │ │
│ │ @Configuration │ │
│ │ public class StreamConsumerConfig { │ │
│ │ │ │
│ │ @Bean │ │
│ │ StreamMessageListenerContainer<String, MapRecord │ │
│ │ <String, String, String>> container( │ │
│ │ RedisConnectionFactory factory) { │ │
│ │ │ │
│ │ var options = StreamMessageListenerContainer │ │
│ │ .StreamMessageListenerContainerOptions │ │
│ │ .builder() │ │
│ │ .pollTimeout(Duration.ofSeconds(1)) │ │
│ │ .build(); │ │
│ │ │ │
│ │ var container = StreamMessageListenerContainer │ │
│ │ .create(factory, options); │ │
│ │ │ │
│ │ container.receive( │ │
│ │ Consumer.from("order-group", "consumer-1"), │ │
│ │ StreamOffset.create("stream:orders", │ │
│ │ ReadOffset.lastConsumed()), │ │
│ │ new OrderStreamListener(redisTemplate) │ │
│ │ ); │ │
│ │ │ │
│ │ container.start(); │ │
│ │ return container; │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 3. Listener (XREADGROUP + XACK) │ │
│ │ public class OrderStreamListener implements │ │
│ │ StreamListener<String, MapRecord │ │
│ │ <String, String, String>> { │ │
│ │ │ │
│ │ private final StringRedisTemplate redisTemplate; │ │
│ │ │ │
│ │ @Override │ │
│ │ public void onMessage(MapRecord<String, String, │ │
│ │ String> message) { │ │
│ │ try { │ │
│ │ String orderId = message.getValue() │ │
│ │ .get("orderId"); │ │
│ │ // 비즈니스 로직 처리 │ │
│ │ log.info("Processing order: {}", orderId); │ │
│ │ │ │
│ │ // 처리 성공 → ACK │ │
│ │ redisTemplate.opsForStream() │ │
│ │ .acknowledge("stream:orders", │ │
│ │ "order-group", │ │
│ │ message.getId()); │ │
│ │ } catch (Exception e) { │ │
│ │ log.error("Failed: {}", message.getId(), e); │ │
│ │ // ACK 안 함 → PEL에 남아서 재처리 대상 │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Spring Kafka │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ // application.yml │ │
│ │ spring: │ │
│ │ kafka: │ │
│ │ bootstrap-servers: localhost:9092 │ │
│ │ producer: │ │
│ │ key-serializer: StringSerializer │ │
│ │ value-serializer: JsonSerializer │ │
│ │ acks: all │ │
│ │ properties: │ │
│ │ enable.idempotence: true │ │
│ │ consumer: │ │
│ │ group-id: order-service │ │
│ │ auto-offset-reset: earliest │ │
│ │ enable-auto-commit: false │ │
│ │ key-deserializer: StringDeserializer │ │
│ │ value-deserializer: JsonDeserializer │ │
│ │ │ │
│ │ // 1. Producer │ │
│ │ @Service │ │
│ │ @RequiredArgsConstructor │ │
│ │ public class OrderKafkaProducer { │ │
│ │ private final KafkaTemplate<String, OrderEvent> │ │
│ │ kafkaTemplate; │ │
│ │ │ │
│ │ public void send(OrderEvent event) { │ │
│ │ kafkaTemplate.send( │ │
│ │ "order.payment.completed.v1", │ │
│ │ event.getOrderId(), // Partition Key │ │
│ │ event │ │
│ │ ).whenComplete((result, ex) -> { │ │
│ │ if (ex != null) { │ │
│ │ log.error("Send failed", ex); │ │
│ │ } │ │
│ │ }); │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 2. Consumer (@KafkaListener) │ │
│ │ @Component │ │
│ │ public class OrderKafkaConsumer { │ │
│ │ │ │
│ │ @RetryableTopic( │ │
│ │ attempts = "3", │ │
│ │ backoff = @Backoff(delay = 1000, │ │
│ │ multiplier = 2.0) │ │
│ │ ) │ │
│ │ @KafkaListener( │ │
│ │ topics = "order.payment.completed.v1", │ │
│ │ groupId = "order-service" │ │
│ │ ) │ │
│ │ public void consume(OrderEvent event, │ │
│ │ Acknowledgment ack) { │ │
│ │ try { │ │
│ │ processOrder(event); │ │
│ │ ack.acknowledge(); // 수동 ACK │ │
│ │ } catch (Exception e) { │ │
│ │ throw e; // @RetryableTopic이 재시도 처리 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ @DltHandler // Dead Letter Topic 핸들러 │ │
│ │ public void handleDlt(OrderEvent event) { │ │
│ │ log.error("DLT로 이동된 메시지: {}", event); │ │
│ │ // 알림 발송, 수동 처리 큐에 저장 등 │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ Spring AMQP (RabbitMQ) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ // application.yml │ │
│ │ spring: │ │
│ │ rabbitmq: │ │
│ │ host: localhost │ │
│ │ port: 5672 │ │
│ │ listener: │ │
│ │ simple: │ │
│ │ acknowledge-mode: manual │ │
│ │ prefetch: 10 │ │
│ │ │ │
│ │ // 1. 설정 (Exchange + Queue + Binding) │ │
│ │ @Configuration │ │
│ │ public class RabbitConfig { │ │
│ │ │ │
│ │ @Bean │ │
│ │ TopicExchange orderExchange() { │ │
│ │ return new TopicExchange("order.exchange"); │ │
│ │ } │ │
│ │ │ │
│ │ @Bean │ │
│ │ Queue orderQueue() { │ │
│ │ return QueueBuilder.durable("order.queue") │ │
│ │ .withArgument("x-dead-letter-exchange", │ │
│ │ "order.dlx") │ │
│ │ .withArgument("x-dead-letter-routing-key", │ │
│ │ "order.dead") │ │
│ │ .build(); │ │
│ │ } │ │
│ │ │ │
│ │ @Bean │ │
│ │ Binding orderBinding() { │ │
│ │ return BindingBuilder │ │
│ │ .bind(orderQueue()) │ │
│ │ .to(orderExchange()) │ │
│ │ .with("order.#"); // 패턴 매칭 │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 2. Producer │ │
│ │ @Service │ │
│ │ @RequiredArgsConstructor │ │
│ │ public class OrderRabbitProducer { │ │
│ │ private final RabbitTemplate rabbitTemplate; │ │
│ │ │ │
│ │ public void send(OrderEvent event) { │ │
│ │ rabbitTemplate.convertAndSend( │ │
│ │ "order.exchange", │ │
│ │ "order.payment.completed", // Routing Key │ │
│ │ event │ │
│ │ ); │ │
│ │ } │ │
│ │ } │ │
│ │ │ │
│ │ // 3. Consumer (Manual ACK) │ │
│ │ @Component │ │
│ │ public class OrderRabbitConsumer { │ │
│ │ │ │
│ │ @RabbitListener(queues = "order.queue") │ │
│ │ public void consume(OrderEvent event, │ │
│ │ Channel channel, │ │
│ │ @Header(AmqpHeaders │ │
│ │ .DELIVERY_TAG) long tag) { │ │
│ │ try { │ │
│ │ processOrder(event); │ │
│ │ channel.basicAck(tag, false); │ │
│ │ // ^^^^^ 개별 메시지 ACK │ │
│ │ } catch (Exception e) { │ │
│ │ channel.basicNack(tag, false, false); │ │
│ │ // ^^^^^ requeue=false │ │
│ │ // → DLX로 이동 │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ■ 4가지 통합 방식 비교 요약 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────┬──────────┬──────────┬─────────┐ │ │
│ │ │ │ 설정 │ 재시도 │ DLQ │ │ │
│ │ │ │ 복잡도 │ │ │ │ │
│ │ ├──────────────┼──────────┼──────────┼─────────┤ │ │
│ │ │Redis Pub/Sub │ ★☆☆ │ 수동구현 │ 수동구현│ │ │
│ │ │Redis Streams │ ★★☆ │ PEL 기반 │ 수동구현│ │ │
│ │ │Kafka │ ★★★ │ @Retry │ @DltHan │ │ │
│ │ │ │ │ ableTopic│ dler │ │ │
│ │ │RabbitMQ │ ★★☆ │ DLX 기반 │ DLX │ │ │
│ │ │ │ │ │ 네이티브│ │ │
│ │ └──────────────┴──────────┴──────────┴─────────┘ │ │
│ │ │ │
│ │ 선택 가이드: │ │
│ │ ├── 이미 Redis 사용 + 간단한 알림 → Redis Pub/Sub │ │
│ │ ├── 이미 Redis 사용 + 안정적 처리 → Redis Streams │ │
│ │ ├── 대용량 이벤트 파이프라인 → Spring Kafka │ │
│ │ └── 복잡한 라우팅 + 작업 큐 → Spring AMQP │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
Redis, In-Memory Database, 인메모리, Cache, 캐시, RDB, AOF, Persistence, 영속성, Pub/Sub, Publish/Subscribe, Message Broker, Message Queue, Event Bus, Sentinel, Cluster, Session, 세션, TTL, Eviction, Memcached, Sorted Set, Rate Limiting, 분산 락, Distributed Lock, Redlock, Cache-Aside, Write-Through, Write-Behind, Salvatore Sanfilippo, antirez, Valkey, SSPL, Spring Data Redis, @Cacheable, @CacheEvict, HyperLogLog, Stream, Bitmap, Geospatial, Hash Slot, I/O Multiplexing, SCAN, Apache Kafka, RabbitMQ, AMQP, MQTT, Consumer Group, Dead Letter Queue, Backpressure, Fan-out, Exactly-once, At-least-once, NATS, Apache Pulsar, ZeroMQ, Redpanda, WarpStream, Redis Streams, XADD, XREADGROUP, XACK, Consumer Lag, Schema Registry, Idempotent Consumer, Circuit Breaker, Claim-Check Pattern, KRaft, Sharded Pub/Sub