.cg-article{–accent:#4f46e5;–accent-soft:#eef2ff;–accent-line:#c7d2fe;–navy:#312e81;–warn:#d97706;–warn-soft:#fffbeb;line-height:1.75;color:#1f2937;font-size:17px;word-break:keep-all}
.cg-article h2{margin:2.2em 0 .7em;padding-left:.55em;border-left:5px solid var(–accent);font-size:1.42em;line-height:1.4;color:var(–navy)}
.cg-article h3{margin:1.6em 0 .5em;font-size:1.13em;color:var(–accent)}
.cg-article p{margin:.7em 0}
.cg-lead{background:var(–accent-soft);border:1px solid var(–accent-line);border-radius:12px;padding:18px 20px;margin:1.2em 0;font-size:1.02em}
.cg-lead strong{color:var(–accent)}
.cg-table{width:100%;border-collapse:collapse;margin:1.1em 0;font-size:.97em}
.cg-table th,.cg-table td{border:1px solid #e5e7eb;padding:11px 13px;text-align:left;vertical-align:top}
.cg-table th{background:var(–accent);color:#fff;font-weight:600}
.cg-table tr:nth-child(even) td{background:#f9fafb}
.cg-check{list-style:none;padding:0;margin:1.1em 0}
.cg-check li{position:relative;padding:9px 9px 9px 34px;margin:6px 0;background:var(–accent-soft);border-radius:8px}
.cg-check li::before{content:”✓”;position:absolute;left:12px;top:9px;color:var(–accent);font-weight:700}
.cg-warn{background:var(–warn-soft);border-left:5px solid var(–warn);border-radius:8px;padding:16px 18px;margin:1.3em 0}
.cg-warn strong{color:var(–warn)}
.cg-faq{margin:1.5em 0}
.cg-faq dt{font-weight:600;margin:1.1em 0 .3em;color:var(–accent)}
.cg-faq dd{margin:0 0 .6em}
.cg-article pre{background:#1e1b2e;color:#e9e7ff;border-radius:10px;padding:14px 16px;overflow-x:auto;font-size:.86em;line-height:1.55;margin:1.1em 0}
.cg-article pre code{background:none;color:inherit;padding:0}
.cg-article code{background:var(–accent-soft);color:var(–navy);padding:.1em .4em;border-radius:5px;font-size:.9em}
.cg-note{font-size:.9em;color:#6b7280;border-top:1px dashed #d1d5db;margin-top:2.4em;padding-top:1em}
배치 API는 수천~수십만 건의 LLM 요청을 한 묶음으로 제출하고, 즉시 응답 대신 일정 시간 안에 비동기로 결과를 받는 방식입니다. 핵심 거래는 단순합니다. “지금 당장”이 아니라 “정해진 시간 안 어딘가”에 처리되는 것을 받아들이는 대신, 같은 모델·같은 토큰을 동기 호출보다 낮은 단가로 처리합니다. 실시간 응답이 필요 없는 대량 작업—분류, 요약, 추출, 임베딩 생성, 데이터 라벨링—에서는 결과 품질이 동일하면서 비용이 의미 있게 줄어듭니다. 절감 폭과 처리 시간 상한은 제공사마다 다르므로 공식 가격·문서 확인을 권장하되, 트레이드오프와 설계 패턴은 어느 제공사에서나 비슷합니다.
배치 처리가 비용을 낮추는 원리
실시간(동기) API는 요청이 들어오는 즉시 응답해야 하므로, 제공사는 순간 부하에 대비해 연산 자원을 항상 여유 있게 띄워 둬야 합니다. 이 “즉시성”에는 비용이 따릅니다. 반면 배치 처리는 마감 시한만 지키면 되므로, 제공사가 자원이 한가한 시점에 작업을 몰아 처리하고 부하를 평탄화할 수 있습니다. 이렇게 절약된 운영 효율이 낮은 단가로 사용자에게 돌아오는 구조입니다.
즉 배치 할인은 “성능이 떨어지는 모델”이나 “품질이 낮은 응답”의 대가가 아닙니다. 동일한 모델이 동일한 추론을 수행하고 출력 품질도 같습니다. 사용자가 내려놓는 것은 오직 응답 시점에 대한 통제권—언제 결과가 나올지 정확히 모른다는 점—뿐입니다. 이 한 가지를 양보할 수 있는 작업이라면 배치는 거의 항상 이득입니다.
이 구조를 이해하면 배치를 어디에 써야 할지 판단이 쉬워집니다. 비용은 결국 “즉시성에 매기는 프리미엄”이고, 배치는 그 프리미엄을 반납하는 대신 할인을 받는 거래이기 때문입니다. 같은 작업이라도 사용자가 화면 앞에서 결과를 기다린다면 즉시성에 값을 치러야 하지만, 밤사이 돌려 두면 되는 색인 작업이나 데이터 가공이라면 그 값을 치를 이유가 없습니다. 그래서 비용 최적화의 첫걸음은 모델을 바꾸거나 프롬프트를 줄이는 것이 아니라, 워크로드를 “실시간이 꼭 필요한 것”과 “그렇지 않은 것”으로 나누는 분류 작업입니다.
어떤 작업에 적합한가
배치의 적합성은 단 하나의 질문으로 갈립니다. “이 결과를 지금 당장 사용자에게 보여 줘야 하는가?” 그렇지 않다면 배치 후보입니다.
| 작업 유형 | 배치 적합도 | 이유 |
|---|---|---|
| 대량 문서 분류·태깅 | 높음 | 실시간 불필요, 건수 많음 |
| 대량 요약·번역 | 높음 | 백그라운드 처리 가능 |
| 임베딩 대량 생성(인덱싱) | 높음 | 일괄 색인은 비실시간 |
| 데이터셋 합성·라벨링·평가 | 높음 | 오프라인 파이프라인 |
| 야간 리포트·정기 일괄 작업 | 높음 | 마감만 지키면 됨 |
| 챗봇·실시간 응답 | 낮음 | 즉시성 필수 |
| 사용자 대기 중인 단건 요청 | 낮음 | 지연 허용 불가 |
동기 호출 대비 트레이드오프
배치의 유일하면서도 결정적인 비용은 지연입니다. 동기 호출이 수 초 안에 답을 주는 반면, 배치는 제출 후 결과가 나오기까지 상당한 시간이 걸릴 수 있고, 처리 완료 시한은 제공사가 보장하는 상한(예: 일정 시간 이내) 안에서 유동적입니다. 빨리 끝날 수도 있지만, 부하 상황에 따라 상한에 가깝게 걸릴 수도 있습니다. 따라서 “정확히 N분 뒤에 필요”한 작업에는 맞지 않습니다.
| 항목 | 동기 호출 | 배치 API |
|---|---|---|
| 응답 시점 | 즉시(초 단위) | 비동기(시한 내 유동) |
| 토큰 단가 | 표준 | 인하(문서 확인) |
| 처리 단위 | 요청 1건 | 대량 묶음 |
| 속도 제한(rate limit) 압박 | 건별로 큼 | 묶음 제출로 완화 |
| 적합 워크로드 | 실시간 UX | 오프라인 대량 처리 |
부수 효과로, 배치는 동기 호출에서 골치였던 속도 제한 관리를 단순화합니다. 수만 건을 동기로 쏘려면 분당 요청·토큰 한도에 맞춰 직접 페이싱·재시도를 구현해야 하지만, 배치는 묶음 하나로 제출하고 제공사가 내부에서 처리합니다. 이 운영 단순화 자체가 적지 않은 이득입니다.
배치 작업 설계
배치는 보통 다음 흐름으로 동작합니다. 요청들을 한 줄에 하나씩 담은 입력 파일(JSONL)을 만들고, 그 파일을 업로드해 배치 작업을 생성한 뒤, 상태를 폴링하다가 완료되면 결과 파일을 내려받아 입력과 매칭합니다. 각 요청에 고유 ID(custom_id)를 붙이는 것이 핵심입니다. 결과는 입력 순서와 무관하게 돌아올 수 있어, ID로 매칭하지 않으면 어떤 응답이 어떤 입력의 것인지 알 수 없습니다.
# 배치 입력(JSONL) — 한 줄 = 한 요청, custom_id로 매칭(개념 예시)
{"custom_id":"row-001","method":"POST","url":"/v1/...","body":{...}}
{"custom_id":"row-002","method":"POST","url":"/v1/...","body":{...}}
# 흐름: 업로드 → 배치 생성 → 상태 폴링 → 결과 다운로드 → ID 매칭
batch = create_batch(input_file_id=uploaded.id)
while batch.status not in ("completed","failed","expired"):
sleep(poll_interval); batch = get_batch(batch.id)
results = download(batch.output_file_id)
merge_by_custom_id(inputs, results) # custom_id 기준으로 결합작업 분할
한 배치에 담을 수 있는 요청 수와 파일 크기에는 상한이 있습니다. 대규모 작업은 이 상한에 맞춰 여러 배치(샤드)로 쪼개 제출합니다. 분할은 단순히 한도를 맞추기 위한 것만이 아니라, 부분 실패의 영향 범위를 좁히는 효과도 있습니다. 10만 건을 한 배치로 던졌다가 문제가 생기면 전체를 다시 봐야 하지만, 1만 건씩 나누면 문제 있는 샤드만 재처리하면 됩니다.
재시도
배치 안의 개별 요청은 일부가 실패할 수 있습니다(입력 오류, 일시적 처리 실패 등). 결과 파일에는 성공과 실패가 섞여 오므로, custom_id로 실패 건만 골라내 별도 배치로 다시 제출하는 재시도 경로를 설계해야 합니다. 멱등성을 위해 custom_id를 입력 데이터의 안정적 키로 두면, 재시도 시 중복 처리나 결과 혼선을 피할 수 있습니다.
재시도를 설계할 때는 실패 사유를 구분하는 것이 중요합니다. 입력 자체가 잘못된 영구적 오류(스키마 위반, 너무 긴 입력)는 같은 내용으로 다시 제출해 봐야 또 실패하므로, 입력을 고치거나 제외 처리해야 합니다. 반면 일시적 처리 실패는 그대로 재제출하면 통과하는 경우가 많습니다. 두 부류를 섞어 무한정 재시도하면 영구 오류 건이 큐를 막고 비용만 새므로, 재시도 횟수 상한과 영구 실패 격리(dead-letter) 분리를 함께 두는 편이 안전합니다.
결과 수집과 후처리
결과 파일을 받은 뒤에는 단순 매칭에서 끝내지 말고, 응답 본문이 기대한 형식인지까지 검사하는 후처리 단계를 둬야 합니다. 배치는 요청이 처리됐다는 것(성공 상태)과 그 응답이 쓸 만하다는 것(내용 유효)을 구분하지 않기 때문입니다. 예를 들어 구조화 출력을 요청했다면 각 결과를 스키마로 검증해, 형식은 통과했지만 내용이 비어 있거나 값이 범위를 벗어난 건을 골라 별도 처리해야 합니다. 대량 작업일수록 이런 “성공처럼 보이는 실패”가 조용히 섞이기 쉬워, 후처리 검증을 자동화해 두는 것이 데이터 품질을 지키는 핵심입니다.
custom_id 매칭과 부분 실패 처리는 선택이 아닙니다.
배치 결과는 순서를 보장하지 않고 일부가 실패할 수 있습니다. custom_id로 입력과 결과를 결합하고, 실패 건만 추려 재시도하는 로직이 없으면 데이터가 어긋나거나 누락됩니다. 대량 작업일수록 이 누락은 조용히 쌓입니다.
비용 추정과 모니터링
배치를 돌리기 전에 대략의 비용을 추정해 두면 예산 사고를 막을 수 있습니다. 전체 입력·출력 토큰 수를 표본으로 가늠하고, 배치 단가를 곱해 상한을 잡습니다. 표본 몇십 건을 동기로 먼저 돌려 평균 입출력 토큰을 측정한 뒤 전체 건수로 외삽하는 방식이 실무적입니다.
# 표본 기반 비용 추정(개념 예시)
avg_in, avg_out = measure_sample(items[:50]) # 평균 입/출력 토큰
est_tokens_in = avg_in * len(items)
est_tokens_out = avg_out * len(items)
# est_cost = est_tokens_in * batch_in_rate + est_tokens_out * batch_out_rate
# (단가는 공식 가격 페이지의 배치 요금 적용)실행 중에는 다음을 모니터링합니다.
- 배치 상태(진행·완료·실패·만료)와 완료까지의 경과 시간
- 전체 건수 대비 성공·실패 건수 비율과 실패 사유 분포
- 실제 소비 토큰과 사전 추정치의 차이(예산 초과 조기 감지)
- 시한 내 미완료 위험—마감이 빠듯하면 샤드를 더 일찍 제출
- 재시도 큐에 쌓인 실패 건이 일정 비율을 넘는지(입력·프롬프트 결함 신호)
실무 적용 체크리스트
- 이 작업이 실시간 응답을 요구하지 않는지 먼저 확인한다.
- 각 요청에 안정적인 custom_id를 부여해 결과를 매칭한다.
- 건수·파일 크기 상한에 맞춰 샤드로 분할한다.
- 성공/실패를 분리하고 실패 건만 재시도하는 경로를 둔다.
- 표본으로 비용을 추정하고 예산 상한을 정한다.
- 처리 시한 상한을 업무 마감보다 충분히 앞서 잡는다.
- 프롬프트 캐싱 등 다른 절감 기법과 병행 가능한지 문서로 확인한다.
자주 묻는 질문
- 배치 API는 응답 품질이 떨어지나요?
- 아닙니다. 동일한 모델이 동일한 추론을 수행하므로 출력 품질은 동기 호출과 같습니다. 사용자가 양보하는 것은 응답 시점에 대한 통제권뿐이고, 그 대가로 토큰 단가가 낮아집니다.
- 결과는 얼마나 빨리 나오나요?
- 제공사가 보장하는 처리 시한 상한 안에서 유동적입니다. 부하가 한가하면 빨리 끝나기도 하지만 상한에 가깝게 걸릴 수도 있습니다. 정확한 상한과 동작은 제공사 공식 문서에서 확인해야 하며, 분 단위로 정확한 시점이 필요한 작업에는 부적합합니다.
- 비용은 얼마나 절감되나요?
- 배치 요금은 동기 요금보다 낮게 책정됩니다. 정확한 할인율은 제공사·모델·시점에 따라 달라 공식 가격 페이지 확인을 권장합니다. 건수가 많고 실시간성이 필요 없을수록 절감 효과가 커집니다.
- 결과 순서는 입력 순서와 같나요?
- 보장되지 않습니다. 그래서 각 요청에 custom_id를 붙여 결과와 입력을 ID로 매칭해야 합니다. 순서에 의존해 결합하면 데이터가 어긋날 수 있습니다.
- 배치와 프롬프트 캐싱을 같이 쓸 수 있나요?
- 두 기법은 절감 원리가 달라 함께 쓸 여지가 있지만, 동시 적용 가능 여부와 상호작용은 제공사마다 다릅니다. 적용 전에 사용하는 API의 공식 문서에서 병행 지원 여부를 확인하는 것이 안전합니다.
이 글은 OpenAI·Anthropic·Google 등의 공개 API 문서에 설명된 배치 처리(Batch API·Message Batches·Batch mode)의 일반 원리와 설계 패턴을 정리한 것입니다. 할인율, 처리 시한 상한, 배치당 건수·용량 제한 등 구체적 수치는 제공사·모델·시점에 따라 다르므로, 실제 적용 전에는 각 API의 공식 문서와 가격 페이지를 확인하시기 바랍니다.