분산 캐시와 Redis 자료구조 선택
타입 선택
| 타입 | 캐시 용도 | 대표 명령 |
|---|
| String | 직렬화한 객체, 카운터 | GET / SETEX / INCR |
| Hash | 객체의 일부 필드만 갱신 | HGET / HSET |
| List | 최근 N건 목록 | LPUSH / LRANGE / LTRIM |
| Set | 중복 없는 집합, 존재 검사 | SADD / SISMEMBER |
| Sorted Set | 점수 기반 랭킹, 시간순 정렬 | ZADD / ZRANGE |
큰 값 하나가 전체를 멈춘다
한 키에 아주 많은 것을 담으면 그 키를 다루는 명령이 오래 걸립니다. 그리고 처리가 한 줄로
이뤄지므로 그동안 다른 요청이 다 기다립니다.
| 위험한 것 | 왜 |
|---|
| 한 키에 수십만 개 항목 | 전체를 훑는 명령이 오래 걸린다 |
| 전체를 한 번에 지우기 | 지우는 동안 멈춘다 |
| 모든 키를 훑는 조회 | 운영에서는 쓰지 않는다 |
하나가 오래 걸리면 나머지가 다 늦어지는 구조라는 것이 핵심입니다. 그래서 큰 값은 쪼개고,
훑는 명령은 조금씩 나눠 도는 방식을 씁니다.
수명을 어디에 두나
키마다 수명을 두는 것이 기본입니다. 두지 않으면 메모리가 차고, 차면 정한 규칙에 따라 무언가
밀려납니다.
수명이 없는 키가 섞여 있으면 그것이 자리를 계속 차지한다
그러면 밀려나는 것은 수명이 있는 쪽뿐이다
그래서 수명 없는 키를 쓸 때는 그 이유를 적어 둡니다. 대개는 잊어서 안 둔 것입니다.
실무 포인트
- 객체 전체를 String으로 넣으면 필드 하나 갱신에도 전체 재직렬화가 필요하다. 부분 갱신이 잦으면 Hash
- 운영에서
KEYS는 금지. 전체 키를 훑어 서버를 멈춘다. SCAN으로 커서 순회한다
- 왕복이 많으면 파이프라인이나
MGET으로 묶는다. 네트워크 RTT가 병목인 경우가 흔하다
- 큰 값이나 원소가 수십만 개인 컬렉션은 단일 명령이 오래 걸린다. 키를 쪼갠다
- Redis는 명령을 사실상 단일 스레드로 처리한다. 무거운 명령 하나가 전체 지연을 만든다
Q.랭킹 기능을 Redis로 구현한다면 어떤 자료구조를 쓰나요?
정렬된 집합(Sorted Set)을 씁니다. 점수로 정렬된 상태를 유지하면서 순위 조회가 로그 시간입니다.
| 연산 | 명령 | 복잡도 |
|---|
| 점수 갱신 | ZADD | O(log n) |
| 상위 N명 | ZREVRANGE | O(log n + N) |
| 특정 사용자 순위 | ZREVRANK | O(log n) |
| 점수 구간 조회 | ZRANGEBYSCORE | O(log n + N) |
다른 자료구조로는 이 조합이 안 됩니다. 리스트는 정렬을 유지하지 못하고, 해시는 순위를 모릅니다. DB 로 하면 매번 정렬이나 인덱스 스캔이 필요합니다.
주의할 점입니다.
| 항목 | 내용 |
|---|
| 동점 처리 | 점수가 같으면 사전순이라, 먼저 달성한 사람을 앞에 두려면 점수에 시간을 섞는다 |
| 기간별 랭킹 | 일간과 주간을 각각 다른 키로 두고 만료를 건다 |
| 규모 | 수천만 명이면 메모리를 계산해 보고 샤딩을 검토한다 |
흔한 실수: 랭킹 전체를 매번 가져오는 것. 상위 100명만 필요하면 그만큼만 요청해야 합니다.
Q.객체 캐싱에 String과 Hash 중 무엇을 선택하나요?
전체를 한 번에 읽고 쓰면 String, 필드별로 접근하면 Hash 입니다.
| 항목 | String (직렬화) | Hash |
|---|
| 부분 조회 | 안 된다. 전체를 읽는다 | 필드만 읽는다 |
| 부분 갱신 | 전체를 다시 쓴다 | 필드만 쓴다 |
| 만료 | 키 단위 | 키 단위. 필드별로는 불가 |
| 메모리 | 작은 객체는 Hash 가 더 효율적 | 작은 해시는 압축 저장된다 |
| 직렬화 비용 | 매번 전체 직렬화 | 필드 단위라 적다 |
기준은 접근 패턴입니다. 프로필 전체를 화면에 뿌린다면 String 이 단순하고, 조회수만 자주 올린다면 Hash 로 그 필드만 증가시키는 것이 훨씬 싸습니다.
흔한 실수: 필드별 만료가 필요한데 Hash 를 고르는 것. Hash 는 필드 단위 만료가 없어서, 필요하면 필드를 별도 키로 나눠야 합니다.
Q.운영 중 KEYS 명령을 쓰면 안 되는 이유는?
Redis 는 단일 스레드로 명령을 처리하는데, KEYS 는 전체 키를 훑는 O(n) 명령이라 그 시간 동안 다른 요청이 전부 멈춥니다.
키가 100만 개면 수백 밀리초에서 수 초
그 사이 모든 클라이언트가 대기한다
대안입니다.
| 목적 | 대안 |
|---|
| 패턴으로 키 찾기 | SCAN. 커서로 조금씩 나눠 훑는다 |
| 특정 묶음 관리 | 별도 집합에 키 목록을 관리한다 |
| 통계 | INFO 나 DBSIZE 로 개수만 본다 |
같은 이유로 위험한 명령이 더 있습니다. FLUSHALL, 큰 컬렉션에 대한 SMEMBERS 나 LRANGE 전체 조회, 큰 키의 DEL 도 오래 걸립니다.
흔한 실수: 개발 환경에서 문제없어 운영에서도 쓰는 것. 키가 적으면 빠르므로 규모가 커진 뒤에야 드러납니다. 운영에서는 위험 명령을 아예 비활성화하는 설정을 걸어두는 편이 안전합니다.
Q.Redis가 갑자기 전체적으로 느려졌다면 무엇을 확인하나요?
단일 스레드라 한 요청이 오래 걸리면 전부 밀립니다. 그 원인부터 찾습니다.
| 확인 | 방법 |
|---|
| 느린 명령 | SLOWLOG 로 오래 걸린 명령을 본다 |
| O(n) 명령 | KEYS, 큰 컬렉션 전체 조회, 큰 키 삭제 |
| 메모리 상태 | 상한 도달, 축출 급증, 단편화 |
| 스냅샷 저장 | 저장 중 fork 와 쓰기 시 복사로 지연이 생긴다 |
| 네트워크 | 대역폭 포화, 큰 값 전송 |
| 만료 폭주 | 대량 키가 동시에 만료되며 정리 부하가 든다 |
스냅샷이 자주 간과됩니다. 저장 시점에 프로세스를 복제하는데, 그 뒤 쓰기가 많으면 복사가 대량으로 일어나 메모리와 지연이 함께 나빠집니다.
흔한 실수: 서버 CPU 사용률만 보는 것. 단일 스레드라 코어 하나가 100%면 그것이 상한입니다. 전체 CPU 가 12%로 보여도 이미 포화일 수 있습니다.
먼저 스스로 답해보고 아래 답변과 견줘보세요. 막히는 부분은 문제로 확인할 수 있어요.
읽었으면 문제로 확인해보세요
캐시/Redis 문제를 풀면 틀린 문제가 자동으로 노트에 쌓입니다. 가입 없이 5문제를 먼저 풀어볼 수도 있어요.