diff --git a/build.gradle b/build.gradle index 8ed3a25..35953d4 100644 --- a/build.gradle +++ b/build.gradle @@ -44,6 +44,8 @@ dependencies { implementation 'org.springframework.retry:spring-retry' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE' + implementation 'org.springframework.boot:spring-boot-starter-cache' + implementation 'com.github.ben-manes.caffeine:caffeine' /* Swagger */ implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.4.0' diff --git a/src/main/java/com/planetrush/planetrush/PlanetrushApplication.java b/src/main/java/com/planetrush/planetrush/PlanetrushApplication.java index 1a8ebeb..238ea48 100644 --- a/src/main/java/com/planetrush/planetrush/PlanetrushApplication.java +++ b/src/main/java/com/planetrush/planetrush/PlanetrushApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.scheduling.annotation.EnableScheduling; +@EnableCaching @EnableScheduling @SpringBootApplication public class PlanetrushApplication { diff --git a/src/main/java/com/planetrush/planetrush/core/config/AppConfig.java b/src/main/java/com/planetrush/planetrush/core/config/AppConfig.java index 52e2fd3..ca530c5 100644 --- a/src/main/java/com/planetrush/planetrush/core/config/AppConfig.java +++ b/src/main/java/com/planetrush/planetrush/core/config/AppConfig.java @@ -1,11 +1,17 @@ package com.planetrush.planetrush.core.config; +import java.util.concurrent.TimeUnit; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.retry.annotation.EnableRetry; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.client.RestTemplate; +import com.github.benmanes.caffeine.cache.Caffeine; + @EnableAsync @EnableRetry @Configuration @@ -16,4 +22,12 @@ public RestTemplate restTemplate() { return new RestTemplate(); } + @Bean + public CacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager("challengeProgressAvg"); + cacheManager.setCaffeine( + Caffeine.newBuilder().expireAfterWrite(24, TimeUnit.HOURS).recordStats() + ); + return cacheManager; + } } diff --git a/src/main/java/com/planetrush/planetrush/member/service/MemberServiceImpl.java b/src/main/java/com/planetrush/planetrush/member/service/MemberServiceImpl.java index 0498a1a..554c005 100644 --- a/src/main/java/com/planetrush/planetrush/member/service/MemberServiceImpl.java +++ b/src/main/java/com/planetrush/planetrush/member/service/MemberServiceImpl.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.stream.Collectors; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -53,7 +54,7 @@ public List getPlanetCollections(CollectionSearchCond searc *

이 메서드는 반환값을 캐싱하여 관리합니다.

*

캐시 미스가 발생하는 경우에만 플라스크 서버로 API 요청을 전송하여 새로운 데이터로 캐시에 저장합니다.

*/ - // TODO: 카페인 캐시를 이용하도록 변경 + @Cacheable(cacheNames = "challengeProgressAvg", key = "#memberId") @Override public GetMyProgressAvgDto getMyProgressAvgPer(Long memberId) { Member member = memberRepository.findById(memberId) diff --git a/src/test/java/com/planetrush/planetrush/ServiceUnitTest.java b/src/test/java/com/planetrush/planetrush/ServiceUnitTest.java new file mode 100644 index 0000000..3464eb3 --- /dev/null +++ b/src/test/java/com/planetrush/planetrush/ServiceUnitTest.java @@ -0,0 +1,8 @@ +package com.planetrush.planetrush; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public abstract class ServiceUnitTest { +} diff --git a/src/test/java/com/planetrush/planetrush/member/MemberIntegrationTest.java b/src/test/java/com/planetrush/planetrush/member/MemberIntegrationTest.java new file mode 100644 index 0000000..9a6f0aa --- /dev/null +++ b/src/test/java/com/planetrush/planetrush/member/MemberIntegrationTest.java @@ -0,0 +1,88 @@ +package com.planetrush.planetrush.member; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +import java.util.Optional; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +import com.planetrush.planetrush.IntegrationTest; +import com.planetrush.planetrush.fixture.MemberFixture; +import com.planetrush.planetrush.infra.flask.util.FlaskApiClient; +import com.planetrush.planetrush.member.domain.Member; +import com.planetrush.planetrush.member.repository.MemberRepository; +import com.planetrush.planetrush.member.service.MemberService; +import com.planetrush.planetrush.member.service.dto.GetMyProgressAvgDto; + +public class MemberIntegrationTest extends IntegrationTest { + + @Autowired + private MemberService memberService; + + @Autowired + private CacheManager cacheManager; + + @MockBean + private MemberRepository memberRepository; + + @MockBean + private FlaskApiClient flaskApiClient; + + @DisplayName("통계 데이터를 조회할 때 캐싱된 값을 반환해야 한다.") + @Test + void should_use_cache_when_call_get_my_progress_avg() { + // GIVEN + Member member = Mockito.spy(MemberFixture.activeMember()); + GetMyProgressAvgDto dto = GetMyProgressAvgDto.builder() + .challengeCnt(10) + .completionCnt(10) + + .beautyAvg(1.0) + .lifeAvg(1.0) + .exerciseAvg(1.0) + .studyAvg(1.0) + .etcAvg(1.0) + .totalAvg(1.0) + + .myBeautyAvg(1.0) + .myLifeAvg(1.0) + .myExerciseAvg(1.0) + .myStudyAvg(1.0) + .myEtcAvg(1.0) + .myTotalAvg(1.0) + + .myBeautyPer(1.0) + .myLifePer(1.0) + .myExercisePer(1.0) + .myStudyPer(1.0) + .myEtcPer(1.0) + .myTotalPer(1.0) + + .build(); + + // WHEN + when(member.getId()).thenReturn(1L); + when(memberRepository.findById(anyLong())).thenReturn(Optional.of(member)); + when(flaskApiClient.getMyProgressAvg(anyLong())).thenReturn(dto); + + for (int i = 0; i < 10; i++) { + memberService.getMyProgressAvgPer(member.getId()); + } + + // THEN + Cache cache = cacheManager.getCache("challengeProgressAvg"); + assertThat(cache).isNotNull() + .extracting(it -> it.get(member.getId())).isNotNull() + .extracting(it -> it.get()).isNotNull() + .isInstanceOf(GetMyProgressAvgDto.class); + verify(flaskApiClient, times(1)).getMyProgressAvg(member.getId()); + } +} \ No newline at end of file diff --git a/src/test/java/com/planetrush/planetrush/member/MemberServiceUnitTest.java b/src/test/java/com/planetrush/planetrush/member/MemberServiceUnitTest.java new file mode 100644 index 0000000..ac291df --- /dev/null +++ b/src/test/java/com/planetrush/planetrush/member/MemberServiceUnitTest.java @@ -0,0 +1,25 @@ +package com.planetrush.planetrush.member; + +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import com.planetrush.planetrush.ServiceUnitTest; +import com.planetrush.planetrush.infra.flask.util.FlaskApiClient; +import com.planetrush.planetrush.member.repository.MemberRepository; +import com.planetrush.planetrush.member.repository.custom.ChallengeHistoryRepositoryCustom; +import com.planetrush.planetrush.member.service.MemberServiceImpl; + +public abstract class MemberServiceUnitTest extends ServiceUnitTest { + + @InjectMocks + protected MemberServiceImpl memberService; + + @Mock + protected FlaskApiClient flaskApiClient; + + @Mock + protected MemberRepository memberRepository; + + @Mock + protected ChallengeHistoryRepositoryCustom challengeHistoryRepositoryCustom; +}