프로젝트 목록으로 돌아가기
Recruiter Briefinterview-coach

AI Interview Coach

JD 분석·질문 생성·피드백 스트리밍 병목을 5개 MSA와 SSE·RAG·Redis로 분리해 검증 가능한 면접 코칭 흐름을 구성했습니다.

Role
백엔드·AI 파이프라인 설계 및 구현
Period
2026
Team
개인 프로젝트
Theme
Observability
Java 21Spring Boot 3Spring SecuritySpring Cloud GatewaySpring Data JPAJWTLangChain4jNext.js 14

30초 요약

문제
이력서 기반 질문 생성에서 LLM 지연, SSE 연결, 세션 조회 N+1이 함께 발생할 수 있었습니다.
해결
Gateway/면접/AI/통계 책임을 나누고 SSE emitter 관리와 조회 최적화를 분리했습니다.
검증
면접 세션 목록 N+1을 1회 쿼리로 줄인 기록과 SSE 관리 경로를 문서화했습니다.

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 관리 경로를 확인했습니다.

11→1
Verified
면접 세션 API

N+1 조회를 1회 쿼리로 줄인 기록

100%
Verified
통계 정합성

100명 동시 완료 테스트 후 누락 없는 집계 결과

p95 <500ms 목표
Target
일반 API p95

일반 API 성능 목표와 SSE 관리 코드를 분리

Verified

11→1

세션 목록

Scenario
면접 세션 목록에서 QnA 개수 조회
Method
@EntityGraph와 COUNT 서브쿼리 적용 전후 비교
Result
면접 목록 조회의 N+1을 1회 쿼리로 줄인 기록을 README에 남겼습니다.
Target

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. 1. Next.js Frontend → Spring Cloud Gateway
  2. 2. Spring Cloud Gateway → User Service
  3. 3. Spring Cloud Gateway → Interview Service
  4. 4. Spring Cloud Gateway → AI Service
  5. 5. Spring Cloud Gateway → Statistics Service
  6. 6. AI Service → LangChain4j
  7. 7. LangChain4j → Claude API
  8. 8. LangChain4j → ChromaDB
  9. 9. User Service → PostgreSQL
  10. 10. Interview Service → PostgreSQL
  11. 11. Statistics Service → PostgreSQL
  12. 12. Interview Service → Redis
  13. 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 --> RD

ERD

전체 ERD

erd

전체 ERD

Mermaid 원본을 파싱해 텍스트는 DOM으로 렌더링합니다.

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

+ 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. #1 users ||--o{ interview_sessions · 진행
  2. #2 users ||--o{ user_statistics · 보유
  3. #3 users ||--o{ daily_activity · 기록
  4. #4 job_descriptions ||--o{ generated_questions · 생성
  5. #5 interview_sessions ||--o{ interview_qna · 포함
  6. #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
ClientGatewayInterview ServiceAI ServiceClaude APIChromaDBPostgreSQL
  1. 1요청

    Client → Gateway

    POST /api/interviews/jd

  2. 2처리

    Gateway → Interview Service

    JD 분석 요청

  3. 3처리

    Interview Service → AI Service

    JD 텍스트 전달

  4. 4처리

    AI Service → Claude API

    키워드 추출 요청

  5. 5처리

    Claude API → AI Service

    직무 키워드

  6. 6처리

    AI Service → ChromaDB

    키워드 임베딩 유사도 검색

  7. 7처리

    ChromaDB → AI Service

    관련 Q&A 컨텍스트

  8. 8처리

    AI Service → Claude API

    컨텍스트 + 질문 생성 요청

