Redis를 사용하여 백엔드 쿼리 최적화
Source: Dev.to
원래 접근 방식 (편안한 방법)
엔드포인트 로직은 간단했습니다:
- 데이터베이스 조회
- 점수 순으로 사용자 정렬
- 상위 10명 반환
SELECT * FROM users ORDER BY score DESC LIMIT 10;
적절한 인덱싱을 하면 소규모에서는 잘 동작했지만, 리더보드는:
- 자주 접근됨
- 자주 업데이트됨
- 경쟁이 치열한 실시간 데이터
매 요청마다 데이터베이스에 접근하는 것은 곧 문제로 떠올랐습니다.
첫 번째 시도: Redis 사용하기
Redis는 메모리 기반, 빠르고 순위 매기기에 최적화돼 있어 완벽해 보였습니다.
하지만 로컬에서 실행하니 오류가 발생했습니다:
Error: Address already in use
Port 6379 was already occupied.
서비스를 재시작하고 프로세스를 종료해 보았지만 해결되지 않아, Redis를 제대로 격리하기로 했습니다.
해결책: Redis 도커화
docker run -d -p 6379:6379 --name redis-server redis
컨테이너에서 Redis를 실행하면:
- 격리됨
- 이식 가능함
- 깔끔하게 실행됨
- 재시작이 쉬움
환경이 정리되니 작업을 진행할 수 있었습니다.
정렬된 집합 (ZSET) 도입
Redis Sorted Sets는 자동으로 멤버를 점수 기준으로 정렬합니다.
- 멤버 → 사용자 ID
- 점수 → 포인트
이 덕분에 SQL 정렬과 무거운 DB 읽기가 필요 없게 되었습니다.
사용자의 점수 업데이트
await redis.zadd("leaderboard", score, userId);
상위 10명 가져오기
await redis.zrevrange("leaderboard", 0, 9, "WITHSCORES");
이제 순위 로직이 전부 메모리에서 처리되며, 지연 시간이 즉시 개선되었습니다.
예상치 못한 숨은 병목 현상
상위 10명의 사용자 ID를 가져온 뒤, 추가적인 사용자 상세 정보(이름, 아바타 등)가 필요했습니다:
for (let userId of topUsers) {
await redis.hgetall(`user:${userId}`);
}
이로 인해 Redis 내부에서 N+1 문제가 발생했습니다:
- 1 요청 → 리더보드 조회
- 10 요청 → 각각의 사용자 조회
결과: 총 11번의 네트워크 왕복, 약 100 ms 추가 지연.
진짜 해결책: Redis 파이프라이닝
Redis 파이프라이닝은 명령을 배치해 왕복 횟수를 줄여줍니다.
const pipeline = redis.pipeline();
for (let userId of topUsers) {
pipeline.hgetall(`user:${userId}`);
}
const users = await pipeline.exec();
이제 한 번의 네트워크 왕복만 필요해 N+1 지연이 사라졌습니다.
결과
| 단계 | 지연 시간 |
|---|---|
| DB 정렬 | ~200 ms |
| Redis (파이프라인 없음) | ~120 ms |
| Redis + 파이프라인 | ~20 ms |
네트워크 호출을 줄인 덕분에 10배 정도의 개선을 이루었습니다.
배운 점
- 인프라 문제가 최우선 – Redis가 정상적으로 실행되지 않으면 다른 건 의미가 없습니다.
- 데이터 구조가 중요 – ZSET 덕분에 반복적인 정렬 자체가 사라졌습니다.
- N+1 문제는 DB에만 국한되지 않음 – 원격 시스템에서도 발생할 수 있습니다.
- 네트워크 지연은 눈에 보이지 않지만 비용이 큼 – “빠른” 시스템도 호출이 잦으면 느려집니다.
- Docker가 백엔드 생활을 단순화 – 의존성을 컨테이너화하면 OS 수준 충돌을 피할 수 있습니다.
최종 아키텍처
- 점수 업데이트 →
ZADD - 상위 10명 조회 →
ZREVRANGE - 사용자 데이터 일괄 조회 → 파이프라인 +
EXEC - 응답 반환
데이터베이스 접근 없이, 전부 메모리에서 처리, 네트워크 호출 최소화, 응답 시간 ~20 ms.
마무리 생각
최적화는 도구를 무작정 사용하는 것이 아니라, 실제 시간이 어디에 쓰이는지 파악하는 일입니다. 이번 경우 가장 큰 성과는 다음에서 나왔습니다:
- 환경 정비
- 올바른 데이터 구조 선택
- 네트워크 왕복 횟수 감소
이 세 가지를 해결한 것이 모든 차이를 만들었습니다.