Gemini API를 활용한 구조화된 JSON 출력 마스터하기
요약
Gemini API를 사용하여 LLM의 응답을 안정적인 JSON 형식으로 강제하는 기술을 설명합니다. 프롬프트 엔지니어링이나 정규 표현식의 한계를 지적하며, 추론 단계에서 어휘 마스킹을 통해 구조화된 출력을 보장하는 제약된 디코딩 방식을 소개합니다.
핵심 포인트
- LLM의 확률적 특성으로 인한 JSON 출력 불안정성 문제 해결
- 프롬프트 엔지니어링과 정규 표현식 방식의 한계점 분석
- Gemini의 어휘 마스킹을 통한 추론 단계에서의 구조 강제 방식
- JSON 스키마 계약을 통한 프로덕션 환경의 안정성 확보
이것은 발췌본입니다. 전체 기사에는 3가지 실제 제약 조건 스키마 (schema) 사이를 전환하며 Gemini 추론 엔진 (inference engine)이 제약된 토큰 (tokens)을 실시간으로 스트리밍하는 것을 볼 수 있는 **라이브 인터랙티브 스키마 샌드박스 (live interactive schema sandbox)**가 포함되어 있습니다. 전체 인터랙티브 버전 읽기 →
문제점: LLM은 유창하지만 예측 가능하지는 않다
언어 모델 (Language models)은 도움이 되는 소통자가 되도록 최적화되어 있습니다. 이것이 바로 인간에게는 강력한 인터페이스가 되게 하지만, 소프트웨어 아키텍처 (software architectures) 측면에서는 매우 취약한 통합 (integrations) 요소가 되게 만드는 지점입니다.
간단한 추출 요청을 가정해 보겠습니다:
"다음 텍스트에서 제품명, 가격, 재고 여부를 추출하여 JSON으로 반환하세요."
테스트 중에는 모델이 깔끔한 JSON 블록을 반환합니다. 하지만 처리량이 높은 프로덕션 환경 (production environments)에서는 모델의 정렬 동작 (alignment behaviors)에 필연적으로 부딪히게 됩니다:
- 대화형 패딩 (Conversational Padding):
"요청하신 데이터는 다음과 같습니다: ..." - 가변적인 키 이름 (Varying Key Names): 어떤 응답은
"product_name"을 반환하고, 다른 응답은"product", 또 다른 응답은"name"을 반환합니다. - 취약한 타이핑 (Brittle Typings): 숫자형 가격인
279.99가 원시 문자열인"$279.99"가 됩니다.
여러분의 다운스트림 (downstream) TypeScript 클래스들은 처리되지 않은 KeyError 예외를 던지게 됩니다. 실행이 실패하는 것입니다.
정규 표현식 (Regex)과 프롬프트 엔지니어링 (Prompt Engineering)이 당신을 배신하는 이유
전형적인 해결책은 프롬프트 강화 (prompt escalation)입니다:
"오직 순수한 JSON 객체만 반환하세요. 마크다운 (markdown)으로 감싸지 마세요. 대화형 텍스트를 절대 작성하지 마세요."
이 방식은 적은 부하 상황에서는 실패를 줄여주지만, 지시 이행 (instruction-following)은 전적으로 **확률적 (probabilistic)**입니다. 예상치 못한 긴 컨텍스트 (long-context) 입력이 들어오면, 모델은 다시 대화형 기본값으로 돌아갑니다. 하루 50,000건의 호출을 처리하는 시스템에서 1%의 실패율은 500건의 치명적인 오류를 의미합니다.
커스텀 정규 표현식 (regex) 파싱은 더 최악입니다. 제공업체가 모델 파라미터 (parameters)를 업데이트하는 순간, 여러분의 정규 표현식은 프로덕션 데이터를 조용히 오염시킵니다.
제약된 디코딩 (Constrained Decoding): 추론 계층에서 구조 강제하기
Gemini의 구조화된 출력 (Structured Output) 시스템은 사후 처리 (Post-processing)가 아닌, **추론 단계 (Inference step) 자체에서 수행되는 어휘 마스킹 (Vocabulary masking)**을 통해 작동합니다.
응답을 생성할 때, 모델은 약 32,000개 이상의 단어로 구성된 어휘 사전 (Vocabulary) 내 모든 토큰 (Token)의 확률을 예측합니다. 제약 조건이 없다면 모델은 자유롭게 샘플링합니다. 하지만 사용자가 JSON 스키마 (JSON Schema) 계약을 강제하면, Gemini는 이를 상태 머신 (State machine)으로 컴파일합니다. 모든 생성 단계에서 허용되지 않는 토큰들은 정확히 0의 확률로 마스킹됩니다.
만약 특정 필드가 number 타입을 기대한다면, 모든 텍스트 토큰("twenty", "$", 모든 알파벳 문자 등)은 수학적으로 제거됩니다. 이는 단순히 다시 시도하거나 필터링하는 것이 아니라, 신경망의 디코딩 루프 (Decoding loop) 단계에서 이루어지는 구조적 제약입니다.
| 표준 디코딩 (Standard Decoding) | 제약된 디코딩 (Constrained Decoding, Gemini) |
|---|---|
"$279.99" → 45% 확률 | "$279.99" → 0% 확률 |
| ... | ... |
두 가지 API 핵심 요소
두 가지 네이티브 파라미터 (Native parameters)를 사용하여 구조화된 실행을 활성화합니다:
import { GoogleGenerativeAI, SchemaType } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
...
responseMimeType: "application/json"은 모델을 가공되지 않은 문자열 처리 방식에서 구조화된 모드로 전환합니다. responseSchema는 응답이 반드시 충족해야 하는 구조적 계약(키, 타입, 열거형 (Enums), 필수 필드 등)을 정의합니다.
JSON 스키마 심층 분석
열거형 (Enums) — 가장 강력한 제약 조건
열거형 (Enums)은 Gemini가 하드코딩된 값 배열 중에서만 선택하도록 강제합니다. 이는 분류 시스템 (Classification systems)에서 가장 영향력 있는 단일 제약 조건입니다:
{
"type": "string",
"enum": ["IN_STOCK", "OUT_OF_STOCK", "BACKORDER"]
...
환각 (Hallucination)된 변형이 발생하지 않습니다. "in stock"과 "In Stock" 사이의 불일치도 발생하지 않습니다. 스키마가 토큰 수준에서 이를 강제하기 때문입니다.
Nullable 속성
{ "type": "string", "nullable": true }
이는 환각된 값이 생성되는 것을 방지합니다. 입력 텍스트에 해당 필드에 대한 참조가 없는 경우, Gemini는 데이터를 임의로 만들어내는 대신 null을 출력합니다.
다단계 오케스트레이션 패턴 (The Multi-Stage Orchestration Pattern)
복잡한 문서의 경우, 단 한 번의 거대한 추출 호출을 시도하지 마십시오. 대신 다음과 같이 모듈형 파이프라인 (Modular Pipelines)으로 분해하십시오:
Raw Document (원문 문서)
↓
Stage 1: Classification (Schema: DocType)
...
각 단계는 좁고 최적화된 스키마 (Schema)를 사용합니다. 이는 비용을 절감하고, 정확도를 높이며, 디버깅을 매우 쉽게 만듭니다.
프로덕션 검증 레이어 (Production Validation Layer)
스키마 강제 (Schema enforcement)는 구조적 정확성 (Structural correctness)을 보장할 뿐, 논리적 정확성 (Logical correctness)을 보장하지는 않습니다. 항상 다운스트림 (Downstream) 검증을 포함하십시오:
import { z } from "zod";
const SentimentSchema = z.object({
...
Gemini는 출력 키 (Output keys)가 존재하고 타입 (Types)이 일치함을 보장합니다. 하지만 할인 값이 음수인지, 혹은 송장 품목 (Invoice line items)의 합계가 명시된 총액과 일치하지 않는지는 알 수 없습니다. 항상 다운스트림에서 의미론적 파라미터 (Semantic parameters)를 검증하십시오.
엔지니어링 핵심 요약 (Engineering Takeaways)
- 지시 사항 준수 (Instruction-following)에만 의존하지 마십시오. 확률적 모델 (Probabilistic models)은 드리프트 (Drift) 현상이 발생할 수 있습니다. API 레벨에서 구조적 제약 (Structural constraints)을 사용하십시오.
responseMimeType+responseSchema조합은 JSON 추출 파이프라인을 위한 유일한 프로덕션 안전 패턴 (Production-safe pattern)입니다.- 열거형 (Enums)은 가장 강력한 도구입니다. 이는 불일치 버그의 특정 범주를 완전히 제거합니다.
- 제약된 디코딩 (Constrained decoding) ≠ 논리적 검증 (Logical validation). 다운스트림에 Zod 또는 Pydantic을 레이어로 추가하십시오.
- 복잡한 문서 구조의 경우, 다단계 파이프라인 (Multi-stage pipelines)이 단일 거대 호출보다 성능이 뛰어납니다.
🔬 전체 기사에는 대화형 Gemini 제약 엔진 (Gemini Constraint Engine) 샌드박스가 포함되어 있습니다 — 3가지 실제 스키마 계약(Sentiment Tracker, Invoice Parser, Code Auditor) 중 하나를 선택하여 제약된 토큰 스트리밍 (Constrained token streaming)을 실시간으로 확인해 보세요. 또한 복잡한 중첩 스키마 (Nested schemas), 엔티티 추출 패턴 (Entity extraction patterns), 비용/지연 시간 최적화 (Cost/latency optimization), 그리고 에이전틱 오케스트레이션 (Agentic orchestration)의 미래에 대해서도 다룹니다.
작성자: Ebenezer Akinseinde — 소프트웨어 개발자 및 AI 자동화 엔지니어. 빠르고 프로덕션급인 AI 파이프라인과 분산 프론트엔드 시스템을 구축하고 있습니다.
포트폴리오: https://akinseinde.netlify.app · GitHub: https://github.com/ebendttl
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기