Redis Sorted Set을 이용한 랭킹 관리

Jeongkuk Seo
sjk5766
Published in
8 min readFeb 26, 2024

서비스에서 어떤 종류의 랭킹 정보를 제공해야 한다면 기술적으로는 어떤 방법들이 있을까? 가장 먼저 떠오르는 방법은 RDB에 쿼리로 조회해서 제공하는 방법이다.

  • Seed 데이터를 합산해서 랭킹 정보를 만들고 정렬해서 제공한다.
  • 배치로 랭킹을 생성해 테이블에 저장하고, 해당 테이블을 단순 조회해서 제공한다.

최근에 Redis Sorted Set을 이용해 랭킹 정보를 제공했고 이와 관련된 내용들을 정리해 보겠다.

왜 Redis를 쓸까?

속도 때문이다. RDB를 사용하면 캐시되지 않았다고 가정할 경우 데이터를 디스크에서 불러오지만 Redis는 항상 메모리에서 가져온다. 실무에서 사용한다면 Redis의 랭킹 정보가 휘발될 수 있기 때문에 Redis 백업을 해두거나, 랭킹 정보 or 랭킹 정보를 생성하는 Seed 데이터가 DB에 있어야 한다.

Redis Sorted Set

Sorted Set은 Redis가 제공하는 자료 구조로 Score를 기준으로 정렬된 Unique string을 관리하는 컬렉션이다. 만약 Score 값이 동일하다면 사전순으로 정렬된다.

대부분의 Sorted Set과 관련된 동작들은 O(log N)의 시간 복잡도를 가진다.

Redis-cli를 이용한 Sorted Set 다루기

백문이 불여일견이라고 더 이상 설명은 그만두고 local에 Redis가 설치되어 있다고 가정하고 redis-cli로 실습해본다.

추가

Sorted Set의 추가는 ZADD 명령어로 추가한다. ZADD는 인자들을 옵셔널하게 더 추가할 수 있지만 기본적으로 ZADD Key Score Member 의 형태를 가진다. Sorted Set이 없는 경우 처음 ZADD 명령어에 의해 Sorted Set이 생성된다.

127.0.0.1:6379> ZADD mySortedSet 10 "1등"
127.0.0.1:6379> ZADD mySortedSet 9 "2등"
127.0.0.1:6379> ZADD mySortedSet 8 "3등"
127.0.0.1:6379> ZADD mySortedSet 7 "4등"
127.0.0.1:6379> ZADD mySortedSet 6 "5등"

조회

ZRANGE 명령어를 이용해 Sorted Set을 조회할 수 있다. 아래 예시에서 명령어는 ZRANGE Key start stop 형태이며 start와 end는 조회하는 인덱스 범위를 의미한다.

127.0.0.1:6379> ZRANGE mySortedSet 0 4
5등
4등
3등
2등
1등

3개의 랭킹 데이터를 조회하고 싶다면 인덱스 범위를 수정하면 된다.

127.0.0.1:6379> ZRANGE mySortedSet 0 2
5등
4등
3등

조회할 때 Score도 같이 보고 싶다면 WITHSCORES 옵션을 준다.

127.0.0.1:6379> ZRANGE mySortedSet 0 2 WITHSCORES
5등
6
4등
7
3등
8

보면 알겠지만 ZRANGE 명령어는 Score가 낮은 순부터 조회한다. 만약 Score가 높을 수록 랭킹이 높다고 가정하면 조회할 때 ZREVRANGE 명령어를 사용한다. Redis 6.2 버전부터 해당 메서드는 deprecated 되며 ZRANGE 명령어에 REV 인자를 추가하는 형태로 활용된다.

127.0.0.1:6379> ZREVRANGE mySortedSet 0 4
1등
2등
3등
4등
5등

수정

동일한 멤버에 대해 ZADD 명령어를 수행하면 SCORE 값이 업데이트 된다.

127.0.0.1:6379> ZREVRANGE mySortedSet 0 4
5등
4등
3등
2등
1등

