메모리에 데이터를 두는 키-값 저장소. 다만 "빠른 맵" 이라고만 보면 절반만 이해한 것이다. Redis 는 값을 통째로 넣고 빼는 것이 아니라 자료구조 자체를 서버에 두는 물건이다.
| 구조 | 쓰임 |
|---|---|
| String | 단순 캐시, 카운터 |
| Hash | 객체를 필드별로 — 한 필드만 고쳐도 전체를 다시 안 보낸다 |
| List | 큐 |
| Set | 중복 없는 집합, 교집합 연산 |
| Sorted Set | 점수 순 정렬 — 랭킹·리더보드 |
| Stream | 추가 전용 로그 |
객체를 JSON 문자열로 통째 저장하면 필드 하나 고치는 데도 전체를 읽고 고쳐 다시 쓴다. Hash 로 두면 그 필드만 건드린다. 자료구조를 고르는 일이 곧 네트워크 왕복량을 고르는 일이다.
단일 스레드인데 왜 빠른가
명령을 한 번에 하나씩 처리한다. 느릴 것 같지만 셋이 맞물려 빠르다.
- 디스크 I/O 가 없다 — 전부 메모리에 있다. 디스크 접근은 밀리초, 메모리 접근은 나노초 단위다. 자릿수가 여섯 개 차이 난다
- 경합 비용이 0 이다 — 스레드가 하나뿐이니 락도, 락을 기다리다 생기는 컨텍스트 스위칭도 없다. 동시성을 포기한 대가로 얻은 것이다
- 연결은 I/O 멀티플렉싱으로 받는다 —
epoll·kqueue로 한 스레드가 수만 연결을 논블로킹하게 다룬다. "데이터가 도착한 연결" 만 커널이 알려 주므로, 연결 수가 스레드 수를 강요하지 않는다
즉 병목이 CPU 가 아니라 메모리 대역폭과 네트워크다. 한 스레드로도 초당 수십만 명령이 나온다.
그 대가가 원자성을 준다
한 번에 하나씩 처리하니 개별 명령이 그 자체로 원자적이다. INCR 로 값을 읽고 더하고 쓰는 사이에 다른 명령이 끼어들 수 없다. 애플리케이션이라면 락을 걸어야 할 일을 명령 하나로 끝낸다. 분산 락(SET key val NX PX 3000)·카운터·레이트 리밋을 별도 동기화 없이 세울 수 있는 근거가 이것이다.
뒤집으면 느린 명령 하나가 전부를 막는다. KEYS * 로 전체 키를 훑거나 원소가 수백만인 컬렉션을 통째로 지우면 그 시간 동안 모든 요청이 줄을 선다. 운영에서 KEYS 대신 커서로 조금씩 훑는 SCAN 을 쓰라는 것이 이 때문이다.
왕복을 줄이는 것이 곧 성능이다
명령 하나하나가 나노초에 끝나는데 네트워크 왕복은 수백 마이크로초다. 명령 100개를 따로 보내면 처리 시간이 아니라 왕복 시간이 전부를 차지한다.
- 파이프라이닝 — 응답을 기다리지 않고 명령을 몰아 보낸 뒤 응답을 한꺼번에 받는다
MGET·HMGET— 여러 키를 한 명령으로 가져온다- Lua 스크립트 — 여러 명령을 서버에서 원자적으로 실행한다. 읽고 판단해서 쓰는 로직을 왕복 한 번으로 끝낸다
"Redis 가 느리다" 는 진단은 대개 Redis 가 아니라 왕복 횟수의 문제다.
만료는 어떻게 처리되나
TTL 이 지났다고 그 순간 지워지는 것이 아니다. 그러면 만료 시각마다 타이머가 필요하다. Redis 는 둘을 섞어 쓴다.
- 접근할 때(lazy) — 조회 시점에 만료됐으면 그때 지우고 없다고 답한다
- 주기적으로(active) — 틈틈이 TTL 있는 키를 표본 추출해 만료된 것을 지운다
그래서 만료된 키가 잠시 메모리를 차지하고 있을 수 있다. 메모리 사용량이 예상보다 큰 이유가 여기일 때가 있다.
면접 함정
- ❌ "단일 스레드라 멀티코어를 못 쓴다" → 맞지만 그래서 느린 것은 아니다. 코어를 더 쓰려면 인스턴스를 여러 개 띄운다. 최근 버전은 네트워크 I/O 만 별도 스레드로 나눌 수 있으나 명령 실행은 여전히 하나다.
- ❌ "Redis 는 캐시다" → 캐시로 많이 쓸 뿐, 자료구조 서버이자 (영속성을 켜면) 저장소다.
- ❌ "메모리에 있으니 무조건 빠르다" → 왕복 횟수와 느린 명령이 실제 병목이다.