diff --git a/build.gradle.kts b/build.gradle.kts
index 68ef752b..a8950869 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -36,6 +36,9 @@ dependencies {
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-aop")
+
+ // Redis
+ implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml
index d0917839..97a316c5 100644
--- a/docker-compose-prod.yml
+++ b/docker-compose-prod.yml
@@ -32,6 +32,11 @@ services:
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
+ # === Redis Configuration ===
+ REDIS_HOST: ${REDIS_HOST}
+ REDIS_PORT: ${REDIS_PORT}
+ REDIS_PASSWORD: ${REDIS_PASSWORD}
+
# === Security Configuration ===
JWT_SECRET_KEY: ${JWT_SECRET_KEY}
JWT_ACCESS_TOKEN_VALIDITY: ${JWT_ACCESS_TOKEN_VALIDITY}
diff --git a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java
index 141e6b1a..b4c857c0 100644
--- a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java
+++ b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java
@@ -8,6 +8,7 @@
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
+import until.the.eternity.auctionhistory.application.service.AuctionHistoryCacheWarmupService;
import until.the.eternity.auctionhistory.application.service.AuctionHistoryService;
import until.the.eternity.auctionhistory.application.service.fetcher.AuctionHistoryFetcher;
import until.the.eternity.auctionhistory.application.service.persister.AuctionHistoryPersister;
@@ -27,6 +28,7 @@ public class AuctionHistoryScheduler {
private final AuctionHistoryFetcher fetcher;
private final AuctionHistoryPersister persister;
private final ApplicationEventPublisher eventPublisher;
+ private final AuctionHistoryCacheWarmupService cacheWarmupService;
@Value("${openapi.auction-history.delay-ms}")
private long delayMs;
@@ -114,12 +116,16 @@ public int fetchAndSaveAuctionHistoryAll() {
"> [SCHEDULE] AuctionHistoryScheduler saved [{}] new auction history records complete",
totalSavedCount);
- // 통계 업데이트를 위한 이벤트 발행
+ // 통계 업데이트를 위한 이벤트 발행 (DailyStatisticsService가 일간/랭킹 캐시를 함께 무효화)
log.debug(
"> [SCHEDULE] Publishing AuctionHistorySavedEvent with {} records",
totalSavedCount);
eventPublisher.publishEvent(new AuctionHistorySavedEvent(totalSavedCount));
+ // 경매 거래 내역 캐시 무효화 + 역대 랭킹 캐시 무효화 + 30가지 조합 워밍업
+ log.info("> [SCHEDULE] Starting cache eviction and warmup");
+ cacheWarmupService.evictAndWarm();
+
return totalSavedCount;
}
}
diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryCacheWarmupService.java b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryCacheWarmupService.java
new file mode 100644
index 00000000..1784ca76
--- /dev/null
+++ b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryCacheWarmupService.java
@@ -0,0 +1,101 @@
+package until.the.eternity.auctionhistory.application.service;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.cache.CacheManager;
+import org.springframework.stereotype.Service;
+import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest;
+import until.the.eternity.common.enums.SortDirection;
+import until.the.eternity.common.enums.SortField;
+import until.the.eternity.common.request.PageRequestDto;
+import until.the.eternity.config.CacheNames;
+
+/**
+ * 경매 거래 내역 캐시 워밍업 서비스.
+ *
+ *
AuctionHistoryScheduler 배치 완료 후 호출되어 다음 작업을 수행한다.
+ *
+ *
+ * - auction-history:search 캐시 전체 무효화
+ *
- auction_history 기반 역대 랭킹 캐시 무효화 (ALLTIME_HIGHEST, ALLTIME_MONTH_VOLUME)
+ *
- page 1~2 × size 20 × sortField 3종 × direction 2종 = 12가지 조합 캐시 워밍
+ *
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AuctionHistoryCacheWarmupService {
+
+ private static final int WARMUP_SIZE = 20;
+ private static final int WARMUP_MAX_PAGE = 2;
+
+ private final AuctionHistoryService auctionHistoryService;
+ private final CacheManager cacheManager;
+
+ /**
+ * 캐시 무효화 후 기본 12가지 조합(page 1~2 × size 20 × sortField 3종 × direction 2종)을 선제적으로 워밍한다.
+ *
+ * 빈 검색 조건(필터 없음) 기준으로 워밍하므로, 단순 목록 조회 요청에 즉시 캐시 히트가 발생한다.
+ */
+ public void evictAndWarm() {
+ evictCaches();
+ warmup();
+ }
+
+ private void evictCaches() {
+ clearCache(CacheNames.AUCTION_HISTORY_SEARCH);
+ // auction_history 전체를 직접 쿼리하는 역대 랭킹도 함께 무효화
+ clearCache(CacheNames.RANKING_ALLTIME_HIGHEST);
+ clearCache(CacheNames.RANKING_ALLTIME_MONTH_VOLUME);
+ log.info(
+ "[Cache Warmup] Evicted: {}, {}, {}",
+ CacheNames.AUCTION_HISTORY_SEARCH,
+ CacheNames.RANKING_ALLTIME_HIGHEST,
+ CacheNames.RANKING_ALLTIME_MONTH_VOLUME);
+ }
+
+ private void warmup() {
+ // 옵션 필터가 모두 null인 빈 검색 조건 (캐싱 condition 충족)
+ AuctionHistorySearchRequest emptyRequest =
+ new AuctionHistorySearchRequest(
+ null, null, null, null, null, null, null, null, null);
+
+ int successCount = 0;
+ int failCount = 0;
+
+ for (int page = 1; page <= WARMUP_MAX_PAGE; page++) {
+ for (SortField sortField : SortField.values()) {
+ for (SortDirection direction : SortDirection.values()) {
+ PageRequestDto pageRequest =
+ new PageRequestDto(page, WARMUP_SIZE, sortField, direction);
+ try {
+ auctionHistoryService.search(emptyRequest, pageRequest);
+ successCount++;
+ } catch (Exception e) {
+ failCount++;
+ log.warn(
+ "[Cache Warmup] Failed: page={}, sortField={}, direction={}",
+ page,
+ sortField,
+ direction,
+ e);
+ }
+ }
+ }
+ }
+
+ log.info(
+ "[Cache Warmup] Completed: success={}, fail={} (total={})",
+ successCount,
+ failCount,
+ WARMUP_MAX_PAGE * SortField.values().length * SortDirection.values().length);
+ }
+
+ private void clearCache(String cacheName) {
+ Cache cache = cacheManager.getCache(cacheName);
+ if (cache != null) {
+ cache.clear();
+ }
+ }
+}
diff --git a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java
index cc15bfd1..30a9460e 100644
--- a/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java
+++ b/src/main/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryService.java
@@ -4,6 +4,7 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -15,6 +16,7 @@
import until.the.eternity.auctionhistory.interfaces.rest.dto.response.ItemOptionResponse;
import until.the.eternity.common.request.PageRequestDto;
import until.the.eternity.common.response.PageResponseDto;
+import until.the.eternity.config.CacheNames;
@Service
@RequiredArgsConstructor
@@ -25,6 +27,29 @@ public class AuctionHistoryService {
private final AuctionHistoryMapper mapper;
private final EntityManager entityManager;
+ /**
+ * 경매 거래 내역을 검색한다.
+ *
+ *
단순 검색(가격/옵션/인챈트/세공 필터 없음)에 한해 캐싱을 적용한다. TTL 2시간, 배치 완료 시 전체 무효화 + 워밍업.
+ */
+ @Cacheable(
+ cacheNames = CacheNames.AUCTION_HISTORY_SEARCH,
+ key =
+ "#pageRequestDto.page() + ':'"
+ + " + #pageRequestDto.size() + ':'"
+ + " + (#pageRequestDto.sortBy() != null ? #pageRequestDto.sortBy().fieldName : 'dateAuctionBuy') + ':'"
+ + " + (#pageRequestDto.direction() != null ? #pageRequestDto.direction().code : 'DESC') + ':'"
+ + " + (#requestDto.itemName() ?: '') + ':'"
+ + " + (#requestDto.isExactItemName() ?: false) + ':'"
+ + " + (#requestDto.itemTopCategory() ?: '') + ':'"
+ + " + (#requestDto.itemSubCategory() ?: '') + ':'"
+ + " + (#requestDto.dateAuctionBuyRequest()?.dateAuctionBuyFrom() ?: '') + ':'"
+ + " + (#requestDto.dateAuctionBuyRequest()?.dateAuctionBuyTo() ?: '')",
+ condition =
+ "#requestDto.itemOptionSearchRequest() == null"
+ + " and #requestDto.enchantSearchRequest() == null"
+ + " and (#requestDto.metalwareSearchRequests() == null or #requestDto.metalwareSearchRequests().isEmpty())"
+ + " and #requestDto.priceSearchRequest() == null")
@Transactional(readOnly = true)
public PageResponseDto> search(
AuctionHistorySearchRequest requestDto, PageRequestDto pageRequestDto) {
diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java
index 17850b42..0deb6dfc 100644
--- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java
+++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java
@@ -203,7 +203,9 @@ private BooleanBuilder buildHistoryPredicate(
QAuctionHistoryItemOption mwOpt = new QAuctionHistoryItemOption("mw" + i);
NumberTemplate mwLevel =
Expressions.numberTemplate(
- Integer.class, "CAST(NULLIF({0}, '') AS integer)", mwOpt.optionValue2);
+ Integer.class,
+ "CAST(NULLIF({0}, '') AS integer)",
+ mwOpt.optionValue2);
var mwSubQuery =
JPAExpressions.select(mwOpt.auctionHistory.auctionBuyId)
diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java
index 67ee1885..e741c7ce 100644
--- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java
+++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java
@@ -3,7 +3,6 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
-import until.the.eternity.common.annotation.MetalwareParameters;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
@@ -15,6 +14,7 @@
import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest;
import until.the.eternity.auctionhistory.interfaces.rest.dto.response.AuctionHistoryDetailResponse;
import until.the.eternity.auctionhistory.interfaces.rest.dto.response.ItemOptionResponse;
+import until.the.eternity.common.annotation.MetalwareParameters;
import until.the.eternity.common.request.PageRequestDto;
import until.the.eternity.common.response.PageResponseDto;
diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java
index 87c01cf1..80b12c29 100644
--- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java
+++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java
@@ -7,7 +7,8 @@
@Schema(description = "경매 거래내역 검색 조건")
public record AuctionHistorySearchRequest(
@Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName,
- @Schema(description = "아이템 이름 완전 일치 검색 여부 (true: eq, false: like)",
+ @Schema(
+ description = "아이템 이름 완전 일치 검색 여부 (true: eq, false: like)",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
defaultValue = "false",
example = "false")
@@ -18,7 +19,8 @@ public record AuctionHistorySearchRequest(
@Schema(description = "가격 검색 조건") PriceSearchRequest priceSearchRequest,
@Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest,
@Schema(description = "인챈트 검색 조건") EnchantSearchRequest enchantSearchRequest,
- @Schema(description = "세공 검색 조건 목록 (최대 3개, AND 조건으로 검색)",
+ @Schema(
+ description = "세공 검색 조건 목록 (최대 3개, AND 조건으로 검색)",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
hidden = true)
List metalwareSearchRequests) {}
diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MetalwareSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MetalwareSearchRequest.java
index d0b3c64d..37f7e63a 100644
--- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MetalwareSearchRequest.java
+++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MetalwareSearchRequest.java
@@ -6,8 +6,7 @@
public record MetalwareSearchRequest(
@Schema(description = "세공 이름 (완전 일치)", example = "불의 연성술") String metalware,
@Schema(description = "세공 레벨 시작값 (null이면 1로 처리)", example = "1") Integer levelFrom,
- @Schema(description = "세공 레벨 종료값 (null이면 30으로 처리)", example = "30")
- Integer levelTo) {
+ @Schema(description = "세공 레벨 종료값 (null이면 30으로 처리)", example = "30") Integer levelTo) {
public int resolvedLevelFrom() {
return levelFrom != null ? levelFrom : 1;
diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java b/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java
index 1adc8e93..74296a41 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/application/scheduler/AuctionRealtimeScheduler.java
@@ -21,6 +21,8 @@
* 10분 간격으로 Nexon Open API /auction/list를 호출하여 현재 판매 중인 아이템 정보를 수집한다.
*
*
각 서브 카테고리별로 전체 데이터를 수집한 뒤, 기존 데이터를 삭제하고 새 데이터로 교체한다. (Full Refresh)
+ *
+ *
Full Refresh 완료 후 auction-realtime:search 캐시 전체를 무효화한다.
*/
@Slf4j
@Component
@@ -130,5 +132,8 @@ public void fetchAndSaveAuctionRealtimeAll() {
totalSavedCount,
totalFailedCount,
deletedExpired);
+
+ // Full Refresh 완료 후 실시간 경매 검색 캐시 전체 무효화
+ service.evictSearchCache();
}
}
diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java
index c34da333..5de388b0 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/AuctionRealtimeService.java
@@ -4,6 +4,8 @@
import java.util.List;
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.Pageable;
import org.springframework.stereotype.Service;
@@ -16,6 +18,7 @@
import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.RealtimeItemOptionResponse;
import until.the.eternity.common.enums.ItemCategory;
import until.the.eternity.common.response.PageResponseDto;
+import until.the.eternity.config.CacheNames;
/** 실시간 경매장 데이터 Service. */
@Slf4j
@@ -29,10 +32,20 @@ public class AuctionRealtimeService {
/**
* 실시간 경매장 아이템을 검색한다.
*
+ *
캐시 키: 주요 검색 파라미터 조합. 10분 배치 완료 시 전체 무효화된다.
+ *
* @param requestDto 검색 조건
* @param pageable 페이지 정보
* @return 검색 결과
*/
+ @Cacheable(
+ cacheNames = CacheNames.AUCTION_REALTIME_SEARCH,
+ key =
+ "(#requestDto.itemTopCategory() ?: '_') + ':'"
+ + " + (#requestDto.itemSubCategory() ?: '_') + ':'"
+ + " + (#requestDto.itemName() ?: '_') + ':'"
+ + " + (#requestDto.isExactItemName() ?: false) + ':'"
+ + " + #pageable.pageNumber + ':' + #pageable.pageSize + ':' + #pageable.sort")
@Transactional(readOnly = true)
public PageResponseDto> search(
AuctionRealtimeSearchRequest requestDto, Pageable pageable) {
@@ -91,4 +104,14 @@ public int deleteExpiredItems(Instant now) {
log.info("[REALTIME] Deleted {} expired auction realtime items", deleted);
return deleted;
}
+
+ /**
+ * 실시간 경매 검색 캐시 전체를 무효화한다.
+ *
+ * AuctionRealtimeScheduler에서 Full Refresh 완료 후 호출한다.
+ */
+ @CacheEvict(cacheNames = CacheNames.AUCTION_REALTIME_SEARCH, allEntries = true)
+ public void evictSearchCache() {
+ log.info("[REALTIME] Cache evicted: {}", CacheNames.AUCTION_REALTIME_SEARCH);
+ }
}
diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java
index e13725d3..bc025406 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/controller/AuctionRealtimeController.java
@@ -2,7 +2,6 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
-import until.the.eternity.common.annotation.MetalwareParameters;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
@@ -13,6 +12,7 @@
import until.the.eternity.auctionrealtime.interfaces.rest.dto.request.RealtimePageRequestDto;
import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.AuctionRealtimeDetailResponse;
import until.the.eternity.auctionrealtime.interfaces.rest.dto.response.RealtimeItemOptionResponse;
+import until.the.eternity.common.annotation.MetalwareParameters;
import until.the.eternity.common.response.PageResponseDto;
@RequestMapping("/auction-realtime")
diff --git a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java
index df2d1f0b..3c66e56b 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/interfaces/rest/dto/request/AuctionRealtimeSearchRequest.java
@@ -11,7 +11,8 @@
@Schema(description = "실시간 경매장 검색 조건")
public record AuctionRealtimeSearchRequest(
@Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName,
- @Schema(description = "아이템 이름 완전 일치 검색 여부 (true: eq, false: like)",
+ @Schema(
+ description = "아이템 이름 완전 일치 검색 여부 (true: eq, false: like)",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
defaultValue = "false",
example = "false")
@@ -21,7 +22,8 @@ public record AuctionRealtimeSearchRequest(
@Schema(description = "가격 검색 조건") PriceSearchRequest priceSearchRequest,
@Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest,
@Schema(description = "인챈트 검색 조건") EnchantSearchRequest enchantSearchRequest,
- @Schema(description = "세공 검색 조건 목록 (최대 3개, AND 조건으로 검색)",
+ @Schema(
+ description = "세공 검색 조건 목록 (최대 3개, AND 조건으로 검색)",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
hidden = true)
List metalwareSearchRequests) {}
diff --git a/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java
index aec4e464..0c3dca98 100644
--- a/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java
+++ b/src/main/java/until/the/eternity/auctionsearchoption/application/service/AuctionSearchOptionService.java
@@ -6,12 +6,14 @@
import java.util.Map;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import until.the.eternity.auctionsearchoption.domain.entity.AuctionSearchOptionMetadata;
import until.the.eternity.auctionsearchoption.domain.repository.AuctionSearchOptionRepositoryPort;
import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.FieldMetadata;
import until.the.eternity.auctionsearchoption.interfaces.rest.dto.response.SearchOptionMetadataResponse;
+import until.the.eternity.config.CacheNames;
@Slf4j
@Service
@@ -26,6 +28,7 @@ public class AuctionSearchOptionService {
*
* @return 검색 옵션 메타데이터 리스트
*/
+ @Cacheable(cacheNames = CacheNames.SEARCH_OPTION_ALL_ACTIVE, key = "'all'")
@Transactional(readOnly = true)
public List getAllActiveSearchOptions() {
List entities = repositoryPort.findAllActive();
diff --git a/src/main/java/until/the/eternity/config/CacheNames.java b/src/main/java/until/the/eternity/config/CacheNames.java
new file mode 100644
index 00000000..be962568
--- /dev/null
+++ b/src/main/java/until/the/eternity/config/CacheNames.java
@@ -0,0 +1,55 @@
+package until.the.eternity.config;
+
+/** Redis 캐시 이름 상수 클래스. */
+public final class CacheNames {
+
+ private CacheNames() {}
+
+ // ===== 랭킹 =====
+ public static final String RANKING_PRICE_TODAY_HIGHEST = "ranking:price:today:highest";
+ public static final String RANKING_PRICE_WEEK_HIGHEST = "ranking:price:week:highest";
+ public static final String RANKING_PRICE_TODAY_VOLUME = "ranking:price:today:volume";
+ public static final String RANKING_VOLUME_TODAY_POPULAR = "ranking:volume:today:popular";
+ public static final String RANKING_VOLUME_WEEK_POPULAR = "ranking:volume:week:popular";
+ public static final String RANKING_CHANGE_PRICE_SURGE = "ranking:change:price:surge";
+ public static final String RANKING_CHANGE_PRICE_DROP = "ranking:change:price:drop";
+ public static final String RANKING_CHANGE_VOLUME_SURGE = "ranking:change:volume:surge";
+ public static final String RANKING_CATEGORY_HIGHEST = "ranking:category:highest";
+ public static final String RANKING_CATEGORY_POPULAR = "ranking:category:popular";
+ public static final String RANKING_ALLTIME_HIGHEST = "ranking:alltime:highest";
+ public static final String RANKING_ALLTIME_MONTH_VOLUME = "ranking:alltime:month:volume";
+
+ // ===== 검색 옵션 메타데이터 =====
+ public static final String SEARCH_OPTION_ALL_ACTIVE = "search-option:all-active";
+
+ // ===== 아이템 마스터 =====
+ public static final String ITEM_INFO_ALL = "item-info:all";
+ public static final String ITEM_INFO_BY_TOP_CATEGORY = "item-info:by-top-category";
+ public static final String ITEM_INFO_BY_SUB_CATEGORY = "item-info:by-sub-category";
+ public static final String ITEM_INFO_DETAIL = "item-info:detail";
+ public static final String ITEM_INFO_SUMMARY = "item-info:summary";
+
+ // ===== 인챈트 마스터 =====
+ public static final String ENCHANT_INFO_ALL = "enchant-info:all";
+ public static final String ENCHANT_INFO_FULLNAMES = "enchant-info:fullnames";
+
+ // ===== 세공 마스터 =====
+ public static final String METALWARE_INFO_ALL = "metalware-info:all";
+ public static final String METALWARE_ATTRIBUTE_INFO_SEARCH = "metalware-attribute-info:search";
+
+ // ===== 통계 (일간) =====
+ public static final String STATISTICS_ITEM_DAILY = "statistics:item:daily";
+ public static final String STATISTICS_SUBCATEGORY_DAILY = "statistics:subcategory:daily";
+ public static final String STATISTICS_TOPCATEGORY_DAILY = "statistics:topcategory:daily";
+
+ // ===== 통계 (주간) =====
+ public static final String STATISTICS_ITEM_WEEKLY = "statistics:item:weekly";
+ public static final String STATISTICS_SUBCATEGORY_WEEKLY = "statistics:subcategory:weekly";
+ public static final String STATISTICS_TOPCATEGORY_WEEKLY = "statistics:topcategory:weekly";
+
+ // ===== 실시간 경매 =====
+ public static final String AUCTION_REALTIME_SEARCH = "auction-realtime:search";
+
+ // ===== 경매 거래 내역 =====
+ public static final String AUCTION_HISTORY_SEARCH = "auction-history:search";
+}
diff --git a/src/main/java/until/the/eternity/config/RedisCacheErrorHandler.java b/src/main/java/until/the/eternity/config/RedisCacheErrorHandler.java
new file mode 100644
index 00000000..8ce10832
--- /dev/null
+++ b/src/main/java/until/the/eternity/config/RedisCacheErrorHandler.java
@@ -0,0 +1,47 @@
+package until.the.eternity.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.Cache;
+import org.springframework.cache.interceptor.CacheErrorHandler;
+
+/**
+ * Redis 장애 시 캐시 오류를 로깅만 하고 예외를 전파하지 않는 핸들러.
+ *
+ * Redis가 다운되어도 캐시를 건너뛰고 실제 메서드(DB 조회 등)를 실행해 서비스를 유지한다.
+ */
+@Slf4j
+public class RedisCacheErrorHandler implements CacheErrorHandler {
+
+ @Override
+ public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
+ log.warn(
+ "[Cache] GET 실패 - cache={}, key={}, error={}",
+ cache.getName(),
+ key,
+ exception.getMessage());
+ }
+
+ @Override
+ public void handleCachePutError(
+ RuntimeException exception, Cache cache, Object key, Object value) {
+ log.warn(
+ "[Cache] PUT 실패 - cache={}, key={}, error={}",
+ cache.getName(),
+ key,
+ exception.getMessage());
+ }
+
+ @Override
+ public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
+ log.warn(
+ "[Cache] EVICT 실패 - cache={}, key={}, error={}",
+ cache.getName(),
+ key,
+ exception.getMessage());
+ }
+
+ @Override
+ public void handleCacheClearError(RuntimeException exception, Cache cache) {
+ log.warn("[Cache] CLEAR 실패 - cache={}, error={}", cache.getName(), exception.getMessage());
+ }
+}
diff --git a/src/main/java/until/the/eternity/config/RedisConfig.java b/src/main/java/until/the/eternity/config/RedisConfig.java
new file mode 100644
index 00000000..965b9506
--- /dev/null
+++ b/src/main/java/until/the/eternity/config/RedisConfig.java
@@ -0,0 +1,107 @@
+package until.the.eternity.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+import org.springframework.cache.annotation.CachingConfigurer;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.interceptor.CacheErrorHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.cache.RedisCacheConfiguration;
+import org.springframework.data.redis.cache.RedisCacheManager;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+@EnableCaching
+public class RedisConfig implements CachingConfigurer {
+
+ @Override
+ public CacheErrorHandler errorHandler() {
+ return new RedisCacheErrorHandler();
+ }
+
+ @Bean
+ public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
+ ObjectMapper objectMapper =
+ new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ GenericJackson2JsonRedisSerializer jsonSerializer =
+ new GenericJackson2JsonRedisSerializer(objectMapper);
+
+ RedisCacheConfiguration defaultConfig =
+ RedisCacheConfiguration.defaultCacheConfig()
+ .entryTtl(Duration.ofMinutes(10))
+ .serializeKeysWith(
+ RedisSerializationContext.SerializationPair.fromSerializer(
+ new StringRedisSerializer()))
+ .serializeValuesWith(
+ RedisSerializationContext.SerializationPair.fromSerializer(
+ jsonSerializer))
+ .disableCachingNullValues();
+
+ Map configs = new HashMap<>();
+
+ // 랭킹 - 10분 TTL (이벤트 기반 eviction으로 실시간 반영)
+ Duration rankingTtl = Duration.ofMinutes(10);
+ configs.put(CacheNames.RANKING_PRICE_TODAY_HIGHEST, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_PRICE_WEEK_HIGHEST, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_PRICE_TODAY_VOLUME, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_VOLUME_TODAY_POPULAR, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_VOLUME_WEEK_POPULAR, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_CHANGE_PRICE_SURGE, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_CHANGE_PRICE_DROP, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_CHANGE_VOLUME_SURGE, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_CATEGORY_HIGHEST, defaultConfig.entryTtl(rankingTtl));
+ configs.put(CacheNames.RANKING_CATEGORY_POPULAR, defaultConfig.entryTtl(rankingTtl));
+ // 역대 최고가 - 1시간 TTL (auction_history 전체 스캔, 잘 바뀌지 않음)
+ configs.put(
+ CacheNames.RANKING_ALLTIME_HIGHEST, defaultConfig.entryTtl(Duration.ofHours(1)));
+ configs.put(CacheNames.RANKING_ALLTIME_MONTH_VOLUME, defaultConfig.entryTtl(rankingTtl));
+
+ // 검색 옵션 메타데이터 - 24시간 TTL (사실상 정적 데이터)
+ configs.put(
+ CacheNames.SEARCH_OPTION_ALL_ACTIVE, defaultConfig.entryTtl(Duration.ofHours(24)));
+
+ // 마스터 데이터 - 1시간 TTL (sync API 호출 시 evict)
+ Duration masterTtl = Duration.ofHours(1);
+ configs.put(CacheNames.ITEM_INFO_ALL, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.ITEM_INFO_BY_TOP_CATEGORY, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.ITEM_INFO_BY_SUB_CATEGORY, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.ITEM_INFO_DETAIL, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.ITEM_INFO_SUMMARY, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.ENCHANT_INFO_ALL, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.ENCHANT_INFO_FULLNAMES, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.METALWARE_INFO_ALL, defaultConfig.entryTtl(masterTtl));
+ configs.put(CacheNames.METALWARE_ATTRIBUTE_INFO_SEARCH, defaultConfig.entryTtl(masterTtl));
+
+ // 통계 - 30분 TTL (이벤트 기반 eviction으로 실시간 반영)
+ Duration statsTtl = Duration.ofMinutes(30);
+ configs.put(CacheNames.STATISTICS_ITEM_DAILY, defaultConfig.entryTtl(statsTtl));
+ configs.put(CacheNames.STATISTICS_SUBCATEGORY_DAILY, defaultConfig.entryTtl(statsTtl));
+ configs.put(CacheNames.STATISTICS_TOPCATEGORY_DAILY, defaultConfig.entryTtl(statsTtl));
+ configs.put(CacheNames.STATISTICS_ITEM_WEEKLY, defaultConfig.entryTtl(statsTtl));
+ configs.put(CacheNames.STATISTICS_SUBCATEGORY_WEEKLY, defaultConfig.entryTtl(statsTtl));
+ configs.put(CacheNames.STATISTICS_TOPCATEGORY_WEEKLY, defaultConfig.entryTtl(statsTtl));
+
+ // 실시간 경매 - 12분 TTL (10분 배치 + 여유 2분)
+ configs.put(
+ CacheNames.AUCTION_REALTIME_SEARCH, defaultConfig.entryTtl(Duration.ofMinutes(12)));
+
+ // 경매 거래 내역 - 2시간 TTL (배치 완료 시 evict + warmup)
+ configs.put(CacheNames.AUCTION_HISTORY_SEARCH, defaultConfig.entryTtl(Duration.ofHours(2)));
+
+ return RedisCacheManager.builder(connectionFactory)
+ .cacheDefaults(defaultConfig)
+ .withInitialCacheConfigurations(configs)
+ .build();
+ }
+}
diff --git a/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java b/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java
index 396b8811..edf03138 100644
--- a/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java
+++ b/src/main/java/until/the/eternity/enchantinfo/application/service/EnchantInfoService.java
@@ -3,11 +3,15 @@
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import until.the.eternity.common.exception.CustomException;
+import until.the.eternity.config.CacheNames;
import until.the.eternity.enchantinfo.domain.exception.EnchantInfoExceptionCode;
import until.the.eternity.enchantinfo.domain.repository.EnchantInfoRepositoryPort;
import until.the.eternity.enchantinfo.interfaces.rest.dto.response.EnchantInfoResponse;
@@ -22,10 +26,14 @@ public class EnchantInfoService {
private final EnchantInfoRepositoryPort enchantInfoRepository;
+ @Cacheable(
+ cacheNames = CacheNames.ENCHANT_INFO_ALL,
+ key = "#pageable.pageNumber + ':' + #pageable.pageSize + ':' + #pageable.sort")
public Page findAll(Pageable pageable) {
return enchantInfoRepository.findAll(pageable).map(EnchantInfoResponse::from);
}
+ @Cacheable(cacheNames = CacheNames.ENCHANT_INFO_FULLNAMES, key = "#affixPosition ?: 'all'")
public List findAllFullnames(String affixPosition) {
if (affixPosition != null) {
if (!ALLOWED_AFFIX_POSITIONS.contains(affixPosition)) {
@@ -36,6 +44,11 @@ public List findAllFullnames(String affixPosition) {
return enchantInfoRepository.findAllFullnames();
}
+ @Caching(
+ evict = {
+ @CacheEvict(cacheNames = CacheNames.ENCHANT_INFO_ALL, allEntries = true),
+ @CacheEvict(cacheNames = CacheNames.ENCHANT_INFO_FULLNAMES, allEntries = true)
+ })
@Transactional
public EnchantInfoSyncResponse syncFromAuctionHistory() {
int upserted = enchantInfoRepository.upsertFromAuctionHistory();
diff --git a/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java b/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java
index f1022c51..5e7491e4 100644
--- a/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java
+++ b/src/main/java/until/the/eternity/iteminfo/application/service/ItemInfoService.java
@@ -6,6 +6,9 @@
import java.util.Set;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
@@ -13,6 +16,7 @@
import until.the.eternity.auctionhistory.domain.repository.AuctionHistoryRepositoryPort;
import until.the.eternity.batchlog.domain.enums.BatchType;
import until.the.eternity.common.annotation.BatchLog;
+import until.the.eternity.config.CacheNames;
import until.the.eternity.iteminfo.domain.entity.ItemInfo;
import until.the.eternity.iteminfo.domain.entity.ItemInfoId;
import until.the.eternity.iteminfo.domain.repository.ItemInfoRepositoryPort;
@@ -35,21 +39,32 @@ public List findItemCategories() {
return ItemCategoryResponse.from();
}
+ @Cacheable(cacheNames = CacheNames.ITEM_INFO_ALL, key = "'all'")
public List findAll() {
List itemInfos = itemInfoRepository.findAll();
return ItemInfoResponse.from(itemInfos);
}
+ @Cacheable(cacheNames = CacheNames.ITEM_INFO_BY_TOP_CATEGORY, key = "#topCategory")
public List findByTopCategory(String topCategory) {
List itemInfos = itemInfoRepository.findByTopCategory(topCategory);
return ItemInfoResponse.from(itemInfos);
}
+ @Cacheable(cacheNames = CacheNames.ITEM_INFO_BY_SUB_CATEGORY, key = "#subCategory")
public List findBySubCategory(String subCategory) {
List itemInfos = itemInfoRepository.findBySubCategory(subCategory);
return ItemInfoResponse.from(itemInfos);
}
+ @Cacheable(
+ cacheNames = CacheNames.ITEM_INFO_DETAIL,
+ key =
+ "(#searchRequest.name() ?: '') + ':'"
+ + " + (#searchRequest.topCategory() ?: '') + ':'"
+ + " + (#searchRequest.subCategory() ?: '') + ':'"
+ + " + #pageable.pageNumber + ':' + #pageable.pageSize"
+ + " + ':' + #pageable.sort")
public Page findAllDetail(
ItemInfoSearchRequest searchRequest, Pageable pageable) {
Page itemInfoPage =
@@ -57,10 +72,16 @@ public Page findAllDetail(
return itemInfoPage.map(ItemInfoResponse::from);
}
+ @Cacheable(
+ cacheNames = CacheNames.ITEM_INFO_SUMMARY,
+ key =
+ "(#searchRequest.name() ?: '') + ':'"
+ + " + (#searchRequest.topCategory() ?: '') + ':'"
+ + " + (#searchRequest.subCategory() ?: '') + ':'"
+ + " + #direction.name()")
public List findAllSummary(
ItemInfoSearchRequest searchRequest,
org.springframework.data.domain.Sort.Direction direction) {
- // direction을 Pageable로 변환
Pageable pageable =
org.springframework.data.domain.PageRequest.of(
0,
@@ -70,6 +91,14 @@ public List findAllSummary(
return ItemInfoSummaryResponse.from(itemInfos);
}
+ @Caching(
+ evict = {
+ @CacheEvict(cacheNames = CacheNames.ITEM_INFO_ALL, allEntries = true),
+ @CacheEvict(cacheNames = CacheNames.ITEM_INFO_BY_TOP_CATEGORY, allEntries = true),
+ @CacheEvict(cacheNames = CacheNames.ITEM_INFO_BY_SUB_CATEGORY, allEntries = true),
+ @CacheEvict(cacheNames = CacheNames.ITEM_INFO_DETAIL, allEntries = true),
+ @CacheEvict(cacheNames = CacheNames.ITEM_INFO_SUMMARY, allEntries = true)
+ })
@BatchLog(type = BatchType.ITEM_INFO_SYNC)
@Transactional
public ItemInfoSyncResponse syncItemInfoFromAuctionHistory() {
diff --git a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareAttributeInfoService.java b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareAttributeInfoService.java
index 2816d4fa..aed51db2 100644
--- a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareAttributeInfoService.java
+++ b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareAttributeInfoService.java
@@ -1,11 +1,14 @@
package until.the.eternity.metalwareinfo.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import until.the.eternity.batchlog.domain.enums.BatchType;
import until.the.eternity.common.annotation.BatchLog;
+import until.the.eternity.config.CacheNames;
import until.the.eternity.metalwareinfo.domain.repository.MetalwareAttributeInfoRepositoryPort;
import until.the.eternity.metalwareinfo.interfaces.rest.dto.request.MetalwareAttributeInfoSearchRequest;
import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareAttributeInfoResponse;
@@ -16,6 +19,7 @@ public class MetalwareAttributeInfoService {
private final MetalwareAttributeInfoRepositoryPort metalwareAttributeInfoRepository;
+ @CacheEvict(cacheNames = CacheNames.METALWARE_ATTRIBUTE_INFO_SEARCH, allEntries = true)
@BatchLog(type = BatchType.METALWARE_ATTRIBUTE_SYNC)
@Transactional
public int sync() {
@@ -27,6 +31,10 @@ public int sync() {
return inserted + updated;
}
+ @Cacheable(
+ cacheNames = CacheNames.METALWARE_ATTRIBUTE_INFO_SEARCH,
+ key =
+ "#request.metalware() + ':' + (#request.page ?: 1) + ':' + (#request.size ?: 25) + ':' + (#request.direction?.name() ?: 'ASC')")
@Transactional(readOnly = true)
public Page search(
MetalwareAttributeInfoSearchRequest request) {
diff --git a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java
index fdc99fbd..aa909d6f 100644
--- a/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java
+++ b/src/main/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoService.java
@@ -2,8 +2,12 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import until.the.eternity.config.CacheNames;
import until.the.eternity.metalwareinfo.domain.repository.MetalwareInfoRepositoryPort;
import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoResponse;
import until.the.eternity.metalwareinfo.interfaces.rest.dto.response.MetalwareInfoSyncResponse;
@@ -15,11 +19,19 @@ public class MetalwareInfoService {
private final MetalwareInfoRepositoryPort metalwareInfoRepository;
+ @Cacheable(cacheNames = CacheNames.METALWARE_INFO_ALL, key = "'all'")
public List findAll() {
List metalwares = metalwareInfoRepository.findAllMetalwares();
return MetalwareInfoResponse.from(metalwares);
}
+ @Caching(
+ evict = {
+ @CacheEvict(cacheNames = CacheNames.METALWARE_INFO_ALL, allEntries = true),
+ @CacheEvict(
+ cacheNames = CacheNames.METALWARE_ATTRIBUTE_INFO_SEARCH,
+ allEntries = true)
+ })
@Transactional
public MetalwareInfoSyncResponse syncFromAttributeInfo() {
int levelAttributeUpserted =
diff --git a/src/main/java/until/the/eternity/ranking/application/service/AllTimeRankingService.java b/src/main/java/until/the/eternity/ranking/application/service/AllTimeRankingService.java
index 1fb59d6d..ac95269f 100644
--- a/src/main/java/until/the/eternity/ranking/application/service/AllTimeRankingService.java
+++ b/src/main/java/until/the/eternity/ranking/application/service/AllTimeRankingService.java
@@ -2,8 +2,10 @@
import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import until.the.eternity.config.CacheNames;
import until.the.eternity.ranking.domain.mapper.RankingMapper;
import until.the.eternity.ranking.interfaces.rest.dto.response.AllTimeRankingResponse;
import until.the.eternity.ranking.repository.RankingRepository;
@@ -16,13 +18,15 @@ public class AllTimeRankingService {
private final RankingRepository rankingRepository;
private final RankingMapper rankingMapper;
- /** 역대 최고가 거래 TOP N (API 11) */
+ /** 역대 최고가 거래 TOP N (API 11) - auction_history 전체 스캔, 1시간 TTL */
+ @Cacheable(cacheNames = CacheNames.RANKING_ALLTIME_HIGHEST, key = "#limit")
public List getAllTimeHighestPrice(int limit) {
List