
OpenAI API로 함수 호출(function calling) 기능을 쓰다 보면 어느 순간 strict: true라는 옵션을 마주치게 됩니다. 이게 Structured Outputs라는 기능인데, 기존 function calling과 뭐가 다른지 헷갈리는 경우가 많습니다. 이 글에서는 OpenAI Structured Outputs strict 모드가 일반 function calling과 구조적으로 어떻게 다른지, 그리고 스키마를 작성할 때 왜 “Invalid schema” 같은 에러가 발생하는지를 실제 코드와 함께 정리합니다. 읽고 나면 어떤 스키마 작성 규칙 때문에 에러가 나는지, 어떻게 고쳐야 하는지 바로 적용할 수 있습니다.
function calling과 strict 모드는 층위가 다른 개념
먼저 짚어야 할 점은 function calling과 strict 모드가 서로 대체 관계가 아니라는 것입니다. function calling은 모델이 사용자 요청을 보고 등록된 함수 중 무엇을 호출할지, 어떤 인자를 넘길지 “생성”하는 기능 자체를 말합니다. 반면 strict 모드는 그 함수 호출 기능 안에서 인자 JSON이 개발자가 정의한 스키마를 반드시 따르도록 강제하는 옵션입니다.
즉 tools 배열의 각 함수 정의에 "strict": true를 추가하지 않으면, 모델은 스키마를 “참고”만 할 뿐 문자 그대로 지키지는 않습니다. 필수 필드를 빼먹거나, enum에 없는 값을 넣거나, 숫자를 문자열로 반환하는 경우가 종종 생깁니다. strict를 켜면 모델의 출력 자체가 스키마에 맞게 제약된 디코딩(constrained decoding) 방식으로 생성되기 때문에, 결과 JSON이 스키마를 어기는 상황이 원천적으로 차단됩니다.
Structured Outputs는 function calling의 strict 옵션뿐 아니라, 응답 자체를 JSON으로 강제하는 response_format: {"type": "json_schema", "json_schema": {...}, "strict": true} 형태로도 쓸 수 있습니다. 함수를 호출하지 않고 바로 정형화된 답변만 받고 싶을 때 이 방식을 사용합니다.
strict 모드가 스키마를 검사하는 시점
strict 모드의 핵심은 “출력이 스키마를 따를 확률이 높다”가 아니라 “따르도록 구조적으로 제약한다”는 데 있습니다. 이를 위해 API는 요청이 들어온 스키마를 모델이 토큰을 생성할 때 참조할 수 있는 형태로 미리 변환하는 과정을 거칩니다. 이 과정에서 스키마 문법 자체를 검증하기 때문에, 실제 모델 추론이 시작되기도 전에 스키마 오류가 잡힙니다.
이 특성 때문에 일반 function calling에서는 문제없이 동작하던 스키마가 strict를 켜는 순간 400 에러로 거절당하는 일이 흔합니다. 일반 모드에서는 스키마가 그저 “힌트”라서 문법이 다소 느슨해도 무시되고 넘어가지만, strict 모드에서는 스키마 자체가 디코딩 제약 조건으로 컴파일되어야 하므로 지원되지 않는 문법이 있으면 요청 단계에서 바로 실패합니다. 처음 이 기능을 쓰는 개발자가 “어제까지 되던 코드가 안 된다”고 느끼는 이유가 대부분 여기에 있습니다.
스키마 에러가 나는 대표적인 원인 5가지
Structured Outputs strict 모드는 JSON Schema 명세 전체가 아니라 그 하위 집합만 지원합니다. 전체 명세는 IETF의 JSON Schema 명세 초안 문서에서 확인할 수 있는데, OpenAI는 이 중 일부 키워드만 허용합니다. 실무에서 가장 자주 마주치는 에러 원인은 다음과 같습니다.

