HiAPI
블로그

튜토리얼

Python에서 AI 비디오 생성 API 통합하기: 폴링, Webhook 및 비용 제어

재시도, 타임아웃, 비용 가드레일을 아우르는 실용적인 생성→폴링→Webhook 실습

Nov 3, 2026
深蓝底上铜色细密平行排线,密度由左向右渐变,中央有节点连线图案,为教程文章封面图。

결과 획득 방식 선택

대부분의 API 호출과 달리 비디오 생성은 속도가 매우 느립니다. 몇 초짜리 비디오를 생성하는 요청은 수십 초에서 수 분이 걸릴 수 있으며, 이는 생성 작업을 만들 때의 HTTP 응답이 비디오를 포함하는 응답인 경우가 거의 없다는 뜻입니다. Python 코드 한 줄을 작성하기 전에 결과를 어떻게 가져올지 먼저 결정하세요. 실용적인 방식은 세 가지입니다.

방식 1: 제출 후 폴링

생성 요청을 보내면 즉시 작업 핸들을 받고, 이후 고정 간격으로 상태 엔드포인트를 호출하여 작업이 종료 상태에 도달할 때까지 기다립니다. 구현과 디버깅이 가장 간단한 방식입니다. 일반적인 요청/응답 코드이며, 어떤 방화벽 뒤에서도 작동하고 인바운드 네트워크 요구 사항이 없습니다. 단점은 지연 하한이 있다는 것입니다. 결과의 신선도는 폴링 간격에만 달려 있으며, 폴링 속도가 작업 진행 속도보다 빠르면 호출을 낭비하게 됩니다.

폴링은 대화형 워크로드에 적합합니다. 사용자가 UI에서 기다리고 있고, 작업이 제한된 시간 내에 완료될 것으로 예상되며, 추론 가능한 단일 코드 경로를 원하는 경우입니다.

방식 2: Webhook 콜백

작업을 제출할 때 콜백 URL을 함께 전달하여 제어권을 호출자에게 돌려주고, 플랫폼이 작업 완료 시 결과를 여러분의 엔드포인트로 POST하도록 합니다. 이는 폴링을 완전히 제거하고 거의 즉각적인 알림을 제공합니다. 또한 작업을 제출한 프로세스가 재시작된 후에도 계속 작동합니다. 전달이 외부에서 주도되기 때문입니다.

대가는 운영 측면입니다. 공개적으로 접근 가능한 HTTPS 엔드포인트가 필요하고, 콜백이 실제로 제공자로부터 왔는지 검증해야 하며, 콜백이 영원히 오지 않는 경우도 처리해야 합니다. 네트워크는 장애가 나고, 엔드포인트는 잘못 구성되고, 페이로드는 유실되기 때문입니다.

Webhook은 장시간 실행되거나 배치 워크로드에 적합합니다. 대형 작업 큐, 비동기 파이프라인, 그리고 몇 분 동안 연결을 유지하거나 유휴 루프를 돌리는 것이 낭비인 모든 시나리오입니다.

방식 3: 폴백을 갖춘 하이브리드 방식

대부분의 프로덕션 시스템이 결국 채택하는 방식입니다. Webhook을 주 신호로 사용하되, 느린 주기적 조정을 유지하여 오래된 임계값을 초과해도 여전히 대기 중으로 표시된 작업을 폴링합니다. 콜백이 누락되면 폴러가 이를 잡아냅니다. 폴러가 지연되면 보통 콜백이 먼저 도착하여 작업을 완료로 표시합니다.

코드베이스가 아니라 워크로드에 따라 선택하세요. 대화형 엔드포인트는 폴링할 수 있고, 백그라운드 큐는 콜백을 사용할 수 있으며, 공유 작업 저장소와 종료 상태 로직은 둘 다에 대해 동일해야 합니다. 그래야 획득 전략을 시스템 나머지 부분을 건드리지 않고 변경할 수 있습니다.

클라이언트 설정 및 작업 제출

