800GB 온디바이스 AI 카탈로그의 API 계약과 직렬화 거버넌스: ZeliDesk.Server의 DTO 불변성 설계, 순환 참조 방어([JsonIgnore]), 및 경로 조작(Path Traversal) 0초 차단 아키텍처

수백 GB의 온디바이스 sLM 모델 메타데이터 전송 시 발생하는 순환 참조 무한 루프 차단, 불필요한 페이로드 낭비를 없애는 조건부 직렬화(WhenWritingNull), 그리고 ../ 경로 조작 공격을 원천 봉쇄하는 TryResolveSafePath 경계 검증 심층 분석.

목차 (Table of Contents)

1. 대규모 AI 카탈로그 전송의 난제: 엔티티 직결의 함정과 순환 참조

전국 12,000개 초·중·고교 교실의 40만 대 PC와 국방 전산실에서 가동되는 ZeliDesk Suite는 각종 양자화 sLM(GGUF), 온디바이스 이미지 확산 모델(Stable Diffusion, ZeliGen), 그리고 음성/OCR 모델을 중앙의 800GB 모델 허브(ZeliDesk.Server)로부터 조회하고 동기화합니다.

이러한 고성능 모델 레지스트리 서버를 구축할 때 흔히 저지르는 치명적인 실수는 데이터베이스의 ORM 엔티티(Entity)를 REST API 컨트롤러에서 클라이언트로 그대로 직렬화하여 반환하는 것입니다. EF Core에서 ModelInfo(부모)와 ModelFile(자식)은 1:N 양방향 Navigation 관계를 맺고 있습니다.

💡 엔티티 직접 직렬화 시 발생하는 3대 재앙:
1. 순환 참조 무한 루프(Circular Reference Loop): ModelInfo.Files[0].Model.Files[0]...로 이어지는 상호 참조로 인해 JsonException: A possible object cycle was detected 크래시 발생.
2. 네트워크 페이로드 폭발: 수백 개의 모델 목록을 단순 조회할 뿐인데 각 모델에 딸린 수십 개의 샤드 파일 메타데이터(URL, SHA-256, 바이트 크기)까지 함께 직렬화되어 응답 크기가 수십 MB로 팽창.
3. 오버포스팅(Over-posting) 보안 취약점: 클라이언트가 보낸 JSON이 서버 내부 엔티티의 관리자 전용 필드(다운로드 횟수, 생성 일시 등)를 임의로 덮어쓰는 무결성 훼손.

ZeliDesk.Server는 이러한 문제를 해결하기 위해 엄격한 DTO(Data Transfer Object) 불변성 설계와 System.Text.Json 어노테이션 기반 직렬화 거버넌스를 확립했습니다.

2. DTO 아키텍처와 API 계약(Contract) 분리: Create·Update·List의 역할

ZeliDesk.Server의 Models/Dtos.cs는 단일 책임 원칙(SRP)에 따라 엔드포인트의 목적별로 명확하게 분리된 3대 DTO를 정의합니다.

DTO 클래스명 HTTP 메서드 주요 설계 특성 및 방어 메커니즘
CreateModelRequest POST /api/models 모델 등록 전용 계약. Id, DisplayName 등 필수 메타데이터를 강제하며, CreatedAt, DownloadCount 등 서버 통제 필드를 원천 차단(오버포스팅 방지).
UpdateModelRequest PUT /api/models/{id} 부분 업데이트(Partial Patch) 전용 계약. 모든 필드가 Nullable로 선언되어 클라이언트가 전송한 필드만 선별적으로 엔티티에 반영(불필요한 덮어쓰기 방지).
ModelListResponse GET /api/models 대용량 페이징 응답 계약. List<ModelInfo>와 TotalCount를 페어링하여 클라이언트 UI의 무한 스크롤 및 페이지네이션을 0ms 레이턴시로 지원.

