모델 출력을 JSON Schema 계약으로 만들기
이 글에서는 Structured Outputs를 이용해 LLM 응답을 JSON Schema 기반 계약으로 다루는 방법을 정리합니다.
LLM 응답을 서비스에 붙일 때 가장 먼저 부딪히는 문제는 “답변이 그럴듯한가”가 아닙니다. “백엔드가 파싱할 수 있는가”입니다.
모델이 자유 텍스트로 답변하면 프론트엔드, DB, 후속 API가 안정적으로 사용할 수 없습니다. 특히 문서 요약, 정보 추출, 분류, 라우팅, 툴 호출 전처리처럼 구조화된 결과가 필요한 작업에서는 모델 출력을 계약으로 만들어야 합니다.
분석 기준일: 2026-05-12
실습 기준 환경: OpenAI API, Python, Pydantic, FastAPI
주요 참고자료: OpenAI Structured Outputs Docs, JSON Schema, Pydantic
핵심 요약
- Structured Outputs는 모델 응답이 지정한 JSON Schema를 따르도록 만드는 기능이다.
- LLM 응답은 서비스 내부에서 API response와 같은 계약으로 다뤄야 한다.
- schema version을 두지 않으면 프롬프트 변경과 응답 구조 변경을 추적하기 어렵다.
- validation 실패는 정상 운영 시나리오로 보고 retry/fallback을 설계해야 한다.
- 모든 필드를 모델에게 맡기지 말고 서버가 계산할 필드는 서버에서 채워야 한다.
1. 왜 구조화된 출력이 필요한가
자유 텍스트 응답은 사람이 읽기에는 좋지만 시스템이 처리하기에는 어렵습니다.
예를 들어 모델에게 아래처럼 요청했다고 가정해보겠습니다.
# 예시입니다.이 문서의 핵심 내용을 요약하고, 출처와 신뢰도를 포함해줘.
모델은 다양한 형태로 답할 수 있습니다.
# 예시입니다.핵심은 Redis 캐시가 DB 부하를 줄인다는 점입니다. 출처는 Redis 공식문서입니다.
또는:
# 예시입니다.{ "summary": "Redis 캐시는 DB 부하를 줄인다", "source": "Redis Docs"}
서비스는 이런 변동성을 싫어합니다. 프론트엔드는 citations 배열을 기대하는데 모델이 source 문자열을 주면 깨집니다.
2. JSON mode와 Structured Outputs의 차이
JSON mode는 유효한 JSON을 생성하는 데 도움을 줍니다. 하지만 특정 schema를 반드시 지키는 것과는 다릅니다.
Structured Outputs는 응답이 개발자가 정의한 JSON Schema를 따르도록 하는 데 초점을 둡니다.
| 구분 | JSON mode | Structured Outputs |
|---|---|---|
| 목표 | JSON 형식 출력 | JSON Schema 준수 |
| 필수 필드 보장 | 약함 | 강함 |
| enum 제약 | 약함 | schema 기반 |
| API 계약 적합성 | 제한적 | 높음 |
실무적으로는 JSON mode보다 schema 기반 출력이 더 안전합니다.
3. 응답 schema 설계 원칙
좋은 schema는 모델에게 모든 것을 맡기지 않습니다.
| 원칙 | 설명 |
|---|---|
| 필요한 필드만 둔다 | 과한 필드는 실패율을 높인다 |
| enum을 적극 사용한다 | 후속 분기 안정성 확보 |
| 서버 계산 필드는 제외한다 | token usage, trace_id 등은 서버가 채운다 |
| nullable을 신중히 쓴다 | null 허용은 후속 처리 복잡도 증가 |
| version을 둔다 | schema 변경 추적 |
4. 문서 Q&A 응답 schema 예제
// 예시 JSON 구조입니다.{ "type": "object", "properties": { "answer": { "type": "string", "description": "사용자 질문에 대한 답변" }, "confidence": { "type": "string", "enum": ["high", "medium", "low"] }, "citations": { "type": "array", "items": { "type": "object", "properties": { "document_id": { "type": "string" }, "chunk_id": { "type": "string" }, "quote": { "type": "string" } }, "required": ["document_id", "chunk_id", "quote"] } }, "needs_human_review": { "type": "boolean" } }, "required": ["answer", "confidence", "citations", "needs_human_review"], "additionalProperties": false}
이 schema는 답변, 신뢰도, 출처, 검토 필요 여부를 명확히 분리합니다.
5. Validation과 fallback
Structured Outputs를 사용해도 백엔드 validation은 필요합니다. 외부 시스템의 출력은 항상 검증해야 합니다.
# 예시 코드입니다.from pydantic import BaseModelfrom typing import Literal # 이 선언은 예시 흐름을 보여줍니다.class Citation(BaseModel): document_id: str chunk_id: str quote: str # 이 선언은 예시 흐름을 보여줍니다.class AnswerOutput(BaseModel): answer: str confidence: Literal["high", "medium", "low"] citations: list[Citation] needs_human_review: bool
실패 시나리오:
# 예시입니다.1. 모델 응답 schema validation 실패2. 한 번 재시도3. 그래도 실패하면 fallback 응답 반환4. eval/debug dataset에 실패 케이스 저장
Fallback 예시:
// 예시 JSON 구조입니다.{ "answer": "답변을 안정적으로 생성하지 못했습니다. 질문을 조금 더 구체적으로 입력해 주세요.", "confidence": "low", "citations": [], "needs_human_review": true}
6. Schema versioning
응답 구조는 시간이 지나며 바뀝니다. 따라서 schema version이 필요합니다.
# 예시입니다.answer.v1answer.v2classification.v1extraction.receipt.v1
schema version은 prompt version과 함께 기록해야 합니다.
| 기록 항목 | 이유 |
|---|---|
schema_version | 응답 구조 추적 |
prompt_version | 프롬프트 변경 추적 |
model | 모델별 품질 비교 |
validation_result | 실패율 분석 |
7. DB 저장과 API 응답 분리
모델 출력 schema와 API 응답 schema를 완전히 같게 만들 필요는 없습니다.
모델 출력:
// 예시 JSON 구조입니다.{ "answer": "...", "confidence": "high", "citations": []}
API 응답:
// 예시 JSON 구조입니다.{ "answer_id": "ans_123", "answer": "...", "confidence": "high", "citations": [], "usage": { "input_tokens": 1000, "output_tokens": 300 }, "trace_id": "0af..."}
usage, trace_id, answer_id는 서버가 붙이는 값입니다.
8. 실무 리스크
| 리스크 | 대응 |
|---|---|
| schema가 너무 복잡함 | 작은 schema부터 시작 |
| enum이 현실을 못 담음 | unknown 또는 needs_review 설계 |
| 모델이 출처를 꾸며냄 | citation은 검색 결과 chunk_id에서만 선택하도록 제한 |
| schema 변경 시 클라이언트 깨짐 | versioning과 migration |
| validation 실패를 장애로만 봄 | 정상 운영 지표로 수집 |
9. 실무 체크리스트
# 예시입니다.[ ] 모델 출력 schema와 API 응답 schema를 구분했는가?[ ] required 필드가 명확한가?[ ] additionalProperties를 제한했는가?[ ] enum 값이 후속 로직과 연결되는가?[ ] schema_version을 저장하는가?[ ] validation 실패율을 metric으로 남기는가?[ ] 실패 시 retry/fallback 정책이 있는가?[ ] citation을 모델이 임의 생성하지 못하게 했는가?
실패 사례: JSON은 받았지만 계약은 없었던 경우
Structured Outputs를 쓴다고 해서 곧바로 안정적인 API 계약이 생기지는 않습니다. 가장 흔한 실패는 schema를 만들었지만 version, validation, fallback 정책이 없는 경우입니다. 모델은 JSON을 반환하고 parser도 통과하지만, downstream service가 기대한 의미와 다를 수 있습니다. 예를 들어 risk: "medium"이 어떤 threshold를 뜻하는지 문서화되지 않으면 UI, DB, alert rule이 서로 다르게 해석합니다.
또 다른 문제는 모든 필드를 모델에게 맡기는 것입니다. createdAt, sourceId, confidenceScore, needsReview 같은 값은 일부는 서버가 계산해야 합니다. 모델이 채운 timestamp나 내부 id를 그대로 믿으면 데이터 정합성이 깨집니다. Structured Outputs는 출력 형식을 제어하는 도구이지, business rule 검증을 대체하는 도구가 아닙니다.
구현 예시: schema version과 서버 계산 필드
type ExtractionV1 = { schemaVersion: "customer_intent.v1"; intent: "refund" | "upgrade" | "bug_report" | "unknown"; evidence: string[]; modelConfidence: number;}; type StoredExtraction = ExtractionV1 & { requestId: string; validatedAt: string; needsReview: boolean;};
모델은 ExtractionV1만 생성합니다. 서버는 requestId, validatedAt, needsReview를 계산합니다. 특히 needsReview는 모델 confidence만 보지 않고 사용자 등급, 민감 키워드, 이전 실패 이력 같은 서버 정보를 함께 봐야 합니다.
비교표: 출력 형식과 운영 계약
| 항목 | Structured Outputs가 도와주는 것 | 서버가 계속 해야 하는 것 |
|---|---|---|
| JSON 형식 | schema에 맞는 응답 생성 | 파싱 실패와 재시도 처리 |
| enum | 허용 값 제한 | 값의 business 의미 정의 |
| required field | 필드 누락 감소 | 신뢰할 수 없는 값 재계산 |
| schema | 구조 고정 | version migration과 호환성 |
이 표를 기준으로 보면 Structured Outputs는 validation layer의 앞단입니다. 백엔드는 여전히 외부 입력을 검증해야 하고, 저장 전에는 business invariant를 확인해야 합니다. 그래야 모델 교체나 prompt 변경이 downstream 장애로 바로 이어지지 않습니다.
10. Q&A
Q1. Structured Outputs를 쓰면 validation 코드를 안 써도 되나요?
아닙니다. 백엔드에서는 여전히 validation을 해야 합니다. 외부 시스템 결과를 신뢰하지 않는 것이 기본입니다.
Q2. 모든 LLM 응답을 JSON으로 만들어야 하나요?
아닙니다. 사용자에게 보여줄 최종 답변은 Markdown일 수 있습니다. 다만 시스템이 후속 처리해야 하는 값은 구조화하는 것이 좋습니다.
Q3. schema가 너무 커지면 어떻게 하나요?
작게 나누는 편이 좋습니다. 추출, 분류, 답변, 검토 필요 여부를 하나의 거대한 schema에 넣기보다 단계별 schema로 분리합니다.
11. 참고자료와 불확실성
참고자료
- OpenAI Structured Outputs: https://platform.openai.com/docs/guides/structured-outputs
- JSON Schema: https://json-schema.org/
- Pydantic: https://docs.pydantic.dev/
불확실성
- 지원 모델과 API 세부 파라미터는 시점에 따라 바뀔 수 있습니다.
- 복잡한 schema의 성능과 실패율은 실제 테스트가 필요합니다.

댓글
GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.