Conversation
- Redis Sorted Set 활용을 위해서 RedisConfig 파일에 직렬화 설정을 추가했습니다. issue #51
- 애플리케이션 첫 실행 이벤트 발생 시, DB에 있는 랭킹 데이터를 Redis ZSET으로 적재합니다. - Redis fallback 로직으로는 DB에서 데이터를 불러오도록 설계했습니다. - 회원인 경우에만 타이핑 결과 생성 시, Redis와 DB 모두에 데이터를 적재합니다. issue #51
- 연쇄 OR 연산으로 이루어진 단일 랭킹 쿼리를 DISTINCT 절을 활용하여 최적화했습니다. - Controller 단에서 hasRole에 있는 오타를 수정했습니다. - 마이페이지에 사용될 회원별 최고 점수 및 최고 등수 데이터를 반환하는 쿼리를 구현했습니다. issue #51
- 정확도 공식에 대한 테스트를 진행합니다. - ROLE 오타 정정에 대한 테스트를 진행합니다. - 정확도(acc)에 대한 자료형 변환에 대한 테스트를 진행합니다. issue #51
There was a problem hiding this comment.
Pull request overview
랭킹 도메인에서 Redis Sorted Set 기반 캐시(warmup + 조회 fallback)를 도입하고, 인증 ROLE 표기/정확도(acc) 관련 테스트를 함께 정리하려는 PR입니다.
Changes:
- Redis ZSET/Hash를 사용하는
RankingCacheService및 애플리케이션 시작 시 랭킹 warmup 로직 추가 - 랭킹 조회는 Redis 우선 조회 후 실패/미적재 시 DB fallback 하도록
RankingService수정, 타이핑 저장 시 캐시 갱신 로직 추가 - Spring Security ROLE 문자열/
hasAnyRole오타 수정 및 일부 정확도(acc) 테스트 보정
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/dasi/typing/domain/typing/TypingRepository.java | warmup용 “회원별 최고 기록” 조회 쿼리 추가 및 랭킹 계산 쿼리 변경 |
| src/main/java/dasi/typing/config/RedisConfig.java | RedisTemplate hash serializer 설정 추가 |
| src/main/java/dasi/typing/api/service/typing/TypingService.java | 회원 타이핑 저장 시 Redis 랭킹 캐시 갱신 + 랭킹 조회 방식 변경, ROLE 체크 수정 |
| src/main/java/dasi/typing/api/service/ranking/RankingWarmupInitializer.java | ApplicationReadyEvent 기반 랭킹 warmup 수행 컴포넌트 추가 |
| src/main/java/dasi/typing/api/service/ranking/RankingService.java | Redis 우선 조회 + 실패 시 DB fallback 로직 추가 |
| src/main/java/dasi/typing/api/service/ranking/RankingCacheService.java | Redis ZSET/Hash에 랭킹 저장/조회/월간키 처리 구현 추가 |
| src/main/java/dasi/typing/api/controller/typing/TypingController.java | hasAnyRole 오타 수정 |
| src/main/java/dasi/typing/api/controller/typing/request/TypingCreateRequest.java | Lombok Builder 구성 방식 변경 |
| src/main/java/dasi/typing/api/controller/ranking/response/RankingResponse.java | Timestamp null 안전 처리 및 캐시용 factory 메서드 추가 |
| src/test/java/dasi/typing/domain/typing/TypingTest.java | 정확도(acc) 입력값 변환(퍼센트→비율) 반영 |
| src/test/java/dasi/typing/domain/typing/TypingRepositoryTest.java | acc 타입 변경 반영 |
| src/test/java/dasi/typing/api/service/typing/TypingServiceTest.java | ROLE 접두어/acc 기대값 업데이트 |
| src/test/java/dasi/typing/api/service/ranking/RankingServiceTest.java | acc 타입 변경 반영 |
Comments suppressed due to low confidence (2)
src/test/java/dasi/typing/domain/typing/TypingRepositoryTest.java:261
Typing.acc는Typing#getPenaltyRate에서 0.90, 0.40 같은 임계값으로 비교하고 있어 0~1 범위의 '비율'로 해석됩니다. 여기서acc를 100 같은 퍼센트 값 그대로(double) acc로 넣으면 테스트가 페널티 로직을 제대로 검증하지 못하니, 퍼센트 입력을 쓰는 경우acc / 100.0로 정규화하거나 테스트 데이터 자체를 0.95 형태로 바꿔 주세요.
private Typing createTyping(int cpm, int acc, Member member, Phrase phrase) {
return Typing.builder()
.cpm(cpm)
.acc((double) acc)
.wpm(0)
.maxCpm(0)
.member(member)
.phrase(phrase).build();
src/test/java/dasi/typing/api/service/ranking/RankingServiceTest.java:118
Typing.acc는 0~1 범위의 정확도 비율로 사용되는데(0.90 등과 비교), 테스트에서(double) acc로 100을 넣으면 정확도 로직 검증이 왜곡됩니다. 퍼센트 입력이라면acc / 100.0로 맞추거나, 테스트 인자를 1.0 기준으로 조정해 주세요.
private Typing createTyping(int cpm, int acc, Member member, Phrase phrase) {
return Typing.builder()
.cpm(cpm)
.acc((double) acc)
.wpm(0)
.maxCpm(0)
.member(member)
.phrase(phrase).build();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| FROM typing t | ||
| WHERE (t.score, t.max_cpm, t.acc) > (:targetScore, :targetMaxCpm, :targetAcc) | ||
| """, nativeQuery = true) | ||
| Long findRanking(@Param("targetScore") int targetScore); |
There was a problem hiding this comment.
findRanking 네이티브 쿼리가 :targetMaxCpm, :targetAcc 파라미터를 참조하지만 메서드 시그니처에는 targetScore만 있어 실행 시 바인딩 오류가 발생합니다. 또한 COUNT(DISTINCT score,max_cpm,acc) 기반 계산은 동점 처리(created_date/id)와 ‘개별 기록’ 기준 랭킹을 깨뜨릴 수 있으니, 기존 정렬 기준( score DESC, maxCpm DESC, acc DESC, created_date ASC, id ASC )에 맞게 모든 파라미터를 포함한 쿼리/시그니처로 복구하거나 window function으로 rank를 계산해 주세요.
| Long findRanking(@Param("targetScore") int targetScore); | |
| Long findRanking( | |
| @Param("targetScore") int targetScore, | |
| @Param("targetMaxCpm") int targetMaxCpm, | |
| @Param("targetAcc") double targetAcc | |
| ); |
| log.warn("[Ranking] Redis 랭킹 업데이트 실패. 다음 Warmup에서 복구됩니다.", e); | ||
| } | ||
|
|
||
| rank = typingRepository.findRanking(savedTyping.getScore()); |
There was a problem hiding this comment.
findRanking(savedTyping.getScore())로 랭킹을 계산하면 maxCpm/acc/createdDate 같은 타이브레이커가 모두 무시됩니다(그리고 현재 Repository 쿼리도 score만으로 호출됩니다). 저장한 기록 기준의 정확한 순위를 반환해야 한다면 Typing의 score/maxCpm/acc/createdDate/id(또는 memberId)를 함께 전달해 동일한 정렬 기준으로 rank를 계산하도록 수정해 주세요.
| rank = typingRepository.findRanking(savedTyping.getScore()); | |
| rank = typingRepository.findRanking( | |
| savedTyping.getScore(), | |
| savedTyping.getMaxCpm(), | |
| savedTyping.getAcc(), | |
| savedTyping.getCreatedDate(), | |
| member.getId() | |
| ); |
| package dasi.typing.api.service.ranking; | ||
|
|
||
| import static dasi.typing.utils.ConstantUtil.RANKING_COUNT; | ||
|
|
||
| import dasi.typing.api.controller.ranking.response.RankingResponse; | ||
| import dasi.typing.domain.typing.TypingRepository; |
There was a problem hiding this comment.
ConstantUtil.RANKING_COUNT를 static import 하고 있는데, 현재 ConstantUtil에 해당 상수가 정의되어 있지 않아 컴파일이 실패합니다. ConstantUtil에 상수를 추가하거나(예: 50), 기존처럼 RankingService 내부 상수로 유지해 주세요.
| package dasi.typing.api.service.ranking; | ||
|
|
||
| import static dasi.typing.utils.ConstantUtil.RANKING_MEMBER_KEY; | ||
| import static dasi.typing.utils.ConstantUtil.RANKING_MEMBER_MONTHLY_KEY_PREFIX; | ||
| import static dasi.typing.utils.ConstantUtil.RANKING_MONTHLY_KEY_PREFIX; | ||
| import static dasi.typing.utils.ConstantUtil.RANKING_REALTIME_KEY; | ||
|
|
There was a problem hiding this comment.
ConstantUtil에 RANKING_REALTIME_KEY, RANKING_MONTHLY_KEY_PREFIX, RANKING_MEMBER_KEY, RANKING_MEMBER_MONTHLY_KEY_PREFIX가 정의되어 있지 않아 현재 코드가 컴파일되지 않습니다. 키 네이밍/TTL 전략에 맞춰 ConstantUtil에 상수를 추가하거나, 이 클래스 내부 상수로 선언해 주세요.
| List<Object[]> bestRecords = typingRepository.findAllBestRecordsForWarmup(); | ||
|
|
||
| if (bestRecords.isEmpty()) { | ||
| log.info("[Ranking] 타이핑 데이터가 없습니다. Warmup을 스킵합니다."); | ||
| return; | ||
| } | ||
|
|
||
| rankingCacheService.warmupRealtime(bestRecords); | ||
| rankingCacheService.warmupMonthly(bestRecords, YearMonth.now()); | ||
|
|
There was a problem hiding this comment.
월간 랭킹 warmup이 findAllBestRecordsForWarmup()(전체 기간 최고 기록 1개/회원) 결과를 그대로 사용해서, '이번 달에는 기록이 있지만 전체 최고 기록은 다른 달'인 회원이 월간 ZSET에서 누락될 수 있습니다. 월간 warmup은 월 범위로 필터링한 뒤 회원별 최고 기록을 조회하도록(예: start/end를 받는 별도 쿼리) 변경해 DB fallback과 동일한 기준을 유지해 주세요.
| .extracting("id", "cpm", "acc", "wpm") | ||
| .containsExactlyInAnyOrder( | ||
| tuple(savedTyping.getId(), 100, 100, 100) | ||
| tuple(savedTyping.getId(), 100, 100.0, 100) |
There was a problem hiding this comment.
이 테스트는 Typing.acc 값이 100.0(퍼센트)로 저장되는 것을 기대하고 있는데, 도메인 로직(Typing#getPenaltyRate)은 0~1 범위의 비율로 처리합니다. API/도메인에서 acc를 어떤 단위로 표준화할지 정한 뒤, 테스트 기대값과 요청 생성(acc(100))을 그 기준(예: 1.0 또는 0.95)으로 맞춰 주세요.
| tuple(savedTyping.getId(), 100, 100.0, 100) | |
| tuple(savedTyping.getId(), 100, 1.0, 100) |
#️⃣ 연관된 이슈
#51
📝 작업 내용
Feat
Refactor
Test
💬 리뷰 요구사항