요약
대규모 트래픽 대응을 위한 Redis 캐시 전략 실전 적용기
본 포스팅은 튜닝 레포트 시스템에서 반복적으로 발생하는 피크 트래픽을 효과적으로 처리하기 위해, Redis 기반 캐시 구조를 도입하고 성능 개선을 검증한 전체 과정을 상세히 기록한 기술 보고서다.
특히 다음과 같은 내용을 중심으로 구성되어 있다.
- 월/목 오후 12:30 정기 발행 → 150명 동시 접속 시나리오 기반 부하 테스트
- 캐시 미적용 시 평균 2.46초, 최대 8.53초 응답 지연 발생
- Cache Aside + Write Back 전략 도입을 통한 응답 시간 및 자원 사용량 최적화
- Redisson 락, TTL Jitter, 캐시 워밍업 등 실전 환경에 맞춘 캐시 안정화 기법 적용
- 적용 후 DB 부하 98% 감소, 응답 속도 53ms로 46배 개선, 캐시 적중률 87% 달성
단순 캐싱을 넘어 정합성과 확장성, 멀티 인스턴스 환경에서의 동시성 문제까지 고려한 고급 전략을 다루며, 실제 운영에 Redis 캐시를 어떻게 효과적으로 녹여낼 수 있는지를 체계적으로 기록되어 있는 글이다.
배경
튜닝 레포트 시스템은 사용자 접속 패턴과 게시글 발행 시점을 고려할 때, 일시적인 트래픽 집중과 높은 데이터 변경 요청이 반복적으로 발생하는 구조.
튜닝 레포트 발행 패턴
- 튜닝 레포트는 매주 월요일, 목요일 오후 12:30 에 집중적으로 발행.
- 이 시간대에 최대 150명의 사용자가 동시 접속하여 튜닝 레포트 목록을 확인하고, 각 레포트에 다양한 반응을 남긴다.
- 사용자 행위는 대부분 다음과 같다.
- 가장 최근에 발행된 1~3개의 레포트에 집중
- 각 레포트에 대해 최대 5가지 반응(예: 👍, 🎉, 💖 등)을 남기거나 취소함
예상되는 서버 부하
- 사용자당 여러 개의 GET/PUT 요청이 발생하므로, 단일 시간대에 수천 건의 DB 접근이 발생
- 특히 반응 수 등록/삭제는 레포트 JSON 데이터를 읽기 → 수정 → 다시 저장하는 구조로, 동시성 문제가 발생할 수 있음
- 따라서 Redis 기반 캐시 전략을 도입하여 부하를 분산하고 응답 속도를 확보하고자 함
부하 테스트 시나리오
테스트 환경: CPU 4코어에 RAM 6GB
목적
- 실제 서비스 운영 시, 새 레포트 발행 직후의 피크 타임 부하 상황을 시뮬레이션
- 튜닝 레포트 조회 + 리액션 등록/해제를 통해 캐시 처리 성능 및 시스템 부하 한계 확인
시나리오 개요 (1명의 가상 사용자 VU 기준)
1. 고정된 10개의 reportId에 대해:
- 각 레포트에 5개 반응 유형 모두 등록 (PUT 요청)
- 이어서 같은 반응을 모두 해제 (PUT 요청)
2. 각 요청 사이에 짧은 sleep 삽입
총: 50개 반응 등록 + 50개 반응 해제 = 100개의 PUT 요청
캐시 전 부하 테스트 결과
1. 응답 속도
| 지표 | 수치 |
| 평균 응답 시간 | 2.46초 |
| 90% 응답 시간 | 3.37초 |
| 최대 응답 시간 | 8.53초 |
2. 시스템 리소스 분석
Application CPU 사용률

- PM2 (Node.js 서버) 및 mysqld(MySQL) 모두 부하 시점에서 높은 CPU 점유율 기록
- 캐시 적용 후 mysqld CPU 점유율이 감소함 → DB 쿼리 수가 줄었음을 간접적으로 입증
- 반면 PM2는 약간 증가 가능 → 캐시 로직 처리 부담이 증가했기 때문이며 정상적인 현상

