Dev Blog로 돌아가기 Dev Log

우선순위 큐 구축: Gemini API 혼란을 멈춘 방법 — 그리고 왜 처음 두 설계 모두 실패했는가

2026. 02. 10 VORA Team 13분 읽기

VORA의 Gemini API 통합을 위한 커밋 이력은 올바르게 보였지만 그렇지 않았던 접근방식의 무덤입니다. 당신은 커밋 메시지만으로도 호를 읽을 수 있습니다: "텍스트 교정 통합", "우선순위 큐 안정화", "병렬과 큐 간 충돌 해결", "안정적인 우선순위 큐 버전." 각각은 시스템의 전체 재설계를 나타냅니다. 버전 4 (오늘 배포되는)까지, 우리는 올바르게 할 만큼 충분히 배웠습니다. 여기는 무엇이 깨졌고 왜인지의 상세한 설명입니다.

경합 조건을 만든 병렬 아키텍처

원래 Gemini API 통합은 병렬 아키텍처를 사용했습니다. Gemini API 접근이 필요한 3가지 작업 유형이 있었습니다: CORRECTION (실시간 음성-텍스트 오류 수정), QA (회의에 대한 사용자 질문에 답변), 그리고 SUMMARY (주기적으로 논의 사항 요약). 첫 번째 구현은 3개 모두를 동시에 시작했습니다 — 각 유형은 자신의 fetch 호출, 자신의 속도 제한 추적, 그리고 독립적으로 실행되었습니다.

이것은 재앙이었습니다. 일반적인 10분 세션의 이벤트 시퀀스:

  1. 사용자가 녹음을 시작합니다. Web Speech API가 2-3초마다 결과를 반환합니다.
  2. 각 결과가 CORRECTION 요청을 트리거합니다. 3개의 CORRECTION 요청이 10초 내에 도착합니다.
  3. 사용자가 질문합니다. QA 요청이 즉시 시작됩니다.
  4. 20초 타이머가 시작됩니다. SUMMARY 요청이 시작됩니다.
  5. Gemini API가 속도 제한 윈도우 내의 4번째 요청에서 429를 반환합니다.
  6. 3개의 동시 재시도 루프가 모두 독립적으로 백오프를 시작합니다. 그들은 모두 약간 다른 시간에 재시도하고 또 다른 버스트를 만듭니다.
  7. 백오프 간격은 이제 일관성이 없습니다 — CORRECTION은 2초 후에 재시도할 수 있다고 생각하고, QA는 5초, SUMMARY는 3초. 또 다른 버스트. 또 다른 429.

2분 내에, 시스템은 완전한 비활성 후 몇 분 동안만 정리되는 429 루프에 갇혔습니다. CORRECTION 출력이 고정되었습니다. 질문 답변이 3-4분 지연되었습니다. 회의 요약이 영구적으로 대기 중이었습니다.

// 깨진 병렬 설계 (v1):
// 3개의 독립적인 fetch 호출, 조정 없음
async correctText(text) {
    const res = await fetch(GEMINI_URL, { body: correctionPayload });
    // 속도 제한이 QA와 SUMMARY와 별도로 추적됨
}
async generateAnswer(question) {
    const res = await fetch(GEMINI_URL, { body: qaPayload });
    // CORRECTION 요청을 완전히 인식하지 못함
}
async generateSummary() {
    const res = await fetch(GEMINI_URL, { body: summaryPayload });
    // 역시 인식하지 못함
}

Mutex 접근: 더 나음, 하지만 여전히 잘못됨

버전 2는 글로벌 세마포를 도입했습니다 — 각 요청이 진행하기 전에 확인할 JavaScript 부울 플래그 isApiLocked. 잠겨 있으면, 요청이 대기합니다. 이것은 동시 요청을 방지했고 버스트 문제를 제거했습니다. 하지만 새로운 실패 모드를 도입했습니다: 우선순위 역전.

이 시나리오를 고려하세요: CORRECTION 작업이 실행을 시작합니다 (잠금을 획득). 그것이 실행 중일 동안 (Gemini 응답 시간: ~1-2초), 사용자가 긴급 질문을 합니다. QA는 좋은 UX를 위해 즉시 응답해야 합니다. 하지만 QA는 CORRECTION 잠금 뒤에 대기합니다. 그러면 SUMMARY가 시작되고 QA 뒤에 대기합니다. 그러면 더 많은 CORRECTION 작업이 도착합니다.

사용자가 질문하고 답변을 기다리는데 3개의 음성 교정 작업과 요약이 모두 그 앞에 대기하고 있습니다. 사용자의 관점에서, AI 어시스턴트는 그들의 질문을 8초 동안 무시했습니다. 끔찍한 경험입니다.

"텍스트 교정을 통합하고 우선순위 큐를 안정화합니다" 커밋이 이것을 고치려고 했습니다. 429 문제를 도왔지만 우선순위 역전을 더 나빠지게 했습니다. 이제 모든 것이 직렬화되었기 때문입니다.

버전 3: 우리가 며칠 동안 잡지 못한 버그가 있는 우선순위 큐

올바른 답변은 명백히 우선순위 큐였습니다: 더 높은 긴급성의 작업은 더 낮은 우선순위 작업을 선점해야 합니다. 우리는 3개의 우선순위를 정의했습니다: QA (0, 가장 높음), CORRECTION (1), SUMMARY (2, 가장 낮음). 적절한 힙 기반 우선순위 큐는 QA가 항상 CORRECTION 이전에 처리되도록, CORRECTION이 SUMMARY 이전에 처리되도록 보장할 것입니다.

