구조화 출력 JSON, 실제로 스키마를 지키는가?
결론부터: "JSON이 나온다"와 "스키마를 지킨다"는 다른 문제다. OpenAI 문서에 따르면 복잡한 JSON Schema 준수 평가에서 gpt-4o-2024-08-06은 거의 모든 테스트 케이스를 통과한 수준으로 보고됐고, 같은 평가에서 gpt-4-0613은 40% 미만이었다 출처. 즉 모델·API 경로에 따라 준수율이 크게 갈린다는 뜻이고, 이는 "구조화 출력은 항상 동일하게 작동한다"는 가정을 깨는 근거다.
핵심 정리
- JSON mode는 파싱 가능한 JSON만 보장하고 필수 키·enum·자료형 준수는 보장하지 않는다.
- 스키마 준수가 필요하면 OpenAI는
json_schema+strict: true, Gemini는responseSchema를 써야 한다. - 프롬프트를 목표·제약·형식·평가 네 블록으로 분리하면 재현성과 디버깅이 쉬워진다.
- 평가는 구문 유효성·구조 준수·의미 정확도·누락률·환각률·운영 비용을 분리해서 측정해야 한다.
- 지원 모델·API 버전·JSON Schema 하위 집합은 계속 바뀌므로 "항상 100% 작동"이라는 일반화는 피해야 한다.
이번 실험에서 검증하고 싶었던 것은 단순했다. "스키마를 강제하는 옵션을 켜면, 안 켰을 때보다 실제로 더 안정적인 JSON이 나오는가. 그리고 그 안정성을 어떻게 숫자로 확인할 수 있는가." 겉보기엔 당연해 보이지만, 막상 측정 지표를 정하려니 "JSON이 파싱되는가"와 "필드 값이 맞는가"를 섞어서 보고하는 실수가 먼저 나왔다. 그래서 측정 축을 먼저 쪼개는 데 실험의 절반을 썼다.
목표·제약·형식을 어떻게 나눠 설계하나?
프롬프트를 하나의 긴 지시문으로 쓰지 않고 네 블록으로 분리하는 게 검증하기 쉬웠다. 목표는 "무엇을 추출·분류·변환할지" 한 문장으로 못박는다. 제약은 "근거 없는 값은 null, 날짜는 ISO 8601, 허용 enum 목록, 최대 항목 수"처럼 실패 조건을 구체적으로 적는다. 형식은 필드명·자료형·필수 여부·배열/중첩 구조를 JSON Schema로 고정하는 부분이고, 평가는 이 세 블록이 실제로 지켜졌는지 확인하는 별도 단계다.
이렇게 나누니 실패가 났을 때 "어느 블록이 문제였는가"를 추적할 수 있었다. 예를 들어 enum 값이 스키마 바깥에서 나오는 경우는 제약 블록의 설명 부족이었고, 필수 키가 통째로 빠지는 경우는 형식 블록의 중첩 구조 표현이 모호했던 경우가 많았다. 목표 문장이 애매하면 의미 정확도가 떨어졌는데, 이건 스키마를 아무리 정교하게 짜도 해결되지 않는 영역이었다.
JSON mode와 Structured Outputs, 구현에서 뭐가 다른가?
JSON mode는 "파싱 가능한 JSON을 내보낸다"만 보장하고, 키가 빠지거나 자료형이 틀려도 통과한다. 반면 OpenAI의 json_schema 기반 Structured Outputs나 strict: true 함수 호출은 스키마 자체를 생성 제약으로 사용한다 출처. Gemini도 비슷하게 application/json만 지정하는 JSON mode는 특정 구조를 보장하지 않고, responseSchema를 함께 써야 구조가 강제된다고 문서에 명시돼 있다 출처.
구현은 세 단계로 나뉜다. 스키마를 정의하고, API 요청에 전달하고, 거부·잘림·안전성 예외 같은 엣지 케이스를 처리하는 것. 실제로 막혔던 지점은 세 번째였다. 모델이 안전 정책에 걸려 거부 응답을 내보내면 JSON 자체가 생성되지 않는데, 이 경우를 "스키마 위반"으로 집계하면 평가 지표가 왜곡된다. 거부와 스키마 위반을 분리해서 집계해야 한다는 걸 뒤늦게 깨달았다.
Gemini 쪽에서는 Google GenAI SDK가 Python Pydantic, JS/TS Zod를 지원해 스키마를 코드로 선언할 수 있었고 출처, 2025년 업데이트 이후 Gemini 2.5 이상에서는 스키마의 키 순서를 출력에서도 유지하고 anyOf, $ref, minimum, maximum, additionalProperties, type: null, prefixItems 지원 범위가 넓어졌다고 밝혔다 출처. 다만 이 지원 범위는 모델·API 버전에 따라 달라지므로, 사용 전에 실제 스냅샷에서 직접 확인하는 과정을 생략할 수 없었다.
무엇을 측정해야 품질을 판단할 수 있나?
"JSON이 나왔는가" 하나로 뭉뚱그리면 품질 판단이 불가능했다. 실험에서 분리한 지표는 이렇다.
| 지표 | 확인 방법 |
|---|---|
| 구문 유효성 | JSON parser 통과율 |
| 구조 준수 | JSON Schema validator 통과율 |
| 의미 정확도 | 정답 데이터와 필드별 일치율 |
| 누락률 | 필수 키가 빠진 비율 |
| 환각률 | 원문에 없는 값이 생성된 비율 |
| 운영 비용 | 평균 출력 토큰, 요청당 비용, 재시도 횟수, 지연시간 |
이 여섯 가지를 따로 기록하니 재미있는 패턴이 나왔다. 구조 준수율은 높은데 의미 정확도가 낮은 케이스, 즉 "스키마는 맞는데 내용이 틀린" 응답이 꽤 있었다. 이건 Structured Outputs가 형식을 강제할 뿐 사실성을 보장하지 않는다는 걸 그대로 보여주는 결과였다. 또한 측정할 때는 모델명과 스냅샷, API 경로, temperature·seed, 입력 문서 수, 스키마 버전, 검증 라이브러리, 재시도 정책, 표본 수를 같이 기록해야 다음 실험에서 재현할 수 있었다.
공개 벤치마크는 어떤 근거를 주나?
가장 참고가 됐던 건 2025년 공개된 JSONSchemaBench다. 실제 사용 사례에서 수집한 10,000개 JSON Schema를 대상으로 제약 준수 생성의 효율성, 다양한 제약 유형 지원, 출력 품질을 평가했고, Guidance·Outlines·llama.cpp·XGrammar·OpenAI·Gemini 등 6개 구조화 생성 프레임워크를 비교 대상으로 삼았다 출처. 이 벤치마크를 인용할 때 주의할 점은 "어떤 제품이 모든 데이터에서 가장 낫다"는 결론으로 쓰지 않는 것이다. 모델 버전, 디코딩 구현, 스키마 구성에 따라 결과가 바뀌기 때문에, 동일한 프롬프트와 동일한 스키마로 재현 실험을 하는 게 더 중요한 메시지였다.
이 글에서 인용한 OpenAI 수치 역시 같은 전제가 붙는다. 모델 버전과 평가 조건이 바뀌면 숫자도 바뀐다. 그래서 내부 실험에서는 공개 벤치마크 수치를 "방향성 참고"로만 쓰고, 실제 판단은 자체 스키마·자체 데이터로 재측정한 값을 기준으로 삼았다.
이 접근은 어디서 막히나?
가장 먼저 부딪힌 한계는 스키마 문법의 부분 지원이었다. OpenAI Structured Outputs는 JSON Schema 전체가 아니라 지원되는 일부 문법만 처리하므로, 복잡한 중첩 구조나 특정 키워드를 쓰면 요청 자체가 오류로 반환됐다 출처. 스키마를 단순화하는 과정에서 원래 표현하려던 의미가 일부 손실되는 트레이드오프가 생겼다.
두 번째는 지원 모델 범위다. json_schema 응답 형식은 gpt-4o-mini, gpt-4o-mini-2024-07-18, gpt-4o-2024-08-06 및 이후 모델에서 제공되는데 출처, 구형 모델이나 다른 API 경로로 전환하면 같은 스키마를 써도 다른 결과가 나올 수 있었다. 그래서 모델을 교체할 때마다 같은 평가 지표로 재측정하는 절차를 거쳤다.
세 번째는 의미 정확도의 한계다. 형식이 맞아도 값이 틀리는 경우를 스키마만으로는 막을 수 없었다. 이 부분은 결국 프롬프트의 목표·제약 블록을 더 구체화하거나, 검증 단계에서 별도의 규칙 기반 체크를 추가하는 방식으로만 보완됐다.
실험을 돌아보면 무엇을 남길 수 있나?
2026년 기준으로 구조화 출력 기능은 이미 안정적으로 쓸 수 있는 수준이지만, "스키마 강제"와 "내용 정확성"은 여전히 분리된 문제였다. 구조화 프롬프트를 목표·제약·형식·평가로 나누는 작업 자체가 품질을 높이는 마법은 아니었고, 대신 실패 지점을 추적 가능하게 만들어줬다는 게 더 정확한 표현이다. 다음 실험에서는 의미 정확도와 환각률을 줄이기 위한 검증 레이어를 스키마 바깥에 추가하는 방향으로 이어갈 계획이다.