제출 뼈대는 간결하게 유지해야 합니다. 아래의 모든 내용은 API 키와 기본 URL을 보관하는 단일 위치와, 프롬프트와 매개변수를 작업 핸들로 변환하는 함수 하나가 있다고 가정합니다.

구성 및 키 처리

키를 절대 하드코딩하지 마세요. 환경 변수에서 읽고, 시작 시 누락되면 즉시 크게 실패하도록 하세요. 그래야 첫 고객 요청에서야 문제를 발견하는 일이 없습니다.

import os

BASE_URL = os.environ.get("HIAPI_BASE_URL", "https://api.hiapi.ai")
API_KEY = os.environ["HIAPI_API_KEY"]  # 시작 시 KeyError는 의도된 것

키가 로그에 남지 않게 하세요. 디버깅을 위해 요청을 기록한다면 프롬프트와 매개변수는 기록하되, 인증 헤더는 마스킹하세요.

단일 제출 호출

제출 호출은 한 가지만 해야 합니다. 여러분이 제어하는 매개변수를 검증하고, 요청을 보내고, 작업 식별자를 반환합니다. 그 안에서 폴링을 시작하거나 아무것도 다운로드하지 마세요.

import requests

def submit_video_job(prompt: str, *, idempotency_key: str, **params) -> dict:
    payload = {"prompt": prompt, **params}
    resp = requests.post(
        f"{BASE_URL}/v1/video/generations",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
            "Idempotency-Key": idempotency_key,
        },
        json=payload,
        timeout=(5, 30),  # 연결, 읽기
    )
    resp.raise_for_status()
    return resp.json()

멱등성 키는 중요합니다. 제출 호출이 타임아웃되면 작업이 생성되었는지 실제로 알 수 없습니다. 동일한 키로 재시도하면 중복 작업을 생성하고 비용을 지불하는 대신 안전하게 재전송할 수 있습니다.

즉시 영속화해야 할 것

제출이 반환되는 즉시, 다른 어떤 작업을 하기 전에 자체 데이터베이스에 한 행을 기록하세요.

  • 작업 ID 및 멱등성 키
  • 자체 내부 요청 ID와 그것이 속한 사용자 또는 배치
  • 정확한 요청 매개변수 (재현하거나 감사할 수 있도록)
  • 상태 pending 및 타임스탬프

제출 후 영속화 전에 프로세스가 충돌하면, 작업은 업스트림에 존재하지만 로컬 기록이 없습니다. 먼저 영속화하고 계속 진행하면 데이터베이스가 조정해야 할 내용의 신뢰할 수 있는 원천이 됩니다.

올바른 방식으로 결과 폴링

폴링 루프는 잘못 작성하기 쉽습니다. 순진한 버전—휴면 없는 while True—은 할당량을 소모하고 API를 과도하게 때리며 로그를 읽기 어렵게 만듭니다. 올바른 루프에는 네 가지 속성이 있습니다. 간격, 백오프, 하드 상한, 종료 상태 검사입니다.

루프

import time, random

def poll_job(job_id: str, *, max_attempts: int = 60, base: float = 2.0, cap: float = 20.0):
    for attempt in range(1, max_attempts + 1):
        resp = requests.get(
            f"{BASE_URL}/v1/video/generations/{job_id}",
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=(5, 15),
        )
        resp.raise_for_status()
        body = resp.json()
        status = body["status"]

        if status == "succeeded":
            return body
        if status in {"failed", "cancelled"}:
            raise RuntimeError(f"작업 {job_id}이(가) {status}(으)로 종료됨: {body.get('error')}")

        delay = min(cap, base * (2 ** (attempt - 1)))
        time.sleep(delay + random.uniform(0, 1.0))

    raise TimeoutError(f"작업 {job_id}이(가) {max_attempts}회 시도 후에도 대기 중")

왜 각 부분이 중요한가

