~ / posts / developer

Structured Outputs vs JSON 모드 vs 재시도, LLM JSON 비용 비교

LLM 출력을 JSON으로 받을 때 프롬프트+재시도, JSON 모드, Structured Outputs, 자체 서빙 제약 디코딩 중 무엇을 쓸지 정리했어요. 실패율별 재시도 비용을 공식 단가로 계산하고, 엔진별 지연·스키마 지원률·품질 실측과 체크리스트를 담았어요.

LLM이 돌려준 답을 json.loads에 넣는 코드를 짜고 있는데, 가끔 터지는 파싱 에러 때문에 재시도 루프를 붙일지, API의 스키마 옵션을 켤지, 직접 띄운 모델에 문법 제약을 걸지 고민하고 계신가요? 결론부터 말하면 쓰는 API가 스키마 제약(Structured Outputs)을 지원하면 그걸 켜는 게 비용·지연·안정성 모두에서 가장 싸고, 직접 서빙한다면 vLLM·SGLang의 제약 디코딩을 켜되 새 스키마의 첫 요청 지연과 스키마 지원 범위를 먼저 재 봐야 해요. 프롬프트로 부탁하고 재시도하는 방식은 실패율이 몇 % 안쪽일 때만 버틸 만해요.

제약 디코딩의 원리는 알고 있다고 보고, 이 글은 고를 때 필요한 숫자만 다뤄요. 공식 단가로 재시도 비용을 실패율별로 계산하고, 논문이 공개한 엔진별 지연·스키마 지원률·정답률을 나란히 놓았어요.

광고

LLM JSON 출력 방식 네 가지, 보장하는 범위와 비용

같은 "JSON으로 받기"라도 어디서 형식을 맞추느냐에 따라 실패를 처리하는 비용이 달라요. 실무에서 고르는 선택지는 네 가지예요.

방식어디서 형식을 맞추나실패했을 때추가로 드는 것
프롬프트 지시 + 재시도모델의 확률에 맡김파싱·검증 실패 시 다시 호출재시도 호출 비용, 꼬리 지연
JSON 모드디코딩 단계(문법만)스키마 위반은 직접 검사 후 재시도스키마 위반분 재시도
Structured Outputs (스키마 + strict)디코딩 단계(스키마까지)길이 초과·거절만 따로 처리새 스키마 첫 요청 지연, 스키마 문법 제한
자체 서빙 제약 디코딩 (vLLM·SGLang·llama.cpp)내 서버의 디코딩 단계엔진이 못 받는 스키마는 에러 또는 무시문법 컴파일 시간, 토큰당 마스크 계산

OpenAI Structured Outputs 가이드는 JSON 모드를 스키마는 따르지 않는 이전 단계로 설명하고, JSON 모드에서 메시지에 "JSON"이라는 지시가 없으면 모델이 공백만 찍다가 토큰 한도까지 갈 수 있어 API가 에러를 낸다고 적어요.

자체 서빙 쪽은 vLLM Structured Outputs 문서 기준으로 choice·regex·json·grammar·structural_tag 제약을 걸 수 있고, 기본 백엔드 auto가 xgrammar나 guidance를 골라요. 예전 guided_* 필드는 v0.12.0에서 제거됐어요.

재시도 비용 계산 — 실패율별 100만 건 비용

프롬프트+재시도 방식의 비용은 실패율 하나로 정해져요. 계산 조건은 이렇게 잡았어요.

한 번 호출 비용은 1,500 × 0.40 + 300 × 1.60 = 1,080달러(100만 건당)예요. n번째 시도는 입력이 (n−1) × 360토큰씩 늘어요. n번째 시도까지 갈 확률은 실패율 p의 (n−1)제곱이라서, 기대 비용은 세 시도 비용에 1, p, p²를 곱해 더한 값이에요. 끝내 실패하는 비율은 p³이에요.

