로컬 LLM에 JSON을 강제하기 — 구조화 출력으로 파싱 실패 없애기
요약: 로컬 LLM을 실제 애플리케이션에 연동할 때, 모델이 JSON만 출력하도록 강제하는 프롬프트 부탁·few-shot 예시·GBNF 문법 제약·JSON 스키마 모드 네 가지 방법을 상황에 맞게 선택·조합하면 설명문과 코드펜스가 섞여 터지는 파싱 실패를 없애고 항상 깨끗한 구조화 출력을 얻을 수 있다.
로컬 LLM을 실제 애플리케이션에 연동할 때, 모델이 JSON만 출력하도록 강제하는 프롬프트 부탁·few-shot 예시·GBNF 문법 제약·JSON 스키마 모드 네 가지 방법을 상황에 맞게 선택·조합하면 설명문과 코드펜스가 섞여 터지는 파싱 실패를 없애고 항상 깨끗한 구조화 출력을 얻을 수 있다.
한 줄 요약: 로컬 LLM에서 JSON 구조화 출력을 강제하는 기술은 파싱 실패 없는 앱 개발의 핵심이며, 네 가지 방법의 강제 세기와 비용만 이해하면 초보자도 안정적인 결과를 빠르게 얻는다.
왜 로컬 모델은 JSON을 자꾸 깨뜨릴까?#
대부분의 로컬 LLM은 '대화형 어시스턴트'로 훈련됐다. 사용자에게 최대한 친절하려는 성향이 몸에 배어 있어서, "다른 설명 없이 JSON만 출력해"라고 못 박아도 종종 "알겠습니다, 요청하신 형식으로 정리해 드릴게요" 같은 인사말을 앞에 붙이거나 결과를 코드펜스(백틱 세 개)로 감싸 내보낸다. 문제는 여기서 시작된다. 당신의 앱은 그 응답을 곧바로 json.loads()에 넣는데, 앞뒤에 붙은 설명문·마크다운 때문에 파싱이 깨진다. 작고 덜 정렬된(instruction-tuned) 모델일수록 이 증상이 심하다. 결국 개발자는 정규식으로 JSON만 도려내거나 실패 시 재시도하는 방어 코드를 덕지덕지 붙이게 되고, 이는 유지보수 부담과 지연으로 되돌아온다.
네 가지 방법은 어떻게 다를까?#
구조화 출력을 강제하는 방법은 '강제의 세기'와 '드는 품'에 따라 네 가지로 나뉜다. 약한 방법일수록 시도는 빠르지만 신뢰도가 낮고, 강한 방법일수록 잘못된 출력을 생성 이전에 원천 차단한다.
| 방법 | 강제 수준 | 대표 지원 런타임 | 추가 작업 | 실패 시 증상 |
|---|---|---|---|---|
| 프롬프트로 부탁 | 없음(권고만) | 사실상 전부 | 없음 | 설명문·코드펜스가 섞여 파싱 실패 |
| few-shot 예시 | 약함 | 사실상 전부 | 예시 2~3개 작성 | 예시와 다른 입력에서 형식 흔들림 |
| 문법 제약(GBNF/정규식) | 강함(디코딩 단계 차단) | llama.cpp·Ollama | 문법 규칙 작성 | 문법 밖 토큰이 원천적으로 안 나옴 |
| JSON 스키마 모드 | 강함(스키마 강제) | Ollama·vLLM·OpenAI 호환 | 스키마 정의 | 스키마 위반 자체가 불가능 |
프롬프트로 부탁하기는 가장 기본이고 추가 비용이 거의 없다. 시스템 프롬프트에 "유효한 JSON 객체 하나만 반환하라. 인사말·설명·마크다운 금지" 같은 강한 지시를 넣는 방식이다. 초보자가 가장 먼저 시도하기 좋지만, 모델에게 완전한 자유를 주므로 실행마다 결과가 흔들리고 길거나 낯선 입력에서 특히 잘 깨진다.
few-shot 예시는 런타임을 바꾸지 않고 신뢰도를 올린다. 원하는 입력-출력 쌍 2~3개를 프롬프트에 미리 보여주면 모델이 필드 이름·중첩 형태·군더더기 없는 형식을 흉내 낸다. 다만 예시와 크게 다른 입력이 오면 강제가 '권고' 수준이라 형식이 다시 흔들릴 수 있다.
문법 제약(GBNF)은 llama.cpp 계열이 제공하는 강한 기능으로, 강제를 디코더 안으로 옮긴다. 허용되는 토큰 시퀀스를 규칙으로 정의하면 런타임이 규칙을 어기는 토큰을 아예 뱉지 않는다. 초보자용 비유로는, 글자 타일 게임에서 규칙에 안 맞는 타일을 보드에서 아예 치워버려 잘못된 단어가 만들어질 가능성 자체를 없애는 것과 같다. 설명문·코드펜스가 원천적으로 나올 수 없다. 대신 문법을 한 번 작성하는 학습 곡선이 있다.
JSON 스키마 모드는 Ollama·vLLM 등 최신 런타임이 지원하는 가장 현대적인 방법이다. 필수 필드·타입·중첩·값 제약을 담은 JSON Schema(또는 Pydantic 모델)를 호출에 넘기면, 런타임이 스키마를 어기는 토큰을 실시간으로 막는다. 스키마 위반이 '드물게 일어나는 일'이 아니라 '불가능한 일'이 되므로, 파서는 매 성공 응답마다 완벽히 형식화된 JSON을 받는다.
그래서 뭘 먼저 써야 하나?#
무작정 가장 센 방법부터 쓰기보다, 데이터 구조의 복잡도와 개발 단계에 맞춰 단계적으로 접근하는 게 효율적이다.
- 간단한 평면 구조(문자열·숫자 몇 개)면 → Ollama·vLLM의 JSON 스키마 모드부터. 스키마도 짧고 강제 효과가 크다.
- 배열 속 객체·조건부 필드·깊은 중첩이면 → GBNF 문법 제약. 스키마로 표현하기 힘든 세밀한 생성 규칙을 건다.
- 프로토타입·요구사항 유동기면 → 프롬프트 부탁 + few-shot 2~3개로 빠르게 검증하고, 안정화가 필요해지면 위 강제 방법으로 승급한다.
대부분의 팀은 가장 가벼운 방법으로 시작해 '파싱 실패가 얼마나 나는지' 측정한 뒤, 데이터 형태가 굳으면 문법·스키마 강제로 올라간다. 강한 방법일수록 LLM 호출 주변에 쌓이는 방어 코드도 줄어든다.
참고: 이 글은 2026-07-12 KST 기준이며, Ollama·llama.cpp·vLLM 등 런타임 버전에 따라 구조화 출력 지원 방식과 문법이 달라질 수 있으니 배포 전 각자 사용하는 런타임의 최신 공식 문서로 반드시 재확인하라.
이 시리즈: 이 시리즈 허브 — 로컬 LLM·RAG 7가지 관문 · 함수 호출(tool calling) 첫걸음 · 프롬프트 캐싱으로 속도 올리기
함께 읽기: Phi-4-mini 3.8B로 128K 장문을 로컬에서 처리한다, 일상 업무에 붙이는 Llama 3.3 70B
참고 링크
Responses
No responses yet. Be the first to respond.