백오프. 생성 작업은 단계적으로 진행됩니다. 대기, 실행, 렌더링입니다. 초기 대기 단계에서 매초 폴링하면 동일한 답변을 스무 번 받게 됩니다. 몇 초에서 시작하여 상한까지 두 배씩 늘리면 호출량이 인내심이 아니라 작업 지속 시간에 비례하게 됩니다.

지터. 무작위로 0.몇 초를 추가하면 배포나 재시도 폭풍 후에 실행 중인 모든 클라이언트가 동기화되어 폴링하는 것을 방지합니다.

최대 시도 횟수 및 실제 경과 시간 예산. 시도 횟수 상한이 없는 지수 백오프는 몇 시간 동안 실행될 수 있습니다. 예상 최악의 작업 지속 시간에 따라 상한을 선택하고, 초과하면 조용히 계속 실행되는 루프가 아니라 드러내야 할 실패로 취급하세요. 경과 시간도 추적하세요. 간격 상한이 적용되면 시도 횟수만으로는 오해를 낳습니다.

종료 상태 감지. 성공, 실패, 취소를 명확히 구분하세요. 실패한 작업은 폴러가 재시도해서는 안 됩니다. 제공자의 오류 보고서와 함께 반환하여 다른 매개변수로 재제출할지 결정할 수 있게 해야 합니다.

또 한 가지 규칙이 있습니다. 웹 요청 처리기 내부에서 폴링하지 마세요. 느린 작업이 요청 스레드 풀을 점유하지 않도록 루프를 백그라운드 워커로 옮기세요.

Webhook 콜백으로 결과 수신

Webhook 엔드포인트는 공개적이고 기본적으로 인증되지 않은 HTTP 라우트로, 누구나 POST할 수 있습니다. 그에 맞게 대하세요.

발신자 검증

플랫폼이 콜백에 서명한다면, JSON을 파싱하기 전에 원시 요청 본문에 대해 서명을 검증하세요. 서명 검사는 바이트에 대해 실행되며, 페이로드를 다시 직렬화하면 바이트가 변경될 수 있습니다. 상수 시간 함수로 비교하고, 불일치는 비2xx 상태로 거부하세요. 서명을 사용할 수 없다면 최소한 비밀 경로 세그먼트에 허용 목록이나 공유 헤더 토큰을 더하고, 페이로드 내용을 생성한 작업과 매칭하기 전까지 절대 신뢰하지 마세요.

빠르게 반환하고 나중에 작업

엔드포인트의 유일한 임무는 수신을 확인하는 것입니다. 파싱, 검증, 큐잉, 응답. 느린 모든 것—비디오 다운로드, 잠금 상태의 데이터베이스 호출, 다운스트림 서비스 호출—은 처리기가 아니라 워커에서 발생합니다. 제공자는 빠른 성공 응답을 받지 못한 콜백을 재시도하며, 느린 처리기는 하나의 이벤트를 여러 개로 만듭니다.

from flask import Flask, request, abort

app = Flask(__name__)

@app.post("/callbacks/video")
def video_callback():
    raw = request.get_data()
    if not verify_signature(raw, request.headers):
        abort(401)
    event = request.get_json(force=True)
    job_id = event.get("job_id")
    if not job_id:
        abort(400)
    queue.enqueue("process_video_event", event)  # 즉시 반환
    return {"ok": True}, 200

자체 기록과 대조하여 조정

콜백에 따라 행동하기 전에 자체 테이블에서 작업 ID를 조회하세요. 이 조회는 세 가지 경우를 낳습니다.

  • 알 수 없는 작업 ID: 삭제된 작업의 오래된 이벤트이거나 위조된 요청일 수 있습니다. 기록하고 버리세요. 인바운드 페이로드로부터 레코드를 생성하지 마세요.
  • 작업이 이미 완료로 표시됨: 중복 전달입니다. 확인하고 다른 작업은 하지 마세요. 동일한 작업을 다시 다운로드하고 다시 청구하는 것은 전형적인 Webhook 결함입니다.
  • 작업 대기 중: 완료(또는 실패)로 표시하고, 자산 참조를 저장하고, 다운로드 단계로 넘기세요.

