<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://seonghak.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://seonghak.com/" rel="alternate" type="text/html" hreflang="ko" /><updated>2026-05-11T23:10:12+09:00</updated><id>https://seonghak.com/feed.xml</id><title type="html">Seonghak — Cloud Solution Architect</title><subtitle>Seonghak은 Google Cloud(GCP) 기반의 AI, 머신러닝, 데이터 엔지니어링을 다루는 클라우드 솔루션 아키텍트입니다. BigQuery, Agent Engine, Gemini Enterprise 등 실전 워크북과 기술 블로그를 제공합니다.</subtitle><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><entry><title type="html">Multi-LLM Lock-in과 Harness Engineering</title><link href="https://seonghak.com/blog/2026/04/14/multi-llm-lockin-and-harness-engineering/" rel="alternate" type="text/html" title="Multi-LLM Lock-in과 Harness Engineering" /><published>2026-04-14T00:00:00+09:00</published><updated>2026-04-14T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/04/14/multi-llm-lockin-and-harness-engineering</id><content type="html" xml:base="https://seonghak.com/blog/2026/04/14/multi-llm-lockin-and-harness-engineering/"><![CDATA[<p>“특정 LLM에 종속되면 안 된다.”</p>

<p>이 말은 맞습니다. 그래서 많은 기업들이 Multi-LLM 전략에 집착합니다. “에이전트에서 LLM 설정값만 바꾸면 그대로 쓸 수 있다”고 생각하죠.</p>

<p>하지만 현실에서 이를 실행해 본 분이라면 아실 겁니다. <strong>LLM을 바꾸는 순간, 에이전트의 행동 자체가 달라집니다.</strong> 같은 도구, 같은 프롬프트, 같은 파이프라인인데 결과가 완전히 다릅니다. Lock-in 방지는 API 엔드포인트를 바꾸는 문제가 아니라, <strong>에이전트를 처음부터 다시 세팅하는 문제</strong>입니다.</p>

<hr />

<h2 id="multi-llm의-진짜-문제-프롬프트는-이식되지-않는다">Multi-LLM의 진짜 문제 “프롬프트는 이식되지 않는다”</h2>

<p>많은 분들이 LLM을 API로 바라봅니다. “입력을 넣으면 출력이 나오는 함수”라고 생각하죠. 그래서 OpenAI 대신 Claude를 쓰고, Gemini로 바꾸면 되지 않느냐고 말합니다.</p>

<p>그런데 실제로 해보면 이런 일이 벌어집니다:</p>

<ul>
  <li><strong>같은 프롬프트, 다른 결과.</strong> GPT-4o에서 잘 동작하던 프롬프트가 Claude에서는 과도하게 cautious한 응답을 내놓고, Gemini에서는 구조가 완전히 달라집니다.</li>
  <li><strong>컨텍스트 윈도우 전략이 달라집니다.</strong> 128K 토큰을 쓸 수 있는 모델과 200K를 쓸 수 있는 모델에서의 컨텍스트 설계는 완전히 다른 엔지니어링입니다.</li>
  <li><strong>System prompt에 대한 반응성이 다릅니다.</strong> 어떤 모델은 system prompt를 충실히 따르고, 어떤 모델은 user turn의 마지막 지시를 더 우선합니다.</li>
</ul>

<p>결국 모델을 바꾸면 <strong>프롬프트 엔지니어링을 처음부터 다시 해야 합니다.</strong></p>

<hr />

<h2 id="llm을-바꾸면-에이전트가-달라진다">LLM을 바꾸면 에이전트가 달라진다</h2>

<p>프롬프트 이식성은 시작일 뿐입니다. 진짜 문제는 에이전트 수준에서 드러납니다. 에이전트는 단순히 프롬프트를 실행하는 것이 아니라, LLM의 판단력에 의존해 <strong>자율적으로 행동을 결정</strong>합니다. LLM이 바뀌면 그 판단이 바뀌고, 에이전트의 행동 전체가 달라집니다.</p>

<p><strong>LLM을 바꾸는 것은 엔진을 바꾸는 것이 아니라 운전자를 바꾸는 것에 가깝습니다.</strong> 같은 차, 같은 길이어도 운전 방식이 완전히 달라집니다.</p>

<p>구체적으로 어떤 일이 벌어지는지 보겠습니다:</p>

<ul>
  <li><strong>Tool calling 패턴이 달라집니다.</strong> 같은 도구 목록을 줘도 모델마다 도구 선택 전략이 다릅니다. 어떤 모델은 도구를 적극적으로 호출하고, 어떤 모델은 자체 추론으로 해결하려 합니다. 도구를 호출하는 순서, 병렬 호출 여부, 파라미터 구성 방식까지 전부 달라집니다.</li>
  <li><strong>판단 기준이 달라집니다.</strong> “충분한 정보를 얻었는가”, “사용자에게 추가 질문이 필요한가”, “이 작업을 중단해야 하는가” — 에이전트의 모든 분기점에서 모델마다 다른 판단을 내립니다. GPT-4o에서 한 번에 끝나던 작업이 Claude에서는 확인 질문을 3번 거치는 식입니다.</li>
  <li><strong>오류 처리 방식이 달라집니다.</strong> 도구 호출이 실패했을 때 재시도할지, 대안 경로를 찾을지, 사용자에게 보고할지 — 이 전략이 모델마다 다릅니다. 한 모델에서 안정적이던 에이전트가 다른 모델에서는 무한 재시도 루프에 빠질 수 있습니다.</li>
  <li><strong>멀티스텝 추론 경로가 달라집니다.</strong> 같은 목표를 줘도 모델마다 문제를 분해(decompose)하는 방식과 순서가 다릅니다. 에이전트의 전체 실행 흐름 — 몇 단계를 거치는지, 어떤 순서로 처리하는지 — 이 LLM에 따라 완전히 달라집니다.</li>
</ul>

<p>결론적으로 <strong>에이전트에서 LLM만 교체하면 “같은 에이전트”가 아닙니다.</strong> 겉은 같아 보여도 행동이 다른, 사실상 새로운 에이전트입니다. 그래서 LLM을 바꾸면 결국 에이전트의 세팅을 처음부터 다시 해야 합니다.</p>

<hr />

<h2 id="lock-in의-본질은-api가-아니라-엔지니어링-투자">Lock-in의 본질은 API가 아니라 “엔지니어링 투자”</h2>

<p>LLM lock-in을 API 호환성의 문제로 보면 간단해 보입니다. OpenAI 호환 API를 쓰면 되니까요. 하지만 진짜 lock-in은 세 가지 층위에서 발생합니다:</p>

<h3 id="1-prompt-engineering-lock-in">1. Prompt Engineering Lock-in</h3>

<p>수개월에 걸쳐 최적화한 프롬프트 체계. Few-shot 예시, chain-of-thought 구조, 출력 포맷 제어 — 이 모든 것이 특정 모델의 행동 패턴에 맞춰져 있습니다.</p>

<h3 id="2-context-engineering-lock-in">2. Context Engineering Lock-in</h3>

<p>RAG 파이프라인에서 어떤 정보를 얼마나, 어떤 순서로 컨텍스트에 넣을지. 이 설계는 모델의 컨텍스트 윈도우 크기, 위치 편향(positional bias), 정보 검색 능력에 종속됩니다.</p>

<h3 id="3-harness-engineering-lock-in">3. Harness Engineering Lock-in</h3>

<p>에이전트의 도구 호출 방식, 오류 처리, 반복 루프 설계, 멀티턴 대화 관리 — 이런 “모델을 감싸는 시스템”이 특정 모델의 function calling 스펙, 응답 구조, latency 특성에 맞춰져 있습니다. 여기에는 눈에 잘 보이지 않는 세팅들이 포함됩니다: 도구 선택 우선순위, 재시도 횟수와 백오프 정책, “충분하다”고 판단하는 임계값, 안전 장치의 트리거 조건 등. 이 모든 세팅이 특정 LLM의 행동 특성에 맞춰 튜닝된 것입니다. LLM을 교체하면 이 세팅을 전부 재검증하고 재조정해야 하며, 이는 사실상 에이전트를 처음부터 다시 만드는 것에 가까운 비용을 발생시킵니다.</p>

<p><strong>모델을 바꾸는 것은 API 엔드포인트를 바꾸는 것이 아닙니다. 이 세 층위의 엔지니어링을 모두 다시 하는 것입니다.</strong></p>

<hr />

<h2 id="관점의-전환-multi-llm이-아니라-multi-agent">관점의 전환: Multi-LLM이 아니라 Multi-Agent</h2>

<p>여기서 발상의 전환이 필요합니다.</p>

<p>“어떤 LLM이든 갈아끼울 수 있게 만들자”는 접근은 <strong>각 모델의 강점을 포기하는 것</strong>과 같습니다. 최소공배수 방식으로 프롬프트를 설계하면, 어떤 모델에서도 “그럭저럭” 동작하지만 어떤 모델에서도 “최적”이 되지 않습니다.</p>

<p>대신 이렇게 생각해야 합니다.</p>

<blockquote>
  <p><strong>각 LLM의 강점을 살리는 에이전트를 설계하고, 이 에이전트들을 오케스트레이션하는 Harness를 만들자.</strong></p>
</blockquote>

<ul>
  <li>복잡한 추론이 필요한 태스크 → Claude에 최적화된 에이전트</li>
  <li>코드 생성과 실행 → Gemini에 최적화된 에이전트</li>
  <li>빠른 분류와 라우팅 → 경량 모델에 최적화된 에이전트</li>
</ul>

<p>이때 중요한 것은 <strong>개별 에이전트가 아니라 이들을 엮는 Harness Engineering</strong>입니다. 에이전트 간 통신 프로토콜, 상태 관리, 오류 전파, 관찰 가능성(observability) — 이것이 진정한 엔지니어링 과제입니다.</p>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="multi-llm" /><category term="lock-in" /><category term="harness-engineering" /><category term="multi-agent" /><category term="prompt-engineering" /><category term="context-engineering" /><summary type="html"><![CDATA[“특정 LLM에 종속되면 안 된다.”]]></summary></entry><entry><title type="html">Google Gemini 제품군 비교: Web, Workspace, Enterprise, Vertex AI + Gemini API</title><link href="https://seonghak.com/blog/2026/04/05/gemini-products-comparison/" rel="alternate" type="text/html" title="Google Gemini 제품군 비교: Web, Workspace, Enterprise, Vertex AI + Gemini API" /><published>2026-04-05T00:00:00+09:00</published><updated>2026-04-05T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/04/05/gemini-products-comparison</id><content type="html" xml:base="https://seonghak.com/blog/2026/04/05/gemini-products-comparison/"><![CDATA[<p>“Gemini”라는 이름이 붙은 Google 제품이 여러 개 있어서 고객분들이 자주 혼동하십니다. <strong>이름은 비슷하지만 대상, 요금 방식, 기능이 완전히 다릅니다.</strong> 이 글에서 한 번에 정리합니다.</p>

<hr />

<h2 id="전체-지도-gemini-4종-분류">전체 지도: Gemini 4종 분류</h2>

<table>
  <thead>
    <tr>
      <th>제품</th>
      <th>결제 주체</th>
      <th>요금 방식</th>
      <th>대표 쓰임새</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Gemini Web</strong> (무료)</td>
      <td>개인</td>
      <td>무료</td>
      <td>가벼운 챗봇 사용</td>
    </tr>
    <tr>
      <td><strong>Google AI Pro/Ultra</strong> (개인 유료)</td>
      <td>개인</td>
      <td>월 정액 (~$20/월)</td>
      <td>리서치, 코딩, 파일 분석</td>
    </tr>
    <tr>
      <td><strong>Gemini for Workspace</strong></td>
      <td>회사 (Workspace)</td>
      <td>사용자당 월 과금</td>
      <td>Gmail/Docs/Meet 내 업무용 AI</td>
    </tr>
    <tr>
      <td><strong>Gemini Enterprise</strong></td>
      <td>회사 (GCP)</td>
      <td>라이선스당 월 ~$30~</td>
      <td>엔터프라이즈 에이전트/포털</td>
    </tr>
    <tr>
      <td><strong>Vertex AI + Gemini API</strong></td>
      <td>개발팀 (GCP)</td>
      <td>토큰/리소스 사용량</td>
      <td>커스텀 앱/에이전트 백엔드</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="1-gemini-web-개인용">1. Gemini Web (개인용)</h2>

<p>gemini.google.com 또는 모바일 앱으로 접근하는 <strong>개인용 AI 챗봇</strong>입니다.</p>

<h3 id="무료-vs-pro-vs-ultra">무료 vs Pro vs Ultra</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>무료</th>
      <th>Google AI Pro</th>
      <th>Ultra</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>가격</strong></td>
      <td>0원</td>
      <td>~$20/월</td>
      <td>~$250+/월</td>
    </tr>
    <tr>
      <td><strong>모델</strong></td>
      <td>Gemini 2.5 Flash</td>
      <td>Gemini 2.5 Pro</td>
      <td>Pro + Deep Think/Ultra</td>
    </tr>
    <tr>
      <td><strong>컨텍스트</strong></td>
      <td>짧은 대화 위주</td>
      <td>~100만 토큰 (약 1,500페이지)</td>
      <td>더 긴 컨텍스트</td>
    </tr>
    <tr>
      <td><strong>파일 분석</strong></td>
      <td>제한적</td>
      <td>대용량 문서/코드/PDF/동영상</td>
      <td>고난도 분석</td>
    </tr>
    <tr>
      <td><strong>Deep Research</strong></td>
      <td>X</td>
      <td>O</td>
      <td>O (강화)</td>
    </tr>
    <tr>
      <td><strong>이미지/영상 생성</strong></td>
      <td>제한적</td>
      <td>일부</td>
      <td>Veo 상위 기능</td>
    </tr>
    <tr>
      <td><strong>Gmail/Drive 연동</strong></td>
      <td>X</td>
      <td>O (개인 데이터)</td>
      <td>O</td>
    </tr>
  </tbody>
</table>

<h3 id="핵심-포인트">핵심 포인트</h3>

<ul>
  <li><strong>무료</strong>는 Flash 모델 기반으로 간단한 질문/번역/요약 용도</li>
  <li><strong>Pro</strong>는 장문 리포트, 대형 문서 분석, 고급 리서치에 적합</li>
  <li><strong>Ultra</strong>는 복잡한 추론, 고난도 코딩, 전문 리서치용</li>
</ul>

<blockquote>
  <p>개인용은 <strong>“월 ~$20 내고 강력한 Gemini를 쓸 것인가, 무료로 가볍게 쓸 것인가”</strong> 선택의 문제입니다.</p>
</blockquote>

<hr />

<h2 id="2-gemini-for-google-workspace">2. Gemini for Google Workspace</h2>

<p>Gmail, Docs, Sheets, Slides, Meet, Chat 등 <strong>Workspace 앱 안에 붙는 AI 기능</strong>입니다.</p>

<h3 id="어디에서-뭘-해주나">어디에서 뭘 해주나?</h3>

<table>
  <thead>
    <tr>
      <th>Workspace 앱</th>
      <th>AI 기능</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Gmail</strong></td>
      <td>이메일 초안 작성, 요약, 답장 제안</td>
    </tr>
    <tr>
      <td><strong>Docs</strong></td>
      <td>문서 작성/수정/요약 보조</td>
    </tr>
    <tr>
      <td><strong>Sheets</strong></td>
      <td>수식 생성, 데이터 분류, 템플릿</td>
    </tr>
    <tr>
      <td><strong>Slides</strong></td>
      <td>슬라이드 자동 생성, 이미지 생성</td>
    </tr>
    <tr>
      <td><strong>Meet</strong></td>
      <td>회의 요약, 자동 노트, 번역 자막</td>
    </tr>
    <tr>
      <td><strong>Chat</strong></td>
      <td>대화 요약</td>
    </tr>
    <tr>
      <td><strong>Drive</strong></td>
      <td>자연어로 파일 검색 및 요약</td>
    </tr>
  </tbody>
</table>

<h3 id="요금-구조">요금 구조</h3>

<p><strong>Google Workspace 요금제 + Gemini 애드온</strong> 구조입니다.</p>

<table>
  <thead>
    <tr>
      <th>결제 방식</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>탄력 요금제 (Flex)</strong></td>
      <td>월 단위, 사용자 수 가변, 일할 계산</td>
    </tr>
    <tr>
      <td><strong>연간/약정 요금제</strong></td>
      <td>1년 약정, Flex보다 사용자당 월 요금이 낮음</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>Workspace Business Starter/Standard/Plus 등에 “Gemini 포함 플랜” 또는 “AI 애드온”으로 추가</li>
  <li>결제 단위: <strong>사용자 수 x 월 요금</strong>, 중도 추가/삭제 시 일할 계산</li>
</ul>

<h3 id="개인용-gemini-web과의-차이">개인용 Gemini Web과의 차이</h3>

<p>같은 Gemini라도 Workspace 계정이면:</p>

<ul>
  <li><strong>조직 데이터</strong>(공유 드라이브, 조직 캘린더 등)에 접근 가능</li>
  <li><strong>관리 콘솔</strong>에서 사용 제한, 로그/감사, DLP, Vault 등과 연계</li>
  <li><strong>기업 약관</strong> + <strong>데이터 미학습 보장</strong> 적용</li>
</ul>

<blockquote>
  <p>Workspace용 Gemini는 <strong>“직원당 월 X달러를 더 내고, 구글 업무 앱마다 AI를 붙인다”</strong>라고 이해하면 됩니다.</p>
</blockquote>

<hr />

<h2 id="3-gemini-enterprise-gcp-기반">3. Gemini Enterprise (GCP 기반)</h2>

<p>Workspace에 붙는 도구가 아니라, <strong>GCP에서 제공되는 엔터프라이즈 AI 포털/에이전트 플랫폼</strong>입니다.</p>

<h3 id="workspace용-gemini와-뭐가-다른가">Workspace용 Gemini와 뭐가 다른가?</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Workspace 속 Gemini</th>
      <th>Gemini Enterprise</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>위치</strong></td>
      <td>Gmail/Docs/Meet 안</td>
      <td>별도 AI 포털 + GCP 콘솔</td>
    </tr>
    <tr>
      <td><strong>역할</strong></td>
      <td>개인의 생산성 도구</td>
      <td>조직 차원의 에이전트 플랫폼</td>
    </tr>
    <tr>
      <td><strong>기능</strong></td>
      <td>앱 내 글쓰기/요약/분석 보조</td>
      <td>AI 에이전트 호스팅, 조직 데이터 인덱싱/검색</td>
    </tr>
    <tr>
      <td><strong>관리</strong></td>
      <td>Workspace 관리 콘솔</td>
      <td>GCP 콘솔 (로깅, 모니터링, 정책)</td>
    </tr>
    <tr>
      <td><strong>보안</strong></td>
      <td>Workspace 수준</td>
      <td>VPC-SC, CMEK, FedRAMP High, HIPAA 등</td>
    </tr>
  </tbody>
</table>

<h3 id="요금">요금</h3>

<ul>
  <li><strong>사용자당 월 ~$30부터</strong> (Standard/Plus 등 SKU별 상이)</li>
  <li>라이선스당 인덱스 스토리지(예: 75GiB) 포함</li>
  <li>볼륨 디스카운트 가능</li>
</ul>

<h3 id="주요-기능">주요 기능</h3>

<ul>
  <li>직원이 접속하는 <strong>AI 허브/포털</strong></li>
  <li>조직 데이터 <strong>인덱싱 및 검색</strong> (Drive, Calendar, Gmail, Chat 연동)</li>
  <li>복잡한 업무용 <strong>AI 에이전트 호스팅</strong></li>
  <li>Vertex AI MCP, Search, API 연동으로 워크플로 에이전트 정의</li>
  <li>고급 보안: VPC-SC, CMEK, 액세스 투명성, 데이터 상주성</li>
</ul>

<blockquote>
  <p>Gemini Enterprise는 <strong>“직원당 월 $30 이상 내고, 회사 전용 AI 포털과 에이전트 플랫폼을 산다”</strong>는 개념입니다.</p>
</blockquote>

<hr />

<h2 id="4-vertex-ai--gemini-api-커스텀-에이전트">4. Vertex AI + Gemini API (커스텀 에이전트)</h2>

<p><strong>Vertex AI에서 Gemini 모델을 API로 호출해 커스텀 에이전트/서비스를 직접 만드는</strong> 개발자용 시나리오입니다.</p>

<h3 id="enterprise와-뭐가-다른가">Enterprise와 뭐가 다른가?</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Gemini Enterprise</th>
      <th>Vertex AI + Gemini API</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>성격</strong></td>
      <td>포털 + 플랫폼 라이선스</td>
      <td>빌딩 블록 (API)</td>
    </tr>
    <tr>
      <td><strong>요금</strong></td>
      <td>사용자당 월 과금</td>
      <td>토큰/리소스 사용량 과금</td>
    </tr>
    <tr>
      <td><strong>UI</strong></td>
      <td>제공됨 (AI 포털)</td>
      <td>없음 (직접 만들어야 함)</td>
    </tr>
    <tr>
      <td><strong>자유도</strong></td>
      <td>플랫폼 범위 내</td>
      <td>완전 자유</td>
    </tr>
    <tr>
      <td><strong>적합 대상</strong></td>
      <td>IT 관리자, 비즈니스 팀</td>
      <td>개발자, 플랫폼 엔지니어</td>
    </tr>
  </tbody>
</table>

<h3 id="요금-토큰-기반-참고용">요금 (토큰 기반, 참고용)</h3>

<table>
  <thead>
    <tr>
      <th>모델</th>
      <th>입력 (100만 토큰당)</th>
      <th>출력 (100만 토큰당)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Gemini 2.5 Pro</strong></td>
      <td>~$1.25 - $2.50</td>
      <td>~$10.00</td>
    </tr>
    <tr>
      <td><strong>Gemini 2.0 Flash</strong></td>
      <td>~$0.10</td>
      <td>~$0.40</td>
    </tr>
    <tr>
      <td><strong>Gemini 2.0 Flash-Lite</strong></td>
      <td>~$0.025</td>
      <td>~$0.10</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>가격은 수시로 변동됩니다. 최신 가격은 <a href="https://cloud.google.com/vertex-ai/generative-ai/pricing">Vertex AI Pricing</a>에서 확인하세요.</p>
</blockquote>

<h3 id="주요-특징">주요 특징</h3>

<ul>
  <li><strong>구독이 아닌 사용량 과금</strong>: 엔드유저 수가 아니라 API 호출량이 비용의 관건</li>
  <li><strong>완전 커스텀</strong>: 프롬프트, 툴, 함수 호출, RAG, 외부 API/DB 연동을 마음대로 설계</li>
  <li><strong>B2C/B2B 서비스에 적합</strong>: 고객 대상 앱, 사내 자동화 시스템 등에 Gemini를 임베드</li>
  <li><strong>멀티모달</strong>: 텍스트, 이미지, 오디오, 비디오 입력 지원</li>
  <li><strong>롱 컨텍스트</strong>: 최대 100만~200만 토큰</li>
  <li><strong>보안</strong>: VPC-SC, CMEK, HIPAA, FedRAMP High 등 지원</li>
</ul>

<blockquote>
  <p>Enterprise는 <strong>포털+플랫폼 라이선스</strong>, Vertex AI는 <strong>빌딩 블록(API) 과금</strong>이라고 보시면 됩니다.</p>
</blockquote>

<hr />

<h2 id="고객이-자주-혼동하는-질문-faq">고객이 자주 혼동하는 질문 (FAQ)</h2>

<h3 id="google-ai-pro를-샀는데-우리-회사-gmail에도-자동으로-붙나요">“Google AI Pro를 샀는데, 우리 회사 Gmail에도 자동으로 붙나요?”</h3>

<p><strong>아닙니다.</strong> Google AI Pro는 개인 계정용입니다. 회사 Workspace 메일에 AI를 붙이려면 <strong>Gemini for Workspace</strong> 라이선스를 별도로 구매해야 합니다.</p>

<h3 id="gemini-enterprise를-사면-vertex-ai-api는-무료인가요">“Gemini Enterprise를 사면, Vertex AI API는 무료인가요?”</h3>

<p><strong>별개입니다.</strong> Enterprise는 직원용 포털/에이전트 라이선스이고, Vertex AI는 API 사용량 기반 과금입니다. 둘 다 비용이 발생합니다.</p>

<h3 id="직원-50명-정도인데-gmaildocs에서-ai만-있으면-됩니다-뭘-사야-하나요">“직원 50명 정도인데, Gmail/Docs에서 AI만 있으면 됩니다. 뭘 사야 하나요?”</h3>

<p><strong>Google Workspace 플랜 + Gemini 포함 옵션</strong>이 기본 선택입니다. 별도 AI 포털이 필요하면 그때 Gemini Enterprise를 검토하시면 됩니다.</p>

<h3 id="커스텀-ai-챗봇을-우리-서비스에-넣고-싶은데요">“커스텀 AI 챗봇을 우리 서비스에 넣고 싶은데요?”</h3>

<p><strong>Vertex AI + Gemini API</strong>가 적합합니다. 사용량 기반 과금이라 서비스 규모에 맞게 비용이 조절됩니다.</p>

<h3 id="개인-gemini-web과-workspace용-gemini-기능이-같은가요">“개인 Gemini Web과 Workspace용 Gemini, 기능이 같은가요?”</h3>

<p><strong>UI는 비슷하지만 보안/감사/데이터 정책이 다릅니다.</strong> Workspace 계정이면 조직 데이터 접근, 관리자 정책, 데이터 미학습 보장 등 기업용 약관이 적용됩니다.</p>

<h3 id="gemini-for-workspace를-구독하면-개인용-google-ai-proultra를-회사-메일로-쓰는-것과-같나요">“Gemini for Workspace를 구독하면 개인용 Google AI Pro/Ultra를 회사 메일로 쓰는 것과 같나요?”</h3>

<p><strong>완전히 같지는 않습니다.</strong> Gemini for Workspace를 구독하면 Workspace 앱 내 AI 기능과 Gemini 앱(gemini.google.com) 접근 권한을 얻지만, 개인용 Pro/Ultra와 차이가 있습니다:</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Google AI Pro (개인)</th>
      <th>Gemini for Workspace</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Gemini 웹 채팅</td>
      <td>O (Pro 모델)</td>
      <td>O (Workspace 정책 적용)</td>
    </tr>
    <tr>
      <td>Deep Research</td>
      <td>O</td>
      <td>플랜에 따라 다름</td>
    </tr>
    <tr>
      <td>모델 등급</td>
      <td>Pro/Ultra 선택 가능</td>
      <td>Google이 자동 선택</td>
    </tr>
    <tr>
      <td>데이터 정책</td>
      <td>소비자 약관</td>
      <td>기업 약관 (데이터 미학습)</td>
    </tr>
    <tr>
      <td>조직 데이터 연동</td>
      <td>X (개인만)</td>
      <td>O (공유 드라이브, 캘린더 등)</td>
    </tr>
  </tbody>
</table>

<p><strong>“Gemini 웹 채팅을 회사 계정으로 쓸 수 있다”는 맞지만, 모델 등급/기능 범위가 개인 Pro/Ultra와 동일하다고 보장되지는 않습니다.</strong> 특히 Ultra급 기능(Deep Think, Veo 등)은 Workspace 플랜에 포함되지 않을 수 있습니다.</p>

<h3 id="회사-메일workspace-계정로-google-ai-proultra를-쓰려면-어떻게-하나요">“회사 메일(Workspace 계정)로 Google AI Pro/Ultra를 쓰려면 어떻게 하나요?”</h3>

<p>개인이 Google One에서 결제하는 것과는 다릅니다. <strong>Workspace 관리자가 Admin Console에서 라이선스를 할당하는 방식</strong>입니다:</p>

<ol>
  <li>Admin Console &gt; 구독 &gt; Gemini 관련 라이선스 추가 구매</li>
  <li>사용자별로 라이선스 할당</li>
</ol>

<p><strong>주의할 점:</strong></p>
<ul>
  <li>개인용 “Google AI Pro”와 Workspace용 “Google AI Pro”는 <strong>SKU가 다릅니다</strong></li>
  <li>관리자가 할당해도 <strong>기업 데이터 정책</strong>(데이터 미학습, 감사 로그 등)은 Workspace 기준으로 적용</li>
  <li>Ultra급이 Workspace SKU로 제공되는지는 아직 명확하지 않음 (Google이 수시로 업데이트 중)</li>
  <li><strong>정확한 SKU 이름과 포함 기능은 Google이 자주 변경하므로, 도입 시점에 Google 또는 메가존소프트를 통해 최신 SKU를 확인하는 것을 권장합니다.</strong></li>
</ul>

<hr />

<h2 id="선택-가이드-어떤-상황에-어떤-제품">선택 가이드: 어떤 상황에 어떤 제품?</h2>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>추천 제품</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>개인적으로 가볍게 AI 챗봇 사용</td>
      <td>Gemini Web (무료)</td>
    </tr>
    <tr>
      <td>개인적으로 심층 리서치/코딩에 활용</td>
      <td>Google AI Pro ($20/월)</td>
    </tr>
    <tr>
      <td>팀의 Gmail/Docs/Meet 생산성 향상</td>
      <td>Gemini for Workspace</td>
    </tr>
    <tr>
      <td>전사 AI 포털 + 에이전트 플랫폼 도입</td>
      <td>Gemini Enterprise</td>
    </tr>
    <tr>
      <td>커스텀 AI 앱/에이전트를 직접 개발</td>
      <td>Vertex AI + Gemini API</td>
    </tr>
  </tbody>
</table>

<p><strong>핵심은 이것입니다:</strong></p>
<ul>
  <li>“조직에서 직원들이 쓸 포털형 AI”를 원하면 → <strong>Enterprise</strong></li>
  <li>“고객/직원 대상 커스텀 앱/에이전트를 직접 개발”하고 싶으면 → <strong>Vertex AI API</strong></li>
  <li>“Workspace 앱에서 바로 AI 보조”가 필요하면 → <strong>Gemini for Workspace</strong></li>
</ul>

<p>도입에 대한 자세한 문의는 <a href="https://www.megazonesoft.com">메가존소프트</a>를 통해 연락해 주세요.</p>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="gemini" /><category term="vertex-ai" /><category term="google-workspace" /><category term="gemini-enterprise" /><category term="비교" /><category term="google-cloud" /><summary type="html"><![CDATA[“Gemini”라는 이름이 붙은 Google 제품이 여러 개 있어서 고객분들이 자주 혼동하십니다. 이름은 비슷하지만 대상, 요금 방식, 기능이 완전히 다릅니다. 이 글에서 한 번에 정리합니다.]]></summary></entry><entry><title type="html">Harness Engineering — AI 에이전트를 프로덕션에서 진짜로 동작하게 만드는 기술</title><link href="https://seonghak.com/blog/2026/04/02/harness-engineering-guide/" rel="alternate" type="text/html" title="Harness Engineering — AI 에이전트를 프로덕션에서 진짜로 동작하게 만드는 기술" /><published>2026-04-02T00:00:00+09:00</published><updated>2026-04-02T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/04/02/harness-engineering-guide</id><content type="html" xml:base="https://seonghak.com/blog/2026/04/02/harness-engineering-guide/"><![CDATA[<p>2025년이 AI 에이전트의 해였다면, 2026년은 <strong>하네스(Harness)</strong>의 해입니다. 모델은 이미 충분히 강력해졌고, 이제 경쟁력은 모델 자체가 아니라 <strong>모델을 감싸는 시스템</strong>에서 갈립니다.</p>

<p>이 글에서는 하네스 엔지니어링의 개념부터 필요한 도구, 실전 적용법, 그리고 현재 트렌드까지 슬라이드 형식으로 정리합니다.</p>

<hr />

<h2 id="slide-1-하네스-엔지니어링이란">Slide 1. 하네스 엔지니어링이란?</h2>

<h3 id="한-줄-정의">한 줄 정의</h3>

<blockquote>
  <p>AI 에이전트가 <strong>무엇을 보고, 무엇을 할 수 있고, 언제 멈추고, 실패하면 어떻게 되는지</strong>를 설계하는 엔지니어링 규율</p>
</blockquote>

<h3 id="비유-말과-마구馬具">비유: 말과 마구(馬具)</h3>

<p>LLM은 <strong>강력하지만 방향 감각이 없는 말</strong>입니다. 하네스는 그 힘을 통제 가능한 작업으로 전환하는 <strong>고삐, 안장, 재갈</strong> 역할을 합니다.</p>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>초점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>프롬프트 엔지니어링</strong></td>
      <td>단일 모델 호출의 품질 개선</td>
    </tr>
    <tr>
      <td><strong>컨텍스트 엔지니어링</strong></td>
      <td>컨텍스트 윈도우에 <em>무엇을</em> 넣을지 결정</td>
    </tr>
    <tr>
      <td><strong>하네스 엔지니어링</strong></td>
      <td>컨텍스트 윈도우 <em>바깥</em>의 모든 것 — 도구, 상태, 검증, 생명주기</td>
    </tr>
  </tbody>
</table>

<p>하네스 엔지니어링은 프롬프트 엔지니어링의 상위 개념입니다. 프롬프트가 “한 번의 대화를 잘하는 법”이라면, 하네스는 “100번의 세션을 걸쳐 일관되게 잘하는 법”입니다.</p>

<hr />

<h2 id="slide-2-왜-하네스가-필요한가">Slide 2. 왜 하네스가 필요한가?</h2>

<h3 id="llm의-근본적-한계">LLM의 근본적 한계</h3>

<p>LLM은 기본적으로 <strong>무상태(stateless)</strong>입니다. 매 세션은 이전 작업에 대한 기억 없이 시작됩니다.</p>

<table>
  <thead>
    <tr>
      <th>문제</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>컨텍스트 붕괴</strong></td>
      <td>도구 결과와 이력으로 윈도우가 채워지면 원래 지시를 놓침</td>
    </tr>
    <tr>
      <td><strong>환각적 도구 호출</strong></td>
      <td>존재하지 않는 API를 참조하거나 잘못된 매개변수 사용</td>
    </tr>
    <tr>
      <td><strong>실패 시 상태 손실</strong></td>
      <td>네트워크 오류 시 진행 상황이 완전히 소실</td>
    </tr>
    <tr>
      <td><strong>조기 완료 선언</strong></td>
      <td>검증 없이 “완료”를 선언하는 경향</td>
    </tr>
  </tbody>
</table>

<h3 id="모델은-상품화되었다">모델은 상품화되었다</h3>

<p>Claude, GPT, Gemini, 오픈소스 모델들의 성능 차이는 좁아지고 있습니다. <strong>동일한 모델을 사용해도 하네스 품질에 따라 작업 완료율이 40%p 차이</strong>가 납니다.</p>

<blockquote>
  <p>모델은 교체 가능한 부품이고, 하네스가 곧 제품이다.</p>
</blockquote>

<hr />

<h2 id="slide-3-하네스의-6대-핵심-구성요소">Slide 3. 하네스의 6대 핵심 구성요소</h2>

<div class="mermaid">
graph TB
    subgraph Harness["Agent Harness"]
        CE["1. Context Engineering<br />컨텍스트 엔지니어링"]
        VL["2. Verification Loops<br />검증 루프"]
        SM["3. State Management<br />상태 관리"]
        TO["4. Tool Orchestration<br />도구 오케스트레이션"]
        HL["5. Human-in-the-Loop<br />인간 개입"]
        LM["6. Lifecycle Management<br />생명주기 관리"]
        LLM["LLM"]
    end
    CE --&gt; LLM
    VL --&gt; LLM
    SM --&gt; LLM
    TO --&gt; LLM
    HL --&gt; LLM
    LM --&gt; LLM
</div>

<h3 id="1-컨텍스트-엔지니어링">1. 컨텍스트 엔지니어링</h3>

<p>에이전트가 <strong>무엇을 볼 수 있는지</strong> 결정합니다.</p>

<ul>
  <li>코드베이스 내 지속적으로 개선되는 <strong>지식 기반</strong> (AGENTS.md, CLAUDE.md 등)</li>
  <li>관찰 가능성 데이터, 브라우저 네비게이션 같은 <strong>동적 컨텍스트</strong></li>
  <li>“Lost in the Middle” 문제 대응 — 가장 중요한 정보를 프롬프트의 시작과 끝에 배치</li>
  <li>3종 메모리 운영: 작업 컨텍스트(임시), 세션 상태(중기), 장기 메모리(영구)</li>
</ul>

<h3 id="2-검증-루프">2. 검증 루프</h3>

<p>에이전트가 <strong>제대로 했는지</strong> 확인합니다.</p>

<ul>
  <li>코딩 에이전트: 테스트 스위트 통과 후에만 기능 완료 표시</li>
  <li>기능 목록을 JSON으로 관리 → 각 기능의 pass/fail 상태를 기계적으로 추적</li>
  <li>결정론적 린터 + 구조 테스트로 아키텍처 제약 위반 감지</li>
</ul>

<h3 id="3-상태-관리">3. 상태 관리</h3>

<p>에이전트가 <strong>어디까지 했는지</strong> 기억합니다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">claude-progress.txt</code> 같은 진행 로그</li>
  <li>Git 커밋으로 각 단계의 진행 상황 문서화</li>
  <li>JSON 형식 선호 (모델이 마크다운보다 JSON을 덜 임의로 수정함)</li>
</ul>

<h3 id="4-도구-오케스트레이션">4. 도구 오케스트레이션</h3>

<p>에이전트가 <strong>무엇을 할 수 있는지</strong> 제어합니다.</p>

<ul>
  <li>파일 시스템 접근, 코드 실행, API 호출, 웹 검색 등</li>
  <li>사전 승인된 도구만 접근 가능하도록 제한</li>
  <li>MCP(Model Context Protocol) 서버를 통한 도구 연결</li>
</ul>

<h3 id="5-휴먼-인-더-루프">5. 휴먼-인-더-루프</h3>

<p>에이전트가 <strong>언제 사람에게 물어봐야 하는지</strong> 결정합니다.</p>

<ul>
  <li>파괴적 작업(삭제, 배포 등)에 대한 인간 승인 요구</li>
  <li>완전 자율은 드물게 적절 — 대부분의 시나리오에서 인간 개입 지점 설계 필수</li>
  <li>민감한 결정에 대한 에스컬레이션 경로</li>
</ul>

<h3 id="6-생명주기-관리">6. 생명주기 관리</h3>

<p>에이전트의 <strong>시작과 끝, 그리고 세션 간 전환</strong>을 관리합니다.</p>

<ul>
  <li>초기화 에이전트 → 코딩 에이전트 분리 패턴</li>
  <li>각 세션 시작 시 표준 절차: <code class="language-plaintext highlighter-rouge">pwd</code> 확인 → git 로그 읽기 → 기능 목록 확인 → 다음 작업 선택</li>
  <li>실패 시 안전한 롤백 경로</li>
</ul>

<hr />

<h2 id="slide-4-하네스-엔지니어링에-필요한-도구들">Slide 4. 하네스 엔지니어링에 필요한 도구들</h2>

<h3 id="코드-품질--제약-도구">코드 품질 &amp; 제약 도구</h3>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Pre-commit hooks</strong></td>
      <td>커밋 전 코드 품질 자동 검증</td>
    </tr>
    <tr>
      <td><strong>Custom linters</strong></td>
      <td>조직 고유 규칙 강제 적용</td>
    </tr>
    <tr>
      <td><strong>ArchUnit / 구조 테스트</strong></td>
      <td>코드 아키텍처 제약을 테스트로 검증</td>
    </tr>
    <tr>
      <td><strong>CI/CD 파이프라인</strong></td>
      <td>에이전트 생성 코드의 자동 빌드/테스트/배포</td>
    </tr>
  </tbody>
</table>

<h3 id="컨텍스트--메모리-도구">컨텍스트 &amp; 메모리 도구</h3>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>AGENTS.md / CLAUDE.md</strong></td>
      <td>코드베이스 내 에이전트 지시사항 문서</td>
    </tr>
    <tr>
      <td><strong>벡터 스토어</strong></td>
      <td>장기 메모리 저장 및 시맨틱 검색</td>
    </tr>
    <tr>
      <td><strong>진행 로그 (JSON)</strong></td>
      <td>세션 간 상태 유지</td>
    </tr>
    <tr>
      <td><strong>Git</strong></td>
      <td>진행 상황 문서화 &amp; 롤백 지점</td>
    </tr>
  </tbody>
</table>

<h3 id="도구-연결--오케스트레이션">도구 연결 &amp; 오케스트레이션</h3>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>MCP 서버</strong></td>
      <td>표준화된 도구 인터페이스 제공</td>
    </tr>
    <tr>
      <td><strong>Puppeteer / Playwright</strong></td>
      <td>브라우저 자동화를 통한 E2E 검증</td>
    </tr>
    <tr>
      <td><strong>Firecrawl</strong></td>
      <td>웹 검색/스크래핑 — 에이전트의 웹 접근 계층</td>
    </tr>
    <tr>
      <td><strong>컨테이너/샌드박스</strong></td>
      <td>에이전트 실행 환경 격리</td>
    </tr>
  </tbody>
</table>

<h3 id="검증--안전-도구">검증 &amp; 안전 도구</h3>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>테스트 프레임워크</strong></td>
      <td>기능 완료 여부 기계적 검증</td>
    </tr>
    <tr>
      <td><strong>레드팀 도구</strong></td>
      <td>에이전트 취약점 사전 탐지</td>
    </tr>
    <tr>
      <td><strong>모니터링/알림</strong></td>
      <td>에이전트 행동 이상 감지</td>
    </tr>
    <tr>
      <td><strong>감사 로그</strong></td>
      <td>에이전트 행동의 추적 가능성 보장</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="slide-5-실전-하네스-엔지니어링은-어떻게-하는가">Slide 5. 실전: 하네스 엔지니어링은 어떻게 하는가?</h2>

<h3 id="아키텍처-패턴-선택">아키텍처 패턴 선택</h3>

<p><strong>패턴 A: 단일 에이전트 + 감독자 루프</strong></p>
<ul>
  <li>하나의 모델이 도구·메모리·검증과 함께 루프</li>
  <li>적합: 고객 지원, 단순 자동화</li>
</ul>

<p><strong>패턴 B: 초기화-실행자 분할</strong> (Anthropic 추천)</p>
<ul>
  <li>초기화 에이전트가 환경 세팅 후, 코딩 에이전트가 증분 진행</li>
  <li>적합: 장기 실행 코딩 작업</li>
</ul>

<p><strong>패턴 C: 멀티 에이전트 조율</strong></p>
<ul>
  <li>연구자·작가·검토자 등 전문가 에이전트 간 작업 위임</li>
  <li>적합: 복잡한 프로젝트, 다단계 파이프라인</li>
</ul>

<h3 id="실전-체크리스트">실전 체크리스트</h3>

<h4 id="step-1-기능-목록-정의">Step 1: 기능 목록 정의</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="s2">"functional"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"사용자가 새 대화를 생성할 수 있다"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"passes"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="s2">"functional"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"메시지 전송 시 실시간 응답이 표시된다"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"passes"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</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">false</code>로 시작하여 <strong>완료 기준을 명확히</strong> 합니다.</p>

<h4 id="step-2-초기화-스크립트-작성">Step 2: 초기화 스크립트 작성</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># init.sh — 에이전트가 매 세션 시작 시 실행</span>
npm <span class="nb">install
</span>npm run dev &amp;
<span class="nb">echo</span> <span class="s2">"Development server started"</span>
</code></pre></div></div>

<h4 id="step-3-에이전트-지시사항-문서-작성">Step 3: 에이전트 지시사항 문서 작성</h4>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># AGENTS.md</span>

<span class="gu">## 작업 규칙</span>
<span class="p">-</span> 한 번에 하나의 기능만 작업할 것
<span class="p">-</span> 기능 완료 전 반드시 E2E 테스트를 실행할 것
<span class="p">-</span> 테스트를 삭제하거나 수정하는 것은 금지
<span class="p">-</span> 작업 완료 후 git commit으로 진행 상황을 기록할 것

<span class="gu">## 세션 시작 절차</span>
<span class="p">1.</span> pwd로 현재 디렉토리 확인
<span class="p">2.</span> git log로 최근 진행 상황 확인
<span class="p">3.</span> features.json에서 다음 미완료 기능 선택
<span class="p">4.</span> init.sh로 개발 서버 시작
</code></pre></div></div>

<h4 id="step-4-검증-자동화-구축">Step 4: 검증 자동화 구축</h4>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/agent-verify.yml</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">]</span>
<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">verify</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm ci</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm run lint</span>        <span class="c1"># 커스텀 린터</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm run test:arch</span>   <span class="c1"># 구조 테스트</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">npm run test:e2e</span>    <span class="c1"># E2E 테스트</span>
</code></pre></div></div>

<h4 id="step-5-엔트로피-관리-가비지-컬렉션">Step 5: 엔트로피 관리 (가비지 컬렉션)</h4>

<p>주기적으로 별도 에이전트가 코드베이스를 스캔합니다:</p>

<ul>
  <li>문서화 불일치 감지 및 수정</li>
  <li>아키텍처 제약 위반 탐지</li>
  <li>코드 부패(code rot) 식별</li>
</ul>

<blockquote>
  <p>OpenAI 팀의 핵심 통찰: <strong>에이전트가 어려움을 겪을 때, 그것을 신호로 해석하라.</strong> 부족한 도구, 가드레일, 문서를 식별하고 저장소에 피드백하는 것이 하네스 엔지니어링의 핵심 루프다.</p>
</blockquote>

<hr />

<h2 id="slide-6-하네스-엔지니어링을-위한-sdk--개발-킷">Slide 6. 하네스 엔지니어링을 위한 SDK &amp; 개발 킷</h2>

<p>하네스를 직접 처음부터 만들 필요는 없습니다. 주요 벤더와 오픈소스 커뮤니티가 에이전트 하네스 구축을 위한 SDK와 프레임워크를 제공합니다.</p>

<h3 id="코딩-에이전트-하네스-완성형">코딩 에이전트 하네스 (완성형)</h3>

<p>이미 하네스가 내장된 에이전트 환경으로, 즉시 사용 가능합니다.</p>

<table>
  <thead>
    <tr>
      <th>제품</th>
      <th>제공사</th>
      <th>핵심 하네스 기능</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Claude Code</strong></td>
      <td>Anthropic</td>
      <td>5계층 권한 모델, 18+ 훅 이벤트, CLAUDE.md 컨텍스트, 자동 스냅샷/롤백, 서브에이전트 스포닝, 워크트리 격리</td>
    </tr>
    <tr>
      <td><strong>Codex CLI</strong></td>
      <td>OpenAI</td>
      <td>샌드박스 실행, AGENTS.md 지시사항, 파일 접근 제어, 도구 정의, 자동 검증 루프</td>
    </tr>
    <tr>
      <td><strong>Cursor</strong></td>
      <td>Cursor Inc.</td>
      <td><code class="language-plaintext highlighter-rouge">.cursor/rules</code> 기반 하네스, IDE 통합, 루프 탐지, 모델별 프롬프트 적응</td>
    </tr>
    <tr>
      <td><strong>Windsurf</strong></td>
      <td>Codeium</td>
      <td>Cascade 에이전트, 컨텍스트 인식 코드 생성, 멀티파일 편집, 터미널 통합</td>
    </tr>
  </tbody>
</table>

<h4 id="claude-code-하네스-아키텍처-상세">Claude Code 하네스 아키텍처 상세</h4>

<p>Claude Code는 가장 정교한 하네스 시스템 중 하나입니다:</p>

<div class="mermaid">
graph LR
    CC["Claude Code Harness"]
    CC --&gt; PM["Permission Model<br />5계층"]
    PM --&gt; PM1["Mode<br />plan/autoEdit/fullAuto"]
    PM --&gt; PM2["Allowlist"]
    PM --&gt; PM3["MCP Permissions"]
    PM --&gt; PM4["Bash Rules"]
    PM --&gt; PM5["User Prompt"]
    CC --&gt; HK["Hooks System<br />18+ 이벤트"]
    HK --&gt; HK1["PreToolUse / PostToolUse"]
    HK --&gt; HK2["SessionStart / SessionEnd"]
    HK --&gt; HK3["Notification / Stop"]
    CC --&gt; CTX["Context Engineering"]
    CTX --&gt; CTX1["CLAUDE.md"]
    CTX --&gt; CTX2["자동 컨텍스트 압축"]
    CTX --&gt; CTX3["3종 메모리"]
    CC --&gt; EX["Execution"]
    EX --&gt; EX1["서브에이전트 스포닝"]
    EX --&gt; EX2["워크트리 격리"]
    EX --&gt; EX3["태스크 의존성 그래프"]
    CC --&gt; SF["Safety"]
    SF --&gt; SF1["자동 스냅샷 &amp; 롤백"]
    SF --&gt; SF2["읽기 전용 기본값"]
    SF --&gt; SF3["파괴적 작업 승인 요구"]
</div>

<h3 id="에이전트-개발-sdk-프레임워크">에이전트 개발 SDK (프레임워크)</h3>

<p>에이전트를 직접 구축할 때 하네스 기능을 제공하는 SDK입니다.</p>

<h4 id="claude-agent-sdk-anthropic">Claude Agent SDK (Anthropic)</h4>

<ul>
  <li><strong>접근 방식</strong>: Tool-use-first — 에이전트 = Claude 모델 + 도구</li>
  <li><strong>하네스 기능</strong>: 훅 시스템, 권한 모델, 다른 에이전트를 도구로 호출</li>
  <li><strong>아키텍처</strong>: 프롬프트 수신 → 필요시 도구 호출 → 구조화된 응답 반환</li>
  <li><strong>강점</strong>: 의도적으로 단순한 설계, Anthropic 플랫폼 네이티브 통합</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">claude_agent_sdk</span> <span class="kn">import</span> <span class="n">Agent</span><span class="p">,</span> <span class="n">Tool</span>

<span class="n">agent</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"claude-sonnet-4-6"</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">filesystem</span><span class="p">,</span> <span class="n">code_runner</span><span class="p">,</span> <span class="n">browser</span><span class="p">],</span>
    <span class="n">hooks</span><span class="o">=</span><span class="p">{</span><span class="s">"pre_tool_use"</span><span class="p">:</span> <span class="n">lint_check</span><span class="p">,</span> <span class="s">"post_tool_use"</span><span class="p">:</span> <span class="n">test_runner</span><span class="p">},</span>
    <span class="n">permissions</span><span class="o">=</span><span class="p">{</span><span class="s">"file_write"</span><span class="p">:</span> <span class="s">"ask"</span><span class="p">,</span> <span class="s">"bash"</span><span class="p">:</span> <span class="s">"restricted"</span><span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="openai-agents-sdk">OpenAI Agents SDK</h4>

<ul>
  <li><strong>접근 방식</strong>: 최소주의 — 핵심 추상화는 “Handoff”</li>
  <li><strong>하네스 기능</strong>: 입력/출력 가드레일, 낙관적 실행 + 롤백, 추적/평가</li>
  <li><strong>아키텍처</strong>: 에이전트 간 명시적 제어 전환 (Handoff 메커니즘)</li>
  <li><strong>강점</strong>: 가장 낮은 학습 곡선, 빠른 프로토타이핑</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">agents</span> <span class="kn">import</span> <span class="n">Agent</span><span class="p">,</span> <span class="n">Runner</span><span class="p">,</span> <span class="n">InputGuardrail</span>

<span class="n">agent</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"code_agent"</span><span class="p">,</span>
    <span class="n">instructions</span><span class="o">=</span><span class="s">"..."</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">file_tool</span><span class="p">,</span> <span class="n">shell_tool</span><span class="p">],</span>
    <span class="n">input_guardrails</span><span class="o">=</span><span class="p">[</span><span class="n">safety_check</span><span class="p">],</span>
    <span class="n">output_guardrails</span><span class="o">=</span><span class="p">[</span><span class="n">quality_check</span><span class="p">]</span>
<span class="p">)</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">Runner</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">agent</span><span class="p">,</span> <span class="s">"Fix the login bug"</span><span class="p">)</span>
</code></pre></div></div>

<h4 id="google-adk-agent-development-kit">Google ADK (Agent Development Kit)</h4>

<ul>
  <li><strong>접근 방식</strong>: 명시적 워크플로우 타입 (Sequential, Parallel, Loop)</li>
  <li><strong>하네스 기능</strong>: 에이전트를 도구로 사용하는 계층 구조, Vertex AI 통합, Developer UI</li>
  <li><strong>아키텍처</strong>: 결정론적 + 동적 흐름 혼합, OpenAPI 자동 변환</li>
  <li><strong>강점</strong>: GCP 생태계 네이티브, 풍부한 내장 도구, 평가 도구 내장</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk</span> <span class="kn">import</span> <span class="n">Agent</span><span class="p">,</span> <span class="n">SequentialAgent</span><span class="p">,</span> <span class="n">ParallelAgent</span>

<span class="n">researcher</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"researcher"</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">search</span><span class="p">,</span> <span class="n">scrape</span><span class="p">])</span>
<span class="n">writer</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"writer"</span><span class="p">,</span> <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">file_write</span><span class="p">])</span>
<span class="n">pipeline</span> <span class="o">=</span> <span class="n">SequentialAgent</span><span class="p">(</span>
    <span class="n">agents</span><span class="o">=</span><span class="p">[</span><span class="n">researcher</span><span class="p">,</span> <span class="n">writer</span><span class="p">],</span>
    <span class="n">guardrails</span><span class="o">=</span><span class="n">safety_filter</span>
<span class="p">)</span>
</code></pre></div></div>

<h4 id="aws-strands-agents-sdk">AWS Strands Agents SDK</h4>

<ul>
  <li><strong>접근 방식</strong>: 모델 주도(model-driven) — Model + Tools + Prompt</li>
  <li><strong>하네스 기능</strong>: 내장 도구 20+, MCP 서버 연결, <code class="language-plaintext highlighter-rouge">@tool</code> 데코레이터</li>
  <li><strong>아키텍처</strong>: 모델이 계획·도구 선택·결과 반영을 모두 주도</li>
  <li><strong>강점</strong>: AWS Bedrock 네이티브, 복잡한 오케스트레이션 불필요</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">strands</span> <span class="kn">import</span> <span class="n">Agent</span>
<span class="kn">from</span> <span class="nn">strands.tools</span> <span class="kn">import</span> <span class="n">tool</span>

<span class="o">@</span><span class="n">tool</span>
<span class="k">def</span> <span class="nf">deploy</span><span class="p">(</span><span class="n">service</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
    <span class="s">"""Deploy a service to production"""</span>
    <span class="k">return</span> <span class="n">run_deploy</span><span class="p">(</span><span class="n">service</span><span class="p">)</span>

<span class="n">agent</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span><span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">deploy</span><span class="p">])</span>
<span class="n">result</span> <span class="o">=</span> <span class="n">agent</span><span class="p">(</span><span class="s">"Deploy the user service"</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="오케스트레이션-프레임워크">오케스트레이션 프레임워크</h3>

<p>에이전트 간 조율과 복잡한 워크플로우를 위한 프레임워크입니다.</p>

<table>
  <thead>
    <tr>
      <th>프레임워크</th>
      <th>핵심 특징</th>
      <th>하네스 관련 기능</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>LangGraph</strong></td>
      <td>그래프 기반 상태 관리, 노드/엣지 명시적 정의</td>
      <td>체크포인트 복구, HITL 내장, LangSmith 가드레일</td>
    </tr>
    <tr>
      <td><strong>CrewAI</strong></td>
      <td>역할 기반 멀티 에이전트 협업</td>
      <td>Flows 이벤트 파이프라인, 에이전트 간 위임</td>
    </tr>
    <tr>
      <td><strong>MS AutoGen</strong></td>
      <td>대화 기반 멀티 에이전트 (Microsoft)</td>
      <td>그룹 채팅 패턴, 코드 실행 샌드박스, 비동기 메시징</td>
    </tr>
    <tr>
      <td><strong>AWS Bedrock Agents</strong></td>
      <td>완전 관리형 서비스</td>
      <td>사전 구축 가드레일, IAM 통합, Knowledge Base RAG</td>
    </tr>
  </tbody>
</table>

<h4 id="microsoft-autogen">Microsoft AutoGen</h4>

<ul>
  <li><strong>접근 방식</strong>: 대화 기반 멀티 에이전트 — 에이전트들이 그룹 채팅처럼 대화</li>
  <li><strong>하네스 기능</strong>: 코드 실행 샌드박스, 비동기 메시징, 그룹 채팅 관리자</li>
  <li><strong>아키텍처</strong>: AssistantAgent, UserProxyAgent 등 역할별 에이전트가 대화로 협업</li>
  <li><strong>강점</strong>: Microsoft 생태계 통합, 인간 참여가 자연스러운 대화 흐름에 녹아듦</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">autogen</span> <span class="kn">import</span> <span class="n">AssistantAgent</span><span class="p">,</span> <span class="n">UserProxyAgent</span><span class="p">,</span> <span class="n">GroupChat</span>

<span class="n">coder</span> <span class="o">=</span> <span class="n">AssistantAgent</span><span class="p">(</span><span class="s">"coder"</span><span class="p">,</span> <span class="n">llm_config</span><span class="o">=</span><span class="n">llm_config</span><span class="p">)</span>
<span class="n">reviewer</span> <span class="o">=</span> <span class="n">AssistantAgent</span><span class="p">(</span><span class="s">"reviewer"</span><span class="p">,</span> <span class="n">llm_config</span><span class="o">=</span><span class="n">llm_config</span><span class="p">)</span>
<span class="n">executor</span> <span class="o">=</span> <span class="n">UserProxyAgent</span><span class="p">(</span><span class="s">"executor"</span><span class="p">,</span> <span class="n">code_execution_config</span><span class="o">=</span><span class="p">{</span><span class="s">"work_dir"</span><span class="p">:</span> <span class="s">"workspace"</span><span class="p">})</span>

<span class="n">group_chat</span> <span class="o">=</span> <span class="n">GroupChat</span><span class="p">(</span><span class="n">agents</span><span class="o">=</span><span class="p">[</span><span class="n">coder</span><span class="p">,</span> <span class="n">reviewer</span><span class="p">,</span> <span class="n">executor</span><span class="p">],</span> <span class="n">messages</span><span class="o">=</span><span class="p">[])</span>
</code></pre></div></div>

<h3 id="도구-연결-표준-mcp-model-context-protocol">도구 연결 표준: MCP (Model Context Protocol)</h3>

<p>모든 SDK를 관통하는 핵심 표준이 <strong>MCP</strong>입니다.</p>

<div class="mermaid">
graph TB
    Agent["AI Agent<br />(SDK 무관)"] &lt;--&gt;|"MCP<br />표준 프로토콜"| Server["MCP Server<br />(도구 제공)"]
    Server --&gt; FS["파일시스템"]
    Server --&gt; DB["데이터베이스"]
    Server --&gt; API["외부 API"]
</div>

<ul>
  <li>어떤 SDK를 사용하든 동일한 도구 인터페이스 제공</li>
  <li>Claude Code, Cursor, Codex 모두 MCP 지원</li>
  <li>한 번 만든 MCP 서버를 여러 에이전트에서 재사용</li>
</ul>

<h3 id="sdk-선택-가이드">SDK 선택 가이드</h3>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>추천 SDK</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>즉시 코딩 에이전트 사용</td>
      <td><strong>Claude Code</strong> / <strong>Codex CLI</strong></td>
      <td>하네스 내장, 설정만으로 시작</td>
    </tr>
    <tr>
      <td>커스텀 에이전트 구축 (Anthropic)</td>
      <td><strong>Claude Agent SDK</strong></td>
      <td>단순한 설계, 강력한 훅/권한</td>
    </tr>
    <tr>
      <td>커스텀 에이전트 구축 (OpenAI)</td>
      <td><strong>OpenAI Agents SDK</strong></td>
      <td>최소 학습 곡선, Handoff 패턴</td>
    </tr>
    <tr>
      <td>GCP 생태계 활용</td>
      <td><strong>Google ADK</strong></td>
      <td>Vertex AI 네이티브, 평가 도구</td>
    </tr>
    <tr>
      <td>AWS 생태계 활용</td>
      <td><strong>Strands Agents</strong></td>
      <td>Bedrock 네이티브, 모델 주도</td>
    </tr>
    <tr>
      <td>복잡한 멀티 에이전트 파이프라인</td>
      <td><strong>LangGraph</strong></td>
      <td>그래프 기반 상태, 벤더 중립</td>
    </tr>
    <tr>
      <td>역할 기반 에이전트 팀</td>
      <td><strong>CrewAI</strong></td>
      <td>직관적 역할 정의, Flows</td>
    </tr>
    <tr>
      <td>관리형 서비스 선호</td>
      <td><strong>AWS Bedrock Agents</strong></td>
      <td>코드 최소, 가드레일 내장</td>
    </tr>
  </tbody>
</table>

<h3 id="비교-요약">비교 요약</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Claude Agent SDK</th>
      <th>OpenAI Agents SDK</th>
      <th>Google ADK</th>
      <th>Strands</th>
      <th>LangGraph</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>학습 곡선</td>
      <td>낮음</td>
      <td>가장 낮음</td>
      <td>높음</td>
      <td>낮음</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>모델 유연성</td>
      <td>Anthropic 중심</td>
      <td>OpenAI 중심</td>
      <td>멀티모델</td>
      <td>Bedrock 중심</td>
      <td>벤더 중립</td>
    </tr>
    <tr>
      <td>가드레일</td>
      <td>훅 기반</td>
      <td>입출력 객체</td>
      <td>콜백 + 필터</td>
      <td>도구 제한</td>
      <td>LangSmith</td>
    </tr>
    <tr>
      <td>HITL</td>
      <td>권한 모델 내장</td>
      <td>코드 구현</td>
      <td>콜백 지원</td>
      <td>코드 구현</td>
      <td>내장 지원</td>
    </tr>
    <tr>
      <td>배포</td>
      <td>자유</td>
      <td>자유</td>
      <td>GCP 최적</td>
      <td>AWS 최적</td>
      <td>자유</td>
    </tr>
    <tr>
      <td>멀티 에이전트</td>
      <td>서브에이전트</td>
      <td>Handoff</td>
      <td>계층 구조</td>
      <td>멀티에이전트</td>
      <td>그래프 노드</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="slide-7-실제-사례-openai-codex">Slide 7. 실제 사례: OpenAI Codex</h2>

<p>OpenAI의 Codex 팀은 하네스 엔지니어링의 가장 대표적인 사례입니다.</p>

<h3 id="프로젝트-개요">프로젝트 개요</h3>
<ul>
  <li><strong>5개월</strong> 동안 프로덕션 애플리케이션 개발</li>
  <li><strong>100만 줄 이상</strong>의 코드, 전부 AI가 생성</li>
  <li>인간 엔지니어는 <strong>코드를 한 줄도 직접 작성하지 않음</strong></li>
  <li>엔지니어의 역할: <strong>AI가 코드를 안정적으로 작성할 수 있는 시스템을 설계</strong></li>
</ul>

<h3 id="3가지-핵심-하네스-전략">3가지 핵심 하네스 전략</h3>

<p><strong>1. 컨텍스트 엔지니어링</strong></p>
<ul>
  <li>코드베이스 내 지속적으로 개선되는 지식 기반</li>
  <li>관찰 가능성 데이터와 동적 컨텍스트 제공</li>
</ul>

<p><strong>2. 아키텍처 제약</strong></p>
<ul>
  <li>결정론적 커스텀 린터와 구조 테스트</li>
  <li>LLM 기반 방식과 결정론적 방식의 혼합</li>
  <li>에이전트의 “해결 공간”을 좁혀서 신뢰성 확보</li>
</ul>

<p><strong>3. 가비지 컬렉션 에이전트</strong></p>
<ul>
  <li>주기적으로 코드베이스 엔트로피를 감지·수정</li>
  <li>문서 불일치, 아키텍처 위반, 코드 부패 자동 탐지</li>
</ul>

<h3 id="핵심-교훈">핵심 교훈</h3>

<blockquote>
  <p>“제약이 클수록 신뢰성이 높다” — 무제한 유연성보다 <strong>제약된 해결 공간</strong>이 더 나은 결과를 만든다</p>
</blockquote>

<hr />

<h2 id="slide-8-실제-사례-anthropic의-장기-실행-에이전트-하네스">Slide 8. 실제 사례: Anthropic의 장기 실행 에이전트 하네스</h2>

<p>Anthropic은 장기 실행 코딩 에이전트를 위한 <strong>초기화-실행자 분할 패턴</strong>을 권장합니다.</p>

<h3 id="2단계-아키텍처">2단계 아키텍처</h3>

<div class="mermaid">
graph LR
    subgraph S0["Session 0 — 초기화"]
        Init["초기화 에이전트"]
        Init --&gt; I1["init.sh 작성"]
        Init --&gt; I2["features.json 생성"]
        Init --&gt; I3["초기 커밋"]
        Init --&gt; I4["환경 검증"]
    end
    subgraph SN["Session 1..N — 실행"]
        Code["코딩 에이전트"]
        Code --&gt; C1["진행 로그 읽기"]
        Code --&gt; C2["다음 기능 선택"]
        Code --&gt; C3["구현 &amp; 테스트"]
        Code --&gt; C4["커밋 &amp; 업데이트"]
    end
    S0 --&gt;|"환경 준비 완료"| SN
</div>

<h3 id="주요-실패-패턴과-해결책">주요 실패 패턴과 해결책</h3>

<table>
  <thead>
    <tr>
      <th>문제</th>
      <th>원인</th>
      <th>해결책</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>조기 완료 선언</td>
      <td>검증 없이 “done”</td>
      <td>기능 목록 + E2E 테스트 강제</td>
    </tr>
    <tr>
      <td>문서화 부족</td>
      <td>세션 간 정보 손실</td>
      <td>Git 커밋 + 진행 로그 의무화</td>
    </tr>
    <tr>
      <td>앱 실행에 시간 낭비</td>
      <td>매번 환경 셋업</td>
      <td>init.sh로 표준화</td>
    </tr>
    <tr>
      <td>기존 코드 파괴</td>
      <td>컨텍스트 부족</td>
      <td>이전 커밋 로그 참조 의무화</td>
    </tr>
  </tbody>
</table>

<h3 id="베스트-프랙티스">베스트 프랙티스</h3>

<ul>
  <li><strong>JSON &gt; Markdown</strong>: 모델이 JSON 파일은 임의로 수정하는 경향이 적음</li>
  <li><strong>강력한 금지 지시</strong>: “테스트를 삭제하거나 수정하는 것은 허용되지 않음”</li>
  <li><strong>E2E 검증 우선</strong>: 단위 테스트보다 실제 사용자 관점의 E2E 테스트</li>
</ul>

<hr />

<h2 id="slide-9-현재-트렌드와-미래-방향">Slide 9. 현재 트렌드와 미래 방향</h2>

<h3 id="트렌드-1-하네스--새로운-경쟁-우위">트렌드 1: 하네스 = 새로운 경쟁 우위</h3>

<p>모델 성능이 수렴하면서, <strong>하네스 품질이 곧 제품 품질</strong>이 되었습니다. 동일 모델에서 하네스 유무에 따라 <strong>2~5배의 신뢰성 차이</strong>가 발생합니다.</p>

<h3 id="트렌드-2-하네스-엔지니어-새로운-직군의-탄생">트렌드 2: 하네스 엔지니어, 새로운 직군의 탄생</h3>

<p>에이전트 기반 제품을 만드는 회사에서 “Harness Engineer”가 독립적인 역할로 등장하고 있습니다. 기존 소프트웨어 엔지니어링 + AI 시스템 설계 + 안전성 엔지니어링의 교차점입니다.</p>

<h3 id="트렌드-3-meta-harness--하네스를-최적화하는-ai">트렌드 3: Meta-Harness — 하네스를 최적화하는 AI</h3>

<p>최신 연구에서는 <strong>하네스 코드 자체를 AI가 최적화</strong>하는 메타 하네스 개념이 등장했습니다. 소스 코드, 점수, 실행 트레이스를 분석하여 더 나은 하네스를 자동으로 제안합니다.</p>

<h3 id="트렌드-4-기술-스택의-수렴">트렌드 4: 기술 스택의 수렴</h3>

<p>개발자의 프레임워크/언어 취향보다 <strong>AI 친화적 구조</strong>가 우선시되는 경향입니다. 하네스가 유지보수하기 좋은 코드 구조가 사실상의 표준으로 자리잡고 있습니다.</p>

<h3 id="트렌드-5-모델-드리프트-감지">트렌드 5: 모델 드리프트 감지</h3>

<p>하네스가 <strong>모델이 100번째 스텝 이후 지시를 따르지 않거나 추론 오류를 범하는 시점</strong>을 정확히 감지하는 도구로 진화하고 있습니다.</p>

<h3 id="트렌드-6-신규-vs-기존-코드베이스-분화">트렌드 6: 신규 vs 기존 코드베이스 분화</h3>

<ul>
  <li><strong>신규 프로젝트</strong>: 처음부터 하네스를 고려하여 설계</li>
  <li><strong>기존 프로젝트</strong>: 하네스 레트로핏이 항상 가치 있지는 않음 — 엔트로피가 높은 레거시 코드는 비용 대비 효과가 낮을 수 있음</li>
</ul>

<hr />

<h2 id="slide-10-핵심-요약">Slide 10. 핵심 요약</h2>

<blockquote>
  <p><strong>“더 나은 모델”이 아니라 “더 나은 제어 환경”이 장기적 코드 품질과 에이전트 신뢰성을 결정한다</strong></p>
</blockquote>

<table>
  <thead>
    <tr>
      <th>원칙</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>모델은 부품, 하네스가 제품</strong></td>
      <td>모델은 교체 가능하지만 하네스는 경쟁 우위</td>
    </tr>
    <tr>
      <td><strong>제약이 신뢰를 만든다</strong></td>
      <td>해결 공간을 좁힐수록 결과가 안정적</td>
    </tr>
    <tr>
      <td><strong>실패를 신호로</strong></td>
      <td>에이전트 실패 → 하네스 개선 기회</td>
    </tr>
    <tr>
      <td><strong>검증은 자동으로</strong></td>
      <td>수동 검토 대신 E2E 테스트와 구조 테스트</td>
    </tr>
    <tr>
      <td><strong>점진적 진행</strong></td>
      <td>한 번에 하나의 기능, 매번 커밋</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li><a href="https://martinfowler.com/articles/exploring-gen-ai/harness-engineering.html">Harness Engineering — Martin Fowler</a></li>
  <li><a href="https://openai.com/index/harness-engineering/">Harness Engineering: Leveraging Codex in an Agent-First World — OpenAI</a></li>
  <li><a href="https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents">Effective Harnesses for Long-Running Agents — Anthropic</a></li>
  <li><a href="https://www.firecrawl.dev/blog/what-is-an-agent-harness">What Is an Agent Harness? — Firecrawl</a></li>
  <li><a href="https://medium.com/be-open/what-is-ai-harness-engineering-your-guide-to-controlling-autonomous-systems-30c9c8d2b489">What is AI Harness Engineering? — Medium</a></li>
  <li><a href="https://www.philschmid.de/agent-harness-2026">The Importance of Agent Harness in 2026 — Philipp Schmid</a></li>
  <li><a href="https://aakashgupta.medium.com/2025-was-agents-2026-is-agent-harnesses-heres-why-that-changes-everything-073e9877655e">2025 Was Agents. 2026 Is Agent Harnesses. — Aakash Gupta</a></li>
  <li><a href="https://medium.com/@roberto.g.infante/the-state-of-ai-agent-frameworks-comparing-langgraph-openai-agent-sdk-google-adk-and-aws-d3e52a497720">The State of AI Agent Frameworks — Roberto Infante</a></li>
  <li><a href="https://www.bighatgroup.com/blog/everything-claude-code-ai-agent-harness-guide/">Everything Claude Code: The Agent Harness — Big Hat Group</a></li>
  <li><a href="https://platform.claude.com/docs/en/agent-sdk/overview">Claude Agent SDK Overview — Anthropic</a></li>
</ul>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="harness-engineering" /><category term="ai-agent" /><category term="context-engineering" /><category term="llm" /><category term="production" /><category term="codex" /><category term="claude-code" /><summary type="html"><![CDATA[2025년이 AI 에이전트의 해였다면, 2026년은 하네스(Harness)의 해입니다. 모델은 이미 충분히 강력해졌고, 이제 경쟁력은 모델 자체가 아니라 모델을 감싸는 시스템에서 갈립니다.]]></summary></entry><entry><title type="html">A2A 프로토콜로 에이전트 간 대화 내역과 메모리를 공유할 수 있을까?</title><link href="https://seonghak.com/blog/2026/03/24/a2a-agent-memory-sharing/" rel="alternate" type="text/html" title="A2A 프로토콜로 에이전트 간 대화 내역과 메모리를 공유할 수 있을까?" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/a2a-agent-memory-sharing</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/a2a-agent-memory-sharing/"><![CDATA[<p>AI 에이전트가 단독으로 작동하는 시대는 지나가고 있습니다. 이제 여러 에이전트가 협력하여 복잡한 업무를 처리하는 <strong>멀티 에이전트 시스템</strong>이 현실이 되고 있는데, 여기서 핵심적인 질문이 하나 있습니다.</p>

<blockquote>
  <p>“에이전트 A가 과거에 나눈 대화 내역과 학습한 맥락을 에이전트 B에게 넘겨줄 수 있을까?”</p>
</blockquote>

<p>이 글에서는 Google의 <strong>A2A(Agent-to-Agent) 프로토콜</strong>을 중심으로 에이전트 간 대화 내역과 메모리를 공유하는 방법을 정리합니다.</p>

<hr />

<h2 id="1-a2a-프로토콜이란">1. A2A 프로토콜이란?</h2>

<p><strong>A2A(Agent-to-Agent)</strong>는 Google이 2024년에 발표하고 2026년 3월에 v1.0.0을 릴리스한 오픈 프로토콜입니다. 서로 다른 프레임워크, 다른 벤더에서 만든 AI 에이전트들이 표준화된 방식으로 통신하고 협업할 수 있게 해줍니다.</p>

<h3 id="기술-스택">기술 스택</h3>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>기술</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>전송 계층</td>
      <td>HTTP/HTTPS</td>
    </tr>
    <tr>
      <td>메시지 형식</td>
      <td>JSON-RPC 2.0</td>
    </tr>
    <tr>
      <td>실시간 스트리밍</td>
      <td>SSE (Server-Sent Events)</td>
    </tr>
    <tr>
      <td>에이전트 발견</td>
      <td>Agent Card (JSON 메타데이터)</td>
    </tr>
  </tbody>
</table>

<h3 id="핵심-데이터-모델">핵심 데이터 모델</h3>

<p>A2A의 통신은 다음 구조로 이루어집니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Task (작업 단위)
├── Message (대화 턴)
│   ├── role: "user" | "agent"
│   ├── contextId
│   ├── messageId
│   └── parts[]
│       ├── TextPart   (텍스트)
│       ├── FilePart   (파일 참조)
│       └── DataPart   (구조화된 JSON 데이터)
└── Artifact (에이전트가 생성한 산출물)
    └── parts[]
</code></pre></div></div>

<ul>
  <li><strong>Task</strong>: 에이전트 간 협업의 기본 단위. 하나의 작업 요청과 그에 대한 응답 전체를 포괄합니다.</li>
  <li><strong>Message</strong>: Task 내의 개별 대화 턴. <code class="language-plaintext highlighter-rouge">contextId</code>로 대화 맥락을 추적합니다.</li>
  <li><strong>Part</strong>: 메시지를 구성하는 최소 콘텐츠 단위. 텍스트, 파일, 구조화된 데이터를 담을 수 있습니다.</li>
  <li><strong>Artifact</strong>: 에이전트가 작업 결과로 생성한 산출물(문서, 이미지, 데이터 등).</li>
</ul>

<hr />

<h2 id="2-a2a의-핵심-원칙-opaque-execution">2. A2A의 핵심 원칙: Opaque Execution</h2>

<p>A2A 프로토콜의 가장 중요한 설계 원칙은 <strong>Opaque Execution(불투명 실행)</strong>입니다.</p>

<blockquote>
  <p>에이전트는 자신의 <strong>내부 상태, 메모리, 도구, 추론 과정</strong>을 상대 에이전트에게 공개하지 않습니다.</p>
</blockquote>

<p>이것은 의도적인 설계입니다. 각 에이전트의 독립성, 보안, 지적 재산을 보호하면서도 협업을 가능하게 하기 위함입니다. 에이전트는 서로를 블랙박스로 취급하되, <strong>선언된 능력(Agent Card)</strong>과 <strong>교환된 메시지(Message/Artifact)</strong>를 기반으로 협력합니다.</p>

<h3 id="이것이-의미하는-것">이것이 의미하는 것</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>에이전트 A                    에이전트 B
┌─────────────┐              ┌─────────────┐
│ 내부 메모리  │   ← 비공개    │ 내부 메모리  │
│ 추론 과정    │              │ 추론 과정    │
│ 사용 도구    │              │ 사용 도구    │
├─────────────┤              ├─────────────┤
│ Agent Card  │ ─── 공개 ───→ │             │
│ Message     │ ←── 교환 ───→ │ Message     │
│ Artifact    │ ←── 교환 ───→ │ Artifact    │
└─────────────┘              └─────────────┘
</code></pre></div></div>

<p>즉, <strong>A2A 프로토콜 자체만으로는 에이전트의 과거 대화 내역이나 내부 메모리를 직접 공유하는 메커니즘이 없습니다.</strong> 하지만 이를 해결하기 위한 여러 접근 방식이 존재합니다.</p>

<hr />

<h2 id="3-a2a-내에서-컨텍스트를-전달하는-방법">3. A2A 내에서 컨텍스트를 전달하는 방법</h2>

<p>A2A가 내부 메모리를 직접 공유하지는 않지만, 프로토콜이 제공하는 메커니즘을 활용하면 <strong>필요한 맥락을 명시적으로 전달</strong>할 수 있습니다.</p>

<h3 id="3-1-datapart를-활용한-구조화된-컨텍스트-전달">3-1. DataPart를 활용한 구조화된 컨텍스트 전달</h3>

<p><code class="language-plaintext highlighter-rouge">DataPart</code>에 과거 대화 요약, 사용자 선호도, 작업 상태 등을 JSON 구조로 담아 전달할 수 있습니다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tasks/send"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"params"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"task-001"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"parts"</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><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"text"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"text"</span><span class="p">:</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="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"data"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"conversation_summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"사용자가 Q1 매출 분석을 요청했고, 특히 아시아 시장에 관심이 많음"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"key_decisions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"분석 기간: 2025 Q1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"비교 대상: 전년 동기"</span><span class="p">],</span><span class="w">
            </span><span class="nl">"user_preferences"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"format"</span><span class="p">:</span><span class="w"> </span><span class="s2">"executive_summary"</span><span class="p">,</span><span class="w">
              </span><span class="nl">"language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ko"</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><span class="p">]</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>

<h3 id="3-2-artifact를-통한-작업-결과-전달">3-2. Artifact를 통한 작업 결과 전달</h3>

<p>에이전트 A의 작업 결과(Artifact)를 에이전트 B의 입력으로 사용하는 <strong>파이프라인 패턴</strong>입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>에이전트 A (데이터 분석) → Artifact(분석 결과) → 에이전트 B (보고서 작성)
</code></pre></div></div>

<p>Artifact에는 텍스트, 파일, 구조화된 데이터를 모두 담을 수 있으므로, 이전 에이전트의 작업 결과를 풍부하게 전달할 수 있습니다.</p>

<h3 id="3-3-contextid를-통한-대화-맥락-유지">3-3. contextId를 통한 대화 맥락 유지</h3>

<p>A2A의 <code class="language-plaintext highlighter-rouge">Message</code>에는 <code class="language-plaintext highlighter-rouge">contextId</code> 필드가 있어, 같은 맥락에 속하는 메시지들을 그룹핑할 수 있습니다. 이를 통해 에이전트가 하나의 대화 흐름 안에서 맥락을 유지하며 통신할 수 있습니다.</p>

<h3 id="한계">한계</h3>

<p>이 방법들은 모두 <strong>명시적 전달</strong>입니다. 오케스트레이터(혹은 클라이언트)가 “어떤 맥락을 전달할지”를 결정하고 직접 구성해야 합니다. 에이전트가 자동으로 과거 기억을 검색하거나 공유하는 것은 아닙니다.</p>

<hr />

<h2 id="4-a2a를-넘어서-에이전트-간-메모리-공유-솔루션">4. A2A를 넘어서: 에이전트 간 메모리 공유 솔루션</h2>

<p>A2A만으로는 부족한 부분을 보완하기 위해 여러 프로토콜과 프레임워크가 등장했습니다.</p>

<h3 id="4-1-macp-multi-agent-cognition-protocol">4-1. MACP (Multi-Agent Cognition Protocol)</h3>

<p><a href="https://github.com/multiagentcognition/macp">MACP</a>는 A2A와 MCP 사이의 간극을 메우는 <strong>공유 조정 프로토콜</strong>입니다.</p>

<p><strong>핵심 아이디어</strong>: 로컬 SQLite 파일을 공유 버스로 사용하여, 에이전트 간에 메모리와 컨텍스트를 교환합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>에이전트 A ──┐
             ├──→ [ SQLite (공유 메모리) ] ←──┤
에이전트 B ──┘                                 ├── 에이전트 C
                                               │
</code></pre></div></div>

<p><strong>제공 기능</strong>:</p>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">macp_ext_set_memory</code></td>
      <td>메모리 저장</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">macp_ext_get_memory</code></td>
      <td>메모리 조회</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">macp_ext_search_memory</code></td>
      <td>메모리 검색</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">macp_ext_list_memories</code></td>
      <td>메모리 목록</td>
    </tr>
  </tbody>
</table>

<p><strong>특징</strong>:</p>
<ul>
  <li>제로 런타임 의존성 (TypeScript, <code class="language-plaintext highlighter-rouge">npm i macp</code>)</li>
  <li>중앙 네트워크 서비스 불필요 (로컬 SQLite 파일로 동작)</li>
  <li>Claude Code, Cursor, Gemini CLI 등 주요 AI 도구 지원</li>
  <li>에이전트별 독립적 워킹 컨텍스트를 유지하면서 공유 메모리 접근</li>
</ul>

<p><strong>활용 시나리오</strong>: 하나의 프로젝트에서 여러 에이전트가 동시에 작업할 때, 파일 소유권 시그널링, 작업 대기열 관리, 발견한 사실의 공유 등에 활용됩니다.</p>

<h3 id="4-2-samep-secure-agent-memory-exchange-protocol">4-2. SAMEP (Secure Agent Memory Exchange Protocol)</h3>

<p><a href="https://arxiv.org/html/2507.10562v1">SAMEP</a>은 2025년 arXiv에 발표된 논문에서 제안된 프로토콜로, 보안이 중요한 환경에서의 에이전트 메모리 공유를 다룹니다.</p>

<p><strong>해결하는 세 가지 문제</strong>:</p>
<ol>
  <li><strong>세션 간 컨텍스트 영속성</strong>: 에이전트가 재시작되어도 과거 대화 맥락을 유지</li>
  <li><strong>안전한 멀티 에이전트 협업</strong>: 세밀한 접근 제어로 필요한 정보만 공유</li>
  <li><strong>효율적인 시맨틱 검색</strong>: 과거 맥락 중 관련된 것만 빠르게 찾기</li>
</ol>

<p><strong>기술 구성</strong>:</p>
<ul>
  <li>분산 메모리 저장소 + 벡터 기반 시맨틱 검색</li>
  <li>AES-256-GCM 암호화 기반 접근 제어</li>
  <li>기존 프로토콜(MCP, A2A)과의 호환성</li>
  <li>HIPAA 등 규제 컴플라이언스 지원</li>
</ul>

<p><strong>실험 결과</strong>:</p>
<ul>
  <li>중복 연산 <strong>73% 감소</strong></li>
  <li>컨텍스트 관련성 점수 <strong>89% 향상</strong></li>
</ul>

<h3 id="4-3-a2a-memory-system-context-engineering-접근">4-3. A2A Memory System (Context Engineering 접근)</h3>

<p><a href="https://github.com/Sardor-M/a2a-memory-system">A2A Memory System</a>은 Google의 “Context Engineering: Sessions &amp; Memory” 원칙을 구현한 오픈소스 프로젝트입니다.</p>

<p><strong>다섯 가지 메모리 유형</strong>:</p>

<table>
  <thead>
    <tr>
      <th>메모리 유형</th>
      <th>설명</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Declarative</strong></td>
      <td>사실과 정책</td>
      <td>“회사 방침은 모든 API에 인증을 필수로 한다”</td>
    </tr>
    <tr>
      <td><strong>Procedural</strong></td>
      <td>워크플로우와 절차</td>
      <td>“배포 프로세스: 빌드 → 테스트 → 스테이징 → 프로덕션”</td>
    </tr>
    <tr>
      <td><strong>Episodic</strong></td>
      <td>특정 상호작용 기록</td>
      <td>“지난주 회의에서 DB 마이그레이션을 3월로 연기하기로 함”</td>
    </tr>
    <tr>
      <td><strong>Semantic</strong></td>
      <td>개념과 관계</td>
      <td>“서비스 A는 서비스 B에 의존한다”</td>
    </tr>
    <tr>
      <td><strong>Working</strong></td>
      <td>현재 작업 컨텍스트</td>
      <td>“지금 처리 중인 Task의 중간 결과”</td>
    </tr>
  </tbody>
</table>

<p>이렇게 메모리를 유형별로 분류하면, 에이전트가 상황에 맞는 메모리를 선택적으로 검색하고 공유할 수 있어 128K 토큰 컨텍스트 윈도우를 효율적으로 활용할 수 있습니다.</p>

<hr />

<h2 id="5-실전-아키텍처-패턴">5. 실전 아키텍처 패턴</h2>

<h3 id="패턴-1-오케스트레이터--공유-메모리">패턴 1: 오케스트레이터 + 공유 메모리</h3>

<p>가장 실용적인 패턴으로, 오케스트레이터가 컨텍스트 라우팅을 담당합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                    ┌──────────────────────┐
                    │    Orchestrator      │
                    │  (A2A Client)        │
                    └──┬───────┬───────┬───┘
                       │       │       │
                  A2A  │  A2A  │  A2A  │
                       │       │       │
                    ┌──┴──┐ ┌──┴──┐ ┌──┴──┐
                    │ 분석 │ │ 작성 │ │ 검증 │
                    │Agent│ │Agent│ │Agent│
                    └──┬──┘ └──┬──┘ └──┬──┘
                       │       │       │
                  MCP  │  MCP  │  MCP  │
                       │       │       │
                    ┌──┴───────┴───────┴──┐
                    │   Shared Memory     │
                    │ (MACP / Vector DB)  │
                    └─────────────────────┘
</code></pre></div></div>

<p><strong>동작 방식</strong>:</p>
<ol>
  <li>오케스트레이터가 사용자 요청을 받아 Task를 생성</li>
  <li>분석 에이전트에게 A2A로 분석 요청 → 결과를 공유 메모리에 저장</li>
  <li>작성 에이전트에게 A2A로 작성 요청 시, DataPart에 이전 분석 결과의 참조를 포함</li>
  <li>작성 에이전트는 공유 메모리(MCP 서버)에서 상세 컨텍스트를 가져와 작업</li>
</ol>

<h3 id="패턴-2-파이프라인--artifact-체인">패턴 2: 파이프라인 + Artifact 체인</h3>

<p>에이전트의 Artifact를 다음 에이전트의 입력으로 직접 연결하는 패턴입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>사용자 요청
    │
    ▼
[리서치 Agent] ──Artifact(조사 결과)──→ [분석 Agent] ──Artifact(분석 보고서)──→ [요약 Agent]
                                                                                    │
                                                                              최종 결과물
</code></pre></div></div>

<p>이 패턴에서는 각 에이전트가 이전 에이전트의 산출물을 “대화 내역”처럼 받아서 작업합니다. Artifact가 곧 공유되는 컨텍스트 역할을 합니다.</p>

<h3 id="패턴-3-메모리-서버--mcp">패턴 3: 메모리 서버 + MCP</h3>

<p>MCP(Model Context Protocol) 서버로 메모리 저장소를 구성하고, A2A 에이전트들이 MCP를 통해 접근하는 패턴입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>에이전트 A ──(MCP)──→ Memory MCP Server ←──(MCP)── 에이전트 B
                          │
                    ┌─────┴─────┐
                    │ Vector DB │
                    │ (메모리)   │
                    └───────────┘
</code></pre></div></div>

<p><strong>장점</strong>:</p>
<ul>
  <li>A2A는 에이전트 간 통신에만 집중</li>
  <li>메모리 관리는 MCP 도구로 분리</li>
  <li>각 에이전트가 필요한 메모리만 시맨틱 검색으로 가져옴</li>
</ul>

<hr />

<h2 id="6-프로토콜-간-역할-비교">6. 프로토콜 간 역할 비교</h2>

<table>
  <thead>
    <tr>
      <th>프로토콜</th>
      <th>역할</th>
      <th>메모리 공유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>A2A</strong></td>
      <td>에이전트 ↔ 에이전트 통신</td>
      <td>Message/Artifact를 통한 명시적 전달</td>
    </tr>
    <tr>
      <td><strong>MCP</strong></td>
      <td>에이전트 → 도구/데이터 접근</td>
      <td>메모리 서버를 도구로 제공 가능</td>
    </tr>
    <tr>
      <td><strong>MACP</strong></td>
      <td>에이전트 간 실시간 조정</td>
      <td>SQLite 기반 공유 메모리</td>
    </tr>
    <tr>
      <td><strong>SAMEP</strong></td>
      <td>보안 메모리 교환</td>
      <td>암호화된 분산 메모리 저장소</td>
    </tr>
  </tbody>
</table>

<p>이 프로토콜들은 상호 배타적이지 않습니다. 오히려 <strong>함께 사용할 때</strong> 완전한 멀티 에이전트 메모리 공유 시스템을 구축할 수 있습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>A2A  → 에이전트가 서로 대화하는 방법
MCP  → 에이전트가 메모리 저장소에 접근하는 방법
MACP → 에이전트가 실시간으로 컨텍스트를 조정하는 방법
SAMEP → 메모리를 안전하게 암호화하고 접근 제어하는 방법
</code></pre></div></div>

<hr />

<h2 id="7-현실적인-한계와-고려사항">7. 현실적인 한계와 고려사항</h2>

<h3 id="표준화의-현재-상태">표준화의 현재 상태</h3>

<p>A2A v1.0.0이 2026년 3월에 출시되었지만, 에이전트 간 메모리 공유에 대한 <strong>공식 표준은 아직 없습니다</strong>. 현재의 인터롭 표준들(A2A, MCP, AG-UI)은 “통신”을 해결하지만 “인지”는 해결하지 않는다는 지적이 있습니다.</p>

<blockquote>
  <p>“Standards without shared memory create connected strangers.” — MemU Blog</p>
</blockquote>

<h3 id="토큰-효율성">토큰 효율성</h3>

<p>과거 대화를 통째로 전달하면 토큰 사용량이 급증합니다. 실제로는 다음과 같은 최적화가 필요합니다.</p>

<ul>
  <li><strong>요약(Summarization)</strong>: 긴 대화를 핵심 포인트로 압축</li>
  <li><strong>시맨틱 검색</strong>: 현재 작업에 관련된 과거 맥락만 선택적으로 가져오기</li>
  <li><strong>계층적 메모리</strong>: 중요도에 따라 메모리를 계층화하여 관리</li>
</ul>

<h3 id="보안-고려사항">보안 고려사항</h3>

<p>에이전트 간 메모리를 공유할 때 반드시 고려해야 할 점들입니다.</p>

<ul>
  <li>어떤 에이전트가 어떤 메모리에 접근 가능한지 <strong>접근 제어</strong></li>
  <li>민감 정보의 <strong>암호화</strong> (SAMEP의 AES-256-GCM 등)</li>
  <li>메모리 교환의 <strong>감사 로그</strong> (특히 규제 환경)</li>
  <li>한 에이전트의 hallucination이 다른 에이전트로 전파되는 <strong>오염 방지</strong></li>
</ul>

<hr />

<h2 id="8-정리">8. 정리</h2>

<p>A2A 프로토콜은 에이전트 간 통신의 표준을 제공하지만, <strong>과거 대화 내역과 메모리의 직접적인 공유 메커니즘은 내장되어 있지 않습니다.</strong> 이는 보안과 독립성을 위한 의도적인 설계입니다.</p>

<p>하지만 다음과 같은 방법으로 에이전트 간 대화 내역과 메모리를 공유할 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>방법</th>
      <th>난이도</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>A2A DataPart로 명시적 전달</strong></td>
      <td>낮음</td>
      <td>오케스트레이터가 필요한 맥락을 DataPart에 담아 전달</td>
    </tr>
    <tr>
      <td><strong>Artifact 체인</strong></td>
      <td>낮음</td>
      <td>이전 에이전트의 산출물을 다음 에이전트의 입력으로 연결</td>
    </tr>
    <tr>
      <td><strong>MCP 메모리 서버</strong></td>
      <td>중간</td>
      <td>Vector DB 기반 MCP 서버를 공유 메모리로 활용</td>
    </tr>
    <tr>
      <td><strong>MACP 프로토콜</strong></td>
      <td>중간</td>
      <td>SQLite 기반 실시간 공유 메모리</td>
    </tr>
    <tr>
      <td><strong>SAMEP 프로토콜</strong></td>
      <td>높음</td>
      <td>암호화된 분산 메모리 저장소로 엔터프라이즈급 메모리 공유</td>
    </tr>
  </tbody>
</table>

<p>멀티 에이전트 시스템에서 메모리 공유는 아직 발전 중인 영역입니다. 하지만 A2A + MCP를 기반으로 MACP나 SAMEP 같은 보완 프로토콜을 결합하면, 에이전트들이 과거 대화와 학습한 맥락을 효과적으로 공유하는 시스템을 지금도 구축할 수 있습니다.</p>

<hr />

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li><a href="https://google.github.io/A2A/">A2A Protocol v1.0.0 공식 문서</a></li>
  <li><a href="https://github.com/google/a2a">A2A GitHub 저장소</a></li>
  <li><a href="https://github.com/multiagentcognition/macp">MACP - Multi-Agent Cognition Protocol</a></li>
  <li><a href="https://arxiv.org/html/2507.10562v1">SAMEP: Secure Agent Memory Exchange Protocol (arXiv)</a></li>
  <li><a href="https://github.com/Sardor-M/a2a-memory-system">A2A Memory System</a></li>
  <li><a href="https://dev.to/pockit_tools/mcp-vs-a2a-the-complete-guide-to-ai-agent-protocols-in-2026-30li">MCP vs A2A: The Complete Guide (2026)</a></li>
</ul>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="a2a" /><category term="multi-agent" /><category term="mcp" /><category term="agent memory" /><category term="macp" /><category term="samep" /><summary type="html"><![CDATA[AI 에이전트가 단독으로 작동하는 시대는 지나가고 있습니다. 이제 여러 에이전트가 협력하여 복잡한 업무를 처리하는 멀티 에이전트 시스템이 현실이 되고 있는데, 여기서 핵심적인 질문이 하나 있습니다.]]></summary></entry><entry><title type="html">Google ADK 멀티에이전트 시스템: 컨텍스트 공유와 에스컬레이션 완전 가이드</title><link href="https://seonghak.com/blog/2026/03/24/adk-agent-context-sharing-escalation/" rel="alternate" type="text/html" title="Google ADK 멀티에이전트 시스템: 컨텍스트 공유와 에스컬레이션 완전 가이드" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/adk-agent-context-sharing-escalation</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/adk-agent-context-sharing-escalation/"><![CDATA[<p>Google의 <strong>Agent Development Kit(ADK)</strong>으로 멀티에이전트 시스템을 구축할 때, 가장 핵심적인 질문 중 하나는 <strong>“에이전트들이 어떻게 정보를 공유하고, 처리할 수 없는 작업을 다른 에이전트에게 넘기는가?”</strong>입니다.</p>

<p>이 글에서는 ADK에서 제공하는 컨텍스트 공유 메커니즘과 에스컬레이션 패턴을 네 가지 축으로 정리합니다.</p>

<ol>
  <li><strong>Agent ↔ Agent</strong> 간 컨텍스트 공유</li>
  <li><strong>Agent ↔ SubAgent</strong> 간 컨텍스트 공유</li>
  <li><strong>Agent ↔ Tool</strong> 간 컨텍스트 공유</li>
  <li><strong>Escalation</strong> — 다른 에이전트나 상위 에이전트로 제어를 되돌리는 방법</li>
</ol>

<hr />

<h2 id="1-adk의-컨텍스트-관리-구조">1. ADK의 컨텍스트 관리 구조</h2>

<p>ADK는 대화형 컨텍스트를 <strong>세 가지 계층</strong>으로 관리합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Session (대화 스레드)
├── Events[] (메시지/액션 이력)
├── State (session.state) ← 현재 대화의 임시 데이터
│   ├── 기본 키 (세션 스코프)
│   ├── user: 접두사 (사용자 스코프)
│   ├── app: 접두사 (앱 전역 스코프)
│   └── temp: 접두사 (단일 호출 스코프)
└── Memory (교차 세션 장기 기억)
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>역할</th>
      <th>지속성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Session</strong></td>
      <td>단일 대화 스레드. 사용자와 에이전트 간 상호작용의 이벤트 시퀀스 포함</td>
      <td>SessionService에 따라 결정</td>
    </tr>
    <tr>
      <td><strong>State</strong></td>
      <td>세션 내 키-값 쌍으로 된 동적 데이터 저장소</td>
      <td>접두사(prefix)에 따라 스코프 결정</td>
    </tr>
    <tr>
      <td><strong>Memory</strong></td>
      <td>여러 세션에 걸친 검색 가능한 장기 지식 저장소</td>
      <td>MemoryService가 관리</td>
    </tr>
  </tbody>
</table>

<h3 id="state의-네-가지-스코프-접두사">State의 네 가지 스코프 접두사</h3>

<p>ADK State의 핵심은 <strong>접두사(prefix)</strong>로 스코프를 구분한다는 점입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 세션 스코프 — 현재 세션에서만 유효
</span><span class="n">session</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">'current_step'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'analysis'</span>

<span class="c1"># 사용자 스코프 — 같은 사용자의 모든 세션에서 공유
</span><span class="n">session</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">'user:preferred_language'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'ko'</span>

<span class="c1"># 앱 스코프 — 모든 사용자와 세션에서 공유
</span><span class="n">session</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">'app:global_config'</span><span class="p">]</span> <span class="o">=</span> <span class="s">'v2'</span>

<span class="c1"># 임시 스코프 — 현재 호출(invocation)에서만 유효, 호출 완료 후 폐기
</span><span class="n">session</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">'temp:intermediate_result'</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="s">'score'</span><span class="p">:</span> <span class="mf">0.95</span><span class="p">}</span>
</code></pre></div></div>

<p>특히 <code class="language-plaintext highlighter-rouge">temp:</code> 접두사는 <strong>부모 에이전트가 서브에이전트를 호출할 때 같은 InvocationContext를 전달</strong>하기 때문에, 동일 호출 체인 내에서 에이전트 간 데이터를 임시로 주고받는 데 유용합니다.</p>

<hr />

<h2 id="2-agent--agent-간-컨텍스트-공유">2. Agent ↔ Agent 간 컨텍스트 공유</h2>

<p>같은 시스템 내 에이전트들이 서로 데이터를 교환하는 방법은 크게 세 가지입니다.</p>

<h3 id="2-1-shared-session-state-공유-세션-상태">2-1. Shared Session State (공유 세션 상태)</h3>

<p>가장 기본적이고 널리 사용되는 방식입니다. 같은 Session을 공유하는 에이전트들은 <code class="language-plaintext highlighter-rouge">session.state</code>를 통해 데이터를 읽고 쓸 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="n">LlmAgent</span><span class="p">,</span> <span class="n">SequentialAgent</span>

<span class="n">agent_a</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"AgentA"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"프랑스의 수도를 찾아주세요."</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"capital_city"</span>  <span class="c1"># 결과를 state['capital_city']에 저장
</span><span class="p">)</span>

<span class="n">agent_b</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"AgentB"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"{capital_city}에 대해 자세히 알려주세요."</span>
    <span class="c1"># {capital_city} → state['capital_city'] 값이 자동 주입됨
</span><span class="p">)</span>

<span class="n">pipeline</span> <span class="o">=</span> <span class="n">SequentialAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"CityInfoPipeline"</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">agent_a</span><span class="p">,</span> <span class="n">agent_b</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>여기서 핵심은 <code class="language-plaintext highlighter-rouge">output_key</code> 파라미터입니다. 에이전트의 최종 텍스트 응답이 자동으로 지정된 State 키에 저장되어, 후속 에이전트가 <code class="language-plaintext highlighter-rouge">{key}</code> 템플릿 구문으로 바로 참조할 수 있습니다.</p>

<h3 id="2-2-llm-driven-delegation-transfer_to_agent">2-2. LLM-Driven Delegation (transfer_to_agent)</h3>

<p>LLM이 상황을 판단하여 다른 에이전트에게 <strong>동적으로 제어를 넘기는</strong> 방식입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="n">LlmAgent</span>

<span class="n">billing_agent</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Billing"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"결제 관련 문의를 처리합니다."</span>
<span class="p">)</span>

<span class="n">support_agent</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Support"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"기술 지원 요청을 처리합니다."</span>
<span class="p">)</span>

<span class="n">coordinator</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"HelpDesk"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"결제 문제는 Billing에게, 기술 문제는 Support에게 전달하세요."</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">billing_agent</span><span class="p">,</span> <span class="n">support_agent</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>사용자가 “결제가 안 돼요”라고 말하면, Coordinator의 LLM이 자동으로 다음과 같은 함수 호출을 생성합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>transfer_to_agent(agent_name='Billing')
</code></pre></div></div>

<p>ADK 프레임워크의 <code class="language-plaintext highlighter-rouge">AutoFlow</code>가 이 호출을 가로채서 <code class="language-plaintext highlighter-rouge">root_agent.find_agent()</code>로 대상 에이전트를 찾고, <code class="language-plaintext highlighter-rouge">InvocationContext</code>를 갱신하여 실행 초점을 전환합니다.</p>

<h3 id="2-3-agenttool을-통한-명시적-호출">2-3. AgentTool을 통한 명시적 호출</h3>

<p>다른 에이전트를 <strong>도구(Tool)처럼 감싸서</strong> 호출하는 방식입니다. <code class="language-plaintext highlighter-rouge">transfer_to_agent</code>가 제어 자체를 넘기는 것과 달리, <code class="language-plaintext highlighter-rouge">AgentTool</code>은 현재 에이전트의 흐름 안에서 다른 에이전트를 실행하고 결과를 받아옵니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="n">LlmAgent</span>
<span class="kn">from</span> <span class="nn">google.adk.tools</span> <span class="kn">import</span> <span class="n">agent_tool</span>

<span class="n">summarizer</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Summarizer"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"텍스트를 요약합니다."</span>
<span class="p">)</span>

<span class="n">research_agent</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Researcher"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"주제를 조사하고, Summarizer 도구를 사용해 결과를 요약하세요."</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">agent_tool</span><span class="p">.</span><span class="n">AgentTool</span><span class="p">(</span><span class="n">agent</span><span class="o">=</span><span class="n">summarizer</span><span class="p">)]</span>
<span class="p">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>제어 흐름</th>
      <th>사용 시나리오</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">transfer_to_agent</code></td>
      <td>제어권 자체가 대상 에이전트로 이동</td>
      <td>전문 에이전트에게 대화를 완전히 위임</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AgentTool</code></td>
      <td>현재 에이전트 안에서 대상 에이전트를 실행 후 결과 수신</td>
      <td>부분 작업을 도구처럼 위임하고 결과를 조합</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="3-agent--subagent-간-컨텍스트-공유">3. Agent ↔ SubAgent 간 컨텍스트 공유</h2>

<p>ADK에서 에이전트 계층은 트리 구조로 구성됩니다. 부모 에이전트가 서브에이전트를 호출할 때 <strong>같은 <code class="language-plaintext highlighter-rouge">InvocationContext</code>를 전달</strong>하므로, 여러 가지 메커니즘으로 컨텍스트를 공유할 수 있습니다.</p>

<h3 id="3-1-invocationcontext-공유">3-1. InvocationContext 공유</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Coordinator (부모)
├── InvocationContext ──── 공유 ────→ SubAgent A
│   ├── session
│   ├── state (temp: 포함)        ──→ SubAgent B
│   └── services
</code></pre></div></div>

<p>부모 에이전트가 <code class="language-plaintext highlighter-rouge">SequentialAgent</code>나 <code class="language-plaintext highlighter-rouge">ParallelAgent</code>로 서브에이전트를 실행하면, 같은 <code class="language-plaintext highlighter-rouge">InvocationContext</code>가 전달됩니다. 이는 곧 <strong>동일한 <code class="language-plaintext highlighter-rouge">temp:</code> 상태를 공유</strong>한다는 의미입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="n">SequentialAgent</span><span class="p">,</span> <span class="n">LlmAgent</span>

<span class="c1"># Step1이 temp: 상태에 데이터를 저장
</span><span class="n">step1</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Analyzer"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"데이터를 분석하세요."</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"temp:analysis_result"</span>
<span class="p">)</span>

<span class="c1"># Step2가 같은 temp: 상태에서 데이터를 읽음
</span><span class="n">step2</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Reporter"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"{temp:analysis_result}를 바탕으로 보고서를 작성하세요."</span>
<span class="p">)</span>

<span class="n">pipeline</span> <span class="o">=</span> <span class="n">SequentialAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"AnalysisPipeline"</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">step1</span><span class="p">,</span> <span class="n">step2</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="3-2-workflow-agent별-컨텍스트-전달-방식">3-2. Workflow Agent별 컨텍스트 전달 방식</h3>

<table>
  <thead>
    <tr>
      <th>Workflow Agent</th>
      <th>실행 방식</th>
      <th>컨텍스트 특성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>SequentialAgent</strong></td>
      <td>순차 실행</td>
      <td>같은 InvocationContext를 순서대로 전달. 이전 에이전트의 State 변경이 다음 에이전트에 즉시 반영</td>
    </tr>
    <tr>
      <td><strong>ParallelAgent</strong></td>
      <td>병렬 실행</td>
      <td>각 자식에게 다른 <code class="language-plaintext highlighter-rouge">branch</code> 경로를 부여하지만, <strong>같은 <code class="language-plaintext highlighter-rouge">session.state</code>를 공유</strong>. 경합 방지를 위해 서로 다른 키를 사용해야 함</td>
    </tr>
    <tr>
      <td><strong>LoopAgent</strong></td>
      <td>반복 실행</td>
      <td>매 반복마다 같은 InvocationContext를 전달. State 변경이 다음 반복에 누적됨</td>
    </tr>
  </tbody>
</table>

<h3 id="3-3-parallel-agent에서의-state-공유-주의점">3-3. Parallel Agent에서의 State 공유 주의점</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="n">ParallelAgent</span><span class="p">,</span> <span class="n">SequentialAgent</span><span class="p">,</span> <span class="n">LlmAgent</span>

<span class="c1"># 병렬 실행 시 서로 다른 output_key를 사용해야 경합 조건을 방지
</span><span class="n">fetch_weather</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"WeatherFetcher"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"날씨 정보를 가져오세요."</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"weather_data"</span>  <span class="c1"># 고유한 키
</span><span class="p">)</span>

<span class="n">fetch_news</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"NewsFetcher"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"뉴스를 가져오세요."</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"news_data"</span>  <span class="c1"># 고유한 키
</span><span class="p">)</span>

<span class="n">gather</span> <span class="o">=</span> <span class="n">ParallelAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"InfoGatherer"</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">fetch_weather</span><span class="p">,</span> <span class="n">fetch_news</span><span class="p">]</span>
<span class="p">)</span>

<span class="n">synthesizer</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Synthesizer"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"{weather_data}와 {news_data}를 종합하세요."</span>
<span class="p">)</span>

<span class="n">workflow</span> <span class="o">=</span> <span class="n">SequentialAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"GatherAndSynthesize"</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">gather</span><span class="p">,</span> <span class="n">synthesizer</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="4-agent--tool-간-컨텍스트-공유">4. Agent ↔ Tool 간 컨텍스트 공유</h2>

<p>도구(Tool)는 에이전트의 실행 환경과 상태에 접근해야 할 때가 많습니다. ADK는 이를 위해 <strong>ToolContext</strong>를 제공합니다.</p>

<h3 id="4-1-toolcontext의-구조">4-1. ToolContext의 구조</h3>

<p><code class="language-plaintext highlighter-rouge">ToolContext</code>는 <code class="language-plaintext highlighter-rouge">CallbackContext</code>를 확장한 것으로, 도구 함수 내에서 세션 상태, 아티팩트, 메모리, 인증 등 프레임워크의 다양한 기능에 접근할 수 있게 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ToolContext (도구 함수에 전달)
├── state (읽기/쓰기 가능)
│   ├── session.state['key'] 읽기
│   └── session.state['key'] 쓰기 → EventActions.state_delta로 자동 추적
├── load_artifact() / save_artifact()
├── list_artifacts()
├── search_memory(query)
├── request_credential() / get_auth_response()
├── function_call_id
└── actions (EventActions 직접 접근)
</code></pre></div></div>

<h3 id="4-2-도구에서-state-읽기쓰기">4-2. 도구에서 State 읽기/쓰기</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.tools</span> <span class="kn">import</span> <span class="n">ToolContext</span>

<span class="k">def</span> <span class="nf">search_database</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="n">ToolContext</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="c1"># State에서 사용자 설정 읽기
</span>    <span class="n">user_lang</span> <span class="o">=</span> <span class="n">tool_context</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"user:preferred_language"</span><span class="p">,</span> <span class="s">"en"</span><span class="p">)</span>
    
    <span class="c1"># 도구 실행 로직
</span>    <span class="n">results</span> <span class="o">=</span> <span class="n">perform_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">language</span><span class="o">=</span><span class="n">user_lang</span><span class="p">)</span>
    
    <span class="c1"># State에 결과 저장 (자동으로 EventActions.state_delta에 추적됨)
</span>    <span class="n">tool_context</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">"last_search_query"</span><span class="p">]</span> <span class="o">=</span> <span class="n">query</span>
    <span class="n">tool_context</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">"temp:search_result_count"</span><span class="p">]</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)</span>
    
    <span class="k">return</span> <span class="p">{</span><span class="s">"results"</span><span class="p">:</span> <span class="n">results</span><span class="p">,</span> <span class="s">"count"</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">results</span><span class="p">)}</span>
</code></pre></div></div>

<p>도구 함수에서 <code class="language-plaintext highlighter-rouge">tool_context.state</code>를 수정하면, ADK 프레임워크가 자동으로 이 변경을 <code class="language-plaintext highlighter-rouge">EventActions.state_delta</code>에 포함시킵니다. 수동으로 <code class="language-plaintext highlighter-rouge">EventActions</code>를 구성할 필요가 없습니다.</p>

<h3 id="4-3-callbackcontext를-통한-콜백에서의-state-접근">4-3. CallbackContext를 통한 콜백에서의 State 접근</h3>

<p>에이전트의 콜백 함수(<code class="language-plaintext highlighter-rouge">before_agent_callback</code>, <code class="language-plaintext highlighter-rouge">after_agent_callback</code> 등)에서도 <code class="language-plaintext highlighter-rouge">CallbackContext</code>를 통해 동일하게 State에 접근할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents.context</span> <span class="kn">import</span> <span class="n">Context</span>
<span class="kn">from</span> <span class="nn">google.adk.models</span> <span class="kn">import</span> <span class="n">LlmRequest</span>
<span class="kn">from</span> <span class="nn">google.genai</span> <span class="kn">import</span> <span class="n">types</span>

<span class="k">def</span> <span class="nf">before_model_callback</span><span class="p">(</span><span class="n">context</span><span class="p">:</span> <span class="n">Context</span><span class="p">,</span> <span class="n">request</span><span class="p">:</span> <span class="n">LlmRequest</span><span class="p">):</span>
    <span class="n">call_count</span> <span class="o">=</span> <span class="n">context</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"model_calls"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
    <span class="n">context</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">"model_calls"</span><span class="p">]</span> <span class="o">=</span> <span class="n">call_count</span> <span class="o">+</span> <span class="mi">1</span>
    
    <span class="c1"># 특정 조건에서 모델 호출을 가로채기
</span>    <span class="k">if</span> <span class="n">context</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"temp:skip_model"</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">types</span><span class="p">.</span><span class="n">Content</span><span class="p">(</span>
            <span class="n">parts</span><span class="o">=</span><span class="p">[</span><span class="n">types</span><span class="p">.</span><span class="n">Part</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="s">"캐시된 응답을 반환합니다."</span><span class="p">)]</span>
        <span class="p">)</span>
    
    <span class="k">return</span> <span class="bp">None</span>  <span class="c1"># 정상적으로 모델 호출 진행
</span></code></pre></div></div>

<h3 id="4-4-도구에서-메모리와-아티팩트-활용">4-4. 도구에서 메모리와 아티팩트 활용</h3>

<p><code class="language-plaintext highlighter-rouge">ToolContext</code>는 State 외에도 메모리 검색과 아티팩트 관리 기능을 제공합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.tools</span> <span class="kn">import</span> <span class="n">ToolContext</span>

<span class="k">async</span> <span class="k">def</span> <span class="nf">intelligent_search</span><span class="p">(</span><span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="n">ToolContext</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="c1"># 과거 세션 메모리에서 관련 정보 검색
</span>    <span class="n">relevant_memories</span> <span class="o">=</span> <span class="k">await</span> <span class="n">tool_context</span><span class="p">.</span><span class="n">search_memory</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="s">와 관련된 과거 대화"</span>
    <span class="p">)</span>
    
    <span class="c1"># 세션 아티팩트 목록 조회
</span>    <span class="n">artifacts</span> <span class="o">=</span> <span class="k">await</span> <span class="n">tool_context</span><span class="p">.</span><span class="n">list_artifacts</span><span class="p">()</span>
    
    <span class="c1"># 특정 아티팩트 로드
</span>    <span class="n">config</span> <span class="o">=</span> <span class="k">await</span> <span class="n">tool_context</span><span class="p">.</span><span class="n">load_artifact</span><span class="p">(</span><span class="s">"search_config.json"</span><span class="p">)</span>
    
    <span class="c1"># 결과를 아티팩트로 저장
</span>    <span class="k">await</span> <span class="n">tool_context</span><span class="p">.</span><span class="n">save_artifact</span><span class="p">(</span>
        <span class="s">"search_results.json"</span><span class="p">,</span>
        <span class="n">types</span><span class="p">.</span><span class="n">Part</span><span class="p">(</span><span class="n">text</span><span class="o">=</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">results</span><span class="p">))</span>
    <span class="p">)</span>
    
    <span class="k">return</span> <span class="p">{</span><span class="s">"results"</span><span class="p">:</span> <span class="n">results</span><span class="p">,</span> <span class="s">"memory_context"</span><span class="p">:</span> <span class="n">relevant_memories</span><span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="5-escalation--제어를-상위-또는-다른-에이전트로-되돌리기">5. Escalation — 제어를 상위 또는 다른 에이전트로 되돌리기</h2>

<p>Escalation은 서브에이전트가 작업을 완료했거나, 자신이 처리할 수 없는 상황에서 <strong>상위 에이전트나 다른 에이전트에게 제어를 반환</strong>하는 메커니즘입니다.</p>

<h3 id="5-1-eventactionsescalate--loopagent-탈출">5-1. EventActions.escalate — LoopAgent 탈출</h3>

<p><code class="language-plaintext highlighter-rouge">LoopAgent</code> 안에서 서브에이전트가 <code class="language-plaintext highlighter-rouge">escalate=True</code>를 설정한 이벤트를 발생시키면, 루프가 즉시 종료됩니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="n">LoopAgent</span><span class="p">,</span> <span class="n">LlmAgent</span><span class="p">,</span> <span class="n">BaseAgent</span>
<span class="kn">from</span> <span class="nn">google.adk.events</span> <span class="kn">import</span> <span class="n">Event</span><span class="p">,</span> <span class="n">EventActions</span>
<span class="kn">from</span> <span class="nn">google.adk.agents.invocation_context</span> <span class="kn">import</span> <span class="n">InvocationContext</span>

<span class="k">class</span> <span class="nc">QualityGate</span><span class="p">(</span><span class="n">BaseAgent</span><span class="p">):</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">_run_async_impl</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ctx</span><span class="p">:</span> <span class="n">InvocationContext</span><span class="p">):</span>
        <span class="n">status</span> <span class="o">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"quality_status"</span><span class="p">,</span> <span class="s">"fail"</span><span class="p">)</span>
        <span class="n">should_stop</span> <span class="o">=</span> <span class="p">(</span><span class="n">status</span> <span class="o">==</span> <span class="s">"pass"</span><span class="p">)</span>
        <span class="k">yield</span> <span class="n">Event</span><span class="p">(</span>
            <span class="n">author</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
            <span class="n">actions</span><span class="o">=</span><span class="n">EventActions</span><span class="p">(</span><span class="n">escalate</span><span class="o">=</span><span class="n">should_stop</span><span class="p">)</span>
        <span class="p">)</span>

<span class="n">code_refiner</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"CodeRefiner"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"코드를 개선하세요. 현재 코드: {current_code}"</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"current_code"</span>
<span class="p">)</span>

<span class="n">quality_checker</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"QualityChecker"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"{current_code}의 품질을 평가하세요. 'pass' 또는 'fail'로 답하세요."</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"quality_status"</span>
<span class="p">)</span>

<span class="n">refinement_loop</span> <span class="o">=</span> <span class="n">LoopAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"RefinementLoop"</span><span class="p">,</span>
    <span class="n">max_iterations</span><span class="o">=</span><span class="mi">5</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">code_refiner</span><span class="p">,</span> <span class="n">quality_checker</span><span class="p">,</span> <span class="n">QualityGate</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"Gate"</span><span class="p">)]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>동작 흐름:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[반복 1] CodeRefiner → QualityChecker → Gate(escalate=False) → 계속
[반복 2] CodeRefiner → QualityChecker → Gate(escalate=False) → 계속
[반복 3] CodeRefiner → QualityChecker(pass!) → Gate(escalate=True) → 루프 종료
</code></pre></div></div>

<h3 id="5-2-transfer_to_agent--부모형제-에이전트로-전환">5-2. transfer_to_agent — 부모/형제 에이전트로 전환</h3>

<p>서브에이전트가 <code class="language-plaintext highlighter-rouge">transfer_to_agent</code>를 호출하여 부모 에이전트나 형제 에이전트에게 제어를 넘길 수 있습니다. 이는 LLM이 자연어 이해를 기반으로 동적으로 결정합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">billing_agent</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Billing"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"결제 관련 문의를 처리합니다."</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"""결제 관련 질문에 답하세요.
    기술 지원이 필요한 질문이면 Support 에이전트로 전환하세요.
    일반적인 문의면 HelpDesk(부모)로 전환하세요."""</span>
<span class="p">)</span>

<span class="n">support_agent</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"Support"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"기술 지원을 제공합니다."</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"""기술 문제를 해결하세요.
    결제 관련 문의면 Billing 에이전트로 전환하세요."""</span>
<span class="p">)</span>

<span class="n">coordinator</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"HelpDesk"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"사용자 요청을 분석해 적절한 에이전트에게 전달하세요."</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">billing_agent</span><span class="p">,</span> <span class="n">support_agent</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>전환 가능한 범위:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>HelpDesk (부모)
├── Billing ←→ Support (형제 간 전환 가능)
│   └── Billing → HelpDesk (자식 → 부모 전환 가능)
└── Support → HelpDesk (자식 → 부모 전환 가능)
</code></pre></div></div>

<p>ADK에서는 <code class="language-plaintext highlighter-rouge">disallow_transfer_to_parent</code>와 <code class="language-plaintext highlighter-rouge">disallow_transfer_to_peers</code> 옵션으로 전환 범위를 세밀하게 제어할 수 있습니다.</p>

<h3 id="5-3-fallback_to_parent--자동-부모-복귀">5-3. fallback_to_parent — 자동 부모 복귀</h3>

<p>ADK에 추가된 <code class="language-plaintext highlighter-rouge">fallback_to_parent</code> 기능은 서브에이전트가 작업을 완료한 후 <strong>자동으로 부모 에이전트에게 제어를 반환</strong>합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">specialist</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"DataAnalyst"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"데이터 분석 전문가입니다."</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"요청된 데이터 분석을 수행하세요."</span><span class="p">,</span>
    <span class="n">fallback_to_parent</span><span class="o">=</span><span class="bp">True</span>  <span class="c1"># 작업 완료 후 자동으로 부모에게 복귀
</span><span class="p">)</span>

<span class="n">coordinator</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"ProjectManager"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"프로젝트 관리를 담당합니다. 데이터 분석이 필요하면 DataAnalyst에게 위임하세요."</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">specialist</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fallback_to_parent=True</code>가 동작하는 조건:</p>

<ol>
  <li>해당 에이전트가 <code class="language-plaintext highlighter-rouge">LlmAgent</code> 인스턴스일 것</li>
  <li><code class="language-plaintext highlighter-rouge">fallback_to_parent=True</code>로 설정되어 있을 것</li>
  <li>부모 에이전트가 존재할 것</li>
  <li>모델 응답에 <strong>명시적 <code class="language-plaintext highlighter-rouge">transfer_to_agent</code> 호출이 없을</strong> 것</li>
</ol>

<p>이 네 가지 조건이 모두 충족되면, 에이전트는 실행을 마친 후 자동으로 <code class="language-plaintext highlighter-rouge">transfer_to_agent(parent_name)</code>을 생성하여 부모에게 돌아갑니다.</p>

<h3 id="5-4-escalation-패턴-비교">5-4. Escalation 패턴 비교</h3>

<table>
  <thead>
    <tr>
      <th>메커니즘</th>
      <th>트리거</th>
      <th>대상</th>
      <th>주요 사용처</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">escalate=True</code></td>
      <td>커스텀 에이전트가 이벤트에 설정</td>
      <td>LoopAgent → 루프 탈출</td>
      <td>반복 작업의 종료 조건</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">transfer_to_agent</code></td>
      <td>LLM이 동적으로 판단</td>
      <td>부모/형제/자식 에이전트</td>
      <td>대화 라우팅, 작업 위임</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fallback_to_parent</code></td>
      <td>자동 (응답 완료 시)</td>
      <td>부모 에이전트</td>
      <td>단일 작업 위임 후 자동 복귀</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AgentTool</code> 반환</td>
      <td>도구 실행 완료 시</td>
      <td>호출한 에이전트</td>
      <td>부분 작업 위임 (도구 패턴)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="6-실전-종합-예제-고객-지원-시스템">6. 실전 종합 예제: 고객 지원 시스템</h2>

<p>지금까지 다룬 모든 메커니즘을 결합한 고객 지원 멀티에이전트 시스템 예제입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.adk.agents</span> <span class="kn">import</span> <span class="p">(</span>
    <span class="n">LlmAgent</span><span class="p">,</span> <span class="n">SequentialAgent</span><span class="p">,</span> <span class="n">ParallelAgent</span><span class="p">,</span> <span class="n">LoopAgent</span><span class="p">,</span> <span class="n">BaseAgent</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">google.adk.tools</span> <span class="kn">import</span> <span class="n">agent_tool</span><span class="p">,</span> <span class="n">ToolContext</span>
<span class="kn">from</span> <span class="nn">google.adk.events</span> <span class="kn">import</span> <span class="n">Event</span><span class="p">,</span> <span class="n">EventActions</span>


<span class="c1"># === 도구 정의: ToolContext를 통한 상태 접근 ===
</span>
<span class="k">def</span> <span class="nf">lookup_customer</span><span class="p">(</span><span class="n">customer_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="n">ToolContext</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="s">"""고객 정보 조회 도구 — State를 통해 결과를 공유"""</span>
    <span class="n">customer</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">get_customer</span><span class="p">(</span><span class="n">customer_id</span><span class="p">)</span>
    <span class="n">tool_context</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">"user:customer_tier"</span><span class="p">]</span> <span class="o">=</span> <span class="n">customer</span><span class="p">[</span><span class="s">"tier"</span><span class="p">]</span>
    <span class="n">tool_context</span><span class="p">.</span><span class="n">state</span><span class="p">[</span><span class="s">"temp:customer_name"</span><span class="p">]</span> <span class="o">=</span> <span class="n">customer</span><span class="p">[</span><span class="s">"name"</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">customer</span>

<span class="k">def</span> <span class="nf">check_order_status</span><span class="p">(</span><span class="n">order_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">tool_context</span><span class="p">:</span> <span class="n">ToolContext</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="s">"""주문 상태 확인 — 메모리 검색으로 과거 맥락 활용"""</span>
    <span class="n">past_context</span> <span class="o">=</span> <span class="n">tool_context</span><span class="p">.</span><span class="n">search_memory</span><span class="p">(</span><span class="sa">f</span><span class="s">"주문 </span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s"> 관련 이력"</span><span class="p">)</span>
    <span class="n">status</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">get_order</span><span class="p">(</span><span class="n">order_id</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"status"</span><span class="p">:</span> <span class="n">status</span><span class="p">,</span> <span class="s">"history"</span><span class="p">:</span> <span class="n">past_context</span><span class="p">}</span>


<span class="c1"># === 전문 서브에이전트 ===
</span>
<span class="n">billing_specialist</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"BillingSpecialist"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"결제, 환불, 청구서 관련 문제를 처리합니다."</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"""결제 관련 문제를 해결하세요.
    고객 등급은 {user:customer_tier}입니다.
    기술 문제라면 TechSupport로 전환하세요.
    해결 완료되면 요약을 작성하세요."""</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"resolution_summary"</span><span class="p">,</span>
    <span class="n">fallback_to_parent</span><span class="o">=</span><span class="bp">True</span>  <span class="c1"># 완료 후 자동 복귀
</span><span class="p">)</span>

<span class="n">tech_support</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"TechSupport"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"기술 지원과 계정 접근 문제를 처리합니다."</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"""기술 문제를 해결하세요.
    결제 문제라면 BillingSpecialist로 전환하세요."""</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"resolution_summary"</span><span class="p">,</span>
    <span class="n">fallback_to_parent</span><span class="o">=</span><span class="bp">True</span>
<span class="p">)</span>


<span class="c1"># === 품질 검증 루프 ===
</span>
<span class="k">class</span> <span class="nc">ResolutionValidator</span><span class="p">(</span><span class="n">BaseAgent</span><span class="p">):</span>
    <span class="s">"""해결 결과 검증 — escalate로 루프 탈출"""</span>
    <span class="k">async</span> <span class="k">def</span> <span class="nf">_run_async_impl</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">ctx</span><span class="p">):</span>
        <span class="n">score</span> <span class="o">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">session</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"satisfaction_score"</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
        <span class="k">yield</span> <span class="n">Event</span><span class="p">(</span>
            <span class="n">author</span><span class="o">=</span><span class="bp">self</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
            <span class="n">actions</span><span class="o">=</span><span class="n">EventActions</span><span class="p">(</span><span class="n">escalate</span><span class="o">=</span><span class="p">(</span><span class="n">score</span> <span class="o">&gt;=</span> <span class="mi">4</span><span class="p">))</span>
        <span class="p">)</span>

<span class="n">satisfaction_check</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"SatisfactionChecker"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"""고객 만족도를 1-5점으로 평가하세요.
    해결 내용: {resolution_summary}"""</span><span class="p">,</span>
    <span class="n">output_key</span><span class="o">=</span><span class="s">"satisfaction_score"</span>
<span class="p">)</span>

<span class="n">quality_loop</span> <span class="o">=</span> <span class="n">LoopAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"QualityAssurance"</span><span class="p">,</span>
    <span class="n">max_iterations</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">satisfaction_check</span><span class="p">,</span> <span class="n">ResolutionValidator</span><span class="p">(</span><span class="n">name</span><span class="o">=</span><span class="s">"Validator"</span><span class="p">)]</span>
<span class="p">)</span>


<span class="c1"># === 루트 에이전트: 전체 오케스트레이션 ===
</span>
<span class="n">root_agent</span> <span class="o">=</span> <span class="n">LlmAgent</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"CustomerServiceHub"</span><span class="p">,</span>
    <span class="n">model</span><span class="o">=</span><span class="s">"gemini-2.0-flash"</span><span class="p">,</span>
    <span class="n">instruction</span><span class="o">=</span><span class="s">"""고객 지원 허브입니다.
    1. 먼저 고객 정보를 조회하세요.
    2. 문제 유형에 따라 적절한 전문가에게 전달하세요.
    3. 해결 후 품질을 검증하세요."""</span><span class="p">,</span>
    <span class="n">tools</span><span class="o">=</span><span class="p">[</span><span class="n">lookup_customer</span><span class="p">,</span> <span class="n">check_order_status</span><span class="p">],</span>
    <span class="n">sub_agents</span><span class="o">=</span><span class="p">[</span><span class="n">billing_specialist</span><span class="p">,</span> <span class="n">tech_support</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<p>이 시스템의 데이터 흐름:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>사용자: "주문 #1234 환불 요청합니다"
    │
    ▼
[CustomerServiceHub]
    ├── lookup_customer() → tool_context.state에 고객 정보 저장
    ├── transfer_to_agent('BillingSpecialist')
    │
    ▼
[BillingSpecialist]
    ├── {user:customer_tier}로 고객 등급 참조 (State 공유)
    ├── 환불 처리
    ├── output_key="resolution_summary"로 결과 저장
    └── fallback_to_parent → CustomerServiceHub로 자동 복귀
    │
    ▼
[CustomerServiceHub]
    └── resolution_summary를 확인하고 최종 응답
</code></pre></div></div>

<hr />

<h2 id="7-컨텍스트-공유-전략-요약">7. 컨텍스트 공유 전략 요약</h2>

<h3 id="메커니즘별-비교표">메커니즘별 비교표</h3>

<table>
  <thead>
    <tr>
      <th>메커니즘</th>
      <th>방향</th>
      <th>데이터 유형</th>
      <th>지속성</th>
      <th>사용 난이도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">session.state</code> (기본)</td>
      <td>양방향</td>
      <td>직렬화 가능한 모든 타입</td>
      <td>세션 내</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">session.state</code> (<code class="language-plaintext highlighter-rouge">user:</code>)</td>
      <td>양방향</td>
      <td>직렬화 가능한 모든 타입</td>
      <td>사용자 전체 세션</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">session.state</code> (<code class="language-plaintext highlighter-rouge">temp:</code>)</td>
      <td>양방향</td>
      <td>직렬화 가능한 모든 타입</td>
      <td>단일 호출 내</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">output_key</code></td>
      <td>단방향 (쓰기)</td>
      <td>텍스트</td>
      <td>세션 내</td>
      <td>매우 낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{key}</code> 템플릿</td>
      <td>단방향 (읽기)</td>
      <td>문자열 변환 가능</td>
      <td>-</td>
      <td>매우 낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ToolContext.state</code></td>
      <td>양방향</td>
      <td>직렬화 가능한 모든 타입</td>
      <td>State 키에 따름</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CallbackContext.state</code></td>
      <td>양방향</td>
      <td>직렬화 가능한 모든 타입</td>
      <td>State 키에 따름</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AgentTool</code> 반환값</td>
      <td>단방향 (결과)</td>
      <td>모델 응답</td>
      <td>-</td>
      <td>중간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">search_memory()</code></td>
      <td>단방향 (읽기)</td>
      <td>검색 결과</td>
      <td>장기</td>
      <td>중간</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Artifact</code></td>
      <td>양방향</td>
      <td>파일/바이너리</td>
      <td>세션 내</td>
      <td>중간</td>
    </tr>
  </tbody>
</table>

<h3 id="설계-원칙">설계 원칙</h3>

<ol>
  <li><strong>State 키 네이밍 컨벤션을 정하세요.</strong> 에이전트 간 공유하는 키는 문서화하고, 접두사로 스코프를 명확히 하세요.</li>
  <li><strong>ParallelAgent에서는 고유 키를 사용하세요.</strong> 병렬 실행 시 같은 키에 쓰면 경합 조건이 발생합니다.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">temp:</code>는 호출 체인 내에서만 사용하세요.</strong> 다음 사용자 입력까지 데이터를 유지하려면 기본 키나 <code class="language-plaintext highlighter-rouge">user:</code> 접두사를 쓰세요.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">fallback_to_parent</code>로 자동 복귀를 보장하세요.</strong> 단일 작업을 위임하는 서브에이전트에 설정하면, 제어 흐름이 예측 가능해집니다.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">ToolContext</code>에서 State 변경을 추적하세요.</strong> <code class="language-plaintext highlighter-rouge">session.state</code>를 직접 수정하지 말고, 항상 Context 객체를 통해 수정해야 변경이 올바르게 추적됩니다.</li>
</ol>

<hr />

<h2 id="참고-자료">참고 자료</h2>

<ul>
  <li><a href="https://google.github.io/adk-docs/agents/multi-agents/">ADK 공식 문서 - Multi-Agent Systems</a></li>
  <li><a href="https://google.github.io/adk-docs/context/">ADK 공식 문서 - Context</a></li>
  <li><a href="https://google.github.io/adk-docs/sessions/state/">ADK 공식 문서 - State</a></li>
  <li><a href="https://google.github.io/adk-docs/sessions/">ADK 공식 문서 - Sessions</a></li>
  <li><a href="https://google.github.io/adk-docs/tools-custom/">ADK 공식 문서 - Custom Tools</a></li>
  <li><a href="https://cloud.google.com/blog/topics/developers-practitioners/remember-this-agent-state-and-memory-with-adk">Google Cloud Blog - Agent state and memory with ADK</a></li>
  <li><a href="https://developers.googleblog.com/developers-guide-to-multi-agent-patterns-in-adk/">Google Developers Blog - Developer’s guide to multi-agent patterns in ADK</a></li>
  <li><a href="https://github.com/google/adk-python/pull/2253">ADK GitHub - fallback_to_parent PR #2253</a></li>
</ul>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="adk" /><category term="google" /><category term="multi-agent" /><category term="context" /><category term="escalation" /><category term="session" /><category term="state" /><category term="tool" /><summary type="html"><![CDATA[Google의 Agent Development Kit(ADK)으로 멀티에이전트 시스템을 구축할 때, 가장 핵심적인 질문 중 하나는 “에이전트들이 어떻게 정보를 공유하고, 처리할 수 없는 작업을 다른 에이전트에게 넘기는가?”입니다.]]></summary></entry><entry><title type="html">BigQuery 테이블 유형 완전 가이드: Native부터 External까지, 메타스토어 관리 방식별 총정리</title><link href="https://seonghak.com/blog/2026/03/24/bigquery-all-table-types-metastore-guide/" rel="alternate" type="text/html" title="BigQuery 테이블 유형 완전 가이드: Native부터 External까지, 메타스토어 관리 방식별 총정리" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/bigquery-all-table-types-metastore-guide</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/bigquery-all-table-types-metastore-guide/"><![CDATA[<p>BigQuery에서 데이터를 다루는 방법은 크게 <strong>Native Table</strong>(BigQuery 내부 저장)과 <strong>External Table</strong>(GCS 등 외부 저장)로 나뉩니다. 특히 External Table은 메타스토어를 누가, 어떻게 관리하느냐에 따라 6가지 이상의 방식이 존재하며, 각각 기능·성능·운영 부담이 다릅니다.</p>

<p>이 글에서는 BigQuery에서 사용할 수 있는 <strong>모든 테이블 유형</strong>을 나열하고, 각 방식의 구조·특징·장단점을 비교합니다.</p>

<hr />

<h2 id="전체-구조-한눈에-보기">전체 구조 한눈에 보기</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BigQuery에서 데이터를 사용하는 방법
│
├── 1. Native Table (BigQuery 내부 저장)
│
├── 2. External Table (GCS 파일 참조)
│   │
│   ├── 메타스토어 없음 (BigQuery 카탈로그만)
│   │   ├── 2-A. 기본 External Table
│   │   └── 2-B. BigLake External Table (flat files)
│   │
│   ├── BigQuery가 메타스토어 관리
│   │   └── 2-C. BigLake Managed Iceberg Table
│   │
│   ├── GCS 파일 자체가 메타스토어 (자체 관리)
│   │   └── 2-D. BigLake External Table (Iceberg / Delta Lake / Hudi)
│   │
│   ├── GCP 관리형 메타스토어
│   │   ├── 2-E. BigLake Metastore (REST Catalog)
│   │   └── 2-F. Dataproc Metastore (Hive Metastore Service)
│   │
│   └── 자체 호스팅 메타스토어
│       └── 2-G. Self-hosted Hive Metastore
│
└── 3. Object Table (비정형 데이터)
</code></pre></div></div>

<hr />

<h2 id="1-bigquery-native-table">1. BigQuery Native Table</h2>

<p><strong>데이터 위치:</strong> BigQuery 내부 (Capacitor 포맷)
<strong>메타스토어:</strong> BigQuery 카탈로그 (완전 관리형)</p>

<p>BigQuery의 기본이자 가장 성능이 좋은 방식입니다. 데이터가 BigQuery 내부 스토리지에 Capacitor라는 독자 컬럼형 포맷으로 저장됩니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">sales</span> <span class="p">(</span>
  <span class="n">order_id</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">customer_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">amount</span> <span class="nb">NUMERIC</span><span class="p">,</span>
  <span class="n">order_date</span> <span class="nb">DATE</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">order_date</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">customer_id</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="핵심-기능">핵심 기능</h3>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>지원 여부</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>DML (INSERT/UPDATE/DELETE/MERGE)</td>
      <td>완전 지원</td>
    </tr>
    <tr>
      <td>Streaming Insert</td>
      <td>지원 (Storage Write API)</td>
    </tr>
    <tr>
      <td>파티셔닝</td>
      <td>지원 (시간/범위/정수)</td>
    </tr>
    <tr>
      <td>클러스터링</td>
      <td>지원</td>
    </tr>
    <tr>
      <td>Time Travel</td>
      <td>최대 7일</td>
    </tr>
    <tr>
      <td>Fail-safe</td>
      <td>추가 7일 (삭제 복구)</td>
    </tr>
    <tr>
      <td>Row/Column 수준 보안</td>
      <td>지원</td>
    </tr>
    <tr>
      <td>캐싱</td>
      <td>자동 (쿼리 결과 캐시)</td>
    </tr>
    <tr>
      <td>Materialized View</td>
      <td>지원</td>
    </tr>
  </tbody>
</table>

<h3 id="장점">장점</h3>

<ul>
  <li><strong>최고 성능</strong>: BigQuery 엔진에 최적화된 Capacitor 포맷, 자동 파티션 pruning과 클러스터링</li>
  <li><strong>완전 관리형</strong>: 인프라 관리 불필요, 자동 스토리지 최적화</li>
  <li><strong>풍부한 기능</strong>: Time Travel, 스냅샷, 복제, CDC 등 엔터프라이즈 기능 전체 사용 가능</li>
  <li><strong>비용 모델 단순</strong>: 스토리지 비용 + 쿼리 비용 (on-demand 또는 슬롯 기반)</li>
</ul>

<h3 id="단점">단점</h3>

<ul>
  <li><strong>데이터 이동 필수</strong>: GCS 등 외부에 있는 데이터를 BigQuery로 로드해야 함</li>
  <li><strong>독점 포맷</strong>: Capacitor는 BigQuery 전용이라 Spark 등 다른 엔진에서 직접 읽을 수 없음</li>
  <li><strong>스토리지 비용 이중화</strong>: 원본 소스와 BigQuery 양쪽에 데이터가 존재하면 비용 증가</li>
  <li><strong>대용량 로드 시간</strong>: 초기 데이터 적재에 시간 소요</li>
</ul>

<h3 id="적합한-경우">적합한 경우</h3>

<ul>
  <li>BigQuery가 <strong>유일한 분석 엔진</strong>인 환경</li>
  <li>최고 쿼리 성능과 SLA가 필요한 프로덕션 대시보드</li>
  <li>DML이 빈번한 워크로드 (실시간 업데이트, CDC)</li>
  <li>데이터가 이미 BigQuery에 있거나, 적재 파이프라인이 확립된 환경</li>
</ul>

<hr />

<h2 id="2-a-기본-external-table-메타스토어-없음">2-A. 기본 External Table (메타스토어 없음)</h2>

<p><strong>데이터 위치:</strong> GCS
<strong>메타스토어:</strong> 없음 (BigQuery 카탈로그에 스키마만 등록)
<strong>BigLake Connection:</strong> 사용하지 않음</p>

<p>가장 단순한 External Table입니다. GCS의 파일을 직접 URI로 참조하며, BigLake Connection 없이 사용자의 GCS 권한으로 직접 접근합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">ext_sales</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'PARQUET'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'gs://my-bucket/sales/*.parquet'</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>

<p>CSV, JSON(newline-delimited), Avro, Parquet, ORC 등 다양한 파일 포맷을 지원합니다.</p>

<h3 id="장점-1">장점</h3>

<ul>
  <li><strong>설정 최소화</strong>: URI만 지정하면 바로 쿼리 가능</li>
  <li><strong>데이터 이동 없음</strong>: GCS에 있는 파일을 그대로 사용</li>
  <li><strong>비용 절감</strong>: BigQuery 스토리지 비용 없음 (쿼리 비용만 발생)</li>
  <li><strong>ETL 파이프라인 간소화</strong>: 별도 로드 단계 불필요</li>
</ul>

<h3 id="단점-1">단점</h3>

<ul>
  <li><strong>성능 제한</strong>: 매 쿼리마다 GCS에서 파일을 읽으므로 Native 대비 느림</li>
  <li><strong>DML 불가</strong>: 읽기 전용 (INSERT/UPDATE/DELETE 불가)</li>
  <li><strong>메타데이터 캐싱 없음</strong>: 쿼리마다 파일 목록을 다시 탐색</li>
  <li><strong>보안 제한</strong>: Row/Column 수준 보안 미지원</li>
  <li><strong>권한 관리 복잡</strong>: 쿼리 실행자가 GCS 버킷에 직접 권한이 있어야 함</li>
  <li><strong>파일 관리 부담</strong>: 파일 추가/삭제 시 URI 패턴에 맞춰야 함</li>
  <li><strong>Hive 파티션 탐색 제한</strong>: <code class="language-plaintext highlighter-rouge">_FILE_NAME</code> 가상 컬럼을 사용하거나 Hive 파티셔닝 옵션을 별도로 설정해야 함</li>
</ul>

<h3 id="적합한-경우-1">적합한 경우</h3>

<ul>
  <li>빠른 PoC나 일회성 분석</li>
  <li>소규모 데이터셋 (수 GB 이하)</li>
  <li>데이터 파이프라인 초기 단계에서 빠르게 테스트</li>
</ul>

<hr />

<h2 id="2-b-biglake-external-table-flat-files">2-B. BigLake External Table (Flat Files)</h2>

<p><strong>데이터 위치:</strong> GCS
<strong>메타스토어:</strong> 없음 (BigQuery 카탈로그에 스키마만 등록)
<strong>BigLake Connection:</strong> 사용</p>

<p>기본 External Table에 BigLake Connection을 추가한 형태입니다. 구조는 동일하지만, <strong>접근 위임(Access Delegation)</strong>이 핵심 차이입니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- BigLake Connection 생성 (1회)</span>
<span class="c1">-- Console: BigQuery → External Connections → Cloud Resource Connection</span>

<span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">bl_sales</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'PARQUET'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'gs://my-bucket/sales/*.parquet'</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>

<p>BigLake Connection의 서비스 계정이 GCS에 접근하므로, 개별 사용자에게 GCS 권한을 부여할 필요가 없습니다.</p>

<h3 id="2-a-대비-추가-장점">2-A 대비 추가 장점</h3>

<ul>
  <li><strong>접근 위임</strong>: 사용자에게 GCS 직접 권한 불필요 → 데이터 유출 위험 감소</li>
  <li><strong>Row/Column 수준 보안</strong>: 정책 태그와 행 수준 보안 필터 적용 가능</li>
  <li><strong>메타데이터 캐싱</strong>: BigQuery가 파일 목록과 스키마를 캐싱하여 쿼리 계획 시간 단축</li>
  <li><strong>BigQuery 옵티마이저 통합</strong>: 더 나은 쿼리 최적화 가능</li>
  <li><strong>통합 거버넌스</strong>: Data Catalog, DLP 등과 연계</li>
</ul>

<h3 id="단점-2">단점</h3>

<ul>
  <li><strong>여전히 읽기 전용</strong>: DML 미지원</li>
  <li><strong>BigLake Connection 설정 필요</strong>: 초기 설정이 기본 External Table보다 복잡</li>
  <li><strong>쿼리 성능</strong>: Native Table 대비 여전히 느림 (GCS I/O)</li>
  <li><strong>메타데이터 캐시 갱신</strong>: 파일이 자주 변경되면 캐시 무효화 전략 필요</li>
</ul>

<h3 id="적합한-경우-2">적합한 경우</h3>

<ul>
  <li><strong>프로덕션 환경</strong>에서 GCS 데이터를 BigQuery로 조회해야 하는 경우</li>
  <li>데이터 거버넌스와 보안이 중요한 조직</li>
  <li>다수의 사용자가 동일 GCS 데이터를 조회하는 환경</li>
</ul>

<hr />

<h2 id="2-c-biglake-managed-iceberg-table">2-C. BigLake Managed Iceberg Table</h2>

<p><strong>데이터 위치:</strong> GCS (Parquet)
<strong>메타스토어:</strong> BigQuery 내부 (Big Metadata)
<strong>BigLake Connection:</strong> 사용</p>

<p>BigQuery가 Iceberg 메타데이터를 <strong>내부적으로 완전 관리</strong>하는 방식입니다. 사용자 입장에서는 Native Table과 거의 동일한 경험을 제공하면서, 데이터는 고객 소유의 GCS 버킷에 열린 포맷(Parquet)으로 저장됩니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">managed_sales</span> <span class="p">(</span>
  <span class="n">order_id</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">customer_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">amount</span> <span class="nb">NUMERIC</span><span class="p">,</span>
  <span class="n">order_date</span> <span class="nb">DATE</span>
<span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">customer_id</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">file_format</span> <span class="o">=</span> <span class="s1">'PARQUET'</span><span class="p">,</span>
  <span class="n">table_format</span> <span class="o">=</span> <span class="s1">'ICEBERG'</span><span class="p">,</span>
  <span class="n">storage_uri</span> <span class="o">=</span> <span class="s1">'gs://my-bucket/managed-sales'</span>
<span class="p">);</span>

<span class="c1">-- Native Table과 동일하게 DML 사용 가능</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">managed_sales</span>
<span class="k">VALUES</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="s1">'C001'</span><span class="p">,</span> <span class="mi">150</span><span class="p">.</span><span class="mi">00</span><span class="p">,</span> <span class="s1">'2026-01-15'</span><span class="p">);</span>

<span class="k">UPDATE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">managed_sales</span>
<span class="k">SET</span> <span class="n">amount</span> <span class="o">=</span> <span class="mi">200</span><span class="p">.</span><span class="mi">00</span>
<span class="k">WHERE</span> <span class="n">order_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="장점-2">장점</h3>

<ul>
  <li><strong>Native Table 수준의 사용성</strong>: DML, Streaming, Time Travel 전부 지원</li>
  <li><strong>열린 포맷</strong>: 데이터가 GCS에 Parquet으로 저장되어 벤더 종속 최소화</li>
  <li><strong>자동 최적화</strong>: 파일 크기 최적화, 클러스터링, 메타데이터 컴팩션, 고아 파일 GC 자동 수행</li>
  <li><strong>Row/Column 보안</strong>: 완전 지원</li>
  <li><strong>외부 엔진 접근 가능</strong>: <code class="language-plaintext highlighter-rouge">EXPORT TABLE METADATA</code>로 Iceberg 메타데이터를 내보내면 Spark 등에서 읽기 가능</li>
</ul>

<h3 id="단점-3">단점</h3>

<ul>
  <li><strong>외부 엔진 읽기가 즉시적이지 않음</strong>: Spark에서 읽으려면 <code class="language-plaintext highlighter-rouge">EXPORT TABLE METADATA</code> 실행 필요</li>
  <li><strong>외부 엔진 쓰기 불가</strong>: BigQuery만 쓰기 가능, Spark에서 직접 INSERT 불가</li>
  <li><strong>BigQuery 종속</strong>: 메타데이터가 BigQuery 내부에 있으므로 BigQuery 없이는 테이블 관리 불가</li>
  <li><strong>BigLake Connection 필수</strong>: 초기 설정 비용</li>
</ul>

<h3 id="적합한-경우-3">적합한 경우</h3>

<ul>
  <li>BigQuery가 <strong>주 분석 엔진</strong>이면서, 가끔 Spark 등에서 데이터를 읽어야 하는 경우</li>
  <li>Native Table의 기능이 필요하면서 데이터를 열린 포맷으로 유지하고 싶은 경우</li>
  <li>벤더 종속을 줄이면서도 BigQuery의 편의성을 포기하고 싶지 않은 경우</li>
</ul>

<hr />

<h2 id="2-d-biglake-external-table-open-table-format">2-D. BigLake External Table (Open Table Format)</h2>

<p><strong>데이터 위치:</strong> GCS (Parquet)
<strong>메타스토어:</strong> GCS의 메타데이터 파일 (자체 관리)
<strong>BigLake Connection:</strong> 사용</p>

<p>Spark, Flink 등 외부 엔진이 GCS에 직접 Open Table Format(Iceberg, Delta Lake, Hudi)으로 데이터를 쓰고, BigQuery에서 이를 읽는 방식입니다. 메타데이터 파일이 GCS에 존재하며, <strong>데이터를 쓰는 엔진이 메타스토어를 직접 관리</strong>합니다.</p>

<h3 id="iceberg">Iceberg</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">ext_iceberg_sales</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'ICEBERG'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="nv">"gs://my-bucket/warehouse/sales/metadata/v3.metadata.json"</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Spark 등이 GCS에 Iceberg 표준 메타데이터(<code class="language-plaintext highlighter-rouge">metadata.json</code>, manifest list, manifest file)를 직접 관리합니다. BigQuery는 이 메타데이터를 읽어서 쿼리합니다.</p>

<h3 id="delta-lake">Delta Lake</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">ext_delta_sales</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'DELTA_LAKE'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="nv">"gs://my-bucket/delta/sales"</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Delta Lake의 트랜잭션 로그(<code class="language-plaintext highlighter-rouge">_delta_log/</code>)를 BigQuery가 직접 해석합니다.</p>

<h3 id="apache-hudi">Apache Hudi</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">ext_hudi_sales</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'HUDI'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="nv">"gs://my-bucket/hudi/sales"</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Hudi 테이블은 manifest 파일 기반으로 BigQuery에서 조회할 수 있습니다.</p>

<h3 id="장점-3">장점</h3>

<ul>
  <li><strong>외부 엔진 주도</strong>: Spark/Flink가 자유롭게 데이터를 쓰고, BigQuery에서 즉시 조회</li>
  <li><strong>추가 인프라 불필요</strong>: GCS만 있으면 되므로 별도 메타스토어 서비스 없음</li>
  <li><strong>오픈 포맷 호환</strong>: 표준 Iceberg/Delta/Hudi 포맷이라 다양한 엔진에서 접근 가능</li>
  <li><strong>비용 효율</strong>: 메타스토어 서비스 비용 없음</li>
</ul>

<h3 id="단점-4">단점</h3>

<ul>
  <li><strong>BigQuery에서 읽기 전용</strong>: BigQuery로는 DML 불가</li>
  <li><strong>메타데이터 수동 관리</strong>: Iceberg의 경우 <code class="language-plaintext highlighter-rouge">metadata.json</code> URI를 직접 관리해야 하고, 스냅샷이 업데이트될 때마다 URI를 갱신해야 할 수 있음</li>
  <li><strong>스키마 진화 복잡</strong>: BigQuery 외부 테이블 정의와 실제 메타데이터 간 불일치 가능</li>
  <li><strong>컴팩션 직접 수행</strong>: small files 문제를 Spark 등에서 직접 해결해야 함</li>
  <li><strong>동시성 제어 제한</strong>: 여러 엔진이 동시에 쓰면 충돌 가능 (특히 Catalog 없이 사용 시)</li>
</ul>

<h3 id="open-table-format-비교">Open Table Format 비교</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Iceberg</th>
      <th>Delta Lake</th>
      <th>Hudi</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>BigQuery 지원 수준</td>
      <td>가장 성숙</td>
      <td>네이티브 지원</td>
      <td>manifest 기반</td>
    </tr>
    <tr>
      <td>Time Travel (BigQuery)</td>
      <td>제한적</td>
      <td>제한적</td>
      <td>제한적</td>
    </tr>
    <tr>
      <td>Schema Evolution</td>
      <td>지원</td>
      <td>지원</td>
      <td>지원</td>
    </tr>
    <tr>
      <td>메타 관리 복잡도</td>
      <td>metadata.json URI 관리</td>
      <td>_delta_log 자동</td>
      <td>manifest 관리</td>
    </tr>
    <tr>
      <td>Partition Pruning</td>
      <td>manifest 기반 file pruning</td>
      <td>통계 기반</td>
      <td>제한적</td>
    </tr>
    <tr>
      <td>GCP 생태계 통합</td>
      <td>최고 (BigLake 완전 지원)</td>
      <td>좋음</td>
      <td>기본</td>
    </tr>
  </tbody>
</table>

<h3 id="적합한-경우-4">적합한 경우</h3>

<ul>
  <li>Spark/Flink가 <strong>주 적재 엔진</strong>이고 BigQuery는 분석 전용인 환경</li>
  <li>별도 메타스토어 서비스를 운영하고 싶지 않은 소규모 팀</li>
  <li>이미 Iceberg/Delta/Hudi로 데이터를 관리하고 있어 BigQuery에서 조회만 하면 되는 경우</li>
</ul>

<hr />

<h2 id="2-e-biglake-metastore--rest-catalog">2-E. BigLake Metastore + REST Catalog</h2>

<p><strong>데이터 위치:</strong> GCS (Parquet)
<strong>메타스토어:</strong> BigLake Metastore (GCP 관리형 서비스)
<strong>BigLake Connection:</strong> 사용</p>

<p>GCP가 제공하는 <strong>관리형 Iceberg REST Catalog</strong> 서비스입니다. Iceberg의 표준 REST Catalog API를 지원하므로, Spark·Flink·BigQuery·Databricks·Trino 등 여러 엔진이 동일한 메타스토어를 공유할 수 있습니다.</p>

<h3 id="spark에서-테이블-생성">Spark에서 테이블 생성</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog"</span><span class="p">,</span> <span class="s">"org.apache.iceberg.spark.SparkCatalog"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.type"</span><span class="p">,</span> <span class="s">"rest"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.uri"</span><span class="p">,</span>
    <span class="s">"https://biglake.googleapis.com/iceberg/v1beta/restcatalog"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.warehouse"</span><span class="p">,</span>
    <span class="s">"projects/PROJECT/locations/REGION/catalogs/CATALOG"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.token"</span><span class="p">,</span> <span class="s">"&lt;access_token&gt;"</span><span class="p">)</span>

<span class="n">spark</span><span class="p">.</span><span class="n">sql</span><span class="p">(</span><span class="s">"""
  CREATE TABLE my_catalog.db.sales (
    order_id BIGINT,
    customer_id STRING,
    amount DECIMAL(10,2),
    order_date DATE
  ) USING iceberg
  PARTITIONED BY (month(order_date))
"""</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="bigquery에서-조회">BigQuery에서 조회</h3>

<p>BigLake Metastore에 등록된 테이블은 BigQuery에서 자동으로 보입니다. 별도의 <code class="language-plaintext highlighter-rouge">CREATE EXTERNAL TABLE</code> 없이도 BigQuery 카탈로그에 나타납니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="nv">`project.dataset.sales`</span>
<span class="k">WHERE</span> <span class="n">order_date</span> <span class="o">&gt;=</span> <span class="s1">'2026-01-01'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="장점-4">장점</h3>

<ul>
  <li><strong>진정한 멀티엔진</strong>: Spark, BigQuery, Databricks, Flink, Trino 모두에서 읽기/쓰기 가능</li>
  <li><strong>서버리스</strong>: 메타스토어 인프라 관리 불필요</li>
  <li><strong>자동 동기화</strong>: 한 엔진에서 변경한 메타데이터가 다른 엔진에서 즉시 반영</li>
  <li><strong>표준 API</strong>: Iceberg REST Catalog 표준을 따르므로 벤더 종속 최소</li>
  <li><strong>Credential Vending</strong>: 세밀한 접근 제어 지원</li>
  <li><strong>metadata.json URI 수동 관리 불필요</strong>: 카탈로그가 자동 추적</li>
</ul>

<h3 id="단점-5">단점</h3>

<ul>
  <li><strong>Iceberg 전용</strong>: Delta Lake, Hudi는 지원하지 않음</li>
  <li><strong>서비스 비용</strong>: BigLake Metastore 사용에 따른 추가 비용</li>
  <li><strong>Region 제약</strong>: 지원 Region이 제한적일 수 있음</li>
  <li><strong>비교적 새로운 서비스</strong>: 아직 GA 전이거나 기능이 빠르게 변화 중</li>
  <li><strong>설정 복잡도</strong>: Spark Catalog 설정, 인증 토큰 관리 등 초기 설정이 복잡</li>
</ul>

<h3 id="적합한-경우-5">적합한 경우</h3>

<ul>
  <li><strong>멀티엔진 레이크하우스</strong>: Spark로 적재하고 BigQuery + Databricks로 분석하는 환경</li>
  <li>Iceberg 기반 데이터 레이크를 구축하면서 메타스토어 운영 부담을 줄이고 싶은 경우</li>
  <li>여러 팀/서비스가 동일 데이터를 다양한 엔진으로 접근하는 대규모 조직</li>
</ul>

<hr />

<h2 id="2-f-dataproc-metastore-hive-metastore-service">2-F. Dataproc Metastore (Hive Metastore Service)</h2>

<p><strong>데이터 위치:</strong> GCS (Parquet, ORC 등)
<strong>메타스토어:</strong> Dataproc Metastore (GCP 관리형 Hive Metastore)
<strong>BigLake Connection:</strong> 사용 가능</p>

<p>GCP가 관리하는 <strong>Hive Metastore 호환 서비스</strong>입니다. 기존 Hadoop/Hive 생태계와의 호환성이 핵심입니다.</p>

<h3 id="dataproc에서-테이블-생성">Dataproc에서 테이블 생성</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Dataproc Spark SQL</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">db</span><span class="p">.</span><span class="n">sales</span> <span class="p">(</span>
  <span class="n">order_id</span> <span class="nb">BIGINT</span><span class="p">,</span>
  <span class="n">customer_id</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">amount</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span>
<span class="p">)</span>
<span class="n">PARTITIONED</span> <span class="k">BY</span> <span class="p">(</span><span class="n">order_date</span> <span class="n">STRING</span><span class="p">)</span>
<span class="n">STORED</span> <span class="k">AS</span> <span class="n">PARQUET</span>
<span class="k">LOCATION</span> <span class="s1">'gs://my-bucket/hive/sales'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="bigquery에서-연동">BigQuery에서 연동</h3>

<p>Dataproc Metastore에 등록된 Hive 테이블을 BigQuery에서 외부 테이블로 연결할 수 있습니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">hive_sales</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'PARQUET'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'gs://my-bucket/hive/sales/*'</span><span class="p">],</span>
  <span class="n">hive_partition_uri_prefix</span> <span class="o">=</span> <span class="s1">'gs://my-bucket/hive/sales'</span><span class="p">,</span>
  <span class="n">require_hive_partition_filter</span> <span class="o">=</span> <span class="k">true</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="장점-5">장점</h3>

<ul>
  <li><strong>Hive 호환</strong>: 기존 Hive 쿼리, Spark SQL, Presto 등과 완벽 호환</li>
  <li><strong>관리형 서비스</strong>: Hive Metastore를 직접 운영할 필요 없음</li>
  <li><strong>Dataproc 통합</strong>: Dataproc 클러스터와 자동 연결</li>
  <li><strong>성숙한 생태계</strong>: 수년간 검증된 Hive Metastore 프로토콜</li>
  <li><strong>다양한 테이블 포맷</strong>: Hive, Iceberg, Delta Lake 등 여러 포맷의 메타데이터 저장 가능</li>
</ul>

<h3 id="단점-6">단점</h3>

<ul>
  <li><strong>BigLake Metastore 대비 레거시</strong>: Google은 BigLake Metastore로의 마이그레이션을 권장</li>
  <li><strong>인스턴스 관리 필요</strong>: 서버리스가 아닌 인스턴스 기반이라 크기 조정 필요</li>
  <li><strong>비용</strong>: 인스턴스 비용이 발생 (사용하지 않아도 과금)</li>
  <li><strong>BigQuery 연동 제한</strong>: BigQuery에서 직접 Hive Metastore를 참조하지 못하고, 외부 테이블을 별도로 생성해야 함</li>
  <li><strong>메타데이터 동기화</strong>: BigQuery 외부 테이블의 스키마가 Hive 테이블 변경을 자동 반영하지 않을 수 있음</li>
</ul>

<h3 id="적합한-경우-6">적합한 경우</h3>

<ul>
  <li><strong>기존 Hadoop/Hive 워크로드</strong>를 GCP로 마이그레이션한 환경</li>
  <li>Hive 호환 메타스토어가 필요한 레거시 시스템과의 연계</li>
  <li>Dataproc 클러스터를 주로 사용하는 팀</li>
</ul>

<hr />

<h2 id="2-g-self-hosted-hive-metastore">2-G. Self-hosted Hive Metastore</h2>

<p><strong>데이터 위치:</strong> GCS (Parquet, ORC 등)
<strong>메타스토어:</strong> 직접 구축한 Hive Metastore (GCE/GKE에서 운영)
<strong>BigLake Connection:</strong> 사용 가능</p>

<p>Hive Metastore를 GCE VM이나 GKE 위에 직접 설치하고 운영하는 방식입니다.</p>

<h3 id="구성-예시">구성 예시</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────┐
│ GCE VM / GKE Pod                    │
│  ├── Hive Metastore Service         │
│  └── Backend DB (MySQL / PostgreSQL)│
└──────────────┬──────────────────────┘
               │ Thrift Protocol
    ┌──────────┼──────────┐
    │          │          │
  Spark     Presto    BigQuery
                    (External Table)
</code></pre></div></div>

<h3 id="장점-6">장점</h3>

<ul>
  <li><strong>완전한 제어</strong>: 버전, 설정, 플러그인 등을 자유롭게 커스터마이징</li>
  <li><strong>비용 최적화 가능</strong>: 소규모 환경에서는 작은 VM으로 운영 가능</li>
  <li><strong>특수 요구사항 대응</strong>: 커스텀 Serde, UDF 등 특수 기능 사용 가능</li>
</ul>

<h3 id="단점-7">단점</h3>

<ul>
  <li><strong>운영 부담 최대</strong>: 가용성, 백업, 업그레이드, 모니터링 전부 직접 관리</li>
  <li><strong>단일 장애점</strong>: Metastore 다운 시 모든 연관 워크로드 영향</li>
  <li><strong>스케일링 직접 관리</strong>: 부하 증가에 따른 스케일업/아웃 직접 수행</li>
  <li><strong>BigQuery 연동 번거로움</strong>: BigQuery에서 직접 참조 불가, 별도 외부 테이블 생성 필요</li>
  <li><strong>보안 관리</strong>: 네트워크, 인증 등 보안 구성 직접 수행</li>
</ul>

<h3 id="적합한-경우-7">적합한 경우</h3>

<ul>
  <li><strong>온프레미스에서 마이그레이션 중</strong>이고 Hive Metastore를 그대로 가져온 경우</li>
  <li>Dataproc Metastore의 기능이나 Region 지원이 부족한 특수 환경</li>
  <li>이미 Hive Metastore 운영 노하우가 있는 팀</li>
</ul>

<hr />

<h2 id="3-object-table-비정형-데이터">3. Object Table (비정형 데이터)</h2>

<p><strong>데이터 위치:</strong> GCS (이미지, PDF, 텍스트, 오디오 등)
<strong>메타스토어:</strong> 없음</p>

<p>구조화된 데이터가 아닌 <strong>비정형 데이터</strong>를 BigQuery에서 다루기 위한 특수한 테이블 유형입니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">product_images</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.my-connection`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">object_metadata</span> <span class="o">=</span> <span class="s1">'SIMPLE'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="s1">'gs://my-bucket/images/*'</span><span class="p">]</span>
<span class="p">);</span>

<span class="c1">-- 파일 메타데이터 조회</span>
<span class="k">SELECT</span> <span class="n">uri</span><span class="p">,</span> <span class="n">content_type</span><span class="p">,</span> <span class="k">size</span><span class="p">,</span> <span class="n">updated</span>
<span class="k">FROM</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">product_images</span><span class="p">;</span>

<span class="c1">-- BigQuery ML과 연동하여 이미지 분류</span>
<span class="k">SELECT</span> <span class="n">uri</span><span class="p">,</span> <span class="n">ml_predict_row</span><span class="p">.</span><span class="n">label</span>
<span class="k">FROM</span> <span class="n">ML</span><span class="p">.</span><span class="n">PREDICT</span><span class="p">(</span>
  <span class="n">MODEL</span> <span class="nv">`project.dataset.vision_model`</span><span class="p">,</span>
  <span class="k">TABLE</span> <span class="nv">`project.dataset.product_images`</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="장점-7">장점</h3>

<ul>
  <li><strong>비정형 데이터 통합</strong>: SQL로 이미지, 문서 등의 메타데이터 조회 가능</li>
  <li><strong>BigQuery ML 연동</strong>: 비정형 데이터에 대한 ML 추론 파이프라인 구성 가능</li>
  <li><strong>Vertex AI 통합</strong>: 비전 모델 등과 연계하여 분석</li>
</ul>

<h3 id="단점-8">단점</h3>

<ul>
  <li><strong>데이터 자체를 읽지는 않음</strong>: 파일의 <strong>메타데이터</strong>만 BigQuery에서 조회</li>
  <li><strong>특수 목적</strong>: 일반적인 데이터 분석과는 용도가 다름</li>
</ul>

<hr />

<h2 id="전체-비교-요약">전체 비교 요약</h2>

<h3 id="기능-비교">기능 비교</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>DML</th>
      <th>Streaming</th>
      <th>Row/Col 보안</th>
      <th>Time Travel</th>
      <th>파티션 Pruning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Native Table</td>
      <td>전체 지원</td>
      <td>지원</td>
      <td>지원</td>
      <td>7일</td>
      <td>지원</td>
    </tr>
    <tr>
      <td>기본 External</td>
      <td>불가</td>
      <td>불가</td>
      <td>미지원</td>
      <td>없음</td>
      <td>Hive 파티션만</td>
    </tr>
    <tr>
      <td>BigLake External (flat)</td>
      <td>불가</td>
      <td>불가</td>
      <td>지원</td>
      <td>없음</td>
      <td>Hive 파티션만</td>
    </tr>
    <tr>
      <td>Managed Iceberg</td>
      <td>전체 지원</td>
      <td>지원</td>
      <td>지원</td>
      <td>지원</td>
      <td>지원</td>
    </tr>
    <tr>
      <td>External Iceberg/Delta</td>
      <td>불가</td>
      <td>불가</td>
      <td>지원</td>
      <td>제한적</td>
      <td>Manifest 기반</td>
    </tr>
    <tr>
      <td>BigLake Metastore</td>
      <td>Spark 쓰기</td>
      <td>불가</td>
      <td>지원</td>
      <td>지원</td>
      <td>지원</td>
    </tr>
    <tr>
      <td>Dataproc Metastore</td>
      <td>Spark 쓰기</td>
      <td>불가</td>
      <td>제한적</td>
      <td>엔진 의존</td>
      <td>Hive 파티션</td>
    </tr>
    <tr>
      <td>Self-hosted HMS</td>
      <td>Spark 쓰기</td>
      <td>불가</td>
      <td>미지원</td>
      <td>엔진 의존</td>
      <td>Hive 파티션</td>
    </tr>
  </tbody>
</table>

<h3 id="메타스토어-관리-비교">메타스토어 관리 비교</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>메타스토어 관리 주체</th>
      <th>관리 부담</th>
      <th>멀티엔진</th>
      <th>비용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Native Table</td>
      <td>BigQuery (내부)</td>
      <td>없음</td>
      <td>BigQuery 전용</td>
      <td>스토리지+쿼리</td>
    </tr>
    <tr>
      <td>기본 External</td>
      <td>없음 (URI 직접)</td>
      <td>최소</td>
      <td>BigQuery 전용</td>
      <td>쿼리만</td>
    </tr>
    <tr>
      <td>BigLake External (flat)</td>
      <td>BigQuery 카탈로그</td>
      <td>낮음</td>
      <td>BigQuery 전용</td>
      <td>쿼리만</td>
    </tr>
    <tr>
      <td>Managed Iceberg</td>
      <td>BigQuery (내부)</td>
      <td>없음</td>
      <td>export 필요</td>
      <td>스토리지+쿼리</td>
    </tr>
    <tr>
      <td>External Iceberg/Delta</td>
      <td>데이터 쓰기 엔진</td>
      <td><strong>높음</strong> (수동)</td>
      <td>제한적</td>
      <td>GCS만</td>
    </tr>
    <tr>
      <td>BigLake Metastore</td>
      <td>GCP (서버리스)</td>
      <td>낮음</td>
      <td><strong>완전 지원</strong></td>
      <td>서비스+GCS</td>
    </tr>
    <tr>
      <td>Dataproc Metastore</td>
      <td>GCP (인스턴스)</td>
      <td>중간</td>
      <td>Hive 호환</td>
      <td>인스턴스+GCS</td>
    </tr>
    <tr>
      <td>Self-hosted HMS</td>
      <td><strong>직접 운영</strong></td>
      <td><strong>최대</strong></td>
      <td>Hive 호환</td>
      <td>VM+GCS</td>
    </tr>
  </tbody>
</table>

<h3 id="성능-비교-상대적">성능 비교 (상대적)</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>쿼리 성능</th>
      <th>적재 성능</th>
      <th>스캔 최적화</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Native Table</td>
      <td>★★★★★</td>
      <td>★★★★</td>
      <td>파티션+클러스터</td>
    </tr>
    <tr>
      <td>기본 External</td>
      <td>★★☆☆☆</td>
      <td>N/A</td>
      <td>제한적</td>
    </tr>
    <tr>
      <td>BigLake External (flat)</td>
      <td>★★★☆☆</td>
      <td>N/A</td>
      <td>메타데이터 캐싱</td>
    </tr>
    <tr>
      <td>Managed Iceberg</td>
      <td>★★★★☆</td>
      <td>★★★★</td>
      <td>파티션+클러스터</td>
    </tr>
    <tr>
      <td>External Iceberg</td>
      <td>★★★★☆</td>
      <td>외부 엔진</td>
      <td>Manifest pruning</td>
    </tr>
    <tr>
      <td>BigLake Metastore</td>
      <td>★★★★☆</td>
      <td>외부 엔진</td>
      <td>Manifest pruning</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="의사결정-가이드">의사결정 가이드</h2>

<h3 id="질문-1-bigquery-외에-다른-엔진이-필요한가">질문 1: BigQuery 외에 다른 엔진이 필요한가?</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BigQuery만 사용
├── DML이 필요한가?
│   ├── Yes → Native Table 또는 Managed Iceberg
│   └── No  → BigLake External Table (flat files)
│
Spark/Flink/Databricks도 사용
├── 어느 엔진이 데이터를 쓰는가?
│   ├── BigQuery가 씀 → Managed Iceberg + EXPORT METADATA
│   ├── Spark가 씀   → BigLake Metastore (REST Catalog) 또는 External Iceberg
│   └── 양쪽 다 씀   → BigLake Metastore (REST Catalog)
</code></pre></div></div>

<h3 id="질문-2-메타스토어-운영에-얼마나-투자할-수-있는가">질문 2: 메타스토어 운영에 얼마나 투자할 수 있는가?</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>전혀 관리하고 싶지 않다
├── BigQuery 중심 → Native Table
└── 멀티엔진      → BigLake Metastore (서버리스)

최소한으로 관리하겠다
├── 메타데이터 파일만 → External Iceberg (GCS)
└── Hive 호환 필요   → Dataproc Metastore

전부 직접 제어하겠다
└── Self-hosted Hive Metastore
</code></pre></div></div>

<h3 id="질문-3-기존-환경은-무엇인가">질문 3: 기존 환경은 무엇인가?</h3>

<table>
  <thead>
    <tr>
      <th>기존 환경</th>
      <th>권장 방식</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>온프레미스 Hive → GCP 마이그레이션</td>
      <td>Dataproc Metastore → 점진적으로 BigLake Metastore</td>
    </tr>
    <tr>
      <td>신규 데이터 레이크 구축</td>
      <td>BigLake Metastore (REST Catalog)</td>
    </tr>
    <tr>
      <td>BigQuery만 사용, 외부 데이터 간헐적 조회</td>
      <td>BigLake External Table (flat files)</td>
    </tr>
    <tr>
      <td>BigQuery 중심, 데이터 portability 필요</td>
      <td>Managed Iceberg Table</td>
    </tr>
    <tr>
      <td>Spark 중심, BigQuery는 분석 전용</td>
      <td>External Iceberg 또는 BigLake Metastore</td>
    </tr>
    <tr>
      <td>PoC / 일회성 분석</td>
      <td>기본 External Table</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="자주-하는-오해">자주 하는 오해</h2>

<h3 id="external-table이면-다-같은-거-아닌가요">“External Table이면 다 같은 거 아닌가요?”</h3>

<p>아닙니다. BigLake Connection 유무에 따라 보안 모델이 완전히 달라지고, Open Table Format 사용 여부에 따라 pruning 성능이 크게 차이납니다. 같은 “External Table”이라는 이름이지만, 기본 External Table과 BigLake Metastore 기반 Iceberg 테이블은 <strong>아키텍처적으로 완전히 다른 방식</strong>입니다.</p>

<h3 id="managed-iceberg이면-native-table이랑-뭐가-다른가요">“Managed Iceberg이면 Native Table이랑 뭐가 다른가요?”</h3>

<p>사용성은 비슷하지만, 데이터가 <strong>고객 소유 GCS 버킷에 Parquet으로 저장</strong>된다는 점이 핵심 차이입니다. Native Table은 BigQuery 내부 Capacitor 포맷이라 다른 엔진에서 읽을 수 없지만, Managed Iceberg는 메타데이터를 export하면 Spark 등에서도 읽을 수 있습니다. 반면 쿼리 성능은 Native Table이 조금 더 좋습니다.</p>

<h3 id="biglake-metastore와-dataproc-metastore는-같은-건가요">“BigLake Metastore와 Dataproc Metastore는 같은 건가요?”</h3>

<p>다릅니다. Dataproc Metastore는 <strong>Hive Metastore 호환</strong> 서비스이고, BigLake Metastore는 <strong>Iceberg REST Catalog 표준</strong> 서비스입니다. Google은 BigLake Metastore로의 마이그레이션을 권장하고 있으며, 이를 위한 마이그레이션 도구도 제공합니다.</p>

<h3 id="gcs에-parquet만-올려두면-바로-쿼리할-수-있나요">“GCS에 Parquet만 올려두면 바로 쿼리할 수 있나요?”</h3>

<p>기본 External Table이나 BigLake External Table(flat files)을 만들면 가능합니다. 하지만 이 방식은 <strong>파일 수준에서만</strong> 작동하므로, 테이블 수준의 스키마 진화, ACID 트랜잭션, Time Travel 등은 지원되지 않습니다. 이런 기능이 필요하면 Iceberg 같은 Open Table Format을 사용해야 합니다.</p>

<hr />

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

<table>
  <thead>
    <tr>
      <th>핵심 판단 기준</th>
      <th>권장 방식</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>최고 성능, BigQuery만 사용</td>
      <td><strong>Native Table</strong></td>
    </tr>
    <tr>
      <td>BigQuery 중심 + 데이터 portability</td>
      <td><strong>Managed Iceberg</strong></td>
    </tr>
    <tr>
      <td>소규모, 빠른 시작, 읽기 전용</td>
      <td><strong>기본 External Table</strong></td>
    </tr>
    <tr>
      <td>프로덕션 외부 데이터 + 거버넌스</td>
      <td><strong>BigLake External (flat)</strong></td>
    </tr>
    <tr>
      <td>Spark 적재 + BigQuery 분석, 간단한 구성</td>
      <td><strong>External Iceberg</strong></td>
    </tr>
    <tr>
      <td>멀티엔진 레이크하우스</td>
      <td><strong>BigLake Metastore (REST Catalog)</strong></td>
    </tr>
    <tr>
      <td>Hive 레거시 마이그레이션</td>
      <td><strong>Dataproc Metastore</strong></td>
    </tr>
  </tbody>
</table>

<p>BigQuery에서 데이터를 다루는 방법은 생각보다 많고, 각 방식마다 메타스토어 관리 주체·성능·기능·운영 부담이 다릅니다. “어떤 방식이 제일 좋은가”보다는 <strong>“우리 조직의 데이터 흐름에서 메타스토어를 누가, 어떻게 관리하는 것이 가장 자연스러운가”</strong>를 기준으로 선택하는 것이 핵심입니다.</p>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="bigquery" /><category term="biglake" /><category term="gcs" /><category term="iceberg" /><category term="delta-lake" /><category term="external-table" /><category term="metastore" /><category term="dataproc" /><category term="gcp" /><category term="data-engineering" /><summary type="html"><![CDATA[BigQuery에서 데이터를 다루는 방법은 크게 Native Table(BigQuery 내부 저장)과 External Table(GCS 등 외부 저장)로 나뉩니다. 특히 External Table은 메타스토어를 누가, 어떻게 관리하느냐에 따라 6가지 이상의 방식이 존재하며, 각각 기능·성능·운영 부담이 다릅니다.]]></summary></entry><entry><title type="html">CSP별 AI Agent 프레임워크와 런타임 비교: 특화 기능, 오픈소스 대안, Lock-in 분석</title><link href="https://seonghak.com/blog/2026/03/24/csp-agent-framework-runtime-comparison/" rel="alternate" type="text/html" title="CSP별 AI Agent 프레임워크와 런타임 비교: 특화 기능, 오픈소스 대안, Lock-in 분석" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/csp-agent-framework-runtime-comparison</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/csp-agent-framework-runtime-comparison/"><![CDATA[<p>AI Agent를 프로덕션에 배포하려면 두 가지 축을 결정해야 합니다. <strong>에이전트 프레임워크</strong>(개발)와 <strong>매니지드 런타임</strong>(배포·운영)입니다. 3대 CSP(AWS, Azure, GCP)는 각각 이 두 축에 대해 자체 솔루션을 제공하면서 동시에 오픈소스 프레임워크도 지원하는 전략을 취하고 있습니다.</p>

<p>이 글에서는 각 CSP의 에이전트 스택을 <strong>프레임워크 ↔ 런타임</strong> 두 축으로 분리하여 비교하고, 오픈소스 대안과 벤더 Lock-in 리스크를 체계적으로 정리합니다.</p>

<hr />

<h2 id="1-전체-구조-한눈에-보기">1. 전체 구조 한눈에 보기</h2>

<table>
  <thead>
    <tr>
      <th>축</th>
      <th>AWS</th>
      <th>Azure</th>
      <th>GCP</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>자체 프레임워크</strong></td>
      <td>Strands Agents SDK</td>
      <td>Microsoft Agent Framework (Semantic Kernel + AutoGen)</td>
      <td>Agent Development Kit (ADK)</td>
    </tr>
    <tr>
      <td><strong>프레임워크 라이선스</strong></td>
      <td>Apache 2.0</td>
      <td>MIT</td>
      <td>Apache 2.0</td>
    </tr>
    <tr>
      <td><strong>매니지드 런타임</strong></td>
      <td>Bedrock AgentCore</td>
      <td>Azure AI Foundry Agent Service</td>
      <td>Vertex AI Agent Engine</td>
    </tr>
    <tr>
      <td><strong>런타임 특화 기능</strong></td>
      <td>Runtime, Memory, Gateway, Identity, Browser, Code Interpreter, Observability, Evaluations, Policy</td>
      <td>세션/메모리 관리, Bing/Azure AI Search 통합, REST API/Function Apps 자동 호출, Copilot 통합</td>
      <td>세션 관리, 메모리(단기/장기), VPC-SC, IAM, CMEK, 자동 스케일링</td>
    </tr>
    <tr>
      <td><strong>지원 외부 프레임워크</strong></td>
      <td>LangGraph, CrewAI, LlamaIndex, Google ADK, OpenAI Agents SDK</td>
      <td>OpenAI, Anthropic, AWS Bedrock, Ollama 등</td>
      <td>LangGraph, LangChain, CrewAI 등</td>
    </tr>
  </tbody>
</table>

<p>핵심 관찰: 3사 모두 <strong>자체 프레임워크는 오픈소스로 공개</strong>하면서, <strong>매니지드 런타임에서 수익을 창출</strong>하는 동일한 전략을 취하고 있습니다. 프레임워크 레벨에서는 Lock-in이 적지만, 런타임 레벨에서 종속성이 발생합니다.</p>

<hr />

<h2 id="2-aws-bedrock-agentcore--strands-agents">2. AWS: Bedrock AgentCore + Strands Agents</h2>

<h3 id="21-strands-agents-sdk-오픈소스-프레임워크">2.1 Strands Agents SDK (오픈소스 프레임워크)</h3>

<p>AWS가 내부적으로 Amazon Q Developer, AWS Glue, VPC Reachability Analyzer 등에서 사용하던 에이전트 프레임워크를 Apache 2.0으로 공개한 것입니다.</p>

<p><strong>핵심 철학 — “모델 주도(Model-Driven)”</strong></p>

<p>기존 프레임워크들이 개발자가 오케스트레이션 로직을 명시적으로 작성하도록 요구했다면, Strands는 최신 LLM의 추론 능력에 의존하여 몇 줄의 코드만으로 에이전트를 구성합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">strands</span> <span class="kn">import</span> <span class="n">Agent</span>

<span class="n">agent</span> <span class="o">=</span> <span class="n">Agent</span><span class="p">(</span><span class="n">system_prompt</span><span class="o">=</span><span class="s">"You are a helpful assistant."</span><span class="p">)</span>
<span class="n">agent</span><span class="p">(</span><span class="s">"서울의 오늘 날씨를 알려줘"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>주요 특징:</strong></p>

<ul>
  <li>Python/TypeScript 듀얼 SDK</li>
  <li>MCP(Model Context Protocol) 네이티브 지원</li>
  <li>Swarm, Graph, A2A 세 가지 멀티에이전트 패턴</li>
  <li>OpenTelemetry 기반 관측성</li>
  <li>20+ 내장 도구 (Retrieve, Thinking, Shell, HTTP 등)</li>
</ul>

<h3 id="22-amazon-bedrock-agentcore-매니지드-런타임">2.2 Amazon Bedrock AgentCore (매니지드 런타임)</h3>

<p>2025년 12월 GA된 AgentCore는 <strong>모듈러 아키텍처</strong>가 특징입니다. 9개 서비스를 독립적으로 또는 조합하여 사용할 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>서비스</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Runtime</strong></td>
      <td>서버리스 배포, 세션별 microVM 격리, 최대 8시간 비동기 처리, 100MB 멀티모달 페이로드</td>
    </tr>
    <tr>
      <td><strong>Memory</strong></td>
      <td>단기 메모리(멀티턴) + 장기 메모리(세션 간 영속), 에이전트 간 메모리 공유</td>
    </tr>
    <tr>
      <td><strong>Gateway</strong></td>
      <td>API/Lambda/MCP 서버를 MCP 호환 도구로 변환, Salesforce·Zoom·Jira·Slack 통합</td>
    </tr>
    <tr>
      <td><strong>Identity</strong></td>
      <td>Okta, Microsoft Entra ID, Cognito, Auth0 등 기존 IdP 연동</td>
    </tr>
    <tr>
      <td><strong>Code Interpreter</strong></td>
      <td>Python, JavaScript, TypeScript 샌드박스 실행 환경</td>
    </tr>
    <tr>
      <td><strong>Browser</strong></td>
      <td>클라우드 기반 브라우저 자동화 (Playwright, BrowserUse 호환)</td>
    </tr>
    <tr>
      <td><strong>Observability</strong></td>
      <td>OpenTelemetry 호환 트레이싱, 디버깅, 모니터링</td>
    </tr>
    <tr>
      <td><strong>Evaluations</strong></td>
      <td>에이전트/도구 품질 자동 평가, CloudWatch 통합</td>
    </tr>
    <tr>
      <td><strong>Policy</strong></td>
      <td>Cedar 정책 언어로 도구 호출 전 세밀한 접근 제어</td>
    </tr>
  </tbody>
</table>

<p><strong>차별점:</strong></p>

<ul>
  <li><strong>프레임워크 무관</strong>: LangGraph, CrewAI, LlamaIndex, Google ADK, OpenAI Agents SDK, Strands 모두 지원</li>
  <li><strong>모델 무관</strong>: OpenAI, Gemini, Claude, Nova, Llama, Mistral 등 자유롭게 선택</li>
  <li><strong>과금</strong>: 실제 리소스 소비 기반, I/O 대기 시간은 무료</li>
  <li><strong>세션 격리</strong>: 각 세션이 전용 microVM에서 실행되어 CPU/메모리/파일시스템 완전 격리</li>
</ul>

<h3 id="23-lock-in-분석">2.3 Lock-in 분석</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Lock-in 수준</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Strands Agents SDK</td>
      <td>❌ 낮음</td>
      <td>Apache 2.0, 어디서든 실행 가능</td>
    </tr>
    <tr>
      <td>Bedrock AgentCore Runtime</td>
      <td>⚠️ 중간~높음</td>
      <td>AWS 전용 서비스, microVM 세션 모델은 타 CSP에 없음</td>
    </tr>
    <tr>
      <td>AgentCore Memory</td>
      <td>⚠️ 중간</td>
      <td>API는 표준적이나 구현은 AWS 종속</td>
    </tr>
    <tr>
      <td>AgentCore Gateway</td>
      <td>⚠️ 중간</td>
      <td>MCP 표준 기반이므로 도구 정의 자체는 이식 가능</td>
    </tr>
    <tr>
      <td>AgentCore Identity</td>
      <td>🔴 높음</td>
      <td>AWS IAM/Cognito 깊은 통합</td>
    </tr>
  </tbody>
</table>

<p><strong>탈출 전략</strong>: Strands SDK + Docker/K8s 자체 배포. Strands는 4가지 배포 아키텍처(로컬, API 모놀리스, 에이전트/도구 분리, Return-of-Control)를 공식 지원합니다.</p>

<hr />

<h2 id="3-azure-ai-foundry-agent-service--microsoft-agent-framework">3. Azure: AI Foundry Agent Service + Microsoft Agent Framework</h2>

<h3 id="31-microsoft-agent-framework-오픈소스-프레임워크">3.1 Microsoft Agent Framework (오픈소스 프레임워크)</h3>

<p>Semantic Kernel과 AutoGen을 통합한 차세대 프레임워크로, 2026년 RC(Release Candidate)에 도달했습니다.</p>

<p><strong>핵심 특징:</strong></p>

<ul>
  <li>.NET과 Python 지원</li>
  <li>그래프 기반 워크플로 (순차, 동시, 핸드오프, 그룹 채팅)</li>
  <li>MCP, A2A, OpenAPI 등 오픈 표준 지원</li>
  <li>스트리밍, 체크포인팅, Human-in-the-Loop</li>
  <li>MIT 라이선스</li>
</ul>

<p><strong>에이전트 타입이 풍부합니다:</strong></p>

<table>
  <thead>
    <tr>
      <th>에이전트 타입</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ChatCompletionAgent</td>
      <td>범용 대화형 에이전트</td>
    </tr>
    <tr>
      <td>OpenAIAssistantAgent</td>
      <td>OpenAI Assistants API 기반</td>
    </tr>
    <tr>
      <td>AzureAIAgent</td>
      <td>Azure AI Foundry 통합 에이전트</td>
    </tr>
    <tr>
      <td>OpenAIResponsesAgent</td>
      <td>OpenAI Responses API 기반</td>
    </tr>
    <tr>
      <td>CopilotStudioAgent</td>
      <td>Microsoft Copilot Studio 연동</td>
    </tr>
  </tbody>
</table>

<p><strong>다중 모델 공급자 지원</strong>: Azure OpenAI, OpenAI, GitHub Copilot, Anthropic Claude, AWS Bedrock, Ollama 등과 호환됩니다. 이 점에서 Semantic Kernel은 특정 CSP에 종속되지 않는 유연성을 가집니다.</p>

<h3 id="32-azure-ai-foundry-agent-service-매니지드-런타임">3.2 Azure AI Foundry Agent Service (매니지드 런타임)</h3>

<p>10,000개 이상의 고객사가 GA 이후 사용 중인 매니지드 서비스입니다.</p>

<p><strong>메모리 관리가 세분화되어 있습니다:</strong></p>

<ul>
  <li><strong>단기 메모리</strong>: 현재 세션 대화를 추적</li>
  <li><strong>장기 메모리</strong>: 세션 간 지속되는 영속 메모리, 3단계 프로세스(추출 → 통합 → 검색)로 운영</li>
  <li><strong>MemorySearchTool</strong>: 네임스페이스로 메모리 격리, 검색 옵션 커스터마이징 가능</li>
</ul>

<p><strong>도구 통합:</strong></p>

<ul>
  <li>Bing Search, Azure AI Search (지식 검색)</li>
  <li>REST API 자동 호출 (Swagger/OpenAPI 3.0 정의 기반)</li>
  <li>Azure Function Apps 연동</li>
  <li>Azure Logic Apps 통합</li>
  <li>RAG (TextSearchProvider)</li>
</ul>

<p><strong>Microsoft 생태계 통합이 최대 강점입니다:</strong></p>

<ul>
  <li>Microsoft 365, Teams 원클릭 배포</li>
  <li>Entra ID 기반 거버넌스 및 SSO</li>
  <li>Copilot Studio 연동</li>
  <li>Application Insights 기반 모니터링</li>
  <li>Azure DevOps CI/CD 통합</li>
</ul>

<h3 id="33-lock-in-분석">3.3 Lock-in 분석</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Lock-in 수준</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Semantic Kernel / Agent Framework</td>
      <td>❌ 낮음</td>
      <td>MIT 라이선스, 멀티 모델·멀티 클라우드 지원</td>
    </tr>
    <tr>
      <td>Azure AI Foundry Agent Service</td>
      <td>🔴 높음</td>
      <td>Azure 전용, Entra ID·M365·Copilot 깊은 통합</td>
    </tr>
    <tr>
      <td>메모리 서비스 (장기)</td>
      <td>🔴 높음</td>
      <td>Azure 매니지드 서비스, 이식 불가</td>
    </tr>
    <tr>
      <td>도구 통합 (Bing, Azure Functions)</td>
      <td>⚠️ 중간~높음</td>
      <td>Azure 서비스 종속, 단 OpenAPI 기반이므로 스펙은 이식 가능</td>
    </tr>
    <tr>
      <td>모델 접근</td>
      <td>⚠️ 중간</td>
      <td>Azure OpenAI가 중심, 타 모델은 제한적</td>
    </tr>
  </tbody>
</table>

<p><strong>탈출 전략</strong>: Semantic Kernel은 OpenAI, Anthropic, Bedrock 등 다양한 모델 백엔드를 지원하므로, 프레임워크 레벨에서는 전환이 용이합니다. 런타임은 Docker/K8s + FastAPI로 직접 구축하되, 메모리 관리와 도구 통합을 재구현해야 합니다.</p>

<hr />

<h2 id="4-gcp-vertex-ai-agent-engine--adk">4. GCP: Vertex AI Agent Engine + ADK</h2>

<h3 id="41-agent-development-kit--adk-오픈소스-프레임워크">4.1 Agent Development Kit — ADK (오픈소스 프레임워크)</h3>

<p>Google이 자체 AI 에이전트 구축에 사용하는 프레임워크를 Apache 2.0으로 공개한 것입니다.</p>

<p><strong>핵심 특징:</strong></p>

<ul>
  <li>SequentialAgent, ParallelAgent, LoopAgent 등 명시적 워크플로 패턴</li>
  <li>세션 기반 상태 관리 (<code class="language-plaintext highlighter-rouge">session.state</code>)</li>
  <li>사용자/앱/세션/임시 4단계 스코프의 상태 관리</li>
  <li>MCP 도구 통합</li>
  <li>A2A 프로토콜 지원</li>
</ul>

<p><strong>ADK v1.2.0+ 부터 CLI 단일 명령 배포 지원:</strong></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Agent Engine 배포</span>
adk deploy agent_engine <span class="se">\</span>
  <span class="nt">--project</span> &lt;project-id&gt; <span class="se">\</span>
  <span class="nt">--region</span> us-central1 <span class="se">\</span>
  <span class="nt">--staging_bucket</span> &lt;bucket-name&gt;

<span class="c"># Cloud Run 배포 (Agent Engine 없이)</span>
adk deploy cloud_run <span class="se">\</span>
  <span class="nt">--project</span> &lt;project-id&gt; <span class="se">\</span>
  <span class="nt">--region</span> us-central1
</code></pre></div></div>

<h3 id="42-vertex-ai-agent-engine-매니지드-런타임">4.2 Vertex AI Agent Engine (매니지드 런타임)</h3>

<p>GCP의 완전 관리형 에이전트 배포 서비스입니다.</p>

<p><strong>주요 기능:</strong></p>

<ul>
  <li>자동 스케일링</li>
  <li>세션 관리 (대화 컨텍스트 유지)</li>
  <li>메모리 서비스: <code class="language-plaintext highlighter-rouge">InMemoryMemoryService</code> (프로토타이핑) / <code class="language-plaintext highlighter-rouge">VertexAiMemoryBankService</code> (영속)</li>
  <li>VPC-SC (서비스 경계) 보안</li>
  <li>IAM 기반 접근 제어</li>
  <li>CMEK (고객 관리 암호화 키)</li>
  <li>ADK API 서버 및 웹 UI 제공</li>
</ul>

<p><strong>Agent Starter Pack(ASP)</strong>: Terraform/CI/CD 템플릿을 제공하여 새 GCP 프로젝트에서 빠르게 시작할 수 있습니다.</p>

<h3 id="43-lock-in-분석">4.3 Lock-in 분석</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Lock-in 수준</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ADK</td>
      <td>❌ 낮음</td>
      <td>Apache 2.0, Cloud Run/GKE/Docker 어디든 배포 가능</td>
    </tr>
    <tr>
      <td>Agent Engine</td>
      <td>🔴 높음</td>
      <td>GCP 전용, Vertex AI 종속</td>
    </tr>
    <tr>
      <td>MemoryBankService</td>
      <td>🔴 높음</td>
      <td>Agent Engine 의존, 자체 구현 시 InMemory만 기본 제공</td>
    </tr>
    <tr>
      <td>VPC-SC / IAM / CMEK</td>
      <td>🔴 높음</td>
      <td>GCP 보안 서비스 고유</td>
    </tr>
    <tr>
      <td>Gemini 모델 통합</td>
      <td>⚠️ 중간</td>
      <td>ADK는 다른 모델도 지원하나 Gemini 최적화</td>
    </tr>
  </tbody>
</table>

<p><strong>탈출 전략</strong>: <code class="language-plaintext highlighter-rouge">adk deploy cloud_run</code>으로 Agent Engine 없이 Cloud Run에 직접 배포. 세션 관리는 Firestore, 메모리는 Cloud SQL + Redis로 직접 구현합니다. GCP 완전 이탈 시 Docker/K8s로 어디든 배포 가능합니다.</p>

<hr />

<h2 id="5-csp-매니지드-런타임-기능-상세-비교">5. CSP 매니지드 런타임 기능 상세 비교</h2>

<h3 id="51-세션-및-메모리">5.1 세션 및 메모리</h3>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>Bedrock AgentCore</th>
      <th>Azure AI Foundry</th>
      <th>Agent Engine</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>세션 격리</td>
      <td>microVM 격리 (CPU/메모리/FS)</td>
      <td>세션 기반 관리</td>
      <td>세션 ID 기반 관리</td>
    </tr>
    <tr>
      <td>단기 메모리</td>
      <td>✅ 멀티턴 대화</td>
      <td>✅ 세션 내 컨텍스트</td>
      <td>✅ 세션 상태</td>
    </tr>
    <tr>
      <td>장기 메모리</td>
      <td>✅ 세션 간 영속, 에이전트 간 공유</td>
      <td>✅ 3단계(추출/통합/검색)</td>
      <td>✅ MemoryBankService</td>
    </tr>
    <tr>
      <td>체크포인팅</td>
      <td>✅ 비동기 태스크 관리</td>
      <td>✅ 체크포인팅</td>
      <td>제한적</td>
    </tr>
  </tbody>
</table>

<h3 id="52-보안-및-거버넌스">5.2 보안 및 거버넌스</h3>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>Bedrock AgentCore</th>
      <th>Azure AI Foundry</th>
      <th>Agent Engine</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>네트워크 격리</td>
      <td>VPC 배포</td>
      <td>VNet/Private Endpoints</td>
      <td>VPC-SC</td>
    </tr>
    <tr>
      <td>암호화</td>
      <td>KMS (at-rest), TLS (in-transit)</td>
      <td>CMK 지원</td>
      <td>CMEK</td>
    </tr>
    <tr>
      <td>ID 관리</td>
      <td>Cognito, Okta, Entra ID, Auth0</td>
      <td>Entra ID (Azure AD)</td>
      <td>IAM</td>
    </tr>
    <tr>
      <td>접근 제어 정책</td>
      <td>Cedar 정책 언어</td>
      <td>RBAC</td>
      <td>IAM 역할</td>
    </tr>
    <tr>
      <td>컴플라이언스</td>
      <td>SOC 2, HIPAA, GDPR, ISO 27001</td>
      <td>SOC 2, HIPAA, GDPR, ISO 27001, FedRAMP</td>
      <td>SOC 2, HIPAA, GDPR, ISO 27001</td>
    </tr>
  </tbody>
</table>

<h3 id="53-도구-통합-및-확장">5.3 도구 통합 및 확장</h3>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>Bedrock AgentCore</th>
      <th>Azure AI Foundry</th>
      <th>Agent Engine</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>MCP 지원</td>
      <td>✅ Gateway 서비스</td>
      <td>✅ (Agent Framework)</td>
      <td>✅ (ADK)</td>
    </tr>
    <tr>
      <td>A2A 지원</td>
      <td>✅ (Strands)</td>
      <td>✅ (Agent Framework)</td>
      <td>✅ (ADK)</td>
    </tr>
    <tr>
      <td>코드 실행</td>
      <td>✅ Code Interpreter</td>
      <td>✅</td>
      <td>제한적</td>
    </tr>
    <tr>
      <td>브라우저 자동화</td>
      <td>✅ Browser 서비스</td>
      <td>✅ (Playwright)</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>외부 서비스 통합</td>
      <td>Salesforce, Zoom, Jira, Slack</td>
      <td>Bing, Azure AI Search, Logic Apps</td>
      <td>Google Search, Cloud Functions</td>
    </tr>
  </tbody>
</table>

<h3 id="54-관측성-및-평가">5.4 관측성 및 평가</h3>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>Bedrock AgentCore</th>
      <th>Azure AI Foundry</th>
      <th>Agent Engine</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>트레이싱</td>
      <td>OpenTelemetry 호환</td>
      <td>Application Insights</td>
      <td>Cloud Trace</td>
    </tr>
    <tr>
      <td>평가 서비스</td>
      <td>✅ Evaluations (자동 품질 평가)</td>
      <td>✅ (groundedness, relevance, coherence)</td>
      <td>제한적</td>
    </tr>
    <tr>
      <td>실험 관리</td>
      <td>CloudWatch</td>
      <td>Azure Experiments</td>
      <td>Vertex AI Experiments</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="6-오픈소스-대안-런타임-레이어">6. 오픈소스 대안: 런타임 레이어</h2>

<p>프레임워크 레벨에서는 ADK(Apache 2.0), Strands(Apache 2.0), Semantic Kernel(MIT) 모두 오픈소스이므로 Lock-in이 없습니다. 진짜 문제는 <strong>매니지드 런타임을 오픈소스로 어떻게 대체하느냐</strong>입니다.</p>

<h3 id="61-langgraph-mit--프레임워크--체크포인팅--메모리">6.1 LangGraph (MIT) — 프레임워크 + 체크포인팅 + 메모리</h3>

<p>LangGraph 프레임워크 자체가 MIT 라이선스로 체크포인팅과 메모리를 내장하고 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langgraph.graph</span> <span class="kn">import</span> <span class="n">StateGraph</span>
<span class="kn">from</span> <span class="nn">langgraph.checkpoint.postgres</span> <span class="kn">import</span> <span class="n">PostgresSaver</span>

<span class="n">checkpointer</span> <span class="o">=</span> <span class="n">PostgresSaver</span><span class="p">.</span><span class="n">from_conn_string</span><span class="p">(</span><span class="s">"postgresql://..."</span><span class="p">)</span>

<span class="n">graph</span> <span class="o">=</span> <span class="n">StateGraph</span><span class="p">(</span><span class="n">State</span><span class="p">)</span>
<span class="n">graph</span><span class="p">.</span><span class="n">add_node</span><span class="p">(</span><span class="s">"agent"</span><span class="p">,</span> <span class="n">agent_node</span><span class="p">)</span>
<span class="c1"># ... 그래프 정의
</span>
<span class="n">app</span> <span class="o">=</span> <span class="n">graph</span><span class="p">.</span><span class="nb">compile</span><span class="p">(</span><span class="n">checkpointer</span><span class="o">=</span><span class="n">checkpointer</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>제공 기능:</strong></p>

<ul>
  <li>Durable execution (장애 자동 복구)</li>
  <li>PostgreSQL 기반 체크포인팅 (<code class="language-plaintext highlighter-rouge">langgraph-checkpoint-postgres</code>, MIT)</li>
  <li>단기/장기 메모리</li>
  <li>Human-in-the-Loop</li>
  <li>스트리밍</li>
</ul>

<p><strong>직접 구축해야 하는 것:</strong> API 서버(FastAPI), 세션 라우팅, 인증/인가, 스케일링, 모니터링</p>

<p>⚠️ <strong>주의</strong>: LangGraph Platform(구 LangSmith Deployments)은 Elastic License 2.0으로, OSI 승인 오픈소스가 아닙니다. 셀프호스팅에는 라이선스 키가 필요하고, Enterprise 계약이 요구됩니다.</p>

<h3 id="62-aegra-apache-20--langgraph-platform-드롭인-대체">6.2 Aegra (Apache 2.0) — LangGraph Platform 드롭인 대체</h3>

<p>LangGraph SDK와 API를 그대로 사용하면서 자체 인프라에서 PostgreSQL 영속성과 함께 운영할 수 있는 Apache 2.0 프로젝트입니다.</p>

<p><strong>제공 기능:</strong></p>

<ul>
  <li>LangGraph SDK 호환 (기존 코드 수정 없이 사용)</li>
  <li>Agent Protocol 스펙 구현</li>
  <li>체크포인트 포함 내구성 있는 대화 저장</li>
  <li>JWT/OAuth/Firebase 인증</li>
  <li>Docker Compose 5분 배포</li>
  <li>OpenTelemetry 관측성</li>
</ul>

<p><strong>성숙도</strong>: v0.8.x 초기 단계로 대규모 프로덕션 검증 사례가 부족합니다. PoC/평가 용도로 적합합니다.</p>

<h3 id="63-dify-수정-apache-20--노코드로우코드-플랫폼">6.3 Dify (수정 Apache 2.0) — 노코드/로우코드 플랫폼</h3>

<p>⚠️ “Apache 2.0”으로 소개되지만 추가 제한 조건이 있습니다:</p>

<ul>
  <li>멀티테넌트 서비스 운영 시 별도 상업 라이선스 필요</li>
  <li>프론트엔드 로고/저작권 정보 제거 불가</li>
</ul>

<p><strong>제공 기능:</strong> 시각적 워크플로 빌더, 빌트인 RAG, 지식 베이스, 100+ LLM 지원, 셀프호스팅</p>

<p>고객사에 멀티테넌트 SaaS로 제공하지 않는다면 사용 가능하지만, 금융권 등 라이선스 심사가 엄격한 환경에서는 주의가 필요합니다.</p>

<h3 id="64-mem0-apache-20--메모리-전용-레이어">6.4 Mem0 (Apache 2.0) — 메모리 전용 레이어</h3>

<p>에이전트 프레임워크가 아닌 <strong>메모리 계층 전용</strong> 오픈소스입니다.</p>

<ul>
  <li>Apache 2.0 라이선스</li>
  <li>온프레미스/프라이빗 클라우드/K8s 배포</li>
  <li>ADK, LangGraph, Strands 등과 조합하여 장기 메모리 레이어로 활용</li>
  <li>50,000+ 개발자 사용 중</li>
</ul>

<h3 id="65-오픈소스-조합-전략-비교">6.5 오픈소스 조합 전략 비교</h3>

<table>
  <thead>
    <tr>
      <th>조합</th>
      <th>세션관리</th>
      <th>체크포인팅</th>
      <th>메모리</th>
      <th>API 서빙</th>
      <th>라이선스</th>
      <th>성숙도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LangGraph + FastAPI + PostgreSQL</td>
      <td>직접 구현</td>
      <td>✅ 빌트인</td>
      <td>✅ 빌트인</td>
      <td>직접 구현</td>
      <td>MIT</td>
      <td>높음</td>
    </tr>
    <tr>
      <td>Aegra</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
      <td>Apache 2.0</td>
      <td>초기</td>
    </tr>
    <tr>
      <td>ADK + Cloud Run + Firestore</td>
      <td>직접 구현</td>
      <td>직접 구현</td>
      <td>직접 구현</td>
      <td>Cloud Run</td>
      <td>Apache 2.0</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>Strands + Lambda/EKS</td>
      <td>직접 구현</td>
      <td>직접 구현</td>
      <td>직접 구현</td>
      <td>Lambda/EKS</td>
      <td>Apache 2.0</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>LangGraph Platform (셀프호스팅)</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
      <td>Elastic 2.0 ⚠️</td>
      <td>매우 높음</td>
    </tr>
    <tr>
      <td>Dify 셀프호스팅</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
      <td>✅</td>
      <td>수정 Apache 2.0 ⚠️</td>
      <td>높음</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="7-lock-in-리스크-종합-매트릭스">7. Lock-in 리스크 종합 매트릭스</h2>

<h3 id="71-프레임워크-레벨">7.1 프레임워크 레벨</h3>

<table>
  <thead>
    <tr>
      <th>프레임워크</th>
      <th>라이선스</th>
      <th>멀티 모델</th>
      <th>멀티 클라우드 배포</th>
      <th>Lock-in</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ADK</td>
      <td>Apache 2.0</td>
      <td>✅ (Gemini 최적화)</td>
      <td>✅ Docker/K8s</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>Strands Agents</td>
      <td>Apache 2.0</td>
      <td>✅ (Bedrock, Anthropic, Ollama 등)</td>
      <td>✅ Docker/K8s</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>Semantic Kernel / Agent Framework</td>
      <td>MIT</td>
      <td>✅ (OpenAI, Claude, Bedrock, Ollama 등)</td>
      <td>✅ Docker/K8s</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>LangGraph</td>
      <td>MIT</td>
      <td>✅</td>
      <td>✅ Docker/K8s</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>CrewAI</td>
      <td>MIT</td>
      <td>✅</td>
      <td>✅ Docker/K8s</td>
      <td>낮음</td>
    </tr>
  </tbody>
</table>

<p><strong>결론</strong>: 프레임워크 레벨에서는 3대 CSP 모두 오픈소스이며, Lock-in 리스크가 낮습니다.</p>

<h3 id="72-매니지드-런타임-레벨">7.2 매니지드 런타임 레벨</h3>

<table>
  <thead>
    <tr>
      <th>런타임</th>
      <th>프레임워크 제한</th>
      <th>모델 제한</th>
      <th>데이터 이식성</th>
      <th>대체 난이도</th>
      <th>Lock-in</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Bedrock AgentCore</td>
      <td>없음 (프레임워크 무관)</td>
      <td>없음 (모델 무관)</td>
      <td>중간 (Memory API)</td>
      <td>높음</td>
      <td>중간~높음</td>
    </tr>
    <tr>
      <td>Azure AI Foundry</td>
      <td>없음 (Agent Framework 중심이나 타 지원)</td>
      <td>OpenAI 중심</td>
      <td>낮음 (M365 통합)</td>
      <td>높음</td>
      <td>높음</td>
    </tr>
    <tr>
      <td>Agent Engine</td>
      <td>ADK 중심 (타 지원)</td>
      <td>Gemini 중심</td>
      <td>중간</td>
      <td>중간 (Cloud Run 대안)</td>
      <td>중간~높음</td>
    </tr>
  </tbody>
</table>

<h3 id="73-lock-in-유형별-분석">7.3 Lock-in 유형별 분석</h3>

<p><strong>1. 모델 Lock-in</strong></p>

<ul>
  <li><strong>AWS</strong>: 가장 적음. 7개 이상의 모델 공급자 지원</li>
  <li><strong>Azure</strong>: 가장 높음. Azure OpenAI가 중심이며 Claude 미지원</li>
  <li><strong>GCP</strong>: 중간. Gemini 최적화이나 Model Garden을 통해 Claude, Llama 등 접근 가능</li>
</ul>

<p><strong>2. 인프라 Lock-in</strong></p>

<ul>
  <li><strong>AWS</strong>: AgentCore의 microVM 세션 격리 모델은 AWS 고유</li>
  <li><strong>Azure</strong>: Entra ID, M365, Copilot Studio 통합은 Azure 고유</li>
  <li><strong>GCP</strong>: VPC-SC, CMEK는 GCP 고유이나, Cloud Run 배포로 런타임 Lock-in 회피 가능</li>
</ul>

<p><strong>3. 데이터/메모리 Lock-in</strong></p>

<ul>
  <li><strong>3사 모두</strong>: 매니지드 메모리 서비스의 데이터를 다른 플랫폼으로 이식하기 어려움</li>
  <li><strong>완화 전략</strong>: PostgreSQL/Redis 같은 표준 저장소에 메모리를 직접 저장하면 이식성 확보</li>
</ul>

<p><strong>4. 생태계 Lock-in</strong></p>

<ul>
  <li><strong>AWS</strong>: AWS Lambda, Step Functions, CloudWatch 등과 깊은 통합</li>
  <li><strong>Azure</strong>: M365, Teams, Dynamics, Power Platform과의 통합이 강력하지만 탈출 비용도 높음</li>
  <li><strong>GCP</strong>: BigQuery, Firestore, Cloud Functions과의 통합</li>
</ul>

<hr />

<h2 id="8-실무-의사결정-프레임워크">8. 실무 의사결정 프레임워크</h2>

<h3 id="81-어떤-csp-런타임을-선택할-것인가">8.1 어떤 CSP 런타임을 선택할 것인가</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>기존 클라우드가 있는가?
├── AWS 사용 중 → Bedrock AgentCore (프레임워크 무관 강점)
├── Azure 사용 중 → AI Foundry Agent Service (M365 통합 강점)
├── GCP 사용 중 → Agent Engine 또는 ADK + Cloud Run
└── 멀티 클라우드 / 없음
    ├── 모델 선택 자유도 우선 → AWS (가장 넓은 모델 선택지)
    ├── 엔터프라이즈 통합 우선 → Azure (M365/Copilot 생태계)
    └── 비용 우선 → GCP (Gemini 기준 가장 저렴)
</code></pre></div></div>

<h3 id="82-오픈소스만으로-구축하고-싶다면">8.2 오픈소스만으로 구축하고 싶다면</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>라이선스 리스크 제로가 필요한가?
├── Yes → LangGraph(MIT) + FastAPI + PostgreSQL
│         또는 ADK(Apache 2.0) + Docker/K8s
├── 초기이지만 올인원이 필요 → Aegra(Apache 2.0) 평가
└── No (약간의 제한 허용)
    ├── 노코드 필요 → Dify (수정 Apache 2.0)
    └── 프로덕션 검증 최우선 → LangGraph Platform (Elastic 2.0)
</code></pre></div></div>

<h3 id="83-하이브리드-전략">8.3 하이브리드 전략</h3>

<p>가장 현실적인 접근은 <strong>프레임워크는 오픈소스, 런타임은 CSP</strong>입니다.</p>

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>프레임워크</th>
      <th>런타임</th>
      <th>Lock-in</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GCP 최적</td>
      <td>ADK (Apache 2.0)</td>
      <td>Cloud Run (Agent Engine 미사용)</td>
      <td>낮음</td>
      <td>세션/메모리 직접 구현 필요</td>
    </tr>
    <tr>
      <td>AWS 최적</td>
      <td>Strands (Apache 2.0)</td>
      <td>AgentCore Runtime만 사용</td>
      <td>중간</td>
      <td>모듈 선택적 사용으로 종속 최소화</td>
    </tr>
    <tr>
      <td>Azure 최적</td>
      <td>Semantic Kernel (MIT)</td>
      <td>Azure AI Foundry</td>
      <td>중간~높음</td>
      <td>M365 생태계 활용 시 가치 극대화</td>
    </tr>
    <tr>
      <td>멀티 클라우드</td>
      <td>LangGraph (MIT)</td>
      <td>Docker/K8s + PostgreSQL</td>
      <td>최소</td>
      <td>운영 부담 높지만 이식성 최대</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="9-금융규제-산업을-위한-추가-고려사항">9. 금융·규제 산업을 위한 추가 고려사항</h2>

<p>금융, 의료, 공공 등 규제 산업에서는 추가적인 Lock-in 고려가 필요합니다.</p>

<h3 id="데이터-주권">데이터 주권</h3>

<ul>
  <li><strong>AWS</strong>: 리전 선택 가능, Data residency 보장</li>
  <li><strong>Azure</strong>: 리전 선택 + Data residency + Sovereign Cloud</li>
  <li><strong>GCP</strong>: 리전 선택 + VPC-SC + Data Residency 제어</li>
</ul>

<h3 id="라이선스-감사-대비">라이선스 감사 대비</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>완전한 오픈소스만 사용하고 싶다면:
✅ ADK (Apache 2.0)
✅ Strands (Apache 2.0)
✅ Semantic Kernel (MIT)
✅ LangGraph (MIT)
✅ CrewAI (MIT)
✅ Mem0 (Apache 2.0)
✅ PostgreSQL (PostgreSQL License)
✅ Redis (BSD)
✅ FastAPI (MIT)

⚠️ 주의가 필요한 것:
⚠️ LangGraph Platform — Elastic License 2.0 (OSI 미승인)
⚠️ Dify — 수정 Apache 2.0 (멀티테넌트 제한)
⚠️ 각 CSP 매니지드 서비스 — 상용 서비스 약관
</code></pre></div></div>

<h3 id="탈출-비용-추정">탈출 비용 추정</h3>

<table>
  <thead>
    <tr>
      <th>전환 시나리오</th>
      <th>프레임워크 전환 비용</th>
      <th>런타임 전환 비용</th>
      <th>데이터 마이그레이션</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GCP → AWS</td>
      <td>중간 (ADK → Strands 코드 전환)</td>
      <td>높음 (Agent Engine → AgentCore)</td>
      <td>높음</td>
    </tr>
    <tr>
      <td>AWS → GCP</td>
      <td>중간 (Strands → ADK 코드 전환)</td>
      <td>높음 (AgentCore → Agent Engine)</td>
      <td>높음</td>
    </tr>
    <tr>
      <td>CSP → 셀프호스팅</td>
      <td>낮음 (오픈소스 프레임워크 유지)</td>
      <td>매우 높음 (전체 인프라 구축)</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>LangGraph(MIT) 기반</td>
      <td>없음</td>
      <td>낮음 (Docker/K8s 이식)</td>
      <td>낮음 (PostgreSQL)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="10-정리">10. 정리</h2>

<h3 id="3대-csp의-공통-전략">3대 CSP의 공통 전략</h3>

<ol>
  <li><strong>프레임워크는 오픈소스로 공개</strong>하여 개발자 생태계를 확보</li>
  <li><strong>매니지드 런타임에서 차별화</strong>하여 수익 창출</li>
  <li><strong>타사 프레임워크도 지원</strong>하여 런타임 Lock-in 유도</li>
</ol>

<h3 id="실무-권장">실무 권장</h3>

<ul>
  <li><strong>프레임워크 선택은 자유롭게</strong>: 3사 모두 오픈소스이므로 기술적 적합성으로 선택</li>
  <li><strong>런타임은 현재 CSP에 맞춰</strong>: 기존 클라우드 인프라와의 통합 비용이 전환 비용보다 낮음</li>
  <li><strong>탈출 전략은 미리 준비</strong>: 표준 프로토콜(MCP, A2A, OpenAPI) 활용, 메모리는 PostgreSQL 등 이식 가능한 저장소 사용</li>
  <li><strong>완전한 오픈소스 구축은 가능하지만 대가가 있음</strong>: 세션 관리, 보안, 스케일링, 모니터링을 직접 구현해야 하는 운영 부담</li>
</ul>

<blockquote>
  <p>Agent Engine이 제공하는 수준의 “완전 매니지드 + 완전 오픈소스” 조합은 아직 시장에 존재하지 않습니다. 어딘가에서는 직접 구현하거나, 라이선스 제약을 받아들이거나, 또는 클라우드 비용을 지불해야 합니다. 이것이 현재 에이전트 인프라 생태계의 현실입니다.</p>
</blockquote>

<hr />

<h2 id="참고-링크">참고 링크</h2>

<ul>
  <li><a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/what-is-bedrock-agentcore.html">Amazon Bedrock AgentCore 공식 문서</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/">Azure AI Foundry Agent Service</a></li>
  <li><a href="https://cloud.google.com/agent-builder/agent-engine">Vertex AI Agent Engine</a></li>
  <li><a href="https://google.github.io/adk-docs/">Google ADK 공식 문서</a></li>
  <li><a href="https://strandsagents.com/">Strands Agents SDK</a></li>
  <li><a href="https://devblogs.microsoft.com/semantic-kernel/introducing-microsoft-agent-framework/">Microsoft Agent Framework</a></li>
  <li><a href="https://github.com/langchain-ai/langgraph">LangGraph GitHub</a></li>
  <li><a href="https://github.com/aegra-ai/aegra">Aegra GitHub</a></li>
  <li><a href="https://github.com/mem0ai/mem0">Mem0 GitHub</a></li>
  <li><a href="https://github.com/langgenius/dify">Dify GitHub</a></li>
</ul>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="agent-engine" /><category term="bedrock-agentcore" /><category term="azure-ai-foundry" /><category term="adk" /><category term="strands-agents" /><category term="semantic-kernel" /><category term="langgraph" /><category term="open-source" /><category term="lock-in" /><category term="multi-cloud" /><summary type="html"><![CDATA[AI Agent를 프로덕션에 배포하려면 두 가지 축을 결정해야 합니다. 에이전트 프레임워크(개발)와 매니지드 런타임(배포·운영)입니다. 3대 CSP(AWS, Azure, GCP)는 각각 이 두 축에 대해 자체 솔루션을 제공하면서 동시에 오픈소스 프레임워크도 지원하는 전략을 취하고 있습니다.]]></summary></entry><entry><title type="html">Firestore로 AI Agent Instruction과 Semantic View를 계층적으로 관리하는 설계</title><link href="https://seonghak.com/blog/2026/03/24/firestore-hierarchical-instruction-semantic-view/" rel="alternate" type="text/html" title="Firestore로 AI Agent Instruction과 Semantic View를 계층적으로 관리하는 설계" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/firestore-hierarchical-instruction-semantic-view</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/firestore-hierarchical-instruction-semantic-view/"><![CDATA[<p>AI Agent 기반 시스템을 운영하다 보면, <strong>“이 에이전트에게 어떤 지시(Instruction)를 줄 것인가”</strong>와 <strong>“어떤 데이터를 어떤 관점(Semantic View)으로 볼 것인가”</strong>를 체계적으로 관리해야 하는 시점이 옵니다.</p>

<p>특히 기업 환경에서는 개인이 만든 설정을 팀에 공유하고, 검증된 것을 전사 표준으로 승격시키는 <strong>계층적 관리 구조</strong>가 필수입니다.</p>

<p>이 글에서는 Firestore를 활용하여 Instruction과 Semantic View를 <strong>사용자 → 부서 → 글로벌</strong> 3단계로 관리하고, 특정 에이전트에 배정하는 구조에 대해서 설명합니다.</p>

<p>가상의 기업 SH은행을 예로 들어 구체적인 설계 방법을 살펴보겠습니다.</p>

<hr />

<h2 id="1-왜-계층적-관리가-필요한가">1. 왜 계층적 관리가 필요한가</h2>

<p>AI Agent 시스템에서 Instruction과 Semantic View를 단순히 1:1로 관리하면 다음과 같은 문제가 발생합니다.</p>

<table>
  <thead>
    <tr>
      <th>문제</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>중복 작업</td>
      <td>같은 팀원 5명이 비슷한 Instruction을 각자 작성</td>
    </tr>
    <tr>
      <td>품질 불균형</td>
      <td>숙련자의 노하우가 개인에게만 존재</td>
    </tr>
    <tr>
      <td>표준 부재</td>
      <td>부서마다 다른 Semantic View로 동일 데이터를 다르게 해석</td>
    </tr>
    <tr>
      <td>관리 불가</td>
      <td>수백 개의 개인 설정이 산재하여 어떤 것이 검증된 것인지 파악 불가</td>
    </tr>
  </tbody>
</table>

<p>이를 해결하기 위한 핵심 개념이 <strong>3단계 계층 구조</strong>입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>글로벌 (Global)         ← 전사 표준. 관리자가 배포
  ↑ 승격(Promote)
부서 (Department)       ← 팀 내 공유. 팀 리더가 관리
  ↑ 승격(Promote)
사용자 (User)           ← 개인 작업 공간. 자유롭게 실험
</code></pre></div></div>

<hr />

<h2 id="2-관리-대상-정의">2. 관리 대상 정의</h2>

<p>설계에 앞서, 이 시스템이 관리하는 두 가지 핵심 리소스를 명확히 정의합니다.</p>

<h3 id="2-1-instruction-에이전트-지시">2-1. Instruction (에이전트 지시)</h3>

<p>AI Agent에게 전달하는 행동 지침입니다. 시스템 프롬프트, 응답 규칙, 제약 조건 등을 포함합니다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">금융</span><span class="nv"> </span><span class="s">상담</span><span class="nv"> </span><span class="s">에이전트</span><span class="nv"> </span><span class="s">지시"</span>
<span class="na">content</span><span class="pi">:</span> <span class="pi">|</span>
  <span class="s">당신은 SH은행의 금융 상담 AI 어시스턴트입니다.</span>
  <span class="s">- 고객 질문에 정확하고 친절하게 답변합니다.</span>
  <span class="s">- 투자 권유는 하지 않습니다.</span>
  <span class="s">- 금리 정보는 반드시 최신 기준으로 안내합니다.</span>
  <span class="s">- 답변 끝에 "추가 문의 사항이 있으시면 말씀해 주세요"를 붙입니다.</span>
</code></pre></div></div>

<h3 id="2-2-semantic-view-시맨틱-뷰">2-2. Semantic View (시맨틱 뷰)</h3>

<p>데이터를 특정 관점으로 바라보는 가상의 뷰 정의입니다. 에이전트가 데이터에 접근할 때 어떤 테이블을 어떤 관점으로 조회할지를 YAML로 기술합니다.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">SV_예금잔액"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">일별</span><span class="nv"> </span><span class="s">전체</span><span class="nv"> </span><span class="s">예금</span><span class="nv"> </span><span class="s">잔액</span><span class="nv"> </span><span class="s">현황"</span>
<span class="na">base_table</span><span class="pi">:</span> <span class="s2">"</span><span class="s">banking.daily_deposit_balance"</span>
<span class="na">columns</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">base_date</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">기준일자"</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">total_balance</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">총</span><span class="nv"> </span><span class="s">예금잔액</span><span class="nv"> </span><span class="s">(원)"</span>
<span class="na">filters</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">base_date</span><span class="nv"> </span><span class="s">&gt;=</span><span class="nv"> </span><span class="s">CURRENT_DATE</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">30"</span>
</code></pre></div></div>

<hr />

<h2 id="3-firestore-컬렉션-설계">3. Firestore 컬렉션 설계</h2>

<p>Firestore의 특성(문서 기반, 서브컬렉션, 컬렉션 그룹 쿼리, 경로 기반 보안 규칙)을 최대한 활용하는 <strong>계층적 컬렉션 구조</strong>를 채택합니다.</p>

<h3 id="3-1-전체-구조">3-1. 전체 구조</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Firestore]
│
├── instructions/                              ← Instruction 최상위 컬렉션
│   ├── _global/                               ← 글로벌 scope
│   │     └── workspaces/
│   │           └── 표준_상담/
│   │                 └── items/
│   │                       ├── 금융상담_기본
│   │                       └── 리스크_안내
│   │
│   ├── dept:IT팀/                             ← 부서 scope
│   │     └── workspaces/
│   │           └── IT_운영/
│   │                 └── items/
│   │                       └── 장애대응_가이드
│   │
│   └── user:hong@sh-bank.com/                      ← 사용자 scope
│         └── workspaces/
│               ├── 내_실험/
│               │     └── items/
│               │           ├── 테스트_지시_v1
│               │           └── 테스트_지시_v2
│               └── 상담_커스텀/
│                     └── items/
│                           └── VIP_상담_지시
│
├── semantic_views/                            ← Semantic View 최상위 컬렉션
│   ├── _global/
│   │     └── workspaces/
│   │           └── 표준_KPI/
│   │                 └── views/
│   │                       ├── SV_예금잔액
│   │                       └── SV_고객수
│   │
│   ├── dept:IT팀/
│   │     └── workspaces/
│   │           └── IT_대시보드/
│   │                 └── views/
│   │                       └── SV_서버현황
│   │
│   └── user:hong@sh-bank.com/
│         └── workspaces/
│               ├── 예금분석/
│               │     └── views/
│               │           ├── SV_예금잔액
│               │           └── SV_예금추이
│               └── 대출리포트/
│                     └── views/
│                           └── SV_연체현황
│
└── agent_assignments/                         ← 에이전트 배정 컬렉션
      └── {agent_id}/
            └── config
                  ├── instruction_ref: "..."
                  └── semantic_view_refs: [...]
</code></pre></div></div>

<h3 id="3-2-경로-패턴">3-2. 경로 패턴</h3>

<p>두 리소스 모두 동일한 경로 패턴을 따릅니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{resource_type}/{scope}/workspaces/{workspace_id}/{item_collection}/{item_id}
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>세그먼트</th>
      <th>설명</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">resource_type</code></td>
      <td>최상위 컬렉션</td>
      <td><code class="language-plaintext highlighter-rouge">instructions</code>, <code class="language-plaintext highlighter-rouge">semantic_views</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">scope</code></td>
      <td>계층 식별자</td>
      <td><code class="language-plaintext highlighter-rouge">_global</code>, <code class="language-plaintext highlighter-rouge">dept:IT팀</code>, <code class="language-plaintext highlighter-rouge">user:hong@sh-bank.com</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">workspace_id</code></td>
      <td>워크스페이스(세트) 이름</td>
      <td><code class="language-plaintext highlighter-rouge">표준_KPI</code>, <code class="language-plaintext highlighter-rouge">예금분석</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">item_collection</code></td>
      <td>아이템 서브컬렉션</td>
      <td><code class="language-plaintext highlighter-rouge">items</code>(Instruction), <code class="language-plaintext highlighter-rouge">views</code>(Semantic View)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">item_id</code></td>
      <td>개별 아이템</td>
      <td><code class="language-plaintext highlighter-rouge">금융상담_기본</code>, <code class="language-plaintext highlighter-rouge">SV_예금잔액</code></td>
    </tr>
  </tbody>
</table>

<h3 id="3-3-scope-문서-id-규칙">3-3. Scope 문서 ID 규칙</h3>

<table>
  <thead>
    <tr>
      <th>Scope</th>
      <th>문서 ID 패턴</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>글로벌</td>
      <td><code class="language-plaintext highlighter-rouge">_global</code></td>
      <td><code class="language-plaintext highlighter-rouge">_global</code></td>
    </tr>
    <tr>
      <td>부서</td>
      <td><code class="language-plaintext highlighter-rouge">dept:{dept_id}</code></td>
      <td><code class="language-plaintext highlighter-rouge">dept:IT팀</code></td>
    </tr>
    <tr>
      <td>사용자</td>
      <td><code class="language-plaintext highlighter-rouge">user:{email}</code></td>
      <td><code class="language-plaintext highlighter-rouge">user:hong@sh-bank.com</code></td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">_global</code>은 언더스코어 접두사로 항상 정렬 최상단에 위치합니다. <code class="language-plaintext highlighter-rouge">dept:</code>와 <code class="language-plaintext highlighter-rouge">user:</code> 접두사로 scope 종류를 경로만 보고 즉시 식별할 수 있습니다.</p>

<hr />

<h2 id="4-문서-스키마-설계">4. 문서 스키마 설계</h2>

<h3 id="4-1-workspace-워크스페이스-문서">4-1. Workspace (워크스페이스) 문서</h3>

<p>사용자가 관련 리소스를 논리적으로 묶는 단위입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># instructions/{scope}/workspaces/{workspace_id}
# semantic_views/{scope}/workspaces/{workspace_id}
</span><span class="p">{</span>
    <span class="s">"name"</span><span class="p">:</span> <span class="s">"예금 분석"</span><span class="p">,</span>
    <span class="s">"description"</span><span class="p">:</span> <span class="s">"예금 관련 시맨틱뷰 모음"</span><span class="p">,</span>
    <span class="s">"owner_id"</span><span class="p">:</span> <span class="s">"hong@sh-bank.com"</span><span class="p">,</span>
    <span class="s">"dept_id"</span><span class="p">:</span> <span class="s">"IT팀"</span><span class="p">,</span>
    <span class="s">"scope"</span><span class="p">:</span> <span class="s">"user"</span><span class="p">,</span>               <span class="c1"># "global" | "dept" | "user"
</span>    <span class="s">"tags"</span><span class="p">:</span> <span class="p">[</span><span class="s">"예금"</span><span class="p">,</span> <span class="s">"분석"</span><span class="p">],</span>
    <span class="s">"item_count"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>               <span class="c1"># 비정규화: 목록 화면에서 건수 표시용
</span>    <span class="s">"created_at"</span><span class="p">:</span> <span class="s">"2026-03-24T09:00:00Z"</span><span class="p">,</span>
    <span class="s">"updated_at"</span><span class="p">:</span> <span class="s">"2026-03-24T14:30:00Z"</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="4-2-instruction-문서">4-2. Instruction 문서</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># instructions/{scope}/workspaces/{workspace_id}/items/{item_id}
</span><span class="p">{</span>
    <span class="s">"name"</span><span class="p">:</span> <span class="s">"금융상담 기본 지시"</span><span class="p">,</span>
    <span class="s">"description"</span><span class="p">:</span> <span class="s">"일반 고객 대상 금융 상담 에이전트용 기본 Instruction"</span><span class="p">,</span>
    <span class="s">"content"</span><span class="p">:</span> <span class="s">"당신은 SH은행의 금융 상담 AI 어시스턴트입니다..."</span><span class="p">,</span>
    <span class="s">"content_type"</span><span class="p">:</span> <span class="s">"text"</span><span class="p">,</span>        <span class="c1"># "text" | "yaml" | "json"
</span>    <span class="s">"owner_id"</span><span class="p">:</span> <span class="s">"hong@sh-bank.com"</span><span class="p">,</span>
    <span class="s">"workspace_id"</span><span class="p">:</span> <span class="s">"표준_상담"</span><span class="p">,</span>   <span class="c1"># 역참조 (collection_group 쿼리용)
</span>    <span class="s">"scope"</span><span class="p">:</span> <span class="s">"global"</span><span class="p">,</span>
    <span class="s">"visibility"</span><span class="p">:</span> <span class="s">"published"</span><span class="p">,</span>     <span class="c1"># "draft" | "shared" | "published"
</span>    <span class="s">"promoted_from"</span><span class="p">:</span> <span class="s">""</span><span class="p">,</span>           <span class="c1"># 승격 전 원본 경로
</span>    <span class="s">"assigned_agents"</span><span class="p">:</span> <span class="p">[</span>           <span class="c1"># 이 Instruction을 사용 중인 에이전트 목록
</span>        <span class="s">"agent:financial-advisor"</span><span class="p">,</span>
        <span class="s">"agent:loan-consultant"</span>
    <span class="p">],</span>
    <span class="s">"tags"</span><span class="p">:</span> <span class="p">[</span><span class="s">"상담"</span><span class="p">,</span> <span class="s">"금융"</span><span class="p">,</span> <span class="s">"기본"</span><span class="p">],</span>
    <span class="s">"version"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
    <span class="s">"created_at"</span><span class="p">:</span> <span class="s">"2026-03-24T09:00:00Z"</span><span class="p">,</span>
    <span class="s">"updated_at"</span><span class="p">:</span> <span class="s">"2026-03-24T14:30:00Z"</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="4-3-semantic-view-문서">4-3. Semantic View 문서</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># semantic_views/{scope}/workspaces/{workspace_id}/views/{view_id}
</span><span class="p">{</span>
    <span class="s">"name"</span><span class="p">:</span> <span class="s">"SV_예금잔액"</span><span class="p">,</span>
    <span class="s">"description"</span><span class="p">:</span> <span class="s">"일별 전체 예금 잔액 현황"</span><span class="p">,</span>
    <span class="s">"yaml_string"</span><span class="p">:</span> <span class="s">"base_table: banking.daily_deposit_balance</span><span class="se">\n</span><span class="s">..."</span><span class="p">,</span>
    <span class="s">"owner_id"</span><span class="p">:</span> <span class="s">"hong@sh-bank.com"</span><span class="p">,</span>
    <span class="s">"workspace_id"</span><span class="p">:</span> <span class="s">"예금분석"</span><span class="p">,</span>
    <span class="s">"scope"</span><span class="p">:</span> <span class="s">"user"</span><span class="p">,</span>
    <span class="s">"visibility"</span><span class="p">:</span> <span class="s">"draft"</span><span class="p">,</span>
    <span class="s">"promoted_from"</span><span class="p">:</span> <span class="s">""</span><span class="p">,</span>
    <span class="s">"tags"</span><span class="p">:</span> <span class="p">[</span><span class="s">"예금"</span><span class="p">,</span> <span class="s">"KPI"</span><span class="p">],</span>
    <span class="s">"version"</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
    <span class="s">"created_at"</span><span class="p">:</span> <span class="s">"2026-03-24T09:00:00Z"</span><span class="p">,</span>
    <span class="s">"updated_at"</span><span class="p">:</span> <span class="s">"2026-03-24T14:30:00Z"</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="4-4-agent-assignment-문서">4-4. Agent Assignment 문서</h3>

<p>에이전트에 Instruction과 Semantic View를 배정하는 구조입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># agent_assignments/{agent_id}/config
</span><span class="p">{</span>
    <span class="s">"agent_id"</span><span class="p">:</span> <span class="s">"financial-advisor"</span><span class="p">,</span>
    <span class="s">"agent_name"</span><span class="p">:</span> <span class="s">"금융 상담 에이전트"</span><span class="p">,</span>
    <span class="s">"instruction_ref"</span><span class="p">:</span> <span class="s">"instructions/_global/workspaces/표준_상담/items/금융상담_기본"</span><span class="p">,</span>
    <span class="s">"semantic_view_refs"</span><span class="p">:</span> <span class="p">[</span>
        <span class="s">"semantic_views/_global/workspaces/표준_KPI/views/SV_예금잔액"</span><span class="p">,</span>
        <span class="s">"semantic_views/dept:IT팀/workspaces/IT_대시보드/views/SV_서버현황"</span>
    <span class="p">],</span>
    <span class="s">"assigned_by"</span><span class="p">:</span> <span class="s">"hong@sh-bank.com"</span><span class="p">,</span>
    <span class="s">"override_instruction_ref"</span><span class="p">:</span> <span class="s">""</span><span class="p">,</span>  <span class="c1"># 사용자별 오버라이드 (선택)
</span>    <span class="s">"is_active"</span><span class="p">:</span> <span class="n">true</span><span class="p">,</span>
    <span class="s">"updated_at"</span><span class="p">:</span> <span class="s">"2026-03-24T14:30:00Z"</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="5-쿼리-패턴">5. 쿼리 패턴</h2>

<p>Firestore의 경로 기반 접근과 <code class="language-plaintext highlighter-rouge">collection_group</code> 쿼리를 조합하면, 다양한 조회 시나리오를 효율적으로 처리할 수 있습니다.</p>

<h3 id="5-1-기본-crud-쿼리">5-1. 기본 CRUD 쿼리</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.cloud</span> <span class="kn">import</span> <span class="n">firestore</span>

<span class="n">db</span> <span class="o">=</span> <span class="n">firestore</span><span class="p">.</span><span class="n">Client</span><span class="p">()</span>

<span class="c1"># ── Instruction 쿼리 ──
</span>
<span class="c1"># 1) 내 워크스페이스 목록
</span><span class="n">my_workspaces</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection</span><span class="p">(</span>
    <span class="s">"instructions/user:hong@sh-bank.com/workspaces"</span>
<span class="p">).</span><span class="n">stream</span><span class="p">()</span>

<span class="c1"># 2) 특정 워크스페이스의 Instruction 목록
</span><span class="n">my_instructions</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection</span><span class="p">(</span>
    <span class="s">"instructions/user:hong@sh-bank.com/workspaces/내_실험/items"</span>
<span class="p">).</span><span class="n">stream</span><span class="p">()</span>

<span class="c1"># 3) 글로벌 Instruction 전체
</span><span class="n">global_instructions</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection</span><span class="p">(</span>
    <span class="s">"instructions/_global/workspaces/표준_상담/items"</span>
<span class="p">).</span><span class="n">stream</span><span class="p">()</span>

<span class="c1"># 4) 부서 Instruction 전체
</span><span class="n">dept_instructions</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection</span><span class="p">(</span>
    <span class="s">"instructions/dept:IT팀/workspaces/IT_운영/items"</span>
<span class="p">).</span><span class="n">stream</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="5-2-컬렉션-그룹-쿼리">5-2. 컬렉션 그룹 쿼리</h3>

<p><code class="language-plaintext highlighter-rouge">collection_group</code>을 사용하면 모든 scope의 같은 이름의 서브컬렉션을 한 번에 검색할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 5) 전체 Instruction 검색 (scope 무관)
</span><span class="n">all_instructions</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection_group</span><span class="p">(</span><span class="s">"items"</span><span class="p">).</span><span class="n">stream</span><span class="p">()</span>

<span class="c1"># 6) 전체 Semantic View 검색 (scope 무관)
</span><span class="n">all_views</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection_group</span><span class="p">(</span><span class="s">"views"</span><span class="p">).</span><span class="n">stream</span><span class="p">()</span>

<span class="c1"># 7) 특정 태그가 포함된 Instruction 검색
</span><span class="n">tagged</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection_group</span><span class="p">(</span><span class="s">"items"</span><span class="p">)</span> \
    <span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="s">"tags"</span><span class="p">,</span> <span class="s">"array_contains"</span><span class="p">,</span> <span class="s">"상담"</span><span class="p">)</span> \
    <span class="p">.</span><span class="n">stream</span><span class="p">()</span>

<span class="c1"># 8) 특정 사용자가 만든 모든 Instruction (scope 무관)
</span><span class="n">user_items</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection_group</span><span class="p">(</span><span class="s">"items"</span><span class="p">)</span> \
    <span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="s">"owner_id"</span><span class="p">,</span> <span class="s">"=="</span><span class="p">,</span> <span class="s">"hong@sh-bank.com"</span><span class="p">)</span> \
    <span class="p">.</span><span class="n">stream</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="5-3-내가-볼-수-있는-모든-리소스-조합">5-3. 내가 볼 수 있는 모든 리소스 조합</h3>

<p>사용자가 접근 가능한 리소스는 <strong>글로벌 + 내 부서 + 내 개인</strong> 3가지 scope를 병합합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">get_visible_instructions</span><span class="p">(</span><span class="n">user_email</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dept_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">:</span>
    <span class="s">"""사용자가 접근 가능한 모든 Instruction을 반환"""</span>
    <span class="n">sources</span> <span class="o">=</span> <span class="p">[</span>
        <span class="sa">f</span><span class="s">"instructions/_global/workspaces"</span><span class="p">,</span>
        <span class="sa">f</span><span class="s">"instructions/dept:</span><span class="si">{</span><span class="n">dept_id</span><span class="si">}</span><span class="s">/workspaces"</span><span class="p">,</span>
        <span class="sa">f</span><span class="s">"instructions/user:</span><span class="si">{</span><span class="n">user_email</span><span class="si">}</span><span class="s">/workspaces"</span><span class="p">,</span>
    <span class="p">]</span>

    <span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">source</span> <span class="ow">in</span> <span class="n">sources</span><span class="p">:</span>
        <span class="n">workspaces</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection</span><span class="p">(</span><span class="n">source</span><span class="p">).</span><span class="n">stream</span><span class="p">()</span>
        <span class="k">for</span> <span class="n">ws</span> <span class="ow">in</span> <span class="n">workspaces</span><span class="p">:</span>
            <span class="n">items</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">collection</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">source</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">ws</span><span class="p">.</span><span class="nb">id</span><span class="si">}</span><span class="s">/items"</span><span class="p">).</span><span class="n">stream</span><span class="p">()</span>
            <span class="n">results</span><span class="p">.</span><span class="n">extend</span><span class="p">(</span><span class="n">items</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">results</span>


<span class="n">visible</span> <span class="o">=</span> <span class="n">get_visible_instructions</span><span class="p">(</span><span class="s">"hong@sh-bank.com"</span><span class="p">,</span> <span class="s">"IT팀"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="6-에이전트-배정-구조">6. 에이전트 배정 구조</h2>

<p>이 설계의 핵심 기능 중 하나는 <strong>사용자가 자신의 Instruction을 특정 에이전트에 배정</strong>하는 것입니다.</p>

<h3 id="6-1-배정-모델">6-1. 배정 모델</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│ Instruction │ ──1:N──▶│  Assignment  │◀──N:1── │    Agent    │
└─────────────┘         └─────────────┘         └─────────────┘
                              │
                              │ N:M
                              ▼
                        ┌─────────────┐
                        │Semantic View│
                        └─────────────┘
</code></pre></div></div>

<p>하나의 에이전트는 하나의 Instruction과 여러 Semantic View를 가질 수 있습니다. 사용자별로 같은 에이전트에 다른 Instruction을 배정할 수 있도록 <strong>사용자별 배정 문서</strong>를 분리합니다.</p>

<h3 id="6-2-사용자별-에이전트-배정">6-2. 사용자별 에이전트 배정</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>agent_assignments/
  └── user:hong@sh-bank.com/                    ← 사용자별 배정
        └── agents/
              ├── financial-advisor/        ← 에이전트별 설정
              │     instruction_ref: "instructions/user:hong@sh-bank.com/..."
              │     semantic_view_refs: [...]
              │     use_global_fallback: true
              │
              └── loan-consultant/
                    instruction_ref: "instructions/dept:IT팀/..."
                    semantic_view_refs: [...]
                    use_global_fallback: false
</code></pre></div></div>

<h3 id="6-3-instruction-해석-우선순위">6-3. Instruction 해석 우선순위</h3>

<p>에이전트가 실행될 때, Instruction을 어디서 가져올지 <strong>우선순위 체인</strong>으로 결정합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 사용자별 배정 (user assignment)     ← 최우선
2. 부서별 기본값 (dept default)
3. 글로벌 기본값 (global default)      ← 폴백
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">resolve_instruction</span><span class="p">(</span><span class="n">agent_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">user_email</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">dept_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="s">"""우선순위에 따라 에이전트의 Instruction을 해석"""</span>

    <span class="c1"># 1. 사용자별 배정 확인
</span>    <span class="n">user_assignment</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"agent_assignments/user:</span><span class="si">{</span><span class="n">user_email</span><span class="si">}</span><span class="s">/agents/</span><span class="si">{</span><span class="n">agent_id</span><span class="si">}</span><span class="s">"</span>
    <span class="p">).</span><span class="n">get</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">user_assignment</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
        <span class="n">ref</span> <span class="o">=</span> <span class="n">user_assignment</span><span class="p">.</span><span class="n">to_dict</span><span class="p">().</span><span class="n">get</span><span class="p">(</span><span class="s">"instruction_ref"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">ref</span><span class="p">:</span>
            <span class="n">doc</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span><span class="n">ref</span><span class="p">).</span><span class="n">get</span><span class="p">()</span>
            <span class="k">if</span> <span class="n">doc</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">to_dict</span><span class="p">()</span>

    <span class="c1"># 2. 부서 기본값 확인
</span>    <span class="n">dept_assignment</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"agent_assignments/dept:</span><span class="si">{</span><span class="n">dept_id</span><span class="si">}</span><span class="s">/agents/</span><span class="si">{</span><span class="n">agent_id</span><span class="si">}</span><span class="s">"</span>
    <span class="p">).</span><span class="n">get</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">dept_assignment</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
        <span class="n">ref</span> <span class="o">=</span> <span class="n">dept_assignment</span><span class="p">.</span><span class="n">to_dict</span><span class="p">().</span><span class="n">get</span><span class="p">(</span><span class="s">"instruction_ref"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">ref</span><span class="p">:</span>
            <span class="n">doc</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span><span class="n">ref</span><span class="p">).</span><span class="n">get</span><span class="p">()</span>
            <span class="k">if</span> <span class="n">doc</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">to_dict</span><span class="p">()</span>

    <span class="c1"># 3. 글로벌 기본값
</span>    <span class="n">global_assignment</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"agent_assignments/_global/agents/</span><span class="si">{</span><span class="n">agent_id</span><span class="si">}</span><span class="s">"</span>
    <span class="p">).</span><span class="n">get</span><span class="p">()</span>

    <span class="k">if</span> <span class="n">global_assignment</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
        <span class="n">ref</span> <span class="o">=</span> <span class="n">global_assignment</span><span class="p">.</span><span class="n">to_dict</span><span class="p">().</span><span class="n">get</span><span class="p">(</span><span class="s">"instruction_ref"</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">ref</span><span class="p">:</span>
            <span class="n">doc</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span><span class="n">ref</span><span class="p">).</span><span class="n">get</span><span class="p">()</span>
            <span class="k">if</span> <span class="n">doc</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
                <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">to_dict</span><span class="p">()</span>

    <span class="k">return</span> <span class="bp">None</span>
</code></pre></div></div>

<h3 id="6-4-배정-api-예시">6-4. 배정 API 예시</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">assign_instruction_to_agent</span><span class="p">(</span>
    <span class="n">user_email</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">agent_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">instruction_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">semantic_view_paths</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
<span class="p">):</span>
    <span class="s">"""사용자가 자신의 Instruction을 특정 에이전트에 배정"""</span>
    <span class="n">doc_ref</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span>
        <span class="sa">f</span><span class="s">"agent_assignments/user:</span><span class="si">{</span><span class="n">user_email</span><span class="si">}</span><span class="s">/agents/</span><span class="si">{</span><span class="n">agent_id</span><span class="si">}</span><span class="s">"</span>
    <span class="p">)</span>

    <span class="n">data</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"agent_id"</span><span class="p">:</span> <span class="n">agent_id</span><span class="p">,</span>
        <span class="s">"instruction_ref"</span><span class="p">:</span> <span class="n">instruction_path</span><span class="p">,</span>
        <span class="s">"semantic_view_refs"</span><span class="p">:</span> <span class="n">semantic_view_paths</span> <span class="ow">or</span> <span class="p">[],</span>
        <span class="s">"assigned_by"</span><span class="p">:</span> <span class="n">user_email</span><span class="p">,</span>
        <span class="s">"updated_at"</span><span class="p">:</span> <span class="n">firestore</span><span class="p">.</span><span class="n">SERVER_TIMESTAMP</span><span class="p">,</span>
    <span class="p">}</span>

    <span class="n">doc_ref</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="n">merge</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>


<span class="c1"># 사용 예시
</span><span class="n">assign_instruction_to_agent</span><span class="p">(</span>
    <span class="n">user_email</span><span class="o">=</span><span class="s">"hong@sh-bank.com"</span><span class="p">,</span>
    <span class="n">agent_id</span><span class="o">=</span><span class="s">"financial-advisor"</span><span class="p">,</span>
    <span class="n">instruction_path</span><span class="o">=</span><span class="s">"instructions/user:hong@sh-bank.com/workspaces/상담_커스텀/items/VIP_상담_지시"</span><span class="p">,</span>
    <span class="n">semantic_view_paths</span><span class="o">=</span><span class="p">[</span>
        <span class="s">"semantic_views/_global/workspaces/표준_KPI/views/SV_예금잔액"</span><span class="p">,</span>
        <span class="s">"semantic_views/user:hong@sh-bank.com/workspaces/예금분석/views/SV_예금추이"</span><span class="p">,</span>
    <span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="7-승격promote-흐름">7. 승격(Promote) 흐름</h2>

<p>개인이 만든 리소스를 부서 또는 글로벌로 승격시키는 워크플로입니다.</p>

<h3 id="7-1-승격-단계">7-1. 승격 단계</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>user:hong@sh-bank.com/workspaces/내_실험/items/VIP_상담_지시
        │
        │  ① 부서 공유 (promote to dept)
        ▼
dept:IT팀/workspaces/공유_지시/items/VIP_상담_지시
        │
        │  ② 글로벌 배포 (promote to global)
        ▼
_global/workspaces/표준_상담/items/VIP_상담_지시
</code></pre></div></div>

<h3 id="7-2-승격-구현">7-2. 승격 구현</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">promote_instruction</span><span class="p">(</span>
    <span class="n">source_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">target_scope</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">target_workspace</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
    <span class="n">promoted_by</span><span class="p">:</span> <span class="nb">str</span>
<span class="p">):</span>
    <span class="s">"""Instruction을 상위 scope로 승격(복사)"""</span>
    <span class="n">source_doc</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span><span class="n">source_path</span><span class="p">).</span><span class="n">get</span><span class="p">()</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">source_doc</span><span class="p">.</span><span class="n">exists</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s">"소스 문서를 찾을 수 없습니다: </span><span class="si">{</span><span class="n">source_path</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

    <span class="n">data</span> <span class="o">=</span> <span class="n">source_doc</span><span class="p">.</span><span class="n">to_dict</span><span class="p">()</span>
    <span class="n">item_id</span> <span class="o">=</span> <span class="n">source_path</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">"/"</span><span class="p">)[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span>

    <span class="n">target_path</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"instructions/</span><span class="si">{</span><span class="n">target_scope</span><span class="si">}</span><span class="s">/workspaces/</span><span class="si">{</span><span class="n">target_workspace</span><span class="si">}</span><span class="s">/items/</span><span class="si">{</span><span class="n">item_id</span><span class="si">}</span><span class="s">"</span>

    <span class="n">data</span><span class="p">.</span><span class="n">update</span><span class="p">({</span>
        <span class="s">"promoted_from"</span><span class="p">:</span> <span class="n">source_path</span><span class="p">,</span>
        <span class="s">"promoted_by"</span><span class="p">:</span> <span class="n">promoted_by</span><span class="p">,</span>
        <span class="s">"scope"</span><span class="p">:</span> <span class="s">"global"</span> <span class="k">if</span> <span class="n">target_scope</span> <span class="o">==</span> <span class="s">"_global"</span> <span class="k">else</span> <span class="s">"dept"</span><span class="p">,</span>
        <span class="s">"visibility"</span><span class="p">:</span> <span class="s">"published"</span><span class="p">,</span>
        <span class="s">"promoted_at"</span><span class="p">:</span> <span class="n">firestore</span><span class="p">.</span><span class="n">SERVER_TIMESTAMP</span><span class="p">,</span>
    <span class="p">})</span>

    <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span><span class="n">target_path</span><span class="p">).</span><span class="nb">set</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>

    <span class="c1"># 원본에 승격 이력 기록
</span>    <span class="n">db</span><span class="p">.</span><span class="n">document</span><span class="p">(</span><span class="n">source_path</span><span class="p">).</span><span class="n">update</span><span class="p">({</span>
        <span class="s">"promoted_to"</span><span class="p">:</span> <span class="n">target_path</span><span class="p">,</span>
        <span class="s">"promoted_at"</span><span class="p">:</span> <span class="n">firestore</span><span class="p">.</span><span class="n">SERVER_TIMESTAMP</span><span class="p">,</span>
    <span class="p">})</span>

    <span class="k">return</span> <span class="n">target_path</span>


<span class="c1"># 사용 예시: 개인 → 부서로 승격
</span><span class="n">promote_instruction</span><span class="p">(</span>
    <span class="n">source_path</span><span class="o">=</span><span class="s">"instructions/user:hong@sh-bank.com/workspaces/내_실험/items/VIP_상담_지시"</span><span class="p">,</span>
    <span class="n">target_scope</span><span class="o">=</span><span class="s">"dept:IT팀"</span><span class="p">,</span>
    <span class="n">target_workspace</span><span class="o">=</span><span class="s">"공유_지시"</span><span class="p">,</span>
    <span class="n">promoted_by</span><span class="o">=</span><span class="s">"hong@sh-bank.com"</span>
<span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="8-firestore-보안-규칙">8. Firestore 보안 규칙</h2>

<p>경로 기반 구조의 장점은 <strong>보안 규칙을 간결하게 작성</strong>할 수 있다는 것입니다.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">rules_version</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">2</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">service</span> <span class="nx">cloud</span><span class="p">.</span><span class="nx">firestore</span> <span class="p">{</span>
  <span class="nx">match</span> <span class="o">/</span><span class="nx">databases</span><span class="o">/</span><span class="p">{</span><span class="nx">database</span><span class="p">}</span><span class="sr">/documents </span><span class="err">{
</span>
    <span class="c1">// Instruction 보안 규칙</span>
    <span class="nx">match</span> <span class="o">/</span><span class="nx">instructions</span><span class="o">/</span><span class="p">{</span><span class="nx">scope</span><span class="p">}</span><span class="sr">/workspaces/</span><span class="p">{</span><span class="nx">wsId</span><span class="p">}</span><span class="sr">/items/</span><span class="p">{</span><span class="nx">itemId</span><span class="p">}</span> <span class="p">{</span>

      <span class="c1">// 글로벌: 누구나 읽기 가능, 관리자만 쓰기</span>
      <span class="nx">allow</span> <span class="na">read</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">_global</span><span class="dl">'</span><span class="p">;</span>
      <span class="nx">allow</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">_global</span><span class="dl">'</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">role</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">admin</span><span class="dl">'</span><span class="p">;</span>

      <span class="c1">// 부서: 같은 부서원만 읽기, 팀 리더만 쓰기</span>
      <span class="nx">allow</span> <span class="na">read</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="dl">'</span><span class="s1">dept:.*</span><span class="dl">'</span><span class="p">)</span>
                  <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">dept_id</span> <span class="o">==</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
      <span class="nx">allow</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="dl">'</span><span class="s1">dept:.*</span><span class="dl">'</span><span class="p">)</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">dept_id</span> <span class="o">==</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">is_team_lead</span> <span class="o">==</span> <span class="kc">true</span><span class="p">;</span>

      <span class="c1">// 사용자: 본인만 읽기/쓰기</span>
      <span class="nx">allow</span> <span class="nx">read</span><span class="p">,</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="dl">'</span><span class="s1">user:.*</span><span class="dl">'</span><span class="p">)</span>
                         <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">email</span> <span class="o">==</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="c1">// Semantic View도 동일한 패턴 적용</span>
    <span class="nx">match</span> <span class="o">/</span><span class="nx">semantic_views</span><span class="o">/</span><span class="p">{</span><span class="nx">scope</span><span class="p">}</span><span class="sr">/workspaces/</span><span class="p">{</span><span class="nx">wsId</span><span class="p">}</span><span class="sr">/views/</span><span class="p">{</span><span class="nx">viewId</span><span class="p">}</span> <span class="p">{</span>
      <span class="nx">allow</span> <span class="na">read</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">_global</span><span class="dl">'</span><span class="p">;</span>
      <span class="nx">allow</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">_global</span><span class="dl">'</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">role</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">admin</span><span class="dl">'</span><span class="p">;</span>

      <span class="nx">allow</span> <span class="na">read</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="dl">'</span><span class="s1">dept:.*</span><span class="dl">'</span><span class="p">)</span>
                  <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">dept_id</span> <span class="o">==</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
      <span class="nx">allow</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="dl">'</span><span class="s1">dept:.*</span><span class="dl">'</span><span class="p">)</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">dept_id</span> <span class="o">==</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">]</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">is_team_lead</span> <span class="o">==</span> <span class="kc">true</span><span class="p">;</span>

      <span class="nx">allow</span> <span class="nx">read</span><span class="p">,</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">matches</span><span class="p">(</span><span class="dl">'</span><span class="s1">user:.*</span><span class="dl">'</span><span class="p">)</span>
                         <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">email</span> <span class="o">==</span> <span class="nx">scope</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">)[</span><span class="mi">1</span><span class="p">];</span>
    <span class="p">}</span>

    <span class="c1">// 에이전트 배정: 본인 배정만 수정 가능</span>
    <span class="nx">match</span> <span class="o">/</span><span class="nx">agent_assignments</span><span class="o">/</span><span class="nx">user</span><span class="p">:{</span><span class="nx">userEmail</span><span class="p">}</span><span class="sr">/agents/</span><span class="p">{</span><span class="nx">agentId</span><span class="p">}</span> <span class="p">{</span>
      <span class="nx">allow</span> <span class="nx">read</span><span class="p">,</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">email</span> <span class="o">==</span> <span class="nx">userEmail</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nx">match</span> <span class="o">/</span><span class="nx">agent_assignments</span><span class="o">/</span><span class="nx">dept</span><span class="p">:{</span><span class="nx">deptId</span><span class="p">}</span><span class="sr">/agents/</span><span class="p">{</span><span class="nx">agentId</span><span class="p">}</span> <span class="p">{</span>
      <span class="nx">allow</span> <span class="na">read</span><span class="p">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">dept_id</span> <span class="o">==</span> <span class="nx">deptId</span><span class="p">;</span>
      <span class="nx">allow</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">dept_id</span> <span class="o">==</span> <span class="nx">deptId</span>
                   <span class="o">&amp;&amp;</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">is_team_lead</span> <span class="o">==</span> <span class="kc">true</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nx">match</span> <span class="o">/</span><span class="nx">agent_assignments</span><span class="o">/</span><span class="nx">_global</span><span class="o">/</span><span class="nx">agents</span><span class="o">/</span><span class="p">{</span><span class="nx">agentId</span><span class="p">}</span> <span class="p">{</span>
      <span class="nx">allow</span> <span class="na">read</span><span class="p">:</span> <span class="k">if</span> <span class="kc">true</span><span class="p">;</span>
      <span class="nx">allow</span> <span class="na">write</span><span class="p">:</span> <span class="k">if</span> <span class="nx">request</span><span class="p">.</span><span class="nx">auth</span><span class="p">.</span><span class="nx">token</span><span class="p">.</span><span class="nx">role</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">admin</span><span class="dl">'</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>경로 자체에 scope 정보가 담겨 있으므로, 문서 내부 필드를 검사하는 복잡한 조건 없이도 접근 제어가 가능합니다.</p>

<hr />

<h2 id="9-전체-워크플로-시나리오">9. 전체 워크플로 시나리오</h2>

<p>실제 사용 흐름을 시나리오로 정리합니다.</p>

<h3 id="시나리오-홍길동이-instruction을-만들어-에이전트에-배정하는-전체-과정">시나리오: 홍길동이 Instruction을 만들어 에이전트에 배정하는 전체 과정</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[1단계] 개인 워크스페이스 생성
  └─ POST instructions/user:hong@sh-bank.com/workspaces/상담_커스텀

[2단계] Instruction 작성
  └─ POST .../상담_커스텀/items/VIP_상담_지시
     { content: "VIP 고객 전용 상담 시 존칭 사용..." }

[3단계] 에이전트에 배정
  └─ PUT agent_assignments/user:hong@sh-bank.com/agents/financial-advisor
     { instruction_ref: ".../VIP_상담_지시",
       semantic_view_refs: [".../SV_예금잔액"] }

[4단계] 에이전트 실행 시 Instruction 해석
  └─ resolve_instruction("financial-advisor", "hong@sh-bank.com", "IT팀")
     → 사용자 배정 확인 → VIP_상담_지시 반환

[5단계] 팀에 공유 (승격)
  └─ promote("VIP_상담_지시", "dept:IT팀", "공유_지시")
     → IT팀 전원이 사용 가능

[6단계] 전사 표준 배포 (승격)
  └─ promote("VIP_상담_지시", "_global", "표준_상담")
     → 전사 에이전트 기본 Instruction으로 사용 가능
</code></pre></div></div>

<h3 id="ui-네비게이션-구조">UI 네비게이션 구조</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─ 리소스 선택 ──────────────────────────────────┐
│  [📋 Instructions]    [📊 Semantic Views]       │
├─ Scope 선택 ───────────────────────────────────┤
│  [👤 내 뷰]    [👥 IT팀]    [🌐 글로벌]        │
├─ Workspace 선택 ───────────────────────────────┤
│  📁 상담_커스텀 (2)                              │
│  📁 내_실험 (3)                                  │
├─ Items ────────────────────────────────────────┤
│  ◆ VIP_상담_지시          [에이전트 배정 ▶]     │
│  ◆ 테스트_지시_v1                               │
├─ 에이전트 배정 ────────────────────────────────┤
│  🤖 financial-advisor                           │
│     Instruction: VIP_상담_지시 ✅               │
│     Views: SV_예금잔액, SV_예금추이              │
│  🤖 loan-consultant                             │
│     Instruction: (글로벌 기본값)                 │
│     Views: SV_대출잔액                           │
└────────────────────────────────────────────────┘
</code></pre></div></div>

<hr />

<h2 id="10-인덱스-설계">10. 인덱스 설계</h2>

<p>Firestore에서 <code class="language-plaintext highlighter-rouge">collection_group</code> 쿼리를 사용하려면 <strong>복합 인덱스</strong>를 사전에 생성해야 합니다.</p>

<table>
  <thead>
    <tr>
      <th>컬렉션 그룹</th>
      <th>인덱스 필드</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">items</code></td>
      <td><code class="language-plaintext highlighter-rouge">owner_id</code> ASC, <code class="language-plaintext highlighter-rouge">updated_at</code> DESC</td>
      <td>특정 사용자의 전체 Instruction 최신순</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">items</code></td>
      <td><code class="language-plaintext highlighter-rouge">tags</code> ARRAY, <code class="language-plaintext highlighter-rouge">scope</code> ASC</td>
      <td>태그+scope 필터링</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">views</code></td>
      <td><code class="language-plaintext highlighter-rouge">owner_id</code> ASC, <code class="language-plaintext highlighter-rouge">updated_at</code> DESC</td>
      <td>특정 사용자의 전체 Semantic View 최신순</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">views</code></td>
      <td><code class="language-plaintext highlighter-rouge">tags</code> ARRAY, <code class="language-plaintext highlighter-rouge">scope</code> ASC</td>
      <td>태그+scope 필터링</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">agents</code></td>
      <td><code class="language-plaintext highlighter-rouge">instruction_ref</code> ASC</td>
      <td>특정 Instruction을 사용 중인 에이전트 역추적</td>
    </tr>
  </tbody>
</table>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">//</span><span class="w"> </span><span class="err">firestore.indexes.json</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"indexes"</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><span class="nl">"collectionGroup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"items"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"queryScope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"COLLECTION_GROUP"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"fields"</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><span class="nl">"fieldPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"owner_id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"order"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ASCENDING"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"fieldPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"updated_at"</span><span class="p">,</span><span class="w"> </span><span class="nl">"order"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DESCENDING"</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><span class="p">{</span><span class="w">
      </span><span class="nl">"collectionGroup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"views"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"queryScope"</span><span class="p">:</span><span class="w"> </span><span class="s2">"COLLECTION_GROUP"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"fields"</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><span class="nl">"fieldPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"owner_id"</span><span class="p">,</span><span class="w"> </span><span class="nl">"order"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ASCENDING"</span><span class="w"> </span><span class="p">},</span><span class="w">
        </span><span class="p">{</span><span class="w"> </span><span class="nl">"fieldPath"</span><span class="p">:</span><span class="w"> </span><span class="s2">"updated_at"</span><span class="p">,</span><span class="w"> </span><span class="nl">"order"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DESCENDING"</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><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h2 id="11-설계-판단-요약">11. 설계 판단 요약</h2>

<h3 id="단일-컬렉션-vs-계층-컬렉션">단일 컬렉션 vs 계층 컬렉션</h3>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>단일 컬렉션 + scope 필드</th>
      <th>계층 컬렉션 (채택)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>쿼리 효율</td>
      <td>매번 <code class="language-plaintext highlighter-rouge">where</code> 필터 필요</td>
      <td>경로만으로 scope 분리</td>
    </tr>
    <tr>
      <td>보안 규칙</td>
      <td>문서 필드 기반 복잡한 조건</td>
      <td>경로 기반으로 간결</td>
    </tr>
    <tr>
      <td>전체 검색</td>
      <td>바로 가능</td>
      <td><code class="language-plaintext highlighter-rouge">collection_group</code> 사용</td>
    </tr>
    <tr>
      <td>데이터 격리</td>
      <td>앱 로직에 의존</td>
      <td>구조적으로 격리</td>
    </tr>
    <tr>
      <td>확장성</td>
      <td>문서 수 폭증 시 성능 저하</td>
      <td>scope별 자연 분산</td>
    </tr>
  </tbody>
</table>

<h3 id="instruction-참조-방식-복사-vs-참조">Instruction 참조 방식: 복사 vs 참조</h3>

<table>
  <thead>
    <tr>
      <th>방식</th>
      <th>장점</th>
      <th>단점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>참조 (Firestore 경로)</strong></td>
      <td>원본 수정 시 자동 반영, 저장 공간 절약</td>
      <td>원본 삭제 시 깨짐, 읽기 시 추가 조회</td>
    </tr>
    <tr>
      <td><strong>복사 (승격 시)</strong></td>
      <td>독립적 버전 관리, 원본 변경에 영향 없음</td>
      <td>동기화 불가, 저장 공간 증가</td>
    </tr>
  </tbody>
</table>

<p>이 설계에서는 <strong>에이전트 배정은 참조</strong>, <strong>승격은 복사</strong> 방식을 혼합합니다. 에이전트가 실행 시점에 항상 최신 Instruction을 사용하되, 승격된 리소스는 독립적으로 관리되어 원본 변경에 영향받지 않도록 합니다.</p>

<hr />

<h2 id="정리">정리</h2>

<p>Firestore의 계층적 컬렉션 구조를 활용하면, AI Agent의 Instruction과 Semantic View를 <strong>사용자 → 부서 → 글로벌</strong> 3단계로 자연스럽게 관리할 수 있습니다.</p>

<p>핵심 설계 원칙을 다시 정리하면 다음과 같습니다.</p>

<table>
  <thead>
    <tr>
      <th>원칙</th>
      <th>구현</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>경로가 곧 권한</td>
      <td><code class="language-plaintext highlighter-rouge">_global</code>, <code class="language-plaintext highlighter-rouge">dept:{id}</code>, <code class="language-plaintext highlighter-rouge">user:{email}</code>로 scope 식별</td>
    </tr>
    <tr>
      <td>워크스페이스로 묶기</td>
      <td>관련 리소스를 논리적 세트로 관리</td>
    </tr>
    <tr>
      <td>참조로 배정</td>
      <td>에이전트에 Firestore 경로로 Instruction/View 연결</td>
    </tr>
    <tr>
      <td>복사로 승격</td>
      <td>상위 scope로 올릴 때는 독립 복사본 생성</td>
    </tr>
    <tr>
      <td>우선순위 체인</td>
      <td>사용자 → 부서 → 글로벌 순으로 폴백</td>
    </tr>
    <tr>
      <td>collection_group으로 전체 검색</td>
      <td>scope를 넘나드는 검색은 컬렉션 그룹 쿼리로 해결</td>
    </tr>
  </tbody>
</table>

<p>이 구조는 Firestore의 100단계 중첩 제한 내에서 충분히 동작하며(최대 6단계), 보안 규칙도 경로 패턴만으로 간결하게 작성할 수 있어 운영 부담을 최소화합니다.</p>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="firestore" /><category term="ai-agent" /><category term="semantic-view" /><category term="instruction" /><category term="architecture" /><category term="gcp" /><category term="multi-tenant" /><summary type="html"><![CDATA[AI Agent 기반 시스템을 운영하다 보면, “이 에이전트에게 어떤 지시(Instruction)를 줄 것인가”와 “어떤 데이터를 어떤 관점(Semantic View)으로 볼 것인가”를 체계적으로 관리해야 하는 시점이 옵니다.]]></summary></entry><entry><title type="html">GCP에서 BigQuery 접근 시 내부망 vs 외부망 판별법과 PSC 구성 가이드</title><link href="https://seonghak.com/blog/2026/03/24/gcp-bigquery-network-path-verification-psc/" rel="alternate" type="text/html" title="GCP에서 BigQuery 접근 시 내부망 vs 외부망 판별법과 PSC 구성 가이드" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/gcp-bigquery-network-path-verification-psc</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/gcp-bigquery-network-path-verification-psc/"><![CDATA[<p>Cloud Run, GCE, Vertex AI Agent Engine에서 BigQuery API를 호출할 때, 트래픽이 <strong>Google 내부망</strong>을 타는 건지 <strong>외부 인터넷</strong>을 경유하는 건지 궁금해지는 순간이 있습니다. 특히 금융권이나 공공기관 프로젝트에서는 “정말 내부망으로 통신하는 게 맞느냐”는 질문이 반드시 나옵니다.</p>

<p>이 글에서는 <strong>네트워크 경로를 확인하는 방법</strong>, 서비스 유형별 네트워킹 구조, 그리고 <strong>Private Service Connect(PSC)</strong> 구성까지 실전 관점에서 정리합니다.</p>

<hr />

<h2 id="1-내부망인지-확인하는-2가지-방법">1. 내부망인지 확인하는 2가지 방법</h2>

<h3 id="1-1-dns-해석-결과로-판별-가장-간편">1-1. DNS 해석 결과로 판별 (가장 간편)</h3>

<p>GCE 인스턴스에서 BigQuery API 엔드포인트의 DNS resolve 결과를 보면 힌트를 얻을 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 방법 A: dig로 확인</span>
dig bigquery.googleapis.com

<span class="c"># 방법 B: Python으로 확인</span>
python3 <span class="nt">-c</span> <span class="s2">"import socket; print(socket.getaddrinfo('bigquery.googleapis.com', 443))"</span>
</code></pre></div></div>

<p>실제 테스트 결과 예시:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>;; ANSWER SECTION:
bigquery.googleapis.com. 300    IN      A       34.128.10.106
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[(</span><span class="o">&lt;</span><span class="n">AddressFamily</span><span class="p">.</span><span class="n">AF_INET</span><span class="p">:</span> <span class="mi">2</span><span class="o">&gt;</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">SocketKind</span><span class="p">.</span><span class="n">SOCK_STREAM</span><span class="p">:</span> <span class="mi">1</span><span class="o">&gt;</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="s">''</span><span class="p">,</span> <span class="p">(</span><span class="s">'34.128.10.106'</span><span class="p">,</span> <span class="mi">443</span><span class="p">)),</span>
 <span class="p">(</span><span class="o">&lt;</span><span class="n">AddressFamily</span><span class="p">.</span><span class="n">AF_INET6</span><span class="p">:</span> <span class="mi">10</span><span class="o">&gt;</span><span class="p">,</span> <span class="o">&lt;</span><span class="n">SocketKind</span><span class="p">.</span><span class="n">SOCK_STREAM</span><span class="p">:</span> <span class="mi">1</span><span class="o">&gt;</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="s">''</span><span class="p">,</span> <span class="p">(</span><span class="s">'2600:1900:4250:d::200a'</span><span class="p">,</span> <span class="mi">443</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)),</span>
 <span class="p">...]</span>
</code></pre></div></div>

<p><strong>판별 기준:</strong></p>

<table>
  <thead>
    <tr>
      <th>해석된 IP 대역</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">199.36.153.4/30</code></td>
      <td><code class="language-plaintext highlighter-rouge">restricted.googleapis.com</code> 경유 → VPC SC + 내부망</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">199.36.153.8/30</code></td>
      <td><code class="language-plaintext highlighter-rouge">private.googleapis.com</code> 경유 → 내부망</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">142.250.x.x</code>, <code class="language-plaintext highlighter-rouge">172.217.x.x</code>, <code class="language-plaintext highlighter-rouge">34.128.x.x</code> 등</td>
      <td>Google 공인 IP → 외부 경로 가능성</td>
    </tr>
  </tbody>
</table>

<p>위 테스트 결과에서는 <code class="language-plaintext highlighter-rouge">34.128.10.106</code>과 <code class="language-plaintext highlighter-rouge">2600:1900:4250:d::200a</code>가 나왔는데, 이는 Google 공인 IP 대역입니다. <code class="language-plaintext highlighter-rouge">restricted</code>도 <code class="language-plaintext highlighter-rouge">private</code>도 아닙니다. 즉 <strong>별도의 Private Service Connect나 restricted/private DNS 존 설정이 되어 있지 않다</strong>는 뜻입니다.</p>

<h3 id="1-2-vpc-flow-logs로-확인-가장-확실">1-2. VPC Flow Logs로 확인 (가장 확실)</h3>

<p>VPC Flow Logs를 켜면 실제 트래픽의 목적지 IP를 볼 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># GCE에서 BigQuery API 엔드포인트의 IP 확인</span>
nslookup bigquery.googleapis.com

<span class="c"># Private Google Access가 활성화된 서브넷에서는</span>
<span class="c"># restricted.googleapis.com (199.36.153.4/30) 또는</span>
<span class="c"># private.googleapis.com (199.36.153.8/30) 대역으로 resolve 됨</span>

<span class="c"># 반면 외부 인터넷 경유 시에는 공인 IP로 resolve 됨</span>
</code></pre></div></div>

<p>VPC Flow Logs에서 destination IP가 위의 private 대역이면 내부망을 사용하는 것이 확실합니다.</p>

<hr />

<h2 id="2-공인-ip로-resolve-되면-무조건-외부망인가">2. 공인 IP로 resolve 되면 무조건 외부망인가?</h2>

<p><strong>아닙니다.</strong> 이 부분이 가장 혼동을 주는 포인트입니다.</p>

<p>GCP 공식 문서에 명확한 근거가 있습니다:</p>

<blockquote>
  <p>“Packets sent from VMs in your VPC network to Google APIs and services remain within Google’s network.”
— <a href="https://cloud.google.com/vpc/docs/private-google-access">Private Google Access 공식 문서</a></p>
</blockquote>

<blockquote>
  <p>“Requests from one Cloud Run resource to another or to other Google Cloud services stay within Google’s internal network.”
— <a href="https://cloud.google.com/run/docs/securing/private-networking">Cloud Run Private Networking 공식 문서</a></p>
</blockquote>

<p>즉, IP 주소가 공개 라우팅 가능(publicly routable)하더라도, <strong>실제 패킷은 Google 내부 네트워크 안에서만 이동합니다.</strong> 인터넷 공중망을 경유하지 않습니다.</p>

<p>경우를 나눠보면:</p>

<table>
  <thead>
    <tr>
      <th>인스턴스 구성</th>
      <th>DNS Resolve 결과</th>
      <th>실제 경로</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>외부 IP 없음 + PGA ON</td>
      <td>공인 IP</td>
      <td><strong>Google 내부 백본</strong> (인터넷 미경유)</td>
    </tr>
    <tr>
      <td>외부 IP 있음</td>
      <td>공인 IP</td>
      <td>Google Front End(GFE) 통해 접근. 물리적으로는 Google 인프라 내부이나 “VPC 내부망 전용 경로”는 아님</td>
    </tr>
    <tr>
      <td>PGA OFF + 외부 IP 없음</td>
      <td>공인 IP</td>
      <td><strong>접근 불가</strong> (패킷이 나갈 경로 자체가 없음)</td>
    </tr>
  </tbody>
</table>

<p>따라서 DNS resolve IP만으로 단정할 수 없고, <strong>인스턴스의 구성을 함께 확인해야</strong> 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1) 해당 인스턴스에 외부 IP가 있는지</span>
curl <span class="nt">-s</span> <span class="nt">-H</span> <span class="s2">"Metadata-Flavor: Google"</span> <span class="se">\</span>
  http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip

<span class="c"># 2) 서브넷에 Private Google Access가 켜져 있는지</span>
gcloud compute networks subnets describe &lt;SUBNET_NAME&gt; <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>&lt;REGION&gt; <span class="se">\</span>
  <span class="nt">--format</span><span class="o">=</span><span class="s2">"get(privateIpGoogleAccess)"</span>

<span class="c"># 3) private/restricted DNS 존이 구성되어 있는지</span>
gcloud dns managed-zones list <span class="nt">--filter</span><span class="o">=</span><span class="s2">"visibility=private"</span>
</code></pre></div></div>

<h3 id="구성별-비교--감사audit-증명-가능-여부">구성별 비교 — 감사(audit) 증명 가능 여부</h3>

<p>“인터넷을 경유하지 않는다”는 사실은 구성 수준에 따라 <strong>증명 가능성</strong>이 달라집니다.</p>

<table>
  <thead>
    <tr>
      <th>구성</th>
      <th>인터넷 경유?</th>
      <th>Google 내부 네트워크?</th>
      <th>외부 IP 노출?</th>
      <th>감사 시 증명 가능?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>기본 (default)</td>
      <td>❌ 경유 안 함</td>
      <td>✅ Google 네트워크 내부</td>
      <td>IP 주소는 공개 라우팅 가능</td>
      <td>어려움</td>
    </tr>
    <tr>
      <td>Private Google Access</td>
      <td>❌ 경유 안 함</td>
      <td>✅ Google 네트워크 내부</td>
      <td>외부 IP 불필요</td>
      <td>부분적</td>
    </tr>
    <tr>
      <td>Private Service Connect</td>
      <td>❌ 경유 안 함</td>
      <td>✅ Google 네트워크 내부</td>
      <td>❌ 완전 사설 IP</td>
      <td>✅ 감사 가능</td>
    </tr>
  </tbody>
</table>

<p>기본 설정에서도 인터넷을 경유하지 않지만, IP 주소가 공개 라우팅 가능하다는 점 때문에 금융권 등 규제 환경에서는 설명이 어렵습니다. <strong>PSC 엔드포인트를 사용하면</strong> DNS도 내부 해석(<code class="language-plaintext highlighter-rouge">*.googleapis.com</code> → 내부 IP)으로 완전히 격리되어, 기술적으로 증명할 수 있습니다.</p>

<hr />

<h2 id="3-bigquery-통신-보안-채널-분석">3. BigQuery 통신 보안 채널 분석</h2>

<p>네트워크 경로와 별개로, <strong>통신 채널 자체의 보안</strong>도 중요합니다. BigQuery API 통신은 TLS와 ALTS 이중 보호 구조로 되어 있습니다.</p>

<h3 id="통신-경로별-보안-메커니즘">통신 경로별 보안 메커니즘</h3>

<table>
  <thead>
    <tr>
      <th>경로</th>
      <th>기술</th>
      <th>암호화 방식</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Client → GFE (외부)</td>
      <td>TLS (BoringSSL)</td>
      <td>HTTPS/TLS 1.2~1.3, Forward Secrecy, FIPS 140-3 Level 1</td>
    </tr>
    <tr>
      <td>GFE → BigQuery</td>
      <td>ALTS</td>
      <td>AES-128-GCM, 서비스 간 인증 포함</td>
    </tr>
    <tr>
      <td>Cloud Run/Agent Engine → BigQuery (내부, Private IP)</td>
      <td>ALTS</td>
      <td>AES-128-GCM, 세션 키 주기적 교체</td>
    </tr>
  </tbody>
</table>

<h3 id="alts-application-layer-transport-security">ALTS (Application Layer Transport Security)</h3>

<p>Google 인프라 내부에서 서비스 간 통신을 보호하는 핵심 기술입니다.</p>

<ul>
  <li>GFE → BigQuery, Cloud Run 백엔드 → BigQuery 모두 ALTS로 보호</li>
  <li>서비스별 Google internal CA 발급 credential로 <strong>상호 인증(mutual auth)</strong></li>
  <li>암호화: AES-128-GCM (기본), 스토리지 레이어는 AES-256</li>
  <li>Handshake: Elliptic Curve + ML-KEM (양자 내성, FIPS 203)</li>
  <li>BoringSSL 또는 PSP로 구현, FIPS 140-2/3 Level 1 검증</li>
</ul>

<h3 id="bigquery-api-자체의-보안">BigQuery API 자체의 보안</h3>

<ul>
  <li><strong>REST API</strong>: HTTPS 강제 (<code class="language-plaintext highlighter-rouge">http://</code> 요청 시 “SSL is required” 오류 반환)</li>
  <li><strong>Storage Read/Write API</strong>: gRPC + TLS, ALTS 캡슐화</li>
  <li><code class="language-plaintext highlighter-rouge">bigquery.googleapis.com</code> 엔드포인트는 GFE를 통해 TLS 종료 후 ALTS로 내부 전달</li>
</ul>

<blockquote>
  <p>참고: <a href="https://cloud.google.com/docs/security/encryption-in-transit">Encryption in transit for Google Cloud</a>, <a href="https://cloud.google.com/docs/security/infrastructure/design">Google infrastructure security design overview</a></p>
</blockquote>

<hr />

<h2 id="4-private-google-accesspga-설정">4. Private Google Access(PGA) 설정</h2>

<p>Private Google Access는 <strong>서브넷 단위</strong>로 설정합니다.</p>

<h3 id="console에서-켜기">Console에서 켜기</h3>

<p><code class="language-plaintext highlighter-rouge">VPC network</code> → <code class="language-plaintext highlighter-rouge">VPC networks</code> → 해당 VPC 선택 → <code class="language-plaintext highlighter-rouge">Subnets</code> 탭 → 서브넷 클릭 → <code class="language-plaintext highlighter-rouge">Edit</code> → <strong>Private Google Access</strong>를 <code class="language-plaintext highlighter-rouge">On</code>으로 변경 → <code class="language-plaintext highlighter-rouge">Save</code></p>

<h3 id="gcloud-cli로-켜기">gcloud CLI로 켜기</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud compute networks subnets update &lt;SUBNET_NAME&gt; <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>&lt;REGION&gt; <span class="se">\</span>
  <span class="nt">--enable-private-ip-google-access</span>
</code></pre></div></div>

<h3 id="현재-상태-확인">현재 상태 확인</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud compute networks subnets describe &lt;SUBNET_NAME&gt; <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>&lt;REGION&gt; <span class="se">\</span>
  <span class="nt">--format</span><span class="o">=</span><span class="s2">"get(privateIpGoogleAccess)"</span>
<span class="c"># True가 나오면 이미 켜져 있음</span>
</code></pre></div></div>

<blockquote>
  <p>PGA가 의미를 갖는 건 <strong>외부 IP가 없는 GCE 인스턴스</strong>일 때입니다. 외부 IP가 있는 인스턴스는 이 설정과 무관하게 이미 Google API에 접근 가능합니다. PGA는 외부 IP 없이 내부 전용으로 운영하는 인스턴스가 <code class="language-plaintext highlighter-rouge">googleapis.com</code> 서비스에 접근할 수 있게 해주는 역할입니다.</p>
</blockquote>

<hr />

<h2 id="5-서비스-유형별-네트워킹-구조">5. 서비스 유형별 네트워킹 구조</h2>

<h3 id="5-1-gce-google-compute-engine">5-1. GCE (Google Compute Engine)</h3>

<p>가장 직관적입니다. 사용자 VPC 안에서 실행되므로, 서브넷의 PGA 설정과 외부 IP 유무에 따라 네트워크 경로가 결정됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GCE 인스턴스 → 서브넷(PGA ON/OFF) → BigQuery API
                  ↓
         외부 IP 없으면 PGA 필요
         외부 IP 있으면 PGA 무관
</code></pre></div></div>

<p>내부망 경로를 명시적으로 강제하고 싶다면, Cloud DNS에서 <code class="language-plaintext highlighter-rouge">googleapis.com</code>을 <code class="language-plaintext highlighter-rouge">restricted.googleapis.com</code>(199.36.153.4/30) 또는 <code class="language-plaintext highlighter-rouge">private.googleapis.com</code>(199.36.153.8/30)으로 매핑하는 <strong>프라이빗 DNS 존</strong>을 생성합니다.</p>

<h3 id="5-2-cloud-run">5-2. Cloud Run</h3>

<p>Cloud Run은 기본적으로 <strong>Google 관리형 인프라</strong>에서 실행됩니다. BigQuery 등 Google API 호출 시 Google 내부 네트워크를 통해 처리됩니다.</p>

<p>사용자 VPC 내 리소스에 접근하려면 <strong>VPC 커넥터</strong>(Serverless VPC Access) 또는 <strong>Direct VPC Egress</strong>를 설정해야 합니다.</p>

<table>
  <thead>
    <tr>
      <th>구성</th>
      <th>Google API 접근</th>
      <th>사용자 VPC 접근</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>기본 (VPC 커넥터 없음)</td>
      <td>Google 내부망</td>
      <td>불가</td>
    </tr>
    <tr>
      <td>VPC 커넥터 사용</td>
      <td>Google 내부망</td>
      <td>가능</td>
    </tr>
    <tr>
      <td>Direct VPC Egress</td>
      <td>Google 내부망</td>
      <td>가능</td>
    </tr>
  </tbody>
</table>

<p>VPC 커넥터를 사용하는 경우 VPC Flow Logs로 트래픽을 확인할 수 있으며, 서브넷의 PGA 설정이 적용됩니다.</p>

<h3 id="5-3-vertex-ai-agent-engine">5-3. Vertex AI Agent Engine</h3>

<p>Agent Engine은 <strong>GCE처럼 사용자 VPC 안에서 돌아가는 게 아닙니다.</strong> Google의 <strong>tenant project</strong>에서 실행되기 때문에 사용자가 서브넷을 직접 제어할 수 없습니다.</p>

<p>그래서 “PGA를 켜야 하나?”라는 질문 자체가 적용되지 않습니다.</p>

<p>Agent Engine에는 <strong>3가지 네트워킹 모드</strong>가 있습니다:</p>

<table>
  <thead>
    <tr>
      <th>모드</th>
      <th>특징</th>
      <th>인터넷 접근</th>
      <th>사용자 VPC 접근</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Standard</strong> (기본)</td>
      <td>별도 설정 없음</td>
      <td>가능</td>
      <td>불가</td>
    </tr>
    <tr>
      <td><strong>VPC Service Controls</strong></td>
      <td>보안 경계 설정</td>
      <td><strong>차단</strong></td>
      <td>불가</td>
    </tr>
    <tr>
      <td><strong>PSC Interface</strong></td>
      <td>사용자 VPC 연결</td>
      <td>설정에 따라</td>
      <td><strong>가능</strong></td>
    </tr>
  </tbody>
</table>

<p>각 모드에서 BigQuery를 호출할 때의 보안 수준:</p>

<table>
  <thead>
    <tr>
      <th>배포 모델</th>
      <th>BigQuery 연결 경로</th>
      <th>보안 수준</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Standard</td>
      <td>공개 API (GFE 경유)</td>
      <td>TLS + ALTS (기본 보호)</td>
    </tr>
    <tr>
      <td>VPC-SC</td>
      <td>Private Google Access</td>
      <td>퍼미터 격리 + ALTS, 인터넷 egress 차단</td>
    </tr>
    <tr>
      <td>PSC-I</td>
      <td>Private Service Connect</td>
      <td>VPC 내 완전 사설 경로 + ALTS</td>
    </tr>
  </tbody>
</table>

<p><strong>Standard 모드</strong>에서 BigQuery 등 Google API를 호출하면 Google 내부 네트워크를 통해 처리됩니다. Agent Engine 자체가 Google 인프라 안에서 돌아가기 때문에, “내부망이냐 외부망이냐” 구분 자체가 GCE와는 다른 맥락입니다.</p>

<hr />

<h2 id="6-agent-engine-psc-interface-구성-가이드">6. Agent Engine PSC Interface 구성 가이드</h2>

<p>에이전트가 사용자 VPC 내의 프라이빗 리소스(온프레미스 DB, Cloud SQL private IP 등)에 접근해야 할 때 PSC Interface를 구성합니다. 설정은 <strong>고객 VPC 구성 → IAM 권한 → DNS 피어링 → Agent Engine 배포</strong> 4개 영역에서 진행됩니다.</p>

<h3 id="아키텍처-흐름">아키텍처 흐름</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Agent Engine (Google-managed tenant VPC)
    ↓  PSC Interface (eth1)
Network Attachment (고객 VPC intf-subnet: 192.168.10.0/28)
    ↓  내부 라우팅
Proxy VM (10.10.10.2) ← DNS: proxy-vm.demo.com
    ↓  Cloud NAT (인터넷 액세스 필요 시)
사설 리소스 또는 인터넷
</code></pre></div></div>

<p>Agent Engine은 PSC-I 구성 시 <strong>직접 인터넷 경로가 없습니다.</strong> 에이전트가 외부 API를 호출해야 하면 Proxy VM을 통해 우회해야 합니다.</p>

<h3 id="step-1-api-활성화">Step 1: API 활성화</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud services <span class="nb">enable </span>compute.googleapis.com
gcloud services <span class="nb">enable </span>aiplatform.googleapis.com
gcloud services <span class="nb">enable </span>dns.googleapis.com
gcloud services <span class="nb">enable </span>storage.googleapis.com
</code></pre></div></div>

<h3 id="step-2-vpc-및-서브넷-생성">Step 2: VPC 및 서브넷 생성</h3>

<p><strong>두 개의 서브넷</strong>이 필요합니다. 용도가 다릅니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># VPC 생성 (기존 VPC 사용 시 생략)</span>
gcloud compute networks create consumer-vpc <span class="nt">--subnet-mode</span><span class="o">=</span>custom

<span class="c"># 서브넷 1: Proxy VM 용 (사설 리소스 또는 인터넷 egress)</span>
gcloud compute networks subnets create rfc1918-subnet1 <span class="se">\</span>
  <span class="nt">--range</span><span class="o">=</span>10.10.10.0/28 <span class="se">\</span>
  <span class="nt">--network</span><span class="o">=</span>consumer-vpc <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1

<span class="c"># 서브넷 2: PSC Network Attachment 전용 (/28 최소)</span>
gcloud compute networks subnets create intf-subnet <span class="se">\</span>
  <span class="nt">--range</span><span class="o">=</span>192.168.10.0/28 <span class="se">\</span>
  <span class="nt">--network</span><span class="o">=</span>consumer-vpc <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">intf-subnet</code>은 <strong>PSC 전용</strong>으로 다른 리소스와 공유하지 않도록 합니다. Agent Engine은 <code class="language-plaintext highlighter-rouge">max_instances × 2</code>개의 IP를 이 서브넷에서 할당합니다.</p>
</blockquote>

<h3 id="step-3-psc-network-attachment-생성">Step 3: PSC Network Attachment 생성</h3>

<p>Agent Engine이 이 Attachment를 통해 고객 VPC에 연결됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud compute network-attachments create psc-network-attachment <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
  <span class="nt">--connection-preference</span><span class="o">=</span>ACCEPT_AUTOMATIC <span class="se">\</span>
  <span class="nt">--subnets</span><span class="o">=</span>intf-subnet

<span class="c"># 생성 확인</span>
gcloud compute network-attachments describe psc-network-attachment <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ACCEPT_AUTOMATIC</code>은 Vertex AI P4SA가 자동으로 연결을 수락합니다. 수동 승인이 필요하면 <code class="language-plaintext highlighter-rouge">ACCEPT_MANUAL</code>을 사용합니다.</p>

<h3 id="step-4-proxy-vm--cloud-nat-구성-인터넷-egress-필요-시">Step 4: Proxy VM + Cloud NAT 구성 (인터넷 egress 필요 시)</h3>

<p>에이전트가 외부 API를 호출해야 하는 경우에만 필요합니다. BigQuery 등 Google API만 호출한다면 이 단계는 생략 가능합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Cloud Router + Cloud NAT 생성 (Proxy VM의 인터넷 egress용)</span>
gcloud compute routers create cloud-router-for-nat <span class="se">\</span>
  <span class="nt">--network</span><span class="o">=</span>consumer-vpc <span class="nt">--region</span><span class="o">=</span>us-central1

gcloud compute routers nats create cloud-nat-us-central1 <span class="se">\</span>
  <span class="nt">--router</span><span class="o">=</span>cloud-router-for-nat <span class="se">\</span>
  <span class="nt">--auto-allocate-nat-external-ips</span> <span class="se">\</span>
  <span class="nt">--nat-all-subnet-ip-ranges</span> <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1

<span class="c"># Proxy VM 생성 (tinyproxy 설치)</span>
gcloud compute instances create proxy-vm <span class="se">\</span>
  <span class="nt">--machine-type</span><span class="o">=</span>e2-micro <span class="se">\</span>
  <span class="nt">--image-family</span><span class="o">=</span>debian-11 <span class="se">\</span>
  <span class="nt">--image-project</span><span class="o">=</span>debian-cloud <span class="se">\</span>
  <span class="nt">--no-address</span> <span class="se">\</span>
  <span class="nt">--can-ip-forward</span> <span class="se">\</span>
  <span class="nt">--zone</span><span class="o">=</span>us-central1-a <span class="se">\</span>
  <span class="nt">--subnet</span><span class="o">=</span>rfc1918-subnet1 <span class="se">\</span>
  <span class="nt">--metadata</span><span class="o">=</span>startup-script<span class="o">=</span><span class="s1">'apt-get update &amp;&amp; apt-get install -y tinyproxy'</span>
</code></pre></div></div>

<p>Proxy VM에 SSH 접속 후 <code class="language-plaintext highlighter-rouge">/etc/tinyproxy/tinyproxy.conf</code>를 수정합니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Listen 10.10.10.2
Allow 192.168.10.0/24   # PSC 서브넷 허용
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl restart tinyproxy
</code></pre></div></div>

<h3 id="step-5-방화벽-규칙-설정">Step 5: 방화벽 규칙 설정</h3>

<p>PSC Interface 서브넷에서 Proxy VM 서브넷으로의 ingress를 허용합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud compute firewall-rules create allow-psc-to-proxy <span class="se">\</span>
  <span class="nt">--network</span><span class="o">=</span>consumer-vpc <span class="se">\</span>
  <span class="nt">--action</span><span class="o">=</span>ALLOW <span class="se">\</span>
  <span class="nt">--rules</span><span class="o">=</span>ALL <span class="se">\</span>
  <span class="nt">--direction</span><span class="o">=</span>INGRESS <span class="se">\</span>
  <span class="nt">--source-ranges</span><span class="o">=</span><span class="s2">"192.168.10.0/28"</span> <span class="se">\</span>
  <span class="nt">--destination-ranges</span><span class="o">=</span><span class="s2">"10.10.10.0/28"</span>
</code></pre></div></div>

<h3 id="step-6-cloud-dns-private-zone--a-레코드">Step 6: Cloud DNS Private Zone + A 레코드</h3>

<p>에이전트가 FQDN으로 VPC 내 리소스에 접근하려면 Cloud DNS private zone이 필요합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Private DNS Zone 생성</span>
gcloud dns managed-zones create private-dns-zone <span class="se">\</span>
  <span class="nt">--dns-name</span><span class="o">=</span><span class="s2">"demo.com."</span> <span class="se">\</span>
  <span class="nt">--visibility</span><span class="o">=</span>private <span class="se">\</span>
  <span class="nt">--networks</span><span class="o">=</span><span class="s2">"consumer-vpc"</span>

<span class="c"># Proxy VM IP 확인</span>
gcloud compute instances describe proxy-vm <span class="se">\</span>
  <span class="nt">--zone</span><span class="o">=</span>us-central1-a | <span class="nb">grep </span>networkIP

<span class="c"># A 레코드 추가 (IP는 실제 값으로 교체)</span>
gcloud dns record-sets create proxy-vm.demo.com. <span class="se">\</span>
  <span class="nt">--zone</span><span class="o">=</span>private-dns-zone <span class="se">\</span>
  <span class="nt">--type</span><span class="o">=</span>A <span class="se">\</span>
  <span class="nt">--ttl</span><span class="o">=</span>300 <span class="se">\</span>
  <span class="nt">--rrdatas</span><span class="o">=</span><span class="s2">"10.10.10.2"</span>
</code></pre></div></div>

<h3 id="step-7-iam-권한-부여">Step 7: IAM 권한 부여</h3>

<p>Vertex AI Service Agent에 <strong>두 가지 권한</strong>이 필요합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">PROJECT_NUMBER</span><span class="o">=</span><span class="si">$(</span>gcloud projects describe <span class="nv">$PROJECT_ID</span> <span class="nt">--format</span><span class="o">=</span><span class="s2">"value(projectNumber)"</span><span class="si">)</span>

<span class="c"># Vertex AI Service Agent 생성 (없으면)</span>
gcloud beta services identity create <span class="se">\</span>
  <span class="nt">--service</span><span class="o">=</span>aiplatform.googleapis.com <span class="se">\</span>
  <span class="nt">--project</span><span class="o">=</span><span class="nv">$PROJECT_NUMBER</span>

<span class="c"># 1) Network Attachment 수정 권한</span>
gcloud projects add-iam-policy-binding <span class="nv">$PROJECT_ID</span> <span class="se">\</span>
  <span class="nt">--member</span><span class="o">=</span><span class="s2">"serviceAccount:service-</span><span class="k">${</span><span class="nv">PROJECT_NUMBER</span><span class="k">}</span><span class="s2">@gcp-sa-aiplatform.iam.gserviceaccount.com"</span> <span class="se">\</span>
  <span class="nt">--role</span><span class="o">=</span><span class="s2">"roles/compute.networkAdmin"</span>

<span class="c"># 2) DNS 피어링 권한</span>
gcloud projects add-iam-policy-binding <span class="nv">$PROJECT_ID</span> <span class="se">\</span>
  <span class="nt">--member</span><span class="o">=</span><span class="s2">"serviceAccount:service-</span><span class="k">${</span><span class="nv">PROJECT_NUMBER</span><span class="k">}</span><span class="s2">@gcp-sa-aiplatform.iam.gserviceaccount.com"</span> <span class="se">\</span>
  <span class="nt">--role</span><span class="o">=</span><span class="s2">"roles/dns.peer"</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">roles/compute.networkAdmin</code> 대신 최소 권한으로 Custom Role(<code class="language-plaintext highlighter-rouge">compute.networkAttachments.get</code>, <code class="language-plaintext highlighter-rouge">compute.networkAttachments.update</code>, <code class="language-plaintext highlighter-rouge">compute.regionOperations.get</code>)을 사용할 수도 있습니다.</p>
</blockquote>

<h3 id="step-8-agent-engine-배포">Step 8: Agent Engine 배포</h3>

<p>배포 시 <code class="language-plaintext highlighter-rouge">psc_interface_config</code>를 지정합니다. 두 가지 방식이 있습니다.</p>

<p><strong>방식 A — ADK 설정 파일 (.agent_engine_config.json)</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span><span class="p">,</span> <span class="n">os</span>

<span class="n">config_data</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"requirements"</span><span class="p">:</span> <span class="p">[</span>
        <span class="s">"google-cloud-aiplatform[agent_engines,adk]"</span><span class="p">,</span>
    <span class="p">],</span>
    <span class="s">"psc_interface_config"</span><span class="p">:</span> <span class="p">{</span>
        <span class="s">"network_attachment"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"projects/</span><span class="si">{</span><span class="n">PROJECT_ID</span><span class="si">}</span><span class="s">/regions/us-central1/networkAttachments/psc-network-attachment"</span><span class="p">,</span>
        <span class="s">"dns_peering_configs"</span><span class="p">:</span> <span class="p">[</span>
            <span class="p">{</span>
                <span class="s">"domain"</span><span class="p">:</span> <span class="s">"demo.com."</span><span class="p">,</span>
                <span class="s">"target_project"</span><span class="p">:</span> <span class="n">PROJECT_ID</span><span class="p">,</span>
                <span class="s">"target_network"</span><span class="p">:</span> <span class="s">"consumer-vpc"</span>
            <span class="p">}</span>
        <span class="p">]</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="n">os</span><span class="p">.</span><span class="n">makedirs</span><span class="p">(</span><span class="s">"my_agent"</span><span class="p">,</span> <span class="n">exist_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"my_agent/.agent_engine_config.json"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">json</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">config_data</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>방식 B — Python SDK 직접 호출</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">google.cloud</span> <span class="kn">import</span> <span class="n">aiplatform</span>

<span class="n">client</span> <span class="o">=</span> <span class="n">aiplatform</span><span class="p">.</span><span class="n">Client</span><span class="p">(</span><span class="n">project</span><span class="o">=</span><span class="s">"my-project"</span><span class="p">,</span> <span class="n">location</span><span class="o">=</span><span class="s">"us-central1"</span><span class="p">)</span>

<span class="n">remote_agent</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">agent_engines</span><span class="p">.</span><span class="n">create</span><span class="p">(</span>
    <span class="n">agent</span><span class="o">=</span><span class="n">local_agent</span><span class="p">,</span>
    <span class="n">config</span><span class="o">=</span><span class="p">{</span>
        <span class="s">"psc_interface_config"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"network_attachment"</span><span class="p">:</span> <span class="s">"projects/my-project/regions/us-central1/networkAttachments/psc-network-attachment"</span><span class="p">,</span>
            <span class="s">"dns_peering_configs"</span><span class="p">:</span> <span class="p">[</span>
                <span class="p">{</span>
                    <span class="s">"domain"</span><span class="p">:</span> <span class="s">"demo.com."</span><span class="p">,</span>
                    <span class="s">"target_project"</span><span class="p">:</span> <span class="s">"my-project"</span><span class="p">,</span>
                    <span class="s">"target_network"</span><span class="p">:</span> <span class="s">"consumer-vpc"</span><span class="p">,</span>
                <span class="p">},</span>
            <span class="p">],</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">)</span>
</code></pre></div></div>

<p><strong>방식 C — REST API 직접 호출</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">requests</span>

<span class="n">ENDPOINT</span> <span class="o">=</span> <span class="s">"https://us-central1-aiplatform.googleapis.com"</span>

<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">post</span><span class="p">(</span>
    <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">ENDPOINT</span><span class="si">}</span><span class="s">/v1beta1/projects/</span><span class="si">{</span><span class="n">PROJECT_ID</span><span class="si">}</span><span class="s">/locations/us-central1/reasoningEngines"</span><span class="p">,</span>
    <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s">"Authorization"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"Bearer </span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="s">"</span><span class="p">},</span>
    <span class="n">json</span><span class="o">=</span><span class="p">{</span>
        <span class="s">"displayName"</span><span class="p">:</span> <span class="s">"My Private Agent"</span><span class="p">,</span>
        <span class="s">"spec"</span><span class="p">:</span> <span class="p">{</span>
            <span class="s">"packageSpec"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"pickleObjectGcsUri"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"gs://</span><span class="si">{</span><span class="n">BUCKET</span><span class="si">}</span><span class="s">/agent.pkl"</span><span class="p">,</span>
                <span class="s">"requirementsGcsUri"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"gs://</span><span class="si">{</span><span class="n">BUCKET</span><span class="si">}</span><span class="s">/requirements.txt"</span><span class="p">,</span>
                <span class="s">"pythonVersion"</span><span class="p">:</span> <span class="s">"3.10"</span>
            <span class="p">},</span>
            <span class="s">"deploymentSpec"</span><span class="p">:</span> <span class="p">{</span>
                <span class="s">"pscInterfaceConfig"</span><span class="p">:</span> <span class="p">{</span>
                    <span class="s">"networkAttachment"</span><span class="p">:</span> <span class="sa">f</span><span class="s">"projects/</span><span class="si">{</span><span class="n">PROJECT_ID</span><span class="si">}</span><span class="s">/regions/us-central1/networkAttachments/psc-network-attachment"</span><span class="p">,</span>
                    <span class="s">"dnsPeeringConfigs"</span><span class="p">:</span> <span class="p">[</span>
                        <span class="p">{</span>
                            <span class="s">"domain"</span><span class="p">:</span> <span class="s">"demo.com."</span><span class="p">,</span>
                            <span class="s">"targetProject"</span><span class="p">:</span> <span class="n">PROJECT_ID</span><span class="p">,</span>
                            <span class="s">"targetNetwork"</span><span class="p">:</span> <span class="s">"consumer-vpc"</span>
                        <span class="p">}</span>
                    <span class="p">]</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">)</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">network_attachment</code>에는 이름만 넣어도 되지만, Shared VPC 등 Agent Engine을 사용하는 프로젝트와 network attachment가 다른 프로젝트에 있는 경우 full path를 넣어야 합니다.</p>
</blockquote>

<h3 id="전체-설정-요약">전체 설정 요약</h3>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>위치</th>
      <th>주요 리소스</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>API 활성화</td>
      <td>고객 프로젝트</td>
      <td>compute, aiplatform, dns</td>
      <td>-</td>
    </tr>
    <tr>
      <td>VPC/서브넷</td>
      <td>고객 프로젝트</td>
      <td>consumer-vpc, intf-subnet(/28 이상)</td>
      <td>PSC 전용 서브넷 분리</td>
    </tr>
    <tr>
      <td>Network Attachment</td>
      <td>고객 프로젝트</td>
      <td>psc-network-attachment</td>
      <td>핵심 리소스</td>
    </tr>
    <tr>
      <td>Proxy VM</td>
      <td>고객 VPC</td>
      <td>proxy-vm + tinyproxy</td>
      <td>인터넷 egress 필요 시만</td>
    </tr>
    <tr>
      <td>Cloud NAT</td>
      <td>고객 프로젝트</td>
      <td>Cloud Router + NAT</td>
      <td>Proxy VM 인터넷 egress</td>
    </tr>
    <tr>
      <td>방화벽</td>
      <td>고객 VPC</td>
      <td>PSC 서브넷 → Proxy 서브넷 허용</td>
      <td>-</td>
    </tr>
    <tr>
      <td>Cloud DNS</td>
      <td>고객 프로젝트</td>
      <td>Private Zone + A 레코드</td>
      <td>FQDN 접근 시</td>
    </tr>
    <tr>
      <td>IAM</td>
      <td>고객 프로젝트</td>
      <td>Vertex AI SA → networkAdmin + dns.peer</td>
      <td>-</td>
    </tr>
    <tr>
      <td>Agent 배포</td>
      <td>Agent Engine</td>
      <td>pscInterfaceConfig 지정</td>
      <td>배포 시 1회 설정</td>
    </tr>
  </tbody>
</table>

<h3 id="shared-vpc-환경에서의-권장-구성">Shared VPC 환경에서의 권장 구성</h3>

<p>Network Attachment는 <strong>Vertex AI 서비스 프로젝트</strong>(Agent Engine 배포 프로젝트)에 생성하는 것을 권장합니다. Vertex AI P4SA가 해당 프로젝트에서 Attachment를 패치하는 방식이므로 권한 관리가 단순해집니다.</p>

<p>여러 에이전트가 하나의 network attachment를 공유하도록 구성할 수도 있고, 각각 전용 network attachment를 사용하도록 할 수도 있습니다.</p>

<hr />

<h2 id="7-서비스별-bigquery-접근-경로-비교-정리">7. 서비스별 BigQuery 접근 경로 비교 정리</h2>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>GCE</th>
      <th>Cloud Run</th>
      <th>Agent Engine</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>실행 환경</strong></td>
      <td>사용자 VPC 내</td>
      <td>Google 관리형 (VPC 커넥터로 연결 가능)</td>
      <td>Google tenant project</td>
    </tr>
    <tr>
      <td><strong>BigQuery 접근</strong></td>
      <td>PGA + 서브넷 설정</td>
      <td>기본적으로 내부망</td>
      <td>기본적으로 내부망</td>
    </tr>
    <tr>
      <td><strong>PGA 설정 필요</strong></td>
      <td>외부 IP 없는 경우 필요</td>
      <td>VPC 커넥터 사용 시 서브넷에 설정</td>
      <td>해당 없음</td>
    </tr>
    <tr>
      <td><strong>내부망 강제</strong></td>
      <td>private DNS 존 구성</td>
      <td>VPC 커넥터 + private DNS 존</td>
      <td>VPC-SC 또는 PSC-I 모드</td>
    </tr>
    <tr>
      <td><strong>사용자 VPC 접근</strong></td>
      <td>기본 가능</td>
      <td>VPC 커넥터 또는 Direct VPC Egress</td>
      <td>PSC Interface 구성 필요</td>
    </tr>
    <tr>
      <td><strong>네트워크 확인</strong></td>
      <td>dig, VPC Flow Logs</td>
      <td>VPC 커넥터 사용 시 Flow Logs</td>
      <td>Google 관리 영역이라 직접 확인 불가</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="8-내부망-경로를-명시적으로-보장하려면">8. 내부망 경로를 명시적으로 보장하려면</h2>

<p>DNS resolve 결과가 공인 IP로 나오더라도 PGA가 켜져 있으면 실질적으로 내부망이지만, 이것만으로는 감사(audit) 시 설명이 어려울 수 있습니다. 명시적으로 내부망을 보장하려면:</p>

<h3 id="cloud-dns-private-zone-구성">Cloud DNS Private Zone 구성</h3>

<p><code class="language-plaintext highlighter-rouge">googleapis.com</code>을 restricted 또는 private 대역으로 매핑하는 DNS 존을 생성합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># restricted.googleapis.com으로 매핑 (VPC-SC 사용 시)</span>
gcloud dns managed-zones create googleapis-restricted <span class="se">\</span>
  <span class="nt">--dns-name</span><span class="o">=</span>googleapis.com <span class="se">\</span>
  <span class="nt">--visibility</span><span class="o">=</span>private <span class="se">\</span>
  <span class="nt">--networks</span><span class="o">=</span>my-vpc <span class="se">\</span>
  <span class="nt">--description</span><span class="o">=</span><span class="s2">"Route googleapis.com to restricted VIPs"</span>

gcloud dns record-sets create googleapis.com <span class="se">\</span>
  <span class="nt">--zone</span><span class="o">=</span>googleapis-restricted <span class="se">\</span>
  <span class="nt">--type</span><span class="o">=</span>CNAME <span class="se">\</span>
  <span class="nt">--ttl</span><span class="o">=</span>300 <span class="se">\</span>
  <span class="nt">--rrdatas</span><span class="o">=</span><span class="s2">"restricted.googleapis.com."</span>

gcloud dns record-sets create restricted.googleapis.com <span class="se">\</span>
  <span class="nt">--zone</span><span class="o">=</span>googleapis-restricted <span class="se">\</span>
  <span class="nt">--type</span><span class="o">=</span>A <span class="se">\</span>
  <span class="nt">--ttl</span><span class="o">=</span>300 <span class="se">\</span>
  <span class="nt">--rrdatas</span><span class="o">=</span><span class="s2">"199.36.153.4,199.36.153.5,199.36.153.6,199.36.153.7"</span>
</code></pre></div></div>

<p>이렇게 설정하면 <code class="language-plaintext highlighter-rouge">dig bigquery.googleapis.com</code> 결과가 <code class="language-plaintext highlighter-rouge">199.36.153.4/30</code> 대역으로 나오게 되어, DNS 레벨에서도 내부망 사용을 확인할 수 있습니다.</p>

<hr />

<h2 id="빠른-판별-플로차트">빠른 판별 플로차트</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BigQuery API 호출 시 내부망인가?
│
├── GCE의 경우
│   ├── 외부 IP 없음 + PGA ON → ✅ 내부망
│   ├── 외부 IP 있음 → ⚠️ Google 인프라 내부이나 전용 경로는 아님
│   └── PGA OFF + 외부 IP 없음 → ❌ 접근 불가
│
├── Cloud Run의 경우
│   ├── 기본 (VPC 커넥터 없음) → ✅ Google 내부망
│   └── VPC 커넥터 사용 → ✅ 내부망 (서브넷 PGA 설정 적용)
│
└── Agent Engine의 경우
    ├── Standard 모드 → ✅ Google 내부망
    ├── VPC-SC 모드 → ✅ 내부망 + 인터넷 차단
    └── PSC-I 모드 → ✅ 내부망 + 사용자 VPC 연결
</code></pre></div></div>

<hr />

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

<p>“BigQuery에 접근할 때 내부망을 통하는가?”라는 질문의 답은 <strong>서비스 유형과 구성에 따라 다릅니다.</strong> GCE는 서브넷과 PGA 설정이 핵심이고, Cloud Run과 Agent Engine은 Google 관리형 서비스이기 때문에 기본적으로 Google 내부 네트워크를 사용합니다.</p>

<p>DNS resolve 결과가 공인 IP로 나온다고 해서 반드시 외부망은 아니라는 점, 그리고 명시적으로 내부망을 보장하려면 Cloud DNS Private Zone이나 VPC-SC를 구성해야 한다는 점을 기억하면 됩니다.</p>

<p>통신 채널 보안 측면에서는, BigQuery API 통신이 TLS + ALTS 이중 보호 구조로 되어 있어 기본적으로 안전합니다. 다만 금융권 등 규제 환경에서는 이것만으로 충분하지 않을 수 있으므로, VPC-SC 또는 PSC를 추가하여 완전 사설 채널로 격리하는 것이 권장됩니다.</p>

<p>Agent Engine에서 사용자 VPC 내부 리소스에 접근해야 하는 경우에는 PSC Interface를 구성하면 되고, 단순히 BigQuery 등 Google API만 호출하는 경우라면 별도 네트워크 설정 없이 Standard 모드로 충분합니다.</p>

<blockquote>
  <p><strong>한줄 요약:</strong> BigQuery와의 통신은 기본적으로 Google 내부망 + TLS + ALTS 이중 보호 구조이며, 고보안 환경이라면 VPC-SC 또는 PSC-I를 추가하여 완전 사설 채널로 격리하고 감사 시 증명 가능한 구조를 갖추는 것이 권장됩니다.</p>
</blockquote>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="google-cloud" /><category term="bigquery" /><category term="vpc" /><category term="private-google-access" /><category term="psc" /><category term="private-service-connect" /><category term="cloud-run" /><category term="gce" /><category term="vertex-ai" /><category term="agent-engine" /><category term="networking" /><summary type="html"><![CDATA[Cloud Run, GCE, Vertex AI Agent Engine에서 BigQuery API를 호출할 때, 트래픽이 Google 내부망을 타는 건지 외부 인터넷을 경유하는 건지 궁금해지는 순간이 있습니다. 특히 금융권이나 공공기관 프로젝트에서는 “정말 내부망으로 통신하는 게 맞느냐”는 질문이 반드시 나옵니다.]]></summary></entry><entry><title type="html">Apache Iceberg × BigLake × BigQuery: 메타데이터 구조부터 쓰기 성능 튜닝까지</title><link href="https://seonghak.com/blog/2026/03/24/iceberg-biglake-bigquery-guide/" rel="alternate" type="text/html" title="Apache Iceberg × BigLake × BigQuery: 메타데이터 구조부터 쓰기 성능 튜닝까지" /><published>2026-03-24T00:00:00+09:00</published><updated>2026-03-24T00:00:00+09:00</updated><id>https://seonghak.com/blog/2026/03/24/iceberg-biglake-bigquery-guide</id><content type="html" xml:base="https://seonghak.com/blog/2026/03/24/iceberg-biglake-bigquery-guide/"><![CDATA[<p>GCP에서 Apache Iceberg를 BigLake/BigQuery와 연동하면, Spark로 적재하고 BigQuery로 분석하는 유연한 데이터 아키텍처를 구현할 수 있습니다. 하지만 실제 운영 과정에서 예상치 못한 스캔량 차이, 쓰기 성능 저하, 한글 컬럼 이슈 등을 마주하게 됩니다.</p>

<p>이 글에서는 Iceberg의 메타데이터 구조를 먼저 이해한 뒤, BigQuery에서의 스캔량 최적화, BigLake 연동 방식 비교, 그리고 Spark 쓰기 성능 튜닝까지 실무에서 필요한 내용을 정리합니다.</p>

<hr />

<h2 id="1-iceberg는-서비스가-아니라-테이블-포맷이다">1. Iceberg는 서비스가 아니라 “테이블 포맷”이다</h2>

<p>Iceberg를 처음 접하면 Hive Metastore처럼 별도 데몬이 돌아가는 것으로 오해하기 쉽습니다. Iceberg는 실행되는 프로세스가 아니라, <strong>메타데이터 파일 구조의 규격(specification)</strong>입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gs://bucket/warehouse/my_table/
├── metadata/
│   ├── v1.metadata.json          ← 테이블 스키마, partition-spec, 스냅샷 목록
│   ├── snap-xxxxx.avro           ← 스냅샷 → manifest list
│   └── manifest-xxxxx.avro       ← 각 데이터 파일의 위치 + 컬럼별 min/max 통계
└── data/
    ├── p_yyyymm=202501/
    │   ├── file1.parquet
    │   └── file2.parquet
    └── p_yyyymm=202502/
        └── file3.parquet
</code></pre></div></div>

<p>비유하자면 Iceberg 메타데이터는 <strong>책의 목차 + 색인</strong>입니다. 책(데이터)을 읽는 사람(BigQuery)이 색인을 보고 필요한 페이지만 펼치는 것이지, 색인이 스스로 뭔가를 하는 게 아닙니다.</p>

<h3 id="메타데이터-3단-구조">메타데이터 3단 구조</h3>

<table>
  <thead>
    <tr>
      <th>계층</th>
      <th>파일</th>
      <th>담고 있는 것</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>metadata.json</td>
      <td>테이블 정의</td>
      <td>스키마, partition-spec, 현재 스냅샷 ID</td>
    </tr>
    <tr>
      <td>manifest-list</td>
      <td><code class="language-plaintext highlighter-rouge">snap-xxxxx.avro</code></td>
      <td>어떤 manifest 파일들이 있는지 목록 + 파티션 범위 요약</td>
    </tr>
    <tr>
      <td>manifest file</td>
      <td><code class="language-plaintext highlighter-rouge">manifest-xxxxx.avro</code></td>
      <td>데이터 파일별 경로, 행 수, <strong>컬럼별 min/max</strong>, null count</td>
    </tr>
  </tbody>
</table>

<p>핵심은 <strong>manifest file</strong>입니다. 여기에 각 Parquet 파일별로 <strong>모든 컬럼의 min/max, null count 등의 통계</strong>가 기록되어 있고, 이것이 쿼리 엔진의 pruning에 활용됩니다.</p>

<hr />

<h2 id="2-bigquery에서-스캔량-확인하는-방법">2. BigQuery에서 스캔량 확인하는 방법</h2>

<p>Iceberg 테이블의 pruning 효과를 제대로 측정하려면 BigQuery의 스캔량을 정확히 확인해야 합니다.</p>

<h3 id="2-1-dry-run으로-예상-스캔량-사전-확인">2-1. Dry Run으로 예상 스캔량 사전 확인</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bq query <span class="nt">--dry_run</span> <span class="nt">--use_legacy_sql</span><span class="o">=</span><span class="nb">false</span> <span class="se">\</span>
  <span class="s1">'SELECT col1, col2 FROM `project.dataset.table` WHERE partition_date = "2025-01-01"'</span>
</code></pre></div></div>

<p>Console에서는 쿼리 편집기 우측 상단에 예상 스캔량이 자동으로 표시됩니다.</p>

<h3 id="2-2-실행-후-job-메타데이터로-확인">2-2. 실행 후 Job 메타데이터로 확인</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">job_id</span><span class="p">,</span>
  <span class="n">query</span><span class="p">,</span>
  <span class="n">total_bytes_processed</span><span class="p">,</span>
  <span class="n">total_bytes_billed</span><span class="p">,</span>
  <span class="n">cache_hit</span>
<span class="k">FROM</span> <span class="nv">`region-asia-northeast3`</span><span class="p">.</span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">JOBS</span>
<span class="k">WHERE</span> <span class="n">creation_time</span> <span class="o">&gt;</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">1</span> <span class="k">DAY</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">creation_time</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">total_bytes_processed</code>가 실제 스캔된 바이트 수이고, <code class="language-plaintext highlighter-rouge">total_bytes_billed</code>가 과금 기준 바이트 수입니다. <strong>캐시 히트 시 0</strong>으로 나올 수 있으니 <code class="language-plaintext highlighter-rouge">cache_hit</code> 컬럼도 함께 확인해야 합니다.</p>

<h3 id="2-3-캐시를-비활성화하고-비교">2-3. 캐시를 비활성화하고 비교</h3>

<p>최적화 전후 효과를 정확히 비교하려면 캐시를 꺼야 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bq query <span class="nt">--use_legacy_sql</span><span class="o">=</span><span class="nb">false</span> <span class="nt">--nouse_cache</span> <span class="se">\</span>
  <span class="s1">'SELECT ... FROM table WHERE ...'</span>
</code></pre></div></div>

<p>Console에서는 <strong>More → Query Settings → Cache 체크 해제</strong>로 설정할 수 있습니다.</p>

<h3 id="2-4-테이블-스토리지-용량-확인">2-4. 테이블 스토리지 용량 확인</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="k">table_name</span><span class="p">,</span>
  <span class="n">total_rows</span><span class="p">,</span>
  <span class="n">total_logical_bytes</span><span class="p">,</span>
  <span class="n">total_physical_bytes</span>
<span class="k">FROM</span> <span class="nv">`project.dataset.INFORMATION_SCHEMA.TABLE_STORAGE`</span>
<span class="k">WHERE</span> <span class="k">table_name</span> <span class="o">=</span> <span class="s1">'my_table'</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="3-파티션-컬럼이-아닌데-스캔량이-줄어드는-이유">3. 파티션 컬럼이 아닌데 스캔량이 줄어드는 이유</h2>

<p><code class="language-plaintext highlighter-rouge">p_yyyymm</code>이 파티션 컬럼이고 <code class="language-plaintext highlighter-rouge">기준년월</code>이 일반 컬럼인 Iceberg 외부 테이블에서, <code class="language-plaintext highlighter-rouge">기준년월</code>으로 필터해도 스캔량이 줄어드는 현상이 발생할 수 있습니다. 이건 BigQuery 파티션 pruning이 아니라 <strong>Iceberg의 파일 pruning</strong>입니다.</p>

<h3 id="column-statistics-기반-data-skipping">Column Statistics 기반 Data Skipping</h3>

<p>Iceberg는 파티션과 별개로, 각 데이터 파일(Parquet)에 대해 <strong>컬럼별 min/max 통계를 manifest에 저장</strong>합니다. BigQuery가 쿼리를 실행하면 다음 순서로 동작합니다:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">metadata.json</code>을 읽어서 현재 스냅샷의 manifest list 확인</li>
  <li>manifest file을 읽어서 각 데이터 파일의 컬럼별 min/max 통계 확인</li>
  <li><code class="language-plaintext highlighter-rouge">WHERE 기준년월 = '202501'</code> 조건과 각 파일의 <code class="language-plaintext highlighter-rouge">기준년월</code> min/max를 대조</li>
  <li>해당 범위에 걸리지 않는 파일은 <strong>아예 읽지 않음</strong></li>
  <li>나머지 파일만 GCS에서 읽어서 처리</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WHERE 기준년월 = '202501' 쿼리 시:

p_yyyymm=202501/file1.parquet → 기준년월 min:202501, max:202501 → ✅ 읽음
p_yyyymm=202501/file2.parquet → 기준년월 min:202501, max:202501 → ✅ 읽음
p_yyyymm=202502/file3.parquet → 기준년월 min:202502, max:202502 → ❌ skip
p_yyyymm=202502/file4.parquet → 기준년월 min:202502, max:202503 → ❌ skip
</code></pre></div></div>

<p>이 전체 과정을 <strong>BigQuery 엔진이 직접 수행</strong>합니다. Iceberg 쪽에서 별도로 뭔가 돌아가는 게 아니라, BigQuery가 Iceberg 메타데이터 파일을 해석할 줄 아는 것입니다.</p>

<h3 id="왜-파티션-컬럼보다-일반-컬럼이-스캔량이-적을-수-있는가">왜 파티션 컬럼보다 일반 컬럼이 스캔량이 적을 수 있는가</h3>

<p>직관과 반대이지만, <strong>pruning 단위가 다르기 때문에</strong> 충분히 발생할 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>p_yyyymm (파티션 컬럼)</th>
      <th>기준년월 (일반 컬럼)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>pruning 단위</td>
      <td><strong>디렉토리 단위</strong> (거친 단위)</td>
      <td><strong>파일 단위</strong> (세밀한 단위)</td>
    </tr>
    <tr>
      <td>동작 방식</td>
      <td>파티션 디렉토리 전체를 읽음</td>
      <td>manifest의 파일별 min/max로 판단</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">p_yyyymm=202501/</code> 디렉토리 안에 <code class="language-plaintext highlighter-rouge">기준년월</code>이 <code class="language-plaintext highlighter-rouge">202412</code>인 데이터가 섞여 있다면, <code class="language-plaintext highlighter-rouge">WHERE p_yyyymm = '202501'</code>은 해당 디렉토리 전체를 읽지만, <code class="language-plaintext highlighter-rouge">WHERE 기준년월 = '202501'</code>은 여러 디렉토리에서 해당 값이 있는 파일만 골라서 읽습니다.</p>

<p>확인하는 가장 확실한 방법은 두 컬럼의 값 불일치를 직접 조회하는 것입니다:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">p_yyyymm</span><span class="p">,</span> <span class="err">기준년월</span><span class="p">,</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">as</span> <span class="n">cnt</span>
<span class="k">FROM</span> <span class="nv">`project.dataset.iceberg_table`</span>
<span class="k">WHERE</span> <span class="n">p_yyyymm</span> <span class="o">=</span> <span class="s1">'202501'</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="mi">2</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h2 id="4-iceberg-hidden-partitioning">4. Iceberg Hidden Partitioning</h2>

<h3 id="hive-스타일-vs-iceberg-스타일">Hive 스타일 vs Iceberg 스타일</h3>

<p>Hive 스타일 파티셔닝은 사용자가 별도의 파티션 컬럼을 직접 관리해야 하지만, Iceberg의 Hidden Partitioning은 <strong>원본 컬럼에 transform 함수를 적용</strong>해서 파티션을 만듭니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Hive 스타일: 별도 파티션 컬럼을 사용자가 직접 관리</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="p">(...)</span> <span class="n">PARTITIONED</span> <span class="k">BY</span> <span class="p">(</span><span class="n">p_yyyymm</span> <span class="n">STRING</span><span class="p">);</span>
<span class="c1">-- 쿼리 시 파티션 컬럼을 명시해야 pruning 작동</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">orders</span> <span class="k">WHERE</span> <span class="n">p_yyyymm</span> <span class="o">=</span> <span class="s1">'202501'</span><span class="p">;</span>

<span class="c1">-- Iceberg hidden partition: 원본 컬럼에 transform 적용</span>
<span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="p">(...)</span> <span class="n">PARTITIONED</span> <span class="k">BY</span> <span class="p">(</span><span class="k">month</span><span class="p">(</span><span class="n">order_date</span><span class="p">));</span>
<span class="c1">-- 쿼리 시 원본 컬럼으로 필터하면 자동 pruning</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">orders</span> <span class="k">WHERE</span> <span class="n">order_date</span> <span class="o">&gt;=</span> <span class="s1">'2025-01-01'</span> <span class="k">AND</span> <span class="n">order_date</span> <span class="o">&lt;</span> <span class="s1">'2025-02-01'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="사용-가능한-transform-함수">사용 가능한 Transform 함수</h3>

<table>
  <thead>
    <tr>
      <th>Transform</th>
      <th>입력 타입</th>
      <th>예시</th>
      <th>파티션 값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">year(col)</code></td>
      <td>DATE, TIMESTAMP</td>
      <td><code class="language-plaintext highlighter-rouge">year(order_date)</code></td>
      <td>2025</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">month(col)</code></td>
      <td>DATE, TIMESTAMP</td>
      <td><code class="language-plaintext highlighter-rouge">month(order_date)</code></td>
      <td>2025-01</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">day(col)</code></td>
      <td>DATE, TIMESTAMP</td>
      <td><code class="language-plaintext highlighter-rouge">day(order_date)</code></td>
      <td>2025-01-15</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hour(col)</code></td>
      <td>TIMESTAMP</td>
      <td><code class="language-plaintext highlighter-rouge">hour(event_ts)</code></td>
      <td>2025-01-15-09</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bucket(N, col)</code></td>
      <td>모든 타입</td>
      <td><code class="language-plaintext highlighter-rouge">bucket(16, customer_id)</code></td>
      <td>0~15</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">truncate(col, W)</code></td>
      <td>STRING, INT 등</td>
      <td><code class="language-plaintext highlighter-rouge">truncate(zipcode, 3)</code></td>
      <td>100, 200</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">identity(col)</code></td>
      <td>모든 타입</td>
      <td><code class="language-plaintext highlighter-rouge">identity(region)</code></td>
      <td>원본 값 그대로</td>
    </tr>
  </tbody>
</table>

<h3 id="metadatajson에서-확인">metadata.json에서 확인</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"partition-specs"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"spec-id"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"fields"</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><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"order_date_month"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"transform"</span><span class="p">:</span><span class="w"> </span><span class="s2">"month"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"source-id"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
        </span><span class="nl">"field-id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000</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><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<h3 id="한글-컬럼명의-hidden-partition-제약">한글 컬럼명의 Hidden Partition 제약</h3>

<p>한글 컬럼을 원본으로 hidden partition을 지정하면 문제가 발생합니다. 이는 Iceberg + Parquet의 <strong>컬럼명 sanitization</strong> 때문입니다.</p>

<p>Iceberg의 Java 라이브러리는 Parquet 파일에 데이터를 쓸 때 non-ASCII 문자를 <code class="language-plaintext highlighter-rouge">_xHH</code> 형태로 인코딩합니다. <code class="language-plaintext highlighter-rouge">기준년월</code>이라는 컬럼명은 Parquet 파일 내부에서 각 한글 바이트가 인코딩되어 전혀 다른 문자열이 됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>metadata.json의 partition-spec
  → source column: "기준년월" (원본 이름으로 저장)

Parquet 파일 내부 컬럼명
  → "_xEA_xB8_xB0_xEC_xA4_x80_xEB_x85_x84_xEC_x9B_x94" (sanitized)

엔진이 partition transform 적용 시
  → metadata의 "기준년월"과 Parquet의 sanitized 이름 매칭 실패
</code></pre></div></div>

<p>Iceberg 스펙상 컬럼은 이름이 아닌 <strong>field ID</strong>로 매칭되어야 하지만, 실제 구현체에서 이 매핑이 완벽하지 않아 non-ASCII 컬럼명에서 문제가 발생합니다.</p>

<p><strong>대응 방안:</strong> 파티션 관련 컬럼은 영문으로 유지하는 것이 가장 안전합니다. 한글 원본 컬럼은 그대로 두고, 파티션용 영문 컬럼(<code class="language-plaintext highlighter-rouge">p_yyyymm</code> 등)을 별도로 관리하는 현재 구조가 올바른 설계입니다.</p>

<hr />

<h2 id="5-biglake--bigquery-연동-방식-3가지">5. BigLake × BigQuery 연동 방식 3가지</h2>

<p>GCP에서 Iceberg 테이블을 다루는 방식은 메타데이터 저장 위치에 따라 세 가지로 나뉩니다.</p>

<h3 id="5-1-biglake-managed-iceberg-table">5-1. BigLake Managed Iceberg Table</h3>

<p><strong>메타데이터:</strong> BigQuery 내부 (Big Metadata)<br />
<strong>데이터:</strong> GCS (Parquet)</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">my_table</span> <span class="p">(</span>
  <span class="n">id</span> <span class="n">INT64</span><span class="p">,</span>
  <span class="n">name</span> <span class="n">STRING</span><span class="p">,</span>
  <span class="n">created_at</span> <span class="nb">TIMESTAMP</span>
<span class="p">)</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.connection_name`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">file_format</span> <span class="o">=</span> <span class="s1">'PARQUET'</span><span class="p">,</span>
  <span class="n">table_format</span> <span class="o">=</span> <span class="s1">'ICEBERG'</span><span class="p">,</span>
  <span class="n">storage_uri</span> <span class="o">=</span> <span class="s1">'gs://my-bucket/my-table'</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>특징:</strong></p>

<ul>
  <li>BigQuery가 메타데이터를 내부적으로 관리하며, 표준 BigQuery 테이블과 동일한 사용 경험 제공</li>
  <li>데이터는 고객 소유의 GCS 버킷에 저장</li>
  <li>자동 파일 크기 최적화, 클러스터링, 메타데이터 컴팩션, 고아 파일 GC 자동 수행</li>
  <li>DML(<code class="language-plaintext highlighter-rouge">INSERT</code>, <code class="language-plaintext highlighter-rouge">UPDATE</code>, <code class="language-plaintext highlighter-rouge">DELETE</code>, <code class="language-plaintext highlighter-rouge">MERGE</code>) 완전 지원</li>
  <li>외부 엔진에서 읽으려면 <code class="language-plaintext highlighter-rouge">EXPORT TABLE METADATA</code> 필요</li>
</ul>

<h3 id="5-2-biglake-external-iceberg-table">5-2. BigLake External Iceberg Table</h3>

<p><strong>메타데이터:</strong> GCS (Iceberg 표준 metadata.json)<br />
<strong>데이터:</strong> GCS (Parquet)</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">EXTERNAL</span> <span class="k">TABLE</span> <span class="n">project</span><span class="p">.</span><span class="n">dataset</span><span class="p">.</span><span class="n">my_external_table</span>
<span class="k">WITH</span> <span class="k">CONNECTION</span> <span class="nv">`project.region.connection_name`</span>
<span class="k">OPTIONS</span> <span class="p">(</span>
  <span class="n">format</span> <span class="o">=</span> <span class="s1">'ICEBERG'</span><span class="p">,</span>
  <span class="n">uris</span> <span class="o">=</span> <span class="p">[</span><span class="nv">"gs://my-bucket/warehouse/table/metadata/iceberg.metadata.json"</span><span class="p">]</span>
<span class="p">);</span>
</code></pre></div></div>

<p><strong>특징:</strong></p>

<ul>
  <li>Spark/Flink 등 외부 엔진이 GCS에 직접 Iceberg 표준 메타데이터를 관리</li>
  <li>BigQuery에서는 <strong>읽기 전용</strong> (쓰기는 Spark 등에서)</li>
  <li><code class="language-plaintext highlighter-rouge">metadata.json</code> URI를 수동으로 업데이트해야 할 수 있음</li>
</ul>

<h3 id="5-3-biglake-metastore--rest-catalog">5-3. BigLake Metastore + REST Catalog</h3>

<p><strong>메타데이터:</strong> BigLake Metastore (관리형 서비스)<br />
<strong>데이터:</strong> GCS (Parquet)</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog"</span><span class="p">,</span> <span class="s">"org.apache.iceberg.spark.SparkCatalog"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.type"</span><span class="p">,</span> <span class="s">"rest"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.uri"</span><span class="p">,</span>
    <span class="s">"https://biglake.googleapis.com/iceberg/v1/restcatalog"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.catalog.my_catalog.warehouse"</span><span class="p">,</span> <span class="s">"gs://my-bucket"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>특징:</strong></p>

<ul>
  <li>Spark, BigQuery 양쪽에서 읽기/쓰기 가능</li>
  <li>메타스토어를 직접 관리할 필요 없음 (서버리스)</li>
  <li>표준 Iceberg REST Catalog API 지원</li>
</ul>

<h3 id="비교-요약">비교 요약</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Managed (BigQuery 내부)</th>
      <th>External (GCS 메타)</th>
      <th>BigLake Metastore</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>메타 위치</td>
      <td>BigQuery 내부</td>
      <td>GCS metadata.json</td>
      <td>BigLake Metastore 서비스</td>
    </tr>
    <tr>
      <td>BigQuery DML</td>
      <td>전체 지원</td>
      <td>읽기 전용</td>
      <td>읽기 + 쓰기</td>
    </tr>
    <tr>
      <td>Spark 쓰기</td>
      <td>Storage Write API</td>
      <td>직접 가능</td>
      <td>REST Catalog로 가능</td>
    </tr>
    <tr>
      <td>자동 최적화</td>
      <td>compaction, GC 자동</td>
      <td>없음 (직접 관리)</td>
      <td>부분 지원</td>
    </tr>
    <tr>
      <td>메타 동기화</td>
      <td>자동</td>
      <td>수동 URI 업데이트</td>
      <td>자동</td>
    </tr>
    <tr>
      <td>멀티엔진</td>
      <td>export 필요</td>
      <td>네이티브 지원</td>
      <td>네이티브 지원</td>
    </tr>
  </tbody>
</table>

<p>Spark로 GCS에 데이터를 올리고 BigQuery에서 읽는 구조라면 <strong>BigLake Metastore + REST Catalog</strong> 방식이 가장 적합합니다. Spark에서 직접 쓰기가 가능하면서 BigQuery에서도 바로 접근이 되고, metadata.json을 수동으로 업데이트할 필요도 없습니다.</p>

<hr />

<h2 id="6-spark--iceberg-쓰기-성능-문제와-해결">6. Spark → Iceberg 쓰기 성능 문제와 해결</h2>

<h3 id="문제-external-table-34분--iceberg-4시간">문제: External Table 3~4분 → Iceberg 4시간</h3>

<p>기존 Hive 스타일 external table에서 3~4분 걸리던 적재가 Iceberg external table로 변경 후 4시간으로 늘어나는 현상이 발생할 수 있습니다.</p>

<h3 id="핵심-원인-writedistribution-mode의-shuffle">핵심 원인: write.distribution-mode의 Shuffle</h3>

<p>Iceberg의 기본 <code class="language-plaintext highlighter-rouge">write.distribution-mode</code>가 <code class="language-plaintext highlighter-rouge">hash</code>이기 때문에, 기존 external table에서는 없던 대규모 shuffle이 발생합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>기존 external table:  데이터 → 바로 Parquet 쓰기 → 끝
Iceberg table:        데이터 → hash shuffle(파티션 키 기준) → 정렬 → Parquet 쓰기 → 메타 커밋
</code></pre></div></div>

<p>하나의 파티션에 대량 데이터가 몰리면, 파티션당 하나의 태스크만 쓰기를 담당하게 되어 쓰기 속도가 극도로 느려집니다.</p>

<h3 id="해결-distribution-mode-변경">해결: distribution mode 변경</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="k">catalog</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="n">my_table</span> <span class="k">SET</span> <span class="n">TBLPROPERTIES</span> <span class="p">(</span>
  <span class="s1">'write.distribution-mode'</span> <span class="o">=</span> <span class="s1">'none'</span><span class="p">,</span>
  <span class="s1">'write.spark.fanout.enabled'</span> <span class="o">=</span> <span class="s1">'true'</span>
<span class="p">);</span>
</code></pre></div></div>

<p>이 설정은 Iceberg 테이블의 <code class="language-plaintext highlighter-rouge">metadata.json</code>에 기록되므로, 한 번 설정하면 이후 어떤 Spark 세션에서든 동일하게 적용됩니다.</p>

<p>현재 설정값 확인:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SHOW</span> <span class="n">TBLPROPERTIES</span> <span class="k">catalog</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="n">my_table</span> <span class="p">(</span><span class="s1">'write.distribution-mode'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="hash-vs-none-모드의-파일-차이">hash vs none 모드의 파일 차이</h3>

<p>100개 Spark Task가 12개 파티션에 10GB를 쓴다고 가정할 때:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>hash (기본값)</th>
      <th>none</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>파일 수</td>
      <td>~12개 (파티션당 1~2개)</td>
      <td>~1,200개 (Task × 파티션 조합)</td>
    </tr>
    <tr>
      <td>파일 크기</td>
      <td>균일 (400~800MB)</td>
      <td>들쭉날쭉 (수KB ~ 수십MB)</td>
    </tr>
    <tr>
      <td>파티션 내 정렬</td>
      <td>정렬됨</td>
      <td>정렬 안 됨</td>
    </tr>
    <tr>
      <td>쓰기 속도</td>
      <td>느림 (shuffle 있음)</td>
      <td>빠름 (shuffle 없음)</td>
    </tr>
    <tr>
      <td>읽기 성능</td>
      <td>좋음</td>
      <td>나쁨 (small files)</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">none</code> 모드에서는 각 Task가 자기가 가진 데이터를 파티션별로 나눠서 쓰기 때문에, 하나의 파티션에 여러 개의 작은 파일이 생성됩니다. 또한 하나의 파일에 여러 파티션의 데이터가 섞이면 min/max 범위가 넓어져서 data skipping 효과도 줄어듭니다.</p>

<h3 id="추가-spark-튜닝">추가 Spark 튜닝</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.adaptive.enabled"</span><span class="p">,</span> <span class="s">"true"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.adaptive.coalescePartitions.enabled"</span><span class="p">,</span> <span class="s">"true"</span><span class="p">)</span>
<span class="n">spark</span><span class="p">.</span><span class="n">conf</span><span class="p">.</span><span class="nb">set</span><span class="p">(</span><span class="s">"spark.sql.shuffle.partitions"</span><span class="p">,</span> <span class="s">"200"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="7-compaction으로-사후-재정돈">7. Compaction으로 사후 재정돈</h2>

<p><code class="language-plaintext highlighter-rouge">none</code> 모드로 빠르게 쓰면 small files가 발생하지만, Iceberg는 이를 위한 테이블 유지보수 프로시저를 제공합니다.</p>

<h3 id="7-1-rewrite_data_files--데이터-파일-병합">7-1. rewrite_data_files — 데이터 파일 병합</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 기본 compaction</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rewrite_data_files</span><span class="p">(</span><span class="s1">'db.my_table'</span><span class="p">);</span>

<span class="c1">-- 특정 파티션만 compaction</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rewrite_data_files</span><span class="p">(</span>
  <span class="k">table</span> <span class="o">=&gt;</span> <span class="s1">'db.my_table'</span><span class="p">,</span>
  <span class="k">where</span> <span class="o">=&gt;</span> <span class="s1">'p_yyyymm = "202501"'</span>
<span class="p">);</span>

<span class="c1">-- 파일 사이즈 기준 지정</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rewrite_data_files</span><span class="p">(</span>
  <span class="k">table</span> <span class="o">=&gt;</span> <span class="s1">'db.my_table'</span><span class="p">,</span>
  <span class="k">options</span> <span class="o">=&gt;</span> <span class="k">map</span><span class="p">(</span>
    <span class="s1">'target-file-size-bytes'</span><span class="p">,</span> <span class="s1">'536870912'</span><span class="p">,</span>
    <span class="s1">'min-file-size-bytes'</span><span class="p">,</span> <span class="s1">'67108864'</span><span class="p">,</span>
    <span class="s1">'max-file-size-bytes'</span><span class="p">,</span> <span class="s1">'1073741824'</span>
  <span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Before (none 모드 적재 직후):
  파티션 202501/
    ├── file_001.parquet (8MB)   ← 기준년월 min:202412, max:202503
    ├── file_002.parquet (3MB)   ← 기준년월 min:202501, max:202502
    └── ... (100개)

After (rewrite_data_files 실행 후):
  파티션 202501/
    └── file_merged_001.parquet (500MB)  ← 기준년월 min:202501, max:202501
</code></pre></div></div>

<p>파일이 합쳐지면서 <strong>column statistics도 타이트해지므로</strong> data skipping 효과도 복원됩니다.</p>

<h3 id="7-2-sort-전략으로-정렬까지-적용">7-2. sort 전략으로 정렬까지 적용</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 정렬 기준 설정</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="k">catalog</span><span class="p">.</span><span class="n">db</span><span class="p">.</span><span class="n">my_table</span>
<span class="k">WRITE</span> <span class="n">ORDERED</span> <span class="k">BY</span> <span class="err">기준년월</span><span class="p">,</span> <span class="n">customer_id</span><span class="p">;</span>

<span class="c1">-- sort 전략으로 compaction</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rewrite_data_files</span><span class="p">(</span>
  <span class="k">table</span> <span class="o">=&gt;</span> <span class="s1">'db.my_table'</span><span class="p">,</span>
  <span class="n">strategy</span> <span class="o">=&gt;</span> <span class="s1">'sort'</span><span class="p">,</span>
  <span class="n">sort_order</span> <span class="o">=&gt;</span> <span class="s1">'기준년월 ASC, customer_id ASC'</span>
<span class="p">);</span>
</code></pre></div></div>

<h3 id="7-3-rewrite_manifests--메타데이터-정돈">7-3. rewrite_manifests — 메타데이터 정돈</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rewrite_manifests</span><span class="p">(</span><span class="s1">'db.my_table'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="7-4-오래된-스냅샷-및-고아-파일-정리">7-4. 오래된 스냅샷 및 고아 파일 정리</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 스냅샷 정리</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">expire_snapshots</span><span class="p">(</span>
  <span class="k">table</span> <span class="o">=&gt;</span> <span class="s1">'db.my_table'</span><span class="p">,</span>
  <span class="n">older_than</span> <span class="o">=&gt;</span> <span class="nb">TIMESTAMP</span> <span class="s1">'2026-02-25 00:00:00'</span><span class="p">,</span>
  <span class="n">retain_last</span> <span class="o">=&gt;</span> <span class="mi">5</span>
<span class="p">);</span>

<span class="c1">-- 고아 파일 정리</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">remove_orphan_files</span><span class="p">(</span><span class="s1">'db.my_table'</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="핵심-재정돈-중에도-테이블은-정상-작동">핵심: 재정돈 중에도 테이블은 정상 작동</h3>

<p>Iceberg의 스냅샷 격리 덕분에, 재정돈 중에도 테이블 읽기가 정상 동작합니다. 재정돈이 완료되면 새 스냅샷으로 커밋되고, 이후 쿼리부터 정돈된 파일을 사용하게 됩니다. 다운타임 없이 온라인으로 수행 가능합니다.</p>

<hr />

<h2 id="8-실무-운영-패턴">8. 실무 운영 패턴</h2>

<h3 id="시간이-이동하는-것-아닌가">시간이 “이동”하는 것 아닌가?</h3>

<p>전체 작업량으로 보면 그렇지 않습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hash 모드:       [shuffle + 정렬 + 쓰기]  = 4시간 (한 덩어리)
none + 재정돈:    [쓰기] 4분  +  [compaction] 30분~1시간  = 총 34분~64분
</code></pre></div></div>

<p>hash 모드의 4시간 중 상당 부분이 <strong>단일 Task에 데이터가 몰리는 비효율</strong> 때문입니다. compaction은 이미 쓰여진 파일을 <strong>병렬로</strong> 읽고 병합하므로 같은 작업이 훨씬 효율적으로 수행됩니다.</p>

<h3 id="결정적-차이-적재와-재정돈의-분리">결정적 차이: 적재와 재정돈의 분리</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>hash 모드:
  09:00 적재 시작 → 13:00 적재 완료 → 13:00 데이터 사용 가능
                     (4시간 동안 데이터 없음)

none + 재정돈:
  09:00 적재 시작 → 09:04 적재 완료 → 09:04 데이터 사용 가능 (비최적 상태)
                                      09:04 compaction 시작
                                      09:40 compaction 완료 → 최적 상태
</code></pre></div></div>

<p><strong>4분 후부터 데이터를 바로 쿼리할 수 있습니다.</strong> small files 상태라서 최적은 아니지만, 급한 리포트나 확인 작업을 기다릴 필요가 없습니다.</p>

<h3 id="매일-배치-적재-패턴">매일 배치 적재 패턴</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 1. 빠르게 적재 (none 모드, 3~4분)
</span><span class="n">spark</span><span class="p">.</span><span class="n">sql</span><span class="p">(</span><span class="s">"INSERT INTO catalog.db.my_table SELECT * FROM source"</span><span class="p">)</span>

<span class="c1"># 2. 당일 파티션만 compaction
</span><span class="n">spark</span><span class="p">.</span><span class="n">sql</span><span class="p">(</span><span class="s">"""
  CALL catalog.system.rewrite_data_files(
    table =&gt; 'db.my_table',
    where =&gt; 'p_yyyymm = "202503"'
  )
"""</span><span class="p">)</span>

<span class="c1"># 3. manifest 재정돈
</span><span class="n">spark</span><span class="p">.</span><span class="n">sql</span><span class="p">(</span><span class="s">"CALL catalog.system.rewrite_manifests('db.my_table')"</span><span class="p">)</span>

<span class="c1"># 4. 주 1회: 오래된 스냅샷 정리
</span><span class="n">spark</span><span class="p">.</span><span class="n">sql</span><span class="p">(</span><span class="s">"""
  CALL catalog.system.expire_snapshots(
    table =&gt; 'db.my_table',
    older_than =&gt; TIMESTAMP '2026-02-25 00:00:00',
    retain_last =&gt; 10
  )
"""</span><span class="p">)</span>
</code></pre></div></div>

<h3 id="상황별-권장">상황별 권장</h3>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>권장</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>적재 후 바로 대시보드/리포트에 사용</td>
      <td>none + 즉시 compaction</td>
    </tr>
    <tr>
      <td>적재 후 다음 날 아침에 사용</td>
      <td>none + 야간 compaction</td>
    </tr>
    <tr>
      <td>적재 빈도가 높고 읽기는 가끔</td>
      <td>none + 주 1회 compaction</td>
    </tr>
    <tr>
      <td>적재 빈도 낮고 읽기가 핵심</td>
      <td>hash 모드 유지</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="9-iceberg-manifest-파일-직접-확인하는-방법">9. Iceberg Manifest 파일 직접 확인하는 방법</h2>

<p>pruning이 실제로 동작하는지 확인하려면 manifest 파일을 직접 열어봐야 합니다. manifest 파일은 Avro 형식이므로 전용 도구가 필요합니다.</p>

<h3 id="avro-tools-java-기반">avro-tools (Java 기반)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://repo1.maven.org/maven2/org/apache/avro/avro-tools/1.11.3/avro-tools-1.11.3.jar
gsutil <span class="nb">cp </span>gs://bucket/path/to/metadata/manifest-xxxxx.avro /tmp/

java <span class="nt">-jar</span> avro-tools-1.11.3.jar tojson /tmp/manifest-xxxxx.avro | python3 <span class="nt">-m</span> json.tool
</code></pre></div></div>

<h3 id="python-fastavro">Python fastavro</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>fastavro
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">fastavro</span>
<span class="kn">import</span> <span class="nn">json</span>

<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">'/tmp/manifest-xxxxx.avro'</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">reader</span> <span class="o">=</span> <span class="n">fastavro</span><span class="p">.</span><span class="n">reader</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">reader</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">record</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="nb">str</span><span class="p">))</span>
</code></pre></div></div>

<h3 id="주로-봐야-할-필드">주로 봐야 할 필드</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"data_file"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gs://bucket/data/p_yyyymm=202501/file1.parquet"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"partition"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"p_yyyymm"</span><span class="p">:</span><span class="w"> </span><span class="s2">"202501"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"record_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">50000</span><span class="p">,</span><span class="w">
    </span><span class="nl">"lower_bounds"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"202501"</span><span class="p">,</span><span class="w"> </span><span class="nl">"2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"A0001"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"upper_bounds"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"202501"</span><span class="p">,</span><span class="w"> </span><span class="nl">"2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Z9999"</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><code class="language-plaintext highlighter-rouge">lower_bounds</code>와 <code class="language-plaintext highlighter-rouge">upper_bounds</code>에 <code class="language-plaintext highlighter-rouge">기준년월</code> 컬럼의 min/max가 기록되어 있다면, BigQuery가 이를 보고 파일 pruning을 하고 있는 것입니다. 컬럼 ID와 실제 컬럼명의 매핑은 <code class="language-plaintext highlighter-rouge">metadata.json</code>의 <code class="language-plaintext highlighter-rouge">schemas</code> 섹션에서 확인할 수 있습니다.</p>

<hr />

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

<table>
  <thead>
    <tr>
      <th>주제</th>
      <th>핵심</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Iceberg 메타데이터</td>
      <td>서비스가 아닌 파일 구조 규격. manifest에 파일별 column statistics 포함</td>
    </tr>
    <tr>
      <td>스캔량 최적화</td>
      <td>파티션 컬럼이 아니어도 manifest의 min/max로 data skipping 가능</td>
    </tr>
    <tr>
      <td>Hidden Partition</td>
      <td>transform 함수 기반 자동 pruning. 한글 컬럼은 sanitization 이슈로 사용 불가</td>
    </tr>
    <tr>
      <td>BigLake 연동</td>
      <td>Managed / External / BigLake Metastore 세 가지 방식. 멀티엔진이면 Metastore 권장</td>
    </tr>
    <tr>
      <td>쓰기 성능</td>
      <td><code class="language-plaintext highlighter-rouge">write.distribution-mode=none</code>으로 shuffle 제거 후 사후 compaction</td>
    </tr>
    <tr>
      <td>운영 패턴</td>
      <td>빠른 적재 → compaction 분리로 데이터 가용성과 읽기 성능 모두 확보</td>
    </tr>
  </tbody>
</table>

<p>Iceberg는 “빠르게 쓰고 나중에 정돈한다”는 운영 방식이 의도된 설계이며, 이를 위한 도구가 잘 갖춰져 있습니다. GCP 환경에서는 BigLake를 통해 Spark 쓰기와 BigQuery 분석을 자연스럽게 연결할 수 있으므로, 각 연동 방식의 특성을 이해하고 환경에 맞는 조합을 선택하는 것이 중요합니다.</p>]]></content><author><name>Seonghak</name><email>seonghak@mz.co.kr</email></author><category term="iceberg" /><category term="biglake" /><category term="bigquery" /><category term="gcp" /><category term="data-engineering" /><category term="spark" /><category term="partition" /><category term="pruning" /><category term="compaction" /><summary type="html"><![CDATA[GCP에서 Apache Iceberg를 BigLake/BigQuery와 연동하면, Spark로 적재하고 BigQuery로 분석하는 유연한 데이터 아키텍처를 구현할 수 있습니다. 하지만 실제 운영 과정에서 예상치 못한 스캔량 차이, 쓰기 성능 저하, 한글 컬럼 이슈 등을 마주하게 됩니다.]]></summary></entry></feed>