블로그/developers·2026년 9월 3일·5분·eroq 팀 작성
프로덕션에서 AI 동영상 생성 확장하기: 큐와 재시도
대량 AI 동영상 운영법: 비동기 작업, 자체 내구성 큐, 레이트 리밋에 맞춘 동시성, 실패 시 환불, 여러 개의 키, 서명된 웹훅.
클립 하나는 데모예요. 일주일에 클립 천 개는 운영 문제이고, 고장 나는 부분은 절대 모델 카드에 적힌 곳이 아니에요. 렌더 도중 재시작된 프로세스, 실패한 작업에 요금이 청구된 고객, 라이브 제품에 필요했던 레이트 리밋을 다 잡아먹은 배치 작업 같은 것들이죠.
물량을 버텨 내는 동영상 파이프라인은 이런 모습이에요. 구조는 /docs/video에 문서화된 eroq의 것이지만, 그 논리는 어떤 비동기 미디어 API에도 그대로 옮겨 쓸 수 있어요.
기다리지 말고 제출하세요
동영상 렌더는 1~5분이 걸려요. HTTP 요청을 그렇게 오래 열어 둘 수는 없으니, 엔드포인트는 비동기예요. 호출하면 크레딧이 차감되고, 렌더가 시작되고, 즉시 202로 응답해요.
{
"id": "b7e6c2d4-…",
"object": "video.generation",
"status": "processing",
"created": 1756118400,
"model": "eroq-motion-one",
"duration": "5s",
"poll": "/v1/videos/generations/b7e6c2d4-…",
"usage": { "credits_spent": 100, "credits_remaining": 887 }
}
다른 무엇보다 먼저 그 id를 저장하세요. 호출한 쪽에 응답을 돌려주기 전에, 로그를 남기기 전에요. 작업 id는 이미 비용을 치른 렌더를 붙잡을 수 있는 유일한 손잡이이고, 아래 내용은 모두 그 id가 언제 재시작될지 모르는 머신의 변수가 아니라 데이터베이스에 들어 있다고 가정해요.
나만의 큐
큐는 꼭 필요하고, 그게 API의 큐여서는 안 돼요.
사용자 수요는 몰렸다 빠졌다 하지만, 렌더 용량은 그렇지 않아요. 버퍼가 없으면 나쁜 선택지 두 개만 남아요. 피크 때 작업을 거절하거나, 레이트 리미터가 거부할 때까지 병렬 요청을 퍼붓거나요. 큐가 있으면 둘 다 스케줄링 문제로 바뀌어요.
최소한의 테이블에는 중요한 컬럼 다섯 개가 있어요.
- 제출 응답의 작업 id, 그리고 마지막으로 확인한 상태
- 요청 페이로드: 재제출이 정확히 같은 렌더가 되도록
- 시도 횟수: 문제 있는 작업이 끝없이 돌지 않고 세 번 만에 멈추도록
- 작업이 속한 고객이나 캠페인: 비용이 올바른 계정에 잡히도록
- 사용한 크레딧: 제출 시점의
usage.credits_spent에서 복사
그다음 큐를 일정한 속도로 비우는 워커 하나와, 아직 처리 중인 작업마다 GET /v1/videos/generations/{id}를 폴링하는 두 번째 루프를 두세요.
크래시 후 복구용으로는 목록 엔드포인트가 있어요. GET /v1/videos/generations는 워크스페이스가 지난 24시간 동안 제출한 모든 작업을 팀원의 렌더까지 포함해 돌려주고, limit와 status 필터를 지원해요. 인라인 미디어는 절대 싣지 않으니 반복해서 조회해도 부담이 적어요.
# what is still in flight right now
curl "https://eroq.ai/v1/videos/generations?status=processing&limit=50" \
-H "Authorization: Bearer $EROQ_API_KEY"
데이터베이스와 이 목록이 다르면, 목록이 맞아요.
레이트 리밋에 맞춘 동시성 설정
서로 다른 숫자가 두 개 있고, 둘을 헷갈리는 게 흔한 실수예요.
제출 속도는 API가 제한해요. 키당 분당 동영상 요청 6개이고, 플랜에 따라 배수가 붙어요(Creator 2×, Studio 4×, Team 6×, Agency 8×). 워커에서 토큰 버킷으로 모델링하세요.
동시 진행 중인 렌더는 인내심과 지갑 말고는 제한하는 게 없어요. 그래도 명시적으로 상한을 두세요. 동시 진행 상한이 있어야 폭주하는 재시도 루프가 오후 한나절 만에 한 달 치 크레딧을 써 버리는 걸 막을 수 있고, 알림을 걸 기준 숫자도 생겨요.
버킷은 실제 한도보다 살짝 낮게 잡고, 차이는 큐가 흡수하게 하세요. 429가 거의 나지 않는다면 429 처리는 그리 중요하지 않아요.
재시도, 환불, 그리고 재시도하면 안 되는 것
과금 구조 덕분에 재시도는 안전해요. 크레딧은 제출 시점에 차감되고, 실패하거나 10분 안에 끝나지 않은 렌더는 자동으로 환불돼요. 정산은 시도가 아니라 전달된 클립 기준으로 하면 돼요.
보장이 두 가지 더 있어요. 콘텐츠 정책에 막힌 요청은 content_blocked를 반환하고 절대 과금되지 않아요. 그리고 게이트 검사는 차감 전에 이뤄져요. 플랜에 포함되지 않은 모델은 403 plan_required, 현재 제공할 수 없는 엔진은 503 model_unavailable로 응답하는데, 둘 다 크레딧이 움직이기 전이고, 어느 쪽도 다른 엔진으로 슬그머니 대체되지 않아요. 요청한 모델을 받거나 오류를 받거나 둘 중 하나예요. 출력을 로그와 일치시켜 주는 건 이 동작뿐이에요.
그러니 재시도 정책은 저절로 정해져요.
- 백오프를 두고 재시도:
429, 그리고 응답을 아예 받지 못한 네트워크 수준의 실패. - 한 번 재시도한 뒤 엔진 전환:
503 model_unavailable. /models의 로스터에서 직접 고른 대체 엔진 id를 설정에 넣어 두세요. - 재시도하지 말고 드러내기:
402 insufficient_credits,403 plan_required,403 role_forbidden, 그리고 모든content_blocked거부. 이 중 어느 것도 재시도 간격 안에 저절로 바뀌지 않아요. - 실패한 렌더를 자동으로 두 번 넘게 재시도하지 마세요. 두 번 실패한 프롬프트는 대개 세 번째도 실패해요. 환불이 되니 비용은 지연 시간인데, 고객이 지켜보는 게 바로 그 지연이에요.
재제출은 옛 작업의 부활이 아니라 새 id를 가진 새 작업이에요. 테이블에서 두 id를 연결해 두지 않으면 고객별 비용 집계가 어긋나요.
키 하나가 아니라 키 여러 개
레이트 리밋은 키 단위로 계산되니, 키는 장애의 영향 범위를 나누는 자연스러운 경계예요. 플랜에는 바로 이 용도로 여러 개의 키가 포함돼 있어요. Hobby 10개, Creator 20개, Studio 50개, Team 100개, Agency 200개예요.
합리적인 분배는 이래요.
- 환경마다 키 하나(프로덕션, 스테이징, 로컬)
- 프로덕션에서는 파이프라인마다 키 하나: 사용자가 기다리는 인터랙티브 경로, 야간 배치 경로, 내부 도구 경로
- 그리고 생성 기능을 재판매한다면 큰 고객마다 키 하나: 그래야 요청 로그에서 그 고객의 사용량이 한눈에 보여요
키는 그 키를 가진 멤버의 워크스페이스 역할도 그대로 물려받으니, 퇴사 처리는 키 감사가 아니라 역할 변경 한 번으로 끝나요. 멤버 탭은 /dashboard/workspace에 있고, 고객별 비용 계산은 AI API의 크레딧 가격 책정에 정리돼 있어요.
폴링 대신 웹훅
폴링은 시간당 렌더 10개일 때는 괜찮지만 천 개일 때는 낭비예요. 엔드포인트를 등록하고 대신 video.generation.succeeded와 video.generation.failed를 받으세요.
페이로드는 서명된 작은 봉투예요. 로드 밸런서를 지나가는 수 메가바이트짜리 base64 같은 건 없어요.
{
"id": "evt_9f21…",
"type": "video.generation.succeeded",
"created": 1756118400,
"data": {
"id": "b7e6c2d4-…",
"model": "eroq-motion-one",
"duration": "5s",
"content_type": "video/mp4",
"result_url": "https://store.eroq.ai/…/clip.mp4"
}
}
전달되는 요청에는 t=<unix>,v1=<hex> 형태의 Stripe 방식 eroq-signature 헤더가 붙는데, 여기서 hex는 생성할 때 딱 한 번 보여 주는 엔드포인트 시크릿으로 timestamp.body를 HMAC-SHA256한 값이에요. 바이트 하나라도 믿기 전에 상수 시간 비교로 검증하고, 몇 분 범위를 벗어난 타임스탬프는 거부해서 재전송 공격을 막으세요.
장애를 막아 주는 핸들러 규칙은 세 가지예요.
- 2xx로 빨리 응답하고, 작업은 나중에. 확인하고, 큐에 넣고, 반환하세요. 썸네일을 인라인으로 렌더하는 핸들러는 결국 타임아웃이 나고, 전달이 실패한 것처럼 보이게 만들어요.
- 전달은 최소 한 번(at-least-once)으로 간주하세요. 처리를 작업 id 기준으로 묶고 멱등하게 만드세요.
- 폴링을 바닥 안전망으로 남겨 두세요. 데이터베이스상 아직 처리 중인 작업을 훑는 스윕이 웹훅이 놓친 것을 잡아 줘요. 그리고 클립이 저장되지 않고 인라인으로 돌아온 경우
result_url이null이니, 그 분기도 처리하세요.
배치: 시퀀스를 리소스로 다루세요
단발성 렌더가 아니라 시퀀스를 렌더한다면, 제출 열두 번을 직접 오케스트레이션하지 말고 시퀀스를 서버 쪽에서 모델링하세요. eroq의 films 리소스는 스토리보드를 저장하고, POST /v1/films/{id}/render는 호출 한 번에 모든 씬을 촬영해요. 씬은 개별로 과금되고, 큐가 순차적이라 지갑이 비면 실행이 깔끔하게 멈추며, 응답에는 씬마다 작업 또는 오류가 보고돼요. 자세한 내용은 /docs/films에 있어요.
비싼 엔진에 앞서 저렴한 엔진으로 파이프라인을 테스트하세요. Seedance 1.0 Lite는 5초에 120 크레딧이라 엔드 투 엔드 리허설을 부담 없이 돌릴 수 있고, 큐는 그 차이를 알아채지 못해요. 플랜별 제한은 /pricing에 있어요.
자주 묻는 질문
실패한 AI 동영상 렌더도 비용을 내야 하나요?
아니요. 크레딧은 제출 시점에 차감되고, 실패하거나 10분 안에 끝나지 않은 렌더는 자동으로 환불돼요. 콘텐츠 정책에 막힌 요청은 content_blocked를 반환하고 절대 과금되지 않아요.
서버가 재시작된 뒤 동영상 작업은 어떻게 복구하나요?
GET /v1/videos/generations를 호출하세요. 워크스페이스가 지난 24시간 동안 제출한 모든 작업을 상태와 함께 보여 줘요. 내 테이블과 대조하고, API의 목록을 기준으로 삼으세요.
동영상 생성에는 폴링과 웹훅 중 무엇을 써야 하나요?
개발 중이거나 물량이 적을 때는 폴링을 쓰고, 렌더가 쉴 새 없이 돌아가면 video.generation.succeeded와 video.generation.failed 웹훅으로 옮기세요. 어느 쪽이든 느린 폴링 스윕은 안전망으로 남겨 두세요.
큐를 먼저 만들고, 그다음 파이프라인을 만드세요. 엔드포인트 레퍼런스는 /docs/video에 있어요.
이 글에 나온 모델로 직접 만들어 보세요 — 무료 크레딧 50개로 시작하거나, 모든 엔진과 가격도 살펴보세요.