상태 전환을 현재 상태에 조건부로 만드세요. 즉, 작업이 여전히 대기 중일 때만 성공하는 업데이트로 만드세요. 그래야 두 개의 동시 전달이 동시에 승리할 수 없습니다. 앞서 설명한 하이브리드 폴러와 결합하면 콜백이 영원히 오지 않더라도 손실되는 작업이 없습니다.

타임아웃, 재시도 및 실패 처리

재시도는 통합이 조용히 망가지는 지점입니다. 다음 규칙은 제출, 폴링, 콜백에 적용되지만 안전 경계는 각각 다릅니다.

먼저 오류 분류

일시적: 연결 재설정, 5xx 응답, 속도 제한 응답, 읽기 타임아웃. 두 번째 시도에서 성공할 수 있습니다.

종료적: 4xx 검증 오류, 지원되지 않는 매개변수, 인증 실패, 그리고 작업 수준 실패(예: 프롬프트 거부). 이를 재시도하면 동일한 실패를 반복할 뿐이며, 이미 제출된 작업의 경우 비용도 반복됩니다.

의문이 들면 모든 것을 기본으로 재시도하지 말고 상태 코드와 오류 본문을 확인하세요.

지터를 더한 백오프와 하드 스톱

일시적 오류에는 무작위 지터를 더한 지수 백오프를 사용하고, 시도 횟수와 총 소요 시간 모두에 상한을 두세요. 상한 없는 재시도 정책은 장애 생성기입니다. 의존성이 악화되면 재시도 볼륨이 가장 감당하기 어려운 시점에 부하를 배로 늘립니다.

def with_retries(fn, *, attempts: int = 4, base: float = 0.5, cap: float = 8.0):
    for i in range(attempts):
        try:
            return fn()
        except TransientError:
            if i == attempts - 1:
                raise
            delay = min(cap, base * (2 ** i))
            time.sleep(delay + random.uniform(0, delay / 2))

경계를 넘는 멱등성

  • 제출: 제출을 재시도할 때는 항상 동일한 멱등성 키를 보내세요. 작업이 이미 생성되었을 수 있는 요청 타임아웃에서 복구하는 유일한 안전한 방법입니다. 키를 한 번 생성하고, 예상 요청과 함께 저장하고, 재사용하세요.
  • 폴링: 본질적으로 멱등성입니다. 읽기 전용입니다. 시간 예산 내에서 자유롭게 재시도할 수 있습니다.
  • 콜백: 전달은 본질적으로 최소 한 번입니다. 작업 ID + 이벤트 유형으로 중복 제거하고, 처리기의 상태 전환을 조건부로 만드세요. 그래야 중복 전달이 무해한 무동작이 됩니다.

종료 상태 노출

모든 작업 경로는 결국 정확히 하나의 결과를 저장해야 합니다. 자산 참조를 동반한 성공, 또는 이유와 시도 이력을 동반한 실패입니다. 작업이 무기한 pending에 머무르게 하지 마세요. 하이브리드 폴러는 오래된 임계값 후에 결정을 강제하기 위해 존재합니다. 해당 상태를 작업 소비자에게 노출하여 업스트림 코드가 영원히 기다리는 대신 재제출, 경고, 또는 주의 반환을 결정할 수 있게 하세요.

완료된 비디오 다운로드 및 저장

완료 페이로드의 URL은 임시적입니다. 서명되었을 수 있고, 만료될 수 있으며, 그 호스트는 애플리케이션이 읽을 때 의존해야 할 대상이 아닙니다. 통합의 마지막 단계는 산출물을 여러분이 제어하는 스토리지로 옮기는 것입니다.

자산 가져오기

스트리밍 요청으로 다운로드하여 대용량 파일이 단일 bytes 객체로 메모리에 떨어지지 않게 하고, 일반적인 API 호출이 아닌 파일 크기를 반영한 타임아웃을 설정하세요.

