관련 Issue & PR
기존 랭킹 시스템
랭킹 시스템 개발기기존 랭킹 시스템은 mysql rank()
에 의존해 구현했다.
사실 단순한 조회이기 때문에 설계상의 문제는 없다고 생각한다.
하지만, 페이지 사이즈 만큼 쿼리가 나가는 과제가 남았다.
923ms까지도 걸리는 레전드 쿼리다…
분석
현재 상황
랭킹 조회는 다음과 같은 과정으로 진행된다:
- SQL로 랭킹, 세션 Id 리스트 조회
- 세션 Id 마다 알맞은 게임 세션 정보를 찾아 매핑
- 응답
sessionId
를 AppleGame
객체로 매핑하는 과정에서 추가 쿼리가 발생한다.
고민해보기
View 사용
Join 사용
게임 캐싱하기
다시 처음으로 돌아가서 ‘게임 점수’라는 도메인을 다시 생각해보자.
게임은 한 번 끝나면 바뀌지 않는 도메인이며, 우리의 목적은 게임에 대한 쿼리를 제거하는 것이다.
게임 자체를 캐싱하면 어떨까?
한 번 랭킹을 조회한 후에는, 랭킹 요청 시 쿼리가 거의 필요하지 않다!
Spring Cache 추상화를 사용해서 캐싱해보자.
@Cacheable(value = "games", key = "#sessionId", unless = "!#result.done")
default AppleGame getBy(Long sessionId) {
return findById(sessionId).orElseThrow(NoSuchSessionException::new);
}
우선 조회된 게임들 중 끝난 게임은 모두 캐싱하도록 했고, 캐시 크기는 100 정도로 설정했다.
과정은 다음과 같다:
이로써 최근 랭킹은 모두 캐싱되며, 새로 추가된 랭킹 몇 개만 쿼리를 날리게 된다.
결과
- 첫 요청
- 두 번째 요청부터:
- 새로 기록을 새운 경우:
아직 캐싱된 내용이 없어 쿼리가 그대로 나간다.
두 번째 요청부터 준수한 성능을 보인다.
최소한의 쿼리만 발생한다 👍
우선 이렇게까지만 해보고, 랭킹 테이블을 운영할지는 뒤에 고민해보도록 하자.
랭킹 테이블 운영하기
랭킹이라는 테이블을 만들어 운영하는 방법도 있다.
조회 성능이 훨씬 개선될 것이다.
대신 게임 쓰기 성능이 살짝 감소할 수도 있다.
왜냐하면, 게임을 마칠 때마다 삽입이 발생하기 하기 때문이다.
랭킹 테이블을 만들지 않은 이유
다음과 같은 문제들이 있었다:
- 단순 조회이므로 해당 객체가 할 일이 딱히 없다.
- 지속적으로 최신화 해줘야 한다 → 비용 발생 + 최신 정보가 아닐 수 있다
- 50개가 딱 맞도록 유지해야 한다
- 순위를 계산하는 쿼리(
rank()
)는 결국 개선되지 않는다.