[AI] 01. ADR 006‐010 ‐ 인프라 및 통합 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki
기술 의사결정 기록 (ADR) - 006~010
Architecture Decision Records - 프로젝트에서 기술 선택의 근거를 기록하는 문서
📚 목차
- ADR-006: 인프라 진화 전략 (EC2 → Docker → Kubernetes)
- ADR-007: Kubernetes vs Kubeflow 도입 시기
- ADR-008: OCR + 임베딩 내부 통합 처리
- ADR-009: AI 채팅 컨텍스트 관리 (Redis 사용)
- ADR-010: OCR 서비스 선택 (Upstage vs 클로바 vs Gemini Vision)
ADR-006: 인프라 진화 전략 (EC2 → Docker → Kubernetes)
📋 메타데이터
| 항목 | 내용 |
|---|---|
| 상태 | ✅ 채택됨 (Accepted) |
| 작성일 | 2026-01-08 |
| 결정자 | AI/클라우드팀 |
| 관련 기능 | AI 서버 배포, 인프라 |
🎯 컨텍스트 (Context)
AI 서버를 운영 환경에 배포해야 합니다. 프로젝트 일정과 팀 역량을 고려하여 점진적으로 인프라를 고도화하는 전략이 필요합니다.
고려 사항:
- MVP 출시까지 시간이 제한적 (7주차 1차 배포)
- 팀의 Kubernetes 경험이 제한적
- 트래픽 증가에 대응할 수 있어야 함
- 비용 효율적이어야 함
🔍 선택지 분석 (Options)
Option 1: 처음부터 Kubernetes (Big Bang)
┌─────────────────────────────────────────┐
│ 처음부터 K8s 클러스터 구축 │
│ - EKS/GKE 설정 │
│ - Helm 차트 작성 │
│ - 모니터링/로깅 구성 │
└─────────────────────────────────────────┘
| 장점 | 단점 |
|---|---|
| 나중에 마이그레이션 불필요 | 초기 학습 곡선 높음 |
| 처음부터 스케일 가능 | MVP 출시 지연 위험 |
| 초기 비용 높음 |
Option 2: 점진적 진화 (EC2 → Docker → K8s) ⭐ 선택
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ V1 (MVP) V1.5 V2 V3 │
│ ┌───────┐ ┌───────────┐ ┌─────────┐ ┌──────────────┐ │
│ │ EC2 │ → │ EC2 + ALB │ → │ Docker │ → │ Kubernetes │ │
│ │ 빅뱅 │ │ 모듈화 │ │ + ECS │ │ + Serverless │ │
│ └───────┘ └───────────┘ └─────────┘ └──────────────┘ │
│ (7주차) (8주차) (10주차) (12주차+) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 장점 | 단점 |
|---|---|
| MVP 빠르게 출시 가능 | 마이그레이션 비용 발생 |
| 단계별 학습 가능 | 일부 재작업 필요 |
| 리스크 분산 | |
| 실제 트래픽 보고 결정 |
Option 3: 서버리스 Only (Lambda/Cloud Functions)
| 장점 | 단점 |
|---|---|
| 관리 부담 없음 | Cold Start 지연 |
| 자동 스케일링 | 실행 시간 제한 (15분) |
| 사용량 기반 과금 | LLM 긴 응답에 부적합 |
✅ 결정 (Decision)
Option 2: 점진적 진화 전략 채택
┌─────────────────────────────────────────────────────────────────────────────┐
│ 채택된 인프라 로드맵 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [V1: EC2 빅뱅] - 7주차 MVP │
│ ├── 단일 EC2 인스턴스 (t3.medium 또는 g4dn.xlarge) │
│ ├── 모든 서비스 단일 서버에 배포 │
│ ├── PM2 또는 systemd로 프로세스 관리 │
│ └── 목표: 빠른 출시, 기능 검증 │
│ │
│ [V1.5: EC2 모듈화] - 8~9주차 │
│ ├── Frontend / Backend / AI 서버 분리 │
│ ├── ALB (Application Load Balancer) 도입 │
│ ├── Auto Scaling Group 설정 │
│ └── 목표: 장애 격리, 수평 확장 기반 마련 │
│ │
│ [V2: Docker 컨테이너화] - 10~11주차 │
│ ├── Dockerfile 작성, 이미지 빌드 │
│ ├── ECR (Elastic Container Registry) 사용 │
│ ├── ECS Fargate 또는 Docker Compose │
│ └── 목표: 환경 일관성, 배포 자동화 │
│ │
│ [V3: Kubernetes + 서버리스] - 12주차 이후 (선택) │
│ ├── EKS 클러스터 구축 │
│ ├── Helm 차트로 배포 관리 │
│ ├── 일부 기능 Lambda로 오프로드 (예: 이미지 처리) │
│ └── 목표: 자동 스케일링, 고가용성 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
📝 근거 (Rationale)
- MVP 우선: 7주차 출시 목표를 맞추려면 단순한 구조로 시작해야 함
- 실제 데이터 기반 결정: 트래픽 패턴을 보고 K8s 필요성 판단
- 팀 역량 성장: 단계별로 학습하며 진행
- 비용 효율: 초기에는 단일 EC2로 비용 절감
- 롤백 가능: 문제 발생 시 이전 단계로 복귀 용이
ADR-007: Kubernetes vs Kubeflow 도입 시기
📋 메타데이터
| 항목 | 내용 |
|---|---|
| 상태 | ✅ 채택됨 (Accepted) |
| 작성일 | 2026-01-08 |
| 결정자 | AI팀 |
| 관련 기능 | MLOps, 모델 학습/배포 |
🎯 컨텍스트 (Context)
MLOps 도구 도입을 검토하고 있습니다. Kubernetes와 Kubeflow의 차이점을 이해하고, 언제 각각을 도입할지 결정해야 합니다.
현재 상황:
- 자체 모델 학습 없음 (Gemini API 사용)
- 향후 오픈소스 LLM 서빙 예정 (Phase 2)
- 향후 모델 파인튜닝 가능성 있음 (Phase 3)
🔍 선택지 분석 (Options)
Kubernetes vs Kubeflow 비교
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Kubeflow │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Pipelines │ │ Katib │ │ KServe │ │ │
│ │ │ (워크플로우) │ │(하이퍼튜닝) │ │(모델서빙) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ 위에서 실행 │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Kubernetes │ │
│ │ Pod, Service, Deployment, Ingress, ConfigMap, Secret ... │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 항목 | Kubernetes | Kubeflow |
|---|---|---|
| 정체 | 컨테이너 오케스트레이션 | ML 워크플로우 플랫폼 |
| 역할 | 컨테이너 배포/관리 | ML 파이프라인 자동화 |
| 필수 조건 | 단독 실행 가능 | Kubernetes 필수 |
| 학습 곡선 | 🔴 높음 | 🔴🔴 매우 높음 |
| 우리 서비스 | V3에서 도입 | 자체 학습 시 도입 |
도입 시기 분석
| 시나리오 | Kubernetes | Kubeflow |
|---|---|---|
| API 사용 (Gemini) | ❌ 불필요 | ❌ 불필요 |
| 오픈소스 LLM 서빙 | ✅ 도입 | ❌ 불필요 |
| 1회성 파인튜닝 | ✅ 필요 | ❌ 불필요 (스크립트로 충분) |
| 정기 재학습 | ✅ 필요 | 🟡 선택적 (Airflow 대안) |
| 하이퍼파라미터 튜닝 | ✅ 필요 | ✅ 도입 (Katib 활용) |
| 여러 모델 동시 학습 | ✅ 필요 | ✅ 도입 |
✅ 결정 (Decision)
┌─────────────────────────────────────────────────────────────────────────────┐
│ Kubernetes / Kubeflow 도입 로드맵 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [현재 ~ V2: Kubernetes 미도입] │
│ ├── EC2 + Docker로 충분 │
│ └── Gemini API 사용, 복잡한 오케스트레이션 불필요 │
│ │
│ [V3: Kubernetes 도입] ⭐ 트리거 조건 │
│ ├── 트래픽 증가로 수동 스케일링 한계 │
│ ├── 오픈소스 LLM 서빙 시작 │
│ ├── 여러 AI 모듈 동시 운영 │
│ └── 예상 시기: 12주차 이후 │
│ │
│ [V4 이후: Kubeflow 도입 검토] ⭐ 트리거 조건 │
│ ├── 자체 데이터로 모델 학습 시작 │
│ ├── 하이퍼파라미터 튜닝 자동화 필요 │
│ ├── 여러 모델 버전 동시 학습/비교 │
│ └── 대안: SageMaker, Vertex AI (관리형 서비스) │
│ │
│ [Kubeflow 대신 가능한 대안] │
│ ├── 정기 배치 학습 → Airflow + 학습 스크립트 │
│ ├── 실험 추적 → MLflow 또는 Weights & Biases │
│ ├── 모델 서빙 → vLLM + Kubernetes (KServe 없이) │
│ └── 간단한 파인튜닝 → RunPod + 수동 실행 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
📝 근거 (Rationale)
- 현재 불필요: 외부 API 사용 중이라 복잡한 ML 파이프라인 불필요
- 오버엔지니어링 방지: Kubeflow는 학습 곡선이 매우 높음
- 단계적 도입: 필요해지는 시점에 도입하여 리소스 낭비 방지
- 대안 존재: Airflow, MLflow로 대부분의 요구사항 충족 가능
- 팀 역량 고려: K8s 먼저 익숙해진 후 Kubeflow 검토
📌 실무 가이드
| 질문 | 답변 |
|---|---|
| 모델 서빙만 하면? | Kubernetes + vLLM |
| 가끔 학습하면? | RunPod + 학습 스크립트 |
| 매일/매주 학습하면? | Airflow + 학습 스크립트 |
| 하이퍼파라미터 튜닝? | Kubeflow Katib 또는 Optuna |
| 복잡한 ML 파이프라인? | Kubeflow Pipelines |
ADR-008: OCR + 임베딩 내부 통합 처리
📋 메타데이터
| 항목 | 내용 |
|---|---|
| 상태 | ✅ 채택됨 (Accepted) |
| 작성일 | 2026-01-08 |
| 결정자 | AI/백엔드팀 |
| 관련 기능 | 이력서/채용공고 업로드, OCR, VectorDB 임베딩 |
🎯 컨텍스트 (Context)
이력서/채용공고 파일 업로드 시 OCR 텍스트 추출 → 임베딩 저장 과정이 필요합니다.
기존 설계:
Backend → AI Server(OCR) → Backend → AI Server(Embed) → Backend
문제점:
- Backend가 OCR 완료를 기다리기 위해 폴링(Polling) 필요
- 폴링은 주기적으로 상태를 확인 → 실질적으로 동기 처리와 다름없음
- 네트워크 왕복 2회 발생 → 레이턴시 증가
- Backend에 OCR 상태 관리 로직 필요 → 복잡도 증가
🔍 선택지 분석 (Options)
Option 1: Backend에서 폴링 후 임베딩 호출 (기존 설계)
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Backend AI Server │
│ │ │ │
│ │── POST /ai/ocr/extract ──────▶│ │
│ │◀── task_id ───────────────────│ │
│ │ │ │
│ │── GET /ai/task/{id} ─────────▶│ ┐ │
│ │◀── processing ────────────────│ │ 폴링 반복 │
│ │── GET /ai/task/{id} ─────────▶│ │ (실질적으로 동기 대기) │
│ │◀── completed ─────────────────│ ┘ │
│ │ │ │
│ │── POST /ai/file/embed ───────▶│ │
│ │◀── 완료 ─────────────────────│ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 장점 | 단점 |
|---|---|
| API 역할이 분리됨 | 폴링 = 실질적 동기 처리 |
| 단계별 디버깅 가능 | 네트워크 왕복 증가 |
| Backend 복잡도 증가 | |
| 레이턴시 증가 |
Option 2: AI Server 내부에서 OCR + 임베딩 통합 처리 ⭐ 선택
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Backend AI Server │
│ │ │ │
│ │── POST /ai/ocr/extract ──────▶│ │
│ │ (type, user_id, doc_id) │ │
│ │ │ │
│ │◀── task_id ───────────────────│ │
│ │ │ │
│ │ ┌────┴────┐ │
│ │ │ 내부 처리 │ │
│ │ │ 1. OCR │ │
│ │ │ 2. 청킹 │ │
│ │ │ 3. 임베딩 │ │
│ │ │ 4. 저장 │ │
│ │ └────┬────┘ │
│ │ │ │
│ │── GET /ai/task/{id} ─────────▶│ │
│ │◀── completed ─────────────────│ │
│ │ (text + vector_id) │ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 장점 | 단점 |
|---|---|
| 네트워크 왕복 감소 | API 역할이 확장됨 |
| 레이턴시 감소 | 내부 로직 복잡도 증가 |
| Backend 단순화 | |
| 진정한 비동기 처리 가능 |
Option 3: 콜백(Callback) 방식
Backend → AI Server(OCR + Embed) → Callback to Backend
| 장점 | 단점 |
|---|---|
| 진정한 비동기 | Backend에 콜백 엔드포인트 필요 |
| 폴링 불필요 | 네트워크 신뢰성 이슈 |
| 재시도 로직 복잡 |
✅ 결정 (Decision)
Option 2: AI Server 내부에서 OCR + 임베딩 통합 처리
┌─────────────────────────────────────────────────────────────────────────────┐
│ 채택된 흐름 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ [Backend] │
│ │ │
│ │── POST /ai/ocr/extract ──────────────────────────────────────▶ │
│ │ { │
│ │ file_url: "s3://...", │
│ │ file_type: "pdf", │
│ │ type: "resume", // 어떤 컬렉션에 저장할지 │
│ │ user_id: "user_456", │
│ │ document_id: "resume_123" │
│ │ } │
│ │ │
│ │◀── task_id 반환 ───────────────────────────────────────────── │
│ │ │
│ │ [AI Server 내부] │
│ │ ┌─────────────────────────────────────┐ │
│ │ │ 1. OCR/VLM으로 텍스트 추출 │ │
│ │ │ 2. 텍스트 청킹 (500 tokens) │ │
│ │ │ 3. Gemini Embedding 생성 │ │
│ │ │ 4. VectorDB에 저장 │ │
│ │ │ 5. 결과 저장 (Redis/Memory) │ │
│ │ └─────────────────────────────────────┘ │
│ │ │
│ │── GET /ai/task/{id} (폴링) ─────────────────────────────────▶ │
│ │ │
│ │◀── completed ────────────────────────────────────────────── │
│ │ { │
│ │ extracted_text: "...", │
│ │ vector_id: "vec_abc123", │
│ │ collection: "resumes" │
│ │ } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
API 역할 변경:
| API | 변경 전 | 변경 후 |
|---|---|---|
/ai/ocr/extract |
OCR만 | OCR + 임베딩 (내부 처리) |
/ai/file/embed |
항상 호출 | 텍스트 직접 입력 시에만 호출 |
📝 근거 (Rationale)
-
폴링의 본질적 한계
- 폴링은 주기적으로 완료 여부를 확인 → 그 동안 Backend는 대기
- 결국 동기 처리와 다를 바 없음 (단지 연결을 끊었다 다시 할 뿐)
- 네트워크 왕복만 늘어나고 실질적 이득 없음
-
성능 최적화
- 네트워크 왕복 2회 → 1회로 감소
- Backend-AI 간 통신 오버헤드 감소
- 전체 레이턴시 감소
-
Backend 단순화
- OCR 상태 관리 로직 불필요
- 임베딩 호출 로직 불필요
- 에러 핸들링 포인트 감소
-
응집도 향상
- "파일 업로드 → VectorDB 저장"이라는 하나의 기능이 하나의 API로 완결
- AI Server가 자신의 도메인(OCR, 임베딩, VectorDB)을 책임짐
-
유연성 유지
- 텍스트 직접 입력 시에는 여전히
/ai/file/embed사용 가능 - 필요시 OCR 결과만 필요한 경우 별도 옵션 추가 가능
- 텍스트 직접 입력 시에는 여전히
ADR-009: AI 채팅 컨텍스트 관리 (Redis 사용)
📋 메타데이터
| 항목 | 내용 |
|---|---|
| 상태 | ✅ 채택됨 (Accepted) |
| 작성일 | 2026-01-08 |
| 결정자 | AI/백엔드팀 |
| 관련 기능 | AI 채팅, 대화 히스토리, 컨텍스트 유지 |
🎯 컨텍스트 (Context)
AI 채팅 서비스에서 대화 히스토리와 컨텍스트를 유지해야 합니다.
요구사항:
- 사용자와의 대화 맥락 유지
- LLM에 전달할 최근 대화 히스토리 관리
- 빠른 읽기/쓰기 성능
- 다중 사용자/채팅방 지원
- 세션 만료 및 자동 정리
현재 데이터 저장소:
| 저장소 | 용도 |
|---|---|
| PostgreSQL | 사용자 정보, 채팅방 메타데이터, 면접 세션 |
| MongoDB | 분석 결과, 채팅 메시지 영구 저장 |
| ChromaDB | 임베딩 벡터 저장 (VectorDB) |
| Redis | 실시간 대화 컨텍스트 관리 |
🔍 선택지 분석 (Options)
Option 1: 요청마다 DB에서 히스토리 조회
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ 매 요청마다 DB 조회 │
│ │
│ User → Backend → MongoDB (최근 N개 조회) → AI Server → LLM │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 장점 | 단점 |
|---|---|
| 추가 인프라 불필요 | 매 요청마다 DB I/O 발생 |
| 데이터 영속성 보장 | 레이턴시 증가 |
| 동시 요청 시 DB 부하 |
Option 2: 인메모리 캐시 (Python 딕셔너리)
# 단일 서버 인메모리
chat_sessions = {
"room_001": ["message1", "message2", ...]
}
| 장점 | 단점 |
|---|---|
| 매우 빠름 | 서버 재시작 시 데이터 손실 |
| 구현 간단 | 다중 서버 환경에서 공유 불가 |
| 메모리 관리 어려움 |
Option 3: Redis 캐시 레이어 ⭐ 선택
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ Redis 캐시 레이어 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Redis │ │
│ │ │ │
│ │ chat:room_001 → ["msg1", "msg2", ...] (TTL: 30분) │ │
│ │ chat:room_002 → ["msg1", "msg2", ...] (TTL: 30분) │ │
│ │ context:user_456 → {preferences, summary, ...} │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ User → Backend → Redis (캐시) → AI Server → LLM │
│ ↓ │
│ MongoDB (영구 저장, 비동기) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
| 장점 | 단점 |
|---|---|
| 빠른 읽기/쓰기 (1ms 이하) | 추가 인프라 필요 |
| TTL 자동 만료 지원 | 관리 포인트 증가 |
| 다중 서버 공유 가능 | |
| Pub/Sub 지원 | |
| LangChain 공식 지원 |
Option 4: LangChain Memory (ChatMessageHistory)
from langchain_community.chat_message_histories import RedisChatMessageHistory
history = RedisChatMessageHistory(
session_id="room_001",
url="redis://localhost:6379"
)
| 장점 | 단점 |
|---|---|
| LangChain 네이티브 통합 | Redis 의존성 |
| 코드 간결 | 커스터마이징 제한적 |
| 검증된 패턴 |
✅ 결정 (Decision)
Option 3 + Option 4: Redis + LangChain Memory 조합
┌─────────────────────────────────────────────────────────────────────────────┐
│ 채택된 아키텍처 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ Redis │ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ 채팅 히스토리│ │ ← LangChain Memory │
│ │ │ (최근 N개) │ │ │
│ │ └───────────┘ │ │
│ │ │ │
│ │ ┌───────────┐ │ │
│ │ │ 사용자 │ │ ← 세션 컨텍스트 │
│ │ │ 컨텍스트 │ │ │
│ │ └───────────┘ │ │
│ │ │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ [AI Server - FastAPI] │ │
│ │ │ │
│ │ from langchain_community.chat_message_histories import │ │
│ │ RedisChatMessageHistory │ │
│ │ │ │
│ │ # 채팅 히스토리 │ │
│ │ history = RedisChatMessageHistory( │ │
│ │ session_id=f"chat:{room_id}", │ │
│ │ url="redis://redis:6379", │ │
│ │ ttl=1800 # 30분 │ │
│ │ ) │ │
│ │ │ │
│ │ # ConversationBufferMemory와 연동 │ │
│ │ memory = ConversationBufferWindowMemory( │ │
│ │ chat_memory=history, │ │
│ │ k=10 # 최근 10개 메시지 │ │
│ │ ) │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 비동기로 MongoDB에도 영구 저장 (나중에 조회 가능) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Redis 키 구조:
| 키 패턴 | 용도 | TTL |
|---|---|---|
chat:{room_id} |
채팅 히스토리 (메시지 리스트) | 30분 |
context:{user_id} |
사용자 선호도, 요약 정보 | 1시간 |
session:{session_id} |
면접 세션 임시 데이터 | 1시간 |
데이터 흐름:
┌─────────────────────────────────────────────────────────────────────────────┐
│ │
│ [사용자 메시지 도착] │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Redis에서 │ │
│ │ 최근 히스토리 │ ← 1ms 이하 │
│ │ 조회 │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ LLM 호출 │ │
│ │ (히스토리 포함) │ │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Redis에 │ │ MongoDB에 │ │
│ │ 응답 추가 │ │ 비동기 저장 │ ← 영구 보관 │
│ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
📝 근거 (Rationale)
-
성능 (레이턴시)
- Redis: < 1ms
- MongoDB: 5-20ms
- 매 채팅마다 DB 조회 시 사용자 체감 지연 발생
-
다중 사용자 지원
- 여러 사용자가 동시에 채팅해도 세션 분리
- 서버 스케일 아웃 시에도 Redis 공유로 일관성 유지
-
자동 메모리 관리
- TTL 설정으로 자동 만료
- 오래된 세션 자동 정리
- 메모리 누수 방지
-
LangChain 네이티브 지원
RedisChatMessageHistory공식 지원ConversationBufferWindowMemory와 쉬운 연동- 검증된 패턴, 안정적
-
PL 피드백 반영
- "Redis 홈페이지 참고해서 보는 게 좋음"
- "Memory 아키텍처, AI Agent 문서 보기"
- "AI랑 백이랑 같이 하는 것도 좋음"
-
비용 효율
- Redis는 경량 → 작은 인스턴스로 운영 가능
- AWS ElastiCache, Redis Cloud 등 관리형 서비스 활용 가능
-
Stateless 아키텍처 구현 ⭐
Redis를 사용하면 서버가 Stateless가 됩니다:
┌─────────────────────────────────────────────────────────────────┐ │ Stateless vs Stateful 비교 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ [Stateful (Redis 없이)] │ │ ┌────────────┐ │ │ │ 서버 A │ ← 세션 메모리에 저장 │ │ │ sessions{} │ (서버 재시작 시 손실) │ │ └────────────┘ │ │ │ │ [Stateless (Redis 사용)] │ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ 서버 A │ │ 서버 B │ │ 서버 C │ │ │ │ (메모리 X) │ │ (메모리 X) │ │ (메모리 X) │ │ │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ │ │ └────────────────┼────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────┐ │ │ │ Redis │ ← 세션 저장소 │ │ │ (공유 저장소)│ (모든 서버가 공유) │ │ └──────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘Stateless의 장점:
항목 Stateful (메모리) Stateless (Redis) 서버 재시작 ❌ 세션 손실 ✅ 세션 유지 로드 밸런싱 ❌ Sticky Session 필요 ✅ 자유롭게 가능 수평 확장 ❌ 어려움 ✅ 쉬움 장애 복구 ❌ 세션 손실 ✅ 세션 유지 메모리 사용 ⚠️ 서버마다 세션 저장 ✅ Redis에만 저장 구현 예시:
# Stateful (메모리에 세션 저장) - 사용 안 함 sessions = {} # 서버 메모리 @app.post("/login") async def login(user: User): session_id = generate_session_id() sessions[session_id] = { # ← 메모리에 저장 (Stateful) "user_id": user.id, "login_time": datetime.now() } return {"session_id": session_id} # Stateless (Redis에 세션 저장) - 채택 ✅ import redis redis_client = redis.Redis(host='localhost', port=6379) @app.post("/login") async def login(user: User): session_id = generate_session_id() redis_client.setex( # ← Redis에 저장 (Stateless) f"session:{session_id}", 3600, # 1시간 TTL json.dumps({ "user_id": user.id, "login_time": datetime.now().isoformat() }) ) return {"session_id": session_id}Stateless 흐름:
1. 사용자 로그인 → 서버 A → Redis에 세션 저장 2. 다음 요청 → 로드 밸런서 → 서버 B로 라우팅 3. 서버 B → Redis에서 세션 조회 → 정상 처리 ✅프로젝트 적용:
용도 Redis 키 TTL Stateless 효과 세션 관리 session:{session_id}1시간 서버 재시작해도 유지 채팅 히스토리 chat:{room_id}30분 모든 서버가 공유 사용자 컨텍스트 context:{user_id}1시간 로드 밸런싱 자유 Rate Limiting rate_limit:{user_id}1분 서버 간 일관성 결론:
- ✅ Redis를 사용하면 서버는 완전히 Stateless
- ✅ 모든 상태는 Redis에 저장
- ✅ 서버는 언제든 교체 가능
- ✅ 수평 확장 용이
📚 참고 자료
ADR-010: OCR 서비스 선택 (Upstage vs 클로바 vs Gemini Vision)
📋 메타데이터
| 항목 | 내용 |
|---|---|
| 상태 | 🔄 검토중 (Under Review) |
| 작성일 | 2026-01-09 |
| 결정자 | AI팀 |
| 관련 기능 | 이력서/채용공고 텍스트 추출, 마스킹 |
📌 PL 피드백 (3주차 2회차): "Upstage OCR, 클로바 등 검토"
🎯 컨텍스트 (Context)
이력서, 채용공고, 포트폴리오 파일(PDF, 이미지)에서 텍스트를 추출해야 합니다. 정확도와 비용 효율 사이의 균형이 필요합니다.
요구사항:
- 한국어 OCR 정확도 높아야 함 (이력서, 채용공고)
- PDF 이미지 형태 문서 처리 가능
- 비용 효율적이어야 함 (API 과금)
- 표, 레이아웃 인식 지원
🔍 선택지 분석 (Options)
| 항목 | Gemini Vision | Upstage Document AI | Clova OCR |
|---|---|---|---|
| 한국어 정확도 | ⭐⭐⭐⭐ (양호) | ⭐⭐⭐⭐⭐ (최상) | ⭐⭐⭐⭐⭐ (최상) |
| 표/레이아웃 | ⭐⭐⭐ (기본) | ⭐⭐⭐⭐⭐ (우수) | ⭐⭐⭐⭐ (양호) |
| 비용 | $0.00025/이미지 | 월 구독형 | 종량제 |
| API 제한 | 15 RPM (Free) | 무제한 (유료) | 무제한 (유료) |
| 장점 | Gemini LLM과 통합 | 문서 특화, 한국어 강점 | 네이버 생태계 |
| 단점 | 문서 특화 아님 | 비용 높음 | 단순 OCR만 |
Option 1: Gemini Vision (V1 MVP)
┌─────────────────────────────────────────────────────────────────────────────┐
│ V1 선택: Gemini Vision │
│ - 이미 Gemini API 사용 중 → 추가 연동 비용 없음 │
│ - 한국어 이력서/채용공고에는 충분한 정확도 │
│ - 정확도 이슈 발생 시 V2에서 전문 OCR로 전환 │
└─────────────────────────────────────────────────────────────────────────────┘
Option 2: Upstage Document AI (V2 검토)
┌─────────────────────────────────────────────────────────────────────────────┐
│ V2 검토: Upstage Document AI │
│ - 한국어 문서 OCR 최상급 정확도 │
│ - 표, 레이아웃, 구조 인식 우수 │
│ - 비용이 높아 트래픽 증가 시 검토 │
└─────────────────────────────────────────────────────────────────────────────┘
Option 3: Clova OCR (대안)
┌─────────────────────────────────────────────────────────────────────────────┐
│ 대안: Clova OCR │
│ - 네이버 한국어 OCR │
│ - 단순 텍스트 추출에 적합 │
│ - 복잡한 레이아웃은 Upstage가 더 우수 │
└─────────────────────────────────────────────────────────────────────────────┘
✅ 결정 (Decision)
단계적 도입 전략:
| 버전 | OCR 선택 | 이유 |
|---|---|---|
| V1 (MVP) | Gemini Vision | 빠른 출시, 추가 연동 비용 없음 |
| V2 (정확도 이슈 시) | Upstage Document AI 검토 | 한국어 문서 특화 |
| 마스킹 (V2) | PaddleOCR + YOLO (RunPod) | 자체 호스팅으로 비용 절감 |
결정 기준:
- V1 배포 후 OCR 정확도 측정 (CER < 5% 기준)
- 정확도 미달 시 Upstage Document AI 비용 분석
- 트래픽 증가 시 자체 모델 (PaddleOCR) 검토
📝 근거 (Rationale)
- MVP 우선: 당장 전문 OCR 불필요, Gemini Vision으로 시작
- 비용 효율: 추가 API 연동 없이 기존 Gemini 활용
- 점진적 개선: 정확도 이슈 발생 시에만 전문 OCR 도입
- 데이터 기반 결정: 실제 사용자 데이터로 정확도 측정 후 판단
- PL 피드백 반영: "Upstage OCR, 클로바 등 검토" → 5주차 비용 대비 성능 평가 예정
📚 참고 자료
📅 이력
| 날짜 | 변경 내용 |
|---|---|
| 2026-01-08 | 파일 생성 (템플릿) |
| 2026-01-09 | PL 피드백 반영하여 OCR 선택 ADR 작성 |