실패율 값은 근거 있는 숫자를 쓰려고 JSONSchemaBench 논문(Geng 외, 2025) 표 4의 "제약 없이 생성(LM only)" 준수율을 가져왔어요. Llama-3.2-1B가 제약 없이 스키마를 지킨 비율이 GlaiveAI 데이터셋 0.90, GitHub Easy 0.65, GitHub Medium 0.38이었어요. 즉 실패율 10%·35%·62%예요. 작은 모델 기준이라 큰 상용 모델은 이보다 낮을 수 있어서, 2%를 예시 값으로 하나 더 넣었어요.

실패율 p평균 호출 수100만 건 기대 비용기본 대비3번 다 실패하는 비율
2% (예시)1.02회약 1,105달러+2.3%0.0008%
10% (GlaiveAI 수준)1.11회약 1,216달러+12.6%0.1%
35% (GitHub Easy 수준)1.47회약 1,676달러+55.2%4.3%
62% (GitHub Medium 수준)2.00회약 2,365달러+119%23.8%
Structured Outputs vs JSON 모드 vs 재시도, LLM JSON 비용 비교
스키마 제약(재시도 없음)스키마 제약(재시도 없음) · 1,080달러1,080달러실패율 2%실패율 2% · 1,105달러1,105달러실패율 10%실패율 10% · 1,216달러1,216달러실패율 35%실패율 35% · 1,676달러1,676달러실패율 62%실패율 62% · 2,365달러2,365달러

스키마 제약 줄은 스키마가 입력 토큰으로 더해지는 몫을 넣지 않은 값이에요. 실제 청구액은 응답의 usage로 확인하세요.

단위: 달러 · 자료: OpenAI 가격표 gpt-4.1-mini 단가(2026-10-25 확인)로 직접 계산, 실패율은 JSONSchemaBench 표 4의 LM only 준수율에서 가져옴
표로 보기
구분기대 비용
스키마 제약(재시도 없음)1,080
실패율 2%1,105
실패율 10%1,216
실패율 35%1,676
실패율 62%2,365

실패율 2~10%에서는 돈보다 끝내 실패하는 건수가 문제예요. 10%여도 100만 건 중 1,000건은 깨진 채 남아요. 같은 입력은 같은 이유로 또 실패하는 경우가 많아서, 독립 가정으로 낸 "3번 다 실패" 비율은 하한으로 보는 편이 안전해요.

스키마 쪽 토큰은 확실하지 않아요. OpenAI 함수 호출(Function calling) 가이드는 함수 정의가 입력 토큰으로 청구된다고 적지만, response_format 스키마의 계산 방식은 가이드에 명시가 없어요. 큰 스키마라면 켜기 전후의 usage 입력 토큰을 비교해 보세요.

지연 비교 — 재시도는 꼬리를, 제약 디코딩은 첫 요청을 늘린다

한 번 호출이 2초라면 재시도 포함 평균 지연은 실패율 10%에서 2.22초, 35%에서 2.94초로 작아 보여요. 문제는 꼬리 지연이에요. 실패율이 5%만 넘어도 상위 5% 요청은 최소 두 번 호출되니 p95가 4초로 두 배가 되고, 실패율 35%면 세 번째 시도까지 가는 요청이 12.25%라서 p95가 6초가 돼요. 재시도는 평균보다 꼬리를 망가뜨려요.

Structured Outputs는 반대로 꼬리가 아니라 첫 요청에 비용을 몰아요. OpenAI 가이드는 어떤 스키마든 첫 요청은 API가 스키마를 처리하느라 지연이 추가되고, 같은 스키마로 보내는 이후 요청에는 추가 지연이 없다고 적어요. 요청마다 스키마를 조금씩 바꿔 만들면 매번 첫 요청이 되니, 스키마는 상수로 고정하고 배포 직후 한 번 미리 호출해 두세요.

자체 서빙에서는 엔진 차이가 훨씬 커요. JSONSchemaBench 표 2는 Llama-3.1-8B-Instruct, A100 80GB 한 장, 배치 1, llama.cpp 추론 엔진 위에서 제약 엔진별 문법 컴파일 시간(GCT), 첫 토큰까지 시간(TTFT), 토큰당 시간(TPOT)의 중앙값을 쟀어요. GlaiveAI 데이터셋 값으로 300토큰 응답 하나 만드는 시간을 TTFT + 300 × TPOT로 계산했어요.

