임베딩 API 호출이 느려질 때 배치 크기와 병렬 처리 조정법

임베딩 API 호출이

결론부터 말씀드리면, 임베딩 API 호출이 느려질 때 가장 먼저 손봐야 할 것은 코드 최적화가 아니라 배치 크기와 동시 요청 수의 조합입니다. 텍스트를 한 건씩 호출하고 있다면 배치로 묶는 것만으로도 체감 속도가 크게 달라지고, 이미 배치를 쓰고 있는데도 느리다면 동시성 설정이 레이트 리밋에 걸려 있을 가능성이 높습니다. 이 글에서는 OpenAI Python SDK(1.x 버전) 기준으로, 실제 동작하는 코드와 함께 어떤 조합이 언제 통하고 언제 안 통하는지 정리해 드립니다.

배치 크기부터 점검해야 하는 이유

텍스트 1만 건을 임베딩해야 한다고 가정해 보겠습니다. for 루프를 돌면서 한 건씩 embeddings.create를 호출하면 요청 1만 번이 순차적으로 나가게 되고, 각 요청마다 네트워크 왕복 시간(RTT)이 그대로 누적됩니다. 반면 OpenAI 임베딩 API는 input 파라미터에 문자열 배열을 받을 수 있어서, 여러 텍스트를 한 번의 요청으로 묶어 보낼 수 있습니다.

공식 문서 기준으로 임베딩 요청 하나에 담을 수 있는 입력 개수와 토큰 총량에는 제한이 있고, 모델별 최대 입력 토큰 수도 다릅니다. 이 한도는 모델이 업데이트되면서 바뀔 수 있으므로, 정확한 수치는 코드에 하드코딩하지 말고 공식 문서를 직접 확인하는 편이 안전합니다.

병렬 처리를 늘리면 속도가 빨라지는 이유와 한계는?

배치로 묶어도 배치 자체를 순차적으로 호출하면 여전히 느립니다. 예를 들어 1만 건을 500개씩 묶으면 20번의 요청이 생기는데, 이 20번을 하나씩 기다리지 않고 동시에 여러 개를 날리면 전체 소요 시간이 줄어듭니다. 이게 바로 병렬 처리(동시성)의 역할입니다.

여러 API 요청이 병렬로 처리되는 서버 네트워크 시각화

다만 무한정 늘릴 수는 없습니다. OpenAI API는 계정 등급(Tier)에 따라 분당 요청 수(RPM)와 분당 토큰 수(TPM) 한도를 두고 있고, 이 한도를 넘기면 429 오류가 돌아옵니다. 동시 요청 수를 늘리는 건 이 한도 안에서만 효과가 있고, 한도를 넘는 순간부터는 오히려 재시도 로직 때문에 더 느려질 수 있습니다. 정확한 한도는 계정 대시보드와 레이트 리밋 문서에서 확인하는 것이 가장 정확합니다.

배치와 동시성을 함께 조정하는 실전 코드

아래는 openai>=1.0 SDK를 기준으로, asyncioSemaphore를 이용해 배치 크기와 동시 요청 수를 동시에 제어하는 예시입니다.

import asyncio
from openai import AsyncOpenAI

client = AsyncOpenAI(max_retries=5)  # 429/5xx 발생 시 SDK가 자동 재시도
semaphore = asyncio.Semaphore(5)     # 동시 요청 수를 5개로 제한

async def embed_batch(batch: list[str]):
    async with semaphore:
        response = await client.embeddings.create(
            model="text-embedding-3-small",
            input=batch
        )
        return [d.embedding for d in response.data]

async def embed_all(texts: list[str], batch_size: int = 300):
    batches = [texts[i:i + batch_size] for i in range(0, len(texts), batch_size)]
    results = await asyncio.gather(*(embed_batch(b) for b in batches))
    return [vec for batch_result in results for vec in batch_result]

# 실행 예시
# texts = ["문장1", "문장2", ..., "문장10000"]
# vectors = asyncio.run(embed_all(texts))

여기서 Semaphore(5)는 동시에 최대 5개 요청만 API로 나가도록 제한하는 역할을 하고, max_retries=5는 SDK 내부에서 지수 백오프(exponential backoff)로 재시도를 처리하도록 합니다. 배치 크기(batch_size)와 동시 요청 수(Semaphore 값)는 데이터 규모에 따라 아래 표 정도를 시작점으로 잡고, 429 오류 빈도를 보면서 조정하는 것을 권장합니다.

텍스트 개수 배치 크기(시작값) 동시 요청 수(시작값) 비고
1,000개 이하 100~200 2~3 배치를 더 키워도 체감 차이가 적음
1,000~10,000개 300~500 4~6 계정 Tier의 TPM 한도 확인 필요
10,000개 이상 500 내외 6~10 429 발생 시 즉시 낮추고 백오프 적용

동시 요청을 늘렸는데 왜 429 에러가 늘어날까요?

터미널에서 파이썬 코드로 API 성능을 최적화하는 개발자

동시성을 5에서 20으로 올렸는데 오히려 전체 처리 시간이 늘어나는 경우가 있습니다. 이건 대부분 분당 토큰 한도(TPM)를 초과했기 때문입니다. 요청 개수 자체는 한도 안에 들어와도, 배치 하나에 담긴 텍스트가 길면 토큰 수가 순식간에 쌓여 한도를 넘기고, 429 응답이 쌓이면서 SDK의 재시도 대기 시간이 누적됩니다.

이럴 때는 동시성을 다시 낮추거나, 배치 크기를 줄여서 요청당 토큰 수를 조절하는 쪽이 더 효과적입니다. asyncio의 개념 자체가 궁금하다면 동시성 항목을 참고하시면 스레드 기반 병렬 처리와 이벤트 루프 기반 비동기 처리의 차이를 이해하는 데 도움이 됩니다.

이 조정이 통하지 않는 경우

배치와 동시성 조정이 만능은 아닙니다. 텍스트 하나하나가 이미 모델의 최대 입력 토큰 수에 가까운 긴 문서라면, 배치로 여러 개를 묶어도 요청당 토큰 수가 그대로 커서 TPM 한도에 금방 걸립니다. 이 경우엔 배치 크기를 키우는 대신 문서를 청크로 나누는 전처리가 먼저 필요합니다.

또한 병목이 API 호출이 아니라 응답받은 벡터를 벡터 데이터베이스에 적재하는 쪽(인덱싱, 디스크 I/O)에 있다면, 동시성을 아무리 올려도 전체 파이프라인 속도는 그대로일 수 있습니다. 이럴 땐 API 호출 구간과 저장 구간을 분리해서 각각 어디가 느린지부터 측정하는 게 순서입니다. 그리고 이미 계정이 낮은 Tier에 머물러 있는 상태라면, 코드를 아무리 손봐도 한도 자체가 낮아서 개선 폭이 제한적일 수 있습니다.

지금 바로 적용해볼 순서

먼저 현재 코드가 한 건씩 호출하고 있는지 확인하고, 그렇다면 배치 크기 300 정도부터 묶어서 호출하도록 바꿔 보시기 바랍니다. 그다음 Semaphore 값을 5로 시작해서 429 오류 로그를 관찰하고, 오류가 없으면 조금씩 올리고 발생하면 낮추는 방식으로 자신의 계정 Tier에 맞는 지점을 찾는 것이 가장 현실적인 접근입니다. 마지막으로 max_retries와 백오프 설정을 빼놓지 않아야, 순간적인 429가 전체 작업을 실패시키지 않습니다.

작업 큐 도입, 지금이 신호일 수 있습니다

Leave a Comment