DEV Community

Cover image for OpenAI 구조화된 출력 활용 방법
Rihpig
Rihpig

Posted on • Originally published at apidog.com

OpenAI 구조화된 출력 활용 방법

이 가이드를 따라 하면 OpenAI 구조화된 출력을 코드에서 호출하고, JSON Schema와 strict: true로 응답 형식을 고정하며, Apidog에서 요청/검증/목업까지 테스트 컬렉션으로 관리할 수 있습니다.

오늘 Apidog를 사용해 보세요

시작하기 전에 필요한 것

구조화된 출력은 모델 응답이 제공한 JSON Schema를 따르도록 생성 결과를 제한합니다. strict: true와 함께 스키마를 전달하면 필수 키, 타입, enum 값이 스키마와 일치하는 응답을 받을 수 있습니다.

준비물은 다음과 같습니다.

  • OPENAI_API_KEY로 설정된 OpenAI API 키
  • 엄격한 스키마 강제를 지원하는 모델
  • 원하는 응답 형식을 설명하는 JSON Schema
  • 응답 계약을 검증할 API 테스트 환경

올바른 모델 선택

구조화된 출력은 GPT-4o 제품군부터 GPT-5 시리즈까지 OpenAI의 최신 모델에서 사용할 수 있습니다. OpenAI 문서는 현재 최신 플래그십 모델(gpt-5.5, 이 글 작성 시점 기준)에서 새 프로젝트를 시작하는 것을 권장합니다.

이전 모델과 gpt-3.5 시대 모델은 JSON 모드를 지원하지만, 엄격한 스키마 강제는 지원하지 않습니다. strict: true에 의존한다면 배포 전에 사용하려는 정확한 모델 ID가 구조화된 출력을 지원하는지 확인하십시오.

OpenAI에는 혼동하기 쉬운 두 기능이 있습니다.

JSON 모드

{
  "response_format": {
    "type": "json_object"
  }
}
Enter fullscreen mode Exit fullscreen mode

JSON 모드는 출력이 구문적으로 유효한 JSON임을 보장합니다. 하지만 필드, 타입, 필수 키, enum 값은 보장하지 않습니다.

구조화된 출력

{
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "strict": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

구조화된 출력은 유효한 JSON을 생성하고, 동시에 스키마 준수까지 강제합니다. 새 작업에는 일반적으로 구조화된 출력을 사용하는 것이 더 안전합니다.

JSON 모드 구조화된 출력 (엄격 모드)
매개변수 response_format: {"type":"json_object"} response_formattype: "json_schema", strict: true
유효한 JSON
스키마 일치 여부 아니요
필수 필드 강제 여부 아니요
타입 및 열거형 강제 여부 아니요
하위에서 여전히 검증 필요 항상 권장됨

API 참고 사항:

  • Chat Completions API는 response_format을 사용합니다.
  • Responses API는 text.format 아래에 type: "json_schema"를 사용합니다.
  • 스키마 규칙은 동일하지만 필드 경로가 다르므로, 사용하는 엔드포인트의 최신 문서를 확인하십시오.

첫 번째 요청 보내기

예제로 지원 티켓을 구조화된 레코드로 추출해 보겠습니다.

다음 요청은 사용자의 자연어 문의를 support_ticket 스키마에 맞는 JSON으로 반환하도록 강제합니다.

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-5.5",
    "messages": [
      {
        "role": "system",
        "content": "Extract the ticket into the schema."
      },
      {
        "role": "user",
        "content": "My checkout 500s every time I use a saved card. Started today. Account: acct_8842."
      }
    ],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "support_ticket",
        "strict": true,
        "schema": {
          "type": "object",
          "properties": {
            "summary": {
              "type": "string"
            },
            "category": {
              "type": "string",
              "enum": ["billing", "bug", "account", "other"]
            },
            "severity": {
              "type": "integer"
            },
            "account_id": {
              "anyOf": [
                { "type": "string" },
                { "type": "null" }
              ]
            }
          },
          "required": ["summary", "category", "severity", "account_id"],
          "additionalProperties": false
        }
      }
    }
  }'
