Skip to content

fix(backend): STT 콜백이 유실되면 면접이 영구히 멈추던 문제 - #188

Merged
i3months merged 1 commit into
devfrom
fix/stale-transcription-recovery
Aug 20, 2026
Merged

fix(backend): STT 콜백이 유실되면 면접이 영구히 멈추던 문제#188
i3months merged 1 commit into
devfrom
fix/stale-transcription-recovery

Conversation

@i3months

Copy link
Copy Markdown
Member

음성 인식 점검에서 나온 가장 큰 건

음성 답변은 (transcribing) placeholder 로 먼저 저장되고 callback.voice 가 도착해야 내용이 채워진다. 그 콜백이 오지 않으면 복구 경로가 없다 — AI 크래시, DLQ 격리, 브로커 단절 중 무엇이든.

프론트 턴 판정(domain/session/lib/turn.ts)에서 이 메시지는:

  • FAILED 가 아니므로 "텍스트로 다시 답변" 분기에 걸리지 않고
  • 질문도 아니므로 askable 도 아니다

WAITING_FOR_QUESTION 으로 고정되어 답변 차례가 영영 오지 않는다. 사용자는 "답변을 받아 적고 있어요…" 화면에 갇히고, 세션 시간 초과로 면접이 통째로 끝날 때까지 아무것도 못 한다.

질문 생성 쪽은 #144 로 실패 신호(callback.questions status=FAILED)를 만들어 같은 문제를 없앴다. 음성 경로만 남아 있었다.

수정

  • StaleTranscriptionSweeper — 기본 2분 주기로 interview.voice.stale-transcription-minutes(기본 5분)를 넘긴 placeholder 를 찾는다. 진행 중(IN_PROGRESS)·미삭제 세션의 것만 본다
  • VoiceTranscriptionRecoveryService.failStaleTranscription — FAILED 로 확정하고 세션·유저 채널 양쪽에 SSE 통지(STT_CALLBACK_TIMEOUT). 메시지마다 독립 트랜잭션이라 하나가 실패해도 나머지는 정리된다
  • 확정 직전 상태를 다시 확인한다 — 스위퍼가 목록을 만든 뒤 콜백이 도착했을 수 있다. 완료된 답변을 실패로 되돌리는 건 더 나쁘다

기존 SessionTimeoutSweeper / SessionTimeoutService 와 같은 구조다(스위퍼는 조회만, 상태 변경은 메시지 단위 트랜잭션 서비스).

새 UX 를 만들지 않았다

FAILED 로 두면 이미 검증된 경로에 합류한다:

  • 프론트 currentTurnINTERVIEWEE + FAILED 분기가 답변 차례로 되돌린다
  • 백엔드 InterviewMessageService.resolveAnswerParent 가 같은 질문에 대한 텍스트 재답변을 받는다

둘 다 STT 실패(callback.voice 의 errorCode 경로)를 위해 이미 존재하던 것이다. 유실도 실패의 한 형태로 합류시키는 게 맞다고 봤다.

테스트

VoiceTranscriptionRecoveryServiceTest (3) — FAILED 확정 + 양 채널 통지, 그 사이 전사가 도착했으면 건드리지 않음, 메시지 부재 시 무동작.

backend test 통과(ArchUnit 포함).

영향 범위

  • DB 마이그레이션: 없음 / API contract: 없음
  • 새 설정 2개(둘 다 기본값 있음): interview.voice.stale-transcription-minutes(5), interview.voice.sweep-interval-ms(120000)
  • @EnableScheduling 은 이미 켜져 있다(SchedulingConfig)

리뷰어 체크포인트

  • 5분이 적절한지. 배치 STT 는 보통 1분 안에 끝나지만, 느린 성공을 실패로 만들지 않으려 보수적으로 잡았다. 짧게 하면 회복은 빨라지고 오탐 위험은 커진다
  • 사용자는 그 5분 동안 "받아 적고 있어요" 를 본다. 30초쯤 지나면 "평소보다 오래 걸리고 있어요" 같은 안내를 덧붙이는 것도 방법이다(이번 범위에는 넣지 않았다)

같은 점검의 다른 발견

  • RT3(실시간 스트리밍 음성)가 프론트에 미연결 — RealTime WSAudioHandler, AI /internal/voice/stream(Deepgram Live), Core POST .../voice/stream-begin 이 모두 구현·문서화("활성")돼 있는데 프론트는 배치 업로드만 쓴다. 스트리밍을 쓰면 실시간 자막 + 대기 시간 대폭 감소. 기능 규모라 별도 판단 필요
  • 모바일 "데스크탑 권장" 배너 미구현docs/ui-patterns.md §9 에 규약만 있다

음성 답변은 `(transcribing)` placeholder 로 먼저 저장되고 `callback.voice` 가 도착해야
내용이 채워진다. 그 콜백이 오지 않으면(AI 크래시·DLQ 격리·브로커 단절) 메시지는 그 상태로
남는다. 프론트 턴 판정(`currentTurn`)에서 이 메시지는 FAILED 도 아니고 질문도 아니라
**답변 차례가 영영 오지 않는다** — 세션 시간 초과로 면접이 통째로 끝날 때까지 사용자는
"답변을 받아 적고 있어요…" 화면에 갇힌다.

질문 생성 쪽은 #144 로 실패 신호(callback status=FAILED)를 만들어 같은 문제를 없앴는데
음성 경로에는 대응이 없었다.

- `StaleTranscriptionSweeper` — 기본 2분 주기로 `interview.voice.stale-transcription-minutes`
  (기본 5분)를 넘긴 placeholder 를 찾는다. 진행 중·미삭제 세션의 것만 본다
- `VoiceTranscriptionRecoveryService.failStaleTranscription` — FAILED 로 확정하고
  세션·유저 채널에 SSE 통지(`STT_CALLBACK_TIMEOUT`). 메시지마다 독립 트랜잭션이라
  하나가 실패해도 나머지는 정리된다
- 확정 직전 상태를 다시 확인한다 — 스위퍼가 목록을 만든 뒤 콜백이 도착했으면 건드리지
  않는다(완료된 답변을 실패로 되돌리는 것이 더 나쁘다)

FAILED 로 두면 기존 STT 실패 경로를 그대로 탄다: 프론트 `currentTurn` 의 INTERVIEWEE+FAILED
분기가 답변 차례로 돌리고, 백엔드 `resolveAnswerParent` 가 같은 질문에 대한 텍스트 재답변을
받는다. 새 UX 를 만들지 않고 이미 검증된 경로에 합류시켰다.

테스트 `VoiceTranscriptionRecoveryServiceTest` (3) — FAILED 확정 + 양 채널 통지,
그 사이 전사가 도착한 경우 건드리지 않음, 메시지 부재 시 무동작.
@i3months
i3months merged commit 5152027 into dev Aug 20, 2026
5 checks passed
@i3months
i3months deleted the fix/stale-transcription-recovery branch August 20, 2026 05:30
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