AI를 함수처럼 자유롭기 쓰는 법

JSON과 Structured Output

by kimdonglin

자판기에 동전을 넣으면


자판기에 동전을 넣으면 음료가 나온다. 버튼을 누르기 전에 이미 결과를 안다. 콜라 버튼을 누르면 콜라가 나온다. 오렌지 주스 버튼을 누르면 오렌지 주스가 나온다. 틀릴 일이 없다.


프로그래밍의 함수도 같은 구조다. add(2, 3)을 호출하면 5가 돌아온다. 백 번을 호출해도 5다. 입력이 같으면 출력도 같다. 이걸 결정론적(deterministic)이라고 부른다.


그런데 AI한테 "2 더하기 3은?"이라고 물으면 어떨까. 대부분 "5"라고 답한다. 하지만 가끔은 "2와 3을 더하면 5입니다"라고 답하고, 또 가끔은 "5요!"라고 답한다. 같은 질문인데 답의 형태가 매번 다르다. 이건 결함인가, 아니면 특성인가?


결론부터 말하면, AI를 자판기처럼 안정적으로 쓸 수 있는 방법이 있다. JSON이라는 형식과 Structured Output이라는 기능을 알면 된다.


JSON — 기계가 읽는 언어


"내일 서울 비 올까?"


사람에게는 자연스러운 질문이다. 하지만 기계에게는 모호하다. '내일'이 2월 24일인지 25일인지 모른다. '비'가 강수확률을 묻는 건지, 우산을 챙기라는 건지 불분명하다. 사람은 맥락으로 알아듣지만, 기계는 맥락이 없다.


JSON은 이 모호함을 없앤다.


{
"query": "날씨",
"date": "2026-02-24",
"location": "서울",
"metric": "precipitation_probability"
}



같은 질문이지만 기계가 읽으면 오해할 여지가 없다. 날짜도, 장소도, 원하는 수치의 종류도 명확하다.


JSON은 키-값 쌍으로 이루어진 데이터 형식이다. 중괄호 {}로 객체를 묶고, 대괄호 []로 배열을 만든다. 문자열, 숫자, 불리언, null, 배열, 객체 — 이 여섯 가지면 왠만한 데이터는 다 담을 수 있다. 웹, 앱, API 어디서든 쓰인다. 기계 세계의 공용어다.


그런데 JSON에는 한 가지 더 강력한 도구가 있다. JSON Schema다. JSON이 데이터를 담는 '상자'라면, JSON Schema는 상자의 '설계도'다. 이 상자에는 어떤 칸이 있어야 하고, 각 칸에는 어떤 종류의 값이 들어가야 하는지를 미리 정의한다.


