1. 서론: 공교육 교권·학사 상담과 국방 법률 세션의 프라이버시 영속화 난제
인공지능 어시스턴트가 실제 교육과 국방 업무 현장에 깊숙이 침투하면서, 사용자가 AI와 나눈 대화 기록(Session History)과 다국어 번역 질의(Translation Query)의 영속화(Persistence) 방식은 가장 민감한 정보보호 규제(개인정보보호법 제29조, 국방보안업무훈령)의 심사 대상이 되고 있습니다.
교원이 학생의 학교생활기록부 기재 사항을 다듬거나, 학교폭력 및 교권 침해에 관한 법률 상담을 AI에게 요청할 때, 혹은 군사 실무자가 암호화 장비 규정이나 군형법 관련 조항을 검토할 때, 이 데이터가 원격 클라우드 서버(SaaS DB, Firebase, Supabase 등)에 저장되는 순간 중대한 보안 위규가 발생합니다:
| 비교 항목 | 일반 SaaS AI 플랫폼 히스토리 | ZeliDesk.Core HistoryService |
|---|---|---|
| 데이터 저장 위치 | 원격 클라우드 RDBMS / NoSQL | 100% 로컬 격리 (%LOCALAPPDATA%\ZeliDesk\History) |
| 네트워크 전송량 | 매 대화 턴마다 양방향 HTTPS 통신 | 외부 전송 0바이트 (순수 로컬 파일 I/O) |
| 저장 용량 제어 | 무제한 축적으로 인한 클라우드 요금 증가 | MaxEntries = 100 링 버퍼 슬라이딩 윈도우 강제 |
| 중복 저장 방어 | 동일 질의 재요청 시 DB 중복 행 누적 | 최근 항목 내용 비교를 통한 중복 I/O 원천 차단 |
| 세션 라이프사이클 | 서버 측 세션 만료 정책 종속 | GUID SessionId 기반 원자적 갱신 및 로컬 영구 보존 |
ZeliDesk.Core의 HistoryService는 이 엄격한 보안 요구사항을 충족하기 위해
경량 JSON 직렬화와 100건 슬라이딩 윈도우 링 버퍼를 결합한
온디바이스 전용 프라이버시 히스토리 거버넌스를 구현했습니다.
2. 클라우드 SaaS 데이터베이스 vs 온디바이스 %LOCALAPPDATA% 로컬 격리
Windows 운영체제에서 다중 사용자 환경(Multi-User Environment)을 안전하게 분리하는 표준 저장소는
Environment.SpecialFolder.LocalApplicationData입니다.
이 경로는 컴퓨터를 사용하는 각 사용자 계정(예: C:\Users\{UserName}\AppData\Local\ZeliDesk\History)마다
Windows 커널의 ACL(Access Control List)에 의해 철저하게 격리됩니다.
따라서 한 컴퓨터실 PC에서 이전 시간의 학생이나 교사가 작업한 상담 내역을
다른 윈도우 계정의 사용자가 열람하는 것이 OS 레벨에서 원천 차단됩니다.
HistoryService는 정적 생성자(Static Constructor)에서
해당 디렉터리를 원자적으로 사전 프로비저닝합니다:
private static readonly string _historyDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ZeliDesk", "History");
static HistoryService()
{
// 사용자 로컬 보안 영역에 격리 디렉터리 원자적 생성
Directory.CreateDirectory(_historyDir);
}
3. 디스크 스토리지 고갈 0초 방어: MaxEntries 100 슬라이딩 윈도우 링 버퍼
온디바이스 소프트웨어가 장기 가동될 때 반드시 통제해야 하는 엔지니어링 지표는 디스크 용량 점유율입니다.
사용자가 수개월 동안 수천 번의 번역과 상담을 수행한다고 해서
로그 파일이 수백 메가바이트(MB)로 비대해지면, 저사양 단말의 파일 읽기/쓰기 속도가 급격히 저하되고
결국 OutOfMemoryException으로 이어집니다.
HistoryService는 MaxEntries = 100 상한을 두는 슬라이딩 윈도우 링 버퍼(Sliding Window Ring Buffer) 패턴을 적용했습니다.
새로운 세션이나 번역 항목이 추가될 때마다 가장 상단(Index 0)에 삽입(Insert(0, item))하고,
100건을 초과하는 과거 항목은 Take(MaxEntries)로 즉시 잘라내어
파일 크기를 항상 50KB ~ 100KB 이내로 영구 제어합니다:
history.Insert(0, session); // 최신 항목을 0번 인덱스에 삽입하여 시간 역순 정렬
if (history.Count > MaxEntries)
{
// 정확히 100건까지만 유지하고 초과분은 즉각 가비지 컬렉션 대상화
history = history.Take(MaxEntries).ToList();
}
4. 동일 쿼리 중복 쓰기 방어와 세션 ID 기반 역색인 갱신 알고리즘
사용자가 번역 결과를 확인하기 위해 단축키를 연타하거나 버튼을 중복 클릭할 경우, 동일한 텍스트가 히스토리에 무의미하게 누적되는 현상은 사용자 경험을 심각하게 해칩니다.
HistoryService는 번역 기록 저장 시
가장 최근에 저장된 항목(Index 0)과의 원문/번역문 일치 여부를 사전 검증하여
중복 디스크 I/O를 0초 차단합니다:
var last = history.FirstOrDefault();
if (last != null && last.SourceText == newItem.SourceText && last.TargetText == newItem.TargetText)
{
// 가장 최근 결과와 완전히 동일한 질의인 경우 디스크 쓰기 생략
return;
}
또한 법률 상담 세션의 경우, 사용자가 동일 세션 내에서 AI와 계속해서 멀티턴(Multi-Turn) 대화를 나눌 때
새로운 세션이 계속 증식하는 것을 방지하기 위해
SessionId를 기준으로 기존 항목을 찾아 제거한 후 갱신된 세션을 맨 앞으로 올리는(Move to Top) 원자적 갱신 알고리즘을 적용했습니다:
var existing = history.FirstOrDefault(h => h.SessionId == session.SessionId);
if (existing != null)
{
history.Remove(existing); // 기존 세션 제거
}
history.Insert(0, session); // 갱신된 최신 세션을 최상단에 재배치
5. 번역 이력과 법률 상담 채팅 메시지의 분리 직렬화 구조
HistoryService는 데이터의 특성과 크기에 따라 저장소를
단순 텍스트 페어인 translate_history.json과
복합 멀티턴 대화 구조체인 legal_history.json으로 완벽히 분리했습니다.
| 엔티티 모델 | 저장 파일 | 데이터 구조 및 특징 |
|---|---|---|
TranslateHistoryItem |
translate_history.json |
출발어(SourceLang), 도착어(TargetLang), 원문, 번역문, 타임스탬프 (단일 플랫 레코드) |
LegalHistoryItem |
legal_history.json |
고유 세션(SessionId), 법률 도메인(Domain), 제목(Title), List<ChatMessage> 대화 트리 |
이와 같이 저장소를 분리함으로써, 가벼운 번역 히스토리 목록을 조회할 때 무거운 법률 상담 대화 트리를 메모리에 로드하지 않아도 되므로 앱의 시작 속도와 메모리 풋프린트를 극단적으로 절약합니다.
6. 핵심 구현 분석: ZeliDesk.Core HistoryService.cs 실코드 워크스루
다음은 공교육과 국방 현장의 민감한 히스토리를
단 1바이트의 외부 유출 없이 로컬에 완벽히 보존하는 HistoryService.cs의 전체 소스코드입니다:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace ZeliDesk.Core.Services
{
/// <summary>
/// 번역 이력 단일 레코드 모델
/// </summary>
public class TranslateHistoryItem
{
public string SourceLang { get; set; }
public string TargetLang { get; set; }
public string SourceText { get; set; }
public string TargetText { get; set; }
public DateTime Timestamp { get; set; } = DateTime.Now;
}
/// <summary>
/// 공공 법률 상담 세션 모델 (멀티턴 대화 트리 포함)
/// </summary>
public class LegalHistoryItem
{
public string SessionId { get; set; }
public string Domain { get; set; }
public string Title { get; set; }
public List<ChatMessage> Messages { get; set; } = new();
public DateTime Timestamp { get; set; } = DateTime.Now;
}
/// <summary>
/// 온디바이스 히스토리 영속화 및 슬라이딩 윈도우 관리자
/// </summary>
public static class HistoryService
{
// %LOCALAPPDATA%\ZeliDesk\History 격리 스토리지 경로
private static readonly string _historyDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ZeliDesk", "History");
private static readonly string _translateHistoryFile = Path.Combine(_historyDir, "translate_history.json");
private static readonly string _legalHistoryFile = Path.Combine(_historyDir, "legal_history.json");
// 디스크 고갈을 방지하는 최대 보존 레코드 상한
private const int MaxEntries = 100;
static HistoryService()
{
Directory.CreateDirectory(_historyDir);
}
// ═════════════════════════════════════════
// 번역 기록 파이프라인
// ═════════════════════════════════════════
public static List<TranslateHistoryItem> LoadTranslateHistory()
{
if (!File.Exists(_translateHistoryFile)) return new List<TranslateHistoryItem>();
try
{
var json = File.ReadAllText(_translateHistoryFile);
return JsonSerializer.Deserialize<List<TranslateHistoryItem>>(json) ?? new List<TranslateHistoryItem>();
}
catch { return new List<TranslateHistoryItem>(); }
}
public static void SaveTranslateHistory(TranslateHistoryItem newItem)
{
var history = LoadTranslateHistory();
// 중복 방지 (가장 최근 결과가 똑같으면 불필요한 디스크 I/O 생략)
var last = history.FirstOrDefault();
if (last != null && last.SourceText == newItem.SourceText && last.TargetText == newItem.TargetText)
return;
history.Insert(0, newItem);
if (history.Count > MaxEntries)
history = history.Take(MaxEntries).ToList();
try
{
var json = JsonSerializer.Serialize(history, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_translateHistoryFile, json);
}
catch { }
}
public static void ClearTranslateHistory()
{
try { if (File.Exists(_translateHistoryFile)) File.Delete(_translateHistoryFile); } catch { }
}
// ═════════════════════════════════════════
// 법률 상담 기록 파이프라인
// ═════════════════════════════════════════
public static List<LegalHistoryItem> LoadLegalHistory()
{
if (!File.Exists(_legalHistoryFile)) return new List<LegalHistoryItem>();
try
{
var json = File.ReadAllText(_legalHistoryFile);
return JsonSerializer.Deserialize<List<LegalHistoryItem>>(json) ?? new List<LegalHistoryItem>();
}
catch { return new List<LegalHistoryItem>(); }
}
public static void SaveLegalSession(LegalHistoryItem session)
{
var history = LoadLegalHistory();
var existing = history.FirstOrDefault(h => h.SessionId == session.SessionId);
// 동일 세션 업데이트 시 기존 레코드 교체 후 최상단으로 승격
if (existing != null)
{
history.Remove(existing);
}
history.Insert(0, session);
if (history.Count > MaxEntries)
history = history.Take(MaxEntries).ToList();
try
{
var json = JsonSerializer.Serialize(history, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_legalHistoryFile, json);
}
catch { }
}
public static void ClearLegalHistory()
{
try { if (File.Exists(_legalHistoryFile)) File.Delete(_legalHistoryFile); } catch { }
}
}
}
7. 결론: 가장 민감한 기록을 가장 안전하게 지켜내는 온디바이스 거버넌스의 완성
데이터가 곧 권력이자 위험이 되는 인공지능 시대에, 무엇을 저장하고 무엇을 버려야 하는지, 그리고 어디에 남겨야 하는지를 명확히 정의하는 것은 아키텍처 설계자의 가장 숭고한 책임입니다.
ZeliDesk.Core의 HistoryService는
화려한 클라우드 동기화 대신,
로컬 디스크 격리, 100건 슬라이딩 윈도우, 중복 I/O 방어, 원자적 세션 갱신이라는
가장 단단한 엔지니어링 기본기를 선택했습니다.
교사의 교무 상담 일지도, 군 장병의 법률 고충 기록도 외부 네트워크 전송 0바이트의 안전지대 안에서 온전히 보호받을 수 있기에, ZeliDesk는 공공과 국방이 망설임 없이 선택할 수 있는 가장 믿음직한 온디바이스 AI의 표준으로 확고히 자리잡고 있습니다.