재시도 횟수는 계층을 지나며 곱해진다
차시 콘텐츠 생성이 실패했을 때 작업 서비스가 모델 호출을 다시 하고, SDK도 그 안에서 통신을 재시도하면 실제 요청 수는 바깥 반복 횟수보다 많다. 각 계층에서 “몇 번 더”를 정해도 차시 하나의 총 호출 수와 대기 시간은 바로 드러나지 않는다.
슬라이드 개요 생성의 내부 3회와 BullMQ 2회가 곱해 최대 6회가 되는 경로를 확인하고, 내부 시도를 2회로 낮춰 최대 4회로 제한했다. 아래의 SDK·형식 오류 분리와 차시당 최대 6회·시간 예산은 다른 차시 콘텐츠 경로에 적용한 정책이다.
AI Commons의 차시 생성 변경은 차시 콘텐츠와 본문 생성에 공통 실행기 runCourseLlmUnit을 도입했다. 기존의 차시별 반복 호출 대신 통신 재시도는 SDK에 맡기고, 작업 서비스의 추가 호출은 응답 형식 오류에 한 번만 허용한다.
SDK maxRetries = 2
한 SDK 호출의 실제 요청 상한 = 최초 1회 + 재시도 2회 = 3회
형식 오류의 추가 호출 = 1회 → SDK 호출은 최대 2회
차시 하나의 실제 요청 상한 = 3 × 2 = 6회 6회는 해당 실행 경로의 상한이다. 언제나 여섯 번 보내는 것은 아니며, 기한이나 작업 예산이 먼저 끝나면 더 적게 호출한다. 전체 과목의 차시 수와 별도의 큐 재실행까지 합친 전역 상한도 아니다.
통신 오류를 소진한 뒤 서비스가 다시 시작하지 않는다
SDK가 통신 오류를 재시도한 뒤 실패했다면 서비스가 같은 일을 다시 시작하지 않는다. JSON 파싱, 스키마 검증, 응답 미완료·빈 파싱 결과 같은 형식 오류에만 두 번째 SDK 호출을 허용한다. 모델이 새 응답을 만들면 해결될 가능성과, 이미 소진한 통신 정책을 반복하는 상황을 나눈 것이다.
| 종료 원인 | 서비스의 새 모델 호출 | 처리 범위 |
|---|---|---|
| 응답 형식 오류 | 한 번만 허용 | 두 번째도 실패하면 해당 차시 실패 |
| 영구 오류 · 일시 장애 소진 · 호출 기한 초과 | 없음 | 해당 차시 실패 |
| 작업 시간 예산 소진 | 시작하지 않음 | 남은 차시는 호출 없이 실패로 집계 |
| 429 · 회로 차단기 열림 | 없음 | 작업 종료 플래그를 세우고 중단 |
| 결과 저장 실패 | 없음 | save-failed로 해당 차시 실패 |
designer가 예외를 감싸면 최상위 예외만 검사해서는 429나 열린 회로 차단기를 놓친다. 분류기는 cause 사슬을 따라 원본 상태·오류 코드를 보고 우선순위를 적용한다. 순환 참조를 피하고 깊이는 20으로 제한한다. 대상 삭제·취소 오류는 사슬 속 원본을 찾아 다시 던져, 바깥 종료 처리도 그 의미를 읽게 한다.
모든 형식 오류를 즉시 실패시키면 호출 수는 더 줄지만 일시적인 출력 불일치를 회복할 기회도 사라진다. 한 번의 형식 재호출은 그 기회를 허용하면서 호출 상한을 계산할 수 있게 한 절충이다.
시도별 시간 제한을 더해 전체 시간으로 보지 않는다
호출 기한은 실행 허가를 얻은 뒤 SDK 재시도를 포함한 한 호출 전체에 적용한다. 기본값은 10분이며 별도 설정으로 조정한다. AbortController 타이머가 전체 호출을 제한하므로 SDK 시도별 timeout과 대기 시간을 단순히 더한 값이 이 경로의 기한은 아니다.
작업에는 더 큰 실행 시간 상한이 있다. 서비스는 시작 시각에 실행 상한 − 호출 기한으로 새 호출을 시작할 수 있는 예산을 정한다. 상한 60분과 호출 기한 10분인 테스트 조건에서는 시작 후 50분부터 새 차시를 호출하지 않는다. 다음은 계산을 줄인 예시다.
budgetEndsAt = startedAt + runtimeLimitMs - deadlineMs;
if (Date.now() >= budgetEndsAt) return { ok: false, reason: "budget-exhausted" };
// 클라이언트도 permit 획득 직후 notAfterMs를 다시 확인한다. 서비스에서 허가됐어도 실행 허가를 기다리는 동안 예산이 끝날 수 있다. 그래서 절대 시각인 notAfterMs를 클라이언트까지 전달하고, permit을 얻은 직후 다시 검사한다. 형식 재호출 전에도 예산과 종료 플래그를 다시 확인한다. 이 여유는 새 외부 호출을 늦게 시작하지 않는 정책이며, 저장과 후처리까지 항상 실행 상한 안에 끝난다는 증명은 아니다.
좋은 응답을 저장하지 못했다고 새 응답을 만들지 않는다
실행기는 call과 save를 분리한다. 모델 호출이 성공하면 종료 플래그를 확인한 뒤 저장한다. 저장 오류는 save-failed로 반환하며 호출 루프로 돌아가지 않는다. DB 오류를 해결하기 위해 유료 모델이 다른 응답을 새로 만들 이유는 없기 때문이다.
여러 차시가 병렬로 실행될 때 한 차시의 429나 회로 차단기 오류는 해당 실행의 종료 플래그를 세운다. 다른 차시가 나중에 응답을 받아도 저장 전 플래그 검사에 걸리면 버린다. 저장 콜백은 대상의 생존도 새로 확인한다. 그러나 이미 시작한 모든 외부 작업을 되돌리는 처리도, 종료 플래그 확인 뒤 진행 중인 저장까지 취소하는 원자적 장치도 아니다. 늦은 완료의 저장 조건과 함께 다뤄야 하는 경계다.
요청 수와 차시 결과를 따로 검증한다
실행기 테스트는 모킹한 호출의 onSettled 정보로 실제 요청 수와 시간을 합산하고, 형식 오류 뒤 한 번의 재호출, 두 번의 형식 실패, 저장 실패 시 추가 호출 없음 등을 확인한다. cause 안의 429·차단기·기한·영구 오류, 분류 우선순위와 순환 참조도 다룬다. 호출 뒤 종료됐으면 저장하지 않고, 형식 재호출 전에 예산이 끝났으면 다시 호출하지 않는 조건을 검사한다.
서비스 테스트는 가상 시각으로 60분·10분 조건의 시작 예산을 넘기고 남은 차시가 요청 0회로 집계되는지 확인한다. 실행 허가 대기 중 예산 소진도 별도 오류로 주입한다. 이는 실제 공급자의 여섯 요청이나 비용을 측정한 실험이 아니다. 구조의 상한과 요청 카운터의 연결을 검증하는 범위다.
차시별 unit_settled 기록에는 요청 수·호출 시간·종료 원인을 남긴다. 시도 횟수 하나로 모델 출력 오류, 통신 소진과 저장 실패를 뭉뚱그리지 않도록 하는 정보다. 재시도 정책의 효과를 평가하려면 이 분류를 따라 실패와 외부 호출을 함께 봐야 한다.