JSON Schema 기초 문법을 학습하고, OpenAI, Anthropic, Google 주요 프로바이더의 구조화된 출력 API를 실습합니다.
JSON Schema는 JSON 데이터의 구조를 정의하는 선언적 명세입니다. LLM의 구조화된 출력에서는 이 스키마가 "출력의 계약서" 역할을 합니다.
JSON Schema가 지원하는 기본 타입은 다음과 같습니다.
{
"string_example": {
"type": "string",
"description": "문자열 타입"
},
"number_example": {
"type": "number",
"description": "실수를 포함한 숫자"
},
"integer_example": {
"type": "integer",
"description": "정수만 허용"
},
"boolean_example": {
"type": "boolean",
"description": "true 또는 false"
},
"array_example": {
"type": "array",
"items": {"type": "string"},
"description": "동일 타입 요소의 배열"
},
"null_example": {
"type": "null",
"description": "null 값만 허용"
}
}실무에서 가장 자주 사용하는 객체 타입의 스키마 정의 방법입니다.
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "상품명"
},
"price": {
"type": "number",
"minimum": 0,
"description": "가격 (원)"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "other"],
"description": "상품 카테고리"
},
"in_stock": {
"type": "boolean",
"description": "재고 여부"
},
"tags": {
"type": "array",
"items": {"type": "string"},
"maxItems": 5,
"description": "상품 태그 (최대 5개)"
}
},
"required": ["name", "price", "category", "in_stock"],
"additionalProperties": false
}additionalProperties: false는 Structured Output에서 매우 중요합니다. 이 설정이 없으면 LLM이 스키마에 정의되지 않은 추가 필드를 생성할 수 있습니다. OpenAI의 strict 모드에서는 이 설정이 필수입니다.
실제 데이터는 대부분 중첩된 구조를 가집니다. JSON Schema에서는 properties 안에 객체를 중첩하여 표현합니다.
{
"type": "object",
"properties": {
"order_id": {"type": "string"},
"customer": {
"type": "object",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"address": {
"type": "object",
"properties": {
"city": {"type": "string"},
"zipcode": {"type": "string"}
},
"required": ["city", "zipcode"],
"additionalProperties": false
}
},
"required": ["name", "email", "address"],
"additionalProperties": false
},
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"product_name": {"type": "string"},
"quantity": {"type": "integer", "minimum": 1},
"unit_price": {"type": "number"}
},
"required": ["product_name", "quantity", "unit_price"],
"additionalProperties": false
}
}
},
"required": ["order_id", "customer", "items"],
"additionalProperties": false
}OpenAI는 2024년 8월 Structured Outputs 기능을 공식 출시했습니다. response_format에 JSON Schema를 전달하면, 모델이 해당 스키마에 100% 부합하는 출력을 생성합니다.
from openai import OpenAI
import json
client = OpenAI()
schema = {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
"description": "전체적인 감성"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "감성 판단 신뢰도 (0-1)"
},
"aspects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"aspect": {"type": "string"},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
}
},
"required": ["aspect", "sentiment"],
"additionalProperties": False
},
"description": "세부 측면별 감성 분석"
}
},
"required": ["sentiment", "confidence", "aspects"],
"additionalProperties": False
}
response = client.chat.completions.create(
model="gpt-4o-2026-02",
messages=[
{
"role": "system",
"content": "리뷰를 분석하여 감성과 세부 측면을 추출하세요."
},
{
"role": "user",
"content": "배송은 빨랐지만 포장이 별로였고, 제품은 만족합니다."
}
],
response_format={
"type": "json_schema",
"json_schema": {
"name": "review_analysis",
"strict": True,
"schema": schema
}
}
)
result = json.loads(response.choices[0].message.content)
print(json.dumps(result, ensure_ascii=False, indent=2))실행 결과는 항상 다음과 같은 구조를 따릅니다.
{
"sentiment": "positive",
"confidence": 0.72,
"aspects": [
{"aspect": "배송", "sentiment": "positive"},
{"aspect": "포장", "sentiment": "negative"},
{"aspect": "제품", "sentiment": "positive"}
]
}OpenAI의 strict: true 모드에서는 몇 가지 제약이 있습니다.
properties에 나열된 필드가 required에 포함되어야 합니다additionalProperties: false가 필수입니다if/then/else, patternProperties 등)anyOf와 null 타입의 조합으로 표현합니다{
"type": "object",
"properties": {
"name": {"type": "string"},
"nickname": {
"anyOf": [
{"type": "string"},
{"type": "null"}
],
"description": "별명 (없을 수 있음)"
}
},
"required": ["name", "nickname"],
"additionalProperties": false
}strict 모드에서 선택적 필드를 표현하려면 required에는 포함하되, 타입을 anyOf: [원래타입, null]로 지정해야 합니다. required에서 제외하는 방식은 허용되지 않습니다.
Anthropic의 Claude는 Tool Use(도구 사용) 기능을 활용하여 구조화된 출력을 생성합니다. 실제 도구를 호출하는 것이 아니라, 도구의 입력 스키마를 출력 형식으로 활용하는 패턴입니다.
import anthropic
import json
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=[
{
"name": "analyze_review",
"description": "리뷰 감성 분석 결과를 구조화된 형태로 반환합니다.",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
},
"confidence": {
"type": "number",
"description": "0-1 사이의 신뢰도"
},
"aspects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"aspect": {"type": "string"},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
}
},
"required": ["aspect", "sentiment"]
}
}
},
"required": ["sentiment", "confidence", "aspects"]
}
}
],
tool_choice={"type": "tool", "name": "analyze_review"},
messages=[
{
"role": "user",
"content": "다음 리뷰를 분석하세요: 배송은 빨랐지만 포장이 별로였고, 제품은 만족합니다."
}
]
)
# 도구 호출 결과에서 구조화된 데이터 추출
for block in response.content:
if block.type == "tool_use":
result = block.input
print(json.dumps(result, ensure_ascii=False, indent=2))tool_choice를 특정 도구로 지정하면, Claude는 반드시 해당 도구를 호출하므로 구조화된 출력을 보장받을 수 있습니다.
Google Gemini는 response_mime_type과 response_schema를 통해 구조화된 출력을 지원합니다.
import google.generativeai as genai
genai.configure(api_key="YOUR_API_KEY")
model = genai.GenerativeModel(
"gemini-2.0-flash",
generation_config={
"response_mime_type": "application/json",
"response_schema": {
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
},
"confidence": {"type": "number"},
"aspects": {
"type": "array",
"items": {
"type": "object",
"properties": {
"aspect": {"type": "string"},
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"]
}
},
"required": ["aspect", "sentiment"]
}
}
},
"required": ["sentiment", "confidence", "aspects"]
}
}
)
response = model.generate_content(
"다음 리뷰를 분석하세요: 배송은 빨랐지만 포장이 별로였고, 제품은 만족합니다."
)
print(response.text)효과적인 스키마를 설계하기 위한 주요 패턴을 살펴봅니다.
자유 텍스트 대신 열거형을 사용하면 일관된 값을 보장할 수 있습니다.
{
"priority": {
"type": "string",
"enum": ["critical", "high", "medium", "low"],
"description": "우선순위 수준"
}
}description 필드는 LLM에게 해당 필드가 무엇을 의미하는지 알려주는 중요한 단서입니다. 상세하고 명확한 설명을 작성할수록 출력 품질이 향상됩니다.
{
"relevance_score": {
"type": "number",
"minimum": 0,
"maximum": 10,
"description": "질문과 문서의 관련성 점수. 0은 완전히 무관, 10은 완벽히 일치. 부분적으로 관련된 경우 5-7 범위를 사용."
}
}배열의 크기를 제한하여 과도한 출력을 방지합니다.
{
"keywords": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 10,
"description": "핵심 키워드 (1-10개)"
}
}LLM의 판단 근거를 함께 출력하면 디버깅과 품질 검증에 유용합니다.
{
"type": "object",
"properties": {
"reasoning": {
"type": "string",
"description": "판단 근거를 단계별로 설명"
},
"classification": {
"type": "string",
"enum": ["spam", "ham"],
"description": "최종 분류 결과"
},
"confidence": {
"type": "number",
"description": "분류 신뢰도 (0-1)"
}
},
"required": ["reasoning", "classification", "confidence"],
"additionalProperties": false
}reasoning 필드를 스키마 상단에 배치하면 LLM이 분류 결과를 내기 전에 먼저 사고 과정을 거치게 됩니다. 이는 Chain of Thought(사고 연쇄) 효과를 주어 분류 정확도를 높이는 데 도움이 됩니다.
이번 장에서는 JSON Schema의 기초 문법부터 주요 LLM 프로바이더별 구조화된 출력 API까지 폭넓게 살펴보았습니다.
핵심 내용을 정리하면 다음과 같습니다.
response_format으로 네이티브 Structured Output을 지원하며, strict 모드에서 100% 스키마 준수를 보장합니다response_mime_type과 response_schema로 JSON 출력을 제어합니다3장에서는 Function Calling의 원리를 심층적으로 다루고, 도구 호출 인터페이스를 활용한 구조화된 출력 생성, 병렬 도구 호출, 에이전트 루프 통합 등 고급 패턴을 학습합니다.
이 글이 도움이 되셨나요?
Function Calling의 원리를 이해하고, OpenAI/Anthropic/Google의 도구 호출 인터페이스로 구조화된 출력을 생성하는 방법을 학습합니다.
LLM 비정형 출력의 한계를 분석하고, 구조화된 출력의 3가지 접근 방식과 제약 디코딩의 원리를 살펴봅니다.
Pydantic v2로 LLM 출력 스키마를 정의하고, Instructor 라이브러리로 자동 재시도와 스트리밍 구조화 출력을 구현합니다.