Redis - 인메모리 데이터 저장소의 모든 것
TL;DR
- Redis는 메모리 기반 자료구조 저장소로 매우 빠른 응답을 제공한다.
- RDB/AOF로 영속성을 선택적으로 제공해 캐시와 DB 역할을 겸한다.
- Pub/Sub, Sentinel, Cluster 등으로 확장성과 고가용성을 지원한다.
1. 개념
Redis는 인메모리 자료구조 저장소로 캐시, 데이터베이스, 메시지 브로커 역할을 수행한다.
2. 배경
디스크 I/O 병목과 Memcached의 기능 한계가 드러나면서 더 빠르고 풍부한 자료구조를 제공하는 저장소가 필요했다.
3. 이유
고속 접근과 데이터 구조 활용, 그리고 선택적 영속성을 통해 서비스 성능과 안정성을 동시에 확보하기 위해 사용된다.
4. 특징
다양한 자료구조, RDB/AOF 영속성, Pub/Sub·Stream, Sentinel/Cluster 기반 확장성이 핵심이다.
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에. │ │
│ │ 둘을 같이 쓰면 빠르면서도 안전하다!" │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
관련 키워드
Redis, In-Memory Database, 인메모리, Cache, 캐시, RDB, AOF, Persistence, 영속성, Pub/Sub, 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