-
[OpenAI][Responses API] response.completed webhook을 받을 때 retrieve(resp.id) 재조회와 자체 감사 로그를 어떤 순서로 나누나기타개발지식/풀스택개발 2026. 7. 8. 09:17
IT 리서치 노트
[OpenAI][Responses API] response.completed webhook을 받을 때 retrieve(resp.id) 재조회와 자체 감사 로그를 어떤 순서로 나누나
Responses API에서 background 작업을 webhook으로 받기 시작하면 가장 많이 꼬이는 지점은 `response.completed` 이벤트, `responses.retrieve(resp.id)` 재조회, 자체 감사 로그를 같은 저장 흐름으로 취급하는 순간이다. 2026년 7월 8일 기준 OpenAI 공식 문서를 다시 보면 webhook 예시 코드는 `event.data.id`를 꺼낸 뒤 다시 `responses.retrieve(response_id)`를 호출하고, background mode는 polling을 위해 응답 상태를 대략 10분 저장한다고 설명한다. 이 글은 완료 이벤트를 받을 때 어떤 채널은 짧게 두고, 어떤 채널만 감사 로그로 남겨야 덜 꼬이는지 정리한 것이다.
1. 개요
결론부터 말하면
response.completedwebhook은 완료 신호 채널이고,responses.retrieve(response_id)는 최종 결과 조회 채널이며, 자체 감사 로그는 최소 증빙 채널이다. 이 셋을 같은 저장소에 그대로 밀어 넣으면 중복 저장과 과도한 본문 적재가 동시에 생기기 쉽다. 운영 기준은response_id를 join key로 삼고, webhook receipt, retrieve 결과, audit metadata를 다른 테이블이나 다른 수명 정책으로 나누는 편이 맞다.이미 background mode와 webhook 기본 흐름 글이 요청 생성, 상태 확인, webhook 검증을 한 번에 다뤘다면, 이번 글은 그 다음 단계인 저장 분리와 idempotency에 가깝다. 또 background mode 10분 보관과 감사 로그 분리 글을 읽었다면, 오늘은 ZDR 여부보다 실제 webhook 처리 순서에 더 가까운 운영 분기다.
2. 어디서 실제로 막히는가
현장에서 자주 꼬이는 지점은 네 가지다. 첫째,
response.completed이벤트가 왔으니 payload 안에 최종 응답 본문까지 들어 있다고 가정하고 바로 장기 저장한다. 둘째, webhook 처리와responses.retrieve를 둘 다 돌리면서 idempotency 키를 두지 않아 같은 작업 결과를 두 번 반영한다. 셋째, background polling용 임시 상태와 장기 감사 로그를 같은 retention으로 둔다. 넷째, retrieve 실패와 감사 로그 실패를 같은 오류로 기록해 어느 단계에서 다시 실행할지 불분명해진다.OpenAI webhooks 가이드는
response.completed예시에서event.data.id를 받은 뒤 다시client.responses.retrieve(response_id)를 호출한다. background 가이드는 polling을 위해 response data가 대략 10분 저장된다고 설명한다. deployment checklist는background=True가store=True를 요구한다고 적고, your-data 가이드는 abuse monitoring logs와 application state를 अलग 층으로 설명한다. 이 네 개를 같이 읽으면 webhook receipt, retrieve 결과, 자체 감사 로그를 같은 저장소로 몰아넣을 이유가 줄어든다.특히 배치 작업, 긴 tool run, 요금 계산 같은 비동기 워크플로에서는 완료 이벤트 재전송이나 재조회 실패가 종종 함께 온다. 이때 webhook payload를 진실의 원본으로 둘지, retrieve 결과를 원본으로 둘지, 장기 감사에 본문 전체를 남길지부터 정하지 않으면 문제 재현이 더 어려워진다. 결과적으로 운영자는 로그를 많이 남겼는데도 실제로는 어떤 단계가 실패했는지 설명하지 못하는 상태가 된다.
- 증상: 같은 response가 webhook과 polling 모두에서 반영되어 중복 저장된다.
- 실패: webhook payload를 최종 결과 본문과 같은 층으로 저장한다.
- 막힘: retrieve 재조회 실패와 감사 로그 적재 실패를 분리하지 않는다.
- 누락: response_id 기준 idempotency와 retention 정책을 문서로 남기지 않는다.
상황 먼저 볼 곳 판단 기준 완료 이벤트가 도착했다 webhook signature, response_id 이벤트 receipt만 먼저 기록한다 최종 결과를 저장해야 한다 responses.retrieve(response_id) 최종 status와 usage를 별도로 조회한다 감사 로그를 남겨야 한다 response_id, model, status, usage 본문 전체 대신 최소 메타데이터를 남긴다 3. 실무에서 적용하는 순서
운영 순서는 다섯 단계로 고정하는 편이 가장 짧다. 먼저 webhook endpoint에서 raw body와 서명 검증을 통과한
response.completed만 receipt 로그에 적는다. 두 번째로response_id가 이미 처리된 작업인지 idempotency 저장소에서 확인한다. 세 번째로 아직 처리되지 않았다면responses.retrieve(response_id)를 호출해 최종 status, usage, output 존재 여부를 확인한다. 네 번째로 retrieve 결과에서 장기 보관이 필요한 최소 메타데이터만 audit bucket에 쓴다. 마지막으로 background polling 창이 끝나면 짧은 receipt 로그와 일시 저장 결과를 만료시킨다.- webhook에서는
response.completed와 서명 검증 결과만 먼저 기록한다. response_id기준으로 이미 반영된 작업인지 확인한다.responses.retrieve(response_id)로 최종 status와 usage를 다시 조회한다.- 장기 감사 로그에는 최소 메타데이터만 남긴다.
- polling용 임시 상태와 receipt 로그는 짧은 수명으로 정리한다.
실제로 팀 문서에는 어떤 버튼을 눌러 webhook endpoint 설정을 열었는지, 어떤 응답 상태를 조회했는지, 어떤 필드가 audit bucket으로 갔는지, 어떤 필드가 임시 저장소에서 사라졌는지까지 남겨 두는 편이 좋다. 그래야 운영 중
response.completed가 왔는데 결과가 안 보이는 상황에서도 webhook 수신 실패인지, retrieve 실패인지, audit 적재 실패인지 순서대로 나눌 수 있다.{ "response_id": "resp_123", "webhook_receipt_saved": true, "retrieve_status": "completed", "audit_saved_fields": ["response_id", "model", "status", "usage"], "temporary_result_ttl": "10m_window_or_shorter" }핵심은 webhook 자체를 진실의 원본으로 삼지 않는 것이다. OpenAI 예시처럼 webhook은
response_id를 전달하는 완료 신호이고, 최종 결과는 retrieve에서 다시 확인한다. 이 원칙을 먼저 잡아 두면 output 본문을 어느 버킷에 몇 분 두고, 어떤 요약만 오래 보관할지 더 명확해진다.4. 공식 문서와 예시 화면으로 확인하기
첫 자료는 OpenAI webhooks 가이드다. 여기서는 background response가 끝났을 때 `response.completed` 이벤트를 받을 수 있고, webhook endpoint는 raw body와 서명 검증을 기준으로 처리해야 한다는 흐름이 먼저 나온다.
즉 webhook은 결과를 '받아 적는 저장소'라기보다 완료 신호를 전달하는 채널이다. 이 지점부터 webhook payload 자체, 이어지는 `responses.retrieve`, 사내 감사 로그를 한 버킷에 몰아넣지 않는 설계가 중요해진다.
두 번째 자료는 같은 가이드의 서버 예시 코드다. 문서는 webhook에서 `event.data.id`를 받고, 그 `response_id`로 다시 `client.responses.retrieve(response_id)`를 호출하는 패턴을 직접 보여 준다.
이 코드가 중요한 이유는 webhook payload와 최종 응답 본문을 같은 객체로 가정하면 안 된다는 점을 분명히 보여 주기 때문이다. 사내 시스템은 `response_id`를 join key로 삼고, 어떤 단계에서 어떤 필드를 저장할지 따로 정해야 중복 반영과 과도한 로그 적재를 줄일 수 있다.
세 번째 자료는 background mode 가이드의 보관 경계다. OpenAI는 polling을 위해 response data를 대략 10분 동안 저장한다고 적고, ZDR과는 호환되지 않는다고 명시한다.
여기서 바로 읽어야 할 포인트는 polling용 임시 상태와 사내 감사 보관을 같은 수명으로 둘 이유가 없다는 점이다. background 상태는 짧은 재조회 창이고, 감사 로그는 더 좁은 최소 메타데이터 규칙으로 따로 관리해야 한다.
네 번째 자료는 deployment checklist의 비동기 경계다. 이 문서는 `background=True`가 `store=True`를 요구한다고 적고, 긴 작업은 background로 보내되 polling과 상태 관리를 염두에 두라고 설명한다.
즉 background 완료 신호를 webhook으로 받는 경로는 애초에 상태 보관을 전제로 한다. 그래서 이 경로의 운영 로그는 '무엇을 영구 저장할지'보다 '무엇을 짧게 재조회하고 언제 버릴지'를 먼저 정하는 편이 맞다.
다섯 번째 자료는 your-data 가이드의 로그 분류다. OpenAI는 abuse monitoring logs와 application state를 구분해서 설명하고, 응답 상태와 고객 보관 통제가 서로 다른 층위임을 보여 준다.
이 분리는 사내 감사 로그에도 그대로 도움이 된다. webhook, retrieve, background state, 자체 감사 로그를 모두 '로그'라고 뭉뚱그리면 실제 저장 목적과 만료 기준이 섞여 버린다.
실무에서는 채널별 역할을 표로 고정해 두는 편이 가장 빠르다. webhook은 완료 신호, retrieve는 최종 본문 조회, 감사 로그는 최소 증빙, background state는 짧은 polling 창이라는 식으로 칸을 갈라 놓으면 저장 위치가 덜 흔들린다.
이렇게 역할을 분리하면 같은 response에 대해 어느 단계에서 무엇을 클릭하고, 무엇을 조회하고, 무엇을 저장했는지 팀원이 다시 설명하기 쉬워진다. 이미 background mode와 webhook 기본 흐름 글을 읽었다면, 이번 표는 그 뒤 저장 책임을 더 촘촘하게 나누는 단계다.
마지막 자료는 중복 저장을 막는 간단한 처리 예시다. webhook 이벤트를 받았다고 바로 최종 본문을 장기 저장하지 않고, 먼저 `response_id` 기준으로 idempotency를 확인한 뒤 retrieve와 감사 로그 단계를 따로 호출하는 흐름을 남기는 편이 좋다.
이 패턴을 쓰면 같은 `response.completed`가 재전송되더라도 중복 적재를 줄일 수 있다. 또 retrieve 실패와 감사 로그 실패를 अलग状態로 남겨 어느 단계에서 다시 실행해야 하는지도 금방 판단할 수 있다.
5. 주의사항과 리스크
첫 번째 리스크는 webhook payload만 믿고 최종 결과 조회를 생략하는 것이다. 두 번째 리스크는
response_id기준 idempotency가 없어 webhook 재전송과 수동 polling이 동시에 같은 결과를 적재하는 것이다. 세 번째 리스크는 background 임시 상태와 감사 로그의 만료 기준을 하나로 묶어 불필요하게 본문을 오래 보관하는 것이다.또 retrieve가 일시적으로 실패했다고 webhook 자체를 실패로 기록하면 재처리 경로가 섞인다. webhook receipt, retrieve 결과, audit save 결과를 다른 상태값으로 남겨 두면 어디서 다시 클릭하고, 무엇을 다시 실행하고, 어느 저장소만 정리하면 되는지 분명해진다.
- 주의: webhook 이벤트와 최종 retrieve 결과는 같은 객체가 아니다.
- 리스크: idempotency가 없으면 response.completed 재전송에 취약해진다.
- 기준: 장기 감사 로그는 최소 메타데이터 중심으로 줄인다.
6. 결론
response.completedwebhook을 받을 때는 webhook receipt,responses.retrieve(response_id), 자체 감사 로그를 같은 저장소로 취급하지 않는 편이 맞다.response_id를 join key로 삼고, webhook은 완료 신호, retrieve는 최종 결과 조회, audit는 최소 증빙이라는 역할을 고정하면 background 작업 운영이 훨씬 덜 꼬인다.여기서 한 단계 더 들어가면 같은 완료 이벤트가 다시 들어올 때 어떤 키를 먼저 dedupe해야 하는지가 남는다. 그 분기까지 같이 보고 싶다면 webhook 재전송이 올 때 webhook-id와 response_id 중 무엇을 먼저 고정해야 하는지 정리한 후속 글이 바로 다음 단계다.
- webhook에서는
response_id와 서명 검증 결과만 먼저 남긴다. - 최종 결과는
responses.retrieve(response_id)로 다시 조회한다. - 장기 감사 로그는 model, status, usage 같은 최소 필드만 저장한다.
7. 참고 링크
'기타개발지식 > 풀스택개발' 카테고리의 다른 글