<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://groou.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://groou.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-04-07T11:58:37+09:00</updated><id>https://groou.com/feed.xml</id><title type="html">Justin Kim</title><subtitle>Personal blog by Justin Kim covering software development, security, and technology topics.</subtitle><author><name>Justin Kim</name><email>justin.joy.9to5@gmail.com</email></author><entry><title type="html">Symbolic AI에서 Datalog가 필요한 이유</title><link href="https://groou.com/essay/datalog/2026/03/26/prolog-datalog-symbolic-ai/" rel="alternate" type="text/html" title="Symbolic AI에서 Datalog가 필요한 이유" /><published>2026-03-26T12:00:00+09:00</published><updated>2026-03-26T12:00:00+09:00</updated><id>https://groou.com/essay/datalog/2026/03/26/prolog-datalog-symbolic-ai</id><content type="html" xml:base="https://groou.com/essay/datalog/2026/03/26/prolog-datalog-symbolic-ai/"><![CDATA[<p>Datalog와 Prolog는 논리 프로그래밍(Logic Programming)이라는 동일한 뿌리에서 파생되어 시각적으로 매우 유사한 문법을 공유하고 있습니다. 때문에 Datalog를 처음 접하게 되면 이미 익숙하고 범용적인 Prolog와의 실질적인 차이를 체감하기 쉽지 않습니다.</p>

<p>하지만 그 동작 원리와 목적을 자세히 들여다보면 이 둘이 지향하는 방향은 완전히 다릅니다. 이 글에서는 겉보기에 비슷해 보이는 두 언어의 근본적인 차이점을 구체적으로 비교하고, 최근 기호주의(Symbolic) AI 및 지식 그래프 영역에서 왜 범용적인 Prolog를 제쳐두고 Datalog가 더욱 각광받고 있는지 그 명확한 이유를 정리해 보겠습니다.</p>

<h2 id="너무-완벽해서-위험한-prolog">너무 완벽해서 위험한 Prolog</h2>

<p>Prolog는 1970년대에 등장한 논리 프로그래밍의 근간이 되는 언어입니다. 일반적인 프로그래밍 언어들이 보통 “A를 수행하고, 그다음 B를 수행하라”와 같이 순차적인 절차를 지시한다면, Prolog는 “이것은 사실(Fact)이고, 저것은 규칙(Rule)이다”라고 선언하는 데 그칩니다. 그러면 추론 엔진이 자체적으로 연산을 반복하며 해답을 도출해 냅니다.</p>

<p>Prolog는 완전한 튜링 머신(Turing-complete)이기 때문에 구현자가 구상하는 거의 모든 형태의 논리를 구현할 수 있습니다. 리스트와 같은 복잡한 자료구조를 자유롭게 다루며, 복잡한 재귀 알고리즘 또한 작성할 수 있습니다.</p>

<p>하지만 이처럼 ‘모든 것을 할 수 있다’는 점은 대규모 지식을 다뤄야 할 때 역설적으로 큰 제약으로 작용합니다. Prolog는 위에서 아래로 한 길만 끝까지 파고드는 깊이 우선 탐색(DFS) 방식으로 작동합니다. 그러다 보니 구현자가 규칙의 순서를 조금만 잘못 작성해도 시스템이 영원히 답을 찾지 못하고 무한 루프에 빠지기 일쑤입니다. 데이터 패턴이 수억 건에 달하는 방대한 지식 그래프 환경에서 이러한 예측 불가능성은 시스템의 치명적인 약점이 됩니다.</p>

<h2 id="제약을-걸어-안전성을-확보한-datalog">제약을 걸어 안전성을 확보한 Datalog</h2>

<p>이러한 문제를 해결하기 위해 등장한 언어가 바로 Datalog입니다. Datalog는 직관적으로 표현하자면 ‘Light-weighted Prolog’라고 할 수 있습니다.</p>

<p>리스트 연산이나 복잡한 함수와 같은 강력한 기능들을 과감히 배제했습니다. 완전한 튜링 머신이 아니기 때문에 범용적인 애플리케이션을 처음부터 끝까지 독립적으로 구현해 낼 수는 없습니다. 하지만 이 강력한 기능들을 제외한 대신 Datalog는 매우 중요한 장점을 얻게 되었는데, 그것이 바로 <strong>종료의 보장(Termination)</strong> 입니다.</p>

<p>아무리 복잡한 질의(Query)를 수행하더라도 언젠가는 반드시 결론을 도출하고 시스템이 종료된다는 사실이 수학적으로 증명되어 있습니다. 따라서 작성자가 실수로 질의를 잘못 작성하더라도 서버 자원을 고갈시키는 무한 루프를 우려할 필요가 전혀 없습니다.</p>

<h2 id="구체적인-예시-그래프에서-경로path-찾기">구체적인 예시: 그래프에서 경로(Path) 찾기</h2>

<p>이 차이를 가장 명확하게 보여주는 예시가 바로 방향 그래프(Directed Graph)에서 두 지점 간의 경로를 탐색하는 문제입니다.</p>

<p>예를 들어, <code class="language-plaintext highlighter-rouge">a -&gt; b</code>, <code class="language-plaintext highlighter-rouge">b -&gt; c</code>, <code class="language-plaintext highlighter-rouge">c -&gt; a</code>로 무한히 순환(Cycle)하는 그래프 구조가 존재한다고 가정해 보겠습니다. 이 상태에서 ‘X에서 Y로 가는 경로가 존재하는가?’를 확인하는 규칙을 작성해 보겠습니다.</p>

<p><strong>Prolog의 경우 (무한 루프 발생)</strong></p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">path</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">edge</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
<span class="ss">path</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">path</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Z</span><span class="p">),</span> <span class="ss">edge</span><span class="p">(</span><span class="nv">Z</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
</code></pre></div></div>

<p><img src="/images/2026-03-26/prolog_loop.gif" alt="Prolog 무한루프 시뮬레이션" /></p>

<p>Prolog에서 위와 같은 ‘좌측 재귀(Left-recursion)’ 형태로 규칙을 작성하고 순환 그래프를 탐색시키면 큰 문제가 발생합니다. Prolog는 위에서 아래로, 왼쪽부터 파고들기 때문에 끝없이 <code class="language-plaintext highlighter-rouge">path(X, Z)</code>를 재귀 호출하며 <code class="language-plaintext highlighter-rouge">a -&gt; b -&gt; c -&gt; a -&gt; b...</code> 순으로 영원히 연산을 반복하게 됩니다. 구현자가 이 함정을 회피하기 위해서는 규칙의 순서를 수동으로 조작하거나, 이미 방문한 노드를 일일이 리스트에 기록해 두는 등 복잡한 예외 처리 코드를 추가해야만 합니다.</p>

<p><strong>Datalog의 경우 (항상 안전하게 종료)</strong></p>

<p>Datalog의 문법은 Prolog와 동일하므로 앞선 코드를 그대로 사용할 수 있습니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">path</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">edge</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
<span class="ss">path</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">path</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Z</span><span class="p">),</span> <span class="ss">edge</span><span class="p">(</span><span class="nv">Z</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
</code></pre></div></div>

<p><img src="/images/2026-03-26/datalog_termination.gif" alt="Datalog의 고정점(Fixed-point) 종료 시뮬레이션" /></p>

<p>하지만 Datalog 엔진은 전혀 다른 방식으로 작동합니다. 위에서 아래로 무작정 파고드는 대신, 이미 확보한 <code class="language-plaintext highlighter-rouge">edge</code>라는 사실(Fact)들로부터 출발하여 도출할 수 있는 모든 <code class="language-plaintext highlighter-rouge">path</code>를 바닥에서부터 한 번에 조립해 나갑니다(상향식 평가, Bottom-up Evaluation). 지속적으로 관계를 유추하다가 “더 이상 새로운 경로가 도출되지 않는” 순간(고정점, Fixed-point 도달) 시스템이 스스로 계산을 종료합니다.</p>

<p>순환 구조의 존재 여부나 질의의 작성 순서는 아무런 상관이 없습니다. <strong>“단순히 논리만 선언해 두면 알아서 실행을 멈추고 답을 찾아주는”</strong> 특성이 바로 Datalog가 가진 진정한 가치입니다.</p>

<h2 id="지식-기반-ai가-datalog를-선택하는-이유">지식 기반 AI가 Datalog를 선택하는 이유</h2>

<p>최근 다시 부상하고 있는 Symbolic AI 생태계, 특히 전문가 시스템이나 룰 베이스, 대규모 지식 그래프 상에서 추론을 수행해야 할 때 기술적 대안으로 Datalog가 자주 언급됩니다. 그 이유는 아주 직관적(Intuitive)입니다.</p>

<p>첫째, 대규모 데이터 처리 구조에 매우 적합합니다. 현대 AI가 다루는 지식은 수천, 수만 건 단위에 그치지 않습니다. 시스템 로그, 소셜 데이터, 웹 스크랩 데이터 등 수백억 건의 데이터를 다룹니다. Prolog는 이러한 방대한 데이터를 연산하기엔 구조적인 성능 한계가 명확합니다. 반면 Datalog는 태생부터 대용량 데이터베이스와의 결합을 목적으로 설계되었습니다. 기본 사실들을 기반으로 차곡차곡 상위 논리를 유추해 내는 상향식 평가를 사용하여 대규모 데이터 셋에서의 복잡한 재귀 조인(Recursive Join)을 훨씬 더 효율적으로 처리합니다.</p>

<p>둘째, 구현자가 철저하게 ‘무엇(What)’을 도출할지에만 집중할 수 있게 해줍니다. 규칙이 수천 개로 늘어나면 사람이 이를 일일이 제어하며 무한 루프를 피하기란 불가능에 가깝습니다. Datalog 환경에서는 탐색의 순서나 실행 방법과 같은 ‘어떻게(How)’에 대한 부분을 엔진 내부의 질의 플래너(Query Planner)가 독자적으로 최적화합니다. 구현자는 데이터 간의 논리적 규칙만 선언형 텍스트로 명시해 두면 충분합니다.</p>

<h2 id="마무리">마무리</h2>

<p>Prolog가 인간의 완벽한 논리 추론 프로세스를 코드로 이식하고자 했던 이상적인 도구였다면, Datalog는 현실의 방대한 데이터 속에서 시스템 다운 없이 빠르고 정확하게 지식을 추출해 내기 위해 다양한 안전장치를 더한 실용적인 도구라 할 수 있습니다.</p>

<p>딥러닝이나 거대 언어 모델과 같은 통계적 방법론이 정답의 ‘확률’을 높이는 데 특화되어 있다면, 의료, 금융, 복잡한 접근 제어 모델링 등 100% 확실한 논리와 설명 가능성(Explainability)이 요구되는 영역에서는 제한적인 유연함을 대가로 완벽한 안정성을 취한 Datalog가 자신만의 확고한 입지를 구축하고 있습니다.</p>]]></content><author><name>Justin Kim</name></author><category term="essay" /><category term="datalog" /><category term="Datalog" /><category term="Symbolic AI" /><category term="Logic Programming" /><category term="AI" /><summary type="html"><![CDATA[Datalog와 Prolog는 논리 프로그래밍(Logic Programming)이라는 동일한 뿌리에서 파생되어 시각적으로 매우 유사한 문법을 공유하고 있습니다. 때문에 Datalog를 처음 접하게 되면 이미 익숙하고 범용적인 Prolog와의 실질적인 차이를 체감하기 쉽지 않습니다.]]></summary></entry><entry><title type="html">캐시 히트율을 위한 Radix Sort 도입 기대</title><link href="https://groou.com/research/datalog/2026/03/25/radix-sort-in-datalog/" rel="alternate" type="text/html" title="캐시 히트율을 위한 Radix Sort 도입 기대" /><published>2026-03-25T00:00:00+09:00</published><updated>2026-03-25T00:00:00+09:00</updated><id>https://groou.com/research/datalog/2026/03/25/radix-sort-in-datalog</id><content type="html" xml:base="https://groou.com/research/datalog/2026/03/25/radix-sort-in-datalog/"><![CDATA[<p>Datalog 엔진을 구현하다 보면 성능 병목을 해결하기 위해 다양한 최적화 기법을 도입하게 됩니다. 특히 다량의 데이터를 처리해야 하는 데이터베이스나 논리 프로그래밍 엔진에서는 어떤 정렬 알고리즘을 선택하느냐에 따라 전체적인 성능 차이가 극명하게 나타날 수 있습니다.</p>

<p>이번 글에서는 기수 정렬(Radix Sort)의 기본 개념에 대해 알아보고, 왜 Datalog 엔진에서 정렬 알고리즘이 필수적인지, 그리고 기존의 <code class="language-plaintext highlighter-rouge">qsort</code>에서 Radix Sort로 전환하여 어떤 성능적 이점을 기대하고 있는지 정리해 보았습니다.</p>

<h2 id="기수-정렬-radix-sort이란">기수 정렬 (Radix Sort)이란?</h2>

<p>기수 정렬(Radix Sort)은 요소를 비교하지 않고 분산(Distribution)하여 정렬하는 알고리즘입니다. 일반적인 비교 기반 정렬 알고리즘(예: Quick Sort, Merge Sort)이 최소 $O(N \log N)$의 시간 복잡도를 가지는 반면, 기수 정렬은 정렬할 키의 크기가 제한적일 때 $O(dN)$ (이때 $d$는 데이터의 최대 자릿수)의 선형 시간에 가까운 속도로 정렬을 완료할 수 있습니다.</p>

<h3 id="작동-방식">작동 방식</h3>

<p>기수 정렬은 데이터의 가장 낮은 자릿수(LSD, Least Significant Digit)부터 가장 높은 자릿수(MSD, Most Significant Digit)까지, 혹은 그 반대로 각 자릿수를 기준으로 정렬을 반복합니다.</p>

<p><img src="/images/2026-03-25/radix_sort_diagram.svg" alt="Radix Sort Process" style="width: 100%; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); margin: 25px 0;" /></p>

<ol>
  <li><strong>초기화</strong>: 0부터 9까지(혹은 진법에 따른 기수만큼)의 버킷(Queue 등)을 준비합니다.</li>
  <li><strong>분배</strong>: 정렬할 데이터의 일의 자리를 기준으로 각 버킷에 데이터를 넣습니다.</li>
  <li><strong>병합</strong>: 0번 버킷부터 순서대로 데이터를 다시 가져옵니다.</li>
  <li><strong>반복</strong>: 십의 자리, 백의 자리 등 가장 큰 자릿수까지 위 과정을 반복합니다.</li>
</ol>

<p>이렇게 요소 간의 직접적인 크기 비교 없이 자릿수 기반의 버킷 배치를 통해 정렬을 수행하기 때문에, 정수나 문자열 같은 일정한 형식의 키를 가진 데이터를 정렬할 때 매우 강력한 성능을 발휘합니다.</p>

<h2 id="datalog-엔진-구현에서-정렬이-필요한-이유">Datalog 엔진 구현에서 정렬이 필요한 이유</h2>

<p>Datalog는 선언형 논리 프로그래밍 언어로, 사실(Fact)과 규칙(Rule)을 기반으로 새로운 사실을 추론해 냅니다. 엔진 내부에서는 이러한 추론 과정이 주로 관계형 대수(Relational Algebra) 연산으로 변환되어 실행됩니다.</p>

<p>Datalog 엔진에서 데이터를 정렬해야 하는 주된 이유는 다음과 같습니다.</p>

<ol>
  <li>
    <p><strong>중복 제거 (Deduplication)</strong>
Datalog의 평가 과정(특히 Bottom-up 방식의 Semi-naïve Evaluation)에서는 매 반복마다 새롭게 생성된 데이터(Fact) 중에서 기존의 데이터와 겹치는 중복을 제거해야 합니다. 데이터가 정렬되어 있다면 선형 탐색 한 번만으로 중복된 튜플을 쉽게 걸러낼 수 있습니다.</p>
  </li>
  <li>
    <p><strong>조인 연산 (Join)</strong>
보통 하나의 Rule은 여러 개의 Predicate(조건)가 엮여 있으며, 이를 모두 만족하는 해를 찾기 위해 조인 연산이 연속해서 일어납니다. 해시 조인(Hash Join)을 사용할 수도 있지만, 메모리 사용량을 예측 가능하게 유지하면서 높은 성능을 내기 위해 정렬 병합 조인(Sort-Merge Join)이 자주 활용됩니다. 데이터를 조인 키 기준으로 미리 정렬해두면 조인 처리가 매우 효율적입니다.</p>
  </li>
</ol>

<p>결과적으로 Datalog 엔진에서 데이터를 릴레이션 형태로 보관하고 연산할 때, 빠른 정렬은 엔진의 핵심 성능을 좌우하는 중요한 요소입니다.</p>

<h2 id="qsort에서-radix-sort로의-전환-캐시-히트율-향상을-향한-기대">QSort에서 Radix Sort로의 전환: 캐시 히트율 향상을 향한 기대</h2>

<p>초기 <a href="https://github.com/justinjoy/wirelog">Datalog 엔진(wirelog)</a> 구현에서는 라이브러리에서 기본 제공하는 <strong>퀵 정렬(<code class="language-plaintext highlighter-rouge">qsort</code> 또는 <code class="language-plaintext highlighter-rouge">std::sort</code>)</strong>을 사용해왔습니다. Quick Sort는 평균적으로 $O(N \log N)$의 우수한 시간 복잡도를 가지며 범용적으로 가장 널리 쓰이는 정렬 방식입니다.</p>

<p>하지만 처리해야 할 일의 양과 사실(Fact) 데이터의 개수가 기하급수적으로 늘어나면서 Quick Sort의 한계가 나타나기 시작했습니다. 가장 큰 문제는 바로 <strong>캐시 히트율(Cache Hit Rate)</strong> 이었습니다.</p>

<p>Quick Sort는 피벗(Pivot)을 기준으로 배열의 양 끝에서부터 스왑(Swap) 연산을 수행하면서 재귀적으로 범위를 좁혀 들어갑니다. 데이터가 커져서 한 번에 CPU 캐시에 담을 수 있는 크기를 넘어서게 되면 메모리 접근 패턴은 무작위(Random)에 가까워집니다. 이로 인해 심각한 <strong>Cache Miss</strong>가 발생하기 시작하고, 결국 CPU 연산 시간보다 메모리에서 데이터를 퍼오는 데 걸리는 대기 시간이 정렬 연산의 병목이 됩니다.</p>

<p>반면, <strong>Radix Sort</strong>는 데이터를 버킷에 순차적으로 분배하고 다시 순차적으로 거두어들이는 패턴을 가집니다. 특히 데이터 엔진에서 주로 다루는 고정 크기의 정수형 키를 정렬할 때, 메모리에 대해서 <strong>연속적이고 예측 가능한 접근(Sequential Memory Access)</strong> 을 가능하게 합니다.</p>

<p>이러한 특성 덕분에 하드웨어 프리패처(Hardware Prefetcher) 동작에 매우 유리하며 다음 메모리 블록을 미리 캐시에 올려둘 수 있어, 결과적으로 <strong>캐시 히트율이 획기적으로 상승</strong>합니다.</p>

<h2 id="마무리">마무리</h2>

