From b397e1cd0c4dd24581ae851e61855e1755e34751 Mon Sep 17 00:00:00 2001 From: passionryu Date: Thu, 24 Jul 2025 23:45:23 +0900 Subject: [PATCH] =?UTF-8?q?=20=F0=9F=9A=91=EF=B8=8F=20[=EB=B0=B0=ED=8F=AC?= =?UTF-8?q?=20=ED=9B=84=20=EA=B8=B4=EA=B8=89=20=EC=88=98=EC=A0=95]=20:=20?= =?UTF-8?q?=EB=89=B4=EC=8A=A4=20=EB=AA=A8=EB=8B=88=ED=84=B0=EB=A7=81=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EC=9D=98=20Count=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20+=20=ED=95=AB=ED=86=A0?= =?UTF-8?q?=ED=94=BD=EC=9D=98=20=EB=89=B4=EC=8A=A4=20=EB=8D=94=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EC=9D=98=20=EC=B5=9C?= =?UTF-8?q?=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ElasticSearch/service/NewsEsService.java | 70 +++++++++- .../IntermediateBatchRedisService.java | 8 ++ .../Service/NewsMonitoringService.java | 129 ++++++++++++++---- 3 files changed, 170 insertions(+), 37 deletions(-) diff --git a/SpringBoot/src/main/java/Baemin/News_Deliver/Global/News/ElasticSearch/service/NewsEsService.java b/SpringBoot/src/main/java/Baemin/News_Deliver/Global/News/ElasticSearch/service/NewsEsService.java index f5e02f9..c46daf3 100644 --- a/SpringBoot/src/main/java/Baemin/News_Deliver/Global/News/ElasticSearch/service/NewsEsService.java +++ b/SpringBoot/src/main/java/Baemin/News_Deliver/Global/News/ElasticSearch/service/NewsEsService.java @@ -169,24 +169,38 @@ private boolean bulkInsert(List docs, String section) throws IOE } /** - * 키워드로 뉴스 기사 검색 + * 어제 날짜 기준으로 뉴스 기사 검색을 수행합니다. * *

{@code combinedTokens} 필드에 대해 match 쿼리를 수행하고, - * 점수(score) 기준으로 내림차순 정렬된 상위 결과를 반환합니다.

+ * {@code publishedAt} 필드가 어제(00:00~24:00)인 데이터만 필터링합니다. + * 결과는 점수(score) 기준으로 내림차순 정렬되어 반환됩니다.

* * @param keyword 검색 키워드 - * @param size 최대 검색 결과 수 - * @return 검색된 뉴스 도큐먼트 리스트 + * @param size 최대 검색 결과 수 + * @return 어제 날짜 중 키워드를 포함하는 뉴스 도큐먼트 리스트 * @throws IOException Elasticsearch 검색 실패 시 발생 */ public List searchByKeyword(String keyword, int size) throws IOException { + LocalDate today = LocalDate.now(); + LocalDate yesterday = today.minusDays(1); + + String from = yesterday.atStartOfDay().toString(); // 어제 00:00:00 + String to = today.atStartOfDay().toString(); // 오늘 00:00:00 + SearchResponse response = elasticsearchClient.search(s -> s .index("news-index-nori") .size(size) .query(q -> q - .match(m -> m - .field("combinedTokens") - .query(keyword) + .bool(b -> b + .must(m -> m.match(mm -> mm + .field("combinedTokens") + .query(keyword) + )) + .filter(f -> f.range(r -> r + .field("publishedAt") + .gte(JsonData.of(from)) + .lt(JsonData.of(to)) + )) ) ) .sort(sort -> sort @@ -235,4 +249,46 @@ public List getTopKeywordsForDateRange(LocalDate gte, LocalDa .buckets() .array(); } + + // ======================= Deprecated ========================= + + /** + * Hot Fix + * + * Deprecated At 2025-07-24 + * By 김원중 + * + */ +// /** +// * 키워드로 뉴스 기사 검색 +// * +// *

{@code combinedTokens} 필드에 대해 match 쿼리를 수행하고, +// * 점수(score) 기준으로 내림차순 정렬된 상위 결과를 반환합니다.

+// * +// * @param keyword 검색 키워드 +// * @param size 최대 검색 결과 수 +// * @return 검색된 뉴스 도큐먼트 리스트 +// * @throws IOException Elasticsearch 검색 실패 시 발생 +// */ +// public List searchByKeyword(String keyword, int size) throws IOException { +// SearchResponse response = elasticsearchClient.search(s -> s +// .index("news-index-nori") +// .size(size) +// .query(q -> q +// .match(m -> m +// .field("combinedTokens") +// .query(keyword) +// ) +// ) +// .sort(sort -> sort +// .score(sc -> sc.order(SortOrder.Desc)) +// ), +// NewsEsDocument.class +// ); +// +// return response.hits().hits().stream() +// .map(Hit::source) +// .filter(Objects::nonNull) +// .toList(); +// } } diff --git a/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/IntermediateBatchRedisService.java b/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/IntermediateBatchRedisService.java index 2499c3c..c55003b 100644 --- a/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/IntermediateBatchRedisService.java +++ b/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/IntermediateBatchRedisService.java @@ -22,6 +22,14 @@ public IntermediateBatchRedisService(@Qualifier("redisSession1Template") RedisTe private static final String BATCH_KEY_PREFIX = "IntermediateBatch:"; + public boolean isBatchAlreadyDone(String key) { + return redisSession1Template.hasKey(key); + } + + public void markBatchDone(String key) { + redisSession1Template.opsForValue().set(key, "done", Duration.ofHours(12)); // 12 + } + /** * 중간 배치 실행 횟수를 조회 메서드 * diff --git a/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/NewsMonitoringService.java b/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/NewsMonitoringService.java index d890e3a..441ac5e 100644 --- a/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/NewsMonitoringService.java +++ b/SpringBoot/src/main/java/Baemin/News_Deliver/Global/NewsMonitoring/Service/NewsMonitoringService.java @@ -27,63 +27,132 @@ public class NewsMonitoringService { private final JobLauncher jobLauncher; private final Job newsDataSaveJob_Monitoring; - /** - * 매 정각마다 실행 되는 모니터링 스케줄러 - * - * 각 섹션 별 현재 몇개의 데이터가 모였는지 모니터링 - * 9000개가 넘는 섹션 발견 시, 즉시 DB에 Batch 실시 - * - */ @Scheduled(cron = "0 0 * * * *") // 매 시간 정각 //@Scheduled(cron = "0 */5 * * * *") // 5분 - // @Scheduled(cron = "0 */3 * * * *") // 3분 + //@Scheduled(cron = "0 */3 * * * *") // 3분 //@Scheduled(cron = "0 */2 * * * *") // 2분 //@Scheduled(cron = "0 * * * * *") // 1분 - public void monitoring(){ + public void monitoring() { /* 날짜 & 시간 확인 */ LocalDate day = LocalDate.now(); // 오늘의 날짜 LocalDateTime time = LocalDateTime.now(); // 현재 날짜와 시간(분 단위 포함) - log.info("// ======================= 오늘의 날짜 : {} =========================", day); // 오늘의 날짜 로그 확인 + log.info("// ======================= 오늘의 날짜 : {} =========================", day); log.info("현재 시간 : {}", time); + // 자정(00시)이면 스킵 + if (time.getHour() == 0) { + log.info("[SKIP] 자정 00시에는 모니터링 작업을 실행하지 않습니다."); + return; + } + /* 날짜 포매팅 작업 */ - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); //원하는 포맷 지정 - String dateTo = day.format(formatter); //문자열로 변환 - String dateFrom = day.format(formatter); //문자열로 변환 + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + String dateStr = day.format(formatter); /* 섹션 별 누적 데이터 수 모니터링 로직 */ for (String section : sections) { - // 현재 섹션의 현 시각 누적 Total_Item 수 확인 - int total_items = newsMonitoringManager.getTotalItems(section, dateFrom, dateTo); + int total_items = newsMonitoringManager.getTotalItems(section, dateStr, dateStr); + String batchDoneKey = "batch_done:" + dateStr + ":" + section; // 하루 1회 체크용 + String batchCountKey = "batch_count:" + dateStr + ":" + section; // 하루 횟수 제한용 - /* 뉴스 데이터 9000개 이상 시 중간 배치 + Redis 기록 */ - if(total_items >= 9000 && total_items <= 18000){ + /* =================== 9000 이상 18000 이하일 경우 =================== */ + if (total_items >= 9000 && total_items <= 18000) { - // 해당 섹션이 이전에도 사전 Batch 작업이 실행 되었는지 확인 (Redis에서 횟수 조회) - int n = intermediateBatchRedisService.getBatchCount(section); - log.info("{} 섹션 기존 중간 배치 현황 : 총 {}회 중간 배치 ",section, n); + // 하루 한 번만 수행: 이미 했는지 Redis에 확인 + if (intermediateBatchRedisService.isBatchAlreadyDone(batchDoneKey)) { + log.info("[SKIP] {} 섹션은 이미 {}에 중간 배치가 수행됨", section, dateStr); + continue; + } - // 현재 total_item이 기준(9000개)을 넘으면 Job Launcher로 Batch 작업 진행 메서드 호출 - runNewsBatch(section); + int prevCount = intermediateBatchRedisService.getBatchCount(batchCountKey); + log.info("{} 섹션 기존 중간 배치 현황 : 총 {}회 중간 배치", section, prevCount); - // 중간 배치가 진행된 섹션 배치 현황 기록 (Redis에 횟수 증가) - intermediateBatchRedisService.incrementBatchCount(section); + runNewsBatch(section); // 배치 실행 - } /* 2번째 중간 배치 부터 주의 메시지 호출 */ - else if(total_items >= 18000){ + intermediateBatchRedisService.incrementBatchCount(batchCountKey); // 횟수 증가 + intermediateBatchRedisService.markBatchDone(batchDoneKey); // 하루 1회 완료 기록 - // Job Launcher로 Batch 작업 진행 메서드 호출 - runNewsBatch(section); + } - // 현재 total_item이 기준(18000개)을 넘으면 Job Launcher로 Batch 작업 진행 메서드 호출 - log.info("[#주의#] {}섹션의 total_items수 : {}",section,total_items); // 추후, 하루에 3번 이상의 중간 호출이 일어나는 것에 대한 방어코드도 작성할 것 + /* =================== 18000 초과일 경우 =================== */ + else if (total_items > 18000) { + int count = intermediateBatchRedisService.getBatchCount(batchCountKey); + + if (count >= 3) { + log.warn("[SKIP] {} 섹션은 이미 {}에 3회 이상 배치됨", section, dateStr); + continue; + } + + runNewsBatch(section); // 배치 실행 + + intermediateBatchRedisService.incrementBatchCount(batchCountKey); // 횟수 증가 + log.warn("[#주의#] {} 섹션의 total_items 수 : {} (누적 {}회 배치됨)", section, total_items, count + 1); } } } + // ======================= deprecated ========================= + + /** + * 매 정각마다 실행 되는 모니터링 스케줄러 + * + * 각 섹션 별 현재 몇개의 데이터가 모였는지 모니터링 + * 9000개가 넘는 섹션 발견 시, 즉시 DB에 Batch 실시 + * + */ +// @Scheduled(cron = "0 0 * * * *") // 매 시간 정각 +// //@Scheduled(cron = "0 */5 * * * *") // 5분 +// // @Scheduled(cron = "0 */3 * * * *") // 3분 +// //@Scheduled(cron = "0 */2 * * * *") // 2분 +// //@Scheduled(cron = "0 * * * * *") // 1분 +// public void monitoring(){ +// +// /* 날짜 & 시간 확인 */ +// LocalDate day = LocalDate.now(); // 오늘의 날짜 +// LocalDateTime time = LocalDateTime.now(); // 현재 날짜와 시간(분 단위 포함) +// log.info("// ======================= 오늘의 날짜 : {} =========================", day); // 오늘의 날짜 로그 확인 +// log.info("현재 시간 : {}", time); +// +// /* 날짜 포매팅 작업 */ +// DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); //원하는 포맷 지정 +// String dateTo = day.format(formatter); //문자열로 변환 +// String dateFrom = day.format(formatter); //문자열로 변환 +// +// /* 섹션 별 누적 데이터 수 모니터링 로직 */ +// for (String section : sections) { +// +// // 현재 섹션의 현 시각 누적 Total_Item 수 확인 +// int total_items = newsMonitoringManager.getTotalItems(section, dateFrom, dateTo); +// +// /* 뉴스 데이터 9000개 이상 시 중간 배치 + Redis 기록 */ +// if(total_items >= 9000 && total_items <= 18000){ +// +// // 해당 섹션이 이전에도 사전 Batch 작업이 실행 되었는지 확인 (Redis에서 횟수 조회) +// int n = intermediateBatchRedisService.getBatchCount(section); +// log.info("{} 섹션 기존 중간 배치 현황 : 총 {}회 중간 배치 ",section, n); +// +// // 현재 total_item이 기준(9000개)을 넘으면 Job Launcher로 Batch 작업 진행 메서드 호출 +// runNewsBatch(section); +// +// // 중간 배치가 진행된 섹션 배치 현황 기록 (Redis에 횟수 증가) +// intermediateBatchRedisService.incrementBatchCount(section); +// +// } /* 2번째 중간 배치 부터 주의 메시지 호출 */ +// else if(total_items >= 18000){ +// +// // Job Launcher로 Batch 작업 진행 메서드 호출 +// runNewsBatch(section); +// +// // 현재 total_item이 기준(18000개)을 넘으면 Job Launcher로 Batch 작업 진행 메서드 호출 +// log.info("[#주의#] {}섹션의 total_items수 : {}",section,total_items); // 추후, 하루에 3번 이상의 중간 호출이 일어나는 것에 대한 방어코드도 작성할 것 +// +// } +// } +// } + // ======================= Spring Batch Job Launcher (Batch Starter Method) ========================= /**