OpenAI Batch API로 AI 심사 비용 53% 절감하기: v1→v2 마이그레이션 설계
들어가며
AI 콘텐츠 심사 시스템은 이미지와 텍스트가 포함된 웹 페이지를 GPT로 분석하여 문제를 자동 검출합니다. v1은 잘 동작했지만, 프로젝트당 평균 13회의 순차 API 호출이 필요했고, 50건 배치 심사에 최대 37분이 소요되었습니다.
OpenAI가 Batch API를 출시하면서, 기존 호출을 일괄 제출하면 비용 50% 할인을 받을 수 있게 되었습니다. 이 글은 실시간 순차 호출(v1)에서 Batch API 기반(v2)으로 전환하기까지의 설계 결정 과정을 다룹니다.
여러 API 요청을 JSONL 파일로 묶어 한 번에 제출하는 방식입니다. 24시간 이내 처리를 보장하며, 일반 API 대비 50% 할인이 적용됩니다. 실시간 응답이 필요하지 않은 대량 처리에 적합합니다.
v1의 문제: 순차 호출의 한계
v1 심사 시스템은 프로젝트 하나를 심사할 때 OpenAI API를 순차적으로 호출합니다.
| 항목 | 수치 |
|---|---|
| 프로젝트당 평균 API 호출 | 13.1회 |
| 프로젝트당 평균 입력 토큰 | 115K |
| 이미지 100개+ 프로젝트 | 16청크로 분할, 5분 이상 소요 |
| 50개 배치 심사 | 15~37분 소요 |
이미지가 많은 프로젝트일수록 청크 수가 증가하고, 순차 호출 시간이 선형으로 늘어납니다. Batch API를 사용하면 이 순차 호출을 한 번의 제출로 병렬 처리할 수 있습니다.
OpenAI Batch API 핵심 스펙
| 항목 | 내용 |
|---|---|
| 입력 형식 | JSONL 파일 (한 줄 = 하나의 API 요청) |
| 지원 API | /v1/chat/completions (Vision/멀티모달 포함) |
| 완료 시간 | 24시간 이내 보장 (소규모는 1~2시간) |
| 비용 | 일반 API 대비 50% 할인 |
| 최대 제한 | 200MB 파일, 50,000 요청/배치 |
| 결과 | JSONL로 다운로드, custom_id로 매핑 |
피저빌러티 테스트: URL vs base64
v2 도입 전, 이미지가 많은 실제 프로젝트(이미지 159개, 16청크)로 두 가지 방식을 테스트했습니다.
테스트 1: URL 모드
이미지 CDN URL을 JSONL에 직접 전달하는 방식입니다.
| 항목 | 결과 |
|---|---|
| JSONL 크기 | 175 KB (매우 작음) |
| 성공률 | 9/11 (82%) — 2건 실패 |
| 실패 원인 | GIF/WebP 이미지 다운로드 타임아웃 |
| 소요 시간 | 278초 (4.6분) |
OpenAI 서버가 외부 CDN에서 이미지를 직접 다운로드하는 과정에서 GIF/WebP 포맷 타임아웃이 발생했습니다.
테스트 2: base64 모드
이미지를 로컬에서 다운로드한 뒤 base64로 변환하여 JSONL에 포함하는 방식입니다.
| 항목 | 결과 |
|---|---|
| JSONL 크기 | 59.7 MB (200MB 제한 대비 30%) |
| 성공률 | 17/17 (100%) — 전건 성공 |
| 소요 시간 | 247초 (4.1분) |
| 비용 | $0.100 (일반가 $0.200의 50%) |
비교 결과
| 항목 | URL 모드 | base64 모드 |
|---|---|---|
| JSONL 크기 | 175 KB | 59.7 MB |
| 성공률 | 82% | 100% |
| GIF/WebP 처리 | 실패 | 정상 |
| 기존 코드 재사용 | 새 함수 필요 | v1 코드 그대로 재사용 |
JSONL 크기가 커지지만 200MB 제한 대비 30%로 안전하고, 전건 성공 + 기존 코드 재사용이 가능합니다. 안정성과 구현 비용 모두에서 우위입니다.
v1 vs v2 실측 비교
동일한 프로젝트(이미지 159개, 16청크)로 v1과 v2를 직접 비교 측정했습니다.
v1 소요 시간 상세:
| 단계 | 소요 시간 |
|---|---|
| 데이터 수집 | 0.2초 |
| 이미지 처리 | 32.3초 |
| 스토리 심사 (순차 16청크) | 323.3초 |
| 리워드 심사 | 11.6초 |
| 종합 판정 | 10.8초 |
| 합계 | 378.3초 (6.3분) |
v1 vs v2 최종 비교:
| 항목 | v1 (실시간 순차) | v2 (Batch API) | 개선 |
|---|---|---|---|
| 총 소요 시간 | 378초 (6.3분) | 247초 (4.1분) | 35% 단축 |
| 스토리 심사 | 323초 | ~180초 | 44% 단축 |
| 비용 | $0.213 | $0.100 | 53% 절감 |
| 성공률 | 100% | 100% | 동일 |
| 검출 이슈 수 | 5건 | 5건 | 동일 수준 |
핵심 설계 결정 6가지
결정 1: 이미지 처리 방식 → base64
- 초기 안: URL 모드 (JSONL 크기 최소화)
- 문제: GIF/WebP 타임아웃으로 82% 성공률
- 최종: base64 모드 (기존 이미지 처리 함수 재사용)
- 근거: JSONL 59.7MB는 200MB 제한 대비 안전, 전건 성공
결정 2: v1 격리 → 별도 모듈 + 동적 import
v2 코드를 별도 모듈 2개로 격리하고, 기존 클래스에서 동적 import로 호출합니다. v2 모듈에 오류가 발생해도 v1에는 일체 영향이 없습니다.
결정 3: 복수 프로젝트 처리 → 프로젝트별 독립 배치
| 관점 | 단일 배치 | 프로젝트별 배치 |
|---|---|---|
| 부분 완료 | 전체 완료까지 결과 없음 | 먼저 완료된 프로젝트부터 표시 |
| 에러 격리 | 배치 실패 시 전체 영향 | 실패 프로젝트만 영향 |
| JSONL 크기 | 프로젝트 수에 비례 | 항상 ~60MB로 안전 |
| 실시간 피드백 | "대기 중..." 하나 | 프로젝트별 진행 상태 표시 |
모든 프로젝트를 하나의 JSONL로 묶는 방식보다, 프로젝트별 독립 배치를 병렬 제출하는 것이 에러 격리와 UX 양면에서 유리합니다.
결정 4: 동시 실행 수 → 기본 3개, 설정 가능
- 이미지 처리(sharp)가 메모리 집약적 → 동시에 너무 많이 실행하면 OOM 위험
- 이미지 처리는 concurrency pool(기본 3개)로 제한
- 배치 제출 후 폴링은 모두 병렬 (OpenAI 측 병렬 처리)
- 환경 변수로 조정 가능
결정 5: 종합 판정 → 배치 후 실시간 호출
종합 판정(synthesis)은 스토리/리워드 결과가 모두 필요합니다. 2단계 배치(청크 배치 → 종합 배치)로 구성하면 복잡도만 올라가고, 종합은 프로젝트당 1건(~10초) 이므로 배치 완료 후 실시간 호출로 처리합니다.
결정 6: UI → 별도 버튼으로 제공
- v1/v2 토글보다 별도 "Batch 심사" 버튼이 의도가 명확
- 검증 기간 동안 v1/v2를 쉽게 비교 가능
- 검증 완료 후 v1 제거 또는 v2를 기본으로 전환
기대 효과 전체 정리
| 항목 | v1 | v2 | 개선 |
|---|---|---|---|
| 1개 프로젝트 | 6.3분 | 4.1분 | 35% 단축 |
| 비용 | $0.21 | $0.10 | 53% 절감 |
| 3개 프로젝트 (추정) | 18분 (순차) | ~7분 (병렬) | 61% 단축 |
| 50개 프로젝트 배치 (추정) | ~5시간 | ~1시간 | 80%+ 단축 |
v1은 프로젝트를 순차 처리하므로 건수에 선형 비례합니다. v2는 프로젝트별 독립 배치를 병렬 제출하므로, 프로젝트 수가 많을수록 개선 폭이 커집니다.
마무리: Batch API 도입을 고려한다면
이번 마이그레이션에서 얻은 교훈을 정리합니다.
-
이미지 방식은 반드시 테스트하세요. URL 모드는 JSONL이 작지만 GIF/WebP 같은 포맷에서 타임아웃이 발생합니다. base64 모드가 안정적입니다.
-
v1과의 격리가 핵심입니다. 새 버전이 실패해도 기존 시스템에 영향이 없어야 합니다. 별도 모듈 + 동적 import 패턴이 효과적입니다.
-
프로젝트별 독립 배치가 단일 배치보다 낫습니다. 에러 격리, 부분 완료 표시, JSONL 크기 관리 모든 면에서 유리합니다.
-
모든 것을 배치에 넣을 필요는 없습니다. 종합 판정처럼 1건짜리 호출은 실시간으로 처리하는 것이 구현 복잡도를 크게 줄입니다.
-
동시 실행 수를 제한하세요. 이미지 처리 같은 메모리 집약 작업은 concurrency pool로 OOM을 방지해야 합니다.