ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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/news

    Request 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/categories

    Response:

    {
      "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 Response

    4.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_total Counter 총 요청 수
    info_news_cache_hits_total Counter 캐시 히트
    info_news_cache_misses_total Counter 캐시 미스
    info_news_api_latency_seconds Histogram 외부 API 지연
    info_news_api_errors_total Counter 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

    댓글

ABOUT ME

🎓 부산대학교 정보컴퓨터공학과 학사: 2017.03 - 2023.08
☁️ Rakuten Symphony Jr. Cloud Engineer: 2024.12.09 - 2025.08.31
🏆 2025 AI 새싹톤 우수상 수상: 2025.10.30 - 2025.12.02
🌏 이코에코(Eco²) 백엔드/인프라 고도화 중: 2025.12 - Present

Designed by Mango