deep-dive · 중급

Structured Output과 JSON Schema로 AI 시스템 계약을 만드는 법

Structured Output은 필드·타입·필수값 같은 구조를 안정화하지만 추출값의 사실성, 업무 규칙, 권한, 최신성까지 보장하지 않는다. 따라서 공급자 종료 상태를 먼저 처리하고 JSON Schema 검증 뒤 도메인 불변식·근거 대조·권한 검사를 거치며, 스키마 버전과 실패 상태를 명시하고 형식 테스트와 의미 평가를 분리해야 한다.

30초 답변

Structured Output은 LLM 응답을 파싱 가능한 계약으로 만드는 강력한 도구지만, 답의 의미가 맞다는 보증은 아니다. JSON Schema로 필드 이름, 타입, 필수 여부, 허용 enum을 제한하면 깨진 JSON과 누락 필드를 크게 줄일 수 있다. 그러나 total_amount: 125000이 원문 청구서의 금액과 같은지, 승인 한도를 넘지 않는지, 사용자가 그 계약을 볼 권한이 있는지는 별도의 검증 문제다.

안전한 구현은 공급자 종료 상태 확인 → JSON 파싱·스키마 검증 → 도메인 규칙 검사 → 원문 근거 대조 → 권한·부작용 통제 순서로 동작한다. 스키마에는 버전과 불확실성·근거를 표현할 필드를 두고, 형식 준수율과 의미 정확성을 서로 다른 평가셋으로 측정한다.

이 글의 핵심

  • JSON mode는 “유효한 JSON”에 가깝고 Structured Output은 “지정한 스키마에 맞는 JSON”을 목표로 한다.
  • 스키마 적합성은 사실성·업무 정확성·권한 적합성을 보장하지 않는다.
  • 거절, token 한도, 연결 중단 같은 종료 상태는 정상 객체와 분리해 먼저 처리한다.
  • null, 빈 문자열, 누락, unknown의 의미를 계약에 명시한다.
  • 스키마 버전, 도메인 validator, 근거 필드, 평가셋을 함께 운영해야 실제 시스템 계약이 된다.

JSON mode와 Structured Output은 무엇이 다른가

OpenAI Structured Outputs 문서는 지정한 JSON Schema에 응답이 맞도록 하는 기능과 JSON mode를 구분하며, 지원하는 JSON Schema가 전체 표준의 일부라고 명시한다. Anthropic Structured Outputs 문서도 JSON 응답과 strict tool input을 스키마에 맞게 제한하되 지원 제약과 예외 종료 상태를 별도로 설명한다. 공급자마다 지원 키워드와 실패 표현이 다르므로 “JSON Schema를 쓴다”는 말만으로 호환된다고 가정하면 안 된다.

방식보장하려는 범위여전히 애플리케이션이 해야 할 일
자유 텍스트없음파싱, 형식·의미·권한 검증 전체
JSON modeJSON 문법에 맞는 출력필드·타입·필수값·의미 검증
Structured Output공급자가 지원하는 스키마 부분집합 준수종료 상태, 의미·근거·업무 규칙·권한 검증
일반 코드의 JSON Schema validator주어진 인스턴스의 스키마 준수모델이 말한 값의 사실성·업무 적합성 검증

따라서 Structured Output의 가장 큰 이점은 모델을 데이터베이스의 진실 원천으로 만드는 것이 아니라 비결정적인 자연어와 결정적인 코드 사이의 인터페이스를 좁히는 것이다.

JSON Schema가 보장하는 것과 보장하지 않는 것

다음 청구서 추출 결과를 생각해 보자.

{
  "schema_version": "invoice-extraction.v1",
  "invoice_number": "A-1042",
  "currency": "KRW",
  "total_amount": 125000,
  "payment_due_date": "2026-08-31",
  "evidence": [
    {"field": "total_amount", "page": 2, "quote": "합계 125,000원"}
  ]
}

스키마는 total_amount가 숫자인지, currency가 허용된 enum인지, 필드가 빠졌는지를 검사할 수 있다. 하지만 다음 질문에는 답하지 못한다.

  • 원문 합계가 정말 125,000원인가?
  • 세액 포함 금액과 공급가액을 혼동하지 않았는가?
  • 날짜가 문서의 발행일인지 지급 기한인지 정확히 구분했는가?
  • 문서가 최신 버전이고 위조되지 않았는가?
  • 현재 사용자가 이 금액을 조회·승인할 권한이 있는가?
  • 이 결과로 자동 지급을 실행해도 되는가?

