Skip to content

[Fix] 검색-RAG 비동기 처리 전환 (동시 요청 시 응답 유실 방지) #218

Description

@kangcheolung

📌 Description

로컬 Ollama(GPU 1개)는 동시 생성 요청을 병렬이 아닌 순차 처리한다 — 실측 결과 단독 요청 11.4초
대비, 동시 요청 3개가 11.2초 / 22.5초 / 33.6초로 계단식 증가한다(Ollama 자체 로그로 검증,
decode 속도는 18토큰/초로 3건 모두 동일 — 처리 자체가 느려진 게 아니라 순서 대기 때문임을 확인).

지금은 혼잡 감지 기준이 read-timeout(27초)뿐이라, 밀린 요청은 27초를 다 기다려야 실패로
인식되고 fallback으로 전환된다. 그런데 프론트는 29초에 먼저 포기하기 때문에, 완성된 fallback을
사용자가 아예 못 받는 경우가 생긴다. 그 시간 동안 Tomcat 스레드도 계속 점유된다.

대안 검토 — 세마포어(JVM Semaphore) 게이트 기각: 짧게(3초) 대기 후 실패시켜 검색 1등 문서
원문 발췌로 fallback하는 방식을 검토·프로토타입까지 했으나 기각했다. 이 fallback은 LLM을
아예 거치지 않아서, 혼잡 시 사용자가 "AI가 답한 것"이 아니라 "그냥 검색 결과 원문"만 받게 되어
이 프로젝트의 핵심 가치(권한 필터 적용된 벡터 검색 + RAG)가 조용히 검색 기능으로 격하된다.
"빠르게 실패시키는 것"보다 "실패 자체를 없애는 것"이 이 프로젝트 정체성에 더 맞는다고 판단해
비동기 처리로 방향을 전환한다.

🎯 설계 방식 — 비동기 Job 큐 (검색·백엔드·프론트 전체)

흐름

  1. POST /search → 벡터 검색(빠름, 1~2초)만 동기 수행 → 검색 결과 + queryId +
    status:PROCESSING을 즉시 반환 (AI 답변은 아직 없음)
  2. RagResponsePROCESSING 상태로 즉시 저장, Job 큐에 등록
  3. 신규 Worker(경량 폴링, 단일 스레드)가 PROCESSING 건을 순서대로 꺼내 OllamaClient.generate()
    호출 — Worker가 1개라 자연히 직렬화되므로 별도 세마포어 불필요
  4. 완료되면 RagResponseSUCCESS/FAILED로 갱신 + 해당 유저 전용 WebSocket 채널로 push
  5. 프론트: 검색 결과(문서 목록)는 1단계 응답으로 즉시 렌더링, AI 답변 카드는 로딩
    스켈레톤으로 표시 → WebSocket 알림 수신 시 텍스트로 전환(2단계 UI)

백엔드 변경

  • RagResponse.answerText NOT NULL 제약 완화 (PROCESSING 시점엔 아직 답이 없음) — 신규 Flyway 마이그레이션
  • RagResponseCommandService.createPending() 추가
  • SearchController 응답 계약 변경 — 검색 결과와 AI 답변을 분리해서 반환
  • 신규 RAG Worker — embedding_jobs용 Worker(heartbeat/lease 등 26개 파일 규모)는 과함,
    단일 인스턴스·단일 GPU 기준이라 @Scheduled 폴링 + 순차 처리 정도의 경량 버전으로 축소
  • WebSocketConfig에 유저별 개인 채널(/user/queue/...) 추가 — 지금 있는 /topic/dashboard
    전체 브로드캐스트(관리자용)라 이 목적에 안 맞음. 구독 인가 로직 신규(자기 queryId인지 검증)
  • ollama.generate-deadline(25초→예: 60초), ollama.server.read-timeout(27초→그보다 크게)
    재튜닝 — 지금은 "프론트 29초 제한 전에 끝내야 함"이 기준이었는데, 비동기 전환 후엔 그 압박이
    없어짐. 대신 "Worker가 멈춘 요청 하나 때문에 큐 전체가 막히지 않도록" 하는 안전장치로 역할이
    바뀌므로, 무한정 늘리지 않고 적정선으로 재설정

프론트 변경

  • SearchPage.tsx: searching(검색 단계) / answering(AI답변 단계) 상태 분리
  • 검색 결과(source-list)는 1차 응답 즉시 렌더링
  • answer-card 자리에 로딩 스켈레톤 → WebSocket 수신 시 텍스트로 자연스럽게 전환(fade-in)
  • WebSocket 구독/구독해제 로직 추가 (페이지 이탈 시 정리 포함)

✅ To-do

  • RagResponse.answerText nullable 마이그레이션
  • RagResponseCommandService.createPending() 추가
  • SearchController 응답 계약 변경(검색 결과 + queryId + status 즉시 반환)
  • RAG Worker(경량 폴링) 신규 구현
  • generate-deadline/read-timeout 재튜닝
  • WebSocketConfig: user-destination-prefix 추가, per-query 구독 인가 인터셉터 신규
  • RAG 전용 WebSocket push 컨트롤러 신규
  • 프론트: searching/answering 상태 분리, 답변 스켈레톤 UI
  • 프론트: WebSocket 구독·수신·전환 애니메이션 처리
  • 단위·통합 테스트(Worker 순차 처리, WebSocket 구독 인가, nullable 제약)

✅ 완료 기준

  • 동시에 여러 질문이 들어와도 모든 요청이 결국 완전한 LLM 답변을 받는다(실패 없음)
  • 검색 결과는 항상 1~2초 안에 먼저 표시된다
  • AI 답변은 준비되는 즉시 새로고침 없이 화면에 반영된다
  • 본인이 요청한 쿼리의 답변만 수신 가능하다(다른 유저 쿼리 구독 시 거부)

📒 기타

  • 배경 실측 데이터: docs/design/kangcheolung-#210-ollama-rag-timeout-fix.md
  • 상세 설계 문서는 구현 완료 후 별도 작성 예정

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions