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

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

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

LLM의 응답을 애플리케이션에서 바로 파싱해야 한다면 “JSON으로 답해 줘”라는 프롬프트만으로는 부족해요. OpenAI의 Structured Outputs는 개발자가 정한 JSON Schema에 맞춰 응답 구조를 만들도록 하는 기능이고, 일반 JSON 모드와 달리 필수 키 누락이나 잘못된 enum 값을 줄이는 목적이 있습니다.[1]

다만 스키마를 등록했다고 모든 요청이 성공하는 건 아닙니다. 사용자 입력을 거부할 수 있고, 출력 토큰 한도에 걸려 응답이 중간에 끝날 수 있으며, 스키마 자체가 지원 범위를 벗어날 수도 있어요.[1] 중급 단계에서는 “JSON이 나왔는가”보다 “내 코드가 실패 유형을 나누어 처리하는가”가 더 중요합니다.

Structured Outputs와 함수 호출을 나누기

목적 선택할 방식 결과를 쓰는 곳
모델의 최종 답변을 일정한 구조로 받기 Structured Outputs / response format UI, 저장 로직, 후속 파서
모델이 외부 도구나 함수를 호출하게 하기 Function calling 애플리케이션의 도구 실행기
자유로운 설명문이 필요함 일반 텍스트 응답 사용자 화면이나 대화 로그

OpenAI 문서는 모델이 도구·함수·데이터에 연결될 때는 function calling을, 모델이 사용자에게 돌려주는 응답 모양을 정할 때는 structured response format을 쓰는 식으로 구분해 설명해요.[1] 둘을 한 기능처럼 취급하면 도구 실행 결과와 사용자 표시용 JSON이 뒤섞입니다.

스키마를 작게 시작하는 이유

첫 스키마는 화면이나 저장소가 정말 필요한 필드만 포함하는 편이 좋아요. 예를 들어 일정 추출이라면 name, date, participants처럼 후속 코드가 즉시 쓰는 값부터 정의합니다. 문서 예시도 객체의 속성, 필수 필드, 배열 항목 타입을 명시하는 형태를 보여 줍니다.[1]

{
  "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을 허용하는 구조처럼, 파서가 항상 같은 키를 받을 수 있게 설계하는 편이 안전합니다.

strict가 보장하는 것과 보장하지 않는 것

보장에 가까운 부분

Structured Outputs의 장점은 응답이 지정한 JSON Schema에 맞는지 애플리케이션이 예측하기 쉬워진다는 데 있어요. OpenAI 문서는 required key가 빠지거나 유효하지 않은 enum 값이 나오는 문제를 줄이고, SDK에서 Pydantic과 Zod로 스키마를 정의할 수 있다고 설명합니다.[1]

그래도 직접 검사해야 하는 부분

  • 문장 속 사실이 맞는지
  • 날짜·통화·단위가 업무 규칙에 맞는지
  • 배열의 항목 수가 서비스 정책을 넘지 않는지
  • 문자열이 비어 있지 않은지
  • 사용자 입력이 민감정보를 포함하지 않는지

스키마 검증은 “형태” 검증이에요. 예를 들어 priorityhigh라는 enum으로 통과해도 실제 우선순위 판단이 옳다는 뜻은 아닙니다. 형식 검증 뒤에 업무 규칙 검증을 두고, 실패하면 자동 저장이나 자동 실행을 멈추세요.

실패 유형을 세 갈래로 나누기

1. 안전 정책에 따른 refusal

모델이 안전상의 이유로 요청을 거부하면 일반적인 스키마 객체 대신 거부 신호가 돌아올 수 있습니다. OpenAI 문서는 거부를 프로그램에서 감지할 수 있는 명시적 refusal로 설명하고, SDK 예시에서도 refusal 여부를 먼저 확인하도록 안내해요.[1] 파서가 객체가 아니라고 곧바로 재시도하는 대신, 사용자에게 거부를 표시하거나 안전한 대체 흐름으로 보내야 합니다.

2. 출력 길이 초과

응답이 max_tokens 또는 해당 API의 출력 한도에 도달하면 JSON이 닫히기 전에 끝날 수 있어요. 문서의 edge case 설명도 토큰 한도에 도달하면 응답이 불완전할 수 있다고 적습니다.[1] 이때는 잘린 문자열을 부분 데이터로 저장하지 말고 finish reason이나 SDK가 제공하는 완료 상태를 검사한 뒤, 출력량을 줄이거나 한 번에 요구하는 필드를 나눠 재요청하세요.

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

스키마가 날짜를 문자열 하나로만 정의했는데 사용자는 반복 일정, 시간대, 여러 날짜를 입력할 수 있어요. 형식은 맞더라도 정보가 소실됩니다. 여러 의미를 한 필드에 욱여넣지 말고 start_date, end_date, timezone처럼 애초에 데이터 구조를 나누세요.

프로덕션 파이프라인에 넣는 순서

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

재시도도 무조건 반복하지 마세요. 같은 입력과 같은 스키마로 계속 실패하면 원인은 네트워크가 아니라 설계일 수 있습니다. refusal은 안전 흐름으로, 길이 초과는 입력 분할로, 스키마 불일치는 필드 재설계로 보내야 합니다.

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

정상적인 한 문장 입력 하나만 통과하는지 확인하면 실제 장애를 놓칩니다. 빈 문자열, 아주 긴 문서, enum 밖의 표현, 여러 날짜가 섞인 문장, 필드가 없는 요청, 안전상 거부될 수 있는 요청을 각각 준비하세요. OpenAI 문서도 프롬프트 동작을 측정할 평가 모음을 만들고 모델 변경 때 동작을 확인하는 방식을 권장합니다.[1]

테스트 결과에는 모델 이름만 남기지 말고 스키마 버전, 입력 유형, 응답 상태, 파싱 결과, 업무 규칙 오류를 함께 기록하세요. 그래야 모델을 바꾼 뒤 “JSON이 나왔다”는 이유만으로 품질이 좋아졌다고 오판하지 않아요.

이 방식을 쓰지 않는 편이 나은 경우

  • 사람이 읽고 바로 버릴 일회성 답변
  • 출력 구조가 매번 달라야 하는 브레인스토밍
  • 애플리케이션이 사실상 자유 형식의 글만 보여 주는 경우
  • 스키마를 검증한 뒤에도 외부 시스템 실행을 통제할 별도 권한 설계가 없는 경우

구조화된 출력은 파싱 문제를 줄이는 도구이지, 모델의 판단을 승인하는 장치가 아닙니다. 특히 결제·삭제·권한 변경처럼 되돌리기 어려운 작업에는 사람이 승인하거나 별도 정책 엔진을 거치는 단계가 필요해요.

적용 체크리스트

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

지금 JSON 파서를 붙이는 중이라면 스키마를 크게 만들기보다 화면이 당장 쓰는 필드 세 개 정도로 시작하세요. 그 다음 refusal, 잘린 응답, 유효하지만 틀린 값까지 서로 다른 실패로 분리하면 운영 중 원인을 훨씬 빨리 찾을 수 있어요.

관련 글

Sources

  1. OpenAI API, Structured model outputs [1]

함께 읽을 글