Concat · 뉴스 수집 엔진
아이티데일리 MVP 아키텍처 가이드
「PR #24를 머지하면 수집이 되나?」에서 출발해 매거진·엔진의 경계, '발행'의 의미, 수집부터 발행까지의 인프라 워크플로우를 정리한 팀 공유 가이드입니다.
📌 특정 시점의 스냅샷입니다 — 착수 지시가 아니고 결정을 바꾸지 않습니다. 수치·상태는 위 기준 시점의 실측이며, 이후 마일스톤 진행에 따라 낡아질 수 있습니다.
⚠️ 이 문서는 TO-BE 재설계(ADR-0013 · PR #25, 2026-08-11) 이전의 AS-IS 기준입니다.
이후 바뀐 것 — 완료 기준이 발행·REST 제공까지 확장(AC 12술어), Cloud SQL 도쿄 제거(서울 단일 인스턴스 + magazine DB),
apps/api에 발행 기사 라우트(/v1/published) 허용, 파트너 어드민이 MVP에 포함. 상세는 레포의
docs/adr/0013-itdaily-e2e-mvp.md와 docs/handoff/260811-tobe-itdaily-e2e-plan.md를 보세요.
두 트랙 — 매거진 vs 엔진
한 레포에 성격이 다른 두 시스템이 공존합니다(ADR-0008 D2 — supersede 아님). 엔진은 "가져와서 그대로 보관·제공"하는 수집 인프라, 매거진은 그걸 받아 "새 기사를 만들어 공개"하는 콘텐츠 서비스입니다.
Engine — 수집 인프라
엔진 트랙
다른 서비스가 구독해 데이터를 받아가는 공급 인프라
- 하는 일수집 → 파싱(규칙 기반) → 저장 → 제공(Pull API)
- LLM0건 — 검사기 2개가 기계로 강제
- 실행 모델Cloud Run Service 3개 + backfill Job + Tasks 큐
- 리전asia-northeast3 서울
- DB
engine.articles= 수집 원문 · UPDATE 금지 - 원본 HTMLGCS 보관이 원칙 — 파싱보다 먼저 저장
- 검수없음 — 생성물이 없으므로 대상도 없음
- 코드
apps/·packages/
Magazine — 콘텐츠 서비스
매거진 트랙
BizCrush 매거진용 기사 생성·발행 서비스
- 하는 일raw_items → 재구성/요약(LLM) → 검수 → 발행
- LLM단계 키 8개 ·
pipeline/llm.py단일 경로 - 실행 모델Cloud Run Job 1개 · 30분 사이클 (ADR-0005)
- 리전도쿄(문서상) — 실물 서울은 미결 → §7
- DB이 레포의
articles= LLM 생성 기사 · 상태머신 - 원본 HTML추출 직후 폐기 (알려진 갭)
- 검수전량 사람 검수 (불변 원칙 4)
- 코드
pipeline/·web/
어댑터가 엔진 Pull API를 당겨 매거진 raw_items에 행을 쓰는 것뿐입니다.
articles라는 이름이 양쪽에 있어 무접두 articles 금지 규칙(ADR-0008 D4b)이 있습니다 —
항상 engine.articles / 「이 레포의 articles」로 한정해 부릅니다.
Cloud Run vs Scheduler — 축부터 정정
"Cloud Run으로 띄울 것 vs 스케줄러로 띄울 것"은 같은 층의 선택지가 아닙니다. Scheduler는 아무것도 실행하지 않습니다 — 컴퓨트는 전부 Cloud Run이고, Scheduler는 깨우는 트리거일 뿐입니다.
실제로 갈리는 축은 둘입니다:
- Cloud Run 안에서 — Service(HTTP 상주·요청 구동) vs Job(1회 실행 후 종료)
- 누가 깨우나 — Scheduler(시계) · Cloud Tasks(큐) · 사람(온디맨드) · 브라우저
아이티데일리 MVP 리소스 전체 표
| 리소스 | 타입 | 깨우는 주체 | 하는 일 | 상태 (08-11) |
|---|---|---|---|---|
| worker | Cloud Run Service | ① Scheduler(OIDC) ② Tasks push |
RSS 분해·태스크 등록 / 원본 저장·파싱·적재 | ❌ 0개 → M3 |
| api | Cloud Run Service | 소비 서비스·어드민 | Pull API·suppression. min 1은 이것뿐 |
❌ 0개 → M3 |
| admin-web | Cloud Run Service | 브라우저 | SSR Next.js 내부 콘솔 (킥오프 #6로 SSR 확정) | ❌ 0개 → M3·M4 |
| backfill | Cloud Run Job | 사람(온디맨드) — Scheduler 아님 | GCS 원본 재파싱 (⛔ 소급 수집 아님) | ❌ 0개 → M3 |
| fetch-feed-itdaily | Cloud Scheduler | 시계(cron) | worker를 깨우기만 — 기사를 읽지 않는다 | ❌ 0개 → M3 |
| engine-normal | Cloud Tasks 큐 | worker가 넣고 큐가 push | 1 req/s·동시 1 강제 — 언론사 서버 배려 | ✅ 있음 (TF) |
| dispatcher | — | — | MVP에서 생략 (Scheduler→worker 직결, ADR-0008 rev2) | 해당 없음 |
지금 실존하는 Scheduler+Cloud Run 조합은 매거진 것 하나뿐입니다: pipeline-worker Job + pipeline-cycle Scheduler(*/30 · PAUSED). 엔진에서 Job은 backfill 하나뿐이고 그건 Scheduler가 깨우지 않으므로, "Job을 스케줄러로 돌려 아이티데일리 수집"이라는 조합 자체가 엔진에 없습니다.
인프라 워크플로우 — 수집부터 발행까지
M6 개통 데모 시점의 목표 상태입니다. 붉은 절취선까지가 MVP의 "끝" = 파일럿 완료이고, 그 아래 매거진 구간은 MVP 밖(이후 운영)입니다.
engine.sources에 itdaily 행
OIDC 인증 HTTP로 worker를 호출. 기사를 읽지 않는다 — 트리거만.
POST /fetch-feed아이티데일리 RSS를 읽고, DB에 없는 신규 기사만 골라 기사 1건 = 태스크 1건으로 큐에 등록.
초당 1건·동시 1로 worker에 되돌려 push — 언론사 서버 배려. 첫 실행 버스트(RSS 50건 연타)는 M3에서 초회 제한 예정.
POST /parse-article- a기사 HTML 취득 — UA는
engine.sources.user_agent(PR #24가 정정한 그 값) - b원본 HTML을 GCS에 저장 Cloud Storage · vertical-news-engine-raw — 🔴 파싱보다 먼저 (셀렉터가 틀려도 복구 경로 확보)
- c파싱 — trafilatura → 셀렉터 2단 fallback. 규칙 기반, 모델 호출 없음
- d파싱 산출물 적재 Cloud SQL · vertical-news-engine —
engine.articles에 body·gcs_object·body_hash INSERT. 이후 불변
GET /articles?updated_since=<cursor> + 서비스별 키.
suppression 행은 최우선 제외. 밀어주지 않고 소비자가 당겨간다 (Pub/Sub push는 MVP 보류).
Pull API를 당겨 매거진 Cloud SQL · 도쿄의 raw_items에 행 삽입.
AC: 「어댑터 1회 → raw_items 행 1건」. 전달되는 것은 파싱 산출물(원문 본문)이지 요약이 아니다.
engine.takedown_requests → suppression → Pull API 미반환 + 매거진 기사 unpublished 전이 (계약 제8조① 이행)
body_hash 변경 → 커서 조회에 다시 걸려 정정이 소비자에게 전파
empty(본문 0 — 파서) · failed(취득 실패 — worker) 모두 연속 실패 카운터 → degraded → Slack 알림
raw_items를 집어 Vertex AI 임베딩 → 재구성 LLM(LiteLLM → Gemini API) → 게이트 → in_review.
매거진에는 큐가 없다 — ADR-0005가 의도적으로 단일 Job에 접은 설계. 아이티데일리(KR+contract)는 markets.py가 원문 투입 경로(reconstruct/)로 라우팅.
전량 사람 검수 — approved 없이 다음 단계로 못 간다 (불변 원칙 4).
articles.status → 'published'. 새 적재가 아니라 이미 있는 행의 값 변경.
공개 표면(KR 서빙 API /api/articles)은 M7 미착수. digest 모드는 ⛔ 발행 금지 게이트(ADR-0009).
GitHub Actions CI 2레인 — 앱 레인(Artifact Registry push + Cloud Run 배포)과
인프라 레인(Terraform apply)은 SA가 다릅니다. 시크릿은 Secret Manager 주입.
Scheduler·Cloud Run 정의는 Terraform 관리라 gcloud/콘솔로 먼저 만들면 다음 plan이 되돌립니다 (infra/README.md).
'발행'이라는 단어 — 4가지 의미
같은 단어가 문서에서 네 가지로 쓰입니다. 대화에서 "발행"이 나오면 ①인지 ③인지 먼저 못 박고 시작하세요 — "엔진이 발행한다"는 ③으로는 (목표 아키텍처에서) 맞고 ①로는 틀립니다.
매거진 기사 발행 — 본래 의미
articles.status → 'published' 상태 전이. 검수 게이트 필수(approved 없이 불가).
종단이 아님 — published → unpublished → in_review 경로가 있고, unpublished → published 직행은 금지(재발행도 재검수).
엔진에는 발행이 없다
DDL 실물로 확인 — engine.articles에는 status 컬럼 자체가 없고 parse_status(ok/failed/empty)뿐.
엔진은 공개 여부를 결정하지 않는다. 계약상 "내림"은 suppression(Pull API 반환 제외)으로 표현.
Pub/Sub 이벤트 발행 — 메시징 용어
article.created/article.updated 토픽 발행(publish). 기사 공개와 무관한 전혀 다른 뜻이고,
MVP에서는 보류(Pull API만 — ADR-0008 rev2 ④)라 지금은 존재하지도 않는다.
발행일 — 메타데이터
아이티데일리가 자기 기사를 낸 날짜(published date). 엔진이 파싱해 저장하는 필드일 뿐, 누구의 행위도 아니다.
PR #21 · #24 — 같은 사다리의 연속된 두 단
둘 다 엔진 트랙의 준비 PR이고, 둘 다 수집을 1건도 하지 않습니다. #21은 전제를 바로잡고, #24는 자리를 만들고 지킵니다.
테크월드 1소스 전제의 낡은 단정 제거(테크월드는 2호 유지 + enabled=false) ·
itdaily-kr.yaml 계약 사실 정정(서명본 기준) · 시드 가드 신설 — 측정 문서 인용 없는 소스 시드는 테스트가 막는다.
apps/worker·apps/api 워크스페이스 골격 · 가드 사각 3건 봉합(import·모델 가드를 apps/까지) ·
봇 UA 라이브 DB 오타 정정(0002_engine_ua_fix.sql) · parse_status 의미 확정(ok/empty/failed).
⚠️ 이 때문에 아이티데일리 시드는 0003으로 밀림(재번호 M1 담당 확인 대기).
두 엔드포인트 · GCS 원본 저장 · Pull API · 알림 · 로컬 e2e (지시서 260811-engine-m2-worker-api.md §3~§5).
epnc 시드로 병행 착수 가능 — 그것이 테크월드를 2호로 유지하는 실질 이유.
Cloud Run Service 3 + backfill Job + Scheduler + CI 2레인 + Dockerfile. 게이트: plan drift 0 → 승인 apply. 🔴 Cloud SQL 재생성 신호 시 즉시 중단.
UA 사전 공유(메일) · robots.txt 조회 · 셀렉터 실측 → 그 측정 문서를 인용하는 0003 시드.
이때 #21의 가드(측정 인용)와 #24의 가드(UA DB 행 대조)가 동시에 그 커밋을 검사한다.
Scheduler 실트리거 → GCS 객체 + engine.articles 행 → 어댑터 → raw_items,
takedown·backfill·실패 알림까지 7개 술어 실증.
핵심 Q&A
이 가이드가 나온 세션의 질문들 — 그대로 다시 물어도 이 답이 나옵니다.
아니요 그 PR에 서비스 로직이 0줄이고, 엔진 Cloud Run Service·Scheduler·itdaily 시드가 전부 없습니다. 개통은 M3 이후 + M0 완료 시점입니다.
0건 ①~⑥ 어디에도 모델 호출이 없습니다 — 우연이 아니라 검사기로 강제되는 계약(ADR-0008 D7a). 모델 API와 스치는 유일한 지점은 M-B⑤ 임베딩 판단인데, 그것도 호출이 아니라 계약 판단 대기이며 M6을 차단하는 블로커입니다.
아이티데일리의 원문입니다. 끝났을 때 존재하는 것은 GCS 원본 HTML · engine.articles 파싱 산출물 · 매거진 raw_items 사본 — 전부 원문 계열이고 재구성 기사는 0건.
함의: 산출물 전부가 아이티데일리 저작물이라, MVP가 증명하는 것은 "기사를 만들 수 있다"가 아니라 "남의 원문을 계약대로 안전하게 보관·전달·회수할 수 있다"입니다.
네 그것도 훨씬 먼저 끝납니다 — GCS는 파싱 전(④-b), engine.articles는 파싱 직후(④-d), raw_items는 ⑥.
발행(⑨)은 새 적재가 아니라 이미 있는 행의 status 값 변경 1건입니다.
포함 그게 바로 ⑤ 엔진 Pull API입니다 — REST + OpenAPI, 서비스별 키. 비즈크러시 매거진은 그 API의 1호 소비자일 뿐입니다.
단, 표면이 둘입니다: 수집 원문 제공 = 엔진 Pull API(MVP 포함) vs 발행 기사 제공 = 매거진 서빙 API /api/articles(M7 · 미착수 · MVP 밖).
enabled: true만 바꾸면 되는데)⛔ 금지 기술적으로 가능해 보여도 사유가 셋: ① GCS 원본 저장이 없어 셀렉터가 틀리면 복구 경로가 사라짐(침묵 실패) ② UA가 다름 — 사전 통지할 값과 실제 보낼 값이 갈림 ③ takedown·suppression 등 계약 이행 축이 매거진 경로에 없음.
미결·주의 — 기준 시점에 열려 있는 것
전체 경로의 선두는 UA 문자열 1개와 메일 1통 — 코드가 아닙니다. 이것이 끝나야 robots.txt 조회 → M0 실측이 열립니다.
보관은 계약이 허용했으나 벡터화(임베딩)가 그 목적의 수단인지 미판단. 어댑터가 raw_items에 넣는 순간 매거진 사이클이 임베딩을 하게 되므로 M6 전에 답이 필요합니다.
ADR-0005 §4·deploy.md §2는 도쿄, 2026-08-11 실물 배포는 서울. 의도면 문서를 rev, 실수면 배포를 이동 — 어느 쪽이든 하나는 고칩니다. 확인 주체: 정훈.
① sources.schedule vs Scheduler cron 이중 진실(권고: Scheduler가 진실) ② 첫 실행 버스트 — RSS 50건 연타 방지(초회 제한/스태거) ③ rate_class 컬럼의 지위 — 큐 1개 공유라 현재 강제 수단 없음.
수집 대상이 "테크월드 epnc.co.kr"로 그려진 그림이 돌고 있습니다. ADR-0012 §rev3 이후 1소스는 아이티데일리(테크월드는 2호·enabled=false) — 구조는 그대로, 매체 이름만 낡았습니다.
본문은 2단 fallback인데 제목·기자·섹션은 셀렉터 단독. 메타 셀렉터가 틀리면 본문은 살아서 ok가 나고 알림 없이 메타만 빕니다.
engine.articles는 불변이라 다시 긁어도 안 고쳐짐 — 진짜 예방책은 M0 셀렉터 실측입니다.