CPU PSI (Pressure Stall Information)
- 부하 시점(예: 11:35:30~11:36:45) 동안 some60 지표가 약 30%까지 상승
- → CPU 스케줄링 지연 발생, 즉시 처리 가능한 프로세스가 처리되지 못하고 대기 중이라는 의미
Memory Pressure 및 Stall Time
- memory.some_pressure 및 stall_time 모두 유의하게 증가
- → 메모리 부족, 워킹셋 스와핑, 페이지 아웃 등이 발생하며 처리 지연 초래
전체 CPU 사용량

- 전체 시스템 CPU 사용량이 거의 100%에 도달
- user, system 모두 증가 → 애플리케이션(WAS)과 DB 모두 병목 지점 존재 가능성
3. DB 쿼리 수 분석
👉 1명의 사용자 기준
| API 유형 | 호출 횟수 | 쿼리 수 | 총 쿼리 |
| GET /reports | 1회 | 2 | 2 |
| PUT /reaction 등록 | 50회 | 4 | 200 |
| PUT /reaction 해제 | 50회 | 4 | 200 |
→ 합계: 402 쿼리 / 1명
전체 150명 기준
- 총 60,300 쿼리 발생
- 대부분의 부하가 반응 수 등록/삭제에 집중됨
Redis를 통한 캐시 전략
Cache Aside(읽기) + Write Back(쓰기) 선택 이유
| 항목 | Cache Aside | Write Back |
| 목적 | 캐시 초기화, TTL 만료 대응 | DB 부하 완화, 빠른 응답 |
| 장점 | 데이터 정합성, 미사용 데이터 미캐싱 | 사용자 경험 개선, 집계 최적화 |
실시간 조회에는 신선도가 중요하고, 사용자 반응 저장에는 쓰기 성능과 유연성이 중요하다.
이에 따라 읽기는 Cache Aside, 쓰기는 Write Back 전략을 채택하여 두 가지 특성을 모두 충족하도록 설계 진행.
1. 파레토 법칙 기반 캐시 구조 설계
문제 정의
- 모든 튜닝 레포트를 Redis에 캐시하는 것은 메모리 낭비와 관리 부담 초래
- 실제 사용자는 주로 발행 직후 1~3개의 최신 레포트만 확인
- 발행 주기: 매주 월/목 12:30 → 특정 시점에 트래픽 집중
- 파레토 법칙(80:20) 적용: 대부분의 접근은 소수의 최신 레포트에 집중됨
해결 전략
- Redis에 최근 10개 레포트만 캐시
- 키 구조
"reports:domain=%s:page=0:size=10:sort=LATEST:list"
| 구성 | 설명 |
| domain=%s | 기업/도메인 식별자 |
| page=0:size=10 | 첫 페이지, 10개 고정 |
| sort=LATEST | 최신순 정렬 |
2. Redis 캐시 구조 선택
Hash 기반 구조
Key: reports:page=... (Hash)
Field: "123" → JSON
Field: "124" → JSON
- 장점: 개별 레포트 수정, 삭제 용이
- 단점: 레포트 리스트에서 중요한 순서를 보장 불가함으로, 리스트 재구성 필요하여 복잡도 상승
List + Value 구조 (선택)
Key: reports:domain=...:...:list → Redis List [123, 124, ...]
Key: report:item:{id} → Redis String "{JSON}"
- 장점
- 순서 보존 가능
- 개별 수정/삭제 용이
- 각 항목별 TTL 관리 가능
3. 사용자 도메인 기반 키 구성 전략
문제
- 캐시 키(reports:domain=...) 생성을 위해 사용자 domain 정보를 DB에서 조회 → 캐시 의미 약화
해결책: 1차 캐시로 도메인 정보 저장
Key: user:{userId}:domain → "kakaotech.com" (TTL 부여)
- 캐시 미스 시 DB 조회 후 Redis에 저장
- 이후 도메인 기반 캐시 키 생성 시 Redis에서 즉시 사용
4. Cache Stampede 방지 전략
문제 정의
- 동일 TTL로 인해 캐시가 동시에 만료
- 인기 도메인에서 동시 다발적 DB 접근 → Cache Stampede
전략 1: TTL Jitter
Duration getTTLDur() = base + random(0~180)
- 도메인별로 서로 다른 TTL 부여
- TTL 만료 시점 분산 → 스탬피드 완화
전략 2: Redisson Mutex Lock
- Redis Lock 사용하여 오직 1개 요청만 DB 접근
- 나머지 요청은 캐시 완료 후 조회
- 스탬피드 완전 방지 + DB 부하 감소
5. 캐시 워밍업 전략
배경
- 특정 시점(월/목 12:30)에 사용자 대량 유입 예상
- 이 시점 직전에 캐시가 만료되면 스탬피드 가능성 ↑
해결 방안: 정기 워밍업 스케줄러
- 매주 지정 시간에 Redis 캐시를 미리 적재
- Redisson 락으로 단일 인스턴스만 수행 보장
RLock lock = redissonClient.getLock("lock:warmup:{domain}");
- 중복 실행 방지, 무중단 운영 가능
6. 리액션 캐시의 동시성 제어
문제: 반응 처리 과정에서 JSON 수정 중 Race Condition 발생 가능
대안 비교
| 전략 | 원자성 | 분산 적합 | 구조 변경 |
| HINCRBY | ✅ | ✅ | ❌ 필요 |
| Lua Script | ✅ | ✅ | ❌ 복잡 |
| WATCH | ⭕ | ❌ | ⚠ 재시도 필요 |
| SessionCallback | ✅ | ❌ | ❌ |
| Redisson Lock | ✅ | ✅ | ✅ 구조 유지 |
최종 선택: Redisson 락 기반 제어
- 직관적이고 Spring Boot + Redisson에서 쉽게 구현
- ReportItem JSON 구조 변경 없이 사용 가능
- 멀티 인스턴스 환경에서도 안정성 확보
캐시 후 부하 테스트 결과
1. 응답 속도
| 지표 | 캐싱 후 | 캐싱 전 |
| 최대 응답 시간 | 1.95초 | 8.53초 |
| 90% 응답 시간 | 29.09ms | 3.37초 |
| 평균 응답 시간 | 53.29ms | 2.46초 |
2. 시스템 리소스 분석
Application별 CPU 사용량

