o2o-castad-backend/docs/architecture.html

789 lines
26 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>O2O CastAD Backend - 인프라 아키텍처</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--surface2: #232733;
--border: #2e3345;
--text: #e1e4ed;
--text-dim: #8b90a0;
--accent: #6c8cff;
--accent2: #a78bfa;
--green: #34d399;
--orange: #fb923c;
--red: #f87171;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.8;
}
/* 네비게이션 */
nav {
position: fixed;
top: 0;
width: 100%;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 100;
display: flex;
align-items: center;
padding: 0 40px;
height: 60px;
gap: 40px;
}
nav .logo {
font-size: 1.1rem;
font-weight: 700;
color: var(--accent);
white-space: nowrap;
}
nav a {
color: var(--text-dim);
text-decoration: none;
font-size: 0.9rem;
transition: color 0.2s;
}
nav a:hover { color: var(--accent); }
/* 메인 */
main {
max-width: 1400px;
margin: 0 auto;
padding: 100px 32px 80px;
}
.hero {
text-align: center;
margin-bottom: 60px;
}
.hero h1 {
font-size: 2.2rem;
margin-bottom: 8px;
background: linear-gradient(135deg, var(--accent), var(--accent2));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero p {
color: var(--text-dim);
font-size: 1rem;
}
/* 섹션 */
section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 36px;
margin-bottom: 40px;
}
.section-header {
margin-bottom: 24px;
}
.section-header h2 {
font-size: 1.4rem;
margin-bottom: 4px;
color: var(--accent);
}
.section-header h2 .num {
display: inline-block;
background: var(--accent);
color: #fff;
width: 28px;
height: 28px;
border-radius: 50%;
text-align: center;
line-height: 28px;
font-size: 0.85rem;
margin-right: 8px;
}
.section-header .desc {
color: var(--text-dim);
font-size: 0.95rem;
}
/* 좌우 2컬럼 (테이블/텍스트용) */
.cols {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 28px;
align-items: start;
}
.col { min-width: 0; }
/* 다이어그램 - 항상 풀 와이드, 아래 배치 */
.diagram-box {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 32px;
margin-top: 28px;
overflow-x: auto;
}
.diagram-box .mermaid {
display: flex;
justify-content: center;
}
.diagram-box .mermaid svg {
max-width: 100%;
height: auto;
min-height: 300px;
}
.diagram-label {
text-align: center;
font-size: 0.85rem;
color: var(--text-dim);
margin-top: 12px;
}
/* 서브 타이틀 */
h3 {
font-size: 1rem;
color: var(--text);
margin: 20px 0 10px;
}
h3:first-child { margin-top: 0; }
/* 테이블 */
table {
width: 100%;
border-collapse: collapse;
margin: 12px 0;
font-size: 0.88rem;
}
th, td {
padding: 9px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th {
background: var(--surface2);
color: var(--accent);
font-weight: 600;
font-size: 0.82rem;
text-transform: uppercase;
white-space: nowrap;
}
td { color: var(--text-dim); }
/* 태그 */
.tag {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
}
.tag-green { background: rgba(52,211,153,0.15); color: var(--green); }
.tag-orange { background: rgba(251,146,60,0.15); color: var(--orange); }
.tag-red { background: rgba(248,113,113,0.15); color: var(--red); }
/* 비용 카드 */
.cost-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin: 16px 0 24px;
}
.cost-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 20px;
text-align: center;
}
.cost-card .stage {
font-size: 0.85rem;
color: var(--text-dim);
margin-bottom: 4px;
}
.cost-card .amount {
font-size: 1.4rem;
font-weight: 700;
}
.cost-card .krw {
font-size: 0.85rem;
color: var(--text-dim);
margin-top: 2px;
}
.cost-card.s1 { border-top: 3px solid var(--green); }
.cost-card.s1 .amount { color: var(--green); }
.cost-card.s2 { border-top: 3px solid var(--orange); }
.cost-card.s2 .amount { color: var(--orange); }
.cost-card.s3 { border-top: 3px solid var(--red); }
.cost-card.s3 .amount { color: var(--red); }
/* 리스트 */
ul {
padding-left: 18px;
margin: 10px 0;
}
ul li {
color: var(--text-dim);
margin-bottom: 5px;
font-size: 0.92rem;
}
ul li strong { color: var(--text); }
/* 노트 */
.note {
background: rgba(108,140,255,0.08);
border-left: 3px solid var(--accent);
padding: 12px 16px;
border-radius: 0 8px 8px 0;
font-size: 0.88rem;
color: var(--text-dim);
margin-top: 20px;
}
code {
background: var(--surface2);
border: 1px solid var(--border);
padding: 1px 5px;
border-radius: 4px;
font-size: 0.84rem;
color: var(--green);
}
/* PDF 다운로드 버튼 */
.btn-pdf {
margin-left: auto;
padding: 6px 16px;
background: var(--accent);
color: #fff;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.btn-pdf:hover { background: #5a7ae6; }
/* 반응형 */
@media (max-width: 1024px) {
main { padding: 80px 20px 60px; }
.cols { grid-template-columns: 1fr; }
.cost-cards { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
nav { padding: 0 16px; gap: 16px; }
nav a { font-size: 0.8rem; }
section { padding: 20px; }
.hero h1 { font-size: 1.6rem; }
table { font-size: 0.8rem; }
th, td { padding: 6px 8px; }
.diagram-box { padding: 16px; }
}
/* 인쇄 / PDF 저장 — 화면과 동일한 다크 테마 유지 */
@media print {
@page {
size: A3 landscape;
margin: 10mm;
}
* {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
nav { display: none !important; }
.btn-pdf { display: none !important; }
main {
max-width: 100% !important;
padding: 10px !important;
}
section {
break-inside: auto;
page-break-inside: auto;
}
.diagram-box {
break-inside: auto;
page-break-inside: auto;
}
.hero {
margin-bottom: 20px;
}
section {
margin-bottom: 20px;
padding: 20px;
}
.cols {
grid-template-columns: 1fr 1fr !important;
}
.cost-cards {
grid-template-columns: repeat(3, 1fr) !important;
}
}
</style>
</head>
<body>
<nav>
<div class="logo">O2O CastAD</div>
<a href="#load-balancing">부하 분산</a>
<a href="#architecture">아키텍처</a>
<a href="#cost">비용 산출</a>
<button class="btn-pdf" onclick="downloadPDF()">PDF 다운로드</button>
</nav>
<main>
<div class="hero">
<h1>O2O CastAD Backend</h1>
<p>인프라 아키텍처 및 비용 산출 문서</p>
</div>
<!-- ==================== 1. 부하 분산 ==================== -->
<section id="load-balancing">
<div class="section-header">
<h2><span class="num">1</span>DB 및 서버 부하 분산 방법</h2>
<p class="desc">Nginx 로드밸런싱, 커넥션 풀 관리, 단계별 수평 확장 전략</p>
</div>
<div class="cols">
<div class="col">
<h3>현재 구현 현황 (단일 인스턴스)</h3>
<ul>
<li><strong>API 커넥션 풀</strong>: pool_size=20, max_overflow=20 → 최대 <code>40</code></li>
<li><strong>백그라운드 풀</strong>: pool_size=10, max_overflow=10 → 최대 <code>20</code></li>
<li><strong>인스턴스당 총 DB 연결</strong>: <code>40 + 20 = 60</code></li>
<li><strong>풀 리사이클</strong>: 280초 (MySQL wait_timeout 300초 이전), pre-ping 활성화</li>
</ul>
<h3>단계별 확장 전략</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>동시접속</th>
<th>App Server</th>
<th>LB</th>
<th>DB ( MySQL Flexible)</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="tag tag-green">S1</span></td>
<td>~50명</td>
<td>x1</td>
<td>Nginx x1</td>
<td>Burstable B1ms</td>
</tr>
<tr>
<td><span class="tag tag-orange">S2</span></td>
<td>50~200명</td>
<td>x2~4</td>
<td>Nginx</td>
<td>GP D2ds_v4 + Replica x1</td>
</tr>
<tr>
<td><span class="tag tag-red">S3</span></td>
<td>200~1,000명</td>
<td><span style="font-size:0.75rem; line-height:1.4;">API ServerxN<br/>+ Scheduler</span></td>
<td>Nginx</td>
<td>BC D4ds_v4 + Replica x2 + Redis P1</td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<h3>커넥션 풀 수치 계산</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Stage 1 (1대)</th>
<th>Stage 2 (4대)</th>
<th>Stage 3 (8대)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Main Pool / 인스턴스</strong></td>
<td>20+20 = 40</td>
<td>10+10 = 20</td>
<td>5+5 = 10</td>
</tr>
<tr>
<td><strong>BG Pool / 인스턴스</strong></td>
<td>10+10 = 20</td>
<td>5+5 = 10</td>
<td>3+3 = 6</td>
</tr>
<tr>
<td><strong>인스턴스당 소계</strong></td>
<td><code>60</code></td>
<td><code>30</code></td>
<td><code>16</code></td>
</tr>
<tr>
<td><strong>Primary 총 연결</strong></td>
<td>60</td>
<td>4 x 30 = <code>120</code></td>
<td>8 x 16 = <code>128</code></td>
</tr>
<tr>
<td><strong>max_connections 권장</strong></td>
<td>100</td>
<td>200</td>
<td>300</td>
</tr>
</tbody>
</table>
<div class="note" style="margin-top: 16px;">
<strong>핵심:</strong>
JWT Stateless 설계로 Nginx 세션 어피니티 불필요 (round-robin / least_conn).
Stage 2부터 Read Replica로 읽기 분산, Redis는 Stage 3에서 캐싱/Rate Limiting 도입.
</div>
</div>
</div>
<!-- 다이어그램: 내용 아래 풀 와이드 -->
<div class="diagram-box">
<pre class="mermaid">
graph TB
subgraph S1["Stage 1: ~50명"]
direction LR
S1N["Nginx<br/>(Reverse Proxy)"] --> S1A["App Server x1"]
S1A --> S1D[" MySQL<br/>Burstable B1ms"]
end
subgraph S2["Stage 2: 50~200명"]
direction LR
S2N["Nginx<br/>(Reverse Proxy)"] --> S2API["APP Server<br/>x 1 ~ 2"]
S2N --> S2WK["Scheduler<br/>Server"]
S2API --> S2P["MySQL BC<br/>Primary<br/>(D4ds_v4)"]
S2API --> S2R1["Read Replica<br/>x1"]
S2WK --> S2P
S2WK --> S2R1
end
subgraph S3["Stage 3: 200~1,000명"]
direction LR
S3N["Nginx<br/>(Reverse Proxy)"] --> S3API["APP Server<br/>x N"]
S3N --> S3WK["Scheduler<br/>Server"]
S3API --> S3P["MySQL BC<br/>Primary<br/>(D4ds_v4)"]
S3API --> S3R1["Read Replica<br/>xN"]
S3API --> S3RD["Redis<br/>Premium P1"]
S3WK --> S3P
S3WK --> S3R1
end
S1 ~~~ S2 ~~~ S3
style S1 fill:#0d3320,stroke:#34d399,stroke-width:2px,color:#e1e4ed
style S2 fill:#3b2506,stroke:#fb923c,stroke-width:2px,color:#e1e4ed
style S3 fill:#3b1010,stroke:#f87171,stroke-width:2px,color:#e1e4ed
</pre>
</div>
</section>
<!-- ==================== 2. 아키텍처 ==================== -->
<section id="architecture">
<div class="section-header">
<h2><span class="num">2</span>전체 아키텍처 다이어그램</h2>
<p class="desc">Nginx + FastAPI App Server 구성과 외부 서비스 연동 구조</p>
</div>
<div class="cols">
<div class="col">
<ul>
<li><strong>로드밸런서</strong>: Nginx (Reverse Proxy, L7 LB, SSL 종단)</li>
<li><strong>App Server</strong>: FastAPI (Python 3.13) — Auth, Home, Lyric, Song, Video, Social, SNS, Archive, Admin, Background Worker</li>
<li><strong>DB</strong>: Database for MySQL Flexible Server — Stage 2+ Read Replica</li>
</ul>
</div>
<div class="col">
<ul>
<li><strong>캐시</strong>: Cache for Redis (Stage 3 도입)</li>
<li><strong>콘텐츠 생성</strong>: 가사(ChatGPT) → 음악(Suno AI) → 영상(Creatomate) → SNS 업로드</li>
<li><strong>외부 연동</strong>: Kakao OAuth, Naver Map/Search API, Blob Storage</li>
</ul>
</div>
</div>
<!-- 다이어그램: 내용 아래 풀 와이드 -->
<div class="diagram-box">
<pre class="mermaid">
graph TB
Client["클라이언트<br/>(Web / App)"]
LB["Nginx<br/>(Reverse Proxy + SSL 종단)"]
subgraph APP["App Server (FastAPI)"]
direction LR
Auth["Auth"] --- Home["Home"] --- Lyric["Lyric"] --- Song["Song"] --- Video["Video"]
Social["Social"] --- SNS["SNS"] --- Archive["Archive"] --- Admin["Admin"] --- BG["BG Worker"]
end
subgraph DB[" MySQL Flexible Server"]
direction LR
Primary["Primary (R/W)"]
Replica["Read Replica"]
end
subgraph AI["AI 콘텐츠 생성 파이프라인"]
direction LR
ChatGPT["ChatGPT<br/>(가사 생성)"]
Suno["Suno AI<br/>(음악 생성)"]
Creatomate["Creatomate<br/>(영상 생성)"]
ChatGPT --> Suno --> Creatomate
end
subgraph EXT["외부 서비스"]
direction LR
Blob[" Blob<br/>Storage"]
Kakao["Kakao<br/>OAuth"]
YT["YouTube /<br/>Instagram"]
Naver["Naver Map /<br/>Search API"]
end
Redis[" Cache for Redis<br/>(Stage 3 도입)"]
Client -->|HTTPS| LB
LB --> APP
APP --> Primary
APP -->|"읽기 전용"| Replica
APP -.->|"Stage 3"| Redis
APP --> AI
APP --> Blob
APP --> Kakao
APP --> YT
APP --> Naver
style Client fill:#1a3a1a,stroke:#34d399,stroke-width:2px,color:#e1e4ed
style LB fill:#1a3a1a,stroke:#34d399,stroke-width:2px,color:#e1e4ed
style APP fill:#1a2744,stroke:#6c8cff,stroke-width:2px,color:#e1e4ed
style DB fill:#2a1f00,stroke:#fb923c,stroke-width:2px,color:#e1e4ed
style AI fill:#2a0f2a,stroke:#a78bfa,stroke-width:2px,color:#e1e4ed
style EXT fill:#0d2a2a,stroke:#34d399,stroke-width:2px,color:#e1e4ed
style Redis fill:#3b1010,stroke:#f87171,stroke-width:1px,color:#e1e4ed
</pre>
</div>
<p class="diagram-label">전체 시스템 아키텍처 구성도</p>
<div class="note">
<strong>콘텐츠 생성 흐름:</strong> 사용자 요청 → Naver 크롤링 → ChatGPT 가사 생성 → Suno AI 음악 생성 → Creatomate 영상 생성 → Blob 저장 → YouTube/Instagram 업로드
</div>
</section>
<!-- ==================== 3. 비용 산출 ==================== -->
<section id="cost">
<div class="section-header">
<h2><span class="num">3</span>예상 리소스 및 비용</h2>
<p class="desc"> 기반 단계별 월 예상 비용 (인프라 + 외부 API)</p>
</div>
<div class="cost-cards">
<div class="cost-card s1">
<div class="stage">Stage 1 · 동시 ~50명</div>
<div class="amount">$170~390</div>
<div class="krw">약 22~51만원/월</div>
</div>
<div class="cost-card s2">
<div class="stage">Stage 2 · 동시 50~200명</div>
<div class="amount">$960~2,160</div>
<div class="krw">약 125~280만원/월</div>
</div>
<div class="cost-card s3">
<div class="stage">Stage 3 · 동시 200~1,000명</div>
<div class="amount">$3,850~8,500</div>
<div class="krw">약 500~1,100만원/월</div>
</div>
</div>
<div class="cols">
<div class="col">
<h3>항목별 비용 상세</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Stage 1</th>
<th>Stage 2</th>
<th>Stage 3</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>App Server</strong></td>
<td>$50~70</td>
<td>$200~400</td>
<td>$600~1,000</td>
</tr>
<tr>
<td><strong>Nginx</strong></td>
<td>-</td>
<td>포함 / VM $15~30</td>
<td>VM $30~60</td>
</tr>
<tr>
<td><strong>MySQL Primary</strong></td>
<td>B1ms $15~25</td>
<td>GP $130~160</td>
<td>BC $350~450</td>
</tr>
<tr>
<td><strong>MySQL Replica</strong></td>
<td>-</td>
<td>GP x1 $130~160</td>
<td>BC x2 $260~360</td>
</tr>
<tr>
<td><strong>Redis</strong></td>
<td>-</td>
<td>-</td>
<td>P1 $225</td>
</tr>
<tr>
<td><strong>스토리지/네트워크</strong></td>
<td>$10~20</td>
<td>$55~100</td>
<td>$160~270</td>
</tr>
<tr>
<td><strong>AI API (합계)</strong></td>
<td>$90~280</td>
<td>$400~1,250</td>
<td>$2,100~5,800</td>
</tr>
</tbody>
</table>
</div>
<div class="col">
<h3>DB 용량 예측 (1년 후)</h3>
<table>
<thead>
<tr>
<th></th>
<th>Stage 1 (500명)</th>
<th>Stage 2 (5,000명)</th>
<th>Stage 3 (50,000명)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>DB 용량</strong></td>
<td>~1.2GB</td>
<td>~12GB</td>
<td>~120GB</td>
</tr>
<tr>
<td><strong>Blob 스토리지</strong></td>
<td>~1.1TB</td>
<td>~11TB</td>
<td>~110TB</td>
</tr>
<tr>
<td><strong>MySQL 추천</strong></td>
<td>32GB SSD</td>
<td>128GB SSD</td>
<td>512GB SSD</td>
</tr>
</tbody>
</table>
<div class="note" style="margin-top: 16px;">
<strong>비용 최적화 팁:</strong>
3rd party 의존도 낮춰야함
<br>
Blob Lifecycle Policy (30일 미접근 → Cool 티어),
</div>
</div>
</div>
<!-- 다이어그램: 내용 아래 풀 와이드 -->
<div class="diagram-box">
<pre class="mermaid">
pie title Stage 3 월 비용 구성 비중
"App Server (APP+Scheduler)" : 800
"Nginx" : 45
"MySQL Primary" : 400
"MySQL Replica x2" : 310
"Redis Premium" : 225
"스토리지/네트워크" : 215
"OpenAI API" : 550
"Suno AI" : 1400
"Creatomate" : 2000
</pre>
</div>
<p class="diagram-label">Stage 3 월간 비용 구성 비율 — AI API 비중이 전체의 약 66%</p>
</section>
</main>
<script>
function downloadPDF() {
// Mermaid SVG가 렌더링된 후 인쇄
window.print();
}
mermaid.initialize({
startOnLoad: true,
theme: 'dark',
themeVariables: {
primaryColor: '#1a2744',
primaryTextColor: '#e1e4ed',
primaryBorderColor: '#6c8cff',
lineColor: '#6c8cff',
secondaryColor: '#232733',
tertiaryColor: '#2e3345',
pieSectionTextColor: '#e1e4ed',
pieLegendTextColor: '#e1e4ed',
pieTitleTextColor: '#e1e4ed',
pieStrokeColor: '#2e3345'
}
});
</script>
</body>
</html>