Skip to content

Commit

Permalink
Refactor/#417 성능 개선을 위한 노래 데이터를 캐싱한다. (#418)
Browse files Browse the repository at this point in the history
* refactor: 노래 데이터 캐싱 정책 추가

* refactor: 테스트를 위한 schedule 시간 변경

* refactor: 메인 top100 노래 데이터 캐싱으로 변경

* refactor: 노래 데이터 캐싱하는 것 수정

세부사항
- cachedSong -> InMemorySongs으로 이름 변경
- util 클래스 -> repository 빈으로 수정

* test: 현재 완성되지 않는 테스트 제거

* test: InMemorySong 테스트 추가

* config: security 스냅샷 생성

* fix: 테스트, 로컬 InMemorySongsScheduler 시간 수정

* config: security 수정 설정 스냅샷 최신화

* config: InMemoryScheduler 시간 수정

* refactor: TODO 제거

---------

Co-authored-by: somsom13 <[email protected]>
Co-authored-by: somin <[email protected]>
Co-authored-by: Eunsol Kim <[email protected]>
  • Loading branch information
4 people authored Sep 20, 2023
1 parent 417c6cc commit 2583129
Show file tree
Hide file tree
Showing 17 changed files with 378 additions and 68 deletions.
2 changes: 2 additions & 0 deletions backend/src/main/java/shook/shook/ShookApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
public class ShookApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package shook.shook.song.application;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import shook.shook.song.domain.InMemorySongs;
import shook.shook.song.domain.repository.SongRepository;

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Component
public class InMemorySongsScheduler {

private final SongRepository songRepository;
private final InMemorySongs inMemorySongs;

@PostConstruct
public void initialize() {
recreateCachedSong();
}

@Scheduled(cron = "${schedules.in-memory-song.cron}")
public void recreateCachedSong() {
inMemorySongs.recreate(songRepository.findAllWithKillingParts());
}
}
69 changes: 15 additions & 54 deletions backend/src/main/java/shook/shook/song/application/SongService.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package shook.shook.song.application;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
Expand All @@ -18,11 +15,11 @@
import shook.shook.song.application.dto.SongSwipeResponse;
import shook.shook.song.application.dto.SongWithKillingPartsRegisterRequest;
import shook.shook.song.application.killingpart.dto.HighLikedSongResponse;
import shook.shook.song.domain.InMemorySongs;
import shook.shook.song.domain.Song;
import shook.shook.song.domain.SongTitle;
import shook.shook.song.domain.killingpart.repository.KillingPartRepository;
import shook.shook.song.domain.repository.SongRepository;
import shook.shook.song.domain.repository.dto.SongTotalLikeCountDto;
import shook.shook.song.exception.SongException;

@RequiredArgsConstructor
Expand All @@ -32,10 +29,12 @@ public class SongService {

private static final int AFTER_SONGS_COUNT = 10;
private static final int BEFORE_SONGS_COUNT = 10;
private static final int TOP_COUNT = 100;

private final SongRepository songRepository;
private final KillingPartRepository killingPartRepository;
private final MemberRepository memberRepository;
private final InMemorySongs inMemorySongs;
private final SongDataExcelReader songDataExcelReader;

@Transactional
Expand All @@ -55,63 +54,29 @@ private Song saveSong(final Song song) {
}

public List<HighLikedSongResponse> showHighLikedSongs() {
final List<SongTotalLikeCountDto> songsWithLikeCount = songRepository.findAllWithTotalLikeCount();
final List<Song> songs = inMemorySongs.getSongs();
final List<Song> top100Songs = songs.subList(0, Math.min(TOP_COUNT, songs.size()));

return HighLikedSongResponse.ofSongTotalLikeCounts(
sortByHighestLikeCountAndId(songsWithLikeCount)
);
}

private List<SongTotalLikeCountDto> sortByHighestLikeCountAndId(
final List<SongTotalLikeCountDto> songWithLikeCounts
) {
return songWithLikeCounts.stream()
.sorted(
Comparator.comparing(
SongTotalLikeCountDto::getTotalLikeCount,
Comparator.reverseOrder()
).thenComparing(dto -> dto.getSong().getId(), Comparator.reverseOrder())
).toList();
return HighLikedSongResponse.ofSongs(top100Songs);
}

public SongSwipeResponse findSongByIdForFirstSwipe(
final Long songId,
final MemberInfo memberInfo
) {
final Song currentSong = findSongById(songId);
final Song currentSong = inMemorySongs.getSongById(songId);

final List<Song> beforeSongs = findBeforeSongs(currentSong);
final List<Song> afterSongs = findAfterSongs(currentSong);
final List<Song> beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT);
final List<Song> afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT);

