Skip to content

refactor: 랭킹 도메인 기능 리팩터링 #53

Merged
Bumnote merged 4 commits intodevfrom
refactor/ranking-architecture
Mar 22, 2026
Merged

refactor: 랭킹 도메인 기능 리팩터링 #53
Bumnote merged 4 commits intodevfrom
refactor/ranking-architecture

Conversation

@Bumnote
Copy link
Copy Markdown
Member

@Bumnote Bumnote commented Mar 22, 2026

#️⃣ 연관된 이슈

#51

📝 작업 내용

image
  • 연쇄 OR 연산으로 이루어진 단일 랭킹 쿼리를 DISTINCT 절을 활용하여 최적화했습니다.

Feat

  • Redis Sorted Set 활용을 위해서 RedisConfig 파일에 직렬화 설정을 추가했습니다.
  • 애플리케이션 첫 실행 이벤트 발생 시, DB에 있는 랭킹 데이터를 Redis ZSET으로 적재합니다.
  • Redis fallback 로직으로는 DB에서 데이터를 불러오도록 설계했습니다.
  • 회원인 경우에만 타이핑 결과 생성 시, Redis와 DB 모두에 데이터를 적재합니다.
  • 마이페이지에 사용될 회원별 최고 점수 및 최고 등수 데이터를 반환하는 쿼리를 구현했습니다.

Refactor

  • Controller 단에서 hasRole에 있는 오타를 수정했습니다.

Test

  • 정확도 공식에 대한 테스트를 진행합니다.
  • ROLE 오타 정정에 대한 테스트를 진행합니다.
  • 정확도(acc)에 대한 자료형 변환에 대한 테스트를 진행합니다.

💬 리뷰 요구사항

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요


Bumnote added 4 commits March 22, 2026 21:27
- 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
@Bumnote Bumnote requested a review from Copilot March 22, 2026 13:12
@Bumnote Bumnote self-assigned this Mar 22, 2026
@Bumnote Bumnote merged commit 69f1697 into dev Mar 22, 2026
2 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.accTyping#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);
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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를 계산해 주세요.

Suggested change
Long findRanking(@Param("targetScore") int targetScore);
Long findRanking(
@Param("targetScore") int targetScore,
@Param("targetMaxCpm") int targetMaxCpm,
@Param("targetAcc") double targetAcc
);

Copilot uses AI. Check for mistakes.
log.warn("[Ranking] Redis 랭킹 업데이트 실패. 다음 Warmup에서 복구됩니다.", e);
}

rank = typingRepository.findRanking(savedTyping.getScore());
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findRanking(savedTyping.getScore())로 랭킹을 계산하면 maxCpm/acc/createdDate 같은 타이브레이커가 모두 무시됩니다(그리고 현재 Repository 쿼리도 score만으로 호출됩니다). 저장한 기록 기준의 정확한 순위를 반환해야 한다면 Typing의 score/maxCpm/acc/createdDate/id(또는 memberId)를 함께 전달해 동일한 정렬 기준으로 rank를 계산하도록 수정해 주세요.

Suggested change
rank = typingRepository.findRanking(savedTyping.getScore());
rank = typingRepository.findRanking(
savedTyping.getScore(),
savedTyping.getMaxCpm(),
savedTyping.getAcc(),
savedTyping.getCreatedDate(),
member.getId()
);

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 6
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;
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstantUtil.RANKING_COUNT를 static import 하고 있는데, 현재 ConstantUtil에 해당 상수가 정의되어 있지 않아 컴파일이 실패합니다. ConstantUtil에 상수를 추가하거나(예: 50), 기존처럼 RankingService 내부 상수로 유지해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +7
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;

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConstantUtilRANKING_REALTIME_KEY, RANKING_MONTHLY_KEY_PREFIX, RANKING_MEMBER_KEY, RANKING_MEMBER_MONTHLY_KEY_PREFIX가 정의되어 있지 않아 현재 코드가 컴파일되지 않습니다. 키 네이밍/TTL 전략에 맞춰 ConstantUtil에 상수를 추가하거나, 이 클래스 내부 상수로 선언해 주세요.

Copilot uses AI. Check for mistakes.
Comment on lines +26 to +35
List<Object[]> bestRecords = typingRepository.findAllBestRecordsForWarmup();

if (bestRecords.isEmpty()) {
log.info("[Ranking] 타이핑 데이터가 없습니다. Warmup을 스킵합니다.");
return;
}

rankingCacheService.warmupRealtime(bestRecords);
rankingCacheService.warmupMonthly(bestRecords, YearMonth.now());

Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

월간 랭킹 warmup이 findAllBestRecordsForWarmup()(전체 기간 최고 기록 1개/회원) 결과를 그대로 사용해서, '이번 달에는 기록이 있지만 전체 최고 기록은 다른 달'인 회원이 월간 ZSET에서 누락될 수 있습니다. 월간 warmup은 월 범위로 필터링한 뒤 회원별 최고 기록을 조회하도록(예: start/end를 받는 별도 쿼리) 변경해 DB fallback과 동일한 기준을 유지해 주세요.

Copilot uses AI. Check for mistakes.
.extracting("id", "cpm", "acc", "wpm")
.containsExactlyInAnyOrder(
tuple(savedTyping.getId(), 100, 100, 100)
tuple(savedTyping.getId(), 100, 100.0, 100)
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 테스트는 Typing.acc 값이 100.0(퍼센트)로 저장되는 것을 기대하고 있는데, 도메인 로직(Typing#getPenaltyRate)은 0~1 범위의 비율로 처리합니다. API/도메인에서 acc를 어떤 단위로 표준화할지 정한 뒤, 테스트 기대값과 요청 생성(acc(100))을 그 기준(예: 1.0 또는 0.95)으로 맞춰 주세요.

Suggested change
tuple(savedTyping.getId(), 100, 100.0, 100)
tuple(savedTyping.getId(), 100, 1.0, 100)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants