아이티데일리 MVP 아키텍처 가이드

Concat · 뉴스 수집 엔진

아이티데일리 MVP 아키텍처 가이드

「PR #24를 머지하면 수집이 되나?」에서 출발해 매거진·엔진의 경계, '발행'의 의미, 수집부터 발행까지의 인프라 워크플로우를 정리한 팀 공유 가이드입니다.

기준 2026-08-11 PR #21 머지됨 PR #24 open · e7b6f2c 근거 ADR-0008 · 0012 · 260811 계획서

📌 특정 시점의 스냅샷입니다 — 착수 지시가 아니고 결정을 바꾸지 않습니다. 수치·상태는 위 기준 시점의 실측이며, 이후 마일스톤 진행에 따라 낡아질 수 있습니다.

⚠️ 이 문서는 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.mddocs/handoff/260811-tobe-itdaily-e2e-plan.md를 보세요.

§1

두 트랙 — 매거진 vs 엔진

한 레포에 성격이 다른 두 시스템이 공존합니다(ADR-0008 D2 — supersede 아님). 엔진은 "가져와서 그대로 보관·제공"하는 수집 인프라, 매거진은 그걸 받아 "새 기사를 만들어 공개"하는 콘텐츠 서비스입니다.

Engine — 수집 인프라

엔진 트랙

다른 서비스가 구독해 데이터를 받아가는 공급 인프라

  • 하는 일수집 → 파싱(규칙 기반) → 저장 → 제공(Pull API)
  • LLM0건 — 검사기 2개가 기계로 강제
  • 실행 모델Cloud Run Service 3개 + backfill Job + Tasks 큐
  • 리전asia-northeast3 서울
  • DBengine.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」로 한정해 부릅니다.

§2

Cloud Run vs Scheduler — 축부터 정정

"Cloud Run으로 띄울 것 vs 스케줄러로 띄울 것"은 같은 층의 선택지가 아닙니다. Scheduler는 아무것도 실행하지 않습니다 — 컴퓨트는 전부 Cloud Run이고, Scheduler는 깨우는 트리거일 뿐입니다.

실제로 갈리는 축은 둘입니다:

아이티데일리 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을 스케줄러로 돌려 아이티데일리 수집"이라는 조합 자체가 엔진에 없습니다.

§3

인프라 워크플로우 — 수집부터 발행까지

M6 개통 데모 시점의 목표 상태입니다. 붉은 절취선까지가 MVP의 "끝" = 파일럿 완료이고, 그 아래 매거진 구간은 MVP 밖(이후 운영)입니다.

선행 — 사람 작업 코드가 아니라 이것이 경로의 선두
UA 사전 공유 메일M-B② · 🔴 현재 blocker robots.txt 최초 조회아직 0회 M0 셀렉터 실측재측정 없는 시드 = 침묵 실패 M1 시드engine.sources에 itdaily 행
MVP 범위 엔진 트랙 asia-northeast3 · 서울 · LLM 0건
시계가 깨운다 Cloud Scheduler fetch-feed-itdaily

OIDC 인증 HTTP로 worker를 호출. 기사를 읽지 않는다 — 트리거만.

RSS를 기사 단위로 분해 Cloud Run Service · worker POST /fetch-feed

아이티데일리 RSS를 읽고, DB에 없는 신규 기사만 골라 기사 1건 = 태스크 1건으로 큐에 등록.

큐가 속도를 강제 Cloud Tasks · engine-normal

초당 1건·동시 1로 worker에 되돌려 push — 언론사 서버 배려. 첫 실행 버스트(RSS 50건 연타)는 M3에서 초회 제한 예정.

기사 1건 처리 Cloud Run Service · worker 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-engineengine.articles에 body·gcs_object·body_hash INSERT. 이후 불변
가져가게 한다 Cloud Run Service · api — Pull API

GET /articles?updated_since=<cursor> + 서비스별 키. suppression 행은 최우선 제외. 밀어주지 않고 소비자가 당겨간다 (Pub/Sub push는 MVP 보류).

매거진에 전달 — 어댑터 (260811 M6)

Pull API를 당겨 매거진 Cloud SQL · 도쿄raw_items에 행 삽입. AC: 「어댑터 1회 → raw_items 행 1건」. 전달되는 것은 파싱 산출물(원문 본문)이지 요약이 아니다.