JSON Schema Core 명세는 JSON 문서의 구조와 제약을 기술하는 언어다. 그것이 외부 현실과 값이 일치함을 확인하는 증명 시스템은 아니다. 형식이 완벽한 오답은 얼마든지 가능하다.

5층 출력 계약으로 설계한다

1. 전송·종료 상태 계약

HTTP 상태만 보고 성공으로 판단하지 않는다. 모델 공급자는 안전 거절, 길이 제한, 콘텐츠 필터, 중단을 별도 필드나 stop reason으로 표현할 수 있다. OpenAI 문서는 안전 거절이 사용자가 지정한 스키마를 따르지 않을 수 있어 refusal을 별도 처리하도록 안내한다. Anthropic 문서도 refusal이나 max_tokens 종료 시 출력이 스키마와 다르거나 불완전할 수 있다고 설명한다.

애플리케이션 내부에서는 공급자별 상태를 다음처럼 정규화한다.

completed | refused | incomplete | provider_error | validation_error

refused를 파싱 오류로 기록하거나, incomplete JSON을 복구해 정상 처리하지 않는다. 사용자가 다시 입력해야 하는지, 담당자 검토가 필요한지 UI 상태로 연결한다.

2. 구조 계약

스키마는 가능한 한 작고 명시적으로 만든다.

{
  "$id": "https://example.com/schemas/invoice-extraction.v1.json",
  "type": "object",
  "properties": {
    "schema_version": {"const": "invoice-extraction.v1"},
    "invoice_number": {"type": ["string", "null"]},
    "currency": {"type": ["string", "null"], "enum": ["KRW", "USD", "EUR", null]},
    "total_amount": {"type": ["number", "null"]},
    "review_required": {"type": "boolean"},
    "missing_fields": {
      "type": "array",
      "items": {"enum": ["invoice_number", "currency", "total_amount"]}
    }
  },
  "required": ["schema_version", "invoice_number", "currency", "total_amount", "review_required", "missing_fields"],
  "additionalProperties": false
}

실제 공급자에 보낼 때는 해당 공급자가 지원하는 키워드만 사용한다. OpenAI 문서는 Structured Outputs가 JSON Schema의 부분집합을 지원한다고 밝히고, Anthropic 문서도 미지원 제약을 SDK가 제거해 설명으로 옮긴 뒤 원래 스키마로 다시 검증할 수 있다고 설명한다. 따라서 로컬의 기준 스키마와 공급자용 변환 스키마를 구분하고 둘 다 테스트한다.

3. 도메인 불변식 계약

스키마를 통과한 뒤 일반 코드로 업무 규칙을 검사한다.

  • 금액은 허용 범위 안인가?
  • currency = null이면 total_amount도 자동 결제에 사용하지 않는가?
  • 시작일이 종료일보다 늦지 않은가?
  • 문서 번호가 현재 테넌트의 형식과 일치하는가?
  • 필수 근거가 없으면 review_required = true인가?
  • 이미 처리된 청구서 번호가 아닌가?

이 검사는 LLM에게 다시 물어보는 것보다 결정적인 코드, 기준 데이터베이스, 정책 엔진으로 수행한다. LLM에게 “네 답이 맞는지 확인해”라고 한 번 더 묻는 것은 독립 검증이 아니다.

4. 근거·의미 계약

추출과 판단 업무에는 결과만 받지 말고 검증 가능한 근거를 함께 요구한다. 페이지, 문단, 문서 ID, 짧은 원문 구간, 검색 결과 ID 같은 포인터가 유용하다. 애플리케이션은 해당 포인터가 실제 입력 범위 안에 있는지, 인용문이 원문과 일치하는지 다시 확인한다.

근거가 있다고 정답은 아니다. 잘못된 구절을 인용할 수 있기 때문이다. 그러나 사용자가 원문을 빠르게 대조하고, 평가셋에서 필드별 오류를 진단하며, 고위험 결과를 보류할 수 있다. confidence: 0.93 같은 모델 자기평가만으로 자동 승인하지 않는다. 대신 evidence_present, 교차 검증 결과, 규칙 위반 여부처럼 관측 가능한 신호를 사용한다.

5. 권한·부작용 계약

