OpenAI Structured Outputs 설계, JSON Schema와 거부 응답을 함께 검증하는 법
AI를 활용하여 생성한 이미지입니다.
LLM 응답을 JSON으로 받는다고 해서 바로 프로그램 입력으로 써도 되는 것은 아니에요. JSON 모드는 문법적으로 올바른 JSON을 만드는 데 초점을 두지만, 필드가 빠지거나 값의 의미가 달라지는 문제까지 막아 주지는 않습니다. OpenAI의 Structured Outputs는 JSON Schema에 맞는 출력을 목표로 하고, Responses API에서는 text.format에 schema 형식을 지정하는 방식으로 사용합니다.[1][2]
JSON 모드와 Structured Outputs를 나눠요
| 방식 | 보장 범위 | 어울리는 경우 |
|---|---|---|
| 일반 텍스트 | 형식 제한 없음 | 사람이 읽는 답변 |
| JSON 모드 | 유효한 JSON을 목표로 함 | 간단한 파싱 결과 |
| Structured Outputs | 제공한 스키마 준수 | 검증된 객체를 다음 코드로 전달 |
| 함수 호출 strict | 도구 인자 스키마 준수 | 외부 동작을 호출하는 에이전트 |
OpenAI 문서도 Structured Outputs를 함수 호출과 응답 형식 두 가지로 나눠 설명해요. 모델이 사용자에게 구조화된 결과를 답하는지, 아니면 도구를 호출하기 위한 인자를 만드는지에 따라 설정 위치가 달라집니다.[1]
스키마를 작게 만들어요
처음부터 모든 예외를 하나의 거대한 스키마에 넣으면 수정과 테스트가 어려워져요. 실제 후속 코드가 반드시 사용하는 필드만 required로 두고, 선택 정보는 nullable 또는 별도 객체로 분리하는 편이 낫습니다. 예를 들어 문서 분류 결과라면 label, confidence, reason 정도로 시작하고, 화면 표시용 문장까지 같은 객체에 섞지 않는 방식이에요.
{
"type": "object",
"properties": {
"label": {"type": "string", "enum": ["billing", "technical", "other"]},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"reason": {"type": "string"}
},
"required": ["label", "confidence", "reason"],
"additionalProperties": false
}
strict 함수 호출을 사용할 때는 객체의 additionalProperties를 false로 두는 요구사항을 확인해야 해요. 함수 도구가 실제로 실행할 작업이라면, 스키마에 맞는 인자라는 사실과 실행해도 안전하다는 사실을 별도로 검사해야 합니다.[3]
스키마 검증과 업무 규칙 검증은 달라요
스키마는 자료형과 필드 구조를 확인하지만, 사업 규칙까지 대신 판단하지는 않아요. confidence가 0.9라는 숫자라도 근거 문서가 비어 있을 수 있고, 파일 경로가 문자열 형식에 맞아도 허용된 작업 폴더 밖을 가리킬 수 있습니다.
- API 응답 상태와 완료 여부를 확인합니다.
- 거부·중단·오류 응답을 정상 객체와 분리합니다.
- JSON을 파싱하고 JSON Schema 검증을 실행합니다.
- 허용된 enum, 범위, 길이, 참조 ID를 업무 코드에서 다시 확인합니다.
- 외부 변경을 일으키는 도구는 미리보기·승인·멱등성 검사를 거칩니다.
거부 응답을 정상 결과로 착각하지 않기
Structured Outputs를 쓰더라도 모델이 안전 정책이나 요청 조건 때문에 거부할 수 있어요. OpenAI 문서는 거부가 주어진 스키마를 따르지 않을 수 있으므로 응답에 별도의 refusal 정보가 포함될 수 있다고 설명합니다.[1] 따라서 파서가 “필수 필드가 없으니 기본값을 넣자”고 처리하면 안 됩니다. 거부는 사용자에게 안내하거나 사람 검토로 보내야 하는 별도 상태예요.
Responses API의 상태도 completed만 있는 것이 아니고 in_progress와 incomplete가 존재합니다. 최대 출력 토큰이나 콘텐츠 필터 같은 중단 원인을 확인한 뒤 재시도 여부를 정해야 합니다.[2]
함수 호출은 승인 경계를 따로 둬요
도구 인자는 스키마에 맞더라도 실제 실행은 서버의 권한으로 일어날 수 있습니다. OpenAI의 함수 호출 흐름은 모델이 함수 이름과 JSON 인자를 만들고, 애플리케이션이 그 호출을 실행한 뒤 결과를 다시 전달하는 구조예요.[3] 이 중간 단계에서 사용자 승인, 허용 목록, 인증 범위, 재시도 키를 점검해야 합니다.
- 읽기 함수와 쓰기 함수를 도구 목록에서 분리하기
- 도구 설명이 아니라 서버 코드에서 권한 확인하기
- 삭제·결제·외부 전송은 기본값을 미리보기로 두기
- 거부·incomplete·검증 실패를 자동 재시도와 분리하기
- 스키마 버전을 응답과 로그에 남겨 이전 클라이언트와 구분하기
여러 도구를 연속으로 호출하는 에이전트라면 도구 호출의 멱등성·재시도·병렬 실행 기준을 함께 설계해야 같은 작업이 중복 실행되지 않아요.
실제 운영 흐름을 분리해요
분류·추출처럼 읽기 작업은 Structured Outputs로 결과를 고정하고, 파일 수정이나 외부 API 호출은 함수 호출로 분리하면 승인 경계를 만들기 쉬워요. 전자는 결과 객체의 구조가 중요하고, 후자는 함수 이름·인자·실행 주체·실행 결과를 한 번 더 확인해야 합니다.[1][3] 같은 모델 응답이라도 후속 단계가 다르면 검증 기준도 달라져야 해요.
스키마를 배포할 때는 새 필드를 추가하는 변경과 기존 필드의 의미를 바꾸는 변경을 구분하세요. 새 필드는 이전 클라이언트가 무시할 수 있는지, enum 값을 늘렸을 때 소비 코드가 예외를 내지 않는지 확인해야 합니다. 외부 서비스에 전달하는 인자는 API 응답을 그대로 재사용하지 말고, 서버에서 필요한 필드만 다시 만들어 보내는 쪽이 안전합니다.
실패했을 때 되돌리는 기준
스키마를 바꾼 뒤 파싱 오류가 늘었다면 모델을 탓하기 전에 응답 상태, 실제 반환된 output 항목, 사용한 스키마 버전을 같은 로그에서 비교하세요. 기존 클라이언트가 새 필드를 이해하지 못한다면 필드를 즉시 삭제하기보다 버전을 나누고 이전 형식으로 변환하는 단계를 두는 편이 안전합니다. 검증 실패 객체를 자동으로 다시 도구에 넘기는 루프는 같은 오류를 반복하거나 외부 변경을 중복 실행할 수 있으니 횟수와 대상 작업을 제한해야 해요.
스키마에 통과한 값도 실행 직전에는 권한과 대상 리소스를 다시 확인하세요. 특히 파일 경로, URL, 사용자 ID처럼 외부 입력과 연결되는 값은 허용 목록과 현재 로그인 주체를 기준으로 검증해야 합니다. Structured Outputs는 이 마지막 정책 판단을 대신하지 않습니다.
[…] 대화와 실제 검증 가능한 결과를 분리하는 태도가 필요해요. 관련해서는 구조화된 출력과 JSON 스키마 검증 글도 참고할 수 […]
[…] 것과 여러분의 업무 결과가 유효한 것은 다른 조건입니다. 기존의 Structured Outputs 검증 글처럼 거부 응답과 필드 누락을 분리해 처리하면, “완료됐지만 사용할 수 […]