주제를 입력하면 8단계 품질 파이프라인이 '출판 품질의 책 PDF'를 생성
이 문서는 고객이 입력한 주제로 "출판 품질의 책"을 PDF로 생성하는 플랫폼(BookForge) 을 하나의 Azure 아키텍처 사례로 보고, 어떤 서비스로 어떻게 구성했는지, 왜 그렇게 설계했는지를 직접 구축하려는 엔지니어 관점에서 설명합니다. (기준일: 2026-06-17, 리전: Container Apps = Korea Central, Azure OpenAI = Korea Central)
⚠️ 면책: 본 데모로 생성되는 책은 AI 저작물입니다. 사실·수치는 참고용이며, 중요한 의사결정 전 원출처 확인을 권장합니다.
BookForge는 "주제·형태·내용·참고자료·분량 입력 → 8단계 품질 파이프라인 → 표지·목차·본문·참고문헌까지 조판된 책 PDF"를 만들어내는 웹 플랫폼입니다. 이 데모의 성패 지표는 단 하나, 책 내용의 품질입니다. 그래서 "한 번에 길게 쓰기"가 아니라 여러 단계로 깎아내는 방식으로 설계했습니다.
gpt-image-1(키리스)로 그립니다. 글은 gpt-5.4가 씁니다.성인 대상 "책"과 달리, 동화책의 성패는 그림 품질 + 연령 적합성 + 캐릭터 일관성입니다. 그래서 별도 파이프라인을 둡니다.
age): 나이를 4구간(0–3 / 4–6 / 7–9 / 10–12)으로 매핑해 문장 길이·어휘·페이지 수·주제 난이도를 조절.
예) 0–3세는 한 쪽 1문장·반복 후렴, 10–12세는 짧은 문단·풍부한 어휘.art_style): 8종 스타일을 동일 피사체(아기 여우) 스와치로 미리 보여주고 고르게 합니다
(web/styles/*.png, /api/styles). 고른 화풍의 프롬프트 조각이 모든 페이지 그림에 적용됩니다.art_anchor(종·색·옷·특징)로 못박고,
모든 페이지 그림 프롬프트 앞에 동일하게 주입해 같은 캐릭터/색감이 유지되게 합니다(이미지 모델엔 seed가 없어
텍스트 앵커로 일관성 확보). 글자는 그림에 넣지 않고(no text) 조판 단계에서 본문을 얹습니다. 인터넷 ──HTTPS──▶ Azure Container Apps (FastAPI + 정적 프런트, Korea Central)
bookforge-api (api/server.py, ingress 8000, 단일 레플리카)
- GET / 입력 폼 + 진행상황 실시간 폴링 UI(web/index.html)
- POST /api/books (multipart) 작업 시작 → {job_id}, 데몬 스레드로 파이프라인 실행
- GET /api/books/{id} 진행상태 폴링(단계·진행률·장별 상태·로그)
- GET /api/books/{id}/pdf 완성 PDF 다운로드
- 시스템 관리 ID(키리스)
│
┌─────────────────┼──────────────────────────────────┐
▼ ▼ ▼
app/grounding.py app/pipeline.py (8단계 오케스트레이션) app/render.py
파일/URL/웹검색 기획→그라운딩→아웃라인→집필→비평·수정 Markdown→HTML→PDF
→ 코퍼스 + 장별 →일관성→교정→렌더 (진행상태 실시간 보고) WeasyPrint(표지/목차/
근거 추출(환각 차단) │ 본문/참고문헌, 나눔폰트)
▼
app/llm.py — gpt-5.4 (키리스 AAD)
DefaultAzureCredential → AzureOpenAI
foundry-uzrz5ojtsjvae · gpt-5.4
(reasoning 모델: max_completion_tokens)
│
▼
app/store.py — 작업상태·산출물 저장
컨테이너 로컬 스토리지(데모) / Blob+PE(프로덕션)
| 서비스 | 역할 | 중요 포인트 |
|---|---|---|
| Azure Container Apps (FastAPI) | 웹 백엔드 + 정적 프런트 bookforge-api |
폼 입력 → 8단계 파이프라인을 데몬 스레드로 실행, 진행상태 폴링. 단일 레플리카(생성·폴링·다운로드 일관성). 시스템 관리 ID(키리스) |
| Azure OpenAI / Foundry — gpt-5.4 | 모든 단계의 추론(기획·집필·비평·일관성) | 키리스(AAD 토큰). reasoning 모델이라 max_completion_tokens 사용 |
| Azure OpenAI — gpt-image-1 (동화책) | 페이지별 일러스트 생성 | 키리스(AAD). koreacentral엔 이미지 모델이 없어 eastus2(foundry-bookimg-mty)에 별도 배포. 표지 1024×1536(high)·페이지 1024×1024(medium) |
| 그라운딩(grounding.py) | 근거의 단일 진실 소스 | 업로드/URL/웹검색 → 코퍼스 → 장별 "검증된 근거" 추출. 수치·인용은 근거 내로 제한(환각 차단) |
| WeasyPrint(render.py) | 책 조판(HTML→PDF) | CSS Paged Media: 표지·판권·자동 목차(페이지번호)·본문(명조)·참고문헌. 컨테이너에 한글 폰트(나눔/노토) 설치 |
| 저장소(store.py) | 작업상태·산출물 보관 | 데모는 컨테이너 로컬(거버넌스 무관·단순). 프로덕션은 Blob + Private Endpoint로 확장 |
| Azure Container Registry (Basic) | 이미지 보관 | az acr build로 WeasyPrint 런타임+한글폰트 포함 이미지 빌드 |
품질이 성패 → 단발 생성이 아니라 다단계 "깎아내기". LLM에게 "좋은 책 한 권 써줘"라고 한 번에 시키면 평균적이고 밋밋한 글이 나옵니다. 그래서 기획으로 방향(thesis)을 고정하고, 아웃라인으로 중복 없는 점층 구성을 짠 뒤, 장을 쓰고 → 별도의 편집자 페르소나가 비평·재작성합니다. 같은 모델이라도 "쓰는 역할"과 "고치는 역할"을 분리하면 결과 밀도가 눈에 띄게 올라갑니다.
근거 그라운딩으로 환각 차단. 책의 신뢰도는 "구체적인데 틀린 숫자" 하나로 무너집니다. 그래서 참고자료 (파일·URL·웹검색)를 코퍼스로 만들고, 각 장마다 그 장에 필요한 근거만 추출해 주입합니다. 집필·수정 프롬프트는 "구체 수치·인용은 이 근거 안에서만"이라고 못박습니다. 참고자료가 없으면 모델 지식으로 쓰되 수치 환각에 주의하도록 명시합니다.
앞 장 요약을 이어붙이는 연속 집필. 장을 독립적으로 쓰면 중복·모순이 생깁니다. 각 장을 쓴 뒤 3문장으로 요약해 다음 장 프롬프트의 맥락으로 넣어, 이어지는 한 권의 책처럼 흐르게 했습니다.
키리스(시크릿 0). 사용자 구독은 Azure OpenAI 키 인증을 차단(disableLocalAuth=true)합니다. 그래서
DefaultAzureCredential로 AAD 토큰을 받아 호출하고, 배포 환경에서는 Managed Identity +
Cognitive Services OpenAI User 역할만 부여합니다. 유출될 키가 존재하지 않습니다.
거버넌스와 싸우지 않는 저장 선택(데모). 처음엔 산출물을 Blob(관리 ID)에 저장하려 했지만, 중앙 거버넌스가
스토리지 publicNetworkAccess를 즉시 Disabled로 강제해 ACA(Consumption, VNet 없음)에서 쓰기가
AuthorizationFailure로 막혔습니다. 데모에서는 컨테이너 로컬 스토리지 + 단일 레플리카로 단순화해
거버넌스와 무관하게 안정적으로 동작시켰습니다. (영속·다중 레플리카가 필요한 프로덕션은 아래 참고.)
출판물다운 조판에 투자. 내용이 좋아도 "메모장 텍스트"처럼 보이면 가치가 전달되지 않습니다. WeasyPrint로
표지·자동 목차(CSS target-counter로 실제 페이지번호)·명조 본문·참고문헌을 만들어 첫인상부터 책이 되게
했습니다. 본문은 세리프(가독성), 제목은 산세리프(위계)로 분리했습니다.
fonts-nanum-coding은 존재하지 않아 빌드 실패 →
fonts-nanum + fonts-noto-cjk만 설치. 한글이 깨지지 않도록 본문/제목 폰트 패밀리에 나눔·노토를 함께 지정.Content-Disposition 파일명/X-Book-Title에
한글을 그대로 넣으면 Starlette가 latin-1로 인코딩하다 깨집니다 → RFC 5987 filename*=UTF-8''<percent-encoded>urllib.parse.quote로 해결.api/ 와 형제 app/, 컨테이너는 /srv 평면 배치 →
server.py가 app/ 위치를 자동 탐지(현재 폴더에 있으면 그대로, 없으면 부모)하도록 sys.path·WEB_DIR 보정.# 로컬 실행
cd demos/bookforge
python3 -m venv .venv && source .venv/bin/activate
pip install -r api/requirements.txt # WeasyPrint는 시스템 pango 필요(macOS: brew install pango)
az login
./run-local.sh # → http://localhost:8000
# Azure 배포(Container Apps, 키리스)
az login && az account set --subscription <SUB_ID>
./deploy.sh # ACR 빌드 → ACA 생성 → 관리ID에 AOAI 역할 부여
# 정리: az group delete -n rg-bookforge --yes --no-wait
프로덕션 확장: 영속·다중 레플리카가 필요하면 산출물 저장을 Blob + Private Endpoint로 전환하세요. MCAPS 거버넌스가 스토리지 공개 접근을 주기적으로 잠그므로, VNet 주입형 Container Apps + Private Endpoint 패턴이 필요합니다(이 포털의 "거버넌스 면역 스토리지" 데모 참고).