return convertToSongSwipeResponse(memberInfo, currentSong, beforeSongs, afterSongs);
}

private Song findSongById(final Long songId) {
return songRepository.findById(songId)
.orElseThrow(() -> new SongException.SongNotExistException(
Map.of("SongId", String.valueOf(songId))
));
}

private List<Song> findBeforeSongs(final Song song) {
final List<Song> result = songRepository.findSongsWithMoreLikeCountThanSongWithId(
song.getId(), PageRequest.of(0, BEFORE_SONGS_COUNT)
);

Collections.reverse(result);
return result;
}

private List<Song> findAfterSongs(final Song song) {
return songRepository.findSongsWithLessLikeCountThanSongWithId(
song.getId(), PageRequest.of(0, AFTER_SONGS_COUNT)
);
}

private SongSwipeResponse convertToSongSwipeResponse(
final MemberInfo memberInfo,
final Song currentSong,
final List<Song> beforeSongs, final List<Song> afterSongs
final List<Song> beforeSongs,
final List<Song> afterSongs
) {
final Authority authority = memberInfo.getAuthority();

Expand All @@ -137,10 +102,8 @@ public List<SongResponse> findSongByIdForBeforeSwipe(
final Long songId,
final MemberInfo memberInfo
) {
final Song currentSong = findSongById(songId);

final List<Song> beforeSongs =
findBeforeSongs(currentSong);
final Song currentSong = inMemorySongs.getSongById(songId);
final List<Song> beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT);

return convertToSongResponses(memberInfo, beforeSongs);
}
Expand Down Expand Up @@ -168,10 +131,8 @@ public List<SongResponse> findSongByIdForAfterSwipe(
final Long songId,
final MemberInfo memberInfo
) {
final Song currentSong = findSongById(songId);

final List<Song> afterSongs =
findAfterSongs(currentSong);
final Song currentSong = inMemorySongs.getSongById(songId);
final List<Song> afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT);

return convertToSongResponses(memberInfo, afterSongs);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package shook.shook.song.application.killingpart.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import shook.shook.song.domain.repository.dto.SongTotalLikeCountDto;
import shook.shook.song.domain.Song;
import java.util.List;

