OpenAI Structured Outputs로 Unity C# 구조체를 자동 매핑하는 알고리즘

OpenAI Structured Outputs의 JSON Schema와 C# 구조체를 1:1로 맞춰 Unity 게임 데이터에 자동 매핑하는 방법을 다룹니다. 스키마 설계, JsonSerializer 역직렬화, 검증과 실패 처리까지 실전 코드로 정리합니다.

✓ 복사 완료!
챕터머티리얼 & 텍스처
난이도중급
상태● 검토 완료
대상 엔진
Unity
최종 업데이트
OpenAI Structured Outputs로 Unity C# 구조체를 자동 매핑하는 알고리즘

핵심 요약

OpenAI Structured Outputs는 모델 출력 형식을 JSON Schema로 제한하므로 그 스키마를 C# 구조체의 필드명·타입·필수 여부와 맞추면 JsonSerializer.Deserialize<T>() 한 번으로 게임 데이터를 매핑할 수 있다. 단, 스키마 적합성은 네트워크·거부·콘텐츠 의미까지 보장하지 않으므로 클라이언트에서는 역직렬화 후 범위 검증을 한 번 더 수행해야 한다.

OpenAI Structured Outputs와 C# 자동 매핑이 필요한 이유

게임에서 AI가 생성한 퀘스트, NPC 대사 선택지, 전리품 후보를 UI에 표시하려면 문자열을 직접 해석하는 방식은 쉽게 깨진다. 예를 들어 보상: 골드 100 같은 자유 텍스트는 숫자와 아이템 식별자를 안정적으로 꺼내기 어렵다.

Structured Outputs는 응답 형식을 json_schema로 지정하고 strict: true를 설정해 제공한 JSON Schema에 맞는 출력을 요청하는 기능이다. 기존 JSON mode는 유효한 JSON을 만드는 데 초점이 있지만 Structured Outputs는 스키마 구조까지 맞추는 용도다. Structured Outputs 가이드는 json_schema 형식과 strict 옵션을 설명하며, 지원 모델에서는 JSON mode보다 Structured Outputs 사용을 권장한다.

항목자유 텍스트JSON modeStructured Outputs
JSON 문법 보장없음있음있음
필드명·타입 구조 제약없음프롬프트 의존JSON Schema 기반
C# 구조체 역직렬화파서 필요런타임 실패 가능단순화 가능
게임 데이터 적용비추천제한적권장

JSON Schema에서 C# 구조체로 이어지는 자동 매핑 과정

C# 구조체 자동 매핑 알고리즘은 어떻게 구현할까?

핵심 알고리즘은 복잡하지 않다. 스키마와 DTO를 같은 계약으로 만들고 JSON 역직렬화 뒤에 도메인 검증을 적용한다. 게임플레이 데이터에는 클래스보다 값 복사 의도가 분명한 struct를 쓸 수 있지만 큰 배열이나 중첩 데이터까지 모두 구조체로 만들 필요는 없다.

Step 1. C# 데이터 계약을 먼저 고정한다

필드명은 API JSON의 snake_case 또는 camelCase와 정확히 맞추거나 [JsonPropertyName]으로 명시한다. 다음 예시는 AI가 생성할 퀘스트 카드 데이터다.

using System;
using System.Text.Json.Serialization;

public struct QuestCard
{
    [JsonPropertyName("title")]
    public string Title { get; set; }

    [JsonPropertyName("objective")]
    public string Objective { get; set; }

    [JsonPropertyName("reward_gold")]
    public int RewardGold { get; set; }

    [JsonPropertyName("difficulty")]
    public QuestDifficulty Difficulty { get; set; }
}

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum QuestDifficulty
{
    Easy,
    Normal,
    Hard
}

JsonStringEnumConverter를 붙이면 JSON 문자열 "Easy"를 열거형으로 변환한다. 프롬프트에서 열거형 값의 대소문자와 표기를 고정해야 한다.

Step 2. 같은 의미의 JSON Schema를 만든다

required는 C#에서 기본값이 존재한다는 사실과 별개다. RewardGold가 int여도 JSON에 필드가 누락되면 0으로 조용히 매핑될 수 있으므로 API 스키마에서는 필수 필드를 명시해야 한다.

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "title": { "type": "string" },
    "objective": { "type": "string" },
    "reward_gold": { "type": "integer", "minimum": 0, "maximum": 10000 },
    "difficulty": {
      "type": "string",
      "enum": ["Easy", "Normal", "Hard"]
    }
  },
  "required": ["title", "objective", "reward_gold", "difficulty"]
}

Responses API에서는 이 스키마를 text.format의 json_schema에 전달하고 strict를 활성화한다. 모델·엔드포인트별 지원 여부는 배포 전에 모델 기능 페이지에서 확인한다.

{
  "model": "gpt-5",
  "input": "마을 주민용 퀘스트 카드 하나를 생성해 줘.",
  "text": {
    "format": {
      "type": "json_schema",
      "name": "quest_card",
      "strict": true,
      "schema": { "...": "위 JSON Schema 객체" }
    }
  }
}

Step 3. 응답 문자열을 구조체로 역직렬화하고 검증한다

응답에서 추출한 JSON 텍스트만 System.Text.Json에 넘긴다. 이 코드는 알 수 없는 필드를 거부하고 생성된 값이 실제 게임 규칙에 맞는지 검사한다.

