-
ADR: Info Service - News 피드 API Draft이코에코(Eco²) Knowledge Base/Reports 2026. 1. 17. 02:37


기존 Info: Dummy / 목표: Perplexity-style 뉴스 피드 환경/에너지/AI 관련 뉴스를 Perplexity 스타일로 제공하는 서비스
작성일: 2026-01-16
최종 수정: 2026-01-17
상태: Approved (Infrastructure Provisioned)
1. 개요
1.1 목적
Eco² 앱에서 환경, 에너지, AI 관련 뉴스/시사 정보를 무한스크롤 피드로 제공.
1.2 우선순위
뉴스 피드 API P0 무한스크롤 지원 한국 뉴스 P0 네이버 검색 API 해외 뉴스 P1 NewsData.io API 카테고리 필터 P1 환경, 에너지, AI, 전체 실시간 갱신 P2 캐시 TTL 기반 1.3 비목표 (Non-Goals)
- 뉴스 영구 저장 (PostgreSQL) - 시의성 있는 데이터, 캐시로 충분
- 사용자별 개인화 - MVP 이후 고려
- 뉴스 요약/분석 (LLM) - MVP 이후 고려
- 푸시 알림 - MVP 이후 고려
2. 아키텍처 결정
2.1 저장소 선택
결정: Redis Cache Only (PostgreSQL 미사용)
뉴스 시의성 오래된 데이터 누적 TTL로 자동 만료 구현 복잡도 스키마, 마이그레이션 단순 key-value 쿼리 필요성 히스토리 분석 (불필요) 시간순 정렬만 필요 장애 시 데이터 유지 API 재호출로 복구 비용 DB 인스턴스 필요 기존 Redis 활용 근거:
- 뉴스는 24시간 이내 데이터만 의미 있음
- 캐시 miss 시 API 재호출로 충분
- 별도 DB 운영 비용/복잡도 불필요
2.2 페이지네이션 방식
결정: Cursor 기반 (Keyset Pagination)
Offset 구현 간단 실시간 데이터에서 중복/누락 Cursor 안정적, 실시간 안전 구현 복잡 Keyset 직관적 (timestamp) Cursor와 유사 선택: Keyset (timestamp 기반 cursor)
GET /api/v1/info/news?cursor=1705398000000&limit=10 │ └── Unix timestamp (ms)근거:
- 새 뉴스가 계속 추가되는 환경
- Offset은 데이터 변동 시 불안정
- Timestamp가 직관적이고 디버깅 용이
2.3 캐시 구조
결정: Redis Sorted Set + Hash
┌─────────────────────────────────────────────────────────────┐ │ Redis Structure │ ├─────────────────────────────────────────────────────────────┤ │ │ │ news:feed:{category} │ │ └── Sorted Set │ │ ├── score: published_at (Unix timestamp ms) │ │ └── member: article_id │ │ │ │ news:article:{article_id} │ │ └── Hash │ │ ├── title: "기사 제목" │ │ ├── url: "https://..." │ │ ├── snippet: "기사 요약..." │ │ ├── source: "naver" | "newsdata" │ │ ├── source_name: "한겨레" | "Reuters" │ │ ├── published_at: 1705398000000 │ │ ├── thumbnail_url: "https://..." (optional) │ │ └── category: "environment" | "energy" | "ai" │ │ │ │ news:meta:{category} │ │ └── Hash │ │ ├── last_fetched: 1705398000000 │ │ └── total_count: 150 │ │ │ └─────────────────────────────────────────────────────────────┘TTL 정책:
news:feed:*: 1시간news:article:*: 1시간news:meta:*: 1시간
2.4 뉴스 소스
결정: 네이버 + NewsData.io 이중화
네이버 검색 API 한국 뉴스 25,000 req 무료, 한국 커버리지 최고 NewsData.io 해외 뉴스 200 크레딧 무료, 206개국 지원 Fallback 전략:
1차: 캐시 조회 │ ├─ HIT → 반환 │ └─ MISS → API 호출 │ ├─ 성공 → 캐시 저장 → 반환 │ └─ 실패 → 빈 결과 + 로그2.5 Worker 필요성
결정: Worker 없음 (On-Demand Fetch)
Background Worker 항상 최신 캐시 인프라 복잡도, 비용 On-Demand 단순, 리소스 효율 첫 요청 지연 선택: On-Demand (캐시 miss 시 실시간 fetch)
근거:- MVP 단계에서 단순함 우선
- 캐시 TTL 1시간이면 API 호출 빈도 낮음
- 필요 시 Worker 추가 가능 (확장성)
2.6 인프라 격리 전략
결정: 전용 노드 + Taint/Toleration
Info 서비스는 외부 뉴스 API에 의존하는 I/O 집약적 워크로드입니다. 다른 서비스와의 리소스 경합을 방지하고, 장애 격리를 위해 전용 노드를 프로비저닝합니다.
리소스 경합 타 서비스 영향 가능 격리됨 장애 전파 OOM 시 Pod eviction 격리됨 비용 효율적 약간 증가 스케일링 복잡 독립적 운영 복잡도 낮음 Taint 관리 필요 선택: 전용 노드 (t3.small, domain=info taint)
구성:# Node Labels role: api domain: info service: info workload: api tier: business-logic phase: "3" # Node Taint domain=info:NoSchedule근거:
- 외부 API 호출 실패 시 재시도 로직으로 인한 메모리 사용량 급증 가능성
- 뉴스 캐싱 로직의 독립적 스케일링 필요 (캐시 TTL 만료 시 부하 집중)
- Phase 3 서비스로서 기존 Phase 1-2 서비스와의 격리 필요
2.7 GitOps 배포 전략
결정: 별도 레포지토리 + ArgoCD Application
Info 서비스는 기존 backend ApplicationSet에 포함하지 않고, 별도의 ArgoCD Application으로 관리합니다.
ApplicationSet 포함 일관된 배포 모노레포 의존, 배포 결합 별도 Application 독립 배포 주기 설정 분산 선택: 별도 ArgoCD Application (backend-info.git 참조)
구조:clusters/ ├── dev/apps/ │ ├── 44-api-info.yaml # ArgoCD Application │ └── 51-route-info.yaml # Routing Application └── prod/apps/ ├── 44-api-info.yaml └── 51-route-info.yaml근거:
- Info 서비스는 실험적 Phase 3 서비스로, 잦은 변경 예상
- 기존 백엔드 서비스 배포에 영향 없이 독립적 릴리스 가능
- 별도 레포에서 뉴스 소스 추가 등 빠른 이터레이션 지원
2.8 Legacy 리소스 정리
결정: Bitnami Redis 제거, Spotahome Operator 유지
기존 클러스터에 Bitnami Redis(StatefulSet 기반)와 Spotahome Redis Operator가 공존하는 상황이었습니다.
리소스 상태 조치 28-redis-operator.yaml(Bitnami)Pending 7일 삭제 27-postgresql.yaml(중복)Orphaned 삭제 Spotahome Redis Operator Active 유지 근거:
- Bitnami Redis는 PVC 바인딩 실패로 7일간 Pending 상태
- Spotahome Operator가 이미 Redis Sentinel 클러스터 관리 중
- Info 서비스는 기존 Redis Sentinel 클러스터의 캐시 사용
3. API 명세
3.1 뉴스 목록 조회
GET /api/v1/info/newsRequest Parameters:
category string N all all, environment, energy, ai cursor string N - 이전 응답의 next_cursor limit int N 10 1-50 source string N all all, naver, newsdata Response:
{ "articles": [ { "id": "naver_12345", "title": "2026년 환경부 분리배출 정책 변경 안내", "url": "https://news.naver.com/...", "snippet": "환경부는 2026년부터 플라스틱 분리배출 기준을...", "source": "naver", "source_name": "한겨레", "published_at": "2026-01-16T10:30:00Z", "thumbnail_url": "https://...", "category": "environment" } ], "next_cursor": "1705398000000", "has_more": true, "meta": { "total_cached": 150, "cache_expires_in": 3540 } }HTTP Status:
200 성공 400 잘못된 파라미터 429 Rate Limit 초과 503 외부 API 장애 3.2 카테고리 목록
GET /api/v1/info/categoriesResponse:
{ "categories": [ {"id": "all", "name": "전체", "count": 150}, {"id": "environment", "name": "환경", "count": 80}, {"id": "energy", "name": "에너지", "count": 45}, {"id": "ai", "name": "AI", "count": 25} ] }
4. 데이터 흐름
4.1 뉴스 조회 플로우
Client Request │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Info API Service │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 1. 파라미터 검증 │ │ │ │ │ ▼ │ │ 2. Redis 캐시 확인 │ │ │ │ │ ├─ HIT ────────────────────────────────────┐ │ │ │ │ │ │ └─ MISS │ │ │ │ │ │ │ ▼ │ │ │ 3. 외부 API 병렬 호출 │ │ │ ├─ 네이버 검색 API │ │ │ └─ NewsData.io API │ │ │ │ │ │ │ ▼ │ │ │ 4. 결과 병합 │ │ │ ├─ URL 기반 중복 제거 │ │ │ ├─ 시간순 정렬 │ │ │ └─ 카테고리 분류 │ │ │ │ │ │ │ ▼ │ │ │ 5. Redis 캐시 저장 (TTL: 1시간) │ │ │ │ │ │ │ └───────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 6. Cursor 기반 페이지네이션 │ │ ZREVRANGEBYSCORE news:feed:{category} │ │ +inf (cursor) LIMIT 0 {limit} │ │ │ │ │ ▼ │ │ 7. Response 생성 │ │ └─ next_cursor = 마지막 article의 published_at │ │ │ └─────────────────────────────────────────────────────────────┘ │ ▼ Client Response4.2 캐시 갱신 전략
Stale-While-Revalidate 패턴 ┌──────────────────────────────────────────────────────────┐ │ Request 도착 │ │ │ │ │ ▼ │ │ 캐시 상태 확인 │ │ │ │ │ ├─ FRESH (TTL 내) │ │ │ └─ 캐시 데이터 반환 │ │ │ │ │ ├─ STALE (TTL 만료, 데이터 있음) │ │ │ ├─ 캐시 데이터 즉시 반환 │ │ │ └─ Background로 API 호출 → 캐시 갱신 │ │ │ │ │ └─ EMPTY (데이터 없음) │ │ └─ API 호출 대기 → 캐시 저장 → 반환 │ │ │ └──────────────────────────────────────────────────────────┘
5. 프로젝트 구조
5.1 Clean Architecture (Layer-First)
apps/info/ ├── main.py # FastAPI app factory ├── setup/ │ ├── config.py # Settings (pydantic-settings) │ └── dependencies.py # DI factory │ ├── domain/ │ └── entities/ │ └── news_article.py # Domain entity │ ├── application/ │ ├── dto/ │ │ ├── news_request.py # Request DTO │ │ └── news_response.py # Response DTO │ ├── ports/ │ │ ├── news_source.py # NewsSourcePort (ABC) │ │ └── news_cache.py # NewsCachePort (ABC) │ ├── services/ │ │ └── news_aggregator.py # 병합/중복제거 로직 │ └── commands/ │ └── fetch_news_command.py # UseCase │ ├── infrastructure/ │ ├── integrations/ │ │ ├── naver/ │ │ │ ├── __init__.py │ │ │ └── naver_news_client.py # 네이버 검색 API │ │ └── newsdata/ │ │ ├── __init__.py │ │ └── newsdata_client.py # NewsData.io API │ └── cache/ │ └── redis_news_cache.py # Redis 캐시 구현 │ └── presentation/ └── http/ ├── router.py # FastAPI router └── controllers/ └── news_controller.py # 엔드포인트 핸들러5.2 Port 정의
# application/ports/news_source.py @dataclass(frozen=True) class NewsArticle: id: str title: str url: str snippet: str source: str # "naver" | "newsdata" source_name: str # "한겨레" | "Reuters" published_at: datetime thumbnail_url: str | None = None category: str | None = None class NewsSourcePort(ABC): @abstractmethod async def fetch_news( self, query: str, category: str | None = None, max_results: int = 50, ) -> list[NewsArticle]: pass# application/ports/news_cache.py class NewsCachePort(ABC): @abstractmethod async def get_articles( self, category: str, cursor: int | None, limit: int, ) -> tuple[list[NewsArticle], int | None]: # (articles, next_cursor) pass @abstractmethod async def set_articles( self, category: str, articles: list[NewsArticle], ttl: int = 3600, ) -> None: pass @abstractmethod async def is_fresh(self, category: str) -> bool: pass
6. 설정
6.1 환경변수
# apps/info/setup/config.py class Settings(BaseSettings): # Environment environment: str = "local" # Redis redis_url: str = "redis://localhost:6379/0" # 네이버 검색 API naver_client_id: str | None = None naver_client_secret: str | None = None naver_api_timeout: float = 10.0 # NewsData.io API newsdata_api_key: str | None = None newsdata_api_timeout: float = 10.0 # 캐시 설정 news_cache_ttl: int = 3600 # 1시간 # 페이지네이션 news_default_limit: int = 10 news_max_limit: int = 50 # 카테고리별 검색 키워드 category_keywords: dict[str, list[str]] = { "environment": ["환경", "분리배출", "재활용", "기후변화"], "energy": ["에너지", "신재생", "탄소중립", "전기차"], "ai": ["인공지능", "AI", "머신러닝", "챗봇"], } class Config: env_prefix = "INFO_"6.2 Kubernetes Secret 매핑
# 이미 생성됨: workloads/secrets/external-secrets/dev/info-api-secrets.yaml INFO_NAVER_CLIENT_ID: '{{ .naverClientId }}' INFO_NAVER_CLIENT_SECRET: '{{ .naverClientSecret }}' INFO_NEWSDATA_API_KEY: '{{ .newsDataApiKey }}'
7. 카테고리 분류 로직
7.1 키워드 기반 분류
# application/services/news_aggregator.py CATEGORY_KEYWORDS = { "environment": [ "환경", "분리배출", "재활용", "쓰레기", "폐기물", "기후변화", "온실가스", "탄소", "생태계", "environment", "recycling", "waste", "climate", "pollution" ], "energy": [ "에너지", "신재생", "태양광", "풍력", "전기차", "배터리", "수소", "탄소중립", "energy", "solar", "renewable", "EV", "battery" ], "ai": [ "인공지능", "AI", "머신러닝", "딥러닝", "챗봇", "GPT", "LLM", "자동화", "artificial intelligence", "machine learning", "automation" ], } def classify_article(article: NewsArticle) -> str: text = f"{article.title} {article.snippet}".lower() scores = {} for category, keywords in CATEGORY_KEYWORDS.items(): scores[category] = sum(1 for kw in keywords if kw.lower() in text) if max(scores.values()) == 0: return "environment" # 기본 카테고리 return max(scores, key=scores.get)
8. Rate Limiting
8.1 외부 API 제한
네이버 25,000 req 100개 ~240/일 (1시간 TTL) NewsData 200 크레딧 10개 ~24/일 (1시간 TTL) 8.2 내부 Rate Limiting
# Redis 기반 rate limiter RATE_LIMIT_KEY = "info:ratelimit:{api}:{date}" RATE_LIMITS = { "naver": 20000, # 여유분 확보 "newsdata": 180, # 여유분 확보 }
9. 모니터링
9.1 메트릭
메트릭 타입 설명 info_news_requests_totalCounter 총 요청 수 info_news_cache_hits_totalCounter 캐시 히트 info_news_cache_misses_totalCounter 캐시 미스 info_news_api_latency_secondsHistogram 외부 API 지연 info_news_api_errors_totalCounter API 에러 (소스별) 9.2 알림
캐시 미스율 > 50% Warning 캐시 TTL 검토 API 에러율 > 10% Critical API 키 확인 응답 지연 > 3초 Warning 성능 분석
10. 구현 계획
Phase 1: Core API (MVP)
- 프로젝트 구조 생성
- Config/Settings 설정
- 네이버 뉴스 클라이언트
- NewsData.io 클라이언트
- Redis 캐시 구현
- FetchNewsCommand (UseCase)
- REST API 엔드포인트
- Cursor 페이지네이션
Phase 2: 배포
- Dockerfile
- Kubernetes manifests
- Istio VirtualService
- ArgoCD Application
Phase 3: 프론트엔드 연동
- useInfiniteQuery 훅 구현
- 뉴스 피드 UI 컴포넌트
- 무한스크롤 구현
11. References
'이코에코(Eco²) Knowledge Base > Reports' 카테고리의 다른 글
Info News 피드 페이지 구현, 배포 리포트 (FE) (0) 2026.01.17 Info News Service 구현, 배포 리포트 (BE) (1) 2026.01.17 Code Reivew: Circuit Breaker 싱글톤 race condition (0) 2026.01.16 ADR: Agentic Chat Worker Layer-First 리팩토링 (1) 2026.01.15 Cursor state.vscdb 16GB 분석 리포트 (0) 2026.01.15