[AI] 00. ADR 001‐005 ‐ 기본 아키텍처 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

기술 의사결정 기록 (ADR)

Architecture Decision Records - 프로젝트에서 기술 선택의 근거를 기록하는 문서


📚 목차


ADR-001: LLM 스트리밍 통신 방식 (SSE vs WebSocket)

📋 메타데이터

항목 내용
상태 ✅ 채택됨 (Accepted)
작성일 2026-01-07
결정자 개발팀
관련 기능 AI 채팅, 분석 결과 스트리밍, 면접 리포트

🎯 컨텍스트 (Context)

LLM(Large Language Model) 기반 채팅 기능을 구현할 때, 사용자에게 실시간으로 응답을 스트리밍해야 합니다. 이를 위해 **SSE(Server-Sent Events)**와 WebSocket 두 가지 방식 중 하나를 선택해야 합니다.

스트리밍이 필요한 API:

  • /ai/chat - 대화 처리 (RAG + 에이전트)
  • /ai/analyze - 분석 + 매칭도
  • /ai/interview/report - 면접 평가 및 피드백

🔍 선택지 분석 (Options)

Option 1: SSE (Server-Sent Events)

[클라이언트] ──HTTP 요청──> [서버]
[클라이언트] <──SSE 스트림── [서버]
            (text/event-stream)

장점:

  • ✅ HTTP/1.1 기반으로 기존 인프라 완벽 호환
  • ✅ 구현이 간단 (FastAPI StreamingResponse)
  • ✅ 로드밸런서, 프록시, CDN 설정 변경 불필요
  • ✅ 브라우저 EventSource API로 자동 재연결 지원
  • ✅ HTTP/2에서 멀티플렉싱 지원
  • ✅ OpenAI, Anthropic, Google 등 LLM 업계 표준

단점:

  • ⚠️ 단방향 통신만 가능 (서버 → 클라이언트)
  • ⚠️ 바이너리 데이터 전송에 비효율적
  • ⚠️ IE에서 지원 안 됨 (현재는 관계없음)

Option 2: WebSocket