{
"type": "object",
"properties": {
"sentiment": {
"type": "string",
"enum":
["positive", "negative", "neutral"]
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"keywords": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required":
["sentiment", "confidence", "keywords"]
}



이 설계도대로라면 sentiment는 반드시 세 가지 값 중 하나여야 하고, confidence는 0과 1 사이의 숫자여야 한다. 설계도에 어긋나는 데이터는 틀린 데이터다.


그러면 이 설계도를 AI에게 줄 수 있지 않을까?


Structured Output — AI의 출력을 규격화하다


Structured Output은 LLM에게 "이 JSON Schema에 맞춰서 답해"라고 지정하는 기능이다.


기존에는 AI에게 "감정 분석해줘"라고 요청하면 이런 답이 돌아왔다.


이 리뷰는 전반적으로 긍정적입니다. 특히 배송 속도에 대한 만족감이 높으며, 제품 품질에 대한 언급도 긍정적입니다.

사람이 읽기에는 좋다. 하지만 이 답을 코드에서 바로 쓸 수는 없다. 'positive'인지 'negative'인지 추출하려면 또 다른 파싱 로직이 필요하다.


Structured Output을 적용하면 답이 달라진다.

{
"sentiment": "positive",
"confidence": 0.92,
"keywords": ["빠른 배송", "품질 좋음"]
}


깔끔하다. 바로 다음 코드에 연결할 수 있다.


당신이 쓰는 AI API도 이 기능을 지원할까? 대부분 그렇다. OpenAI는 response_format에 JSON Schema를 지정하는 방식으로 구현했다. Anthropic은 2025년 11월에 Structured Outputs를 공개 베타로 출시했다. JSON Schema를 grammar로 컴파일해 추론 시 토큰 생성 자체를 제한하는 방식이다. Google Gemini도 JSON 모드를 지원한다. 구현 방법은 제각각이지만, 노리는 건 같다. AI의 출력을 미리 정의된 형태로 고정하는 것이다.


다만 주의할 점이 있다. Structured Output은 형식의 정합성을 보장하지, 내용의 정확성을 보장하지는 않는다. 스키마에 완벽히 맞는 JSON이지만, 안에 담긴 수치가 사실과 다를 수 있다. 예를 들어 감정 분석 결과가 "sentiment": "positive", "confidence": 0.95로 돌아왔는데, 실제 리뷰는 부정적인 내용일 수 있다. 형식은 완벽하지만 내용이 틀린 것이다. 이걸 '정합한 환각'이라 부르기도 한다.


AI를 함수처럼 쓴다는 것


정합한 환각이라는 한계가 있지만, 출력의 형태를 고정할 수 있다는 것 자체는 큰 전진이다. 여기서 한 걸음 더 나가보자. 형태가 고정되면, AI를 함수처럼 쓸 수 있지 않을까?


전통적 함수는 이렇게 생겼다.


def add(a: int, b: int) -> int:
return a + b
add(2, 3) # 항상 5


입력의 타입이 정해져 있고, 출력의 타입도 정해져 있다. 같은 입력이면 같은 출력이 나온다.


AI 함수는 좀 다르다.


def extract_sentiment(review: str) -> SentimentResult:
# LLM이 추론하여 결과 반환
...

extract_sentiment("배송 빠르고 품질 좋아요")
# {"sentiment": "positive", "confidence": 0.92, "keywords": ["빠른 배송", "품질 좋음"]}


입력의 타입(문자열)은 같다. 출력의 형태(SentimentResult)도 고정이다. 하지만 내부 작동 방식이 다르다. 전통적 함수는 규칙을 계산한다. AI 함수는 가장 확률이 높은 답을 고른다.



이 비교에서 패턴이 보인다. 함수 시그니처 = JSON Schema, 함수 본문 = LLM 추론, 반환값 = Structured Output. 이 세 가지를 연결하면 AI 응답을 코드 파이프라인의 한 부품으로 끼워넣을 수 있다. 파싱 에러도, 예외 처리도 대폭 줄어든다.


정규분포 — AI 출력의 성격을 이해하는 열쇠


"형태는 고정, 내용은 확률적"이라는 말을 좀 더 정확히 이해해보자. 당신이 같은 프롬프트를 100번 던진다면 어떤 일이 벌어질까?


감정 분석 결과의 confidence 값이 매번 조금씩 다르게 나올 것이다. 0.91, 0.93, 0.92, 0.90, 0.92... 여기서 비유를 하나 꺼내자. 이 값들을 그래프로 그리면 종 모양에 가까운 곡선이 된다. 가운데가 가장 높고 양쪽으로 갈수록 낮아진다. 정규분포와 비슷한 형태다.


전통적 함수는 이 그래프에서 하나의 점이다. 항상 같은 값. AI 함수는 이 종 모양 곡선의 꼭대기에서 답을 고른다. 대부분 꼭대기 근처에서 나오지만, 가끔 약간 벗어난 곳에서 나오기도 한다.


여기서 temperature라는 파라미터가 등장한다. 이걸 종의 폭을 조절하는 다이얼이라고 생각하면 된다. temperature를 낮추면 종이 뾰족해진다. 출력이 좁은 범위에 집중된다. 일관성이 높아진다. temperature를 높이면 종이 넓어진다. 다양한 답이 나올 수 있다. 창의성은 올라가지만 예측 가능성은 떨어진다.


OpenAI API의 temperature는 0.0에서 2.0 사이이고 기본값은 1.0이다. Anthropic은 0.0에서 1.0 사이다. temperature를 0으로 설정하면 거의 결정론적인 출력을 얻을 수 있다. 하지만 '거의'라는 단서가 붙는다. GPU 연산의 부동소수점 비결합성, 배치 처리 순서, 인프라 구성 차이 등 여러 요인 때문에 완전히 동일한 결과를 보장하지는 못한다.


실제 메커니즘은 더 복잡하지만, 핵심 직관은 유효하다. LLM은 연속적인 하나의 값을 뽑는 게 아니라 토큰 단위로 조건부 확률을 순차 샘플링한다. 정규분포는 직관을 위한 비유이지, 정확한 설명은 아니다. 하지만 핵심은 전달된다. AI의 불확실성은 버그가 아니라 특성이다. 그리고 Structured Output + 낮은 temperature를 조합하면, 이 특성을 통제 가능한 범위 안에 가둘 수 있다.


그래서 어떻게 쓰는가


다시 자판기로 돌아가자. 실전 코드는 의외로 간결하다.

from openai import OpenAI
client = OpenAI()
result = client.responses.create(
model="gpt-4o",
input="배송 빠르고 품질 좋아요",
text={"format": {"type": "json_schema", "name": "sentiment",
"schema": sentiment_schema}} # sec02에서 정의한 Schema
)


Schema를 정의하고, AI에게 건네고, 구조화된 답을 받는다. 이 짧은 코드 안에 지금까지 설명한 모든 개념이 들어 있다. Schema 정의, 형태 고정, 함수처럼 호출.


정리하면 이렇다. JSON을 알면 AI의 출력 형태를 정의할 수 있다. 형태를 정의할 수 있으면 AI를 함수처럼 쓸 수 있다. 함수처럼 쓸 수 있으면 코드의 부품이 된다. 'AI는 채팅 상대'라는 인식에서 'AI는 호출 가능한 함수'로 관점이 전환되는 순간이다.


처음에 자판기는 정해진 메뉴판대로 음료를 내주는 기계였다. 이제 당신이 그 메뉴판을 직접 설계할 수 있다.


JSON Schema가 메뉴판이고, AI가 자판기다. 다만 이 자판기는 매번 정확히 같은 음료를 내놓지는 않는다. 거의 같은 음료를 내놓는다. 그 '거의'를 이해하고 받아들이는 것이 AI를 제대로 쓰는 첫걸음이다.


매거진의 이전글트렌드를 쫓는 피로