이와 같은 분리를 통해 클라이언트는 서버 내부 데이터 스키마의 변경에 영향받지 않으며, 서버는 악의적이거나 불완전한 페이로드가 내부 저장소로 침투하는 것을 게이트웨이 레벨에서 완벽하게 차단합니다.

3. System.Text.Json 직렬화 다이어트: [JsonIgnore]와 조건부 생략

양방향 관계를 유지하면서도 순환 참조를 방지하고 네트워크 전송량을 최소화하기 위해 ZeliDesk.Server는 두 가지 전략적 JSON 어노테이션을 적용했습니다.

C# ModelInfo & ModelFile 직렬화 제어 (ZeliDesk.Server/Models) System.Text.Json
// 자식 엔티티: 부모로의 역참조를 무조건 직렬화에서 제외
public class ModelFile
{
    public int Id { get; set; }
    public string ModelId { get; set; } = string.Empty;
    public string FileName { get; set; } = string.Empty;
    public long SizeBytes { get; set; }
    public string Sha256 { get; set; } = string.Empty;

    // Navigation: 부모 ModelInfo로 향하는 순환 참조 원천 봉쇄
    [JsonIgnore]
    public ModelInfo? Model { get; set; }
}

// 부모 엔티티: null인 경우 필드 자체를 JSON에서 완전히 생략
public class ModelInfo
{
    [Key]
    public string Id { get; set; } = string.Empty;
    public string DisplayName { get; set; } = string.Empty;
    public long TotalSizeBytes { get; set; }

    // 목록 조회(GetModels)에서는 Files가 로드되지 않거나 명시적으로 배제될 수 있음
    // null일 경우 "files": null 키 자체를 생성하지 않아 JSON 전송량 30% 절감
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public List<ModelFile>? Files { get; set; }
}

ModelFile.Model에 붙은 [JsonIgnore]는 부모-자식-부모로 꼬리를 무는 직렬화 무한 루프를 0바이트의 오버헤드로 원천 단절합니다. 또한 ModelInfo.Files에 선언된 JsonIgnoreCondition.WhenWritingNull은 수백 개 모델의 메타데이터만 훑어보는 가벼운 쿼리에서 불필요한 null 필드를 전송하지 않음으로써 학교 교실의 협소한 무선 네트워크 대역폭을 획기적으로 아껴줍니다.

4. 원격 파일 서빙의 보안 방패: TryResolveSafePath 경로 조작(Path Traversal) 0초 차단

대용량 모델 허브는 클라이언트의 요청에 따라 GGUF 가중치 파일, 토크나이저 설정 파일(tokenizer.json), 샤드 인덱스 파일 등을 개별적으로 다운로드할 수 있는 원격 스트리밍 엔드포인트를 제공합니다. 이때 가장 빈번하게 발생하는 보안 취약점이 바로 디렉터리 탐색 공격(Directory / Path Traversal)입니다.

⚠️ 디렉터리 탐색 공격 시나리오:
공격자가 파일명 파라미터에 ../../../../windows/system32/cmd.exe 또는 ../../appsettings.json을 주입하여 모델 저장소 디렉터리를 탈출하고 서버의 핵심 시스템 파일이나 비밀 설정 파일을 무단으로 탈취하려는 시도.

ZeliDesk.Server는 파일 접근이 일어나는 모든 엔드포인트의 최전선에 TryResolveSafePath 검증 메서드를 배치하여 경로 조작을 0초 만에 무력화합니다.

