OpenAI Structured Outputs 설계, JSON Schema가 맞아도 실패하는 경우

색상별로 정렬된 구조화 데이터 블록을 보여주는 현실적인 사진풍 이미지 1

AI를 활용하여 생성한 이미지입니다.

애플리케이션에서 LLM의 응답을 바로 파싱해야 한다면 “JSON으로 답해 줘”라는 프롬프트만으로는 부족합니다. OpenAI의 Structured Outputs는 개발자가 정한 JSON Schema에 맞춰 응답 구조를 만들도록 돕는 기능입니다.[1] 필수 키나 enum처럼 형식에서 자주 깨지는 부분을 줄이는 데 유용하지만, 스키마를 등록했다고 모든 요청이 성공하거나 값이 사실이 되는 것은 아닙니다.

중급 단계에서 중요한 질문은 JSON이 나왔는지가 아니라 실패를 구분할 수 있는지입니다. 안전 정책에 따른 refusal, 출력 토큰 한도 초과, 파싱 오류, 스키마에는 맞지만 업무적으로 틀린 값은 서로 다른 처리 경로를 가져야 합니다.

Structured Outputs와 함수 호출 구분하기

목적 선택할 방식 결과를 사용하는 곳
최종 답변의 모양을 고정 Structured Outputs·response format UI, 저장, 후속 파서
외부 도구를 실행 Function calling 애플리케이션 도구 실행기
자유로운 설명 일반 텍스트 응답 화면·대화 로그

OpenAI 문서는 모델을 도구·함수·데이터에 연결할 때 function calling을, 사용자에게 반환할 응답 모양을 정할 때 구조화된 응답 형식을 쓰는 식으로 설명합니다.[1] 둘을 한 기능처럼 취급하면 도구 실행 결과와 화면 표시용 JSON이 섞이고, 어느 단계에서 검증해야 하는지 불분명해집니다.

작은 스키마로 시작하는 방법

첫 스키마는 화면이나 저장소가 당장 사용하는 필드만 포함하세요. 일정 추출이라면 제목, 시작일, 참석자처럼 후속 코드가 바로 읽는 값부터 정의합니다. 필드가 많아질수록 입력의 다양한 표현을 억지로 하나의 타입에 맞추게 되고, 실패 원인도 복잡해집니다.

{
  "type": "object",
  "properties": {
    "title": {"type": "string"},
    "priority": {"type": "string", "enum": ["low", "normal", "high"]},
    "tags": {"type": "array", "items": {"type": "string"}}
  },
  "required": ["title", "priority", "tags"],
  "additionalProperties": false
}

additionalProperties: false는 정의하지 않은 키를 임의로 추가하지 못하게 하는 제약입니다. strict 스키마를 사용할 때는 required 목록과 이 설정이 의도대로 작성됐는지 먼저 확인하세요. 선택적인 정보가 필요하다면 키가 사라지는 방식과 null을 허용하는 방식을 비교해, 파서가 어떤 상태를 받는지 명시해야 합니다.

형식 검증과 업무 검증은 다릅니다

Structured Outputs의 장점은 지정한 JSON Schema에 맞는 응답을 애플리케이션이 다루기 쉬워진다는 데 있습니다. OpenAI 문서는 required 키나 유효하지 않은 enum 값 문제를 줄이는 방향과 SDK의 Pydantic·Zod 스키마 사용을 안내합니다.[1] 그러나 priority가 high라는 enum을 통과했다고 실제 우선순위 판단이 옳다는 뜻은 아닙니다.

  • 날짜·시간대·통화가 업무 규칙에 맞는지 확인합니다.
  • 문자열이 비어 있지 않은지와 길이 제한을 검사합니다.
  • 배열의 항목 수와 중복이 서비스 정책을 넘지 않는지 봅니다.
  • 민감정보가 저장 대상에 들어오지 않았는지 확인합니다.
  • 외부 시스템을 실행할 권한과 승인 상태를 별도로 확인합니다.

따라서 파이프라인은 “모델 응답 → 스키마 검증 → 업무 규칙 검증 → 저장 또는 실행”처럼 나누는 것이 좋습니다. 두 번째 검증을 통과하지 못하면 자동 저장이나 자동 실행을 멈추고 사람이 확인할 수 있는 상태로 남겨야 합니다.

실패를 세 가지로 나누기

1. 안전 정책에 따른 refusal

모델이 안전상의 이유로 요청을 거부하면 일반적인 스키마 객체 대신 명시적인 refusal 신호가 반환될 수 있습니다. OpenAI 문서는 SDK 예시에서도 객체를 파싱하기 전에 refusal 여부를 확인하도록 안내합니다.[1] 객체가 아니라고 곧바로 같은 요청을 재시도하면 거부 이유를 무시하게 되므로, 사용자에게 안내하거나 안전한 대체 흐름으로 보내야 합니다.

2. 출력 길이 초과

출력 한도에 도달하면 JSON이 닫히기 전에 응답이 끝날 수 있습니다.[1] 잘린 문자열을 부분 데이터로 저장하지 말고 finish reason이나 SDK가 제공하는 완료 상태를 검사하세요. 출력 필드를 줄이거나 긴 입력을 나누고, 재요청 시에는 이미 처리된 부분을 중복 실행하지 않도록 설계해야 합니다.

3. 스키마와 입력의 의미 불일치

