<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ko-KR"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://hajekim.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hajekim.github.io/" rel="alternate" type="text/html" hreflang="ko-KR" /><updated>2026-06-06T10:40:01+09:00</updated><id>https://hajekim.github.io/feed.xml</id><title type="html">haje.log</title><subtitle>Solutions Architect</subtitle><entry><title type="html">Oracle 쿼리를 다른 DB에 그대로 옮기면 틀리는 이유</title><link href="https://hajekim.github.io/data/2026/06/02/financial-settlement-db-precision-guide/" rel="alternate" type="text/html" title="Oracle 쿼리를 다른 DB에 그대로 옮기면 틀리는 이유" /><published>2026-06-02T00:00:00+09:00</published><updated>2026-06-02T00:00:00+09:00</updated><id>https://hajekim.github.io/data/2026/06/02/financial-settlement-db-precision-guide</id><content type="html" xml:base="https://hajekim.github.io/data/2026/06/02/financial-settlement-db-precision-guide/"><![CDATA[<h2 id="1-도입">1. 도입</h2>
<p>기업 환경에서 라이선스 비용이나 클라우드 도입을 위해 불가피하게 오라클 데이터베이스에서 다른 데이터베이스로 전환해야 하는 일을 겪게 됩니다. 오라클은 좋은 데이터베이스이며 오랫동안 잘 사용해왔다면 오라클과 다른 데이터베이스의 차이점과 특징을 간과하여 슬픈 현실들을 마주하게 됩니다.</p>

<p>여기 수년간 잘 돌아가던 대출 이자 산출 쿼리가 있습니다. 문법만 바꿔서 여러 데이터베이스에서 돌려봤습니다. 그런데 오라클과 1원의 차이가 발생합니다. 같은 쿼리인데 말이죠. 특히나 금융 환경 같은 경우는 1원의 오차를 그냥 넘어갈 수 없습니다. 허용 오차는 0입니다.</p>

<p>이 포스트에서 그 1원의 오차가 왜 생기는지 정리해보았습니다. 원인은 생각보다 깊은 곳에 있었고, 해결책은 두 가지였습니다. <strong>후순위 나눗셈</strong>과 <strong>명시적 형변환</strong>이었습니다. BigQuery, Spanner, PostgreSQL, MySQL, Presto, Trino 6종의 데이터베이스를 테스트해보았습니다.</p>

<h2 id="2-테스트-환경">2. 테스트 환경</h2>

<ul>
  <li><strong>원천 환경</strong>: Oracle Database 21c (대출 이자 산출 쿼리, 100,000건 데이터)</li>
  <li><strong>테스트 대상</strong>: Google BigQuery, Google Spanner, PostgreSQL, MySQL, Presto, Trino 총 6종</li>
  <li><strong>테스트 목표</strong>: Oracle 결과와 동일하게 1원도 오차가 발생하지 않도록</li>
</ul>

<h3 id="종합-테스트-결과-요약">종합 테스트 결과 요약</h3>

<p>같은 쿼리를 각 플랫폼에서 그대로 실행했을 때와, 후순위 나눗셈 적용 후 결과를 나란히 정리했습니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">데이터베이스</th>
      <th style="text-align: left">수치 타입</th>
      <th style="text-align: left">중간 연산 정밀도 처리</th>
      <th style="text-align: left">번역 쿼리 일치율 (10만 건)</th>
      <th style="text-align: left">후순위 나눗셈 적용 후</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Oracle</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
      <td style="text-align: left">가변 길이 Base-100 인코딩, 단계별 고정 스케일 정규화 없음</td>
      <td style="text-align: left"><strong>100%</strong> (0건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>BigQuery</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BIGNUMERIC</code></td>
      <td style="text-align: left">scale=38 고정 소수점, 연산 단계별 정규화</td>
      <td style="text-align: left"><strong>99.9990%</strong> (1건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Spanner</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMERIC</code></td>
      <td style="text-align: left">scale=9 고정 소수점, ROUND_HALF_UP</td>
      <td style="text-align: left"><strong>99.9840%</strong> (16건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PostgreSQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMERIC</code></td>
      <td style="text-align: left">임의 정밀도(Arbitrary Precision)</td>
      <td style="text-align: left"><strong>100%</strong> (0건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>MySQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">div_precision_increment</code> 한계</td>
      <td style="text-align: left"><strong>99.9980%</strong> (2건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Trino</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: left">38자리 고정, 초과 시 스케일 축소</td>
      <td style="text-align: left"><strong>81.7680%</strong> (18,232건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Presto</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">max(s1,s2)</code> 엄격 적용, 조기 스케일 축소</td>
      <td style="text-align: left"><strong>4.5840%</strong> (95,416건 차이)</td>
      <td style="text-align: left"><strong>100%</strong></td>
    </tr>
  </tbody>
</table>

<h3 id="테스트-절차-요약">테스트 절차 요약</h3>

<pre><code class="language-mermaid">graph TD
    A[Oracle 데이터 추출] --&gt; B[대상 DB 스키마 정의 및 데이터 로드]
    B --&gt; C[1차 번역 쿼리 실행]
    C --&gt; D[정합성 테스트 및 오차 발생]
    D --&gt; E[DB별 소수점 연산 방식 분석]
    E --&gt; F[대수적 통분 및 후순위 나눗셈 수식 설계]
    F --&gt; G[최적화 쿼리 실행]
    G --&gt; H[6종 DB 전수 테스트 및 결과 기록]
</code></pre>

<h2 id="3-대출-이자-수식-및-스키마-이행">3. 대출 이자 수식 및 스키마 이행</h2>

<h3 id="31-스키마-매핑-분석">3.1 스키마 매핑 분석</h3>

<p>소수점 정밀도 손실을 막기 위해 Oracle <code class="language-plaintext highlighter-rouge">NUMBER</code> 타입에 BigQuery <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code> 타입을 매핑했습니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">컬럼명</th>
      <th style="text-align: left">Oracle 타입</th>
      <th style="text-align: left">BigQuery 타입</th>
      <th style="text-align: left">설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">기준년월</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">VARCHAR2(6)</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">STRING</code></td>
      <td style="text-align: left">정산 기준년월</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">고객관리번호</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER(15)</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BIGNUMERIC(15)</code></td>
      <td style="text-align: left">고객 식별 번호</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">대출관리번호</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">VARCHAR2(20)</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">STRING</code></td>
      <td style="text-align: left">대출 건 식별 번호</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">대출잔액</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER(15)</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BIGNUMERIC(15)</code></td>
      <td style="text-align: left">이자 계산 기준 잔액</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">약정금리</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER(5,2)</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BIGNUMERIC(5,2)</code></td>
      <td style="text-align: left">연 이자율 (%)</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">대출실행일자</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DATE</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DATE</code></td>
      <td style="text-align: left">이자 기산 시작일</td>
    </tr>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">이자계산기준일</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DATE</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DATE</code></td>
      <td style="text-align: left">이자 정산 기준일</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong><code class="language-plaintext highlighter-rouge">BIGNUMERIC(precision, scale)</code></strong>: 괄호 안 첫 번째 숫자는 전체 유효 자릿수, 두 번째는 소수점 이하 자릿수입니다. <code class="language-plaintext highlighter-rouge">BIGNUMERIC(5,2)</code>는 최대 <code class="language-plaintext highlighter-rouge">999.99</code>까지 표현할 수 있으며, 이자율 <code class="language-plaintext highlighter-rouge">8.50</code>, <code class="language-plaintext highlighter-rouge">14.60</code> 같은 값을 저장합니다.</p>
</blockquote>

<p><strong>BigQuery 스키마 정의 (<code class="language-plaintext highlighter-rouge">schema.json</code>)</strong>:</p>

<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="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"기준년월"</span><span class="p">,</span><span class="w">       </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"STRING"</span><span class="p">,</span><span class="w">     </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"고객관리번호"</span><span class="p">,</span><span class="w">   </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BIGNUMERIC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"대출관리번호"</span><span class="p">,</span><span class="w">   </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"STRING"</span><span class="p">,</span><span class="w">     </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"대출잔액"</span><span class="p">,</span><span class="w">       </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BIGNUMERIC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"약정금리"</span><span class="p">,</span><span class="w">       </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BIGNUMERIC"</span><span class="p">,</span><span class="w"> </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"대출실행일자"</span><span class="p">,</span><span class="w">   </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DATE"</span><span class="p">,</span><span class="w">       </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">},</span><span class="w">
  </span><span class="p">{</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"이자계산기준일"</span><span class="p">,</span><span class="w"> </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DATE"</span><span class="p">,</span><span class="w">       </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"REQUIRED"</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p><strong>데이터 로드 스크립트 (<code class="language-plaintext highlighter-rouge">prepare_and_load.py</code>)</strong>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">csv</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">subprocess</span>

<span class="k">def</span> <span class="nf">prepare_csv</span><span class="p">():</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"oracle_data.csv"</span><span class="p">,</span> <span class="s">"r"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">fin</span><span class="p">:</span>
        <span class="n">reader</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">reader</span><span class="p">(</span><span class="n">fin</span><span class="p">)</span>
        <span class="n">rows</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">reader</span><span class="p">)</span>

    <span class="n">bq_rows</span> <span class="o">=</span> <span class="p">[</span><span class="n">rows</span><span class="p">[</span><span class="mi">0</span><span class="p">][:</span><span class="mi">7</span><span class="p">]]</span>
    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">[</span><span class="mi">1</span><span class="p">:]:</span>
        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">r</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">7</span><span class="p">:</span>
            <span class="n">bq_rows</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">r</span><span class="p">[:</span><span class="mi">7</span><span class="p">])</span>

    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"bq_load_data.csv"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">,</span> <span class="n">newline</span><span class="o">=</span><span class="s">""</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">fout</span><span class="p">:</span>
        <span class="n">writer</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">writer</span><span class="p">(</span><span class="n">fout</span><span class="p">)</span>
        <span class="n">writer</span><span class="p">.</span><span class="n">writerows</span><span class="p">(</span><span class="n">bq_rows</span><span class="p">)</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Prepared bq_load_data.csv with </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">bq_rows</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="si">}</span><span class="s"> rows."</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">create_and_load_bq</span><span class="p">():</span>
    <span class="n">schema</span> <span class="o">=</span> <span class="p">[</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">"type"</span><span class="p">:</span> <span class="s">"STRING"</span><span class="p">,</span>     <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</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">"type"</span><span class="p">:</span> <span class="s">"BIGNUMERIC"</span><span class="p">,</span> <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</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">"type"</span><span class="p">:</span> <span class="s">"STRING"</span><span class="p">,</span>     <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</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">"type"</span><span class="p">:</span> <span class="s">"BIGNUMERIC"</span><span class="p">,</span> <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</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">"type"</span><span class="p">:</span> <span class="s">"BIGNUMERIC"</span><span class="p">,</span> <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</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">"type"</span><span class="p">:</span> <span class="s">"DATE"</span><span class="p">,</span>       <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</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">"type"</span><span class="p">:</span> <span class="s">"DATE"</span><span class="p">,</span>       <span class="s">"mode"</span><span class="p">:</span> <span class="s">"REQUIRED"</span><span class="p">},</span>
    <span class="p">]</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"schema.json"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">sf</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">schema</span><span class="p">,</span> <span class="n">sf</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">ensure_ascii</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>

    <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">([</span><span class="s">"bq"</span><span class="p">,</span> <span class="s">"rm"</span><span class="p">,</span> <span class="s">"-f"</span><span class="p">,</span> <span class="s">"-t"</span><span class="p">,</span> <span class="s">"DM.월별대출이자산출"</span><span class="p">],</span> <span class="n">check</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s">"bq"</span><span class="p">,</span> <span class="s">"load"</span><span class="p">,</span> <span class="s">"--source_format=CSV"</span><span class="p">,</span> <span class="s">"--skip_leading_rows=1"</span><span class="p">,</span>
        <span class="s">"DM.월별대출이자산출"</span><span class="p">,</span> <span class="s">"bq_load_data.csv"</span><span class="p">,</span> <span class="s">"schema.json"</span>
    <span class="p">]</span>
    <span class="n">res</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">res</span><span class="p">.</span><span class="n">returncode</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Successfully loaded data into BigQuery table DM.월별대출이자산출!"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Failed:"</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="n">stderr</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">prepare_csv</span><span class="p">()</span>
    <span class="n">create_and_load_bq</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="32-대출-이자-수식-정의">3.2 대출 이자 수식 정의</h3>

<p>보통 이자는 대출 실행일 다음 날부터 계산합니다.<br />
정산 주기 내 최소 이자를 보장하는 <strong>기산가산일수(30일)</strong> 조건이 수식에 붙어 있습니다.</p>

\[\text{이자금액} = \text{TRUNC}\!\left(\text{대출잔액} \times \frac{\text{약정금리}}{100} \times \frac{\text{경과일수} + 30}{365},\ 0\right)\]

<blockquote>
  <p><strong><code class="language-plaintext highlighter-rouge">TRUNC(값, 소수점_자릿수)</code></strong>: 두 번째 인자가 소수점 이하 몇 자리까지 남길지를 결정합니다. <code class="language-plaintext highlighter-rouge">TRUNC(76551.789, 0)</code>은 소수점 아래를 모두 버려 <code class="language-plaintext highlighter-rouge">76551</code>이 됩니다. 반올림이 아니라 버림이라, <code class="language-plaintext highlighter-rouge">76551.999</code>도 <code class="language-plaintext highlighter-rouge">76551</code>입니다.</p>
</blockquote>

<p><strong>Oracle 원천 SQL</strong>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">N10</span><span class="p">.</span><span class="err">약정금리</span>
     <span class="p">,</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">약정금리</span> <span class="o">/</span> <span class="mi">100</span><span class="p">)</span>                                                <span class="k">AS</span> <span class="err">연산</span><span class="mi">1</span>
     <span class="p">,</span> <span class="n">N10</span><span class="p">.</span><span class="err">대출잔액</span>
     <span class="p">,</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">대출잔액</span> <span class="o">*</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">약정금리</span> <span class="o">/</span> <span class="mi">100</span><span class="p">))</span>                               <span class="k">AS</span> <span class="err">연산</span><span class="mi">2</span>
     <span class="p">,</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">이자계산기준일</span> <span class="o">-</span> <span class="n">N10</span><span class="p">.</span><span class="err">대출실행일자</span><span class="p">)</span>                              <span class="k">AS</span> <span class="err">연산</span><span class="mi">3</span>
     <span class="p">,</span> <span class="p">((</span><span class="n">N10</span><span class="p">.</span><span class="err">이자계산기준일</span> <span class="o">-</span> <span class="n">N10</span><span class="p">.</span><span class="err">대출실행일자</span><span class="p">)</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span>                      <span class="k">AS</span> <span class="err">연산</span><span class="mi">4</span>
     <span class="p">,</span> <span class="p">(((</span><span class="n">N10</span><span class="p">.</span><span class="err">이자계산기준일</span> <span class="o">-</span> <span class="n">N10</span><span class="p">.</span><span class="err">대출실행일자</span><span class="p">)</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span> <span class="o">+</span> <span class="mi">30</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span>        <span class="k">AS</span> <span class="err">연산</span><span class="mi">5</span>
     <span class="p">,</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">대출잔액</span> <span class="o">*</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">약정금리</span> <span class="o">/</span> <span class="mi">100</span><span class="p">))</span>
       <span class="o">*</span> <span class="p">(((</span><span class="n">N10</span><span class="p">.</span><span class="err">이자계산기준일</span> <span class="o">-</span> <span class="n">N10</span><span class="p">.</span><span class="err">대출실행일자</span><span class="p">)</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span> <span class="o">+</span> <span class="mi">30</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span>      <span class="k">AS</span> <span class="err">연산</span><span class="mi">6</span>
     <span class="p">,</span> <span class="n">TRUNC</span><span class="p">(</span>
         <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">대출잔액</span> <span class="o">*</span> <span class="p">(</span><span class="n">N10</span><span class="p">.</span><span class="err">약정금리</span> <span class="o">/</span> <span class="mi">100</span><span class="p">))</span>
         <span class="o">*</span> <span class="p">(((</span><span class="n">N10</span><span class="p">.</span><span class="err">이자계산기준일</span> <span class="o">-</span> <span class="n">N10</span><span class="p">.</span><span class="err">대출실행일자</span><span class="p">)</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span> <span class="o">+</span> <span class="mi">30</span> <span class="o">/</span> <span class="mi">365</span><span class="p">)</span>
       <span class="p">,</span> <span class="mi">0</span><span class="p">)</span>                                                                <span class="k">AS</span> <span class="err">이자금액</span>
  <span class="k">FROM</span> <span class="err">월별대출이자산출</span> <span class="n">N10</span>
</code></pre></div></div>

<h2 id="4-문제-발견-그대로-번역한-쿼리에서-1원-오차-발생">4. 문제 발견: 그대로 번역한 쿼리에서 1원 오차 발생</h2>

<p>Oracle 원천 SQL을 각 데이터베이스 문법에 맞게 번역하여 실행했습니다. 여기서는 BigQuery를 예시로 설명합니다.</p>

<p><strong>번역 쿼리</strong>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- naive: 나눗셈을 수식 중간에 배치 (오차 발생 구간)</span>
<span class="k">SELECT</span> <span class="err">대출관리번호</span>
     <span class="p">,</span> <span class="n">TRUNC</span><span class="p">(</span>
         <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="err">대출잔액</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="err">약정금리</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">100</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)))</span>
         <span class="o">*</span> <span class="p">((</span><span class="k">CAST</span><span class="p">(</span><span class="n">DATE_DIFF</span><span class="p">(</span><span class="err">이자계산기준일</span><span class="p">,</span> <span class="err">대출실행일자</span><span class="p">,</span> <span class="k">DAY</span><span class="p">)</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">365</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">))</span>
            <span class="o">+</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">30</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">365</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">))</span>
       <span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="err">이자금액</span>
  <span class="k">FROM</span> <span class="nv">`DM.월별대출이자산출`</span>
 <span class="k">ORDER</span> <span class="k">BY</span> <span class="err">대출관리번호</span>
</code></pre></div></div>

<h3 id="41-db별-오차-발생-사례">4.1 DB별 오차 발생 사례</h3>

<p>10만 건 테스트에서 각 데이터베이스별로 확인된 번역 쿼리 오차입니다. <code class="language-plaintext highlighter-rouge">PostgreSQL</code>을 제외한 모든 데이터베이스에서 오차가 발생했습니다. 해결 방법과 최적화 방안은 6장과 7장에 정리하였습니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">데이터베이스</th>
      <th style="text-align: left">조건</th>
      <th style="text-align: center">Oracle 기준값</th>
      <th style="text-align: center">번역 쿼리 결과</th>
      <th style="text-align: center">오차</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>BigQuery</strong></td>
      <td style="text-align: left">잔액 2,184,500원 / 금리 8.5% / 52일</td>
      <td style="text-align: center">76,551원</td>
      <td style="text-align: center">76,550원</td>
      <td style="text-align: center">-1원</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Spanner</strong></td>
      <td style="text-align: left">잔액 2,193,667원 / 금리 17.1% / 67일</td>
      <td style="text-align: center">73,217원</td>
      <td style="text-align: center">73,218원</td>
      <td style="text-align: center">+1원</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PostgreSQL</strong></td>
      <td style="text-align: left">10만 건 전건</td>
      <td style="text-align: center">—</td>
      <td style="text-align: center">오차 없음</td>
      <td style="text-align: center">0원</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>MySQL</strong></td>
      <td style="text-align: left">잔액 3,550,720원 / 금리 7.5% / 55일</td>
      <td style="text-align: center">139,992원</td>
      <td style="text-align: center">139,991원</td>
      <td style="text-align: center">-1원</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Trino</strong></td>
      <td style="text-align: left">10만 건 중 18,232건 오차 (81.77%)</td>
      <td style="text-align: center">—</td>
      <td style="text-align: center">대규모 오차</td>
      <td style="text-align: center">—</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Presto</strong></td>
      <td style="text-align: left">10만 건 중 95,416건 오차 (95.42%)</td>
      <td style="text-align: center">—</td>
      <td style="text-align: center">대규모 오차</td>
      <td style="text-align: center">—</td>
    </tr>
  </tbody>
</table>

<p>Spanner는 <code class="language-plaintext highlighter-rouge">ROUND_HALF_UP</code> 정책 특성상 +1원 상향 오차도 발생합니다. Presto와 Trino는 나눗셈 스케일 규칙이 엄격해 거의 대부분의 케이스에서 오차가 발생합니다.</p>

<h3 id="42-오차가-집중되는-케이스만-골라-테스트">4.2 오차가 집중되는 케이스만 골라 테스트</h3>

<p>BigQuery <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>의 scale=38 반올림을 Python으로 에뮬레이션하여, 참값이 정수 경계에 정확히 걸리는 케이스만 추출해 5,003건을 테스트한 결과:</p>

<ul>
  <li><strong>번역 쿼리 오차율</strong>: <strong>47.43%</strong> (5,003건 중 2,373건에서 1원 하향 오차)</li>
  <li><strong>최적화 쿼리 오차율</strong>: <strong>0.00%</strong> (0건 오차, 전건 정합)</li>
</ul>

<p>중간 나눗셈의 반올림 오차가 누적되어 최종값이 정수 경계 아래로 내려갈 때 TRUNC가 1원 하향 오차를 만들어냅니다.</p>

<h2 id="5-원인-데이터베이스별-수치-연산-아키텍처-차이">5. 원인: 데이터베이스별 수치 연산 아키텍처 차이</h2>

<h3 id="51-플랫폼별-정밀도-처리-방식-비교">5.1 플랫폼별 정밀도 처리 방식 비교</h3>

<p>대출 이자 수식의 핵심은 <code class="language-plaintext highlighter-rouge">30 / 365</code> 나눗셈입니다. 이 값은 무한 순환소수입니다:</p>

\[\frac{30}{365} = 0.08219178082191780821\ldots \quad \text{(무한 순환소수)}\]

<p>각 데이터베이스는 이 무한소수를 처리하는 방식이 다릅니다.</p>

<h4 id="oracle-number-가변-길이-십진-연산">Oracle NUMBER: 가변 길이 십진 연산</h4>

<p>Oracle <code class="language-plaintext highlighter-rouge">NUMBER</code> 타입은 1바이트 지수부와 최대 20바이트 가수부로 구성된 가변 길이 Base-100 인코딩을 사용하며, 최대 38자리 십진 정밀도를 지원합니다. OCI 헤더에 정의된 <code class="language-plaintext highlighter-rouge">OCINumber</code> 구조체는 22바이트입니다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/* Oracle OCI Number Type (oci.h) */</span>
<span class="k">struct</span> <span class="n">OCINumber</span> <span class="p">{</span>
    <span class="n">ub1</span> <span class="n">OCINumberData</span><span class="p">[</span><span class="mi">22</span><span class="p">];</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Oracle은 연산 단계마다 결과를 고정 스케일로 강제 정규화하지 않습니다. 중간 나눗셈에서 소수 자릿수가 조기에 잘리지 않기 때문에, 번역 쿼리로도 오차가 발생하지 않습니다. 10만 건 테스트에서 단 1건의 오차도 없었습니다.</p>
<ul>
  <li><a href="https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html">Oracle Database Concepts: Numeric Data Types</a></li>
</ul>

<h4 id="bigquery-bignumeric-scale38-단계별-정규화">BigQuery BIGNUMERIC: scale=38 단계별 정규화</h4>

<p><code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>은 소수점 이하 38자리 고정 소수점입니다. 연산 단계마다 결과를 38자리로 강제 정규화합니다:</p>

\[\frac{30}{365} \xrightarrow{\text{scale=38 반올림}} 0.08219178082191780821917808219178082191\]

<p>참값이 정수 경계에 걸릴 때, 중간 단계의 반올림 오차가 누적되어 최종값이 진짜 정수보다 미세하게 낮아지면 TRUNC에서 1원 하향 오차가 됩니다.</p>
<ul>
  <li><a href="https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#numeric_types">BigQuery NUMERIC/BIGNUMERIC 타입</a></li>
</ul>

<h4 id="spanner-numeric-scale9-round_half_up">Spanner NUMERIC: scale=9, ROUND_HALF_UP</h4>

<p><code class="language-plaintext highlighter-rouge">NUMERIC</code>은 소수점 이하 9자리로 고정되며 반올림(<code class="language-plaintext highlighter-rouge">ROUND_HALF_UP</code>)을 적용합니다. 절사 방향이 일정하지 않아 오차가 +1원 또는 -1원 양방향으로 발생합니다.</p>
<ul>
  <li><a href="https://cloud.google.com/spanner/docs/reference/standard-sql/data-types#numeric_type">Spanner NUMERIC 타입</a></li>
</ul>

<h4 id="postgresql-numeric-임의-정밀도-oracle과-동일">PostgreSQL NUMERIC: 임의 정밀도 (Oracle과 동일)</h4>

<p>연산 중 자릿수가 동적으로 확장되는 임의 정밀도(Arbitrary Precision)를 지원합니다. Oracle과 동일하게 별도 보정 없이도 오차가 발생하지 않습니다.</p>
<ul>
  <li><a href="https://www.postgresql.org/docs/current/datatype-numeric.html">PostgreSQL Numeric Types</a></li>
</ul>

<h4 id="mysql-decimal-div_precision_increment-한계">MySQL DECIMAL: div_precision_increment 한계</h4>

<p>나눗셈 정밀도가 시스템 변수 <code class="language-plaintext highlighter-rouge">div_precision_increment</code>(기본값: 4자리 추가)에 의해 제한됩니다. 이 값은 MySQL 설정에 따라 달라질 수 있으며, 이 테스트는 기본값(4) 환경에서 수행했습니다.</p>
<ul>
  <li><a href="https://dev.mysql.com/doc/refman/8.0/en/fixed-point-types.html">MySQL Fixed-Point Types</a></li>
</ul>

<h4 id="presto--trino-decimal-maxs1-s2-엄격-적용">Presto / Trino DECIMAL: max(s1, s2) 엄격 적용</h4>

<p>ANSI SQL 나눗셈 스케일 규칙(<code class="language-plaintext highlighter-rouge">max(s1, s2)</code>)을 엄격하게 적용하여 중간 나눗셈 결과의 소수 자릿수가 조기에 심각하게 축소됩니다. Presto는 번역 쿼리 기준 4.58% 일치율에 그칩니다.</p>
<ul>
  <li><a href="https://trino.io/docs/current/language/types.html#decimal">Trino DECIMAL</a></li>
  <li><a href="https://prestodb.io/docs/current/language/types.html#decimal">Presto DECIMAL</a></li>
</ul>

<hr />

<p>플랫폼별 수치 처리 아키텍처를 정리하면 다음과 같습니다.</p>

<pre><code class="language-mermaid">graph TD
    Input["수치 리터럴 입력 (예: 30/365)"] --&gt; Oracle["Oracle NUMBER"]
    Input --&gt; Binary["FLOAT / double (IEEE 754)"]
    Input --&gt; Decimal["DECIMAL / NUMERIC"]

    Oracle --&gt; OraclePhys["가변 길이 100진법 인코딩 (최대 22바이트)"]
    Binary --&gt; BinaryPhys["고정 길이 2진법 인코딩 (8바이트)"]
    Decimal --&gt; DecimalPhys["정수/소수부 분리 고정 인코딩 (선언 자릿수)"]

    OraclePhys --&gt; Software["연산 중 고정 스케일 강제 정규화 없음"]
    BinaryPhys --&gt; FPU["CPU FPU 하드웨어 병렬 연산"]
    DecimalPhys --&gt; Premature["사칙연산 단계별 강제 스케일 정제"]

    Software --&gt; OracleResult["중간 자릿수 유실 없는 정합성 보존"]
    FPU --&gt; BinaryResult["2진 무한소수화 — 가수부 누적 오차"]
    Premature --&gt; DecimalResult["나눗셈 중간 연산 시 소수 하위 잘림"]
</code></pre>

<h3 id="52-goldberg-이론이-설명하는-것-설명-못하는-것">5.2 Goldberg 이론이 설명하는 것, 설명 못하는 것</h3>

<p>지금까지 각 데이터베이스가 소수를 서로 다른 방식으로 처리한다는 것을 봤습니다. 이 오차를 짚어볼 때 컴퓨터 과학에서 가장 많이 인용되는 논문 하나를 빼놓을 수 없습니다.</p>

<blockquote>
  <p>Goldberg, D. (1991). <em>What Every Computer Scientist Should Know About Floating-Point Arithmetic</em>. ACM Computing Surveys, 23(1), 5–48.<br />
<a href="https://dl.acm.org/doi/10.1145/103162.103163">DOI: 10.1145/103162.103163</a><br />
<a href="https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html">전문 보기</a></p>
</blockquote>

<p>Goldberg의 1991년 논문은 IEEE 754 이진 부동소수점 연산의 수학적 토대를 제시합니다. 반올림 오차(Rounding Error), 가드 디지트(Guard Digits), 유효자리 소실(Catastrophic Cancellation)을 단일 코어 직렬 FPU 환경에서 정밀하게 증명했습니다.</p>

<p><code class="language-plaintext highlighter-rouge">FLOAT64</code> 타입으로 연산할 때 발생하는 오차는 이 이론으로 직접 설명됩니다. <code class="language-plaintext highlighter-rouge">0.5</code>나 <code class="language-plaintext highlighter-rouge">14.6</code> 같은 십진 소수는 이진수로 정확하게 표현할 수 없어 미세한 근사 오차가 생기고, 연산이 누적될수록 커집니다. 5.3절에서 다루는 <strong>UNION ALL 정밀도 손실</strong>가 대표적입니다 — 고정소수점 컬럼이 <code class="language-plaintext highlighter-rouge">FLOAT64</code>로 강제 변환되는 순간, 바로 이 이진 근사 오차가 개입합니다.</p>

<p>그런데 우리가 마주친 주요 오차는 <code class="language-plaintext highlighter-rouge">FLOAT64</code>가 아니라 <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>이나 <code class="language-plaintext highlighter-rouge">NUMERIC</code> 같은 <strong>고정소수점 타입</strong>에서 발생했습니다. 이 타입들은 이진 부동소수점을 쓰지 않으므로 Goldberg 이론의 적용 범위 밖입니다. 우리가 테스트한 환경은 Goldberg가 상정한 환경과 구조적으로 다릅니다:</p>

<ol>
  <li><strong>고정소수점 라이브러리</strong>: BigQuery <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>, Spanner <code class="language-plaintext highlighter-rouge">NUMERIC</code> 등은 이진 부동소수점 대신 소프트웨어 에뮬레이션 십진 고정소수점 라이브러리를 사용합니다.</li>
  <li><strong>조기 정규화(Premature Normalization)</strong>: 분산 환경의 데이터 일관성을 위해 이항 연산 단계마다 결과를 고정 스케일(BigQuery scale=38, Spanner scale=9)로 강제 조정합니다.</li>
  <li><strong>MPP 비결정론적 셔플</strong>: 부동소수점 덧셈은 교환법칙이 성립하나 $(A + B) + C \neq A + (B + C)$ 결합법칙이 성립하지 않습니다. 분산 집계 순서가 매 실행마다 달라지면 결과도 달라집니다.</li>
  <li><strong>CBO 대수적 변환</strong>: 비용 기반 옵티마이저가 $A \times B + A \times C$를 $A \times (B + C)$로 내부 치환할 때, 유한 정밀도 환경에서는 수치 경로가 달라집니다.</li>
</ol>

<p>Goldberg 이론은 FLOAT64 오차를 설명하는 데 유효합니다. 다만 고정소수점 타입의 정규화 오차는 현대 분산 데이터베이스 환경에 맞는 별도의 관점이 필요합니다.</p>

<h3 id="53-union-all에서-정밀도가-희생되는-이유">5.3 UNION ALL에서 정밀도가 희생되는 이유</h3>

<p>고정소수점과 부동소수점을 <code class="language-plaintext highlighter-rouge">UNION ALL</code>로 합치면 부동소수점이 공통 상위 타입이 됩니다. BigQuery를 예시로 들면, <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>과 <code class="language-plaintext highlighter-rouge">FLOAT64</code>를 병합할 때 표현 범위가 넓은 <code class="language-plaintext highlighter-rouge">FLOAT64</code>를 슈퍼타입으로 결정합니다.</p>

<p>SQL 표준은 <code class="language-plaintext highlighter-rouge">UNION ALL</code> 결과 타입을 “공통 상위 타입(Supertype)”으로 결정하도록 규정합니다. 대부분의 데이터베이스는 표현 정밀도보다 표현 범위를 우선하기 때문에, 고정소수점과 부동소수점이 섞이면 부동소수점이 슈퍼타입이 됩니다. Oracle의 <code class="language-plaintext highlighter-rouge">FLOAT</code> 타입이 유일한 예외입니다 — <code class="language-plaintext highlighter-rouge">FLOAT</code>은 NUMBER의 서브타입이라 정밀도 손실이 발생하지 않습니다. 단, Oracle에도 진짜 IEEE 754 타입인 <code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code>이 있으며, 이 경우 다른 DB와 동일하게 정밀도가 손실됩니다.</p>

<p>Oracle이 이런 동작을 보이는 근본 이유 중 하나는 <strong>숫자 리터럴의 기본 타입</strong>입니다. Oracle은 정수든 소수든 모든 숫자 리터럴을 <code class="language-plaintext highlighter-rouge">NUMBER</code>로 처리합니다. 반면 다른 DB들은 소수 리터럴을 이진 부동소수점으로 파싱합니다.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">DB</th>
      <th style="text-align: center">정수 리터럴 <code class="language-plaintext highlighter-rouge">1</code></th>
      <th style="text-align: center">소수 리터럴 <code class="language-plaintext highlighter-rouge">1.0</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Oracle</strong></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PostgreSQL</strong></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">INTEGER</code></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">NUMERIC</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>BigQuery</strong></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">INT64</code></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">FLOAT64</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>MySQL</strong></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">BIGINT</code></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Presto / Trino</strong></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">INTEGER</code></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
    </tr>
  </tbody>
</table>

<p>Oracle에서는 어떤 숫자 리터럴을 써도 NUMBER 계열이라 UNION ALL에서 정밀도가 유지됩니다. BigQuery에서 <code class="language-plaintext highlighter-rouge">1.0</code>을 명시적 캐스팅 없이 쓰면 FLOAT64가 개입해 정밀도가 손실되는 것과 대조됩니다.</p>

<blockquote>
  <p><strong>정밀도(Precision)와 범위(Range)</strong>: 두 개념은 다릅니다.</p>
  <ul>
    <li><strong>정밀도</strong>: 소수점 몇 자리까지 정확하게 표현하는가. 고정소수점(<code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>, <code class="language-plaintext highlighter-rouge">DECIMAL</code>, <code class="language-plaintext highlighter-rouge">NUMERIC</code>)이 훨씬 높습니다.</li>
    <li><strong>범위</strong>: 얼마나 큰 숫자까지 표현할 수 있는가. 부동소수점(<code class="language-plaintext highlighter-rouge">FLOAT64</code>, <code class="language-plaintext highlighter-rouge">DOUBLE</code>, <code class="language-plaintext highlighter-rouge">DOUBLE PRECISION</code>)이 훨씬 넓습니다.</li>
  </ul>

  <table>
    <thead>
      <tr>
        <th style="text-align: left">데이터베이스</th>
        <th style="text-align: center">고정소수점 타입 / 최대 범위</th>
        <th style="text-align: left">비교 타입 (부동소수점)</th>
        <th style="text-align: center">비교 타입 최대 범위</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td style="text-align: left"><strong>Oracle</strong></td>
        <td style="text-align: center">NUMBER / ≈ 10¹²⁶</td>
        <td style="text-align: left">FLOAT (NUMBER 서브타입, 십진수 저장)</td>
        <td style="text-align: center">≈ 10¹²⁶ → 정밀도 유지</td>
      </tr>
      <tr>
        <td style="text-align: left"><strong>Oracle</strong></td>
        <td style="text-align: center">NUMBER / ≈ 10¹²⁶</td>
        <td style="text-align: left">BINARY_DOUBLE (IEEE 754, 이진수 저장)</td>
        <td style="text-align: center">≈ 10³⁰⁸ → 정밀도 손실</td>
      </tr>
      <tr>
        <td style="text-align: left">BigQuery</td>
        <td style="text-align: center">BIGNUMERIC / ≈ 10³⁸</td>
        <td style="text-align: left">FLOAT64 (IEEE 754, 이진수 저장)</td>
        <td style="text-align: center">≈ 10³⁰⁸ → 정밀도 손실</td>
      </tr>
      <tr>
        <td style="text-align: left">PostgreSQL</td>
        <td style="text-align: center">NUMERIC(38,22) / ≈ 10¹⁶</td>
        <td style="text-align: left">DOUBLE PRECISION (IEEE 754, 이진수 저장)</td>
        <td style="text-align: center">≈ 10³⁰⁸ → 정밀도 손실</td>
      </tr>
      <tr>
        <td style="text-align: left">MySQL</td>
        <td style="text-align: center">DECIMAL(38,22) / ≈ 10¹⁶</td>
        <td style="text-align: left">DOUBLE (IEEE 754, 이진수 저장)</td>
        <td style="text-align: center">≈ 10³⁰⁸ → 정밀도 손실</td>
      </tr>
      <tr>
        <td style="text-align: left">Presto / Trino</td>
        <td style="text-align: center">DECIMAL(38,22) / ≈ 10¹⁶</td>
        <td style="text-align: left">DOUBLE (IEEE 754, 이진수 저장)</td>
        <td style="text-align: center">≈ 10³⁰⁸ → 정밀도 손실</td>
      </tr>
    </tbody>
  </table>

  <p>Oracle의 <code class="language-plaintext highlighter-rouge">FLOAT</code>는 다른 DB의 <code class="language-plaintext highlighter-rouge">DOUBLE</code>/<code class="language-plaintext highlighter-rouge">FLOAT64</code>와 다릅니다. Oracle 공식 문서는 이를 명확히 정의합니다:</p>

  <blockquote>
    <p><em>“The <code class="language-plaintext highlighter-rouge">FLOAT</code> data type is a subtype of the <code class="language-plaintext highlighter-rouge">NUMBER</code> data type.”</em><br />
<em>“A <code class="language-plaintext highlighter-rouge">FLOAT</code> value is represented internally as <code class="language-plaintext highlighter-rouge">NUMBER</code>.”</em><br />
— <a href="https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/Data-Types.html">Oracle Database SQL Language Reference</a></p>
  </blockquote>

  <p>즉, Oracle <code class="language-plaintext highlighter-rouge">FLOAT</code>는 IEEE 754 이진 부동소수점이 아니라 <strong>NUMBER의 서브타입</strong>으로, 내부적으로 NUMBER와 동일한 십진수 방식으로 저장됩니다. 범위도 NUMBER와 같아 사실상 같은 계열이기 때문에 UNION ALL 시 NUMBER로 유지됩니다.</p>

  <p>Oracle에서 진짜 IEEE 754 이진 부동소수점이 필요하다면 <code class="language-plaintext highlighter-rouge">BINARY_FLOAT</code>(32비트)나 <code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code>(64비트)을 사용합니다. 이 타입들은 다른 DB의 DOUBLE과 동일한 이진 저장 방식입니다.</p>
</blockquote>

<table>
  <thead>
    <tr>
      <th style="text-align: left">데이터베이스</th>
      <th style="text-align: left">고정소수점 타입</th>
      <th style="text-align: left">부동소수점 타입</th>
      <th style="text-align: left">UNION ALL 결과</th>
      <th style="text-align: left">결정 기준</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Oracle</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FLOAT</code> (NUMBER 서브타입)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
      <td style="text-align: left">FLOAT가 NUMBER와 같은 계열 → 정밀도 유지</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Oracle</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code> (IEEE 754)</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code></td>
      <td style="text-align: left">표현 범위 우선 → 정밀도 손실</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>BigQuery</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BIGNUMERIC</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FLOAT64</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FLOAT64</code></td>
      <td style="text-align: left">표현 범위 우선 → 정밀도 손실</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PostgreSQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMERIC</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE PRECISION</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE PRECISION</code></td>
      <td style="text-align: left">표현 범위 우선 → 정밀도 손실</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>MySQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
      <td style="text-align: left">표현 범위 우선 → 정밀도 손실</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Presto</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
      <td style="text-align: left">표현 범위 우선 → 정밀도 손실</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Trino</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DOUBLE</code></td>
      <td style="text-align: left">표현 범위 우선 → 정밀도 손실</td>
    </tr>
  </tbody>
</table>

<p>이 슈퍼타입 결정 규칙은 시스템 설정으로 변경할 수 없습니다. SQL 컴파일러에 하드코딩된 동작이기 때문에, 유일한 방어 수단은 명시적 캐스팅뿐입니다.</p>

<p><strong>테스트 데이터</strong>: <code class="language-plaintext highlighter-rouge">123456789012345.6789012345678901234567</code> (소수부 22자리)와 <code class="language-plaintext highlighter-rouge">1.0</code>을 <code class="language-plaintext highlighter-rouge">UNION ALL</code>로 병합.</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">데이터베이스</th>
      <th style="text-align: left">판별 함수</th>
      <th style="text-align: left">최종 타입</th>
      <th style="text-align: left">출력값</th>
      <th style="text-align: left">정합성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Oracle 21c</strong></td>
      <td style="text-align: left">내장 타입 검사</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER(38,22)</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">123456789012345.6789012345678901234567</code></td>
      <td style="text-align: left"><strong>100% 보존</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>BigQuery</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">bq show</code> 스키마</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">FLOAT64</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">1.2345678901234567E14</code></td>
      <td style="text-align: left"><strong>20자리 유실</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PostgreSQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">pg_typeof()</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">double precision</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">123456789012345.67</code></td>
      <td style="text-align: left"><strong>20자리 유실</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>MySQL 8.0</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DESCRIBE</code> 임시 테이블</td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">double</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">123456789012345.67</code></td>
      <td style="text-align: left"><strong>20자리 유실</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Presto</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">typeof()</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">double</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">1.2345678901234567E14</code></td>
      <td style="text-align: left"><strong>20자리 유실</strong></td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Trino</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">typeof()</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">double</code></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">1.2345678901234567E14</code></td>
      <td style="text-align: left"><strong>20자리 유실</strong></td>
    </tr>
  </tbody>
</table>

<p>이 현상이 특히 위험한 이유는 <strong>SQL 컴파일 타임에 조용히 발생</strong>한다는 점입니다. 런타임 오류가 아니기 때문에 결과값이 그냥 틀린 값으로 반환됩니다.</p>

<p>실무에서 이 문제가 자주 발생하는 세 가지 상황:</p>

<ol>
  <li><strong>UNION ALL로 브랜치 병합</strong>: 한 브랜치에 소수 리터럴(<code class="language-plaintext highlighter-rouge">1.0</code>, <code class="language-plaintext highlighter-rouge">0.5</code> 등)이 포함되면 그 브랜치 전체가 <code class="language-plaintext highlighter-rouge">FLOAT64</code>로 추론됩니다. <code class="language-plaintext highlighter-rouge">UNION ALL</code>로 합쳐지는 순간 나머지 브랜치의 <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code> 컬럼도 <code class="language-plaintext highlighter-rouge">FLOAT64</code>로 강제 정밀도가 손실됩니다.</li>
  <li><strong>CTE 타입 추론</strong>: <code class="language-plaintext highlighter-rouge">WITH temp AS (SELECT 대출잔액 / 365.0 + 0.5 AS ratio ...)</code>  소수 리터럴이 섞인 CTE는 결과 타입이 <code class="language-plaintext highlighter-rouge">FLOAT64</code>로 추론되어, 이후 이 CTE를 참조하는 모든 연산이 부동소수점 영역에서 진행됩니다.</li>
  <li><strong>혼합 타입 산술</strong>: <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code> 컬럼에 명시적 <code class="language-plaintext highlighter-rouge">CAST</code> 없이 소수 리터럴을 더하거나 곱하면 BigQuery는 컴파일 오류(<code class="language-plaintext highlighter-rouge">No matching signature for operator</code>)를 내거나, 조용히 <code class="language-plaintext highlighter-rouge">FLOAT64</code>로 변환합니다.</li>
</ol>

<p>모든 경우의 해결책은 같습니다. 수식에 등장하는 모든 수치 요소를 동일한 고정소수점 타입으로 명시적으로 캐스팅합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- BigQuery: 모든 브랜치를 BIGNUMERIC으로 통일</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span><span class="p">;</span>
<span class="c1">-- 결과: 123456789012345.6789012345678901234567 (100% 보존)</span>
</code></pre></div></div>

<h4 id="db별-실증-쿼리">DB별 실증 쿼리</h4>

<p>각 데이터베이스에서 정밀도 손실을 직접 확인하고 해결하는 방법입니다.</p>

<p><strong>BigQuery : 쿼리 실행 결과로 정밀도 손실 확인</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 정밀도 손실: BIGNUMERIC + FLOAT64 → FLOAT64</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="n">FLOAT64</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+------------------------+
|          val           |
+------------------------+
| 1.2345678901234567e+14 |   ← 과학적 표기법, 20자리 유실
|                    1.0 |
+------------------------+
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 해결: 모든 브랜치를 BIGNUMERIC으로 통일</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+----------------------------------------+
|                  val                   |
+----------------------------------------+
| 123456789012345.6789012345678901234567 |   ← 100% 보존
|                                    1.0 |
+----------------------------------------+
</code></pre></div></div>

<p><strong>PostgreSQL : <code class="language-plaintext highlighter-rouge">pg_typeof()</code>로 타입 확인</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 정밀도 손실: NUMERIC + DOUBLE PRECISION → DOUBLE PRECISION</span>
<span class="k">SELECT</span> <span class="n">pg_typeof</span><span class="p">(</span><span class="n">val</span><span class="p">),</span> <span class="n">val</span>
<span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">DOUBLE</span> <span class="nb">PRECISION</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span>
<span class="p">)</span> <span class="n">q</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    pg_typeof     |        val
------------------+--------------------
 double precision | 123456789012345.67   ← 20자리 유실
 double precision |                  1
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 해결: 모든 브랜치를 NUMERIC으로 통일</span>
<span class="k">SELECT</span> <span class="n">pg_typeof</span><span class="p">(</span><span class="n">val</span><span class="p">),</span> <span class="n">val</span>
<span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
<span class="p">)</span> <span class="n">q</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> pg_typeof |                  val
-----------+----------------------------------------
 numeric   | 123456789012345.6789012345678901234567  ← 100% 보존
 numeric   |           1.00000000000000000000
</code></pre></div></div>

<p><strong>MySQL : <code class="language-plaintext highlighter-rouge">DESCRIBE</code>로 임시 테이블 스키마 확인</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 정밀도 손실: DECIMAL + DOUBLE → DOUBLE</span>
<span class="k">CREATE</span> <span class="k">TEMPORARY</span> <span class="k">TABLE</span> <span class="n">temp_union</span> <span class="k">AS</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">DOUBLE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span><span class="p">;</span>

<span class="k">DESCRIBE</span> <span class="n">temp_union</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-------+--------+------+
| Field | Type   | Null |
+-------+--------+------+
| val   | double | NO   |   ← DOUBLE로 변환 (정밀도 손실)
+-------+--------+------+
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 해결: 모든 브랜치를 DECIMAL(38,22)로 통일</span>
<span class="k">CREATE</span> <span class="k">TEMPORARY</span> <span class="k">TABLE</span> <span class="n">temp_union_safe</span> <span class="k">AS</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span><span class="p">;</span>

<span class="k">DESCRIBE</span> <span class="n">temp_union_safe</span><span class="p">;</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-------+----------------+------+
| Field | Type           | Null |
+-------+----------------+------+
| val   | decimal(38,22) | NO   |   ← 고정소수점 유지
+-------+----------------+------+
</code></pre></div></div>

<p><strong>Presto / Trino : <code class="language-plaintext highlighter-rouge">typeof()</code>로 결과 타입 확인</strong></p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 정밀도 손실: DECIMAL + DOUBLE → DOUBLE</span>
<span class="k">SELECT</span> <span class="n">typeof</span><span class="p">(</span><span class="n">val</span><span class="p">),</span> <span class="n">val</span>
<span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">DOUBLE</span><span class="p">)</span> <span class="k">AS</span> <span class="n">val</span>
<span class="p">);</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  _col0  |         _col1
---------+------------------------
 double  | 1.2345678901234567E14   ← 과학적 표기법, 20자리 유실
 double  | 1.0
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 해결: 모든 브랜치를 DECIMAL(38,22)로 통일</span>
<span class="k">SELECT</span> <span class="n">typeof</span><span class="p">(</span><span class="n">val</span><span class="p">),</span> <span class="n">val</span>
<span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span>
<span class="p">);</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>     _col0      |                  _col1
----------------+----------------------------------------
 decimal(38,22) | 123456789012345.6789012345678901234567  ← 100% 보존
 decimal(38,22) | 1.0000000000000000000000
</code></pre></div></div>

<p><strong>Oracle : <code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code>(IEEE 754)과 병합 시 정밀도 손실</strong></p>

<p>Oracle <code class="language-plaintext highlighter-rouge">FLOAT</code>은 NUMBER 서브타입이라 정밀도 손실이 없지만, 진짜 IEEE 754 타입인 <code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code>과 병합하면 동일하게 정밀도가 손실됩니다. 아래 두 쿼리를 비교하면 차이가 명확합니다.</p>

<blockquote>
  <p>아래 쿼리는 <code class="language-plaintext highlighter-rouge">TO_CHAR(val)</code>로 감쌌습니다. sqlplus 클라이언트가 BINARY_DOUBLE 값을 바로 출력하지 못하는 경우가 있어 문자열로 변환한 것이며, 실제 업무 쿼리에는 필요 없습니다.</p>
</blockquote>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- NUMBER + FLOAT(NUMBER 서브타입) → NUMBER 유지</span>
<span class="k">SELECT</span> <span class="n">TO_CHAR</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="s1">'123456789012345.6789012345678901234567'</span> <span class="k">AS</span> <span class="n">NUMBER</span><span class="p">(</span><span class="mi">38</span><span class="p">,</span><span class="mi">22</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span> <span class="k">FROM</span> <span class="n">dual</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="nb">FLOAT</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">dual</span>
<span class="p">);</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>123456789012345.6789012345678901234567  ← 소수 22자리 완전 보존
1
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- NUMBER + BINARY_DOUBLE(IEEE 754) → BINARY_DOUBLE로 변환 (정밀도 손실)</span>
<span class="c1">-- (BINARY_DOUBLE 표시 제약으로 짧은 값 사용)</span>
<span class="k">SELECT</span> <span class="n">TO_CHAR</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">12345</span><span class="p">.</span><span class="mi">6789</span> <span class="k">AS</span> <span class="n">NUMBER</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">4</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span> <span class="k">FROM</span> <span class="n">dual</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="n">BINARY_DOUBLE</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">dual</span>
<span class="p">);</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1.2345678900000001E+004   ← 과학적 표기법, 이진 근사 오차 발생
1.0E+000
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">1.2345678900000001E+004</code>는 실제 값 <code class="language-plaintext highlighter-rouge">12345.6789</code>가 IEEE 754 이진수로 저장되면서 <code class="language-plaintext highlighter-rouge">12345.6789000000001...</code>로 변환된 결과입니다. <code class="language-plaintext highlighter-rouge">12345.6789</code>는 이진수로 정확하게 표현할 수 없어 끝 자리에 미세한 오차가 붙습니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 해결: BINARY_DOUBLE 대신 NUMBER 계열로 통일</span>
<span class="k">SELECT</span> <span class="n">TO_CHAR</span><span class="p">(</span><span class="n">val</span><span class="p">)</span> <span class="k">FROM</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">12345</span><span class="p">.</span><span class="mi">6789</span> <span class="k">AS</span> <span class="n">NUMBER</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">4</span><span class="p">))</span> <span class="k">AS</span> <span class="n">val</span> <span class="k">FROM</span> <span class="n">dual</span>
  <span class="k">UNION</span> <span class="k">ALL</span>
  <span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">1</span><span class="p">.</span><span class="mi">0</span> <span class="k">AS</span> <span class="n">NUMBER</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">4</span><span class="p">))</span> <span class="k">FROM</span> <span class="n">dual</span>
<span class="p">);</span>
</code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12345.6789  ← 정확히 보존
1
</code></pre></div></div>

<p>Oracle이 슈퍼타입 결정에서 예외로 불리는 이유는 <code class="language-plaintext highlighter-rouge">FLOAT</code>이 NUMBER와 같은 계열이기 때문이며, <code class="language-plaintext highlighter-rouge">BINARY_DOUBLE</code>(IEEE 754)을 쓰면 Oracle도 동일하게 정밀도가 손실됩니다.</p>

<h2 id="6-해결-두-가지-핵심-기법">6. 해결: 두 가지 핵심 기법</h2>

<p>문제는 두 가지입니다. 해결책도 두 가지입니다.</p>

<table>
  <thead>
    <tr>
      <th>문제</th>
      <th>원인</th>
      <th>해결 기법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>연산 중 소수점 유실</td>
      <td>나눗셈 중간 단계에서 스케일 정규화</td>
      <td>후순위 나눗셈</td>
    </tr>
    <tr>
      <td>묵시적 정밀도 손실</td>
      <td>혼합 타입 연산 시 고정소수점이 부동소수점으로 수렴</td>
      <td>명시적 형변환(CAST)</td>
    </tr>
  </tbody>
</table>

<p>두 문제는 독립적으로 발생합니다. 후순위 나눗셈만 적용해도 UNION ALL에서 타입이 정밀도가 손실되면 다시 오차가 생깁니다. 두 기법을 함께 써야 완전히 방어됩니다.</p>

<h3 id="61-후순위-나눗셈-연산-중-소수점-유실-방지">6.1 후순위 나눗셈: 연산 중 소수점 유실 방지</h3>

<p>아이디어는 단순합니다. 수식 중간의 나눗셈을 모두 없애고, 딱 한 번만 <code class="language-plaintext highlighter-rouge">TRUNC</code> 직전에 나누는 겁니다. 수학적으로는 기존 수식과 같지만, 중간에 소수점이 잘릴 일이 없습니다.</p>

<h4 id="수학적-동치-증명">수학적 동치 증명</h4>

<p>번역 쿼리 수식:</p>

\[\text{이자}_{\text{번역}} = \text{TRUNC}\!\left(\text{잔액} \times \frac{\text{금리}}{100} \times \left(\frac{D}{365} + \frac{30}{365}\right),\ 0\right)\]

<p>대수적 통분 : 나눗셈을 마지막으로 후순위 배치:</p>

\[= \text{TRUNC}\!\left(\text{잔액} \times \frac{\text{금리}}{100} \times \frac{D + 30}{365},\ 0\right)\]

\[= \text{TRUNC}\!\left(\frac{\text{잔액} \times \text{금리} \times (D + 30)}{36{,}500},\ 0\right)\]

\[\therefore\quad \text{이자}_{\text{opt}} = \text{TRUNC}\!\left(\frac{\text{잔액} \times \text{금리} \times (D + 30)}{36{,}500},\ 0\right) \quad \blacksquare\]

<h4 id="bigquery-적용-sql">BigQuery 적용 SQL</h4>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- optimized: 후순위 나눗셈 — 나눗셈 1회, TRUNC 직전 최종 단계에서만</span>
<span class="k">SELECT</span> <span class="err">대출관리번호</span>
     <span class="p">,</span> <span class="n">TRUNC</span><span class="p">(</span>
         <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="err">대출잔액</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span>
          <span class="o">*</span> <span class="k">CAST</span><span class="p">(</span><span class="err">약정금리</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span>
          <span class="o">*</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="n">DATE_DIFF</span><span class="p">(</span><span class="err">이자계산기준일</span><span class="p">,</span> <span class="err">대출실행일자</span><span class="p">,</span> <span class="k">DAY</span><span class="p">)</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span>
             <span class="o">+</span> <span class="n">BIGNUMERIC</span> <span class="s1">'30'</span><span class="p">))</span>
         <span class="o">/</span> <span class="n">BIGNUMERIC</span> <span class="s1">'36500'</span>
       <span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">AS</span> <span class="err">이자금액</span>
  <span class="k">FROM</span> <span class="nv">`DM.월별대출이자산출`</span>
 <span class="k">ORDER</span> <span class="k">BY</span> <span class="err">대출관리번호</span>
</code></pre></div></div>

<h3 id="62-명시적-형변환-타입-안전성-보장">6.2 명시적 형변환: 타입 안전성 보장</h3>

<p>올바른 수식 설계만으로는 충분하지 않습니다. SQL에서 리터럴 상수, 함수 반환값, UNION ALL 브랜치의 타입이 하나라도 <code class="language-plaintext highlighter-rouge">FLOAT64</code>나 <code class="language-plaintext highlighter-rouge">DOUBLE</code>이면 전체 연산 결과가 부동소수점으로 수렴합니다. 수식이 아무리 정교해도 타입이 정밀도가 손실되는 순간 오차가 다시 생깁니다.</p>

<h4 id="리터럴-상수-처리">리터럴 상수 처리</h4>

<p>BigQuery SQL에서 <code class="language-plaintext highlighter-rouge">30</code>이나 <code class="language-plaintext highlighter-rouge">36500</code> 같은 정수 리터럴은 <code class="language-plaintext highlighter-rouge">INT64</code>로, 소수 리터럴 <code class="language-plaintext highlighter-rouge">0.5</code>는 <code class="language-plaintext highlighter-rouge">FLOAT64</code>로 자동 추론됩니다. <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code> 컬럼과 다른 타입 리터럴을 섞어서 산술 연산하면 컴파일 타임에 이런 오류가 납니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>No matching signature for operator * for argument types: BIGNUMERIC, FLOAT64
</code></pre></div></div>

<p>오류를 마주한 개발자가 빠지기 쉬운 함정이 있습니다. 오류 메시지를 없애려고 컬럼을 반대 방향으로 낮추는 것입니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 잘못된 해결: BIGNUMERIC을 FLOAT64로 낮춰서 오류를 없앰</span>
<span class="k">CAST</span><span class="p">(</span><span class="err">대출잔액</span> <span class="k">AS</span> <span class="n">FLOAT64</span><span class="p">)</span> <span class="o">*</span> <span class="err">약정금리</span> <span class="o">*</span> <span class="p">(</span><span class="err">경과일수</span> <span class="o">+</span> <span class="mi">30</span><span class="p">.</span><span class="mi">0</span><span class="p">)</span> <span class="o">/</span> <span class="mi">36500</span><span class="p">.</span><span class="mi">0</span>
</code></pre></div></div>

<p>오류는 사라지지만 결과가 더 나빠집니다. <code class="language-plaintext highlighter-rouge">FLOAT64</code>는 IEEE 754 이진 부동소수점이라 십진 소수를 정확하게 표현하지 못합니다. <code class="language-plaintext highlighter-rouge">30.0</code>이나 <code class="language-plaintext highlighter-rouge">36500.0</code>처럼 단순해 보이는 값도 내부적으로 근사치로 저장됩니다. 이 상태에서 계산을 이어가면 오차가 연산마다 누적되어 최종 결과에서 원치 않는 차이가 발생합니다.</p>

<p>올바른 방향은 반대입니다. 리터럴을 <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code>으로 올려서 수식 전체를 고정소수점 영역에서 유지합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 올바른 해결: 리터럴을 BIGNUMERIC으로 올림</span>
<span class="k">CAST</span><span class="p">(</span><span class="err">대출잔액</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="o">*</span> <span class="k">CAST</span><span class="p">(</span><span class="err">약정금리</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span>
  <span class="o">*</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="err">경과일수</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="o">+</span> <span class="n">BIGNUMERIC</span> <span class="s1">'30'</span><span class="p">)</span>
  <span class="o">/</span> <span class="n">BIGNUMERIC</span> <span class="s1">'36500'</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">BIGNUMERIC '30'</code> 처럼 프리픽스 리터럴 표기를 쓰면 <code class="language-plaintext highlighter-rouge">CAST(30 AS BIGNUMERIC)</code>과 동일하게 동작하면서 더 간결합니다.</p>

<h4 id="union-all-4단계-방어">UNION ALL 4단계 방어</h4>

<p>Oracle에서 이식한 쿼리에 <code class="language-plaintext highlighter-rouge">UNION ALL</code>이 포함된 경우, 브랜치 하나의 타입 실수가 전체 결과셋을 망칩니다. 아래 4단계를 적용합니다.</p>

<p><strong>Tier 1 : 모든 브랜치 수치 컬럼 명시적 형변환</strong>:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="err">대출잔액</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="err">총액</span> <span class="k">FROM</span> <span class="err">대출</span><span class="n">_</span><span class="err">브랜치</span><span class="n">A</span>
<span class="k">UNION</span> <span class="k">ALL</span>
<span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="err">대출잔액</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="err">총액</span> <span class="k">FROM</span> <span class="err">대출</span><span class="n">_</span><span class="err">브랜치</span><span class="n">B</span>
</code></pre></div></div>

<p><strong>Tier 2 : 리터럴 상수 BIGNUMERIC 기입</strong>:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">BIGNUMERIC</span> <span class="s1">'30'</span> <span class="k">AS</span> <span class="err">기산가산일수</span>
</code></pre></div></div>

<p><strong>Tier 3 : 수식 피연산자 사전 업캐스팅</strong>:</p>
<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">CAST</span><span class="p">(</span><span class="n">DATE_DIFF</span><span class="p">(</span><span class="err">이자계산기준일</span><span class="p">,</span> <span class="err">대출실행일자</span><span class="p">,</span> <span class="k">DAY</span><span class="p">)</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span>
       <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">365</span> <span class="k">AS</span> <span class="n">BIGNUMERIC</span><span class="p">)</span> <span class="k">AS</span> <span class="err">일수비율</span>
</code></pre></div></div>

<p><strong>Tier 4 : 마이그레이션 린터</strong>: <code class="language-plaintext highlighter-rouge">UNION ALL</code> 구조가 있는 쿼리는 빌드 파이프라인에서 수치 컬럼 타입을 전수 테스트합니다. 배포 전에 타입 불일치를 잡아내는 게 운영 중에 오차를 발견하는 것보다 훨씬 낫습니다.</p>

<h2 id="7-6종-데이터베이스-교차-테스트-결과">7. 6종 데이터베이스 교차 테스트 결과</h2>

<h3 id="71-테스트-자동화-스크립트">7.1 테스트 자동화 스크립트</h3>

<p><strong>Oracle vs. BigQuery 대조 (<code class="language-plaintext highlighter-rouge">compare_results.py</code>)</strong>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">csv</span><span class="p">,</span> <span class="n">subprocess</span><span class="p">,</span> <span class="n">json</span><span class="p">,</span> <span class="n">os</span>

<span class="k">def</span> <span class="nf">run_bq_query</span><span class="p">(</span><span class="n">query</span><span class="p">):</span>
    <span class="n">temp_file</span> <span class="o">=</span> <span class="s">"temp_query.sql"</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">temp_file</span><span class="p">,</span> <span class="s">"w"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">f</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="n">cmd</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"bq query --use_legacy_sql=false --max_rows=150000 --format=json &lt; </span><span class="si">{</span><span class="n">temp_file</span><span class="si">}</span><span class="s">"</span>
    <span class="n">res</span> <span class="o">=</span> <span class="n">subprocess</span><span class="p">.</span><span class="n">run</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">shell</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">capture_output</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">os</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="n">temp_file</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">res</span><span class="p">.</span><span class="n">returncode</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"Query failed:"</span><span class="p">,</span> <span class="n">res</span><span class="p">.</span><span class="n">stderr</span><span class="p">)</span>
        <span class="k">return</span> <span class="p">[]</span>
    <span class="k">return</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">res</span><span class="p">.</span><span class="n">stdout</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">oracle_rows</span> <span class="o">=</span> <span class="p">{}</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"oracle_data.csv"</span><span class="p">,</span> <span class="s">"r"</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s">"utf-8"</span><span class="p">)</span> <span class="k">as</span> <span class="n">fin</span><span class="p">:</span>
        <span class="n">reader</span> <span class="o">=</span> <span class="n">csv</span><span class="p">.</span><span class="n">DictReader</span><span class="p">(</span><span class="n">fin</span><span class="p">)</span>
        <span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">reader</span><span class="p">:</span>
            <span class="n">oracle_rows</span><span class="p">[</span><span class="n">row</span><span class="p">[</span><span class="s">'대출관리번호'</span><span class="p">]]</span> <span class="o">=</span> <span class="p">{</span>
                <span class="s">'oracle_fee'</span><span class="p">:</span> <span class="nb">int</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s">'oracle_이자금액'</span><span class="p">])</span>
            <span class="p">}</span>

    <span class="n">query_naive</span> <span class="o">=</span> <span class="s">"""
    SELECT 대출관리번호,
           TRUNC(
             (CAST(대출잔액 AS BIGNUMERIC) * (CAST(약정금리 AS BIGNUMERIC) / CAST(100 AS BIGNUMERIC)))
             * ((CAST(DATE_DIFF(이자계산기준일, 대출실행일자, DAY) AS BIGNUMERIC) / CAST(365 AS BIGNUMERIC))
                + CAST(30 AS BIGNUMERIC) / CAST(365 AS BIGNUMERIC))
           , 0) AS 이자금액_naive
      FROM `DM.월별대출이자산출`
     ORDER BY 대출관리번호
    """</span>

    <span class="n">query_optimized</span> <span class="o">=</span> <span class="s">"""
    SELECT 대출관리번호,
           TRUNC(
             (CAST(대출잔액 AS BIGNUMERIC)
              * CAST(약정금리 AS BIGNUMERIC)
              * (CAST(DATE_DIFF(이자계산기준일, 대출실행일자, DAY) AS BIGNUMERIC) + BIGNUMERIC '30'))
             / BIGNUMERIC '36500'
           , 0) AS 이자금액_optimized
      FROM `DM.월별대출이자산출`
     ORDER BY 대출관리번호
    """</span>

    <span class="k">print</span><span class="p">(</span><span class="s">"Running naive query..."</span><span class="p">)</span>
    <span class="n">res_naive</span> <span class="o">=</span> <span class="p">{</span><span class="n">r</span><span class="p">[</span><span class="s">'대출관리번호'</span><span class="p">]:</span> <span class="nb">int</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">r</span><span class="p">[</span><span class="s">'이자금액_naive'</span><span class="p">]))</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">run_bq_query</span><span class="p">(</span><span class="n">query_naive</span><span class="p">)}</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Running optimized query..."</span><span class="p">)</span>
    <span class="n">res_opt</span> <span class="o">=</span> <span class="p">{</span><span class="n">r</span><span class="p">[</span><span class="s">'대출관리번호'</span><span class="p">]:</span> <span class="nb">int</span><span class="p">(</span><span class="nb">float</span><span class="p">(</span><span class="n">r</span><span class="p">[</span><span class="s">'이자금액_optimized'</span><span class="p">]))</span> <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">run_bq_query</span><span class="p">(</span><span class="n">query_optimized</span><span class="p">)}</span>

    <span class="n">diff_naive</span> <span class="o">=</span> <span class="n">diff_opt</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">lid</span><span class="p">,</span> <span class="n">data</span> <span class="ow">in</span> <span class="n">oracle_rows</span><span class="p">.</span><span class="n">items</span><span class="p">():</span>
        <span class="k">if</span> <span class="n">data</span><span class="p">[</span><span class="s">'oracle_fee'</span><span class="p">]</span> <span class="o">!=</span> <span class="n">res_naive</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">lid</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">):</span> <span class="n">diff_naive</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">data</span><span class="p">[</span><span class="s">'oracle_fee'</span><span class="p">]</span> <span class="o">!=</span> <span class="n">res_opt</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">lid</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">):</span>  <span class="n">diff_opt</span> <span class="o">+=</span> <span class="mi">1</span>

    <span class="n">n</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">oracle_rows</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">Total: </span><span class="si">{</span><span class="n">n</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"naive      불일치: </span><span class="si">{</span><span class="n">diff_naive</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> / </span><span class="si">{</span><span class="n">n</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">diff_naive</span><span class="o">/</span><span class="n">n</span><span class="o">*</span><span class="mi">100</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">%)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"optimized  불일치: </span><span class="si">{</span><span class="n">diff_opt</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s">  / </span><span class="si">{</span><span class="n">n</span><span class="si">:</span><span class="p">,</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">diff_opt</span><span class="o">/</span><span class="n">n</span><span class="o">*</span><span class="mi">100</span><span class="si">:</span><span class="p">.</span><span class="mi">4</span><span class="n">f</span><span class="si">}</span><span class="s">%)"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<blockquote>
  <p><strong>주의</strong>: <code class="language-plaintext highlighter-rouge">bq CLI</code> 기본 결과 출력 한계가 100건이므로 반드시 <code class="language-plaintext highlighter-rouge">--max_rows=150000</code>을 명시합니다.</p>
</blockquote>

<p><strong>정수 경계 시뮬레이션 (<code class="language-plaintext highlighter-rouge">verify.py</code>)</strong>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">decimal</span><span class="p">,</span> <span class="n">math</span>
<span class="kn">from</span> <span class="nn">decimal</span> <span class="kn">import</span> <span class="n">Decimal</span>

<span class="n">decimal</span><span class="p">.</span><span class="n">getcontext</span><span class="p">().</span><span class="n">prec</span> <span class="o">=</span> <span class="mi">100</span>

<span class="k">def</span> <span class="nf">bq_div</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">a</span> <span class="o">/</span> <span class="n">b</span><span class="p">).</span><span class="n">quantize</span><span class="p">(</span><span class="n">Decimal</span><span class="p">(</span><span class="s">'1e-38'</span><span class="p">),</span> <span class="n">rounding</span><span class="o">=</span><span class="n">decimal</span><span class="p">.</span><span class="n">ROUND_HALF_UP</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">run_naive</span><span class="p">(</span><span class="n">amount</span><span class="p">,</span> <span class="n">rate</span><span class="p">,</span> <span class="n">days</span><span class="p">):</span>
    <span class="n">s1</span> <span class="o">=</span> <span class="n">bq_div</span><span class="p">(</span><span class="n">rate</span><span class="p">,</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'100'</span><span class="p">))</span>
    <span class="n">s2</span> <span class="o">=</span> <span class="n">bq_div</span><span class="p">(</span><span class="n">days</span><span class="p">,</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'365'</span><span class="p">))</span>
    <span class="n">s3</span> <span class="o">=</span> <span class="n">s2</span> <span class="o">+</span> <span class="n">bq_div</span><span class="p">(</span><span class="n">Decimal</span><span class="p">(</span><span class="s">'30'</span><span class="p">),</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'365'</span><span class="p">))</span>
    <span class="k">return</span> <span class="nb">int</span><span class="p">((</span><span class="n">amount</span> <span class="o">*</span> <span class="n">s1</span> <span class="o">*</span> <span class="n">s3</span><span class="p">).</span><span class="n">quantize</span><span class="p">(</span><span class="n">Decimal</span><span class="p">(</span><span class="s">'1'</span><span class="p">),</span> <span class="n">rounding</span><span class="o">=</span><span class="n">decimal</span><span class="p">.</span><span class="n">ROUND_DOWN</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">run_optimized</span><span class="p">(</span><span class="n">amount</span><span class="p">,</span> <span class="n">rate</span><span class="p">,</span> <span class="n">days</span><span class="p">):</span>
    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">bq_div</span><span class="p">(</span><span class="n">amount</span> <span class="o">*</span> <span class="n">rate</span> <span class="o">*</span> <span class="p">(</span><span class="n">days</span> <span class="o">+</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'30'</span><span class="p">)),</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'36500'</span><span class="p">))</span>
               <span class="p">.</span><span class="n">quantize</span><span class="p">(</span><span class="n">Decimal</span><span class="p">(</span><span class="s">'1'</span><span class="p">),</span> <span class="n">rounding</span><span class="o">=</span><span class="n">decimal</span><span class="p">.</span><span class="n">ROUND_DOWN</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">run_true</span><span class="p">(</span><span class="n">amount</span><span class="p">,</span> <span class="n">rate</span><span class="p">,</span> <span class="n">days</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">amount</span> <span class="o">*</span> <span class="p">(</span><span class="n">rate</span> <span class="o">/</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'100'</span><span class="p">))</span> <span class="o">*</span> <span class="p">((</span><span class="n">days</span> <span class="o">+</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'30'</span><span class="p">))</span> <span class="o">/</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'365'</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
    <span class="n">tested</span> <span class="o">=</span> <span class="n">naive_err</span> <span class="o">=</span> <span class="n">opt_err</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">d</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">366</span><span class="p">):</span>
        <span class="n">factor</span> <span class="o">=</span> <span class="n">d</span> <span class="o">+</span> <span class="mi">30</span>
        <span class="k">for</span> <span class="n">r_int</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span> <span class="mi">240</span><span class="p">,</span> <span class="mi">5</span><span class="p">):</span>
            <span class="n">rate_dec</span> <span class="o">=</span> <span class="n">Decimal</span><span class="p">(</span><span class="n">r_int</span><span class="p">)</span> <span class="o">/</span> <span class="n">Decimal</span><span class="p">(</span><span class="s">'10'</span><span class="p">)</span>
            <span class="n">g</span> <span class="o">=</span> <span class="n">math</span><span class="p">.</span><span class="n">gcd</span><span class="p">(</span><span class="n">r_int</span> <span class="o">*</span> <span class="n">factor</span><span class="p">,</span> <span class="mi">3650000</span><span class="p">)</span>
            <span class="n">base_A</span> <span class="o">=</span> <span class="mi">3650000</span> <span class="o">//</span> <span class="n">g</span>
            <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">5</span><span class="p">):</span>
                <span class="n">amt</span> <span class="o">=</span> <span class="n">Decimal</span><span class="p">(</span><span class="n">base_A</span> <span class="o">*</span> <span class="n">k</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>
                <span class="k">if</span> <span class="n">amt</span> <span class="o">&gt;</span> <span class="mi">100_000_000</span><span class="p">:</span> <span class="k">continue</span>
                <span class="n">true_val</span> <span class="o">=</span> <span class="n">run_true</span><span class="p">(</span><span class="n">amt</span><span class="p">,</span> <span class="n">rate_dec</span><span class="p">,</span> <span class="n">Decimal</span><span class="p">(</span><span class="n">d</span><span class="p">))</span>
                <span class="k">if</span> <span class="n">true_val</span> <span class="o">%</span> <span class="mi">1</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">:</span> <span class="k">continue</span>
                <span class="n">tested</span> <span class="o">+=</span> <span class="mi">1</span>
                <span class="k">if</span> <span class="n">run_naive</span><span class="p">(</span><span class="n">amt</span><span class="p">,</span> <span class="n">rate_dec</span><span class="p">,</span> <span class="n">Decimal</span><span class="p">(</span><span class="n">d</span><span class="p">))</span> <span class="o">!=</span> <span class="nb">int</span><span class="p">(</span><span class="n">true_val</span><span class="p">):</span> <span class="n">naive_err</span> <span class="o">+=</span> <span class="mi">1</span>
                <span class="k">if</span> <span class="n">run_optimized</span><span class="p">(</span><span class="n">amt</span><span class="p">,</span> <span class="n">rate_dec</span><span class="p">,</span> <span class="n">Decimal</span><span class="p">(</span><span class="n">d</span><span class="p">))</span> <span class="o">!=</span> <span class="nb">int</span><span class="p">(</span><span class="n">true_val</span><span class="p">):</span> <span class="n">opt_err</span> <span class="o">+=</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">tested</span> <span class="o">&gt;=</span> <span class="mi">5000</span><span class="p">:</span> <span class="k">break</span>

    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"정수 경계 케이스: </span><span class="si">{</span><span class="n">tested</span><span class="si">}</span><span class="s">건"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"naive      오차율: </span><span class="si">{</span><span class="n">naive_err</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">tested</span><span class="si">}</span><span class="s"> (</span><span class="si">{</span><span class="n">naive_err</span><span class="o">/</span><span class="n">tested</span><span class="o">*</span><span class="mi">100</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">%)"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"optimized  오차율: </span><span class="si">{</span><span class="n">opt_err</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">tested</span><span class="si">}</span><span class="s">  (</span><span class="si">{</span><span class="n">opt_err</span><span class="o">/</span><span class="n">tested</span><span class="o">*</span><span class="mi">100</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">%)"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="n">main</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="72-bigquery-10만-건-테스트-결과">7.2 BigQuery 10만 건 테스트 결과</h3>

<ul>
  <li><strong>번역 쿼리 <code class="language-plaintext highlighter-rouge">BIGNUMERIC</code> 불일치</strong>: <strong>1건</strong> (일치율 99.9990%)</li>
  <li><strong>최적화 쿼리 불일치</strong>: <strong>0건</strong> (일치율 <strong>100%</strong>)</li>
</ul>

<p><strong>번역 쿼리 불일치 사례</strong>:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: left">대출관리번호</th>
      <th style="text-align: left">조회 정보</th>
      <th style="text-align: center">Oracle 기준값</th>
      <th style="text-align: center">번역 쿼리 결과</th>
      <th style="text-align: center">최적화 쿼리 결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">LOAN043217</code></td>
      <td style="text-align: left">잔액 2,184,500원 / 금리 8.5% / 52일</td>
      <td style="text-align: center"><strong>76,551원</strong></td>
      <td style="text-align: center"><code class="language-plaintext highlighter-rouge">76,550원</code> (-1원)</td>
      <td style="text-align: center"><strong>76,551원</strong> ✅</td>
    </tr>
  </tbody>
</table>

<h3 id="73-spanner-10만-건-테스트-결과">7.3 Spanner 10만 건 테스트 결과</h3>

<ul>
  <li><strong>번역 쿼리 <code class="language-plaintext highlighter-rouge">NUMERIC</code> 불일치</strong>: <strong>16건</strong> (일치율 99.9840%)</li>
  <li><strong>최적화 쿼리 불일치</strong>: <strong>0건</strong> (일치율 <strong>100%</strong>)</li>
</ul>

<p>Spanner는 <code class="language-plaintext highlighter-rouge">ROUND_HALF_UP</code> 정책으로 오차가 +1원 또는 -1원 양방향으로 발생합니다.</p>

<p><strong>상향 오차 사례 (<code class="language-plaintext highlighter-rouge">+1원</code>)</strong>:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Spanner naive SQL</span>
<span class="k">SELECT</span> <span class="n">TRUNC</span><span class="p">(</span>
  <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="mi">2193667</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="mi">17</span><span class="p">.</span><span class="mi">1</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">100</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)))</span>
  <span class="o">*</span> <span class="p">((</span><span class="k">CAST</span><span class="p">(</span><span class="mi">67</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">365</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">))</span> <span class="o">+</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">30</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">365</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">))</span>
<span class="p">)</span>
<span class="c1">-- 결과: 73,218원 (Oracle: 73,217원, +1원 격차)</span>
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Spanner optimized SQL (후순위 나눗셈)</span>
<span class="k">SELECT</span> <span class="n">TRUNC</span><span class="p">(</span>
  <span class="k">CAST</span><span class="p">(</span><span class="mi">2193667</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">*</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">17</span><span class="p">.</span><span class="mi">1</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">*</span> <span class="p">(</span><span class="k">CAST</span><span class="p">(</span><span class="mi">67</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span> <span class="o">+</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">30</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">))</span>
  <span class="o">/</span> <span class="k">CAST</span><span class="p">(</span><span class="mi">36500</span> <span class="k">AS</span> <span class="nb">NUMERIC</span><span class="p">)</span>
<span class="p">)</span>
<span class="c1">-- 결과: 73,217원 ✅ (정합)</span>
</code></pre></div></div>

<h3 id="74-mysql--presto--trino-결과-요약">7.4 MySQL / Presto / Trino 결과 요약</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">DB</th>
      <th style="text-align: center">번역 쿼리 불일치</th>
      <th style="text-align: center">최적화 쿼리 불일치</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left">MySQL</td>
      <td style="text-align: center">2건 (99.9980%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left">Trino</td>
      <td style="text-align: center">18,232건 (81.7680%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left">Presto</td>
      <td style="text-align: center">95,416건 (4.5840%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
  </tbody>
</table>

<p>Presto와 Trino는 번역 쿼리에서 <code class="language-plaintext highlighter-rouge">max(s1,s2)</code> 나눗셈 스케일 규칙으로 인해 사실상 대부분의 케이스에서 오차가 발생합니다. 후순위 나눗셈 적용 후 전건 정합을 달성했습니다.</p>

<h3 id="75-6종-데이터베이스-종합-결과">7.5 6종 데이터베이스 종합 결과</h3>

<table>
  <thead>
    <tr>
      <th style="text-align: left">데이터베이스</th>
      <th style="text-align: left">수치 타입</th>
      <th style="text-align: center">번역 쿼리 불일치</th>
      <th style="text-align: center">최적화 쿼리 불일치</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: left"><strong>Oracle</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMBER</code></td>
      <td style="text-align: center">0건 (100%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>BigQuery</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">BIGNUMERIC</code></td>
      <td style="text-align: center">1건 (99.9990%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Spanner</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMERIC</code></td>
      <td style="text-align: center">16건 (99.9840%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>PostgreSQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">NUMERIC</code></td>
      <td style="text-align: center">0건 (100%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>MySQL</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: center">2건 (99.9980%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Trino</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: center">18,232건 (81.7680%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
    <tr>
      <td style="text-align: left"><strong>Presto</strong></td>
      <td style="text-align: left"><code class="language-plaintext highlighter-rouge">DECIMAL</code></td>
      <td style="text-align: center">95,416건 (4.5840%)</td>
      <td style="text-align: center"><strong>0건</strong> ✅</td>
    </tr>
  </tbody>
</table>

<p>후순위 나눗셈과 명시적 형변환을 적용한 최적화 쿼리는 Oracle을 포함한 7종 데이터베이스 전체에서 단 1건의 오차도 없이 100% 정합성을 달성했습니다.</p>

<h2 id="8-프로덕션-방어-수칙">8. 프로덕션 방어 수칙</h2>

<p>Oracle에서 이식한 정산 쿼리를 분산 데이터베이스 환경에서 안전하게 운영하기 위한 필수 체크리스트입니다.</p>

<p><strong>후순위 나눗셈 : 연산 중 소수점 유실 방지</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />나눗셈은 항상 마지막 단계로 배치 : <code class="language-plaintext highlighter-rouge">TRUNC</code> 직전 단 1회만 수행</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />중간 나눗셈이 불가피한 경우: 분자·분모를 먼저 곱셈으로 통합 후 단일 나눗셈으로 대체</li>
</ul>

<p><strong>명시적 형변환 : 정밀도 손실 방지</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />모든 수치 컬럼에 명시적 <code class="language-plaintext highlighter-rouge">CAST(col AS BIGNUMERIC)</code> 선언</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />소수 리터럴은 <code class="language-plaintext highlighter-rouge">BIGNUMERIC '30'</code> 프리픽스 또는 <code class="language-plaintext highlighter-rouge">CAST(30 AS BIGNUMERIC)</code> 형태로 기입</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">FLOAT64</code> / <code class="language-plaintext highlighter-rouge">DOUBLE</code> 타입과의 혼합 연산 금지</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />CTE 내 중간 결과 컬럼에도 타입 명시 (<code class="language-plaintext highlighter-rouge">FLOAT64</code>로 추론되지 않도록)</li>
</ul>

<p><strong>UNION ALL : 슈퍼정밀도 손실 방지</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />모든 브랜치의 수치 컬럼 타입을 동일한 고정소수점 타입으로 명시적 통일 (Tier 1)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />상수 리터럴 브랜치도 예외 없이 타입 프리픽스 적용 (Tier 2)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />수식 피연산자 사전 업캐스팅 (Tier 3)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />마이그레이션 린터로 빌드 타임 사전 테스트 (Tier 4)</li>
</ul>

<p><strong>테스트 파이프라인</strong></p>
<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><code class="language-plaintext highlighter-rouge">bq CLI</code> 대량 쿼리 시 <code class="language-plaintext highlighter-rouge">--max_rows=150000</code> 명시 (기본값 100건 제한 우회)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />정수 경계 케이스 전용 시뮬레이션(<code class="language-plaintext highlighter-rouge">verify.py</code>) 마이그레이션 전 실행</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Oracle 원천 결과와 1:1 전수 대조 (<code class="language-plaintext highlighter-rouge">compare_results.py</code>) 실행</li>
</ul>

<h2 id="9-결론">9. 결론</h2>

<p>후순위 나눗셈과 명시적 형변환, 두 기법을 함께 적용해 6종 데이터베이스 전체에서 100% 정합성을 달성했습니다.</p>

<p>정리하면 이렇습니다:</p>

<blockquote>
  <p><strong>수학적으로 동치인 수식이라도, 연산 순서와 타입 관리가 정밀도를 결정한다.</strong></p>
</blockquote>

<p>Oracle에서 오차가 발생하지 않았던 이유는 운이 아닙니다. Oracle <code class="language-plaintext highlighter-rouge">NUMBER</code> 타입이 연산 단계마다 소수 자릿수를 고정 스케일로 강제 정규화하지 않기 때문입니다.<br />
반면 BigQuery, Spanner, MySQL, Presto, Trino는 연산 단계마다 고정 스케일 정규화를 강제 적용하므로, 중간 나눗셈이 있는 수식은 정수 경계 케이스에서 오차를 유발합니다. 여기에 UNION ALL이나 소수 리터럴이 섞이면 타입이 조용히 정밀도가 손실되어 오차가 재발합니다.</p>

<p>플랫폼을 바꿀 때는 수식과 타입을 함께 검토해야 합니다. <code class="language-plaintext highlighter-rouge">/ 365</code>, <code class="language-plaintext highlighter-rouge">/ 12</code>, <code class="language-plaintext highlighter-rouge">/ 100</code>처럼 무한 순환소수를 만드는 나눗셈이 수식 중간에 있다면 후순위 나눗셈을 적용하고, 모든 수치 컬럼과 리터럴에 명시적 CAST를 선언해야 합니다. 어느 하나만으로는 절반만 방어한 겁니다.</p>]]></content><author><name></name></author><category term="data" /><category term="BigQuery" /><category term="Oracle" /><category term="Spanner" /><category term="PostgreSQL" /><category term="MySQL" /><category term="Presto" /><category term="Trino" /><category term="소수점 정밀도" /><category term="마이그레이션" /><category term="금융" /><summary type="html"><![CDATA[Oracle에서 문법만 바꿔 이식한 쿼리가 왜 1원 오차를 내는지 파고든 기록. 후순위 나눗셈과 명시적 형변환으로 BigQuery·Spanner·PostgreSQL·MySQL·Presto·Trino 6종 데이터베이스에서 100% 정합성을 달성했습니다.]]></summary></entry><entry><title type="html">Gemini CLI + Claude GAN: MCP로 AI 두 개를 협력시키는 코드 생성 루프</title><link href="https://hajekim.github.io/ai/2026/04/14/claude-gan-mcp-gemini-cli/" rel="alternate" type="text/html" title="Gemini CLI + Claude GAN: MCP로 AI 두 개를 협력시키는 코드 생성 루프" /><published>2026-04-14T09:00:00+09:00</published><updated>2026-04-14T09:00:00+09:00</updated><id>https://hajekim.github.io/ai/2026/04/14/claude-gan-mcp-gemini-cli</id><content type="html" xml:base="https://hajekim.github.io/ai/2026/04/14/claude-gan-mcp-gemini-cli/"><![CDATA[<p>Gemini CLI로 코드를 짜다 보면 “이게 진짜 괜찮은 코드인지”를 다시 Gemini한테 물어보는 루프가 자연스럽게 생깁니다. 그러다 문득 — 이 평가를 Gemini가 하고, 생성은 Claude한테 맡기면 어떨까 싶었습니다.</p>

<p>GAN(Generative Adversarial Network)에서 빌려온 아이디어입니다. Generator가 만들고, Discriminator가 걸러내고, 반복하면 품질이 올라가는 구조를 AI 코드 생성에 적용해봤습니다. Gemini가 평가 기준(Sprint Contract)을 직접 작성하고, Claude가 그걸 보고 구현하고, Gemini가 다시 채점합니다. Grade A가 나올 때까지.</p>

<p><strong><a href="https://github.com/hajekim/claude-gan">GitHub에서 보기 → hajekim/claude-gan</a></strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Gemini CLI (Evaluator — 네이티브 추론)
  │
  ├─ 1. Sprint Contract(JSON DoD)를 자체 추론으로 직접 작성
  ├─ 2. claude_generate MCP 도구 호출 ──▶ Claude 4.6 Sonnet (Vertex AI)
  ├─ 3. 반환된 코드를 계약 기준으로 직접 평가
  └─ 4. Grade A 달성까지 구체적 피드백으로 반복
</code></pre></div></div>

<p>핵심은 Claude를 서브프로세스로 띄우지 않고 <strong>MCP 도구</strong>로 등록한다는 점입니다. Gemini CLI가 세션을 주도하면서 필요할 때 <code class="language-plaintext highlighter-rouge">claude_generate</code>를 호출하는 방식이라, 루프 전체가 Gemini CLI 안에서 돌아갑니다.</p>

<h2 id="사전-준비">사전 준비</h2>

<ul>
  <li>Google Cloud Project (Vertex AI API 활성화, <code class="language-plaintext highlighter-rouge">global</code> 리전에서 Claude 4.6 Sonnet 접근 권한)</li>
  <li><code class="language-plaintext highlighter-rouge">gcloud auth application-default login</code> 완료</li>
  <li>Gemini CLI 설치 및 인증 완료</li>
  <li>Python 3.10+</li>
</ul>

<h2 id="설치">설치</h2>

<h3 id="1-리포지토리-클론">1. 리포지토리 클론</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/hajekim/claude-gan.git
<span class="nb">cd </span>claude-gan
</code></pre></div></div>

<h3 id="2-python-의존성-설치">2. Python 의존성 설치</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt
</code></pre></div></div>

<p>Anthropic Vertex AI SDK, FastMCP, python-dotenv, pytest가 들어있습니다.</p>

<h3 id="3-mcp-서버-전역-등록">3. MCP 서버 전역 등록</h3>

<p><code class="language-plaintext highlighter-rouge">~/.gemini/settings.json</code>의 <code class="language-plaintext highlighter-rouge">mcpServers</code>에 추가합니다:</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">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"claude-gan"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/python3"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"/절대경로/claude-gan/src/mcp_server.py"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"GOOGLE_CLOUD_PROJECT"</span><span class="p">:</span><span class="w"> </span><span class="s2">"your-gcp-project-id"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"GOOGLE_CLOUD_LOCATION"</span><span class="p">:</span><span class="w"> </span><span class="s2">"global"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"PYTHONPATH"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/절대경로/claude-gan"</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>

<p><code class="language-plaintext highlighter-rouge">python3</code> 경로는 <code class="language-plaintext highlighter-rouge">which python3</code>로, <code class="language-plaintext highlighter-rouge">/절대경로/claude-gan</code>는 클론한 실제 경로로 교체합니다.</p>

<h3 id="4-기존-geminimd에-gan-지시문-추가">4. 기존 GEMINI.md에 GAN 지시문 추가</h3>

<p><code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code>를 <strong>덮어쓰지 말고</strong>, 아래 두 항목을 기존 내용에 추가합니다. 이미 공들여 작성한 GEMINI.md가 있을 테니까요.</p>

<p><strong>MCP 서버 목록에 추가:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- **claude-gan**: GAN 루프 코드 생성 전용. `claude_generate(task, contract, feedback)`으로
  Claude 4.6 Sonnet (Vertex AI)에게 구현을 위임한다.
  `save_artifact(content, filename)`, `save_progress(sprint_id, status, grade)`로 결과를 저장한다.
</code></pre></div></div>

<p><strong>실행 워크플로우(ACT 단계 또는 GAN 루프 섹션)에 추가:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>코드 생성이 필요한 경우, 직접 작성하지 않고 claude-gan MCP에 위임한다:
1. 작업 요구사항을 분석하여 Sprint Contract(JSON DoD)를 직접 작성한다.
2. claude_generate(task, contract, feedback="")를 호출하여 Claude의 구현을 받는다.
3. 반환된 코드를 Skeptical Judge로서 엄격하게 평가한다 (Gemini = Evaluator).
4. Grade B/C이면 구체적인 피드백으로 claude_generate를 재호출한다. 최대 3회.
5. Grade A 달성 시 save_artifact와 save_progress로 결과를 저장한다.
코드 생성이 아닌 작업(기존 코드 수정, 설정 변경 등)은 직접 처리한다.
</code></pre></div></div>

<p>Gemini CLI를 재시작하고 <code class="language-plaintext highlighter-rouge">mcp list</code>로 <code class="language-plaintext highlighter-rouge">claude-gan</code>가 Connected 상태인지 확인합니다.</p>

<h2 id="실제로-쓰면-이렇게-됩니다">실제로 쓰면 이렇게 됩니다</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Python으로 가위바위보 게임을 만들어줘. 점수 기록 기능도 포함해줘.
</code></pre></div></div>

<p>Gemini가 먼저 Sprint Contract를 잡습니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✦ 가위바위보 게임과 점수 기록 기능을 구현하기 위한 개발 계획 PLAN.md를 작성했습니다.
  GAN 루프(Gemini가 평가자, Claude가 생성자)를 사용하여 PEP 8 표준을 준수하고
  기능적으로 완벽한 코드를 작성할 예정입니다.
  해당 계획에 동의하신다면, 상세 작업을 스프린트로 나누는 DEFINE.md를 작성하고
  구현을 시작하겠습니다. 계획을 진행할까요?

  PLAN.md 내용 요약:
   - 목표: 사용자 입력을 받아 컴퓨터와 대결하고 승/무/패 점수를 기록하는 Python 가위바위보 게임 개발.
   - 전략: Claude 4.6 Sonnet(claude_generate)을 통해 코드를 생성하고, Gemini가 이를 검증하는 GAN 루프 적용.
   - 검증 계약: 정확한 승패 로직, 점수 기록/표시, 예외 처리 및 PEP 8 준수 여부 확인.
   - 등급 기준: 완벽한 기능과 스타일(Grade A)을 목표로 함.
</code></pre></div></div>

<p>승인하면 Claude가 구현하고, Gemini가 채점하는 루프가 돌고, 최종 결과를 받습니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>✦ GAN 루프(Claude Generator + Gemini Evaluator)를 통해 고품질의 가위바위보 게임 구현을 완료했습니다.

  구현 결과: rps_game.py
   - 핵심 기능:
       - 사용자 입력(rock, paper, scissors)과 컴퓨터의 무작위 선택 대결.
       - 매 라운드 종료 후 현재 점수(승, 패, 무) 실시간 표시.
       - quit 입력 시 게임이 종료되며 전체 세션의 최종 성적 요약 출력.
       - 잘못된 입력에 대한 예외 처리 및 재입력 요청.

  GAN 루프 검증 결과
   - Generator (Claude 4.6 Sonnet): 명확한 함수 구조와 가독성 높은 코드 생성.
   - Evaluator (Gemini): PEP 8 준수 여부, 점수 기록 로직의 정확성, 예외 처리 완성도를
     검토하여 Grade A로 최종 승인.
</code></pre></div></div>

<h2 id="mcp-도구-목록">MCP 도구 목록</h2>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">claude_generate(task, contract, feedback)</code></td>
      <td>Claude 4.6 Sonnet (Vertex AI)으로 코드 생성/개선</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">save_artifact(content, filename)</code></td>
      <td><code class="language-plaintext highlighter-rouge">artifacts/</code> 디렉토리에 결과물 저장</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">load_progress()</code></td>
      <td><code class="language-plaintext highlighter-rouge">state/progress.json</code>에서 현재 스프린트 상태 로드</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">save_progress(sprint_id, status, grade)</code></td>
      <td>스프린트 상태 저장</td>
    </tr>
  </tbody>
</table>

<h2 id="프로젝트-구조">프로젝트 구조</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude-gan/
├── src/
│   ├── mcp_server.py                # FastMCP 서버 (stdio transport)
│   └── tools/
│       └── claude_tool.py           # Claude Vertex AI 호출 핵심 로직
├── tests/
│   └── test_claude_tool.py          # 단위 테스트
├── state/
│   └── progress.json                # 스프린트 상태 (런타임, gitignored)
├── artifacts/                       # 생성 코드 출력 (런타임, gitignored)
├── GEMINI.md                        # Gemini CLI용 Evaluator 지시문
├── gemini-extension.json.template   # Extension 매니페스트 템플릿
└── requirements.txt
</code></pre></div></div>

<hr />

<p>직접 써보면서 느낀 건, 모델 하나한테 생성과 평가를 동시에 맡기는 것보다 역할을 분리했을 때 결과물 품질이 체감상 다릅니다. 자기가 짠 코드를 자기가 검토하면 놓치는 게 많은 건 사람도 마찬가지니까요.</p>

<p>Vertex AI Claude 접근 권한이 있는 GCP 프로젝트가 필요하다는 진입장벽이 있긴 합니다만, Gemini CLI를 이미 쓰고 있다면 설정 몇 줄로 바로 붙일 수 있습니다.</p>]]></content><author><name></name></author><category term="ai" /><category term="Gemini" /><category term="Gemini CLI" /><category term="Claude" /><category term="MCP" /><category term="Vertex AI" /><category term="GCP" /><category term="코드 생성" /><summary type="html"><![CDATA[Gemini CLI가 Evaluator로 직접 동작하고 Claude 4.6 Sonnet(Vertex AI)을 MCP 도구로 호출하는 GAN 기반 코드 생성 시스템 설치와 사용법]]></summary></entry><entry><title type="html">Gemini CLI Extension으로 AI 에이전트 디자인 패턴 28개를 한 번에 — Agentic Design Patterns</title><link href="https://hajekim.github.io/ai/2026/03/28/agentic-design-patterns-extension/" rel="alternate" type="text/html" title="Gemini CLI Extension으로 AI 에이전트 디자인 패턴 28개를 한 번에 — Agentic Design Patterns" /><published>2026-03-28T00:00:00+09:00</published><updated>2026-03-28T00:00:00+09:00</updated><id>https://hajekim.github.io/ai/2026/03/28/agentic-design-patterns-extension</id><content type="html" xml:base="https://hajekim.github.io/ai/2026/03/28/agentic-design-patterns-extension/"><![CDATA[<p>AI 에이전트를 설계하다 보면 반복적으로 등장하는 구조적 문제들이 있습니다. “여러 단계를 순서대로 실행해야 하는데”, “에러가 나면 어떻게 복구하지?”, “외부 도구를 연결하는 방법은?”, “멀티에이전트는 어떻게 구성하지?” 등등.</p>

<p>이런 문제들에 대한 검증된 해법이 <strong>에이전트 디자인 패턴(Agentic Design Patterns)</strong> 입니다. Antonio Gulli의 동명 저서(424페이지, 21챕터)를 기반으로 28개의 패턴을 Gemini CLI Extension으로 패키징했습니다.</p>

<hr />

<h2 id="무엇을-제공하는가">무엇을 제공하는가</h2>

<p><strong>28개의 에이전트 스킬</strong>을 단일 Extension으로 설치하면, Gemini CLI가 사용자의 의도를 파악해 자동으로 관련 패턴 가이드를 불러옵니다. 코드 스켈레톤 생성, 패턴 추천, 코드 리뷰까지 하나의 도구에서 처리합니다.</p>

<table>
  <thead>
    <tr>
      <th>카테고리</th>
      <th>패턴 수</th>
      <th>포함 패턴</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Core</strong></td>
      <td>7</td>
      <td>Prompt Chaining, Routing, Parallelization, Reflection, Tool Use, Planning, Multi-Agent Collaboration</td>
    </tr>
    <tr>
      <td><strong>State Management</strong></td>
      <td>4</td>
      <td>Memory Management, Learning &amp; Adaptation, MCP, Goal Setting</td>
    </tr>
    <tr>
      <td><strong>Reliability</strong></td>
      <td>3</td>
      <td>Exception Handling, Human-in-the-Loop, RAG</td>
    </tr>
    <tr>
      <td><strong>Advanced</strong></td>
      <td>7</td>
      <td>A2A, Resource-Aware, Reasoning, Guardrails, Evaluation, Prioritization, Exploration</td>
    </tr>
    <tr>
      <td><strong>Appendix</strong></td>
      <td>7</td>
      <td>Prompt Engineering, GUI Agents, Agentic Frameworks, AgentSpace, AI CLI, Coding Agents, Reasoning Engines</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="설치">설치</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini extensions <span class="nb">install </span>https://github.com/hajekim/agentic-design-patterns-extension
</code></pre></div></div>

<p>설치 후 Gemini CLI를 재시작하면 28개의 스킬이 자동으로 활성화됩니다. 추가 설정은 필요하지 않습니다.</p>

<p>설치된 Extension 확인:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini extensions list
</code></pre></div></div>

<hr />

<h2 id="스킬-자동-활성화--어떻게-동작하는가">스킬 자동 활성화 — 어떻게 동작하는가</h2>

<p>Gemini CLI는 세션 시작 시 모든 스킬의 <code class="language-plaintext highlighter-rouge">name</code>과 <code class="language-plaintext highlighter-rouge">description</code>을 색인합니다. 사용자의 요청이 스킬 설명과 의미적으로 매칭되면, 모델이 자율적으로 해당 스킬의 전체 내용을 로드합니다. 별도의 명령어 없이 <strong>자연어 요청만으로 패턴 가이드가 활성화</strong>됩니다.</p>

<p><strong>한국어 요청 예시:</strong></p>

<table>
  <thead>
    <tr>
      <th>요청</th>
      <th>활성화되는 패턴</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>“프롬프트 체이닝으로 파이프라인 만들어줘”</td>
      <td>Prompt Chaining</td>
    </tr>
    <tr>
      <td>“MCP 구성을 해줘”</td>
      <td>MCP</td>
    </tr>
    <tr>
      <td>“메모리뱅크 만들어줘”</td>
      <td>Memory Management</td>
    </tr>
    <tr>
      <td>“멀티에이전트 시스템 설계해줘”</td>
      <td>Multi-Agent Collaboration</td>
    </tr>
    <tr>
      <td>“에러 처리 어떻게 해?”</td>
      <td>Exception Handling</td>
    </tr>
    <tr>
      <td>“추론 모델 언제 써야 해?”</td>
      <td>Reasoning Engines</td>
    </tr>
    <tr>
      <td>“RAG 파이프라인 구축해줘”</td>
      <td>RAG</td>
    </tr>
  </tbody>
</table>

<p>4개 언어(영어·한국어·일본어·중국어) 총 <strong>1,376개의 트리거 문구</strong>가 등록되어 있습니다.</p>

<hr />

<h2 id="슬래시-커맨드">슬래시 커맨드</h2>

<h3 id="pattern-summary--패턴-목록-탐색"><code class="language-plaintext highlighter-rouge">/pattern-summary</code> — 패턴 목록 탐색</h3>

<p>28개 패턴 전체를 카테고리별로 한눈에 봅니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 전체 28개 패턴 목록 (카테고리별 테이블)</span>
/pattern-summary

<span class="c"># 특정 카테고리만 보기</span>
/pattern-summary core
/pattern-summary reliability
/pattern-summary advanced

<span class="c"># 특정 패턴 상세 보기 (언제 쓰는지 + 관련 패턴 추천)</span>
/pattern-summary planning
/pattern-summary rag
</code></pre></div></div>

<h3 id="gen-skeleton--코드-스켈레톤-생성"><code class="language-plaintext highlighter-rouge">/gen-skeleton</code> — 코드 스켈레톤 생성</h3>

<p>패턴 이름을 인자로 넘기면 즉시 실행 가능한 Python 코드 스켈레톤을 생성합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/gen-skeleton planning
/gen-skeleton rag
/gen-skeleton multi-agent-collaboration
/gen-skeleton exception-handling
</code></pre></div></div>

<p>생성되는 코드는 다음 규칙을 강제합니다:</p>
<ul>
  <li><strong>Gemini SDK</strong>: <code class="language-plaintext highlighter-rouge">google-genai</code> (<code class="language-plaintext highlighter-rouge">import google.genai as genai</code>)</li>
  <li><strong>ADK</strong>: <code class="language-plaintext highlighter-rouge">LlmAgent</code> + <code class="language-plaintext highlighter-rouge">Runner</code> + <code class="language-plaintext highlighter-rouge">InMemorySessionService</code></li>
  <li><strong>기본 모델</strong>: <code class="language-plaintext highlighter-rouge">gemini-2.5-flash</code></li>
</ul>

<p>스켈레톤은 Imports → Configuration → Core 패턴 구현 → Main 실행 블록 순서로 구성됩니다.</p>

<hr />

<h2 id="서브에이전트">서브에이전트</h2>

<h3 id="architect--패턴-조합-추천"><code class="language-plaintext highlighter-rouge">@architect</code> — 패턴 조합 추천</h3>

<p>어떤 패턴을 써야 할지 모를 때, 문제를 설명하면 최적의 패턴 조합을 추천합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@architect <span class="s2">"피드백을 학습하는 고객 지원 봇을 만들고 싶어"</span>
@architect <span class="s2">"실시간 데이터를 분석해서 이상 탐지를 하는 에이전트 설계해줘"</span>
@architect <span class="s2">"여러 전문 에이전트가 협력해서 리포트를 작성하는 시스템 필요해"</span>
</code></pre></div></div>

<p>출력 형식:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## Pattern Recommendation

Primary Pattern: `planning`
Supporting Patterns: `reflection`, `memory-management`

### Why This Combination
...

### Implementation Sequence
1. Start with `planning` — ...
2. Add `reflection` — ...

### Watch Out For
- ...

### Next Step
Run `/gen-skeleton planning` to generate the code skeleton.
</code></pre></div></div>

<h3 id="reviewer--코드-패턴-컴플라이언스-검토"><code class="language-plaintext highlighter-rouge">@reviewer</code> — 코드 패턴 컴플라이언스 검토</h3>

<p>작성한 에이전트 코드를 붙여넣으면 패턴 구현 적합성과 SDK 사용 오류를 검토합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@reviewer &lt;코드 붙여넣기&gt;
</code></pre></div></div>

<p>검토 항목:</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>올바른 사용</th>
      <th>잘못된 사용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Gemini SDK import</td>
      <td><code class="language-plaintext highlighter-rouge">import google.genai as genai</code></td>
      <td><code class="language-plaintext highlighter-rouge">import google.generativeai</code></td>
    </tr>
    <tr>
      <td>ADK Agent 클래스</td>
      <td><code class="language-plaintext highlighter-rouge">LlmAgent</code></td>
      <td><code class="language-plaintext highlighter-rouge">Agent</code></td>
    </tr>
    <tr>
      <td>ADK Runner</td>
      <td><code class="language-plaintext highlighter-rouge">Runner</code> + <code class="language-plaintext highlighter-rouge">InMemorySessionService</code></td>
      <td><code class="language-plaintext highlighter-rouge">InMemoryRunner</code></td>
    </tr>
    <tr>
      <td>LangChain vector store</td>
      <td><code class="language-plaintext highlighter-rouge">langchain-chroma</code></td>
      <td><code class="language-plaintext highlighter-rouge">langchain_community.vectorstores.Chroma</code></td>
    </tr>
    <tr>
      <td>LangChain conversation</td>
      <td><code class="language-plaintext highlighter-rouge">RunnableWithMessageHistory</code></td>
      <td><code class="language-plaintext highlighter-rouge">ConversationChain</code></td>
    </tr>
  </tbody>
</table>

<p>출력 형식:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## Pattern Review: `rag`

### SDK Compliance
✅ Gemini SDK import correct
❌ ADK Runner: `InMemoryRunner` → use `Runner` + `InMemorySessionService`

### Pattern Implementation
✅ Retrieval step present before generation
❌ Source attribution missing

### Overall: NEEDS FIXES
</code></pre></div></div>

<hr />

<h2 id="mcp-서버-스킬-검색">MCP 서버 (스킬 검색)</h2>

<p>Extension이 설치되면 <code class="language-plaintext highlighter-rouge">mcp_server.py</code>가 자동으로 MCP 서버로 등록됩니다. 모델이 직접 패턴을 검색하고 스킬 내용을 가져올 수 있으며, 대화 중 자연스럽게 호출됩니다.</p>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">list_patterns([category])</code></td>
      <td>28개 패턴 목록 반환. 카테고리 필터 가능 (<code class="language-plaintext highlighter-rouge">core</code> / <code class="language-plaintext highlighter-rouge">state</code> / <code class="language-plaintext highlighter-rouge">reliability</code> / <code class="language-plaintext highlighter-rouge">advanced</code> / <code class="language-plaintext highlighter-rouge">appendix</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">get_skill(pattern_name)</code></td>
      <td>패턴 이름으로 <code class="language-plaintext highlighter-rouge">SKILL.md</code> 전체 내용 조회. 오타 시 유사 패턴 제안</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">search_skills(query)</code></td>
      <td>키워드로 전체 28개 스킬을 검색, 매칭된 라인과 패턴 반환</td>
    </tr>
  </tbody>
</table>

<h3 id="list_patterns--패턴-목록-조회"><code class="language-plaintext highlighter-rouge">list_patterns</code> — 패턴 목록 조회</h3>

<p>카테고리 없이 호출하면 28개 전체 목록을, 카테고리를 지정하면 해당 카테고리만 반환합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 전체 28개 목록 (카테고리별 그룹)
list_patterns()

# 특정 카테고리만
list_patterns("core")       → Core 7개 패턴
list_patterns("state")      → State Management 4개 패턴
list_patterns("reliability") → Reliability 3개 패턴
list_patterns("advanced")   → Advanced 7개 패턴
list_patterns("appendix")   → Appendix 7개 패턴
</code></pre></div></div>

<p>잘못된 카테고리를 입력하면 유효한 카테고리 목록을 안내합니다:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Unknown category 'xxx'. Valid categories: `core`, `state`, `reliability`, `advanced`, `appendix`.
</code></pre></div></div>

<h3 id="get_skill--스킬-전체-내용-조회"><code class="language-plaintext highlighter-rouge">get_skill</code> — 스킬 전체 내용 조회</h3>

<p>패턴 이름으로 해당 <code class="language-plaintext highlighter-rouge">SKILL.md</code> 전체를 반환합니다. 패턴의 정의, 트리거 문구, 구현 예제 코드를 모두 포함합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>get_skill("planning")
get_skill("rag")
get_skill("mcp-setup")
get_skill("multi-agent-collaboration")
</code></pre></div></div>

<p>오타나 불완전한 이름 입력 시 유사 패턴을 제안합니다:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 입력: get_skill("plan")
Pattern 'plan' not found. Did you mean: `planning`?

# 입력: get_skill("memory")
Pattern 'memory' not found. Did you mean: `memory-management`?
</code></pre></div></div>

<p>패턴 이름을 전혀 모를 경우에는 <code class="language-plaintext highlighter-rouge">list_patterns()</code>로 전체 목록을 먼저 확인합니다.</p>

<h3 id="search_skills--키워드-검색"><code class="language-plaintext highlighter-rouge">search_skills</code> — 키워드 검색</h3>

<p>키워드가 포함된 스킬을 28개 전체에서 검색합니다. 매칭된 스킬 이름, 매칭 횟수, 첫 번째 매칭 줄 미리보기를 반환합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>search_skills("LangGraph")
search_skills("Thinking Budget")
search_skills("fallback")
search_skills("human approval")
</code></pre></div></div>

<p>출력 예시:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Found 'Thinking Budget' in 3 pattern(s):

`reasoning` (5 matches)
  → Thinking Budget controls how deeply the model reasons before responding...

`resource-aware` (3 matches)
  → Set Thinking Budget high when accuracy matters more than latency...

`appendix-reasoning-engines` (8 matches)
  → Thinking Budget is configured via thinking_config, not a separate model...
</code></pre></div></div>

<p>특정 기술(예: <code class="language-plaintext highlighter-rouge">LangGraph</code>, <code class="language-plaintext highlighter-rouge">Chroma</code>, <code class="language-plaintext highlighter-rouge">Redis</code>)이나 개념(예: <code class="language-plaintext highlighter-rouge">retry</code>, <code class="language-plaintext highlighter-rouge">timeout</code>, <code class="language-plaintext highlighter-rouge">fallback</code>)으로 검색해 관련 패턴을 빠르게 찾을 때 유용합니다.</p>

<hr />

<h2 id="권장-워크플로우">권장 워크플로우</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. @architect로 패턴 추천받기
       ↓
2. /pattern-summary &lt;패턴명&gt;으로 상세 확인
       ↓
3. /gen-skeleton &lt;패턴명&gt;으로 코드 스켈레톤 생성
       ↓
4. 코드 작성
       ↓
5. @reviewer로 패턴 컴플라이언스 검토
</code></pre></div></div>

<hr />

<h2 id="모델-선택-가이드">모델 선택 가이드</h2>

<table>
  <thead>
    <tr>
      <th>작업 유형</th>
      <th>권장 모델</th>
      <th>Thinking Budget</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>단순 파이프라인 (prompt-chaining, routing)</td>
      <td><code class="language-plaintext highlighter-rouge">gemini-2.5-flash-lite</code></td>
      <td>미지원</td>
    </tr>
    <tr>
      <td>중간 복잡도 (tool-use, RAG, parallelization)</td>
      <td><code class="language-plaintext highlighter-rouge">gemini-2.5-flash</code></td>
      <td>Dynamic (기본값)</td>
    </tr>
    <tr>
      <td>복잡한 추론 (planning, reasoning, evaluation)</td>
      <td><code class="language-plaintext highlighter-rouge">gemini-2.5-flash</code> 또는 <code class="language-plaintext highlighter-rouge">gemini-2.5-pro</code></td>
      <td>높게 설정</td>
    </tr>
    <tr>
      <td>대규모 조율 (multi-agent, a2a)</td>
      <td><code class="language-plaintext highlighter-rouge">gemini-2.5-pro</code></td>
      <td>높게 설정</td>
    </tr>
  </tbody>
</table>

<p>Thinking Budget은 Flash와 Pro 모델에서 조절 가능한 추론 깊이 파라미터입니다. 대부분의 작업에서는 기본값(Dynamic)을 유지하면 모델이 자동으로 결정합니다.</p>

<hr />

<h2 id="플랫폼-호환성">플랫폼 호환성</h2>

<table>
  <thead>
    <tr>
      <th>플랫폼</th>
      <th>설치 방법</th>
      <th>활성화 방식</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Gemini CLI</strong></td>
      <td><code class="language-plaintext highlighter-rouge">gemini extensions install &lt;url&gt;</code></td>
      <td>시맨틱 — 모델이 설명 기반으로 자율 판단</td>
    </tr>
    <tr>
      <td><strong>Antigravity</strong></td>
      <td><code class="language-plaintext highlighter-rouge">skills/</code>를 <code class="language-plaintext highlighter-rouge">.agents/skills/</code>에 복사</td>
      <td>키워드 패턴 매칭</td>
    </tr>
    <tr>
      <td><strong>Claude Code</strong></td>
      <td><code class="language-plaintext highlighter-rouge">skills/</code>를 <code class="language-plaintext highlighter-rouge">.claude/skills/</code>에 심볼릭 링크</td>
      <td>시맨틱 판단 + 슬래시 커맨드</td>
    </tr>
  </tbody>
</table>

<p>Antigravity와 Claude Code에는 <a href="https://github.com/hajekim/agentic-design-patterns-skills">Skills-only 버전</a>을 사용하세요.</p>

<hr />

<h2 id="extension-관리">Extension 관리</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 최신 버전으로 업데이트</span>
gemini extensions update agentic-design-patterns

<span class="c"># 임시 비활성화 (제거 없이)</span>
gemini extensions disable agentic-design-patterns

<span class="c"># 제거</span>
gemini extensions uninstall agentic-design-patterns
</code></pre></div></div>

<hr />

<h2 id="extension-구조">Extension 구조</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>agentic-design-patterns/
├── gemini-extension.json      ← Extension 매니페스트
├── GEMINI.md                  ← 전역 컨텍스트 (28개 패턴 퀵레퍼런스, 모델 가이드)
├── mcp_server.py              ← 스킬 검색 MCP 서버
├── commands/
│   ├── gen-skeleton.toml      ← /gen-skeleton &lt;패턴명&gt;
│   └── pattern-summary.toml  ← /pattern-summary [필터]
├── agents/
│   ├── architect.md           ← @architect 서브에이전트
│   └── reviewer.md            ← @reviewer 서브에이전트
└── skills/                    ← 28개 스킬 정의
    ├── planning/SKILL.md
    ├── rag/SKILL.md
    └── ...
</code></pre></div></div>

<p>각 스킬은 <strong>DEFINE → PLAN → ACTION</strong> 워크플로우를 따르며, Google ADK, LangChain, LangGraph 구현 예제를 포함합니다.</p>

<hr />

<h2 id="설치-링크">설치 링크</h2>

<ul>
  <li><strong>Extension (Gemini CLI용):</strong> <a href="https://github.com/hajekim/agentic-design-patterns-extension">hajekim/agentic-design-patterns-extension</a></li>
  <li><strong>Skills-only (Antigravity / Claude Code용):</strong> <a href="https://github.com/hajekim/agentic-design-patterns-skills">hajekim/agentic-design-patterns-skills</a></li>
</ul>

<p>현재 버전: <strong>v2.2.3</strong></p>]]></content><author><name></name></author><category term="ai" /><category term="Gemini" /><category term="Gemini CLI" /><category term="Agent" /><category term="AI Agent" /><category term="GCP" /><category term="ADK" /><summary type="html"><![CDATA[28개의 AI 에이전트 디자인 패턴을 Gemini CLI Extension 하나로 설치하고, 슬래시 커맨드와 서브에이전트로 즉시 활용하는 방법]]></summary></entry><entry><title type="html">Agent Harness와 Ralph Loop — Google Cloud 구현 가이드</title><link href="https://hajekim.github.io/ai/2026/02/20/agent-harness-ralph-loop/" rel="alternate" type="text/html" title="Agent Harness와 Ralph Loop — Google Cloud 구현 가이드" /><published>2026-02-20T00:00:00+09:00</published><updated>2026-02-20T00:00:00+09:00</updated><id>https://hajekim.github.io/ai/2026/02/20/agent-harness-ralph-loop</id><content type="html" xml:base="https://hajekim.github.io/ai/2026/02/20/agent-harness-ralph-loop/"><![CDATA[<h2 id="part-1-agent-harness와-ralph-loop">Part 1. Agent Harness와 Ralph Loop</h2>

<h2 id="1-agent-harness란-무엇인가">1. Agent Harness란 무엇인가</h2>

<p><strong>Agent Harness는 AI 모델(LLM)을 감싸는 인프라스트럭처 소프트웨어 시스템</strong>으로, 에이전트가 현실 세계에서 신뢰성 있게 작동할 수 있도록 도구 제공, 컨텍스트 관리, 안전 장치, 워크플로우 구조를 제공합니다. 핵심은 에이전트 자체(두뇌)가 아니라 <strong>에이전트가 동작하는 몸체(body)</strong> 라는 점입니다.</p>

<p>LangChain의 Harrison Chase는 AI 에이전트 생태계를 세 가지 계층으로 분류합니다:</p>

<table>
  <thead>
    <tr>
      <th>계층</th>
      <th>역할</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Agent Framework</strong></td>
      <td>추상화·빌딩블록 제공</td>
      <td>LangChain, Vercel AI SDK, Google ADK</td>
    </tr>
    <tr>
      <td><strong>Agent Runtime</strong></td>
      <td>내구성 실행, 스트리밍, 상태 영속성</td>
      <td>LangGraph, Temporal, Inngest, Google Cloud Run, GKE, Vertex Agent Engine</td>
    </tr>
    <tr>
      <td><strong>Agent Harness</strong></td>
      <td>배터리 포함(batteries included), 프롬프트·도구·계획 기본 탑재</td>
      <td>DeepAgents, Claude Agent SDK, Vertex Memory Bank + Google ADK Memory Tools + Agent Engine</td>
    </tr>
  </tbody>
</table>

<p>Agent Harness는 이 중 <strong>가장 상위 레벨</strong>에 위치하며, 프레임워크 위에 구축되어 기본 프롬프트, 파일시스템 접근, 서브에이전트 관리, 도구 호출의 의견이 반영된(opinionated) 처리를 제공합니다. 자동차에 비유하면, 에이전트가 엔진이라면 하네스는 핸들·브레이크·내비게이션·연료 시스템을 포함한 <strong>자동차 전체</strong>입니다.</p>

<h3 id="agent-harness의-핵심-기능">Agent Harness의 핵심 기능</h3>

<ul>
  <li><strong>도구 통합 레이어(Tool Integration):</strong> 웹 검색, 코드 실행, DB 쿼리, 이미지 생성 등 외부 도구를 LLM에 연결하고, 모델의 도구 호출을 감지·실행·결과 반환</li>
  <li><strong>컨텍스트 관리 &amp; 메모리:</strong> 세션 내 대화 이력, 컨텍스트 압축(compaction), 작업 로그 관리</li>
  <li><strong>오케스트레이션:</strong> 사용자 목표를 하위 작업으로 분해하고, 각 단계에서 모델에 필요한 컨텍스트와 도구를 제공</li>
  <li><strong>신뢰성 &amp; 안전 장치:</strong> 진행 상태 저장, 오류 복구, 유해 출력 필터링, 사용자 확인 요청</li>
</ul>

<hr />

<h2 id="2-ralph-loop란-무엇인가">2. Ralph Loop란 무엇인가</h2>

<p><img src="/assets/image/posts/ralph-loop-diagram.png" alt="Ralph Loop 개념도" /></p>

<p><strong>Ralph Loop(Ralph Wiggum Technique)</strong> 은 AI 에이전트를 활용한 장기 실행 자율 작업을 위한 오케스트레이션 기법입니다. Geoffrey Huntley가 개발·대중화했으며, <em>The Simpsons</em>의 캐릭터 Ralph Wiggum에서 이름을 따왔습니다.</p>

<p>Ralph Wiggum은 사랑스럽지만 건망증이 심하고, 열정적이지만 실수를 반복하는 캐릭터입니다. AI 에이전트도 마찬가지입니다. <strong>이전 시도를 기억하지 못하고, 같은 실수를 반복합니다.</strong> Ralph Loop의 해법은 이 한계를 극복하려 하지 않고 <strong>받아들이는 것</strong>입니다.</p>

<h3 id="가장-순수한-형태">가장 순수한 형태</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span> :<span class="p">;</span> <span class="k">do </span><span class="nb">cat </span>PROMPT.md | claude-code <span class="p">;</span> <span class="k">done</span>
</code></pre></div></div>

<p>이 한 줄의 bash 루프가 Ralph의 본질입니다. <strong>매 반복마다 새로운 세션(fresh context)</strong> 을 시작하고, <strong>파일 시스템을 통한 외부 메모리</strong> 로 세션 간 연속성을 유지합니다.</p>

<h3 id="ralph-loop의-핵심-원리">Ralph Loop의 핵심 원리</h3>

<p><strong>① Fresh Context Per Iteration (매 반복 새 컨텍스트)</strong>
가장 중요한 원칙입니다. 각 반복이 완전히 새로운 세션으로 시작되므로 <strong>Context Rot</strong>(컨텍스트 창에 불필요하거나 충돌하는 정보가 쌓여 출력 품질이 저하되는 현상)을 완전히 회피합니다.</p>

<p>Michael Arnaldi의 경고: <em>“에이전트 하네스 안에서 Ralph를 skill/command로 구현한다면, Ralph의 핵심을 놓치고 있는 것”</em></p>

<p><strong>② 파일 기반 외부 메모리</strong>
세션 간 기억은 오직 파일로 전달됩니다:</p>

<table>
  <thead>
    <tr>
      <th>파일</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PROMPT.md</code></td>
      <td>매 반복 에이전트에 전달되는 지시사항</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">specs/*</code></td>
      <td>기능별 사양 문서 (요구사항의 진실 소스)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fix_plan.md</code></td>
      <td>동적 작업 추적기 (할 일, 발견된 버그, 완료 항목)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AGENT.md</code></td>
      <td>프로젝트 컨벤션과 행동 가이드 (“표지판”)</td>
    </tr>
  </tbody>
</table>

<p><strong>③ 루프당 하나의 작업(One Item Per Loop)</strong>
컨텍스트 윈도우를 보존하기 위해 한 번의 반복에서 <strong>하나의 작업만</strong> 수행합니다.</p>

<p><strong>④ Backpressure(역압)</strong>
에이전트의 출력을 검증하고 수정을 강제하는 메커니즘입니다. 타입 시스템, 테스트, 린터, 보안 스캐너 등이 이에 해당하며, 이 검증 휠이 빠르게 돌수록 더 많은 반복이 가능합니다.</p>

<p><strong>⑤ Planning Mode vs Build Mode</strong>
Planning Mode에서 모든 사양을 읽고 <code class="language-plaintext highlighter-rouge">fix_plan.md</code>를 생성한 뒤, Build Mode에서 항목을 하나씩 구현하는 린(lean) 루프를 반복합니다.</p>

<hr />

<h2 id="3-두-개념의-관계">3. 두 개념의 관계</h2>

<h3 id="계층적-관계-ralph-loop는-agent-harness-위에서-동작한다">계층적 관계: Ralph Loop는 Agent Harness 위에서 동작한다</h3>

<p>Ralph Loop와 Agent Harness는 <strong>경쟁 관계가 아니라 상호 보완적인 계층 관계</strong>입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────────┐
│  Ralph Loop (외부 오케스트레이션 - bash while loop)           │
│  ┌──────────┐   ┌──────────┐   ┌──────────┐               │
│  │ Session 1 │──▶│ Session 2 │──▶│ Session 3 │──▶ ...      │
│  │┌────────┐│   │┌────────┐│   │┌────────┐│               │
│  ││ Agent  ││   ││ Agent  ││   ││ Agent  ││               │
│  ││Harness ││   ││Harness ││   ││Harness ││               │
│  ││ ┌────┐ ││   ││ ┌────┐ ││   ││ ┌────┐ ││               │
│  ││ │LLM │ ││   ││ │LLM │ ││   ││ │LLM │ ││               │
│  ││ │+Tool│ ││   ││ │+Tool│ ││   ││ │+Tool│ ││               │
│  ││ └────┘ ││   ││ └────┘ ││   ││ └────┘ ││               │
│  │└────────┘│   │└────────┘│   │└────────┘│               │
│  └──────────┘   └──────────┘   └──────────┘               │
│        ❌ context        ❌ context                         │
│        ✅ files          ✅ files                           │
├─────────────────────────────────────────────────────────────┤
│  [영속 계층] Git commits │ fix_plan.md │ specs/* │ AGENT.md │
└─────────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p>ZeroSync의 용어집이 이 관계를 명확하게 정리합니다: <em>“Agent Harness: LLM API 호출을 감싸는 프로그램. 하네스는 세션, 도구, 오케스트레이션을 관리한다. ‘같은 에이전트’라 할 때, 같은 하네스를 의미하지만 매 루프는 새로운 세션을 생성한다”</em>.</p>

<table>
  <thead>
    <tr>
      <th>관점</th>
      <th>Agent Harness</th>
      <th>Ralph Loop</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>본질</strong></td>
      <td>인프라/소프트웨어 시스템</td>
      <td>오케스트레이션 패턴/기법</td>
    </tr>
    <tr>
      <td><strong>범위</strong></td>
      <td>단일 세션 내부</td>
      <td>세션 간(cross-session)</td>
    </tr>
    <tr>
      <td><strong>컨텍스트</strong></td>
      <td>세션 내 컨텍스트 관리·압축</td>
      <td>매 반복 fresh context 보장</td>
    </tr>
    <tr>
      <td><strong>메모리</strong></td>
      <td>세션 내 메모리 관리</td>
      <td>파일 기반 외부 메모리 (git, MD 파일)</td>
    </tr>
    <tr>
      <td><strong>도구</strong></td>
      <td>도구 제공·실행·결과 반환</td>
      <td>도구를 직접 제공하지 않음</td>
    </tr>
    <tr>
      <td><strong>구현</strong></td>
      <td>SDK, 라이브러리</td>
      <td>bash while loop (단순)</td>
    </tr>
    <tr>
      <td><strong>비유</strong></td>
      <td>자동차 전체 (핸들, 브레이크, 내비)</td>
      <td>운행 스케줄 (매일 새 운전자, 운행일지 인수인계)</td>
    </tr>
  </tbody>
</table>

<h3 id="anthropic의-사례-두-개념의-실제-결합">Anthropic의 사례: 두 개념의 실제 결합</h3>

<p>Anthropic은 Claude Agent SDK(Agent Harness)를 활용하면서, 사실상 Ralph Loop와 동일한 패턴을 공식화했습니다:</p>

<ul>
  <li><strong>Initializer Agent</strong> = Ralph의 Planning Mode → 환경 설정, 기능 목록(feature_list.json) 작성, init.sh 스크립트 생성</li>
  <li><strong>Coding Agent</strong> = Ralph의 Build Mode → 매 세션 <code class="language-plaintext highlighter-rouge">claude-progress.txt</code>와 git log를 읽고, 하나의 기능 구현 후 커밋</li>
  <li><code class="language-plaintext highlighter-rouge">feature_list.json</code> = Ralph의 <code class="language-plaintext highlighter-rouge">specs/*</code>, <code class="language-plaintext highlighter-rouge">claude-progress.txt</code> = Ralph의 <code class="language-plaintext highlighter-rouge">fix_plan.md</code></li>
</ul>

<p>이 접근법으로 발견한 핵심 교훈은 Ralph Loop의 원칙과 정확히 일치합니다: <strong>한 번에 하나의 기능만 구현</strong>, <strong>매 세션 클린 상태로 종료</strong>, <strong>진행 파일과 git으로 다음 세션에 인수인계</strong>.</p>

<hr />

<h2 id="4-활용-방안">4. 활용 방안</h2>

<h3 id="방안-1-대규모-소프트웨어-프로젝트-자율-개발">방안 1: 대규모 소프트웨어 프로젝트 자율 개발</h3>

<p><a href="https://ghuntley.com/ralph/">Geoffrey Huntley</a>는 Ralph Loop로 <strong>CURSED</strong>라는 프로그래밍 언어를 3개월간 자율 운영하여 완성했습니다. LLM 학습 데이터에 없는 언어의 컴파일러(렉서, 파서, LLVM 코드젠, 표준 라이브러리)를 구축한 사례입니다. Y Combinator 해커톤에서는 6개 저장소를 하룻밤에 배포하고, 약 $297의 API 비용으로 $50,000 상당의 작업을 완료했습니다.</p>

<p><strong>구현 방법:</strong></p>
<ul>
  <li>Agent Harness(Claude Agent SDK 또는 DeepAgents)로 코드 실행, 웹 검색, 파일 시스템 접근 제공</li>
  <li>Ralph Loop으로 외부 bash 루프를 돌리며 매 세션 fresh context 보장</li>
  <li><code class="language-plaintext highlighter-rouge">specs/</code> 디렉토리에 기능별 사양서를 작성하고, <code class="language-plaintext highlighter-rouge">fix_plan.md</code>로 진행 상황 추적</li>
</ul>

<h3 id="방안-2-iacinfrastructure-as-code-자동화">방안 2: IaC(Infrastructure as Code) 자동화</h3>

<p>클라우드 인프라 코드(Terraform, Pulumi 등)를 Ralph Loop로 반복 개선할 수 있습니다.</p>

<ul>
  <li><strong>Planning Mode:</strong> 아키텍처 요구사항을 specs로 작성 → 인프라 구현 계획 생성</li>
  <li><strong>Build Mode:</strong> 매 반복 하나의 리소스(VPC, 서브넷, GKE 클러스터 등) 구현 → <code class="language-plaintext highlighter-rouge">terraform validate</code> + <code class="language-plaintext highlighter-rouge">terraform plan</code>을 backpressure로 활용</li>
  <li>Agent Harness가 클라우드 API 문서 검색, 코드 실행, 보안 스캔 도구 제공</li>
</ul>

<h3 id="방안-3-cicd-파이프라인과-통합한-지속적-코드-품질-개선">방안 3: CI/CD 파이프라인과 통합한 지속적 코드 품질 개선</h3>

<p>Ralph Loop의 backpressure 메커니즘을 기존 CI/CD와 연결하면 강력한 자동화가 가능합니다.</p>

<ul>
  <li>테스트 실패 → Ralph가 자동으로 수정 시도 → 재테스트 → 통과 시 커밋</li>
  <li>린터/타입체커 경고 → 자동 수정 루프</li>
  <li>보안 스캐너 발견 사항 → 자동 패치 생성</li>
</ul>

<h3 id="방안-4-멀티-에이전트-병렬-처리">방안 4: 멀티 에이전트 병렬 처리</h3>

<p>Ralph Loop는 두 가지 형태의 병렬성을 지원합니다:</p>

<ul>
  <li><strong>Subagent(세션 내 병렬):</strong> 메인 Ralph의 컨텍스트 윈도우를 소비하지 않는 읽기 전용 하위 프로세스 (파일 검색, 테스트 실행, 문서 업데이트)</li>
  <li><strong>Multiple Loops(세션 간 병렬):</strong> VM, 컨테이너, git worktree로 격리된 독립 Ralph 루프를 동시 실행</li>
</ul>

<p>이를 활용하면 마이크로서비스 아키텍처의 각 서비스를 별도 Ralph Loop으로 동시 개발할 수 있습니다.</p>

<h3 id="방안-5-비코딩-영역으로의-확장">방안 5: 비코딩 영역으로의 확장</h3>

<p>이 패턴은 코딩을 넘어 <strong>과학 연구, 금융 모델링, 기술 문서 작성</strong> 등 장기 실행이 필요한 모든 에이전트 작업에 일반화할 수 있습니다.</p>

<ul>
  <li><strong>리서치 에이전트:</strong> specs에 연구 질문 정의 → 매 반복 하나의 논문/데이터소스 분석 → fix_plan.md에 발견 사항 누적</li>
  <li><strong>투자 분석 에이전트:</strong> specs에 분석 프레임워크 정의 → 매 반복 하나의 기업/지표 분석 → 최종 보고서 자동 생성</li>
</ul>

<hr />

<h2 id="5-part-1-핵심-요약">5. Part 1 핵심 요약</h2>

<p><img src="/assets/image/posts/agent-harness-w-ralph.png" alt="Agent Harness와 Ralph Loop 관계도" /></p>

<p>Agent Harness는 개별 세션의 <strong>“품질”</strong> 을 보장하고, Ralph Loop는 세션 간의 <strong>“연속성과 진행”</strong> 을 보장합니다. 이 둘은 서로 다른 계층에서 동작하는 보완적 개념입니다.</p>

<blockquote>
  <p><em>“오케스트레이션은 작전의 두뇌이고, 하네스는 손과 인프라다. 둘 다 복잡한 AI 에이전트에 필수적이다.”</em> — Parallel AI</p>
</blockquote>

<blockquote>
  <p><em>“Ralph는 결정론적으로 나쁘다, 비결정론적 세계에서. Ralph의 실패는 예측 가능한 패턴을 따르며, 이 한계의 예측 가능성이야말로 유용한 이유다.”</em> — Geoffrey Huntley</p>
</blockquote>

<p>가장 강력한 활용은 이 두 개념을 <strong>의도적으로 분리하되 함께 사용하는 것</strong>입니다. Agent Harness 안에서 Ralph를 구현하면 Fresh Context라는 핵심 원칙이 깨지고, Ralph 없이 Harness만 사용하면 장기 작업에서 컨텍스트 부패와 조기 완료 선언이라는 실패 모드에 빠집니다. 둘의 올바른 결합이 신뢰할 수 있는 장기 자율 AI 작업의 핵심 요소입니다.</p>

<h3 id="references">References</h3>
<ul>
  <li>[1] What is an agent harness in the context of large-language models? https://parallel.ai/articles/what-is-an-agent-harness</li>
  <li>[2] Agent vs Harness: What’s the Difference? https://ezz.sh/posts/agent_vs_harness</li>
  <li>[3] Agent Frameworks, Runtimes, and Harnesses- oh my! https://blog.langchain.com/agent-frameworks-runtimes-and-harnesses-oh-my/</li>
  <li>[4] The importance of Agent Harness in 2026 https://www.philschmid.de/agent-harness-2026</li>
  <li>[5] Effective harnesses for long-running agents https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents</li>
  <li>[6] The Ralph Loop: Long-Running AI Agents http://www.zerosync.co/blog/ralph-loop-technical-deep-dive</li>
  <li>[7] Ralph Wiggum - Viral Agentic Coding Loop, Simplified https://ralph-wiggum.ai</li>
  <li>[8] ralph-loop-agent - GitHub https://github.com/vercel-labs/ralph-loop-agent</li>
  <li>[9] Ralph Loop Revolutionizes AI Coding https://www.linkedin.com/posts/rakeshgohel01_have-you-ever-tried-using-coding-ai-agents-activity-7414649482870665217-O6LF</li>
  <li>[11] Ralph Mode for Deep Agents: Running an Agent Forever https://www.youtube.com/watch?v=yi4XNKcUS8Q</li>
  <li>[12] Ralph Loop, OpenClaw - 새로운건 없었다 https://channel.io/ko/team/blog/articles/tech-ralph-loop-openclaw-9a2e654c</li>
  <li>[13] 코딩 에이전트의 결과물을 개선하는 Ralph 방법론 https://discuss.pytorch.kr/t/ralph-playbook-ralph/8705</li>
  <li>[14] truly the ultimate ralph loop explainer https://x.com/round/status/2011926496730886407</li>
  <li>[15] Understanding Agent Frameworks, Runtimes &amp; Harnesses https://www.youtube.com/watch?v=inVO14Tabn4</li>
  <li>[16] WTF is a Ralph Loop? https://www.youtube.com/watch?v=2ItHHAKO4T0</li>
</ul>

<hr />

<h2 id="part-2-google-cloud-기반-agent-harness-구현-방안">Part 2. Google Cloud 기반 Agent Harness 구현 방안</h2>

<h3 id="개요-google-cloud-서비스-매핑">개요: Google Cloud 서비스 매핑</h3>

<p>이제 Agent Harness와 Ralph Loop의 개념적 정의를 바탕으로, 이를 Google Cloud 환경에서 실제로 어떻게 구성하고 구현하는지 살펴보겠습니다.</p>

<p>Agent Harness는 LLM 에이전트를 감싸는 인프라 시스템으로, 도구 통합·컨텍스트 관리·메모리·보안·오케스트레이션을 제공합니다. Google Cloud는 이를 구현하기 위한 포괄적인 서비스 스택을 보유하고 있으며, 핵심 축은 <strong>ADK(Agent Development Kit)</strong> + <strong>Vertex AI Agent Engine</strong> + <strong>MCP/A2A 프로토콜</strong> 조합입니다.</p>

<p>Google Cloud의 Agent Harness는 8개 계층으로 구성됩니다. 아래에서 각 계층을 상세히 검토합니다.</p>

<p><img src="/assets/image/posts/gcp-agent-harness-layers.png" alt="Google Cloud Agent Harness 8계층 구조" /></p>

<hr />

<h2 id="1계층-google-adk-agent-development-framework">1계층: Google ADK (Agent Development Framework)</h2>

<p><strong>ADK(Agent Development Kit)</strong> 는 Google이 제공하는 오픈소스 에이전트 개발 프레임워크로, 누적 700만+ 다운로드를 기록하고 있습니다. Agent Harness를 코드로 정의하는 핵심 도구입니다.</p>

<h3 id="핵심-특성">핵심 특성</h3>

<ul>
  <li><strong>Code-first</strong>: 에이전트 로직, 도구, 오케스트레이션을 코드로 직접 정의하여 테스트·버전관리 용이</li>
  <li><strong>Model-agnostic</strong>: Gemini 최적화이나 다른 LLM도 지원</li>
  <li><strong>Deployment-agnostic</strong>: Agent Engine, Cloud Run, GKE 어디든 배포 가능</li>
  <li><strong>Multi-language</strong>: Python, TypeScript, Go, Java 지원</li>
</ul>

<h3 id="adk-프로젝트-구조">ADK 프로젝트 구조</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>parent_folder/
├── requirements.txt          # google-adk
└── my_agent/
    ├── __init__.py            # from . import agent
    ├── agent.py               # root_agent 정의
    └── .env                   # API keys, PROJECT_ID
</code></pre></div></div>

<h3 id="adk-핵심-추상화">ADK 핵심 추상화</h3>

<table>
  <thead>
    <tr>
      <th>개념</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Agent</strong></td>
      <td>LLM + 지시사항 + 도구 + 서브에이전트를 하나로 묶는 단위</td>
    </tr>
    <tr>
      <td><strong>Session</strong></td>
      <td>사용자와 에이전트 간 대화 스레드</td>
    </tr>
    <tr>
      <td><strong>Event</strong></td>
      <td>세션 내 개별 상호작용 (메시지, 도구 호출 등)</td>
    </tr>
    <tr>
      <td><strong>State</strong></td>
      <td>세션 내 key-value 스크래치패드</td>
    </tr>
    <tr>
      <td><strong>Runner</strong></td>
      <td>에이전트를 세션 컨텍스트에서 실행하는 엔진</td>
    </tr>
  </tbody>
</table>

<p>State에는 네 가지 스코프가 존재합니다:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">state["key"]</code> — 세션 스코프 (현재 대화만)</li>
  <li><code class="language-plaintext highlighter-rouge">state["user:key"]</code> — 유저 스코프 (동일 유저의 모든 세션에서 공유)</li>
  <li><code class="language-plaintext highlighter-rouge">state["app:key"]</code> — 앱 스코프 (전체 사용자 공유)</li>
  <li><code class="language-plaintext highlighter-rouge">state["temp:key"]</code> — 임시 (턴 종료 시 삭제)</li>
</ul>

<hr />

<h2 id="2계층-agent-runtime--실행-환경-선택">2계층: Agent Runtime — 실행 환경 선택</h2>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>Vertex AI Agent Engine ⭐</th>
      <th>Cloud Run</th>
      <th>GKE</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>관리 오버헤드</strong></td>
      <td>최소 (fully managed)</td>
      <td>낮음 (serverless)</td>
      <td>높음 (클러스터 관리)</td>
    </tr>
    <tr>
      <td><strong>언어 지원</strong></td>
      <td>Python 전용</td>
      <td>Any (컨테이너)</td>
      <td>Any (컨테이너)</td>
    </tr>
    <tr>
      <td><strong>내장 메모리</strong></td>
      <td>✅ (세션, Memory Bank)</td>
      <td>❌ (외부 필요)</td>
      <td>❌ (외부 필요)</td>
    </tr>
    <tr>
      <td><strong>자동 스케일링</strong></td>
      <td>✅ (Cloud Run 기반)</td>
      <td>✅ (0까지 축소)</td>
      <td>✅ (노드풀 단위)</td>
    </tr>
    <tr>
      <td><strong>MCP 서버 호스팅</strong></td>
      <td>❌</td>
      <td>✅</td>
      <td>✅</td>
    </tr>
    <tr>
      <td><strong>배포 방법</strong></td>
      <td><code class="language-plaintext highlighter-rouge">adk deploy cloud_run</code> 또는 SDK</td>
      <td><code class="language-plaintext highlighter-rouge">gcloud run deploy</code></td>
      <td><code class="language-plaintext highlighter-rouge">kubectl apply</code></td>
    </tr>
    <tr>
      <td><strong>추천 시나리오</strong></td>
      <td>Python 에이전트, 빠른 프로덕션</td>
      <td>다국어, 서버리스, 비용 최적화</td>
      <td>복잡한 stateful, 보안 격리</td>
    </tr>
  </tbody>
</table>

<h3 id="배포-명령-예시">배포 명령 예시</h3>

<p><strong>Agent Engine 배포 (SDK)</strong>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">vertexai</span> <span class="kn">import</span> <span class="n">agent_engines</span>
<span class="n">agent_engine</span> <span class="o">=</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">my_adk_app</span><span class="p">,</span>
    <span class="n">requirements</span><span class="o">=</span><span class="p">[</span><span class="s">"google-adk"</span><span class="p">],</span>
    <span class="n">display_name</span><span class="o">=</span><span class="s">"my-agent"</span>
<span class="p">)</span>
</code></pre></div></div>

<p><strong>Cloud Run 배포 (CLI)</strong>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adk deploy cloud_run <span class="se">\</span>
  <span class="nt">--project</span><span class="o">=</span><span class="nv">$PROJECT_ID</span> <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
  <span class="nt">--with-ui</span>
</code></pre></div></div>

<p><strong>권장 선택:</strong> 대부분의 경우 <strong>Vertex AI Agent Engine</strong>을 우선 고려합니다. Python 에이전트에 최적화되어 있으며, 세션 관리·메모리·스케일링·보안이 내장되어 운영 부담이 최소화됩니다.</p>

<hr />

<h2 id="3계층-ai-models--추론-엔진">3계층: AI Models — 추론 엔진</h2>

<table>
  <thead>
    <tr>
      <th>모델</th>
      <th>입력 토큰 ($/1M)</th>
      <th>출력 토큰 ($/1M)</th>
      <th>추천 용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Gemini 3 Pro</strong></td>
      <td>$2 (≤200K) / $4 (&gt;200K)</td>
      <td>$12 (≤200K) / $18 (&gt;200K)</td>
      <td>복잡한 추론, 멀티스텝 계획</td>
    </tr>
    <tr>
      <td><strong>Gemini 3 Flash</strong></td>
      <td>$0.50</td>
      <td>$3</td>
      <td>일반 작업, 비용 최적화</td>
    </tr>
    <tr>
      <td><strong>Gemini 2.5 Pro</strong></td>
      <td>$1.25 (≤200K) / $2.25 (&gt;200K)</td>
      <td>$10 (≤200K) / $15 (&gt;200K)</td>
      <td>복잡한 추론, 멀티스텝 계획</td>
    </tr>
    <tr>
      <td><strong>Gemini 2.5 Flash</strong></td>
      <td>$0.30</td>
      <td>$2.5</td>
      <td>일반 작업, 비용 최적화</td>
    </tr>
    <tr>
      <td><strong>Open Models</strong> (Gemma 등)</td>
      <td>Cloud Run/GKE 컴퓨트 비용</td>
      <td>-</td>
      <td>Data Residency, Custom Fine-tuning</td>
    </tr>
  </tbody>
</table>

<p><strong>Model Routing 전략:</strong></p>
<ul>
  <li>간단한 요청 (분류, 번역) → Gemini Flash 또는 SLM</li>
  <li>복잡한 추론 (다단계 계획, 코드 생성) → Gemini Pro</li>
  <li>특수 보안/데이터 요건 → Self-hosted open model on GKE</li>
</ul>

<hr />

<h2 id="4계층-tool-integration--에이전트의-손">4계층: Tool Integration — 에이전트의 손</h2>

<h3 id="a-built-in-tools">(A) Built-in Tools</h3>
<p>ADK에 내장된 도구들로, 별도 설정 없이 즉시 사용 가능합니다:</p>
<ul>
  <li><strong>Google Search</strong>: 실시간 웹 정보 접근</li>
  <li><strong>Code Execution</strong>: 보안 샌드박스에서 코드 실행</li>
  <li><strong>RAG</strong>: 엔터프라이즈 데이터에서 정보 검색</li>
  <li><strong>Database Query</strong>: 클라우드 DB 구조화 데이터 접근</li>
</ul>

<h3 id="b-mcp-서버-cloud-run-호스팅">(B) MCP 서버 (Cloud Run 호스팅)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud run deploy mcp-server <span class="se">\</span>
  <span class="nt">--image</span> us-central1-docker.pkg.dev/<span class="nv">$PROJECT_ID</span>/mcp-servers/my-server:latest <span class="se">\</span>
  <span class="nt">--region</span><span class="o">=</span>us-central1 <span class="se">\</span>
  <span class="nt">--no-allow-unauthenticated</span>
</code></pre></div></div>

<h3 id="통신-프로토콜-요약">통신 프로토콜 요약</h3>

<table>
  <thead>
    <tr>
      <th>프로토콜</th>
      <th>용도</th>
      <th>표준</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>MCP</strong></td>
      <td>에이전트 ↔ 도구</td>
      <td>Open standard (Anthropic 주도)</td>
    </tr>
    <tr>
      <td><strong>A2A</strong></td>
      <td>에이전트 ↔ 에이전트</td>
      <td>Open standard (Google 주도)</td>
    </tr>
    <tr>
      <td><strong>AG-UI</strong></td>
      <td>에이전트 ↔ 프론트엔드</td>
      <td>Open standard</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="5계층-memory--상태-영속성">5계층: Memory — 상태 영속성</h2>

<h3 id="short-term-memory">Short-term Memory</h3>

<table>
  <thead>
    <tr>
      <th>구현 옵션</th>
      <th>환경</th>
      <th>특성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InMemorySessionService</code></td>
      <td>개발/테스트</td>
      <td>인스턴스 재시작 시 소실</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DatabaseSessionService</code> (Cloud SQL)</td>
      <td>프로덕션</td>
      <td>수평 스케일링 가능, ADK 네이티브</td>
    </tr>
    <tr>
      <td>Firestore</td>
      <td>프로덕션</td>
      <td>서버리스, 자동 스케일링</td>
    </tr>
    <tr>
      <td>Memorystore (Redis)</td>
      <td>프로덕션</td>
      <td>초저지연, 캐시 적합</td>
    </tr>
    <tr>
      <td>Agent Engine Sessions</td>
      <td>프로덕션</td>
      <td>Agent Engine에 내장, 관리 불필요</td>
    </tr>
  </tbody>
</table>

<p><strong>핵심 설계 원칙:</strong> 프로덕션에서는 반드시 <strong>Stateless Agent Application + 외부 상태 저장소</strong> 패턴을 사용합니다.</p>

<h3 id="long-term-memory">Long-term Memory</h3>

<ul>
  <li><strong>Memory Bank</strong>: Google Cloud의 관리형 장기 메모리 서비스</li>
  <li><strong>Firestore / AlloyDB</strong>: 커스텀 지식 베이스 구축 시</li>
  <li><strong>Vertex AI Search</strong>: RAG 기반 엔터프라이즈 지식 검색</li>
</ul>

<hr />

<h2 id="6계층-security--governance--안전장치">6계층: Security &amp; Governance — 안전장치</h2>

<table>
  <thead>
    <tr>
      <th>서비스</th>
      <th>역할</th>
      <th>적용 범위</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Model Armor</strong></td>
      <td>프롬프트 인젝션 차단, 유해 콘텐츠 필터링, 민감 데이터 유출 방지</td>
      <td>Vertex AI, GKE</td>
    </tr>
    <tr>
      <td><strong>Agent Identity (IAM)</strong></td>
      <td>에이전트별 고유 Cloud IAM ID 부여, 최소 권한 원칙 적용</td>
      <td>모든 런타임</td>
    </tr>
    <tr>
      <td><strong>VPC Service Controls</strong></td>
      <td>네트워크 수준 격리, 데이터 유출 방지</td>
      <td>엔터프라이즈</td>
    </tr>
    <tr>
      <td><strong>Secret Manager</strong></td>
      <td>API 키, 자격증명 안전 저장</td>
      <td>모든 런타임</td>
    </tr>
    <tr>
      <td><strong>GKE Sandbox</strong></td>
      <td>신뢰할 수 없는 코드 격리 실행</td>
      <td>GKE</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="7계층-observability--모니터링--디버깅">7계층: Observability — 모니터링 &amp; 디버깅</h2>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>기능</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Cloud Trace</strong></td>
      <td>에이전트 워크플로우 내 에러 추적, 지연 시간 분석</td>
    </tr>
    <tr>
      <td><strong>Cloud Logging</strong></td>
      <td>에이전트 로그 중앙 수집</td>
    </tr>
    <tr>
      <td><strong>Cloud Monitoring</strong></td>
      <td>메트릭 대시보드, 알림 설정</td>
    </tr>
    <tr>
      <td><strong>Agent Engine Dashboard</strong></td>
      <td>토큰 사용량, 지연 시간, 에러율 실시간 추적</td>
    </tr>
    <tr>
      <td><strong>Managed Prometheus</strong> (GKE)</td>
      <td>서드파티·커스텀 메트릭 수집</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="8계층-multi-agent-패턴--고급-구성">8계층: Multi-Agent 패턴 — 고급 구성</h2>

<h3 id="지원되는-디자인-패턴">지원되는 디자인 패턴</h3>

<ul>
  <li><strong>Sequential Pattern</strong>: 에이전트 A → 에이전트 B → 에이전트 C 순차 실행</li>
  <li><strong>Iterative Refinement</strong>: 작업 에이전트 → 품질 평가 에이전트 → 프롬프트 개선 → 재시도</li>
  <li><strong>Coordinator Pattern</strong>: 코디네이터 에이전트가 전문 서브에이전트에 위임</li>
  <li><strong>Human-in-the-Loop(HITL)</strong>: 중요 결정에서 인간 개입 경로 제공</li>
</ul>

<h3 id="실제-활용-사례">실제 활용 사례</h3>

<table>
  <thead>
    <tr>
      <th>유스케이스</th>
      <th>에이전트 구성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>금융 어드바이저</strong></td>
      <td>데이터 수집 → 분석 → 추천 → 거래 실행 (Sequential)</td>
    </tr>
    <tr>
      <td><strong>리서치 어시스턴트</strong></td>
      <td>계획 → 조사 → 평가 → 보고서 작성 (Sequential + Iterative)</td>
    </tr>
    <tr>
      <td><strong>공급망 최적화</strong></td>
      <td>재고관리 + 배송추적 + 공급자 커뮤니케이션 (Coordinator)</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="실전-구성-시나리오-3-tier-접근">실전 구성 시나리오: 3-Tier 접근</h2>

<h3 id="tier-1-프로토타이핑-비용-최소화">Tier 1: 프로토타이핑 (비용 최소화)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADK (Python) + Gemini Flash
├── InMemorySessionService
├── Built-in Tools (Google Search)
├── ADK Dev UI (local: adk web)
└── 배포: adk deploy cloud_run --with-ui
</code></pre></div></div>
<ul>
  <li><strong>비용</strong>: Gemini API 사용량만 (pay-per-use)</li>
  <li><strong>소요 시간</strong>: 30분 이내 구축 가능</li>
  <li><strong>적합</strong>: PoC, 내부 데모, 해커톤</li>
</ul>

<h3 id="tier-2-프로덕션-단일-에이전트">Tier 2: 프로덕션 단일 에이전트</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADK (Python) + Gemini Pro (추론) / Flash (단순작업)
├── Vertex AI Agent Engine Runtime
├── Agent Engine Sessions (단기) + Memory Bank (장기)
├── MCP 서버 on Cloud Run (커스텀 도구)
├── Model Armor + Agent Identity (IAM)
├── Cloud Trace + Monitoring
└── 배포: agent_engines.create() via SDK
</code></pre></div></div>
<ul>
  <li><strong>비용</strong>: Agent Engine ~$0.0864/vCPU-hr + 모델 사용량</li>
  <li><strong>적합</strong>: 고객 서비스 봇, 내부 업무 자동화, RAG 어시스턴트</li>
</ul>

<h3 id="tier-3-엔터프라이즈-멀티에이전트">Tier 3: 엔터프라이즈 멀티에이전트</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ADK + A2A + MCP
├── Coordinator Agent + 전문 Subagents
├── GKE 또는 Agent Engine (하이브리드)
├── Apigee API Hub (엔터프라이즈 API 관리)
├── AlloyDB/Firestore (메모리) + Vertex AI Search (RAG)
├── Model Routing (Pro + Flash + Open Models)
├── Full Governance: Model Armor + IAM + VPC SC + SCC
├── Cloud Trace + Logging + Monitoring + Prometheus
└── 배포: Terraform/Pulumi IaC
</code></pre></div></div>
<ul>
  <li><strong>적합</strong>: 금융 분석 시스템, 공급망 자동화, 멀티부서 에이전트 플랫폼</li>
</ul>

<hr />

<h2 id="비용-참고-2026년-2월-기준">비용 참고 (2026년 2월 기준)</h2>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>가격</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Agent Engine Runtime (vCPU, &gt;50hours)</td>
      <td>$0.0864/vCPU-hour</td>
      <td>50 vCPU-hours 초과시</td>
    </tr>
    <tr>
      <td>Agent Engine Runtime (Memory, &gt;100GiB)</td>
      <td>$0.0090/GiB-hour</td>
      <td>100 GiB-hours 초과 시</td>
    </tr>
    <tr>
      <td>Gemini 2.5 Flash (Input)</td>
      <td>$0.30/1M tokens</td>
      <td> </td>
    </tr>
    <tr>
      <td>Gemini 2.5 Flash (Output)</td>
      <td>$2.5/1M tokens</td>
      <td> </td>
    </tr>
    <tr>
      <td>Gemini 2.5 Pro (Input, ≤200K)</td>
      <td>$1.25/1M tokens</td>
      <td> </td>
    </tr>
    <tr>
      <td>Gemini 2.5 Pro (Output, ≤200K)</td>
      <td>$10/1M tokens</td>
      <td> </td>
    </tr>
    <tr>
      <td>Grounding with Google Search</td>
      <td>$14/1,000 requests</td>
      <td> </td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="ralph-loop-연동-포인트">Ralph Loop 연동 포인트</h2>

<p>Part 1에서 다룬 Ralph Loop를 Agent Harness on Google Cloud와 결합하면 <strong>장기 실행 자율 에이전트</strong>를 구현할 수 있습니다. 핵심은 Ralph Loop이 Agent Harness <strong>외부</strong>에서 동작해야 한다는 점입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Cloud Scheduler / Cloud Build Trigger]
    │
    ▼ (주기적 또는 이벤트 기반 트리거)
[Cloud Run Job: Ralph Outer Loop]
    │
    ├── Iteration 1 → [Agent Engine: ADK Agent] → git commit
    ├── Iteration 2 → [Agent Engine: ADK Agent] → git commit
    ├── Iteration 3 → [Agent Engine: ADK Agent] → git commit
    └── ...
    │
[Cloud Source Repos / GitHub: fix_plan.md, specs/*, AGENT.md]
</code></pre></div></div>

<ul>
  <li><strong>Cloud Scheduler</strong>: Ralph Loop의 외부 트리거</li>
  <li><strong>Cloud Run Jobs</strong>: 각 반복의 bash loop 실행</li>
  <li><strong>Agent Engine</strong>: 각 세션의 Agent Harness (fresh context)</li>
  <li><strong>Cloud Source Repositories / GitHub</strong>: 파일 기반 외부 메모리 (영속 계층)</li>
</ul>

<p>이 구성으로 Anthropic이 시연한 <strong>Initializer Agent + Coding Agent</strong> 패턴을 GCP 네이티브 서비스로 완전히 재현할 수 있으며, Vertex AI의 관리형 인프라 위에서 비용 효율적으로 운영할 수 있습니다.</p>

<h3 id="references-part-2">References (Part 2)</h3>
<ul>
  <li>[1] google/adk-docs https://github.com/google/adk-docs</li>
  <li>[2] Choose your agentic AI architecture components https://docs.cloud.google.com/architecture/choose-agentic-ai-architecture-components</li>
  <li>[3] Index - Agent Development Kit https://google.github.io/adk-docs/</li>
  <li>[4] Run your agent https://docs.cloud.google.com/run/docs/ai/build-and-deploy-ai-agents/deploy-adk-agent</li>
  <li>[5] ADK - Session, State &amp; Memory https://arjunprabhulal.com/adk-sessions-state/</li>
  <li>[6] Overview of Agent Development Kit https://docs.cloud.google.com/agent-builder/agent-development-kit/overview</li>
  <li>[7] Vertex AI Pricing https://cloud.google.com/vertex-ai/generative-ai/pricing</li>
  <li>[8] Build and deploy a remote MCP server on Cloud Run https://cloud.google.com/run/docs/tutorials/deploy-remote-mcp-server</li>
  <li>[9] Remember this: Agent state and memory with ADK https://cloud.google.com/blog/topics/developers-practitioners/remember-this-agent-state-and-memory-with-adk</li>
  <li>[10] Multi-agent AI system in Google Cloud https://docs.cloud.google.com/architecture/multiagent-ai-system</li>
  <li>[11] Choose a design pattern for your agentic AI system https://docs.cloud.google.com/architecture/choose-design-pattern-agentic-ai-system</li>
  <li>[12] Deploy to Vertex AI Agent Engine https://google.github.io/adk-docs/deploy/agent-engine/</li>
  <li>[13] Effective harnesses for long-running agents https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents</li>
</ul>]]></content><author><name></name></author><category term="ai" /><category term="AI" /><category term="Agent" /><category term="GCP" /><category term="Google Cloud" /><category term="ADK" /><category term="Vertex AI" /><summary type="html"><![CDATA[Agent Harness와 Ralph Loop의 개념적 정의부터 Google Cloud 환경에서의 실제 구현 방안까지 다룬 종합 가이드]]></summary></entry><entry><title type="html">Gemini CLI &amp;amp; Antigravity IDE 설정 완전 가이드</title><link href="https://hajekim.github.io/ai/2026/02/12/gemini-cli-antigravity-guide/" rel="alternate" type="text/html" title="Gemini CLI &amp;amp; Antigravity IDE 설정 완전 가이드" /><published>2026-02-12T00:00:00+09:00</published><updated>2026-02-12T00:00:00+09:00</updated><id>https://hajekim.github.io/ai/2026/02/12/gemini-cli-antigravity-guide</id><content type="html" xml:base="https://hajekim.github.io/ai/2026/02/12/gemini-cli-antigravity-guide/"><![CDATA[<blockquote>
  <p><strong>작성일:</strong> 2026-02-12
<strong>대상:</strong> Gemini CLI (v0.3.x+), Google Antigravity IDE
<strong>목적:</strong> 양 플랫폼의 설정 체계·인증·MCP·Agent Skills·세션 관리 등의 상세 구조 분석 및 통합 활용 가이드</p>
</blockquote>

<hr />

<h2 id="목차">목차</h2>

<ol>
  <li><a href="#1-개요">개요</a></li>
  <li><a href="#2-geminimd---컨텍스트-및-메모리-파일">GEMINI.md - 컨텍스트 및 메모리 파일</a></li>
  <li><a href="#3-settingsjson---동작-설정-및-에이전트-모드">settings.json - 동작 설정 및 에이전트 모드</a></li>
  <li><a href="#4-auth-설정---인증-체계">Auth 설정 - 인증 체계</a></li>
  <li><a href="#5-env-설정---환경변수-관리">.env 설정 - 환경변수 관리</a></li>
  <li><a href="#6-rules---항상-적용되는-가이드라인">Rules - 항상 적용되는 가이드라인</a></li>
  <li><a href="#7-workflows---사용자-트리거-매크로">Workflows - 사용자 트리거 매크로</a></li>
  <li><a href="#8-agent-skills---핵심-상호호환-계층">Agent Skills - 핵심 상호호환 계층</a></li>
  <li><a href="#9-mcp---외부-통합">MCP - 외부 통합</a></li>
  <li><a href="#10-agent-아키텍처-및-특수-에이전트">Agent 아키텍처 및 특수 에이전트</a></li>
  <li><a href="#11-세션-및-작업-관리">세션 및 작업 관리</a></li>
  <li><a href="#12-상호호환성-종합-매핑">상호호환성 종합 매핑</a></li>
  <li><a href="#13-통합-실전-구성-예시-및-팁">통합 실전 구성 예시 및 팁</a></li>
  <li><a href="#14-참고-자료">참고 자료</a></li>
</ol>

<hr />

<h2 id="1-개요">1. 개요</h2>

<p>Gemini CLI는 터미널 기반 AI 에이전트이고, Antigravity는 VSCode 포크 기반의 에이전트 중심 IDE입니다. 두 도구는 동일한 <strong>Gemini 에이전트 코어</strong> 엔진을 공유하며, 핵심적인 도구 시스템과 설정 로직은 호환되지만 사용자의 작업 환경에 따라 제공되는 인터페이스와 관리 방식에서 차이가 있습니다. 핵심은 <strong>Agent Skills라는 오픈 표준(<a href="agentskills.io">agentskills.io</a>)을 통해 양 플랫폼 간 상호호환이 가능하다는 점입니다.</strong></p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Gemini CLI</th>
      <th>Antigravity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>형태</strong></td>
      <td>터미널 CLI 에이전트입니다.</td>
      <td>VSCode 포크 IDE입니다.</td>
    </tr>
    <tr>
      <td><strong>인터페이스</strong></td>
      <td>커맨드라인 (터미널 상호작용 최적화)</td>
      <td>GUI (에디터 + Agent Panel)</td>
    </tr>
    <tr>
      <td><strong>에이전트</strong></td>
      <td>단일 순차 에이전트 (셸 명령, 파일 직접 제어)</td>
      <td>멀티 에이전트 병렬 실행 (시각적 워크플로우)</td>
    </tr>
    <tr>
      <td><strong>모델</strong></td>
      <td>Gemini 2.5 Pro/Flash (Free), API키로 상위 모델</td>
      <td>Gemini 3 모델 (역할별 특화)</td>
    </tr>
    <tr>
      <td><strong>설정 방식</strong></td>
      <td>JSON 파일 + .env + CLI 인수</td>
      <td>IDE UI + JSON 파일</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-geminimd---컨텍스트-및-메모리-파일">2. GEMINI.md - 컨텍스트 및 메모리 파일</h2>

<p>GEMINI.md는 양 플랫폼에서 <strong>항상 로드되는 지속적 컨텍스트 파일</strong>로, 프로젝트별 지시사항·코딩 스타일·아키텍처 규칙을 정의합니다. 에이전트는 대화 시작 시 여러 위치의 파일을 찾아 하나의 시스템 지침으로 결합합니다.</p>

<h3 id="21-계층-구조-비교">2.1 계층 구조 비교</h3>

<table>
  <thead>
    <tr>
      <th>계층</th>
      <th>Gemini CLI</th>
      <th>Antigravity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>전역</strong></td>
      <td><code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code></td>
    </tr>
    <tr>
      <td><strong>프로젝트</strong></td>
      <td>프로젝트 루트 + 상위 디렉토리 (<code class="language-plaintext highlighter-rouge">.git</code>까지)</td>
      <td>전역 규칙으로 적용됩니다.</td>
    </tr>
    <tr>
      <td><strong>서브디렉토리</strong></td>
      <td>하위 디렉토리 GEMINI.md 자동 스캔 (<code class="language-plaintext highlighter-rouge">.gitignore</code> 존중)</td>
      <td>지원하지 않습니다.</td>
    </tr>
    <tr>
      <td><strong>커스텀 이름</strong></td>
      <td><code class="language-plaintext highlighter-rouge">context.fileName</code>으로 변경 가능합니다. (예: <code class="language-plaintext highlighter-rouge">["AGENTS.md", "CONTEXT.md", "GEMINI.md"]</code>)</td>
      <td>GEMINI.md로 고정되어 있습니다.</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>💡 프로젝트 내 GEMINI.md 배치 팁</strong></p>
  <ul>
    <li><strong>표준 위치 (권장)</strong>: 프로젝트 루트 디렉토리에 배치합니다. 에이전트가 별도 설정 없이 즉시 파일을 인식하여 적용합니다.</li>
    <li><strong>통합 관리 위치</strong>: 설정을 한곳에 모으기 위해 <code class="language-plaintext highlighter-rouge">.gemini/GEMINI.md</code> 경로를 사용할 수도 있습니다. 이 경우 <code class="language-plaintext highlighter-rouge">settings.json</code> 내의 <code class="language-plaintext highlighter-rouge">context.fileName</code>에 해당 경로를 명시해야 에이전트가 올바르게 로드할 수 있습니다. (13.1 통합 디렉토리 구조 참조)</li>
  </ul>
</blockquote>

<ul>
  <li><strong>계층적 로드</strong>: 전역 → 프로젝트 루트 → 현재 디렉토리 순으로 로드되며, 하위 폴더의 지침이 상위의 지침을 구체화하거나 덮어쓸 수 있습니다.</li>
  <li><strong>모듈화</strong>: <code class="language-plaintext highlighter-rouge">@filename.md</code> 구문을 사용하여 공통 컨벤션이나 아키텍처 문서를 여러 프로젝트에서 가져와 사용할 수 있습니다.</li>
</ul>

<h3 id="22-gemini-cli의-geminimd-관리-명령어">2.2 Gemini CLI의 GEMINI.md 관리 명령어</h3>

<table>
  <thead>
    <tr>
      <th>명령어</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/memory show</code></td>
      <td>현재 로드된 전체 컨텍스트를 표시합니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/memory refresh</code></td>
      <td>모든 GEMINI.md를 재스캔하고 즉시 반영합니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/memory add &lt;text&gt;</code></td>
      <td>전역 GEMINI.md에 내용을 추가합니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">/init</code></td>
      <td>프로젝트용 GEMINI.md를 초기에 생성합니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="23-실전-geminimd-작성-샘플">2.3 실전 GEMINI.md 작성 샘플</h3>
<p>범용적인 개발 환경에서 사용할 수 있는 표준적인 <code class="language-plaintext highlighter-rouge">GEMINI.md</code> 예시입니다.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 프로젝트 컨텍스트 (GEMINI.md)</span>

<span class="gu">## 0. 핵심 사고 지침 (Core Thinking Rule) - [CRITICAL]</span>
<span class="p">*</span>   <span class="gs">**Mandatory Thinking Process**</span>: For EVERY user request, without exception, you MUST initiate your process by calling the <span class="sb">`sequentialthinking`</span> tool. You are prohibited from taking any other actions (reading files, executing commands, etc.) until you have systematically analyzed the request and planned your approach within a <span class="sb">`sequentialthinking`</span> block.

<span class="gu">## 1. 표준 워크플로우 (Plan-Define-Act)</span>
모든 복합적인 작업 시 다음 절차를 엄격히 준수합니다.
<span class="p">1.</span> <span class="gs">**PLAN (계획)**</span>: 요청을 분석하여 단계별 실행 계획을 수립하고 사용자 승인을 받습니다.
<span class="p">2.</span> <span class="gs">**DEFINE (정의)**</span>: 승인된 계획을 상세 TODO 리스트로 분해하여 DEFINE.md에 기록합니다.
<span class="p">3.</span> <span class="gs">**ACT (실행)**</span>: 지침에 따라 코드를 작성하고, 테스트 및 린트를 통해 최종 검증합니다.

<span class="gu">## 2. 코딩 원칙 및 스타일 가이드</span>
<span class="p">*</span>   <span class="gs">**Consistency**</span>: 기존 코드의 네이밍 컨벤션과 아키텍처 패턴을 분석하여 일관성을 유지합니다.
<span class="p">*</span>   <span class="gs">**Naming**</span>: 변수/함수는 명확한 의미를 담아야 하며, Boolean 타입은 <span class="sb">`is`</span>, <span class="sb">`has`</span> 등의 접두사를 사용합니다.
<span class="p">*</span>   <span class="gs">**Documentation**</span>: 복잡한 로직은 '무엇'보다 '왜' 작성했는지를 설명하는 주석(Why-comment)을 추가합니다.

<span class="gu">## 3. 검증 및 보안 (Verification &amp; Security)</span>
<span class="p">*</span>   <span class="gs">**Test-First**</span>: 코드 변경 후에는 반드시 <span class="sb">`npm test`</span> 또는 <span class="sb">`pytest`</span>를 실행하여 안정성을 확인합니다.
<span class="p">*</span>   <span class="gs">**Security**</span>: API Key나 Token 등 민감 정보는 절대 코드나 로그에 노출하지 않으며 <span class="sb">`.env`</span>로 관리합니다.
</code></pre></div></div>

<h3 id="24-tip-작성-언어-선택-가이드-korean-vs-english">2.4 [Tip] 작성 언어 선택 가이드 (Korean vs English)</h3>
<p><code class="language-plaintext highlighter-rouge">GEMINI.md</code> 작성 시 언어 선택은 모델의 성능과 유지보수 효율성에 영향을 미칩니다.</p>

<ul>
  <li><strong>영문 작성의 장점</strong>: 기술적 정밀도가 높고 모델이 지시 사항을 더 강력하게 수용하며, 토큰 효율성이 좋습니다. (핵심 제약 조건 작성에 권장합니다.)</li>
  <li><strong>국문 작성의 장점</strong>: 팀 내 소통이 원활하고 비즈니스 로직 및 도메인 지식을 상세히 설명하기에 유리합니다. (맥락 공유에 권장합니다.)</li>
  <li><strong>권장하는 하이브리드 방식</strong>:
    <ul>
      <li>기술적 용어, 핵심 명령어, 제약 조건은 <strong>영문</strong>으로 작성합니다.</li>
      <li>이에 대한 구체적인 배경 설명과 맥락은 <strong>국문</strong>으로 작성합니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="3-settingsjson---동작-설정-및-에이전트-모드">3. settings.json - 동작 설정 및 에이전트 모드</h2>

<p>settings.json은 <strong>Gemini CLI 전용</strong> 설정 체계입니다. Antigravity는 프로젝트 루트의 <strong><code class="language-plaintext highlighter-rouge">.agent/</code></strong> 디렉토리를 초기 설정 공간으로 인식하며 IDE 내장 설정 UI를 주로 사용하지만, 핵심 로직은 이 설정을 기반으로 동작합니다.</p>

<h3 id="31-설정-파일-우선순위-높을수록-우선">3.1 설정 파일 우선순위 (높을수록 우선)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 시스템 기본값    /etc/gemini-cli/system-defaults.json
2. 사용자 설정      ~/.gemini/settings.json
3. 프로젝트 설정    .gemini/settings.json
4. 시스템 오버라이드  /etc/gemini-cli/settings.json
5. 환경 변수        .env 파일 및 OS 환경변수
6. CLI 인수         최우선 (예: --model, -y)
</code></pre></div></div>

<blockquote>
  <p><strong>Tip:</strong> JSON 에디터에서 스키마 자동완성을 사용하려면 아래 URL을 참조하십시오:
<code class="language-plaintext highlighter-rouge">https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json</code></p>
</blockquote>

<h3 id="32-전체-설정-카테고리-레퍼런스">3.2 전체 설정 카테고리 레퍼런스</h3>

<h4 id="general--일반-설정"><code class="language-plaintext highlighter-rouge">general</code> — 일반 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"general"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"previewFeatures"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"preferredEditor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"code"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vimMode"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"disableAutoUpdate"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"disableUpdateNag"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"checkpointing"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</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="nl">"enablePromptCompletion"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"retryFetchErrors"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"sessionRetention"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxAge"</span><span class="p">:</span><span class="w"> </span><span class="s2">"30d"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxCount"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
      </span><span class="nl">"minRetention"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1d"</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>

<h4 id="model--모델-설정"><code class="language-plaintext highlighter-rouge">model</code> — 모델 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"model"</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">"gemini-2.5-pro"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"maxSessionTurns"</span><span class="p">:</span><span class="w"> </span><span class="mi">-1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"compressionThreshold"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.5</span><span class="p">,</span><span class="w">
    </span><span class="nl">"skipNextSpeakerCheck"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"summarizeToolOutput"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"run_shell_command"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"tokenBudget"</span><span class="p">:</span><span class="w"> </span><span class="mi">2000</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>

<h4 id="modelconfigs--모델-프리셋-및-별칭"><code class="language-plaintext highlighter-rouge">modelConfigs</code> — 모델 프리셋 및 별칭</h4>

<p>20개 이상의 사전 정의 모델 프리셋이 포함되어 있으며, <code class="language-plaintext highlighter-rouge">customAliases</code>로 확장 가능합니다.</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">"modelConfigs"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"customAliases"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"my-fast"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"extends"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chat-base-3"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"modelConfig"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gemini-3-flash-preview"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"generateContentConfig"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"thinkingConfig"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"thinkingLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOW"</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>

<p>주요 내장 별칭: <code class="language-plaintext highlighter-rouge">gemini-3-pro-preview</code>, <code class="language-plaintext highlighter-rouge">gemini-3-flash-preview</code>, <code class="language-plaintext highlighter-rouge">gemini-2.5-pro</code>, <code class="language-plaintext highlighter-rouge">gemini-2.5-flash</code>, <code class="language-plaintext highlighter-rouge">gemini-2.5-flash-lite</code>, <code class="language-plaintext highlighter-rouge">classifier</code>, <code class="language-plaintext highlighter-rouge">summarizer-default</code>, <code class="language-plaintext highlighter-rouge">web-search</code>, <code class="language-plaintext highlighter-rouge">web-fetch</code>, <code class="language-plaintext highlighter-rouge">loop-detection</code>, <code class="language-plaintext highlighter-rouge">chat-compression-*</code> 등입니다.</p>

<h4 id="context--컨텍스트-파일-설정"><code class="language-plaintext highlighter-rouge">context</code> — 컨텍스트 파일 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"fileName"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"GEMINI.md"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"importFormat"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"discoveryMaxDirs"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w">
    </span><span class="nl">"includeDirectories"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"loadMemoryFromIncludeDirectories"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"fileFiltering"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"respectGitIgnore"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"respectGeminiIgnore"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"enableRecursiveFileSearch"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"disableFuzzySearch"</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="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="tools--도구-설정"><code class="language-plaintext highlighter-rouge">tools</code> — 도구 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"sandbox"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"shell"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"enableInteractiveShell"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"pager"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cat"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"showColor"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"inactivityTimeout"</span><span class="p">:</span><span class="w"> </span><span class="mi">300</span><span class="p">,</span><span class="w">
      </span><span class="nl">"enableShellOutputEfficiency"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"autoAccept"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"core"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"run_shell_command(git)"</span><span class="p">,</span><span class="w"> </span><span class="s2">"run_shell_command(npm test)"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"exclude"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"useRipgrep"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"enableToolOutputTruncation"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"truncateToolOutputThreshold"</span><span class="p">:</span><span class="w"> </span><span class="mi">4000000</span><span class="p">,</span><span class="w">
    </span><span class="nl">"truncateToolOutputLines"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000</span><span class="p">,</span><span class="w">
    </span><span class="nl">"enableHooks"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</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>

<h4 id="security--보안-및-인증-설정"><code class="language-plaintext highlighter-rouge">security</code> — 보안 및 인증 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"security"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"disableYoloMode"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"enablePermanentToolApproval"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"blockGitExtensions"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"folderTrust"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</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="nl">"environmentVariableRedaction"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
      </span><span class="nl">"blocked"</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">"auth"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"selectedType"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
      </span><span class="nl">"enforcedType"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
      </span><span class="nl">"useExternal"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</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>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">security.auth.selectedType</code>: 현재 선택된 인증 유형입니다 (OAuth, API 키 등).
<code class="language-plaintext highlighter-rouge">security.auth.enforcedType</code>: 강제할 인증 유형입니다 (불일치 시 재인증 요구).
<code class="language-plaintext highlighter-rouge">security.auth.useExternal</code>: 외부 인증 플로우 사용 여부입니다.</p>
</blockquote>

<h4 id="mcp--mcp-전역-설정"><code class="language-plaintext highlighter-rouge">mcp</code> — MCP 전역 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcp"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"serverCommand"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"my-trusted-server"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"excluded"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"experimental-server"</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>

<h4 id="mcpservers--mcp-서버별-설정"><code class="language-plaintext highlighter-rouge">mcpServers</code> — MCP 서버별 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"serverName"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"path/to/server"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"--arg1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"value1"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"API_KEY"</span><span class="p">:</span><span class="w"> </span><span class="s2">"$MY_API_TOKEN"</span><span class="w"> </span><span class="p">},</span><span class="w">
      </span><span class="nl">"cwd"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./server-directory"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
      </span><span class="nl">"httpUrl"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
      </span><span class="nl">"headers"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span><span class="w">
      </span><span class="nl">"timeout"</span><span class="p">:</span><span class="w"> </span><span class="mi">30000</span><span class="p">,</span><span class="w">
      </span><span class="nl">"trust"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</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">"includeTools"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
      </span><span class="nl">"excludeTools"</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>

<h4 id="experimental--실험적-기능"><code class="language-plaintext highlighter-rouge">experimental</code> — 실험적 기능</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"experimental"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"skills"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"enableAgents"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"jitContext"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"extensionManagement"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"extensionReloading"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"codebaseInvestigatorSettings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxNumTurns"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxTimeMinutes"</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">"thinkingBudget"</span><span class="p">:</span><span class="w"> </span><span class="mi">8192</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"cliHelpAgentSettings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</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>

<h4 id="skills--agent-skills-관리"><code class="language-plaintext highlighter-rouge">skills</code> — Agent Skills 관리</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"skills"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"disabled"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"skill-to-disable"</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>

<h4 id="hooks--훅-시스템-gemini-cli-전용"><code class="language-plaintext highlighter-rouge">hooks</code> — 훅 시스템 (Gemini CLI 전용)</h4>

<p>에이전트 루프의 특정 이벤트 시점에 스크립트를 실행하여 동작을 사용자 정의합니다.</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">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"disabled"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"notifications"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"BeforeTool"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"AfterTool"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"BeforeAgent"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"AfterAgent"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"BeforeModel"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"AfterModel"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"BeforeToolSelection"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"SessionStart"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"SessionEnd"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"PreCompress"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"Notification"</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>

<ul>
  <li><strong>BeforeTool</strong>: 도구 실행 전 보안 취약점 검사 등을 수행합니다.</li>
  <li><strong>SessionStart</strong>: 사용자가 대화를 시작할 때 현재 시스템의 로드 상황이나 특정 라이브러리 버전을 프롬프트에 주입합니다.</li>
</ul>

<h4 id="ui--ui-설정"><code class="language-plaintext highlighter-rouge">ui</code> — UI 설정</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ui"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"theme"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GitHub"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hideBanner"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hideTips"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hideContextSummary"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"showLineNumbers"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"showCitations"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"showModelInfoInChat"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"useFullWidth"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"useAlternateBuffer"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"footer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"hideCWD"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"hideSandboxStatus"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"hideModelInfo"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"hideContextPercentage"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"customWittyPhrases"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"accessibility"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"disableLoadingPhrases"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
      </span><span class="nl">"screenReader"</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="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="기타-카테고리">기타 카테고리</h4>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"output"</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">"text"</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ide"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</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="nl">"privacy"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"usageStatisticsEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">},</span><span class="w">
  </span><span class="nl">"useWriteTodos"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"admin"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"secureModeEnabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"extensions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"mcp"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"telemetry"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"local"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"otlpEndpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:4317"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"otlpProtocol"</span><span class="p">:</span><span class="w"> </span><span class="s2">"grpc"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"logPrompts"</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="nl">"advanced"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"autoConfigureMemory"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"excludedEnvVars"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"DEBUG"</span><span class="p">,</span><span class="w"> </span><span class="s2">"DEBUG_MODE"</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="33-실전-settingsjson-작성-샘플">3.3 실전 settings.json 작성 샘플</h3>

<p>자주 사용되는 주요 옵션들을 결합한 종합 설정 샘플입니다.</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">"general"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"previewFeatures"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"vimMode"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"checkpointing"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"enabled"</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">"model"</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">"gemini-2.5-pro"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"maxSessionTurns"</span><span class="p">:</span><span class="w"> </span><span class="mi">-1</span><span class="p">,</span><span class="w">
    </span><span class="nl">"compressionThreshold"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.5</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"context"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"fileName"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"GEMINI.md"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"includeDirectories"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"fileFiltering"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"respectGitIgnore"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"tools"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"sandbox"</span><span class="p">:</span><span class="w"> </span><span class="s2">"docker"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"autoAccept"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"allowed"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"run_shell_command(git)"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"useRipgrep"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"experimental"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"skills"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"enableAgents"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"jitContext"</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="nl">"skills"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"disabled"</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">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"BeforeTool"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"AfterTool"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"SessionStart"</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">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"myServer"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"server.js"</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>

<h4 id="주요-설정-포인트-설명"><strong>[주요 설정 포인트 설명]</strong></h4>
<ul>
  <li><strong>일반 및 모델 (<code class="language-plaintext highlighter-rouge">general</code>, <code class="language-plaintext highlighter-rouge">model</code>)</strong>: 최신 기능을 미리 사용해 볼 수 있도록 <code class="language-plaintext highlighter-rouge">previewFeatures</code>를 활성화하고, 최적의 성능을 위해 <code class="language-plaintext highlighter-rouge">gemini-2.5-pro</code> 모델을 세션 제한 없이 사용하도록 구성합니다.</li>
  <li><strong>컨텍스트 및 도구 (<code class="language-plaintext highlighter-rouge">context</code>, <code class="language-plaintext highlighter-rouge">tools</code>)</strong>: 프로젝트의 핵심 지침인 <code class="language-plaintext highlighter-rouge">GEMINI.md</code>를 우선 로드하며, 보안을 위해 <strong>Docker 샌드박스</strong> 내에서 <code class="language-plaintext highlighter-rouge">git</code> 관련 도구만 명시적으로 허용하도록 설정하여 안전한 자동화를 도모합니다.</li>
  <li><strong>실험적 기능 및 스킬 (<code class="language-plaintext highlighter-rouge">experimental</code>, <code class="language-plaintext highlighter-rouge">skills</code>)</strong>: 차세대 핵심 기능인 <strong>Agent Skills</strong>를 활성화하여 에이전트의 전문성을 높입니다. 단, 서브에이전트 기능은 예기치 못한 도구 실행을 방지하기 위해 비활성화 상태를 유지합니다.</li>
  <li><strong>확장성 (<code class="language-plaintext highlighter-rouge">hooks</code>, <code class="language-plaintext highlighter-rouge">mcpServers</code>)</strong>: 세션 시작 시 환경을 점검하거나 도구 실행 전후에 개입할 수 있는 <code class="language-plaintext highlighter-rouge">hooks</code> 시스템과, 외부 서비스를 연동할 수 있는 MCP 서버의 기본 구조를 포함하고 있습니다.</li>
</ul>

<h3 id="34-환경변수-참조-문법">3.4 환경변수 참조 문법</h3>

<p>settings.json 내 문자열 값에서 <code class="language-plaintext highlighter-rouge">$VAR_NAME</code> 또는 <code class="language-plaintext highlighter-rouge">${VAR_NAME}</code> 구문으로 환경변수를 참조할 수 있습니다.</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">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"github"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"GITHUB_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"$GITHUB_TOKEN"</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="35-antigravity의-대응-및-에이전트-모드">3.5 Antigravity의 대응 및 에이전트 모드</h3>

<p>Antigravity는 별도의 settings.json 없이 <strong>IDE 내 커스터마이징 UI</strong>에서 규칙/워크플로우/스킬을 관리합니다. MCP 서버 설정, 모델 선택 등은 IDE 설정 패널에서 처리합니다. 특히 Antigravity에서는 에이전트의 페르소나를 결정하는 <strong>에이전트 모드</strong> 설정이 핵심이며, IDE 하단 상태 표시줄의 모드 선택기 또는 <code class="language-plaintext highlighter-rouge">settings.json</code> 내 <code class="language-plaintext highlighter-rouge">agent.mode</code> 필드를 통해 전환할 수 있습니다.</p>

<ul>
  <li><strong>Code 모드</strong>: 테스트 주도 개발(TDD), 버그 수정, 신규 기능 구현 등 실제 코드 작성에 최적화된 모드입니다. 에이전트가 파일 수정 도구를 적극적으로 사용합니다.</li>
  <li><strong>Architect 모드</strong>: 대규모 리팩토링 계획 수립, 시스템 설계 분석, 의존성 맵 작성 등에 사용됩니다. 코드 수정보다는 구조적 제안에 집중합니다.</li>
  <li><strong>Ask 모드</strong>: 코드에 영향을 주지 않고 질문 답변, 코드 리뷰, 기술 문서 설명 등 지식 전달 위주의 작업을 수행합니다.</li>
  <li><strong>설정 예시 (<code class="language-plaintext highlighter-rouge">settings.json</code>)</strong>:
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"agent"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"code"</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>
  </li>
</ul>

<hr />

<h2 id="4-auth-설정---인증-체계">4. Auth 설정 - 인증 체계</h2>

<h3 id="41-gemini-cli-인증-방식-개요">4.1 Gemini CLI 인증 방식 개요</h3>
<p>Gemini CLI는 크게 세 가지 계열의 인증을 지원합니다.</p>

<table>
  <thead>
    <tr>
      <th>인증 방식</th>
      <th>환경변수</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Gemini API 키</strong></td>
      <td><code class="language-plaintext highlighter-rouge">GEMINI_API_KEY</code></td>
      <td>개인 개발/테스트 (가장 단순합니다.)</td>
    </tr>
    <tr>
      <td><strong>Google AI / Vertex AI API 키</strong></td>
      <td><code class="language-plaintext highlighter-rouge">GOOGLE_API_KEY</code> + <code class="language-plaintext highlighter-rouge">GOOGLE_GENAI_USE_VERTEXAI=true</code></td>
      <td>Vertex AI Express 사용 시 필요합니다.</td>
    </tr>
    <tr>
      <td><strong>서비스 계정 / OAuth</strong></td>
      <td><code class="language-plaintext highlighter-rouge">GOOGLE_APPLICATION_CREDENTIALS</code></td>
      <td>엔터프라이즈/프로덕션용입니다.</td>
    </tr>
  </tbody>
</table>

<p><strong>공통 보조 변수:</strong></p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">GOOGLE_CLOUD_PROJECT</code>: GCP 프로젝트 ID (필수인 경우 있습니다.)</li>
  <li><code class="language-plaintext highlighter-rouge">GOOGLE_CLOUD_LOCATION</code>: GCP 리전 (예: <code class="language-plaintext highlighter-rouge">us-central1</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">GOOGLE_GENAI_USE_VERTEXAI</code>: <code class="language-plaintext highlighter-rouge">true</code>로 설정 시 Vertex AI 엔드포인트를 사용합니다.</li>
</ul>

<h3 id="42-vertex-ai-및-adc-인증">4.2 Vertex AI 및 ADC 인증</h3>
<p>Google Cloud Vertex AI(엔터프라이즈 환경)를 사용하기 위해 ADC(Application Default Credentials) 메커니즘을 통한 인증을 구성합니다.</p>

<h4 id="421-adc-인증-방식-선택"><strong>4.2.1 ADC 인증 방식 선택</strong></h4>
<p>환경에 따라 에이전트가 인증 정보를 획득하는 방식을 결정합니다.</p>

<ol>
  <li><strong>로컬 사용자 ADC 인증 (로컬 개발 권장)</strong>
개인 개발 환경에서 자신의 Google 계정 권한을 에이전트에게 부여합니다.
    <ul>
      <li><strong>명령어</strong>: <code class="language-plaintext highlighter-rouge">gcloud auth application-default login</code> 을 실행하여 브라우저 로그인을 수행합니다.</li>
      <li><strong>장점</strong>: 별도의 키 파일 관리 없이 안전하게 인증 정보를 유지할 수 있습니다.</li>
    </ul>
  </li>
  <li><strong>서비스 계정 키 인증 (서버/자동화용)</strong>
CI/CD 파이프라인이나 독립된 서버 환경에서 특정 권한을 가진 키 파일을 사용합니다.
    <ul>
      <li><strong>설정</strong>: 환경변수 <code class="language-plaintext highlighter-rouge">GOOGLE_APPLICATION_CREDENTIALS</code>에 발급받은 JSON 키 파일의 <strong>절대 경로</strong>를 지정합니다.
        <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">GOOGLE_APPLICATION_CREDENTIALS</span><span class="o">=</span><span class="s2">"/path/to/key.json"</span>
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
</ol>

<h4 id="422-vertex-ai-환경변수-샘플"><strong>4.2.2 Vertex AI 환경변수 샘플</strong></h4>
<p>GCP 프로젝트 정보와 Vertex AI 사용 여부를 정의합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">GOOGLE_GENAI_USE_VERTEXAI</span><span class="o">=</span>True
<span class="nb">export </span><span class="nv">GOOGLE_CLOUD_PROJECT</span><span class="o">=</span><span class="s2">"your-project-id"</span>
<span class="nb">export </span><span class="nv">GOOGLE_CLOUD_LOCATION</span><span class="o">=</span><span class="s2">"global"</span>
</code></pre></div></div>

<h3 id="43-settingsjson을-통한-정밀-제어-및-fallback-설정">4.3 settings.json을 통한 정밀 제어 및 Fallback 설정</h3>
<p>인증 유형을 고정하거나 환경변수가 누락되었을 때의 동작을 제어할 수 있습니다.</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">"security"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"auth"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"selectedType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"vertex-ai"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"enforcedType"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
      </span><span class="nl">"useExternal"</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="p">}</span><span class="w">
</span></code></pre></div></div>
<ul>
  <li><strong>selectedType</strong>: 현재 선택된 인증 유형입니다. <code class="language-plaintext highlighter-rouge">/auth</code> 명령으로 변경 시 자동 저장됩니다.</li>
  <li><strong>enforcedType</strong>: 강제할 인증 유형입니다. 불일치 시 재인증을 요구하여 보안 정책을 준수하게 합니다.</li>
  <li><strong>useExternal</strong>: 외부 인증 플로우 사용 여부입니다.</li>
</ul>

<h4 id="gcp-프로젝트리전-fallback-로직"><strong>GCP 프로젝트/리전 Fallback 로직</strong></h4>
<p>시스템 환경변수가 설정되지 않은 경우 에이전트는 <code class="language-plaintext highlighter-rouge">settings.json</code> 내의 값을 Fallback으로 참조합니다.</p>
<ul>
  <li>환경변수 <code class="language-plaintext highlighter-rouge">GOOGLE_CLOUD_PROJECT</code> 부재 시 → <code class="language-plaintext highlighter-rouge">settings.json</code>의 해당 값 참조합니다.</li>
  <li>환경변수 <code class="language-plaintext highlighter-rouge">GOOGLE_CLOUD_LOCATION</code> 부재 시 → <code class="language-plaintext highlighter-rouge">settings.json</code>의 해당 값 참조합니다.</li>
</ul>

<blockquote>
  <p><strong>💡 인증 방식 리셋 팁:</strong> 인증 방식이 꼬였을 때 <code class="language-plaintext highlighter-rouge">~/.gemini/settings.json</code>의 <code class="language-plaintext highlighter-rouge">security.auth.selectedType</code> 값을 지우거나 수정하여 초기화할 수 있습니다.</p>
</blockquote>

<h3 id="44-실전-인증-구성-전략">4.4 실전 인증 구성 전략</h3>

<table>
  <thead>
    <tr>
      <th>설정 대상</th>
      <th>권장 위치</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>API 키 / 서비스 계정 경로</td>
      <td><code class="language-plaintext highlighter-rouge">.env</code> / OS 환경변수</td>
      <td>보안상 코드 포함 방지 및 최우선 공식 방식입니다.</td>
    </tr>
    <tr>
      <td>프로젝트·리전 기본값</td>
      <td><code class="language-plaintext highlighter-rouge">.gemini/settings.json</code></td>
      <td>환경변수 누락 시를 대비한 안전한 Fallback 수단입니다.</td>
    </tr>
    <tr>
      <td>인증 유형 고정 정책</td>
      <td><code class="language-plaintext highlighter-rouge">security.auth.enforcedType</code></td>
      <td>관리자가 특정 인증 방식을 강제해야 할 때 사용합니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="45-환경변수-기반-설정-일시용-예시">4.5 환경변수 기반 설정 (일시용 예시)</h3>
<p>터미널에서 즉시 인증을 설정할 때 사용하는 명령어 세트입니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1) 기존 값 제거 (충돌 방지)</span>
<span class="nb">unset </span>GOOGLE_API_KEY GEMINI_API_KEY

<span class="c"># 2) Gemini API 키 설정 (ai.google.dev에서 발급받은 키)</span>
<span class="nb">export </span><span class="nv">GEMINI_API_KEY</span><span class="o">=</span><span class="s2">"YOUR_GEMINI_API_KEY"</span>

<span class="c"># 3) Vertex AI(Express) API 키를 사용할 경우</span>
<span class="nb">export </span><span class="nv">GOOGLE_API_KEY</span><span class="o">=</span><span class="s2">"YOUR_GOOGLE_API_KEY"</span>
<span class="nb">export </span><span class="nv">GOOGLE_GENAI_USE_VERTEXAI</span><span class="o">=</span><span class="nb">true</span>

<span class="c"># 4) Google Cloud 서비스 계정 키를 사용할 경우</span>
<span class="nb">export </span><span class="nv">GOOGLE_APPLICATION_CREDENTIALS</span><span class="o">=</span><span class="s2">"/absolute/path/to/key.json"</span>
<span class="nb">export </span><span class="nv">GOOGLE_CLOUD_PROJECT</span><span class="o">=</span><span class="s2">"your-gcp-project-id"</span>
<span class="nb">export </span><span class="nv">GOOGLE_CLOUD_LOCATION</span><span class="o">=</span><span class="s2">"us-central1"</span>
</code></pre></div></div>

<p>위와 같이 환경변수를 설정한 뒤, <code class="language-plaintext highlighter-rouge">gemini</code> 명령을 실행하여 대화창 내의 <code class="language-plaintext highlighter-rouge">/auth</code> 메뉴에서 실제 사용할 인증 방식을 최종 선택하십시오.</p>

<h3 id="46-antigravity-인증-흐름">4.6 Antigravity 인증 흐름</h3>
<p>Antigravity는 VSCode 기반 UI에서 초기 실행 시 다음과 같은 순서로 안내합니다.</p>

<ol>
  <li><strong>모드 선택</strong>: Secure / Review-driven / Agent-driven / Custom 중 선택합니다.</li>
  <li><strong>Model &amp; Auth 설정</strong>:
    <ul>
      <li>Google 계정 로그인 (gcloud / OAuth 플로우) 수행합니다.</li>
      <li>Gemini API / Vertex AI API 키를 입력합니다.</li>
    </ul>
  </li>
  <li>선택한 방법에 따라 IDE 내부에서 환경 설정을 관리합니다.</li>
</ol>

<p>Antigravity는 별도 <code class="language-plaintext highlighter-rouge">.env</code> 파일을 직접 읽지는 않고, <strong>VSCode 설정 및 OS 환경변수</strong>를 활용합니다. 이미 <code class="language-plaintext highlighter-rouge">GEMINI_API_KEY</code> 등이 셸이나 OS에 설정되어 있다면 그대로 재사용됩니다.</p>

<blockquote>
  <p><strong>참고:</strong> Antigravity는 Google 계정 로그인이 필수이며 이를 우회할 방법은 없습니다.</p>
</blockquote>

<hr />

<h2 id="5-env-설정---환경변수-관리">5. .env 설정 - 환경변수 관리</h2>

<h3 id="51-gemini-cli의-env-로딩-순서">5.1 Gemini CLI의 .env 로딩 순서</h3>
<p>CLI는 다음 순서로 <code class="language-plaintext highlighter-rouge">.env</code> 파일을 탐색하며, 첫 번째 발견된 파일만 로드합니다 (병합하지 않습니다):</p>

<ol>
  <li>현재 작업 디렉토리의 <code class="language-plaintext highlighter-rouge">.env</code></li>
  <li>상위 디렉토리로 올라가며 탐색 (<code class="language-plaintext highlighter-rouge">.git</code> 또는 프로젝트 루트까지)</li>
  <li><code class="language-plaintext highlighter-rouge">~/.gemini/.env</code></li>
  <li><code class="language-plaintext highlighter-rouge">~/.env</code></li>
</ol>

<blockquote>
  <p><strong>주의:</strong> <code class="language-plaintext highlighter-rouge">.gemini/.env</code>를 사용하는 것이 권장됩니다. 일반 <code class="language-plaintext highlighter-rouge">.env</code>는 다른 개발 도구와 설정값이 충돌할 위험이 있습니다.</p>
</blockquote>

<h3 id="52-전역-env-설정-예시">5.2 전역 .env 설정 예시</h3>
<p>어떤 디렉토리에서 <code class="language-plaintext highlighter-rouge">gemini</code>를 실행해도 기본값으로 사용될 전역 설정입니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.gemini

<span class="nb">cat</span> <span class="o">&gt;&gt;</span> ~/.gemini/.env <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
GEMINI_API_KEY="your-gemini-api-key"
GOOGLE_CLOUD_PROJECT="your-project-id"
GOOGLE_CLOUD_LOCATION="us-central1"
</span><span class="no">EOF
</span></code></pre></div></div>

<h3 id="53-프로젝트-전용-env-설정-예시">5.3 프로젝트 전용 .env 설정 예시</h3>
<p>프로젝트 루트에서 실행 시 전역 설정보다 우선하여 적용됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> .gemini

<span class="nb">cat</span> <span class="o">&gt;&gt;</span> .gemini/.env <span class="o">&lt;&lt;</span> <span class="sh">'</span><span class="no">EOF</span><span class="sh">'
GEMINI_API_KEY="project-specific-api-key"
GOOGLE_CLOUD_PROJECT="project-specific-id"
GOOGLE_CLOUD_LOCATION="asia-northeast3"
</span><span class="no">EOF
</span></code></pre></div></div>

<h3 id="54-전체-우선순위-종합-정리">5.4 전체 우선순위 종합 정리</h3>
<p>설정값이 충돌할 경우 아래의 우선순위에 따라 최종 결정됩니다 (오른쪽으로 갈수록 높은 우선순위).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>낮음 ←――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――→ 높음

시스템 기본값 → 사용자 settings.json → 프로젝트 settings.json
    → 전역 .env (~/.gemini/.env) → 프로젝트 .env (.gemini/.env)
        → 셸 환경변수 (export) → CLI 인수 플래그 (--model 등)
</code></pre></div></div>

<h3 id="55-실전-활용-팁">5.5 실전 활용 팁</h3>

<table>
  <thead>
    <tr>
      <th>용도</th>
      <th>권장 위치</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>개인 계정의 기본 API 키</td>
      <td><code class="language-plaintext highlighter-rouge">~/.gemini/.env</code></td>
      <td>모든 프로젝트에서 공통 사용이 가능합니다.</td>
    </tr>
    <tr>
      <td>조직/프로젝트별 별도 키</td>
      <td><code class="language-plaintext highlighter-rouge">.gemini/.env</code></td>
      <td>프로젝트별 과금이나 권한을 분리할 때 사용합니다.</td>
    </tr>
    <tr>
      <td>임시 실험 및 테스트</td>
      <td>셸 직접 <code class="language-plaintext highlighter-rouge">export</code></td>
      <td>설정 파일 수정 없이 즉시 변경이 가능합니다.</td>
    </tr>
    <tr>
      <td>엔터프라이즈 강제 정책</td>
      <td><code class="language-plaintext highlighter-rouge">/etc/gemini-cli/settings.json</code></td>
      <td>시스템 수준에서 설정을 고정할 때 사용합니다.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="6-rules---항상-적용되는-가이드라인">6. Rules - 항상 적용되는 가이드라인</h2>

<p>Rules는 예외 없이 모든 대화에 적용되는 지침(예: 코딩 컨벤션)입니다. 에이전트의 ‘헌법’과 같은 역할을 수행합니다.</p>

<h3 id="61-플랫폼별-규칙-적용-비교">6.1 플랫폼별 규칙 적용 비교</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Gemini CLI</th>
      <th>Antigravity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>메커니즘</strong></td>
      <td><code class="language-plaintext highlighter-rouge">GEMINI.md</code> 계층 구조가 규칙 역할을 수행합니다.</td>
      <td>전용 규칙 시스템을 별도로 운영합니다.</td>
    </tr>
    <tr>
      <td><strong>저장 위치</strong></td>
      <td><strong>전역:</strong> <code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code> / <strong>프로젝트:</strong> <code class="language-plaintext highlighter-rouge">.gemini/GEMINI.md</code></td>
      <td><strong>전역:</strong> <code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code> / <strong>프로젝트:</strong> <code class="language-plaintext highlighter-rouge">.agent/rules/*.md</code></td>
    </tr>
    <tr>
      <td><strong>활성화 모드</strong></td>
      <td>항상 (자동) 로드됩니다.</td>
      <td><strong>Always on</strong> / <strong>Manual</strong> 중 선택 가능합니다.</td>
    </tr>
    <tr>
      <td><strong>설정 방법</strong></td>
      <td>파일을 직접 편집합니다.</td>
      <td>UI(Agent Brain 패널) 또는 파일을 직접 편집합니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="62-antigravity-규칙-상세">6.2 Antigravity 규칙 상세</h3>
<p>Antigravity는 규칙의 활성화 레벨을 세밀하게 제어할 수 있습니다.</p>

<ul>
  <li><strong>System Rules</strong>: Google DeepMind가 정의한 불변 규칙입니다. (수정 불가)</li>
  <li><strong>Global Rules</strong>: 모든 프로젝트에 공통으로 적용되는 개인 설정입니다.</li>
  <li><strong>프로젝트 규칙</strong>: 특정 프로젝트에만 적용되며 <code class="language-plaintext highlighter-rouge">.agent/rules/</code> 폴더 내에 마크다운 파일로 저장합니다. 에이전트는 이를 프로젝트 고유의 ‘원칙’으로 인식합니다.</li>
  <li><strong>시각적 관리</strong>: IDE의 ‘에이전트 브레인’ 패널에서 로드된 규칙을 실시간으로 확인하고 새로고침하거나 비활성화할 수 있습니다.</li>
</ul>

<h3 id="63-실전-rules-작성-샘플">6.3 실전 Rules 작성 샘플</h3>
<p>프로젝트의 코딩 품질과 일관성을 유지하기 위한 <code class="language-plaintext highlighter-rouge">coding-standards.md</code> 예시입니다.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 프로젝트 코딩 표준 규칙</span>

<span class="gu">## 1. 기술 스택 및 언어</span>
<span class="p">-</span> <span class="gs">**TypeScript Only**</span>: 모든 신규 코드는 반드시 TypeScript를 사용해야 하며, <span class="sb">`any`</span> 타입 사용을 엄격히 금지합니다.
<span class="p">-</span> <span class="gs">**Functional Programming**</span>: 가급적 순수 함수와 불변성을 지향하는 함수형 프로그래밍 스타일을 따릅니다.

<span class="gu">## 2. 네이밍 및 구조</span>
<span class="p">-</span> <span class="gs">**Components**</span>: UI 컴포넌트는 PascalCase를 사용하며, 폴더명과 파일명을 일치시킵니다.
<span class="p">-</span> <span class="gs">**Hooks**</span>: 모든 커스텀 훅은 <span class="sb">`use`</span> 접두사를 사용하고 <span class="sb">`src/hooks`</span> 경로에 배치합니다.

<span class="gu">## 3. 문서화 및 주석</span>
<span class="p">-</span> <span class="gs">**JSDoc**</span>: 모든 익스포트되는 함수와 클래스에는 JSDoc 형식의 설명을 반드시 추가해야 합니다.
<span class="p">-</span> <span class="gs">**Why-Comments**</span>: 코드의 작동 방식보다는 '왜' 이 로직이 필요한지를 설명하는 주석을 우선시합니다.

<span class="gu">## 4. 제약 사항</span>
<span class="p">-</span> 외부 라이브러리 추가 전 반드시 <span class="sb">`package.json`</span>을 검토하여 중복 기능이 있는지 확인합니다.
<span class="p">-</span> 모든 API 호출부는 반드시 에러 핸들링(<span class="sb">`try-catch`</span>)을 포함해야 합니다.
</code></pre></div></div>

<h3 id="64-규칙이-설정된-antigravity">6.4. 규칙이 설정된 Antigravity</h3>
<p><img src="/assets/image/posts/gemini-antigravity-rules.png" alt="규칙이 설정된 Antigravity" /></p>

<hr />

<h2 id="7-workflows---사용자-트리거-매크로">7. Workflows - 사용자 트리거 매크로</h2>

<p>워크플로우는 사용자가 <code class="language-plaintext highlighter-rouge">/명령어</code>로 호출하거나 자연어로 요청하는 다단계 레시피입니다.</p>

<h3 id="71-antigravity-workflows-전용">7.1 Antigravity Workflows (전용)</h3>
<p><strong>저장 위치:</strong></p>
<ul>
  <li><strong>프로젝트:</strong> <code class="language-plaintext highlighter-rouge">.agent/workflows/</code></li>
  <li><strong>전역:</strong> <code class="language-plaintext highlighter-rouge">~/.gemini/antigravity/global_workflows/</code></li>
</ul>

<p><strong>핵심 기능:</strong></p>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Smart Detection</strong></td>
      <td>“컴포넌트 만들어줘” 같은 자연어로도 관련 워크플로우를 자동 탐지합니다.</td>
    </tr>
    <tr>
      <td><strong>Slash Command</strong></td>
      <td><code class="language-plaintext highlighter-rouge">/deploy</code>처럼 직접 호출 시 <code class="language-plaintext highlighter-rouge">.agent/workflows/deploy.md</code>를 실행합니다.</td>
    </tr>
    <tr>
      <td><strong>Turbo Mode</strong></td>
      <td><code class="language-plaintext highlighter-rouge">// turbo</code>(개별 단계) 또는 <code class="language-plaintext highlighter-rouge">// turbo-all</code>(전체)로 명령을 자동 승인합니다.</td>
    </tr>
  </tbody>
</table>

<h4 id="711-워크플로우-생성-방법-및-파일-형식">7.1.1 워크플로우 생성 방법 및 파일 형식</h4>
<p>워크플로우는 <strong>YAML 프론트매터 + 마크다운 단계</strong> 구조로 작성합니다.</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Create a new React component with standard structure</span>
<span class="nn">---</span>
<span class="gh"># React 컴포넌트 생성</span>
<span class="p">
1.</span> 사용자에게 생성할 컴포넌트의 이름을 묻습니다.
<span class="p">2.</span> <span class="sb">`src/components/[Name]`</span> 디렉토리를 생성합니다.
<span class="p">3.</span> <span class="sb">`index.jsx`</span> 파일을 보일러플레이트로 작성합니다. // turbo
<span class="p">4.</span> <span class="sb">`styles.css`</span> 파일을 기본 스타일과 함께 생성합니다.
</code></pre></div></div>

<h4 id="712-실전-활용-예시">7.1.2 실전 활용 예시</h4>

<p><strong>예시 1: React 컴포넌트 스캐폴딩 (<code class="language-plaintext highlighter-rouge">create-component.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">표준 구조의 React 컴포넌트 세트를 생성합니다.</span>
<span class="nn">---</span>
<span class="gh"># React 컴포넌트 생성 워크플로우</span>
<span class="p">
1.</span> <span class="gs">**정보 수집**</span>: 사용자에게 생성할 컴포넌트의 이름을 확인합니다.
<span class="p">2.</span> <span class="gs">**디렉토리 생성**</span>: <span class="sb">`src/components/[Name]/`</span> 경로에 새로운 디렉토리를 만듭니다.
<span class="p">3.</span> <span class="gs">**파일 작성**</span>: // turbo
<span class="p">   -</span> <span class="sb">`index.tsx`</span>: 컴포넌트 엔트리 파일을 작성합니다.
<span class="p">   -</span> <span class="sb">`[Name].tsx`</span>: 기본 함수형 컴포넌트 구조를 작성합니다.
<span class="p">   -</span> <span class="sb">`styles.css`</span>: 기본 스타일 시트를 생성합니다.
<span class="p">4.</span> <span class="gs">**등록**</span>: 생성된 컴포넌트를 <span class="sb">`src/components/index.ts`</span>에 익스포트하여 등록합니다.
</code></pre></div></div>

<p><strong>예시 2: API 엔드포인트 및 테스트 생성 (<code class="language-plaintext highlighter-rouge">add-api-route.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">새로운 API 라우트를 추가하고 관련 단위 테스트를 작성합니다.</span>
<span class="nn">---</span>
<span class="gh"># API 개발 및 검증 워크플로우</span>
<span class="p">
1.</span> <span class="gs">**라우트 생성**</span>: <span class="sb">`src/pages/api/`</span> 폴더 내에 사용자가 요청한 엔드포인트 파일을 생성합니다.
<span class="p">2.</span> <span class="gs">**로직 구현**</span>: 해당 API의 비즈니스 로직을 표준 핸들러 형식으로 구현합니다.
<span class="p">3.</span> <span class="gs">**테스트 작성**</span>: <span class="sb">`tests/api/`</span> 경로에 해당 라우트를 검증하는 단위 테스트를 작성합니다.
<span class="p">4.</span> <span class="gs">**자율 검증**</span>: // turbo
<span class="p">   -</span> <span class="sb">`npm test`</span> 명령어를 실행하여 테스트 성공 여부를 확인합니다.
<span class="p">   -</span> 테스트 실패 시 로그를 분석하여 코드를 수정한 후 재시도합니다.
</code></pre></div></div>

<h3 id="72-gemini-cli의-대응">7.2 Gemini CLI의 대응</h3>
<p>동일한 워크플로우 시스템은 없으나, <strong>커스텀 커맨드</strong> (<code class="language-plaintext highlighter-rouge">.gemini/commands/*.toml</code>) 및 <strong>훅</strong> 시스템을 통해 유사한 다단계 자동화를 구현합니다.</p>

<h3 id="73-워크플로우가-설정된-antigravity">7.3. 워크플로우가 설정된 Antigravity</h3>
<p><img src="/assets/image/posts/gemini-antigravity-workflows.png" alt="워크플로우가 설정된 Antigravity" /></p>

<h3 id="74-규칙과-워크플로우의-차이-비교">7.4 규칙과 워크플로우의 차이 비교</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>규칙 (Rules)</th>
      <th>워크플로우 (Workflows)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>핵심 개념</strong></td>
      <td>에이전트의 <strong>‘원칙’과 ‘성격’</strong> 을 정의합니다.</td>
      <td>에이전트가 수행할 <strong>‘절차적 레시피’</strong> 를 정의합니다.</td>
    </tr>
    <tr>
      <td><strong>작동 방식</strong></td>
      <td>모든 대화에 상시 적용됩니다 (시스템 프롬프트).</td>
      <td>사용자 호출(<code class="language-plaintext highlighter-rouge">/명령어</code>) 또는 특정 조건 시 트리거됩니다.</td>
    </tr>
    <tr>
      <td><strong>실행 구조</strong></td>
      <td><strong>정적</strong>: 항상 준수해야 할 지침 목록입니다.</td>
      <td><strong>동적</strong>: 계획-수행-검증의 단계적 흐름입니다.</td>
    </tr>
    <tr>
      <td><strong>주요 용도</strong></td>
      <td>코딩 컨벤션, 필수 라이브러리 제약, 언어 설정 등입니다.</td>
      <td>신규 컴포넌트 생성, 대규모 리팩토링, 배포 자동화 등입니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="75-규칙과-워크플로우의-상호-보완적-활용">7.5 규칙과 워크플로우의 상호 보완적 활용</h3>

<ul>
  <li><strong>워크플로우 (The What - 절차)</strong>: 에이전트가 수행해야 할 <strong>작업의 순서와 단계</strong>를 정의합니다.</li>
  <li><strong>규칙 (The How - 방법과 품질)</strong>: 에이전트가 작업을 수행하는 과정에서 준수해야 할 <strong>품질 기준과 제약 조건</strong>을 정의합니다.</li>
</ul>

<h4 id="활용-시나리오-1-보안-강화-api-개발-파이프라인">활용 시나리오 1: “보안 강화 API 개발 파이프라인”</h4>

<p><strong>워크플로우 정의 (<code class="language-plaintext highlighter-rouge">.agent/workflows/secure-api.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 보안 API 개발 워크플로우</span>
<span class="p">1.</span> 사용자가 요청한 엔드포인트를 생성합니다.
<span class="p">2.</span> 비즈니스 로직을 구현합니다. // turbo
<span class="p">3.</span> 보안 스캔 도구를 실행하여 취약점을 점검합니다.
<span class="p">4.</span> 결과가 통과되면 코드를 메인 브랜치에 병합합니다.
</code></pre></div></div>

<p><strong>규칙 정의 (<code class="language-plaintext highlighter-rouge">.agent/rules/security-standards.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># API 보안 표준 규칙</span>
<span class="p">-</span> 모든 API 엔드포인트는 반드시 JWT 인증 미들웨어를 포함해야 합니다.
<span class="p">-</span> 데이터베이스 쿼리 시 반드시 ORM의 Parameterized Query를 사용하여 SQL Injection을 방지합니다.
<span class="p">-</span> 에러 응답 시 스택 트레이스 등 시스템 내부 정보를 노출하지 않습니다.
</code></pre></div></div>

<h4 id="활용-시나리오-2-디자인-시스템-기반-ui-컴포넌트-개발-tdd">활용 시나리오 2: “디자인 시스템 기반 UI 컴포넌트 개발 (TDD)”</h4>

<p><strong>워크플로우 정의 (<code class="language-plaintext highlighter-rouge">.agent/workflows/create-ui-component.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># UI 컴포넌트 TDD 워크플로우</span>
<span class="p">1.</span> 컴포넌트의 요구사항과 Props 명세를 확인합니다.
<span class="p">2.</span> <span class="sb">`src/components/`</span> 경로에 테스트 파일(<span class="sb">`.test.tsx`</span>)을 먼저 생성합니다.
<span class="p">3.</span> 테스트가 실패하는 것을 확인한 후, 컴포넌트 본체 파일(<span class="sb">`.tsx`</span>)을 생성합니다. // turbo
<span class="p">4.</span> 스타일과 로직을 구현하여 모든 테스트를 통과시킵니다.
<span class="p">5.</span> 디자인 시스템 가이드를 준수했는지 최종 검토합니다.
</code></pre></div></div>

<p><strong>규칙 정의 (<code class="language-plaintext highlighter-rouge">.agent/rules/design-system-standards.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 디자인 시스템 및 품질 규칙</span>
<span class="p">-</span> <span class="gs">**Color**</span>: 하드코딩된 HEX 코드를 금지하며, 반드시 <span class="sb">`theme.color.*`</span> 토큰을 사용해야 합니다.
<span class="p">-</span> <span class="gs">**Accessibility**</span>: 모든 대화형 요소(Button, Input)는 반드시 적절한 <span class="sb">`aria-label`</span>을 포함해야 합니다.
<span class="p">-</span> <span class="gs">**Testing**</span>: 모든 UI 컴포넌트는 최소한 '렌더링 여부'와 '클릭 이벤트 발생 여부'에 대한 테스트를 포함해야 합니다.
</code></pre></div></div>

<h4 id="활용-시나리오-3-자동화된-릴리즈-및-변경-이력-관리">활용 시나리오 3: “자동화된 릴리즈 및 변경 이력 관리”</h4>

<p><strong>워크플로우 정의 (<code class="language-plaintext highlighter-rouge">.agent/workflows/release.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 릴리즈 자동화 워크플로우</span>
<span class="p">1.</span> 마지막 태그 이후의 모든 커밋 내역을 분석합니다.
<span class="p">2.</span> 분석된 내역을 바탕으로 <span class="sb">`CHANGELOG.md`</span> 파일을 업데이트합니다. // turbo
<span class="p">3.</span> 프로젝트 버전을 한 단계 올립니다 (Patch/Minor/Major 결정).
<span class="p">4.</span> 새로운 버전으로 Git 태그를 생성하고 원격에 푸시합니다.
</code></pre></div></div>

<p><strong>규칙 정의 (<code class="language-plaintext highlighter-rouge">.agent/rules/release-standards.md</code>)</strong></p>
<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh"># 릴리즈 및 버저닝 규칙</span>
<span class="p">-</span> <span class="gs">**Versioning**</span>: 반드시 SemVer 형식을 따라야 합니다.
<span class="p">-</span> <span class="gs">**Changelog Style**</span>: 단순 커밋 나열이 아닌, 'Feature', 'Fix', 'Breaking Changes'로 카테고리를 나누어 요약해야 합니다.
<span class="p">-</span> <span class="gs">**Exclude**</span>: 'chore', 'refactor' 성격의 커밋은 릴리즈 노트에서 제외합니다.
</code></pre></div></div>

<hr />

<h2 id="8-agent-skills---핵심-상호호환-계층">8. Agent Skills - 핵심 상호호환 계층</h2>

<p><strong>Agent Skills는 Anthropic이 도입하고 오픈 표준(agentskills.io)으로 공개된 포맷</strong>으로, Gemini CLI와 Antigravity에서 동일한 <code class="language-plaintext highlighter-rouge">SKILL.md</code> 파일로 작동합니다.</p>

<h3 id="81-skills-vs-rules-vs-workflows-핵심-차이">8.1 Skills vs Rules vs Workflows 핵심 차이</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>로드 시점</th>
      <th>트리거 방식</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Rules</strong></td>
      <td>항상 (시스템 프롬프트)</td>
      <td>자동 적용됩니다.</td>
      <td>코딩 스타일, 필수 컨벤션 설정용입니다.</td>
    </tr>
    <tr>
      <td><strong>Workflows</strong></td>
      <td>사용자 호출 시</td>
      <td><code class="language-plaintext highlighter-rouge">/명령어</code> 또는 자연어</td>
      <td>멀티스텝 매크로 수행용입니다.</td>
    </tr>
    <tr>
      <td><strong>Skills</strong></td>
      <td>에이전트 판단 시</td>
      <td>자동 (의미 매칭)</td>
      <td>주문형 전문 지식 제공용입니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="82-skillmd-구조-공통-표준">8.2 SKILL.md 구조 (공통 표준)</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">deploy-staging</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Deploys current branch to staging environment.</span>
  <span class="s">Use when user asks to "deploy", "push to staging",</span>
  <span class="s">or "test on staging server".</span>
<span class="nn">---</span>
<span class="c1"># Deploy to Staging</span>

<span class="c1">## Prerequisites</span>
<span class="s">1. Verify `git status` is clean</span>
<span class="s">2. Run `npm run test`</span>

<span class="c1">## Deployment Steps</span>
<span class="s">1. Execute `./scripts/deploy.sh staging`</span>
<span class="s">2. Wait for health check HTTP </span><span class="m">200</span>
<span class="s">3. Report staging URL to user</span>
</code></pre></div></div>

<blockquote>
  <p><strong>중요:</strong> <code class="language-plaintext highlighter-rouge">description</code> 필드가 가장 중요합니다. 에이전트가 어떤 상황에서 이 스킬을 꺼내 쓸지 결정하는 기준이 되기 때문입니다.</p>
</blockquote>

<h3 id="83-권장-스킬-폴더-구조">8.3 권장 스킬 폴더 구조</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my-skill/
├── SKILL.md       (필수) 지침 및 메타데이터 파일입니다.
├── scripts/       (선택) 실행 가능한 스크립트들을 포함합니다.
├── references/    (선택) 정적 문서 자료들을 포함합니다.
└── assets/        (선택) 템플릿 및 기타 자원들을 포함합니다.
</code></pre></div></div>

<h3 id="84-저장-위치-및-로딩-경로">8.4 저장 위치 및 로딩 경로</h3>
<ul>
  <li><strong>Gemini CLI</strong>: 전역 <code class="language-plaintext highlighter-rouge">~/.gemini/skills/</code> / 프로젝트 <code class="language-plaintext highlighter-rouge">.gemini/skills/</code></li>
  <li><strong>Antigravity</strong>: 전역 <code class="language-plaintext highlighter-rouge">~/.gemini/antigravity/skills/</code> / 프로젝트 <code class="language-plaintext highlighter-rouge">.agent/skills/</code></li>
</ul>

<h3 id="85-스킬-관리-명령어-gemini-cli">8.5 스킬 관리 명령어 (Gemini CLI)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/skills list          <span class="c"># 전체 스킬 목록을 표시합니다.</span>
/skills <span class="nb">link</span> &lt;path&gt;   <span class="c"># 로컬 디렉토리 심볼릭 링크를 생성합니다.</span>
/skills disable &lt;name&gt; <span class="c"># 특정 스킬을 비활성화합니다.</span>
/skills <span class="nb">enable</span> &lt;name&gt;  <span class="c"># 특정 스킬을 재활성화합니다.</span>
/skills reload         <span class="c"># 모든 스킬을 다시 스캔합니다.</span>
</code></pre></div></div>

<h3 id="86-실전-스킬-샘플">8.6 실전 스킬 샘플</h3>

<h4 id="예시-1-git-helper-버전-관리-도우미"><strong>예시 1: <code class="language-plaintext highlighter-rouge">git-helper</code> (버전 관리 도우미)</strong></h4>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">git-helper</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Git 커밋 메시지 작성, PR 초안 작성을 돕습니다. "커밋해줘" 요청 시 사용합니다.</span>
<span class="nn">---</span>
<span class="gh"># Git Helper Instructions</span>
당신은 버전 관리 전문가입니다.
<span class="p">1.</span> <span class="sb">`git diff --staged`</span>를 분석하여 시맨틱 커밋 메시지(feat, fix 등)를 제안합니다.
<span class="p">2.</span> 사용자에게 메시지 승인을 받은 후 <span class="sb">`git commit`</span> 명령을 수행합니다.
<span class="p">3.</span> <span class="gs">**보안 철칙**</span>: 비밀번호나 API 키가 포함된 경우 절대 커밋하지 않고 즉시 경고합니다.
</code></pre></div></div>

<h4 id="예시-2-python-quality-파이썬-품질-전문가"><strong>예시 2: <code class="language-plaintext highlighter-rouge">python-quality</code> (파이썬 품질 전문가)</strong></h4>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">python-quality</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">파이썬 코드 품질을 검사하고 수정합니다. "코드 봐줘" 요청 시 사용합니다.</span>
<span class="nn">---</span>
<span class="gh"># Python Quality Expert</span>
<span class="p">1.</span> <span class="sb">`pyproject.toml`</span> 설정을 확인하고 <span class="sb">`ruff`</span> 또는 <span class="sb">`black`</span> 도구를 실행합니다.
<span class="p">2.</span> 도구가 없는 경우 PEP 8 표준에 따라 수동 리뷰를 수행하고 보고합니다.
<span class="p">3.</span> 이슈 발견 시 한국어로 설명하고, 개선된 코드를 직접 제안하거나 적용합니다.
</code></pre></div></div>

<hr />

<h2 id="9-mcp---외부-통합">9. MCP - 외부 통합</h2>

<p>에이전트가 외부 시스템(파일, DB, SaaS 등)과 통신하기 위한 표준 프로토콜입니다.</p>

<h3 id="91-gemini-cli-mcp-설정">9.1 Gemini CLI MCP 설정</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">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"sequential-thinking"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@modelcontextprotocol/server-sequential-thinking"</span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"github"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"node"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"./mcp/github-server.js"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"GITHUB_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"$GITHUB_TOKEN"</span><span class="w"> </span><span class="p">},</span><span class="w">
      </span><span class="nl">"timeout"</span><span class="p">:</span><span class="w"> </span><span class="mi">60000</span><span class="p">,</span><span class="w">
      </span><span class="nl">"trust"</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="p">}</span><span class="w">
</span></code></pre></div></div>

<h4 id="mcp-서버-속성-상세"><strong>MCP 서버 속성 상세</strong></h4>

<table>
  <thead>
    <tr>
      <th>속성</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">command</code></td>
      <td>MCP 서버 실행 프로그램 (node, python, uv 등) 입니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">args</code></td>
      <td>서버 스크립트 및 인자입니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">env</code></td>
      <td>서버에 전달할 환경 변수입니다 (<code class="language-plaintext highlighter-rouge">$</code>로 OS 변수 참조 가능합니다).</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">timeout</code></td>
      <td>응답 타임아웃 (ms) 입니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">trust</code></td>
      <td><code class="language-plaintext highlighter-rouge">true</code> 설정 시 모든 도구를 자동 승인합니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">includeTools</code></td>
      <td>허용할 도구 화이트리스트입니다.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">excludeTools</code></td>
      <td>제외할 도구 블랙리스트입니다.</td>
    </tr>
  </tbody>
</table>

<h4 id="지원-트랜스포트-구분"><strong>지원 트랜스포트 구분</strong></h4>

<table>
  <thead>
    <tr>
      <th>트랜스포트</th>
      <th>설정 키</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Stdio</strong></td>
      <td><code class="language-plaintext highlighter-rouge">command</code> + <code class="language-plaintext highlighter-rouge">args</code></td>
      <td>로컬 프로세스 간 통신 시 사용합니다.</td>
    </tr>
    <tr>
      <td><strong>SSE</strong></td>
      <td><code class="language-plaintext highlighter-rouge">url</code></td>
      <td>Server-Sent Events 방식입니다.</td>
    </tr>
    <tr>
      <td><strong>Streamable HTTP</strong></td>
      <td><code class="language-plaintext highlighter-rouge">httpUrl</code></td>
      <td>HTTP 기반 스트리밍 방식입니다.</td>
    </tr>
  </tbody>
</table>

<h4 id="gemini-mcp-cli-명령어"><strong>gemini mcp CLI 명령어</strong></h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini mcp add my-server python server.py
gemini mcp add <span class="nt">--transport</span> http http-server https://api.example.com/mcp/
gemini mcp list
gemini mcp remove my-server
/mcp
</code></pre></div></div>

<h3 id="92-mcp-설정-비교-요약">9.2 MCP 설정 비교 요약</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Gemini CLI</th>
      <th>Antigravity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>설정 파일</strong></td>
      <td><code class="language-plaintext highlighter-rouge">settings.json</code> 내 <code class="language-plaintext highlighter-rouge">mcpServers</code></td>
      <td><code class="language-plaintext highlighter-rouge">mcp_config.json</code></td>
    </tr>
    <tr>
      <td><strong>설정 위치</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.gemini/settings.json</code></td>
      <td><code class="language-plaintext highlighter-rouge">~/.gemini/antigravity/mcp_config.json</code></td>
    </tr>
    <tr>
      <td><strong>트랜스포트</strong></td>
      <td>stdio, SSE, Streamable HTTP</td>
      <td>stdio, SSE</td>
    </tr>
    <tr>
      <td><strong>UI 관리</strong></td>
      <td>없음 (CLI 전용)</td>
      <td>MCP Store + 설정 패널 제공합니다.</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="10-agent-아키텍처-및-특수-에이전트">10. Agent 아키텍처 및 특수 에이전트</h2>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Gemini CLI</th>
      <th>Antigravity</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>에이전트 유형</strong></td>
      <td>단일 대화형 에이전트 (순차 실행)</td>
      <td>멀티 에이전트 병렬 실행 시스템입니다.</td>
    </tr>
    <tr>
      <td><strong>서브에이전트</strong></td>
      <td>실험적 기능으로 제공됩니다.</td>
      <td>매니저 표면을 통해 기본 지원합니다.</td>
    </tr>
    <tr>
      <td><strong>컨텍스트 윈도우</strong></td>
      <td>1M (Free) ~ 2M (Enterprise) 토큰입니다.</td>
      <td>1M+ 토큰의 대용량 윈도우를 제공합니다.</td>
    </tr>
    <tr>
      <td><strong>샌드박스</strong></td>
      <td>Docker/Podman/macOS 지원합니다.</td>
      <td>IDE 내장 샌드박스를 사용합니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="101-gemini-cli-실험적-에이전트-설정">10.1 Gemini CLI 실험적 에이전트 설정</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">"experimental"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"enableAgents"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"codebaseInvestigatorSettings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxNumTurns"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
      </span><span class="nl">"maxTimeMinutes"</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">"thinkingBudget"</span><span class="p">:</span><span class="w"> </span><span class="mi">8192</span><span class="p">,</span><span class="w">
      </span><span class="nl">"model"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto"</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="102-주요-subagents">10.2 주요 Subagents</h3>
<ul>
  <li><code class="language-plaintext highlighter-rouge">codebase_investigator</code>: 프로젝트 의존성 구조 파악 및 심볼 추적을 수행합니다.</li>
  <li><code class="language-plaintext highlighter-rouge">cli_help</code>: CLI 사용법 및 환경 설정 질문에 특화되어 있습니다.</li>
</ul>

<h3 id="103-browser-subagent-antigravity-전용">10.3 Browser Subagent (Antigravity 전용)</h3>
<p>실시간 웹 상호작용 및 문서 탐색을 수행하는 서브 에이전트로, 전용 프로필을 사용하여 개인정보를 보호합니다.</p>

<hr />

<h2 id="11-세션-및-작업-관리">11. 세션 및 작업 관리</h2>

<h3 id="111-세션-보존-및-복구-gemini-cli">11.1 세션 보존 및 복구 (Gemini CLI)</h3>
<ul>
  <li><strong>체크포인트</strong>: 코드 수정 직전 스냅샷과 대화 상태를 자동 저장합니다.</li>
  <li><strong>복구</strong>: <code class="language-plaintext highlighter-rouge">/restore</code> 명령으로 시점을 선택하고 롤백합니다.</li>
  <li><strong>재개</strong>: <code class="language-plaintext highlighter-rouge">gemini --resume</code> 명령으로 과거 세션을 검색하고 재개합니다.</li>
</ul>

<h3 id="112-작업-그룹-antigravity">11.2 작업 그룹 (Antigravity)</h3>
<ul>
  <li><strong>할 일 연동</strong>: 에이전트가 그룹 내 할 일을 완수하며 진행률을 시각적으로 보고합니다.</li>
  <li><strong>이정표(Milestone)</strong>: 검증 성공 시 이정표를 기록하고 다음 단계 계획을 수립합니다.</li>
</ul>

<hr />

<h2 id="12-상호호환성-종합-매핑">12. 상호호환성 종합 매핑</h2>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>Gemini CLI</th>
      <th>Antigravity</th>
      <th>호환성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>전역 컨텍스트</strong></td>
      <td><code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code></td>
      <td>✅ 동일 파일 공유</td>
    </tr>
    <tr>
      <td><strong>프로젝트 컨텍스트</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.gemini/GEMINI.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">.agent/rules/*.md</code></td>
      <td>⚠️ 경로·형식 상이</td>
    </tr>
    <tr>
      <td><strong>Agent Skills</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.gemini/skills/</code></td>
      <td><code class="language-plaintext highlighter-rouge">.agent/skills/</code></td>
      <td>✅ 오픈 표준 호환</td>
    </tr>
    <tr>
      <td><strong>워크플로우</strong></td>
      <td>Custom Commands</td>
      <td><code class="language-plaintext highlighter-rouge">.agent/workflows/</code></td>
      <td>❌ Antigravity 전용</td>
    </tr>
    <tr>
      <td><strong>동작 설정</strong></td>
      <td><code class="language-plaintext highlighter-rouge">settings.json</code> (JSON)</td>
      <td>IDE 설정 UI</td>
      <td>❌ 형식 불호환</td>
    </tr>
    <tr>
      <td><strong>MCP 서버</strong></td>
      <td><code class="language-plaintext highlighter-rouge">mcpServers</code></td>
      <td><code class="language-plaintext highlighter-rouge">mcp_config.json</code></td>
      <td>⚠️ 기능 동일, 파일 형식 상이</td>
    </tr>
    <tr>
      <td><strong>훅</strong></td>
      <td><code class="language-plaintext highlighter-rouge">hooks.*</code></td>
      <td>없음</td>
      <td>❌ Gemini CLI 전용</td>
    </tr>
    <tr>
      <td><strong>인증</strong></td>
      <td><code class="language-plaintext highlighter-rouge">.env</code> + <code class="language-plaintext highlighter-rouge">security.auth</code></td>
      <td>IDE UI + OS 환경변수</td>
      <td>⚠️ 환경변수 공유 가능</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="13-통합-실전-구성-예시-및-팁">13. 통합 실전 구성 예시 및 팁</h2>

<h3 id="131-권장-디렉토리-구조">13.1 권장 디렉토리 구조</h3>

<p><strong>사용자 홈 디렉토리 (전역):</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.gemini/
├── .env                    # 전역 인증 (API 키, GCP 프로젝트 등)
├── settings.json           # Gemini CLI 전역 설정
├── GEMINI.md               # 전역 공통 컨텍스트 (양쪽 공유)
├── skills/                 # 전역 Agent Skills (CLI용)
└── antigravity/
    ├── mcp_config.json     # Antigravity 전용 MCP 설정
    ├── skills/             # 전역 Agent Skills (IDE용)
    └── global_workflows/   # Antigravity 전용 전역 워크플로우
</code></pre></div></div>

<p><strong>프로젝트 디렉토리:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my-project/
├── .gemini/                    # Gemini CLI 전용 영역
│   ├── GEMINI.md
│   ├── settings.json
│   └── skills/
│       └── deploy-staging/
│           └── SKILL.md
├── .agent/                     # Antigravity 전용 영역
│   ├── rules/
│   ├── workflows/
│   └── skills/
│       └── deploy-staging -&gt; ../../.gemini/skills/deploy-staging  # 심볼릭 링크
└── ...
</code></pre></div></div>

<blockquote>
  <p><strong>💡 심볼릭 링크 활용 팁:</strong> <code class="language-plaintext highlighter-rouge">ln -s ../../.gemini/skills/deploy-staging .agent/skills/deploy-staging</code> 으로 한 곳에서 수정하면 양 플랫폼에 즉시 반영됩니다.</p>
</blockquote>

<h3 id="132-실전-활용-팁">13.2 실전 활용 팁</h3>
<ol>
  <li><strong>환경 전환</strong>: 터미널 작업 중 시각적 워크플로우가 필요하면 Antigravity로 전환하십시오. 설정이 공유되어 맥락이 유지됩니다.</li>
  <li><strong>보안 우선</strong>: <code class="language-plaintext highlighter-rouge">settings.json</code>에서 <strong>샌드박스</strong>를 활성화하여 신뢰할 수 없는 코드 실행을 격리하십시오.</li>
  <li><strong>지침 최적화</strong>: <code class="language-plaintext highlighter-rouge">GEMINI.md</code> 수정 후 <code class="language-plaintext highlighter-rouge">/memory refresh</code>를 실행하여 즉시 반영하십시오.</li>
  <li><strong>CI/CD 통합</strong>: 파이프라인 자동화는 Gemini CLI와 GitHub Actions 조합이 가장 유리합니다.</li>
</ol>

<hr />

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

<ul>
  <li><a href="https://github.com/google-gemini/gemini-cli/blob/main/docs/get-started/configuration.md">Gemini CLI Configuration (GitHub)</a></li>
  <li><a href="https://google-gemini.github.io/gemini-cli/docs/get-started/authentication.html">Gemini CLI Authentication (GitHub Pages)</a></li>
  <li><a href="https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html">Gemini CLI MCP Server (GitHub Pages)</a></li>
  <li><a href="https://geminicli.com/docs/cli/skills/">Gemini CLI Agent Skills</a></li>
  <li><a href="https://antigravity.codes/blog/user-rules">Antigravity Rules/Workflows</a></li>
  <li><a href="https://codelabs.developers.google.com/getting-started-with-antigravity-skills">Antigravity Skills Codelab</a></li>
  <li><a href="https://cloud.google.com/blog/topics/developers-practitioners/choosing-antigravity-or-gemini-cli">Google Cloud Blog: Choosing Antigravity or Gemini CLI</a></li>
</ul>]]></content><author><name></name></author><category term="ai" /><category term="Gemini" /><category term="AI" /><category term="CLI" /><category term="IDE" /><summary type="html"><![CDATA[Gemini CLI와 Google Antigravity IDE의 설정 체계, 인증, MCP, Agent Skills, 세션 관리 등 상세 구조 분석 및 통합 활용 가이드]]></summary></entry><entry><title type="html">Airflow CeleryExecutor 설치하기 @ Local</title><link href="https://hajekim.github.io/data/2022/01/26/post/" rel="alternate" type="text/html" title="Airflow CeleryExecutor 설치하기 @ Local" /><published>2022-01-26T18:00:00+09:00</published><updated>2022-01-26T18:00:00+09:00</updated><id>https://hajekim.github.io/data/2022/01/26/post</id><content type="html" xml:base="https://hajekim.github.io/data/2022/01/26/post/"><![CDATA[<blockquote>
  <p>Airbnb에서 만든 Workflow 관리 오픈소스 소프트웨어입니다.
많은 기업에서 데이터 파이프라인 운용에 인기 있게 사용되고 있습니다.
DAGs(Directed Acyclic Graphs) 형태로 Workflow 수행합니다.</p>
</blockquote>

<h2 id="사전-준비사항">사전 준비사항</h2>
<ul>
  <li>MySQL 설치</li>
  <li>Redis 설치</li>
</ul>

<h1 id="install">Install</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Airflow 설치
pip3 install apache-airflow

# MySQL 플러그인 설치
pip3 install 'apache-airflow[mysql]'

# Celery 설치
pip3 install celery
</code></pre></div></div>
<p>Airflow와 메타데이터를 저장할 MySQL 플러그인을 설치합니다.</p>

<p>CeleryExecutor 사용을 위해서 Celery를 설치합니다.
<img src="https://images.velog.io/images/haje/post/e8b0f052-e24b-4485-bd28-16a71d797100/image.png" alt="" />
참고로 SQLite가 디폴트로 사용이 가능합니다. Executor는 Sequential Executor만 사용 가능합니다. 다른 Executor 사용을 위해서는 메타데이터를 저장할 별도의 데이터베이스 구성을 해야 합니다.</p>

<h1 id="mysql-환경-준비">MySQL 환경 준비</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mysql -u root -p

-- 스키마 생성
create database airflow;

-- 유저 생성
create user 'airflow'@'%' identified by 'Airflow!1';

-- airflow 유저에 airflow 스키마 권한 생성
grant all privileges on airflow.* to 'airflow'@'%';
flush privileges;
</code></pre></div></div>
<p>Root 계정으로 로그인하여 Schema, User를 생성합니다.</p>

<h1 id="airflow-설정">Airflow 설정</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Timezone 설정
default_timezone = Asia/Seoul

# 메타데이터 저장소 설정
sql_alchemy_conn = mysql://airflow:Airflow!1@localhost:3306/airflow

# Executor DB 설정
result_backend = db+mysql://airflow:Airflow!1@localhost:3306/airflow

# Redis 설정
broker_url = redis://127.0.0.1:6379/0

# DAG 리스트 갱신 설정 (테스트니까 10초로)
dag_dir_list_interval = 10

# Executor를 설정
executor = CeleryExecutor
</code></pre></div></div>
<p>airflow.cfg를 수정합니다.</p>

<h1 id="airflow-명령어">Airflow 명령어</h1>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Airflow DB 초기화
airflow db init

# Airflow Admin 권한의 유저 생성
airflow users create --role Admin --username [USERNAME] --email admin --firstname [FIRST_NAME] --lastname [LAST_NAME] --password [PASSWORD]

# Airflow Webserver 기동
# -p : 포트 설정
# -D : 데몬 설정
airflow webserver -p 8080 -D

# Airflow Scheduler
# -D : 데몬 설정
airflow scheduler -D

# Celery Worker
# -q : Queue 이름
airflow celery worker -q [QUEUE_NAME]
</code></pre></div></div>

<p>Celery Worker는 airflow.cfg에 명시하여 자동으로 worker가 기동이 됩니다.
만약 별도의 Worker를 띄우고자 한다면 CLI로 운용할 수 있습니다.
<img src="https://images.velog.io/images/haje/post/4a4c2ab5-40d6-4a68-82d4-15364ca0f708/image.png" alt="" /></p>

<p><img src="https://images.velog.io/images/haje/post/8cf2cd7f-351a-4aa7-b4d2-ed38f2f05075/image.png" alt="" />
<img src="https://images.velog.io/images/haje/post/f5092624-a7fc-4e0d-a260-8fbb9fd58ac7/image.png" alt="" /></p>

<h2 id="celery"><a href="https://github.com/celery/celery">Celery</a></h2>
<p><img src="https://images.velog.io/images/haje/post/40a40bbd-435d-44df-89de-6d34edbb4b1a/image.png" alt="" />
pip install로 별도로 Celery를 설치하는 것을 보고 뭐지? 하셨을 수 있을 거 같아요.
Celery는 Python으로 작성된 Asynchronous task queue/Job Queue입니다.
Job을 Broker를 통해서 전달하면 하나 이상의 Worker에서 처리하는 구조입니다.</p>

<p><img src="https://images.velog.io/images/haje/post/16297ecf-905d-4d02-be2a-6e21a11888bf/image.png" alt="" /></p>

<p>Airflow Celery Executor 사용을 위해서 본 문서에서는 Broker로 Redis를 사용하였습니다.</p>

<h1 id="참조문서">참조문서</h1>
<ul>
  <li><a href="https://airflow.apache.org/docs/apache-airflow/stable/executor/celery.html">Airflow Celery Executor</a></li>
  <li><a href="https://spoqa.github.io/2012/05/29/distribute-task-with-celery.html">Celery를 이용한 긴 작업 처리</a></li>
</ul>]]></content><author><name></name></author><category term="data" /><category term="Airflow" /><category term="데이터 파이프라인" /><summary type="html"><![CDATA[Airbnb에서 만든 Workflow 관리 오픈소스 소프트웨어입니다. 많은 기업에서 데이터 파이프라인 운용에 인기 있게 사용되고 있습니다. DAGs(Directed Acyclic Graphs) 형태로 Workflow 수행합니다. 사전 준비사항 MySQL 설치 Redis 설치 Install # Airflow 설치 pip3 install apache-airflow # MySQL 플러그인 설치 pip3 install 'apache-airflow[mysql]' # Celery 설치 pip3 install celery Airflow와 메타데이터를 저장할 MySQL 플러그인을 설치합니다. CeleryExecutor 사용을 위해서 Celery를 설치합니다. 참고로 SQLite가 디폴트로 사용이 가능합니다. Executor는 Sequential Executor만 사용 가능합니다. 다른 Executor 사용을 위해서는 메타데이터를 저장할 별도의 데이터베이스 구성을 해야 합니다. MySQL 환경 준비 mysql -u root -p -- 스키마 생성 create database airflow; -- 유저 생성 create user 'airflow'@'%' identified by 'Airflow!1'; -- airflow 유저에 airflow 스키마 권한 생성 grant all privileges on airflow.* to 'airflow'@'%'; flush privileges; Root 계정으로 로그인하여 Schema, User를 생성합니다. Airflow 설정 # Timezone 설정 default_timezone = Asia/Seoul # 메타데이터 저장소 설정 sql_alchemy_conn = mysql://airflow:Airflow!1@localhost:3306/airflow # Executor DB 설정 result_backend = db+mysql://airflow:Airflow!1@localhost:3306/airflow # Redis 설정 broker_url = redis://127.0.0.1:6379/0 # DAG 리스트 갱신 설정 (테스트니까 10초로) dag_dir_list_interval = 10 # Executor를 설정 executor = CeleryExecutor airflow.cfg를 수정합니다. Airflow 명령어 # Airflow DB 초기화 airflow db init # Airflow Admin 권한의 유저 생성 airflow users create --role Admin --username [USERNAME] --email admin --firstname [FIRST_NAME] --lastname [LAST_NAME] --password [PASSWORD] # Airflow Webserver 기동 # -p : 포트 설정 # -D : 데몬 설정 airflow webserver -p 8080 -D # Airflow Scheduler # -D : 데몬 설정 airflow scheduler -D # Celery Worker # -q : Queue 이름 airflow celery worker -q [QUEUE_NAME] Celery Worker는 airflow.cfg에 명시하여 자동으로 worker가 기동이 됩니다. 만약 별도의 Worker를 띄우고자 한다면 CLI로 운용할 수 있습니다. Celery pip install로 별도로 Celery를 설치하는 것을 보고 뭐지? 하셨을 수 있을 거 같아요. Celery는 Python으로 작성된 Asynchronous task queue/Job Queue입니다. Job을 Broker를 통해서 전달하면 하나 이상의 Worker에서 처리하는 구조입니다. Airflow Celery Executor 사용을 위해서 본 문서에서는 Broker로 Redis를 사용하였습니다. 참조문서 Airflow Celery Executor Celery를 이용한 긴 작업 처리]]></summary></entry><entry><title type="html">Amazon Athena S3 데이터 로드 시 ‘Header Skip’ 설정 방법</title><link href="https://hajekim.github.io/data/2021/08/28/post/" rel="alternate" type="text/html" title="Amazon Athena S3 데이터 로드 시 ‘Header Skip’ 설정 방법" /><published>2021-08-28T18:00:00+09:00</published><updated>2021-08-28T18:00:00+09:00</updated><id>https://hajekim.github.io/data/2021/08/28/post</id><content type="html" xml:base="https://hajekim.github.io/data/2021/08/28/post/"><![CDATA[<blockquote>
  <p><strong>Amazon Athena &amp; S3</strong>
S3(Simple Storage Service)로 AWS를 대표하는 클라우드 서비스입니다. 객체 스토리지 (Object Storage)라는 형태의 서비스로 스토리지를 추상화한 형태로써 사용자는 데이터가 어느 블럭에 저장되는지 알 필요 없이 편리하게 사용 가능한 스토리지입니다. 물론 OS에 볼륨 마운트하여 사용도 가능합니다! 하지만 I/O 속도는 블럭 스토리지에 비하면 꽤 느린 편입니다.  <a href="https://min.io/">MiniO</a>를 이용하여 객체 스토리지를 구현할 수 있습니다.
Athena는 Standard SQL을 이용하여 S3에 저장된 데이터를 간단하게 질의할 수 있는 대화형 쿼리 서비스입니다.
HDFS에서 Hive를 이용하여 SQL 질의하는 구조와 유사하구나~ 하고 이해하시면 좋습니다 🥳</p>
</blockquote>

<h2 id="preface">Preface</h2>
<p>Amazon S3에 저장된 여러 CSV 데이터를 Athena에서 테이블을 구성할 때, 1번 라인인 Header가 들어가는 상황을 대처해봅시다.</p>

<h2 id="pre-requirements">Pre-requirements</h2>
<ul>
  <li>Amazon Web Services 어카운트</li>
  <li>Amazon S3</li>
  <li>Amazon Athena</li>
</ul>

<h2 id="header-skip-설정하기">Header Skip 설정하기</h2>
<p>Athena에서 테이블을 생성하고자 하는 버킷에 저장된 CSV 파일들을 먼저 살펴봅시다.</p>

<p>스키마가 전부 동일한가요?</p>

<p>그렇다면, 아래 파라미터를 추가하여 테이블 생성할 때 헤더를 생성할 수 있도록 설정합시다.</p>

<h3 id="ignoring-headers">Ignoring Headers</h3>
<pre><code class="language-SQL">TBLPROPERTIES ('skip.header.line.count' = '1')
</code></pre>
<p>파라미터를 입력하여 아래와 같이 입력합니다.</p>

<h3 id="query-sample">Query Sample</h3>

<pre><code class="language-SQL">CREATE EXTERNAL TABLE IF NOT EXISTS [TABLE] (
    `COLUMN_01` string,
    `COLUMN_02` int,
    `COLUMN_03` timestamp,
    `COLUMN_04` string
)
ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe'
WITH SERDEPROPERTIES (
    'serialization.format' = ',',
    'field.delim' = ','
) LOCATION 's3://athena-examples-myregion/flight/csv/';
TBLPROPERTIES (
    'skip.header.line.count' = '1'
)
</code></pre>

<p>Athena에서 External Table을 생성해서 테스트해봅시다.
더 이상 파일의 숫자만큼 Header값이 Row에 추가되지 않고 온전한 값이 들어가는 것을 확인할 수 있습니다.</p>

<h3 id="use-case">Use case</h3>
<p><img src="https://images.velog.io/images/haje/post/bd4342cb-b165-4a6d-ab47-ad6163959900/athena-s3.png" alt="" /></p>

<h3 id="참고-문서">참고 문서</h3>
<p><a href="https://docs.aws.amazon.com/athena/latest/ug/lazy-simple-serde.html#lazy-simple-serde-ignoring-headers">AWS Athena Documents - LazySimpleSerDe for CSV, TSV, and Custom-Delimited Files - Ignoring Headers</a></p>]]></content><author><name></name></author><category term="data" /><category term="AWS" /><category term="Athena" /><summary type="html"><![CDATA[Amazon Athena &amp; S3 S3(Simple Storage Service)로 AWS를 대표하는 클라우드 서비스입니다. 객체 스토리지 (Object Storage)라는 형태의 서비스로 스토리지를 추상화한 형태로써 사용자는 데이터가 어느 블럭에 저장되는지 알 필요 없이 편리하게 사용 가능한 스토리지입니다. 물론 OS에 볼륨 마운트하여 사용도 가능합니다! 하지만 I/O 속도는 블럭 스토리지에 비하면 꽤 느린 편입니다. MiniO를 이용하여 객체 스토리지를 구현할 수 있습니다. Athena는 Standard SQL을 이용하여 S3에 저장된 데이터를 간단하게 질의할 수 있는 대화형 쿼리 서비스입니다. HDFS에서 Hive를 이용하여 SQL 질의하는 구조와 유사하구나~ 하고 이해하시면 좋습니다 🥳 Preface Amazon S3에 저장된 여러 CSV 데이터를 Athena에서 테이블을 구성할 때, 1번 라인인 Header가 들어가는 상황을 대처해봅시다. Pre-requirements Amazon Web Services 어카운트 Amazon S3 Amazon Athena Header Skip 설정하기 Athena에서 테이블을 생성하고자 하는 버킷에 저장된 CSV 파일들을 먼저 살펴봅시다. 스키마가 전부 동일한가요? 그렇다면, 아래 파라미터를 추가하여 테이블 생성할 때 헤더를 생성할 수 있도록 설정합시다. Ignoring Headers TBLPROPERTIES ('skip.header.line.count' = '1') 파라미터를 입력하여 아래와 같이 입력합니다. Query Sample CREATE EXTERNAL TABLE IF NOT EXISTS [TABLE] ( `COLUMN_01` string, `COLUMN_02` int, `COLUMN_03` timestamp, `COLUMN_04` string ) ROW FORMAT SERDE 'org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe' WITH SERDEPROPERTIES ( 'serialization.format' = ',', 'field.delim' = ',' ) LOCATION 's3://athena-examples-myregion/flight/csv/'; TBLPROPERTIES ( 'skip.header.line.count' = '1' ) Athena에서 External Table을 생성해서 테스트해봅시다. 더 이상 파일의 숫자만큼 Header값이 Row에 추가되지 않고 온전한 값이 들어가는 것을 확인할 수 있습니다. Use case 참고 문서 AWS Athena Documents - LazySimpleSerDe for CSV, TSV, and Custom-Delimited Files - Ignoring Headers]]></summary></entry><entry><title type="html">Google Vertex AI로 신용카드 이상탐지 AutoML 모델 만들기 2/2</title><link href="https://hajekim.github.io/ai/2021/08/18/post/" rel="alternate" type="text/html" title="Google Vertex AI로 신용카드 이상탐지 AutoML 모델 만들기 2/2" /><published>2021-08-18T18:00:00+09:00</published><updated>2021-08-18T18:00:00+09:00</updated><id>https://hajekim.github.io/ai/2021/08/18/post</id><content type="html" xml:base="https://hajekim.github.io/ai/2021/08/18/post/"><![CDATA[<blockquote>
  <p><em>** Google Vertex AI 간단한 소개 **</em>
<img src="https://images.velog.io/images/haje/post/43164411-4675-4d98-acaa-ed84658341ba/1_aeXlwnOS3DvVHiMVgBZbpQ.png" alt="" />
Google Cloud의 AI Platform, AutoML 등 다양한 머신러닝 서비스를 하나의 인터페이스(UI / API)에서 사용할 수 있도록 구성된 종합선물 과자세트 같은 서비스입니다.
데이터셋 구성부터 모델 생성, 학습, 테스트, 검증하고 애플리케이션에서 Endpoint로 사용할 수 있도록 배포까지 하나의 파이프라인으로 구성 가능합니다.
API를 이용하여 Jupyter Notebook에서 구성할 수도 있고, 클라우드 엔지니어가 직접 UI에서 AutoML을 이용하여 개발할 수도 있습니다.</p>
</blockquote>

<h1 id="preface">Preface</h1>
<h3 id="지난-이야기">지난 이야기</h3>
<p>지난 포스팅에서는 신용카드 이상탐지 분류 모델을 AutoML을 이용하여 만들고 훈련(Train)을 시켜보았습니다.
이어서 모델을 평가하고 컨테이너로 배포하여 엔드포인트로 서비스를 테스트 해봅시다.</p>

<h2 id="pre-hands-on-requirements">Pre Hands-On Requirements</h2>
<ul>
  <li>Google Cloud Platform 계정 (Google 계정으로 무료 크레딧을 받아봅시다!)</li>
  <li><del>아주 약간의 머신러닝 지식</del>
    <ul>
      <li>Confusion Matrix (혼동행렬)</li>
      <li>Feature Importance</li>
      <li>Precision</li>
      <li>Recall</li>
      <li>ROC Curve</li>
    </ul>
  </li>
  <li><del>아주 약간의 컴퓨터 사이언스 지식</del>
    <ul>
      <li>Application Programming Interface</li>
      <li>컨테이너🐳</li>
      <li>Python 쓸 줄 안다</li>
      <li>Jupyter Notebook 가 뭔지 안다</li>
    </ul>
  </li>
  <li><del>아주 약간의 클라우드 컴퓨팅 지식</del>
    <ul>
      <li>CLI 쓸 줄 안다</li>
    </ul>
  </li>
  <li>Vertex AI에 Fraud Detection 모델
    <ul>
      <li><a href="https://velog.io/@haje/Vertex-AI-AutoML-%EC%82%AC%EA%B8%B0-%ED%83%90%EC%A7%80-%EB%AA%A8%EB%8D%B8-%EA%B5%AC%EC%B6%95">Google Vertex AI로 신용카드 이상탐지 AutoML 모델 만들기 1/2편</a>를 먼저 하고 오세요!</li>
    </ul>
  </li>
</ul>

<h1 id="hands-on">Hands-On</h1>
<h2 id="6-모델-평가하기">6. 모델 평가하기</h2>
<p>AutoML을 통해 만든 Classification 분류 모델을 학습을 약 1시간 22분 걸려서 완료하였습니다.
머신러닝 모델을 학습하면 일반적으로는 평가라는 단계를 거칩니다. 이 모델의 성능이 잘 나왔는지, 살펴보는 검증의 시간인 것이지요.
자, 학습된 모델을 클릭해서 들어가봅시다!
<img src="https://images.velog.io/images/haje/post/ee2733cb-fce0-434c-bbf0-e22a6b29e64f/image.png" alt="" />
Evaluate 탭이 첫 화면으로 보일 거에요.
BI 화면도 아닌 것이 무슨 그래프 여러개와 알파벳과 숫자들의 향연을 마주하시게 될 것입니다.
<img src="https://images.velog.io/images/haje/post/b860d21b-8e35-41e3-a11f-ad0d29759e37/image.png" alt="" /></p>

<p>여러분들은 당황한 티 내지 마시고 
우선 말 없이 고개를 두 번 끄덕입시다.
<img src="https://images.velog.io/images/haje/post/ce4cba8e-a7e6-4e73-8d44-d4d256449147/image.png" alt="" />
머신러닝도 잘 하는구나 하고 사람들이 생각할 것입니다.
<del>좋아 자연스러웠어</del></p>

<h3 id="61-confusion-matrix-이해하기">6.1. Confusion Matrix 이해하기</h3>
<p>먼저 Classification 모델에서 평가지표로 사용되는 Confusion Matrix입니다.
아! 맞다 그 전에 왜 머신러닝 용어들은 긴 영어 단어를 굳이 꿋꿋하게 쓰고 있는지 설명 드릴게요!</p>

<p><strong><em>매우매우매우 중요합니다!!</em></strong></p>

<p>머신러닝 논문을 읽거나 사람들과 대화할 때 영어 단어를 자주 사용하는 편입니다.
그러니까 우리도 머신러닝 스페셜리스트인냥 위장을 위해 익숙해집시다.</p>

<p>다시 우리가 만든 모델의 Confustion Matrix를 살펴봅시다!
<img src="https://images.velog.io/images/haje/post/9bfda264-bc12-4d43-85cf-1b99be058516/image.png" alt="" /></p>

<p>각 항에는 True label, Predicted label이 있고 0,1이 써있습니다.
Classification은 지도학습으로 답(label)을 알려주고 예측을 시키는 방법입니다.
우리가 예측하고 싶은 이상여부의 실제값(True label)과 예측값(Predicted label)을 비교해서 정확도를 나타내는 것입니다.</p>

<ul>
  <li>실제 0인데, 예측도 0인 확률 (정답입니다)</li>
  <li>실제 0인데, 예측이 1인 확률</li>
  <li>실제 1인데, 예측이 0인 확률</li>
  <li>실제 1인데, 예측도 1인 확률 (정답입니다)</li>
</ul>

<p>순서대로 TP(True Positive),FP(False Positive), FN(False Negative), TN(True Negative)라고 부릅니다.
그리고 이 항목들을 요리조리 더하고 나누면 Accuracy, Precision, Recall 항목을 도출할 수 있습니다.</p>

<p>이야기를 더하고 싶지만 <del>사실 어려워서 도망</del>,
제가 머리가 아픈 관계로 다음에 설명 드리도록 하고 모델 평가를 이어 하겠습니다.</p>

<p><img src="https://images.velog.io/images/haje/post/46e9ecbf-5f7d-4fac-96b8-4864de08bef6/image.png" alt="" /></p>

<p>성능이 엄청나군요!!
정상 처리인 0을 100% 맞췄습니다.
그리고 사기 케이스인 1을 87% 맞췄습니다.</p>

<p>축하합니다! 🎉🎉🎉
여러분은 자신도 모르는 사이에 이미 훌륭한 머신러닝 스페셜리스트로 진화하였습니다.👩‍🏫</p>

<h3 id="62-feature-importance">6.2. Feature Importance</h3>
<p><img src="https://images.velog.io/images/haje/post/1e8bc882-0cf3-4641-ae24-091516fa0c7a/image.png" alt="" /></p>

<p>Confusion Matrix 밑에 요 그래프가 보이실 거에요.
모델에서 예측할 때 가장 영향이 높았던 Feature들입니다.
실제 데이터를 비식별화하여 Feature 이름만 보고 알 수 없지만,
가지고 계신 데이터를 이용해서 돌려보면, 이해도가 높기 때문에 데이터 이해와 통찰력을 더 빠르게 얻을 수 있습니다.</p>

<h3 id="63-rocreceiver-operating-characteristic-curve">6.3. ROC(Receiver operating characteristic) Curve</h3>
<p><img src="https://images.velog.io/images/haje/post/19e3d555-5272-4dc3-a253-ad13c41a5fe8/image.png" alt="" />
설명하기 위해서 찾아봤더니 한국말로는 _수신자 조작 특성_이라고 부르나 봐요.
<img src="https://images.velog.io/images/haje/post/c05dec22-4169-4f05-ac06-dd3fe24f30a1/image.png" alt="" />
괜찮습니다. 몰라도 되요.</p>

<p>다 필요 없고 ROC Curve라는 그래프 선이 왼쪽 상단 모서리에 가까운 모양일수록 성능이 좋다는 뜻입니다.
ROC Curve 그래프가 그리는 선 아래에 넓이를 AUC라고 부르고, Curve 그래프와 달리, 넓이로 수치화하여 성능을 표현할 수 있기 떄문에 사용합니다.
1에 가까울 수록 좋은 겁니다.</p>

<p>우리가 만든 모델의 ROC Curve를 봅시다.
<strong>왼쪽 상단 모서리를 찍었습니다.
값은 1입니다.</strong></p>

<p>다시 한 번 말씀 드리지만,
여러분은 엄청난 Classification 모델을 만들었습니다.</p>

<p><img src="https://images.velog.io/images/haje/post/d461d790-47b7-49bb-a6f1-fb832478ed8a/image.png" alt="" />
대단하시니까, 쌍따봉 드립니다.</p>

<h2 id="7-엔드포인트-배포">7. 엔드포인트 배포</h2>
<p>훈련과 평가가 끝난 이 훌륭한 모델을 사용해보도록 합시다!
과거 Monolithic 애플리케이션에서는 배포를 위해 중단이 필요했습니다.
하지만 엔드포인트로 통신하며 업데이트된 모델을 중단 없이 바로 사용할 수 있도록 엔드포인트 배포를 해보도록 하겠습니다.</p>

<p><img src="https://images.velog.io/images/haje/post/8967f357-3e30-44a8-bfd7-cab70473d5f3/image.png" alt="" />
모델에서 DEPLOY &amp; TEST를 클릭합니다.</p>

<p><img src="https://images.velog.io/images/haje/post/c0ab3a2d-0db3-4de0-8c0e-9a2667d9a7f3/image.png" alt="" />
그리고 Deploy your model 섹션에 위치한 DEPLOY TO ENDPOINT를 클릭합니다.</p>

<p><img src="https://images.velog.io/images/haje/post/3c32bac6-2a61-4049-aec2-60d9e8c3765a/image.png" alt="" />
Endpoint name은 적절하게 명명하고 CONTINUE를 클릭합니다.
저는 엄청난 모델을 만든 기쁨을 표현해보았습니다.</p>

<p><img src="https://images.velog.io/images/haje/post/935e95b5-ce0f-4167-afe6-5e620d5ab418/image.png" alt="" />
ADVANCED SCALING OPTIONS에서 적절한 Machine type을 선택합니다.</p>

<p>저는 돈을 아끼고 싶기 때문에 선택지에서 가장 작은 n1-standard-2를 선택하고 DEPLOY 하겠습니다.</p>

<h2 id="8-api로-서비스-테스트하기">8. API로 서비스 테스트하기</h2>
<p>모델을 엔드포인트로 배포 되었나요?
이제 여러분의 엄청난 모델을 서비스 할 시간입니다.</p>

<p>아까 만든 Jupyter Notebook을 가볼까요
<img src="https://images.velog.io/images/haje/post/5e7a518e-5a81-4ace-8fdb-102dd33f2e1b/image.png" alt="" />
OPEN JUPYTERLAB을 클릭합니다.</p>

<p><img src="https://images.velog.io/images/haje/post/48e5cbf8-d0f2-4287-985a-ca2ccd29dd29/image.png" alt="" />
그 다음에 Notebook &gt; Python 3를 클릭합니다.</p>

<p>빈 칸에 아래 코드를 입력한 다음 OPTION + ENTER (ALT + ENTER)를 쳐봅니다.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>!pip3 install google-cloud-aiplatform --upgrade --user
</code></pre></div></div>

<p>해당 라인이 실행되면서 다음 칸이 추가됩니다.</p>

<p><img src="https://images.velog.io/images/haje/post/efdf2791-b39d-433f-9231-ec0208829160/image.png" alt="" /></p>

<p>그 다음 칸에 아래 코드를 넣고 Project number와 Endpoint ID를 복사해서 수정합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>from google.cloud import aiplatform

endpoint = aiplatform.Endpoint(
    endpoint_name="projects/YOUR-PROJECT-NUMBER/locations/us-central1/endpoints/YOUR-ENDPOINT-ID"
)
</code></pre></div></div>

<p><img src="https://images.velog.io/images/haje/post/48499152-cceb-43fa-aafa-70b4a3bd25a0/image.png" alt="" />
프로젝트 번호는 상단 우측에 콜론이 진화한 <strong>점 세 개 클릭 &gt; Project settings</strong> 클릭하면 아래와 같이 Project number를 확인할 수 있습니다.</p>

<p><img src="https://images.velog.io/images/haje/post/837627f3-6245-4a95-84f6-1acedf95c54f/image.png" alt="" /></p>

<p>Endpoint ID는 <strong>Vertex AI &gt; Endpoints</strong> 를 클릭합니다.
생성한 Endpoint의 ID가 보입니다.
<img src="https://images.velog.io/images/haje/post/a5316214-f951-4904-8c82-072ae2952ae0/image.png" alt="" /></p>

<p>위의 코드에 수정해서 넣은 후에 OPTION + ENTER</p>

<p>실행되고 새로 추가된 칸에 아래 테스트 데이터를 넣고 실행합니다.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>test_instance={
    'Time': 80422,
    'Amount': 17.99,
    'V1': -0.24,
    'V2': -0.027,
    'V3': 0.064,
    'V4': -0.16,
    'V5': -0.152,
    'V6': -0.3,
    'V7': -0.03,
    'V8': -0.01,
    'V9': -0.13,
    'V10': -0.18,
    'V11': -0.16,
    'V12': 0.06,
    'V13': -0.11,
    'V14': 2.1,
    'V15': -0.07,
    'V16': -0.033,
    'V17': -0.14,
    'V18': -0.08,
    'V19': -0.062,
    'V20': -0.08,
    'V21': -0.06,
    'V22': -0.088,
    'V23': -0.03,
    'V24': 0.01,
    'V25': -0.04,
    'V26': -0.99,
    'V27': -0.13,
    'V28': 0.003
}

response = endpoint.predict([test_instance])

print('API response: ', response)
</code></pre></div></div>

<p>Tabular 데이터로 생각해보면 위의 JSON은 Row 1개입니다.</p>

<p>그 다음에 
Row 1개의 데이터를 test_instance 변수에 담고
모델이 배포된 엔드포인트에 API로 던져서 결과를 받아봅시다.</p>

<p><img src="https://images.velog.io/images/haje/post/178bf584-5e1d-462a-b4d0-0acb1d8fd94b/image.png" alt="" /></p>

<p>Score가 매우 높습니다.
축하합니다!</p>

<p>이제 나의 돈 또는 크레딧을 지키기 위해 엔드포인트와 Jupyter Notebook을 삭제합시다!</p>

<p>프로덕션 환경이라면 그대로 유지하시면 됩니다 ㅋㅋ</p>

<p>다음에는 직접 개발한 ML Code로 파이프라인을 만들어보도록 하겠습니다.
고생 많으셨습니다! 🥳</p>

<h1 id="참고문서">참고문서</h1>
<ul>
  <li><a href="https://codelabs.developers.google.com/vertex-automl-tabular">Vertex AI:Building a fraud detection model with AutoML</a></li>
  <li><a href="https://cloud.google.com/vertex-ai/docs/start/introduction-unified-platform?hl=ko">Vertex AI 소개</a></li>
</ul>]]></content><author><name></name></author><category term="ai" /><category term="Google Cloud" /><category term="머신러닝" /><summary type="html"><![CDATA[** Google Vertex AI 간단한 소개 ** Google Cloud의 AI Platform, AutoML 등 다양한 머신러닝 서비스를 하나의 인터페이스(UI / API)에서 사용할 수 있도록 구성된 종합선물 과자세트 같은 서비스입니다. 데이터셋 구성부터 모델 생성, 학습, 테스트, 검증하고 애플리케이션에서 Endpoint로 사용할 수 있도록 배포까지 하나의 파이프라인으로 구성 가능합니다. API를 이용하여 Jupyter Notebook에서 구성할 수도 있고, 클라우드 엔지니어가 직접 UI에서 AutoML을 이용하여 개발할 수도 있습니다. Preface 지난 이야기 지난 포스팅에서는 신용카드 이상탐지 분류 모델을 AutoML을 이용하여 만들고 훈련(Train)을 시켜보았습니다. 이어서 모델을 평가하고 컨테이너로 배포하여 엔드포인트로 서비스를 테스트 해봅시다. Pre Hands-On Requirements Google Cloud Platform 계정 (Google 계정으로 무료 크레딧을 받아봅시다!) 아주 약간의 머신러닝 지식 Confusion Matrix (혼동행렬) Feature Importance Precision Recall ROC Curve 아주 약간의 컴퓨터 사이언스 지식 Application Programming Interface 컨테이너🐳 Python 쓸 줄 안다 Jupyter Notebook 가 뭔지 안다 아주 약간의 클라우드 컴퓨팅 지식 CLI 쓸 줄 안다 Vertex AI에 Fraud Detection 모델 Google Vertex AI로 신용카드 이상탐지 AutoML 모델 만들기 1/2편를 먼저 하고 오세요! Hands-On 6. 모델 평가하기 AutoML을 통해 만든 Classification 분류 모델을 학습을 약 1시간 22분 걸려서 완료하였습니다. 머신러닝 모델을 학습하면 일반적으로는 평가라는 단계를 거칩니다. 이 모델의 성능이 잘 나왔는지, 살펴보는 검증의 시간인 것이지요. 자, 학습된 모델을 클릭해서 들어가봅시다! Evaluate 탭이 첫 화면으로 보일 거에요. BI 화면도 아닌 것이 무슨 그래프 여러개와 알파벳과 숫자들의 향연을 마주하시게 될 것입니다. 여러분들은 당황한 티 내지 마시고 우선 말 없이 고개를 두 번 끄덕입시다. 머신러닝도 잘 하는구나 하고 사람들이 생각할 것입니다. 좋아 자연스러웠어 6.1. Confusion Matrix 이해하기 먼저 Classification 모델에서 평가지표로 사용되는 Confusion Matrix입니다. 아! 맞다 그 전에 왜 머신러닝 용어들은 긴 영어 단어를 굳이 꿋꿋하게 쓰고 있는지 설명 드릴게요! 매우매우매우 중요합니다!! 머신러닝 논문을 읽거나 사람들과 대화할 때 영어 단어를 자주 사용하는 편입니다. 그러니까 우리도 머신러닝 스페셜리스트인냥 위장을 위해 익숙해집시다. 다시 우리가 만든 모델의 Confustion Matrix를 살펴봅시다! 각 항에는 True label, Predicted label이 있고 0,1이 써있습니다. Classification은 지도학습으로 답(label)을 알려주고 예측을 시키는 방법입니다. 우리가 예측하고 싶은 이상여부의 실제값(True label)과 예측값(Predicted label)을 비교해서 정확도를 나타내는 것입니다. 실제 0인데, 예측도 0인 확률 (정답입니다) 실제 0인데, 예측이 1인 확률 실제 1인데, 예측이 0인 확률 실제 1인데, 예측도 1인 확률 (정답입니다) 순서대로 TP(True Positive),FP(False Positive), FN(False Negative), TN(True Negative)라고 부릅니다. 그리고 이 항목들을 요리조리 더하고 나누면 Accuracy, Precision, Recall 항목을 도출할 수 있습니다. 이야기를 더하고 싶지만 사실 어려워서 도망, 제가 머리가 아픈 관계로 다음에 설명 드리도록 하고 모델 평가를 이어 하겠습니다. 성능이 엄청나군요!! 정상 처리인 0을 100% 맞췄습니다. 그리고 사기 케이스인 1을 87% 맞췄습니다. 축하합니다! 🎉🎉🎉 여러분은 자신도 모르는 사이에 이미 훌륭한 머신러닝 스페셜리스트로 진화하였습니다.👩‍🏫 6.2. Feature Importance Confusion Matrix 밑에 요 그래프가 보이실 거에요. 모델에서 예측할 때 가장 영향이 높았던 Feature들입니다. 실제 데이터를 비식별화하여 Feature 이름만 보고 알 수 없지만, 가지고 계신 데이터를 이용해서 돌려보면, 이해도가 높기 때문에 데이터 이해와 통찰력을 더 빠르게 얻을 수 있습니다. 6.3. ROC(Receiver operating characteristic) Curve 설명하기 위해서 찾아봤더니 한국말로는 _수신자 조작 특성_이라고 부르나 봐요. 괜찮습니다. 몰라도 되요. 다 필요 없고 ROC Curve라는 그래프 선이 왼쪽 상단 모서리에 가까운 모양일수록 성능이 좋다는 뜻입니다. ROC Curve 그래프가 그리는 선 아래에 넓이를 AUC라고 부르고, Curve 그래프와 달리, 넓이로 수치화하여 성능을 표현할 수 있기 떄문에 사용합니다. 1에 가까울 수록 좋은 겁니다. 우리가 만든 모델의 ROC Curve를 봅시다. 왼쪽 상단 모서리를 찍었습니다. 값은 1입니다. 다시 한 번 말씀 드리지만, 여러분은 엄청난 Classification 모델을 만들었습니다. 대단하시니까, 쌍따봉 드립니다. 7. 엔드포인트 배포 훈련과 평가가 끝난 이 훌륭한 모델을 사용해보도록 합시다! 과거 Monolithic 애플리케이션에서는 배포를 위해 중단이 필요했습니다. 하지만 엔드포인트로 통신하며 업데이트된 모델을 중단 없이 바로 사용할 수 있도록 엔드포인트 배포를 해보도록 하겠습니다. 모델에서 DEPLOY &amp; TEST를 클릭합니다. 그리고 Deploy your model 섹션에 위치한 DEPLOY TO ENDPOINT를 클릭합니다. Endpoint name은 적절하게 명명하고 CONTINUE를 클릭합니다. 저는 엄청난 모델을 만든 기쁨을 표현해보았습니다. ADVANCED SCALING OPTIONS에서 적절한 Machine type을 선택합니다. 저는 돈을 아끼고 싶기 때문에 선택지에서 가장 작은 n1-standard-2를 선택하고 DEPLOY 하겠습니다. 8. API로 서비스 테스트하기 모델을 엔드포인트로 배포 되었나요? 이제 여러분의 엄청난 모델을 서비스 할 시간입니다. 아까 만든 Jupyter Notebook을 가볼까요 OPEN JUPYTERLAB을 클릭합니다. 그 다음에 Notebook &gt; Python 3를 클릭합니다. 빈 칸에 아래 코드를 입력한 다음 OPTION + ENTER (ALT + ENTER)를 쳐봅니다. !pip3 install google-cloud-aiplatform --upgrade --user 해당 라인이 실행되면서 다음 칸이 추가됩니다. 그 다음 칸에 아래 코드를 넣고 Project number와 Endpoint ID를 복사해서 수정합니다. from google.cloud import aiplatform endpoint = aiplatform.Endpoint( endpoint_name="projects/YOUR-PROJECT-NUMBER/locations/us-central1/endpoints/YOUR-ENDPOINT-ID" ) 프로젝트 번호는 상단 우측에 콜론이 진화한 점 세 개 클릭 &gt; Project settings 클릭하면 아래와 같이 Project number를 확인할 수 있습니다. Endpoint ID는 Vertex AI &gt; Endpoints 를 클릭합니다. 생성한 Endpoint의 ID가 보입니다. 위의 코드에 수정해서 넣은 후에 OPTION + ENTER 실행되고 새로 추가된 칸에 아래 테스트 데이터를 넣고 실행합니다. test_instance={ 'Time': 80422, 'Amount': 17.99, 'V1': -0.24, 'V2': -0.027, 'V3': 0.064, 'V4': -0.16, 'V5': -0.152, 'V6': -0.3, 'V7': -0.03, 'V8': -0.01, 'V9': -0.13, 'V10': -0.18, 'V11': -0.16, 'V12': 0.06, 'V13': -0.11, 'V14': 2.1, 'V15': -0.07, 'V16': -0.033, 'V17': -0.14, 'V18': -0.08, 'V19': -0.062, 'V20': -0.08, 'V21': -0.06, 'V22': -0.088, 'V23': -0.03, 'V24': 0.01, 'V25': -0.04, 'V26': -0.99, 'V27': -0.13, 'V28': 0.003 } response = endpoint.predict([test_instance]) print('API response: ', response) Tabular 데이터로 생각해보면 위의 JSON은 Row 1개입니다. 그 다음에 Row 1개의 데이터를 test_instance 변수에 담고 모델이 배포된 엔드포인트에 API로 던져서 결과를 받아봅시다. Score가 매우 높습니다. 축하합니다! 이제 나의 돈 또는 크레딧을 지키기 위해 엔드포인트와 Jupyter Notebook을 삭제합시다! 프로덕션 환경이라면 그대로 유지하시면 됩니다 ㅋㅋ 다음에는 직접 개발한 ML Code로 파이프라인을 만들어보도록 하겠습니다. 고생 많으셨습니다! 🥳 참고문서 Vertex AI:Building a fraud detection model with AutoML Vertex AI 소개]]></summary></entry><entry><title type="html">Install Kubeflow on Minikube - v20210817</title><link href="https://hajekim.github.io/container/2021/08/17/post/" rel="alternate" type="text/html" title="Install Kubeflow on Minikube - v20210817" /><published>2021-08-17T18:00:00+09:00</published><updated>2021-08-17T18:00:00+09:00</updated><id>https://hajekim.github.io/container/2021/08/17/post</id><content type="html" xml:base="https://hajekim.github.io/container/2021/08/17/post/"><![CDATA[<blockquote>
  <h3 id="kubeflow-간단한-소개">Kubeflow 간단한 소개</h3>
  <p><img src="https://upload.wikimedia.org/wikipedia/en/2/21/Kubeflow-logo.png" alt="" />
머신러닝 워크플로우를 Kubernetes에서 손쉽게 이식하고 확장 가능하게 배포 및 운영을 목적으로 시작된 프로젝트입니다.
Google 내부에서 서비스에 사용하기 위해 개발된 서비스가 오픈소스 프로젝트로 릴리즈되어 시작하게 되었습니다.
국내는 <a href="https://medium.com/daangn/kubeflow-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EC%9A%B4%EC%9A%A9%ED%95%98%EA%B8%B0-6c6d7bc98c30">당근마켓</a>, <a href="https://tech.socarcorp.kr/data/2020/03/10/ml-model-serving.html">SOCAR</a> 등 머신러닝/딥러닝이 응용되는 애플리케이션 서비스에서 널리 적용하고 사용하고 있는 ML Model Serving Tool입니다.</p>
</blockquote>

<h1 id="preface">Preface</h1>
<p>Kubernetes 클러스터에 배포할 수 있습니다.
우선 이 문서에서는 Minikube에 간단하게 구성하도록 하겠습니다.
다음 포스팅에서 CentOS7에 K8s 클러스터를 구성하고 설치해보도록 하겠습니다.</p>

<h2 id="pre-installation-requirements">Pre-Installation Requirements</h2>
<ul>
  <li>kubectl</li>
  <li>kustomize</li>
  <li>passlib (pip3)</li>
  <li>py_bcrypt (pip3)</li>
  <li>Minikube</li>
</ul>

<h2 id="minikube-spec">Minikube Spec</h2>
<p>제가 구성한 Minikube의 스펙입니다.</p>
<ul>
  <li>4 CPUs</li>
  <li>8GB Memory</li>
  <li>40GB Storage</li>
</ul>

<p>macOS에서 Homebrew를 이용하여 손쉽게 설치할 수 있습니다.
CentOS나 Ubuntu에서도 패키지 매니저를 이용하여 손쉽게 설치해봅시다!</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Intsall Minikube</span>
brew <span class="nb">install </span>minikube

<span class="c"># Start Minikube</span>
minikube start <span class="nt">--cpus</span> 4 <span class="nt">--memory</span> 8096 <span class="nt">--disk-size</span><span class="o">=</span>40g
</code></pre></div></div>

<h2 id="software-version">Software Version</h2>

<p>본 문서에서 사용된 K8s와 Kubeflow의 버전입니다.</p>

<ul>
  <li>Kubeflow 1.3.0</li>
  <li>Kubernetes 1.21.2</li>
</ul>

<p>과거 이 문서가 Kubeflow 1.15 버전을 사용할 때와는 좀 차이가 있습니다. <strong>Kubeflow 1.3.0</strong>에서는 버전 1.1x와 다르게 <strong><a href="https://kubernetes.io/ko/docs/tasks/manage-kubernetes-objects/kustomization/">kustomize</a></strong>를 이용하여 설치하고 관리합니다.</p>

<p>Kustomize도 설치해보도록 합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>kustomize
</code></pre></div></div>

<h2 id="components-of-kubeflow-manifests">Components of Kubeflow Manifests</h2>
<p>아래의 컴포넌트가 포함되어 있습니다.
모두 설치할 수도 있고, 필요한 것만 개별로 설치할 수도 있습니다.</p>
<ul>
  <li>TFJob Operator</li>
  <li>PyTorch Operator</li>
  <li>MPI Operator</li>
  <li>MXNet Operator</li>
  <li>XGBoost Operator</li>
  <li>Notebook Controller</li>
  <li>Tensorboard Controller</li>
  <li>Central Dashboard</li>
  <li>Profiles + KFAM</li>
  <li>PodDefaults Webhook</li>
  <li>Jupyter Web App</li>
  <li>Tensorboards Web App</li>
  <li>Volumes Web App</li>
  <li>Katib</li>
  <li>KFServing</li>
  <li>Kubeflow Pipelines</li>
  <li>Kubeflow Tekton Pipelines</li>
</ul>

<h1 id="설치하기">설치하기</h1>

<h2 id="01-kubeflow-manifest-준비">01. Kubeflow Manifest 준비</h2>

<p>본 문서에서 설치에 사용될 바이너리를 준비합니다. <a href="https://github.com/kubeflow/manifests">Kubeflow Manifest</a>를 사용할 것입니다.
여러 컴포넌트가 준비가 되어 있으므로, 손쉽게 설치가 가능합니다. 또한 불필요한 컴포넌트를 제외하고 필요한 컴포넌트만 메뉴얼하게 설치도 가능합니다.</p>

<p>우선, GitHub에서 클론으로 가져와 manifests 디렉토리에 들어가도록 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/kubeflow/manifests
<span class="nb">cd </span>manifests
</code></pre></div></div>

<h2 id="02-dex-수정하기">02. Dex 수정하기</h2>
<p>OIDC(OpenID Connect) Identity Service인 <a href="https://github.com/dexidp/dex">Dex</a>를 통해 Kubernetes 사용자 인증을 관리합니다.
그런데 이슈가 하나 있습니다. Kubeflow 1.21 이후 버전에서는 Dex Manifest가 작동하지 않는 이슈가 있습니다.
해당 이슈: <a href="https://github.com/kubeflow/manifests/pull/1883">Fix dex for Kubernetes 1.21 #1883</a></p>

<p>해결 방법은 간단합니다.
문서를 보면 2가지 해결안을 제시(snap or yaml)하는데, snap을 이용하는 방법보다 쉬운 yaml 수정을 하도록 하겠습니다.</p>

<p><del>사실 macOS에서 snapd가 제대로 동작을 안하네요…</del></p>

<p>아래의 파일에서 31번째 줄에 추가해주세요.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vi manifests/common/dex/base/deployment.yaml
</code></pre></div></div>
<p>파일을 수정할 때 31번 라인에 아래의 NAMESPACE 설정을 추가해주도록 합니다.
띄어쓰기를 반드시 신경써서 넣어줍시다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="nb">env</span>:
        - name: KUBERNETES_POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
</code></pre></div></div>
<p>아래 이미지와 같이 보이면 됩니다.
<img src="https://images.velog.io/images/haje/post/3d8a1f1c-e9f0-4748-9565-3ea7189d5422/image.png" alt="" /></p>

<h2 id="02-1-dex-사용자-수정하기-생략-가능">02-1. Dex 사용자 수정하기 (생략 가능)</h2>
<p>Kubeflow Manifests에 설정된 Default User와 Password는 아래와 같습니다.</p>
<ul>
  <li>user@example.com</li>
  <li>12341234</li>
</ul>

<p>하지만 수정해보도록 하겠습니다.
<strong>bcrypt</strong>를 이용하여 비밀번호 Hash를 생성합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-c</span> <span class="s1">'from passlib.hash import bcrypt; import getpass; print(bcrypt.using(rounds=12, ident="2y").hash(getpass.getpass()))'</span>
</code></pre></div></div>
<p><img src="https://images.velog.io/images/haje/post/1462d6b8-ef68-422e-a512-6f5dedde29b3/image.png" alt="" />
마지막 라인의 Hash값을 복사하여 아래 파일에 입력합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vi manifests/common/dex/base/config-map.yaml
</code></pre></div></div>

<p>21번 라인에 가서 User를 수정하고 Hash를 입력합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code> 21     - email: hello@kubeflow.com
 22       <span class="nb">hash</span>: 요기에 입력
</code></pre></div></div>

<h2 id="03-설치하기">03. 설치하기</h2>
<p>기본적인 세팅은 끝났습니다.
이제 간편하게 명령어 한 줄로 Kubeflow Manifests를 Minikube에 구성해보도록 하겠습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">while</span> <span class="o">!</span> kustomize build example | kubectl apply <span class="nt">-f</span> -<span class="p">;</span> <span class="k">do </span><span class="nb">echo</span> <span class="s2">"Retrying to apply resources"</span><span class="p">;</span> <span class="nb">sleep </span>10<span class="p">;</span> <span class="k">done</span>
</code></pre></div></div>

<h2 id="04-pods-상태-확인">04. Pods 상태 확인</h2>
<p>배포 요청한 컴포넌트들의 Pods가 정상적으로 떠있는지 확인해보도록 합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">-A</span>
NAMESPACE                   NAME                                                         READY   STATUS    RESTARTS   AGE
auth                        dex-5ddf47d88d-w5vbc                                         1/1     Running   1          164m
cert-manager                cert-manager-7dd5854bb4-ckkqz                                1/1     Running   0          3h3m
cert-manager                cert-manager-cainjector-64c949654c-n72pz                     1/1     Running   1          3h3m
cert-manager                cert-manager-webhook-6b57b9b886-h7htl                        1/1     Running   1          3h3m
istio-system                authservice-0                                                1/1     Running   0          174m
istio-system                cluster-local-gateway-75cb7c6c88-kw59j                       1/1     Running   0          174m
istio-system                istio-ingressgateway-79b665c95-8rsss                         1/1     Running   0          3h2m
istio-system                istiod-86457659bb-gqzng                                      1/1     Running   0          3h2m
knative-eventing            eventing-controller-575584745f-p8hhs                         1/1     Running   0          174m
knative-eventing            eventing-webhook-6d6f75c565-7zn7n                            1/1     Running   1          174m
knative-eventing            imc-controller-c8d86c869-qp5pj                               1/1     Running   0          174m
knative-eventing            imc-dispatcher-7bf75b8999-fndtx                              1/1     Running   0          174m
knative-eventing            mt-broker-controller-7778d47797-sbg2t                        1/1     Running   0          174m
knative-eventing            mt-broker-filter-857c746446-hzddr                            1/1     Running   1          174m
knative-eventing            mt-broker-ingress-685cd6b57-sz6vr                            1/1     Running   1          174m
knative-serving             activator-859796b66c-r5zsr                                   2/2     Running   2          174m
knative-serving             autoscaler-565454fb69-4kgpj                                  2/2     Running   2          174m
knative-serving             controller-dd58865b5-dlwg6                                   2/2     Running   1          174m
knative-serving             istio-webhook-68fddcc567-n7pzq                               2/2     Running   1          174m
knative-serving             networking-istio-5664b9fb9c-jlq7q                            2/2     Running   1          174m
knative-serving             webhook-6c8b54d9-nkcq8                                       2/2     Running   2          174m
kube-system                 coredns-558bd4d5db-qdl2g                                     1/1     Running   1          3h19m
kube-system                 etcd-minikube                                                1/1     Running   0          3h19m
kube-system                 kube-apiserver-minikube                                      1/1     Running   0          3h19m
kube-system                 kube-controller-manager-minikube                             1/1     Running   0          3h19m
kube-system                 kube-proxy-7qwz6                                             1/1     Running   0          3h19m
kube-system                 kube-scheduler-minikube                                      1/1     Running   0          3h19m
kube-system                 storage-provisioner                                          1/1     Running   1          3h19m
kubeflow-user-example-com   ml-pipeline-ui-artifact-767659f9df-8p8ht                     2/2     Running   0          134m
kubeflow-user-example-com   ml-pipeline-visualizationserver-6ff9f47c6b-52lpd             2/2     Running   0          134m
kubeflow                    admission-webhook-deployment-f5d8f47f8-j6spk                 1/1     Running   0          172m
kubeflow                    cache-deployer-deployment-6dbb64ddcd-sn42c                   2/2     Running   1          173m
kubeflow                    cache-server-79d58845f5-6szhs                                2/2     Running   0          173m
kubeflow                    centraldashboard-9846cbb75-d2n2c                             1/1     Running   1          172m
kubeflow                    jupyter-web-app-deployment-554975dd5d-t5jbn                  1/1     Running   0          172m
kubeflow                    katib-controller-7b98cd6865-7khbj                            1/1     Running   0          172m
kubeflow                    katib-db-manager-7f5f684dd5-rjk5z                            1/1     Running   1          172m
kubeflow                    katib-mysql-85fc9c74b8-r4j2j                                 1/1     Running   1          172m
kubeflow                    katib-ui-64fbdf4d94-kc88w                                    1/1     Running   0          172m
kubeflow                    kfserving-controller-manager-0                               2/2     Running   0          173m
kubeflow                    kubeflow-pipelines-profile-controller-596b896f8d-jjgfr       1/1     Running   0          173m
kubeflow                    metacontroller-0                                             1/1     Running   0          173m
kubeflow                    metadata-envoy-deployment-95b58bbbb-xcl2t                    1/1     Running   0          173m
kubeflow                    metadata-grpc-deployment-c8f784fdf-n8cds                     2/2     Running   3          173m
kubeflow                    metadata-writer-76b6b98985-jb8md                             2/2     Running   1          173m
kubeflow                    minio-5b65df66c9-4hjx6                                       2/2     Running   0          173m
kubeflow                    ml-pipeline-5c5d8f4959-4bb5d                                 2/2     Running   3          173m
kubeflow                    ml-pipeline-persistenceagent-6ff46967ff-k4b4s                2/2     Running   1          173m
kubeflow                    ml-pipeline-scheduledworkflow-66bdf9948d-vqdzx               2/2     Running   0          173m
kubeflow                    ml-pipeline-ui-57fdfc58cc-mdnvr                              2/2     Running   1          173m
kubeflow                    ml-pipeline-viewer-crd-64dddf4597-p9xb9                      2/2     Running   1          173m
kubeflow                    ml-pipeline-visualizationserver-77b748f8fd-vxc7s             2/2     Running   1          173m
kubeflow                    mpi-operator-d5bfb8489-2mwpr                                 1/1     Running   1          172m
kubeflow                    mxnet-operator-6cffc568b7-wd5xv                              1/1     Running   1          172m
kubeflow                    mysql-f7b9b7dd4-2pxbh                                        2/2     Running   0          173m
kubeflow                    notebook-controller-deployment-7bd85f9f7d-cdknp              1/1     Running   1          172m
kubeflow                    profiles-deployment-74b4f94d5c-skcv6                         2/2     Running   2          172m
kubeflow                    pytorch-operator-56bffbbd86-7svvr                            2/2     Running   1          172m
kubeflow                    tensorboard-controller-controller-manager-77c89cd644-hcbln   3/3     Running   3          172m
kubeflow                    tensorboards-web-app-deployment-59ff4c7bd8-7sfss             1/1     Running   0          172m
kubeflow                    tf-job-operator-859885c8c4-c7lhb                             1/1     Running   2          172m
kubeflow                    volumes-web-app-deployment-6457c9bcfc-dl5mq                  1/1     Running   0          172m
kubeflow                    workflow-controller-67bf6d848b-kczq2                         2/2     Running   2          173m
kubeflow                    xgboost-operator-deployment-c6ddb584-j7zsz                   2/2     Running   1          172m
</code></pre></div></div>

<p>모든 Pod의 Status가 Running을 확인하였습니다.
만약 Pods의 상태가 Running이 아닌 아래와 같은 상태라면 배포 요청한 컴포넌트들이 순차적으로 처리가 될 것입니다.</p>
<ul>
  <li>Init:0/1</li>
  <li>PodInitializing</li>
  <li>ContainerCreating</li>
</ul>

<p>그런데 <strong>CrashLoopBackOff</strong>이 있다면, 설정에 문제가 있거나 리소스가 부족할 수 있습니다.
Log를 확인해 봅니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl <span class="nt">-n</span> <span class="o">[</span>NAMESPACE] logs pods/[POD_NAME]
</code></pre></div></div>

<h2 id="05-kubeflow-대시보드-접속">05. Kubeflow 대시보드 접속!</h2>
<p>Istio Ingress Gateway를 통해 대시보드에 접속해보도록 합니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl port-forward svc/istio-ingressgateway <span class="nt">-n</span> istio-system 8080:80
</code></pre></div></div>

<p><img src="https://images.velog.io/images/haje/post/af181b7b-a5cb-48d8-9c13-74718e8b7776/image.png" alt="" /></p>

<p>http://127.0.0.1:8080/</p>

<p><img src="https://images.velog.io/images/haje/post/f3bbf791-10a5-4a37-b1ef-8770e724a2f7/image.png" alt="" /></p>

<h1 id="참고문서">참고문서</h1>
<ul>
  <li><a href="https://github.com/kubeflow/manifests">Kubeflow Manifests</a></li>
  <li><a href="https://github.com/kubeflow/manifests/pull/1883">Fix dex for Kubernetes 1.21 #1883</a></li>
</ul>]]></content><author><name></name></author><category term="container" /><category term="Kubeflow" /><category term="Minikube" /><summary type="html"><![CDATA[Kubeflow 간단한 소개 머신러닝 워크플로우를 Kubernetes에서 손쉽게 이식하고 확장 가능하게 배포 및 운영을 목적으로 시작된 프로젝트입니다. Google 내부에서 서비스에 사용하기 위해 개발된 서비스가 오픈소스 프로젝트로 릴리즈되어 시작하게 되었습니다. 국내는 당근마켓, SOCAR 등 머신러닝/딥러닝이 응용되는 애플리케이션 서비스에서 널리 적용하고 사용하고 있는 ML Model Serving Tool입니다. Preface Kubernetes 클러스터에 배포할 수 있습니다. 우선 이 문서에서는 Minikube에 간단하게 구성하도록 하겠습니다. 다음 포스팅에서 CentOS7에 K8s 클러스터를 구성하고 설치해보도록 하겠습니다. Pre-Installation Requirements kubectl kustomize passlib (pip3) py_bcrypt (pip3) Minikube Minikube Spec 제가 구성한 Minikube의 스펙입니다. 4 CPUs 8GB Memory 40GB Storage macOS에서 Homebrew를 이용하여 손쉽게 설치할 수 있습니다. CentOS나 Ubuntu에서도 패키지 매니저를 이용하여 손쉽게 설치해봅시다! # Intsall Minikube brew install minikube # Start Minikube minikube start --cpus 4 --memory 8096 --disk-size=40g Software Version 본 문서에서 사용된 K8s와 Kubeflow의 버전입니다. Kubeflow 1.3.0 Kubernetes 1.21.2 과거 이 문서가 Kubeflow 1.15 버전을 사용할 때와는 좀 차이가 있습니다. Kubeflow 1.3.0에서는 버전 1.1x와 다르게 kustomize를 이용하여 설치하고 관리합니다. Kustomize도 설치해보도록 합니다. brew install kustomize Components of Kubeflow Manifests 아래의 컴포넌트가 포함되어 있습니다. 모두 설치할 수도 있고, 필요한 것만 개별로 설치할 수도 있습니다. TFJob Operator PyTorch Operator MPI Operator MXNet Operator XGBoost Operator Notebook Controller Tensorboard Controller Central Dashboard Profiles + KFAM PodDefaults Webhook Jupyter Web App Tensorboards Web App Volumes Web App Katib KFServing Kubeflow Pipelines Kubeflow Tekton Pipelines 설치하기 01. Kubeflow Manifest 준비 본 문서에서 설치에 사용될 바이너리를 준비합니다. Kubeflow Manifest를 사용할 것입니다. 여러 컴포넌트가 준비가 되어 있으므로, 손쉽게 설치가 가능합니다. 또한 불필요한 컴포넌트를 제외하고 필요한 컴포넌트만 메뉴얼하게 설치도 가능합니다. 우선, GitHub에서 클론으로 가져와 manifests 디렉토리에 들어가도록 합니다. git clone https://github.com/kubeflow/manifests cd manifests 02. Dex 수정하기 OIDC(OpenID Connect) Identity Service인 Dex를 통해 Kubernetes 사용자 인증을 관리합니다. 그런데 이슈가 하나 있습니다. Kubeflow 1.21 이후 버전에서는 Dex Manifest가 작동하지 않는 이슈가 있습니다. 해당 이슈: Fix dex for Kubernetes 1.21 #1883 해결 방법은 간단합니다. 문서를 보면 2가지 해결안을 제시(snap or yaml)하는데, snap을 이용하는 방법보다 쉬운 yaml 수정을 하도록 하겠습니다. 사실 macOS에서 snapd가 제대로 동작을 안하네요… 아래의 파일에서 31번째 줄에 추가해주세요. vi manifests/common/dex/base/deployment.yaml 파일을 수정할 때 31번 라인에 아래의 NAMESPACE 설정을 추가해주도록 합니다. 띄어쓰기를 반드시 신경써서 넣어줍시다. env: - name: KUBERNETES_POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace 아래 이미지와 같이 보이면 됩니다. 02-1. Dex 사용자 수정하기 (생략 가능) Kubeflow Manifests에 설정된 Default User와 Password는 아래와 같습니다. user@example.com 12341234 하지만 수정해보도록 하겠습니다. bcrypt를 이용하여 비밀번호 Hash를 생성합니다. python3 -c 'from passlib.hash import bcrypt; import getpass; print(bcrypt.using(rounds=12, ident="2y").hash(getpass.getpass()))' 마지막 라인의 Hash값을 복사하여 아래 파일에 입력합니다. vi manifests/common/dex/base/config-map.yaml 21번 라인에 가서 User를 수정하고 Hash를 입력합니다. 21 - email: hello@kubeflow.com 22 hash: 요기에 입력 03. 설치하기 기본적인 세팅은 끝났습니다. 이제 간편하게 명령어 한 줄로 Kubeflow Manifests를 Minikube에 구성해보도록 하겠습니다. while ! kustomize build example | kubectl apply -f -; do echo "Retrying to apply resources"; sleep 10; done 04. Pods 상태 확인 배포 요청한 컴포넌트들의 Pods가 정상적으로 떠있는지 확인해보도록 합니다. kubectl get pods -A NAMESPACE NAME READY STATUS RESTARTS AGE auth dex-5ddf47d88d-w5vbc 1/1 Running 1 164m cert-manager cert-manager-7dd5854bb4-ckkqz 1/1 Running 0 3h3m cert-manager cert-manager-cainjector-64c949654c-n72pz 1/1 Running 1 3h3m cert-manager cert-manager-webhook-6b57b9b886-h7htl 1/1 Running 1 3h3m istio-system authservice-0 1/1 Running 0 174m istio-system cluster-local-gateway-75cb7c6c88-kw59j 1/1 Running 0 174m istio-system istio-ingressgateway-79b665c95-8rsss 1/1 Running 0 3h2m istio-system istiod-86457659bb-gqzng 1/1 Running 0 3h2m knative-eventing eventing-controller-575584745f-p8hhs 1/1 Running 0 174m knative-eventing eventing-webhook-6d6f75c565-7zn7n 1/1 Running 1 174m knative-eventing imc-controller-c8d86c869-qp5pj 1/1 Running 0 174m knative-eventing imc-dispatcher-7bf75b8999-fndtx 1/1 Running 0 174m knative-eventing mt-broker-controller-7778d47797-sbg2t 1/1 Running 0 174m knative-eventing mt-broker-filter-857c746446-hzddr 1/1 Running 1 174m knative-eventing mt-broker-ingress-685cd6b57-sz6vr 1/1 Running 1 174m knative-serving activator-859796b66c-r5zsr 2/2 Running 2 174m knative-serving autoscaler-565454fb69-4kgpj 2/2 Running 2 174m knative-serving controller-dd58865b5-dlwg6 2/2 Running 1 174m knative-serving istio-webhook-68fddcc567-n7pzq 2/2 Running 1 174m knative-serving networking-istio-5664b9fb9c-jlq7q 2/2 Running 1 174m knative-serving webhook-6c8b54d9-nkcq8 2/2 Running 2 174m kube-system coredns-558bd4d5db-qdl2g 1/1 Running 1 3h19m kube-system etcd-minikube 1/1 Running 0 3h19m kube-system kube-apiserver-minikube 1/1 Running 0 3h19m kube-system kube-controller-manager-minikube 1/1 Running 0 3h19m kube-system kube-proxy-7qwz6 1/1 Running 0 3h19m kube-system kube-scheduler-minikube 1/1 Running 0 3h19m kube-system storage-provisioner 1/1 Running 1 3h19m kubeflow-user-example-com ml-pipeline-ui-artifact-767659f9df-8p8ht 2/2 Running 0 134m kubeflow-user-example-com ml-pipeline-visualizationserver-6ff9f47c6b-52lpd 2/2 Running 0 134m kubeflow admission-webhook-deployment-f5d8f47f8-j6spk 1/1 Running 0 172m kubeflow cache-deployer-deployment-6dbb64ddcd-sn42c 2/2 Running 1 173m kubeflow cache-server-79d58845f5-6szhs 2/2 Running 0 173m kubeflow centraldashboard-9846cbb75-d2n2c 1/1 Running 1 172m kubeflow jupyter-web-app-deployment-554975dd5d-t5jbn 1/1 Running 0 172m kubeflow katib-controller-7b98cd6865-7khbj 1/1 Running 0 172m kubeflow katib-db-manager-7f5f684dd5-rjk5z 1/1 Running 1 172m kubeflow katib-mysql-85fc9c74b8-r4j2j 1/1 Running 1 172m kubeflow katib-ui-64fbdf4d94-kc88w 1/1 Running 0 172m kubeflow kfserving-controller-manager-0 2/2 Running 0 173m kubeflow kubeflow-pipelines-profile-controller-596b896f8d-jjgfr 1/1 Running 0 173m kubeflow metacontroller-0 1/1 Running 0 173m kubeflow metadata-envoy-deployment-95b58bbbb-xcl2t 1/1 Running 0 173m kubeflow metadata-grpc-deployment-c8f784fdf-n8cds 2/2 Running 3 173m kubeflow metadata-writer-76b6b98985-jb8md 2/2 Running 1 173m kubeflow minio-5b65df66c9-4hjx6 2/2 Running 0 173m kubeflow ml-pipeline-5c5d8f4959-4bb5d 2/2 Running 3 173m kubeflow ml-pipeline-persistenceagent-6ff46967ff-k4b4s 2/2 Running 1 173m kubeflow ml-pipeline-scheduledworkflow-66bdf9948d-vqdzx 2/2 Running 0 173m kubeflow ml-pipeline-ui-57fdfc58cc-mdnvr 2/2 Running 1 173m kubeflow ml-pipeline-viewer-crd-64dddf4597-p9xb9 2/2 Running 1 173m kubeflow ml-pipeline-visualizationserver-77b748f8fd-vxc7s 2/2 Running 1 173m kubeflow mpi-operator-d5bfb8489-2mwpr 1/1 Running 1 172m kubeflow mxnet-operator-6cffc568b7-wd5xv 1/1 Running 1 172m kubeflow mysql-f7b9b7dd4-2pxbh 2/2 Running 0 173m kubeflow notebook-controller-deployment-7bd85f9f7d-cdknp 1/1 Running 1 172m kubeflow profiles-deployment-74b4f94d5c-skcv6 2/2 Running 2 172m kubeflow pytorch-operator-56bffbbd86-7svvr 2/2 Running 1 172m kubeflow tensorboard-controller-controller-manager-77c89cd644-hcbln 3/3 Running 3 172m kubeflow tensorboards-web-app-deployment-59ff4c7bd8-7sfss 1/1 Running 0 172m kubeflow tf-job-operator-859885c8c4-c7lhb 1/1 Running 2 172m kubeflow volumes-web-app-deployment-6457c9bcfc-dl5mq 1/1 Running 0 172m kubeflow workflow-controller-67bf6d848b-kczq2 2/2 Running 2 173m kubeflow xgboost-operator-deployment-c6ddb584-j7zsz 2/2 Running 1 172m 모든 Pod의 Status가 Running을 확인하였습니다. 만약 Pods의 상태가 Running이 아닌 아래와 같은 상태라면 배포 요청한 컴포넌트들이 순차적으로 처리가 될 것입니다. Init:0/1 PodInitializing ContainerCreating 그런데 CrashLoopBackOff이 있다면, 설정에 문제가 있거나 리소스가 부족할 수 있습니다. Log를 확인해 봅니다. kubectl -n [NAMESPACE] logs pods/[POD_NAME] 05. Kubeflow 대시보드 접속! Istio Ingress Gateway를 통해 대시보드에 접속해보도록 합니다. kubectl port-forward svc/istio-ingressgateway -n istio-system 8080:80 http://127.0.0.1:8080/ 참고문서 Kubeflow Manifests Fix dex for Kubernetes 1.21 #1883]]></summary></entry><entry><title type="html">Prepare CentOS 7 with Vagrant &amp;amp; VirtualBox</title><link href="https://hajekim.github.io/container/2021/08/17/post2/" rel="alternate" type="text/html" title="Prepare CentOS 7 with Vagrant &amp;amp; VirtualBox" /><published>2021-08-17T18:00:00+09:00</published><updated>2021-08-17T18:00:00+09:00</updated><id>https://hajekim.github.io/container/2021/08/17/post2</id><content type="html" xml:base="https://hajekim.github.io/container/2021/08/17/post2/"><![CDATA[<blockquote>
  <h3 id="vagrant-간단한-소개">Vagrant 간단한 소개</h3>
  <p>단일 워크플로에서 가상 머신 환경을 구축하고 관리하기 위한 도구입니다.
현재는 HashiCorp에서 관리하고 있으며, 포터블한 가상화 개발 환경 관리 도구입니다.
VirutalBox, VMWare, Docker 등과 연동하여 인스턴스를 관리할 수 있습니다.
본 문서에서는 Oracle VM VirtualBox를 사용합니다.</p>
</blockquote>

<h1 id="preface">Preface</h1>
<p>Vagrant를 이용하여 2개의 CentOS 7 인스턴스를 구성해봅니다.
<img src="https://images.velog.io/images/haje/post/15f2a5da-130c-4662-8376-26c489bc1d5a/image.png" alt="" /></p>

<h1 id="설치하기">설치하기</h1>

<h2 id="01-vagrant--virtualbox-설치">01. Vagrant &amp; VirtualBox 설치</h2>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Vagrant 설치</span>
brew <span class="nb">install</span> <span class="nt">--cask</span> vagrant vagrant-manager

<span class="c"># VirtualBox 설치</span>
brew <span class="nb">install</span> <span class="nt">--cask</span> virtualbox
</code></pre></div></div>
<p>Homebrew를 이용하여 Vagrant와 VirtualBox를 설치합니다.
각 사이트에서 바이너리 파일을 다운 받아 설치할 수도 있습니다.
취향에 따라 구성하세요.</p>

<h2 id="02-vagrantfile-디렉토리-생성">02. Vagrantfile 디렉토리 생성</h2>
<p>Vagrant에서 프로젝트를 관리하기 위해 사용할 디렉토리를 만든 후에, 디렉토리 하위에 Configuration File을 구성합니다.
init 명령어를 이용하면 디렉토리 하위에 Vagrantfile이 생성됩니다.
Vagrant에서 제공하는 다양한 OS 이미지는 <a href="https://app.vagrantup.com/boxes/search">Vagrant Cloud</a> 확인할 수 있습니다.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> ~/vagrant
<span class="nb">cd</span> ~/vagrant
vagrant init centos/7
</code></pre></div></div>

<h2 id="03-vagrantfile-수정">03. Vagrantfile 수정</h2>
<p>생성할 인스턴스의 설정을 입력합니다.
2개의 CentOS 7을 아래의 스펙과 Hostname으로 구성합니다.</p>

<ul>
  <li>Hostname : cent01 / cent02</li>
  <li>CPU : 1</li>
  <li>Memory : 512</li>
  <li>IP : 192.168.2.11 / 192.168.2.12
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vi ~/vagrant/Vagrantfile
Vagrant.configure<span class="o">(</span><span class="s2">"2"</span><span class="o">)</span> <span class="k">do</span> |config|
config.vm.box <span class="o">=</span> <span class="s2">"centos/7"</span>
config.vm.box_check_update <span class="o">=</span> <span class="nb">true</span>
 
<span class="c"># Create the cent01</span>
config.vm.define <span class="s2">"cent01"</span> <span class="k">do</span> |cent01|
 cent01.vm.hostname <span class="o">=</span> <span class="s2">"cent01"</span>
 cent01.vm.network <span class="s2">"private_network"</span>, ip:<span class="s2">"192.168.2.11"</span>
 cent01.vm.provider <span class="s2">"virtualbox"</span> <span class="k">do</span> |v|
  v.name <span class="o">=</span> <span class="s2">"cent01"</span>
  v.memory <span class="o">=</span> 512
  v.cpus <span class="o">=</span> 1
  v.linked_clone <span class="o">=</span> <span class="nb">true
  </span>v.gui <span class="o">=</span> <span class="nb">false
</span>end
end
 
<span class="c"># Create the cent02</span>
config.vm.define <span class="s2">"cent02"</span> <span class="k">do</span> |cent02|
 cent02.vm.hostname <span class="o">=</span> <span class="s2">"cent02"</span>
 cent02.vm.network <span class="s2">"private_network"</span>, ip:<span class="s2">"192.168.2.12"</span>
 cent02.vm.provider <span class="s2">"virtualbox"</span> <span class="k">do</span> |v|
  v.name <span class="o">=</span> <span class="s2">"cent02"</span>
  v.memory <span class="o">=</span> 512
  v.cpus <span class="o">=</span> 1
  v.linked_clone <span class="o">=</span> <span class="nb">true
  </span>v.gui <span class="o">=</span> <span class="nb">false
</span>end
end
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="04-centos-7-스타트">04. CentOS 7 스타트!</h2>
<p>생성한 vagrant 프로젝트 디렉토리에서 명령어를 입력하여 인스턴스를 켜보도록 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant up

Bringing machine <span class="s1">'cent01'</span> up with <span class="s1">'virtualbox'</span> provider...
Bringing machine <span class="s1">'cent02'</span> up with <span class="s1">'virtualbox'</span> provider...
<span class="o">==&gt;</span> cent01: Checking <span class="k">if </span>box <span class="s1">'centos/7'</span> version <span class="s1">'2004.01'</span> is up to date...
<span class="o">==&gt;</span> cent01: Clearing any previously <span class="nb">set </span>forwarded ports...
<span class="o">==&gt;</span> cent01: Clearing any previously <span class="nb">set </span>network interfaces...
<span class="o">==&gt;</span> cent01: Preparing network interfaces based on configuration...
    cent01: Adapter 1: nat
    cent01: Adapter 2: hostonly
<span class="o">==&gt;</span> cent01: Forwarding ports...
    cent01: 22 <span class="o">(</span>guest<span class="o">)</span> <span class="o">=&gt;</span> 2222 <span class="o">(</span>host<span class="o">)</span> <span class="o">(</span>adapter 1<span class="o">)</span>
<span class="o">==&gt;</span> cent01: Running <span class="s1">'pre-boot'</span> VM customizations...
<span class="o">==&gt;</span> cent01: Booting VM...
<span class="o">==&gt;</span> cent01: Waiting <span class="k">for </span>machine to boot. This may take a few minutes...
    cent01: SSH address: 127.0.0.1:2222
    cent01: SSH username: vagrant
    cent01: SSH auth method: private key
<span class="o">==&gt;</span> cent01: Machine booted and ready!
<span class="o">==&gt;</span> cent01: Checking <span class="k">for </span>guest additions <span class="k">in </span>VM...
    cent01: No guest additions were detected on the base box <span class="k">for </span>this VM! Guest
    cent01: additions are required <span class="k">for </span>forwarded ports, shared folders, host only
    cent01: networking, and more. If SSH fails on this machine, please <span class="nb">install
    </span>cent01: the guest additions and repackage the box to <span class="k">continue</span><span class="nb">.</span>
    cent01:
    cent01: This is not an error message<span class="p">;</span> everything may <span class="k">continue </span>to work properly,
    cent01: <span class="k">in </span>which <span class="k">case</span> you may ignore this message.
<span class="o">==&gt;</span> cent01: Setting hostname...
<span class="o">==&gt;</span> cent01: Configuring and enabling network interfaces...
<span class="o">==&gt;</span> cent01: Rsyncing folder: /Users/haje/vgrt/ <span class="o">=&gt;</span> /vagrant
<span class="o">==&gt;</span> cent01: Machine already provisioned. Run <span class="sb">`</span>vagrant provision<span class="sb">`</span> or use the <span class="sb">`</span><span class="nt">--provision</span><span class="sb">`</span>
<span class="o">==&gt;</span> cent01: flag to force provisioning. Provisioners marked to run always will still run.
<span class="o">==&gt;</span> cent02: Checking <span class="k">if </span>box <span class="s1">'centos/7'</span> version <span class="s1">'2004.01'</span> is up to date...
<span class="o">==&gt;</span> cent02: Clearing any previously <span class="nb">set </span>forwarded ports...
<span class="o">==&gt;</span> cent02: Fixed port collision <span class="k">for </span>22 <span class="o">=&gt;</span> 2222. Now on port 2200.
<span class="o">==&gt;</span> cent02: Clearing any previously <span class="nb">set </span>network interfaces...
<span class="o">==&gt;</span> cent02: Preparing network interfaces based on configuration...
    cent02: Adapter 1: nat
    cent02: Adapter 2: hostonly
<span class="o">==&gt;</span> cent02: Forwarding ports...
    cent02: 22 <span class="o">(</span>guest<span class="p">)</span> <span class="o">=&gt;</span> 2200 <span class="o">(</span>host<span class="o">)</span> <span class="o">(</span>adapter 1<span class="o">)</span>
<span class="o">==&gt;</span> cent02: Running <span class="s1">'pre-boot'</span> VM customizations...
<span class="o">==&gt;</span> cent02: Booting VM...
<span class="o">==&gt;</span> cent02: Waiting <span class="k">for </span>machine to boot. This may take a few minutes...
    cent02: SSH address: 127.0.0.1:2200
    cent02: SSH username: vagrant
    cent02: SSH auth method: private key
<span class="o">==&gt;</span> cent02: Machine booted and ready!
<span class="o">==&gt;</span> cent02: Checking <span class="k">for </span>guest additions <span class="k">in </span>VM...
    cent02: No guest additions were detected on the base box <span class="k">for </span>this VM! Guest
    cent02: additions are required <span class="k">for </span>forwarded ports, shared folders, host only
    cent02: networking, and more. If SSH fails on this machine, please <span class="nb">install
    </span>cent02: the guest additions and repackage the box to <span class="k">continue</span><span class="nb">.</span>
    cent02:
    cent02: This is not an error message<span class="p">;</span> everything may <span class="k">continue </span>to work properly,
    cent02: <span class="k">in </span>which <span class="k">case</span> you may ignore this message.
<span class="o">==&gt;</span> cent02: Setting hostname...
<span class="o">==&gt;</span> cent02: Configuring and enabling network interfaces...
<span class="o">==&gt;</span> cent02: Rsyncing folder: /Users/haje/vgrt/ <span class="o">=&gt;</span> /vagrant
<span class="o">==&gt;</span> cent02: Machine already provisioned. Run <span class="sb">`</span>vagrant provision<span class="sb">`</span> or use the <span class="sb">`</span><span class="nt">--provision</span><span class="sb">`</span>
<span class="o">==&gt;</span> cent02: flag to force provisioning. Provisioners marked to run always will still run.
</code></pre></div></div>

<h2 id="05-vagrant-인스턴스-ssh-접속">05. Vagrant 인스턴스 SSH 접속</h2>
<p>생성한 인스턴스에 손쉽게 접속하도록 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant ssh cent01
</code></pre></div></div>]]></content><author><name></name></author><category term="container" /><category term="Vagrant" /><category term="VirtualBox" /><summary type="html"><![CDATA[Vagrant 간단한 소개 단일 워크플로에서 가상 머신 환경을 구축하고 관리하기 위한 도구입니다. 현재는 HashiCorp에서 관리하고 있으며, 포터블한 가상화 개발 환경 관리 도구입니다. VirutalBox, VMWare, Docker 등과 연동하여 인스턴스를 관리할 수 있습니다. 본 문서에서는 Oracle VM VirtualBox를 사용합니다. Preface Vagrant를 이용하여 2개의 CentOS 7 인스턴스를 구성해봅니다. 설치하기 01. Vagrant &amp; VirtualBox 설치 # Vagrant 설치 brew install --cask vagrant vagrant-manager # VirtualBox 설치 brew install --cask virtualbox Homebrew를 이용하여 Vagrant와 VirtualBox를 설치합니다. 각 사이트에서 바이너리 파일을 다운 받아 설치할 수도 있습니다. 취향에 따라 구성하세요. 02. Vagrantfile 디렉토리 생성 Vagrant에서 프로젝트를 관리하기 위해 사용할 디렉토리를 만든 후에, 디렉토리 하위에 Configuration File을 구성합니다. init 명령어를 이용하면 디렉토리 하위에 Vagrantfile이 생성됩니다. Vagrant에서 제공하는 다양한 OS 이미지는 Vagrant Cloud 확인할 수 있습니다. mkdir ~/vagrant cd ~/vagrant vagrant init centos/7 03. Vagrantfile 수정 생성할 인스턴스의 설정을 입력합니다. 2개의 CentOS 7을 아래의 스펙과 Hostname으로 구성합니다. Hostname : cent01 / cent02 CPU : 1 Memory : 512 IP : 192.168.2.11 / 192.168.2.12 vi ~/vagrant/Vagrantfile Vagrant.configure("2") do |config| config.vm.box = "centos/7" config.vm.box_check_update = true # Create the cent01 config.vm.define "cent01" do |cent01| cent01.vm.hostname = "cent01" cent01.vm.network "private_network", ip:"192.168.2.11" cent01.vm.provider "virtualbox" do |v| v.name = "cent01" v.memory = 512 v.cpus = 1 v.linked_clone = true v.gui = false end end # Create the cent02 config.vm.define "cent02" do |cent02| cent02.vm.hostname = "cent02" cent02.vm.network "private_network", ip:"192.168.2.12" cent02.vm.provider "virtualbox" do |v| v.name = "cent02" v.memory = 512 v.cpus = 1 v.linked_clone = true v.gui = false end end 04. CentOS 7 스타트! 생성한 vagrant 프로젝트 디렉토리에서 명령어를 입력하여 인스턴스를 켜보도록 합니다. vagrant up Bringing machine 'cent01' up with 'virtualbox' provider... Bringing machine 'cent02' up with 'virtualbox' provider... ==&gt; cent01: Checking if box 'centos/7' version '2004.01' is up to date... ==&gt; cent01: Clearing any previously set forwarded ports... ==&gt; cent01: Clearing any previously set network interfaces... ==&gt; cent01: Preparing network interfaces based on configuration... cent01: Adapter 1: nat cent01: Adapter 2: hostonly ==&gt; cent01: Forwarding ports... cent01: 22 (guest) =&gt; 2222 (host) (adapter 1) ==&gt; cent01: Running 'pre-boot' VM customizations... ==&gt; cent01: Booting VM... ==&gt; cent01: Waiting for machine to boot. This may take a few minutes... cent01: SSH address: 127.0.0.1:2222 cent01: SSH username: vagrant cent01: SSH auth method: private key ==&gt; cent01: Machine booted and ready! ==&gt; cent01: Checking for guest additions in VM... cent01: No guest additions were detected on the base box for this VM! Guest cent01: additions are required for forwarded ports, shared folders, host only cent01: networking, and more. If SSH fails on this machine, please install cent01: the guest additions and repackage the box to continue. cent01: cent01: This is not an error message; everything may continue to work properly, cent01: in which case you may ignore this message. ==&gt; cent01: Setting hostname... ==&gt; cent01: Configuring and enabling network interfaces... ==&gt; cent01: Rsyncing folder: /Users/haje/vgrt/ =&gt; /vagrant ==&gt; cent01: Machine already provisioned. Run `vagrant provision` or use the `--provision` ==&gt; cent01: flag to force provisioning. Provisioners marked to run always will still run. ==&gt; cent02: Checking if box 'centos/7' version '2004.01' is up to date... ==&gt; cent02: Clearing any previously set forwarded ports... ==&gt; cent02: Fixed port collision for 22 =&gt; 2222. Now on port 2200. ==&gt; cent02: Clearing any previously set network interfaces... ==&gt; cent02: Preparing network interfaces based on configuration... cent02: Adapter 1: nat cent02: Adapter 2: hostonly ==&gt; cent02: Forwarding ports... cent02: 22 (guest) =&gt; 2200 (host) (adapter 1) ==&gt; cent02: Running 'pre-boot' VM customizations... ==&gt; cent02: Booting VM... ==&gt; cent02: Waiting for machine to boot. This may take a few minutes... cent02: SSH address: 127.0.0.1:2200 cent02: SSH username: vagrant cent02: SSH auth method: private key ==&gt; cent02: Machine booted and ready! ==&gt; cent02: Checking for guest additions in VM... cent02: No guest additions were detected on the base box for this VM! Guest cent02: additions are required for forwarded ports, shared folders, host only cent02: networking, and more. If SSH fails on this machine, please install cent02: the guest additions and repackage the box to continue. cent02: cent02: This is not an error message; everything may continue to work properly, cent02: in which case you may ignore this message. ==&gt; cent02: Setting hostname... ==&gt; cent02: Configuring and enabling network interfaces... ==&gt; cent02: Rsyncing folder: /Users/haje/vgrt/ =&gt; /vagrant ==&gt; cent02: Machine already provisioned. Run `vagrant provision` or use the `--provision` ==&gt; cent02: flag to force provisioning. Provisioners marked to run always will still run. 05. Vagrant 인스턴스 SSH 접속 생성한 인스턴스에 손쉽게 접속하도록 합니다. vagrant ssh cent01]]></summary></entry></feed>