C# TryResolveSafePath 경로 무결성 검증 (ModelsController.cs) Path Traversal Defense
/// <summary>
/// {id}/{relativeName} 경로가 모델 저장 디렉터리 안에 있는지 엄격히 검증한다.
/// ".." 등으로 디렉터리를 벗어나는 경로 탐색(path traversal)을 0초 차단한다.
/// </summary>
private bool TryResolveSafePath(string id, string relativeName, out string fullPath)
{
    fullPath = string.Empty;
    if (string.IsNullOrEmpty(relativeName)) return false;

    // 1. 모델의 합법적인 루트 디렉터리를 절대 경로로 정규화
    string root = Path.GetFullPath(Path.Combine(_storagePath, id));

    // 2. 요청된 상대 경로를 결합한 대상 경로를 절대 경로로 정규화 ('..' 등이 모두 해석됨)
    string candidate = Path.GetFullPath(Path.Combine(_storagePath, id, relativeName));

    // 3. 후보 경로가 모델 루트(root)와 정확히 일치하거나, 루트 바로 하위 디렉터리여야 한다.
    if (!candidate.Equals(root, StringComparison.Ordinal) &&
        !candidate.StartsWith(root + Path.DirectorySeparatorChar, StringComparison.Ordinal))
    {
        return false; // 경계를 벗어난 모든 시도를 즉시 차단
    }

    fullPath = candidate;
    return true;
}

이 알고리즘의 핵심은 Path.GetFullPath를 통해 .과 .. 등 모든 상대 경로 지시자를 운영체제 커널 레벨의 실제 물리 경로로 완전히 계산(Canonicalization)한 뒤, 대상 경로가 반드시 root + DirectorySeparatorChar로 시작하는지 StringComparison.Ordinal로 엄밀히 비교하는 것입니다. 운영체제별 구분자 차이나 대소문자 우회 기법이 개입할 여지를 수학적으로 차단합니다.

5. 식별자 정규화(ToLowerInvariant)와 레이스 컨디션 상호 배제

RESTful API에서 데이터 무결성을 파괴하는 또 다른 복병은 문자열 식별자의 대소문자 파편화와 비동기 작업 중 삭제(Delete) 레이스 컨디션입니다.

위험 요소 잠재적 장애 ZeliDesk.Server의 아키텍처적 해결책
식별자 대소문자 불일치 클라이언트가 Qwen2.5-7B로 등록하고 qwen2.5-7b로 조회할 때 404가 발생하거나, 중복 검사(AnyAsync)는 통과했으나 SQLite 고유 인덱스에서 PK 충돌(500 Internal Error) 발생. string newId = req.Id.Trim().ToLowerInvariant();를 통해 입력 즉시 소문자 불변값으로 정규화하여 중복 검사와 저장을 동일한 단일 기준으로 통일.
미러링 중 모델 삭제 충돌 백그라운드 MirrorService가 수십 GB의 샤드 파일을 디스크에 쓰는 도중 관리자가 모델 삭제 API를 호출하면 쓰기 잠금 충돌 또는 유령 디렉터리 재생성 버그 발생. if (MirrorService.IsMirroring(id)) return Conflict(...);를 통해 원자적 활성 작업 여부를 검사하고 HTTP 409 Conflict로 파괴적 삭제를 즉시 거부.

6. 프로덕션 소스 코드 정밀 해부: Dtos.cs & ModelsController.cs

실제 운영 환경에서 검증된 ZeliDesk.Server의 모델 등록(Create), 부분 수정(Update), 상세 조회(Get) 컨트롤러 로직입니다.

