코딩못하는사람

Redis를 통한 랭킹보드 구현해보기 (SpringBoot+Redis) 본문

스프링부트(SpringBoot)/활용

Redis를 통한 랭킹보드 구현해보기 (SpringBoot+Redis)

공부절대안함 2021. 10. 1. 19:16

발단

저번 우아한 Redis 강연을 듣고 공부하면서(https://cantcoding.tistory.com/70) Redis의 Sorted Set기능이 눈에 들어왔다.

현재 진행하고 있는 프로젝트에서 서비스 사용시 주어지는 포인트 시스템이 있는데, 그 포인트와 닉네임을 key-value 형태 Sorted Set에 저장하여 랭킹보드를 구현하면 재밌을거같다는 생각이 들어서 실제 적용하였다.

 

Redis 설치

필자는 AWS EC2 서버에 Docker 를 통하여 Redis를 설치하였다. (http://redisgate.kr/redis/education/docker_intro.php )

저 레퍼런스에 들어가면 쉽게 따라할 수 있다.

레디스 서버 실행하기

docker run --name myredis -d -p 6379:6379 redis

Docker의 redis-cli로 접속하기

docker run -it --link myredis:redis --rm redis redis-cli -h redis -p 6379

두개의 명령어는 자주 쓰이니기억하도록 하자.

 

Spring Boot에 Redis의존성 및 Configuration 추가

나는 gradle을 사용하기 때문에 다음 의존성을 추가해주고

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

 

Redis를 사용하기 위한 template을 만들어 Spring Bean에 등록해줍니다.

@Configuration
@EnableRedisRepositories
public class RedisConfig {

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.host}")
    private String host;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());//key 깨짐 방지
        redisTemplate.setValueSerializer(new StringRedisSerializer());//value 깨짐 방지
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }

}

제 프로젝트에서는 key-value를 (회원 닉네임 - 회원 포인트)로 저장하기 때문에 제네릭을 <String,String>로 설정해주었고, yml파일에 host,port를 지정해두었습니다.

다음과 같은 설정을 해주면 redisTemplate을 사용해서 Redis를 다룰 수 있게 된다.

설정을 완료했으니 이번에 사용할 Redis Sorted Set(Zset) 을 알아보자.

 

Redis Sorted Set(Zset) 

Sorted set 은 이름에서 알 수 있듯이 중복되지 않은 데이터를 가지는 Collection이다. 가중치(Score)를 가지고 있어서 Score에 따라 정렬된 순서로 저장된다는 특징이 있다(score가 같으면 value에 따라 정렬된다).

기본적인 명령어로는 zadd,zcard,zrange,zrank,zrangebyscore 정도가 있다.

 

1.zadd 집합에 Score와 Value를 추가한다.

ex) zadd key score value

 

2.zcard 집합에 속해있는 value의 갯수를 알려준다.

ex) zcard key

 

3.zrange 집합에서 내가 출력하고 싶은 원소의 시작index와 끝 index를 넣으면 출력된다.(범위 탐색)

ex) zrange key startindex endindex

 

4.zrank value가 몇번째 데이터인지 조회한다.

ex) zrank key value

 

5.zrangebyscore 내가 출력하고 싶은 스코어 범위에 있는 원소들을 출력한다.

ex) zrangebyscore key 100 500 (score가 100점이상 500점 이하 원소들을 출력합니다)

+ zrevrangebyscore로 하면 내림차순으로 정렬된다.

 

실제 코드 및 적용

프로젝트에서 회원 Point에 변동이 생길때 PointService를 거쳐가도록 되어있어서 PointService단에 Redis관련 코드를 추가해주었다 (nickName을 중복불가여야 가능하다)

    @Transactional
    public void create(Member member,String content, int score) {
        Point point = Point.createPoint(member, content, score);
        member.setPoint(member.getPoint()+score);
        if (member.getRole() != Role.ADMIN) {
            redisTemplate.opsForZSet().add("ranking", member.getNickname(), member.getPoint());
        }
        pointRepository.save(point);
    }

 

 

전체 랭킹조회 및 내 랭킹 조회 API에 사용될 서비스단 코드도 제작하였다.

@RequiredArgsConstructor
public class RankingService {
    private final RedisTemplate redisTemplate;
    private final JwtService jwtService;

    public List<ResponseRankingDto> getRankingList() {
        String key = "ranking";
        ZSetOperations<String, String> stringStringZSetOperations = redisTemplate.opsForZSet();
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringStringZSetOperations.reverseRangeWithScores(key, 0, 10);
        List<ResponseRankingDto> collect = typedTuples.stream().map(ResponseRankingDto::convertToResponseRankingDto).collect(Collectors.toList());
        return collect;
    }
    public Long getMyRank(String token){
        Long ranking=0L;
        Double ranking1 = redisTemplate.opsForZSet().score("ranking", jwtService.findMemberByToken(token).getNickname());
        Set<String> ranking2 = redisTemplate.opsForZSet().reverseRangeByScore("ranking", ranking1, ranking1, 0, 1);
        for (String s : ranking2) {
            ranking = redisTemplate.opsForZSet().reverseRank("ranking", s);
        }
        return ranking+1;//index가 0부터 시작되어서 1 더해준다
    }
}

+문제 발생)

코드를 적용하여 테스트해볼 때 랭킹조회 API는 문제가 없었는데 내 랭킹 조회에서 문제가 생긴다.

동점자가 여러명이면 자신의 랭킹을 제대로 확인할 수 없다는 문제가 있었다. 예를 들어 

10점 5점 5점 5점의 score를 가진 4명이 존재한다면  랭킹확인시 5점 3명은 2등으로 표시되길 원하기 때문이다.

이러한 문제를 해결하고자 reverseRangeByScore메서드를 사용했다. 이 메서드를 사용하면 원하는 score 범위의 value값을 원하는 갯수만큼 가져올 수 있는 기능을 한다. 하지만 위에서 Sorted Set은 score가 같을때 value값을 기준으로 정렬한다고 했다. 따라서 나의 점수와 같은 사람들 중 등수가 제일 높은사람의 등수를 반환할 수 있는 로직을 구성해서 문제를 해결했다. 

 

결과 화면

전체 랭킹화면 및 내 랭킹 조회

 

배운점

  • 회원 변경 및 삭제가 일어날때도 Redis value값을 바꿔줘야한다는 걸 서비스 도중 알게되어서 수정했다.
  • 중복 score관련 랭킹조회 문제를 해결했다.
  • 적용하고 싶은것을 스터디하여 바로 적용하니 뿌듯하다.

나아갈 점

  • Redis는 휘발성 메모리다.서버에 이상이 생기면 날아갈 수 있다는 뜻이다. BackUp방식에 대해 스터디 해봐야겠다.

 

 

 

 

Comments