Enter fullscreen mode Exit fullscreen mode

핵심 설정은 다음 세 가지입니다.

{
  "type": "json_schema",
  "strict": true,
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode
  • type: "json_schema": 구조화된 출력 사용
  • strict: true: 스키마 준수 강제
  • additionalProperties: false: 정의되지 않은 키 생성 차단

응답 읽기

모델은 스키마와 일치하는 JSON 문자열을 content로 반환합니다.

예상 응답 예시는 다음과 같습니다.

{
  "summary": "Checkout returns HTTP 500 when paying with a saved card",
  "category": "bug",
  "severity": 3,
  "account_id": "acct_8842"
}
Enter fullscreen mode Exit fullscreen mode

account_id는 다음처럼 string 또는 null을 허용합니다.

"account_id": {
  "anyOf": [
    { "type": "string" },
    { "type": "null" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

구조화된 출력에서는 일반적인 의미의 선택적 필드가 없습니다. 키는 항상 존재해야 하며, 값이 없을 수 있는 필드는 null 허용으로 모델링합니다.

스키마를 지원되는 하위 집합 안에 유지하기

구조화된 출력은 JSON Schema 전체가 아니라 하위 집합을 사용합니다. 스키마를 작성할 때 다음 규칙을 지키십시오.

1. 루트는 객체여야 합니다

최상위 레벨은 배열이나 문자열이 될 수 없습니다.

잘못된 예:

{
  "type": "array",
  "items": {
    "type": "string"
  }
}
Enter fullscreen mode Exit fullscreen mode

권장 방식:

{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["items"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

2. 모든 속성은 required에 포함되어야 합니다

구조화된 출력에서는 모든 속성이 required에 있어야 합니다.

선택적 값을 표현하려면 null을 허용하십시오.

{
  "type": "object",
  "properties": {
    "phone": {
      "anyOf": [
        { "type": "string" },
        { "type": "null" }
      ]
    }
  },
  "required": ["phone"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

3. 모든 객체에 additionalProperties: false를 설정하십시오

모델이 스키마에 없는 키를 추가하지 못하도록 모든 객체에 설정합니다.

{
  "type": "object",
  "properties": {
    "user": {
      "type": "object",
      "properties": {
        "id": { "type": "string" }
      },
      "required": ["id"],
      "additionalProperties": false
    }
  },
  "required": ["user"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

4. 스키마 크기를 제한하십시오

스키마에는 크기 제한이 있습니다.

  • 약 100개의 객체 속성
  • 최대 5단계 중첩

깊고 넓은 스키마는 거부될 수 있으므로, 가능한 한 평평하게 설계하십시오.

5. 일부 검증 키워드는 보장되지 않습니다

다음과 같은 유효성 검사 전용 키워드는 모델이 보장하지 않을 수 있습니다.

  • pattern
  • format
  • minLength
  • minimum

예를 들어 이메일 형식, 정규식, 숫자 범위 같은 비즈니스 규칙은 응답을 받은 뒤 애플리케이션 코드나 테스트에서 다시 검증해야 합니다.

oneOf/anyOf/allOf로 선택적 또는 유니온 필드를 모델링해 본 적이 있다면 같은 방식으로 접근하면 됩니다. 스키마는 구조를 제한하고, 실제 값의 의미는 별도로 검증합니다.

6. 첫 호출은 느릴 수 있습니다

새 스키마로 첫 요청을 보내면 스키마 컴파일 시간이 필요합니다. 일반적으로 몇 초가 걸리며, 복잡한 스키마는 최대 1분까지 걸릴 수 있습니다. 이후에는 캐시되어 더 빨라집니다.

거부 및 잘림 처리

구조화된 출력에서도 항상 content에 스키마 형태의 JSON이 들어오는 것은 아닙니다.

모델이 안전하지 않은 요청을 거부하면 메시지에 refusal 필드가 포함될 수 있습니다. JSON을 파싱하기 전에 먼저 분기하십시오.

import json

msg = response.choices[0].message

if msg.refusal:
    handle_refusal(msg.refusal)
else:
    ticket = json.loads(msg.content)
Enter fullscreen mode Exit fullscreen mode

이 방식은 응답 텍스트에서 사과 문구를 검색하는 것보다 안정적입니다.

또한 다음 상황에서는 응답이 스키마를 만족하지 못할 수 있습니다.

  • max_tokens에 도달해 JSON이 중간에서 잘림
  • 구조화된 출력이 지원하지 않는 병렬 도구 호출 사용

도구 호출과 함께 사용할 때는 다음처럼 병렬 호출을 비활성화하십시오.

{
  "parallel_tool_calls": false
}
Enter fullscreen mode Exit fullscreen mode

Apidog에서 테스트하는 방법

엄격 모드는 생성 시점에 스키마를 강제합니다. 하지만 테스트를 생략해도 된다는 뜻은 아닙니다.

다음 변경은 언제든 발생할 수 있습니다.

  • 모델 변경
  • 프롬프트 변경
  • 스키마 변경
  • required 배열 수정
  • 거부 처리 경로 변경
  • 하위 서비스의 계약 변경

이런 변경을 CI에서 감지하려면 API 응답이 계약과 일치하는지 테스트해야 합니다. 이때 Apidog를 사용할 수 있습니다.

역할을 명확히 나누면 다음과 같습니다.

  • OpenAI 구조화된 출력: 스키마에 맞는 JSON 생성
  • Apidog: 받은 응답이 예상 스키마와 일치하는지 검증

Apidog가 모델에 스키마를 강제하는 것은 아닙니다. Apidog는 응답을 검증하고, 계약 위반을 프로덕션이 아니라 테스트 단계에서 발견하도록 도와줍니다.

Apidog 테스트 워크플로

1. Chat Completions 요청 만들기

Apidog에서 OpenAI Chat Completions 요청을 생성합니다.

요청 본문에는 앞에서 사용한 response_format 블록을 그대로 넣습니다.

{
  "model": "gpt-5.5",
  "messages": [
    {
      "role": "system",
      "content": "Extract the ticket into the schema."
    },
    {
      "role": "user",
      "content": "My checkout 500s every time I use a saved card. Started today. Account: acct_8842."
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "support_ticket",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "summary": { "type": "string" },
          "category": {
            "type": "string",
            "enum": ["billing", "bug", "account", "other"]
          },
          "severity": { "type": "integer" },
          "account_id": {
            "anyOf": [
              { "type": "string" },
              { "type": "null" }
            ]
          }
        },
        "required": ["summary", "category", "severity", "account_id"],
        "additionalProperties": false
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

반복 실행할 수 있도록 컬렉션에 저장하십시오.

2. 응답 스키마 검증 추가하기

Apidog에서 응답 검증을 추가합니다.

검증 대상은 다음과 같습니다.

  • summary가 문자열인지
  • categorybilling, bug, account, other 중 하나인지
  • severity가 정수인지
  • account_id가 문자열 또는 null인지
  • 스키마에 없는 추가 필드가 없는지

Apidog는 응답을 JSON Schema에 대해 검증할 수 있으므로, OpenAI에 전달한 스키마와 동일한 스키마를 테스트에도 사용하십시오.

3. CI에서 컬렉션 실행하기

모델, 프롬프트, 스키마가 변경될 때마다 컬렉션을 실행하도록 파이프라인에 추가합니다.

목표는 단순합니다.

  • 스키마가 깨지면 테스트 실패
  • 응답 계약이 바뀌면 빌드 실패
  • 하위 서비스가 잘못된 페이로드를 받기 전에 감지

4. 목업 API로 하위 소비자 테스트하기

실제 OpenAI 호출을 붙이기 전에도 하위 소비자를 개발할 수 있습니다.

Apidog에서 목업 API를 설정하고, 동일한 스키마를 만족하는 샘플 응답을 반환하도록 구성하십시오.

이 방식은 다음 상황에서 유용합니다.

  • 토큰을 사용하지 않고 프론트엔드 개발
  • OpenAI 호출이 준비되기 전 통합 테스트
  • CI에서 안정적인 테스트 데이터 사용
  • 하위 서비스의 계약 검증

동일한 스키마를 따르는 목업을 먼저 사용하고, 준비가 되면 실제 OpenAI 호출로 교체하면 됩니다. Apidog를 다운로드하면 요청, 어설션, 목업을 한 곳에서 구성할 수 있습니다.

자주 묻는 질문

구조화된 출력이 있으면 JSON 모드는 사용 중단되었나요?

아니요. JSON 모드는 여전히 작동하며 유효한 JSON을 보장합니다.

다만 스키마를 강제하지 않습니다. 새 코드에서는 strict: true를 사용하는 구조화된 출력이 더 강력한 선택입니다.

JSON 모드는 다음 경우에만 고려하십시오.

  • 사용하는 모델이 엄격 모드를 지원하지 않음
  • 고정된 응답 형태가 필요 없음
  • 직접 후처리 검증을 수행할 계획이 있음

스키마의 루트가 배열일 수 있나요?

아니요. 최상위 레벨은 객체여야 합니다.

배열이 필요하면 객체 속성으로 감싸십시오.

{
  "type": "object",
  "properties": {
    "items": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  },
  "required": ["items"],
  "additionalProperties": false
}
Enter fullscreen mode Exit fullscreen mode

필드를 선택 사항으로 만드는 방법은 무엇입니까?

구조화된 출력에서는 모든 속성이 required에 있어야 합니다.

따라서 선택 사항은 “키가 없음”이 아니라 “값이 null일 수 있음”으로 모델링합니다.

{
  "anyOf": [
    { "type": "string" },
    { "type": "null" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

키는 항상 존재하고, 값만 null이 될 수 있습니다.

엄격 모드에서는 유효성 검사를 완전히 건너뛰어도 되나요?

형태 검사는 대부분 줄일 수 있습니다. 하지만 비즈니스 유효성 검사는 여전히 필요합니다.

특히 다음은 별도로 확인하십시오.

  • 정규식 패턴
  • 이메일/URL 같은 포맷
  • 숫자 범위
  • 문자열 길이
  • 도메인별 허용 값
  • 거부 응답
  • 잘린 응답

JSON Schema가 익숙하지 않다면 JSON Schema 입문서를 먼저 확인하고, 배포마다 검증을 실행하십시오.

어떤 모델을 사용해야 합니까?

구조화된 출력은 GPT-4o 및 이후 모델, GPT-5 시리즈에서 작동합니다. OpenAI 문서는 현재 플래그십 모델에 새 프로젝트를 집중하도록 안내합니다.

엄격 모드 지원은 모델 버전별로 다를 수 있으므로, 사용하려는 정확한 모델 ID가 strict: true를 지원하는지 확인한 뒤 의존하십시오.

마무리

구현 흐름은 다음과 같습니다.

  1. 엄격 모드를 지원하는 모델을 선택합니다.
  2. response_format.typejson_schema로 설정합니다.
  3. strict: true를 활성화합니다.
  4. 루트 객체, required, additionalProperties: false 규칙을 지킵니다.
  5. 선택적 필드는 null 허용으로 모델링합니다.
  6. refusal과 잘림 응답을 분기 처리합니다.
  7. 값 수준 비즈니스 규칙은 별도로 검증합니다.
  8. Apidog에서 요청, 응답 검증, 목업을 구성하고 CI에서 실행합니다.

구조화된 출력은 모델 응답의 형태를 고정합니다. 테스트는 그 형태가 계속 유지되는지 증명합니다.

Top comments (0)