병행 배관 — takedown 접수구(구글 폼 수준) → engine.takedown_requests → suppression → Pull API 미반환 + 매거진 기사 unpublished 전이 (계약 제8조① 이행)
병행 배관 — backfill Cloud Run Job 온디맨드 실행 → GCS 원본 재파싱 → body_hash 변경 → 커서 조회에 다시 걸려 정정이 소비자에게 전파
실패 신호 empty(본문 0 — 파서) · failed(취득 실패 — worker) 모두 연속 실패 카운터 → degraded → Slack 알림
MVP 밖 — 이후 운영 매거진 트랙 asia-northeast1 도쿄 (문서상 · 실물 서울은 미결 → §7)
30분 사이클 — 모델 배선은 여기 Cloud Scheduler · pipeline-cycleCloud Run Job · pipeline-worker

raw_items를 집어 Vertex AI 임베딩재구성 LLM(LiteLLM → Gemini API) → 게이트 → in_review. 매거진에는 큐가 없다 — ADR-0005가 의도적으로 단일 Job에 접은 설계. 아이티데일리(KR+contract)는 markets.py가 원문 투입 경로(reconstruct/)로 라우팅.

사람 검수 Cloud SQL Studio / TablePlus

전량 사람 검수 — approved 없이 다음 단계로 못 간다 (불변 원칙 4).

발행 = 상태 전이 1건

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

'발행'이라는 단어 — 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). 엔진이 파싱해 저장하는 필드일 뿐, 누구의 행위도 아니다.

§5

PR #21 · #24 — 같은 사다리의 연속된 두 단

둘 다 엔진 트랙의 준비 PR이고, 둘 다 수집을 1건도 하지 않습니다. #21은 전제를 바로잡고, #24는 자리를 만들고 지킵니다.

PR #21머지됨 08-11
전제 정정 — 설정·문서·시드 가드

테크월드 1소스 전제의 낡은 단정 제거(테크월드는 2호 유지 + enabled=false) · itdaily-kr.yaml 계약 사실 정정(서명본 기준) · 시드 가드 신설 — 측정 문서 인용 없는 소스 시드는 테스트가 막는다.

PR #24open
자리 + 가드 — 서비스 로직 0줄

apps/worker·apps/api 워크스페이스 골격 · 가드 사각 3건 봉합(import·모델 가드를 apps/까지) · 봇 UA 라이브 DB 오타 정정(0002_engine_ua_fix.sql) · parse_status 의미 확정(ok/empty/failed). ⚠️ 이 때문에 아이티데일리 시드는 0003으로 밀림(재번호 M1 담당 확인 대기).

다음 회차
worker + api 실구현

두 엔드포인트 · GCS 원본 저장 · Pull API · 알림 · 로컬 e2e (지시서 260811-engine-m2-worker-api.md §3~§5). epnc 시드로 병행 착수 가능 — 그것이 테크월드를 2호로 유지하는 실질 이유.

M3
인프라 — Terraform

Cloud Run Service 3 + backfill Job + Scheduler + CI 2레인 + Dockerfile. 게이트: plan drift 0 → 승인 apply. 🔴 Cloud SQL 재생성 신호 시 즉시 중단.

M0 · M1
사람 작업 + 시드

UA 사전 공유(메일) · robots.txt 조회 · 셀렉터 실측 → 그 측정 문서를 인용하는 0003 시드. 이때 #21의 가드(측정 인용)와 #24의 가드(UA DB 행 대조)가 동시에 그 커밋을 검사한다.

M6
개통 데모 = 파일럿 완료

Scheduler 실트리거 → GCS 객체 + engine.articles 행 → 어댑터 → raw_items, takedown·backfill·실패 알림까지 7개 술어 실증.

§6

핵심 Q&A

이 가이드가 나온 세션의 질문들 — 그대로 다시 물어도 이 답이 나옵니다.

PR #24를 머지하면 아이티데일리가 수집되나?

아니요 그 PR에 서비스 로직이 0줄이고, 엔진 Cloud Run Service·Scheduler·itdaily 시드가 전부 없습니다. 개통은 M3 이후 + M0 완료 시점입니다.

MVP 워크플로우가 LLM(모델 배선)을 거치나?