C# ModelsController CRUD 및 DTO 매핑 로직 (ZeliDesk.Server) Production C#
[HttpPost]
[EnableRateLimiting("general")]
public async Task<ActionResult<ModelInfo>> CreateModel([FromBody] CreateModelRequest req)
{
    if (string.IsNullOrWhiteSpace(req.Id))
        return BadRequest(new { error = "모델 ID는 필수입니다." });

    // 저장 시 정규화(lowercase)하므로 중복 검사도 정규화된 ID로 통일
    string newId = req.Id.Trim().ToLowerInvariant();
    if (await _db.Models.AnyAsync(m => m.Id == newId))
        return Conflict(new { error = $"모델 '{newId}'이(가) 이미 존재합니다." });

    var model = new ModelInfo
    {
        Id = newId,
        DisplayName = req.DisplayName,
        Description = req.Description,
        Author = req.Author,
        Format = req.Format,
        QuantType = req.QuantType,
        ParamSize = req.ParamSize,
        Icon = req.Icon,
        CreatedAt = DateTime.UtcNow,
        UpdatedAt = DateTime.UtcNow,
        Files = new List<ModelFile>()
    };

    // 파일 저장소 격리 디렉터리 자동 생성
    string modelDir = Path.Combine(_storagePath, model.Id);
    Directory.CreateDirectory(modelDir);

    _db.Models.Add(model);
    await _db.SaveChangesAsync();

    _logger.LogInformation("모델 등록: {Id} ({DisplayName})", model.Id, model.DisplayName);
    return CreatedAtAction(nameof(GetModel), new { id = model.Id }, model);
}

[HttpPut("{id}")]
[EnableRateLimiting("general")]
public async Task<ActionResult<ModelInfo>> UpdateModel(string id, [FromBody] UpdateModelRequest req)
{
    var model = await _db.Models.FindAsync(id);
    if (model == null)
        return NotFound(new { error = $"모델 '{id}'을 찾을 수 없습니다." });

    // Nullable DTO 패턴: 전송된 필드만 선별적으로 패치
    if (req.DisplayName != null) model.DisplayName = req.DisplayName;
    if (req.Description != null) model.Description = req.Description;
    if (req.Author != null) model.Author = req.Author;
    if (req.Format != null) model.Format = req.Format;
    if (req.QuantType != null) model.QuantType = req.QuantType;
    if (req.ParamSize != null) model.ParamSize = req.ParamSize;
    if (req.Icon != null) model.Icon = req.Icon;
    if (req.Tier != null) model.Tier = req.Tier;
    model.UpdatedAt = DateTime.UtcNow;

    await _db.SaveChangesAsync();
    _logger.LogInformation("모델 수정: {Id}", id);
    return Ok(model);
}

7. 결론: 공교육 AIDT와 국방망 클라이언트를 지탱하는 신뢰의 데이터 파이프라인

거대 AI 모델 카탈로그를 운영하는 시스템에서 가장 중요한 것은 단순한 기능 구현이 아니라, 네트워크를 타고 흐르는 데이터의 순수성과 물리 디스크의 안전을 보장하는 엄격한 거버넌스입니다.

ZeliDesk.Server는 DTO를 통한 명확한 API 계약 분리, [JsonIgnore]를 통한 순환 참조 차단과 페이로드 다이어트, 그리고 TryResolveSafePath를 통한 완벽한 디렉터리 탈출 방어로 단 1바이트의 불필요한 트래픽도, 단 1건의 보안 취약점도 허용하지 않는 무결점 아키텍처를 완성했습니다.

이러한 견고한 백엔드 엔지니어링이 뒷받침되기에, 전국의 초·중·고 교실과 국방 AI 교육 현장은 언제나 안심하고 최신 AI 모델을 원클릭으로 공급받아 실습에 전념할 수 있습니다.

← 이전 기술 백서 #869 공공 클라우드 제로 트러스트 API 게이트웨이: ZeliDesk.Server ApiKeyAuthMiddleware의 상수 시간 비교(FixedTimeEquals) 및 동적 화이트리스트 보안 아키텍처 다음 기술 백서 → #867 RTX 5090과 노후 PC를 아우르는 온디바이스 생성 AI의 자가 치유: ZeliGen의 CPython 런타임 6단계 무결성 진단(DiagnoseRuntime) 및 VC++ 종속성 자동 복구 아키텍처
⚡ ZeliDesk 고성능 AI 모델 허브 인프라 체험하기

순환 참조 없는 경량 JSON 직렬화와 디렉터리 탈출 방어막으로 무장한 ZeliDesk AI 모델 허브를 지금 경험하세요.

ZeliDesk Suite 무료 다운로드