LLM JSON Output Parser로 Unity C# 인게임 데이터 직렬화와 예외 처리 구현하기
LLM 응답 JSON을 Unity C# 인게임 데이터로 안전하게 변환하는 방법을 다룹니다. 스키마 검증, JSON 역직렬화, 재시도 정책, 예외 분류와 폴백까지 실전 코드로 정리합니다.

LLM JSON Output Parser 연동의 핵심은 모델 응답을 곧바로 게임 상태로 적용하지 않는 것이다. C# DTO로 역직렬화한 뒤 스키마·범위·참조 무결성을 검증하고 실패 원인별로 재시도·폴백·사용자 알림을 분리하면 잘못된 응답이 인게임 데이터를 오염시키는 일을 막을 수 있다.
LLM JSON Output Parser가 필요한 이유는 무엇인가?
인게임 NPC 대사, 퀘스트 보상, 아이템 추천처럼 LLM이 구조화된 데이터를 반환하는 기능은 일반 텍스트보다 후처리가 쉽다. 하지만 JSON 형식처럼 보이는 문자열이 항상 게임에서 쓸 수 있는 데이터라는 뜻은 아니다.
대표적인 실패 사례는 다음과 같다.
| 실패 유형 | 예시 | 클라이언트 영향 |
|---|---|---|
| 문법 오류 | 닫히지 않은 중괄호, Markdown 코드 펜스 포함 | 역직렬화 예외 |
| 타입 불일치 | gold: "많이" | 숫자 필드 변환 실패 |
| 필수 필드 누락 | items 누락 | null 참조 또는 불완전한 UI |
| 범위 초과 | 보상 골드가 99999999 | 밸런스 붕괴 |
| 존재하지 않는 ID | 등록되지 않은 아이템 ID | 런타임 참조 실패 |
따라서 Parser의 책임은 단순히 JsonConvert.DeserializeObject를 호출하는 데서 끝나지 않는다. 응답을 정제하고 DTO로 변환하고 게임 규칙을 검증한 뒤에만 도메인 데이터로 승인해야 한다.