def download_asset(url: str, dest_path: str) -> None:
    with requests.get(url, stream=True, timeout=(5, 300)) as r:
        r.raise_for_status()
        with open(dest_path, "wb") as f:
            for chunk in r.iter_content(chunk_size=1 << 20):
                f.write(chunk)

스토리지 백엔드가 스트리밍 쓰기를 지원한다면 로컬 파일에 임시 저장하는 대신 응답 반복자를 직접 넘기세요.

신뢰하기 전에 검증

최소한 응답 상태의 성공 여부, 선언된 크기와의 콘텐츠 길이 일치, 그리고 사소하지 않은 파일 크기를 확인하세요. 완료 페이로드에 지속 시간이나 형식 메타데이터가 포함되어 있다면, 다운로드한 산출물이 오류 페이지나 잘린 부분 파일이 아닌 비디오일 가능성을 표본 검사하세요. 사용자에게 도달하는 손상된 자산은 여러분이 실패로 표시하고 재제출한 작업보다 더 나쁩니다.

자체 스토리지로 이동

객체를 자체 버킷이나 CDN 원본에 기록하고, 키는 내부 작업 ID에서 파생하며, 해당 키를 작업 행에 기록하세요. 자체 URL로 사용자에게 서비스하세요. 이점은 누적됩니다.

  • 임시 제공자 URL이 프론트엔드, 이메일, 로그에 절대 유출되지 않습니다.
  • 만료가 더 이상 중요하지 않습니다. 자체 일정에 따라 파생물을 재생성하거나 재인코딩할 수 있습니다.
  • 보존, 액세스 제어, 삭제가 제공자가 아닌 여러분의 정책이 됩니다.

다운로드 단계를 멱등성 있게 유지하세요. 대상 키가 이미 존재하고 유효하면 가져오기를 건너뛰세요. 중복 콜백 시 이 검사가 바로 두 번째 다운로드와 두 번째 스토리지 쓰기를 방지합니다.

생성 비용 제어

생성은 요청 작업량에 따라 청구됩니다. 일반적으로 출력 지속 시간이나 토큰 등가 단위로 청구되며, 정확한 요율은 플랫폼의 가격 패널에 표시됩니다. 각 호출이 일반적인 텍스트 요청보다 훨씬 비싸므로 비용 제어는 월간 검토가 아니라 호출 지점에 두어야 합니다.

동시성 제한

가장 효과적인 단일 가드레일은 동시 생성 작업 수를 제한하는 것입니다. 이것이 없으면 재시도 폭풍, 통제 불능 루프, 갑작스러운 사용자 요청이 어떤 대시보드도 표시하기 전에 지출을 배로 늘릴 수 있습니다. 제출 전에 세마포어나 유한 워커 풀을 배치하고 그 크기를 의도적으로 설정하세요.

from threading import Semaphore

_submit_gate = Semaphore(5)  # 예산과 제공자 제한에 맞게 조정

def guarded_submit(prompt, *, idempotency_key, **params):
    with _submit_gate:
        return submit_video_job(prompt, idempotency_key=idempotency_key, **params)

제출 전 검증

대부분의 우발적 초과 지출은 보내지 말아야 할 요청에서 발생합니다. 기능에 필요한 것보다 훨씬 높은 지속 시간 매개변수, 여전히 실행 중인 작업을 재제출하는 재시도 루프, 클라이언트 결함을 유발하는 빈 프롬프트입니다. 요청이 프로세스를 떠나기 전에 매개변수—지속 시간, 해상도, 프롬프트 길이, 입력 미디어 참조—를 검증하고 명확한 오류로 조기에 거부하세요.

오류뿐 아니라 작업량 기록

모든 제출과 그 매개변수, 요청 지속 시간, 사용자, 멱등성 키를 기록하세요. 기능별 및 사용자별로 주기당 작업 수를 집계하세요. 집계 작업량은 지출을 실제로 추적하는 지표이며, 어떤 호출자가 정상 속도의 열 배로 생성하는 것과 같은 비정상 패턴을 아직 작을 때 잡을 수 있게 합니다.