+ 5개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → GatewayPOST /api/interviews/jd-
2Gateway → Interview ServiceJD 분석 요청-
3Interview Service → AI ServiceJD 텍스트 전달-
4AI Service → Claude API키워드 추출 요청-
5Claude API → AI Service직무 키워드-
6AI Service → ChromaDB키워드 임베딩 유사도 검색-
7ChromaDB → AI Service관련 Q&A 컨텍스트-
8AI Service → Claude API컨텍스트 + 질문 생성 요청-
9Claude API → AI Service맞춤 질문 목록-
10AI Service → Interview Service생성된 질문-
11Interview Service → PostgreSQL질문 저장-
12Interview Service → Gateway질문 목록 응답-
13Gateway → Client200 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
ClientGatewayInterview ServicePostgreSQLRedis
  1. 1요청

    Client → Gateway

    POST /api/interviews/sessions

  2. 2처리

    Gateway → Interview Service

    세션 생성

  3. 3저장

    Interview Service → PostgreSQL

    세션 저장

  4. 4처리

    Interview Service → Redis

    세션 상태 캐싱

  5. 5처리

    Interview Service → Client

    세션 ID + 첫 질문

  6. 6loop 각 질문마다

    control

    loop 각 질문마다

    조건: loop 각 질문마다

  7. 7요청

    Client → Gateway

    POST /api/interviews/sessions/{id}/answer

    조건: loop 각 질문마다

  8. 8loop 각 질문마다

    Gateway → Interview Service

    답변 제출

    조건: loop 각 질문마다

+ 7개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → GatewayPOST /api/interviews/sessions-
2Gateway → Interview Service세션 생성-
3Interview Service → PostgreSQL세션 저장-
4Interview Service → Redis세션 상태 캐싱-
5Interview Service → Client세션 ID + 첫 질문-
6controlloop 각 질문마다loop 각 질문마다
7Client → GatewayPOST /api/interviews/sessions/{id}/answerloop 각 질문마다
8Gateway → Interview Service답변 제출loop 각 질문마다
9Interview Service → PostgreSQLQnA 저장loop 각 질문마다
10Interview Service → Redis진행 상태 업데이트loop 각 질문마다
11Interview Service → Client다음 질문loop 각 질문마다
12Client → GatewayPOST /api/interviews/sessions/{id}/complete-
13Gateway → Interview Service세션 완료-
14Interview Service → PostgreSQL최종 결과 저장-
15Interview 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
ClientGatewayAI ServiceClaude APIChromaDBRedis
  1. 1요청

    Client → Gateway

    GET /api/ai/feedback/{qnaId} (SSE)

  2. 2응답

    Gateway → AI Service

    SSE 연결

  3. 3처리

    AI Service → Redis

    캐시 확인

  4. 4alt 캐시 히트

    control

    alt 캐시 히트

    조건: alt 캐시 히트

  5. 5응답

    AI Service → Client

    SSE: 캐싱된 피드백 전송

    조건: alt 캐시 히트

  6. 6else 캐시 미스

    control

    else 캐시 미스

    조건: else 캐시 미스

  7. 7else 캐시 미스

    AI Service → ChromaDB

    답변 임베딩 유사도 검색

    조건: else 캐시 미스

  8. 8else 캐시 미스

    ChromaDB → AI Service

    참고 컨텍스트

    조건: else 캐시 미스

+ 6개 단계는 아래 상세 메시지에서 확인할 수 있습니다.

전체 메시지 상세
StepFrom → ToMessageCondition
1Client → GatewayGET /api/ai/feedback/{qnaId} (SSE)-
2Gateway → AI ServiceSSE 연결-
3AI Service → Redis캐시 확인-
4controlalt 캐시 히트alt 캐시 히트
5AI Service → ClientSSE: 캐싱된 피드백 전송alt 캐시 히트
6controlelse 캐시 미스else 캐시 미스
7AI Service → ChromaDB답변 임베딩 유사도 검색else 캐시 미스
8ChromaDB → AI Service참고 컨텍스트else 캐시 미스
9AI Service → Claude API스트리밍 평가 요청else 캐시 미스
10controlloop 토큰 단위else 캐시 미스 / loop 토큰 단위
11Claude API → AI Service토큰else 캐시 미스 / loop 토큰 단위
12AI Service → ClientSSE: data 토큰else 캐시 미스 / loop 토큰 단위
13AI Service → Redis전체 피드백 캐싱else 캐시 미스
14AI Service → ClientSSE: [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