Redis의 용량을 고려하라.

정웅·2026년 5월 4일

데이터베이스

목록 보기
5/5
post-thumbnail

면접에서 놓친 Redis 용량 문제 — 캐시는 만능이 아니다

들어가며

최근 백엔드 면접에서 한 면접관님이 던진 질문이 머리에 오래 남았다.

"1,000만 건의 데이터가 전부 Redis에 올라가면 문제가 생기지 않을까요?"

처음엔 "TTL을 짧게 걸면 되지 않나?" 정도로 답했지만, 면접관님의 표정에서 이미 부족한 답변이라는 걸 알 수 있었다. 면접이 끝나고 곰곰이 생각해보니, 나는 캐시를 '도구'로만 알고 있었지 '비용'까지는 보지 못하고 있었다.

이 글은 그 면접에서 받은 피드백을 계기로 Redis 용량 문제를 처음부터 정리한 회고이자 학습 노트다.


1. 1차 답변에서 빠져 있던 것

내가 처음 떠올린 답은 이런 흐름이었다.

  • TTL을 60초로 설정했으니 오래된 데이터는 사라진다
  • 따라서 메모리는 무한정 쌓이지 않는다

하지만 면접관님이 의도한 본질은 다른 곳에 있었다.

"Redis는 In-Memory DB라는 사실을, 정말로 인지하고 있는가?"

In-Memory라는 단어를 알고만 있는 것과, 그것이 만들어내는 비용/한계/위험까지 이해하는 것은 다르다.


2. 1,000만 건이 모두 Redis에 올라간다면?

문제점 1 — 메모리 비용

Redis는 모든 데이터를 RAM에 올린다. 한 건당 500바이트만 잡아도:

  • 원본: 5GB
  • Redis 오버헤드(키 메타데이터, 포인터, 자료구조) 포함: 10~15GB

RAM은 디스크보다 수십 배 비싸다. 즉, 10GB의 캐시는 단순히 "메모리를 좀 쓴다" 수준이 아니라 운영 비용 그 자체이다.

문제점 2 — Maxmemory 도달 시 위험

Redis는 메모리 한계에 도달하면 maxmemory-policy에 따라 동작한다.

정책동작
noeviction (기본값)쓰기 명령 거부 → 서비스 장애
allkeys-lru모든 키 중 가장 오래 안 쓴 것 제거
volatile-lruTTL 있는 키 중 LRU 제거
allkeys-lfu모든 키 중 가장 적게 쓴 것 제거

정책 설정을 안 하면 기본값이 noeviction이라, 캐시가 그대로 장애 트리거가 된다. 이걸 모르면 운영 환경에서 캐시가 가득 찰 때 갑자기 쓰기가 거부되는 사고가 난다.

문제점 3 — Single-Threaded 영향

Redis는 빠르기 위해 단일 스레드 구조를 택했다. 이 선택이 빛을 발하려면 데이터셋이 적당히 작아야 한다.

데이터셋이 커질수록 발생하는 두 가지 부담:

(1) 백그라운드 작업의 fork 비용

RDB(Redis Database) 스냅샷이나 AOF(Append Only File) rewrite는 자식 프로세스를 fork해서 디스크에 쓴다. 그런데 fork 자체가 메모리 크기에 비례해 페이지 테이블 복사 시간이 든다. 이 시간 동안 단일 스레드라 모든 요청이 블로킹된다.

<RDB 스냅샷 만들기>

[목표]
"메모리에 있는 50GB 데이터를 디스크에 dump.rdb 파일로 저장"

[딜레마]
Redis는 단일 스레드.
저장 작업을 메인 스레드가 직접 하면?
→ 50GB 디스크 쓰는 동안 모든 요청 블로킹
→ 수십 초간 서비스 마비 ⚠️

[해결]
fork()로 자식 프로세스 만들기
→ 자식이 디스크 쓰기 담당
→ 부모(메인 Redis)는 평소처럼 요청 처리
<AOF Rewrite>

[Rewrite 작업]
1. 부모가 fork() 호출
2. 자식이 현재 메모리 상태를 보고
   "이 상태를 만드는 최소한의 명령어 세트"를 새 AOF 파일에 작성
3. 자식 종료, 새 파일이 기존 AOF 대체

→ 이것도 메인 스레드 블로킹 안 됨 (자식 담당)
fork가 느려지는 이유

메모리 1GB:
- 페이지 약 26만 개
- 페이지 테이블 작음
- fork 시간: 수 ms

메모리 50GB:
- 페이지 약 1,300만 개
- 페이지 테이블 큼
- fork 시간: 수백 ms

메모리 200GB:
- 페이지 약 5,200만 개
- 페이지 테이블 매우 큼
- fork 시간: 수 초 ⚠️
  • 1GB → fork 수십 ms
  • 50GB → fork 수백 ms
  • 200GB → 수 초

fork() 진행 중에는 단일 스레드인 부모도 블로킹되므로, 데이터셋이 클수록 그 시간만큼 모든 요청이 정지.
P99 지연이 튀는 가장 흔한 원인 중 하나다.

(2) KEYS * 한 번이면 전체 블로킹

KEYS * 같은 명령은 모든 키를 순회한다. 단일 스레드라 그 시간 동안 모든 요청이 멈춘다. 키가 1천만 개면 수 초간 서비스가 정지할 수 있다.

운영 환경에선 이런 명령을 아예 비활성화해야 한다.