구조화된 tool argument가 유효하더라도 실행 권한이 생기는 것은 아니다. {"account_id":"A1","amount":125000}이 스키마에 맞아도 사용자가 A1 계정에 지급할 권한이 있는지, 승인 단계가 끝났는지, 동일 요청이 이미 실행됐는지는 서버가 확인해야 한다.

OpenAI의 에이전트 안전 가이드는 구조화 출력을 노드 사이 자유형 데이터 흐름을 줄이는 방어로 권하지만, 이것만으로 공격 위험이 사라지지 않는다고 명시한다. 도구 실행은 서버 측 allowlist, 최소 권한, 승인, 멱등성 키, 결과 검증을 별도로 적용한다.

좋은 스키마를 만드는 실무 원칙

필드 이름과 설명은 업무 의미를 담는다

date1, value, type보다 payment_due_date, total_amount_tax_included, contract_change_type처럼 의미가 분명한 이름을 쓴다. 설명에는 단순 번역이 아니라 포함·제외 조건과 반례를 적는다.

누락과 빈 값을 구분한다

"", 0, null, 필드 누락을 같은 뜻으로 쓰면 후속 코드가 오판한다. 예를 들면 모든 필드는 required로 두되 원문에 없으면 null, 읽을 수 없으면 null + missing_reason: "unreadable", 해당 없음은 null + missing_reason: "not_applicable"로 표현할 수 있다. 어떤 방식을 택하든 소비자 코드와 평가셋에서 동일하게 정의한다.

열린 문자열보다 제한된 어휘를 쓴다

상태·분류는 가능하면 enum을 쓴다. 단, 현실의 분류가 완전하지 않으면 otherother_description을 함께 둔다. 새 분류가 생겼다고 기존 enum의 뜻을 조용히 바꾸지 말고 스키마 버전을 올린다.

거대한 만능 스키마를 피한다

업무 단계마다 필요한 필드만 주고받는다. 큰 중첩 스키마는 모델 혼동, 컴파일 지연, 공급자 제한, 변경 영향 범위를 키운다. Anthropic 문서는 복잡한 스키마가 grammar 컴파일 비용을 높이며 일부 미지원 제약과 복잡도 한도가 있음을 설명한다. 공급자별 정확한 제한은 발행 시점 문서를 다시 확인한다.

스키마에 민감정보를 넣지 않는다

속성명, enum, const, 설명에 고객명·주민번호·계약 내용을 넣지 않는다. 스키마는 캐시되거나 관측 로그에 남을 수 있다. 민감한 실제 값은 요청 데이터에만 두고, 스키마는 일반화된 계약으로 재사용한다.

스키마 버전과 호환성

스키마는 API다. 프롬프트 파일에 익명으로 묻어 두지 말고 저장소에서 버전 관리한다.

변경호환성 위험권장 처리
optional 필드 추가낮음소비자가 알 수 없는 필드를 무시하는지 확인
required 필드 추가높음새 버전 발행, 생산자·소비자 동시 전환
enum 값 추가중간exhaustive switch가 깨지는지 계약 테스트
필드 의미 변경매우 높음이름 또는 major 버전 변경
타입 변경매우 높음새 필드 병행 후 단계적 폐기
필드 삭제높음사용량 확인, deprecation 기간 제공

응답에 schema_version을 넣고 trace에는 prompt_version, model_snapshot, validator_version을 함께 남긴다. 폴백 모델도 같은 버전의 계약 테스트를 통과해야 한다. 배포는 새 스키마를 먼저 읽을 수 있는 소비자를 배포한 뒤 생산자를 바꾸는 순서가 안전하다.

테스트는 형식과 의미를 분리한다

계약 테스트

  • 모든 필수 필드와 타입이 맞는가?
  • additionalProperties 정책을 지키는가?
  • 거절·길이 제한·빈 응답을 정상 데이터로 오인하지 않는가?
  • 기준 스키마를 공급자용으로 변환해도 핵심 제약이 남는가?
  • 구버전 소비자가 새 응답을 안전하게 처리하는가?
  • 각 폴백 모델과 SDK 버전에서 동일하게 동작하는가?

의미 평가

  • 필드별 정확도와 누락률은 어떤가?
  • null이어야 할 때 값을 지어내지 않는가?
  • 금액·날짜·부정 표현·표 병합에서 어떤 오류가 나는가?
  • 근거 위치가 원문과 실제로 일치하는가?
  • 도메인 validator가 위험한 오답을 얼마나 차단하는가?
  • 자동 처리, 사람 검토, 보류의 분기 품질은 어떤가?