날짜를 문자열 하나로만 정의했는데 사용자가 반복 일정, 여러 날짜, 시간대를 함께 입력할 수 있다면 형식은 맞아도 정보가 사라집니다. start_date, end_date, timezone처럼 의미를 분리하거나 “표현할 수 없는 경우”를 명시적인 상태로 모델링하세요.

프로덕션에 넣는 순서

  1. 입력 단계에서 필수값과 개인정보를 검사합니다.
  2. 화면이 실제로 쓰는 작은 스키마로 응답을 받습니다.
  3. SDK 파서 또는 JSON Schema 검증기를 통과시킵니다.
  4. refusal·불완전 응답·파싱 실패를 서로 다른 상태로 기록합니다.
  5. 날짜·통화·권한·중복 같은 업무 규칙을 다시 확인합니다.
  6. 모든 검증이 끝난 값만 데이터베이스나 외부 도구로 전달합니다.

재시도는 실패 원인에 따라 달라야 합니다. refusal은 안전한 대체 흐름으로 보내고, 길이 초과는 입력 분할이나 출력 축소로 처리하세요. 같은 입력과 스키마에서 반복해서 실패한다면 네트워크 문제가 아니라 스키마 설계나 입력 전처리 문제일 수 있습니다.

테스트 케이스를 정상 입력만으로 만들지 않기

짧고 정상적인 한 문장만 통과하는지 확인하면 운영 장애를 놓치기 쉽습니다. 빈 문자열, 아주 긴 문서, enum 밖의 표현, 여러 날짜가 섞인 문장, 필수 필드가 없는 요청, 안전상 거부될 수 있는 요청을 각각 준비하세요. OpenAI 문서가 평가 모음을 만들고 모델 변경 때 동작을 확인하는 방식을 권장하는 이유도 이 때문입니다.[1]

기록에는 모델 이름만 남기지 말고 스키마 버전, 입력 유형, 응답 상태, 파싱 결과, 업무 규칙 오류를 포함하세요. 그래야 모델이나 SDK를 바꾼 뒤 “JSON이 나왔다”는 사실만으로 품질이 개선됐다고 오판하지 않습니다.

사용하지 않는 편이 나은 경우

  • 사람이 읽고 버릴 일회성 답변
  • 매번 구조가 달라져야 하는 브레인스토밍
  • 애플리케이션이 자유 형식의 글만 보여 주는 경우
  • 검증 후 외부 시스템 실행을 통제할 승인·권한 설계가 없는 경우

구조화된 출력은 파싱 문제를 줄이는 도구이지 모델의 판단을 승인하는 장치가 아닙니다. 결제, 삭제, 권한 변경처럼 되돌리기 어려운 작업에는 사람이 승인하거나 별도 정책 엔진을 통과시키는 단계가 필요합니다. API 키를 코드나 채팅에 노출하지 않는 기본 원칙은 AI API 키를 채팅창에 넣으면 안 되는 이유와 안전한 보관법도 참고하세요.

적용 체크리스트

  1. 최종 답변 구조와 도구 호출을 서로 다른 흐름으로 나눕니다.
  2. required와 additionalProperties: false를 검사합니다.
  3. null·빈 값·누락의 의미를 애플리케이션에서 정합니다.
  4. refusal과 토큰 부족을 파싱 실패와 구분합니다.
  5. 형식 검증 뒤 날짜·단위·권한을 다시 검사합니다.
  6. 긴 입력과 예외 입력을 포함한 평가 케이스를 유지합니다.

스키마가 바뀔 때의 운영 규칙

처음부터 모든 필드를 완벽하게 정하기보다 스키마에 버전을 붙이고 변경 기록을 남기세요. 필드 이름을 바꾸거나 enum 값을 삭제하면 기존 파서와 저장 데이터가 함께 깨질 수 있습니다. 새 필드를 추가할 때도 소비하는 코드가 없는 응답을 어떻게 처리할지 정하고, 구버전 응답을 읽는 호환 기간을 두는 것이 좋습니다.

관측 항목도 형식 검증만으로 끝내지 마세요. 요청 유형별 refusal 비율, 길이 초과 비율, 업무 규칙 오류, 재시도 횟수를 따로 기록하면 어느 문제가 모델인지 입력인지 스키마인지 구분할 수 있습니다. 개인정보가 포함된 입력은 로그에 원문으로 남기지 말고 식별 가능한 값은 마스킹하세요.

자주 묻는 질문

JSON Schema가 맞으면 사실도 맞나요?

아닙니다. 스키마는 타입과 구조를 확인합니다. 날짜가 실제 일정과 맞는지, 통화와 권한이 업무 규칙에 맞는지는 별도 검증이 필요합니다.

파싱 오류면 무조건 재시도하면 되나요?

아닙니다. refusal, 출력 길이 초과, 스키마와 입력의 의미 불일치는 서로 다른 대응이 필요합니다. 원인을 기록한 뒤 그에 맞는 경로로 보내세요.

필드를 많이 넣을수록 정확해지나요?

필드가 많으면 필요한 정보를 표현할 수 있지만, 입력과 검증이 복잡해집니다. 화면이나 저장소가 바로 사용하는 필드부터 시작하고 필요할 때 확장하세요.

기초 개념부터 구조화 출력을 이해하려면 AI가 JSON 형식을 자꾸 바꿀 때, 구조화된 출력을 쓰는 법도 함께 볼 수 있습니다.

Sources

  1. [1] OpenAI API, Structured model outputs

함께 읽을 글