제약 엔진 (llama.cpp 위)컴파일TTFTTPOT300토큰 응답 시간
제약 없음없음0.10초15.40ms약 4.72초
Guidance0.00초0.24초6.37ms약 2.15초
llama.cpp 문법0.05초0.20초29.98ms약 9.19초
Outlines3.48초3.65초30.33ms약 12.75초
Structured Outputs vs JSON 모드 vs 재시도, LLM JSON 비용 비교
GuidanceGuidance · 2.1초2.1초제약 없음제약 없음 · 4.7초4.7초llama.cpp 문법llama.cpp 문법 · 9.2초9.2초OutlinesOutlines · 12.8초12.8초

Guidance가 제약 없음보다 빠른 건 문법이 정해 둔 토큰을 모델 없이 건너뛰는 fast-forward 덕분이에요.

단위: 초 · 자료: JSONSchemaBench 표 2 GlaiveAI 중앙값(TTFT + 300 × TPOT)으로 직접 계산
표로 보기
구분응답 시간
Guidance2.1
제약 없음4.7
llama.cpp 문법9.2
Outlines12.8

스키마 지원 범위 — 엔진마다 받아 주는 스키마가 다르다

지연보다 먼저 볼 것은 엔진이 내 스키마를 받아 주기는 하는지예요. 지원하지 않는 키워드를 만나면 엔진은 에러를 내거나 조용히 무시해요. llama.cpp 문법 안내는 JSON 스키마의 일부만 GBNF로 바꾸고 지원하지 않는 기능은 조용히 건너뛴다고 적어요. 에러 없이 제약이 빠지는 쪽이 더 위험해요.

JSONSchemaBench 표 4의 "실제 커버리지"는 스키마를 받아 주고, 그 스키마에 맞는 출력까지 낸 비율이에요. 실제 GitHub 저장소에서 모은 중간 난이도 스키마(GitHub Medium) 값이에요.

Structured Outputs vs JSON 모드 vs 재시도, LLM JSON 비용 비교
GuidanceGuidance · 69%69%llama.cppllama.cpp · 57%57%XGrammarXGrammar · 52%52%제약 없음(Llama-3.2-1B)제약 없음(Llama-3.2-1B) · 38%38%OutlinesOutlines · 29%29%OpenAIOpenAI · 12%12%

논문은 OpenAI·Gemini가 다른 모델을 써서 오픈소스 엔진과 직접 비교할 수 없다고 적어요. OpenAI 수치가 낮은 주된 이유는 지원하지 않는 스키마를 거부해서예요.

단위: % · 자료: Geng 외, JSONSchemaBench 표 4 Empirical 열 (오픈소스 엔진은 Llama-3.2-1B, OpenAI는 자사 모델)
표로 보기
구분커버리지
Guidance69
llama.cpp57
XGrammar52
제약 없음(Llama-3.2-1B)38
Outlines29
OpenAI12

OpenAI는 받아 준 스키마에 대해서는 준수율이 데이터셋별로 0.92~1.00이었어요. 받아 주는 스키마 자체가 적은 거예요. 가이드 규칙을 벗어난 스키마는 strict: true에서 에러로 돌아오는데, 기존 스키마를 넣을 때 자주 걸리는 규칙은 이래요.

OpenAPI 명세에서 뽑은 스키마를 그대로 넣으면 이 규칙에 걸리기 쉬워요. LLM용 스키마는 따로 작게 만들고, 쉼표·따옴표 실수는 JSON Validator로 먼저 확인하세요. pattern을 걸 계획이면 vLLM 문서상 xgrammar·guidance·outlines가 러스트 스타일 정규식을 쓰니, Regex Lab에서 시험할 때 전방탐색 같은 기능은 빼고 만드는 편이 안전해요.

품질 — 형식을 묶으면 답이 나빠지나

여기는 연구 결과가 엇갈려요. Let Me Speak Freely? 논문(Tam 외, 2024)은 형식 제한을 걸면 추론 능력이 눈에 띄게 떨어지고, 제약이 엄격할수록 하락이 크다고 보고했어요. 반면 JSONSchemaBench 표 8은 Llama-3.1-8B-Instruct에서 제약 디코딩이 오히려 정답률을 올렸다고 보고해요.