첫째, additionalProperties: false를 모든 object 타입 스키마에 명시하지 않은 경우입니다. strict 모드는 정의되지 않은 필드가 끼어드는 것을 허용하지 않으므로, 중첩된 object마다 이 속성을 빠짐없이 넣어야 합니다.
둘째, required 배열에 모든 속성 이름을 넣지 않은 경우입니다. 일반 JSON Schema에서는 required가 선택 사항이지만, strict 모드에서는 properties에 정의한 모든 키가 required에도 들어가야 합니다. 값이 선택적이어야 한다면 required에서 빼는 대신, 타입을 ["string", "null"]처럼 null을 포함한 배열로 지정해 “값이 없을 수도 있음”을 표현해야 합니다.
셋째, 지원하지 않는 키워드를 사용한 경우입니다. 예를 들어 문자열 길이 제한이나 숫자 범위 제한처럼 세부 검증용 키워드는 버전에 따라 지원 범위가 다르므로, 스키마를 최대한 단순하게 유지하는 편이 에러를 줄이는 방법입니다.
넷째, 스키마 중첩 깊이나 전체 속성 개수가 지나치게 많은 경우입니다. object를 여러 겹으로 중첩하거나 속성 수가 많은 대형 스키마는 컴파일 단계에서 제한에 걸릴 수 있습니다.
다섯째, enum 값 목록이 지나치게 크거나 재귀 참조($ref)를 잘못된 방식으로 구성한 경우입니다. 재귀 구조 자체는 지원되지만, 참조 경로가 스키마 루트에서 명확하게 해석 가능해야 합니다.
코드로 보는 정상 스키마와 에러 스키마
아래는 Python openai 패키지를 이용해 strict 모드를 켠 function calling 예시입니다. 도시 이름과 온도 단위를 인자로 받는 get_weather 함수를 정의했습니다.
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "주어진 도시의 현재 날씨를 반환합니다.",
"strict": True,
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"]
}
},
"required": ["city", "unit"],
"additionalProperties": False
}
}
}
]
response = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=[{"role": "user", "content": "서울 날씨 알려줘"}],
tools=tools
)
print(response.choices[0].message.tool_calls[0].function.arguments)
# 출력 예: {"city":"서울","unit":"celsius"}
여기서 unit을 선택적 필드로 만들고 싶다고 required에서 unit을 빼버리면 어떻게 될까요. strict 모드에서는 이 요청이 아예 거부됩니다.
# 잘못된 예 - strict 모드에서 에러 발생
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
},
"required": ["city"], # unit이 빠져 있음
"additionalProperties": False
}

이 경우 API는 요청 자체를 400 오류로 반려하며, “properties에 정의된 모든 키는 required에 포함되어야 한다”는 취지의 메시지를 반환합니다. unit을 정말 선택적으로 두려면 아래처럼 타입에 null을 추가하고 required에는 그대로 포함시켜야 합니다.
"unit": {
"type": ["string", "null"],
"enum": ["celsius", "fahrenheit", None]
}
일반 function calling vs strict 모드 비교
두 방식의 차이를 표로 정리하면 다음과 같습니다.
| 구분 | 일반 function calling (strict 미지정) | Structured Outputs strict: true |
|---|---|---|
| 인자 JSON 스키마 준수 | best-effort, 어긋날 수 있음 | 디코딩 단계에서 강제 준수 |
| 선택적 필드 표현 | required에서 제외 | required 유지 + 타입에 null 포함 |
| additionalProperties | 없어도 동작 | 모든 object에 false 명시 필요 |
| 지원 JSON Schema 범위 | 제한 사실상 없음 | 하위 집합만 지원 |
| 에러 발생 시점 | 응답 파싱 시 뒤늦게 발견 | 요청 시 스키마 검증 단계에서 즉시 발견 |
| 적합한 상황 | 스키마가 단순하고 유연성이 필요할 때 | 후속 로직이 파싱 실패에 취약할 때 |
정리하면, 스키마가 복잡하지 않고 다소 형식이 어긋나도 애플리케이션 쪽에서 방어 로직으로 처리할 수 있다면 strict 없이 써도 무방합니다. 반대로 받은 JSON을 바로 DB에 넣거나 다른 시스템에 전달해야 해서 파싱 실패를 절대 허용할 수 없다면 strict 모드가 적합합니다.
마무리
OpenAI Structured Outputs strict 모드는 function calling의 상위 옵션으로, 모델이 만드는 인자 JSON을 스키마에 정확히 맞춰 생성하도록 디코딩 단계에서 강제하는 기능입니다. 에러가 나는 대부분의 원인은 additionalProperties: false 누락, required에 모든 속성을 넣지 않은 경우, 지원되지 않는 JSON Schema 키워드 사용, 과도한 중첩·속성 개수, enum 구성 오류로 요약됩니다. 지금 쓰고 있는 함수 정의에 strict: true를 붙이기 전에, 위 다섯 가지 항목부터 하나씩 점검해 보는 것을 추천합니다. 스키마를 최대한 평평하고 단순하게 유지하는 것만으로도 에러 대부분을 피할 수 있습니다.
자주 묻는 질문(FAQ)
Q1. strict 모드를 켜면 응답 속도가 느려지나요?
스키마를 처음 사용할 때 내부적으로 제약 조건으로 컴파일하는 과정이 있어 첫 호출에서 약간의 지연이 발생할 수 있습니다. 이후 같은 스키마를 재사용할 때는 이 오버헤드가 줄어드는 것으로 알려져 있습니다.
Q2. response_format의 json_schema와 tools의 strict는 같은 기능인가요?
둘 다 Structured Outputs 기술을 사용하지만 용도가 다릅니다. response_format은 함수 호출 없이 모델의 최종 답변 자체를 정해진 JSON 형태로 강제할 때 쓰고, tools의 strict는 함수 호출 인자를 스키마에 맞춰 강제할 때 씁니다.
Q3. 기존에 쓰던 느슨한 스키마를 strict용으로 바꾸려면 뭐부터 손봐야 하나요?
모든 object에 additionalProperties: false를 추가하고, properties에 있는 키를 전부 required에 넣은 뒤, 선택적이어야 하는 필드만 타입에 null을 추가하는 순서로 수정하면 대부분의 에러가 해결됩니다.