127.0.0.1:6379> ZADD mySortedSet 1"1등"
0

127.0.0.1:6379> ZREVRANGE mySortedSet 0 4
1등
5등
4등
3등
2등

NestJS에서 인물 랭킹 추가 및 조회하기

예제의 전체 코드는 여기서 확인할 수 있다.

NestJS와 Redis의 Sorted Set을 사용해 인물 랭킹을 추가/조회 하는 간단한 기능을 제공해 보겠다. local에 Redis가 설치되어 있다고 가정한다. 우선 nest-cli를 활용해 NestJS 프로젝트를 생성하고 Redis 동작에 필요한 모듈을 설치한다.

yarn add @nestjs-modules/ioredis ioredis

Redis의 설정 정보를 가지고 있는 Redis Module을 먼저 작성하자.

import { Module } from '@nestjs/common';
// export 하는 모듈 이름이 RedisMoudle인데 import 하는 라이브러도 이름이 Redis Module이라 alias 해줌
import { RedisModule as IORedisModule } from '@nestjs-modules/ioredis';
import { RedisService } from './redis.service';

@Module({
imports: [
IORedisModule.forRoot({
type: 'single',
url: 'localhost',
}),
],
providers: [RedisService],
exports: [RedisService],
})
export class RedisModule {}

다른 모듈에서 호출 할 RedisService를 작성한다. RedisService가 제공하는 함수에는 Sorted Set에 추가할 때 호출할 zadd와 조회할 때 활용할 zrange, zrevrange 메서드를 작성해 두었다.

import { InjectRedis } from '@nestjs-modules/ioredis';
import { Injectable } from '@nestjs/common';
import { Redis } from 'ioredis';

@Injectable()
export class RedisService {
constructor(@InjectRedis() private redis: Redis) {}

async zadd(key: string, score: number, member: string) {
return this.redis.zadd(key, score, member);
}

async zrange(key: string, max: number) {
return this.redis.zrange(key, 0, max);
}

async zrevrange(key: string, max: number) {
return this.redis.zrevrange(key, 0, max);
}
}

인물에 대한 랭킹 정보를 제공하기 위해 Person 모듈을 만들고, 서비스를 아래와 같이 작성한다. 실무에선 addPersonRanking 메서드에서 인자로 인물 정보를 받겠지만 편리성을 위해 그냥 하드코딩 해 두었다.

Score가 점수라서 높을 수록 랭킹이 높다면 redisService의 ZREVRANGE 메서드를 호출하겠지만 아래 예제에서는 Score가 랭킹과 동일하므로 ZRANGE로 조회했다.

import { Injectable } from '@nestjs/common';
import { RedisService } from '../redis/redis.service';

@Injectable()
export class PersonService {
constructor(private readonly redisService: RedisService) {}

async getPersonRanking() {
return this.redisService.zrange('mySortedSet', 10);
}

async addPersonRanking() {
const persons = [
{ ranking: 1, name: '마동석' },
{ ranking: 2, name: '손석구' },
{ ranking: 3, name: '아이유' },
{ ranking: 4, name: '유재석' },
{ ranking: 5, name: '이광수' },
];

for (const person of persons) {
await this.redisService.zadd(
'mySortedSet',
person.ranking,
person.name
);
}
return true;
}
}

Person Controller를 통해 Get / Post 메서드를 제공하고 Postman을 이용해 정상적으로 조회할 수 있었다.

정리하며

Redis는 단순히 캐시 용도로만 사용하는 줄 알았는데 알면 알수록 제공하는 기능들이 많았다. Sorted Set은 랭킹 외에도 RateLimit을 구현하는데 유용하며 Redis가 제공하는 기능들로 캐싱 / 세션 스토어 / 메시지 큐 / 실시간 분석 / 실시간 채팅 / 지리 공간 데이터 등의 다양한 기능들을 어렵지 않게 다룰 수 있다.

--

--