Unity C#에서 LLM 출력용 JSON 스키마를 어떻게 설계할까?
예제로 NPC가 생성하는 퀘스트 보상 응답을 사용하겠다. Unity 프로젝트에서는 Newtonsoft Json 패키지를 쓰면 JsonProperty와 예외 처리 구성이 간결하다.
LLM에 전달하는 출력 계약은 가능한 한 작고 엄격하게 유지한다.
{
"questId": "forest_intro",
"gold": 120,
"items": [
{ "itemId": "potion_small", "quantity": 2 }
],
"summary": "숲 입구의 정찰을 완료한 보상입니다."
}
C# DTO는 API 응답 구조만 표현한다. 아이템 DB 조회나 보상 지급 같은 게임 규칙은 DTO에 넣지 않는다.
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
[Serializable]
public sealed class LlmQuestRewardDto
{
[JsonProperty("questId", Required = Required.Always)]
public string QuestId { get; set; }
[JsonProperty("gold", Required = Required.Always)]
public int Gold { get; set; }
[JsonProperty("items", Required = Required.Always)]
public List<LlmRewardItemDto> Items { get; set; }
[JsonProperty("summary", Required = Required.Always)]
public string Summary { get; set; }
}
[Serializable]
public sealed class LlmRewardItemDto
{
[JsonProperty("itemId", Required = Required.Always)]
public string ItemId { get; set; }
[JsonProperty("quantity", Required = Required.Always)]
public int Quantity { get; set; }
}
JSON 스키마와 C# DTO의 역할은 어떻게 나눌까?
| 계층 | 검증 대상 | 예시 |
|---|---|---|
| LLM 출력 스키마 | JSON의 키, 타입, 배열 구조 | gold는 정수여야 함 |
| DTO 역직렬화 | 필수 속성, JSON 문법 | questId가 누락되지 않음 |
| 도메인 검증 | 게임 규칙과 데이터베이스 참조 | 골드 상한, 실제 아이템 ID |
| 적용 계층 | 상태 변경의 원자성 | 검증 완료 후 한 번만 지급 |
모델 프롬프트에는 “설명 없이 JSON 객체만 반환”이라고 명시하고 가능하다면 사용하는 LLM API의 JSON Schema 또는 Structured Outputs 기능으로 같은 계약을 강제한다. 다만 API 레벨 제약이 있어도 네트워크 전송과 클라이언트 경계를 통과한 데이터는 다시 검증해야 한다.
LLM JSON 응답을 안전하게 파싱하는 3단계
1. 코드 펜스와 빈 응답을 먼저 정리한다
일부 모델은 JSON만 요청해도 Markdown 코드 펜스를 덧붙인다. 정제는 최소한으로 하고 임의의 문자열 복구를 과도하게 시도하지 않는 편이 좋다. 잘못된 JSON을 억지로 수정하면 원인을 추적하기 어려워진다.
using System;
public static class LlmJsonSanitizer
{
public static string ExtractJsonObject(string raw)
{
if (string.IsNullOrWhiteSpace(raw))
throw new ArgumentException("LLM 응답이 비어 있습니다.", nameof(raw));
var text = raw.Trim();
if (text.StartsWith("```", StringComparison.Ordinal))
{
var firstNewLine = text.IndexOf('\n');
var lastFence = text.LastIndexOf("```", StringComparison.Ordinal);
if (firstNewLine >= 0 && lastFence > firstNewLine)
text = text.Substring(firstNewLine + 1, lastFence - firstNewLine - 1).Trim();
}
if (!text.StartsWith("{", StringComparison.Ordinal) ||
!text.EndsWith("}", StringComparison.Ordinal))
{
throw new FormatException("JSON 객체 형식의 응답이 아닙니다.");
}
return text;
}
}
2. 역직렬화 예외를 별도 경로로 처리한다
JsonSerializationException은 JSON 문법, 타입, 필수 속성 문제를 구분하는 출발점이다. 통신 실패와 동일하게 처리하면 재시도 원인을 파악하기 어렵다.
using Newtonsoft.Json;
public static class LlmRewardParser
{
private static readonly JsonSerializerSettings Settings = new()
{
MissingMemberHandling = MissingMemberHandling.Error,
NullValueHandling = NullValueHandling.Include
};
public static LlmQuestRewardDto Parse(string rawResponse)
{
var json = LlmJsonSanitizer.ExtractJsonObject(rawResponse);
var dto = JsonConvert.DeserializeObject<LlmQuestRewardDto>(json, Settings);
if (dto == null)
throw new JsonSerializationException("보상 JSON을 DTO로 변환하지 못했습니다.");
return dto;
}
}
MissingMemberHandling.Error는 계약에 없던 키를 조기에 발견하는 데 유용하다. 반대로 서버가 점진적으로 필드를 추가하는 환경이라면 DTO 버전과 API 버전을 함께 관리하고 호환이 필요한 구간에서만 이 설정을 완화한다.
3. 게임 규칙을 검증한 뒤 도메인 객체로 변환한다
역직렬화 성공은 안전한 보상 지급을 보장하지 않는다. 예를 들어 gold: -10은 정수이므로 파싱되지만 올바른 보상은 아니다.
using System;
using System.Collections.Generic;
public interface IItemCatalog
{
bool Contains(string itemId);
}
public sealed class QuestRewardValidator
{
private const int MaxGold = 10000;
private const int MaxItemQuantity = 99;
private readonly IItemCatalog _itemCatalog;
public QuestRewardValidator(IItemCatalog itemCatalog)
{
_itemCatalog = itemCatalog;
}
public void Validate(LlmQuestRewardDto reward, string expectedQuestId)
{
if (reward.QuestId != expectedQuestId)
throw new InvalidOperationException("요청한 퀘스트와 응답 퀘스트 ID가 다릅니다.");
if (reward.Gold < 0 || reward.Gold > MaxGold)
throw new InvalidOperationException("골드 값이 허용 범위를 벗어났습니다.");
if (reward.Items == null || reward.Items.Count > 10)
throw new InvalidOperationException("아이템 목록 개수가 올바르지 않습니다.");
foreach (var item in reward.Items)
{
if (string.IsNullOrWhiteSpace(item.ItemId) || !_itemCatalog.Contains(item.ItemId))
throw new InvalidOperationException("알 수 없는 아이템 ID가 포함되어 있습니다.");
if (item.Quantity < 1 || item.Quantity > MaxItemQuantity)
throw new InvalidOperationException("아이템 수량이 허용 범위를 벗어났습니다.");
}
}
}
예외 처리와 재시도 정책은 어떻게 구성할까?
파싱 실패와 네트워크 실패는 같은 재시도 정책을 쓰면 안 된다. JSON 계약 오류는 동일한 입력을 그대로 재시도해도 반복될 가능성이 높으므로 재시도 시에는 더 엄격한 재생성 프롬프트나 구조화 출력 요청으로 전환하는 편이 낫다.
flowchart TD
A[LLM 응답 수신] --> B{네트워크 오류인가?}
B -- 예 --> C[지수 백오프 후 제한 재시도]
B -- 아니오 --> D[JSON 정제 및 DTO 역직렬화]
D --> E{파싱 성공?}
E -- 아니오 --> F[구조화 출력 재요청 또는 안전한 폴백]
E -- 예 --> G[도메인 규칙 검증]
G --> H{검증 통과?}
H -- 아니오 --> I[응답 폐기 및 로그 기록]
H -- 예 --> J[원자적으로 게임 상태에 적용]
실제 호출부는 사용자에게는 일관된 결과를 보여 주고 개발 환경에서는 실패 원인을 남겨야 한다.
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
public sealed class RewardGenerationService
{
private readonly QuestRewardValidator _validator;
public RewardGenerationService(QuestRewardValidator validator)
{
_validator = validator;
}
public async Task<LlmQuestRewardDto> GetRewardAsync(
Func<Task<string>> requestLlmAsync,
string questId)
{
try
{
var raw = await requestLlmAsync();
var reward = LlmRewardParser.Parse(raw);
_validator.Validate(reward, questId);
return reward;
}
catch (JsonReaderException exception)
{
UnityEngine.Debug.LogWarning($"LLM JSON 문법 오류: {exception.Message}");
throw new LlmOutputException("응답 JSON 형식이 올바르지 않습니다.", exception);
}
catch (JsonSerializationException exception)
{
UnityEngine.Debug.LogWarning($"LLM JSON 계약 오류: {exception.Message}");
throw new LlmOutputException("응답 데이터 구조가 계약과 다릅니다.", exception);
}
catch (FormatException exception)
{
UnityEngine.Debug.LogWarning($"LLM 응답 정제 실패: {exception.Message}");
throw new LlmOutputException("JSON 객체를 찾지 못했습니다.", exception);
}
catch (InvalidOperationException exception)
{
UnityEngine.Debug.LogWarning($"LLM 응답 검증 실패: {exception.Message}");
throw new LlmOutputException("게임 규칙 검증에 실패했습니다.", exception);
}
}
}
public sealed class LlmOutputException : Exception
{
public LlmOutputException(string message, Exception innerException)
: base(message, innerException) { }
}
재시도 횟수는 몇 번이 적절한가?
클라이언트 UI를 오래 묶지 않도록 일반적인 인게임 생성 요청은 최대 1~2회 재시도로 제한하는 편이 안전하다. 네트워크 오류에는 지수 백오프를 적용할 수 있지만 검증 실패에는 같은 응답을 다시 파싱하지 말고 새 요청을 보내거나 사전 정의 보상으로 폴백해야 한다.
| 오류 분류 | 권장 처리 | 게임 상태 적용 |
|---|---|---|
| 타임아웃, 일시적 5xx | 짧은 지연 후 제한 재시도 | 적용 금지 |
| JSON 문법 오류 | 구조화 출력 조건을 강화해 재요청 | 적용 금지 |
| 스키마 불일치 | 서버·프롬프트 버전 확인, 폴백 | 적용 금지 |
| 범위·ID 검증 실패 | 응답 폐기, 진단 로그 기록 | 적용 금지 |
| 검증 성공 | 도메인 명령으로 한 번만 지급 | 허용 |
인게임 데이터 직렬화에서 놓치기 쉬운 보안과 상태 관리
LLM이 생성한 보상, 재화, 인벤토리 변경을 신뢰할 수 있는 클라이언트 상태로 취급하면 안 된다. 경쟁 요소나 경제 시스템이 있는 게임이라면 최종 보상 판정과 지급은 서버 권한으로 처리해야 한다. 클라이언트는 표시용 후보 데이터와 입력 검증에 집중하고 서버는 퀘스트 완료 여부와 보상 테이블을 기준으로 최종 결과를 결정한다.
또한 요청마다 requestId를 만들고 완료 처리 기록을 보관하면 네트워크 재시도나 화면 중복 탭으로 같은 보상이 두 번 반영되는 문제를 줄일 수 있다. Unity UI에서는 로딩 중 버튼을 비활성화하고 성공적으로 검증된 결과를 받은 뒤에만 버튼 상태와 보상 패널을 갱신한다.