방식Last LettersShuffle ObjectsGSM8K
제약 없음50.7%52.6%80.1%
XGrammar51.2%52.7%83.7%
llama.cpp 문법52.0%52.6%82.4%
Outlines53.3%53.0%81.6%
Guidance54.0%55.9%83.8%

실무 규칙으로 바꾸면 이래요.

상황별 추천 — 무엇을 켤까

출력이 사람에게 보여 줄 글인지, API가 스키마 제약을 지원하는지, 모델을 직접 서빙하는지, JSON 모드라도 켤 수 있는지를 차례로 물어 제약 없음, Structured Outputs, 엔진 제약 디코딩, JSON 모드와 검증, 프롬프트와 재시도 중 하나를 고르는 순서도
위에서부터 묻고 처음 '예'가 나오는 칸의 방식을 먼저 써요
상황추천이유
문서에서 필드 추출, 상용 APIStructured Outputs (strict)재시도 비용과 끝내 실패하는 건수가 거의 사라져요
에이전트·도구 호출도구 정의에 strict: true인자 형식 오류로 도구가 터지는 경우를 막아요
분류(긍정/부정 등 몇 개 중 하나)enum 또는 vLLM choice출력 토큰이 몇 개라 제약 비용도 거의 없어요
직접 서빙, 스키마가 몇 개로 고정vLLM·SGLang 기본 백엔드(auto)컴파일은 스키마당 한 번이라 금방 상쇄돼요
직접 서빙, 요청마다 스키마가 바뀜컴파일이 짧은 엔진을 실측으로 고르기컴파일 시간이 매 요청 TTFT에 그대로 더해져요
복잡한 기존 스키마(조합 키워드 많음)LLM용 축약 스키마 + 앱 쪽 전체 검증엔진이 못 받거나 무시하는 부분을 피해요

vLLM을 OpenAI 호환 서버로 띄웠다면 JSON 스키마는 response_format에 "type": "json_schema"로, 선택지·정규식·문법은 extra_body={"structured_outputs": {...}}로 넘겨요. 문서는 백엔드를 바꾸면 허용 토큰과 출력이 달라질 수 있다고 경고하고, lm-format-enforcer 백엔드는 v0.35.0에서 제거될 예정이라고 적어요. 추론(reasoning) 모델과 함께 쓸 때는 문서 예시처럼 --reasoning-parser deepseek_r1 같은 추론 파서를 지정해 서버를 띄워요.

어느 칸을 고르든 응답을 받는 코드는 비슷한 모양이 돼요. 아래는 형식 실패와 내용 실패를 나눠 처리하는 뼈대예요. 길이 초과는 재시도해도 같은 한도에 또 걸리니 따로 분기해요.

from pydantic import BaseModel, ValidationError

class Invoice(BaseModel):
    reasoning: str            # 근거를 먼저 쓰게 앞에 둔다
    vendor: str | None        # 원문에 없으면 null
    total: int | None

def parse_invoice(call_llm, prompt, max_attempts=3):
    for attempt in range(1, max_attempts + 1):
        text, finish = call_llm(prompt)
        if finish == "length":
            raise RuntimeError("토큰 한도 초과 — 재시도 말고 한도·입력 길이 조정")
        try:
            return Invoice.model_validate_json(text)
        except ValidationError as e:
            if attempt == max_attempts:
                raise
            prompt += f"\n\n이전 출력이 형식에 맞지 않았어요: {e.errors()[:3]}"

자주 보는 파싱 에러 메시지가 무엇을 뜻하는지는 JSON 파싱 에러 원인 6가지에 정리해 두었어요.

도입 체크리스트

