# Qwen-Image-Layered로 포스터 자동 레이어 분해 (2/10): 기술 스택 선정과 아키텍처 설계
## 기술 스택 선정의 원칙
AI 서비스를 구축할 때 가장 먼저 직면하는 질문: "어떤 언어와 프레임워크를 쓸 것인가?"
이번 프로젝트의 선택 기준:
1. **AI 모델 통합 용이성** - Qwen-Image-Layered는 Python 기반
2. **개발 속도** - 빠른 프로토타이핑과 검증
3. **운영 안정성** - 프로덕션 배포 시 신뢰성
4. **확장성** - 향후 기능 추가 가능성
## 백엔드: Python FastAPI vs Node.js
### 최종 선택: Python + FastAPI
**선택 이유**:
1. **AI 모델 네이티브 지원**
```python
# Qwen-Image-Layered 로딩이 자연스러움
from transformers import Qwen2VLForConditionalGeneration
model = Qwen2VLForConditionalGeneration.from_pretrained(
"Qwen/Qwen-Image-Layered"
)
```
Node.js에서는 Python 브릿지(child_process)가 필요하여 복잡도 증가.
2. **이미지 처리 라이브러리 풍부**
- PIL/Pillow: 이미지 조작
- OpenCV: 고급 후처리
- numpy: 행렬 연산
- scikit-image: Alpha matting
3. **FastAPI의 현대적 기능**
```python
from fastapi import FastAPI, UploadFile, File
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.post("/api/decompose")
async def decompose(
image: UploadFile = File(...),
num_layers: int = 5
):
# 타입 힌팅, 자동 검증, OpenAPI 문서 생성
...
```
- 자동 API 문서 (Swagger UI)
- 비동기 처리 (async/await)
- WebSocket 지원
- Pydantic 데이터 검증
### 거부한 대안: Node.js + Express
**장점**:
- JavaScript 풀스택 가능
- npm 생태계 방대
- 비동기 I/O 우수
**단점**:
- Python AI 모델 통합 복잡
- 이미지 처리 라이브러리 빈약
- GPU 연산 지원 제한적
**결론**: AI 중심 프로젝트에서는 Python이 압도적으로 유리.
## 프론트엔드: Vanilla JS vs React
### 최종 선택: Vanilla HTML/CSS/JavaScript
**선택 이유**:
1. **단순성**
- 빌드 도구 불필요
- 의존성 최소화
- 정적 파일 서빙만으로 배포
2. **certificate-automation UI 재사용**
20251219-make-certificate-automation 프로젝트의 검증된 인터페이스:
```html
```
3. **빠른 프로토타이핑**
React 같은 프레임워크는 다음이 필요:
- npm install
- webpack/vite 설정
- JSX 컴파일
- 상태 관리 라이브러리
단순 파일 업로드 UI에는 과도한 복잡도.
### 거부한 대안: React
**장점**:
- 컴포넌트 재사용성
- 상태 관리 체계적
- 대규모 UI 확장 용이
**단점**:
- 초기 설정 시간
- 번들 크기 증가
- 서버 사이드 렌더링 고려 필요
**결론**: MVP에서는 Vanilla JS로 시작. 향후 복잡도 증가 시 React 마이그레이션 고려.
## AI 인프라: 로컬 GPU vs Vertex AI
### 하이브리드 전략 채택
**Phase 1 (개발): 로컬 GPU**
- RTX 3090 (24GB VRAM) 사용
- 빠른 실험 및 디버깅
- 비용 제로
**Phase 2 (프로덕션): Vertex AI Prediction**
- 사용자 요청 폭증 시 클라우드로 오프로드
- 자동 스케일링
- GPU 유휴 시간 제거
### 로컬 GPU 설정
```python
import torch
# GPU 사용 가능 여부 확인
if torch.cuda.is_available():
device = "cuda"
print(f"GPU: {torch.cuda.get_device_name(0)}")
else:
device = "cpu"
print("Warning: CPU 모드 (느림)")
# 모델 로딩
model = Qwen2VLForConditionalGeneration.from_pretrained(
"Qwen/Qwen-Image-Layered",
torch_dtype=torch.float16, # GPU 메모리 절약
device_map="auto"
)
```
### Vertex AI 백업 전략
```python
from google.cloud import aiplatform
class HybridDecomposer:
def __init__(self):
self.local_model = QwenLocalModel()
self.vertex_endpoint = aiplatform.Endpoint(
endpoint_name="qwen-layered-endpoint"
)
async def decompose(self, image):
if self.local_model.is_available():
# 로컬 GPU 우선 사용
return await self.local_model.process(image)
else:
# GPU 사용 중이면 Vertex AI로 폴백
return await self.vertex_endpoint.predict(image)
```
## 전체 시스템 아키텍처
```
┌─────────────────────────────────────────────────────┐
│ Web Browser │
│ ┌──────────────────────────────────────────────┐ │
│ │ Vanilla HTML/CSS/JavaScript │ │
│ │ - Drag & Drop Upload │ │
│ │ - Progress Bar (WebSocket) │ │
│ │ - Layer Preview │ │
│ └──────────────┬───────────────────────────────┘ │
└─────────────────┼───────────────────────────────────┘
│ HTTP/WebSocket
┌─────────────────▼───────────────────────────────────┐
│ FastAPI Backend (Python 3.11) │
│ ┌──────────────────────────────────────────────┐ │
│ │ API Routes │ │
│ │ - POST /api/upload │ │
│ │ - POST /api/decompose │ │
│ │ - GET /api/status/:job_id (WebSocket) │ │
│ │ - GET /api/download/:job_id │ │
│ └──────────────┬───────────────────────────────┘ │
│ ┌──────────────▼───────────────────────────────┐ │
│ │ Business Logic Layer │ │
│ │ - ImageProcessor │ │
│ │ - JobQueue (Redis) │ │
│ │ - StorageManager │ │
│ └──────────────┬───────────────────────────────┘ │
└─────────────────┼───────────────────────────────────┘
│
┌─────────────────▼───────────────────────────────────┐
│ AI Processing Layer │
│ ┌────────────────────┐ ┌──────────────────┐ │
│ │ QwenDecomposer │ │ GeminiAnalyzer │ │
│ │ (Local GPU) │ │ (Vertex AI) │ │
│ │ - Model Loading │ │ - Layer Desc │ │
│ │ - Inference │ │ - Recommend │ │
│ │ - RGBA Export │ │ - Quality Check │ │
│ └────────────────────┘ └──────────────────┘ │
└─────────────────┬───────────────────────────────────┘
│
┌─────────────────▼───────────────────────────────────┐
│ Storage Layer │
│ /storage/ │
│ /uploads/ - 원본 이미지 임시 저장 │
│ /results/ - 처리 완료 레이어 │
│ /{job_id}/ │
│ - layer_0.png │
│ - layer_1.png │
│ - metadata.json │
│ - preview.jpg (썸네일) │
└─────────────────────────────────────────────────────┘
```
## 데이터 흐름
### 사용자 → 레이어 다운로드 전체 흐름
```
1. 사용자: 이미지 업로드 (poster.jpg, 5 레이어)
↓
2. FastAPI: /api/decompose 엔드포인트
- 파일 검증 (크기, 형식)
- Job ID 생성 (UUID)
- Redis 큐에 추가
↓
3. Background Worker: Job 처리 시작
- 이미지 로드
- Qwen-Image-Layered 추론
- 진행 상황 → WebSocket 전송 (10%, 30%, 50%...)
↓
4. AI Processing:
- 레이어 생성 (RGBA PNG)
- 각 레이어 저장
↓
5. Gemini Vision API (선택):
- 각 레이어 분석
- 설명 생성 ("배경", "텍스트", "메인 이미지")
↓
6. 완료:
- metadata.json 생성
- WebSocket: 100% 완료 알림
- 프론트엔드: 다운로드 버튼 활성화
↓
7. 사용자: ZIP 다운로드 또는 개별 레이어 다운로드
```
## 비동기 작업 큐: Redis
왜 작업 큐가 필요한가?
이미지 분해는 30초 ~ 10분 소요. HTTP 요청은 기본 타임아웃 30초.
→ 백그라운드 작업 필수.
### Redis 기반 작업 큐
```python
# job_queue.py
import redis
import json
import uuid
class JobQueue:
def __init__(self):
self.redis = redis.Redis(
host='localhost',
port=6379,
db=0
)
def enqueue(self, image_path, num_layers):
"""작업 큐에 추가"""
job_id = str(uuid.uuid4())
job_data = {
"job_id": job_id,
"image_path": image_path,
"num_layers": num_layers,
"status": "queued",
"progress": 0,
"created_at": datetime.now().isoformat()
}
# Redis에 저장
self.redis.set(f"job:{job_id}", json.dumps(job_data))
self.redis.lpush("job_queue", job_id)
return job_id
def update_progress(self, job_id, progress, message):
"""진행 상황 업데이트"""
job_data = json.loads(self.redis.get(f"job:{job_id}"))
job_data["progress"] = progress
job_data["message"] = message
self.redis.set(f"job:{job_id}", json.dumps(job_data))
# WebSocket으로 브로드캐스트
self.broadcast_update(job_id, job_data)
```
### Worker 프로세스
```python
# worker.py
async def process_jobs():
"""백그라운드 워커"""
queue = JobQueue()
decomposer = QwenDecomposer()
while True:
# 큐에서 작업 가져오기
job_id = queue.dequeue()
if job_id:
job_data = queue.get_job(job_id)
try:
# 상태 업데이트
queue.update_progress(job_id, 10, "모델 로딩 중...")
# AI 처리
layers = await decomposer.decompose(
image_path=job_data["image_path"],
num_layers=job_data["num_layers"],
progress_callback=lambda p: queue.update_progress(
job_id, p, f"레이어 {p//20} 생성 중..."
)
)
# 완료
queue.update_progress(job_id, 100, "완료!")
queue.mark_completed(job_id, layers)
except Exception as e:
queue.mark_failed(job_id, str(e))
await asyncio.sleep(0.1)
```
## 환경 변수 관리
`.env` 파일:
```bash
# App 설정
APP_ENV=development
HOST=0.0.0.0
PORT=8000
DEBUG=true
# Qwen-Image-Layered
MODEL_NAME=Qwen/Qwen-Image-Layered
MODEL_CACHE_DIR=./models
DEFAULT_RESOLUTION=1024
DEFAULT_NUM_LAYERS=5
INFERENCE_STEPS=50
# GPU 설정
CUDA_VISIBLE_DEVICES=0
PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512
# Google Cloud Vertex AI
GOOGLE_CLOUD_PROJECT=planitai-project
GOOGLE_APPLICATION_CREDENTIALS=./service-account.json
VERTEX_AI_LOCATION=us-central1
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
# Storage
UPLOAD_DIR=./storage/uploads
RESULTS_DIR=./storage/results
MAX_FILE_SIZE=10485760 # 10MB
CLEANUP_AFTER_HOURS=24
```
## 프로젝트 파일 구조
```
20251221-qwen-image-layered/
├── backend/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── main.py # FastAPI 앱 진입점
│ │ ├── config.py # 환경 변수 로딩
│ │ │
│ │ ├── api/ # API 라우트
│ │ │ ├── __init__.py
│ │ │ ├── upload.py
│ │ │ ├── decompose.py
│ │ │ └── status.py
│ │ │
│ │ ├── models/ # AI 모델
│ │ │ ├── __init__.py
│ │ │ ├── qwen_decomposer.py
│ │ │ └── gemini_analyzer.py
│ │ │
│ │ ├── services/ # 비즈니스 로직
│ │ │ ├── __init__.py
│ │ │ ├── processor.py
│ │ │ ├── storage.py
│ │ │ └── job_queue.py
│ │ │
│ │ └── utils/
│ │ ├── logger.py
│ │ └── validators.py
│ │
│ ├── worker.py # 백그라운드 워커
│ ├── requirements.txt
│ ├── Dockerfile
│ └── .env
│
├── frontend/
│ ├── index.html
│ ├── style.css
│ └── app.js
│
├── storage/
│ ├── uploads/
│ └── results/
│
└── docs/
└── architecture.md
```
## 다음 단계
v3에서는 **Qwen-Image-Layered 모델**을 깊이 분석한다:
- 논문 리뷰: 어떻게 레이어 분해를 하는가?
- Qwen2.5-VL 백본 이해
- Diffusion 기반 레이어 생성 원리
- 입력/출력 형식 상세
기술 스택이 결정되었으니, 이제 핵심 AI 모델이 실제로 어떻게 작동하는지 이해할 차례다.
---
**이전 글**: [프로젝트 시작과 목표 (1/10)](./qwen-image-layered-v1.md)
**다음 글**: [Qwen-Image-Layered 모델 깊이 이해 (3/10)](./qwen-image-layered-v3.md)