| 프로세스 | 캐시 전 | 캐시 후 | 개선 내용 |
| mysqld | 약 80% | 1% | 🔻 DB CPU 부하 98.7% 감소 |
| redis-server | 0% | 31.4% | ⬆ Redis 캐시 부하로 자연스러운 증가 |
| PM2 (Node) | 180% | 296% | ⬆ 캐시 로직 추가로 증가, 성능 저하 없음 |
→ DB의 병목이 Redis로 옮겨가며 분산 처리된 구조로 해석 가능
→ PM2의 CPU는 올라갔지만, 이는 캐시 로직 처리 오버헤드로 정상이므로 문제 없음
Total CPU Pressure (PSI)

| 지표 | 캐시 전 | 캐시 후 | 변화 |
| cpu.some60 | 30% | 16.38% | 🔻 45% 감소 |
| cpu.stall_time | 약 800ms | 60.8ms | 🔻 92% 감소 |
→ 스레드가 CPU를 기다리는 시간 및 병목이 대폭 개선됨
→ 전반적인 시스템 응답성이 향상되었음을 의미
시각화 요약 (위 그래프 기준)
- mysqld CPU: 80% → 1%
- CPU Stall: 800ms → 60ms
- CPU PSI some60: 30% → 16%
Redis 를 통한 캐시 적용 이후, 메모리 상태