스키마 설계, 요청 처리, 운영 측정 세 칸으로 나눠 근거 필드 순서·null 허용·enum·maxItems·스키마 고정, 길이 초과·refusal 분기·앱 검증·재시도 상한, 실패율·첫 요청 지연·필드별 정답률 감시를 정리한 체크리스트
왼쪽 칸부터 차례로 확인해요
  1. 지금 실패율부터 재요. 실제 트래픽 몇천 건으로 파싱 실패율과 스키마 위반율을 따로 기록하면 위 비용표의 어느 줄인지 정해져요.
  2. 스키마를 LLM용으로 작게 만들어요. 필드 수를 줄이고, 분류 값은 enum으로, 배열은 maxItems로 상한을 둬요. 상한 없는 배열은 출력 토큰 한도에 걸려 잘리기 쉬워요.
  3. 스키마는 상수로 고정해요. 스키마 문자열에 날짜·사용자 이름 같은 값을 넣지 마세요.
  4. 프롬프트에도 필드 의미를 적어요. llama.cpp 문서는 JSON 스키마가 프롬프트에 주입되지 않는다고 명시해요.
  5. 세 가지 예외를 분기해요. 토큰 한도로 잘린 응답(OpenAI 가이드 예제는 상태 incomplete·사유 max_output_tokens를 확인), 모델이 거절한 응답(refusal 필드), 콘텐츠 필터로 끊긴 응답이에요. 이 셋은 스키마 제약을 켜도 스키마에 맞지 않을 수 있어요.
  6. 앱 쪽 검증은 남겨 둬요. 제약 엔진이 조용히 무시한 키워드, 백엔드 교체로 바뀐 동작을 마지막에 잡아 줘요.
  7. 운영 지표를 정해요. 실패율·재시도율, 첫 요청 지연, TTFT·TPOT p99, 필드별 정답률, null 비율을 모델·엔진을 바꿀 때마다 다시 봐요.

직접 서빙하면서 지연을 더 줄이려면 투기적 디코딩 방식별 속도·비용 비교도 보세요. 제약 디코딩과 함께 켤 수 있는지는 엔진·버전마다 달라요.

자주 묻는 질문

JSON 모드만 켜도 되지 않나요?

JSON 모드는 파싱되는 JSON까지만 보장해요. 키 이름 오타, 빠진 필수 필드, 잘못된 타입은 그대로 나올 수 있어서 스키마 검증과 재시도가 여전히 필요해요. 스키마 제약을 지원하는 모델이라면 Structured Outputs가 같은 일을 재시도 없이 해요.

Structured Outputs를 켜면 비용이 더 드나요?

가격표에 Structured Outputs 전용 요금은 따로 없어요. 함수 호출의 도구 정의는 입력 토큰으로 청구된다고 공식 가이드에 적혀 있고, response_format 스키마의 토큰 계산 방식은 가이드에 명시가 없어요. 큰 스키마라면 켜기 전후의 usage 입력 토큰을 비교해 보세요.

재시도는 몇 번이 적당한가요?

실패가 독립이라고 가정해도 실패율 10%에서 재시도 2회 뒤 남는 실패가 0.1%이고, 3회째부터는 줄어드는 폭이 작아요. 같은 입력은 같은 이유로 반복 실패하는 경우가 많아서, 2회를 넘기기보다 스키마를 단순화하거나 제약 디코딩으로 바꾸는 편이 효과가 커요.

제약 디코딩을 쓰면 환각이 줄어드나요?

형식 오류는 사라지지만 내용의 정확성은 그대로예요. 필수 필드에 넣을 정보가 원문에 없으면 오히려 그럴듯한 값을 지어낼 수 있어요. 모를 수 있는 필드는 null을 허용하고, 필드별 정답률을 따로 측정해야 해요.

정리

참고한 자료

#Structured Outputs 선택#JSON 모드 차이#LLM JSON 재시도 비용#제약 디코딩 지연#vLLM structured_outputs#XGrammar Outlines 비교#JSON 스키마 강제#LLM 파싱 실패율
← 이전 글투기적 디코딩 방식 고르기, EAGLE·MTP·n-gram 속도와 GPU 비용 비교다음 글 →테라폼 plan 옵션 고르는 법, refresh·저장된 plan·상태 분리 판단표

더 둘러보기

💻 개발자 다른 도구

🔥 많이 찾는 도구

🔮 재미로 한 번 더

📝 읽어 볼 만한 글

글 전체 보기 →