๐์ ์ฒด ์ํคํ ์ฒ
์นํฐ ์์ฑ ์๋น์ค๋ ํฌ๊ฒ 5๊ฐ์ง ํต์ฌ ์์๋ก ๊ตฌ์ฑ๋ฉ๋๋ค.
- Frontend (์น/์ฑ)
- ์ ์ ๊ฐ ์นํฐ์ ๋ง๋ค๊ณ ์ ๋ก๋ํ๋ ์ธํฐํ์ด์ค
- Backend API (Spring Boot)
- ์ ์ ์ธ์ฆ, ์นํฐ ์์ฑ ์์ฒญ ์ฒ๋ฆฌ, ์นํฐ ๋ฐ์ดํฐ ๊ด๋ฆฌ
- AI Model Server (Python, FastAPI, Flask) ---> ai ์๋ฒ๋ฅผ ๋ง๋ค์ง ๋ง์ง
- AI ๋ชจ๋ธ์ ํ์ฉํด ์ด๋ฏธ์ง ์์ฑ, ํธ์ง ๋ฐ ์คํ์ผ ๋ณํ ์ํ
- Database (PostgreSQL, Redis)
- ์ ์ , ์นํฐ ์ ๋ณด, ์ค์ ๊ฐ ์ ์ฅ
- Storage (S3/MinIO)
- ์นํฐ ์๋ณธ ์ด๋ฏธ์ง ๋ฐ ์์ฑ๋ ์ด๋ฏธ์ง ์ ์ฅ
- S3๋ฅผ ์ง์ ๊ตฌ์ถํด์ ์ฐ๊ณ ์ถ๋ค๋ฉด MinIO
๐ SSE๋ฅผ ํ์ฉํ ์๋ฒ ์ค๊ณ
โ
SSE(Server-Sent Events)์ Kafka๋ฅผ ํ์ฉํ์ฌ ์นํฐ ์์ฑ ์ํ๋ฅผ ์ค์๊ฐ์ผ๋ก ์ ๊ณตํ๋ ๊ตฌ์กฐ ์ค๊ณ
โ
์นํฐ ์์ฑ ์์ฒญ์ Kafka๋ฅผ ํตํด AI ์๋ฒ๋ก ์ ๋ฌ๋๋ฉฐ, AI ์๋ฒ๋ SSE๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ์ค์๊ฐ์ผ๋ก ์ํ ์ ์ก
โ
์ต์ข
์นํฐ ์ด๋ฏธ์ง๋ S3/MinIO์ ์ ์ฅ๋๋ฉฐ, ํด๋ผ์ด์ธํธ๋ ์์ฑ์ด ์๋ฃ๋๋ฉด ํด๋น ์ด๋ฏธ์ง๋ฅผ ๋ณผ ์ ์์
๐ ์ด๋ฅผ ํตํด ์ฌ์ฉ์๋ ์นํฐ์ด ์ค์๊ฐ์ผ๋ก ์์ฑ๋๋ ๊ณผ์ ์ ๋ณผ ์ ์์ผ๋ฉฐ, ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ ๊ฐ๋ฅ! ๐ฅ
๐ SSE(Server-Sent Events)๋?
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์ฐ๊ฒฐ์ ์ ์งํ๊ณ , ์๋ฒ๊ฐ ์ค์๊ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ ์ ์๋ HTTP ๊ธฐ๋ฐ์ ํต์ ๋ฐฉ์.
- ์ฑํ , ์คํธ๋ฆฌ๋ฐ, AI ์์ ์ํ ์ ๋ฐ์ดํธ ๊ฐ์ ์ค์๊ฐ ์์ฉ์ ์ ํฉ.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Client (React) โ
โโโโโโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโค
โ โ โ
โ SSE (์นํฐ ์์ฑ ์ํ) โ
โ โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ โ
โ Webtoon API Server โ โ
โ (Spring Boot + SSE) โ โ
โโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโค โ
โ โ Kafka โ โ
โโโโโโโโโโโโโผโโโโโโโโโโโโโโโโค โ
โ AI Model Server โ โ
โ (FastAPI, PyTorch) โ โ
โโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโค โ
โ S3 / MinIO (Storage) โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
(1) ์นํฐ ์์ฑ ์์ฒญ
๐ ์์ฒญ API (POST /api/webtoon/generate)
- ์ ์ ๊ฐ ์นํฐ ์์ฑ ์์ฒญ์ ๋ณด๋ด๋ฉด, ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌ๋จ.
- Kafka๋ฅผ ํตํด AI ๋ชจ๋ธ ์๋ฒ๋ก ์นํฐ ์์ฑ ์์ฒญ์ ๋ณด๋.
@PostMapping("/api/webtoon/generate")
public ResponseEntity<String> generateWebtoon(@RequestBody WebtoonRequest request) {
String taskId = UUID.randomUUID().toString();
// Kafka๋ฅผ ํตํด AI ์๋ฒ๋ก ์นํฐ ์์ฑ ์์ฒญ
webtoonProducer.sendMessage("webtoon-tasks", taskId, request);
return ResponseEntity.ok(taskId);
}
(2) ํด๋ผ์ด์ธํธ๊ฐ ์นํฐ ์์ฑ ์ํ ๊ตฌ๋ (SSE)
๐ SSE ์ฐ๊ฒฐ API (GET /api/webtoon/status/{taskId})
- ํด๋ผ์ด์ธํธ๋ SSE๋ฅผ ํตํด ์นํฐ ์์ฑ ์ํ๋ฅผ ์ค์๊ฐ์ผ๋ก ํ์ธํ ์ ์์.
- AI ๋ชจ๋ธ์ด ์งํ ์ํ๋ฅผ Kafka๋ก ์ ์กํ๋ฉด, ์ด๋ฅผ SSE๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌ.
@GetMapping(value = "/api/webtoon/status/{taskId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter subscribeWebtoonStatus(@PathVariable String taskId) {
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
webtoonStatusService.addEmitter(taskId, emitter);
emitter.onCompletion(() -> webtoonStatusService.removeEmitter(taskId));
emitter.onTimeout(() -> webtoonStatusService.removeEmitter(taskId));
return emitter;
}
(3) AI ์๋ฒ๊ฐ ์นํฐ ์์ฑ ํ ์ํ ์ ๋ฐ์ดํธ
๐ AI ์๋ฒ (FastAPI)
- AI ๋ชจ๋ธ์ด Kafka์์ ๋ฉ์์ง๋ฅผ ๋ฐ์ ์นํฐ์ ์์ฑํ๊ณ ์ํ๋ฅผ ์ ๋ฐ์ดํธํจ.
from kafka import KafkaConsumer, KafkaProducer
import json
import time
consumer = KafkaConsumer(
'webtoon-tasks',
bootstrap_servers='kafka:9092',
value_deserializer=lambda x: json.loads(x.decode('utf-8'))
)
producer = KafkaProducer(
bootstrap_servers='kafka:9092',
value_serializer=lambda x: json.dumps(x).encode('utf-8')
)
for message in consumer:
task_id = message.key
data = message.value
# 1. ์นํฐ ์์ฑ ์์
producer.send('webtoon-status', key=task_id, value={"status": "started"})
time.sleep(2)
# 2. AI ๋ชจ๋ธ ์คํ (๊ฐ์ )
producer.send('webtoon-status', key=task_id, value={"status": "generating"})
time.sleep(5)
# 3. ์์ฑ ์๋ฃ
image_url = f"https://s3.bucket.com/{task_id}.png"
producer.send('webtoon-status', key=task_id, value={"status": "completed", "url": image_url})
(4) ์๋ฒ๊ฐ SSE๋ฅผ ํตํด ํด๋ผ์ด์ธํธ์ ์ํ ์ ๋ฌ
//๐ Spring Boot์์ Kafka ๋ฉ์์ง๋ฅผ ๋ฐ์ SSE๋ก ์ ์ก
@KafkaListener(topics = "webtoon-status")
public void listenWebtoonStatus(@Payload String message, @Header(KafkaHeaders.RECEIVED_KEY) String taskId) {
webtoonStatusService.sendUpdate(taskId, message);
}
//๐ SSE ์ํ ์
๋ฐ์ดํธ ์๋น์ค
@Service
public class WebtoonStatusService {
private final Map<String, SseEmitter> emitters = new ConcurrentHashMap<>();
public void addEmitter(String taskId, SseEmitter emitter) {
emitters.put(taskId, emitter);
}
public void sendUpdate(String taskId, String status) {
SseEmitter emitter = emitters.get(taskId);
if (emitter != null) {
try {
emitter.send(SseEmitter.event().data(status));
} catch (IOException e) {
emitter.complete();
emitters.remove(taskId);
}
}
}
public void removeEmitter(String taskId) {
emitters.remove(taskId);
}
}
4. ๐ SSE๋ฅผ ํ์ฉํ ํด๋ผ์ด์ธํธ ์ฝ๋ (React)
//๐ React์์ SSE๋ฅผ ํตํด ์ํ ์
๋ฐ์ดํธ ๋ฐ๊ธฐ
import { useState, useEffect } from "react";
const WebtoonGenerator = ({ taskId }) => {
const [status, setStatus] = useState("Waiting...");
useEffect(() => {
const eventSource = new EventSource(`/api/webtoon/status/${taskId}`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
setStatus(data.status);
if (data.status === "completed") {
console.log("์นํฐ ์์ฑ ์๋ฃ:", data.url);
eventSource.close();
}
};
return () => eventSource.close();
}, [taskId]);
return <div>์นํฐ ์ํ: {status}</div>;
};
๐ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ (MSA)
โ Spring Boot API + FastAPI AI ์๋ฒ ๋ถ๋ฆฌํ์ฌ ํ์ฅ์ฑ ์๋ ์ํคํ ์ฒ ์ค๊ณ
โ Kafka ๋น๋๊ธฐ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์นํฐ ์์ฑ ์ฒ๋ฆฌ
โ Redis ์บ์ฑ ๋ฐ JWT ์ธ์ฆ ์ ์ฉ
โ S3 / MinIO๋ฅผ ์ด์ฉํ ์ด๋ฏธ์ง ์ ์ฅ
โ Kubernetes ๊ธฐ๋ฐ AI ์๋ฒ ๋ฐฐํฌ (GPU ์ค์ผ์ผ๋ง)
๐ ์นํฐ ์์ฑ AI ์๋น์ค๊ฐ ํ์ฅ ๊ฐ๋ฅํ๊ณ , ๋๋์ ์นํฐ ์์ฒญ๋ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์! ๐
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FRONTEND (React) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ API GATEWAY (Spring Cloud Gateway) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โโโโโโโโดโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโดโโโโโโโโโ
โ Webtoon API (Spring) โ โ AI Model Server โ
โ - ์ ์ ์ธ์ฆ (JWT) โ โ (FastAPI, PyTorch) โ
โ - ์นํฐ ์์ฑ ์์ฒญ โ โ - ์นํฐ ์คํ์ผ ์ ์ฉ โ
โ - DB ์ ์ฅ โ โ - AI ์ด๋ฏธ์ง ์์ฑ โ
โ - ์์
์ํ ๊ด๋ฆฌ โ โ - Task Queue ๊ด๋ฆฌ โ
โโโโโโโโโโโโฌโโโโโโโโโโโโ โโโโโโโโโโโโฌโโโโโโโโโโโ
โ โ
โ โ
โโโโโโโโดโโโโโโโโโโโโโ โโโโโโโโดโโโโโโโโโโโโโ
โ PostgreSQL โ โ S3 โ
โ (์ ์ & ์นํฐ ์ ๋ณด) โ โ (์ด๋ฏธ์ง ์ ์ฅ) โ
โโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโ
์นํฐ ์์ฑ ์์ฒญ์ด ๋ค์ด์ค๋ฉด Kafka๋ฅผ ํตํด AI ์๋ฒ๋ก ์ ๋ฌ
AI ์๋ฒ๊ฐ ๊ฒฐ๊ณผ๋ฅผ ์์ฑํ๋ฉด Webtoon API ์๋ฒ์์ ์ต์ข ๊ฒฐ๊ณผ ์ ์ฅ
๊ฐ ์๋น์ค ์ญํ
- Gateway (Spring Cloud Gateway)
- API ๋ผ์ฐํ ๋ฐ ์ธ์ฆ ์ฒ๋ฆฌ
- Webtoon API (Spring Boot)
- ์ ์ ์ธ์ฆ, ์นํฐ ์์ฑ ์์ฒญ ์ฒ๋ฆฌ, DB ์ฐ๋
- AI Model Server (Python, FastAPI)
- ์นํฐ ์ด๋ฏธ์ง ์์ฑ ๋ฐ ํํฐ ์ ์ฉ
- File Storage (S3 / MinIO)
- ์นํฐ ์ด๋ฏธ์ง ์ ์ฅ ๋ฐ ์บ์ฑ
Gateway (Spring Cloud Gateway)
- JWT ์ธ์ฆ ์ ์ฉ (X-User-Id, X-User-Role ํค๋ ์ ๋ฌ)
- API ์์ฒญ์ ์ ์ ํ ์๋ฒ๋ก ๋ผ์ฐํ (/api/** โ Webtoon API, /ml/** โ AI ์๋ฒ)
Webtoon API (Spring Boot)
๐ ์ฃผ์ ๊ธฐ๋ฅ
- ์ ์ ์ธ์ฆ ๋ฐ ์ธ์ ๊ด๋ฆฌ (JWT)
- ์นํฐ ์์ฑ ์์ฒญ ๊ด๋ฆฌ (๋น๋๊ธฐ ์ฒ๋ฆฌ)
- ์์ฑ๋ ์นํฐ ์ ์ฅ ๋ฐ ์กฐํ
- Redis ์บ์ฑ ์ ์ฉ (์์ฃผ ์กฐํ๋๋ ๋ฐ์ดํฐ)
Webtoon API ๋ช ์ธ์
Method | Endpoint | |
POST | /api/webtoon | ์๋ก์ด ์นํฐ ์์ฑ ์์ฒญ |
GET | /api/webtoon/{id} | ํน์ ์นํฐ ์์ธ ์กฐํ |
GET | /api/webtoon/list | ์ฌ์ฉ์์ ์นํฐ ๋ฆฌ์คํธ |
DELETE | /api/webtoon/{id} | ํน์ ์นํฐ ์ญ์ |
AI Model Server (FastAPI)
๐ ์ญํ
- ์นํฐ ์คํ์ผ ๋ณํ ๋ฐ AI ์ด๋ฏธ์ง ์์ฑ
- ์นํฐ ์๊ฐ ๋ณด์ , ์คํ์ผ ๋ณํ ๋ฑ ์ถ๊ฐ ๋ชจ๋ธ ์ ๊ณต ๊ฐ๋ฅ
AI API ๋ช ์ธ์
Method | Endpoint | |
POST | /ml/generate | ์นํฐ ์์ฑ ์์ฒญ |
POST | /ml/style/{id} | ์คํ์ผ ๋ณํ |
POST | /ml/status/{taskId} | ์์ ์ํ ์กฐํ |
๊ทธ๋ฆผ AI ๋ชจ๋ธ
https://modulabs.co.kr/blog/controlnet-stable-diffusion
ControlNet ์ผ๋ก ์ด๋ฏธ์ง์์ฑ (feat. Stable Diffusion)
Stable Diffusion์ ๋๋ผ์ด ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฌ์ฃผ์์ผ๋ ๋์ฑ ์ธ๋ถ์ ์ธ ์ ์ด๊ฐ ํ์ํจ์ ์๊ฒ ๋์ด ControlNet ์ด๋ผ๋ ์๋ก์ด ๊ธฐ์ ์ด ์ ์๋์์ต๋๋ค. ControlNet ์ ์ฌ์ฉ์๊ฐ ์์ฑ ๊ณผ์ ์ ๋์ฑ ์ธ๋ถ์ ์ผ๋ก ์ ์ด
modulabs.co.kr
Database (PostgreSQL + Redis)
- PostgreSQL โ ์ ์ ์ ๋ณด, ์นํฐ ๋ฉํ๋ฐ์ดํฐ ์ ์ฅ
- Redis โ ์์ ์ํ, ์บ์ฑ, ์ธ์ ๊ด๋ฆฌ
CREATE TABLE webtoon (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
title VARCHAR(255) NOT NULL,
status VARCHAR(50) CHECK (status IN ('pending', 'processing', 'completed', 'failed')),
image_url TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Storage (S3)
AI ๋ชจ๋ธ์ด ์์ฑํ ์นํฐ ์ด๋ฏธ์ง๋ฅผ S3์ ์ ์ฅ
s3://webtoon-images/{userId}/{webtoonId}/cover.jpg
s3://webtoon-images/{userId}/{webtoonId}/page1.jpg
'ํ์ต ๊ธฐ๋ก (Learning Logs) > CS Study' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
AI๋ฅผ ํ์ฉํ webtoon maker ๋ง๋ค๊ธฐ (0) | 2025.02.20 |
---|---|
WebRTC - Communicating with clients (0) | 2025.02.20 |
SSE (Server Sent Events) (0) | 2025.02.20 |
Real-time Notification Service Using WebSocket (0) | 2025.02.20 |
concurrenct problem (0) | 2025.02.13 |