[클라이언트] ──WebSocket 핸드셰이크──> [서버]
[클라이언트] <────양방향 통신────> [서버]
               (ws:// 또는 wss://)

장점:

  • ✅ 양방향 실시간 통신 가능
  • ✅ 바이너리 데이터 효율적 전송
  • ✅ 하나의 연결로 여러 기능 처리 가능
  • ✅ 낮은 레이턴시

단점:

  • ⚠️ 별도 프로토콜 (ws://)로 인프라 설정 필요
  • ⚠️ 로드밸런서/프록시 설정 변경 필요할 수 있음
  • ⚠️ 연결 끊김 시 재연결 로직 직접 구현 필요
  • ⚠️ HTTP/2 미지원 (HTTP/1.1만)
  • ⚠️ 상태 관리 복잡도 증가

📊 상세 비교표

항목 SSE WebSocket
통신 방향 단방향 (서버 → 클라이언트) 양방향
프로토콜 HTTP/1.1, HTTP/2 독자 프로토콜 (ws://)
연결 방식 표준 HTTP 요청 업그레이드 핸드셰이크
Content-Type text/event-stream N/A (바이너리 프레임)
구현 복잡도 ⭐ 낮음 ⭐⭐⭐ 높음
인프라 호환성 ⭐⭐⭐ 높음 ⭐⭐ 설정 필요
자동 재연결 브라우저 기본 지원 직접 구현
HTTP/2 지원 ✅ 멀티플렉싱
모바일 친화성 ⭐⭐⭐ 높음 ⭐⭐ 배터리 이슈 가능

✅ 결정 (Decision)

SSE(Server-Sent Events)를 선택합니다.


📝 근거 (Rationale)

1. LLM 응답 특성과 일치

사용자 요청 → [서버에서 LLM 호출] → 토큰 단위 스트리밍 응답
  • LLM 응답은 서버 → 클라이언트 단방향
  • 사용자 메시지는 일반 HTTP POST로 전송 가능
  • 양방향 통신의 필요성 없음

2. 업계 표준 준수

LLM 서비스 스트리밍 방식
OpenAI SSE
Anthropic Claude SSE
Google Gemini SSE
Cohere SSE
Mistral SSE

대부분의 LLM API가 SSE를 사용 → 참고 자료 풍부, 검증된 방식

3. 구현 및 운영 단순화

FastAPI 서버 코드:

from fastapi.responses import StreamingResponse

@app.post("/ai/chat")
async def chat(request: ChatRequest):
    async def generate():
        async for chunk in llm.stream(request.message):
            yield f"data: {json.dumps({'content': chunk})}\n\n"
        yield f"data: {json.dumps({'type': 'done'})}\n\n"
    
    return StreamingResponse(generate(), media_type="text/event-stream")

Frontend 코드:

const eventSource = new EventSource('/ai/chat');
eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    if (data.type === 'done') {
        eventSource.close();
    } else {
        displayChunk(data.content);
    }
};

4. 인프라 친화성

  • 기존 HTTP 인프라 그대로 사용
  • Nginx, AWS ALB 등 별도 설정 불필요
  • CORS 설정도 일반 HTTP와 동일

⚠️ 트레이드오프 (Trade-offs)

감수하는 제약사항

제약사항 대응 방안
클라이언트 → 서버 실시간 통신 불가 일반 HTTP POST로 충분
바이너리 데이터 비효율 LLM 응답은 텍스트, 해당 없음
연결당 하나의 스트림 동시 스트림 필요 시 별도 연결

향후 WebSocket 도입 검토 시점

  • 실시간 협업 기능 추가 시 (여러 사용자 동시 편집)
  • 실시간 알림 시스템 고도화 시
  • 타이핑 중 인디케이터 기능 추가 시

🔗 참고 자료


📅 이력

날짜 변경 내용
2026-01-07 초기 결정 및 문서 작성

ADR-002: 분석 결과 저장 형식 (JSON vs Text)

📋 메타데이터

항목 내용
상태 ✅ 채택됨 (Accepted)
작성일 2026-01-07
결정자 개발팀
관련 기능 /ai/analyze (채용공고/이력서 분석 + 매칭도), /ai/interview/report (면접 평가 및 피드백)

🎯 컨텍스트 (Context)

LLM이 생성한 분석 결과(이력서 분석, 채용공고 분석, 매칭도 등)를 DB에 저장할 때, JSON 구조화 형식으로 저장할지 원본 텍스트로 저장할지 결정해야 합니다.

저장이 필요한 분석 결과:

API 설명 저장 데이터
/ai/analyze 채용공고/이력서(포트폴리오) 분석 + 매칭도 강점, 약점, 개선제안, 점수, 등급, 스킬 비교
/ai/interview/report 면접 평가 및 피드백 질문별 점수, 피드백, 종합 리포트

🔍 선택지 분석 (Options)

Option 1: JSON 구조화 저장

{
  "resume_analysis": {
    "strengths": ["프론트엔드 개발 경험 3년", "React 숙련"],
    "weaknesses": ["클라우드 경험 부족"],
    "suggestions": ["AWS 자격증 취득 추천"]
  },
  "matching": {
    "score": 85,
    "grade": "A",
    "matched_skills": ["React", "TypeScript"]
  }
}

장점:

  • ✅ 필드별 직접 접근 가능 (result.matching.score)
  • ✅ Frontend에서 파싱 없이 바로 사용
  • ✅ 특정 필드 검색/필터링 가능 (점수 80 이상인 분석 결과)
  • ✅ 일관된 데이터 구조 보장
  • ✅ API 응답 그대로 저장 가능
  • ✅ 통계/대시보드 구현 용이

단점:

  • ⚠️ 스키마 변경 시 마이그레이션 필요
  • ⚠️ LLM 응답이 스키마와 다를 경우 파싱 에러
  • ⚠️ RAG 검색에는 텍스트가 더 적합할 수 있음

Option 2: 원본 텍스트 저장

{
  "analysis_text": "이력서 분석 결과입니다.\n\n강점:\n- 프론트엔드 개발 경험 3년...\n\n약점:\n- 클라우드 경험이 부족합니다..."
}

장점:

  • ✅ 스키마 변경에 자유로움
  • ✅ LLM 원본 응답 그대로 보존
  • ✅ RAG 검색에 바로 사용 가능
  • ✅ 자연어 형태로 사용자에게 표시 용이

단점:

  • ⚠️ 특정 필드 추출 시 파싱 필요
  • ⚠️ Frontend에서 구조화된 UI 렌더링 어려움
  • ⚠️ 검색/필터링 어려움
  • ⚠️ 일관성 보장 어려움

Option 3: 하이브리드 (JSON + 원본 텍스트)

{
  "structured_data": {
    "score": 85,
    "grade": "A",
    "strengths": [...],
    "matched_skills": [...]
  },
  "raw_text": "이력서 분석 결과입니다...전체 텍스트...",
  "summary": "매칭도 85%, A등급입니다."
}

장점:

  • ✅ 구조화된 데이터로 UI 렌더링
  • ✅ 원본 텍스트로 RAG 검색
  • ✅ 필요에 따라 유연하게 사용

단점:

  • ⚠️ 저장 용량 증가
  • ⚠️ 두 데이터 간 동기화 필요

📊 상세 비교표

항목 JSON Text 하이브리드
필드별 접근 ✅ 쉬움 ❌ 파싱 필요 ✅ 쉬움
Frontend 렌더링 ✅ 바로 사용 ⚠️ 가공 필요 ✅ 바로 사용
검색/필터링 ✅ 가능 ❌ 어려움 ✅ 가능
RAG 검색 ⚠️ 변환 필요 ✅ 바로 사용 ✅ 가능
스키마 유연성 ❌ 낮음 ✅ 높음 ⚠️ 중간
저장 용량 ⭐ 작음 ⭐ 작음 ⭐⭐ 큼
구현 복잡도 ⭐ 낮음 ⭐ 낮음 ⭐⭐ 중간

✅ 결정 (Decision)

JSON 구조화 저장을 기본으로 하되, 필요 시 원본 텍스트도 함께 저장합니다. (하이브리드)


📝 근거 (Rationale)

1. 주요 사용 케이스가 구조화된 데이터 필요

[Frontend UI 요구사항]
├── 매칭도 점수 차트 표시 → score 필드 필요
├── 등급별 뱃지 표시 → grade 필드 필요
├── 스킬 태그 나열 → skills 배열 필요
├── 강점/약점 카드 표시 → strengths/weaknesses 배열 필요
└── 면접 질문별 점수 표시 → evaluations 배열 필요

대부분의 UI가 구조화된 데이터를 직접 접근해야 함

2. API 응답과 DB 저장 형식 일치

/ai/analyze 응답 예시 (채용공고/이력서 분석 + 매칭도):

{
  "success": true,
  "resume_analysis": {
    "strengths": ["..."],
    "weaknesses": ["..."]
  },
  "matching": {
    "score": 85,
    "grade": "A"
  }
}

API 응답을 그대로 DB에 저장 → 일관성 유지, 변환 로직 불필요

3. 검색/통계 기능 지원

-- MongoDB 쿼리 예시
db.analysis_results.find({ "matching.score": { $gte: 80 } })
db.analysis_results.find({ "matching.grade": "A" })

JSON 필드 검색으로 "매칭도 80점 이상인 분석 결과" 쿼리 가능

4. Text만 저장해도 되는 경우

[Text 저장이 적합한 경우]
├── 단순 채팅 로그 저장 (대화 이력)
├── LLM 원본 응답 디버깅/로깅
├── RAG 검색용 데이터 (별도 VectorDB)
└── 자유 형식 메모/피드백

분석 결과는 구조화된 활용이 목적이므로 JSON이 적합


🗂️ 저장 구조 설계

MongoDB Collection: analysis_results

{
  "_id": "ObjectId",
  "user_id": "user_123",
  "resume_id": "resume_456",
  "posting_id": "posting_789",
  "created_at": "2026-01-07T12:00:00Z",
  
  // 구조화된 분석 결과 (JSON)
  "resume_analysis": {
    "strengths": ["프론트엔드 개발 경험 3년", "React, TypeScript 숙련"],
    "weaknesses": ["클라우드 경험 부족"],
    "suggestions": ["AWS 자격증 취득 추천"]
  },
  "posting_analysis": {
    "company": "카카오",
    "position": "프론트엔드 개발자",
    "required_skills": ["React", "TypeScript"],
    "preferred_skills": ["GraphQL", "AWS"]
  },
  "matching": {
    "score": 85,
    "grade": "A",
    "matched_skills": ["React", "TypeScript"],
    "missing_skills": ["GraphQL"]
  },
  
  // 원본 텍스트 (RAG 검색용, 선택적)
  "raw_response": "전체 LLM 응답 텍스트..."
}

⚠️ 트레이드오프 (Trade-offs)

감수하는 제약사항

제약사항 대응 방안
스키마 변경 시 마이그레이션 MongoDB 유연한 스키마 활용, 버전 필드 추가
LLM 응답 파싱 실패 가능성 Output Parser로 스키마 강제, 실패 시 재시도
저장 용량 증가 (하이브리드) raw_response는 선택적 저장

LLM 응답 스키마 강제 방법

# LangChain Output Parser 사용
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel

class AnalysisResult(BaseModel):
    strengths: list[str]
    weaknesses: list[str]
    suggestions: list[str]
    score: int
    grade: str

parser = PydanticOutputParser(pydantic_object=AnalysisResult)
prompt = prompt_template.format(format_instructions=parser.get_format_instructions())

🔗 참고 자료


📅 이력

날짜 변경 내용
2026-01-07 초기 결정 및 문서 작성

ADR-003: VectorDB 선택 (ChromaDB vs Pinecone vs Qdrant)

📋 메타데이터

항목 내용
상태 ✅ 채택됨 (Accepted)
작성일 2026-01-08
결정자 개발팀
관련 기능 이력서/채용공고 임베딩 저장, RAG 검색, 면접 피드백 저장

🎯 컨텍스트 (Context)

RAG(Retrieval-Augmented Generation) 기반 AI 서비스를 구현하기 위해 텍스트 임베딩을 저장하고 검색할 VectorDB를 선택해야 합니다.

VectorDB가 필요한 컬렉션:

컬렉션 용도 예상 크기
resumes 이력서 + 포트폴리오 임베딩 ~10K 문서
job_postings 채용공고 임베딩 ~50K 문서
analysis_results 분석 결과 임베딩 ~20K 문서
interview_feedback 면접 Q&A 임베딩 ~100K 문서

🔍 선택지 분석 (Options)

Option 1: ChromaDB

장점:

  • ✅ 오픈소스 (무료)
  • ✅ 로컬 설치 가능 → 개발/테스트 용이
  • ✅ Python 네이티브 → FastAPI와 통합 간편
  • ✅ LangChain 공식 지원
  • ✅ 충분한 성능 (100K~1M 문서)

단점:

  • ⚠️ 클라우드 관리형 버전 없음 (직접 운영 필요)
  • ⚠️ 대규모 확장 시 성능 한계

Option 2: Pinecone

장점:

  • ✅ 완전 관리형 서비스 (No DevOps)
  • ✅ 글로벌 분산 인프라
  • ✅ 무제한 확장성

단점:

  • ⚠️ 유료 (Free tier 제한적)
  • ⚠️ 외부 의존성 (네트워크 레이턴시)
  • ⚠️ 벤더 종속

Option 3: Qdrant

장점:

  • ✅ 오픈소스 + 클라우드 버전 모두 제공
  • ✅ Rust 기반 고성능
  • ✅ 다양한 필터링 지원

단점:

  • ⚠️ LangChain 통합이 ChromaDB보다 덜 성숙
  • ⚠️ 커뮤니티/문서가 상대적으로 작음

📊 상세 비교표

항목 ChromaDB Pinecone Qdrant
비용 무료 유료 무료/유료
설치 로컬/Docker 클라우드 Only 로컬/클라우드
LangChain 통합 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐
확장성 중간 (~1M) 높음 (무제한) 높음
개발 편의성 ⭐⭐⭐ ⭐⭐ ⭐⭐
운영 복잡도 중간 낮음 중간

✅ 결정 (Decision)

ChromaDB를 선택합니다.


📝 근거 (Rationale)

1. 현재 규모에 적합

예상 문서 수: ~200K 이하
ChromaDB 권장 범위: ~1M 문서
→ 충분한 성능 확보 가능

2. 개발 생산성

# ChromaDB 사용 예시 (간단함)
from langchain_chroma import Chroma

vectorstore = Chroma(
    collection_name="resumes",
    embedding_function=embeddings,
    persist_directory="./chroma_db"
)

# 검색
docs = vectorstore.similarity_search(query, k=5)

3. 비용 효율성

  • 초기 MVP/POC 단계에서 무료 사용
  • 추후 규모 확장 시 Pinecone 마이그레이션 가능

4. LangChain 생태계 적합

  • LangChain 공식 문서에서 가장 많이 사용
  • 풍부한 예제 코드

⚠️ 트레이드오프 (Trade-offs)

제약사항 대응 방안
클라우드 관리형 없음 Docker로 배포 → Kubernetes에서 관리
대규모 확장 한계 1M 문서 초과 시 Pinecone 마이그레이션 검토
백업/복구 직접 구현 persist_directory 백업 스크립트 구축

📅 이력

날짜 변경 내용
2026-01-08 초기 결정 및 문서 작성

ADR-004: 비동기 처리 방식 (폴링 vs 콜백 vs WebSocket)

📋 메타데이터

항목 내용
상태 ✅ 채택됨 (Accepted)
작성일 2026-01-08
결정자 개발팀
관련 기능 OCR 텍스트 추출 (/ai/ocr/extract), 마스킹 (/ai/masking/draft)

🎯 컨텍스트 (Context)

OCR이나 마스킹처럼 처리 시간이 오래 걸리는 API의 경우, 클라이언트가 결과를 받는 방식을 선택해야 합니다.

비동기 처리가 필요한 API:

API 예상 처리 시간 이유
/ai/ocr/extract 5~30초 파일 크기, 페이지 수에 따라 변동
/ai/masking/draft 3~15초 VLM 분석 + 이미지 처리

🔍 선택지 분석 (Options)

Option 1: 일반 폴링 (Short Polling) ⭐ 채택

1. POST /ai/ocr/extract → task_id 즉시 반환
2. GET /ai/task/{task_id} → status 확인 (즉시 응답, 반복)
3. status === 'completed' → 결과 획득

장점:

  • ✅ 구현 단순 (일반 HTTP 요청)
  • ✅ 상태 비저장 (Stateless)
  • ✅ 클라이언트 구현 간편
  • ✅ 네트워크 문제에 강건
  • ✅ 서버 연결 즉시 해제 → 리소스 효율적

단점:

  • ⚠️ 불필요한 요청 발생
  • ⚠️ 결과 확인 지연 (폴링 간격만큼)

Option 1-1: 롱 폴링 (Long Polling)

1. POST /ai/ocr/extract → task_id 즉시 반환
2. GET /ai/task/{task_id} → 완료될 때까지 연결 유지 (타임아웃까지 대기)
3. 완료 시 즉시 응답 / 타임아웃 시 재연결

장점:

  • ✅ 불필요한 요청 감소
  • ✅ 완료 즉시 결과 수신

단점:

  • ⚠️ 서버 연결 장시간 유지 → 리소스 점유
  • ⚠️ 로드밸런서/프록시 타임아웃 설정 필요
  • ⚠️ 구현 복잡도 증가 (asyncio.Event 등 필요)
  • ⚠️ 연결 유지 중 네트워크 끊김 시 재연결 로직 필요

일반 폴링 vs 롱 폴링 비교:

항목 일반 폴링 (Short) 롱 폴링 (Long)
연결 유지 즉시 해제 완료까지 유지
서버 부하 요청 수 많음 연결 수 많음
실시간성 폴링 간격만큼 지연 즉시 응답
구현 복잡도 낮음 중간
인프라 설정 불필요 타임아웃 조정 필요

결정: 일반 폴링(Short Polling) 채택 — 구현 단순성, 서버 리소스 효율성, 인프라 설정 불필요

Option 2: 콜백 (Webhook)

1. POST /ai/ocr/extract → task_id + callback_url 전달
2. AI Server가 완료 시 callback_url 호출

장점:

  • ✅ 불필요한 요청 없음
  • ✅ 즉시 알림

단점:

  • ⚠️ Backend가 콜백 엔드포인트 구현 필요
  • ⚠️ 네트워크 이슈 시 콜백 실패 처리 복잡
  • ⚠️ 보안 고려 필요 (콜백 URL 검증)

Option 3: WebSocket

1. WebSocket 연결 유지
2. 작업 완료 시 서버가 푸시

장점:

  • ✅ 실시간 푸시
  • ✅ 양방향 통신

단점:

  • ⚠️ 연결 관리 복잡
  • ⚠️ 인프라 설정 필요
  • ⚠️ 단발성 작업에 오버헤드

📊 상세 비교표

항목 폴링 콜백 WebSocket
구현 복잡도 ⭐ 낮음 ⭐⭐ 중간 ⭐⭐⭐ 높음
실시간성 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐
서버 부하 ⭐⭐ (폴링 요청) ⭐⭐⭐ ⭐⭐ (연결 유지)
신뢰성 ⭐⭐⭐ ⭐⭐ ⭐⭐
인프라 요구 없음 콜백 엔드포인트 WebSocket 서버

✅ 결정 (Decision)

폴링(Polling) 방식을 선택합니다.


📝 근거 (Rationale)

1. 구현 단순성

# AI Server (FastAPI)
@app.post("/ai/ocr/extract")
async def extract_text(request: OCRRequest):
    task_id = str(uuid.uuid4())
    background_tasks.add_task(process_ocr, task_id, request)
    return {"task_id": task_id, "status": "processing"}

@app.get("/ai/task/{task_id}")
async def get_task_status(task_id: str):
    return task_store.get(task_id)

2. 클라이언트 제어권

  • 클라이언트가 폴링 간격, 재시도 로직 직접 제어
  • 네트워크 끊김 시 자연스럽게 재시도

3. 지수 백오프로 효율화

// Frontend 폴링 로직
async function pollTask(taskId) {
    let interval = 1000; // 1초 시작
    const maxInterval = 8000; // 최대 8초
    
    while (true) {
        const result = await fetch(`/ai/task/${taskId}`);
        const data = await result.json();
        
        if (data.status === 'completed') return data.result;
        if (data.status === 'failed') throw new Error(data.error);
        
        await sleep(interval);
        interval = Math.min(interval * 2, maxInterval); // 지수 백오프
    }
}

4. 기존 인프라 활용

  • 별도 WebSocket 서버 불필요
  • 콜백 엔드포인트 구현 불필요

⚠️ 트레이드오프 (Trade-offs)

제약사항 대응 방안
불필요한 폴링 요청 지수 백오프로 요청 최소화
결과 확인 지연 초기 폴링 간격 1초로 설정
서버 부하 캐싱 + Rate Limiting

📅 이력

날짜 변경 내용
2026-01-08 초기 결정 및 문서 작성
2026-02-26 롱 폴링 vs 일반 폴링 비교 추가 — 일반 폴링(Short Polling) 채택 근거 명확화

ADR-005: 면접 Q&A 개별 저장 방식

📋 메타데이터

항목 내용
상태 ✅ 채택됨 (Accepted)
작성일 2026-01-08
결정자 개발팀
관련 기능 모의 면접 (/ai/interview/question, /ai/interview/save, /ai/interview/report)

🎯 컨텍스트 (Context)

모의 면접에서 꼬리질문을 생성할 때, LLM이 이전 대화 맥락을 파악해야 합니다. 이를 위해 Q&A 데이터를 어떻게 저장하고 전달할지 결정해야 합니다.

요구사항:

  • 꼬리질문 생성 시 이전 Q&A 맥락 필요
  • 면접 종료 시 전체 Q&A 기반 리포트 생성
  • 세션 중간에 끊겨도 복구 가능해야 함

🔍 선택지 분석 (Options)

Option 1: 프론트엔드에서 히스토리 누적 전달

// 매 요청 시 전체 히스토리 전달
{
    "session_id": "...",
    "history": [
        { "question": "Q1", "answer": "A1" },
        { "question": "Q2", "answer": "A2" }
    ]
}

장점:

  • ✅ 서버 상태 관리 불필요

단점:

  • ⚠️ 요청 크기 증가
  • ⚠️ 세션 중단 시 복구 불가
  • ⚠️ 프론트엔드 상태 관리 복잡

Option 2: 매 문답 개별 저장 (DB)

답변 입력 → POST /ai/interview/save → DB 저장
            ↓
 POST /ai/interview/question → DB에서 히스토리 조회 → 꼬리질문 생성

장점:

  • ✅ 세션 복구 가능
  • ✅ 요청 크기 일정
  • ✅ 데이터 영속성 보장
  • ✅ 면접 통계/분석 가능

단점:

  • ⚠️ DB 저장/조회 추가 비용
  • ⚠️ API 호출 증가

Option 3: 면접 종료 시 일괄 저장

프론트엔드 → 면접 종료 → POST /ai/interview/report (전체 Q&A 포함)

장점:

  • ✅ API 호출 최소화

단점:

  • ⚠️ 세션 중단 시 데이터 유실
  • ⚠️ 꼬리질문 맥락 파악 어려움

✅ 결정 (Decision)

Option 2: 매 문답 개별 저장 (DB) 방식을 선택합니다.


📝 근거 (Rationale)

1. 세션 안정성

사용자가 면접 중 브라우저 새로고침/종료 시:
- 개별 저장: 기존 Q&A 보존, 이어서 진행 가능
- 일괄 저장: 모든 데이터 유실

2. 꼬리질문 맥락 파악

# /ai/interview/question 내부 로직
async def generate_question(room_id: int, interview_id: int):
    # DB에서 히스토리 조회 (ai_chat_messages 테이블에서 면접 관련 메시지 조회)
    history = await db.ai_chat_messages.find({"room_id": room_id, "type": {"$in": ["INTERVIEW_QUESTION", "INTERVIEW_ANSWER"]}})
    
    # VectorDB에서 관련 컨텍스트 검색
    context = await vectordb.search(history[-1].answer)
    
    # LLM에 히스토리 + 컨텍스트 전달
    question = await llm.generate(history=history, context=context)
    return question

3. 면접 분석 가능

-- 예: 사용자별 면접 참여 통계
SELECT r.user_id, COUNT(*) as interview_count
FROM ai_chat_interview i
JOIN ai_chat_rooms r ON i.room_id = r.id
GROUP BY r.user_id;

🗂️ API 흐름

[면접 시작]
    ↓
POST /ai/interview/question → 첫 질문 생성
    ↓
[사용자 답변 입력]
    ↓
POST /ai/interview/save → Q&A DB 저장 (qa_id 반환)
    ↓
POST /ai/interview/question → 꼬리질문 생성 (DB에서 히스토리 조회)
    ↓
... 반복 (최대 5개) ...
    ↓
[면접 종료]
    ↓
POST /ai/interview/report → 전체 Q&A 기반 리포트 생성

⚠️ 트레이드오프 (Trade-offs)

제약사항 대응 방안
DB 저장/조회 비용 인덱싱 최적화 (session_id)
API 호출 증가 저장과 질문 생성 분리로 UX 개선
데이터 정합성 트랜잭션 처리