# redis.conf
rename-command KEYS ""
rename-command FLUSHALL ""

키 패턴 조회가 필요하면 커서 기반의 SCAN을 써야 한다.

문제점 4 — 캐시 본질에 어긋남

가장 본질적인 문제는 이거다.

캐시는 전체 데이터의 복제본이 아니라, 핫 데이터의 단축 경로다.

1,000만 건 전체를 캐싱한다는 건 사실상 Redis를 메인 DB처럼 쓰는 것이다. 그러면 MySQL을 굳이 둘 이유가 없다. 보통 80/20 법칙에 따라 20%의 데이터가 80%의 트래픽을 처리하기 때문에, 그 20%만 캐싱하는 게 효율적이다.


3. 데이터로 정량 분석

CreatorLink의 통계 캐시 한 건이 이런 JSON이라고 가정하면,

{
  "creatorId": 12345678,
  "creatorName": "일류 크리에이터",
  "channelId": 98765432,
  "channelDisplayName": "유튜브_공식채널_메인",
  "todayClicks": 1250,
  "rangeClicks": 45000,
  "totalClicks": 1200000
}

한 건당 크기 추정:

  1. 순수 텍스트 크기: 한글 한 글자 3바이트 + 따옴표/괄호 → 약 200~250 byte
  2. Redis 오버헤드: 키 이름 + 관리용 메타데이터 → 약 80 byte
  3. 합계: 약 300~330 byte

1,000만 건 저장 시:

  • 약 3.07GB (이상적인 경우)
  • 이름이 더 길거나 메모리 파편화 발생 시 5GB+

이 수치를 보고 나서야 "캐시가 무겁다"는 말이 추상적이지 않게 다가왔다.


4. 캐시가 폭발하는 시나리오 — TTL만으로는 못 막는다

여기서 한 단계 더 깊은 의문이 생겼다.

트래픽이 균등 분포라면, 60초 안에 1,000만 건이 모두 캐시에 들어올 수도 있는 것 아닌가?

이 시나리오는 이론적으로 완전히 가능하다. 그리고 이게 바로 TTL의 한계다.

TTL = 60초
60초 안에 천만 개의 서로 다른 키가 조회됨
→ 어느 것도 만료 전이라 모두 캐시에 누적
→ 메모리 폭발

TTL은 시간 기반 제거일 뿐, 메모리 한계 보호 기능이 아니다. 이를 보완하는 게 다음 조합이다.

maxmemory 4gb
maxmemory-policy allkeys-lru

이렇게 설정하면 새 키가 들어와 메모리 한계에 도달할 때 가장 오래 사용 안 된 키가 자동으로 제거된다. 천만 건이 들어와도 메모리는 4GB를 절대 넘지 않는다.


5. 그런데 균등 분포면 캐시가 의미 있나?

예리한 후속 질문이 따라온다. 균등 분포 트래픽에서는 캐시 히트율 자체가 낮아진다.

80/20 분포: 핵심 20%만 캐싱해도 80% 히트율 ✅
균등 분포: 캐시 크기 / 전체 크기 만큼만 히트 (예: 30%) ⚠️

캐시는 본질적으로 트래픽 편향을 활용하는 도구다. 분포가 균등하면 캐시 자체가 효과가 없다.

이런 데이터는 캐시 대신 다른 카드를 검토해야 한다.

  • 인덱스 튜닝
  • 파티셔닝
  • Read Replica
  • DB Buffer Pool 확대 (MySQL 자체의 버퍼풀)
  • 캐시 단위 재정의 (예: 집계 결과만 캐싱해서 인위적으로 분포 편향 만들기)

6. 정리 — 캐시 설계의 실제 사고 흐름

이번 학습으로 정리된 내 생각은 이렇다.

캐시 도입 시 검토 순서

1. 트래픽 분포가 편향적인가? (편향이 없으면 캐시 효과 낮음)
2. 어떤 데이터를 캐싱할지 선별 (Hot Data만)
3. TTL + maxmemory + Eviction Policy 조합 설정
4. 캐싱 데이터의 직렬화/구조 최적화(캐시에 데이터를 저장할 떄 어떤 형식(포맷)으로 변환해서 저장할 것인가)
5. 단일 스레드 한계를 의식한 운영 (위험 명령 차단, 모니터링)

한 줄로 압축하면

"캐시는 전체 데이터의 복제본이 아니라, 핫 데이터의 단축 경로다."
"TTL은 시간을 막을 뿐, 메모리는 maxmemory가 막는다."


마치며

면접에서 답변이 부족했던 게 결과적으로 좋은 학습 계기가 됐다. "캐시 적용했어요"가 아니라 "왜 그 패턴을, 왜 그 구현으로, 왜 그 트레이드오프를 선택했는가" 를 답할 수 있어야 한다는 걸 배웠다.

다음 면접에서 같은 질문을 받으면 이렇게 답할 것이다.

"Redis는 In-Memory DB이기 때문에 모든 데이터를 RAM에 올리는 비용이 큽니다. 1,000만 건 캐싱은 메모리 폭발 위험과 fork 시 블로킹, 캐시 본질 훼손까지 여러 문제를 만듭니다. 그래서 TTL만이 아니라 maxmemory와 Eviction Policy를 함께 설정하고, 핫 데이터만 선택적으로 캐싱하는 게 캐시 설계의 출발점이라고 생각합니다."

profile
즐거운 개발 :) 신입 서버 개발자입니다

0개의 댓글