동일 작업에 두 번 비용 지불하지 않기

중복 생성 실패 모드에는 세 가지 흔한 원인이 있으며, 각각에 방어책이 있습니다.

  • 멱등성 키 없이 제출 타임아웃을 재시도하는 경우. 해당 키를 재사용하세요.
  • Webhook 재전달이 새 다운로드나 새 작업을 트리거하는 경우. 작업 ID로 중복 제거하고 전환을 조건부로 만드세요.
  • 폴러가 포기하고 원래 작업이 여전히 실행 중인데 재제출하는 경우. 대체 작업을 생성하기 전에 기존 작업의 상태를 검증하고, 작업 테이블에서 대체 관계를 추적하세요.

모든 재제출을 기본 복구 경로가 아니라 이유를 기록해야 하는 결정으로 취급하세요.

출시 전 체크리스트

통합을 출시하기 전에 이러한 결정과 실패 모드를 엔드투엔드로 검증하세요.

획득 및 수명 주기

  • 워크로드에 따라 획득 방식을 선택하고, 누락된 콜백이 작업을 잃지 않도록 하이브리드 폴백을 갖추세요.
  • 제출 후 다른 부수 효과보다 먼저 작업 행을 기록하고, 매개변수, 멱등성 키, 소유자를 기록하세요.
  • 모든 작업이 정확히 하나의 종료 상태에 도달하며, 오래된 임계값이 여전히 대기 중인 모든 항목에 대한 결정을 강제합니다.
  • 상태 전환이 현재 상태에 조건부이므로 중복 전달과 동시 폴링이 무해합니다.

네트워크 동작

  • 다운로드를 포함한 모든 요청에 연결 및 읽기 타임아웃이 설정됩니다.
  • 일시적 오류와 종료적 오류를 분류하고 일시적 오류만 재시도합니다.
  • 재시도는 지터를 더한 지수 백오프를 사용하고 시도 횟수와 경과 시간에 하드 상한을 둡니다.
  • 폴링 백오프에 간격 상한과 실제 경과 시간 예산이 모두 있으며, 폴링 루프는 요청 처리기가 아닌 워커에서 실행됩니다.

콜백 및 스토리지

  • 콜백 엔드포인트는 원시 본문에 대해 발신자를 검증하고, 빠르게 반환하며, 작업을 큐에 넣습니다.
  • 모든 콜백은 어떤 조치를 취하기 전에 자체 작업 기록과 대조하여 조정됩니다.
  • 완료된 자산은 자체 스토리지로 스트리밍되고, 검증되며, 자체 URL로만 제공됩니다. 다운로드 단계는 멱등성 있습니다.

비용 및 운영

  • 제출은 동시성 제한과 지속 시간, 해상도, 입력에 대한 매개변수 검증 게이트로 제어됩니다.
  • 기능별 및 사용자별로 작업량을 기록하므로 지출이 놀라움이 아니라 추세로 보입니다.
  • 중복 생성 경로—제출 재시도, Webhook 재전달, 폴러 재제출—가 모두 명시적으로 처리됩니다.
  • 임계값을 초과해도 대기 중으로 멈춘 작업과 지속적으로 동시성 제한에 도달하는 상황에 대한 경고가 존재합니다.

각 경로를 통해 실제 작업을 실행하세요. 성공, 실패, 누락된 콜백, 중복 콜백의 경우를 확인하고 종료 상태, 스토리지 쓰기, 카운터가 올바르게 보이는지 확인하세요. 위의 모든 항목이 해당 리허설에서 성립한다면, 통합은 HiAPI.ai 비디오 생성 API에 대해 출시할 준비가 된 것입니다.

영구 AI 아티팩트로 빌드 시작하기

API 키를 받아 태스크 기반의 하나의 API로 이미지, 비디오, 오디오 모델을 호출하세요.

API 문서 보기