LLM 출력이 들쭉날쭉할 때: 구조화 출력과 JSON 강제 응답 다루기
LLM 응답을 JSON 스키마로 안정화하고, 파싱 실패·검증 오류·재시도까지 1인 개발자가 운영 가능한 형태로 연결하는 방법을 정리합니다.
목차
이 글의 목차
LLM을 기능에 연결하면 데모 단계에서는 꽤 잘 작동합니다. 하지만 사용자가 늘고 후속 로직이 붙는 순간 문제가 드러납니다. 어떤 응답은 JSON이고, 어떤 응답은 설명이 섞이며, 같은 질문에도 필드가 빠지거나 값의 형식이 달라집니다.
조코헌트에 출시된 프로덕트 312개 가운데 AI 도구 분야가 40%를 차지합니다. 그만큼 LLM을 붙이는 일은 흔해졌지만, 실제 품질 차이는 모델 선택보다 “출력 계약을 얼마나 단단하게 만들었는가”에서 갈리는 경우가 많습니다.
자유 텍스트가 운영에서 깨지는 이유
모델에게 기대하는 것과 코드가 필요한 것
사람은 “대략 이런 내용이구나”를 이해할 수 있지만, 코드는 정해진 키와 타입을 기대합니다. title이 문자열이어야 하는데 배열로 오거나, items가 없어지는 순간 후속 로직은 예외를 낼 수 있습니다.
또한 “JSON으로 답해줘”라는 지시만으로는 충분하지 않을 때가 많습니다. 모델은 친절한 설명, 마크다운 코드펜스, 추가 주석을 함께 붙일 수 있습니다. 문제는 모델이 말을 잘했느냐가 아니라, 애플리케이션이 안전하게 소비할 수 있는 결과를 받았느냐입니다.
먼저 출력 계약을 적는다
호출 전에 결과를 문서처럼 정의하세요.
- 필수 키와 선택 키
- 각 필드의 타입
- 허용된 값 목록(enum)
- 빈 값이 허용되는지 여부
- 실패를 표현하는 방식
예를 들어 분류 기능이라면 category는 문자열, confidence는 숫자, reasons는 문자열 배열처럼 구체적으로 적습니다. 제품 범위가 아직 넓다면 혼자서 LLM 앱 MVP 1주일에 만들기: 기능 범위 좁히는 법을 참고해 출력 형식부터 작게 잡는 편이 좋습니다.
1. 스키마를 프롬프트가 아닌 계약으로 다루기

JSON 예시보다 스키마가 먼저다
프롬프트에 예시 JSON만 넣으면 모델이 예시의 값이나 구조를 그대로 흉내 낼 수 있습니다. 예시는 보조 자료로 두고, 실제 검증 기준은 JSON Schema나 코드 기반 스키마로 관리하세요.
스키마에는 다음처럼 “허용 범위”를 명시합니다.
result: object
result.summary: string, required
result.tags: array of string, maximum 5 items
result.status: one of "ready", "review", "reject"
구조화 출력 기능을 제공하는 API라면 해당 기능을 우선 검토하고, 그렇지 않다면 응답 형식 지시와 애플리케이션 검증을 함께 사용합니다. 중요한 점은 모델이 지킨다고 가정하지 않고, 서버가 다시 확인하는 것입니다.
후속 로직에 필요한 값만 받는다
LLM에게 화면에 보여줄 긴 문장과 데이터베이스에 저장할 상태값을 한 번에 맡기면 검증 범위가 커집니다. 저장용 응답은 최소 필드로 줄이고, 설명 문장은 별도 출력으로 분리하는 방식이 관리하기 쉽습니다. 필드가 적을수록 누락·형식 오류를 찾기도 쉽습니다.
2. JSON 강제 응답과 서버 검증 조합
JSON 강제는 “파싱 가능한 형식”을 얻는 장치이고, 스키마 검증은 “업무에 쓸 수 있는 값”인지 확인하는 장치입니다. 둘을 같은 것으로 보면 안 됩니다.
응답 처리 순서는 다음처럼 고정해두세요.
- 원문 응답을 받는다.
- 코드펜스나 불필요한 앞뒤 문자를 허용할지 정책을 정한다.
- JSON 파싱을 시도한다.
- 스키마로 타입·필수값·허용값을 검증한다.
- 검증된 객체만 저장하거나 다음 함수에 넘긴다.
파싱 전 문자열을 무리하게 잘라내는 방식은 임시 응급처치로만 두는 편이 좋습니다. 중괄호를 찾아 강제로 추출하면 문자열 안의 중괄호나 잘린 JSON을 놓칠 수 있습니다. 가능하면 원문, 파싱 결과, 검증 오류를 서로 구분해 로그로 남기세요.
3. 실패를 예외가 아니라 흐름으로 설계하기

재시도는 같은 요청의 반복이 아니다
파싱에 실패했다고 같은 프롬프트를 그대로 다시 보내면 같은 실패를 반복할 수 있습니다. 재시도 요청에는 오류 원인을 짧게 포함하세요.
이전 응답은 JSON 파싱에 실패했습니다.
오류: 필수 키 "status"가 없습니다.
설명 없이 아래 스키마에 맞는 JSON 객체만 반환하세요.
재시도 횟수에는 상한을 두고, 매번 원본 요청 전체를 불필요하게 반복하지 않도록 입력과 출력 길이도 확인합니다. 비용과 지연을 함께 관리하려면 LLM API 비용 폭탄 피하기: 토큰 절약·캐싱·모델 선택 실전 전략에서 다룬 캐싱·모델 선택 관점도 연결해볼 수 있습니다.
재시도 실패 뒤에는 안전한 대체 경로를 둔다
최종 실패 시 빈 객체를 반환하면 나중에 더 모호한 오류가 납니다. 대신 다음 중 하나를 명확히 선택하세요.
- 사용자에게 다시 입력을 요청한다.
- 사람이 검토할 상태로 저장한다.
- 기능을 제한한 기본 응답을 보여준다.
- 작업 자체를 실패 처리하고 재실행 버튼을 제공한다.
어떤 경로를 택하든 사용자가 본 결과와 내부 상태가 어긋나지 않게 해야 합니다.
4. 출시 전에 확인할 운영 체크리스트
실제 코드에서는 정상 응답만 테스트하지 마세요. 필드 누락, 잘못된 타입, 빈 배열, 긴 입력, 중간에 끊긴 응답, 설명이 섞인 응답을 각각 만들어 검증합니다.
또한 로그에는 프롬프트 전체나 개인정보를 무심코 남기지 않도록 주의하세요. 요청 ID, 모델 응답 상태, 검증 실패 필드, 재시도 횟수처럼 원인 추적에 필요한 정보만 남기는 방식이 안전합니다. AI가 만든 코드를 검토할 때도 같은 원칙이 적용되므로 AI가 짠 코드를 믿어도 될까: 바이브코딩 결과물 검증 체크리스트의 테스트 관점을 함께 확인해보세요.
구조화 출력의 목표는 LLM을 완벽하게 통제하는 것이 아닙니다. 실패해도 어디서 깨졌는지 알고, 재시도할지 중단할지 결정할 수 있는 경계를 만드는 것입니다. 스키마, 파싱, 검증, 대체 경로를 한 흐름으로 묶으면 모델이 조금 흔들려도 제품 전체가 함께 흔들리는 일은 줄어듭니다.