From 231e82faccd204f80806a1f61851f9f27c36e81f Mon Sep 17 00:00:00 2001 From: ddingjoo Date: Thu, 22 Jan 2026 13:30:20 +0900 Subject: [PATCH] =?UTF-8?q?refactor(news):=20NewsAPI=20=EC=A0=9C=EA=B1=B0,?= =?UTF-8?q?=20RSS=EB=A7=8C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NewsApiClient 삭제 - SSM Parameter 의존성 제거 - RSS 소스당 7개씩 수집 (BBC, VOA, NPR = 약 21개/일) --- .../news/handler/NewsCollectionHandler.java | 7 +- .../domain/news/service/NewsApiClient.java | 166 ------------------ .../news/service/NewsCollectorService.java | 42 ++--- ServerlessFunction/template.yaml | 2 - 4 files changed, 15 insertions(+), 202 deletions(-) delete mode 100644 ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsApiClient.java diff --git a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/handler/NewsCollectionHandler.java b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/handler/NewsCollectionHandler.java index b5702a5e..4d17463f 100644 --- a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/handler/NewsCollectionHandler.java +++ b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/handler/NewsCollectionHandler.java @@ -34,14 +34,13 @@ public Map handleRequest(ScheduledEvent event, Context context) try { NewsCollectorService.CollectionResult result = collectorService.collectNews(); - logger.info("뉴스 수집 완료 - NewsAPI: {}, RSS: {}, 저장: {}, 소요: {}ms", - result.newsApiCount(), result.rssCount(), result.savedCount(), result.elapsedMs()); + logger.info("뉴스 수집 완료 - 수집: {}, 저장: {}, 소요: {}ms", + result.collectedCount(), result.savedCount(), result.elapsedMs()); return Map.of( "statusCode", 200, "message", "News collection completed", - "newsApiCount", result.newsApiCount(), - "rssCount", result.rssCount(), + "collectedCount", result.collectedCount(), "savedCount", result.savedCount(), "elapsedMs", result.elapsedMs() ); diff --git a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsApiClient.java b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsApiClient.java deleted file mode 100644 index dba1af09..00000000 --- a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsApiClient.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.mzc.secondproject.serverless.domain.news.service; - -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import com.mzc.secondproject.serverless.common.config.AwsClients; -import com.mzc.secondproject.serverless.domain.news.dto.RawNewsArticle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import software.amazon.awssdk.services.ssm.model.GetParameterRequest; - -import java.net.URI; -import java.net.URLEncoder; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; - -/** - * NewsAPI 연동 클라이언트 - * 무료 플랜: 100 requests/day, 최대 100 articles/request - */ -public class NewsApiClient { - - private static final Logger logger = LoggerFactory.getLogger(NewsApiClient.class); - private static final String NEWS_API_BASE_URL = "https://newsapi.org/v2"; - private static final String API_KEY_PARAM_NAME = "/englishstudy/news/api-key"; - - private static String cachedApiKey = null; - - private final HttpClient httpClient; - - public NewsApiClient() { - this.httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(10)) - .build(); - } - - /** - * API Key 조회 (Parameter Store + 캐싱) - */ - private String getApiKey() { - if (cachedApiKey != null) { - return cachedApiKey; - } - - try { - logger.debug("Fetching NewsAPI Key from Parameter Store"); - var response = AwsClients.ssm().getParameter( - GetParameterRequest.builder() - .name(API_KEY_PARAM_NAME) - .withDecryption(true) - .build() - ); - cachedApiKey = response.parameter().value(); - logger.info("NewsAPI Key loaded from Parameter Store"); - return cachedApiKey; - } catch (Exception e) { - logger.error("Failed to get NewsAPI Key from Parameter Store", e); - throw new RuntimeException("NewsAPI Key 로드 실패", e); - } - } - - /** - * 헤드라인 뉴스 조회 - */ - public List getTopHeadlines(String category, int pageSize) { - String url = String.format("%s/top-headlines?language=en&category=%s&pageSize=%d&apiKey=%s", - NEWS_API_BASE_URL, category, pageSize, getApiKey()); - - return fetchArticles(url, "NewsAPI-Headlines"); - } - - /** - * 검색어 기반 뉴스 조회 - */ - public List searchNews(String query, int pageSize) { - String encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8); - String url = String.format("%s/everything?q=%s&language=en&sortBy=publishedAt&pageSize=%d&apiKey=%s", - NEWS_API_BASE_URL, encodedQuery, pageSize, getApiKey()); - - return fetchArticles(url, "NewsAPI-Search"); - } - - /** - * 뉴스 API 호출 및 파싱 - */ - private List fetchArticles(String url, String source) { - List articles = new ArrayList<>(); - - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(url)) - .header("Accept", "application/json") - .timeout(Duration.ofSeconds(30)) - .GET() - .build(); - - HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - - if (response.statusCode() != 200) { - logger.error("NewsAPI 요청 실패 - status: {}, body: {}", response.statusCode(), response.body()); - return articles; - } - - JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject(); - String status = json.get("status").getAsString(); - - if (!"ok".equals(status)) { - logger.error("NewsAPI 응답 오류 - status: {}", status); - return articles; - } - - JsonArray articlesArray = json.getAsJsonArray("articles"); - for (JsonElement element : articlesArray) { - JsonObject articleJson = element.getAsJsonObject(); - RawNewsArticle article = parseArticle(articleJson, source); - if (article.isValid()) { - articles.add(article); - } - } - - logger.info("NewsAPI에서 {}개 기사 수집 완료", articles.size()); - - } catch (Exception e) { - logger.error("NewsAPI 호출 중 오류 발생", e); - } - - return articles; - } - - /** - * JSON을 RawNewsArticle로 변환 - */ - private RawNewsArticle parseArticle(JsonObject json, String defaultSource) { - String sourceName = defaultSource; - if (json.has("source") && json.get("source").isJsonObject()) { - JsonObject sourceObj = json.getAsJsonObject("source"); - if (sourceObj.has("name") && !sourceObj.get("name").isJsonNull()) { - sourceName = sourceObj.get("name").getAsString(); - } - } - - return RawNewsArticle.builder() - .title(getStringOrNull(json, "title")) - .description(getStringOrNull(json, "description")) - .url(getStringOrNull(json, "url")) - .imageUrl(getStringOrNull(json, "urlToImage")) - .source(sourceName) - .publishedAt(getStringOrNull(json, "publishedAt")) - .content(getStringOrNull(json, "content")) - .build(); - } - - private String getStringOrNull(JsonObject json, String key) { - if (json.has(key) && !json.get(key).isJsonNull()) { - return json.get(key).getAsString(); - } - return null; - } -} diff --git a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsCollectorService.java b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsCollectorService.java index e46bb6ef..9842f4b3 100644 --- a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsCollectorService.java +++ b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/news/service/NewsCollectorService.java @@ -10,37 +10,33 @@ import java.time.Instant; import java.time.LocalDate; import java.time.ZoneOffset; -import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * 뉴스 수집 서비스 - * NewsAPI, RSS 피드에서 뉴스를 수집하고 저장 + * RSS 피드에서 뉴스를 수집하고 저장 (BBC, VOA, NPR) */ public class NewsCollectorService { private static final Logger logger = LoggerFactory.getLogger(NewsCollectorService.class); - private static final int NEWS_API_LIMIT = 10; - private static final int RSS_LIMIT_PER_SOURCE = 5; + private static final int RSS_LIMIT_PER_SOURCE = 7; private static final long TTL_DAYS = 30; - private final NewsApiClient newsApiClient; private final RssFeedParser rssFeedParser; private final NewsDuplicateChecker duplicateChecker; private final NewsArticleRepository articleRepository; public NewsCollectorService() { - this.newsApiClient = new NewsApiClient(); this.rssFeedParser = new RssFeedParser(); this.duplicateChecker = new NewsDuplicateChecker(); this.articleRepository = new NewsArticleRepository(); } - public NewsCollectorService(NewsApiClient newsApiClient, RssFeedParser rssFeedParser, - NewsDuplicateChecker duplicateChecker, NewsArticleRepository articleRepository) { - this.newsApiClient = newsApiClient; + public NewsCollectorService(RssFeedParser rssFeedParser, + NewsDuplicateChecker duplicateChecker, + NewsArticleRepository articleRepository) { this.rssFeedParser = rssFeedParser; this.duplicateChecker = duplicateChecker; this.articleRepository = articleRepository; @@ -53,29 +49,16 @@ public CollectionResult collectNews() { logger.info("뉴스 수집 시작"); long startTime = System.currentTimeMillis(); - List allArticles = new ArrayList<>(); - int newsApiCount = 0; - int rssCount = 0; - - try { - List newsApiArticles = newsApiClient.getTopHeadlines("technology", NEWS_API_LIMIT); - allArticles.addAll(newsApiArticles); - newsApiCount = newsApiArticles.size(); - logger.info("NewsAPI에서 {}개 수집", newsApiCount); - } catch (Exception e) { - logger.error("NewsAPI 수집 실패", e); - } - + List rssArticles; try { - List rssArticles = rssFeedParser.fetchAllFeeds(RSS_LIMIT_PER_SOURCE); - allArticles.addAll(rssArticles); - rssCount = rssArticles.size(); - logger.info("RSS에서 {}개 수집", rssCount); + rssArticles = rssFeedParser.fetchAllFeeds(RSS_LIMIT_PER_SOURCE); + logger.info("RSS에서 {}개 수집", rssArticles.size()); } catch (Exception e) { logger.error("RSS 수집 실패", e); + return new CollectionResult(0, 0, System.currentTimeMillis() - startTime); } - List uniqueArticles = duplicateChecker.filterDuplicates(allArticles); + List uniqueArticles = duplicateChecker.filterDuplicates(rssArticles); logger.info("중복 제거 후 {}개 기사", uniqueArticles.size()); int savedCount = 0; @@ -92,7 +75,7 @@ public CollectionResult collectNews() { long elapsed = System.currentTimeMillis() - startTime; logger.info("뉴스 수집 완료 - 저장: {}, 소요시간: {}ms", savedCount, elapsed); - return new CollectionResult(newsApiCount, rssCount, savedCount, elapsed); + return new CollectionResult(rssArticles.size(), savedCount, elapsed); } /** @@ -130,8 +113,7 @@ private NewsArticle convertToNewsArticle(RawNewsArticle raw) { * 수집 결과 레코드 */ public record CollectionResult( - int newsApiCount, - int rssCount, + int collectedCount, int savedCount, long elapsedMs ) { diff --git a/ServerlessFunction/template.yaml b/ServerlessFunction/template.yaml index 7828e7a0..552cb8e9 100644 --- a/ServerlessFunction/template.yaml +++ b/ServerlessFunction/template.yaml @@ -1741,8 +1741,6 @@ Resources: Policies: - DynamoDBCrudPolicy: TableName: !Ref NewsTable - - SSMParameterReadPolicy: - ParameterName: englishstudy/news/* Events: DailySchedule: Type: Schedule