-
Notifications
You must be signed in to change notification settings - Fork 3
[refactor] 랭킹 조회 Redis 적용 #111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
37f12dd
:recycle: refactor: 랭킹 조회 Redis 적용
dlsrks1021 f8ab68e
chore: Java 스타일 수정
1519c09
chore: 불필요 slf4j 제거
dlsrks1021 0fe2db0
chore: updateRank의 hashkey 값 이름 정정
dlsrks1021 3059076
chore: Redis MQ 이벤트 전달을 위한 딜레이 값을 1초로 변경
dlsrks1021 cff6ebe
refactor: ZSET, STRINGS의 value 값을 HSET의 key에서 id 값으로 변경
dlsrks1021 cbdd37b
chore: Java 스타일 수정
65157e6
refactor: UserNickname DTO를 UserSummary로 이름 변경
dlsrks1021 63f67cf
chore: Java 스타일 수정
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 26 additions & 24 deletions
50
backend/src/main/java/io/f1/backend/domain/stat/app/StatService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,53 +1,55 @@ | ||
| package io.f1.backend.domain.stat.app; | ||
|
|
||
| import static io.f1.backend.domain.stat.mapper.StatMapper.toStatListPageResponse; | ||
|
|
||
| import io.f1.backend.domain.stat.dao.StatRepository; | ||
| import io.f1.backend.domain.stat.dto.StatPageResponse; | ||
| import io.f1.backend.domain.stat.dto.StatWithNickname; | ||
| import io.f1.backend.global.exception.CustomException; | ||
| import io.f1.backend.global.exception.errorcode.RoomErrorCode; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Sort; | ||
| import org.springframework.data.domain.Sort.Direction; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Service | ||
| @Transactional | ||
| @RequiredArgsConstructor | ||
| public class StatService { | ||
|
|
||
| private final StatRepository statRepository; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public StatPageResponse getRanks(Pageable pageable) { | ||
| Page<StatWithNickname> stats = statRepository.findAllStatsWithUser(pageable); | ||
| return toStatListPageResponse(stats); | ||
| } | ||
| public StatPageResponse getRanks(Pageable pageable, String nickname) { | ||
| StatPageResponse response; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public StatPageResponse getRanksByNickname(String nickname, int pageSize) { | ||
| if (StringUtils.isBlank(nickname)) { | ||
| response = statRepository.getRanks(pageable); | ||
| } else { | ||
| response = statRepository.getRanksByNickname(nickname, pageable.getPageSize()); | ||
| } | ||
|
|
||
| Page<StatWithNickname> stats = | ||
| statRepository.findAllStatsWithUser(getPageableFromNickname(nickname, pageSize)); | ||
| if (response.totalElements() == 0) { | ||
| throw new CustomException(RoomErrorCode.PLAYER_NOT_FOUND); | ||
| } | ||
|
|
||
| return toStatListPageResponse(stats); | ||
| return response; | ||
| } | ||
|
|
||
| private Pageable getPageableFromNickname(String nickname, int pageSize) { | ||
| long score = | ||
| statRepository | ||
| .findScoreByNickname(nickname) | ||
| .orElseThrow(() -> new CustomException(RoomErrorCode.PLAYER_NOT_FOUND)); | ||
| // TODO: 게임 종료 후 호출 필요 | ||
| public void updateRank(long userId, boolean win, int deltaScore) { | ||
| statRepository.updateRank(userId, win, deltaScore); | ||
| } | ||
|
|
||
| long rowNum = statRepository.countByScoreGreaterThan(score); | ||
| public void addUser(long userId, String nickname) { | ||
| statRepository.addUser(userId, nickname); | ||
| } | ||
|
|
||
| public void removeUser(long userId) { | ||
| statRepository.removeUser(userId); | ||
| } | ||
|
|
||
| int pageNumber = rowNum > 0 ? (int) (rowNum / pageSize) : 0; | ||
| return PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "score")); | ||
| public void updateNickname(long userId, String newNickname) { | ||
| statRepository.updateNickname(userId, newNickname); | ||
| } | ||
| } |
66 changes: 66 additions & 0 deletions
66
backend/src/main/java/io/f1/backend/domain/stat/dao/StatJpaRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package io.f1.backend.domain.stat.dao; | ||
|
|
||
| import io.f1.backend.domain.stat.dto.StatWithNickname; | ||
| import io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId; | ||
| import io.f1.backend.domain.stat.entity.Stat; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Modifying; | ||
| import org.springframework.data.jpa.repository.Query; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Optional; | ||
|
|
||
| public interface StatJpaRepository extends JpaRepository<Stat, Long> { | ||
|
|
||
| @Query( | ||
| """ | ||
| SELECT | ||
| new io.f1.backend.domain.stat.dto.StatWithNickname | ||
| (u.nickname, s.totalGames, s.winningGames, s.score) | ||
| FROM | ||
| Stat s JOIN s.user u | ||
| """) | ||
| Page<StatWithNickname> findAllStatsWithUser(Pageable pageable); | ||
|
|
||
| @Query("SELECT s.score FROM Stat s WHERE s.user.nickname = :nickname") | ||
| Optional<Long> findScoreByNickname(String nickname); | ||
|
|
||
| long countByScoreGreaterThan(Long score); | ||
|
|
||
| @Query( | ||
| """ | ||
| SELECT | ||
| new io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId | ||
| (u.id, u.nickname, s.totalGames, s.winningGames, s.score) | ||
| FROM | ||
| Stat s JOIN s.user u | ||
| """) | ||
| List<StatWithNicknameAndUserId> findAllStatWithNicknameAndUserId(); | ||
|
|
||
| @Modifying | ||
| @Query( | ||
| """ | ||
| UPDATE | ||
| Stat s | ||
| SET | ||
| s.totalGames = s.totalGames + 1, s.winningGames = s.winningGames + 1, s.score = s.score + :deltaScore | ||
| WHERE | ||
| s.user.id = :userId | ||
| """) | ||
| void updateStatByUserIdCaseWin(long deltaScore, long userId); | ||
|
|
||
| @Modifying | ||
| @Query( | ||
| """ | ||
| UPDATE | ||
| Stat s | ||
| SET | ||
| s.totalGames = s.totalGames + 1, s.score = s.score + :deltaScore | ||
| WHERE | ||
| s.user.id = :userId | ||
| """) | ||
| void updateStatByUserIdCaseLose(long deltaScore, long userId); | ||
| } |
152 changes: 152 additions & 0 deletions
152
backend/src/main/java/io/f1/backend/domain/stat/dao/StatRedisRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| package io.f1.backend.domain.stat.dao; | ||
|
|
||
| import static java.util.Objects.requireNonNull; | ||
|
|
||
| import io.f1.backend.domain.stat.dto.StatPageResponse; | ||
| import io.f1.backend.domain.stat.dto.StatResponse; | ||
| import io.f1.backend.domain.stat.dto.StatWithNicknameAndUserId; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Sort; | ||
| import org.springframework.data.domain.Sort.Direction; | ||
| import org.springframework.data.redis.core.HashOperations; | ||
| import org.springframework.data.redis.core.RedisTemplate; | ||
| import org.springframework.data.redis.core.ValueOperations; | ||
| import org.springframework.data.redis.core.ZSetOperations; | ||
| import org.springframework.data.redis.core.ZSetOperations.TypedTuple; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
|
|
||
| @Repository | ||
| @RequiredArgsConstructor | ||
| public class StatRedisRepository { | ||
| private static final String STAT_RANK = "stat:rank"; | ||
| private static final String STAT_USER = "stat:user:%d"; | ||
| private static final String STAT_NICKNAME = "stat:%s"; | ||
|
|
||
| private final RedisTemplate<String, Object> redisTemplate; | ||
| private ZSetOperations<String, Object> zSetOps; | ||
| private HashOperations<String, Object, Object> hashOps; | ||
| private ValueOperations<String, Object> valueOps; | ||
|
|
||
| public void setup() { | ||
| zSetOps = redisTemplate.opsForZSet(); | ||
| hashOps = redisTemplate.opsForHash(); | ||
| valueOps = redisTemplate.opsForValue(); | ||
| } | ||
|
|
||
| public void initialize(StatWithNicknameAndUserId stat) { | ||
| String statUserKey = getStatUserKey(stat.userId()); | ||
| String statNicknameKey = getStatNickname(stat.nickname()); | ||
|
|
||
| // stat:user:{id} | ||
| hashOps.put(statUserKey, "nickname", stat.nickname()); | ||
| hashOps.put(statUserKey, "totalGames", stat.totalGames()); | ||
| hashOps.put(statUserKey, "winningGames", stat.winningGames()); | ||
|
|
||
| // stat:rank | ||
| zSetOps.add(STAT_RANK, statUserKey, stat.score()); | ||
|
|
||
| // stat:{nickname} | ||
| valueOps.set(statNicknameKey, statUserKey); | ||
| } | ||
|
|
||
| public void removeUser(long userId) { | ||
| String statUserKey = getStatUserKey(userId); | ||
| String nickname = (String) hashOps.get(statUserKey, "nickname"); | ||
| redisTemplate.delete(getStatUserKey(userId)); | ||
| valueOps.getAndDelete(getStatNickname(nickname)); | ||
| zSetOps.remove(STAT_RANK, statUserKey); | ||
| } | ||
|
|
||
| public StatPageResponse findAllStatsWithUser(Pageable pageable) { | ||
| int page = pageable.getPageNumber(); | ||
| int size = pageable.getPageSize(); | ||
| long startIdx = (long) page * size; | ||
|
|
||
| long totalCount = getTotalMemberCount(); | ||
| int totalPages = (int) Math.ceil((double) totalCount / size); | ||
|
|
||
| Set<TypedTuple<Object>> rankSet = getRankSet(startIdx, size); | ||
|
|
||
| AtomicInteger rankCounter = new AtomicInteger((int) startIdx + 1); | ||
| List<StatResponse> ranks = | ||
| rankSet.stream() | ||
| .map(rank -> convertToStatResponse(rank, rankCounter.getAndIncrement())) | ||
| .toList(); | ||
|
|
||
| return new StatPageResponse(totalPages, page + 1, rankSet.size(), ranks); | ||
| } | ||
|
|
||
| public Pageable getPageableFromNickname(String nickname, int pageSize) { | ||
| String statUserKey = getStatUserKeyFromNickname(nickname); | ||
| long rowNum = requireNonNull(zSetOps.reverseRank(STAT_RANK, statUserKey)) + 1; | ||
| int pageNumber = rowNum > 0 ? (int) (rowNum / pageSize) : 0; | ||
| return PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "score")); | ||
| } | ||
|
|
||
| private long getTotalMemberCount() { | ||
| long count = requireNonNull(zSetOps.zCard(STAT_RANK)); | ||
| if (count == 0) { | ||
| throw new NullPointerException("No member found in redis"); | ||
| } | ||
| return count; | ||
| } | ||
|
|
||
| private Set<TypedTuple<Object>> getRankSet(long startIdx, int size) { | ||
| Set<TypedTuple<Object>> result = | ||
| zSetOps.reverseRangeWithScores(STAT_RANK, startIdx, startIdx + size - 1); | ||
| return requireNonNull(result); | ||
| } | ||
|
|
||
| public void updateRank(long userId, boolean win, int deltaScore) { | ||
| String statUserKey = getStatUserKey(userId); | ||
|
|
||
| zSetOps.incrementScore(STAT_RANK, getStatUserKey(userId), deltaScore); | ||
| hashOps.increment(statUserKey, "totalGame", 1); | ||
| if (win) { | ||
| hashOps.increment(statUserKey, "winningGame", 1); | ||
| } | ||
| } | ||
|
|
||
| public void updateNickname(long userId, String newNickname) { | ||
| String statUserKey = getStatUserKey(userId); | ||
| valueOps.set(getStatNickname(newNickname), statUserKey); | ||
|
|
||
| String oldNickname = (String) hashOps.get(statUserKey, "nickname"); | ||
| valueOps.getAndDelete(getStatNickname(oldNickname)); | ||
|
|
||
| hashOps.put(statUserKey, "nickname", newNickname); | ||
| } | ||
|
|
||
| private StatResponse convertToStatResponse(TypedTuple<Object> rank, int rankValue) { | ||
| String statUserKey = (String) requireNonNull(rank.getValue()); | ||
| Map<Object, Object> statUserMap = hashOps.entries(statUserKey); | ||
|
|
||
| return new StatResponse( | ||
| rankValue, | ||
| (String) statUserMap.get("nickname"), | ||
| (long) statUserMap.get("totalGames"), | ||
| (long) statUserMap.get("winningGames"), | ||
| requireNonNull(rank.getScore()).longValue()); | ||
| } | ||
|
|
||
| private static String getStatUserKey(long userId) { | ||
| return String.format(STAT_USER, userId); | ||
| } | ||
|
|
||
| private static String getStatNickname(String nickname) { | ||
| return String.format(STAT_NICKNAME, nickname); | ||
| } | ||
|
|
||
| private String getStatUserKeyFromNickname(String nickname) { | ||
| return (String) requireNonNull(valueOps.get(getStatNickname(nickname))); | ||
| } | ||
| } | ||
29 changes: 10 additions & 19 deletions
29
backend/src/main/java/io/f1/backend/domain/stat/dao/StatRepository.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,29 +1,20 @@ | ||
| package io.f1.backend.domain.stat.dao; | ||
|
|
||
| import io.f1.backend.domain.stat.dto.StatWithNickname; | ||
| import io.f1.backend.domain.stat.entity.Stat; | ||
| import io.f1.backend.domain.stat.dto.StatPageResponse; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
|
|
||
| import java.util.Optional; | ||
| public interface StatRepository { | ||
|
|
||
| public interface StatRepository extends JpaRepository<Stat, Long> { | ||
| StatPageResponse getRanks(Pageable pageable); | ||
|
|
||
| @Query( | ||
| """ | ||
| SELECT | ||
| new io.f1.backend.domain.stat.dto.StatWithNickname | ||
| (u.nickname, s.totalGames, s.winningGames, s.score) | ||
| FROM | ||
| Stat s JOIN s.user u | ||
| """) | ||
| Page<StatWithNickname> findAllStatsWithUser(Pageable pageable); | ||
| StatPageResponse getRanksByNickname(String nickname, int pageSize); | ||
|
|
||
| @Query("SELECT s.score FROM Stat s WHERE s.user.nickname = :nickname") | ||
| Optional<Long> findScoreByNickname(String nickname); | ||
| void addUser(long userId, String nickname); | ||
|
|
||
| long countByScoreGreaterThan(Long score); | ||
| void updateRank(long userId, boolean win, int deltaScore); | ||
|
|
||
| void updateNickname(long userId, String nickname); | ||
|
|
||
| void removeUser(long userId); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[L4-변경제안]
totalGame과 winnigGame 키 명에 s가 붙어야 할 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
헉... update 로직은 아직 사용 위치가 없어 테스트를 안했더니,, 큰일날뻔했네요!
감사합니다 수정하겠습니다!