0건 ①~⑥ 어디에도 모델 호출이 없습니다 — 우연이 아니라 검사기로 강제되는 계약(ADR-0008 D7a). 모델 API와 스치는 유일한 지점은 M-B⑤ 임베딩 판단인데, 그것도 호출이 아니라 계약 판단 대기이며 M6을 차단하는 블로커입니다.

MVP의 산출물은 원문 기사인가, 재구성된 기사인가?

아이티데일리의 원문입니다. 끝났을 때 존재하는 것은 GCS 원본 HTML · engine.articles 파싱 산출물 · 매거진 raw_items 사본 — 전부 원문 계열이고 재구성 기사는 0건.

함의: 산출물 전부가 아이티데일리 저작물이라, MVP가 증명하는 것은 "기사를 만들 수 있다"가 아니라 "남의 원문을 계약대로 안전하게 보관·전달·회수할 수 있다"입니다.

"끝"까지 가면 Cloud SQL·GCS 적재는 완료인가?

그것도 훨씬 먼저 끝납니다 — GCS는 파싱 전(④-b), engine.articles는 파싱 직후(④-d), raw_items는 ⑥. 발행(⑨)은 새 적재가 아니라 이미 있는 행의 status 값 변경 1건입니다.

결과물을 외부 서비스(예: 비즈크러시)에 REST API로 제공하는 건 워크플로우에 없나?

포함 그게 바로 ⑤ 엔진 Pull API입니다 — REST + OpenAPI, 서비스별 키. 비즈크러시 매거진은 그 API의 1호 소비자일 뿐입니다.

단, 표면이 둘입니다: 수집 원문 제공 = 엔진 Pull API(MVP 포함) vs 발행 기사 제공 = 매거진 서빙 API /api/articles(M7 · 미착수 · MVP 밖).

매거진 워커로 아이티데일리를 그냥 수집하면 안 되나? (enabled: true만 바꾸면 되는데)

⛔ 금지 기술적으로 가능해 보여도 사유가 셋: ① GCS 원본 저장이 없어 셀렉터가 틀리면 복구 경로가 사라짐(침묵 실패) ② UA가 다름 — 사전 통지할 값과 실제 보낼 값이 갈림 ③ takedown·suppression 등 계약 이행 축이 매거진 경로에 없음.

§7

미결·주의 — 기준 시점에 열려 있는 것

BLOCKER
UA 사전 공유 메일 (M-B②)

전체 경로의 선두는 UA 문자열 1개와 메일 1통 — 코드가 아닙니다. 이것이 끝나야 robots.txt 조회 → M0 실측이 열립니다.

BLOCKER
M-B⑤ 임베딩 판단 — M6 차단

보관은 계약이 허용했으나 벡터화(임베딩)가 그 목적의 수단인지 미판단. 어댑터가 raw_items에 넣는 순간 매거진 사이클이 임베딩을 하게 되므로 M6 전에 답이 필요합니다.

미결
매거진 워커 리전 — 문서(도쿄) vs 실물(서울)

ADR-0005 §4·deploy.md §2는 도쿄, 2026-08-11 실물 배포는 서울. 의도면 문서를 rev, 실수면 배포를 이동 — 어느 쪽이든 하나는 고칩니다. 확인 주체: 정훈.

미결
M3에서 확정할 것 3건

sources.schedule vs Scheduler cron 이중 진실(권고: Scheduler가 진실) ② 첫 실행 버스트 — RSS 50건 연타 방지(초회 제한/스태거) ③ rate_class 컬럼의 지위 — 큐 1개 공유라 현재 강제 수단 없음.

주의
기존 다이어그램의 낡은 표기

수집 대상이 "테크월드 epnc.co.kr"로 그려진 그림이 돌고 있습니다. ADR-0012 §rev3 이후 1소스는 아이티데일리(테크월드는 2호·enabled=false) — 구조는 그대로, 매체 이름만 낡았습니다.

주의
🔴 메타 셀렉터의 침묵 실패 (PR #24가 남긴 함정)

본문은 2단 fallback인데 제목·기자·섹션은 셀렉터 단독. 메타 셀렉터가 틀리면 본문은 살아서 ok가 나고 알림 없이 메타만 빕니다. engine.articles는 불변이라 다시 긁어도 안 고쳐짐 — 진짜 예방책은 M0 셀렉터 실측입니다.