Available RAM
- 평균 1.05 ~ 1.11 GiB 사이에서 안정적으로 유지
- 순간적인 하락 구간은 있으나 전반적으로 플랫하며, 메모리 부족 징후 없음
- 그래프 우측 마지막엔 오히려 소폭 증가 → GC 종료 or Redis eviction 이후로 추정
해석
- 전체 워킹셋이 메모리 내에 안정적으로 수용되고 있음
- Redis가 캐싱에 사용한 메모리 외에도 여유 공간 충분함
- OS가 메모리 pressure를 느끼지 않고 있다는 의미
System RAM 상세
| 항목 | 수치 | 의미 |
| free | 0.172 GiB (≈ 176MB) | 실제 아무것도 안 쓰고 비어있는 공간 |
| used | 4.43 GiB | 현재 사용 중인 공간 |
| cached | 1.13 GiB | 페이지 캐시 (Redis 포함 가능성 큼) |
| buffers | 0.05 GiB | 디스크 I/O 버퍼 |
특징
- 거의 모든 RAM이 캐시 or 사용 상태로 채워져 있음
- 하지만 pressure가 없다는 점에서 정상이며, 이는 리눅스 메모리 관리 방식상 매우 건강한 상태
리눅스는 여유 RAM이 있으면 가능한 한 캐시로 사용 → 사용률 90% 넘는 건 정상
부테 동안 Redis 캐시 적중률(hit ratio)
| 항목 | 값 | 설명 |
| keyspace_hits | 126,060 | 캐시에 존재하는 키 조회 성공 횟수 |
| keyspace_misses | 18,599 | 캐시에 존재하지 않아 miss된 횟수 |
결과 해석
| 항목 | 값 |
| 총 조회 시도 | 144,659회 |
| 캐시 적중률 | 87.13% |
| 캐시 미스율 | 12.87% |
의미
- 전체 Redis 조회 중 약 87%는 캐시에서 성공적으로 데이터를 반환
- DB 조회를 10번 중 8.7번은 피하고 있음
- 나머지 13%는 캐시 미스
캐시 미스율의 이유와 실행된 DB 총 쿼리 수
각 사용자는 테스트 시점에 총 2개의 DB 쿼리를 반드시 발생시킨다.
- 첫 번째 쿼리는 게시글 목록 캐시 키("reports:domain=%s:page=0:size=10:sort=%s:list")를 생성하기 위해, 해당 사용자의 도메인 정보를 DB에서 조회한 뒤, "user:{userId}:domain" 형식의 키로 Redis에 저장한다. (이는 게시글 키 조립 시 필요한 값)
- 두 번째 쿼리는 게시글 목록 내 각 게시글에 대해 사용자가 과거에 어떤 반응을 눌렀는지를 확인하기 위한 것으로, 사용자의 리액션 정보를 DB에서 조회한 후 "reports:{reportId}:user:{userId}" 형식의 Redis 해시로 캐싱한다.
따라서, 사용자 1인당 2개의 DB 조회 쿼리가 발생하며, 총 150명의 사용자에 대해 불가피한 DB 쿼리는 총 300건.
'카카오테크 부트캠프' 카테고리의 다른 글
| Kafka 기반 SSE 알림 시스템 아키텍처 설계기 (3) | 2025.07.25 |
|---|---|
| 동적인 데이터 캐시 동시성 제어 전략 비교 및 분산 락 선택 (0) | 2025.07.15 |
| Redis 캐시, Cache Stampede 방지 전략 보고서 (TTL Jitter, Mutex Locking) (0) | 2025.07.13 |
| Redis 캐시 키 설계 관련 보고서: 사용자 domain 정보 처리 전략 (1) | 2025.07.11 |
| SSE 도입 후 궁금증 완전 해소 – Polling 한계 극복부터 Nginx 설정, Tomcat 병목까지 총정리 (1) | 2025.06.29 |