자주 묻는 질문 (FAQ)
LLM이 항상 JSON만 반환하도록 프롬프트를 쓰면 파서는 필요 없나?
아니다. 프롬프트는 출력 품질을 높이지만 네트워크, 모델, 버전 변경, 악의적 입력으로 인한 계약 위반을 제거하지 못한다. 클라이언트 경계에서는 역직렬화와 도메인 검증이 계속 필요하다.
Unity에서 JsonUtility 대신 Newtonsoft Json을 써야 하나?
단순하고 고정된 Unity 직렬화 데이터에는 JsonUtility도 적합하다. 그러나 필수 필드 검사, 상세 예외, 다양한 JSON 형태, 명시적인 속성 이름이 필요하면 Newtonsoft Json이 파서 구현과 진단에 더 유리하다.
파싱에 성공한 LLM 보상은 바로 인벤토리에 추가해도 되나?
안 된다. 아이템 ID 존재 여부, 수량 상한, 퀘스트 ID 일치, 중복 지급 여부를 검사한 후 적용해야 한다. 경제나 경쟁성에 영향을 주는 보상은 서버에서 최종 승인해야 한다.
정리
LLM JSON Output Parser를 Unity C#에 연결할 때는 정제 → DTO 역직렬화 → 도메인 검증 → 원자적 적용 순서를 지킨다. JSON 문법 오류, 스키마 오류, 게임 규칙 위반을 구분해 처리하고 검증에 실패한 데이터는 절대 인게임 상태에 반영하지 않으면 생성형 기능을 게임 클라이언트에 안정적으로 넣을 수 있다.