우리는 그것을 구현했습니다. 행복한 경로 테스트에서 작동했습니다. 그러면 우리는 스트레스 테스트 중에 미묘한 버그를 잡았습니다: 큐가 삽입 시 정렬되어 올바른 것처럼 보였습니다. 하지만 새로운 높은 우선순위 작업이 큐 프로세서가 낮은 우선순위 작업을 실행 중일 때 도착하면, 그것은 정렬된 큐에 참여했습니다 — 하지만 현재 작업은 선점되지 않았습니다. 그래서 QA 작업은 여전히 작업 도착 순간에 이미 진행 중이던 CORRECTION을 기다릴 수 있었습니다.

더 문제가 있는 것: SUMMARY 작업이 굶었습니다. 지속적인 음성 (많은 CORRECTION 작업)과 자주 질문 (QA 작업)이 있으면, SUMMARY은 절대 큐의 앞에 도달하지 못했습니다. 회의는 SUMMARY이 처리되기 전에 CORRECTION과 QA 작업이 도착하는 것보다 빠르게 20분을 실행할 것입니다.

해결책: 유형당 별도 Throttle + 단일 큐

최종 설계, 오늘 GeminiAPI v4.6이 구현하는 것, 큐를 유형당 throttle 간격과 글로벌 최소 갭과 결합합니다. 여기가 실제로 작동하는 아키텍처입니다:

// GeminiAPI v4.6 - 붙은 설계
this.minIntervals = {
    CORRECTION: 5000,  // 발화당 최대 5초마다 한 번씩 교정
    QA: 500,           // 사용자 질문을 위해 거의 즉시
    SUMMARY: 10000,    // Summary API 호출 속도 제한
    GLOBAL: 1500       // 절대 1.5초 이내에 2개의 요청을 시작하지 않음
};

// 우선순위: QA=0 > CORRECTION=1 > SUMMARY=2
// 단일 큐, 우선순위로 정렬, 그 다음 타임스탬프
// 각 작업은 실행하기 전에 글로벌 AND 유형별 쿨다운 확인

유형별 throttle은 SUMMARY 굶주림 문제를 다르게 해결합니다: SUMMARY는 큐 우선순위 전투에서 이기기 위해 필요하지 않으며, 단지 60초마다 보장된 실행을 필요로 합니다 (summary 타이머). 유형별 최소 간격을 10초 (RPM을 보호하기 위해)로 유지하면, SUMMARY 요청은 큐에 스팸하지 않지만 CORRECTION 작업 사이의 짧은 윈도우에서 결국 그들의 차례를 얻습니다.

1.5초 글로벌 최소 간격은 429 디버깅으로부터의 핵심 통찰이었습니다. CORRECTION과 QA가 기술적으로 자신의 유형 한계 내에 있더라도, 100ms 떨어져 2개의 요청을 시작하면 실제로 Gemini의 자유 계층 RPM 카운터가 짧은 롤링 윈도우에서 측정되기 때문에 속도 제한을 트리거합니다. 글로벌 갭은 버스트 효과가 멈출 때까지 충분한 간격을 만듭니다.

비차단 배경 교정 패턴

UX를 받을 만한 또 다른 부분이 있습니다: AI 교정을 트랜스크립트 표시에서 분리하기. 초기 버전에서, 우리는 Gemini 교정을 기다린 후 텍스트를 트랜스크립트에 추가했습니다. 이것은 사용자가 말하기 시작한 후 자신의 단어가 나타나기까지 1-2초 갭을 봤다는 것을 의미했습니다. 회의 맥락에서, 그 갭은 깊이 불편합니다 — 시스템이 느리거나 깨진 것을 시사합니다.

돌파점은 로컬 교정 (즉시, 로컬 사전 조회에서)을 AI 교정 (비동기, Gemini에서)에서 분리하는 것이었습니다. 흐름이 다음과 같이 되었습니다:

  1. Web Speech API가 텍스트를 반환합니다. 즉시 로컬 사전 교정을 적용합니다 (0ms).
  2. 텍스트를 트랜스크립트에 즉시 표시합니다.
  3. 배경 큐에서 AI 교정을 큐합니다.
  4. Gemini가 응답할 때 (1-3초 나중), 조용히 개선된 텍스트로 트랜스크립트 항목을 업데이트하고, ✨ 배지를 추가합니다.

사용자는 그들의 음성이 즉시 나타나는 것을 본다 (로컬 교정), 그리고 몇 초 후 텍스트는 조용히 개선될 수 있습니다. 이것은 기다리는 것보다 극적으로 더 나은 UX입니다. onCorrected 콜백 패턴과 트랜스크립트 항목의 data-id 속성은 전체 트랜스크립트를 다시 렌더링하지 않고 대상 DOM 업데이트를 가능하게 합니다.

개선 측정

우선순위 큐 이전: 일반적인 30분 세션에서 분당 40-50+ Gemini API 호출, 처음 2분 후 일관된 429 오류. 우선순위 큐 이후: 분당 8-12 API 호출, 최대 2시간 세션에서 0개의 429 오류. 호출 개수는 75% 떨어졌습니다. 우리가 덜 교정하기 때문이 아니라, 중복 버스트 요청과 그들이 만드는 재시도 폭풍을 제거했기 때문입니다.

버전 이력은 자신의 방식대로 이야기를 말합니다. GeminiAPI.js는 그 반복을 통해 "v1.0"에서 "v4.6 (안정적 우선순위 큐 버전)"으로 갔습니다. 각 주요 버전 번호는 요청 관리 아키텍처의 완전한 재설계였습니다. .6 패치 버전은 실제 세션 데이터에 기반한 특정 간격 값을 조정했습니다. "안정적"이 그것의 이름에 있을 만큼 안정적인 것은 처음 많은 것을 깨뜨리는 결과입니다.