Skip to content

fix(document): 분석 실패 재분석 + 에러 문구 사람말 + LLM 타임아웃 조정 - #246

Merged
i3months merged 1 commit into
devfrom
fix/document-reanalysis
Sep 24, 2026
Merged

i3months merged 1 commit into
devfrom
fix/document-reanalysis

Conversation

@i3months

Copy link
Copy Markdown
Member

작업 내용

  • POST /api/documents/{id}/reanalyze (DocumentReanalysisService, 202) — 실패 문서를 같은 원본으로 다시 분석
  • 프론트: errorCode → 한국어 문구 매핑(analysisError.ts) + '다시 분석하기' 버튼
  • LLM_PRO_TIMEOUT_SEC 30 → 60
  • DEEPGRAM_TIMEOUT_SEC 30 → 20
  • VOICE_RETRANSCRIBE_NOT_ALLOWED 의 deprecated UNPROCESSABLE_ENTITY → UNPROCESSABLE_CONTENT (직전 PR feat(session): STT 실패한 음성 답변 재전사 #245 에서 들어간 것)

변경 이유 / 배경

운영 데이터 점검 중 사용자의 웹 이력서 1건이 사흘째 FAILED 로 방치된 걸 발견했습니다.

resume 18 · portfolio.i3months.com · 2026-09-21 06:00 · FAILED
error_code: UNEXPECTED
error_message: APITimeoutError: Request timed out.

DocumentList.tsx 가 errorMessage 를 그대로 렌더해서 한글 UI 에 영문 예외 문자열이 노출되고 있었고, 재분석 API 가 없어 자료를 지우고 다시 등록하는 것 말고는 방법이 없었습니다. #244/#245 에서 고친 STT 실패와 정확히 같은 결함 유형입니다 — 일시 장애 → 영구 실패, 복구 경로 없음.

타임아웃을 올리는 근거 (운영 ai_request_logs)

모델 요청 유형 n p50 p90 max
pro feedback.synthesis 19 21.6s 31.9s 56.1s
pro generate.questions 7 14.8s 21.7s 25.7s
pro feedback.panel 58 12.4s 18.9s 25.2s

56초짜리 성공 호출이 존재한다는 게 핵심입니다. llm_pro_timeout_sec=30 인데 56초가 SUCCESS 로 찍혔다는 건 첫 시도가 30초에 걸려 죽고 내부 재시도가 26초에 성공했다는 뜻입니다. 즉 Pro 는 이미 상습적으로 타임아웃에 걸려 같은 호출을 두 번 물고 있었고(지연 2배, 비용 2배), 입력이 더 큰 문서 분석은 3회 모두 30초를 넘겨 영구 실패했습니다. 60초는 관측 p90(31.9초)의 1.9배입니다.

반대로 STT 는 성공이 p50 3.8초 / p90 8.1초 / 최대 12.1초 라 30초가 과하게 느슨했습니다. 어제 실측에서 멎은 호출을 31초나 붙잡았는데, 그 시간이 곧 재시도 전 낭비 대기입니다. 20초면 최대치의 1.65배.

설계 결정

  • 재분석은 새 문서를 만든다 — 기존 요청 경로(AnalysisRequestService)가 매번 AnalyzedDocument 를 새로 만들기 때문에 그 흐름을 그대로 탑니다. 대신 실패 행을 soft delete 해서 목록에 실패 카드와 진행 카드가 같이 뜨지 않게 했습니다.
  • 재시도해도 같은 실패엔 버튼을 안 냅니다 — 빈 PDF, 차단된 URL, 해석 안 되는 호스트 등. 눌러도 같은 자리로 돌아오면 사용자는 고장으로 받아들입니다.
  • errorMessage 는 API 에 그대로 둡니다 — 지원 문의·운영 디버깅에 필요합니다. 화면에서만 안 씁니다.

테스트

  • 백엔드 6건 — 웹/파일 이력서·레포·자소서 각 출처 디스패치, 실패 아닌 문서 거절, 소유권 거절, soft delete 확인
  • 프론트 6건 — 문구 매핑, 영문 예외 미노출, 폴백, 재시도 가능/불가 분류
  • 프론트 전체 vitest run 46 files / 197 tests, tsc --noEmit, eslint --max-warnings=0 clean
  • AI uv run pytest 405 passed, black/flake8 clean
  • backend/openapi.json + generated.ts 재생성 (추가만)

영향 범위

  • DB 마이그레이션: 없음
  • API contract 변경: 있음 — 엔드포인트 1개 추가
  • 환경변수: 기본값 변경 2건 (LLM_PRO_TIMEOUT_SEC 30→60, DEEPGRAM_TIMEOUT_SEC 30→20). 운영 .env 에 두 키 모두 없어 코드 기본값이 적용됩니다.

리뷰어 체크포인트

  • LLM_PRO_TIMEOUT_SEC 60초가 피드백 예산과 충돌하지 않는지 봐주세요. 피드백은 asyncio.gather 병렬이고 대략 2분 예산인데, 최악의 경우 60초 × 내부 재시도로 그 예산을 넘길 수 있습니다. 재시도 횟수를 같이 줄이는 게 맞을지 판단이 필요합니다.
  • 재분석이 새 문서를 만들면서 실패 문서를 soft delete 하는 방식이 맞는지 — 실패 이력을 남겨야 한다면 다른 방식이 필요합니다.
  • 문구 매핑 테이블의 한국어 표현.

🤖 Generated with Claude Code

운영 점검에서 사용자의 웹 이력서 1건(portfolio URL)이 2026-09-21 부터 사흘째
FAILED 로 남아 있는 걸 발견했다. 화면에는 "APITimeoutError: Request timed out."
이라는 AI 서버 예외 문자열이 한글 UI 에 그대로 노출됐고, 재분석 API 가 없어
자료를 지우고 다시 등록하는 것 말고는 방법이 없었다.

방금 고친 STT 실패와 같은 결함 유형이다 — 일시 장애가 영구 실패로 굳고,
되돌릴 경로가 없고, 사용자는 영문 예외를 본다.

- POST /api/documents/{id}/reanalyze (DocumentReanalysisService, 202)
  실패 문서를 soft delete 하고 출처(resume/web/repository/coverLetter)에 맞는
  AnalysisRequestService.request*Analysis 로 새 문서를 만들어 재발행. 실패 행을
  남기면 목록에 실패 카드와 진행 카드가 나란히 보인다.
- 프론트는 errorCode 로 고른 한국어 문구만 표시(analysisError.ts). 원문은 서버
  로그용으로만. 재시도해도 같은 실패(빈 PDF·차단 URL 등)엔 버튼을 내지 않는다.
- LLM_PRO_TIMEOUT_SEC 30 → 60. 운영 성공 호출이 synthesis p90 31.9초, 최대 56초
  (=타임아웃 30초 + 내부 재시도 26초)로 찍힌다. 즉 Pro 는 이미 30초에 상습적으로
  걸려 재시도로 겨우 성공하고 있었고(같은 호출을 두 번 물어 비용도 두 배),
  문서 분석은 3회 모두 30초를 넘겨 영구 실패했다.
- DEEPGRAM_TIMEOUT_SEC 30 → 20. STT 성공은 p50 3.8초/p90 8.1초/최대 12.1초라
  20초면 1.65배 여유. 멎은 호출을 붙잡는 시간이 곧 재시도 전 낭비 대기다.
- VOICE_RETRANSCRIBE_NOT_ALLOWED 가 쓰던 deprecated UNPROCESSABLE_ENTITY 를
  UNPROCESSABLE_CONTENT 로 정리(직전 PR 에서 들어간 것).
@i3months
i3months merged commit 1b9d1cf into dev Sep 24, 2026
5 checks passed
i3months added a commit to i3months/stackup that referenced this pull request Oct 6, 2026
PR Team-StackUp#246 에서 LLM_PRO_TIMEOUT_SEC 를 30→60 으로 올렸는데 운영에 도달하지 못했다.
docker-compose.yml 의 `${LLM_PRO_TIMEOUT_SEC:-30.0}` 과 운영 .env 의 30.0 이
코드 기본값을 덮고 있었다. 그래서 웹 이력서 재분석이 92초(=30초×3) 만에 또
타임아웃으로 죽었다 — 60초를 실제로 적용하니 37.4초에 성공했다. 30초로는
구조적으로 불가능한 작업이었고, 그걸 고친 PR 이 조용히 무효가 되어 있었다.

기본값이 세 군데(settings.py / compose / .env)에 흩어져 있고 코드가 제일 약한
구조가 원인이다. ai 서비스 32개 키 중 24개가 compose 에 기본값을 중복으로
갖고 있었다.

- docker-compose.yml: ai 환경변수를 리스트 형태로 바꾸고, 튜닝 값은 이름만 적어
  **전달만** 한다(`- LLM_PRO_TIMEOUT_SEC`). .env 에 있으면 그 값이 가고 없으면
  아무것도 안 가서 코드 기본값이 산다. 미설정이 진짜 미설정으로 전달되는 것은
  서버에서 `docker compose run` 으로 실측 확인했다(빈 문자열이 아님).
  값을 유지한 것은 컨테이너 토폴로지·이름 변환·로컬 기동용 기본값뿐이다.
- .env.example: 코드 기본값과 같은 17개 키를 주석 처리. deploy-app.yml 이
  `KEY=값` 줄을 운영 .env 에 영구 복사하므로, 여기 적는 순간 코드 기본값이
  영원히 가려진다.
- .env.example 에 EMBEDDING_PROVIDER=gemini 추가. 코드 기본값이 mock 이라
  신규 배포가 **가짜 임베딩으로 조용히 뜨는** 지뢰였다(현재 운영은 누군가
  .env 에 수동으로 넣어둔 덕에 정상).
- ai/tests/test_compose_defaults.py: 중복 기본값·빈 문자열 fallback·오타 키를
  CI 에서 차단. 세 가지 회귀를 실제로 잡는지 확인했다.
- docs/environment.md §0 에 우선순위와 예외 규칙 명시.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant