프로그램이 소비할 출력은 사람이 읽을 산문이 아니라 스키마를 따르는 데이터여야 합니다. 이 수업에서는 JSON 출력을 얻는 세 가지 방법 — 프롬프트 지시, 응답 사전 채우기(prefill), 툴 사용(tool use) 강제 — 과 각 방법의 신뢰도 차이, 그리고 스키마 설계 원칙을 다룹니다.
세 가지 방법과 신뢰도 스펙트럼
Claude에게 JSON을 출력시키는 방법은 신뢰도 순으로 세 단계가 있습니다.
1단계: 프롬프트 지시. "JSON으로만 응답하세요"라고 지시하고 스키마를 보여 주는 방법입니다. 간단하지만 "물론입니다! 요청하신 JSON은 다음과 같습니다:" 같은 서두(preamble)나 마크다운 코드 펜스가 섞일 위험이 있습니다.
2단계: 응답 사전 채우기(prefilling). assistant 메시지의 시작을 {로 미리 채워 Claude가 그 뒤를 이어 쓰게 하는 기법입니다. 서두가 원천 차단되어 첫 글자부터 JSON이 시작됩니다.
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
messages=[
{"role": "user", "content": "이 리뷰에서 감정과 키워드를 추출: ..."},
{"role": "assistant", "content": "{"}, # 사전 채우기
],
)
result = "{" + response.content[0].text
3단계: 툴 사용 강제. 출력 스키마를 툴의 input_schema로 정의하고 tool_choice로 해당 툴 호출을 강제하는 방법입니다. Claude는 스키마에 맞는 인자를 생성하도록 제약되므로 세 방법 중 구조 준수율이 가장 높습니다.
tools = [{
"name": "record_analysis",
"description": "리뷰 분석 결과를 기록한다",
"input_schema": {
"type": "object",
"properties": {
"sentiment": {"type": "string",
"enum": ["positive", "negative", "neutral"]},
"keywords": {"type": "array", "items": {"type": "string"}},
},
"required": ["sentiment", "keywords"],
},
}]
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
tools=tools,
tool_choice={"type": "tool", "name": "record_analysis"},
messages=[{"role": "user", "content": "리뷰 분석: ..."}],
)
data = response.content[0].input # 파싱된 dict가 바로 나온다
시험에서는 "가장 신뢰할 수 있는 구조화 출력 방법"을 물으면 툴 사용 강제가 정답이고, "가장 간단한 서두 제거 방법"을 물으면 사전 채우기가 정답입니다.
스키마 설계 원칙
구조화 출력의 품질은 스키마 설계에서 절반이 결정됩니다. 핵심 원칙은 다음과 같습니다.
enum으로 선택지를 제한합니다. 자유 문자열 필드는 "긍정", "positive", "Positive!" 같은 변형을 만듭니다. 가능한 값이 유한하면 반드시 enum으로 고정하세요.
필드 설명(description)을 판단 기준까지 포함해 작성합니다. 스키마의 description은 단순 라벨이 아니라 미니 프롬프트입니다. "confidence: 분석 확신도. 리뷰가 반어법이거나 맥락이 모호하면 0.5 이하로 설정"처럼 판단 기준을 담으면 출력 일관성이 올라갑니다.
선택 필드보다 명시적 null을 선호합니다. 값이 없을 때 필드를 생략하게 두면 후처리 코드가 존재 여부 검사로 복잡해집니다. "type": ["string", "null"]로 항상 필드가 존재하게 설계하는 편이 안전합니다.
중첩 깊이를 제한합니다. 3단계 이상 중첩된 스키마는 준수율이 떨어집니다. 깊은 구조가 필요하면 작업을 나누어 여러 번 호출하는 것을 검토하세요.
{
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum": ["positive", "negative", "neutral"],
"description": "리뷰 전체의 지배적 감정. 반어법이 의심되면 문자 그대로가 아니라 의도된 감정으로 판단"
},
"refund_requested": {
"type": "boolean",
"description": "환불·교환을 명시적으로 요구했는지. 단순 불만 표출은 false"
}
},
"required": ["sentiment", "refund_requested"]
}
이 예시처럼 description에 경계 사례 처리 기준("단순 불만 표출은 false")을 넣는 것이 실무의 핵심 기술입니다.
파싱 실패에 대비한 방어적 소비
어떤 방법을 쓰든 출력을 소비하는 코드는 방어적으로 작성해야 합니다. 프롬프트 지시나 사전 채우기 방식에서는 JSON 파싱 자체가 실패할 수 있고, 툴 강제 방식에서도 의미적 오류(빈 배열, 모순된 값)는 남습니다.
방어 계층은 세 겹으로 구성합니다.
- 구문 검증:
json.loads가 성공하는지. 실패 시 마크다운 펜스 제거 같은 정규화를 시도한 뒤 재파싱합니다. - 스키마 검증: Pydantic 같은 라이브러리로 타입·필수 필드·enum 값을 검증합니다.
- 의미 검증: 도메인 규칙(합계가 100%인지, 날짜가 미래인지 등)을 코드로 확인합니다.
import json
from pydantic import BaseModel, ValidationError
class Analysis(BaseModel):
sentiment: str
keywords: list[str]
def parse_response(raw: str) -> Analysis | None:
text = raw.strip()
if text.startswith("~~~"):
text = text.split("\n", 1)[1].rsplit("~~~", 1)[0]
try:
return Analysis.model_validate(json.loads(text))
except (json.JSONDecodeError, ValidationError):
return None # 호출자가 재시도 루프로 처리
검증 실패 시의 대응은 다음 수업(4.4 검증과 재시도 루프)에서 다루지만, 여기서 기억할 원칙은 실패를 감지하는 코드 없이 구조화 출력을 신뢰해서는 안 된다는 것입니다. 시험에서 "툴 강제를 쓰면 검증이 불필요하다"는 선택지는 오답입니다. 구조는 보장되어도 의미는 보장되지 않기 때문입니다.
시험 함정
- '프롬프트에 JSON으로 답하라고 쓰면 항상 순수 JSON이 나온다'는 선택지는 오답입니다. 서두와 코드 펜스가 섞일 수 있습니다.
- 사전 채우기(prefill)의 효과를 '토큰 절약'으로 설명하면 오답입니다. 핵심 효과는 서두 제거와 형식 고정입니다.
- 가장 높은 구조 준수율을 묻는 문제의 정답은 tool_choice로 툴 호출을 강제하는 방법입니다.
- '툴 강제를 쓰면 출력 검증이 필요 없다'는 선택지는 함정입니다. 구문은 보장되지만 의미적 오류는 남습니다.
- 가능한 값이 유한한 필드를 자유 문자열로 두는 스키마 설계는 오답 선택지의 단골입니다. enum으로 제한해야 합니다.
- 스키마 description을 장식으로 취급하는 선택지는 오답입니다. description은 판단 기준을 전달하는 미니 프롬프트입니다.
실습 시나리오
실제 시험과 같은 형식의 시나리오 문제입니다.
송장(invoice) PDF에서 추출한 텍스트를 구조화된 데이터로 변환하는 파이프라인을 만들고 있습니다. 다운스트림 시스템은 파싱 실패에 민감하며, 금액 필드의 정확성이 특히 중요합니다. 어떤 조합이 가장 적절합니까?
빌드 연습 · 송장 추출 파이프라인의 3계층 검증 구현
약 60분1.추출 스키마 정의
공급자명, 발행일, 품목 배열(품명·수량·단가), 총액을 담는 input_schema를 작성합니다. enum과 description을 활용합니다.
기대 결과 · 필수 필드와 판단 기준이 담긴 JSON Schema가 완성됩니다.
2.툴 강제 호출 구현
tool_choice로 추출 툴 호출을 강제하고 response.content에서 input을 꺼내는 함수를 만듭니다.
기대 결과 · 샘플 송장 텍스트에서 파싱된 dict가 반환됩니다.
3.Pydantic 검증 계층
추출 결과를 Pydantic 모델로 검증하고, 실패 시 오류 목록을 반환합니다.
기대 결과 · 타입 오류·필수 필드 누락이 예외가 아닌 구조화된 오류로 잡힙니다.
4.의미 검증 계층
품목별 수량×단가 합계와 총액의 일치, 발행일이 미래가 아닌지 검사하는 규칙을 추가합니다.
기대 결과 · 총액 불일치 송장이 '검토 필요' 상태로 분류됩니다.
5.실패 사례 수집
일부러 손상시킨 송장 텍스트 3종(총액 오류, 날짜 누락, 품목 없음)으로 각 계층이 잡아내는지 확인합니다.
기대 결과 · 각 손상 유형이 서로 다른 검증 계층에서 감지됩니다.