본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 30. 03:49

LLM 구조화된 출력 검증: 프로덕션에 도달하기 전 JSON 오류 방지하기

요약

LLM이 생성한 JSON 출력이 프로덕션 환경에서 시스템 오류를 일으키지 않도록 검증 레이어를 구축하는 방법을 다룹니다. 단순 파싱을 넘어 데이터의 유효성과 비즈니스 규칙 준수를 확인하는 '출력 계약(Output Contract)' 사고방식을 제안합니다.

핵심 포인트

  • LLM의 구조화된 출력은 단순 파싱 이상의 검증이 필요함
  • JSON 앞뒤의 불필요한 텍스트나 필수 필드 누락 등의 실패 사례 주의
  • 출력 계약(Output Contract)을 통해 모델과 앱 사이의 데이터 약속 정의
  • 데이터 형태, 허용 값, 필수 필드, 복구 가능성 등을 포함한 3계층 검증 권장

AI 기능이 일반 텍스트를 반환한다면, 잘못된 답변은 단순히 짜증을 유발하는 정도에 그칩니다. 하지만 그 답변이 결제, 티켓 생성, 데이터베이스 쓰기, 자동화 또는 고객 대면 워크플로를 구동하는 JSON을 반환한다면, 잘못된 답변은 제품 자체를 망가뜨릴 수 있습니다.

이는 많은 개발자가 뒤늦게 발견하게 되는 조용한 실패 모드(failure mode)입니다. 데모는 잘 작동합니다. 스키마(schema)는 단순해 보입니다. 모델은 대부분의 경우 지침을 따릅니다. 그러다 프로덕션(production) 요청 중 하나가 JSON 앞에 문장을 추가하거나, 필수 필드를 누락하거나, 열거형(enum)을 변경하거나, 존재하지 않는 키를 만들어내거나, 혹은 안전하지 않은 값을 가진 유효한 객체를 반환하게 됩니다.

이 가이드는 이러한 실패가 프로덕션 시스템에 영향을 미치기 전에 잡아낼 수 있는 LLM 구조화된 출력 검증(structured output validation) 레이어를 구축하는 방법을 보여줍니다.

실제 앱에서 구조화된 출력이 깨지는 이유

구조화된 출력(Structured output)은 언어와 소프트웨어 사이의 가교 역할을 합니다. 여러분은 모델에게 다음과 같은 형태를 반환하도록 요청합니다:

{
  "intent": "refund_request",
  "confidence": 0.87,
...

그다음 여러분의 앱은 해당 응답을 데이터처럼 취급합니다.

문제는 언어 모델(language models)이 일반적인 API 서버가 아니라는 점입니다. 모델은 텍스트를 예측합니다. 제공업체가 JSON 모드(JSON mode), 함수 호출(function calling), 도구 호출(tool calling) 또는 스키마 제약 디코딩(schema-constrained decoding)을 제공하더라도, 결과물에 대한 안전 경계(safety boundary)를 관리하는 책임은 여전히 여러분의 애플리케이션에 있습니다.

일반적인 프로덕션 실패 사례는 다음과 같습니다:

  • JSON 앞뒤에 붙는 추가적인 산문(prose)
  • 필수 필드 누락
  • 앱이 문자열을 기대하는 곳에 null 허용 필드(nullable fields)가 들어오는 경우
  • canceled 대신 cancelled가 들어오는 것과 같은 열거형(enum) 드리프트
  • 실제 입력 대신 예시에서 복사된 ID
  • 안전하지 않은 도구 인자(tool arguments)
  • 배포 간에 혼합된 스키마 버전
  • 비즈니스 규칙을 위반하는 유효한 JSON
  • 출력 동작을 변경하는 모델의 조용한 폴백(fallback)

구조화된 출력을 신뢰할 수 있게 만드는 가장 빠른 방법은 파싱(parsing)을 유일한 문제로 취급하지 않는 것입니다. 파싱은 "이것이 JSON인가?"라는 질문에 답합니다. 프로덕션 검증(Production validation)은 더 나은 질문을 던집니다: "이 객체가 다음 단계를 안전하게 구동할 수 있는가?"

출력 계약(Output contract) 사고방식

출력 계약(output contract)은 모델과 여러분의 앱 사이의 작은 약속입니다:

  1. 답변이 어떤 형태(shape)를 가져야 하는가?
  2. 어떤 값들이 허용되는가?
  3. 이 워크플로우(workflow)를 위해 어떤 필드들이 필수적인가?
  4. 어떤 필드들은 복구(repaired) 가능한가?
  5. 어떤 실패가 워크플로우를 중단시켜야 하는가?
  6. 어떤 스키마(schema) 버전이 해당 객체를 생성했는가?

이것이 중요한 이유는 모델이 시스템의 일부분일 뿐이기 때문입니다. 여러분의 계약(contract)은 큐 워커(queue worker), 웹훅 핸들러(webhook handler), 데이터베이스 트랜잭션(database transaction), 알림 작업(notification job), 그리고 사용자 인터페이스(user interface)도 보호합니다.

유용한 계약은 세 가지 계층(layer)을 가집니다:

계층 (Layer)확인 사항예시
구문 (Syntax)파싱(parse)이 가능한가?유효한 JSON 객체
...

대부분의 망가진 AI 워크플로우는 첫 번째 계층에서 멈춥니다. 신뢰할 수 있는 워크플로우는 세 계층 모두를 강제합니다.

작업을 수행할 수 있는 가장 작은 스키마로 시작하세요

거대한 스키마는 더 많은 실패 지점(failure points)을 만듭니다. 다음 단계에서 의도(intent)와 신뢰도 점수(confidence score)만 필요하다면, 전체 CRM 레코드를 요구하지 마세요.

나쁜 스키마:

{
  "customer": {
    "name": "string",
...

더 나은 스키마:

{
  "intent": "refund_request | bug_report | billing_question | unknown",
  "confidence": 0.0,
...

두 번째 스키마가 검증(validate)하기 더 쉽고, 테스트하기 더 쉬우며, 복구(recover)하기도 더 쉽습니다.

좋은 규칙: 모델에게는 결정(decisions), 레이블(labels), 그리고 짧은 설명(short explanations)을 요청하세요. 권위 있는 데이터(authoritative data)는 여러분의 자체 시스템에서 가져오십시오.

모델에게 사용자 ID, 송장 ID, 구독 상태, 또는 권한 수준을 만들어내라고 요구하지 마세요. 모델이 다음 단계를 선택한 후, 신뢰할 수 있는 서비스로부터 해당 정보들을 전달받으세요.

제공업체의 기능을 사용하되, 검증을 외주 주지는 마세요

최신 LLM API는 종종 JSON 모드(JSON mode), 함수 호출(function calling), 도구 호출(tool calling), 또는 스키마 제약 조건(schema constraints)을 통해 구조화된 출력(structured outputs)을 지원합니다. 이를 사용하세요. 이는 지저분한 파싱(parsing) 문제를 줄여줍니다.

하지만 이것들이 신뢰성 계층(reliability layer)의 전부인 것은 아닙니다.

제공업체 측의 제약 조건은 구문(syntax)과 스키마의 일부를 도와줍니다. 여러분의 애플리케이션은 여전히 다음 사항들을 검증해야 합니다:

  • 테넌트 소유권 (tenant ownership)
  • 권한 부여 (authorization)
  • 필드 수준의 비즈니스 규칙 (field-level business rules)
  • 최대 금액 (maximum amounts)
  • 날짜 범위 (date ranges)
  • 허용된 워크플로 전환 (allowed workflow transitions)
  • 멱등성 키 (idempotency keys)
  • 스키마 버전 호환성 (schema version compatibility)
  • 출력이 자동화를 수행할 만큼 충분히 확신할 수 있는지 여부 (whether the output is confident enough to automate)

제공업체의 구조화된 출력 (structured output)을 최종 관문이 아닌, 유용한 첫 번째 관문으로 생각하세요.

실용적인 TypeScript 검증 패턴

다음은 Zod를 사용한 작은 TypeScript 패턴입니다. 동일한 아이디어를 Pydantic, Valibot, JSON Schema 또는 여러분이 선택한 검증 라이브러리에 적용할 수 있습니다.

import { z } from "zod";

const TicketIntentSchema = z.object({
...

이를 통해 명확한 실패 모드 (failure mode)를 확보할 수 있습니다. 워크플로를 중단하거나, 재시도하거나, 복구하거나, 또는 잘못된 형식의 객체가 다운스트림으로 전달되는 대신 사람의 검토 단계로 라우팅할 수 있습니다.

스키마 검증 이후에 의미론적 검사 (semantic checks) 추가하기

스키마는 amount_cents가 숫자라는 사실은 알려줄 수 있습니다. 하지만 환불이 허용되는지 여부는 알려줄 수 없습니다.

워크플로 경계 근처에 의미론적 검증 (semantic validation)을 추가하세요:

type RefundDecision = {
  schema_version: "refund_decision.v1";
  action: "approve" | "deny" | "needs_review";
...

이 지점이 많은 AI 애플리케이션이 더 안전해지는 구간입니다. 모델은 동작을 제안할 수 있지만, 해당 동작이 허용되는지는 시스템이 결정합니다.

엄격한 예산(budget)을 가진 복구 루프 (repair loop) 구축하기

모든 잘못된 출력이 즉시 실패해야 하는 것은 아닙니다. 어떤 실패는 복구 비용이 저렴합니다:

  • 응답에 JSON을 감싸는 산문 (prose)이 포함된 경우
  • 열거형 (enum)이 유사한 변형을 사용하는 경우
  • 선택적 필드 (optional field)가 누락된 경우
  • 숫자가 문자열로 반환된 경우
  • 스키마 버전은 누락되었지만 경로는 알려진 경우

하지만 복구 루프는 비용이 많이 들고 예측 불가능해질 수 있습니다. 엄격한 예산을 사용하세요.

안전한 복구 정책의 예시는 다음과 같습니다:

  • 일반적인 생성을 한 번 시도합니다.
  • 파싱에 실패하면, 한 번의 추출 또는 복구 호출을 시도합니다.
  • 스키마 검증에 실패하면, 정확한 오류 메시지를 포함하여 한 번의 복구 호출을 시도합니다.
  • 의미론적 검증에 실패하면, 자동으로 복구하지 마세요. 검토 단계로 라우팅하거나 폴백 (fallback) 처리합니다.

복구 프롬프트 구성 예시:

ticket_intent.v1 스키마에 대해 유효한 JSON만 반환하세요.
설명을 추가하지 마세요.
아래 나열된 검증 오류만 수정하세요.
...

"fix only(오직 ~만 수정)"라는 문구는 매우 중요합니다. 이 문구가 없으면 모델이 전체 작업을 재해석하여 이미 유효했던 필드까지 변경할 수 있습니다.

자동화할 수 있는 범위를 결정하세요

구조화된 출력 검증 (Structured output validation)은 단순히 정확성에 관한 것만이 아닙니다. 이는 제어 (control)에 관한 것이기도 합니다.

작업을 위험 계층 (risk tiers)으로 분류하세요:

계층예시 출력자동화 규칙
낮음 (Low)주제 분류, 요약 초안 작성, 편지함 라우팅스키마 검증 후 자동화
...

유효한 JSON을 반환한다고 해서 반드시 워크플로우 (workflow)를 실행해야 한다는 의미는 아닙니다.

위험도가 높은 작업의 경우, 구조화된 출력은 작업을 직접 실행하는 것이 아니라 제안 (proposal)을 생성해야 합니다.

{
  "schema_version": "action_proposal.v1",
  "proposed_action": "refund_invoice",
...

해당 객체는 근거 자료, 테넌트 컨텍스트 (tenant context), 감사 추적 (audit trail)과 함께 사람 검토자에게 보여질 수 있습니다.

모든 스키마에 버전을 부여하세요

스키마 드리프트 (Schema drift)는 소리 없는 살인자입니다. priority를 반환하는 새로운 프롬프트를 배포했지만, 기존 워커 (worker)는 urgency를 기대할 수 있습니다. 또는 큐 소비자 (queue consumer)보다 프론트엔드가 먼저 업데이트될 수도 있습니다. 혹은 폴백 (fallback) 모델이 프롬프트의 오래된 예시를 따를 수도 있습니다.

모든 구조화된 출력에 schema_version 필드를 추가하세요.

좋은 버전 관리는 지루할 정도로 단순해야 합니다:

{
  "schema_version": "ticket_intent.v1",
  "intent": "bug_report",
...

그런 다음 버전을 명시적으로 처리하세요:

switch (output.schema_version) {
  case "ticket_intent.v1":
    return handleTicketIntentV1(output);
...

프롬프트 이름에만 의존하지 마세요. 프롬프트는 런타임 계약 (runtime contracts)이 아닙니다.

검증 실패를 제품 시그널처럼 기록하세요

검증 실패는 단순한 오류가 아닙니다. 이는 제품의 어느 부분이 불분명한지를 알려줍니다.

다음 항목을 추적하세요:

  • 모델 이름 및 버전
  • 프롬프트 버전
  • 스키마 버전
  • 검증 오류 카테고리
  • 복구 시도 횟수
  • 최종 결과
  • 적절하고 개인정보 보호가 안전한 경우, 테넌트 또는 플랜 계층
  • 워크플로우 단계
  • 지연 시간 (latency) 및 토큰 비용

유용한 지표는 다음과 같습니다:

  • 파싱 실패율 (parse failure rate)
  • 스키마 실패율 (schema failure rate)
  • 의미론적 실패율 (semantic failure rate)
  • 복구 성공률 (repair success rate)
  • 잘못된 출력 비용 (invalid output cost)
  • 자동화 방어율 (automation deflection rate)
  • 사람의 검토율 (human review rate)
  • 다운스트림 장애 발생 횟수 (downstream incident count)

특정 의도(intent)가 다른 의도보다 더 자주 검증에 실패한다면, 프롬프트가 불분명할 수 있습니다. 특정 모델에서 열거형 드리프트 (enum drift)가 더 많이 발생한다면, 해당 작업을 다른 곳으로 라우팅하십시오. 제품 변경 후 의미론적 실패 (semantic failures)가 급증한다면, 스키마가 더 이상 워크플로우를 반영하지 못하고 있을 수 있습니다.

적대적 사례(adversarial cases)와 지루한 사례(boring cases)로 테스트하기

대부분의 팀은 해피 패스 (happy paths)를 테스트합니다. 하지만 프로덕션은 이상하지만 정상적인 입력값에서 무너집니다.

모든 출력 계약 (output contract)에 대해 작은 테스트 세트를 만드십시오:

  • 빈 사용자 메시지
  • 매우 긴 메시지
  • 다국어 메시지
  • 상충하는 지침
  • 프롬프트 인젝션 (prompt injection) 시도
  • 오래된 제품 용어
  • 문서에서 복사한 JSON
  • 누락된 테넌트 데이터
  • 지원되지 않는 요청
  • 모호한 요청
  • 고위험 작업 요청
  • 실제 ID처럼 보이는 예시

각 사례에 대해 다음 세 가지 결과 중 하나를 단언(assert)하십시오:

  1. 유효한 구조화된 출력 (valid structured output)
  2. 안전한 폴백 (safe fallback)
  3. 사람의 검토 (human review)

단순히 JSON이 파싱되는지만 확인하는 테스트는 피하십시오. 워크플로우 결정 사항을 테스트하십시오.

예시:

it("송장이 다른 테넌트에 속해 있을 때 환불을 승인하지 않는다", async () => {
  const decision = {
    schema_version: "refund_decision.v1",
...

프롬프트와 스키마를 가깝게 유지하기

흔한 실수는 프롬프트는 한 곳에 저장하고 스키마는 다른 곳에 저장하는 것입니다. 시간이 지나면서 이들은 서로 어긋나게(drift) 됩니다.

다음 파일들을 함께 유지하십시오:

ai/
  ticket-intent/
    prompt.md
...

README에는 다음 내용을 설명해야 합니다:

  • 계약이 수행하는 역할
  • 절대로 해서는 안 되는 일
  • 허용된 열거형 (enum) 값
  • 폴백 (fallback) 동작
  • 소유자
  • 마지막 주요 변경 사항

이렇게 하면 풀 리퀘스트 (pull requests)에서 AI 워크플로우를 검토하기가 더 쉬워집니다. 리뷰어는 프롬프트 변경이 계약에 언제 영향을 미치는지, 그리고 그에 따라 테스트가 변경되었는지 확인할 수 있습니다.

피해야 할 흔한 실수

실수 1: JSON 모드를 완전한 안전 시스템으로 신뢰하는 것

JSON 모드 (JSON mode)는 구문 오류 (syntax failures)를 줄일 수 있습니다. 하지만 테넌트 접근 권한 (tenant access), 비즈니스 규칙 (business rules), 또는 워크플로우 리스크 (workflow risk)를 검증하지는 않습니다.

실수 2: 하나의 객체에 너무 많은 것을 요구하는 것

하나의 거대한 스키마 (schema)는 종종 여러 개의 의사결정을 숨깁니다. 가능할 때마다 분류 (classification), 추출 (extraction), 그리고 액션 제안 (action proposal)을 별도의 계약 (contracts)으로 분리하세요.

실수 3: 의미론적 실패 (semantic failures)를 자동으로 복구하는 것

만약 모델이 송장 금액보다 더 많은 금액을 환불하도록 제안한다면, 모델이 승인할 때까지

  • Pillar (핵심 축): 프로덕션 AI 애플리케이션 아키텍처 (production AI application architecture)
  • Cluster (클러스터): 출력 신뢰성 (output reliability), 워크플로우 안전성 (workflow safety), 스키마 검증 (schema validation), 모델 라우팅 (model routing)
  • Search intent (검색 의도): 실무 구현 가이드 (practical implementation guide)
  • Funnel stage (퍼널 단계): 중간 단계 (middle); 독자는 이미 AI 기능을 구축했거나 구축 중이며 신뢰성이 필요한 상태임
  • Internal link targets (내부 링크 대상): 에이전트 관측성 (agent observability), 청구 검증 (claim verification), 평가 하네스 (evaluation harness), 승인 게이트 (approval gates), 모델 페일오버 (model failover)
  • Next useful articles (다음 유용한 문서): AI 워크플로우를 위한 스키마 마이그레이션 (schema migration for AI workflows), 도구 호출을 위한 의미론적 검증 (semantic validation for tool calls), 구조화된 AI 출력의 회귀 테스트 (regression testing structured AI outputs)

FAQ

LLM 구조화된 출력 검증 (LLM structured output validation)이란 무엇인가요?

LLM 구조화된 출력 검증이란 모델의 응답을 소프트웨어 워크플로우에서 사용하기 전에 구문 (syntax), 스키마 (schema), 그리고 비즈니스 규칙 (business rules)에 따라 확인하는 프로세스입니다. 이를 통해 응답이 유효한 JSON일 뿐만 아니라, 다음 단계로 넘어가기에 안전한지 보장합니다.

프로덕션 AI 앱에서 JSON 모드 (JSON mode)만으로 충분한가요?

JSON 모드가 도움이 되기는 하지만, 그것만으로는 충분하지 않습니다. JSON 모드는 포맷팅을 개선할 수는 있지만, 애플리케이션에는 여전히 스키마 체크 (schema checks), 권한 확인 (authorization checks), 의미론적 검증 (semantic validation), 로깅 (logging), 그리고 폴백 동작 (fallback behavior)이 필요합니다.

파싱 (parsing)과 검증 (validation)의 차이점은 무엇인가요?

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0