Problem
Problem
면접 코칭 흐름은 JD 분석, 질문 생성, 답변 평가, 피드백 스트리밍이 이어져 있어 LLM 지연과 일반 API 응답이 서로 영향을 줄 수 있었습니다.
Decision
Decision
서비스를 Gateway, 면접, AI, 통계 책임으로 나누고, SSE 스트리밍과 일반 API 처리 기준을 분리해 병목을 설명 가능한 구조로 만들었습니다.
Build
Implementation
- 면접 세션 목록은 @EntityGraph와 COUNT 쿼리로 QnA 개수 조회의 N+1 문제를 줄였습니다.
- SSE 연결은 emitter 관리 코드에서 TTL과 정리 흐름을 두고, 일반 API 성능 기준 문서와 분리했습니다.
- RAG 구성은 LangChain4j와 ChromaDB를 사용하되, LLM 품질 의존성은 한계로 명시했습니다.
AI 기능은 모델 호출 자체보다 주변 API의 책임 분리, 스트리밍 생명주기 관리, 측정 기준을 분리해 설명해야 설계가 구체적으로 보인다는 점을 얻었습니다.
Proof
Verification
README의 성능 개선 기록과 SseEmitterManager 코드, performance README의 일반 API p95 목표 기준을 근거로 N+1 개선과 SSE 관리 경로를 확인했습니다.
N+1 조회를 1회 쿼리로 줄인 기록
100명 동시 완료 테스트 후 누락 없는 집계 결과
일반 API 성능 목표와 SSE 관리 코드를 분리
11→1
세션 목록
- Scenario
- 면접 세션 목록에서 QnA 개수 조회
- Method
- @EntityGraph와 COUNT 서브쿼리 적용 전후 비교
- Result
- 면접 목록 조회의 N+1을 1회 쿼리로 줄인 기록을 README에 남겼습니다.
p95 <500ms 목표
SSE 중 API
- Scenario
- 피드백 SSE 스트리밍과 일반 API가 함께 있는 구조
- Method
- SSE emitter 관리 코드와 일반 API p95 목표 기준을 분리
- Result
- SSE 연결 관리와 성능 기준을 문서/코드로 분리해 검증 경로를 마련했습니다.
Boundaries
Trade-offs & Limitations
Trade-offs
- SSE는 구현이 단순하고 브라우저 친화적이지만 양방향 상호작용이 필요한 경우 WebSocket보다 제약이 있습니다.
- RAG 캐시는 반복 답변을 빠르게 하지만 JD나 질문 맥락이 바뀌면 캐시 키 설계가 중요합니다.
Limitations
- LLM 응답 품질은 외부 모델과 프롬프트에 의존합니다.
- Kubernetes/HPA 구성은 실서비스 트래픽이 아닌 검증 환경 기준입니다.
아키텍처
전체 아키텍처▼
architecture
전체 아키텍처
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Client
사용자 진입과 실시간 상태 확인
Next.js Frontend
Client
Application
API, 도메인 로직, 워커 처리
Spring Cloud Gateway
Gateway
User Service
Services
Interview Service
Services
AI Service
Services
Statistics Service
Services
LangChain4j
AI_Infra
Claude API
AI_Infra
Data / Messaging
상태 저장, 캐시, 이벤트 전달
PostgreSQL
Data
Redis
Data
ChromaDB
Data
핵심 연결 흐름
- 1. Next.js Frontend → Spring Cloud Gateway
- 2. Spring Cloud Gateway → User Service
- 3. Spring Cloud Gateway → Interview Service
- 4. Spring Cloud Gateway → AI Service
- 5. Spring Cloud Gateway → Statistics Service
- 6. AI Service → LangChain4j
- 7. LangChain4j → Claude API
- 8. LangChain4j → ChromaDB
- 9. User Service → PostgreSQL
- 10. Interview Service → PostgreSQL
- 11. Statistics Service → PostgreSQL
- 12. Interview Service → Redis
- 13. AI Service → Redis
원본 Mermaid 보기
flowchart LR
subgraph Client
FE[Next.js Frontend]
end
subgraph Gateway
GW[Spring Cloud Gateway]
end
subgraph Services
US[User Service]
IS[Interview Service]
AI[AI Service]
SS[Statistics Service]
end
subgraph AI_Infra["AI Infrastructure"]
LC[LangChain4j]
LLM[Claude API]
end
subgraph Data
PG[(PostgreSQL)]
RD[(Redis)]
CR[(ChromaDB)]
end
FE --> GW
GW --> US
GW --> IS
GW --> AI
GW --> SS
AI --> LC
LC --> LLM
LC --> CR
US --> PG
IS --> PG
SS --> PG
IS --> RD
AI --> RDERD
전체 ERD▼
erd
전체 ERD
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
users
- bigint
- id
- PK
- varchar
- UK
- varchar
- password
- varchar
- nickname
- varchar
- target_job
- datetime
- created_at
job_descriptions
- bigint
- id
- PK
- bigint
- user_id
- FK
- text
- jd_text
- varchar
- company_name
- varchar
- position
- json
- extracted_keywords
+ 1개 필드
generated_questions
- bigint
- id
- PK
- bigint
- jd_id
- FK
- text
- question_text
- varchar
- category
- varchar
- difficulty
- text
- reference_answer
interview_sessions
- bigint
- id
- PK
- bigint
- user_id
- FK
- bigint
- jd_id
- FK
- enum
- status
- int
- total_questions
- int
- answered_count
+ 3개 필드
interview_qna
- bigint
- id
- PK
- bigint
- session_id
- FK
- bigint
- question_id
- FK
- text
- user_answer
- text
- ai_feedback
- double
- score
+ 2개 필드
user_statistics
- bigint
- id
- PK
- bigint
- user_id
- FK
- int
- total_sessions
- int
- total_questions_answered
- double
- avg_score
- double
- best_score
+ 3개 필드
daily_activity
- bigint
- id
- PK
- bigint
- user_id
- FK
- date
- activity_date
- int
- sessions_count
- int
- questions_count
- double
- avg_score
관계 요약
- #1 users ||--o{ interview_sessions · 진행
- #2 users ||--o{ user_statistics · 보유
- #3 users ||--o{ daily_activity · 기록
- #4 job_descriptions ||--o{ generated_questions · 생성
- #5 interview_sessions ||--o{ interview_qna · 포함
- #6 generated_questions ||--o{ interview_qna · 사용
원본 Mermaid 보기
erDiagram
users ||--o{ interview_sessions : "진행"
users ||--o{ user_statistics : "보유"
users ||--o{ daily_activity : "기록"
job_descriptions ||--o{ generated_questions : "생성"
interview_sessions ||--o{ interview_qna : "포함"
generated_questions ||--o{ interview_qna : "사용"
users {
bigint id PK
varchar email UK
varchar password
varchar nickname
varchar target_job
datetime created_at
}
job_descriptions {
bigint id PK
bigint user_id FK
text jd_text
varchar company_name
varchar position
json extracted_keywords
datetime created_at
}
generated_questions {
bigint id PK
bigint jd_id FK
text question_text
varchar category
varchar difficulty
text reference_answer
}
interview_sessions {
bigint id PK
bigint user_id FK
bigint jd_id FK
enum status
int total_questions
int answered_count
double avg_score
datetime started_at
datetime completed_at
}
interview_qna {
bigint id PK
bigint session_id FK
bigint question_id FK
text user_answer
text ai_feedback
double score
int order_num
datetime answered_at
}
user_statistics {
bigint id PK
bigint user_id FK
int total_sessions
int total_questions_answered
double avg_score
double best_score
varchar strongest_category
varchar weakest_category
datetime last_updated
}
daily_activity {
bigint id PK
bigint user_id FK
date activity_date
int sessions_count
int questions_count
double avg_score
}시퀀스 다이어그램
JD 분석 & 질문 생성▼
sequence
JD 분석 & 질문 생성
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → Gateway
POST /api/interviews/jd
- 2처리
Gateway → Interview Service
JD 분석 요청
- 3처리
Interview Service → AI Service
JD 텍스트 전달
- 4처리
AI Service → Claude API
키워드 추출 요청
- 5처리
Claude API → AI Service
직무 키워드
- 6처리
AI Service → ChromaDB
키워드 임베딩 유사도 검색
- 7처리
ChromaDB → AI Service
관련 Q&A 컨텍스트
- 8처리
AI Service → Claude API
컨텍스트 + 질문 생성 요청
+ 5개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → Gateway | POST /api/interviews/jd | - |
| 2 | Gateway → Interview Service | JD 분석 요청 | - |
| 3 | Interview Service → AI Service | JD 텍스트 전달 | - |
| 4 | AI Service → Claude API | 키워드 추출 요청 | - |
| 5 | Claude API → AI Service | 직무 키워드 | - |
| 6 | AI Service → ChromaDB | 키워드 임베딩 유사도 검색 | - |
| 7 | ChromaDB → AI Service | 관련 Q&A 컨텍스트 | - |
| 8 | AI Service → Claude API | 컨텍스트 + 질문 생성 요청 | - |
| 9 | Claude API → AI Service | 맞춤 질문 목록 | - |
| 10 | AI Service → Interview Service | 생성된 질문 | - |
| 11 | Interview Service → PostgreSQL | 질문 저장 | - |
| 12 | Interview Service → Gateway | 질문 목록 응답 | - |
| 13 | Gateway → Client | 200 OK | - |
원본 Mermaid 보기
sequenceDiagram
participant C as Client
participant GW as Gateway
participant IS as Interview Service
participant AI as AI Service
participant LLM as Claude API
participant CR as ChromaDB
participant PG as PostgreSQL
C->>GW: POST /api/interviews/jd
GW->>IS: JD 분석 요청
IS->>AI: JD 텍스트 전달
AI->>LLM: 키워드 추출 요청
LLM-->>AI: 직무 키워드
AI->>CR: 키워드 임베딩 유사도 검색
CR-->>AI: 관련 Q&A 컨텍스트
AI->>LLM: 컨텍스트 + 질문 생성 요청
LLM-->>AI: 맞춤 질문 목록
AI-->>IS: 생성된 질문
IS->>PG: 질문 저장
IS-->>GW: 질문 목록 응답
GW-->>C: 200 OK모의 면접 세션▼
sequence
모의 면접 세션
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → Gateway
POST /api/interviews/sessions
- 2처리
Gateway → Interview Service
세션 생성
- 3저장
Interview Service → PostgreSQL
세션 저장
- 4처리
Interview Service → Redis
세션 상태 캐싱
- 5처리
Interview Service → Client
세션 ID + 첫 질문
- 6loop 각 질문마다
control
loop 각 질문마다
조건: loop 각 질문마다
- 7요청
Client → Gateway
POST /api/interviews/sessions/{id}/answer
조건: loop 각 질문마다
- 8loop 각 질문마다
Gateway → Interview Service
답변 제출
조건: loop 각 질문마다
+ 7개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → Gateway | POST /api/interviews/sessions | - |
| 2 | Gateway → Interview Service | 세션 생성 | - |
| 3 | Interview Service → PostgreSQL | 세션 저장 | - |
| 4 | Interview Service → Redis | 세션 상태 캐싱 | - |
| 5 | Interview Service → Client | 세션 ID + 첫 질문 | - |
| 6 | control | loop 각 질문마다 | loop 각 질문마다 |
| 7 | Client → Gateway | POST /api/interviews/sessions/{id}/answer | loop 각 질문마다 |
| 8 | Gateway → Interview Service | 답변 제출 | loop 각 질문마다 |
| 9 | Interview Service → PostgreSQL | QnA 저장 | loop 각 질문마다 |
| 10 | Interview Service → Redis | 진행 상태 업데이트 | loop 각 질문마다 |
| 11 | Interview Service → Client | 다음 질문 | loop 각 질문마다 |
| 12 | Client → Gateway | POST /api/interviews/sessions/{id}/complete | - |
| 13 | Gateway → Interview Service | 세션 완료 | - |
| 14 | Interview Service → PostgreSQL | 최종 결과 저장 | - |
| 15 | Interview Service → Client | 세션 요약 | - |
원본 Mermaid 보기
sequenceDiagram
participant C as Client
participant GW as Gateway
participant IS as Interview Service
participant PG as PostgreSQL
participant RD as Redis
C->>GW: POST /api/interviews/sessions
GW->>IS: 세션 생성
IS->>PG: 세션 저장
IS->>RD: 세션 상태 캐싱
IS-->>C: 세션 ID + 첫 질문
loop 각 질문마다
C->>GW: POST /api/interviews/sessions/{id}/answer
GW->>IS: 답변 제출
IS->>PG: QnA 저장
IS->>RD: 진행 상태 업데이트
IS-->>C: 다음 질문
end
C->>GW: POST /api/interviews/sessions/{id}/complete
GW->>IS: 세션 완료
IS->>PG: 최종 결과 저장
IS-->>C: 세션 요약AI 피드백 (SSE 스트리밍)▼
sequence
AI 피드백 (SSE 스트리밍)
Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.
Participants
- 1요청
Client → Gateway
GET /api/ai/feedback/{qnaId} (SSE)
- 2응답
Gateway → AI Service
SSE 연결
- 3처리
AI Service → Redis
캐시 확인
- 4alt 캐시 히트
control
alt 캐시 히트
조건: alt 캐시 히트
- 5응답
AI Service → Client
SSE: 캐싱된 피드백 전송
조건: alt 캐시 히트
- 6else 캐시 미스
control
else 캐시 미스
조건: else 캐시 미스
- 7else 캐시 미스
AI Service → ChromaDB
답변 임베딩 유사도 검색
조건: else 캐시 미스
- 8else 캐시 미스
ChromaDB → AI Service
참고 컨텍스트
조건: else 캐시 미스
+ 6개 단계는 아래 상세 메시지에서 확인할 수 있습니다.
전체 메시지 상세
| Step | From → To | Message | Condition |
|---|---|---|---|
| 1 | Client → Gateway | GET /api/ai/feedback/{qnaId} (SSE) | - |
| 2 | Gateway → AI Service | SSE 연결 | - |
| 3 | AI Service → Redis | 캐시 확인 | - |
| 4 | control | alt 캐시 히트 | alt 캐시 히트 |
| 5 | AI Service → Client | SSE: 캐싱된 피드백 전송 | alt 캐시 히트 |
| 6 | control | else 캐시 미스 | else 캐시 미스 |
| 7 | AI Service → ChromaDB | 답변 임베딩 유사도 검색 | else 캐시 미스 |
| 8 | ChromaDB → AI Service | 참고 컨텍스트 | else 캐시 미스 |
| 9 | AI Service → Claude API | 스트리밍 평가 요청 | else 캐시 미스 |
| 10 | control | loop 토큰 단위 | else 캐시 미스 / loop 토큰 단위 |
| 11 | Claude API → AI Service | 토큰 | else 캐시 미스 / loop 토큰 단위 |
| 12 | AI Service → Client | SSE: data 토큰 | else 캐시 미스 / loop 토큰 단위 |
| 13 | AI Service → Redis | 전체 피드백 캐싱 | else 캐시 미스 |
| 14 | AI Service → Client | SSE: [DONE] | else 캐시 미스 |
원본 Mermaid 보기
sequenceDiagram
participant C as Client
participant GW as Gateway
participant AI as AI Service
participant LLM as Claude API
participant CR as ChromaDB
participant RD as Redis
C->>GW: GET /api/ai/feedback/{qnaId} (SSE)
GW->>AI: SSE 연결
AI->>RD: 캐시 확인
alt 캐시 히트
AI-->>C: SSE: 캐싱된 피드백 전송
else 캐시 미스
AI->>CR: 답변 임베딩 유사도 검색
CR-->>AI: 참고 컨텍스트
AI->>LLM: 스트리밍 평가 요청
loop 토큰 단위
LLM-->>AI: 토큰
AI-->>C: SSE: data 토큰
end
AI->>RD: 전체 피드백 캐싱
AI-->>C: SSE: [DONE]
end