RoomPick 인기 숙소 Redis 랭킹 / 캐시

임선구·2026년 8월 28일

스파르타

목록 보기
23/28

🚀 Redis 캐시를 붙였더니 처리량이 4.57배 증가했다 — 인기 숙소 랭킹 개선

RoomPick에는 사용자에게 인기 숙소를 보여주는 기능이 있다.

초기 구현은 단순 누적 조회수를 기준으로 인기 숙소를 계산했다.

처음에는 문제가 없어 보였지만 기능을 다시 살펴보면서 두 가지 문제가 보였다.

  1. 과거에 조회수가 높았던 숙소가 계속 상위에 남는다.
  2. 인기 숙소 요청마다 Redis와 MySQL을 반복해서 조회한다.

이번 글에서는 이를 Redis Sorted Set과 응답 캐시를 이용해 개선한 과정을 정리한다.


🔥 누적 조회수 방식의 문제

기존 방식은 숙소가 조회될 때마다 단순하게 조회수를 증가시키는 구조였다.

숙소 A : 10,000
숙소 B : 8,000
숙소 C : 5,000

문제는 시간이 지나도 과거에 많이 조회된 숙소가 계속 유리하다는 점이었다.

최근 인기가 높아진 숙소가 있어도 과거 누적 데이터가 워낙 크다면 상위 순위에 진입하기 어려웠다.

그래서 랭킹을 기간별로 나누기로 했다.

DAILY  → 오늘 인기 숙소
WEEKLY → 이번 주 인기 숙소

✅ Redis Sorted Set

랭킹은 Redis Sorted Set으로 구성했다.

member = 숙소 ID
score  = 예약 발생 횟수

예약이 발생하면 해당 숙소의 Score를 증가시키는 방식이다.

Sorted Set은 Score를 기준으로 이미 정렬된 데이터를 조회할 수 있기 때문에 인기 순위를 구현하기 적합했다.

일간 데이터는 날짜별로 분리했고, 주간 데이터는 해당 주의 월요일을 기준으로 관리했다.

서버 실행 환경에 따라 날짜 기준이 달라지는 것을 막기 위해 Asia/Seoul을 기준으로 처리했고, 테스트에서는 Clock을 주입해 원하는 날짜를 고정한 상태로 검증할 수 있도록 했다.


🔒 Score 증가와 TTL 설정을 하나의 작업으로

새로운 랭킹 Key가 생성되면

Score 증가
+
TTL 설정

두 작업이 함께 수행되어야 한다.

두 명령을 각각 실행할 경우 첫 번째 작업은 성공했지만 두 번째 작업이 실패하면 만료시간이 없는 랭킹 데이터가 남을 수 있다.

그래서 Lua Script를 사용해 Score 증가와 TTL 설정을 하나의 원자적인 작업으로 처리했다.


🔥 랭킹은 Redis인데 왜 DB 조회가 계속 발생할까?

Redis Sorted Set에는 숙소 ID와 순위 정보가 저장되어 있다.

하지만 사용자에게 최종적으로 응답하려면

  • 숙소 이름
  • 이미지
  • 가격
  • 상태

등의 추가 정보가 필요하다.

그래서 요청마다

Redis 랭킹 조회
      ↓
숙소 ID 획득
      ↓
MySQL 숙소 조회
      ↓
DTO 생성

과정이 반복되고 있었다.

인기 숙소 API는 짧은 시간 동안 같은 결과가 반복적으로 요청될 가능성이 높다.

그래서 최종 응답에도 Cache Aside 방식의 응답 캐시를 추가했다.

TTL은 60초로 설정했다.

Request
 ↓
Response Cache
 ↓
HIT → 바로 반환

MISS
 ↓
Redis Ranking
 ↓
MySQL
 ↓
DTO 생성
 ↓
Cache 저장

🔍 랭킹을 요청 개수보다 더 많이 가져온 이유

랭킹에 존재하는 숙소라고 해서 반드시 사용자에게 보여줄 수 있는 숙소라는 보장은 없다.

랭킹에 들어간 이후 운영 중 비활성 상태로 변경된 숙소가 있을 수도 있다.

예를 들어 사용자가 인기 숙소 5개를 요청했는데 상위 5개 중 2개가 비활성 상태라면 최종적으로 3개밖에 반환하지 못한다.

그래서 정확히 limit만 조회하지 않고 limit × 5 범위의 후보를 먼저 가져온 후 현재 ACTIVE 상태인 숙소만 필터링했다.

이를 통해 비활성 숙소 때문에 최종 응답 개수가 부족해지는 상황을 줄였다.


🚨 Redis가 장애 나면 인기 숙소 API도 죽어야 할까?

Redis는 인기 순위를 빠르게 조회하기 위한 구성 요소다.

Redis에 장애가 생겼다고 해서 숙소 목록 자체를 사용자에게 제공하지 못하는 상황은 피하고 싶었다.

그래서 Redis 랭킹 조회에 실패하면 MySQL에서 최신 ACTIVE 숙소를 조회하도록 fallback을 구성했다.

Redis 정상
→ 인기 랭킹 반환

Redis 장애
→ DB 최신 ACTIVE 숙소 반환

Redis에서 계산한 정확한 인기 순위는 아니더라도 사용자에게 숙소 데이터 자체는 계속 제공할 수 있도록 한 것이다.


🚀 성능 테스트

10 VU, 30초 조건으로 캐시 적용 전후를 비교했다.

캐시 적용 전

평균 응답시간 : 6.27ms
p95          : 9.15ms
처리량        : 1566.50 RPS
에러율        : 0%

Warm Cache

평균 응답시간 : 1.31ms
p95          : 2.21ms
처리량        : 7166.16 RPS
에러율        : 0%

결과적으로

평균 응답시간
6.27ms → 1.31ms
79.1% 감소

처리량
1566.50 → 7166.16 RPS
4.57배 향상

을 확인했다.


💡 이번 개선에서 배운 점

Redis를 사용한다고 무조건 모든 조회가 빨라지는 것은 아니었다.

Redis에서 순위만 빠르게 조회하더라도 이후 DB 조회와 DTO 생성이 요청마다 반복된다면 동일한 작업은 계속 발생한다.

그래서

랭킹 데이터 관리
→ Redis Sorted Set

반복되는 최종 응답
→ Response Cache

Redis 장애
→ DB fallback

처럼 문제에 따라 Redis의 역할을 나누어 설계했다.

이번 경험을 통해 캐시를 단순히 성능 향상을 위한 기술로 사용하는 것이 아니라 반복되는 작업은 무엇인지, 장애가 발생하면 서비스는 어떻게 동작해야 하는지까지 함께 생각해야 한다는 것을 배웠다.

profile
끝까지 가면 내가 다 이겨

0개의 댓글