전체 JSON 일치율 하나만 보면 어느 필드가 문제인지 알기 어렵다. 고위험 필드에는 더 높은 기준을 두고, 정답이 없는 문서와 모호한 문서를 반드시 평가셋에 포함한다. 모델·프롬프트·스키마·validator가 바뀔 때 같은 회귀 평가를 실행한다.

사용자 경험과 실패 시나리오

JSON은 완벽한데 금액이 틀렸다

스키마 검증 성공을 업무 성공으로 기록한 경우다. 금액의 근거 구간을 원문과 대조하고, 합계·세액 관계를 결정적 규칙으로 검사한다. 고위험 금액은 검토 화면에서 원문과 나란히 보여주고 사용자가 수정한 값도 평가 데이터로 추적한다.

안전 거절이 “서버 오류”로 보인다

공급자 거절을 정상 스키마로 강제 파싱했기 때문이다. provider adapter에서 거절·필터·길이 제한을 먼저 정규화하고, UI에는 정책상 처리 불가인지 입력이 부족한지 구분된 안내를 제공한다. 공급자를 바꿔 거절을 자동 우회하지 않는다.

모델을 교체하자 enum과 날짜 처리가 달라졌다

공급자별 스키마 부분집합과 출력 동작을 호환된다고 가정한 경우다. 모델·SDK·스키마 조합별 contract test를 CI에서 실행하고, 폴백은 사전 승인된 조합으로만 제한한다. 의미 평가는 실제 업무 문서로 다시 수행한다.

필드가 없는데 모델이 그럴듯한 값을 채운다

빈 값을 허용하지 않는 스키마가 모델을 추측으로 몰았을 수 있다. null, unknown, not_applicable을 표현할 방법과 missing_reason을 제공하고, “근거가 없으면 추정하지 않는다”는 예시를 넣는다. 평가셋에 의도적으로 정보가 없는 문서를 포함한다.

구현 체크리스트

  • JSON mode와 Structured Output을 구분해 선택했는가?
  • 공급자·모델별 지원 JSON Schema 부분집합을 확인했는가?
  • 공급자 종료 상태를 스키마 파싱보다 먼저 처리하는가?
  • 스키마에 $id 또는 schema_version이 있는가?
  • 필드명·설명에 업무 의미와 포함·제외 조건이 있는가?
  • 누락, null, 빈 문자열, 해당 없음의 뜻이 정의됐는가?
  • additionalProperties와 enum 확장 정책이 정해졌는가?
  • 스키마 뒤에 결정적인 도메인 validator가 있는가?
  • 원문 근거 포인터를 서버가 다시 검증하는가?
  • 구조화된 tool argument에도 권한·승인·멱등성 검사가 적용되는가?
  • 형식 계약 테스트와 의미 평가셋을 따로 운영하는가?
  • 거절·잘림·잘못된 근거·정보 없음 사례를 테스트하는가?
  • 스키마 변경의 호환성·배포·롤백 절차가 있는가?
  • 스키마와 로그에 민감정보가 들어가지 않는가?

자주 묻는 질문

Q1. Structured Output을 쓰면 JSON validator가 필요 없나?

필요하다. 공급자 기능이 스키마 준수를 목표로 해도 거절, token 한도, 스트림 중단, SDK 변환, 모델·기능 지원 차이 같은 예외가 있다. 애플리케이션 경계에서 다시 파싱·검증하고, 그다음 도메인 규칙도 검사해야 한다. 외부 입력을 신뢰 경계에서 검증한다는 일반 원칙은 그대로다.

Q2. 스키마 설명을 자세히 쓰면 의미 정확성도 보장되나?

정확도를 높이는 데 도움은 되지만 보장은 아니다. 명확한 설명과 예시는 모델이 필드 의미를 이해하게 하고, JSON Schema는 구조를 제한한다. 의미 정확성은 실제 업무 평가셋, 원문 근거 대조, 결정적 규칙, 필요 시 사람 검토로 확인한다.

Q3. 모델이 낸 confidence 점수로 자동 처리해도 되나?

모델의 자기 보고 점수만으로 고위험 결정을 자동화하지 않는 편이 안전하다. 점수의 보정 여부를 별도 데이터로 검증하고, 근거 존재·교차 검사·도메인 규칙·문서 품질 같은 관측 가능한 신호와 결합한다. 임계치 주변과 고위험 항목에는 사람 검토 경로를 둔다.

근거 자료