가입하면 프리미엄 플랜 할인할인 받기

블로그/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한 값이에요. 바이트 하나라도 믿기 전에 상수 시간 비교로 검증하고, 몇 분 범위를 벗어난 타임스탬프는 거부해서 재전송 공격을 막으세요.

장애를 막아 주는 핸들러 규칙은 세 가지예요.

  1. 2xx로 빨리 응답하고, 작업은 나중에. 확인하고, 큐에 넣고, 반환하세요. 썸네일을 인라인으로 렌더하는 핸들러는 결국 타임아웃이 나고, 전달이 실패한 것처럼 보이게 만들어요.
  2. 전달은 최소 한 번(at-least-once)으로 간주하세요. 처리를 작업 id 기준으로 묶고 멱등하게 만드세요.
  3. 폴링을 바닥 안전망으로 남겨 두세요. 데이터베이스상 아직 처리 중인 작업을 훑는 스윕이 웹훅이 놓친 것을 잡아 줘요. 그리고 클립이 저장되지 않고 인라인으로 돌아온 경우 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에 있어요.

태그video-apiqueueswebhooksproductionscaling

이 글에 나온 모델로 직접 만들어 보세요 — 무료 크레딧 50개로 시작하거나, 모든 엔진과 가격도 살펴보세요.