본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 26. 03:47

LLM 구조화된 출력 (Structured Outputs) 벤치마킹

요약

LLM의 구조화된 출력(Structured Outputs) 기능이 실제 운영 환경에서 얼마나 신뢰할 수 있는지 6개 모델을 대상으로 벤치마킹한 결과입니다. 다양한 스키마 스트레스 테스트를 통해 모델별 준수율과 실패 패턴을 분석하며, 방어적 파싱의 필요성을 강조합니다.

핵심 포인트

  • LLM의 구조화된 출력은 완벽한 계약이 아닌 최선의 제안일 뿐임
  • 복잡한 중첩 및 유니온 스키마에서 모델별 준수율 차이 발생
  • 운영 환경에서는 다단계 폴백 파서 등 방어적 파싱 전략이 필수적임
  • 모델별로 실패하는 구조적 스트레스 요인이 상이함

carrick.tools에서 교차 게시됨.

OpenAI, Anthropic 또는 Google Gemini의 API 문서를 읽다 보면, "구조화된 출력 (structured outputs)"이라는 기능이 이미 해결된 문제처럼 보입니다. JSON 스키마 (JSON schema)를 전달하면, 그에 부합하는 JSON을 돌려받는 방식 말입니다.

하지만 실제 운영 환경 (production)에서 이것은 계약 (contract)이 아닙니다. 그것은 타입이 잘 지정된, 최선을 다한 제안 (best-effort suggestion)일 뿐입니다.

제가 작업 중인 코드 분석 스캐너인 Carrick의 경우, 포스트-LLM (post-LLM) 파이프라인은 4단계 폴백 파서 (fallback parser)에 의존합니다. 우리는 직접 파싱을 시도하고, 마크다운 펜스 (markdown fences)를 제거하며, 주변의 불필요한 텍스트 내에서 배열 범위 (array bounds)를 스캔한 다음, 마지막으로 정규 표현식 (regex) 정리를 적용합니다. 이 네 단계가 모두 실패하면 페이로드 (payload)를 버리고 진행합니다. 만약 구조화된 출력이 광고된 대로 작동했다면, 이는 단 한 줄의 serde_json::from_str(response)로 끝났을 것입니다.

이러한 방어적 파싱 (defensive parsing)이 왜 필요한지 격리하여 확인하기 위해, 저는 6개의 모델(각 제공업체의 플래그십 및 저가형 티어)을 대상으로 8개의 합성 스키마 (synthetic schemas)를 테스트하는 벤치마크를 구축했습니다. 각 스키마는 하나의 구조적 스트레스 요인 (structural stressor)을 격리합니다: 평면적인 베이스라인 (flat baseline), 3단계 중첩 객체 (3-level nested object), 7단계 중첩 체인 (7-level nested chain), 긴 열거형 (long enum), oneOf 태그가 붙은 유니온 (tagged union), Nullable + 포맷 필드 (format fields), 20개 항목의 배열 (20-item array), 그리고 additionalProperties: false가 설정된 폐쇄형 객체 (closed object)입니다. 모든 응답은 두 개의 독립적인 검증기 (ajvhyperjump)를 사용하여 원래 스키마에 대해 검증됩니다. 두 검증기가 모두 동의할 때만 엄격한 준수 (strict adherence)로 간주됩니다.

실제 구현이 어떻게 작동하는지는 다음과 같습니다.

한눈에 보기

8개의 스트레스 스키마 중, 각 모델이 매 실행마다 완전한 엄격한 준수를 달성한 횟수와 특정 실패 모드 (failure mode)에 걸려든 횟수는 다음과 같습니다.

Horizontal bar chart showing each model's outcome distribution across 8 schemas. OpenAI gpt-5.5 and gpt-5.4-mini both pass 2 schemas and pre-reject 6. Anthropic Opus 4.7 passes 7 schemas and partially-fails one (S3 deep nesting, 65 percent strict). Sonnet 4.6 passes 7 schemas and silently fails one (S3, 0 percent strict). Gemini Pro 3.1 and Flash 3.5 both pass 6 schemas and pre-reject 2.

세 가지 패턴이 나타납니다. OpenAI는 제출 시점에 대부분의 스키마 (Schema)를 거부한 뒤, 남은 스키마에 대해서는 완벽하게 준수합니다. Anthropic은 모든 스키마를 수용하지만 특정 구조 하나를 조용히 손상시킵니다. Gemini는 좁은 범위의 기능들을 거부하고 나머지에 대해서는 완벽하게 준수합니다. 각 패턴은 서로의 대칭적인 거울과 같습니다.