@Schema(description = "좋아요 순 노래 응답")
@AllArgsConstructor(access = AccessLevel.PRIVATE)
Expand All @@ -27,19 +27,17 @@ public class HighLikedSongResponse {
@Schema(description = "총 좋아요 개수", example = "40")
private final long totalLikeCount;

public static HighLikedSongResponse from(final SongTotalLikeCountDto songTotalVoteCountDto) {
private static HighLikedSongResponse from(final Song song) {
return new HighLikedSongResponse(
songTotalVoteCountDto.getSong().getId(),
songTotalVoteCountDto.getSong().getTitle(),
songTotalVoteCountDto.getSong().getSinger(),
songTotalVoteCountDto.getSong().getAlbumCoverUrl(),
songTotalVoteCountDto.getTotalLikeCount()
song.getId(),
song.getTitle(),
song.getSinger(),
song.getAlbumCoverUrl(),
song.getTotalLikeCount()
);
}

public static List<HighLikedSongResponse> ofSongTotalLikeCounts(
final List<SongTotalLikeCountDto> songs
) {
public static List<HighLikedSongResponse> ofSongs(final List<Song> songs) {
return songs.stream()
.map(HighLikedSongResponse::from)
.toList();
Expand Down
76 changes: 76 additions & 0 deletions backend/src/main/java/shook/shook/song/domain/InMemorySongs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package shook.shook.song.domain;

import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Repository;
import shook.shook.song.exception.SongException;

@Repository
public class InMemorySongs {

private Map<Long, Song> songsSortedInLikeCountById = new LinkedHashMap<>();

public void recreate(final List<Song> songs) {
songsSortedInLikeCountById = getSortedSong(songs);
}

private static Map<Long, Song> getSortedSong(final List<Song> songs) {
songs.sort(Comparator.comparing(
Song::getTotalLikeCount,
Comparator.reverseOrder()
).thenComparing(Song::getId, Comparator.reverseOrder()));

return songs.stream()
.collect(Collectors.toMap(
Song::getId,
song -> song,
(prev, update) -> update,
LinkedHashMap::new
));
}

public List<Song> getSongs() {
return songsSortedInLikeCountById.values()
.stream()
.toList();
}

public Song getSongById(final Long id) {
if (songsSortedInLikeCountById.containsKey(id)) {
return songsSortedInLikeCountById.get(id);
}
throw new SongException.SongNotExistException(
Map.of("song id", String.valueOf(id))
);
}

public List<Song> getPrevLikedSongs(final Song currentSong, final int prevSongCount) {
final List<Long> songIds = songsSortedInLikeCountById.keySet()
.stream()
.toList();
final int currentSongIndex = songIds.indexOf(currentSong.getId());

return songIds.subList(Math.max(0, currentSongIndex - prevSongCount), currentSongIndex).stream()
.map(songsSortedInLikeCountById::get)
.toList();
}

public List<Song> getNextLikedSongs(final Song currentSong, final int nextSongCount) {
final List<Long> songIds = songsSortedInLikeCountById.keySet().stream()
.toList();
final int currentSongIndex = songIds.indexOf(currentSong.getId());

if (currentSongIndex == songIds.size() - 1) {
return Collections.emptyList();
}

return songIds.subList(Math.min(currentSongIndex + 1, songIds.size() - 1), Math.min(songIds.size(), currentSongIndex + nextSongCount + 1))
.stream()
.map(songsSortedInLikeCountById::get)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,9 @@ public List<KillingPart> getKillingPartsSortedByLikeCount() {
.thenComparing(KillingPart::getStartSecond))
.toList();
}

public int getKillingPartsTotalLikeCount() {
return killingParts.stream()
.reduce(0, (sum, killingPart) -> sum + killingPart.getLikeCount(), Integer::sum);
}
}
4 changes: 4 additions & 0 deletions backend/src/main/java/shook/shook/song/domain/Song.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ public List<KillingPart> getLikeCountSortedKillingParts() {
return killingParts.getKillingPartsSortedByLikeCount();
}

public int getTotalLikeCount() {
return killingParts.getKillingPartsTotalLikeCount();
}

@Override
public boolean equals(final Object o) {
if (this == o) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public interface SongRepository extends JpaRepository<Song, Long> {
+ "GROUP BY s.id")
List<SongTotalLikeCountDto> findAllWithTotalLikeCount();

@Query("SELECT s AS song "
+ "FROM Song s "
+ "LEFT JOIN FETCH s.killingParts.killingParts kp "
+ "GROUP BY s.id, kp.id")
List<Song> findAllWithKillingParts();

@Query("SELECT s FROM Song s "
+ "LEFT JOIN s.killingParts.killingParts kp "
+ "GROUP BY s.id "
Expand Down
4 changes: 4 additions & 0 deletions backend/src/main/resources/application-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ excel:
video-url-delimiter: "="
killingpart-data-delimiter: " "
song-length-suffix: "s"

schedules:
in-memory-song:
cron: "0/1 * * * * *" # 1초
4 changes: 4 additions & 0 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ excel:
video-url-delimiter: "v="
killingpart-data-delimiter: " "
song-length-suffix: ""

schedules:
in-memory-song:
cron: "0 0 0/1 * * *" #1시간
2 changes: 1 addition & 1 deletion backend/src/main/resources/shook-security
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package shook.shook.song.application;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collections;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.test.context.jdbc.Sql;
import shook.shook.song.domain.InMemorySongs;

@Sql(value = "classpath:/killingpart/initialize_killing_part_song.sql")
@EnableScheduling
@SpringBootTest
class InMemorySongsSchedulerTest {

@Autowired
private InMemorySongs inMemorySongs;

@Autowired
private InMemorySongsScheduler scheduler;

@DisplayName("InMemorySongs 를 재생성한다.")
@Test
void recreateCachedSong() {
// given
// when
scheduler.recreateCachedSong();

// then
assertThat(inMemorySongs.getSongs()).hasSize(3);
}

@DisplayName("Scheduler 가 1초마다 실행된다.")
@Test
void schedule() throws InterruptedException {
// given
// when
inMemorySongs.recreate(Collections.emptyList());
Thread.sleep(1000);

// then
assertThat(inMemorySongs.getSongs()).hasSize(3);
}
}
Loading

0 comments on commit 2583129

Please sign in to comment.