using System;
using System.Text.Json;

public static class QuestCardMapper
{
    private static readonly JsonSerializerOptions Options = new()
    {
        PropertyNameCaseInsensitive = false
    };

    public static bool TryMap(string json, out QuestCard card)
    {
        card = default;

        try
        {
            card = JsonSerializer.Deserialize<QuestCard>(json, Options);
            return !string.IsNullOrWhiteSpace(card.Title)
                && !string.IsNullOrWhiteSpace(card.Objective)
                && card.RewardGold is >= 0 and <= 10000
                && Enum.IsDefined(typeof(QuestDifficulty), card.Difficulty);
        }
        catch (JsonException)
        {
            return false;
        }
    }
}

자동 매핑의 흐름은 다음과 같다.

flowchart LR
    A[사용자 입력] --> B[서버: Responses API]
    B --> C[JSON Schema에 맞는 응답]
    C --> D[Unity: JsonSerializer.Deserialize]
    D --> E{도메인 검증}
    E -->|통과| F[QuestCard UI 갱신]
    E -->|실패| G[기본 카드 또는 재시도]

구조체와 JSON Schema를 1:1로 맞추는 규칙

C# 타입JSON Schema 타입주의점
stringstring빈 문자열 여부는 별도 검증
intintegerminimum, maximum으로 게임 범위 제한
float, doublenumberNaN, Infinity는 JSON 값이 아님
boolboolean문자열 "true"를 허용하지 않음
enumstring + enumC# 이름과 JSON 값을 통일
T[]arrayitems와 maxItems를 설정
중첩 구조체object하위 properties와 required를 재귀적으로 작성

자동 생성기를 만들고 싶어질 수 있지만 퀘스트·대사·UI 카드처럼 스키마 수가 적은 게임 기능에서는 DTO와 Schema를 함께 작성하는 편이 더 작고 검토하기 쉽다. 타입 수가 많아져 중복이 실제 비용이 될 때만 Roslyn Source Generator 또는 서버 측 스키마 생성 도구를 도입한다.

Unity 클라이언트에서 API 키를 직접 호출하면 안 되는 이유

OpenAI API 키를 Unity 빌드에 넣으면 APK, IPA, 메모리 또는 네트워크 프록시에서 탈취될 수 있다. 구조화된 AI 응답 요청은 서버가 OpenAI API를 호출하고 Unity는 게임 서버가 검증·정규화한 결과만 받는 구조가 안전하다.

클라이언트는 다음만 담당하면 충분하다.

  1. 서버가 반환한 JSON을 QuestCardMapper.TryMap으로 변환한다.
  2. 제목 길이, 보상 상한, 허용된 enum처럼 UI·게임플레이 규칙을 검증한다.
  3. 실패하면 빈 UI 대신 미리 준비한 기본 카드 또는 일반 오류 상태를 표시한다.

strict: true여도 모델 응답이 거부되거나 API 요청 자체가 실패할 수 있다. 따라서 성공 응답만 곧바로 역직렬화하지 말고 서버에서 응답 상태와 거부 상태를 먼저 분기해야 한다.

Unity 퀘스트 카드에 검증된 구조화 AI 데이터를 표시하는 화면

성능과 안정성은 어디까지 최적화해야 할까?

퀘스트 카드 한 건처럼 작은 JSON의 병목은 대개 역직렬화가 아니라 네트워크 왕복과 모델 생성 시간이다. 매 프레임 호출하거나 매번 리플렉션 기반 변환기를 만드는 방식만 피하면 된다.

  • JsonSerializerOptions는 위 예시처럼 재사용한다.
  • UI 표시용 문자열은 길이 제한을 둔다. 예: 제목 40자, 목표 120자.
  • 같은 입력에 대해 결과 재현이 필요하면 서버에서 입력과 생성 결과를 캐시한다.
  • AI가 제안한 ID를 인벤토리 데이터의 진짜 ID로 신뢰하지 않는다. 허용 목록에서 다시 조회한다.

자주 묻는 질문 (FAQ)

Structured Outputs면 C# 검증을 생략해도 될까?

아니다. Structured Outputs는 출력 구조 계약을 강화하지만 보상 상한이나 존재하는 아이템 ID처럼 게임 도메인 규칙은 클라이언트와 서버에서 검증해야 한다.

Unity의 JsonUtility 대신 System.Text.Json을 써야 할까?

중첩 구조, enum 문자열, 알 수 없는 필드 거부가 필요하면 System.Text.Json이 적합하다. 단순한 Unity 직렬화 객체만 다룬다면 기존 JsonUtility를 유지해도 된다.

struct 대신 class를 써도 될까?

된다. 값 복사와 작은 불변성에 의미가 없다면 DTO는 class가 더 자연스러울 수 있다. 자동 매핑의 핵심은 struct 여부가 아니라 JSON Schema와 C# 계약의 일치다.

정리

OpenAI Structured Outputs와 C# 자동 매핑의 최소 구현은 명시적인 DTO, 동일한 JSON Schema, JsonSerializer.Deserialize<T>(), 도메인 검증 네 단계다. 클라이언트에 복잡한 범용 매퍼를 만들기보다 서버에서 안전하게 생성하고 Unity에서는 검증된 작은 DTO를 UI와 게임플레이에 연결하는 방식이 유지보수 비용을 가장 낮춘다.