1. Anthropic은 복잡한 스키마를 수용하지만, 조용히 잘못된 형태를 반환한다

Anthropic의 도구 사용 (Tool-use) API는 세 가지 중 가장 허용 범위가 넓습니다. 이 API는 도구의 input_schema로 거의 모든 표준 JSON 스키마 (JSON Schema)를 수용하며, 이번 벤치마크의 8개 스키마 중 7개에 대해 Claude Sonnet 4.6과 Claude Opus 4.7 모두 100%의 확률로 엄격하게 준수하는 출력 (Strict-conforming output)을 생성합니다. 실패 모드 (Failure mode)는 하나의 스키마에 집중되어 있습니다: 7단계로 중첩된 객체 체인 (S3)입니다.

모델당 n=20회 실행 기준 S3 결과:

  • Claude Sonnet 4.6: 20회 실행 중 20회 모두 조용히 실패 (Silent-failed). 엄격한 준수율 0% (95% 신뢰 구간: 0%–16.1%).
  • Claude Opus 4.7: 20회 실행 중 7회 조용히 실패 (Silent-failed). 엄격한 준수율 65% (95% 신뢰 구간: 43.2%–82.3%).

Grouped bar chart showing strict adherence rate by schema depth for Claude Sonnet 4.6 and Claude Opus 4.7. Both models achieve 100 percent on the flat baseline (S1) and the 3-level schema (S2). On the 7-level schema (S3), Sonnet drops to 0 percent and Opus drops to 65 percent.

이 실패 모드는 특이합니다. 모델은 7단계 중첩 객체를 반환하는 대신, 전체 중첩 구조를 루트 level1 필드에 할당된 단일 JSON 인코딩된 _문자열 (String)_로 방출합니다. 다음은 Opus의 실패 사례 중 하나를 그대로 옮긴 것입니다:

{"level1":"{\"name\":\"system\",\"child\":{\"name\":\"ingest_pipeline\",
"child\":{\"name\":\"batch_24a17\",\"child\":{\"name\":\"parse_stage\",
"child\":{\"name\":\"error_handling\",\"child\":{\"name\":\"dlq_promotion\",
...

스키마는 level1type: object로 선언했습니다. 하지만 모델은 객체가 되었어야 할 내용의 JSON 직렬화 (JSON serialization)를 포함하는 type: string을 반환했습니다. ajv의 진단 결과는 다음과 같습니다:

/level1 must be object {

- **전송 계층(Transport layer)은 성공을 알립니다.** API는 에러 필드나 거부 신호 없이 HTTP 200을 반환합니다.
- **SDK는 검증하지 않습니다.** Anthropic 클라이언트는 사용자가 보낸 `input_schema`를 준수하는지 확인하지 않고 `tool_use.input`을 애플리케이션으로 그대로 전달합니다.
- **출력이 깔끔하게 파싱됩니다.** `JSON.parse(response)`가 성공하며 `{ level1: "{\"name\": ..." }`를 반환합니다. 명시적인 스키마 검증기(Schema validator)만이 이러한 타입 드리프트(Type drift)를 잡아낼 수 있습니다.

이 메커니즘은 데이터셋 내의 27개 모든 침묵하는 실패 사례(Sonnet 20개 및 Opus 7개)에서 일관되게 나타납니다. 모델이 전체 중첩 페이로드(Nested payload)를 단일 문자열 값으로 감싸버리는 것입니다. 실행 시마다 발생하는 변동성은 문자열 경계가 어디에 위치하느냐의 문제일 뿐, 감싸기 현상 자체가 발생하는지 여부의 문제는 아닙니다.

## 2. OpenAI는 표준 스키마를 거부함으로써 준수를 강제함

OpenAI의 `strict: true` 모드는 Anthropic과 대칭적인 거울 구조를 가집니다. 스키마를 수용하는 경우에는 엄격하게 준수하는 출력을 생성합니다. 반면, 스키마가 strict 모드의 좁은 방언(Dialect)을 충족하지 못하는 경우, 요청은 모델에 도달조차 하지 못합니다.

8개의 벤치마크 스키마 중 오직 2개만이 OpenAI의 strict 모드 규칙을 통과합니다 (의도적으로 strict 준수하도록 설계한 S1 baseline과 S8 closed object). 나머지 6개는 호출이 전송되기 전에 거부됩니다.

OpenAI strict 모드의 요구 사항은 다음과 같습니다:

- 모든 객체(Object)는 반드시 `additionalProperties: false`를 명시적으로 선언해야 합니다.
- 모든 속성(Property)은 `required` 배열에 나열되어야 합니다.
- 타입 배열(예: `type: ["string", "null"]`) 및 `oneOf` 유니온(Union)은 지원되지 않습니다.

본 벤치마크는 제출 전 로컬에서 OpenAI API가 수행할 것과 동일한 스키마 검증을 수행합니다. 대표적인 거부 사례(7단계 스키마의 경우)는 다음과 같습니다:

OpenAI strict mode violations:
$: object missing additionalProperties: false;
$.level1: object missing additionalProperties: false;
...


거부율은 gpt-5.4-mini와 gpt-5.5 사이에서 동일합니다. 이 검사는 모델이 호출되기 전, 스키마 제출 계층의 서버 측에서 실행되므로 플래그십 지능(Flagship intelligence)의 차이가 결과에 영향을 미치지 않습니다.

만약 OpenAPI 명세(spec)나 `package.json`에서 스키마를 가져온다면, 아마도 실패할 것입니다. 당신의 선택지는 스키마를 엄격한 방언(strict dialect)에 맞춰 다시 작성하거나, strict mode를 비활성화하고 Anthropic의 silent-failure(조용한 실패) 문제를 그대로 떠안는 것뿐입니다.

## 3. Gemini는 경직된 중간 지점이다

Gemini의 스키마 검증기(validator)는 OpenAI strict가 금지하는 현대적인 JSON Schema 기능들(`oneOf`, type-arrays, `$ref`)을 거부하지만, OpenAI strict가 거부하는 더 느슨한 형태들은 허용합니다. Gemini의 사전 검사(pre-flight)를 통과한 8개의 벤치마크 스키마 중 6개에 대해, Gemini Pro 3.1과 Gemini Flash 3.5 모두 각각 n=5에서 100%의 엄격한 준수율(strict adherence)을 유지했습니다 (n=5일 때 5/5에 대한 Wilson 95% 신뢰 구간(CI): 56.6%–100%; 6개의 스키마 전반에 걸쳐 이 패턴을 뒷받침하기에 충분히 타이트함).

거부된 두 개의 스키마는 S5(`oneOf` 사용)와 S6(`type: ["string", "null"]` 및 `format: date-time` 사용)입니다. Gemini는 제출 시점에 지원되지 않는 기능을 명시하는 명확한 에러 메시지와 함께 거부 사실을 드러냅니다.

주목할 점은, Gemini가 Anthropic을 무너뜨렸던 것과 동일한 7단계 깊이의 중첩된 스키마(deeply nested schema)를 모든 실행에서 100% 엄격한 준수율로 처리했다는 것입니다. Gemini가 스키마를 수락하는 곳에서는, 반드시 그 형식을 따릅니다.

## 결과 매트릭스 (The outcome matrix)

전체 파일럿 테스트를 하나의 그리드로 압축했습니다. S3와 S7은 Anthropic의 경우 n=20으로 실행되었으며, 나머지 모든 셀은 n=5로 실행되었습니다.

[![Heatmap of 8 schemas across 6 models. OpenAI columns are dominated by amber pre-call rejections except S1 and S8. Anthropic columns are mostly green with red silent failure on the deep nesting row (S3, 0 percent on Sonnet, 65 percent on Opus). Gemini columns are mostly green except S5 and S6 which are amber pre-call rejections.](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7z2mg9094lnv8wxye2ov.png)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7z2mg9094lnv8wxye2ov.png)

## 방어적 구현 패턴 (Defensive implementation patterns)

제공업체의

1. **독립적인 검증 단계 실행 (Run an independent validation step).** 제공업체로부터 받은 HTTP 200 응답은 아무것도 보장하지 않습니다. 데이터를 애플리케이션 로직으로 전달하기 전에 `ajv`, `hyperjump` 또는 자체 코드베이스의 커스텀 워커 (custom walker)를 사용하여 모든 응답 페이로드 (payload)를 스키마 (schema)에 따라 검증하십시오.
2. **성공 기준 재정의 (Redefine success criteria).** 표준 파싱 오류 (parse error), 스키마 위반 (schema violation), 그리고 거부 (refusal)를 모두 동일한 실패 모드로 취급하십시오. 이 모든 경우에 대해 동일한 재시도/폴백 (retry/fallback) 로직을 트리거하십시오.
3. **Anthropic 스키마 평탄화 (Flatten Anthropic schemas).** 깊은 중첩 (deep nesting)은 플래그십 티어를 포함한 Claude에서 조용한 데이터 손상 (silent corruption)을 유발합니다. 가능한 한 구조를 형제 객체들의 최상위 배열 (top-level arrays of sibling objects)로 평탄화하십시오. 스키마의 깊이가 3~4단계를 초과하는 경우 리팩토링 (refactoring)을 고려하십시오.
4. **OpenAI 방언으로 스키마 컴파일 (Compile schemas to the OpenAI dialect).** OpenAI의 엄격 모드 (strict mode)를 대상으로 하는 경우, 처음부터 모든 하위 레벨에 `additionalProperties: false`가 전파되고 선택적 필드 (optional fields)가 없는 방식으로 스키마를 작성하십시오.
5. **Gemini를 위한 유니온 제거 (Strip unions for Gemini).** `oneOf` 및 `["string", "null"]` 사용을 피하십시오. 유니온 (unions)에는 `anyOf`를 사용하고 단일 nullable 타입 제약 조건에 의존하십시오.

## 이 벤치마크가 측정하는 것과 측정하지 않는 것

명시적으로 밝혀둘 만한 세 가지 주의 사항이 있습니다:

**OpenAI 거부는 벤치마크 측에서 서버 규칙을 미러링하여 처리됩니다.** OpenAI에 의해 거부된 것으로 보고된 8개 중 6개의 스키마는 문서화된 엄격 모드 규칙(`additionalProperties: false`, 모든 속성 필수, 타입 배열 불가, `oneOf` 불가)을 구현한 벤치마크 내부의 사전 검증기 (pre-flight validator)에 의해 거부되었습니다. 각 스키마를 OpenAI API에 별도로 제출하여 서버의 400 응답을 관찰한 것이 아니므로, 여기서 보고된 거부율은 OpenAI 서버가 에러를 반환하는 비율이 아니라, OpenAI의 문서화된 엄격 모드 규칙이 일반적인 JSON 스키마 (JSON Schema)를 탈락시키는 비율입니다. 만약 OpenAI가 내일 엄격 모드를 완화하더라도, 이 벤치마크는 이를 감지하지 못할 것입니다.

**Gemini 스키마는 제출 전에 정규화됩니다.** Gemini의 구조화된 출력 (Structured Output) API는 OpenAPI / draft-2020-12 JSON Schema보다 더 좁은 키워드 집합을 지원합니다. 벤치마크의 `convertSchemaToGemini` 함수는 Gemini 문서에서 지원한다고 나열된 키워드(`type`, `enum`, `format`, `min/max`, `required`, `properties`, `items`)는 통과시키고, 나머지는 제출 전에 삭제합니다. 검증기(Validator)는 여전히 Gemini의 출력을 원래의 스키마와 대조하여 확인하므로, 변환기가 삭제한 모든 제약 조건은 Gemini 측에서 암묵적으로 통과됩니다. 현재 코퍼스(Corpus)에서는 이것이 S5와 S6(이미 사전 점검 단계에서 거부됨)에만 영향을 미치지만, `const`, `pattern`, 또는 `additionalProperties`를 실제 제약 조건으로 사용하는 향후 스키마에서는 문제가 될 수 있습니다.

**샘플 크기가 불균등합니다.** 기사에서 구체적으로 인용한 두 셀(S3의 깊은 중첩 구조에 대한 Anthropic Sonnet 및 Opus)은 각각 n=20으로 실행되었습니다. S7 긴 배열 (Long-array) 셀 또한 초기 파일럿 테스트에서 Anthropic 어댑터가 `max_tokens: 4096`으로 제한되어 절단율 (Truncation rate)이 높아지는 것이 확인된 후 n=20으로 실행되었습니다. 제한을 8192로 높이자 Anthropic의 두 모델 모두 S7에서 100% 엄격한 준수율을 보였습니다. 그 외의 모든 곳에서 벤치마크는 셀당 n=5로 실행되었으며, 이는 지배적인 결과를 확인하기에는 충분하지만 정확한 비율을 주장하기에는 부족합니다.

방법론, 원본 JSONL, 스키마 및 재현 가능한 스크립트는 [carrick-llm-structured-bench](https://github.com/daveymoores/carrick-llm-structured-bench)에서 확인할 수 있습니다. 위의 수치를 뒷받침하는 전체 재실행에는 약 8달러의 API 크레딧 비용과 약 1시간의 실제 시간 (Wall time)이 소요되었습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0