리더보드

API 요구사항

<aside> 💡 사용자가 많을 수록 생성과 업데이트가 빈번하고 조회 또한 매우 많이 일어나기 때문에 빠른 속도가 필요

</aside>

데이터베이스 성능과 선택

구현

RankingService

@Service
@RequiredArgsConstructor
public class RankingService {

    final StringRedisTemplate redisTemplate;

    private static final String LEADERBOARD_KEY = "leaderBoard";

    public boolean setUserScore(String userId, int score){
        ZSetOperations zSetOps = redisTemplate.opsForZSet();
        zSetOps.add(LEADERBOARD_KEY, userId, score);

        return true;
    }

    public Long getUserScore(String userId){
        ZSetOperations zSetOps = redisTemplate.opsForZSet();

        return zSetOps.reverseRank(LEADERBOARD_KEY, userId);
    }

    public List<String> getTopRank(int limit){
        ZSetOperations zSetOps = redisTemplate.opsForZSet();
        Set<String> rangeSet = zSetOps.reverseRange(LEADERBOARD_KEY, 0, limit - 1);

        assert rangeSet != null;
        return new ArrayList<>(rangeSet);
    }

}

Redis의 SortedSet을 사용하여 랭킹 시스템을 구현하였다. 각 사용자별 랭크를 확인할 수 있는 getUserScore과 n번째 랭킹까지 확인할 수 있는 getTopRank로 구성하였다.

다음은, 100만개의 사용자 정보를 이용한 테스트 코드이다. Sort를 이용한 정렬 시간과 Redis의 SortedSet을 이용한 랭킹 요청 시간을 비교한다. (물론, 완벽히 같은 조건은 아니므로 참고만 하자)

@SpringBootTest
public class RankingTest {

    @Autowired
    private RankingService rankingService;

    @Test
    void getUserRank(){
        //1) 사용자 별 랭크
        Instant before = Instant.now();
        String userId = "user_100";
        Long userRank = rankingService.getUserScore(userId);
        Duration elapsed = Duration.between(before, Instant.now());
        System.out.printf("%s RANK (%d), TOOK %d ms%n", userId, userRank, elapsed.getNano() / 1000000);

        //2) 상위 랭킹
        before = Instant.now();
        int limit = 10;
        List<String> topRank = rankingService.getTopRank(limit);
        elapsed = Duration.between(before, Instant.now());
        System.out.printf("Top %d ranking, TOOK %d ms%n ", limit, elapsed.getNano() / 1000000);

    }

    // 100만개의 사용자 정보 Insert
    @Test
    void insertScore(){
        for (int i=0; i<100*10000; i++){
            int score = (int)(Math.random() * 100 * 10000);
            String userId = "user_" + i;
            rankingService.setUserScore(userId, score);
        }
    }

    // sort를 이용한 정렬 시간 비교
    @Test
    void inMemorySortPerformance(){
        ArrayList<Integer> list = new ArrayList<>();
        for (int i=0; i<100*10000; i++){
            int score = (int)(Math.random() * 100*10000);
            list.add(score);
        };
        Instant before = Instant.now();
        Collections.sort(list);
        Duration elapsed = Duration.between(before, Instant.now());
        System.out.println((elapsed.getNano() / 1000000) + "ms");

    }
}