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 1e9c1586..1adc8e93 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
@@ -19,6 +19,8 @@
* 실시간 경매장 데이터 수집 스케줄러.
*
*
10분 간격으로 Nexon Open API /auction/list를 호출하여 현재 판매 중인 아이템 정보를 수집한다.
+ *
+ *
각 서브 카테고리별로 전체 데이터를 수집한 뒤, 기존 데이터를 삭제하고 새 데이터로 교체한다. (Full Refresh)
*/
@Slf4j
@Component
@@ -53,7 +55,6 @@ public void fetchAndSaveAuctionRealtimeAll() {
for (int topIndex = 0; topIndex < topCategories.size(); topIndex++) {
String topCategory = topCategories.get(topIndex);
List subCategories = categoriesByTopCategory.get(topCategory);
- List newEntities = new ArrayList<>();
log.debug("[REALTIME] Processing top category [{}]", topCategory);
@@ -62,7 +63,7 @@ public void fetchAndSaveAuctionRealtimeAll() {
try {
log.debug("[REALTIME] Processing category [{}]", category.getSubCategory());
- // API 호출 및 데이터 수집
+ // API 호출 및 전체 데이터 수집
FetchResult fetchResult = fetcher.fetch(category);
if (fetchResult.items().isEmpty()) {
@@ -70,21 +71,17 @@ public void fetchAndSaveAuctionRealtimeAll() {
continue;
}
- // 엔티티 변환 및 필터링
+ // 엔티티 변환
List entities =
- persister.prepareEntities(
- fetchResult.items(), category, fetchResult.latestDate());
+ persister.prepareEntities(fetchResult.items(), category);
if (entities.isEmpty()) {
continue;
}
- // 동일 날짜 데이터가 있으면 삭제 후 저장
- if (fetchResult.hasEqualDate() && fetchResult.latestDate() != null) {
- service.deleteAndSave(category, fetchResult.latestDate(), entities);
- } else {
- newEntities.addAll(entities);
- }
+ // 기존 데이터 삭제 후 새 데이터 저장 (Full Refresh)
+ service.replaceBySubCategory(category, entities);
+ totalSavedCount += entities.size();
// 마지막 서브 카테고리가 아닌 경우에만 delay 적용
if (subIndex < subCategories.size() - 1) {
@@ -108,16 +105,6 @@ public void fetchAndSaveAuctionRealtimeAll() {
}
}
- // Top Category별로 저장 (동일 날짜가 아닌 신규 데이터)
- if (!newEntities.isEmpty()) {
- service.saveAll(newEntities);
- totalSavedCount += newEntities.size();
- log.info(
- "[REALTIME] Saved [{}] new auction realtime items for top category [{}]",
- newEntities.size(),
- topCategory);
- }
-
// 마지막 탑 카테고리가 아닌 경우에만 delay 적용
if (topIndex < topCategories.size() - 1) {
try {
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 8aacbe22..c34da333 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
@@ -62,26 +62,16 @@ public AuctionRealtimeDetailResponse findByIdOrElseT
}
/**
- * 해당 카테고리 & 동일 date_auction_expire 레코드 삭제 후 새 엔티티들을 저장한다.
+ * 해당 카테고리의 기존 데이터를 모두 삭제하고 새 데이터를 저장한다. (Full Refresh)
*
* @param category 아이템 카테고리
- * @param dateAuctionExpire 삭제할 date_auction_expire
* @param entities 저장할 엔티티 리스트
*/
@Transactional
- public void deleteAndSave(
- ItemCategory category, Instant dateAuctionExpire, List entities) {
+ public void replaceBySubCategory(ItemCategory category, List entities) {
+ int deleted = repository.deleteBySubCategory(category);
+ log.info("[REALTIME] [{}] Deleted {} existing records", category.getSubCategory(), deleted);
- // 동일 date_auction_expire 레코드 삭제
- int deleted =
- repository.deleteBySubCategoryAndDateAuctionExpire(category, dateAuctionExpire);
- log.info(
- "[REALTIME] [{}] Deleted {} records with date_auction_expire={}",
- category.getSubCategory(),
- deleted,
- dateAuctionExpire);
-
- // 새 엔티티 저장
repository.saveAll(entities);
log.info(
"[REALTIME] [{}] Saved {} new auction realtime items",
@@ -89,20 +79,6 @@ public void deleteAndSave(
entities.size());
}
- /**
- * 엔티티들을 저장한다. (삭제 없이)
- *
- * @param entities 저장할 엔티티 리스트
- */
- @Transactional
- public void saveAll(List entities) {
- if (entities == null || entities.isEmpty()) {
- return;
- }
- repository.saveAll(entities);
- log.debug("[REALTIME] Saved {} auction realtime items", entities.size());
- }
-
/**
* 만료된 아이템을 삭제한다.
*
diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java
index 1bd26535..b2da4f82 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcher.java
@@ -1,34 +1,32 @@
package until.the.eternity.auctionrealtime.application.service.fetcher;
-import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
-import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker;
-import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker.DuplicateCheckResult;
import until.the.eternity.auctionrealtime.domain.service.fetcher.AuctionRealtimeFetcherPort;
import until.the.eternity.auctionrealtime.infrastructure.client.AuctionRealtimeClient;
import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeListResponse;
import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse;
import until.the.eternity.common.enums.ItemCategory;
-/** 실시간 경매장 데이터 Fetcher 구현체. Cursor 기반 페이징으로 API를 호출하고, 중복 감지 시 호출을 중단한다. */
+/**
+ * 실시간 경매장 데이터 Fetcher 구현체.
+ *
+ * Cursor 기반 페이징으로 API를 호출하여 해당 카테고리의 전체 데이터를 수집한다.
+ */
@Slf4j
@Component
@RequiredArgsConstructor
public class AuctionRealtimeFetcher implements AuctionRealtimeFetcherPort {
private final AuctionRealtimeClient client;
- private final AuctionRealtimeDuplicateChecker duplicateChecker;
@Override
public FetchResult fetch(ItemCategory category) {
List result = new ArrayList<>();
String cursor = "";
- boolean hasEqualDate = false;
- Instant latestDate = null;
while (true) {
OpenApiAuctionRealtimeListResponse response =
@@ -51,39 +49,7 @@ public FetchResult fetch(ItemCategory category) {
break;
}
- List batch = response.auctionItems();
-
- // 중복 체크
- DuplicateCheckResult checkResult =
- duplicateChecker.checkDuplicateInBatch(batch, category);
-
- if (checkResult.isDuplicate()) {
- int index = checkResult.duplicateIndex();
- latestDate = checkResult.latestDate();
- hasEqualDate = checkResult.hasEqualDate();
-
- if (index > 0) {
- result.addAll(batch.subList(0, index));
- }
-
- if (hasEqualDate) {
- log.debug(
- "[REALTIME] [{}] equal date found at index {}, need to delete and re-save, added {} items",
- category.getSubCategory(),
- index,
- index);
- } else {
- log.debug(
- "[REALTIME] [{}] duplicate found at index {}, added {} items before duplicate",
- category.getSubCategory(),
- index,
- index);
- }
- break;
- }
-
- latestDate = checkResult.latestDate();
- result.addAll(batch);
+ result.addAll(response.auctionItems());
cursor = response.nextCursor();
@@ -95,6 +61,6 @@ public FetchResult fetch(ItemCategory category) {
}
}
- return new FetchResult(result, hasEqualDate, latestDate);
+ return new FetchResult(result);
}
}
diff --git a/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java b/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java
index 8132c675..10e0a9fc 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/application/service/persister/AuctionRealtimePersister.java
@@ -1,13 +1,11 @@
package until.the.eternity.auctionrealtime.application.service.persister;
-import java.time.Instant;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem;
import until.the.eternity.auctionrealtime.domain.mapper.OpenApiAuctionRealtimeMapper;
-import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker;
import until.the.eternity.auctionrealtime.domain.service.persister.AuctionRealtimePersisterPort;
import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse;
import until.the.eternity.common.enums.ItemCategory;
@@ -19,20 +17,13 @@
public class AuctionRealtimePersister implements AuctionRealtimePersisterPort {
private final OpenApiAuctionRealtimeMapper mapper;
- private final AuctionRealtimeDuplicateChecker duplicateChecker;
@Override
public List prepareEntities(
- List dtoList,
- ItemCategory category,
- Instant latestDate) {
-
- // 저장 가능한 데이터만 필터링
- List filtered =
- duplicateChecker.filterForSave(dtoList, latestDate);
+ List dtoList, ItemCategory category) {
// Entity 변환
- List entities = mapper.toEntityList(filtered, category);
+ List entities = mapper.toEntityList(dtoList, category);
// 아이템 옵션 링크 설정
entities.forEach(AuctionRealtimeItem::linkItemOptions);
diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java
index 56513424..1ff1e35a 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/domain/repository/AuctionRealtimeItemRepositoryPort.java
@@ -30,21 +30,12 @@ public interface AuctionRealtimeItemRepositoryPort {
Optional findById(Long id);
/**
- * 해당 subcategory의 최신 date_auction_expire를 조회한다.
+ * 해당 subcategory의 모든 레코드를 삭제한다.
*
* @param category 아이템 카테고리
- * @return 최신 date_auction_expire (없으면 Optional.empty())
- */
- Optional findLatestDateAuctionExpireBySubCategory(ItemCategory category);
-
- /**
- * 해당 subcategory & date_auction_expire에 해당하는 모든 레코드를 삭제한다.
- *
- * @param category 아이템 카테고리
- * @param dateAuctionExpire 만료 시각
* @return 삭제된 레코드 수
*/
- int deleteBySubCategoryAndDateAuctionExpire(ItemCategory category, Instant dateAuctionExpire);
+ int deleteBySubCategory(ItemCategory category);
/**
* date_auction_expire가 현재 시각보다 이전인 모든 레코드를 삭제한다.
diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java
deleted file mode 100644
index 54c24f6d..00000000
--- a/src/main/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateChecker.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package until.the.eternity.auctionrealtime.domain.service;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort;
-import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse;
-import until.the.eternity.common.enums.ItemCategory;
-
-/**
- * 실시간 경매장 데이터의 중복 체크 로직.
- *
- * 중복 판단 기준:
- *
- *
- * - date_auction_expire < latestDate → 중복 (과거 데이터)
- *
- date_auction_expire == latestDate → 해당 시간대 데이터 삭제 후 재저장 필요
- *
- date_auction_expire > latestDate → 신규 데이터
- *
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class AuctionRealtimeDuplicateChecker {
-
- private final AuctionRealtimeItemRepositoryPort repository;
-
- /**
- * 중복 체크 결과.
- *
- * @param isDuplicate 중복 여부
- * @param hasEqualDate 동일 날짜 데이터 존재 여부 (삭제 후 재저장 필요)
- * @param duplicateIndex 중복이 발생한 첫 번째 인덱스 (-1이면 중복 없음)
- * @param latestDate DB의 최신 date_auction_expire
- */
- public record DuplicateCheckResult(
- boolean isDuplicate, boolean hasEqualDate, int duplicateIndex, Instant latestDate) {
-
- public static DuplicateCheckResult noDuplicate() {
- return new DuplicateCheckResult(false, false, -1, null);
- }
-
- public static DuplicateCheckResult noDuplicateWithLatestDate(Instant latestDate) {
- return new DuplicateCheckResult(false, false, -1, latestDate);
- }
-
- public static DuplicateCheckResult duplicateFound(int index, Instant latestDate) {
- return new DuplicateCheckResult(true, false, index, latestDate);
- }
-
- public static DuplicateCheckResult equalDateFound(int index, Instant latestDate) {
- return new DuplicateCheckResult(true, true, index, latestDate);
- }
- }
-
- /**
- * 배치에서 중복 또는 동일 날짜 데이터를 체크한다.
- *
- * @param batch 검사할 배치 데이터
- * @param category 아이템 카테고리
- * @return 중복 체크 결과
- */
- public DuplicateCheckResult checkDuplicateInBatch(
- List batch, ItemCategory category) {
- if (batch == null || batch.isEmpty()) {
- return DuplicateCheckResult.noDuplicate();
- }
-
- Optional latestDateOpt =
- repository.findLatestDateAuctionExpireBySubCategory(category);
-
- if (latestDateOpt.isEmpty()) {
- // DB에 데이터가 없으면 모두 신규
- return DuplicateCheckResult.noDuplicate();
- }
-
- Instant latestDate = latestDateOpt.get();
-
- for (int i = 0; i < batch.size(); i++) {
- OpenApiAuctionRealtimeResponse dto = batch.get(i);
- Instant dtoDate = dto.dateAuctionExpire();
-
- if (dtoDate == null) {
- continue;
- }
-
- if (dtoDate.isBefore(latestDate)) {
- // 과거 데이터 → 중복
- return DuplicateCheckResult.duplicateFound(i, latestDate);
- }
-
- if (dtoDate.equals(latestDate)) {
- // 동일 날짜 → 삭제 후 재저장 필요
- return DuplicateCheckResult.equalDateFound(i, latestDate);
- }
- }
-
- // 모든 데이터가 latestDate보다 이후
- return DuplicateCheckResult.noDuplicateWithLatestDate(latestDate);
- }
-
- /**
- * API 응답 데이터를 필터링하여 저장 가능한 데이터만 반환한다.
- *
- * @param dtos 필터링할 DTO 리스트
- * @param latestDate DB의 최신 date_auction_expire (null이면 모두 저장)
- * @return 저장할 데이터 리스트
- */
- public List filterForSave(
- List dtos, Instant latestDate) {
- if (dtos == null || dtos.isEmpty()) {
- return List.of();
- }
-
- if (latestDate == null) {
- return dtos;
- }
-
- return dtos.stream()
- .filter(
- dto -> {
- Instant dtoDate = dto.dateAuctionExpire();
- // latestDate 이후 또는 동일한 날짜만 저장
- return dtoDate != null && !dtoDate.isBefore(latestDate);
- })
- .toList();
- }
-}
diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java
index 8f31c645..31220874 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/fetcher/AuctionRealtimeFetcherPort.java
@@ -1,6 +1,5 @@
package until.the.eternity.auctionrealtime.domain.service.fetcher;
-import java.time.Instant;
import java.util.List;
import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse;
import until.the.eternity.common.enums.ItemCategory;
@@ -12,19 +11,16 @@ public interface AuctionRealtimeFetcherPort {
* 페칭 결과.
*
* @param items 수집된 아이템 리스트
- * @param hasEqualDate 동일 날짜 데이터 존재 여부 (삭제 후 재저장 필요)
- * @param latestDate DB의 최신 date_auction_expire
*/
- record FetchResult(
- List items, boolean hasEqualDate, Instant latestDate) {
+ record FetchResult(List items) {
public static FetchResult empty() {
- return new FetchResult(List.of(), false, null);
+ return new FetchResult(List.of());
}
}
/**
- * 해당 카테고리의 실시간 경매장 데이터를 수집한다.
+ * 해당 카테고리의 실시간 경매장 데이터를 전체 수집한다.
*
* @param category 아이템 카테고리
* @return 페칭 결과
diff --git a/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java b/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java
index 265327f7..41357a28 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/domain/service/persister/AuctionRealtimePersisterPort.java
@@ -1,6 +1,5 @@
package until.the.eternity.auctionrealtime.domain.service.persister;
-import java.time.Instant;
import java.util.List;
import until.the.eternity.auctionitem.domain.entity.AuctionRealtimeItem;
import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse;
@@ -10,15 +9,12 @@
public interface AuctionRealtimePersisterPort {
/**
- * API 응답 데이터를 Entity로 변환하고 필터링한다.
+ * API 응답 데이터를 Entity로 변환한다.
*
* @param dtoList API 응답 DTO 리스트
* @param category 아이템 카테고리
- * @param latestDate DB의 최신 date_auction_expire (null이면 모두 저장)
* @return 저장할 Entity 리스트
*/
List prepareEntities(
- List dtoList,
- ItemCategory category,
- Instant latestDate);
+ List dtoList, ItemCategory category);
}
diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java
index 399678c1..53cbdbf4 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepository.java
@@ -1,7 +1,6 @@
package until.the.eternity.auctionrealtime.infrastructure.persistence;
import java.time.Instant;
-import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@@ -11,22 +10,9 @@
/** AuctionRealtimeItem JPA Repository. */
public interface AuctionRealtimeItemRepository extends JpaRepository {
- @Query(
- value =
- "SELECT date_auction_expire FROM auction_realtime_item "
- + "WHERE item_sub_category = :subCategory "
- + "ORDER BY date_auction_expire DESC LIMIT 1",
- nativeQuery = true)
- Optional findLatestDateAuctionExpireBySubCategory(
- @Param("subCategory") String subCategory);
-
@Modifying
- @Query(
- "DELETE FROM AuctionRealtimeItem a "
- + "WHERE a.itemSubCategory = :subCategory AND a.dateAuctionExpire = :dateAuctionExpire")
- int deleteBySubCategoryAndDateAuctionExpire(
- @Param("subCategory") String subCategory,
- @Param("dateAuctionExpire") Instant dateAuctionExpire);
+ @Query("DELETE FROM AuctionRealtimeItem a " + "WHERE a.itemSubCategory = :subCategory")
+ int deleteBySubCategory(@Param("subCategory") String subCategory);
@Modifying
@Query("DELETE FROM AuctionRealtimeItem a WHERE a.dateAuctionExpire < :now")
diff --git a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java
index 4e9fca60..6da0da26 100644
--- a/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java
+++ b/src/main/java/until/the/eternity/auctionrealtime/infrastructure/persistence/AuctionRealtimeItemRepositoryPortImpl.java
@@ -38,15 +38,8 @@ public Optional findById(Long id) {
}
@Override
- public Optional findLatestDateAuctionExpireBySubCategory(ItemCategory category) {
- return jpaRepository.findLatestDateAuctionExpireBySubCategory(category.getSubCategory());
- }
-
- @Override
- public int deleteBySubCategoryAndDateAuctionExpire(
- ItemCategory category, Instant dateAuctionExpire) {
- return jpaRepository.deleteBySubCategoryAndDateAuctionExpire(
- category.getSubCategory(), dateAuctionExpire);
+ public int deleteBySubCategory(ItemCategory category) {
+ return jpaRepository.deleteBySubCategory(category.getSubCategory());
}
@Override
diff --git a/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java b/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java
index 3c2c7bb3..cb22f7b3 100644
--- a/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java
+++ b/src/test/java/until/the/eternity/auctionrealtime/application/service/fetcher/AuctionRealtimeFetcherTest.java
@@ -15,8 +15,6 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
-import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker;
-import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker.DuplicateCheckResult;
import until.the.eternity.auctionrealtime.domain.service.fetcher.AuctionRealtimeFetcherPort.FetchResult;
import until.the.eternity.auctionrealtime.infrastructure.client.AuctionRealtimeClient;
import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeListResponse;
@@ -28,8 +26,6 @@ class AuctionRealtimeFetcherTest {
@Mock AuctionRealtimeClient client;
- @Mock AuctionRealtimeDuplicateChecker duplicateChecker;
-
@InjectMocks AuctionRealtimeFetcher fetcher;
private OpenApiAuctionRealtimeResponse dummy() {
@@ -38,8 +34,8 @@ private OpenApiAuctionRealtimeResponse dummy() {
}
@Nested
- @DisplayName("OPEN API 끝까지 호출 시나리오")
- class NormalFlow {
+ @DisplayName("전체 페이지 수집 시나리오")
+ class FullFetchFlow {
@Test
@DisplayName("모든 페이지를 수집하고 cursor가 null이면 종료한다")
@@ -52,114 +48,42 @@ void fetchAllPages() {
when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-1"))
.thenReturn(Mono.just(page2));
- when(duplicateChecker.checkDuplicateInBatch(any(), eq(ItemCategory.SWORD)))
- .thenReturn(DuplicateCheckResult.noDuplicate());
// when
FetchResult result = fetcher.fetch(ItemCategory.SWORD);
// then
assertThat(result.items()).hasSize(3);
- assertThat(result.hasEqualDate()).isFalse();
-
verify(client, times(2)).fetchAuctionList(eq(ItemCategory.SWORD), any());
- verify(duplicateChecker, times(2)).checkDuplicateInBatch(any(), eq(ItemCategory.SWORD));
}
- }
-
- @Nested
- @DisplayName("OPEN API 호출 중단 시나리오")
- class EarlyBreakFlow {
@Test
- @DisplayName("첫 배치 첫 항목에서 중복이면 빈 리스트를 반환한다")
- void stopOnDuplicateAtFirstItem() {
+ @DisplayName("3페이지 이상도 끝까지 수집한다")
+ void fetchMultiplePages() {
// given
var page1 =
new OpenApiAuctionRealtimeListResponse(List.of(dummy(), dummy()), "cursor-1");
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
-
- when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
- when(duplicateChecker.checkDuplicateInBatch(page1.auctionItems(), ItemCategory.SWORD))
- .thenReturn(DuplicateCheckResult.duplicateFound(0, latestDate));
-
- // when
- FetchResult result = fetcher.fetch(ItemCategory.SWORD);
-
- // then
- assertThat(result.items()).isEmpty();
- assertThat(result.latestDate()).isEqualTo(latestDate);
- verify(client, times(1)).fetchAuctionList(ItemCategory.SWORD, "");
- verifyNoMoreInteractions(client);
- }
-
- @Test
- @DisplayName("첫 배치 중간에서 중복이면 중복 전까지만 반환한다")
- void stopOnDuplicateAtMiddle() {
- // given
- var batch = List.of(dummy(), dummy(), dummy());
- var page1 = new OpenApiAuctionRealtimeListResponse(batch, "cursor-1");
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
-
- when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
- when(duplicateChecker.checkDuplicateInBatch(batch, ItemCategory.SWORD))
- .thenReturn(DuplicateCheckResult.duplicateFound(2, latestDate));
-
- // when
- FetchResult result = fetcher.fetch(ItemCategory.SWORD);
-
- // then
- assertThat(result.items()).hasSize(2);
- verify(client, times(1)).fetchAuctionList(ItemCategory.SWORD, "");
- verifyNoMoreInteractions(client);
- }
-
- @Test
- @DisplayName("동일 날짜 데이터가 감지되면 hasEqualDate가 true로 반환된다")
- void stopOnEqualDate() {
- // given
- var batch = List.of(dummy(), dummy());
- var page1 = new OpenApiAuctionRealtimeListResponse(batch, "cursor-1");
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
-
- when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
- when(duplicateChecker.checkDuplicateInBatch(batch, ItemCategory.SWORD))
- .thenReturn(DuplicateCheckResult.equalDateFound(1, latestDate));
-
- // when
- FetchResult result = fetcher.fetch(ItemCategory.SWORD);
-
- // then
- assertThat(result.items()).hasSize(1);
- assertThat(result.hasEqualDate()).isTrue();
- assertThat(result.latestDate()).isEqualTo(latestDate);
- }
-
- @Test
- @DisplayName("두 번째 배치에서 중복이면 첫 배치 전체 + 중복 전까지만 반환한다")
- void stopOnDuplicateAtSecondBatch() {
- // given
- var batch1 = List.of(dummy(), dummy());
- var batch2 = List.of(dummy(), dummy(), dummy());
- var page1 = new OpenApiAuctionRealtimeListResponse(batch1, "cursor-1");
- var page2 = new OpenApiAuctionRealtimeListResponse(batch2, "cursor-2");
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
+ var page2 = new OpenApiAuctionRealtimeListResponse(List.of(dummy()), "cursor-2");
+ var page3 = new OpenApiAuctionRealtimeListResponse(List.of(dummy(), dummy()), null);
when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-1"))
.thenReturn(Mono.just(page2));
- when(duplicateChecker.checkDuplicateInBatch(batch1, ItemCategory.SWORD))
- .thenReturn(DuplicateCheckResult.noDuplicate());
- when(duplicateChecker.checkDuplicateInBatch(batch2, ItemCategory.SWORD))
- .thenReturn(DuplicateCheckResult.duplicateFound(1, latestDate));
+ when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-2"))
+ .thenReturn(Mono.just(page3));
// when
FetchResult result = fetcher.fetch(ItemCategory.SWORD);
// then
- assertThat(result.items()).hasSize(3); // 2 from batch1 + 1 from batch2
- verify(client, times(2)).fetchAuctionList(eq(ItemCategory.SWORD), any());
+ assertThat(result.items()).hasSize(5);
+ verify(client, times(3)).fetchAuctionList(eq(ItemCategory.SWORD), any());
}
+ }
+
+ @Nested
+ @DisplayName("수집 중단 시나리오")
+ class EarlyBreakFlow {
@Test
@DisplayName("첫 응답이 null(Mono.empty)이면 빈 리스트를 반환한다")
@@ -169,7 +93,6 @@ void responseNull() {
FetchResult result = fetcher.fetch(ItemCategory.SWORD);
assertThat(result.items()).isEmpty();
- verify(duplicateChecker, never()).checkDuplicateInBatch(any(), any());
}
@Test
@@ -182,7 +105,6 @@ void auctionItemsEmpty() {
FetchResult result = fetcher.fetch(ItemCategory.SWORD);
assertThat(result.items()).isEmpty();
- verify(duplicateChecker, never()).checkDuplicateInBatch(any(), any());
}
@Test
@@ -191,8 +113,6 @@ void stopWhenNextCursorIsEmptyString() {
// given
var page1 = new OpenApiAuctionRealtimeListResponse(List.of(dummy()), "");
when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
- when(duplicateChecker.checkDuplicateInBatch(any(), eq(ItemCategory.SWORD)))
- .thenReturn(DuplicateCheckResult.noDuplicate());
// when
FetchResult result = fetcher.fetch(ItemCategory.SWORD);
@@ -213,8 +133,6 @@ void stopWhenMiddlePageIsEmpty() {
when(client.fetchAuctionList(ItemCategory.SWORD, "")).thenReturn(Mono.just(page1));
when(client.fetchAuctionList(ItemCategory.SWORD, "cursor-1"))
.thenReturn(Mono.just(emptyPage));
- when(duplicateChecker.checkDuplicateInBatch(any(), eq(ItemCategory.SWORD)))
- .thenReturn(DuplicateCheckResult.noDuplicate());
// when
FetchResult result = fetcher.fetch(ItemCategory.SWORD);
@@ -240,7 +158,6 @@ void stopWhenAuctionItemsListIsNull() {
assertThat(result.items()).isEmpty();
verify(client, times(1)).fetchAuctionList(eq(ItemCategory.SWORD), any());
verifyNoMoreInteractions(client);
- verify(duplicateChecker, never()).checkDuplicateInBatch(any(), any());
}
}
}
diff --git a/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java b/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java
deleted file mode 100644
index b731130b..00000000
--- a/src/test/java/until/the/eternity/auctionrealtime/domain/service/AuctionRealtimeDuplicateCheckerTest.java
+++ /dev/null
@@ -1,237 +0,0 @@
-package until.the.eternity.auctionrealtime.domain.service;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.when;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import until.the.eternity.auctionrealtime.domain.repository.AuctionRealtimeItemRepositoryPort;
-import until.the.eternity.auctionrealtime.domain.service.AuctionRealtimeDuplicateChecker.DuplicateCheckResult;
-import until.the.eternity.auctionrealtime.interfaces.external.dto.OpenApiAuctionRealtimeResponse;
-import until.the.eternity.common.enums.ItemCategory;
-
-@ExtendWith(MockitoExtension.class)
-class AuctionRealtimeDuplicateCheckerTest {
-
- @Mock AuctionRealtimeItemRepositoryPort repository;
-
- @InjectMocks AuctionRealtimeDuplicateChecker checker;
-
- private static final ItemCategory CATEGORY = ItemCategory.SWORD;
-
- private OpenApiAuctionRealtimeResponse dto(Instant dateAuctionExpire) {
- return new OpenApiAuctionRealtimeResponse(
- "페러시우스 타이탄 블레이드", "신성한 페러시우스 타이탄 블레이드", 1L, 100L, dateAuctionExpire, null);
- }
-
- @Nested
- @DisplayName("checkDuplicateInBatch 테스트")
- class CheckDuplicateInBatchTest {
-
- @Test
- @DisplayName("DB에 데이터가 없으면 중복 없음으로 판정")
- void noDuplicateWhenNoDataInDb() {
- // given
- Instant now = Instant.now();
- var batch = List.of(dto(now), dto(now.minusSeconds(10)));
- when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY))
- .thenReturn(Optional.empty());
-
- // when
- DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY);
-
- // then
- assertThat(result.isDuplicate()).isFalse();
- assertThat(result.hasEqualDate()).isFalse();
- assertThat(result.latestDate()).isNull();
- }
-
- @Test
- @DisplayName("빈 배치이면 중복 없음으로 판정")
- void noDuplicateWhenEmptyBatch() {
- // given
- List batch = List.of();
-
- // when
- DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY);
-
- // then
- assertThat(result.isDuplicate()).isFalse();
- assertThat(result.hasEqualDate()).isFalse();
- }
-
- @Test
- @DisplayName("모든 데이터가 latestDate 이후면 중복 없음")
- void noDuplicateWhenAllDataAfterLatestDate() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- Instant afterLatest = latestDate.plusSeconds(100);
- var batch = List.of(dto(afterLatest), dto(afterLatest.plusSeconds(10)));
-
- when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY))
- .thenReturn(Optional.of(latestDate));
-
- // when
- DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY);
-
- // then
- assertThat(result.isDuplicate()).isFalse();
- assertThat(result.latestDate()).isEqualTo(latestDate);
- }
-
- @Test
- @DisplayName("중간에 latestDate 이전 데이터가 있으면 해당 인덱스와 중복 반환")
- void duplicateFoundWhenDataBeforeLatestDate() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- Instant afterLatest = latestDate.plusSeconds(100);
- Instant beforeLatest = latestDate.minusSeconds(100);
- var batch = List.of(dto(afterLatest), dto(afterLatest), dto(beforeLatest));
-
- when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY))
- .thenReturn(Optional.of(latestDate));
-
- // when
- DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY);
-
- // then
- assertThat(result.isDuplicate()).isTrue();
- assertThat(result.hasEqualDate()).isFalse();
- assertThat(result.duplicateIndex()).isEqualTo(2);
- assertThat(result.latestDate()).isEqualTo(latestDate);
- }
-
- @Test
- @DisplayName("동일 날짜 데이터가 있으면 equalDateFound 반환")
- void equalDateFoundWhenSameDate() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- Instant afterLatest = latestDate.plusSeconds(100);
- var batch = List.of(dto(afterLatest), dto(latestDate));
-
- when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY))
- .thenReturn(Optional.of(latestDate));
-
- // when
- DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY);
-
- // then
- assertThat(result.isDuplicate()).isTrue();
- assertThat(result.hasEqualDate()).isTrue();
- assertThat(result.duplicateIndex()).isEqualTo(1);
- assertThat(result.latestDate()).isEqualTo(latestDate);
- }
-
- @Test
- @DisplayName("첫 번째 항목이 과거면 인덱스 0 반환")
- void duplicateAtFirstIndex() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- Instant beforeLatest = latestDate.minusSeconds(100);
- var batch = List.of(dto(beforeLatest), dto(latestDate));
-
- when(repository.findLatestDateAuctionExpireBySubCategory(CATEGORY))
- .thenReturn(Optional.of(latestDate));
-
- // when
- DuplicateCheckResult result = checker.checkDuplicateInBatch(batch, CATEGORY);
-
- // then
- assertThat(result.isDuplicate()).isTrue();
- assertThat(result.duplicateIndex()).isEqualTo(0);
- }
- }
-
- @Nested
- @DisplayName("filterForSave 테스트")
- class FilterForSaveTest {
-
- @Test
- @DisplayName("latestDate가 null이면 모든 데이터 반환")
- void returnAllWhenLatestDateNull() {
- // given
- Instant now = Instant.now();
- var dtos = List.of(dto(now), dto(now.minusSeconds(10)));
-
- // when
- var result = checker.filterForSave(dtos, null);
-
- // then
- assertThat(result).hasSize(2);
- }
-
- @Test
- @DisplayName("빈 리스트이면 빈 리스트 반환")
- void returnEmptyWhenEmptyList() {
- // given
- List dtos = List.of();
-
- // when
- var result = checker.filterForSave(dtos, Instant.now());
-
- // then
- assertThat(result).isEmpty();
- }
-
- @Test
- @DisplayName("latestDate 이전 데이터는 필터링됨")
- void filterOutDataBeforeLatestDate() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- Instant afterLatest = latestDate.plusSeconds(100);
- Instant beforeLatest = latestDate.minusSeconds(100);
- var dtos = List.of(dto(afterLatest), dto(beforeLatest), dto(afterLatest));
-
- // when
- var result = checker.filterForSave(dtos, latestDate);
-
- // then
- assertThat(result).hasSize(2);
- }
-
- @Test
- @DisplayName("동일 날짜 데이터는 포함됨")
- void includeSameDateData() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- var dtos = List.of(dto(latestDate), dto(latestDate));
-
- // when
- var result = checker.filterForSave(dtos, latestDate);
-
- // then
- assertThat(result).hasSize(2);
- }
-
- @Test
- @DisplayName("복합 시나리오: 이전/동일/이후 데이터")
- void complexScenario() {
- // given
- Instant latestDate = Instant.parse("2024-01-01T00:00:00Z");
- Instant afterLatest = latestDate.plusSeconds(100);
- Instant beforeLatest = latestDate.minusSeconds(100);
-
- var dtos =
- List.of(
- dto(afterLatest), // 신규: 포함
- dto(beforeLatest), // 과거: 제외
- dto(latestDate), // 동일 날짜: 포함
- dto(afterLatest) // 신규: 포함
- );
-
- // when
- var result = checker.filterForSave(dtos, latestDate);
-
- // then
- assertThat(result).hasSize(3);
- }
- }
-}