튜토리얼
AI Structured Output이란? JSON Schema가 LLM JSON을 안정적으로 만드는 방법
프롬프트의 “JSON만”은 확률만 낮춥니다. Structured Output은 형태를 디코드 단계에서 고정하고, JSON Schema가 필드·타입·enum을 계약으로 만듭니다.
모델을 파이프라인에 연결할 때 무서운 것은 문장이 서툰 것이 아니라, 코드가 먹을 수 없는 반환값입니다. 쉼표 누락, 필드 이름 변경, 숫자가 문자열이 되는 일. Structured Output이 푸는 문제는 바로 그것입니다. 모델이 token을 낼 때부터 선언한 형태로 말하고, 산문을 쓴 뒤 정규식으로 JSON을 긁어내길 바라지 않습니다. JSON Schema는 그 형태의 가장 흔한 서면입니다. 읽고 나면 막힌 곳이 “구문”인지 “형태”인지 “업무의 맞고 틀림”인지 가릴 수 있고, Schema 뒤에도 로컬 검증이 남는다는 것을 알게 됩니다.
프롬프트만으로 만든 JSON이 믿을 수 없는 이유
“JSON만 반환하고 설명은 하지 마라”는 가장 흔한 임시방편입니다. 선호를 맥락에 쓰기 때문에 가끔 통합니다. 믿을 수 없는 이유는 선호가 제약이 아니기 때문입니다. 모델은 객체를 markdown 코드 펜스로 감싸거나, 필수 키를 빼거나,priority을 더 “자연스러운”urgency으로 바꿀 수 있습니다. 하류가JSON.parse하는 순간 실패는 문구 문제가 아니라 파이프라인 전체의 중단입니다.
더 조용한 실패는 “parse는 되지만 형태가 틀린” 경우입니다.items은 배열이어야 하는데 객체가 오고, 점수는 숫자여야 하는데"0.91"가 옵니다. 코드는undefined를 읽거나 문자열을 이어 붙이고, 문제는 한참 뒤에서 터집니다. 프롬프트는 이런 드리프트를 막지 못합니다. 디코드 단계에서 불법 token을 거절하지 않기 때문입니다.
그래서 Structured Output을 “더 엄한 프롬프트”로 보지 마세요. 생성 메커니즘의 일부입니다. 서버가 Schema를 다음에 허용되는 token 집합으로 컴파일하고, 모델은 괄호가 안 맞거나 명단에 없는 키를 낼 수 없습니다. 프롬프트는 작업 설명, 형태는 Schema입니다.
Structured Output이 제약하는 층
세 층으로 생각하면 됩니다. 둘을 섞으면 “모델이 제멋대로”처럼 보입니다. 첫째는 구문: parse 가능한 JSON. 둘째는 형태: 이름, 타입, 필수, enum이 Schema와 맞을 것. 셋째는 의미: 분류가 맞는지, 금액이 진짜인지. Structured Output은 앞의 둘을 덮습니다. 셋째는 언제나 당신 몫입니다.
| 단계 | 무엇을 설정하는가 | 실제 보장 |
|---|---|---|
| 프롬프트 약속 | “JSON만” | 선호일 뿐, 계약이 아님 |
| JSON 모드 | MIME 또는 json_object |
유효한 JSON일 가능성은 높지만 필드 이름은 모델이 정함 |
| Schema 모드 | JSON Schema와 엄격 스위치 | 형태·타입·필수·enum을 Schema가 제약 |
OpenAI는 Structured Outputs를 JSON mode의 다음 단계로 씁니다. 둘 다 유효한 JSON을 낼 수 있지만, 제공한 Schema를 보장하는 것은 전자뿐입니다. 공식 대조는Structured model outputs. Gemini도 “JSON이면 된다”와 “Schema의 필드를 낸다”를 나눕니다. 스위치는Gemini API JSON 출력 가이드를 보세요. 이 글은 SDK 세부 사항을 반복하지 않습니다.
놓치기 쉬운 경계도 있습니다. 안전 거절, 절단, 도구 호출 실패는 성공 객체에 들어가지 않을 수 있습니다. 어떤 API는 별도의refusal나 빈 내용을 줍니다. Schema가 제약하는 것은 “형식으로 말하기 시작한 구간”이지 “이번 호출은 반드시 성공한다”가 아닙니다.
JSON Schema가 계약이 되는 과정
JSON Schema는 원래 JSON 문서를 위한 어휘입니다. 타입, 필수, enum, 숫자 범위, 배열 요소. 표준 설명은Understanding JSON Schema. 모델에 연결하면 같은 어휘에 일이 하나 더 생깁니다. 사후 검증만이 아니라, 생성하는 동안 탐색 공간을 줄입니다.
엔지니어링에서 Schema는 컴파일 타임과 런타임이 공유하는 계약입니다. Pydantic, Zod, Swift의 생성 가능 타입은 결국 언어 무관의 JSON Schema로 떨어지는 경우가 많아 클라우드 모델에 넘기고, 로그에 쓰고, 픽스처로 재생합니다. 필드 표는 하나만 유지해 App이totalCents, API가amount, 프롬프트가 “금액”이라고 말하는 어긋남을 피하세요.
계약에는 “실제로 읽을 객체”를 쓰고 “완전한 세계 모델”은 쓰지 마세요. 선택 필드가 늘수록 잘못 채우거나 비울 기회가 늘어납니다. 필수는required, 닫힌 집합은enum, 숫자 범위는minimum / maximum. 속성의description는 프롬프트에서 다시 설명하는 것보다 보통 더 안정적입니다. 제약과 디코드가 묶여 있기 때문입니다.
엄격 모드에는 흔한 규칙이 하나 더 있습니다. 객체는additionalProperties: false여야 하고, 선언한 필드는 모두required에 넣습니다. 진짜 선택 값은 “required에서 빼는” 것이 아니라null을 허용합니다. 그렇지 않으면 모델이 키를 빼고, 코드는obj.field가 반드시 있다고 가정합니다.
각 API가 Schema를 붙이는 방법
제품 이름은 모두 Structured Output이지만 감싸는 필드는 다릅니다. 먼저 두 가지를 물으세요. 이 Schema는 최종 답변을 제약하는가, 도구 인자를 제약하는가. 현재 모델 스냅샷이 정말 엄격 모드를 지원하는가. 문서의 키워드가 모든 엔드포인트에서 같다고 가정하지 마세요.
OpenAI: json_schema와 strict
Chat Completions에서는response_format를json_schema로 두고strict: true를 켭니다. Responses API는 같은 일을 텍스트 형식 필드에 씁니다. Schema 규칙은 같고 껍데기만 다릅니다. 아래 요청은 티켓을 추출합니다. 범주는 enum, 계정은 null일 수 있습니다.
{
"model": "gpt-4o-2024-08-06",
"messages": [
{ "role": "system", "content": "Extract the ticket into the schema." },
{ "role": "user", "content": "Checkout 500s on a saved card. Account acct_8842." }
],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "support_ticket",
"strict": true,
"schema": {
"type": "object",
"properties": {
"summary": { "type": "string" },
"category": {
"type": "string",
"enum": ["billing", "bug", "account", "other"]
},
"severity": { "type": "integer" },
"account_id": {
"anyOf": [{ "type": "string" }, { "type": "null" }]
}
},
"required": ["summary", "category", "severity", "account_id"],
"additionalProperties": false
}
}
}
}
응답은 “전체가 JSON”으로 다루세요. 정규식으로 코드 펜스를 먼저 뜯지 마세요. SDK parse 도우미는 타입 객체로 역직렬화할 수 있습니다. 거절 분기도 처리하세요. 안전 정책이 발동하면 성공 Schema와 맞지 않을 수 있습니다.
Gemini와 다른 스택
Gemini는 MIME 타입으로 “이것은 JSON”이라고 선언한 뒤responseSchema 또는 responseJsonSchema로 필드를 고정합니다. 후자가 표준 JSON Schema에 더 가깝고anyOf, $ref, 숫자 범위에 맞습니다. 세부와 Python / JS 예는Gemini Structured Output 튜토리얼.
Apple의 on-device 모델은@Generable처럼 컴파일 타임 형태를 쓰며, 손으로 쓴 JSON Schema 파일이 아닙니다. 프로세스를 떠나 HTTP를 치고 로그를 쓸 때는 여전히 직렬화 가능한 JSON이 필요합니다. 세 호출 체인을 나누는 방법은Apple AI Agent와 JSON. 외부 Agent가 MCP나 REST를 쓰면 페이로드는 거의 항상 JSON이고, Schema는 어댑터 층이 맞춰야 할 표입니다.
배포할 수 있는 Schema
아래 Schema는 티켓 분류를 흉내 냅니다. 범주는 닫혀 있고, 우선순위는 정수, 요약은 문자열. Schema가 맡아야 할 것은 형태이지 “이 티켓을 업무상 긴급으로 표시해야 하는가”가 아닙니다.
{
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["billing", "bug", "feature"],
"description": "Ticket category"
},
"priority": {
"type": "integer",
"minimum": 1,
"maximum": 5
},
"summary": {
"type": "string"
}
},
"required": ["category", "priority", "summary"],
"additionalProperties": false
}
배열 요소는items로 적습니다. 튜플에 가까운 고정 길이 목록은 JSON Schema 경로에서prefixItems를 보세요. 중첩 객체는 먼저 인라인. 같은 구조가 세 번째이거나 트리 노드가 자신을 가리킬 때만$defs + $ref를 쓰세요. 너무 이른 추상화는 오류를 읽기 어렵게 합니다. 서버가 Schema를 거절하면 펼쳐진 큰 객체를 마주하게 됩니다.
Prompt와 Schema에 구조를 두 번 쓰지 마세요. 중복 설명은 모델을 두 이야기 사이에서 흔들고 입력 token도 낭비합니다. 작업 설명은 프롬프트, 이름과 타입은 Schema에만. 로컬 타입이 필드를 바꾸면 요청 Schema도 같이 바꿔야 합니다. 그렇지 않으면 운영에서 새 키나 빠진 키가 조용히 나타납니다.
제약한 뒤에도 로컬 검증은 필요하다
Structured Output은 파싱 층을 많이 조용하게 합니다. 분류가 맞는지, 숫자가 업무 상한을 지키는지까지는 보장하지 않습니다. enum은 집합을 제한할 수 있어도 잘못된 선택을 막지는 못합니다. 우선순위 5는 유효하고, “사실은 2여야 한다”도 유효한 JSON입니다. 중요 경로는 표본 검사하거나 규칙을 올리세요.
배포 시에는 모델 반환값을 평범한 JSON으로 보세요. 먼저JSON 포맷로 중첩을 보고,JSON 검증로 parse를 확인한 뒤 같은 객체를JSON Schema에 넣어 로컬에서 한 번 더 검증하세요. 골든셋과 비교할 때는JSON Diff로 키 순서나 nullable 드리프트를 한눈에 봅니다.
픽스처 세 개를 남기세요: ticket.valid.json, ticket.missing-field.json, ticket.wrong-enum.json. 첫 번째는 happy path, 나머지 둘은 로컬 검증이 정말 거절하는지 확인합니다. 모델 쪽이 이미 제약한 오류는 로컬에서도 재현되어야 합니다. 그렇지 않으면 모델을 바꾸거나 엄격 모드를 끈 날, 오프라인 파이프라인에서야 알게 됩니다.
자주 묻는 질문 FAQ
Structured Output과 JSON mode는 무엇이 다른가?
JSON mode는 parse 가능한 JSON을 보장합니다. Structured Output은 그 위에 제공한 JSON Schema 준수를 보장합니다. 이름, 타입, 필수, enum이 계약대로입니다. 코드가 이름 있는 필드를 읽으면 후자를 쓰세요.
Schema가 있는데도 “JSON만”이라고 써야 하나?
짧은 한 줄은 남겨도 됩니다. 필드 표를 프롬프트에 다시 복사하지 마세요. 형태는 Schema가 기준입니다. 두 벌 설명은 품질을 낮추고 할당량도 낭비합니다.
엄격 모드에서 선택 필드는 어떻게 쓰나?
대부분의 엄격 구현은 추가 속성을 닫고 나열한 필드를 모두 필수로 둡니다. 선택 값은 nullable로 씁니다. 문자열 또는 null의 유니언이지, required에서 키를 지우는 것이 아닙니다.
Schema를 통과해도 업무는 틀릴 수 있나?
틀릴 수 있습니다. Schema는 형태를 보고 진위를 보지 않습니다. 잘못된 분류, 금액 환각, 거절해야 하는데 억지로 채우기 모두 유효한 JSON일 수 있습니다. 운영에는 규칙, 표본 검사, 사람 확인이 남습니다.
요약과 다음 단계
Structured Output은 문구 기교가 아닙니다. JSON 형태를 “바람”에서 “디코드 제약”으로 바꿉니다. JSON Schema는 그 제약을 쓰는 가장 흔한 방법입니다. 필수, enum, 범위, 여분 키 금지가 하류가 안정적으로JSON.parse하고 기대한 필드를 읽을 수 있는지를 결정합니다. API마다 스위치 이름은 달라도 층은 같습니다. 구문, 형태, 의미. 섞지 마세요.
다음은 실제로 읽을 작은 Schema를 쓰고, 엄격 모드로 추출 또는 분류를 한 줄 통과시킨 뒤 브라우저에서 반환값을 로컬 검증하세요. Gemini 요청 예는 이전 글, Agent 인자가 JSON으로 떨어지는 모습은 Apple 글을 여세요.