블로그 운영을 프로그래밍 방식으로 완벽히 통제하기 위해 설계된 시스템의 전체 내부를 공개합니다. Java 17, Spring Boot 3.2.0 환경에서 WebFlux(WebClient)의 비동기 논블로킹 통신을 기반으로 구축된 이 서버는 단순한 API 스크립트가 아닙니다.
할루시네이션(환각) 제어, 비동기 병렬 번역, 정교한 데이터 검증 로직이 결합된 이 파이프라인의 전체 시스템 다이어그램과 클래스별 세부 구현, 그리고 프롬프트 엔지니어링까지 단 하나의 포스팅으로 모두 분석해 봅니다.
1. 전체 시스템 파이프라인 및 스케줄링 로직
시스템의 메인 스케줄러인 BlogPostScheduler는 데이터 수집부터 다국어 번역 발행까지의 전체 라이프사이클을 통제합니다.
%%{init: {"theme": "dark"}}%%
graph LR
%% 1. 수집 파트
Start((Trigger)) --> RSS{RssService}
RSS -->|피드 소진| Fallback[15단계 Fallback] -.-> RSS
RSS -->|항목 발견| Check[유사도 및 차단 검증]
%% 2. 생성 파트 (위아래 분기)
Check -->|검증 통과| Engine{AI 콘텐츠 엔진}
Engine --> Text[Gemini 2.5: 본문/링크]
Engine --> Img[Vertex AI: 16:9 이미지]
%% 3. 발행 파트 (위아래 분기)
Text --> WP_KR[WordPress 한국어 발행]
Img --> WP_KR
WP_KR --> i18n{Polylang 스케줄링}
i18n --> WP_EN[영어 번역본 발행]
i18n --> WP_ZH[중국어 번역본 발행]
style Start fill:#375A7F,stroke:#fff
style RSS fill:#466B7F,stroke:#fff
style Fallback fill:#5D6470,stroke:#fff
style Check fill:#5D6470,stroke:#fff
style Engine fill:#3D5A53,stroke:#fff
style Text fill:#466B7F,stroke:#fff
style Img fill:#495D5B,stroke:#fff
style WP_KR fill:#375A7F,stroke:#fff
style i18n fill:#44476A,stroke:#fff
style WP_EN fill:#466B7F,stroke:#fff
style WP_ZH fill:#466B7F,stroke:#fff- 순서 보장형 이미지 생성: 원본(한국어) 발행 시에는
concatMap을 활용해 H2 섹션별 이미지를 순서대로 정확하게 생성하고 삽입합니다. - 비동기 병렬 다국어 번역: 영어/중국어 번역본 생성은 병렬(Parallel)로 처리되며, 번역이 실패하더라도 원본 한국어 포스트 발행에는 영향을 주지 않도록 철저히 격리되어 있습니다.
2. 코어 비즈니스 로직: 주요 도메인 클래스
각각의 역할을 수행하는 핵심 서비스 모듈들의 세부 로직입니다.
RssService(데이터 파이프라인): 구글 트렌드 한국 피드를 메인으로 쓰되, 피드가 소진되면 매경, 블로터 등 15개의 예비 URL로 자동 전환합니다(fetchNextEntry). 무의미한 중복을 막기 위해feedCache를 사용하며, Levenshtein Distance로 기존 포스트 제목과 80% 이상 유사할 경우 즉시 스킵합니다. 한 번의 사이클에 최대 20개의 항목을 탐색합니다.ImageGenerationService(비주얼 제어): RSS 원본 이미지가 있다면 Gemini Vision을 통해 외모와 컨텍스트를 분석합니다. 이후 Vertex AI Imagen 3 Fast 모델을 호출해 16:9 비율의 특성 이미지와 H2 섹션 이미지를 생성합니다. AI의 기괴한 얼굴 왜곡과 텍스트 깨짐 현상을 막기 위해negativePrompt를 강력하게 주입합니다.GeminiResponseParser(데이터 정제): LLM 응답에서 ‘thinking’ 파트를 제거하고,escapeUnquotedQuotes복구 및##를<h2>로 자동 변환합니다. 이때 MAX_TOKENS 제한에 걸리거나 H2 태그 누락 등 포맷이 불완전하면 포스트를 발행하지 않고 과감히 스킵합니다.
3. 콘텐츠 생성 및 링크 무결성 검증
가장 많은 환각(Hallucination)이 발생하는 영역이므로, GeminiService 내부에 복합적인 검증 루프를 설계했습니다.
%%{init: {"theme": "dark"}}%%
graph LR
Sch((Scheduler)) --> Prep{LLM 사전 준비}
%% 상하 병렬 배치 1
Prep --> Google[1\. Google Search: 차단 검증]
Prep --> Prompts[2\. prompts/ 핫리로딩 로드]
Google --> LLM[Gemini API 호출]
Prompts --> LLM
LLM --> Post{응답 데이터 정제}
%% 상하 병렬 배치 2
Post --> Link[3\. 서버 단위 링크 Fetch 무결성 검증]
Post --> Clean[4\. 마크다운 및 JSON 파싱 복구]
Link --> End((검증 완료))
Clean --> End
style Sch fill:#375A7F,stroke:#fff
style Prep fill:#5D6470,stroke:#fff
style Google fill:#466B7F,stroke:#fff
style Prompts fill:#495D5B,stroke:#fff
style LLM fill:#3D5A53,stroke:#fff
style Post fill:#5D6470,stroke:#fff
style Link fill:#466B7F,stroke:#fff
style Clean fill:#44476A,stroke:#fff
style End fill:#375A7F,stroke:#fff스마트 재시도 (shouldRetry): 툴 호출(Tool Call) 도중 응답이 짤리거나 불완전하면 도구 개입 없이 내부적으로 자동 재시도합니다.
insertLinkOnce 알고리즘: 본문에 직접 링크 삽입을 지시하면 AI가 가짜 URL을 지어냅니다. 이를 막기 위해 허용된 도메인(나무위키, 정부기관 등)의 후보군(linkCandidates)만 수집합니다. UNSTABLE_URL 정규식으로 1차 필터링 후, 서버 단에서 직접 해당 링크를 Fetch하여 살아있는지 확인합니다. 검증된 링크는 H2/H3 소제목에 우선 병합하고, 없으면 본문 첫 등장 위치에 단 1회만 삽입합니다.
4. 핫리로딩 프롬프트 엔지니어링
이 시스템은 서버를 재시작할 필요 없이 prompts/ 디렉토리의 파일들을 매 호출 시 실시간으로 읽어옵니다. String.formatted() 사용 시의 오류를 방지하기 위해 {{today}}, {{articleContent}} 같은 독자적인 치환 변수를 사용합니다.
주요 프롬프트 제약 조건 (user-prompt.txt):
- 금지어 및 어조: 과도한 유행어나 “난리자베스”, “설레는 마음으로”, “기대컨” 같은 오글거리는 표현, “첫째/둘째”, “결론적으로” 같은 형식적 단어를 전면 금지하여 자연스러운 구어체를 강제합니다.
- 구조 및 분량: 나열할 데이터는 반드시
<ul><li>리스트를 사용해야 합니다. 서론, 본론(H2 3~4개), 결론으로 나누어 600~700 단어(약 1200~1400자) 분량을 칼같이 지킵니다. - SEO 키워드 밀도: Focus Keyword는 명사 조합으로만 생성하고, 제목 앞과 본문 첫 단락에 1회씩 배치하여 밀도를 1% 미만으로 억제합니다.
5. WP REST API 엔티티 맵핑
생성된 데이터(GeminiResponse DTO)는 WordPressService를 통해 워드프레스 생태계와 직접 연동됩니다.직접 결합됩니다.
%%{init: {"theme": "dark"}}%%
graph LR
DTO[GeminiResponse DTO] -->|converts to| WP[WordPress Post]
WP -->|정밀 매칭| Tags[WP Term Taxonomy]
WP -->|직접 주입| SEO[RankMath Meta]
WP -->|컨텍스트 분리| Poly[Polylang i18n]
style DTO fill:#3D5A53,stroke:#fff
style WP fill:#375A7F,stroke:#fff
style Tags fill:#466B7F,stroke:#fff
style SEO fill:#495D5B,stroke:#fff
style Poly fill:#44476A,stroke:#fff태그 무결성: equalsIgnoreCase로 대소문자까지 일치하는 기존 태그만 찾아 재사용하고, lang 파라미터로 언어별 태그 컨텍스트를 완벽히 분리합니다.
SEO 직분사: Application Password (Basic Auth)를 활용해 Rank Math 플러그인의 메타 필드(rank_math_focus_keyword, rank_math_description)에 데이터를 꽂아 넣습니다.
다국어 리소스 최적화: Polylang 엔드포인트에 영/중 번역본을 맵핑할 때, 이미지 비용 절감을 위해 새 이미지를 뽑지 않고 원본 한국어 글의 <figure> 태그를 H2 섹션 순서대로 그대로 복사해 옵니다. 이때 정확한 제목 -> 내부 태그 제거 문자열 -> 끝에 append 하는 3단계 Fallback 매칭을 거칩니다.
6. 핵심 설정 파일 (.env & application.yml)
보안 데이터와 서버 제어 변수를 물리적으로 분리하여 관리합니다.
- .env (인증 정보):
GEMINI_API_KEY,VERTEX_AI_API_KEY및 WP Auth 정보를 관리합니다. - application.yml (비즈니스 제어):
rss.check-interval: 14,400,000ms (4시간) 주기로 구동rss.blocked-keywords: ‘윤석열, 이재명, 한동훈’ 등 강제 차단어 배열gemini-2.5-flash모델 세팅 및maxOutputTokens 16384설정
[주의사항] 서버 구동 시 생성된 모든 글은 임시 저장이 아닌 Publish 상태로 즉시 공개 발행됩니다.
※ 부록 – 프롬프트 개발 가이드
1. 프롬프트 파이프라인 아키텍처 (Prompt Workflow)
7개의 프롬프트가 어느 단계에서 호출되어 시스템을 제어하는지 보여주는 흐름도입니다.
%%{init: {"theme": "dark"}}%%
graph LR
%% 단계별 그룹핑
Start((시작)) --> Block[1\. 사전 검증<br/>block-check-prompt.txt]
Block -->|통과| Sys[2\. 페르소나 주입<br/>system-prompt.txt]
Sys --> Gen[3\. 본문 생성<br/>user-prompt.txt]
Gen --> Valid[4\. 링크 검증<br/>link-verify-prompt.txt]
Valid --> Trans[5\. 다국어 번역<br/>translation-prompt.txt]
Valid --> Img[6\. 이미지 제어<br/>prefix & negative]
style Start fill:#375A7F,stroke:#fff
style Block fill:#5D6470,stroke:#fff
style Sys fill:#466B7F,stroke:#fff
style Gen fill:#3D5A53,stroke:#fff
style Valid fill:#466B7F,stroke:#fff
style Trans fill:#44476A,stroke:#fff
style Img fill:#495D5B,stroke:#fff2. 자아 설정: system-prompt.txt
AI에게 흔들리지 않는 뼈대(Persona)를 심어주는 최상위 지시어입니다.
- 역할 부여: AI에게 “당신은 프로페셔널하고 매력적인 워드프레스 블로그 포스팅 전문가입니다(You are a professional, engaging WordPress blog posting expert)”라는 명확한 자아를 부여합니다.
- 핵심 목표: 고품질의 SEO에 최적화되고 가독성이 높은 한국어 블로그 포스트를 작성하는 것을 목표로 설정합니다.
- 기계적 톤 배제: AI처럼 들리지 않아야 하며(Do not sound like an AI), 뻔한 AI 문구 사용을 금지하고, 실제 가치와 흥미로운 관점을 제공하는 대화형이면서도 권위 있는 어조를 유지하도록 강력하게 지시합니다.
3. 콘텐츠 생성 및 제어: user-prompt.txt
글의 문체, 구조, SEO 규칙, 링크 수집 방식까지 나노 단위로 통제하는 가장 방대하고 핵심적인 프롬프트입니다. {{today}}, {{visionInstruction}}, {{articleContent}} 변수를 주입받아 작동합니다.
3.1 문체 및 가독성 제어 (Tone & Readability)
- 금지어 및 어조: “난리자베스”, “충격자빠” 같은 무리한 합성어 밈이나 “설레는 마음으로”, “기대컨”, “부디” 같은 억지 감성 표현을 전면 금지합니다. “~거든요”, “솔직히” 같은 자연스러운 구어체를 유도하되, “우리 모두 ~해야 할 것 같아요” 식의 도덕적 훈계를 엄격히 차단합니다.
- 문단 구조: 모든 문장마다 줄바꿈하는 것을 금지하고, 2~3개의 연관된 문장을 하나의 단락으로 묶도록 강제합니다.
- 리스트 강제: 본문에 2개 이상 나열되는 항목이 있다면 무조건
<ul><li>또는<ol><li>리스트를 사용해야 하며, H2 섹션 본문에는 최소 1개 이상의 리스트 포함을 기본값으로 설정합니다.
3.2 하드코어 SEO 및 분량 제한
- 분량 규정: 총 600~700 한국어 단어로 제한하며, 서론 80~100단어, 결론 50~70단어, 각 H2 섹션은 100~130단어로 세밀하게 제어합니다.
- 포커스 키워드 제어: 키워드는 2단어 이내의 명사 조합으로만 생성하며(구문 금지), 제목의 맨 앞과 첫 번째 단락에 무조건 1회씩 배치하도록 합니다. 가장 중요한 것은 키워드 밀도를 1% 미만으로 유지하기 위해 전체 글에서 최대 3회까지만 사용하도록 기계적으로 횟수를 제한합니다.
3.3 링크 무결성 및 환각 제어 (Link Candidates)
- 직접 삽입 불가: 본문에 링크를 직접 넣지 말고
linkCandidates배열 필드에 URL 후보, 링크 텍스트(linkText), 그리고 문맥(context)만 수집하도록 지시합니다. - 화이트리스트 통제: 개별 뉴스 기사, 커뮤니티 개별 글, 유튜브 영상은 영구적이지 않으므로 강력히 금지합니다. 오직 나무위키, 위키백과, 공식 사이트, 커뮤니티 메인/카테고리(디시인사이드, 펨코 등)만 허용하며 한국어 포스트의 경우 최소 4~5개를 수집하도록 강제합니다.
4. 데이터 검증 프롬프트 (Validation)
AI가 스스로 자신이 다루는 주제와 링크의 위험성을 판단하게 만드는 방화벽입니다.
- 사전 차단 (
block-check-prompt.txt): 주어진 주제({{topic}})가 한국의 정치인, 정부 관계자, 연예인, 인플루언서, 방송인과 관련된 것인지 판단하여, 일치할 경우{"block": true}를 반환하도록 지시합니다. - 링크 검증 (
link-verify-prompt.txt): 서버가 직접 긁어온 실제 웹페이지 텍스트({{pageText}})가 AI가 의도한 문맥({{context}}) 및 앵커 텍스트({{linkText}})와 일치하는지 분석하여isValid불리언 값을 반환하게 합니다.
5. 다국어 및 비전 통제 (i18n & Vision)
번역과 이미지 생성 과정에서 발생하는 오류를 원천 차단하는 지시어들입니다.
5.1 번역 제약 (translation-prompt.txt)
- 타겟 언어(
{{lang}})로 전체 JSON을 번역하되,tags는 쉼표로 구분된 문자열로 반환하고slug에는 CJK(한중일) 문자를 철저히 배제하여 로마자, 병음, 영단어로만 구성되도록 강제합니다. - 가장 중요한 규칙:
featuredImagePrompt와sectionImages.imagePrompt는 절대로 번역하지 말고 원본 한국어를 100% 그대로 유지하도록 지시하여 이미지 생성 API 연동 시 정합성이 깨지는 것을 막습니다.
5.2 이미지 생성 제어 (image-prompt-prefix.txt & image-negative-prompt.txt)
Vertex AI Imagen 3 Fast 모델 호출 시 직접 주입되는 텍스트입니다.
- Prefix (강제 포함): 고품질 렌더링을 위해 “high quality, detailed”를 앞에 붙이고, 인물 왜곡과 외계어 삽입을 막기 위해 “natural realistic face, no facial distortion, no text, no letters, no writing, no signs, no watermarks, “를 모든 프롬프트에 강제로 이어 붙입니다.
- Negative (강제 배제): 이미지 안에 텍스트가 박히거나 인체가 기괴하게 렌더링되는 것을 막기 위해 “text, writing, letters, words, signature, watermark, logo, bad anatomy, distorted face, weird eyes, extra fingers, mutated, unnatural, cartoon, illustration, drawing, poor quality, blurry, deformed, disfigured”를 네거티브 프롬프트로 강력하게 세팅합니다.

