OpenAI Responses API의 장시간 작업은 background: true로 시작하고 완료 결과는 웹훅으로 받는 구조가 적합합니다. HTTP 연결이 끝나도 작업은 계속되며, 애플리케이션은 응답 ID로 상태와 결과를 조회할 수 있습니다. 이 글은 요청 시작부터 중복 실행 방지까지 필요한 최소 구성을 다룹니다.
언제 background mode를 써야 하나
background mode는 모델 응답을 기다리는 동안 웹 요청이나 작업 실행기를 점유하고 싶지 않을 때 사용합니다. 긴 문서 분석이나 여러 도구를 거치는 에이전트 작업처럼 완료 시간이 짧게 고정되지 않는 처리에 맞습니다. 짧은 분류나 한두 문장 생성은 일반 동기 요청이 더 단순합니다.
| 방식 | 완료 확인 | 잘 맞는 상황 | 운영 시 주의점 |
|---|---|---|---|
| 동기 요청 | 같은 HTTP 응답 | 짧고 예측 가능한 생성 | 요청 제한 시간 안에 끝나야 함 |
| background + 폴링 | Responses 조회 API | 로컬 검증, 소수 작업 | 조회 간격과 종료 조건 필요 |
| background + 웹훅 | OpenAI가 이벤트 전송 | 서버 자동화, 다수 작업 | 서명 검증과 중복 처리 필요 |
OpenAI의 background mode 공식 문서에 따르면 백그라운드 요청은 비동기로 시작됩니다. 상태가 queued 또는 in_progress인 동안 응답 객체를 조회할 수 있습니다. 이 두 상태를 벗어나면 작업은 최종 상태에 도달한 것입니다.
background mode로 작업을 시작하는 법
OpenAI 공식 SDK를 설치하고 API 키와 사용할 모델을 환경 변수에 둡니다. 호출할 때 background: true를 추가한 뒤 반환된 응답 ID를 내부 작업 ID와 함께 저장합니다.
import OpenAI from "openai";
const client = new OpenAI();
const model = process.env.OPENAI_MODEL;
if (!model) throw new Error("OPENAI_MODEL을 설정하세요.");
const response = await client.responses.create({
model,
input: "업로드된 계약서의 의무, 기한, 위험 조항을 표로 정리하세요.",
background: true,
});
console.log({ id: response.id, status: response.status });
요청 직후에는 결과 본문보다 response.id와 현재 status가 중요합니다. 내부 작업 레코드에는 사용자 요청 ID, OpenAI 응답 ID, 생성 시각, 현재 상태를 함께 기록합니다. 웹훅이 늦거나 운영자가 상태를 확인해야 할 때 같은 응답 ID로 복구할 수 있습니다.
폴링과 웹훅 중 무엇을 고를까
기능을 처음 확인할 때는 폴링이 빠릅니다. 공식 문서가 안내하는 종료 조건은 queued와 in_progress 동안만 조회를 반복하는 것입니다. 완료뿐 아니라 failed, cancelled, incomplete도 최종 상태로 처리해야 무한 반복을 피할 수 있습니다.
let latest = await client.responses.retrieve(response.id);
while (latest.status === "queued" || latest.status === "in_progress") {
await new Promise((resolve) => setTimeout(resolve, 2_000));
latest = await client.responses.retrieve(response.id);
}
if (latest.status !== "completed") {
throw new Error(`작업 종료 상태: ${latest.status}`);
}
console.log(latest.output_text);
운영 서버에서 작업 수가 늘면 폴링 대신 웹훅을 사용합니다. 웹훅은 완료 시점에 이벤트를 보내므로 조회 루프를 계속 유지하지 않아도 됩니다. 다만 웹훅 본문에는 결과 전체가 아니라 응답 ID가 들어오므로, response.completed를 받은 작업자가 Responses 조회 API로 최종 결과를 가져옵니다.
OpenAI 웹훅을 설정하고 서명을 검증하는 법
OpenAI 대시보드의 Webhooks 설정에서 엔드포인트를 만듭니다. 웹훅은 프로젝트별로 설정되며 이름, 공개 URL, 구독할 이벤트를 지정합니다. 완료만 필요해도 response.completed와 함께 response.failed, response.cancelled, response.incomplete를 받아야 내부 작업이 대기 상태에 남지 않습니다.
생성 직후 표시되는 signing secret은 다시 볼 수 없으므로 비밀 저장소에 보관합니다. 서버에서는 이를 OPENAI_WEBHOOK_SECRET로 주입합니다. 서명 검증에는 JSON으로 파싱하기 전의 원문 본문이 필요합니다.
import OpenAI from "openai";
import express from "express";
const app = express();
const client = new OpenAI({
webhookSecret: process.env.OPENAI_WEBHOOK_SECRET,
});
const received = new Set<string>();
type WebhookJob = {
webhookId: string;
type: string;
responseId: string;
};
async function enqueueOnce(job: WebhookJob): Promise<boolean> {
if (received.has(job.webhookId)) return false;
received.add(job.webhookId);
console.log("Queue this job:", job);
return true;
}
app.use(express.text({ type: "application/json" }));
app.post("/openai/webhook", async (req, res) => {
try {
const event = await client.webhooks.unwrap(req.body, req.headers);
const webhookId = req.header("webhook-id");
if (!webhookId) return res.status(400).send("Missing webhook-id");
const accepted = await enqueueOnce({
webhookId,
type: event.type,
responseId: event.data.id,
});
return res.sendStatus(accepted ? 202 : 200);
} catch (error) {
if (error instanceof OpenAI.InvalidWebhookSignatureError) {
return res.status(400).send("Invalid signature");
}
return res.sendStatus(500);
}
});
app.listen(8000, () => console.log("Webhook server: http://localhost:8000"));
예시의 Set은 한 프로세스에서 동작 원리를 확인하기 위한 임시 저장소입니다. 운영 환경의 enqueueOnce는 webhookId를 데이터베이스 고유 키로 저장한 뒤 후속 작업을 큐에 넣도록 교체합니다. 서명 검증이 끝나기 전에 본문을 파싱하거나 큐에 넣지 않습니다. OpenAI 웹훅 공식 문서도 공식 SDK의 unwrap()으로 원문 본문과 헤더를 함께 검증하는 방식을 안내합니다.
재시도와 중복 실행을 어떻게 막을까
웹훅 엔드포인트는 수신 사실을 저장한 뒤 빠르게 2xx를 반환해야 합니다. OpenAI 공식 문서에 따르면 몇 초 안에 성공 응답이 없거나 2xx가 아니면 지수 백오프로 최대 72시간 재시도합니다. 3xx 리다이렉트는 따라가지 않고 실패로 처리됩니다.
같은 웹훅 이벤트가 중복 전달될 수도 있습니다. 요청 헤더의 webhook-id를 데이터베이스 고유 키로 사용하면 같은 이벤트가 두 번 큐에 들어가는 것을 막을 수 있습니다. 수신 기록 저장과 큐 등록은 하나의 트랜잭션으로 묶고, 이미 처리한 키라면 추가 작업 없이 2xx를 반환합니다.
| 상황 | 반환 코드 | 내부 처리 |
|---|---|---|
| 서명 오류 | 400 |
기록하지 않고 보안 로그 남김 |
| 처음 받은 이벤트 | 202 |
고유 키 저장 후 작업 큐 등록 |
| 이미 받은 이벤트 | 200 |
후속 작업을 다시 만들지 않음 |
| 저장소·큐 장애 | 500 |
OpenAI 재시도를 받도록 실패 처리 |
결과를 이메일 전송이나 데이터 수정으로 연결한다면 응답 ID에도 멱등성 기준을 둡니다. 서로 다른 완료 이벤트가 들어오더라도 같은 응답 결과가 외부 시스템에 한 번만 반영되게 해야 합니다. 웹훅 중복 제거와 업무 결과 중복 제거는 별개의 두 단계입니다.
배포 전에 확인할 운영 체크리스트
- 웹훅 URL이 공개 주소이며 리다이렉트 없이 바로 응답하는지 확인합니다.
response.completed,response.failed,response.cancelled,response.incomplete를 각각 내부 상태로 매핑합니다.- signing secret을 코드나 로그에 남기지 않고 노출 시 대시보드에서 교체합니다.
webhook-id고유 제약과 큐 등록이 함께 성공하는지 확인합니다.- 완료 이벤트를 받은 작업자가 응답 ID로 결과를 조회하고
output_text를 저장하는지 확인합니다. - 민감한 입력을 처리한다면 프로젝트의 데이터 보존 설정과 background mode 보존 동작을 배포 전에 다시 확인합니다.
첫 배포에서는 완료된 결과를 내부 저장소에 적재하는 단계까지만 연결합니다. 같은 웹훅을 두 번 보내도 레코드와 큐 작업이 하나만 생기는지 시험합니다. 그 검증이 끝난 뒤 이메일 발송이나 외부 데이터 수정처럼 되돌리기 어려운 후속 동작을 추가합니다.

댓글 남기기