From 2583129353cf66ae7e6c0f7a6c6411c31a227841 Mon Sep 17 00:00:00 2001 From: SeokHwan An <70303795+seokhwan-an@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:39:26 +0900 Subject: [PATCH] =?UTF-8?q?Refactor/#417=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=EC=9D=84=20=EC=9C=84=ED=95=9C=20=EB=85=B8=EB=9E=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20=EC=BA=90=EC=8B=B1?= =?UTF-8?q?=ED=95=9C=EB=8B=A4.=20(#418)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: somin <70891072+somsom13@users.noreply.github.com> Co-authored-by: Eunsol Kim <61370551+Cyma-s@users.noreply.github.com> --- .../java/shook/shook/ShookApplication.java | 2 + .../application/InMemorySongsScheduler.java | 28 ++++ .../shook/song/application/SongService.java | 69 +++------- .../dto/HighLikedSongResponse.java | 20 ++- .../shook/song/domain/InMemorySongs.java | 76 +++++++++++ .../shook/shook/song/domain/KillingParts.java | 5 + .../java/shook/shook/song/domain/Song.java | 4 + .../domain/repository/SongRepository.java | 6 + .../src/main/resources/application-test.yml | 4 + backend/src/main/resources/application.yml | 4 + backend/src/main/resources/shook-security | 2 +- .../InMemorySongsSchedulerTest.java | 47 +++++++ .../song/application/SongServiceTest.java | 14 +- .../shook/song/domain/InMemorySongsTest.java | 127 ++++++++++++++++++ .../domain/killingpart/KillingPartsTest.java | 22 +++ .../song/ui/HighLikedSongControllerTest.java | 7 +- .../shook/song/ui/SongControllerTest.java | 9 ++ 17 files changed, 378 insertions(+), 68 deletions(-) create mode 100644 backend/src/main/java/shook/shook/song/application/InMemorySongsScheduler.java create mode 100644 backend/src/main/java/shook/shook/song/domain/InMemorySongs.java create mode 100644 backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java create mode 100644 backend/src/test/java/shook/shook/song/domain/InMemorySongsTest.java diff --git a/backend/src/main/java/shook/shook/ShookApplication.java b/backend/src/main/java/shook/shook/ShookApplication.java index 6daf6e171..b4aa1aba9 100644 --- a/backend/src/main/java/shook/shook/ShookApplication.java +++ b/backend/src/main/java/shook/shook/ShookApplication.java @@ -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 { diff --git a/backend/src/main/java/shook/shook/song/application/InMemorySongsScheduler.java b/backend/src/main/java/shook/shook/song/application/InMemorySongsScheduler.java new file mode 100644 index 000000000..bb7f04a35 --- /dev/null +++ b/backend/src/main/java/shook/shook/song/application/InMemorySongsScheduler.java @@ -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()); + } +} diff --git a/backend/src/main/java/shook/shook/song/application/SongService.java b/backend/src/main/java/shook/shook/song/application/SongService.java index a5ce344e5..826b0ae81 100644 --- a/backend/src/main/java/shook/shook/song/application/SongService.java +++ b/backend/src/main/java/shook/shook/song/application/SongService.java @@ -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; @@ -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 @@ -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 @@ -55,63 +54,29 @@ private Song saveSong(final Song song) { } public List showHighLikedSongs() { - final List songsWithLikeCount = songRepository.findAllWithTotalLikeCount(); + final List songs = inMemorySongs.getSongs(); + final List top100Songs = songs.subList(0, Math.min(TOP_COUNT, songs.size())); - return HighLikedSongResponse.ofSongTotalLikeCounts( - sortByHighestLikeCountAndId(songsWithLikeCount) - ); - } - - private List sortByHighestLikeCountAndId( - final List 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 beforeSongs = findBeforeSongs(currentSong); - final List afterSongs = findAfterSongs(currentSong); + final List beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT); + final List 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 findBeforeSongs(final Song song) { - final List result = songRepository.findSongsWithMoreLikeCountThanSongWithId( - song.getId(), PageRequest.of(0, BEFORE_SONGS_COUNT) - ); - - Collections.reverse(result); - return result; - } - - private List 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 beforeSongs, final List afterSongs + final List beforeSongs, + final List afterSongs ) { final Authority authority = memberInfo.getAuthority(); @@ -137,10 +102,8 @@ public List findSongByIdForBeforeSwipe( final Long songId, final MemberInfo memberInfo ) { - final Song currentSong = findSongById(songId); - - final List beforeSongs = - findBeforeSongs(currentSong); + final Song currentSong = inMemorySongs.getSongById(songId); + final List beforeSongs = inMemorySongs.getPrevLikedSongs(currentSong, BEFORE_SONGS_COUNT); return convertToSongResponses(memberInfo, beforeSongs); } @@ -168,10 +131,8 @@ public List findSongByIdForAfterSwipe( final Long songId, final MemberInfo memberInfo ) { - final Song currentSong = findSongById(songId); - - final List afterSongs = - findAfterSongs(currentSong); + final Song currentSong = inMemorySongs.getSongById(songId); + final List afterSongs = inMemorySongs.getNextLikedSongs(currentSong, AFTER_SONGS_COUNT); return convertToSongResponses(memberInfo, afterSongs); } diff --git a/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java b/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java index 81377b81f..1342e4db7 100644 --- a/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java +++ b/backend/src/main/java/shook/shook/song/application/killingpart/dto/HighLikedSongResponse.java @@ -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) @@ -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 ofSongTotalLikeCounts( - final List songs - ) { + public static List ofSongs(final List songs) { return songs.stream() .map(HighLikedSongResponse::from) .toList(); diff --git a/backend/src/main/java/shook/shook/song/domain/InMemorySongs.java b/backend/src/main/java/shook/shook/song/domain/InMemorySongs.java new file mode 100644 index 000000000..65501638a --- /dev/null +++ b/backend/src/main/java/shook/shook/song/domain/InMemorySongs.java @@ -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 songsSortedInLikeCountById = new LinkedHashMap<>(); + + public void recreate(final List songs) { + songsSortedInLikeCountById = getSortedSong(songs); + } + + private static Map getSortedSong(final List 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 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 getPrevLikedSongs(final Song currentSong, final int prevSongCount) { + final List 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 getNextLikedSongs(final Song currentSong, final int nextSongCount) { + final List 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(); + } +} diff --git a/backend/src/main/java/shook/shook/song/domain/KillingParts.java b/backend/src/main/java/shook/shook/song/domain/KillingParts.java index ee3b794c5..5300cdbd4 100644 --- a/backend/src/main/java/shook/shook/song/domain/KillingParts.java +++ b/backend/src/main/java/shook/shook/song/domain/KillingParts.java @@ -61,4 +61,9 @@ public List getKillingPartsSortedByLikeCount() { .thenComparing(KillingPart::getStartSecond)) .toList(); } + + public int getKillingPartsTotalLikeCount() { + return killingParts.stream() + .reduce(0, (sum, killingPart) -> sum + killingPart.getLikeCount(), Integer::sum); + } } diff --git a/backend/src/main/java/shook/shook/song/domain/Song.java b/backend/src/main/java/shook/shook/song/domain/Song.java index 638b3ab1b..7343bdbc0 100644 --- a/backend/src/main/java/shook/shook/song/domain/Song.java +++ b/backend/src/main/java/shook/shook/song/domain/Song.java @@ -136,6 +136,10 @@ public List getLikeCountSortedKillingParts() { return killingParts.getKillingPartsSortedByLikeCount(); } + public int getTotalLikeCount() { + return killingParts.getKillingPartsTotalLikeCount(); + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java b/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java index dae4462c2..fca5fd529 100644 --- a/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java +++ b/backend/src/main/java/shook/shook/song/domain/repository/SongRepository.java @@ -18,6 +18,12 @@ public interface SongRepository extends JpaRepository { + "GROUP BY s.id") List findAllWithTotalLikeCount(); + @Query("SELECT s AS song " + + "FROM Song s " + + "LEFT JOIN FETCH s.killingParts.killingParts kp " + + "GROUP BY s.id, kp.id") + List findAllWithKillingParts(); + @Query("SELECT s FROM Song s " + "LEFT JOIN s.killingParts.killingParts kp " + "GROUP BY s.id " diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml index 69422dbfa..9f6b6246e 100644 --- a/backend/src/main/resources/application-test.yml +++ b/backend/src/main/resources/application-test.yml @@ -40,3 +40,7 @@ excel: video-url-delimiter: "=" killingpart-data-delimiter: " " song-length-suffix: "s" + +schedules: + in-memory-song: + cron: "0/1 * * * * *" # 1초 diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 4277ac4e3..60d063b31 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -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시간 diff --git a/backend/src/main/resources/shook-security b/backend/src/main/resources/shook-security index 30d5e0dbe..df2277d8f 160000 --- a/backend/src/main/resources/shook-security +++ b/backend/src/main/resources/shook-security @@ -1 +1 @@ -Subproject commit 30d5e0dbed67a9255a4d2674648c4f1cba526ebf +Subproject commit df2277d8f06fecff4e9836a14adccb908149c76a diff --git a/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java b/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java new file mode 100644 index 000000000..15632ac7b --- /dev/null +++ b/backend/src/test/java/shook/shook/song/application/InMemorySongsSchedulerTest.java @@ -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); + } +} diff --git a/backend/src/test/java/shook/shook/song/application/SongServiceTest.java b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java index fc3fa48bb..cf19ca79c 100644 --- a/backend/src/test/java/shook/shook/song/application/SongServiceTest.java +++ b/backend/src/test/java/shook/shook/song/application/SongServiceTest.java @@ -20,6 +20,7 @@ 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.killingpart.KillingPart; import shook.shook.song.domain.killingpart.KillingPartLike; @@ -43,6 +44,9 @@ class SongServiceTest extends UsingJpaTest { @Autowired private MemberRepository memberRepository; + + private InMemorySongs inMemorySongs = new InMemorySongs(); + private SongService songService; @BeforeEach @@ -51,6 +55,7 @@ public void setUp() { songRepository, killingPartRepository, memberRepository, + inMemorySongs, new SongDataExcelReader(" ", " ", " ") ); } @@ -93,6 +98,7 @@ void findById_exist_login_member() { final Member member = createAndSaveMember("email@naver.com", "email"); final Song song = registerNewSong("title"); addLikeToEachKillingParts(song, member); + inMemorySongs.recreate(songRepository.findAllWithKillingParts()); //when saveAndClearEntityManager(); @@ -129,8 +135,9 @@ void findById_exist_login_member() { void findById_exist_not_login_member() { //given final Song song = registerNewSong("title"); + inMemorySongs.recreate(songRepository.findAllWithKillingParts()); - //when + //when인 saveAndClearEntityManager(); final SongSwipeResponse response = songService.findSongByIdForFirstSwipe(song.getId(), @@ -165,6 +172,7 @@ void findById_exist_not_login_member() { void findById_notExist() { //given final Member member = createAndSaveMember("email@naver.com", "email"); + //when //then assertThatThrownBy(() -> songService.findSongByIdForFirstSwipe( @@ -192,6 +200,7 @@ void showHighLikedSongs() { addLikeToEachKillingParts(fourthSong, member1); saveAndClearEntityManager(); + inMemorySongs.recreate(songRepository.findAllWithKillingParts()); // when final List result = songService.showHighLikedSongs(); @@ -260,6 +269,7 @@ void firstFindByMember() { addLikeToEachKillingParts(thirdSong, member); addLikeToEachKillingParts(fourthSong, member); + inMemorySongs.recreate(songRepository.findAllWithKillingParts()); saveAndClearEntityManager(); @@ -323,6 +333,7 @@ void findSongByIdForBeforeSwipe() { addLikeToEachKillingParts(fourthSong, member2); addLikeToEachKillingParts(firstSong, member2); + inMemorySongs.recreate(songRepository.findAllWithKillingParts()); // 정렬 순서: 2L, 4L, 1L, 5L, 3L saveAndClearEntityManager(); @@ -355,6 +366,7 @@ void findSongByIdForAfterSwipe() { addLikeToEachKillingParts(secondSong, member2); addLikeToEachKillingParts(standardSong, member2); addLikeToEachKillingParts(firstSong, member2); + inMemorySongs.recreate(songRepository.findAllWithKillingParts()); // 정렬 순서: 2L, 4L, 1L, 5L, 3L diff --git a/backend/src/test/java/shook/shook/song/domain/InMemorySongsTest.java b/backend/src/test/java/shook/shook/song/domain/InMemorySongsTest.java new file mode 100644 index 000000000..78b9f8d5e --- /dev/null +++ b/backend/src/test/java/shook/shook/song/domain/InMemorySongsTest.java @@ -0,0 +1,127 @@ +package shook.shook.song.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; +import shook.shook.member.domain.Member; +import shook.shook.member.domain.repository.MemberRepository; +import shook.shook.song.domain.killingpart.KillingPart; +import shook.shook.song.domain.killingpart.KillingPartLike; +import shook.shook.song.domain.repository.SongRepository; +import shook.shook.support.UsingJpaTest; + +@Sql("classpath:/killingpart/initialize_killing_part_song.sql") +class InMemorySongsTest extends UsingJpaTest { + + private static Member MEMBER; + + private InMemorySongs inMemorySongs; + + @Autowired + private SongRepository songRepository; + + @Autowired + private MemberRepository memberRepository; + + @BeforeEach + void setUp() { + MEMBER = memberRepository.findById(1L).get(); + inMemorySongs = new InMemorySongs(); + } + + + @DisplayName("InMemorySong 을 1.좋아요 순, 2. id 순으로 정렬된 노래로 초기화한다.") + @Test + void recreate() { + // given + final List songs = songRepository.findAllWithKillingParts(); + likeAllKillingPartsInSong(songs.get(0)); + likeAllKillingPartsInSong(songs.get(1)); + + // when + inMemorySongs.recreate(songs); + // 정렬 순서: 2L, 1L, 3L + + // then + final List songsInMemory = inMemorySongs.getSongs(); + assertAll( + () -> assertThat(songsInMemory.get(0)).hasFieldOrPropertyWithValue("id", 2L), + () -> assertThat(songsInMemory.get(1)).hasFieldOrPropertyWithValue("id", 1L), + () -> assertThat(songsInMemory.get(2)).hasFieldOrPropertyWithValue("id", 3L) + ); + } + + private void likeAllKillingPartsInSong(final Song song) { + final List killingParts = song.getKillingParts(); + for (final KillingPart killingPart : killingParts) { + killingPart.like(new KillingPartLike(killingPart, MEMBER)); + } + } + + @DisplayName("노래 id 로 Song 을 조회한다.") + @Test + void getSongById() { + // given + final List songs = songRepository.findAllWithKillingParts(); + inMemorySongs.recreate(songs); + + // when + final Song foundSong = inMemorySongs.getSongById(3L); + + // then + final Song expectedSong = songs.get(0); + assertThat(foundSong).isEqualTo(expectedSong); + } + + @DisplayName("특정 노래에 대해 1. 좋아요 수가 더 적거나 2. 좋아요 수가 같은 경우 id가 더 작은 노래 목록을 조회한다.") + @Test + void getPrevLikedSongs() { + // given + final List songs = songRepository.findAllWithKillingParts(); + final Song firstSong = songs.get(0); + final Song secondSong = songs.get(1); + final Song thirdSong = songs.get(2); + likeAllKillingPartsInSong(firstSong); + likeAllKillingPartsInSong(secondSong); + inMemorySongs.recreate(songs); // second, first, third + + // when + final List prevLikedSongs = inMemorySongs.getPrevLikedSongs(thirdSong, 2); + + // then + assertAll( + () -> assertThat(prevLikedSongs).hasSize(2), + () -> assertThat(prevLikedSongs.get(0)).isEqualTo(secondSong), + () -> assertThat(prevLikedSongs.get(1)).isEqualTo(firstSong) + ); + } + + @DisplayName("특정 노래에 대해 1. 좋아요 수가 더 많거나 2. 좋아요 수가 같은 경우 id가 더 큰 노래 목록을 조회한다.") + @Test + void getNextLikedSongs() { + // given + final List songs = songRepository.findAllWithKillingParts(); + final Song firstSong = songs.get(0); + final Song secondSong = songs.get(1); + final Song thirdSong = songs.get(2); + likeAllKillingPartsInSong(firstSong); + likeAllKillingPartsInSong(secondSong); + inMemorySongs.recreate(songs); // second, first, third + + // when + final List nextLikedSongs = inMemorySongs.getNextLikedSongs(secondSong, 2); + + // then + assertAll( + () -> assertThat(nextLikedSongs).hasSize(2), + () -> assertThat(nextLikedSongs.get(0)).isEqualTo(firstSong), + () -> assertThat(nextLikedSongs.get(1)).isEqualTo(thirdSong) + ); + } +} diff --git a/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartsTest.java b/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartsTest.java index 7ed0e7cd3..1538ddc76 100644 --- a/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartsTest.java +++ b/backend/src/test/java/shook/shook/song/domain/killingpart/KillingPartsTest.java @@ -102,4 +102,26 @@ void getKillingPartsSortedByLikeCount() { () -> assertThat(result.get(2).getId()).isEqualTo(3L) ); } + + @DisplayName("한 킬링파트 묶음의 총 좋아요 개수를 계산한다.") + @Test + void getTotalLikeCount() { + // given + final Member member = new Member("email@naver.com", "nickname"); + final KillingPart killingPart1 = KillingPart.saved(1L, 15, PartLength.SHORT, EMPTY_SONG); + final KillingPart killingPart2 = KillingPart.saved(2L, 10, PartLength.SHORT, EMPTY_SONG); + final KillingPart killingPart3 = KillingPart.saved(3L, 20, PartLength.SHORT, EMPTY_SONG); + + killingPart1.like(new KillingPartLike(killingPart1, member)); + killingPart2.like(new KillingPartLike(killingPart2, member)); + + final KillingParts killingParts = new KillingParts(List.of(killingPart1, killingPart3, + killingPart2)); + + // when + final int totalLikeCount = killingParts.getKillingPartsTotalLikeCount(); + + // then + assertThat(totalLikeCount).isEqualTo(2); + } } diff --git a/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java b/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java index b9ceaaf8e..cf1cd594a 100644 --- a/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/HighLikedSongControllerTest.java @@ -14,11 +14,11 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.test.context.jdbc.Sql; +import shook.shook.song.application.InMemorySongsScheduler; import shook.shook.song.application.killingpart.KillingPartLikeService; import shook.shook.song.application.killingpart.dto.HighLikedSongResponse; import shook.shook.song.application.killingpart.dto.KillingPartLikeRequest; - @Sql("classpath:/killingpart/initialize_killing_part_song.sql") @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class HighLikedSongControllerTest { @@ -42,6 +42,9 @@ void setUp() { @Autowired private KillingPartLikeService likeService; + @Autowired + private InMemorySongsScheduler inMemorySongsScheduler; + @DisplayName("좋아요 많은 순으로 노래 목록 조회 시 200 상태코드, 좋아요 순으로 정렬된 노래 목록이 반환된다.") @Test void showHighLikedSongs() { @@ -53,6 +56,8 @@ void showHighLikedSongs() { likeService.updateLikeStatus(SECOND_SONG_KILLING_PART_ID_1, MEMBER_ID, new KillingPartLikeRequest(true)); + inMemorySongsScheduler.recreateCachedSong(); + //when final List responses = RestAssured.given().log().all() .when().log().all() diff --git a/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java b/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java index a15a28e5e..a4f8a3d68 100644 --- a/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java +++ b/backend/src/test/java/shook/shook/song/ui/SongControllerTest.java @@ -13,6 +13,7 @@ import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.test.context.jdbc.Sql; +import shook.shook.song.application.InMemorySongsScheduler; import shook.shook.song.application.dto.SongResponse; import shook.shook.song.application.dto.SongSwipeResponse; import shook.shook.song.application.killingpart.KillingPartLikeService; @@ -38,6 +39,9 @@ void setUp() { @Autowired private KillingPartLikeService likeService; + @Autowired + private InMemorySongsScheduler inMemorySongsScheduler; + @DisplayName("노래 정보 처음 조회할 때, 가운데 노래를 기준으로 조회한 경우 200 상태코드, 현재 노래, 이전 / 이후 노래 리스트를 반환한다.") @Test void showSongById() { @@ -50,6 +54,8 @@ void showSongById() { likeService.updateLikeStatus(SECOND_SONG_KILLING_PART_ID_1, MEMBER_ID, new KillingPartLikeRequest(true)); + inMemorySongsScheduler.recreateCachedSong(); + //when final SongSwipeResponse response = RestAssured.given().log().all() .param("memberId", MEMBER_ID) @@ -76,6 +82,8 @@ void showSongsBeforeSongWithId() { likeService.updateLikeStatus(FIRST_SONG_KILLING_PART_ID_2, MEMBER_ID, new KillingPartLikeRequest(true)); + inMemorySongsScheduler.recreateCachedSong(); + //when final List response = RestAssured.given().log().all() .param("memberId", MEMBER_ID) @@ -99,6 +107,7 @@ void showSongsBeforeSongWithId() { void showSongsAfterSongWithId() { // given final Long songId = 3L; + inMemorySongsScheduler.recreateCachedSong(); //when final List response = RestAssured.given().log().all()