<p>현재 Datalog 엔진의 핵심 병목 중 하나였던 대용량 튜플의 정렬 단계를 기존의 <code class="language-plaintext highlighter-rouge">qsort</code>에서 Radix Sort 기반으로 교체하는 작업을 진행하고 있습니다(관련 이슈: <a href="https://github.com/justinjoy/wirelog/issues/308">justinjoy/wirelog#308</a>). 연산 복잡도 자체의 감소($O(N \log N) \rightarrow O(dN)$)도 의미가 있지만, 무엇보다 메모리 계층 구조에 친화적인(Cache-friendly) 특성 덕분에 실제 하드웨어 최우선 과제인 캐시 히트율을 대폭 향상시켜 훨씬 더 빠른 쿼리 응답 속도를 얻을 수 있을 것으로 기대하고 있습니다. 향후 최적화가 완료되면 벤치마킹을 통해 얼마나 속도가 개선되었는지 다시 정리해 보도록 하겠습니다.</p>]]></content><author><name>Justin Kim</name></author><category term="research" /><category term="datalog" /><category term="Datalog" /><category term="Algorithm" /><category term="Optimization" /><summary type="html"><![CDATA[Datalog 엔진을 구현하다 보면 성능 병목을 해결하기 위해 다양한 최적화 기법을 도입하게 됩니다. 특히 다량의 데이터를 처리해야 하는 데이터베이스나 논리 프로그래밍 엔진에서는 어떤 정렬 알고리즘을 선택하느냐에 따라 전체적인 성능 차이가 극명하게 나타날 수 있습니다.]]></summary></entry><entry><title type="html">시맨틱 태깅, 단순한 키워드를 넘어 지식의 연결로</title><link href="https://groou.com/essay/knowledge-graph/2026/03/22/semantic-tagging/" rel="alternate" type="text/html" title="시맨틱 태깅, 단순한 키워드를 넘어 지식의 연결로" /><published>2026-03-22T00:00:00+09:00</published><updated>2026-03-22T00:00:00+09:00</updated><id>https://groou.com/essay/knowledge-graph/2026/03/22/semantic-tagging</id><content type="html" xml:base="https://groou.com/essay/knowledge-graph/2026/03/22/semantic-tagging/"><![CDATA[<p>기록이 쌓일수록 고민도 깊어집니다. 우리는 매일 수많은 노트를 작성하고, 나중에 찾기 쉽게 ‘태그’를 답니다. <code class="language-plaintext highlighter-rouge">#datalog</code>, <code class="language-plaintext highlighter-rouge">#pkm</code>, <code class="language-plaintext highlighter-rouge">#ai</code> 같은 키워드들이 그 예입니다. 하지만 시간이 흘러 노트가 수백, 수천 개가 되었을 때, 이 태그들이 정말 우리에게 의미 있는 ‘지식’으로 기능하고 있는지는 돌이켜볼 문제입니다.</p>

<h2 id="키워드-태깅의-한계-단순한-문자열의-나열">키워드 태깅의 한계: 단순한 문자열의 나열</h2>

<p>우리가 흔히 쓰는 태깅은 텍스트를 있는 그대로 대조하는 <strong>문자열(String) 매칭</strong>에 불과합니다. 내가 ‘Datalog’라는 태그를 달았다고 해서 기계가 그것을 ‘선언형 논리 프로그래밍 언어’나 ‘Prolog의 일종’으로 이해하지는 않습니다. 그저 ‘D-a-t-a-l-o-g’라는 7개의 알파벳 조합으로 무미건조하게 받아들일 뿐이죠.</p>

<p>문자가 의미를 제대로 담지 못할 때 여러 답답한 상황이 벌어집니다. 당장 ‘Apple’이라는 태그만 봐도 이것이 과실을 뜻하는지 거대 IT 기업을 의미하는지 문맥 없이는 알 길이 없습니다. 게다가 ‘Datalog’와 ‘Logic Programming’이 서로 어떤 포함 관계인지도 파악하지 못하기 때문에, “논리 프로그래밍에 관한 노트를 다 찾아줘”라고 검색하면 정작 ‘Datalog’ 태그가 달린 핵심 노트들은 모조리 누락되고 맙니다. 기계 입장에서는 두 문자열이 완전히 배타적이기 때문입니다.</p>

<h2 id="시맨틱-태깅이란">시맨틱 태깅이란?</h2>

<p>시맨틱 태깅(Semantic Tagging)은 텍스트를 단순한 문자열이 아닌 <strong>개념(Concept)과 개체(Entity)</strong>로 연결하는 과정입니다. 태그를 다는 행위가 단순히 ‘이름표’를 붙이는 것이 아니라, 전 세계적으로 정의된 지식 체계(Ontology)나 개인의 지식 그래프에 해당 노드를 <strong>‘위치’시키는 작업</strong>이 됩니다.</p>

<p>예를 들어, “Datalog”라는 단어에 시맨틱 태깅을 한다는 것은 다음과 같은 정보를 포함하는 것을 의미합니다.</p>
<ul>
  <li><strong>URI</strong>: <code class="language-plaintext highlighter-rouge">https://www.wikidata.org/wiki/Q1191141</code> (Wikidata의 Datalog 항목)</li>
  <li><strong>Type</strong>: Programming Language</li>
  <li><strong>Subclass of</strong>: Declarative Programming, Logic Programming</li>
</ul>

<p>이렇게 태깅된 데이터는 더 이상 고립된 섬이 아닙니다. 이미 정의된 거대한 지식의 네트워크(Linked Data)와 연결됩니다.</p>

<h2 id="지식-그래프의-기초-태그가-엣지가-되는-순간">지식 그래프의 기초: 태그가 엣지가 되는 순간</h2>

<p>시맨틱 태깅을 거치면, 우리의 노트 테이킹은 조금 다르게 동작하게 됩니다. 태그는 단순한 분류 도구가 아니라, 지식 그래프의 <strong>엣지(Edge)</strong>가 됩니다.</p>

<p><img src="/images/2026-03-22/semantic-tagging-graph.svg" alt="시맨틱 태깅 지식 그래프" /></p>

<p>이 연결망이 구축되면, 우리는 “Datalog”라고 직접 태그하지 않은 노트라 할지라도, 그것이 논리 프로그래밍과 관련되어 있다는 사실을 시스템을 통해 찾아낼 수 있습니다. 태깅이 <strong>분류(Classification)에서 연결(Connection)</strong>로 진화하는 것입니다.</p>

<h2 id="흩어진-메모가-지식의-그물망이-될-때">흩어진 메모가 지식의 그물망이 될 때</h2>

<p>시맨틱 태깅이 적용된 환경에서는 검색의 차원이 달라집니다. “사과”를 검색하더라도 이것이 과일인지 기업인지 명확히 구분하여, 작성자의 본래 의도(Intent)에 부합하는 결과를 얻을 수 있습니다. 단순한 키워드 매칭이 아닌, 개념 기반의 탐색이 이루어지기 때문입니다.</p>

<p>이러한 맥락의 연결은 자연스럽게 자동화된 추론의 기반이 됩니다. 이 블로그에서 자주 다루는 Datalog나 RDF 같은 도구들을 사용할 수 밖에 없는 부분입니다. 데이터가 정보의 그물망 위에 시맨틱하게 엮여 있다면, 단방향 검색을 넘어 “20세기 후반에 등장한 논리 프로그래밍 언어 중, 현재 내가 학습 중인 것들은 무엇인가”와 같은 복합적인 질의를 시스템에 넘길 수 있게 됩니다.</p>

<p>Obsidian이나 Logseq처럼 개인 지식 관리(PKM) 도구를 깊게 활용하는 사람들에게도 이는 중요한 시사점을 던집니다. 매번 태그를 꼼꼼히 관리하고 노트를 수동으로 ‘분류’하는 수고를 크게 덜어주기 때문이죠. 새로운 메모를 올바른 개념 공간에 연결해두기만 하면, 시간이 흘러 쌓인 지식의 연결망 속에서 의미 있는 통찰이 자연스럽게 출현(Emergence)하게 됩니다.</p>

<h2 id="llm이-낮춘-지식-연결의-문턱">LLM이 낮춘 지식 연결의 문턱</h2>

<p>그렇다면 시맨틱 태깅은 어떻게 실천할 수 있을까요? 예전에는 사용자가 직접 복잡한 온톨로지(Ontology) 구조를 학습하고, 지루한 수작업으로 일일이 메타데이터를 입력해야만 했습니다. 개념의 정합성을 맞추는 일 자체가 거대한 노동이었죠.</p>

<p>이 견고했던 장벽은 거대 언어 모델(LLM)의 등장으로 빠르게 허물어지고 있습니다. LLM은 다듬어지지 않은 일상적인 글에서 핵심 개념을 짚어내고, 이를 적절한 지식 베이스의 URI와 매핑하는 작업에 놀라운 적성을 보입니다. 글쓴이가 무심코 메모를 남기기만 해도, 모델이 문맥을 소화하여 백그라운드에서 지식의 엣지를 이어붙이는 식입니다. 시맨틱 그래프를 엮어내는 비용이 극적으로 낮아진 셈입니다.</p>

<h2 id="마치며-정리를-넘어-추론을-향해">마치며: ‘정리’를 넘어 ‘추론’을 향해</h2>

<p>결국 시맨틱 태깅은 ‘내 기록을 어떻게 다룰 것인가’에 대한 관점의 전환입니다. 정성껏 쓴 메모들을 그저 서랍장에 쌓아두는 데 만족할 것인지, 서로 관계를 맺고 새로운 결론을 엮어내는 동적인 지식 기반으로 키울 것인지의 선택이기도 합니다.</p>

<p>기계적인 키워드 분류는 언젠가 분명한 한계에 부딪히게 마련입니다. 파편화된 단어들의 늪에서 헤매지 않으려면, 이제는 단순 문자열의 나열이 아닌 ‘연결 가능한 의미’를 기록하는 연습을 시작해봐야 할 것입니다.</p>

<hr />

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/essay/datalog/2026/03/15/datalog-everyday-use/">Datalog, 일상의 도구가 될 수 있을까</a></li>
  <li><a href="/essay/ai/2026/02/01/introducing-datalog/">Datalog 소개</a></li>
</ul>]]></content><author><name>Justin Kim</name></author><category term="essay" /><category term="knowledge-graph" /><category term="Knowledge Graph" /><category term="Linked Data" /><category term="Ontology" /><summary type="html"><![CDATA[기록이 쌓일수록 고민도 깊어집니다. 우리는 매일 수많은 노트를 작성하고, 나중에 찾기 쉽게 ‘태그’를 답니다. #datalog, #pkm, #ai 같은 키워드들이 그 예입니다. 하지만 시간이 흘러 노트가 수백, 수천 개가 되었을 때, 이 태그들이 정말 우리에게 의미 있는 ‘지식’으로 기능하고 있는지는 돌이켜볼 문제입니다.]]></summary></entry><entry><title type="html">Datalog, 일상의 도구가 될 수 있을까</title><link href="https://groou.com/essay/datalog/2026/03/15/datalog-everyday-use/" rel="alternate" type="text/html" title="Datalog, 일상의 도구가 될 수 있을까" /><published>2026-03-15T00:00:00+09:00</published><updated>2026-03-15T00:00:00+09:00</updated><id>https://groou.com/essay/datalog/2026/03/15/datalog-everyday-use</id><content type="html" xml:base="https://groou.com/essay/datalog/2026/03/15/datalog-everyday-use/"><![CDATA[<p>Datalog에 대한 글을 몇 편 써오면서, 한 가지 고민이 계속 머릿속을 맴돕니다. <strong>“Datalog는 정말 일상적으로 쓸 수 있는 도구인가?”</strong>라는 질문입니다.</p>

<h2 id="특수한-도메인에서는-빛나지만">특수한 도메인에서는 빛나지만</h2>

<p>Datalog의 활용 사례를 찾으면 대부분 ‘특수한 도메인’에 집중되어 있습니다.</p>

<ul>
  <li><strong>정적 프로그램 분석</strong>: 포인터 분석이나 데이터 흐름 분석에서 Datalog는 이미 검증된 도구입니다. Soufflé 같은 고성능 Datalog 엔진은 아예 이 용도에 최적화되어 있습니다.</li>
  <li><strong>보안 정책 검증</strong>: 접근 제어 규칙을 사실과 규칙으로 표현하고, 의도하지 않은 권한 경로가 존재하는지 추론하는 데 탁월합니다.</li>
  <li><strong>네트워크 구성 분석</strong>: 라우팅 테이블의 도달가능성을 재귀적으로 계산하는 것은 Datalog의 전형적인 적용 사례입니다.</li>
  <li><strong>온톨로지 추론</strong>: 지식 그래프 위에서 RDFS/OWL 규칙을 적용하여 암묵적 관계를 도출하는 데 사용됩니다.</li>
</ul>

<p>이런 예제들은 만들기 어렵지 않습니다. 도메인 자체가 “사실과 규칙으로부터 새로운 사실을 도출한다”는 Datalog의 본질과 정확히 맞아떨어지기 때문입니다. 문제는 이런 사례들을 보여주면 돌아오는 반응이 늘 비슷하다는 점입니다.</p>

<blockquote>
  <p>“저는 컴파일러도 안 만들고, 보안 정책도 안 다루는데, 이걸 언제 쓰나요?”</p>
</blockquote>

<p>이 질문에 선뜻 대답하기가 어렵습니다.</p>

<h2 id="일상적-유즈케이스가-떠오르지-않는-이유">일상적 유즈케이스가 떠오르지 않는 이유</h2>

<p>곰곰이 생각해보면, Datalog의 일상적 유즈케이스를 만들기 어려운 데는 구조적인 이유가 있습니다.</p>

<p><strong>첫째, 일상의 데이터 문제는 대부분 SQL로 충분합니다.</strong> 테이블에서 조건에 맞는 행을 필터링하고, 집계하고, 조인하는 것—이것이 우리가 매일 하는 ‘데이터 작업’의 대부분입니다. SQL은 이 영역에서 반세기 넘게 검증된 도구이고, 생태계도 압도적입니다.</p>

<p><strong>둘째, 재귀적 추론이 필요한 일상적 상황이 드뭅니다.</strong> Datalog가 SQL에 대해 가지는 가장 명확한 이점은 재귀(recursion)입니다. “조상의 조상”이나 “친구의 친구”를 재귀적으로 탐색하는 것은 Datalog의 강점이지만, 일상적인 데이터 작업에서 이런 패턴이 얼마나 자주 등장할까요? SQL의 <code class="language-plaintext highlighter-rouge">WITH RECURSIVE</code>가 어색하기는 하지만, 그마저도 필요한 상황 자체가 흔치 않습니다.</p>

<p><strong>셋째, 도구의 접근성 문제가 있습니다.</strong> Python에서 Pandas 한 줄이면 해결되는 일에 Datalog 엔진을 별도로 설치하고 사실을 정의하고 규칙을 작성하는 것은 상당한 인지적 전환 비용을 수반합니다. 도구가 주는 가치가 전환 비용을 넘어서지 못하면 채택은 일어나지 않습니다.</p>

<h2 id="그럼에도-가능성이-보이는-영역들">그럼에도 가능성이 보이는 영역들</h2>

<p>그래도 Datalog가 일상의 도구로 가치를 가질 수 있는 지점이 전혀 없는 것은 아닙니다. 최근에 떠올린 몇 가지 방향을 정리해봅니다.</p>

<h3 id="1-개인-지식-관리-personal-knowledge-management">1. 개인 지식 관리 (Personal Knowledge Management)</h3>

<p>Obsidian이나 Logseq 같은 도구를 쓰면서 노트 사이의 관계를 <code class="language-plaintext highlighter-rouge">[[링크]]</code>로 연결하는 사람이 늘고 있습니다. 이 링크 그래프는 본질적으로 사실(Facts)의 집합입니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">references</span><span class="p">(</span><span class="s2">"Datalog 소개"</span><span class="p">,</span> <span class="s2">"논리 프로그래밍"</span><span class="p">).</span>
<span class="ss">references</span><span class="p">(</span><span class="s2">"논리 프로그래밍"</span><span class="p">,</span> <span class="s2">"Prolog"</span><span class="p">).</span>
<span class="ss">tagged</span><span class="p">(</span><span class="s2">"Datalog 소개"</span><span class="p">,</span> <span class="s2">"datalog"</span><span class="p">).</span>
<span class="ss">tagged</span><span class="p">(</span><span class="s2">"논리 프로그래밍"</span><span class="p">,</span> <span class="s2">"logic"</span><span class="p">).</span>

<span class="c1">% 간접적으로 관련된 노트 찾기</span>
<span class="ss">related</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">references</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
<span class="ss">related</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">references</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Z</span><span class="p">),</span> <span class="ss">related</span><span class="p">(</span><span class="nv">Z</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>

<span class="c1">% 같은 태그를 공유하는 노트 클러스터</span>
<span class="ss">cluster</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">tagged</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">T</span><span class="p">),</span> <span class="ss">tagged</span><span class="p">(</span><span class="nv">Y</span><span class="p">,</span> <span class="nv">T</span><span class="p">),</span> <span class="nv">X</span> <span class="err">\</span><span class="o">=</span> <span class="nv">Y</span><span class="p">.</span>
</code></pre></div></div>

<p>“이 노트와 간접적으로 연결된 노트가 뭐지?”, “내가 놓치고 있는 연결 고리가 있을까?”—이런 질문에 Datalog는 SQL보다 자연스러운 표현력을 제공합니다. 특히 노트가 수백 개를 넘어가면, 단순한 링크 그래프를 넘어 <strong>추론 기반의 지식 탐색</strong>이 의미를 가지기 시작합니다.</p>

<h3 id="2-의존성과-영향-분석">2. 의존성과 영향 분석</h3>

<p>소프트웨어 프로젝트의 의존성 관리는 개발자의 일상입니다. 패키지 A가 B에 의존하고, B가 C에 의존할 때, C에 보안 취약점이 발견되면 영향받는 모든 패키지를 찾아야 합니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">depends_on</span><span class="p">(</span><span class="s2">"my-app"</span><span class="p">,</span> <span class="s2">"web-framework"</span><span class="p">).</span>
<span class="ss">depends_on</span><span class="p">(</span><span class="s2">"web-framework"</span><span class="p">,</span> <span class="s2">"http-lib"</span><span class="p">).</span>
<span class="ss">depends_on</span><span class="p">(</span><span class="s2">"http-lib"</span><span class="p">,</span> <span class="s2">"openssl"</span><span class="p">).</span>
<span class="ss">vulnerable</span><span class="p">(</span><span class="s2">"openssl"</span><span class="p">).</span>

<span class="c1">% 전이적 의존성</span>
<span class="ss">transitive_dep</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">depends_on</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
<span class="ss">transitive_dep</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">depends_on</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Z</span><span class="p">),</span> <span class="ss">transitive_dep</span><span class="p">(</span><span class="nv">Z</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>

<span class="c1">% 취약한 패키지에 간접적으로 의존하는 모든 패키지</span>
<span class="ss">at_risk</span><span class="p">(</span><span class="nv">X</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">transitive_dep</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">),</span> <span class="ss">vulnerable</span><span class="p">(</span><span class="nv">Y</span><span class="p">).</span>
</code></pre></div></div>

<p>물론 <code class="language-plaintext highlighter-rouge">npm audit</code>이나 <code class="language-plaintext highlighter-rouge">pip-audit</code> 같은 전용 도구가 이미 존재합니다. 하지만 Datalog의 장점은 <strong>규칙을 자유롭게 조합할 수 있다</strong>는 점에 있습니다. “이 모듈을 수정하면 영향받는 테스트는?”, “이 설정을 변경하면 어떤 서비스가 재시작되어야 하는가?”—이런 질문들은 모두 전이적 관계(transitive relation) 위에서의 도달가능성(reachability) 문제이고, Datalog는 이를 선언적으로 표현하는 데 최적화된 언어입니다.</p>

<h3 id="3-접근-권한의-선언적-관리">3. 접근 권한의 선언적 관리</h3>

<p>RBAC(Role-Based Access Control)은 어디에나 있습니다. 사내 시스템, 클라우드 인프라, 심지어 Google Drive의 공유 설정까지. 문제는 권한이 중첩되고 상속될 때 “이 사람이 이 리소스에 접근할 수 있는가?”라는 질문에 답하기가 점점 어려워진다는 것입니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">role</span><span class="p">(</span><span class="s2">"김과장"</span><span class="p">,</span> <span class="s2">"팀장"</span><span class="p">).</span>
<span class="ss">role</span><span class="p">(</span><span class="s2">"이대리"</span><span class="p">,</span> <span class="s2">"개발자"</span><span class="p">).</span>
<span class="ss">inherits</span><span class="p">(</span><span class="s2">"팀장"</span><span class="p">,</span> <span class="s2">"개발자"</span><span class="p">).</span>
<span class="ss">permission</span><span class="p">(</span><span class="s2">"개발자"</span><span class="p">,</span> <span class="s2">"코드저장소"</span><span class="p">,</span> <span class="s2">"read"</span><span class="p">).</span>
<span class="ss">permission</span><span class="p">(</span><span class="s2">"팀장"</span><span class="p">,</span> <span class="s2">"코드저장소"</span><span class="p">,</span> <span class="s2">"write"</span><span class="p">).</span>

<span class="c1">% 역할 상속</span>
<span class="ss">has_role</span><span class="p">(</span><span class="nv">User</span><span class="p">,</span> <span class="nv">Role</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">role</span><span class="p">(</span><span class="nv">User</span><span class="p">,</span> <span class="nv">Role</span><span class="p">).</span>
<span class="ss">has_role</span><span class="p">(</span><span class="nv">User</span><span class="p">,</span> <span class="nv">Role</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">role</span><span class="p">(</span><span class="nv">User</span><span class="p">,</span> <span class="nv">R</span><span class="p">),</span> <span class="ss">inherits</span><span class="p">(</span><span class="nv">R</span><span class="p">,</span> <span class="nv">Role</span><span class="p">).</span>

<span class="c1">% 최종 권한 계산</span>
<span class="ss">can_access</span><span class="p">(</span><span class="nv">User</span><span class="p">,</span> <span class="nv">Resource</span><span class="p">,</span> <span class="nv">Action</span><span class="p">)</span> <span class="p">:-</span>
    <span class="ss">has_role</span><span class="p">(</span><span class="nv">User</span><span class="p">,</span> <span class="nv">Role</span><span class="p">),</span>
    <span class="ss">permission</span><span class="p">(</span><span class="nv">Role</span><span class="p">,</span> <span class="nv">Resource</span><span class="p">,</span> <span class="nv">Action</span><span class="p">).</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">can_access("김과장", "코드저장소", "read")</code>가 참인지를 확인하는 것은 단순한 조회가 아니라 <strong>추론</strong>입니다. 팀장 역할이 개발자 역할을 상속하고, 개발자에게 read 권한이 있으므로 김과장도 read 권한을 가집니다. 역할 계층이 깊어지고 조건이 복잡해질수록, 이런 추론을 SQL의 다중 조인으로 표현하는 것은 점점 고통스러워집니다. 실제로 Google의 Zanzibar나 Oso 같은 권한 관리 시스템이 내부적으로 Datalog 혹은 그와 유사한 추론 엔진을 채택한 것은 우연이 아닙니다.</p>

<h3 id="4-설정과-규칙의-검증">4. 설정과 규칙의 검증</h3>

<p>인프라 설정(Infrastructure as Code)이 복잡해지면, 설정 간의 일관성을 검증하는 것이 중요해집니다. “프로덕션 DB는 반드시 암호화가 활성화되어야 한다”, “외부에 노출된 서비스는 인증 미들웨어를 거쳐야 한다”—이런 정책을 Datalog 규칙으로 표현하고, 현재 설정(Facts)에 대해 위반 여부를 검증할 수 있습니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">service</span><span class="p">(</span><span class="s2">"api-gateway"</span><span class="p">,</span> <span class="s2">"external"</span><span class="p">).</span>
<span class="ss">service</span><span class="p">(</span><span class="s2">"internal-api"</span><span class="p">,</span> <span class="s2">"internal"</span><span class="p">).</span>
<span class="ss">has_auth</span><span class="p">(</span><span class="s2">"api-gateway"</span><span class="p">).</span>

<span class="c1">% 정책: 외부 서비스는 반드시 인증이 있어야 한다</span>
<span class="ss">violation</span><span class="p">(</span><span class="nv">S</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">service</span><span class="p">(</span><span class="nv">S</span><span class="p">,</span> <span class="s2">"external"</span><span class="p">),</span> <span class="err">\</span><span class="o">+</span> <span class="ss">has_auth</span><span class="p">(</span><span class="nv">S</span><span class="p">).</span>
</code></pre></div></div>

<p>OPA(Open Policy Agent)의 Rego 언어가 이미 이 영역에서 자리 잡고 있지만, Rego의 설계 자체가 Datalog의 영향을 강하게 받았습니다. 이는 <strong>“정책은 사실과 규칙의 문제”</strong>라는 Datalog의 세계관이 이 영역과 자연스럽게 맞아떨어진다는 것을 방증합니다.</p>

<h2 id="datalog가-일상의-도구가-되려면">Datalog가 일상의 도구가 되려면</h2>

<p>이런 가능성에도 불구하고, Datalog가 일상의 도구로 자리 잡기 위해서는 여전히 넘어야 할 벽이 있습니다.</p>

<p><strong>생태계와 통합의 문제입니다.</strong> SQL이 강력한 이유는 언어 자체의 표현력보다 PostgreSQL, MySQL 같은 성숙한 DBMS 생태계, 수십 년간 축적된 라이브러리, 그리고 모든 프로그래밍 언어에서의 네이티브 지원 때문입니다. Datalog에도 Soufflé, Differential Datalog, Naga 같은 구현체가 있지만, “pip install 하고 바로 쓴다”는 수준의 접근성에는 아직 거리가 있습니다.</p>

<p>결국 Datalog가 일상에 스며들려면, 그 자체로 독립적인 도구가 아니라 <strong>기존 워크플로우에 자연스럽게 녹아드는 형태</strong>가 되어야 할 것 같습니다. Rego가 Kubernetes 생태계에 녹아든 것처럼, 혹은 SQLite가 “서버 없는 임베디드 DB”로 어디에나 쓰이게 된 것처럼. 가벼운 임베디드 Datalog 엔진이 Python이나 JavaScript 한 줄로 호출되고, 일상적인 의존성 분석이나 권한 검증에 자연스럽게 사용되는 모습—그것이 Datalog가 일상의 도구가 되는 경로일 것입니다.</p>

<h2 id="마치며">마치며</h2>

<p>Datalog의 일상적 유즈케이스를 고민하면서 깨달은 것은, 이 질문 자체가 <strong>“이 도구는 누구를 위한 것인가”</strong>에 대한 근본적인 물음이라는 점입니다.</p>

<p>특수한 도메인에서 Datalog의 가치는 이미 증명되어 있습니다. 정적 분석, 보안, 네트워크—이 영역에서 Datalog를 대체할 도구를 찾기는 어렵습니다. 그러나 “일반적인 일상”이라는 맥락에서 Datalog는 아직 자신의 자리를 찾고 있는 중입니다.</p>

<p>어쩌면 Datalog의 진짜 일상적 유즈케이스는, 우리가 아직 “이건 Datalog 문제야”라고 인식하지 못하는 곳에 이미 숨어 있을지도 모릅니다. 의존성 트리를 따라가고, 권한 상속을 추적하고, 설정의 정합성을 검증하는 일—이 모든 것이 사실 “사실과 규칙으로부터 결론을 도출하는” 문제이니까요.</p>

<p>다음에는 이 고민을 코드로 풀어보겠습니다. 실제로 가벼운 Datalog 엔진을 일상적인 문제에 적용해보는 실험을 해볼 생각입니다.</p>

<hr />

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/essay/ai/2026/02/01/introducing-datalog/">SPARQL의 SQL 유사성이 주는 함정, 그리고 Datalog</a></li>
  <li><a href="/essay/ai/2026/02/03/asp-and-datalog/">ASP와 Datalog: 논리 프로그래밍의 두 가지 시선</a></li>
  <li><a href="/essay/ai/2026/02/14/differential-dataflow/">Datalog의 증분 계산</a></li>
</ul>]]></content><author><name>Justin Kim</name></author><category term="essay" /><category term="datalog" /><category term="Datalog" /><category term="Logic Programming" /><category term="Use Cases" /><category term="Knowledge Graph" /><summary type="html"><![CDATA[Datalog에 대한 글을 몇 편 써오면서, 한 가지 고민이 계속 머릿속을 맴돕니다. “Datalog는 정말 일상적으로 쓸 수 있는 도구인가?”라는 질문입니다.]]></summary></entry><entry><title type="html">C/C++에서 SIMD와 NEON 동시에 지원하게 작성하기 및 벤치마크</title><link href="https://groou.com/research/2026/03/12/simd-and-neon-in-c/" rel="alternate" type="text/html" title="C/C++에서 SIMD와 NEON 동시에 지원하게 작성하기 및 벤치마크" /><published>2026-03-12T16:00:00+09:00</published><updated>2026-03-12T16:00:00+09:00</updated><id>https://groou.com/research/2026/03/12/simd-and-neon-in-c</id><content type="html" xml:base="https://groou.com/research/2026/03/12/simd-and-neon-in-c/"><![CDATA[<p>현대 CPU 연산 방식은 데이터를 처리하는 단위에 따라 크게 스칼라(Scalar) 연산과 벡터(Vector) 연산으로 나눌 수 있습니다. 코어 모듈의 성능을 끌어올리기 위해서는 이 차이를 이해하고 적절한 명령어 셋을 활용하는 것이 필수적입니다.</p>

<h3 id="스칼라scalar와-simd-그리고-neon의-차이">스칼라(Scalar)와 SIMD, 그리고 NEON의 차이</h3>

<ul>
  <li><strong>스칼라(Scalar) 연산</strong>: 스칼라는 수학에서 방향성 없이 크기만 가지는 단일 값을 뜻합니다. 컴퓨팅 맥락에서 스칼라 연산은 한 번의 CPU 명령어(Instruction)로 단 한 개의 데이터 연산만 처리하는 아주 기본적이고 전통적인 방식을 의미합니다. 예를 들어 덧셈 명령을 내리면 단일 변수 두 개만 더합니다. 다량의 데이터 배열을 처리할 때는 원소 개수만큼 반복문을 수행해야 하므로 병목이 발생하기 쉽습니다.</li>
  <li><strong>SIMD (Single Instruction Multiple Data)</strong>: SIMD는 단 하나의 명령어(Single Instruction)로 여러 개의 데이터(Multiple Data)를 동시에 병렬로 연산하는 기법을 뜻하는 <strong>범용적인 개념</strong>입니다. 스칼라가 한 번에 단일 값을 처리할 때, SIMD는 큰 크기의 전용 레지스터(예: 128-bit) 내부에 여러 개의 데이터(예: 32-bit float 4개)를 패킹한 뒤, 덧셈 지시 한 번으로 4쌍의 요소를 한 번에 연산해 냅니다.</li>
  <li><strong>NEON 연산</strong>: NEON(Advanced SIMD)은 ARM 아키텍처(Apple M1/M2, 안드로이드 AP 등) 내부에 탑재된 <strong>전용 SIMD 엔진 및 명령어 규격의 이름</strong>입니다. “SIMD”가 병렬 처리 모델을 뜻하는 넓은 의미의 기술 분류라면, “NEON”은 ARM에서 SIMD를 구체적으로 하드웨어 상에 구현해 둔 상표이자 규격명입니다. 만약 x64/x86 진영(Intel, AMD)의 CPU라면 NEON이 아닌 <strong>SSE</strong>나 <strong>AVX</strong>라는 이름의 SIMD 명령어 셋을 사용하게 됩니다.</li>
</ul>

<p>서버 시장은 보통 x64 환경이 지배적이었지만, 최근에는 모바일뿐만 아니라 Mac(Apple Silicon)이나 AWS Graviton 같은 ARM 기반 프로세서의 점유율이 높아지고 있습니다. 따라서 연산 집약적인 코어 모듈을 C/C++로 작성하실 때는, <strong>동일한 동작을 C 코드로 작성하되 Intel/AMD에서는 SSE/AVX가 실행되고 ARM에서는 NEON이 실행되도록 코드를 분기 처리해 주는 것</strong>이 매우 중요해졌습니다.</p>

<p>이번 글에서는 C언어를 이용해 1억 개(100 Million)의 float 배열 두 개의 내적(Dot Product)을 구하는 연산을 수행하면서, <strong>하나의 코드베이스로 x86의 SSE/AVX와 ARM의 NEON을 동시에 지원하도록 작성하는 방법</strong>, 그리고 실제 <strong>Mac M1 (ARM NEON) 환경에서 벤치마크를 돌렸을 때 어느 정도의 속도 차이가 나는지</strong> 비교해보겠습니다.</p>

<h2 id="스칼라-vs-파이프라인-simd-neon-코드-작성법">스칼라 vs 파이프라인 (SIMD, NEON) 코드 작성법</h2>

<p>C/C++에서 아키텍처에 따라 SIMD 명령어를 다르게 적용하려면 전처리기 매크로(<code class="language-plaintext highlighter-rouge">#if defined(...)</code>)를 사용하여 분기하고, 각 아키텍처에 맞는 헤더 파일과 데이터 타입, Intrinsic 함수들을 사용해야 합니다.</p>

<p>아래는 1억 개의 요소를 갖는 내적 연산 예제 코드입니다.</p>

<div class="language-cc highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include &lt;stdio.h&gt;</span>
<span class="cp">#include &lt;stdlib.h&gt;</span>
<span class="cp">#include &lt;time.h&gt;</span>

<span class="c1">// 아키텍처에 따른 헤더 파일 및 매크로 분기</span>
<span class="cp">#if defined(__x86_64__) || defined(_M_X64)</span>
<span class="cp">#include &lt;immintrin.h&gt;</span> <span class="c1">// For SSE/AVX</span>
<span class="cp">#define SIMD_NAME "SSE/AVX"</span>
<span class="cp">#elif defined(__aarch64__) || defined(_M_ARM64)</span>
<span class="cp">#include &lt;arm_neon.h&gt;</span>  <span class="c1">// For NEON</span>
<span class="cp">#define SIMD_NAME "NEON"</span>
<span class="cp">#else</span>
<span class="cp">#define SIMD_NAME "Unknown"</span>
<span class="cp">#endif</span>

<span class="cp">#define ARRAY_SIZE 100000000</span>
<span class="cp">#define NUM_RUNS 10</span>

<span class="kt">float</span> <span class="n">a</span><span class="p">[</span><span class="n">ARRAY_SIZE</span><span class="p">];</span>
<span class="kt">float</span> <span class="n">b</span><span class="p">[</span><span class="n">ARRAY_SIZE</span><span class="p">];</span>

<span class="c1">// 1. 일반적인 스칼라 (Scalar) 연산</span>
<span class="kt">float</span> <span class="nf">dot_product_scalar</span><span class="p">(</span><span class="kr">const</span> <span class="kt">float</span><span class="o">*</span> <span class="n">x</span><span class="p">,</span> <span class="kr">const</span> <span class="kt">float</span><span class="o">*</span> <span class="n">y</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">sum</span> <span class="o">=</span> <span class="mf">0.0f</span><span class="p">;</span>
    <span class="c1">// 컴파일러의 자동 벡터화(Auto-vectorization)를 방지하여 순수 스칼라 성능을 측정</span>
    <span class="err">#</span><span class="n">pragma</span> <span class="n">clang</span> <span class="n">loop</span> <span class="nf">vectorize</span><span class="p">(</span><span class="n">disable</span><span class="p">)</span>
    <span class="err">#</span><span class="n">pragma</span> <span class="n">GCC</span> <span class="n">ivdep</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sum</span> <span class="o">+=</span> <span class="n">x</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">sum</span><span class="p">;</span>
<span class="p">}</span>

<span class="c1">// 2. SIMD (SSE / NEON) 연산</span>
<span class="kt">float</span> <span class="nf">dot_product_simd</span><span class="p">(</span><span class="kr">const</span> <span class="kt">float</span><span class="o">*</span> <span class="n">x</span><span class="p">,</span> <span class="kr">const</span> <span class="kt">float</span><span class="o">*</span> <span class="n">y</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">size</span><span class="p">)</span> <span class="p">{</span>
    <span class="kt">float</span> <span class="n">sum</span> <span class="o">=</span> <span class="mf">0.0f</span><span class="p">;</span>
    <span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="cp">#if defined(__x86_64__) || defined(_M_X64)</span>
    <span class="c1">// [x86/x64 SSE 버전] (한 번에 4개의 float 처리)</span>
    <span class="kt">__m128</span> <span class="n">v_sum</span> <span class="o">=</span> <span class="nf">_mm_setzero_ps</span><span class="p">();</span> <span class="c1">// 4개의 float를 0으로 초기화</span>
    <span class="k">for</span> <span class="p">(;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">3</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">__m128</span> <span class="n">v_x</span> <span class="o">=</span> <span class="nf">_mm_loadu_ps</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
        <span class="kt">__m128</span> <span class="n">v_y</span> <span class="o">=</span> <span class="nf">_mm_loadu_ps</span><span class="p">(</span><span class="o">&amp;</span><span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
        <span class="n">v_sum</span> <span class="o">=</span> <span class="nf">_mm_add_ps</span><span class="p">(</span><span class="n">v_sum</span><span class="p">,</span> <span class="nf">_mm_mul_ps</span><span class="p">(</span><span class="n">v_x</span><span class="p">,</span> <span class="n">v_y</span><span class="p">));</span>
    <span class="p">}</span>
    <span class="c1">// 4개의 부분합을 하나로 합침</span>
    <span class="kt">float</span> <span class="n">temp</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span>
    <span class="nf">_mm_storeu_ps</span><span class="p">(</span><span class="n">temp</span><span class="p">,</span> <span class="n">v_sum</span><span class="p">);</span>
    <span class="n">sum</span> <span class="o">+=</span> <span class="n">temp</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">+</span> <span class="n">temp</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">+</span> <span class="n">temp</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">+</span> <span class="n">temp</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>

<span class="cp">#elif defined(__aarch64__) || defined(_M_ARM64)</span>
    <span class="c1">// [ARM NEON 버전] (한 번에 4개의 float 처리)</span>
    <span class="kt">float32x4_t</span> <span class="n">v_sum</span> <span class="o">=</span> <span class="nf">vdupq_n_f32</span><span class="p">(</span><span class="mf">0.0f</span><span class="p">);</span> <span class="c1">// 4개의 float를 0으로 초기화</span>
    <span class="k">for</span> <span class="p">(;</span> <span class="n">i</span> <span class="o">+</span> <span class="mi">3</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="n">i</span> <span class="o">+=</span> <span class="mi">4</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">float32x4_t</span> <span class="n">v_x</span> <span class="o">=</span> <span class="nf">vld1q_f32</span><span class="p">(</span><span class="o">&amp;</span><span class="n">x</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
        <span class="kt">float32x4_t</span> <span class="n">v_y</span> <span class="o">=</span> <span class="nf">vld1q_f32</span><span class="p">(</span><span class="o">&amp;</span><span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
        <span class="c1">// vmlaq_f32: Multiply-Accumulate (v_sum += v_x * v_y)</span>
        <span class="n">v_sum</span> <span class="o">=</span> <span class="nf">vmlaq_f32</span><span class="p">(</span><span class="n">v_sum</span><span class="p">,</span> <span class="n">v_x</span><span class="p">,</span> <span class="n">v_y</span><span class="p">);</span> 
    <span class="p">}</span>
    <span class="c1">// 부분합을 모두 더해 스칼라로 반환</span>
    <span class="n">sum</span> <span class="o">+=</span> <span class="nf">vaddvq_f32</span><span class="p">(</span><span class="n">v_sum</span><span class="p">);</span>
<span class="cp">#endif</span>

    <span class="c1">// 4의 배수로 떨어지지 않고 남은 요소(Tail 처리)를 스칼라로 계산</span>
    <span class="k">for</span> <span class="p">(;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">size</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">sum</span> <span class="o">+=</span> <span class="n">x</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">*</span> <span class="n">y</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
    <span class="p">}</span>
    
    <span class="k">return</span> <span class="n">sum</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"Initializing arrays...\n"</span><span class="p">);</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">ARRAY_SIZE</span><span class="p">;</span> <span class="o">++</span><span class="n">i</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">a</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">float</span><span class="p">)(</span><span class="n">i</span> <span class="o">%</span> <span class="mi">100</span><span class="p">)</span> <span class="o">/</span> <span class="mf">100.0f</span><span class="p">;</span>
        <span class="n">b</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">float</span><span class="p">)(</span><span class="n">i</span> <span class="o">%</span> <span class="mi">100</span><span class="p">)</span> <span class="o">/</span> <span class="mf">100.0f</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kd">struct</span> <span class="n">timespec</span> <span class="n">start</span><span class="p">,</span> <span class="n">end</span><span class="p">;</span>
    <span class="kt">double</span> <span class="n">time_scalar</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">,</span> <span class="n">time_simd</span> <span class="o">=</span> <span class="mf">0.0</span><span class="p">;</span>
    <span class="kt">float</span> <span class="n">result_scalar</span> <span class="o">=</span> <span class="mf">0.0f</span><span class="p">,</span> <span class="n">result_simd</span> <span class="o">=</span> <span class="mf">0.0f</span><span class="p">;</span>

    <span class="c1">// 스칼라 벤치마크 (10회 평균)</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">run</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">run</span> <span class="o">&lt;</span> <span class="n">NUM_RUNS</span><span class="p">;</span> <span class="o">++</span><span class="n">run</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">clock_gettime</span><span class="p">(</span><span class="n">CLOCK_MONOTONIC</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">start</span><span class="p">);</span>
        <span class="n">result_scalar</span> <span class="o">=</span> <span class="nf">dot_product_scalar</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">ARRAY_SIZE</span><span class="p">);</span>
        <span class="nf">clock_gettime</span><span class="p">(</span><span class="n">CLOCK_MONOTONIC</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">end</span><span class="p">);</span>
        <span class="n">time_scalar</span> <span class="o">+=</span> <span class="p">(</span><span class="n">end</span><span class="p">.</span><span class="n">tv_sec</span> <span class="o">-</span> <span class="n">start</span><span class="p">.</span><span class="n">tv_sec</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-9</span> <span class="o">*</span> <span class="p">(</span><span class="n">end</span><span class="p">.</span><span class="n">tv_nsec</span> <span class="o">-</span> <span class="n">start</span><span class="p">.</span><span class="n">tv_nsec</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">time_scalar</span> <span class="o">/=</span> <span class="n">NUM_RUNS</span><span class="p">;</span>

    <span class="c1">// SIMD 벤치마크 (10회 평균)</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">run</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">run</span> <span class="o">&lt;</span> <span class="n">NUM_RUNS</span><span class="p">;</span> <span class="o">++</span><span class="n">run</span><span class="p">)</span> <span class="p">{</span>
        <span class="nf">clock_gettime</span><span class="p">(</span><span class="n">CLOCK_MONOTONIC</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">start</span><span class="p">);</span>
        <span class="n">result_simd</span> <span class="o">=</span> <span class="nf">dot_product_simd</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">ARRAY_SIZE</span><span class="p">);</span>
        <span class="nf">clock_gettime</span><span class="p">(</span><span class="n">CLOCK_MONOTONIC</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">end</span><span class="p">);</span>
        <span class="n">time_simd</span> <span class="o">+=</span> <span class="p">(</span><span class="n">end</span><span class="p">.</span><span class="n">tv_sec</span> <span class="o">-</span> <span class="n">start</span><span class="p">.</span><span class="n">tv_sec</span><span class="p">)</span> <span class="o">+</span> <span class="mf">1e-9</span> <span class="o">*</span> <span class="p">(</span><span class="n">end</span><span class="p">.</span><span class="n">tv_nsec</span> <span class="o">-</span> <span class="n">start</span><span class="p">.</span><span class="n">tv_nsec</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">time_simd</span> <span class="o">/=</span> <span class="n">NUM_RUNS</span><span class="p">;</span>

    <span class="kt">char</span> <span class="n">simd_res_label</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
    <span class="nf">sprintf</span><span class="p">(</span><span class="n">simd_res_label</span><span class="p">,</span> <span class="s">"%s Result"</span><span class="p">,</span> <span class="n">SIMD_NAME</span><span class="p">);</span>
    <span class="kt">char</span> <span class="n">simd_time_label</span><span class="p">[</span><span class="mi">32</span><span class="p">];</span>
    <span class="nf">sprintf</span><span class="p">(</span><span class="n">simd_time_label</span><span class="p">,</span> <span class="s">"%s Time"</span><span class="p">,</span> <span class="n">SIMD_NAME</span><span class="p">);</span>

    <span class="nf">printf</span><span class="p">(</span><span class="s">"------------------------------------\n"</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"Architecture : %s\n"</span><span class="p">,</span> <span class="n">SIMD_NAME</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"Scalar Result: %.2f\n"</span><span class="p">,</span> <span class="n">result_scalar</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"%-13s: %.2f\n"</span><span class="p">,</span> <span class="n">simd_res_label</span><span class="p">,</span> <span class="n">result_simd</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"Scalar Time  : %.5f sec\n"</span><span class="p">,</span> <span class="n">time_scalar</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"%-13s: %.5f sec\n"</span><span class="p">,</span> <span class="n">simd_time_label</span><span class="p">,</span> <span class="n">time_simd</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"Speedup      : %.2f x\n"</span><span class="p">,</span> <span class="n">time_scalar</span> <span class="o">/</span> <span class="n">time_simd</span><span class="p">);</span>
    <span class="nf">printf</span><span class="p">(</span><span class="s">"------------------------------------\n"</span><span class="p">);</span>

    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="mac-m1-neon-벤치마크-결과-비교">Mac M1 (NEON) 벤치마크 결과 비교</h2>

<p>위 코드를 Apple Silicon(M1) 칩을 탑재한 맥에서 <code class="language-plaintext highlighter-rouge">clang -O3</code> 최적화 플래그를 주어 컴파일 후 실행한 결과입니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Initializing arrays...
Benchmarking scalar...
Benchmarking NEON...
------------------------------------
Architecture : NEON
Scalar Result: 16777216.00
NEON Result  : 31929088.00
Scalar Time  : 0.02787 sec
NEON Time    : 0.00507 sec
Speedup      : 5.50 x
------------------------------------
</code></pre></div></div>

<h3 id="결과-분석">결과 분석</h3>
<ol>
  <li><strong>성능 향상 (Speedup)</strong>: 스칼라 버전은 평균 <strong>0.027초</strong>, NEON을 활용한 코드는 <strong>0.005초</strong>가 걸리며 <strong>약 5.5배의 성능 향상</strong>을 보였습니다. 4개의 float를 한 번에 연산할 뿐 아니라 <code class="language-plaintext highlighter-rouge">vmlaq_f32</code>를 통해 곱셈과 덧셈(MAC 연산)을 명령어 단 하나로 처리하기 때문에 극한의 성능 이득을 얻을 수 있었습니다.</li>
  <li><strong>연산 결과의 차이 (Accumulation Error)</strong>: 아마 결과를 보시고 “계산 결과가 맞나?” 하는 의문이 드셨을 수 있습니다. 실제로 저 1억 개 배열의 내적을 수학적으로 오차 없이 식행하면(<code class="language-plaintext highlighter-rouge">double</code> 정밀도 기준) 정확한 정답은 <strong><code class="language-plaintext highlighter-rouge">32,835,000</code></strong> 부근이 나와야 합니다.
하지만 32비트 부동소수점(<code class="language-plaintext highlighter-rouge">float</code>)을 하나의 변수에 선형적으로 계속 더하는 <strong>스칼라 방식</strong>에서는, 누적합이 <code class="language-plaintext highlighter-rouge">16,777,216</code> (즉, $2^{24}$)에 도달하는 순간 1.0 미만의 소수점 단위 값들을 더해도 유효숫자(정밀도) 범위를 벗어나 아예 더해지지 않고 무시되어 버립니다. 그래서 스칼라 버전의 결과는 기계적으로 정확히 <code class="language-plaintext highlighter-rouge">16777216.00</code>에서 멈춰버린 것입니다.
반면 <strong>NEON 코드</strong>에서는 누적합을 4개의 전용 레지스터로 쪼개어 독립적인 부분합(Partial Sum)을 구하기 때문에, 각 레지스터마다 숫자가 천천히 커지게 되어 오차가 누적되고 소실되는 시점이 훨씬 늦어집니다. 그 덕분에 기계적 한계 내에서도 정답(<code class="language-plaintext highlighter-rouge">32,835,000</code>)에 훨씬 가까운 <code class="language-plaintext highlighter-rouge">31,929,088.00</code> 이라는 결괏값을 훨씬 빠르고 정확하게 계산해 낼 수 있었습니다. 데이터가 방대해질수록 이처럼 분할 정복 성격을 띠는 SIMD가 속도뿐 아니라 <strong>실질적인 부동소수점 연산 정밀도</strong> 면에서도 더 나은 결과를 줍니다.</li>
</ol>

<h2 id="마치며">마치며</h2>

<p>이렇듯 무거운 배열 연산이나 행렬 연산, 딥러닝 추론 등의 코어 루틴에서는 SIMD 사용 유무가 수 배 이상의 극적인 성능 차이를 만듭니다. 
크로스 플랫폼 개발을 하신다면, 위 코드의 예시처럼 매크로로 분기하여 Intel/AMD에서는 SSE/AVX를, ARM 계열에서는 NEON을 각각 호출하도록 대응하면 어떠한 빌드 환경에서도 극강의 퍼포먼스를 내는 애플리케이션을 작성할 수 있습니다.</p>]]></content><author><name>Justin Kim</name></author><category term="research" /><category term="SIMD" /><category term="NEON" /><category term="C Language" /><category term="Optimization" /><summary type="html"><![CDATA[현대 CPU 연산 방식은 데이터를 처리하는 단위에 따라 크게 스칼라(Scalar) 연산과 벡터(Vector) 연산으로 나눌 수 있습니다. 코어 모듈의 성능을 끌어올리기 위해서는 이 차이를 이해하고 적절한 명령어 셋을 활용하는 것이 필수적입니다.]]></summary></entry><entry><title type="html">같은 사실, 다른 출처: Named Graph로 신뢰의 경계를 긋다</title><link href="https://groou.com/ontology/2026/02/26/rdf-named-graphs/" rel="alternate" type="text/html" title="같은 사실, 다른 출처: Named Graph로 신뢰의 경계를 긋다" /><published>2026-02-26T09:00:00+09:00</published><updated>2026-02-26T09:00:00+09:00</updated><id>https://groou.com/ontology/2026/02/26/rdf-named-graphs</id><content type="html" xml:base="https://groou.com/ontology/2026/02/26/rdf-named-graphs/"><![CDATA[<h2 id="1-김철수의-상충하는-프로필">1. 김철수의 상충하는 프로필</h2>

<p>이번에는 유사한 상황에서 Reification이 답이 아닌 상황, 그래서 Named Graph를 사용해야 하는 상황이 있다는 것을 이야기해보려고 합니다.</p>

<p>Knowledge Graph를 구축하다 보면, 서로 다른 출처에서 가져온 데이터가 충돌하는 상황을 자주 만납니다. 앞의 Reification에서 다룬 예시와 유사하지만, 이번에는 Reification이 답이 아닌 상황을 다룹니다.</p>

<p>헤드헌터가 김철수의 인재 데이터베이스를 조회하니 다음과 같은 정보가 나옵니다.</p>

<blockquote>
  <p><strong>HR 시스템:</strong> 김철수는 네이버 재직 중
<strong>LinkedIn 프로필:</strong> 김철수는 삼성전자 Senior Engineer</p>
</blockquote>

<p>두 출처가 서로 다른 이야기를 합니다. LinkedIn 프로필은 이직 후 업데이트되지 않았을 가능성이 높지만, 데이터만 보면 구분이 안 됩니다. <strong>어느 쪽을 믿어야 할까요?</strong></p>

<p>기본 RDF 트리플로 표현하면 이렇게 됩니다.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>Reification을 사용하면 각 트리플에 <code class="language-plaintext highlighter-rouge">ex:source "HR 시스템"</code>, <code class="language-plaintext highlighter-rouge">ex:source "LinkedIn"</code> 같은 메타데이터를 붙일 수 있습니다. 하지만 현실의 Knowledge Graph에는 수만, 수억 개의 트리플이 있고, 하나하나 Reify하면 트리플 수가 4~7배로 폭발합니다.</p>

<p>더 중요한 문제는, <strong>출처는 개별 트리플이 아니라 데이터셋 단위의 속성</strong>이라는 점입니다. HR 시스템에서 가져온 1만 개의 트리플은 모두 “HR 시스템에서 2026년 2월 20일에 수집”이라는 출처 정보를 공유합니다. 각 트리플마다 같은 출처 정보를 중복해서 붙일 필요가 없습니다. 즉, 이러한 경우에는 Reification은 적절한 해결책이라고 할 수 없습니다.</p>

<figure style="margin: 1.5rem 0;">
<svg viewBox="0 0 800 270" width="100%" height="auto" role="img" aria-label="출처가 다른 트리플의 충돌: 어느 쪽을 믿어야 하는가" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aBlue" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
    </marker>
    <style>
      .bg-problem { fill: #f7f9ff; }
      .nd-person { fill: #dbeafe; stroke: #3b82f6; stroke-width: 2; }
      .nd-company { fill: #dcfce7; stroke: #22c55e; stroke-width: 2; }
      .edge-blue { stroke: #2563eb; stroke-width: 2; fill: none; marker-end: url(#aBlue); }
      .t-title { font: 800 22px Pretendard, system-ui, -apple-system, sans-serif; fill: #111827; }
      .t-node { font: 700 15px Pretendard, system-ui, sans-serif; fill: #111827; text-anchor: middle; dominant-baseline: central; }
      .t-edge { font: 500 13px 'Roboto Mono', ui-monospace, monospace; fill: #6b7280; text-anchor: middle; }
      .t-warn { font: 700 14px Pretendard, system-ui, sans-serif; fill: #dc2626; }
      .warn-bg { fill: #fef2f2; stroke: #fecaca; stroke-width: 1.5; rx: 10; }
    </style>
  </defs>

  <rect class="bg-problem" width="800" height="270" rx="16" />
  <text class="t-title" x="28" y="38">출처가 다른 정보의 충돌</text>
  <line x1="28" y1="52" x2="300" y2="52" stroke="#3b82f6" stroke-width="3" />

  <!-- 김철수 -->
  <rect class="nd-person" x="50" y="100" width="120" height="40" rx="20" />
  <text class="t-node" x="110" y="120">김철수</text>

  <!-- 네이버 -->
  <rect class="nd-company" x="320" y="80" width="130" height="40" rx="20" />
  <text class="t-node" x="385" y="100">네이버</text>

  <!-- 삼성전자 -->
  <rect class="nd-company" x="320" y="150" width="130" height="40" rx="20" />
  <text class="t-node" x="385" y="170">삼성전자</text>

  <!-- Arrows -->
  <line class="edge-blue" x1="172" y1="113" x2="318" y2="100" />
  <text class="t-edge" x="245" y="98">worksAt</text>

  <line class="edge-blue" x1="172" y1="127" x2="318" y2="170" />
  <text class="t-edge" x="245" y="160">worksAt</text>

  <!-- Warning annotations -->
  <rect class="warn-bg" x="510" y="76" width="260" height="110" />
  <text class="t-warn" x="530" y="104">? 어느 출처인가?</text>
  <text class="t-warn" x="530" y="132">? 언제 수집했나?</text>
  <text class="t-warn" x="530" y="160">? 어느 쪽이 최신인가?</text>

  <!-- Caption -->
  <text style="font: 500 13px Pretendard, system-ui, sans-serif; fill: #6b7280;" x="28" y="252">트리플에는 출처와 신뢰도를 기록할 자리가 없습니다.</text>
</svg>
</figure>

<p>여기서 <strong>Named Graph</strong>가 등장합니다. 트리플을 개별적으로 관리하는 대신, 관련된 트리플들을 <strong>하나의 그래프로 묶고</strong>, 그 그래프에 출처 정보를 부여하는 방식입니다.</p>

<h2 id="2-named-graph란-무엇인가">2. Named Graph란 무엇인가</h2>

<p>RDF의 기본 단위는 <strong>트리플(Triple)</strong>: 주어(Subject) – 술어(Predicate) – 목적어(Object)입니다. Named Graph는 여기에 <strong>네 번째 요소</strong>를 추가합니다. 바로 <strong>그래프 이름(Graph Name)</strong>입니다.</p>

<p>트리플 하나에 그래프 이름을 더하면 <strong>쿼드(Quad, S-P-O-G)</strong>가 됩니다. 같은 그래프 이름을 가진 쿼드들을 모으면 하나의 <strong>Named Graph</strong>가 됩니다. 그리고 이 Named Graph는 IRI로 식별되는 자원이므로, 그래프 자체에 대해 “출처는 어디인가”, “언제 수집했는가”, “신뢰도는 얼마인가” 같은 메타데이터를 붙일 수 있습니다.</p>

<p>RDF 1.1<a class="citation" href="#rdf11concepts">[1]</a>에서는 이를 <strong>RDF Dataset</strong>이라는 구조로 정의합니다. RDF Dataset은 하나의 <strong>Default Graph</strong>(이름이 없는 기본 그래프)와 0개 이상의 <strong>Named Graph</strong>로 구성됩니다.</p>

<table>
  <thead>
    <tr>
      <th>요소</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Triple (S-P-O)</td>
      <td>주어-술어-목적어, RDF의 기본 단위</td>
    </tr>
    <tr>
      <td>Quad (S-P-O-G)</td>
      <td>트리플 + 그래프 이름, Named Graph의 단위</td>
    </tr>
    <tr>
      <td>Default Graph</td>
      <td>이름이 없는 기본 그래프 (메타데이터 기록 용도)</td>
    </tr>
    <tr>
      <td>Named Graph</td>
      <td>IRI로 이름이 붙은 그래프, 메타데이터 부여 가능</td>
    </tr>
    <tr>
      <td>RDF Dataset</td>
      <td>Default Graph + Named Graph들의 집합</td>
    </tr>
  </tbody>
</table>

<p>비유하자면, 개별 서류(트리플)에 하나하나 도장을 찍는 것(Reification)이 아니라, 서류를 <strong>폴더(Named Graph)</strong>에 넣고 폴더 겉표지에 “출처: HR팀, 수집일: 2026-02-20, 신뢰도: 높음”이라고 적는 것입니다. 폴더 안의 모든 서류는 자동으로 같은 출처 정보를 공유합니다.</p>

<h2 id="3-trig-문법으로-표현하기">3. TriG 문법으로 표현하기</h2>

<p>Named Graph를 표현하는 표준 문법이 <strong>TriG</strong><a class="citation" href="#trig11">[2]</a>입니다. TriG는 Turtle의 확장으로, <code class="language-plaintext highlighter-rouge">GRAPH_IRI { ... }</code> 블록으로 트리플을 그래프에 묶을 수 있습니다.</p>

<p>김철수 시나리오를 TriG로 표현해 보겠습니다.</p>

<div class="language-trig highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="w">  </span><span class="nl">&lt;http://example.org/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">xsd</span><span class="p">:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2001/XMLSchema#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="c1"># ── HR 시스템에서 가져온 데이터 (Named Graph 1) ──</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="nt">graph</span><span class="err">/hr</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Person</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">ex</span><span class="p">:</span><span class="nt">name</span><span class="w"> </span><span class="s2">"김철수"</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="p">.</span><span class="w">

    </span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Company</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">ex</span><span class="p">:</span><span class="nt">name</span><span class="w"> </span><span class="s2">"네이버"</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c1"># ── LinkedIn에서 크롤링한 데이터 (Named Graph 2) ──</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="nt">graph</span><span class="err">/linkedin</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">ex</span><span class="p">:</span><span class="nt">jobTitle</span><span class="w"> </span><span class="s2">"Senior Engineer"</span><span class="w"> </span><span class="p">.</span><span class="w">

    </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Company</span><span class="w"> </span><span class="p">;</span><span class="w">
        </span><span class="nn">ex</span><span class="p">:</span><span class="nt">name</span><span class="w"> </span><span class="s2">"삼성전자"</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c1"># ── 그래프 메타데이터 (Default Graph에 기록) ──</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="nt">graph</span><span class="err">/hr</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">source</span><span class="w">      </span><span class="s2">"사내 HR 시스템"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">retrievedAt</span><span class="w"> </span><span class="s2">"2026-02-20"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">trustLevel</span><span class="w">  </span><span class="mi">3</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">ex</span><span class="p">:</span><span class="nt">graph</span><span class="err">/linkedin</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">source</span><span class="w">      </span><span class="s2">"LinkedIn 크롤링"</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">retrievedAt</span><span class="w"> </span><span class="s2">"2026-01-15"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">trustLevel</span><span class="w">  </span><span class="mi">2</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>핵심을 정리하면 이렇습니다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ex:graph/hr { ... }</code> 블록 안의 모든 트리플은 <code class="language-plaintext highlighter-rouge">ex:graph/hr</code>라는 Named Graph에 속합니다.</li>
  <li>그래프 이름(<code class="language-plaintext highlighter-rouge">ex:graph/hr</code>, <code class="language-plaintext highlighter-rouge">ex:graph/linkedin</code>) 자체도 IRI이므로, Default Graph에서 이들에 대한 메타데이터를 기술할 수 있습니다.</li>
  <li><code class="language-plaintext highlighter-rouge">trustLevel</code>을 숫자로 표현하여, 신뢰도 순서를 명확히 할 수 있습니다. 3(높음), 2(중간)처럼 숫자가 클수록 신뢰도가 높습니다.</li>
</ul>

<p>TriG는 Turtle을 아는 사람이라면 쉽게 배울 수 있습니다. Turtle 문법에 <code class="language-plaintext highlighter-rouge">{ }</code> 블록만 추가하면 됩니다.</p>

<h2 id="4-시각화-named-graph가-만드는-구조">4. 시각화: Named Graph가 만드는 구조</h2>

<p>아래 다이어그램은 Named Graph가 적용된 김철수의 프로필 데이터를 시각화한 것입니다. 두 개의 독립된 그래프가 각각의 출처 정보와 함께 관리되는 구조를 보여줍니다.</p>

<figure style="margin: 1.5rem 0;">
<svg viewBox="0 0 880 500" width="100%" height="auto" role="img" aria-label="Named Graph 구조: 서로 다른 출처의 트리플이 각각의 그래프에 담겨 있다" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aEdge" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
    </marker>
    <marker id="aMeta" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#d97706" />
    </marker>
    <style>
      .bg-sol { fill: #faf5ff; }
      .graph-boundary-hr { fill: #eff6ff; stroke: #60a5fa; stroke-width: 2.5; stroke-dasharray: 8,4; }
      .graph-boundary-li { fill: #f0fdf4; stroke: #4ade80; stroke-width: 2.5; stroke-dasharray: 8,4; }
      .nd-person { fill: #dbeafe; stroke: #3b82f6; stroke-width: 2; }
      .nd-company { fill: #dcfce7; stroke: #22c55e; stroke-width: 2; }
      .nd-meta { fill: #fef3c7; stroke: #f59e0b; stroke-width: 1.5; }
      .e-rdf { stroke: #2563eb; stroke-width: 1.8; fill: none; marker-end: url(#aEdge); }
      .e-meta { stroke: #d97706; stroke-width: 1.8; fill: none; marker-end: url(#aMeta); }
      .t-h { font: 800 20px Pretendard, system-ui, sans-serif; fill: #111827; }
      .t-n { font: 700 14px Pretendard, system-ui, sans-serif; fill: #111827; text-anchor: middle; dominant-baseline: central; }
      .t-ns { font: 600 11px Pretendard, system-ui, sans-serif; fill: #6b7280; text-anchor: middle; dominant-baseline: central; }
      .t-e { font: 500 10px 'Roboto Mono', ui-monospace, monospace; fill: #6b7280; text-anchor: middle; }
      .t-e-left { font: 500 10px 'Roboto Mono', ui-monospace, monospace; fill: #6b7280; text-anchor: start; }
      .t-graph { font: 700 12px 'Roboto Mono', ui-monospace, monospace; text-anchor: start; }
      .t-graph-hr { fill: #1e40af; }
      .t-graph-li { fill: #15803d; }
      .t-sec { font: 700 14px Pretendard, system-ui, sans-serif; fill: #92400e; }
      .t-leg { font: 500 11px Pretendard, system-ui, sans-serif; fill: #6b7280; }
    </style>
  </defs>

  <rect class="bg-sol" width="880" height="500" rx="16" />
  <text class="t-h" x="28" y="36">Named Graph: 출처별로 경계를 긋다</text>
  <line x1="28" y1="50" x2="380" y2="50" stroke="#7c3aed" stroke-width="3" />

  <!-- ═══ HR Graph ═══ -->
  <rect class="graph-boundary-hr" x="30" y="78" width="390" height="180" rx="16" />
  <text class="t-graph t-graph-hr" x="46" y="100">ex:graph/hr</text>

  <!-- 김철수 (HR) -->
  <rect class="nd-person" x="70" y="130" width="120" height="36" rx="18" />
  <text class="t-n" x="130" y="148">김철수</text>

  <!-- 네이버 -->
  <rect class="nd-company" x="260" y="130" width="120" height="36" rx="18" />
  <text class="t-n" x="320" y="148">네이버</text>

  <!-- worksAt arrow -->
  <line class="e-rdf" x1="192" y1="148" x2="258" y2="148" />
  <text class="t-e" x="225" y="138">worksAt</text>

  <!-- ═══ LinkedIn Graph ═══ -->
  <rect class="graph-boundary-li" x="460" y="78" width="390" height="180" rx="16" />
  <text class="t-graph t-graph-li" x="476" y="100">ex:graph/linkedin</text>

  <!-- 김철수 (LinkedIn) -->
  <rect class="nd-person" x="500" y="130" width="120" height="36" rx="18" />
  <text class="t-n" x="560" y="148">김철수</text>

  <!-- 삼성전자 -->
  <rect class="nd-company" x="690" y="130" width="120" height="36" rx="18" />
  <text class="t-n" x="750" y="148">삼성전자</text>

  <!-- worksAt arrow -->
  <line class="e-rdf" x1="622" y1="148" x2="688" y2="148" />
  <text class="t-e" x="655" y="138">worksAt</text>

  <!-- ═══ Default Graph (Metadata) ═══ -->
  <text class="t-sec" x="30" y="292">Default Graph (그래프 메타데이터)</text>

  <!-- HR metadata -->
  <rect class="nd-meta" x="50" y="312" width="330" height="130" rx="10" />
  <text class="t-ns" x="215" y="334">ex:graph/hr 메타데이터</text>
  <text class="t-e-left" x="72" y="362">source: "사내 HR 시스템"</text>
  <text class="t-e-left" x="72" y="386">retrievedAt: 2026-02-20</text>
  <text class="t-e-left" x="72" y="410">trustLevel: 3 (높음)</text>

  <!-- LinkedIn metadata -->
  <rect class="nd-meta" x="500" y="312" width="330" height="130" rx="10" />
  <text class="t-ns" x="665" y="334">ex:graph/linkedin 메타데이터</text>
  <text class="t-e-left" x="522" y="362">source: "LinkedIn 크롤링"</text>
  <text class="t-e-left" x="522" y="386">retrievedAt: 2026-01-15</text>
  <text class="t-e-left" x="522" y="410">trustLevel: 2 (중간)</text>

  <!-- Metadata arrows -->
  <line class="e-meta" x1="215" y1="260" x2="215" y2="310" />
  <line class="e-meta" x1="665" y1="260" x2="665" y2="310" />

  <!-- ═══ 범례 ═══ -->
  <g transform="translate(28, 474)">
    <line x1="0" y1="8" x2="28" y2="8" stroke="#2563eb" stroke-width="2" marker-end="url(#aEdge)" />
    <text class="t-leg" x="36" y="12">RDF 관계</text>
    <line x1="130" y1="8" x2="158" y2="8" stroke="#d97706" stroke-width="2" marker-end="url(#aMeta)" />
    <text class="t-leg" x="166" y="12">메타데이터</text>
    <rect x="270" y="2" width="30" height="12" fill="none" stroke="#60a5fa" stroke-width="2" stroke-dasharray="4,2" rx="2" />
    <text class="t-leg" x="308" y="12">Named Graph</text>
  </g>
</svg>
</figure>

<p>두 개의 Named Graph(<code class="language-plaintext highlighter-rouge">ex:graph/hr</code>, <code class="language-plaintext highlighter-rouge">ex:graph/linkedin</code>)가 점선 경계로 구분되어 있습니다. 각 그래프는 독립된 트리플 집합이며, Default Graph에 기록된 메타데이터를 통해 출처, 수집 시점, 신뢰도 정보를 가집니다. 이 구조 덕분에 같은 개체(김철수)에 대한 서로 다른 정보를 출처별로 분리하여 관리할 수 있습니다.</p>

<h2 id="5-sparql로-질의하기-graph의-힘">5. SPARQL로 질의하기: GRAPH의 힘</h2>

<p>Named Graph의 진가는 SPARQL<a class="citation" href="#sparql11query">[3]</a>에서 정의하는 <code class="language-plaintext highlighter-rouge">GRAPH</code> 키워드를 사용할 때 드러납니다. <code class="language-plaintext highlighter-rouge">GRAPH</code> 패턴을 사용하면 “어느 Named Graph 안에서” 트리플을 매칭할지 지정할 수 있습니다.</p>

<p><strong>쿼리 1: 신뢰도 높은 출처에서 김철수의 현재 직장 찾기</strong></p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://example.org/&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?company</span><span class="w"> </span><span class="nv">?source</span><span class="w"> </span><span class="nv">?trust</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="k">GRAPH</span><span class="w"> </span><span class="nv">?g</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nn">ex</span><span class="o">:</span><span class="ss">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">worksAt</span><span class="w"> </span><span class="nv">?company</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="nv">?g</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">trustLevel</span><span class="w"> </span><span class="nv">?trust</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="nv">?g</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">source</span><span class="w">     </span><span class="nv">?source</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">DESC</span><span class="p">(</span><span class="nv">?trust</span><span class="p">)</span><span class="w">
</span></code></pre></div></div>

<p>결과:</p>

<table>
  <thead>
    <tr>
      <th>?company</th>
      <th>?source</th>
      <th>?trust</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ex:네이버</td>
      <td>“사내 HR 시스템”</td>
      <td>3</td>
    </tr>
    <tr>
      <td>ex:삼성전자</td>
      <td>“LinkedIn 크롤링”</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">GRAPH ?g { ... }</code> 패턴은 “어떤 Named Graph 안에서든” 패턴에 매칭되는 트리플을 찾습니다. 그래프 변수 <code class="language-plaintext highlighter-rouge">?g</code>를 통해 그래프의 메타데이터(<code class="language-plaintext highlighter-rouge">trustLevel</code>, <code class="language-plaintext highlighter-rouge">source</code>)에 접근할 수 있습니다. <code class="language-plaintext highlighter-rouge">ORDER BY DESC(?trust)</code>로 신뢰도가 높은 순서로 정렬하면, 가장 믿을 만한 정보가 먼저 나옵니다. 이제 헤드헌터는 “네이버”를 김철수의 현재 직장으로 판단할 수 있습니다.</p>

<p><strong>쿼리 2: 특정 출처의 데이터만 조회</strong></p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://example.org/&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?s</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="k">GRAPH</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">graph</span><span class="o">/</span><span class="err">hr</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nv">?s</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>그래프 IRI를 직접 지정하면, 해당 출처의 데이터만 필터링할 수 있습니다. 수억 개의 트리플이 있어도 그래프 단위로 범위를 좁힐 수 있으므로, 쿼리 성능에도 유리합니다. “HR 시스템이 말하는 정보만 보고 싶다”는 요구사항을 단 한 줄의 <code class="language-plaintext highlighter-rouge">GRAPH</code> 절로 해결할 수 있습니다.</p>

<p><strong>쿼리 3: 오래된 출처 데이터 탐지</strong></p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="w">  </span><span class="nn">&lt;http://example.org/&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">xsd</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/2001/XMLSchema#&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="k">DISTINCT</span><span class="w"> </span><span class="nv">?g</span><span class="w"> </span><span class="nv">?source</span><span class="w"> </span><span class="nv">?date</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="k">GRAPH</span><span class="w"> </span><span class="nv">?g</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nv">?s</span><span class="w"> </span><span class="nv">?p</span><span class="w"> </span><span class="nv">?o</span><span class="w"> </span><span class="p">.</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="nv">?g</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">retrievedAt</span><span class="w"> </span><span class="nv">?date</span><span class="w"> </span><span class="p">;</span><span class="w">
       </span><span class="nn">ex</span><span class="o">:</span><span class="ss">source</span><span class="w">      </span><span class="nv">?source</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="nv">?date</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="s2">"2026-02-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="o">:</span><span class="ss">date</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>이 쿼리는 2월 1일 이전에 수집된 그래프를 찾습니다. LinkedIn 그래프(2026-01-15 수집)가 결과에 나타나므로, “이 데이터는 오래되었으니 재수집이 필요하다”는 판단을 내릴 수 있습니다. Named Graph는 단순히 출처를 기록하는 것을 넘어, <strong>데이터의 신선도(freshness)</strong>를 관리하는 도구가 됩니다.</p>

<h2 id="6-reification과-named-graph-언제-무엇을">6. Reification과 Named Graph: 언제 무엇을</h2>

<p>이전 글에서 배운 Reification과 Named Graph는 <strong>경쟁이 아니라 보완</strong> 관계입니다. 두 기법이 해결하는 문제가 다르기 때문입니다.</p>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>Reification</th>
      <th>Named Graph</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>메타데이터 대상</td>
      <td>개별 트리플</td>
      <td>트리플 묶음(그래프)</td>
    </tr>
    <tr>
      <td>추가 트리플 수</td>
      <td>트리플당 4~7개</td>
      <td>그래프당 수 개 (내부 트리플 수에 무관)</td>
    </tr>
    <tr>
      <td>출처 관리</td>
      <td>트리플마다 <code class="language-plaintext highlighter-rouge">ex:source</code> 부착</td>
      <td>그래프에 한 번만 <code class="language-plaintext highlighter-rouge">ex:source</code></td>
    </tr>
    <tr>
      <td>시간 맥락</td>
      <td>트리플마다 시간 메타데이터</td>
      <td>그래프 단위 시점 기록 (스냅샷)</td>
    </tr>
    <tr>
      <td>SPARQL 패턴</td>
      <td><code class="language-plaintext highlighter-rouge">?stmt a rdf:Statement</code></td>
      <td><code class="language-plaintext highlighter-rouge">GRAPH ?g { ... }</code></td>
    </tr>
    <tr>
      <td>적합한 용도</td>
      <td>특정 트리플에 세밀한 메타데이터</td>
      <td>데이터셋 단위 출처/버전/접근 관리</td>
    </tr>
    <tr>
      <td>확장성</td>
      <td>대규모에서 트리플 폭발</td>
      <td>대규모에서 효율적</td>
    </tr>
    <tr>
      <td>표준</td>
      <td>RDF 1.1 (rdf:Statement)</td>
      <td>RDF 1.1 (RDF Dataset)</td>
    </tr>
  </tbody>
</table>

<p><strong>언제 무엇을 사용할 것인가?</strong></p>

<ul>
  <li>특정 트리플 하나에 세밀한 시간 정보나 확신도를 붙여야 한다면 → <strong>Reification</strong> (또는 RDF-star)</li>
  <li>데이터셋 전체 또는 일부를 출처, 버전, 접근 권한 단위로 관리해야 한다면 → <strong>Named Graph</strong></li>
  <li>실무에서는 두 기법을 함께 사용하는 것이 일반적입니다. 예를 들어, Named Graph로 출처를 관리하면서, 그 안의 특정 트리플에만 Reification을 적용하여 추가 맥락을 기록할 수 있습니다.</li>
</ul>

<h2 id="7-실전-활용-버전-접근-제어-신뢰">7. 실전 활용: 버전, 접근 제어, 신뢰</h2>

<p>Named Graph는 실무 Knowledge Graph에서 다양한 방식으로 활용됩니다. 대표적인 세 가지 사례를 소개합니다.</p>

<p><strong>7.1 데이터 버전 관리 (Temporal Snapshots)</strong></p>

<p>시점마다 Named Graph를 만들어 스냅샷을 관리할 수 있습니다. 예를 들어 <code class="language-plaintext highlighter-rouge">ex:graph/employees/2026-Q1</code>, <code class="language-plaintext highlighter-rouge">ex:graph/employees/2025-Q4</code>처럼 분기별 그래프를 두면, 특정 시점의 조직도를 통째로 조회하거나, 시점 간 변화를 비교할 수 있습니다. 이는 “이번 분기에 누가 입사했는가”나 “작년 대비 팀 구성이 어떻게 변했는가” 같은 질문에 답하는 데 유용합니다.</p>

<p><strong>7.2 접근 제어 (Access Control)</strong></p>

<p>Named Graph 단위로 읽기/쓰기 권한을 설정할 수 있습니다. 예를 들어 <code class="language-plaintext highlighter-rouge">ex:graph/public</code>은 누구나 조회 가능, <code class="language-plaintext highlighter-rouge">ex:graph/internal</code>은 사내 직원만, <code class="language-plaintext highlighter-rouge">ex:graph/hr-confidential</code>은 HR팀만 접근 가능하도록 설정합니다. GraphDB나 Stardog 같은 트리플스토어에서는 그래프 단위 ACL(Access Control List)을 지원하므로, 민감한 데이터를 별도 그래프로 분리하여 보호할 수 있습니다.</p>

<p>다만 주의할 점이 있습니다. 모든 트리플스토어가 Named Graph를 물리적으로 분리하여 저장하는 것은 아닙니다. 예를 들어 Apache Jena의 TDB 엔진은 모든 그래프의 트리플을 동일한 인덱스에 저장하므로, Named Graph 이름만으로는 물리적 접근 제어가 보장되지 않습니다. Named Graph 기반 접근 제어를 도입할 때는 사용하는 트리플스토어의 구현 방식을 반드시 확인해야 합니다.</p>

<p><strong>7.3 신뢰 점수 기반 추론 (Trust-aware Reasoning)</strong></p>

<p>앞의 김철수 시나리오가 정확히 이 패턴입니다. 여러 출처가 상충하는 정보를 제공할 때, 각 그래프에 <code class="language-plaintext highlighter-rouge">trustLevel</code>을 부여하고, 추론 엔진이나 쿼리 로직이 신뢰도가 높은 쪽을 우선하도록 설계할 수 있습니다. 이는 외부 크롤링 데이터, 사용자 입력, 자동 추론 결과 등 신뢰도가 다른 데이터 소스를 통합할 때 필수적입니다.</p>

<h2 id="8-마치며">8. 마치며</h2>

<p>Reification이 “트리플에 대해 말하기”였다면, Named Graph는 “트리플 묶음에 이름을 붙이기”입니다. 현실 세계의 데이터는 항상 어딘가에서 옵니다. 누가 만들었는지, 언제 수집했는지, 얼마나 믿을 수 있는지. Named Graph는 이 질문들에 답할 수 있는 구조를 RDF에 부여합니다.</p>

<p>하지만 기법을 아는 것보다 더 중요한 것은, <strong>어디에 온톨로지를 적용할 것인가</strong>를 결정하는 일입니다. Reification이든 Named Graph든, 결국 도메인 지식을 바탕으로 “이 데이터에서 무엇이 중요한가”, “어떤 맥락을 보존해야 하는가”를 판단하는 것이 온톨로지 설계의 핵심입니다. 기법은 도구일 뿐, 도메인에 대한 깊은 이해 없이는 어떤 도구도 올바르게 쓸 수 없습니다.</p>

<p>서로 다른 출처의 정보를 통합할 때, “어느 쪽을 믿어야 하는가”라는 질문에 부딪혔다면 Named Graph를 떠올려 보세요. 트리플을 그래프 단위로 묶는 순간, Knowledge Graph는 비로소 <strong>신뢰의 경계</strong>를 그을 수 있게 됩니다.</p>

<hr />

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/ontology/2026/02/24/rdf-reification/">시간이 흐르면 사실도 변한다: RDF Reification으로 맥락 기록하기</a></li>
  <li><a href="/ontology/2026/01/25/spo-decomposition-solution/">S-P-O 분해 연습문제 풀이: 전력 모듈-센서 노이즈 간섭</a></li>
  <li><a href="/ontology/2026/01/16/icalendar-format-and-ontology/">iCalendar의 진화: 텍스트에서 의미론적 웹으로</a></li>
</ul>

<h2 id="참고-문헌">참고 문헌</h2>

<ol class="bibliography"><li>R. Cyganiak, D. Wood, and M. Lanthaler, “RDF 1.1 Concepts and Abstract Syntax,” W3C, W3C Recommendation, Feb. 2014. Available at: https://www.w3.org/TR/rdf11-concepts/
</li>
<li>G. Carothers and A. Seaborne, “RDF 1.1 TriG: RDF Dataset Language,” W3C, W3C Recommendation, Feb. 2014. Available at: https://www.w3.org/TR/trig/
</li>
<li>S. Harris and A. Seaborne, “SPARQL 1.1 Query Language,” W3C, W3C Recommendation, Mar. 2013. Available at: https://www.w3.org/TR/sparql11-query/
</li></ol>]]></content><author><name>Justin Kim</name></author><category term="ontology" /><category term="RDF" /><category term="Named Graph" /><category term="SPARQL" /><category term="TriG" /><category term="Knowledge Graph" /><category term="Provenance" /><summary type="html"><![CDATA[HR 시스템은 '네이버 재직 중'이라 하고, LinkedIn은 '삼성전자 재직 중'이라 합니다. 서로 다른 출처가 상충할 때, 어떤 정보를 믿어야 할까요? Named Graph는 트리플 묶음에 이름을 붙여 출처와 신뢰도를 관리하는 RDF의 구조입니다.]]></summary></entry><entry><title type="html">시간이 흐르면 사실도 변한다: RDF Reification으로 맥락 기록하기</title><link href="https://groou.com/ontology/2026/02/24/rdf-reification/" rel="alternate" type="text/html" title="시간이 흐르면 사실도 변한다: RDF Reification으로 맥락 기록하기" /><published>2026-02-24T09:00:00+09:00</published><updated>2026-02-24T09:00:00+09:00</updated><id>https://groou.com/ontology/2026/02/24/rdf-reification</id><content type="html" xml:base="https://groou.com/ontology/2026/02/24/rdf-reification/"><![CDATA[<h2 id="1-김철수의-이직-기록">1. 김철수의 이직 기록</h2>

<p>Knowledge Graph를 구축하다 보면, 시간의 흐름에 따라 변하는 사실을 기록해야 하는 순간이 반드시 찾아옵니다. 간단한 예를 들어보겠습니다.</p>

<blockquote>
  <p>김철수는 <strong>2020년부터 2022년까지</strong> 삼성전자에서 근무했고, <strong>2023년부터 현재까지</strong> 네이버에서 근무하고 있다.</p>
</blockquote>

<p>사람이 읽으면 명확합니다. 하지만 이것을 RDF 트리플로 표현하면 어떻게 될까요?</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>이 두 트리플만 보면 다음과 같은 질문에 답할 수 없습니다.</p>

<ul>
  <li>김철수는 <strong>지금</strong> 어디에서 일하는가?</li>
  <li>삼성전자에는 <strong>언제부터 언제까지</strong> 다녔는가?</li>
  <li>두 회사에 <strong>동시에</strong> 다닌 것인가, 순서대로 다닌 것인가?</li>
</ul>

<p>이 문제의 핵심은, RDF 트리플이 <strong>주어(Subject) – 술어(Predicate) – 목적어(Object)</strong>의 세 요소만으로 구성되어 있어 “언제”, “누가 말했는지”, “확실한가” 같은 <strong>맥락(Context)</strong>을 붙일 자리가 없다는 점입니다.</p>

<figure style="margin: 1.5rem 0;">
<svg viewBox="0 0 780 270" width="100%" height="auto" role="img" aria-label="기본 RDF 트리플의 한계: 시간 정보를 표현할 수 없다" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aBlue" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#2563eb" />
    </marker>
    <style>
      .bg-problem { fill: #f7f9ff; }
      .nd-person { fill: #dbeafe; stroke: #3b82f6; stroke-width: 2; }
      .nd-company { fill: #dcfce7; stroke: #22c55e; stroke-width: 2; }
      .edge-blue { stroke: #2563eb; stroke-width: 2; fill: none; marker-end: url(#aBlue); }
      .t-title { font: 800 22px Pretendard, system-ui, -apple-system, sans-serif; fill: #111827; }
      .t-node { font: 700 15px Pretendard, system-ui, sans-serif; fill: #111827; text-anchor: middle; dominant-baseline: central; }
      .t-edge { font: 500 13px 'Roboto Mono', ui-monospace, monospace; fill: #6b7280; text-anchor: middle; }
      .t-warn { font: 700 15px Pretendard, system-ui, sans-serif; fill: #dc2626; }
      .warn-bg { fill: #fef2f2; stroke: #fecaca; stroke-width: 1.5; rx: 10; }
    </style>
  </defs>

  <rect class="bg-problem" width="780" height="270" rx="16" />
  <text class="t-title" x="28" y="38">기본 트리플의 한계</text>
  <line x1="28" y1="52" x2="260" y2="52" stroke="#3b82f6" stroke-width="3" />

  <!-- 김철수 -->
  <rect class="nd-person" x="60" y="100" width="110" height="40" rx="20" />
  <text class="t-node" x="115" y="120">김철수</text>

  <!-- 삼성전자 -->
  <rect class="nd-company" x="340" y="80" width="120" height="40" rx="20" />
  <text class="t-node" x="400" y="100">삼성전자</text>

  <!-- 네이버 -->
  <rect class="nd-company" x="340" y="150" width="120" height="40" rx="20" />
  <text class="t-node" x="400" y="170">네이버</text>

  <!-- Arrows -->
  <line class="edge-blue" x1="172" y1="113" x2="338" y2="100" />
  <text class="t-edge" x="255" y="98">worksAt</text>

  <line class="edge-blue" x1="172" y1="127" x2="338" y2="170" />
  <text class="t-edge" x="255" y="160">worksAt</text>

  <!-- Warning annotations -->
  <rect class="warn-bg" x="540" y="76" width="210" height="110" />
  <text class="t-warn" x="560" y="104">? 동시 근무인가?</text>
  <text class="t-warn" x="560" y="132">? 순서는?</text>
  <text class="t-warn" x="560" y="160">? 기간은?</text>

  <!-- Caption -->
  <text style="font: 500 13px Pretendard, system-ui, sans-serif; fill: #6b7280;" x="28" y="252">트리플에는 "언제"를 기록할 자리가 없습니다.</text>
</svg>
</figure>

<h2 id="2-우회로는-왜-안-되는가">2. 우회로는 왜 안 되는가?</h2>

<p>Reification을 알기 전에, 흔히 시도하는 우회 방법들이 왜 실패하는지 먼저 살펴봅시다.</p>

<h3 id="21-술어에-시간을-녹이기">2.1 술어에 시간을 녹이기</h3>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt_2020_2022</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt_2023_now</span><span class="w">  </span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>연도를 술어(Predicate) 이름에 포함시키면 되지 않을까요? 기술적으로는 동작하지만, <strong>SPARQL로 질의할 수가 없습니다.</strong> “김철수가 근무한 모든 회사”를 찾으려면 <code class="language-plaintext highlighter-rouge">ex:worksAt</code>로 시작하는 술어를 전부 패턴 매칭해야 하는데, 이는 표준 SPARQL이 지원하지 않는 방식입니다. 술어의 이름 자체가 데이터가 되어 버리면, 온톨로지의 스키마와 인스턴스가 뒤섞여 관리가 불가능해집니다.</p>

<h3 id="22-리터럴에-날짜-병기">2.2 리터럴에 날짜 병기</h3>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="s2">"삼성전자 (2020-2022)"</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>목적어를 URI 대신 문자열 리터럴로 바꾸고, 날짜를 같이 넣는 방법입니다. 이렇게 하면 <code class="language-plaintext highlighter-rouge">ex:삼성전자</code>라는 자원과의 <strong>연결이 끊어집니다.</strong> “삼성전자에 근무하는 모든 사람”을 찾으려면 문자열 파싱을 해야 하고, 삼성전자라는 개체에 연결된 다른 정보(위치, 업종 등)에 접근할 수 없게 됩니다. 의미(Semantics)가 문자열 안에 갇혀 버리는 것입니다.</p>

<h3 id="23-중간-노드-만들기-n-ary-relation">2.3 중간 노드 만들기 (N-ary Relation)</h3>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">hasEmployment</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Employment_1</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="nt">Employment_1</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">company</span><span class="w">   </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">;</span><span class="w">
                </span><span class="nn">ex</span><span class="p">:</span><span class="nt">startDate</span><span class="w"> </span><span class="s2">"2020-01-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
                </span><span class="nn">ex</span><span class="p">:</span><span class="nt">endDate</span><span class="w">   </span><span class="s2">"2022-12-31"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>이 방법은 실무에서 자주 사용되며, 실제로 잘 동작합니다. 하지만 원래의 <code class="language-plaintext highlighter-rouge">worksAt</code>라는 직관적인 관계가 사라지고, <code class="language-plaintext highlighter-rouge">hasEmployment</code> → <code class="language-plaintext highlighter-rouge">company</code>라는 <strong>2-hop 경로</strong>로 대체됩니다. 이는 기존 온톨로지와의 호환성을 깨뜨리고, “김철수가 근무하는 회사”라는 단순한 질문에도 중간 노드를 경유하는 복잡한 쿼리가 필요해집니다.</p>

<p>세 가지 시도 모두 한계를 가지는 이유는 근본적으로 같습니다. <strong>트리플 자체에 대한 메타데이터를 트리플 구조 안에서 표현하려 하기 때문입니다.</strong> 여기서 Reification이 등장합니다.</p>

<table>
  <thead>
    <tr>
      <th>우회 방법</th>
      <th>문제점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>술어에 시간 포함</td>
      <td>SPARQL 질의 불가, 스키마/인스턴스 혼재</td>
    </tr>
    <tr>
      <td>리터럴에 날짜 병기</td>
      <td>자원 연결 단절, 의미 소실</td>
    </tr>
    <tr>
      <td>중간 노드 (N-ary)</td>
      <td>원래 관계 소실, 쿼리 복잡화</td>
    </tr>
  </tbody>
</table>

<h2 id="3-reification-트리플에-대해-말하기">3. Reification: 트리플에 대해 말하기</h2>

<p><strong>Reification</strong>(구체화)은 하나의 트리플을 <strong>그 자체로 하나의 자원(Resource)</strong>으로 만드는 기법입니다. 쉽게 말해, “김철수가 삼성전자에서 근무한다”라는 <strong>진술(Statement)</strong> 자체에 이름을 붙여, 그 진술에 대해 추가적인 설명을 할 수 있게 하는 것입니다.</p>

<p>RDF<a class="citation" href="#rdf11concepts">[1]</a>에서는 이를 위해 네 가지 어휘를 제공합니다.</p>

<table>
  <thead>
    <tr>
      <th>어휘</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rdf:Statement</code></td>
      <td>“이것은 하나의 진술이다”라는 타입 선언</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rdf:subject</code></td>
      <td>원래 트리플의 주어를 가리킴</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rdf:predicate</code></td>
      <td>원래 트리플의 술어를 가리킴</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rdf:object</code></td>
      <td>원래 트리플의 목적어를 가리킴</td>
    </tr>
  </tbody>
</table>

<p>즉, 원래 트리플 <code class="language-plaintext highlighter-rouge">ex:김철수 ex:worksAt ex:삼성전자</code>를 다음과 같이 “풀어서” 표현합니다.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="nt">stmt1</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">Statement</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">subject</span><span class="w">   </span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">predicate</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">object</span><span class="w">    </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>이제 <code class="language-plaintext highlighter-rouge">ex:stmt1</code>은 하나의 자원이므로, 여기에 시간 정보를 자유롭게 붙일 수 있습니다.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">ex</span><span class="p">:</span><span class="nt">stmt1</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">startDate</span><span class="w"> </span><span class="s2">"2020-01-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
         </span><span class="nn">ex</span><span class="p">:</span><span class="nt">endDate</span><span class="w">   </span><span class="s2">"2022-12-31"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
         </span><span class="nn">ex</span><span class="p">:</span><span class="nt">source</span><span class="w">    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">HR_Database</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>핵심을 정리하면 이렇습니다. 기본 트리플은 <strong>사실을 진술</strong>합니다. Reification은 <strong>그 진술 자체를 자원으로 만들어</strong>, 진술에 대한 진술을 가능하게 합니다. 이것이 “트리플에 대해 말하기”의 본질입니다.</p>

<h2 id="4-김철수의-이직-기록-reification-적용">4. 김철수의 이직 기록: Reification 적용</h2>

<p>이제 처음의 문제를 Reification으로 완전히 해결해 보겠습니다.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">@prefix</span><span class="w"> </span><span class="nn">rdf</span><span class="p">:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="w">  </span><span class="nl">&lt;http://example.org/&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="kd">@prefix</span><span class="w"> </span><span class="nn">xsd</span><span class="p">:</span><span class="w"> </span><span class="nl">&lt;http://www.w3.org/2001/XMLSchema#&gt;</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="c1"># ── 개체 정의 ──</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Person</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">name</span><span class="w"> </span><span class="s2">"김철수"</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Company</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">name</span><span class="w"> </span><span class="s2">"삼성전자"</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">Company</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">name</span><span class="w"> </span><span class="s2">"네이버"</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="c1"># ── 진술 1: 삼성전자 근무 (2020–2022) ──</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="nt">stmt1</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">Statement</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">subject</span><span class="w">   </span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">predicate</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">object</span><span class="w">    </span><span class="nn">ex</span><span class="p">:</span><span class="err">삼성전자</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">startDate</span><span class="w">  </span><span class="s2">"2020-01-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">endDate</span><span class="w">    </span><span class="s2">"2022-12-31"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">source</span><span class="w">     </span><span class="nn">ex</span><span class="p">:</span><span class="nt">HR_Database</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">status</span><span class="w">     </span><span class="s2">"과거"</span><span class="w"> </span><span class="p">.</span><span class="w">

</span><span class="c1"># ── 진술 2: 네이버 근무 (2023–현재) ──</span><span class="w">
</span><span class="nn">ex</span><span class="p">:</span><span class="nt">stmt2</span><span class="w"> </span><span class="kt">a</span><span class="w"> </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">Statement</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">subject</span><span class="w">   </span><span class="nn">ex</span><span class="p">:</span><span class="err">김철수</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">predicate</span><span class="w"> </span><span class="nn">ex</span><span class="p">:</span><span class="nt">worksAt</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">rdf</span><span class="p">:</span><span class="nt">object</span><span class="w">    </span><span class="nn">ex</span><span class="p">:</span><span class="err">네이버</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">startDate</span><span class="w">  </span><span class="s2">"2023-03-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">source</span><span class="w">     </span><span class="nn">ex</span><span class="p">:</span><span class="nt">HR_Database</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">status</span><span class="w">     </span><span class="s2">"현재"</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p>이제 처음의 세 가지 질문에 모두 답할 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>질문</th>
      <th>SPARQL 조건</th>
      <th>답변</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>지금 어디에서 일하는가?</td>
      <td><code class="language-plaintext highlighter-rouge">?s ex:status "현재"</code> 필터</td>
      <td>네이버</td>
    </tr>
    <tr>
      <td>삼성전자에는 언제까지?</td>
      <td><code class="language-plaintext highlighter-rouge">stmt1</code>의 <code class="language-plaintext highlighter-rouge">ex:endDate</code> 조회</td>
      <td>2022-12-31</td>
    </tr>
    <tr>
      <td>동시 근무인가?</td>
      <td><code class="language-plaintext highlighter-rouge">startDate</code>/<code class="language-plaintext highlighter-rouge">endDate</code> 비교</td>
      <td>순차 근무 (기간 겹침 없음)</td>
    </tr>
  </tbody>
</table>

<h2 id="5-시각화-reification이-만드는-구조">5. 시각화: Reification이 만드는 구조</h2>

<p>아래 다이어그램은 Reification이 적용된 김철수의 이직 기록을 시각화한 것입니다. 각 진술(Statement)이 하나의 자원으로 존재하며, 원래 트리플의 주어·술어·목적어를 가리키는 동시에 시간 정보를 품고 있는 구조를 보여줍니다.</p>

<figure style="margin: 1.5rem 0;">
<svg viewBox="0 0 820 540" width="100%" height="auto" role="img" aria-label="Reification 적용: 진술에 시간 맥락을 부여한 Knowledge Graph" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aStmt" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#7c3aed" />
    </marker>
    <marker id="aMeta" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#d97706" />
    </marker>
    <style>
      .bg-sol { fill: #faf5ff; }
      .card-stmt { fill: #fef3c7; stroke: #f59e0b; stroke-width: 2; }
      .nd-per { fill: #dbeafe; stroke: #3b82f6; stroke-width: 2; }
      .nd-com { fill: #dcfce7; stroke: #22c55e; stroke-width: 2; }
      .nd-pred { fill: #ede9fe; stroke: #7c3aed; stroke-width: 2; }
      .nd-lit { fill: #f3f4f6; stroke: #9ca3af; stroke-width: 1.5; }
      .e-stmt { stroke: #7c3aed; stroke-width: 1.8; fill: none; marker-end: url(#aStmt); }
      .e-meta { stroke: #d97706; stroke-width: 1.8; fill: none; marker-end: url(#aMeta); }
      .t-h { font: 800 20px Pretendard, system-ui, sans-serif; fill: #111827; }
      .t-n { font: 700 14px Pretendard, system-ui, sans-serif; fill: #111827; text-anchor: middle; dominant-baseline: central; }
      .t-ns { font: 600 12px Pretendard, system-ui, sans-serif; fill: #111827; text-anchor: middle; dominant-baseline: central; }
      .t-e { font: 500 11px 'Roboto Mono', ui-monospace, monospace; fill: #6b7280; text-anchor: middle; }
      .t-sec { font: 700 14px Pretendard, system-ui, sans-serif; fill: #92400e; }
      .t-leg { font: 500 11px Pretendard, system-ui, sans-serif; fill: #6b7280; }
    </style>
  </defs>

  <rect class="bg-sol" width="820" height="540" rx="16" />
  <text class="t-h" x="28" y="36">Reification: 진술에 맥락을 부여하다</text>
  <line x1="28" y1="50" x2="380" y2="50" stroke="#7c3aed" stroke-width="3" />

  <!-- ═══ 진술 1 영역 ═══ -->
  <rect x="16" y="68" width="788" height="200" rx="14" fill="#fffbeb" stroke="#fde68a" stroke-width="1" />
  <text class="t-sec" x="32" y="92">진술 1 (stmt1)</text>

  <!-- stmt1 node -->
  <rect class="card-stmt" x="310" y="110" width="150" height="44" rx="14" />
  <text class="t-n" x="385" y="126">stmt1</text>
  <text class="t-ns" x="385" y="143">rdf:Statement</text>

  <!-- 김철수 -->
  <rect class="nd-per" x="50" y="116" width="110" height="36" rx="18" />
  <text class="t-n" x="105" y="134">김철수</text>

  <!-- worksAt -->
  <rect class="nd-pred" x="340" y="190" width="100" height="30" rx="12" />
  <text class="t-ns" x="390" y="205">ex:worksAt</text>

  <!-- 삼성전자 -->
  <rect class="nd-com" x="580" y="116" width="120" height="36" rx="18" />
  <text class="t-n" x="640" y="134">삼성전자</text>

  <!-- stmt1 → 김철수 (rdf:subject) -->
  <line class="e-stmt" x1="308" y1="132" x2="162" y2="134" />
  <text class="t-e" x="236" y="124">rdf:subject</text>

  <!-- stmt1 → worksAt (rdf:predicate) -->
  <line class="e-stmt" x1="385" y1="156" x2="390" y2="188" />
  <text class="t-e" x="415" y="174">rdf:predicate</text>

  <!-- stmt1 → 삼성전자 (rdf:object) -->
  <line class="e-stmt" x1="462" y1="132" x2="578" y2="134" />
  <text class="t-e" x="520" y="124">rdf:object</text>

  <!-- 시간 메타데이터 (stmt1) -->
  <rect class="nd-lit" x="50" y="196" width="120" height="50" rx="8" />
  <text class="t-ns" x="110" y="214">startDate</text>
  <text class="t-ns" x="110" y="232" fill="#6b7280">2020-01-01</text>

  <rect class="nd-lit" x="186" y="196" width="110" height="50" rx="8" />
  <text class="t-ns" x="241" y="214">endDate</text>
  <text class="t-ns" x="241" y="232" fill="#6b7280">2022-12-31</text>

  <line class="e-meta" x1="340" y1="156" x2="170" y2="198" />
  <line class="e-meta" x1="356" y1="156" x2="242" y2="196" />

  <!-- ═══ 진술 2 영역 ═══ -->
  <rect x="16" y="284" width="788" height="200" rx="14" fill="#eff6ff" stroke="#bfdbfe" stroke-width="1" />
  <text class="t-sec" x="32" y="308" fill="#1e40af">진술 2 (stmt2)</text>

  <!-- stmt2 node -->
  <rect class="card-stmt" x="310" y="326" width="150" height="44" rx="14" />
  <text class="t-n" x="385" y="342">stmt2</text>
  <text class="t-ns" x="385" y="359">rdf:Statement</text>

  <!-- 김철수 (shared) -->
  <rect class="nd-per" x="50" y="332" width="110" height="36" rx="18" />
  <text class="t-n" x="105" y="350">김철수</text>

  <!-- worksAt -->
  <rect class="nd-pred" x="340" y="406" width="100" height="30" rx="12" />
  <text class="t-ns" x="390" y="421">ex:worksAt</text>

  <!-- 네이버 -->
  <rect class="nd-com" x="580" y="332" width="120" height="36" rx="18" />
  <text class="t-n" x="640" y="350">네이버</text>

  <!-- stmt2 → 김철수 (rdf:subject) -->
  <line class="e-stmt" x1="308" y1="348" x2="162" y2="350" />
  <text class="t-e" x="236" y="340">rdf:subject</text>

  <!-- stmt2 → worksAt (rdf:predicate) -->
  <line class="e-stmt" x1="385" y1="372" x2="390" y2="404" />
  <text class="t-e" x="415" y="390">rdf:predicate</text>

  <!-- stmt2 → 네이버 (rdf:object) -->
  <line class="e-stmt" x1="462" y1="348" x2="578" y2="350" />
  <text class="t-e" x="520" y="340">rdf:object</text>

  <!-- 시간 메타데이터 (stmt2) -->
  <rect class="nd-lit" x="50" y="412" width="120" height="50" rx="8" />
  <text class="t-ns" x="110" y="430">startDate</text>
  <text class="t-ns" x="110" y="448" fill="#6b7280">2023-03-01</text>

  <rect class="nd-lit" x="186" y="412" width="110" height="50" rx="8" />
  <text class="t-ns" x="241" y="430">status</text>
  <text class="t-ns" x="241" y="448" fill="#059669">현재</text>

  <line class="e-meta" x1="340" y1="372" x2="170" y2="414" />
  <line class="e-meta" x1="356" y1="372" x2="242" y2="412" />

  <!-- ═══ 범례 ═══ -->
  <g transform="translate(540, 498)">
    <line x1="0" y1="8" x2="28" y2="8" stroke="#7c3aed" stroke-width="2" marker-end="url(#aStmt)" />
    <text class="t-leg" x="36" y="12">rdf 구조</text>
    <line x1="110" y1="8" x2="138" y2="8" stroke="#d97706" stroke-width="2" marker-end="url(#aMeta)" />
    <text class="t-leg" x="146" y="12">메타데이터</text>
  </g>
</svg>
</figure>

<p>진술 1(<code class="language-plaintext highlighter-rouge">stmt1</code>)과 진술 2(<code class="language-plaintext highlighter-rouge">stmt2</code>)가 각각 독립된 자원으로 존재하면서, <code class="language-plaintext highlighter-rouge">rdf:subject</code>, <code class="language-plaintext highlighter-rouge">rdf:predicate</code>, <code class="language-plaintext highlighter-rouge">rdf:object</code>로 원래 트리플의 구성 요소를 가리킵니다. 그리고 각 진술에 <code class="language-plaintext highlighter-rouge">startDate</code>, <code class="language-plaintext highlighter-rouge">endDate</code>, <code class="language-plaintext highlighter-rouge">status</code> 같은 시간 메타데이터가 자연스럽게 달려 있습니다.</p>

<h2 id="6-sparql로-질의하기">6. SPARQL로 질의하기</h2>

<p>Reification된 데이터는 표준 SPARQL로 자연스럽게 질의할 수 있습니다. 예를 들어 “김철수가 <strong>현재</strong> 근무하는 회사”를 찾으려면 다음과 같이 작성합니다.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">PREFIX</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="w"> </span><span class="nn">&lt;http://www.w3.org/1999/02/22-rdf-syntax-ns#&gt;</span><span class="w">
</span><span class="k">PREFIX</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="w">  </span><span class="nn">&lt;http://example.org/&gt;</span><span class="w">

</span><span class="k">SELECT</span><span class="w"> </span><span class="nv">?company</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?stmt</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">Statement</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">subject</span><span class="w">   </span><span class="nn">ex</span><span class="o">:</span><span class="ss">김철수</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">predicate</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">worksAt</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">object</span><span class="w">    </span><span class="nv">?company</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">ex</span><span class="o">:</span><span class="ss">status</span><span class="w">     </span><span class="s2">"현재"</span><span class="w"> </span><span class="p">.</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>결과는 <code class="language-plaintext highlighter-rouge">ex:네이버</code>입니다. 중간 노드 방식(2.3절)에서 필요했던 2-hop 경로와 달리, 원래 트리플의 의미(<code class="language-plaintext highlighter-rouge">worksAt</code>)가 <code class="language-plaintext highlighter-rouge">rdf:predicate</code>로 명시적으로 보존되어 있으므로 온톨로지의 의미 구조가 유지됩니다.</p>

<p>“2021년 기준으로 김철수가 재직 중이던 회사”처럼 시간 범위 질의도 가능합니다.</p>

<div class="language-sparql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span><span class="w"> </span><span class="nv">?company</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">?stmt</span><span class="w"> </span><span class="k">a</span><span class="w"> </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">Statement</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">subject</span><span class="w">   </span><span class="nn">ex</span><span class="o">:</span><span class="ss">김철수</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">predicate</span><span class="w"> </span><span class="nn">ex</span><span class="o">:</span><span class="ss">worksAt</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">rdf</span><span class="o">:</span><span class="ss">object</span><span class="w">    </span><span class="nv">?company</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">ex</span><span class="o">:</span><span class="ss">startDate</span><span class="w">  </span><span class="nv">?start</span><span class="w"> </span><span class="p">;</span><span class="w">
          </span><span class="nn">ex</span><span class="o">:</span><span class="ss">endDate</span><span class="w">    </span><span class="nv">?end</span><span class="w"> </span><span class="p">.</span><span class="w">
    </span><span class="k">FILTER</span><span class="w"> </span><span class="p">(</span><span class="nv">?start</span><span class="w"> </span><span class="nn">&lt;= "2021-06-01"^^xsd:date &amp;&amp; ?end &gt;</span><span class="p">=</span><span class="w"> </span><span class="s2">"2021-06-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="o">:</span><span class="ss">date</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h2 id="7-reification의-비용과-현대적-대안">7. Reification의 비용과 현대적 대안</h2>

<h3 id="71-트리플-폭발-문제">7.1 트리플 폭발 문제</h3>

<p>Reification은 강력하지만, 하나의 트리플을 표현하기 위해 <strong>최소 4개의 트리플</strong>(<code class="language-plaintext highlighter-rouge">rdf:type</code>, <code class="language-plaintext highlighter-rouge">rdf:subject</code>, <code class="language-plaintext highlighter-rouge">rdf:predicate</code>, <code class="language-plaintext highlighter-rouge">rdf:object</code>)이 필요합니다. 시간 정보까지 더하면 6~7개가 됩니다. 원본 트리플 1개 당 7배의 저장 공간을 소비하는 셈입니다.</p>

<table>
  <thead>
    <tr>
      <th>표현 방식</th>
      <th>트리플 수 (진술 1개당)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>기본 트리플</td>
      <td>1</td>
    </tr>
    <tr>
      <td>Reification (구조만)</td>
      <td>4</td>
    </tr>
    <tr>
      <td>Reification + 시간 메타데이터</td>
      <td>6–7</td>
    </tr>
  </tbody>
</table>

<p>대규모 Knowledge Graph에서 모든 트리플을 Reify하면 저장 공간과 쿼리 성능에 부담을 줄 수 있습니다. 따라서 <strong>시간에 따라 변하거나 출처 추적이 필요한 트리플만 선택적으로 Reify</strong>하는 것이 실무적인 접근입니다.</p>

<h3 id="72-rdf-star-간결한-대안">7.2 RDF-star: 간결한 대안</h3>

<p>이러한 트리플 폭발 문제를 해결하기 위해 <strong>RDF-star</strong><a class="citation" href="#rdf12concepts">[2]</a>가 제안되었습니다. RDF-star에서는 트리플을 중첩(Nesting)하여 직접 메타데이터를 붙일 수 있습니다.</p>

<div class="language-turtle highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">&lt;&lt; ex:김철수 ex:worksAt ex:삼성전자 &gt;</span><span class="err">&gt;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">startDate</span><span class="w"> </span><span class="s2">"2020-01-01"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">;</span><span class="w">
    </span><span class="nn">ex</span><span class="p">:</span><span class="nt">endDate</span><span class="w">   </span><span class="s2">"2022-12-31"</span><span class="o">^^</span><span class="nn">xsd</span><span class="p">:</span><span class="nt">date</span><span class="w"> </span><span class="p">.</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">&lt;&lt; ... &gt;&gt;</code> 안에 원래 트리플을 넣고, 바로 그 트리플에 대한 속성을 추가합니다. 내부적으로는 Reification과 동일한 의미를 가지지만, 문법이 훨씬 간결하고 트리플 수도 줄어듭니다. RDF-star는 W3C의 <strong>RDF 1.2</strong> 표준에 반영되어, 기존 Reification의 실질적인 후속 기법으로 자리 잡아가고 있습니다.</p>

<h2 id="8-마치며">8. 마치며</h2>

<p>RDF 트리플은 “무엇이 어떻다”라는 사실을 간결하게 표현하는 데 최적화되어 있습니다. 하지만 현실 세계의 사실은 항상 맥락을 동반합니다. <strong>언제</strong> 그랬는지, <strong>누가</strong> 그렇게 말했는지, <strong>얼마나 확실한지</strong>. Reification은 이 맥락을 RDF의 틀 안에서 표현하는 표준적인 방법입니다.</p>

<p>시간에 따라 변하는 데이터를 다룰 때, 기본 트리플의 한계에 부딪혔다면 Reification을 떠올려 보세요. 트리플 하나하나가 “자원”이 되는 순간, Knowledge Graph는 비로소 시간의 흐름을 기록할 수 있게 됩니다.</p>

<hr />

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/ontology/2026/01/25/spo-decomposition-solution/">S-P-O 분해 연습문제 풀이: 전력 모듈-센서 노이즈 간섭</a></li>
  <li><a href="/ontology/2026/01/16/icalendar-format-and-ontology/">iCalendar의 진화: 텍스트에서 의미론적 웹으로</a></li>
</ul>

<h2 id="참고-문헌">참고 문헌</h2>

<ol class="bibliography"><li>R. Cyganiak, D. Wood, and M. Lanthaler, “RDF 1.1 Concepts and Abstract Syntax,” W3C, W3C Recommendation, Feb. 2014. Available at: https://www.w3.org/TR/rdf11-concepts/
</li>
<li>R. Cyganiak, D. Wood, M. Lanthaler, O. Hartig, and P.-A. Champin, “RDF 1.2 Concepts and Abstract Syntax,” W3C, W3C Working Draft, 2024. Available at: https://www.w3.org/TR/rdf12-concepts/
</li></ol>]]></content><author><name>Justin Kim</name></author><category term="ontology" /><category term="RDF" /><category term="Reification" /><category term="Ontology" /><category term="Knowledge Graph" /><summary type="html"><![CDATA[RDF 트리플은 '누가 무엇이다'만 말할 수 있을 뿐, '언제부터 언제까지'라는 맥락을 담지 못합니다. Reification을 통해 트리플 자체를 자원으로 만들어 시간과 출처 같은 메타데이터를 부여하는 방법을 알아봅니다.]]></summary></entry><entry><title type="html">Apache Arrow를 Timely Dataflow에 적용하기 위한 계획</title><link href="https://groou.com/research/2026/02/22/arrow-for-timely-dataflow/" rel="alternate" type="text/html" title="Apache Arrow를 Timely Dataflow에 적용하기 위한 계획" /><published>2026-02-22T00:00:00+09:00</published><updated>2026-02-22T00:00:00+09:00</updated><id>https://groou.com/research/2026/02/22/arrow-for-timely-dataflow</id><content type="html" xml:base="https://groou.com/research/2026/02/22/arrow-for-timely-dataflow/"><![CDATA[<p><a href="/essay/research/2026/02/17/timely-dataflow-protocol/">이전 글</a>에서 Timely Dataflow를 위한 전송 프로토콜을 왜 새로 설계해야 하는지, 그리고 5개 레이어 구조의 밑그림을 그렸습니다. 이번 글에서는 그 설계의 한 축이 될 수 있는 <strong>Apache Arrow</strong>를 살펴보고, 구체적으로 어디에 어떻게 적용할 수 있을지 계획을 세워봅니다.</p>

<h2 id="apache-arrow란-무엇인가">Apache Arrow란 무엇인가</h2>

<p>Apache Arrow<a class="citation" href="#ArrowSpec">[1]</a>는 언어 중립적인 <strong>컬럼 기반 인메모리 데이터 포맷</strong>입니다. 핵심 아이디어는 단순합니다: 데이터를 행(row) 단위가 아니라 열(column) 단위로 메모리에 배치하면, 분석 워크로드에서 극적인 성능 향상을 얻을 수 있다는 것입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>행 기반 (Row-oriented):             컬럼 기반 (Column-oriented):
┌──────┬──────┬──────┐             ┌──────┬──────┬──────┐
│ key₁ │ ts₁  │ diff₁│             │ key₁ │ key₂ │ key₃ │  ← keys 버퍼
├──────┼──────┼──────┤             ├──────┼──────┼──────┤
│ key₂ │ ts₂  │ diff₂│             │ ts₁  │ ts₂  │ ts₃  │  ← timestamps 버퍼
├──────┼──────┼──────┤             ├──────┼──────┼──────┤
│ key₃ │ ts₃  │ diff₃│             │ diff₁│ diff₂│ diff₃│  ← diffs 버퍼
└──────┴──────┴──────┘             └──────┴──────┴──────┘
</code></pre></div></div>

<p>컬럼 기반 레이아웃의 이점은 세 가지입니다.</p>

<ol>
  <li>
    <p><strong>캐시 친화성.</strong> <code class="language-plaintext highlighter-rouge">diff</code> 컬럼만 합산할 때, 행 기반에서는 <code class="language-plaintext highlighter-rouge">key</code>와 <code class="language-plaintext highlighter-rouge">ts</code>를 건너뛰며 캐시 라인을 낭비하지만, 컬럼 기반에서는 <code class="language-plaintext highlighter-rouge">diff</code> 값만 연속으로 읽습니다. 순차 스캔에서 약 <strong>25배의 캐시 효율</strong> 차이가 발생합니다.</p>
  </li>
  <li>
    <p><strong>SIMD 벡터화.</strong> 동일 타입의 값이 연속 메모리에 있으면 AVX2/AVX-512 명령어로 한 번에 8~16개 값을 처리할 수 있습니다. Arrow의 compute 커널은 산술 연산에서 <strong>4~8배</strong>, 비교 연산에서 <strong>5~10배</strong>의 SIMD 가속을 달성합니다.</p>
  </li>
  <li>
    <p><strong>직렬화 제거.</strong> Arrow 포맷 자체가 와이어 포맷이 되므로, 프로세스 간 데이터 교환 시 직렬화/역직렬화가 필요 없습니다. IPC 공유 메모리 경로에서 <strong>10μs</strong> 지연으로 <strong>0회 복사</strong> 전달이 가능합니다.</p>
  </li>
</ol>

<p>Arrow의 메모리 레이아웃은 정밀하게 규격화되어 있습니다. 모든 버퍼는 <strong>64바이트 캐시 라인 경계</strong>에 정렬되고, 각 컬럼은 유효성 비트맵(validity bitmap) + 오프셋 버퍼 + 데이터 버퍼의 3중 구조를 가집니다. 이 규격의 총 메모리 오버헤드는 약 <strong>3%</strong>(정렬 패딩 1.6% + 유효성 비트맵 1.25%)입니다.</p>

<h2 id="왜-arrow인가--differential-dataflow의-관점에서">왜 Arrow인가 — Differential Dataflow의 관점에서</h2>

<p>Differential Dataflow<a class="citation" href="#McSherry2013">[2]</a>의 핵심 자료구조는 델타 배치(delta batch)입니다. <code class="language-plaintext highlighter-rouge">(data, time, diff)</code> 트리플의 모음이며, 이것이 워커 간에 교환되고, 컴팩션(consolidation)을 거쳐 누적됩니다.</p>

<p>현재 Timely Dataflow의 Rust 구현체는 이 델타 배치를 <strong>Abomonation</strong><a class="citation" href="#Abomonation">[3]</a>으로 직렬화합니다. Abomonation은 Rust 구조체의 메모리 레이아웃을 그대로 바이트 배열로 변환하는 제로카피 라이브러리입니다. 역직렬화가 사실상 <code class="language-plaintext highlighter-rouge">transmute</code>(바이트 재해석)이므로, 속도는 경이적입니다 — 직렬화 62,717 MB/s, 역직렬화 4,108,000 MB/s.</p>

<p>하지만 Abomonation에는 구조적 한계가 있습니다.</p>

<ul>
  <li><strong>안전성.</strong> <code class="language-plaintext highlighter-rouge">unsafe</code> 기반의 <code class="language-plaintext highlighter-rouge">transmute</code>로, 정의되지 않은 동작(UB)의 위험이 있습니다<a class="citation" href="#RUSTSEC2021_0120">[4]</a>.</li>
  <li><strong>Rust 전용.</strong> C, Python, Java 등 다른 언어에서 Abomonation 포맷을 읽을 수 없습니다.</li>
  <li><strong>스키마 진화 불가.</strong> 필드 하나를 추가하면 와이어 포맷이 깨집니다.</li>
  <li><strong>컴팩션 비효율.</strong> 행 기반 레이아웃이라 <code class="language-plaintext highlighter-rouge">diff</code> 컬럼만 합산하려 해도 전체 행을 순회해야 합니다.</li>
</ul>

<p>Arrow는 이 문제들을 해결합니다. 특히 컴팩션 — Differential Dataflow에서 CPU 시간의 상당 부분을 차지하는 연산 — 에서 컬럼 기반 레이아웃의 이점이 큽니다.</p>

<h2 id="47행의-경계--arrow가-이기는-지점과-지는-지점">47행의 경계 — Arrow가 이기는 지점과 지는 지점</h2>

<p>Arrow가 Abomonation보다 항상 나은 것은 아닙니다. 배치 크기에 따라 명확한 <strong>교차점</strong>이 존재합니다.</p>

<p>Arrow RecordBatch에는 고정 비용이 있습니다: 스키마 메타데이터, 버퍼 정렬 패딩, 유효성 비트맵. 이 고정 비용은 배치가 클수록 행당 비용으로 희석되지만, 배치가 작으면 지배적이 됩니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">배치 크기</th>
      <th style="text-align: right">Arrow (행당)</th>
      <th style="text-align: right">Abomonation (행당)</th>
      <th style="text-align: right">비율</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">1행</td>
      <td style="text-align: right">1,280 B</td>
      <td style="text-align: right">96 B</td>
      <td style="text-align: right">Arrow 13.3x 큼</td>
    </tr>
    <tr>
      <td style="text-align: right">10행</td>
      <td style="text-align: right">224 B</td>
      <td style="text-align: right">96 B</td>
      <td style="text-align: right">Arrow 2.3x 큼</td>
    </tr>
    <tr>
      <td style="text-align: right"><strong>47행</strong></td>
      <td style="text-align: right"><strong>96 B</strong></td>
      <td style="text-align: right"><strong>96 B</strong></td>
      <td style="text-align: right"><strong>교차점</strong></td>
    </tr>
    <tr>
      <td style="text-align: right">100행</td>
      <td style="text-align: right">82 B</td>
      <td style="text-align: right">88 B</td>
      <td style="text-align: right">Arrow 7% 작음</td>
    </tr>
    <tr>
      <td style="text-align: right">1,000행</td>
      <td style="text-align: right">70 B</td>
      <td style="text-align: right">88 B</td>
      <td style="text-align: right">Arrow 20% 작음</td>
    </tr>
    <tr>
      <td style="text-align: right">1,000,000행</td>
      <td style="text-align: right">69 B</td>
      <td style="text-align: right">88 B</td>
      <td style="text-align: right">Arrow 22% 작음</td>
    </tr>
  </tbody>
</table>

<p><strong>47행</strong>이 교차점입니다. 이보다 작으면 Abomonation이 작고, 이보다 크면 Arrow가 작습니다.</p>

<p>Differential Dataflow의 델타 배치는 <strong>이중 모드(bimodal) 분포</strong>를 따릅니다. 초기 적재(bulk load)에서는 수백만 행의 배치가 생성되고, 증분 업데이트에서는 1~100행 수준입니다. 이 분포의 두 봉우리가 교차점의 양쪽에 놓인다는 점이 핵심입니다.</p>

<p>이것은 두 가지 중요한 결론을 암시합니다:</p>

<ol>
  <li><strong>핫 패스(증분 업데이트, 1~100행)</strong>: Abomonation 스타일의 행 기반 포맷이 여전히 유리합니다.</li>
  <li><strong>콜드 패스(컴팩션된 스파인, 1,000행 이상)</strong>: Arrow 컬럼 포맷이 공간 효율과 분석 성능 모두에서 이깁니다.</li>
</ol>

<p>따라서 <strong>하이브리드 전략</strong>이 필요합니다.</p>

<h2 id="하이브리드-설계-행과-열의-공존">하이브리드 설계: 행과 열의 공존</h2>

<svg viewBox="0 0 640 420" xmlns="http://www.w3.org/2000/svg" style="max-width:640px;width:100%;font-family:'Pretendard','Manrope',sans-serif;">
  <style>
    .box { stroke-width: 1.5; rx: 5; }
    .hot { fill: #fff3e0; stroke: #e65100; }
    .cold { fill: #e8eaf6; stroke: #283593; }
    .conv { fill: #e8f5e9; stroke: #2e7d32; }
    .wire { fill: #fce4ec; stroke: #c62828; }
    .label { font-size: 13px; font-weight: 700; fill: #212529; }
    .desc { font-size: 11px; fill: #495057; }
    .note { font-size: 10px; fill: #757575; font-style: italic; }
    .arrow { stroke: #616161; stroke-width: 1.5; marker-end: url(#ah); fill: none; }
    .dasharrow { stroke: #9e9e9e; stroke-width: 1; stroke-dasharray: 5,3; marker-end: url(#ah2); fill: none; }
  </style>
  <defs>
    <marker id="ah" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
      <path d="M0,0 L8,3 L0,6" fill="#616161" />
    </marker>
    <marker id="ah2" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
      <path d="M0,0 L8,3 L0,6" fill="#9e9e9e" />
    </marker>
  </defs>
  <!-- Title -->
  <text class="label" x="20" y="24" font-size="14">델타 배치의 생애주기와 포맷 전환</text>
  <!-- Hot path -->
  <rect class="box hot" x="20" y="40" width="280" height="80" />
  <text class="label" x="36" y="62" fill="#e65100">핫 패스 (행 기반)</text>
  <text class="desc" x="36" y="80">오퍼레이터 출력 → 델타 배치 (1~100행)</text>
  <text class="desc" x="36" y="96">packed struct · Abomonation 스타일</text>
  <text class="note" x="36" y="112">지연 최소화: 직렬화 0ns</text>
  <!-- Arrow to conversion -->
  <path class="arrow" d="M160,120 L160,150" />
  <!-- Conversion point -->
  <rect class="box conv" x="60" y="150" width="200" height="60" />
  <text class="label" x="76" y="172" fill="#2e7d32">컴팩션 경계</text>
  <text class="desc" x="76" y="190">행 → 컬럼 변환 (Arrow RecordBatch)</text>
  <text class="note" x="76" y="204">배치 ≥ 47행일 때 Arrow가 유리</text>
  <!-- Arrow to cold path -->
  <path class="arrow" d="M160,210 L160,240" />
  <!-- Cold path -->
  <rect class="box cold" x="20" y="240" width="280" height="80" />
  <text class="label" x="36" y="262" fill="#283593">콜드 패스 (컬럼 기반)</text>
  <text class="desc" x="36" y="280">컴팩션된 스파인 · Arrow RecordBatch</text>
  <text class="desc" x="36" y="296">SIMD 컴팩션 · Dictionary 인코딩</text>
  <text class="note" x="36" y="312">공간 22% 절감, 분석 10~100x 가속</text>
  <!-- Wire format -->
  <rect class="box wire" x="360" y="100" width="260" height="100" />
  <text class="label" x="376" y="122" fill="#c62828">와이어 포맷 (UDP)</text>
  <text class="desc" x="376" y="142">L1~L3: 16B packed header (C)</text>
  <text class="desc" x="376" y="158">L4 페이로드: Arrow IPC 또는</text>
  <text class="desc" x="376" y="174">packed struct (배치 크기에 따라)</text>
  <text class="note" x="376" y="192">FPGA: packed struct → DMA 직접 전달</text>
  <!-- Hot to wire -->
  <path class="arrow" d="M300,80 L360,130" />
  <!-- Cold to wire -->
  <path class="dasharrow" d="M300,270 L360,180" />
  <!-- IPC -->
  <rect class="box cold" x="360" y="260" width="260" height="70" />
  <text class="label" x="376" y="282" fill="#283593">프로세스 간 교환</text>
  <text class="desc" x="376" y="300">Arrow IPC (mmap) · 0 복사</text>
  <text class="note" x="376" y="316">C Data Interface로 Rust ↔ C 전달</text>
  <!-- Cold to IPC -->
  <path class="arrow" d="M300,290 L360,290" />
  <!-- Persistence -->
  <rect class="box cold" x="360" y="345" width="260" height="55" />
  <text class="label" x="376" y="367" fill="#283593">영속 저장</text>
  <text class="desc" x="376" y="385">Arrow IPC / Parquet · 체크포인트</text>
  <!-- Cold to persistence -->
  <path class="dasharrow" d="M210,320 L210,372 L360,372" />
</svg>

<p>핵심 원칙은 이것입니다: <strong>포맷 전환은 컴팩션 경계에서 한 번만 일어난다.</strong> 핫 패스에서는 packed struct의 제로카피 속도를 유지하고, 컴팩션을 거쳐 배치 크기가 충분히 커진 시점에서 Arrow 컬럼 포맷으로 전환합니다.</p>

<h2 id="differential-dataflow와-arrow의-계층-구조">Differential Dataflow와 Arrow의 계층 구조</h2>

<p>Arrow를 Timely Dataflow 프로토콜 스택에 통합하면, Differential Dataflow와의 관계는 다음과 같은 계층 구조가 됩니다.</p>

<svg viewBox="0 0 620 500" xmlns="http://www.w3.org/2000/svg" style="max-width:620px;width:100%;font-family:'Pretendard','Manrope',sans-serif;">
  <style>
    .layer { stroke-width: 1.5; rx: 5; }
    .dd  { fill: #ede7f6; stroke: #4527a0; }
    .td  { fill: #e3f2fd; stroke: #1565c0; }
    .arrow-l { fill: #fff8e1; stroke: #f57f17; }
    .proto { fill: #e8f5e9; stroke: #2e7d32; }
    .hw  { fill: #fce4ec; stroke: #c62828; }
    .lbl { font-size: 13px; font-weight: 700; }
    .sub { font-size: 11px; fill: #424242; }
    .side { font-size: 10px; fill: #757575; font-style: italic; }
    .conn { stroke: #78909c; stroke-width: 1.2; marker-end: url(#tr); fill: none; }
    .bidi { stroke: #ff8f00; stroke-width: 1.2; stroke-dasharray: 4,3; fill: none; }
  </style>
  <defs>
    <marker id="tr" markerWidth="7" markerHeight="5" refX="7" refY="2.5" orient="auto">
      <path d="M0,0 L7,2.5 L0,5" fill="#78909c" />
    </marker>
  </defs>
  <!-- DD Layer -->
  <rect class="layer dd" x="40" y="20" width="460" height="64" />
  <text class="lbl" x="56" y="44" fill="#4527a0">Differential Dataflow</text>
  <text class="sub" x="56" y="62">증분 계산 엔진 · (data, time, diff) 컬렉션 · 컴팩션 · 조인</text>
  <text class="side" x="510" y="44">Rust</text>
  <!-- TD Layer -->
  <rect class="layer td" x="40" y="100" width="460" height="64" />
  <text class="lbl" x="56" y="124" fill="#1565c0">Timely Dataflow</text>
  <text class="sub" x="56" y="142">데이터플로우 런타임 · 프론티어 · 워커 스케줄링 · 진행 추적</text>
  <text class="side" x="510" y="124">Rust</text>
  <!-- L5 Binding -->
  <rect class="layer td" x="40" y="180" width="460" height="50" />
  <text class="lbl" x="56" y="202" fill="#1565c0">L5: Dataflow Binding</text>
  <text class="sub" x="56" y="218">Rust FFI (extern "C") · Communication trait 구현</text>
  <text class="side" x="510" y="202">Rust</text>
  <!-- Arrow Layer -->
  <rect class="layer arrow-l" x="40" y="246" width="460" height="64" />
  <text class="lbl" x="56" y="268" fill="#f57f17">L4: Delta Batch Layer + Arrow</text>
  <text class="sub" x="56" y="286">nanoarrow C · RecordBatch 변환 · SIMD 컴팩션 · Dictionary 인코딩</text>
  <text class="side" x="510" y="268">C</text>
  <!-- Arrow C Data Interface annotation -->
  <rect x="520" y="222" width="90" height="36" rx="4" fill="#fff8e1" stroke="#f57f17" stroke-width="1" />
  <text font-size="9" x="530" y="236" fill="#f57f17" font-weight="600">Arrow C Data</text>
  <text font-size="9" x="530" y="249" fill="#f57f17" font-weight="600">Interface</text>
  <line class="bidi" x1="500" y1="210" x2="520" y2="232" />
  <line class="bidi" x1="500" y1="270" x2="520" y2="248" />
  <!-- Protocol Layers -->
  <rect class="layer proto" x="40" y="326" width="220" height="50" />
  <text class="lbl" x="56" y="348" fill="#2e7d32">L3: Progress</text>
  <text class="sub" x="56" y="364">프론티어 · 포인트스탬프</text>
  <text class="side" x="270" y="348">C</text>
  <rect class="layer proto" x="280" y="326" width="220" height="50" />
  <text class="lbl" x="296" y="348" fill="#2e7d32">L2: Channel</text>
  <text class="sub" x="296" y="364">멀티플렉싱 · 배압</text>
  <text class="side" x="510" y="348">C</text>
  <!-- Hardware Layer -->
  <rect class="layer hw" x="40" y="392" width="460" height="50" />
  <text class="lbl" x="56" y="414" fill="#c62828">L1: Wire Layer</text>
  <text class="sub" x="56" y="430">UDP · DPDK PMD · FPGA SmartNIC · 16B packed header</text>
  <text class="side" x="510" y="414">C</text>
  <!-- FPGA -->
  <rect class="layer hw" x="40" y="456" width="460" height="32" />
  <text class="sub" x="200" y="476" fill="#c62828" font-weight="600">하드웨어 (FPGA / NIC)</text>
  <!-- Connections -->
  <line class="conn" x1="270" y1="84" x2="270" y2="100" />
  <line class="conn" x1="270" y1="164" x2="270" y2="180" />
  <line class="conn" x1="270" y1="230" x2="270" y2="246" />
  <line class="conn" x1="160" y1="310" x2="160" y2="326" />
  <line class="conn" x1="390" y1="310" x2="390" y2="326" />
  <line class="conn" x1="160" y1="376" x2="160" y2="392" />
  <line class="conn" x1="390" y1="376" x2="390" y2="392" />
  <line class="conn" x1="270" y1="442" x2="270" y2="456" />
</svg>

<p>Arrow는 <strong>L4 (Delta Batch Layer)</strong>에 위치합니다. L1~L3은 이전 글에서 설계한 대로 순수 C로 유지합니다 — 이 레이어들은 FPGA HLS 합성 대상이고, Arrow의 컬럼 포맷과는 독립적으로 동작합니다. L5는 Rust FFI로 Differential Dataflow와 연결합니다.</p>

<p>L4와 L5 사이의 인터페이스는 <strong>Arrow C Data Interface</strong><a class="citation" href="#ArrowCDataInterface">[5]</a>가 담당합니다. <code class="language-plaintext highlighter-rouge">ArrowArray</code>(88바이트)와 <code class="language-plaintext highlighter-rouge">ArrowSchema</code>(72바이트) 두 개의 C 구조체만으로 Rust와 C 사이에서 RecordBatch를 복사 없이 전달합니다.</p>

<h2 id="c-구현체의-선택-glib인가-stdlib인가-아무것도-아닌가">C 구현체의 선택: GLib인가, stdlib인가, 아무것도 아닌가</h2>

<p>Arrow의 C 구현체를 선택하는 것은 생각보다 복잡한 문제입니다. 선택지가 세 가지 있습니다.</p>

<h3 id="apache-arrow-glib">Apache Arrow GLib</h3>

<p>Arrow GLib<a class="citation" href="#ArrowGLib">[6]</a>는 Arrow C++ 라이브러리의 GObject 기반 C 바인딩입니다. GLib<a class="citation" href="#GLib">[7]</a>의 타입 시스템(<code class="language-plaintext highlighter-rouge">GObject</code>), 메모리 관리(<code class="language-plaintext highlighter-rouge">g_malloc</code>/<code class="language-plaintext highlighter-rouge">g_free</code>), 컨테이너(<code class="language-plaintext highlighter-rouge">GArray</code>, <code class="language-plaintext highlighter-rouge">GHashTable</code>), 에러 처리(<code class="language-plaintext highlighter-rouge">GError</code>)를 활용합니다.</p>

<p>장점은 명확합니다. 풍부한 자료구조, 참조 카운팅 기반 메모리 관리, GObject Introspection을 통한 Python/JavaScript 바인딩 자동 생성. GNOME 생태계와의 통합도 매끄럽습니다.</p>

<p>하지만 우리의 맥락에서 치명적인 문제가 있습니다:</p>

<ul>
  <li><strong>C++ 의존성.</strong> Arrow GLib는 Arrow C++ 위의 래퍼입니다. C++ 런타임, 예외 처리, RTTI가 딸려옵니다. 이전 글에서 C를 선택한 이유 — FPGA HLS 합성, ABI 안정성, 런타임 의존성 제거 — 를 정면으로 위배합니다.</li>
  <li><strong>GLib 자체의 무게.</strong> GLib는 glibc 위에 또 하나의 추상 계층을 올립니다. <code class="language-plaintext highlighter-rouge">GMainLoop</code>, <code class="language-plaintext highlighter-rouge">GType</code> 등 우리가 필요 없는 인프라까지 포함하면 수 MB의 추가 의존성입니다.</li>
  <li><strong>HLS 합성 불가.</strong> <code class="language-plaintext highlighter-rouge">g_object_new()</code>, 가상 함수 테이블, 동적 타입 캐스팅 — GObject의 핵심 메커니즘은 FPGA 로직으로 합성할 수 없습니다.</li>
</ul>

<h3 id="c-stdlib만으로">C stdlib만으로</h3>

<p>반대편 극단은 C 표준 라이브러리(<code class="language-plaintext highlighter-rouge">stdlib.h</code>, <code class="language-plaintext highlighter-rouge">string.h</code>, <code class="language-plaintext highlighter-rouge">stdint.h</code>)만 사용하는 것입니다. <code class="language-plaintext highlighter-rouge">malloc</code>/<code class="language-plaintext highlighter-rouge">free</code>, <code class="language-plaintext highlighter-rouge">memcpy</code>, <code class="language-plaintext highlighter-rouge">qsort</code> — POSIX 환경이라면 어디서든 컴파일됩니다.</p>

<p>이 접근은 FPGA 경로에서는 이상적이지만, Arrow 포맷의 복잡성을 직접 구현해야 합니다. 가변 길이 버퍼의 오프셋 관리, 중첩 타입의 재귀적 버퍼 할당, Dictionary 인코딩의 해시 테이블 — 이 모든 것을 <code class="language-plaintext highlighter-rouge">malloc</code>과 포인터 산술로 처음부터 작성하는 것은 삽질의 냄새가 짙습니다.</p>

<h3 id="nanoarrow-제3의-길">Nanoarrow: 제3의 길</h3>

<p><strong>Nanoarrow</strong><a class="citation" href="#Nanoarrow">[8]</a>는 이 딜레마의 답이 될 수 있습니다. Apache Arrow 프로젝트의 일부로 개발된, Arrow C Data Interface와 C Stream Interface의 <strong>최소 구현체</strong>입니다.</p>

<p>핵심 특성:</p>

<ul>
  <li><strong>~100KB.</strong> GLib 없이, C++ 없이, 순수 C로 약 100KB입니다.</li>
  <li><strong>Arrow C Data Interface 호환.</strong> <code class="language-plaintext highlighter-rouge">ArrowArray</code>, <code class="language-plaintext highlighter-rouge">ArrowSchema</code> 구조체를 직접 생산하고 소비합니다.</li>
  <li><strong>의존성 없음.</strong> C99 표준 라이브러리만 필요합니다.</li>
  <li><strong>헤더 온리 모드.</strong> 단일 <code class="language-plaintext highlighter-rouge">.h</code> 파일로 인클루드 가능합니다.</li>
</ul>

<p>Nanoarrow가 제공하는 것과 제공하지 않는 것:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">기능</th>
      <th style="text-align: center">Nanoarrow</th>
      <th style="text-align: center">Arrow GLib</th>
      <th style="text-align: center">직접 구현</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">RecordBatch 생성/소비</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">직접</td>
    </tr>
    <tr>
      <td style="text-align: left">Arrow C Data Interface</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">직접</td>
    </tr>
    <tr>
      <td style="text-align: left">IPC 읽기/쓰기</td>
      <td style="text-align: center">O (nanoarrow_ipc)</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">직접</td>
    </tr>
    <tr>
      <td style="text-align: left">Dictionary 인코딩</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">직접</td>
    </tr>
    <tr>
      <td style="text-align: left">Compute 커널 (SIMD)</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">직접</td>
    </tr>
    <tr>
      <td style="text-align: left">FPGA HLS 합성 가능</td>
      <td style="text-align: center">부분적</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">O</td>
    </tr>
    <tr>
      <td style="text-align: left">GObject/GLib 의존성</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center"><strong>필수</strong></td>
      <td style="text-align: center">X</td>
    </tr>
    <tr>
      <td style="text-align: left">C++ 의존성</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center"><strong>필수</strong></td>
      <td style="text-align: center">X</td>
    </tr>
    <tr>
      <td style="text-align: left">코드 크기</td>
      <td style="text-align: center">~100KB</td>
      <td style="text-align: center">~50MB</td>
      <td style="text-align: center">가변</td>
    </tr>
  </tbody>
</table>

<p>Nanoarrow를 L4의 기반으로 사용하고, SIMD 컴팩션 커널은 C로 직접 구현하며, FPGA 오프로드 경로의 가장 안쪽 루프만 순수 C stdlib로 작성하는 것이 현실적인 전략입니다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">"nanoarrow/nanoarrow.h"</span><span class="cp">
</span>
<span class="c1">// Nanoarrow로 델타 배치를 Arrow RecordBatch로 변환</span>
<span class="kt">int</span> <span class="nf">delta_batch_to_arrow</span><span class="p">(</span><span class="k">const</span> <span class="n">delta_tuple_t</span> <span class="o">*</span><span class="n">tuples</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">n</span><span class="p">,</span>
                         <span class="k">struct</span> <span class="n">ArrowArray</span> <span class="o">*</span><span class="n">out_array</span><span class="p">,</span>
                         <span class="k">struct</span> <span class="n">ArrowSchema</span> <span class="o">*</span><span class="n">out_schema</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">struct</span> <span class="n">ArrowSchemaView</span> <span class="n">schema_view</span><span class="p">;</span>

    <span class="c1">// 스키마 정의: key(binary), time(fixed_size_list&lt;uint32&gt;), diff(int64)</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaInitFromType</span><span class="p">(</span><span class="n">out_schema</span><span class="p">,</span> <span class="n">NANOARROW_TYPE_STRUCT</span><span class="p">));</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaAllocateChildren</span><span class="p">(</span><span class="n">out_schema</span><span class="p">,</span> <span class="mi">3</span><span class="p">));</span>

    <span class="n">ArrowSchemaInit</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaSetType</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">NANOARROW_TYPE_BINARY</span><span class="p">));</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaSetName</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="s">"key"</span><span class="p">));</span>

    <span class="n">ArrowSchemaInit</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">1</span><span class="p">]);</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaSetType</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">NANOARROW_TYPE_UINT64</span><span class="p">));</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaSetName</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="s">"time"</span><span class="p">));</span>

    <span class="n">ArrowSchemaInit</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaSetType</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="n">NANOARROW_TYPE_INT64</span><span class="p">));</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowSchemaSetName</span><span class="p">(</span><span class="n">out_schema</span><span class="o">-&gt;</span><span class="n">children</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span> <span class="s">"diff"</span><span class="p">));</span>

    <span class="c1">// ArrowArray 빌더로 데이터 채우기</span>
    <span class="k">struct</span> <span class="n">ArrowArray</span> <span class="n">tmp</span><span class="p">;</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowArrayInitFromSchema</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tmp</span><span class="p">,</span> <span class="n">out_schema</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">));</span>
    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowArrayStartAppending</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tmp</span><span class="p">));</span>

    <span class="k">for</span> <span class="p">(</span><span class="kt">size_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">n</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowArrayAppendNull</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tmp</span><span class="p">,</span> <span class="mi">0</span><span class="p">));</span>  <span class="c1">// struct validity</span>
        <span class="c1">// ... 각 컬럼에 값 추가</span>
    <span class="p">}</span>

    <span class="n">NANOARROW_RETURN_NOT_OK</span><span class="p">(</span><span class="n">ArrowArrayFinishBuildingDefault</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tmp</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">));</span>
    <span class="n">ArrowArrayMove</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tmp</span><span class="p">,</span> <span class="n">out_array</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">NANOARROW_OK</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="udp-기반-프로토콜에서-arrow-데이터-전송">UDP 기반 프로토콜에서 Arrow 데이터 전송</h2>

<p>이전 글에서 설계한 16바이트 <code class="language-plaintext highlighter-rouge">wire_hdr_t</code> 헤더는 그대로 유지합니다. Arrow는 L4 페이로드 포맷으로 동작하며, L1의 UDP 패킷 위에 올라갑니다. gRPC 기반의 Arrow Flight는 고려 대상이 아닙니다 — 데이터센터 내부의 워커 메시에서 gRPC의 HTTP/2 프레이밍과 TLS 오버헤드는 불필요합니다.</p>

<p>대신, Arrow IPC 스트리밍 포맷<a class="citation" href="#ArrowIPC">[9]</a>을 <strong>UDP 위에 직접</strong> 실어 보내는 방식을 구상하고 있습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────┐
│ UDP Datagram                                        │
│ ┌──────────────┬──────────────────────────────────┐ │
│ │ wire_hdr_t   │ Payload                          │ │
│ │ (16B, C)     │                                  │ │
│ │ ┌──────────┐ │ ┌──────────────────────────────┐ │ │
│ │ │epoch     │ │ │ 배치 ≥ 47행:                 │ │ │
│ │ │iteration │ │ │   Arrow IPC RecordBatch      │ │ │
│ │ │channel_id│ │ │   (FlatBuffers 메타데이터     │ │ │
│ │ │flags     │ │ │    + 컬럼 버퍼)              │ │ │
│ │ │seq_num   │ │ │                              │ │ │
│ │ │payload_le│ │ │ 배치 &lt; 47행:                 │ │ │
│ │ │checksum  │ │ │   packed struct (기존 방식)   │ │ │
│ │ └──────────┘ │ └──────────────────────────────┘ │ │
│ └──────────────┴──────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">wire_hdr_t</code>의 <code class="language-plaintext highlighter-rouge">flags</code> 필드에 1비트를 추가하여 페이로드가 Arrow IPC인지 packed struct인지 구분합니다. 수신 측은 이 플래그를 보고 적절한 디코더를 선택합니다.</p>

<p>이 설계에서 Arrow IPC의 FlatBuffers 메타데이터는 한 가지 우려를 만듭니다: 소규모 배치에서 메타데이터 비용이 커질 수 있습니다. 하지만 47행 이상의 배치만 Arrow IPC로 보내므로, 메타데이터 비율은 5% 미만으로 유지됩니다.</p>

<p>MTU(1,500바이트)를 초과하는 RecordBatch의 단편화도 L2 Channel Layer에서 처리합니다. Arrow IPC 메시지를 MTU 크기의 청크로 분할하고, 채널별 시퀀스 번호로 재조립합니다. 이것은 이미 L2가 담당하도록 설계된 영역입니다.</p>

<h2 id="dictionary-인코딩-differential-dataflow의-숨겨진-기회">Dictionary 인코딩: Differential Dataflow의 숨겨진 기회</h2>

<p>Arrow의 Dictionary 인코딩은 Differential Dataflow에서 예상 외의 큰 효과를 발휘할 수 있습니다.</p>

<p>Differential Dataflow의 많은 워크로드에서 <code class="language-plaintext highlighter-rouge">data</code> 키는 제한된 도메인(bounded domain)에서 반복됩니다. 그래프 연산에서 정점 ID, RDF 처리에서 URI 문자열, 로그 분석에서 이벤트 타입 — 고유 값의 수가 전체 행 수보다 훨씬 적습니다.</p>

<p>Dictionary 인코딩은 이런 패턴에서 <strong>90% 이상의 저장 공간 절감</strong>을 달성합니다. 10개의 고유 키가 1,000,000행에 반복될 때, 일반 인코딩 대비 90.7%를 절감합니다. 반면 키가 대부분 고유한 경우(1,000,000 고유 / 1,000,000 행)에는 오히려 14.7%의 패널티가 발생합니다.</p>

<p>이것은 컴팩션 연산에도 영향을 미칩니다. Dictionary로 인코딩된 키 컬럼에서 같은 키끼리 그룹화(group-by)할 때, 키 비교가 정수 비교로 환원됩니다. 가변 길이 문자열의 바이트 단위 비교 대신 <code class="language-plaintext highlighter-rouge">uint32_t</code> 비교 한 번 — 이것은 SIMD로 한 번에 8개씩 처리할 수 있습니다.</p>

<h2 id="컴팩션을-arrow로-재구현하면">컴팩션을 Arrow로 재구현하면</h2>

<p>Differential Dataflow의 컴팩션(consolidation)은 핵심 연산입니다: 동일 키의 diff를 합산하고, 합이 0인 엔트리를 제거합니다. 현재 Rust 구현은 정렬 기반 단일 패스(sort → merge → accumulate → filter zero)로 동작합니다.</p>

<p>Arrow로 재구현하면 연산은 다음과 같이 분해됩니다:</p>

<ol>
  <li><strong>Group-by + Sum</strong>: <code class="language-plaintext highlighter-rouge">hash_aggregate</code> 커널로 <code class="language-plaintext highlighter-rouge">(key, time)</code> 기준 그룹화 후 <code class="language-plaintext highlighter-rouge">diff</code> 합산</li>
  <li><strong>Filter zero</strong>: <code class="language-plaintext highlighter-rouge">filter</code> 커널로 <code class="language-plaintext highlighter-rouge">diff ≠ 0</code>인 행만 선택</li>
</ol>

<p>이것은 원래의 단일 패스보다 <strong>다중 패스(multi-pass)</strong>입니다 — 각 커널이 독립적으로 전체 컬럼을 순회합니다. 알고리즘 복잡도만 보면 불리합니다.</p>

<p>하지만 SIMD 벡터화가 이 차이를 상쇄하거나 역전시킬 수 있습니다. Arrow의 <code class="language-plaintext highlighter-rouge">hash_aggregate</code> 커널은 AVX2로 한 사이클에 8개의 해시를 계산하고, <code class="language-plaintext highlighter-rouge">filter</code> 커널은 비트 마스크로 한 번에 64개 행을 평가합니다. 배치 크기가 1,000행 이상일 때, 다중 패스 + SIMD가 단일 패스 + 스칼라를 이기기 시작합니다.</p>

<p>다만 정직하게 말하면, 이 교차점이 정확히 어디인지는 실측 없이는 알 수 없습니다. 이론적 추정과 실제 성능은 캐시 계층 동작, 분기 예측, 메모리 대역폭 포화 등의 변수에 따라 크게 달라집니다.</p>

<h2 id="claude-code를-믿을-것인가">Claude Code를 믿을 것인가</h2>

<p>솔직한 고백을 하나 하겠습니다. 이 글을 쓰면서, 그리고 프로토콜 설계를 진행하면서, <strong>Claude Code의 도움을 상당히 받고 있습니다.</strong> Arrow의 내부 구조를 조사하고, 바이트 단위 오버헤드를 계산하고, C 코드 스케치를 생성하는 과정에서 AI 코드 생성 도구를 활용하고 있습니다.</p>

<p>이것은 기묘한 위치에 놓인 신뢰의 문제입니다. 두 가지 차원이 있습니다.</p>

<p><strong>첫째, 정확성.</strong> AI가 생성한 코드와 수치를 얼마나 믿을 수 있는가? Arrow의 메모리 레이아웃, 배치별 오버헤드 계산, SIMD 가속 비율 — 이런 것들은 공식 문서와 대조해서 검증할 수 있습니다. 실제로 이 글의 수치 중 상당수는 Arrow 공식 스펙<a class="citation" href="#ArrowSpec">[1]</a>에서 직접 확인한 것입니다. 하지만 “47행”이라는 교차점처럼 여러 가정이 결합된 추정은, 개별 가정이 각각 합리적이더라도 결합된 결론이 현실과 얼마나 가까운지 보장하기 어렵습니다.</p>

<p><strong>둘째, 설계 판단.</strong> “Nanoarrow를 쓰라”거나 “하이브리드 전략이 맞다”는 판단은 코드 정확성보다 더 미묘한 영역입니다. AI는 검색된 정보를 합리적으로 종합하는 데 능숙하지만, 프로젝트의 장기적 방향성, 유지보수 부담, 커뮤니티 생태계의 미래 — 이런 것들은 경험과 직관의 영역이고, 현재의 AI가 가장 취약한 부분입니다.</p>

<p>제가 취하는 태도는 이렇습니다: <strong>AI를 리서치 어시스턴트로 사용하되, 아키텍처 결정은 제가 내린다.</strong> 구체적으로:</p>

<ul>
  <li><strong>수치와 사실 확인</strong>: AI가 빠르게 조사하고, 제가 공식 문서에서 교차 검증합니다.</li>
  <li><strong>코드 스케치</strong>: AI가 초안을 생성하고, 제가 한 줄씩 검토합니다. 특히 C 코드에서 메모리 관리, 정렬, 바운더리 조건은 반드시 직접 확인합니다.</li>
  <li><strong>설계 대안 탐색</strong>: AI에게 “왜 이 접근이 안 되는가”를 질문하면, 놓쳤던 제약 조건을 발견하는 데 도움이 됩니다.</li>
  <li><strong>최종 결정</strong>: 하이브리드 전략, Nanoarrow 선택, UDP 직접 전송 — 이 결정들은 AI의 제안을 참고하되, 결국 제가 이해하고 설명할 수 있는 범위에서만 채택합니다.</li>
</ul>

<p>C 프로토콜 코드에서 AI 생성 코드를 그대로 사용하는 것은 위험합니다. 전송 프로토콜은 모든 엣지 케이스 — 패킷 재조립 실패, 부분 수신, 정렬 위반, 버퍼 오버플로우 — 를 정확히 처리해야 합니다. 이 영역에서 “대체로 맞는” 코드는 프로덕션에서 재앙입니다. AI가 생성한 C 코드는 반드시 단위 테스트, 퍼즈 테스팅, Valgrind/ASan으로 검증한 뒤에만 채택할 수 있습니다.</p>

<h2 id="구현-로드맵">구현 로드맵</h2>

<p>정리하면, Arrow를 Timely Dataflow 프로토콜에 통합하는 로드맵은 다음과 같습니다.</p>

<h3 id="phase-1-l4-내부-포맷으로-arrow-recordbatch-도입">Phase 1: L4 내부 포맷으로 Arrow RecordBatch 도입</h3>

<ul>
  <li>Nanoarrow를 L4에 통합</li>
  <li>델타 배치 → Arrow RecordBatch 변환 함수 구현</li>
  <li>47행 이상 배치만 Arrow로 변환, 이하는 packed struct 유지</li>
  <li>소프트웨어 전용, FPGA 무관</li>
</ul>

<h3 id="phase-2-arrow-기반-컴팩션">Phase 2: Arrow 기반 컴팩션</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">diff</code> 컬럼 합산을 SIMD C 커널로 직접 구현 (Nanoarrow에는 compute 커널 없음)</li>
  <li>Dictionary 인코딩 적용하여 키 비교 가속</li>
  <li>교차점 실측: 단일 패스 스칼라 vs 다중 패스 SIMD</li>
</ul>

<h3 id="phase-3-arrow-ipc를-udp-와이어-포맷으로">Phase 3: Arrow IPC를 UDP 와이어 포맷으로</h3>

<ul>
  <li>Arrow IPC 스트리밍 포맷을 L4 페이로드로 채택</li>
  <li>L2에서 MTU 단편화/재조립</li>
  <li><code class="language-plaintext highlighter-rouge">wire_hdr_t.flags</code>에 Arrow IPC 플래그 추가</li>
  <li>워커 간 통신에서 직렬화 비용 제거</li>
</ul>

<h3 id="phase-4-fpga-dma-경로의-arrow-연동">Phase 4: FPGA DMA 경로의 Arrow 연동</h3>

<ul>
  <li>Arrow의 64바이트 정렬 버퍼를 DMA 버퍼로 직접 사용</li>
  <li>FPGA에서는 컬럼 → 행 전치(transpose) 수행 (라인 레이트)</li>
  <li>Nanoarrow 기반 DMA 디스크립터 생성</li>
</ul>

<h2 id="마치며">마치며</h2>

<p>Apache Arrow는 Timely Dataflow 프로토콜의 <strong>L4 Delta Batch Layer</strong>에 자연스럽게 들어맞습니다. 컬럼 기반 레이아웃은 컴팩션과 분석에서 명확한 이점을 제공하고, C Data Interface는 Rust와 C 사이의 제로카피 브릿지가 됩니다. Nanoarrow라는 경량 C 구현체의 존재가 “GLib도 C++도 없이 순수 C로”라는 제약 조건을 충족시킵니다.</p>

<p>다만 Arrow가 만능은 아닙니다. 47행 미만의 소규모 배치에서는 Abomonation이 여전히 우위이고, 컴팩션의 다중 패스 오버헤드가 SIMD 가속으로 실제 얼마나 상쇄되는지는 실측이 필요합니다. FPGA 경로에서 컬럼-행 전치의 비용도 아직 미지수입니다.</p>

<p>그래서 이 글의 결론은 “Arrow를 쓰자”가 아니라, “Arrow를 <strong>이 경계 안에서</strong> 쓰자”입니다. 핫 패스는 packed struct, 콜드 패스는 Arrow, 그 사이의 전환점은 컴팩션 경계 — 이 하이브리드 설계가 두 세계의 장점을 취하는 길입니다.</p>

<p>다음 글에서는 Nanoarrow 기반의 L4 프로토타입 구현과, 실제 델타 배치에서의 성능 측정 결과를 다룰 예정입니다.</p>

<hr />

<h3 id="참고-문헌">참고 문헌</h3>

<ol class="bibliography"><li>Apache Arrow Authors, “Apache Arrow Columnar Format Specification.” 2024. Available at: https://arrow.apache.org/docs/format/Columnar.html
</li>
<li>F. McSherry, D. G. Murray, R. Isaacs, and M. Isard, “Differential Dataflow,” in <i>Proceedings of the 6th Biennial Conference on Innovative Data Systems Research (CIDR ’13)</i>, CIDR, Jan. 2013. <a href="https://www.cidrdb.org/cidr2013/Papers/CIDR13_Paper111.pdf">[Link]</a>
</li>
<li>F. McSherry, “Abomonation: A mortifying serialization library for Rust.” 2015. Available at: https://github.com/TimelyDataflow/abomonation
</li>
<li>RustSec Advisory Database, “RUSTSEC-2021-0120: abomonation transmutes references to and from byte slices.” 2021. Available at: https://rustsec.org/advisories/RUSTSEC-2021-0120.html
</li>
<li>Apache Arrow Authors, “Arrow C Data Interface.” 2024. Available at: https://arrow.apache.org/docs/format/CDataInterface.html
</li>
<li>Apache Arrow Authors, “Apache Arrow GLib (C API).” 2024. Available at: https://arrow.apache.org/docs/c_glib/
</li>
<li>GNOME Project, “GLib Reference Manual.” 2024. Available at: https://docs.gtk.org/glib/
</li>
<li>D. Dunnington and Apache Arrow Authors, “nanoarrow: Helpers for Arrow C Data and C Stream Interfaces.” 2024. Available at: https://github.com/apache/arrow-nanoarrow
</li>
<li>Apache Arrow Authors, “Arrow IPC Streaming Format.” 2024. Available at: https://arrow.apache.org/docs/format/IPC.html
</li></ol>

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/essay/research/2026/02/17/timely-dataflow-protocol/">Timely Dataflow를 위한 전송 프로토콜은 왜 새로 필요한가</a></li>
  <li><a href="/essay/ai/2026/02/14/differential-dataflow/">Datalog의 증분 계산: Differential Dataflow</a>
`</li>
</ul>]]></content><author><name>Justin Kim</name></author><category term="research" /><category term="Apache Arrow" /><category term="Differential Dataflow" /><category term="Timely Dataflow" /><category term="C Language" /><category term="Nanoarrow" /><category term="Zero-Copy" /><category term="FPGA" /><category term="Protocol Design" /><summary type="html"><![CDATA[이전 글에서 Timely Dataflow를 위한 전송 프로토콜을 왜 새로 설계해야 하는지, 그리고 5개 레이어 구조의 밑그림을 그렸습니다. 이번 글에서는 그 설계의 한 축이 될 수 있는 Apache Arrow를 살펴보고, 구체적으로 어디에 어떻게 적용할 수 있을지 계획을 세워봅니다.]]></summary></entry><entry><title type="html">Timely Dataflow를 위한 전송 프로토콜은 왜 새로 필요한가</title><link href="https://groou.com/essay/research/2026/02/17/timely-dataflow-protocol/" rel="alternate" type="text/html" title="Timely Dataflow를 위한 전송 프로토콜은 왜 새로 필요한가" /><published>2026-02-17T00:00:00+09:00</published><updated>2026-02-17T00:00:00+09:00</updated><id>https://groou.com/essay/research/2026/02/17/timely-dataflow-protocol</id><content type="html" xml:base="https://groou.com/essay/research/2026/02/17/timely-dataflow-protocol/"><![CDATA[<p>Differential Dataflow는 데이터의 변화를 차이(Difference)의 축적으로 표현하고, 변경분만 전파하여 증분 계산을 수행합니다. 이 증분 계산이 단일 머신에서 동작할 때는 공유 메모리를 통해 워커(Worker) 간 데이터를 교환하면 됩니다. 하지만 데이터가 수십억 건으로 늘어나고, 워커를 수십 대의 머신에 분산 배치해야 하는 시점이 오면, <strong>네트워크</strong>가 개입합니다. 델타 <code class="language-plaintext highlighter-rouge">(data, time, diff)</code> 튜플이 머신 경계를 넘어야 하고, 각 워커의 진행 상태(Frontier)가 클러스터 전체에 전파되어야 합니다.</p>

<p>Timely Dataflow<a class="citation" href="#Murray2013">[1]</a>는 이 분산 실행을 가능하게 하는 데이터플로우 런타임이고, Differential Dataflow<a class="citation" href="#McSherry2013">[2]</a>는 그 위에서 증분 계산의 논리를 담당합니다. 문제는 이 두 시스템의 네트워크 계층입니다. Rust 구현체는 워커 간 통신에 TCP를 사용하는데, TCP 경로에서 메시지 하나당 <strong>7번의 메모리 복사</strong>가 발생하고, 그 중 4번은 본질적으로 불필요합니다<a class="citation" href="#McSherry_copies">[3]</a>. 수신 경로(<code class="language-plaintext highlighter-rouge">ProcessBinary::pre_work</code>)에서 전체 메모리 할당의 99.6%가 집중되며, CPU 프로파일의 상위를 <code class="language-plaintext highlighter-rouge">_platform_memmove</code>가 차지합니다. 데이터플로우의 계산 논리가 아무리 정교해도, 전송 계층이 병목이 되면 시스템 전체의 지연 시간은 전송 계층이 결정합니다.</p>

<p>그렇다면 이미 존재하는 고성능 전송 프로토콜을 가져다 쓰면 되지 않을까요? SRT, QUIC, Aeron, KCP — 모두 UDP 기반의 저지연 프로토콜입니다. 하나씩 살펴보았습니다.</p>

<h2 id="기존-프로토콜이-맞지-않는-이유">기존 프로토콜이 맞지 않는 이유</h2>

<h3 id="srt-라이브-영상-전송의-정석">SRT: 라이브 영상 전송의 정석</h3>

<p>SRT(Secure Reliable Transport)는 Haivision이 공개한 라이브 영상 전송 프로토콜입니다<a class="citation" href="#SRT_IETF">[4]</a>. UDT에서 파생되어, 불안정한 공용 인터넷 위에서도 MPEG-TS 스트림을 안정적으로 전달하는 데 탁월한 성능을 보여줍니다. NACK 기반 ARQ와 지연 한계 내의 재전송, AES 암호화까지 — 라이브 미디어 전송이라는 도메인에서 SRT의 설계는 매우 정교합니다.</p>

<p>그러나 SRT가 풀고자 하는 문제와 Differential Dataflow의 문제는 성격이 다릅니다. SRT의 핵심 메커니즘인 <strong>TLPKTDROP</strong>은 지연 한계(기본 120ms)를 초과한 패킷을 폐기합니다. 라이브 방송에서 늦은 프레임은 의미가 없으므로 이것은 올바른 트레이드오프입니다. 반면 Differential Dataflow의 델타 튜플 <code class="language-plaintext highlighter-rouge">(data, time, diff)</code>은 <strong>모든 변경분의 완전한 전달</strong>을 전제로 합니다. 가중 집합(Z-set)의 정확성이 이에 달려 있기 때문입니다. “늦더라도 반드시 도착해야 하는” 데이터와 “늦으면 버려야 하는” 데이터 — 이 두 요구는 근본적으로 양립하기 어렵습니다.</p>

<p>또한 SRT의 페이로드는 MPEG-TS에 맞춘 1,316바이트인 반면, Differential Dataflow의 델타 레코드는 평균 50~500바이트 수준입니다. CBR(상수 비트레이트) 페이싱 역시 입력 배치와 반복 경계에 따라 폭발적으로 발생하는 델타 트래픽과는 다른 리듬입니다. SRT의 강점이 빛나는 영역과 Differential Dataflow가 필요로 하는 영역이 겹치지 않는 것입니다.</p>

<h3 id="quic-웹을-위한-프로토콜">QUIC: 웹을 위한 프로토콜</h3>

<p>QUIC는 HTTP/3의 전송 계층을 목표로 탄생한 프로토콜입니다<a class="citation" href="#RFC9000">[5]</a>. 스트림 멀티플렉싱, TLS 1.3 통합, 헤드오브라인 블로킹 해소 — 브라우저와 CDN 사이의 요청-응답 패턴에서 탁월한 성능을 보여줍니다.</p>

<p>하지만 데이터센터 내부의 워커 메시(Worker Mesh)에 적용하려 하면, QUIC의 설계 전제가 이 환경과 맞지 않는다는 점이 드러납니다. QUIC는 <strong>TLS 1.3을 프로토콜에 내장</strong>하고 있으며(RFC 9001), 이를 비활성화할 수 없습니다. 신뢰할 수 있는 클러스터 내부 통신에서 레코드당 22바이트의 AEAD 오버헤드와 AES-GCM 셋업 비용(레코드당 ~200ns)은 불필요한 비용이 됩니다.</p>

<p><strong>혼잡 제어</strong> 역시 인터넷 경로를 전제로 두고 있습니다(RFC 9002). 초기 RTT 추정치가 333ms인데, 데이터센터 내부 RTT는 0.1ms, FPGA PCIe 경로는 0.001ms입니다. 3,330배에서 333,000배의 과대 추정이며, 새 연결마다 처음 333ms 동안 혼잡 제어가 현실을 반영하지 못합니다.</p>

<p>스트림 관리의 비용도 무시하기 어렵습니다. 64개 워커 × 20개 오퍼레이터 구성에서는 <strong>80,640개의 동시 스트림</strong>이 필요하고, 스트림당 약 4KB의 상태를 유지하면 전송 계층만으로 315MB의 메모리를 소비합니다. QUIC가 해결하고자 한 웹 트래픽의 문제와, 데이터플로우 워커 메시의 문제는 규모와 방향이 다릅니다.</p>

<h3 id="aeron-메시징을-위한-프로토콜">Aeron: 메시징을 위한 프로토콜</h3>

<p>Aeron은 금융 거래 시스템을 위한 고성능 메시징 프레임워크입니다. 미디어 드라이버(Media Driver)라는 별도 프로세스가 공유 메모리 링 버퍼를 통해 애플리케이션과 통신하며, Sender·Receiver·Conductor 세 스레드가 busy-spinning으로 동작합니다.</p>

<p>성능은 인상적입니다. IPC 경로에서 1μs 미만의 지연을 달성합니다. 하지만 Differential Dataflow의 관점에서 보면 구조적 불일치가 있습니다.</p>

<p>Aeron은 <strong>발행-구독(Pub/Sub)</strong> 모델입니다. Differential Dataflow가 필요로 하는 것은 포인트-투-포인트 델타 스트리밍과 <strong>프론티어(Frontier) 전파</strong>입니다. Timely Dataflow에서 프론티어란, 더 이상 특정 시점 이전의 메시지가 도착하지 않을 것임을 알려주는 진행 신호입니다<a class="citation" href="#Murray2013">[1]</a>. 이 신호는 데이터 메시지보다 <strong>우선순위가 높아야</strong> 합니다 — 프론티어가 지연되면 파이프라인 뒤쪽의 모든 연산이 대기 상태에 빠지기 때문입니다. Aeron에는 이런 우선순위 레인이 없습니다.</p>

<p>또한 미디어 드라이버는 인스턴스당 <strong>3개의 전용 CPU 코어</strong>를 소비합니다. 16개 워커를 운영하면 전송 인프라만으로 48코어가 필요합니다. 이것은 프로토콜이 아니라 미들웨어의 비용입니다.</p>

<h3 id="kcp-게임을-위한-프로토콜">KCP: 게임을 위한 프로토콜</h3>

<p>KCP는 순수 C 2,000줄 미만으로 구현한 경량 ARQ 프로토콜입니다. 공격적인 재전송으로 게임 및 실시간 애플리케이션에서 낮은 지연을 달성합니다. C 구현이라는 점에서 처음에는 가장 가능성 있어 보였습니다.</p>

<p>하지만 KCP는 <strong>단일 스트림 프로토콜</strong>입니다. Differential Dataflow의 K개 데이터 채널에 K개의 독립적인 <code class="language-plaintext highlighter-rouge">ikcpcb</code> 인스턴스가 필요하며, K=1,000일 때 <code class="language-plaintext highlighter-rouge">ikcp_update()</code> 호출만으로 초당 200ms의 CPU 시간을 소비합니다. 메시지당 3~4회의 메모리 복사가 발생하고, 이중 링크드 리스트 탐색과 동적 메모리 할당은 FPGA 파이프라인으로 합성할 수 없는 구조입니다.</p>

<h3 id="공통적으로-빠진-것">공통적으로 빠진 것</h3>

<p>네 프로토콜 모두에서 공통적으로 부재한 것은 <strong>부분 순서 타임스탬프(Partially Ordered Timestamp)</strong>에 대한 인식입니다.</p>

<p>Timely Dataflow에서 ‘시간’이란 0, 1, 2로 증가하는 단순한 번호가 아닙니다. <code class="language-plaintext highlighter-rouge">(epoch, iteration_0, iteration_1, ...)</code>처럼 여러 차원이 중첩된 격자(Lattice) 구조이고, 이 격자 위에서 “어느 시점까지 처리가 끝났는가”를 추적합니다. 그런데 SRT든 QUIC든 Aeron이든 KCP든, 이들이 아는 시간이란 단조 증가하는 시퀀스 번호가 전부입니다. 전송 계층이 부분 순서 타임스탬프와 프론티어를 모르면, 결국 애플리케이션 계층에서 이 논리를 전부 다시 구현해야 합니다. 기존 프로토콜 위에 <strong>사실상 새로운 프로토콜을 하나 더 올리는</strong> 꼴입니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">요구사항</th>
      <th style="text-align: center">SRT</th>
      <th style="text-align: center">QUIC</th>
      <th style="text-align: center">Aeron</th>
      <th style="text-align: center">KCP</th>
      <th style="text-align: center">필요</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>정확한 전달 (Exactly-once)</strong></td>
      <td style="text-align: center">TLPKTDROP 위반</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center"><strong>필수</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>부분 순서 타임스탬프</strong></td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center"><strong>필수</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>프론티어 우선 전파</strong></td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center"><strong>필수</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>적응적 배칭</strong></td>
      <td style="text-align: center">CBR 페이싱</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center"><strong>필수</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>제로카피 DMA</strong></td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">IPC만</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">권장</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>FPGA 합성 가능</strong></td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">권장</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>멀티캐스트</strong></td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">O</td>
      <td style="text-align: center">X</td>
      <td style="text-align: center">권장</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>클러스터 내 혼잡 제어</strong></td>
      <td style="text-align: center">인터넷</td>
      <td style="text-align: center">인터넷</td>
      <td style="text-align: center">제한적</td>
      <td style="text-align: center">없음</td>
      <td style="text-align: center">권장</td>
    </tr>
  </tbody>
</table>

<h2 id="구현-언어는-왜-c인가">구현 언어는 왜 C인가</h2>

<p>새로운 프로토콜을 구현한다면, 그 언어는 <strong>C</strong>여야 합니다. Differential Dataflow의 정식 구현체가 Rust인 점을 고려하면 다소 역설적으로 들릴 수 있습니다. 하지만 이 프로토콜은 애플리케이션 로직이 아니라 <strong>전송 인프라</strong>이고, 전송 인프라의 최종 목표는 <strong>FPGA 가속</strong>입니다.</p>

<h3 id="커널-모드가-아닌-유저-모드">커널 모드가 아닌 유저 모드</h3>

<p>FPGA 가속의 전제는 <strong>커널 우회(Kernel Bypass)</strong>입니다. 리눅스 커널 네트워크 스택은 패킷당 두 번의 컨텍스트 스위치, 스케줄러 지터, 캐시 오염을 수반합니다. FPGA가 400ns 만에 패킷을 처리해도, 커널 경유 시 20μs의 왕복 지연이 가속의 이점을 전부 상쇄합니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">네트워크 스택</th>
      <th style="text-align: right">p50 지연<sup id="fnref:p50"><a href="#fn:p50" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></th>
      <th style="text-align: right">커널 TCP 대비</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">커널 TCP (syscall)</td>
      <td style="text-align: right">20,000 ns</td>
      <td style="text-align: right">기준</td>
    </tr>
    <tr>
      <td style="text-align: left">커널 UDP (syscall)</td>
      <td style="text-align: right">15,000 ns</td>
      <td style="text-align: right">−25%</td>
    </tr>
    <tr>
      <td style="text-align: left">io_uring (배치)</td>
      <td style="text-align: right">8,000 ns</td>
      <td style="text-align: right">−60%</td>
    </tr>
    <tr>
      <td style="text-align: left">AF_XDP (제로카피)</td>
      <td style="text-align: right">3,500 ns</td>
      <td style="text-align: right">−82.5%</td>
    </tr>
    <tr>
      <td style="text-align: left">DPDK (폴 모드)</td>
      <td style="text-align: right">1,200 ns</td>
      <td style="text-align: right">−94%</td>
    </tr>
    <tr>
      <td style="text-align: left">DPDK + FPGA SmartNIC</td>
      <td style="text-align: right">400 ns</td>
      <td style="text-align: right">−98%</td>
    </tr>
    <tr>
      <td style="text-align: left">순수 FPGA (NIC 위)</td>
      <td style="text-align: right">100 ns</td>
      <td style="text-align: right">−99.5%</td>
    </tr>
  </tbody>
</table>

<p>DPDK의 폴 모드 드라이버(PMD)는 커널을 완전히 우회하여 유저 모드에서 패킷을 처리합니다. 여기에 FPGA SmartNIC을 결합하면 p50 400ns를 달성합니다. 이 모든 것이 <strong>유저 모드</strong>에서 동작해야 하는 이유입니다.</p>

<h3 id="c와-fpga의-공생">C와 FPGA의 공생</h3>

<p>그리고 유저 모드 네트워킹에서 C가 유일한 선택인 이유는 <strong>HLS(High-Level Synthesis)</strong> 때문입니다.</p>

<p>FPGA 벤더의 HLS 툴체인 — Xilinx Vivado/Vitis HLS, Intel HLS Compiler — 은 <strong>C 코드를 직접 하드웨어(RTL)로 합성</strong>합니다. 동일한 C 함수를 소프트웨어에서 실행할 수도 있고, <code class="language-plaintext highlighter-rouge">#pragma HLS PIPELINE</code> 한 줄을 추가하면 FPGA 로직으로 변환할 수도 있습니다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 소프트웨어에서 실행 가능하고, 동시에 FPGA로 합성 가능한 패킷 파서</span>
<span class="kt">void</span> <span class="nf">parse_delta_header</span><span class="p">(</span><span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="n">delta_hdr_t</span> <span class="o">*</span><span class="n">hdr</span><span class="p">)</span> <span class="p">{</span>
    <span class="cp">#pragma HLS PIPELINE II=1
</span>    <span class="n">hdr</span><span class="o">-&gt;</span><span class="n">epoch</span>     <span class="o">=</span> <span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">24</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">16</span><span class="p">)</span> <span class="o">|</span> <span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span> <span class="o">|</span> <span class="n">buf</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>
    <span class="n">hdr</span><span class="o">-&gt;</span><span class="n">iteration</span> <span class="o">=</span> <span class="p">(</span><span class="n">buf</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">&lt;&lt;</span> <span class="mi">8</span><span class="p">)</span>  <span class="o">|</span> <span class="n">buf</span><span class="p">[</span><span class="mi">5</span><span class="p">];</span>
    <span class="n">hdr</span><span class="o">-&gt;</span><span class="n">diff_type</span> <span class="o">=</span> <span class="n">buf</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>
    <span class="n">hdr</span><span class="o">-&gt;</span><span class="n">count</span>     <span class="o">=</span> <span class="n">buf</span><span class="p">[</span><span class="mi">7</span><span class="p">];</span>
<span class="p">}</span>
</code></pre></div></div>

<p>C++은 HLS 툴체인이 부분적으로만 지원하고(가상 함수, RTTI, 예외 불가), Rust는 어떤 HLS 툴체인도 지원하지 않습니다. Go는 가비지 컬렉터가 DMA 핀 메모리를 재배치할 수 있어 원천적으로 불가능합니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">기준</th>
      <th style="text-align: center">C</th>
      <th style="text-align: center">C++</th>
      <th style="text-align: center">Rust</th>
      <th style="text-align: center">Go</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">HLS 합성</td>
      <td style="text-align: center">완전 지원</td>
      <td style="text-align: center">부분 지원</td>
      <td style="text-align: center">미지원</td>
      <td style="text-align: center">미지원</td>
    </tr>
    <tr>
      <td style="text-align: left">ABI 안정성</td>
      <td style="text-align: center">표준 (System V)</td>
      <td style="text-align: center">불안정</td>
      <td style="text-align: center">불안정</td>
      <td style="text-align: center">불안정</td>
    </tr>
    <tr>
      <td style="text-align: left">DMA 메모리 레이아웃</td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">packed</code> struct</td>
      <td style="text-align: center">vtable 간섭</td>
      <td style="text-align: center">fat pointer</td>
      <td style="text-align: center">GC 재배치</td>
    </tr>
    <tr>
      <td style="text-align: left">DPDK/libibverbs 통합</td>
      <td style="text-align: center">네이티브</td>
      <td style="text-align: center">FFI</td>
      <td style="text-align: center">FFI</td>
      <td style="text-align: center">CGo</td>
    </tr>
    <tr>
      <td style="text-align: left">런타임 의존성</td>
      <td style="text-align: center">없음</td>
      <td style="text-align: center">언와인더</td>
      <td style="text-align: center">언와인더</td>
      <td style="text-align: center">GC</td>
    </tr>
  </tbody>
</table>

<p>DPDK, SPDK, libibverbs, libfabric, XDMA, QDMA, OpenNIC — FPGA/네트워크 생태계의 주요 라이브러리 <strong>10개 모두</strong> C로 구현하고 있습니다. 이것은 관성이 아니라, ABI 안정성과 HLS 합성이라는 구조적 요구가 만들어낸 결과입니다.</p>

<p>핵심은 이것입니다: <strong>동일한 C 소스 파일이 (1) 소프트웨어 레퍼런스 구현, (2) HLS 합성 입력, (3) DPDK PMD 통합 계층, (4) FPGA와 공유하는 DMA 디스크립터 정의를 동시에 수행합니다.</strong> 이 네 가지 역할을 하나의 언어로 충족할 수 있는 것은 C뿐입니다.</p>

<h2 id="레이어-분할-설계">레이어 분할 설계</h2>

<p>잠깐 스케치해 본 레이어 분할 설계 아이디어입니다. 5개 레이어로 나누고, 각 레이어를 독립적으로 테스트할 수 있게 하면서 하위 레이어부터 점진적으로 FPGA로 오프로드하는 구조를 생각하고 있습니다.</p>

<svg viewBox="0 0 600 340" xmlns="http://www.w3.org/2000/svg" style="max-width:600px;width:100%;font-family:'Pretendard','Manrope',sans-serif;">
  <style>
    .layer { fill: #f8f9fa; stroke: #343a40; stroke-width: 1.5; }
    .label { font-size: 13px; font-weight: 700; fill: #212529; }
    .desc  { font-size: 11px; fill: #495057; }
    .arrow { stroke: #868e96; stroke-width: 1; marker-end: url(#arrowhead); }
    .note  { font-size: 10px; fill: #868e96; font-style: italic; }
  </style>
  <defs>
    <marker id="arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
      <path d="M0,0 L8,3 L0,6" fill="#868e96" />
    </marker>
  </defs>
  <!-- L5 -->
  <rect class="layer" x="40" y="10" width="440" height="56" rx="4" />
  <text class="label" x="60" y="32">L5: Dataflow Binding Layer</text>
  <text class="desc" x="60" y="50">Rust FFI ↔ C 라이브러리 바인딩 · timely-dataflow 통합</text>
  <!-- L4 -->
  <rect class="layer" x="40" y="74" width="440" height="56" rx="4" />
  <text class="label" x="60" y="96">L4: Delta Batch Layer</text>
  <text class="desc" x="60" y="114">델타 직렬화/역직렬화 · 적응적 배칭 · 제로카피 DMA 매핑</text>
  <!-- L3 -->
  <rect class="layer" x="40" y="138" width="440" height="56" rx="4" />
  <text class="label" x="60" y="160">L3: Progress Layer</text>
  <text class="desc" x="60" y="178">프론티어 추적 · 포인트스탬프 브로드캐스트 · 타임스탬프 인코딩</text>
  <!-- L2 -->
  <rect class="layer" x="40" y="202" width="440" height="56" rx="4" />
  <text class="label" x="60" y="224">L2: Channel Layer</text>
  <text class="desc" x="60" y="242">멀티플렉싱 · 배압(Backpressure) · 진행 채널 우선순위</text>
  <!-- L1 -->
  <rect class="layer" x="40" y="266" width="440" height="56" rx="4" />
  <text class="label" x="60" y="288">L1: Wire Layer</text>
  <text class="desc" x="60" y="306">UDP 송수신 · 패킷 프레이밍 · DPDK PMD / FPGA SmartNIC</text>
  <!-- FPGA offload arrow -->
  <line class="arrow" x1="530" y1="310" x2="530" y2="150" />
  <text class="note" x="538" y="240" transform="rotate(90,538,240)">FPGA offload →</text>
</svg>

<h3 id="l1-wire-layer--선線-위의-바이트">L1: Wire Layer — 선(線) 위의 바이트</h3>

<p>가장 낮은 계층입니다. UDP 소켓 또는 DPDK PMD를 통해 원시 패킷을 송수신합니다. 이 계층의 핵심 설계 원칙은 <strong>드라이버 교체 가능성</strong>입니다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">send</span><span class="p">)(</span><span class="kt">void</span> <span class="o">*</span><span class="n">ctx</span><span class="p">,</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">);</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">recv</span><span class="p">)(</span><span class="kt">void</span> <span class="o">*</span><span class="n">ctx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">buf</span><span class="p">,</span> <span class="kt">size_t</span> <span class="n">max_len</span><span class="p">);</span>
    <span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">flush</span><span class="p">)(</span><span class="kt">void</span> <span class="o">*</span><span class="n">ctx</span><span class="p">);</span>
<span class="p">}</span> <span class="n">wire_ops_t</span><span class="p">;</span>
</code></pre></div></div>

<p>소프트웨어 개발 초기에는 <code class="language-plaintext highlighter-rouge">sendmsg()</code>/<code class="language-plaintext highlighter-rouge">recvmsg()</code> 기반의 커널 UDP 드라이버를 사용하고, 성능이 필요한 시점에서 DPDK PMD 드라이버로 교체하며, 최종적으로 FPGA SmartNIC(Xilinx QDMA, Intel IPU)의 DMA 링으로 전환합니다. 상위 계층의 코드는 한 줄도 바꿀 필요가 없습니다.</p>

<p>패킷 프레이밍은 16바이트 고정 헤더를 사용합니다. 64바이트 델타 레코드 기준으로 20%의 오버헤드이며, 이것은 SRT(38.5%)나 Aeron(38.5%), KCP(33.3%)보다 효율적입니다. 다만 암호화는 아직 고려하지 못한 부분입니다. SRT의 AES-CTR이나 QUIC의 TLS 1.3처럼 전송 계층 수준의 암호화를 어떻게 통합할지는 별도의 설계가 필요합니다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">typedef</span> <span class="k">struct</span> <span class="nf">__attribute__</span><span class="p">((</span><span class="n">packed</span><span class="p">))</span> <span class="p">{</span>
    <span class="kt">uint32_t</span> <span class="n">epoch</span><span class="p">;</span>         <span class="c1">// 외부 시점</span>
    <span class="kt">uint16_t</span> <span class="n">iteration</span><span class="p">;</span>     <span class="c1">// 내부 반복 인덱스</span>
    <span class="kt">uint8_t</span>  <span class="n">channel_id</span><span class="p">;</span>    <span class="c1">// 멀티플렉싱 채널</span>
    <span class="kt">uint8_t</span>  <span class="n">flags</span><span class="p">;</span>         <span class="c1">// PROGRESS | DATA | ACK | COMPACT</span>
    <span class="kt">uint32_t</span> <span class="n">seq_num</span><span class="p">;</span>       <span class="c1">// 채널별 시퀀스 번호</span>
    <span class="kt">uint16_t</span> <span class="n">payload_len</span><span class="p">;</span>   <span class="c1">// 페이로드 길이</span>
    <span class="kt">uint16_t</span> <span class="n">checksum</span><span class="p">;</span>      <span class="c1">// CRC-16 (FPGA 오프로드 대상)</span>
<span class="p">}</span> <span class="n">wire_hdr_t</span><span class="p">;</span>               <span class="c1">// 16 bytes, FPGA 레지스터 맵과 1:1 대응</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">__attribute__((packed))</code>로 선언한 이 구조체는 FPGA의 레지스터 맵과 바이트 단위로 정확히 일치합니다. HLS로 합성하면 클럭 사이클당 하나의 헤더를 처리하는 패킷 파서 파이프라인을 얻습니다.</p>

<h3 id="l2-channel-layer--채널의-분리">L2: Channel Layer — 채널의 분리</h3>

<p>데이터플로우 그래프에서 오퍼레이터 사이의 각 에지(Edge)가 하나의 논리 채널이 됩니다. 이 계층은 하나의 물리적 연결 위에서 다수의 논리 채널을 멀티플렉싱하고, <strong>진행 채널(Progress Channel)이 데이터 채널보다 항상 우선</strong>하도록 스케줄링합니다.</p>

<p>이것이 Aeron이나 QUIC의 스트림 멀티플렉싱과 다른 점입니다. 일반적인 멀티플렉싱은 공정성(Fairness)을 추구하지만, Timely Dataflow에서 프론티어 메시지가 늦으면 파이프라인 뒤쪽의 모든 연산이 멈춥니다. 따라서 진행 채널은 엄격한 우선순위(Strict Priority)를 가져야 합니다.</p>

<p>배압(Backpressure)도 <strong>타임스탬프 단위</strong>로 작동합니다. 수신 측이 “시점 $t$의 델타는 아직 처리 중이니 잠깐 멈춰 달라”고 요청하면, 전송 측은 해당 시점의 델타만 보류합니다. 나머지 시점의 데이터는 영향 없이 계속 흐릅니다. TCP처럼 바이트 수로 흐름을 조절하는 것이 아니라, 데이터의 의미를 기준으로 흐름을 조절하는 셈입니다.</p>

<h3 id="l3-progress-layer--진행의-언어">L3: Progress Layer — 진행의 언어</h3>

<p>이 계층이 기존 프로토콜과의 가장 본질적인 차별점입니다. <strong>부분 순서 타임스탬프를 전송 프로토콜의 일급 시민(First-class Citizen)으로 다룹니다.</strong></p>

<p>Naiad 논문<a class="citation" href="#Murray2013">[1]</a>의 진행 추적 프로토콜에서, 워커는 포인트스탬프(Pointstamp)의 발생 카운트(Occurrence Count)와 선행 카운트(Precursor Count)를 관리하며, 변경 사항을 모든 피어에게 브로드캐스트합니다. 64개 워커 기준으로 한 번의 프론티어 갱신이 63개의 브로드캐스트 메시지를 발생시킵니다 — $O(N^2)$의 통신 복잡도입니다.</p>

<p>이 계층에서 프론티어 계산 자체를 FPGA로 오프로드하면 극적인 효과가 있습니다. 250MHz FPGA에 16개의 병렬 비교기를 배치하면 초당 약 <strong>40억 회의 포인트스탬프 비교</strong>가 가능합니다<a class="citation" href="#ClickNP2016">[6]</a>. 일반적인 워크로드가 초당 약 100만 회의 비교를 요구하는 점을 고려하면, 4,000배의 여유가 생깁니다. CPU는 프론티어 계산에서 완전히 해방되어 본래의 작업 — 오퍼레이터 로직 실행 — 에 집중할 수 있습니다.</p>

<h3 id="l4-delta-batch-layer--델타의-포장">L4: Delta Batch Layer — 델타의 포장</h3>

<p>Differential Dataflow의 델타 배치는 이중 모드(Bimodal) 크기 분포를 보입니다. 초기 적재 시에는 수백만 개의 <code class="language-plaintext highlighter-rouge">(data, time, diff)</code> 트리플을 한꺼번에 만들어내지만, 증분 업데이트 시에는 1~100개 수준에 그칩니다.</p>

<p>이 계층의 핵심은 <strong>적응적 배칭(Adaptive Batching)</strong>입니다. 부하가 낮을 때는 델타를 즉시 전달하여 지연을 최소화하고, 부하가 높을 때는 델타를 합쳐서(Coalesce) 처리량을 극대화합니다. 결정 기준은 두 가지입니다: (1) 큐 깊이가 임계값을 초과하는가, (2) 마지막 전송 이후 경과 시간이 타이머를 초과하는가.</p>

<p>컴팩션(Consolidation) — 동일 키에 대한 diff를 합산하고, 합이 0인 엔트리를 제거하는 연산 — 도 이 계층에서 수행합니다. 일반적인 워크로드에서 컴팩션은 엔트리의 <strong>30~80%를 제거</strong>합니다. 이 연산은 스트리밍 병합 정렬과 누산(Accumulation)의 조합으로, 데이터 의존적 분기가 없는 고정된 데이터플로우 파이프라인입니다 — FPGA 합성에 이상적인 패턴입니다.</p>

<p>직렬화는 Timely Dataflow가 사용하는 Abomonation<a class="citation" href="#Abomonation">[7]</a> 스타일의 제로카피 방식을 따릅니다. 구조체의 메모리 레이아웃이 곧 와이어 포맷이 되도록 <code class="language-plaintext highlighter-rouge">packed</code> struct를 설계하면, 직렬화 비용은 0에 수렴합니다. C의 <code class="language-plaintext highlighter-rouge">__attribute__((packed))</code>가 필요한 이유가 바로 여기에 있습니다.</p>

<h3 id="l5-dataflow-binding-layer--두-세계의-접점">L5: Dataflow Binding Layer — 두 세계의 접점</h3>

<p>최상위 계층은 C로 작성된 전송 라이브러리를 Rust의 <code class="language-plaintext highlighter-rouge">timely-dataflow</code> 크레이트와 연결합니다. Rust FFI(<code class="language-plaintext highlighter-rouge">extern "C"</code>)를 통해 C 함수를 호출하고, C ABI의 안정성 덕분에 양쪽이 독립적으로 버전을 올릴 수 있습니다.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Rust 측: timely-dataflow의 Communication 트레이트 구현</span>
<span class="k">extern</span> <span class="s">"C"</span> <span class="p">{</span>
    <span class="k">fn</span> <span class="nf">tdd_channel_open</span><span class="p">(</span><span class="n">peer</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span> <span class="n">channel</span><span class="p">:</span> <span class="nb">u8</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="o">*</span><span class="k">mut</span> <span class="n">TddChannel</span><span class="p">;</span>
    <span class="k">fn</span> <span class="nf">tdd_channel_send</span><span class="p">(</span><span class="n">ch</span><span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="n">TddChannel</span><span class="p">,</span> <span class="n">buf</span><span class="p">:</span> <span class="o">*</span><span class="k">const</span> <span class="nb">u8</span><span class="p">,</span> <span class="n">len</span><span class="p">:</span> <span class="nb">usize</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span><span class="p">;</span>
    <span class="k">fn</span> <span class="nf">tdd_frontier_advance</span><span class="p">(</span><span class="n">ch</span><span class="p">:</span> <span class="o">*</span><span class="k">mut</span> <span class="n">TddChannel</span><span class="p">,</span> <span class="n">epoch</span><span class="p">:</span> <span class="nb">u32</span><span class="p">,</span> <span class="n">iter</span><span class="p">:</span> <span class="nb">u16</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">i32</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 바인딩 계층만 Rust로 작성하고, 나머지 L1~L4는 모두 C로 구현합니다. 소프트웨어 모드에서 충분히 검증한 뒤 L1(패킷 파싱, 체크섬)부터 L3(프론티어 계산), L4(컴팩션)까지 점진적으로 FPGA로 내려보내는 <strong>상향식 하드웨어 오프로드</strong> 전략을 취합니다.</p>

<svg viewBox="0 0 620 320" xmlns="http://www.w3.org/2000/svg" style="max-width:620px;width:100%;font-family:'Pretendard','Manrope',sans-serif;">
  <style>
    .phase-box  { stroke-width: 1.5; rx: 6; }
    .phase1     { fill: #e8f5e9; stroke: #43a047; }
    .phase2     { fill: #e3f2fd; stroke: #1e88e5; }
    .phase3     { fill: #fce4ec; stroke: #e53935; }
    .phase-label { font-size: 13px; font-weight: 700; }
    .phase-desc  { font-size: 11px; fill: #424242; }
    .phase-effect { font-size: 10.5px; fill: #616161; font-style: italic; }
    .title      { font-size: 14px; font-weight: 700; fill: #212529; }
    .arrow-line { stroke: #9e9e9e; stroke-width: 1.5; stroke-dasharray: 6,3; marker-end: url(#tri); }
    .time-label { font-size: 10px; fill: #9e9e9e; }
  </style>
  <defs>
    <marker id="tri" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
      <path d="M0,0 L8,3 L0,6" fill="#9e9e9e" />
    </marker>
  </defs>
  <!-- Title -->
  <text class="title" x="20" y="28">FPGA 오프로드 로드맵</text>
  <!-- Timeline arrow -->
  <line class="arrow-line" x1="30" y1="52" x2="590" y2="52" />
  <text class="time-label" x="560" y="46">time →</text>
  <!-- Phase 1 -->
  <rect class="phase-box phase1" x="20" y="70" width="176" height="110" />
  <text class="phase-label" x="36" y="94" fill="#2e7d32">Phase 1 — L1 오프로드</text>
  <text class="phase-desc" x="36" y="116">패킷 파싱</text>
  <text class="phase-desc" x="36" y="132">CRC 검증</text>
  <text class="phase-desc" x="36" y="148">시퀀스 번호 처리</text>
  <text class="phase-effect" x="36" y="170">→ CPU가 원시 패킷을 볼 필요 없음</text>
  <!-- Phase 2 -->
  <rect class="phase-box phase2" x="214" y="70" width="186" height="110" />
  <text class="phase-label" x="230" y="94" fill="#1565c0">Phase 2 — L3 오프로드</text>
  <text class="phase-desc" x="230" y="116">포인트스탬프 비교</text>
  <text class="phase-desc" x="230" y="132">프론티어 안티체인 계산</text>
  <text class="phase-desc" x="230" y="148">멀티캐스트</text>
  <text class="phase-effect" x="230" y="170">→ CPU가 진행 추적에서 완전 해방</text>
  <!-- Phase 3 -->
  <rect class="phase-box phase3" x="418" y="70" width="186" height="110" />
  <text class="phase-label" x="434" y="94" fill="#c62828">Phase 3 — L4 오프로드</text>
  <text class="phase-desc" x="434" y="116">스트리밍 컴팩션</text>
  <text class="phase-desc" x="434" y="132">(병합 정렬 + 누산 + 영(0) 제거)</text>
  <text class="phase-effect" x="434" y="158">→ 델타가 이미 압축된 상태로</text>
  <text class="phase-effect" x="446" y="172">호스트에 도착</text>
  <!-- Layer badges -->
  <rect x="155" y="188" width="30" height="18" rx="3" fill="#43a047" />
  <text x="162" y="201" font-size="10" fill="#fff" font-weight="700">L1</text>
  <rect x="355" y="188" width="30" height="18" rx="3" fill="#1e88e5" />
  <text x="362" y="201" font-size="10" fill="#fff" font-weight="700">L3</text>
  <rect x="559" y="188" width="30" height="18" rx="3" fill="#e53935" />
  <text x="566" y="201" font-size="10" fill="#fff" font-weight="700">L4</text>
  <!-- CPU offload bar -->
  <text class="phase-desc" x="20" y="238" font-weight="700">CPU 부하 감소</text>
  <rect x="20" y="248" width="580" height="14" rx="3" fill="#e0e0e0" />
  <rect x="20" y="248" width="176" height="14" rx="3" fill="#a5d6a7" />
  <rect x="196" y="248" width="204" height="14" rx="3" fill="#90caf9" />
  <rect x="400" y="248" width="200" height="14" rx="3" fill="#ef9a9a" />
  <text x="80" y="259" font-size="9" fill="#1b5e20" font-weight="600">패킷 처리</text>
  <text x="262" y="259" font-size="9" fill="#0d47a1" font-weight="600">+ 진행 추적</text>
  <text x="464" y="259" font-size="9" fill="#b71c1c" font-weight="600">+ 컴팩션</text>
  <!-- Legend -->
  <text class="time-label" x="20" y="290">각 Phase는 독립 배포 가능 · 이전 Phase 없이도 소프트웨어 폴백 동작</text>
</svg>

<h2 id="마치며">마치며</h2>

<p>SRT는 영상을 위해, QUIC는 웹을 위해, Aeron은 메시징을 위해, KCP는 게임을 위해 만든 프로토콜입니다. 각각의 도메인에서는 훌륭한 프로토콜입니다. 하지만 Differential Dataflow가 요구하는 것 — 부분 순서 타임스탬프, 프론티어 우선 전파, 적응적 델타 배칭, 의미론적 배압 — 은 이 프로토콜들의 설계 공간(Design Space)에 존재하지 않습니다.</p>

<p>기존 프로토콜 위에 이 기능들을 쌓으려면 결국 전송 계층 대부분을 다시 구현하게 됩니다. 그렇다면 처음부터 Differential Dataflow의 의미론에 맞춰 설계하는 편이 더 깨끗합니다. 그리고 그 구현이 C여야 하는 이유는 감상적인 것이 아닙니다. FPGA HLS 툴체인이 합성할 수 있는 유일한 범용 언어가 C이고, 유저 모드 네트워킹 생태계 전체가 C ABI 위에 서 있기 때문입니다.</p>

<p>Differential Dataflow는 “무엇이 바뀌었는가”를 계산의 출발점으로 삼습니다. 이 프로토콜은 같은 질문을 네트워크에도 던집니다. 프론티어가 어디까지 진행했는지, 타임스탬프의 순서가 어떻게 구성되어 있는지를 전송 계층이 직접 이해하면, 애플리케이션이 매번 바이트 배열로 변환하고 다시 해석하는 과정이 필요 없어집니다.</p>

<hr />

<h3 id="참고-문헌">참고 문헌</h3>

<ol class="bibliography"><li>D. G. Murray, F. McSherry, R. Isaacs, M. Isard, P. Barham, and M. Abadi, “Naiad: A Timely Dataflow System,” in <i>Proceedings of the 24th ACM Symposium on Operating Systems Principles (SOSP ’13)</i>, ACM, 2013, pp. 439–455. Available at: https://sigops.org/s/conferences/sosp/2013/papers/p439-murray.pdf
</li>
<li>F. McSherry, D. G. Murray, R. Isaacs, and M. Isard, “Differential Dataflow,” in <i>Proceedings of the 6th Biennial Conference on Innovative Data Systems Research (CIDR ’13)</i>, CIDR, Jan. 2013. <a href="https://www.cidrdb.org/cidr2013/Papers/CIDR13_Paper111.pdf">[Link]</a>
</li>
<li>F. McSherry, “Minimize copies – GitHub Issue #111.” 2019. Available at: https://github.com/TimelyDataflow/timely-dataflow/issues/111
</li>
<li>M. P. Sharabayko, M. A. Sharabayko, J. Dube, and J. Kim, “SRT: The Secure Reliable Transport Protocol,” IETF, Internet-Draft draft-sharabayko-srt-01, 2020. Available at: https://datatracker.ietf.org/doc/draft-sharabayko-srt/
</li>
<li>J. Iyengar and M. Thomson, “QUIC: A UDP-Based Multiplexed and Secure Transport,” RFC Editor, RFC 9000, May 2021. Available at: https://www.rfc-editor.org/rfc/rfc9000
</li>
<li>B. Li <i>et al.</i>, “ClickNP: Highly Flexible and High Performance Network Processing with Reconfigurable Hardware,” in <i>Proceedings of the ACM SIGCOMM 2016 Conference</i>, ACM, 2016, pp. 1–14.
</li>
<li>F. McSherry, “Abomonation: A mortifying serialization library for Rust.” 2015. Available at: https://github.com/TimelyDataflow/abomonation
</li></ol>

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/essay/ai/2026/02/14/differential-dataflow/">Datalog의 증분 계산: Differential Dataflow</a></li>
  <li><a href="/essay/ai/2026/01/31/rdf-hardware-cpu-vs-gpu/">RDF 처리에 CPU와 메모리가 중요한 이유</a></li>
  <li><a href="/essay/ai/2026/02/01/introducing-datalog/">SPARQL의 SQL 유사성이 주는 함정, 그리고 Datalog</a></li>
</ul>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:p50">
      <p>p50은 전체 측정값을 오름차순으로 정렬했을 때 50번째 백분위수(중앙값)에 해당하는 지연 시간입니다. 예를 들어 p50이 1,200ns라면, 요청의 절반은 1,200ns 이내에 처리된다는 뜻입니다. <a href="#fnref:p50" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Justin Kim</name></author><category term="essay" /><category term="research" /><category term="Differential Dataflow" /><category term="Timely Dataflow" /><category term="Protocol Design" /><category term="FPGA" /><category term="Network" /><category term="System Design" /><summary type="html"><![CDATA[Differential Dataflow는 데이터의 변화를 차이(Difference)의 축적으로 표현하고, 변경분만 전파하여 증분 계산을 수행합니다. 이 증분 계산이 단일 머신에서 동작할 때는 공유 메모리를 통해 워커(Worker) 간 데이터를 교환하면 됩니다. 하지만 데이터가 수십억 건으로 늘어나고, 워커를 수십 대의 머신에 분산 배치해야 하는 시점이 오면, 네트워크가 개입합니다. 델타 (data, time, diff) 튜플이 머신 경계를 넘어야 하고, 각 워커의 진행 상태(Frontier)가 클러스터 전체에 전파되어야 합니다.]]></summary></entry><entry><title type="html">Datalog의 증분 계산</title><link href="https://groou.com/essay/ai/2026/02/14/differential-dataflow/" rel="alternate" type="text/html" title="Datalog의 증분 계산" /><published>2026-02-14T00:00:00+09:00</published><updated>2026-02-14T00:00:00+09:00</updated><id>https://groou.com/essay/ai/2026/02/14/differential-dataflow</id><content type="html" xml:base="https://groou.com/essay/ai/2026/02/14/differential-dataflow/"><![CDATA[<p>Datalog는 사실(Facts)과 규칙(Rules)으로부터 새로운 지식을 연역해내는 논리 프로그래밍 언어입니다. 간결한 재귀 규칙 몇 줄로 소셜 네트워크의 도달가능성을 분석하거나, 지식 그래프 위에서 복잡한 추론을 수행할 수 있습니다.</p>

<p>하지만 Datalog를 실제 시스템에 적용하려면, <strong>“데이터가 변하면 어떻게 하는가”</strong>라는 문제를 피할 수 없습니다.</p>

<p>쇼핑몰의 추천 시스템을 예로 들면, 고객이 새 상품을 하나 구매할 때마다 모든 고객의 추천 목록을 처음부터 다시 계산하는 것은 현실적이지 않습니다. 영향받는 추천만 갱신하면 됩니다. 그런데 전통적인 Datalog 엔진은 사실 하나가 바뀌면 전체 추론을 처음부터 다시 수행합니다.</p>

<h2 id="정적-datalog의-한계">정적 Datalog의 한계</h2>

<p>쇼핑몰에서 “함께 구매되는 상품” 데이터를 기반으로 연관 제품을 추천하는 시나리오로 이 문제를 살펴봅니다. 노트북을 산 사람은 마우스, 파우치, 가방을 함께 사는 경향이 있고, 마우스를 산 사람은 마우스패드를, 파우치를 산 사람은 가방을 새로 구매하는 경향이 있습니다. 이 연쇄를 따라가면, 노트북 구매자에게 마우스패드까지 추천할 수 있습니다. Datalog로 표현하면 다음과 같습니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">bought_together</span><span class="p">(</span><span class="s2">"노트북"</span><span class="p">,</span> <span class="s2">"마우스"</span><span class="p">).</span>
<span class="ss">bought_together</span><span class="p">(</span><span class="s2">"노트북"</span><span class="p">,</span> <span class="s2">"파우치"</span><span class="p">).</span>
<span class="ss">bought_together</span><span class="p">(</span><span class="s2">"노트북"</span><span class="p">,</span> <span class="s2">"가방"</span><span class="p">).</span>
<span class="ss">bought_together</span><span class="p">(</span><span class="s2">"마우스"</span><span class="p">,</span> <span class="s2">"마우스패드"</span><span class="p">).</span>
<span class="ss">bought_together</span><span class="p">(</span><span class="s2">"파우치"</span><span class="p">,</span> <span class="s2">"가방"</span><span class="p">).</span>

<span class="c1">% 연관 추천 (재귀)</span>
<span class="ss">recommends</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">bought_together</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
<span class="ss">recommends</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Y</span><span class="p">)</span> <span class="p">:-</span> <span class="ss">bought_together</span><span class="p">(</span><span class="nv">X</span><span class="p">,</span> <span class="nv">Z</span><span class="p">),</span> <span class="ss">recommends</span><span class="p">(</span><span class="nv">Z</span><span class="p">,</span> <span class="nv">Y</span><span class="p">).</span>
</code></pre></div></div>

<p>이 규칙을 평가하면 직접 연관뿐 아니라, <code class="language-plaintext highlighter-rouge">recommends("노트북", "마우스패드")</code>처럼 재귀적으로 도출하는 추천까지 만들어냅니다. 특히 <code class="language-plaintext highlighter-rouge">recommends("노트북", "가방")</code>은 직접 구매 경로와 파우치를 경유하는 경로, 두 가지로 도출됩니다.</p>

<p>여기에 새로운 구매 패턴을 발견하여 사실 하나를 추가하는 상황을 살펴봅니다.</p>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="ss">bought_together</span><span class="p">(</span><span class="s2">"가방"</span><span class="p">,</span> <span class="s2">"보조배터리"</span><span class="p">).</span>   <span class="c1">% 새로운 사실 추가!</span>
</code></pre></div></div>

<p>나이브(Naive)한 Datalog 엔진은 기존에 도출된 모든 <code class="language-plaintext highlighter-rouge">recommends</code> 관계를 버리고, 처음부터 전체 추론을 다시 실행합니다. 상품이 네 개일 때는 문제가 되지 않지만, 수백만 개의 상품과 수억 건의 구매 이력을 가진 대형 쇼핑몰에서는 치명적인 비용이 됩니다.</p>

<p>이에 대한 고전적인 해법으로 <strong>세미나이브 평가(Semi-naive Evaluation)</strong>가 있습니다. 이전 반복에서 새로 도출된 사실만을 다음 반복의 입력으로 사용하여 중복 계산을 줄이는 기법입니다. 하지만 세미나이브 평가에도 한계가 있습니다. 사실의 <strong>추가</strong>에는 비교적 잘 대응하지만, <strong>삭제</strong>에는 취약합니다. 마우스가 단종되어 <code class="language-plaintext highlighter-rouge">bought_together("노트북", "마우스")</code>를 삭제하면, 마우스를 경유해서 도출했던 <code class="language-plaintext highlighter-rouge">recommends("노트북", "마우스패드")</code> 같은 추천도 무효화해야 합니다. 어떤 추천이 영향받는지를 추적하려면, 결국 상당 부분을 다시 계산해야 합니다.</p>

<h2 id="differential-dataflow-차이만-계산하는-발상">Differential Dataflow: 차이만 계산하는 발상</h2>

<p>2013년, Frank McSherry 등은 이 문제에 대한 근본적으로 다른 접근법인 <strong>Differential Dataflow</strong>를 제안했습니다<a class="citation" href="#McSherry2013">[1]</a>.</p>

<p>핵심 아이디어는 단순합니다. 컬렉션을 정적인 집합으로 보지 않고, <strong>시간에 따른 차이(Difference)의 축적</strong>으로 표현하는 것입니다.</p>

\[\text{Collection}[t] = \sum_{s \leq t} \Delta[s]\]

<p>시점 $t$에서의 컬렉션은, 그 시점 이전에 발생한 모든 변경분($\Delta$)의 합산입니다. 삽입은 $+1$, 삭제는 $-1$로 기록합니다. 이 방식은 메모리 관리에서의 <strong>레퍼런스 카운팅(Reference Counting)</strong>과 구조적으로 닮아 있습니다. 레퍼런스 카운팅에서 객체의 참조가 생기면 카운트를 $+1$, 참조가 사라지면 $-1$하여 카운트가 $0$이 되면 객체를 해제하듯, Differential Dataflow에서도 각 튜플의 가중치가 $0$이 되면 해당 사실이 컬렉션에서 사라집니다. 다만 Differential Dataflow는 이를 단순한 생존 여부를 넘어 <strong>가중 집합(weighted set, 혹은 Z-set)</strong>으로 일반화합니다. 가중 집합이란 각 원소에 정수 가중치를 부여한 집합으로, 일반 집합이 “있다/없다”만 표현할 수 있는 반면, 가중 집합은 “몇 번 도출되었는가”까지 표현합니다. 앞의 예에서 <code class="language-plaintext highlighter-rouge">recommends("노트북", "가방")</code>은 직접 구매 경로와 파우치를 경유하는 경로, 두 가지로 도출되므로 가중치가 $2$입니다. 이때 파우치와 가방의 연관이 사라져 한 경로가 무효화($-1$)되더라도 가중치가 $1$로 남아 있으므로 해당 추천은 여전히 유효합니다. 레퍼런스 카운팅이 참조 수로 객체의 생존을 결정하듯, 가중 집합은 도출 경로의 수로 사실의 유효성을 결정하는 것입니다.</p>

<p><img src="/assets/images/weighted-set-comparison.svg" alt="가중 집합 비교: 가중치 2 vs 1" /></p>

<p>추천 시스템의 예에 적용하면, 추천 목록(Collection)은 과거 구매 이력 변화(Difference)의 누적이며, 새 구매 패턴을 발견하면 영향받는 추천만 갱신하면 됩니다.</p>

<p>이 접근이 강력한 이유는 ‘시간’의 개념을 단순한 일련번호가 아니라 <strong>부분 순서(Partial Order)</strong>로 일반화하기 때문입니다. 예를 들어 Datalog의 재귀 규칙을 평가할 때, 시간은 <code class="language-plaintext highlighter-rouge">(외부 시점, 내부 반복 횟수)</code>라는 2차원 좌표가 됩니다. Differential Dataflow는 <strong>뫼비우스 역변환(Möbius Inversion)</strong>이라는 수학적 기법을 사용하여 이러한 다차원 시간 축에서도 차이를 정확하게 계산합니다.</p>

<p>이 모든 것은 <strong>Timely Dataflow</strong>라는 분산 데이터플로우 런타임 위에서 동작합니다. Timely Dataflow가 데이터의 흐름과 병렬 실행을 관리하고, Differential Dataflow가 그 위에서 증분 계산의 논리를 담당하는 구조입니다.</p>

<h2 id="일괄-처리-스트림-처리-그리고-differential-dataflow">일괄 처리, 스트림 처리, 그리고 Differential Dataflow</h2>

<p>기존의 데이터 처리 패러다임과 비교하면 Differential Dataflow의 위치가 명확해집니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">구분</th>
      <th style="text-align: left">일괄 처리 (Batch)</th>
      <th style="text-align: left">스트림 처리 (Stream)</th>
      <th style="text-align: left">Differential Dataflow</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>데이터 모델</strong></td>
      <td style="text-align: left">정적 데이터셋</td>
      <td style="text-align: left">무한 이벤트 흐름</td>
      <td style="text-align: left">차이(Difference)의 축적</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>변경 대응</strong></td>
      <td style="text-align: left">전체 재계산</td>
      <td style="text-align: left">개별 이벤트 처리</td>
      <td style="text-align: left">변경분만 전파</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>재귀 지원</strong></td>
      <td style="text-align: left">매 반복 전체 재실행</td>
      <td style="text-align: left">제한적 (윈도우 기반)</td>
      <td style="text-align: left">네이티브 (중첩된 반복 추적)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>지연 시간</strong></td>
      <td style="text-align: left">높음</td>
      <td style="text-align: left">낮음</td>
      <td style="text-align: left">낮음 (변경 크기에 비례)</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>처리량</strong></td>
      <td style="text-align: left">높음</td>
      <td style="text-align: left">중간</td>
      <td style="text-align: left">높음</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>대표 시스템</strong></td>
      <td style="text-align: left">Hadoop, Spark (batch)</td>
      <td style="text-align: left">Kafka Streams, Flink</td>
      <td style="text-align: left">differential-dataflow, Materialize</td>
    </tr>
  </tbody>
</table>

<p>일괄 처리는 전체를 다시 계산하므로 정확하지만 느리고, 스트림 처리는 빠르지만 재귀적 추론을 표현하기 어렵습니다. Differential Dataflow는 <strong>변경의 크기에 비례하는 비용</strong>으로 <strong>재귀를 포함한 복잡한 연산</strong>을 증분적으로 유지합니다.</p>

<h2 id="ddlog-datalog를-증분으로-만들다">DDlog: Datalog를 증분으로 만들다</h2>

<p>Differential Dataflow의 개념을 Datalog에 직접 적용한 것이 <strong>Differential Datalog(DDlog)</strong>입니다.</p>

<p>DDlog의 핵심은 다음과 같습니다.</p>

<blockquote>
  <p>프로그래머는 평범한 Datalog 규칙을 작성하고, DDlog 컴파일러가 이를 자동으로 증분 실행 가능한 Differential Dataflow 파이프라인으로 변환한다.</p>
</blockquote>

<div class="language-prolog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">//</span> <span class="nv">DDlog</span> <span class="err">규칙</span>
<span class="ss">input</span> <span class="ss">relation</span> <span class="nv">BoughtTogether</span><span class="p">(</span><span class="ss">item</span><span class="o">:</span> <span class="ss">string</span><span class="p">,</span> <span class="ss">related</span><span class="o">:</span> <span class="ss">string</span><span class="p">)</span>
<span class="ss">output</span> <span class="ss">relation</span> <span class="nv">Recommends</span><span class="p">(</span><span class="ss">item</span><span class="o">:</span> <span class="ss">string</span><span class="p">,</span> <span class="ss">recommended</span><span class="o">:</span> <span class="ss">string</span><span class="p">)</span>

<span class="nv">Recommends</span><span class="p">(</span><span class="ss">x</span><span class="p">,</span> <span class="ss">y</span><span class="p">)</span> <span class="p">:-</span> <span class="nv">BoughtTogether</span><span class="p">(</span><span class="ss">x</span><span class="p">,</span> <span class="ss">y</span><span class="p">).</span>
<span class="nv">Recommends</span><span class="p">(</span><span class="ss">x</span><span class="p">,</span> <span class="ss">y</span><span class="p">)</span> <span class="p">:-</span> <span class="nv">BoughtTogether</span><span class="p">(</span><span class="ss">x</span><span class="p">,</span> <span class="ss">z</span><span class="p">),</span> <span class="nv">Recommends</span><span class="p">(</span><span class="ss">z</span><span class="p">,</span> <span class="ss">y</span><span class="p">).</span>

<span class="o">//</span> <span class="err">런타임</span> <span class="err">동작</span><span class="o">:</span>
<span class="o">//</span> <span class="nv">BoughtTogether</span><span class="p">(</span><span class="s2">"가방"</span><span class="p">,</span> <span class="s2">"보조배터리"</span><span class="p">)</span> <span class="err">삽입</span> <span class="err">→</span>
<span class="o">//</span> <span class="nv">Recommends</span><span class="err">에서</span> <span class="err">변경된</span> <span class="err">튜플만</span> <span class="err">자동으로</span> <span class="err">전파</span>
<span class="o">//</span> <span class="nv">BoughtTogether</span><span class="p">(</span><span class="s2">"파우치"</span><span class="p">,</span> <span class="s2">"가방"</span><span class="p">)</span> <span class="err">삭제</span> <span class="err">→</span>
<span class="o">//</span> <span class="nv">Recommends</span><span class="p">(</span><span class="s2">"노트북"</span><span class="p">,</span> <span class="s2">"가방"</span><span class="p">)</span><span class="err">의</span> <span class="err">가중치가</span> <span class="m">2</span><span class="err">→</span><span class="m">1</span><span class="err">로</span> <span class="err">갱신</span> <span class="p">(</span><span class="err">직접</span> <span class="err">구매</span> <span class="err">경로</span> <span class="err">유지</span><span class="p">)</span>
</code></pre></div></div>

<p>프로그래머 입장에서는 일반적인 Datalog를 작성하는 것과 동일합니다. 하지만 런타임의 동작은 근본적으로 다릅니다. 입력 관계(relation)에 삽입이나 삭제가 스트리밍으로 들어오면, 출력 관계에서 <strong>변경된 부분만</strong> 자동으로 갱신합니다. 세미나이브 평가가 해결하지 못했던 삭제 문제까지 처리합니다.</p>

<h2 id="rust로-만져보는-differential-dataflow">Rust로 만져보는 Differential Dataflow</h2>

<p>Differential Dataflow의 정식 구현체(canonical implementation)는 Rust로 작성한 라이브러리입니다. <code class="language-plaintext highlighter-rouge">differential-dataflow</code> 크레이트(crate)를 사용하면, 앞서 살펴본 연관 추천 계산을 다음과 같이 표현할 수 있습니다.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">differential_dataflow</span><span class="p">::</span><span class="nn">input</span><span class="p">::</span><span class="n">Input</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">differential_dataflow</span><span class="p">::</span><span class="nn">operators</span><span class="p">::</span><span class="n">Iterate</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">differential_dataflow</span><span class="p">::</span><span class="nn">operators</span><span class="p">::</span><span class="n">Join</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">differential_dataflow</span><span class="p">::</span><span class="nn">operators</span><span class="p">::</span><span class="n">Consolidate</span><span class="p">;</span>

<span class="nn">timely</span><span class="p">::</span><span class="nf">execute_directly</span><span class="p">(</span><span class="k">move</span> <span class="p">|</span><span class="n">worker</span><span class="p">|</span> <span class="p">{</span>
    <span class="k">let</span> <span class="p">(</span><span class="k">mut</span> <span class="n">input</span><span class="p">,</span> <span class="n">probe</span><span class="p">)</span> <span class="o">=</span> <span class="n">worker</span><span class="nf">.dataflow</span><span class="p">(|</span><span class="n">scope</span><span class="p">|</span> <span class="p">{</span>
        <span class="k">let</span> <span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">edges</span><span class="p">)</span> <span class="o">=</span> <span class="n">scope</span><span class="py">.new_collection</span><span class="p">::</span><span class="o">&lt;</span><span class="p">(</span><span class="nb">String</span><span class="p">,</span> <span class="nb">String</span><span class="p">),</span> <span class="nb">isize</span><span class="o">&gt;</span><span class="p">();</span>

        <span class="c1">// Datalog의 recommends 규칙과 동일한 연관 추천 계산</span>
        <span class="k">let</span> <span class="n">recommendations</span> <span class="o">=</span> <span class="n">edges</span><span class="nf">.iterate</span><span class="p">(|</span><span class="n">reach</span><span class="p">|</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">edges</span> <span class="o">=</span> <span class="n">edges</span><span class="nf">.enter</span><span class="p">(</span><span class="o">&amp;</span><span class="n">reach</span><span class="nf">.scope</span><span class="p">());</span>
            <span class="n">reach</span>
                <span class="nf">.join_map</span><span class="p">(</span><span class="o">&amp;</span><span class="n">edges</span><span class="p">,</span> <span class="p">|</span><span class="n">_mid</span><span class="p">,</span> <span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">|</span> <span class="p">(</span><span class="n">dst</span><span class="nf">.clone</span><span class="p">(),</span> <span class="n">src</span><span class="nf">.clone</span><span class="p">()))</span>
                <span class="nf">.concat</span><span class="p">(</span><span class="o">&amp;</span><span class="n">edges</span><span class="p">)</span>
                <span class="nf">.distinct</span><span class="p">()</span>
        <span class="p">});</span>

        <span class="k">let</span> <span class="n">probe</span> <span class="o">=</span> <span class="n">recommendations</span><span class="nf">.consolidate</span><span class="p">()</span><span class="nf">.probe</span><span class="p">();</span>
        <span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">probe</span><span class="p">)</span>
    <span class="p">});</span>

    <span class="c1">// 시점 0: 초기 구매 패턴 입력</span>
    <span class="n">input</span><span class="nf">.insert</span><span class="p">((</span><span class="s">"노트북"</span><span class="nf">.into</span><span class="p">(),</span> <span class="s">"마우스"</span><span class="nf">.into</span><span class="p">()));</span>
    <span class="n">input</span><span class="nf">.insert</span><span class="p">((</span><span class="s">"노트북"</span><span class="nf">.into</span><span class="p">(),</span> <span class="s">"파우치"</span><span class="nf">.into</span><span class="p">()));</span>
    <span class="n">input</span><span class="nf">.insert</span><span class="p">((</span><span class="s">"노트북"</span><span class="nf">.into</span><span class="p">(),</span> <span class="s">"가방"</span><span class="nf">.into</span><span class="p">()));</span>
    <span class="n">input</span><span class="nf">.insert</span><span class="p">((</span><span class="s">"마우스"</span><span class="nf">.into</span><span class="p">(),</span> <span class="s">"마우스패드"</span><span class="nf">.into</span><span class="p">()));</span>
    <span class="n">input</span><span class="nf">.insert</span><span class="p">((</span><span class="s">"파우치"</span><span class="nf">.into</span><span class="p">(),</span> <span class="s">"가방"</span><span class="nf">.into</span><span class="p">()));</span>
    <span class="n">input</span><span class="nf">.advance_to</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
    <span class="n">input</span><span class="nf">.flush</span><span class="p">();</span>
    <span class="n">worker</span><span class="nf">.step_while</span><span class="p">(||</span> <span class="n">probe</span><span class="nf">.less_than</span><span class="p">(</span><span class="n">input</span><span class="nf">.time</span><span class="p">()));</span>

    <span class="c1">// 시점 1: 새로운 구매 패턴 추가 — 변경분만 재계산</span>
    <span class="n">input</span><span class="nf">.insert</span><span class="p">((</span><span class="s">"가방"</span><span class="nf">.into</span><span class="p">(),</span> <span class="s">"보조배터리"</span><span class="nf">.into</span><span class="p">()));</span>
    <span class="n">input</span><span class="nf">.advance_to</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
    <span class="n">input</span><span class="nf">.flush</span><span class="p">();</span>
    <span class="n">worker</span><span class="nf">.step_while</span><span class="p">(||</span> <span class="n">probe</span><span class="nf">.less_than</span><span class="p">(</span><span class="n">input</span><span class="nf">.time</span><span class="p">()));</span>
<span class="p">});</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">advance_to(1)</code>에서 <code class="language-plaintext highlighter-rouge">advance_to(2)</code>로 진행할 때, 시스템은 <code class="language-plaintext highlighter-rouge">("가방", "보조배터리")</code> 추가로 인해 영향받는 추천 관계만 증분적으로 계산합니다. 기존의 <code class="language-plaintext highlighter-rouge">("노트북", "마우스")</code>, <code class="language-plaintext highlighter-rouge">("마우스", "마우스패드")</code> 등 기존 추천 추론은 그대로 유지됩니다.</p>

<h2 id="진화하는-지식-그래프를-위하여">진화하는 지식 그래프를 위하여</h2>

<p>현실 세계의 지식 그래프는 정적이지 않습니다. 새로운 논문이 나오면 인용 관계가 바뀌고, 조직 개편이 일어나면 보고 체계가 변하며, 쇼핑몰에서는 매 시간 새로운 구매 패턴이 생깁니다. 대화형 AI 에이전트의 경우에도, 사용자가 요구사항을 추가하거나 수정할 때마다 지식 그래프의 상태는 계속 변합니다. 이런 상황에서 매번 전체 추론을 다시 수행하는 것은 비현실적입니다.</p>

<p>Differential Dataflow는 지식 그래프의 변경을 차이(Difference)로 표현하고, 추론 결과를 증분적으로 유지함으로써, <strong>변화하는 데이터 위에서 실시간으로 추론할 수 있게</strong> 합니다.</p>

<p>최근에는 이 핵심 아이디어를 데이터베이스와 스트림 처리에 특화시킨 <strong>DBSP(Database Stream Processing)</strong>도 등장했습니다. Materialize와 같은 시스템이 이를 기반으로 SQL 뷰의 증분 유지(Incremental View Maintenance)를 구현하고 있으며, 증분 계산의 영향력은 논리 프로그래밍을 넘어 더 넓은 데이터 생태계로 확장되고 있습니다.</p>

<h2 id="마치며">마치며</h2>

<p>전통적인 Datalog는 “변하지 않는 사실”을 전제로 합니다. 모든 데이터를 확정한 뒤에야 추론을 시작할 수 있는 구조입니다. 하지만 현실의 데이터는 계속 변합니다. 신상품이 들어오고, 인기 상품이 사라지며, 구매 패턴은 시시각각 달라집니다.</p>

<p>Differential Dataflow는 데이터의 변화 자체를 계산의 일급 시민(first-class citizen)으로 다룹니다. 변경분만을 추적하고 전파함으로써, 전체 재계산 없이도 정확한 추론 결과를 유지할 수 있게 합니다. 이것이 Differential Dataflow가 논리 프로그래밍에 가져다 준 가장 본질적인 전환입니다.</p>

<hr />

<h3 id="참고-문헌">참고 문헌</h3>

<ol class="bibliography"><li>F. McSherry, D. G. Murray, R. Isaacs, and M. Isard, “Differential Dataflow,” in <i>Proceedings of the 6th Biennial Conference on Innovative Data Systems Research (CIDR ’13)</i>, CIDR, Jan. 2013. <a href="https://www.cidrdb.org/cidr2013/Papers/CIDR13_Paper111.pdf">[Link]</a>
</li></ol>

<h3 id="관련-글">관련 글</h3>

<ul>
  <li><a href="/essay/ai/2026/02/01/introducing-datalog/">SPARQL의 SQL 유사성이 주는 함정, 그리고 Datalog</a></li>
  <li><a href="/essay/ai/2026/02/03/asp-and-datalog/">ASP와 Datalog: 논리 프로그래밍의 두 가지 시선</a></li>
  <li><a href="/essay/ai/2026/02/13/askitect-prototype/">Askitect 프로토타입: 청사진에서 동작하는 코드로</a></li>
</ul>]]></content><author><name>Justin Kim</name></author><category term="essay" /><category term="ai" /><category term="Datalog" /><category term="Differential Dataflow" /><category term="Incremental Computation" /><category term="Logic Programming" /><category term="Knowledge Graph" /><summary type="html"><![CDATA[Datalog는 사실(Facts)과 규칙(Rules)으로부터 새로운 지식을 연역해내는 논리 프로그래밍 언어입니다. 간결한 재귀 규칙 몇 줄로 소셜 네트워크의 도달가능성을 분석하거나, 지식 그래프 위에서 복잡한 추론을 수행할 수 있습니다.]]></summary></entry></feed>