From bc7d613eaa0a3147c9b7ea20f93a03cb425214d2 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 01:49:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?MOSU-267=20feat:=20cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=A0=20=EB=8F=84=EB=A9=94=EC=9D=B8=EB=93=A4=20enu?= =?UTF-8?q?m=EC=9C=BC=EB=A1=9C=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/caffeine/CacheGroup.java | 33 +++++++++++++++++++ .../mosuserver/domain/caffeine/CacheType.java | 13 ++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java create mode 100644 src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java diff --git a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java new file mode 100644 index 00000000..eaee8b2c --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java @@ -0,0 +1,33 @@ +package life.mosu.mosuserver.domain.caffeine; + +import java.time.Duration; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum CacheGroup { + + NOTICE( + "notice", + Duration.ofMinutes(10), + CacheType.LOCAL + ), + + GLOBAL_ONLY( + "globalOnly", + Duration.ofMinutes(10), + CacheType.GLOBAL + ), + + COMPOSITE_ALL( + "composite", + Duration.ofMinutes(10), + CacheType.COMPOSITE + ); + + + private final String cacheName; + private final Duration expiredAfterWrite; + private final CacheType cacheType; +} diff --git a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java new file mode 100644 index 00000000..98c4e603 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheType.java @@ -0,0 +1,13 @@ +package life.mosu.mosuserver.domain.caffeine; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public enum CacheType { + LOCAL("로컬 타입만 적용"), + GLOBAL("분산 캐시만 적용"), + COMPOSITE("로컬 + 분산 캐시 모두 적용"); + + private final String type; + +} \ No newline at end of file From 40f61d0bd20b6d55f465875e5887799a72adb70f Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 01:50:12 +0900 Subject: [PATCH 2/4] =?UTF-8?q?MOSU-267=20feat:=20LocalCacheManager=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../caffeine/LocalCacheConfig.java | 42 +++++++++++++ .../caffeine/LocalCacheManager.java | 59 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java create mode 100644 src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java diff --git a/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java new file mode 100644 index 00000000..44de15be --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheConfig.java @@ -0,0 +1,42 @@ +package life.mosu.mosuserver.application.caffeine; + +import com.github.benmanes.caffeine.cache.Caffeine; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import life.mosu.mosuserver.domain.caffeine.CacheGroup; +import life.mosu.mosuserver.domain.caffeine.CacheType; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCache; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@EnableCaching +@Configuration +public class LocalCacheConfig { + + @Bean + public LocalCacheManager localCacheManager() { + List caches = Arrays.stream(CacheGroup.values()) + .filter(g -> g.getCacheType() == CacheType.LOCAL + || g.getCacheType() == CacheType.COMPOSITE) + .map(g -> new CaffeineCache( + g.getCacheName(), + Caffeine.newBuilder() + .recordStats() + .expireAfterWrite(g.getExpiredAfterWrite()) + .build() + )).collect(Collectors.toList()); + + return new LocalCacheManager(caches); + } + + @Bean + @Primary + public CacheManager appCacheManager(LocalCacheManager localCacheManager) { + return localCacheManager; + } +} diff --git a/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java new file mode 100644 index 00000000..e800e56d --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/caffeine/LocalCacheManager.java @@ -0,0 +1,59 @@ +package life.mosu.mosuserver.application.caffeine; + +import jakarta.annotation.Nullable; +import jakarta.annotation.PostConstruct; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; + +public class LocalCacheManager implements CacheManager, UpdatableCacheManager { + + private final List caches; + private Map cacheMap = new ConcurrentHashMap<>(); + private volatile Set cacheNames = Collections.emptySet(); + + public LocalCacheManager(List caches) { + this.caches = (caches != null) ? caches : Collections.emptyList(); + } + + @PostConstruct + public void init() { + Set cacheNamesSet = new LinkedHashSet<>(caches.size()); + Map cacheMapTemp = new ConcurrentHashMap<>(16); + + for (Cache cache : caches) { + String name = cache.getName(); + cacheNamesSet.add(name); + cacheMapTemp.put(name, cache); + } + this.cacheMap = cacheMapTemp; + this.cacheNames = cacheNamesSet; + } + + @Override + @Nullable + public Cache getCache(String name) { + return cacheMap.get(name); + } + + @Override + public Collection getCacheNames() { + return cacheNames; + } + + @Override + public void putIfAbsent(Cache cache, String key, Object value) { + Cache localCache = getCache(cache.getName()); + if (localCache != null) { + localCache.putIfAbsent(key, value); + } + } + + +} From c5be4ad5d22ca72f287c0c76fe42d6db3405e417 Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 16:11:07 +0900 Subject: [PATCH 3/4] =?UTF-8?q?MOSU-267=20fix:=20=EB=B3=80=EC=88=98?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 99ddb470..5d656bc3 100644 --- a/build.gradle +++ b/build.gradle @@ -106,8 +106,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-testcontainers:3.3.5' testImplementation 'org.testcontainers:testcontainers:1.19.3' testImplementation 'org.testcontainers:junit-jupiter:1.19.3' - testImplementation 'org.testcoscntainers:mysql:1.20.0' - + testImplementation 'org.testcontainers:mysql:1.20.0' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.apache.commons:commons-pool2:2.12.1' From a0fd7db899abf24506649bd57e0503e3ff3bea0d Mon Sep 17 00:00:00 2001 From: jbh010204 Date: Sun, 10 Aug 2025 16:11:30 +0900 Subject: [PATCH 4/4] =?UTF-8?q?MOSU-267=20feat:=20=EA=B3=B5=EC=A7=80=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=BA=90=EC=8B=B1=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/caffeine/UpdatableCacheManager.java | 8 ++++++++ .../mosuserver/application/inquiry/InquiryService.java | 4 ---- .../mosu/mosuserver/application/notice/NoticeService.java | 8 ++++++++ .../life/mosu/mosuserver/domain/caffeine/CacheGroup.java | 4 ++-- ...ineCacheConfig.java => CaffeineFilterCacheConfig.java} | 2 +- 5 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java rename src/main/java/life/mosu/mosuserver/global/config/{CaffeineCacheConfig.java => CaffeineFilterCacheConfig.java} (98%) diff --git a/src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java b/src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java new file mode 100644 index 00000000..92939647 --- /dev/null +++ b/src/main/java/life/mosu/mosuserver/application/caffeine/UpdatableCacheManager.java @@ -0,0 +1,8 @@ +package life.mosu.mosuserver.application.caffeine; + +import org.springframework.cache.Cache; + +public interface UpdatableCacheManager { + + void putIfAbsent(Cache cache, String key, Object value); +} diff --git a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java index 965f5f42..a6fbdb69 100644 --- a/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java +++ b/src/main/java/life/mosu/mosuserver/application/inquiry/InquiryService.java @@ -3,10 +3,8 @@ import life.mosu.mosuserver.domain.inquiry.entity.InquiryJpaEntity; import life.mosu.mosuserver.domain.inquiry.entity.InquiryStatus; import life.mosu.mosuserver.domain.inquiry.repository.InquiryJpaRepository; -import life.mosu.mosuserver.domain.inquiryAnswer.repository.InquiryAnswerJpaRepository; import life.mosu.mosuserver.domain.user.entity.UserJpaEntity; import life.mosu.mosuserver.domain.user.entity.UserRole; -import life.mosu.mosuserver.domain.user.repository.UserJpaRepository; import life.mosu.mosuserver.global.exception.CustomRuntimeException; import life.mosu.mosuserver.global.exception.ErrorCode; import life.mosu.mosuserver.presentation.inquiry.dto.InquiryCreateRequest; @@ -26,11 +24,9 @@ @RequiredArgsConstructor public class InquiryService { - private final UserJpaRepository userJpaRepository; private final InquiryAttachmentService inquiryAttachmentService; private final InquiryJpaRepository inquiryJpaRepository; private final InquiryAnswerService inquiryAnswerService; - private final InquiryAnswerJpaRepository inquiryAnswerJpaRepository; @Transactional public void createInquiry(UserJpaEntity user, InquiryCreateRequest request) { diff --git a/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java b/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java index 4405792c..379df88b 100644 --- a/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java +++ b/src/main/java/life/mosu/mosuserver/application/notice/NoticeService.java @@ -12,6 +12,8 @@ import life.mosu.mosuserver.presentation.notice.dto.NoticeUpdateRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -28,12 +30,15 @@ public class NoticeService { private final NoticeJpaRepository noticeJpaRepository; private final NoticeAttachmentService attachmentService; + @CacheEvict(cacheNames = "notice", allEntries = true) @Transactional public void createNotice(NoticeCreateRequest request, UserJpaEntity user) { NoticeJpaEntity noticeEntity = noticeJpaRepository.save(request.toEntity(user)); attachmentService.createAttachment(request.attachments(), noticeEntity); } + @Cacheable(cacheNames = "notice", + key = "'page=' + #page + ',size=' + #size") @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public List getNotices(int page, int size) { Pageable pageable = PageRequest.of(page, size, Sort.by("id")); @@ -44,6 +49,7 @@ public List getNotices(int page, int size) { .toList(); } + @Cacheable(cacheNames = "notice", key = "#noticeId") @Transactional(readOnly = true, propagation = Propagation.SUPPORTS) public NoticeDetailResponse getNoticeDetail(Long noticeId) { NoticeJpaEntity notice = getNotice(noticeId); @@ -51,12 +57,14 @@ public NoticeDetailResponse getNoticeDetail(Long noticeId) { return toNoticeDetailResponse(notice); } + @CacheEvict(cacheNames = "notice", allEntries = true) @Transactional public void deleteNotice(Long noticeId) { NoticeJpaEntity noticeEntity = getNotice(noticeId); noticeJpaRepository.delete(noticeEntity); } + @CacheEvict(cacheNames = "notice", allEntries = true) @Transactional public void updateNotice(Long noticeId, NoticeUpdateRequest request, UserJpaEntity user) { NoticeJpaEntity noticeEntity = getNotice(noticeId); diff --git a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java index eaee8b2c..af1f57e4 100644 --- a/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java +++ b/src/main/java/life/mosu/mosuserver/domain/caffeine/CacheGroup.java @@ -14,8 +14,8 @@ public enum CacheGroup { CacheType.LOCAL ), - GLOBAL_ONLY( - "globalOnly", + INQUIRY( + "inquiry", Duration.ofMinutes(10), CacheType.GLOBAL ), diff --git a/src/main/java/life/mosu/mosuserver/global/config/CaffeineCacheConfig.java b/src/main/java/life/mosu/mosuserver/global/config/CaffeineFilterCacheConfig.java similarity index 98% rename from src/main/java/life/mosu/mosuserver/global/config/CaffeineCacheConfig.java rename to src/main/java/life/mosu/mosuserver/global/config/CaffeineFilterCacheConfig.java index 79d43652..52fab7fa 100644 --- a/src/main/java/life/mosu/mosuserver/global/config/CaffeineCacheConfig.java +++ b/src/main/java/life/mosu/mosuserver/global/config/CaffeineFilterCacheConfig.java @@ -12,7 +12,7 @@ import org.springframework.context.annotation.Configuration; @Configuration -public class CaffeineCacheConfig { +public class CaffeineFilterCacheConfig { @Bean public Cache ipRequestCountsCache(IpRateLimitingProperties properties) {