From 4f763ed65bb8ba27d510bc828aa50d692f2f0cc9 Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Fri, 26 Jun 2026 23:53:42 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EA=B3=B5=ED=86=B5=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EB=AA=A9=EB=A1=9D=EC=9D=98=20=EC=98=A4=EB=9E=98?= =?UTF-8?q?=EB=90=9C=20=EC=9D=B8=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=BA=90?= =?UTF-8?q?=EC=8B=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GeneralUnivApplyInfoRecommendService.java | 31 ++++--- .../UnivApplyInfoRecommendService.java | 36 +++++--- ...eralUnivApplyInfoRecommendServiceTest.java | 88 +++++++++++++++---- .../UnivApplyInfoRecommendServiceTest.java | 11 +-- 4 files changed, 118 insertions(+), 48 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java b/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java index 18978aa2c..511ceb259 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java @@ -2,36 +2,41 @@ import static com.example.solidconnection.university.service.UnivApplyInfoRecommendService.RECOMMEND_UNIV_APPLY_INFO_NUM; -import com.example.solidconnection.term.repository.TermRepository; +import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.university.domain.UnivApplyInfo; +import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse; +import com.example.solidconnection.university.dto.UnivApplyInfoRecommendsResponse; import com.example.solidconnection.university.repository.UnivApplyInfoRepository; import java.util.List; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor public class GeneralUnivApplyInfoRecommendService { + private static final long GENERAL_RECOMMEND_CACHE_TTL_SEC = 3600; + /* * 해당 시기에 열리는 대학교들 중 랜덤으로 선택해서 목록을 구성한다. * */ private final UnivApplyInfoRepository univApplyInfoRepository; - private final TermRepository termRepository; - @Getter - private List generalRecommends; + @Transactional(readOnly = true) + @ThunderingHerdCaching( + key = "university:recommend:general:{0}", + cacheManager = "customCacheManager", + ttlSec = GENERAL_RECOMMEND_CACHE_TTL_SEC + ) + public UnivApplyInfoRecommendsResponse getGeneralRecommends(long termId, String termName) { + Pageable page = PageRequest.of(0, RECOMMEND_UNIV_APPLY_INFO_NUM); + List generalRecommends = univApplyInfoRepository.findRandomByTermId(termId, page); - @EventListener(ApplicationReadyEvent.class) - public void init() { - termRepository.findByIsCurrentTrue().ifPresent(term -> { - Pageable page = PageRequest.of(0, RECOMMEND_UNIV_APPLY_INFO_NUM); - generalRecommends = univApplyInfoRepository.findRandomByTermId(term.getId(), page); - }); + return new UnivApplyInfoRecommendsResponse(generalRecommends.stream() + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, termName)) + .toList()); } } diff --git a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java index 1ce568a71..57f1fe8b2 100644 --- a/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendService.java @@ -2,7 +2,6 @@ import static com.example.solidconnection.common.exception.ErrorCode.CURRENT_TERM_NOT_FOUND; -import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.repository.TermRepository; @@ -47,7 +46,7 @@ public UnivApplyInfoRecommendsResponse getPersonalRecommends(long siteUserId) { // 맞춤 추천 대학교의 수가 6개보다 적다면, 일반 추천 대학교를 부족한 수 만큼 불러온다. if (trimmedRecommends.size() < RECOMMEND_UNIV_APPLY_INFO_NUM) { - trimmedRecommends.addAll(getGeneralRecommendsExcludingSelected(trimmedRecommends)); + return new UnivApplyInfoRecommendsResponse(getPersonalRecommendPreviewsWithGeneralFallback(trimmedRecommends, term)); } return new UnivApplyInfoRecommendsResponse(trimmedRecommends.stream() @@ -55,9 +54,29 @@ public UnivApplyInfoRecommendsResponse getPersonalRecommends(long siteUserId) { .toList()); } - private List getGeneralRecommendsExcludingSelected(List alreadyPicked) { - List generalRecommend = new ArrayList<>(generalUnivApplyInfoRecommendService.getGeneralRecommends()); - generalRecommend.removeAll(alreadyPicked); + private List getPersonalRecommendPreviewsWithGeneralFallback( + List personalRecommends, + Term term + ) { + List recommendPreviews = new ArrayList<>(personalRecommends.stream() + .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) + .toList()); + recommendPreviews.addAll(getGeneralRecommendsExcludingSelected(personalRecommends, term)); + + return recommendPreviews; + } + + private List getGeneralRecommendsExcludingSelected( + List alreadyPicked, + Term term + ) { + List alreadyPickedIds = alreadyPicked.stream() + .map(UnivApplyInfo::getId) + .toList(); + List generalRecommend = new ArrayList<>(generalUnivApplyInfoRecommendService + .getGeneralRecommends(term.getId(), term.getName()) + .recommendedUniversities()); + generalRecommend.removeIf(univApplyInfo -> alreadyPickedIds.contains(univApplyInfo.id())); Collections.shuffle(generalRecommend); int needed = RECOMMEND_UNIV_APPLY_INFO_NUM - alreadyPicked.size(); return generalRecommend.subList(0, Math.min(needed, generalRecommend.size())); @@ -67,15 +86,10 @@ private List getGeneralRecommendsExcludingSelected(List new CustomException(CURRENT_TERM_NOT_FOUND)); - List generalRecommends = new ArrayList<>(generalUnivApplyInfoRecommendService.getGeneralRecommends()); - - return new UnivApplyInfoRecommendsResponse(generalRecommends.stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) - .toList()); + return generalUnivApplyInfoRecommendService.getGeneralRecommends(term.getId(), term.getName()); } } diff --git a/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java index 7bf901a0a..90f87872a 100644 --- a/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java @@ -4,11 +4,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; +import com.example.solidconnection.cache.manager.CustomCacheManager; import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.term.domain.Term; import com.example.solidconnection.term.fixture.TermFixture; import com.example.solidconnection.university.domain.UnivApplyInfo; +import com.example.solidconnection.university.dto.UnivApplyInfoPreviewResponse; +import com.example.solidconnection.university.dto.UnivApplyInfoRecommendsResponse; import com.example.solidconnection.university.fixture.UnivApplyInfoFixture; +import com.example.solidconnection.university.repository.UnivApplyInfoRepository; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -25,39 +29,89 @@ class GeneralUnivApplyInfoRecommendServiceTest { @Autowired private UnivApplyInfoFixture univApplyInfoFixture; + @Autowired + private UnivApplyInfoRepository univApplyInfoRepository; + + @Autowired + private CustomCacheManager cacheManager; + @Autowired private TermFixture termFixture; private Term term; + private List currentTermUnivApplyInfos; @BeforeEach void setUp() { term = termFixture.현재_학기("2025-2"); - univApplyInfoFixture.괌대학_A_지원_정보(term.getId()); - univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()); - univApplyInfoFixture.네바다주립대학_라스베이거스_지원_정보(term.getId()); - univApplyInfoFixture.메모리얼대학_세인트존스_A_지원_정보(term.getId()); - univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId()); - univApplyInfoFixture.코펜하겐IT대학_지원_정보(term.getId()); - univApplyInfoFixture.그라츠대학_지원_정보(term.getId()); - univApplyInfoFixture.그라츠공과대학_지원_정보(term.getId()); - univApplyInfoFixture.린츠_카톨릭대학_지원_정보(term.getId()); - univApplyInfoFixture.메이지대학_지원_정보(term.getId()); - generalUnivApplyInfoRecommendService.init(); + currentTermUnivApplyInfos = List.of( + univApplyInfoFixture.괌대학_A_지원_정보(term.getId()), + univApplyInfoFixture.버지니아공과대학_지원_정보(term.getId()), + univApplyInfoFixture.네바다주립대학_라스베이거스_지원_정보(term.getId()), + univApplyInfoFixture.메모리얼대학_세인트존스_A_지원_정보(term.getId()), + univApplyInfoFixture.서던덴마크대학교_지원_정보(term.getId()), + univApplyInfoFixture.코펜하겐IT대학_지원_정보(term.getId()) + ); } @Test void 모집_시기의_대학_지원_정보_중에서_랜덤하게_N개를_추천_목록으로_구성한다() { + // when + UnivApplyInfoRecommendsResponse response = generalUnivApplyInfoRecommendService.getGeneralRecommends( + term.getId(), + term.getName() + ); + List currentTermUnivApplyInfoIds = currentTermUnivApplyInfos.stream() + .map(UnivApplyInfo::getId) + .toList(); + + // then + assertAll( + () -> assertThat(response.recommendedUniversities()).hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM), + () -> assertThat(response.recommendedUniversities()) + .extracting(UnivApplyInfoPreviewResponse::id) + .isSubsetOf(currentTermUnivApplyInfoIds) + ); + } + + @Test + void 캐시를_삭제하면_서버_재기동_없이_DB_기준으로_추천_목록을_다시_구성한다() { // given - List universities = generalUnivApplyInfoRecommendService.getGeneralRecommends(); + UnivApplyInfo deletedUnivApplyInfo = currentTermUnivApplyInfos.get(0); + UnivApplyInfoRecommendsResponse cachedResponse = generalUnivApplyInfoRecommendService.getGeneralRecommends( + term.getId(), + term.getName() + ); + + univApplyInfoRepository.delete(deletedUnivApplyInfo); + UnivApplyInfo addedUnivApplyInfo = univApplyInfoFixture.그라츠대학_지원_정보(term.getId()); + + // when + UnivApplyInfoRecommendsResponse responseBeforeCacheEvict = generalUnivApplyInfoRecommendService.getGeneralRecommends( + term.getId(), + term.getName() + ); + cacheManager.evict("university:recommend:general:" + term.getId()); + UnivApplyInfoRecommendsResponse responseAfterCacheEvict = generalUnivApplyInfoRecommendService.getGeneralRecommends( + term.getId(), + term.getName() + ); - // when & then + // then assertAll( - () -> assertThat(universities) - .extracting("termId") - .allMatch(term.getId()::equals), - () -> assertThat(universities).hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) + () -> assertThat(cachedResponse.recommendedUniversities()) + .extracting(UnivApplyInfoPreviewResponse::id) + .contains(deletedUnivApplyInfo.getId()), + () -> assertThat(responseBeforeCacheEvict.recommendedUniversities()) + .extracting(UnivApplyInfoPreviewResponse::id) + .contains(deletedUnivApplyInfo.getId()) + .doesNotContain(addedUnivApplyInfo.getId()), + () -> assertThat(responseAfterCacheEvict.recommendedUniversities()) + .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) + .extracting(UnivApplyInfoPreviewResponse::id) + .contains(addedUnivApplyInfo.getId()) + .doesNotContain(deletedUnivApplyInfo.getId()) ); } } diff --git a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java index f6b425e3f..d67b0a922 100644 --- a/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UnivApplyInfoRecommendServiceTest.java @@ -78,7 +78,6 @@ void setUp() { univApplyInfoFixture.그라츠공과대학_지원_정보(term.getId()); univApplyInfoFixture.린츠_카톨릭대학_지원_정보(term.getId()); univApplyInfoFixture.메이지대학_지원_정보(term.getId()); - generalUnivApplyInfoRecommendService.init(); } @Test @@ -148,9 +147,8 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsExactlyInAnyOrderElementsOf( - generalUnivApplyInfoRecommendService.getGeneralRecommends().stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) - .toList() + generalUnivApplyInfoRecommendService.getGeneralRecommends(term.getId(), term.getName()) + .recommendedUniversities() ); } @@ -163,9 +161,8 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIV_APPLY_INFO_NUM) .containsExactlyInAnyOrderElementsOf( - generalUnivApplyInfoRecommendService.getGeneralRecommends().stream() - .map(univApplyInfo -> UnivApplyInfoPreviewResponse.of(univApplyInfo, term.getName())) - .toList() + generalUnivApplyInfoRecommendService.getGeneralRecommends(term.getId(), term.getName()) + .recommendedUniversities() ); } } From 30c579bce293da72f59ff04683f20de760ea0d0e Mon Sep 17 00:00:00 2001 From: Hexeong <123macanic@naver.com> Date: Sat, 27 Jun 2026 00:51:10 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=EA=B3=B5=ED=86=B5=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EC=BA=90=EC=8B=9C=20=ED=82=A4=EC=97=90=20=ED=95=99?= =?UTF-8?q?=EA=B8=B0=EB=AA=85=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/GeneralUnivApplyInfoRecommendService.java | 2 +- .../service/GeneralUnivApplyInfoRecommendServiceTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java b/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java index 511ceb259..3b2f4e3ed 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendService.java @@ -27,7 +27,7 @@ public class GeneralUnivApplyInfoRecommendService { @Transactional(readOnly = true) @ThunderingHerdCaching( - key = "university:recommend:general:{0}", + key = "university:recommend:general:{0}:{1}", cacheManager = "customCacheManager", ttlSec = GENERAL_RECOMMEND_CACHE_TTL_SEC ) diff --git a/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java index 90f87872a..7bc40a266 100644 --- a/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/GeneralUnivApplyInfoRecommendServiceTest.java @@ -44,6 +44,7 @@ class GeneralUnivApplyInfoRecommendServiceTest { @BeforeEach void setUp() { term = termFixture.현재_학기("2025-2"); + cacheManager.evictUsingPrefix("university:recommend:general:" + term.getId()); currentTermUnivApplyInfos = List.of( univApplyInfoFixture.괌대학_A_지원_정보(term.getId()), @@ -92,7 +93,7 @@ void setUp() { term.getId(), term.getName() ); - cacheManager.evict("university:recommend:general:" + term.getId()); + cacheManager.evictUsingPrefix("university:recommend:general:" + term.getId()); UnivApplyInfoRecommendsResponse responseAfterCacheEvict = generalUnivApplyInfoRecommendService.getGeneralRecommends( term.getId(), term.getName()