<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Coverage on jyukki's Blog</title><link>https://jyukki.com/tags/coverage/</link><description>Recent content in Coverage on jyukki's Blog</description><generator>Hugo -- 0.147.0</generator><language>ko-KR</language><lastBuildDate>Tue, 17 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://jyukki.com/tags/coverage/index.xml" rel="self" type="application/rss+xml"/><item><title>Go로 PostgreSQL 프록시 만들기 (61) - E2E 커버리지 갭 분석과 미커버 기능 테스트</title><link>https://jyukki.com/posts/2026-03-17-pgmux-61-e2e-coverage-gap/</link><pubDate>Tue, 17 Mar 2026 00:00:00 +0000</pubDate><guid>https://jyukki.com/posts/2026-03-17-pgmux-61-e2e-coverage-gap/</guid><description>22개 E2E 테스트의 커버리지 갭을 분석하고, Route Hint·Timeout Hint·Digest Reset·Semantic Cache·Slow Query·Config Reload·CancelRequest 등 8개 미커버 기능 테스트를 추가하여 30개로 확장한다.</description><content:encoded><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>이전 포스트에서 E2E 테스트를 14개에서 22개로 확장했다. &ldquo;이 정도면 다 커버되었나?&ldquo;라는 질문에 답하기 위해 pgmux의 전체 기능 목록과 테스트를 대조해봤다. 결과는 <strong>약 60%</strong>. 핵심 데이터 경로는 잘 커버되어 있지만, 운영/관측 기능과 힌트 기반 기능에 상당한 갭이 있었다.</p>
<hr>
<h2 id="커버리지-갭-분석">커버리지 갭 분석</h2>
<p>pgmux의 구현 단계 21개와 이후 추가된 기능들을 기존 22개 테스트와 대조했다.</p>
<h3 id="이미-커버된-기능-22개-테스트">이미 커버된 기능 (22개 테스트)</h3>
<table>
  <thead>
      <tr>
          <th>기능</th>
          <th>테스트</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>R/W 라우팅 (Simple Query)</td>
          <td><code>ProxyIntegration</code></td>
      </tr>
      <tr>
          <td>트랜잭션 처리 / Pooling</td>
          <td><code>ProxyIntegration</code>, <code>TransactionPooling</code></td>
      </tr>
      <tr>
          <td>커넥션 풀링</td>
          <td><code>TransactionPooling</code> (SessionReset, PoolExhaustion)</td>
      </tr>
      <tr>
          <td>쿼리 캐시</td>
          <td><code>ProxyIntegration</code> (CacheHit), <code>CacheFlush</code></td>
      </tr>
      <tr>
          <td>Causal Consistency (LSN)</td>
          <td><code>CausalConsistency</code></td>
      </tr>
      <tr>
          <td>Extended Query Protocol</td>
          <td><code>ExtendedQueryProtocol</code></td>
      </tr>
      <tr>
          <td>Prepared Statement Reuse</td>
          <td><code>PreparedStatementReuse</code></td>
      </tr>
      <tr>
          <td>COPY IN/OUT</td>
          <td><code>CopyProtocol</code></td>
      </tr>
      <tr>
          <td>Admin API</td>
          <td><code>AdminAPI</code> (6개 서브테스트)</td>
      </tr>
      <tr>
          <td>Auth / Firewall / Connection Limits</td>
          <td><code>Auth</code>, <code>FirewallRules</code>, <code>ConnectionLimits</code></td>
      </tr>
      <tr>
          <td>Data API / Rate Limit</td>
          <td><code>DataAPI</code>, <code>RateLimitMetric</code></td>
      </tr>
      <tr>
          <td>Session Compatibility / SQL Redaction</td>
          <td><code>SessionCompatibility</code>, <code>SQLRedaction</code></td>
      </tr>
  </tbody>
</table>
<h3 id="미커버-기능">미커버 기능</h3>
<table>
  <thead>
      <tr>
          <th>기능</th>
          <th style="text-align: center">인프라 필요 여부</th>
          <th style="text-align: center">테스트 가능성</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Route Hint</strong> (<code>/* route:writer */</code>)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>Timeout Hint</strong> (<code>/* timeout:Xs */</code>)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>Digest Reset</strong> (<code>POST /admin/queries/reset</code>)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>Semantic Cache Key</strong> (AST 정규화)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>Slow Query Detection</strong> (감사 메트릭)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>Config Reload</strong> (<code>POST /admin/reload</code>)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>CancelRequest</strong> (PG 쿼리 취소)</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td><strong>Mirror Stats</strong> 엔드포인트</td>
          <td style="text-align: center">없음</td>
          <td style="text-align: center"><strong>가능</strong></td>
      </tr>
      <tr>
          <td>Query Mirroring (Shadow DB)</td>
          <td style="text-align: center">Shadow DB 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>Circuit Breaker</td>
          <td style="text-align: center">장애 주입 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>Reader Failover</td>
          <td style="text-align: center">Reader 중단 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>TLS</td>
          <td style="text-align: center">인증서 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>Redis Pub/Sub 캐시 무효화</td>
          <td style="text-align: center">Redis 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>OpenTelemetry Span</td>
          <td style="text-align: center">OTLP 수집기 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>Admin RBAC / IP Allowlist</td>
          <td style="text-align: center">별도 설정 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
      <tr>
          <td>Prepared Statement Multiplexing</td>
          <td style="text-align: center">별도 설정 필요</td>
          <td style="text-align: center">보류</td>
      </tr>
  </tbody>
</table>
<p>기존 인프라(docker-compose 3대 PostgreSQL + pgmux)만으로 테스트 가능한 8개 기능을 추가하기로 했다.</p>
<hr>
<h2 id="추가-테스트-1-route-hint">추가 테스트 1: Route Hint</h2>
<p>pgmux는 쿼리 주석에 <code>/* route:writer */</code> 또는 <code>/* route:reader */</code>를 넣어 라우팅을 오버라이드할 수 있다. 이 기능은 P3에서 구현했지만 E2E 테스트가 없었다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>t.<span style="color:#50fa7b">Run</span>(<span style="color:#f1fa8c">&#34;RouteSelectToWriter&#34;</span>, <span style="color:#8be9fd;font-style:italic">func</span>(t <span style="color:#ff79c6">*</span>testing.T) {
</span></span><span style="display:flex;"><span>    <span style="color:#8be9fd;font-style:italic">var</span> result <span style="color:#8be9fd">int</span>
</span></span><span style="display:flex;"><span>    err <span style="color:#ff79c6">:=</span> db.<span style="color:#50fa7b">QueryRowContext</span>(ctx, <span style="color:#f1fa8c">&#34;/* route:writer */ SELECT 1&#34;</span>).<span style="color:#50fa7b">Scan</span>(<span style="color:#ff79c6">&amp;</span>result)
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> err <span style="color:#ff79c6">!=</span> <span style="color:#ff79c6">nil</span> {
</span></span><span style="display:flex;"><span>        t.<span style="color:#50fa7b">Fatalf</span>(<span style="color:#f1fa8c">&#34;route hint SELECT failed: %v&#34;</span>, err)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p>SELECT는 원래 reader로 가지만, <code>/* route:writer */</code> 힌트가 있으면 writer로 라우팅된다. 이를 검증하기 위해 <strong>메트릭 스냅샷 비교</strong>를 사용한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>writerBefore <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_queries_routed_total&#34;</span>, <span style="color:#f1fa8c">`target=&#34;writer&#34;`</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// /* route:writer */ SELECT 실행</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>writerAfter <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_queries_routed_total&#34;</span>, <span style="color:#f1fa8c">`target=&#34;writer&#34;`</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">if</span> writerAfter <span style="color:#ff79c6">&lt;=</span> writerBefore {
</span></span><span style="display:flex;"><span>    t.<span style="color:#50fa7b">Error</span>(<span style="color:#f1fa8c">&#34;writer metric did not increase&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Prometheus <code>/metrics</code> 엔드포인트에서 <code>pgmux_queries_routed_total{target=&quot;writer&quot;}</code> 값을 힌트 실행 전후로 비교한다. writer 카운터가 증가하면 SELECT가 writer로 라우팅된 것이 증명된다.</p>
<hr>
<h2 id="추가-테스트-2-timeout-hint">추가 테스트 2: Timeout Hint</h2>
<p><code>/* timeout:200ms */</code> 힌트는 쿼리별 타임아웃을 지정한다. 타임아웃이 발생하면 pgmux가 PG CancelRequest를 백엔드로 전송하여 쿼리를 취소한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>t.<span style="color:#50fa7b">Run</span>(<span style="color:#f1fa8c">&#34;TimeoutCancelsQuery&#34;</span>, <span style="color:#8be9fd;font-style:italic">func</span>(t <span style="color:#ff79c6">*</span>testing.T) {
</span></span><span style="display:flex;"><span>    start <span style="color:#ff79c6">:=</span> time.<span style="color:#50fa7b">Now</span>()
</span></span><span style="display:flex;"><span>    _, err <span style="color:#ff79c6">:=</span> db.<span style="color:#50fa7b">ExecContext</span>(ctx, <span style="color:#f1fa8c">&#34;/* timeout:200ms */ SELECT pg_sleep(5)&#34;</span>)
</span></span><span style="display:flex;"><span>    elapsed <span style="color:#ff79c6">:=</span> time.<span style="color:#50fa7b">Since</span>(start)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> err <span style="color:#ff79c6">==</span> <span style="color:#ff79c6">nil</span> {
</span></span><span style="display:flex;"><span>        t.<span style="color:#50fa7b">Fatal</span>(<span style="color:#f1fa8c">&#34;expected timeout error, got nil&#34;</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">if</span> elapsed &gt; <span style="color:#bd93f9">3</span><span style="color:#ff79c6">*</span>time.Second {
</span></span><span style="display:flex;"><span>        t.<span style="color:#50fa7b">Errorf</span>(<span style="color:#f1fa8c">&#34;query took %v, expected cancellation within ~200ms&#34;</span>, elapsed)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p><code>pg_sleep(5)</code>는 5초가 걸리지만, 200ms 타임아웃으로 인해 약 200ms 만에 에러가 반환되어야 한다. 핵심 검증 포인트:</p>
<ol>
<li><strong>에러 발생</strong>: <code>err != nil</code> (타임아웃으로 쿼리 취소)</li>
<li><strong>빠른 완료</strong>: <code>elapsed &lt; 3s</code> (5초가 아닌 ~200ms에 완료)</li>
</ol>
<p>반대 케이스도 테스트한다. 충분한 타임아웃(<code>/* timeout:5s */</code>)과 빠른 쿼리(<code>SELECT 42</code>)는 정상 완료되어야 한다.</p>
<hr>
<h2 id="추가-테스트-3-digest-reset">추가 테스트 3: Digest Reset</h2>
<p>Query Digest는 쿼리 패턴별 실행 통계(횟수, P50/P99 레이턴시)를 수집한다. <code>POST /admin/queries/reset</code>으로 통계를 초기화할 수 있다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#6272a4">// 쿼리 실행으로 digest 적재</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">for</span> i <span style="color:#ff79c6">:=</span> <span style="color:#bd93f9">0</span>; i &lt; <span style="color:#bd93f9">5</span>; i<span style="color:#ff79c6">++</span> {
</span></span><span style="display:flex;"><span>    db.<span style="color:#50fa7b">QueryRowContext</span>(ctx, <span style="color:#f1fa8c">&#34;SELECT name FROM users WHERE id = 1&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// 초기화 전: entries &gt; 0</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// POST /admin/queries/reset</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// 초기화 후: entries == 0</span>
</span></span></code></pre></div><p>테스트 흐름:</p>
<ol>
<li>쿼리 5회 실행 → <code>GET /admin/queries/top</code> → 패턴 존재 확인</li>
<li><code>POST /admin/queries/reset</code> → 200 응답 확인</li>
<li><code>GET /admin/queries/top</code> → 빈 배열 확인</li>
</ol>
<hr>
<h2 id="추가-테스트-4-semantic-cache-key">추가 테스트 4: Semantic Cache Key</h2>
<p><code>routing.ast_parser: true</code>일 때, pgmux는 pg_query의 Parse+Deparse를 통해 <strong>구조적으로 동일한 쿼리에 같은 캐시 키</strong>를 부여한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#6272a4">-- 이 두 쿼리는 같은 캐시 엔트리를 공유해야 한다
</span></span></span><span style="display:flex;"><span><span style="color:#6272a4"></span><span style="color:#ff79c6">SELECT</span>  name  <span style="color:#ff79c6">FROM</span>  users  <span style="color:#ff79c6">WHERE</span>  id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">1</span>
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">select</span> name <span style="color:#ff79c6">from</span> users <span style="color:#ff79c6">where</span> id <span style="color:#ff79c6">=</span> <span style="color:#bd93f9">1</span>
</span></span></code></pre></div><p>공백과 대소문자만 다르고 리터럴 값은 같으므로, AST 기반 정규화 후 동일한 캐시 키가 생성된다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#6272a4">// 캐시 플러시 후 시작</span>
</span></span><span style="display:flex;"><span>missBefore <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_cache_misses_total&#34;</span>, <span style="color:#f1fa8c">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// 첫 번째 쿼리 (여분 공백) → 캐시 미스</span>
</span></span><span style="display:flex;"><span>db.<span style="color:#50fa7b">QueryRowContext</span>(ctx, <span style="color:#f1fa8c">&#34;SELECT  name  FROM  users  WHERE  id = 1&#34;</span>)
</span></span><span style="display:flex;"><span>missAfterFirst <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_cache_misses_total&#34;</span>, <span style="color:#f1fa8c">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// 두 번째 쿼리 (소문자, 공백 정리) → 캐시 히트 (미스 증가 없음)</span>
</span></span><span style="display:flex;"><span>db.<span style="color:#50fa7b">QueryRowContext</span>(ctx, <span style="color:#f1fa8c">&#34;select name from users where id = 1&#34;</span>)
</span></span><span style="display:flex;"><span>missAfterSecond <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_cache_misses_total&#34;</span>, <span style="color:#f1fa8c">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// missAfterSecond == missAfterFirst → 캐시 히트!</span>
</span></span></code></pre></div><p><code>pgmux_cache_misses_total</code> 메트릭이 두 번째 쿼리에서 증가하지 않으면 성공이다.</p>
<hr>
<h2 id="추가-테스트-5-slow-query-detection">추가 테스트 5: Slow Query Detection</h2>
<p><code>audit.slow_query_threshold: 100ms</code>가 설정되어 있으므로, 100ms 이상 걸리는 쿼리는 <code>pgmux_slow_queries_total</code> 메트릭에 카운트된다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>slowBefore <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_slow_queries_total&#34;</span>, <span style="color:#f1fa8c">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>db.<span style="color:#50fa7b">ExecContext</span>(ctx, <span style="color:#f1fa8c">&#34;SELECT pg_sleep(0.2)&#34;</span>) <span style="color:#6272a4">// 200ms &gt; 100ms threshold</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>time.<span style="color:#50fa7b">Sleep</span>(<span style="color:#bd93f9">200</span> <span style="color:#ff79c6">*</span> time.Millisecond) <span style="color:#6272a4">// 비동기 audit 처리 대기</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>slowAfter <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">getMetricValue</span>(t, client, <span style="color:#f1fa8c">&#34;pgmux_slow_queries_total&#34;</span>, <span style="color:#f1fa8c">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// slowAfter &gt; slowBefore</span>
</span></span></code></pre></div><p><code>time.Sleep(200ms)</code>는 audit 이벤트가 비동기 채널을 통해 처리되는 시간을 고려한 것이다.</p>
<hr>
<h2 id="추가-테스트-6-config-reload">추가 테스트 6: Config Reload</h2>
<p><code>POST /admin/reload</code>는 설정 파일을 다시 읽어 적용한다. 테스트에서는 reload 후 프록시가 정상 동작하는지까지 검증한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>resp, err <span style="color:#ff79c6">:=</span> client.<span style="color:#50fa7b">Post</span>(e2eAdminURL<span style="color:#ff79c6">+</span><span style="color:#f1fa8c">&#34;/admin/reload&#34;</span>, <span style="color:#f1fa8c">&#34;application/json&#34;</span>, <span style="color:#ff79c6">nil</span>)
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// status == 200</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// 리로드 후에도 쿼리가 정상 동작해야 함</span>
</span></span><span style="display:flex;"><span>db2 <span style="color:#ff79c6">:=</span> <span style="color:#50fa7b">e2eCheckProxy</span>(t)
</span></span><span style="display:flex;"><span>db2.<span style="color:#50fa7b">QueryRowContext</span>(ctx, <span style="color:#f1fa8c">&#34;SELECT 1&#34;</span>)
</span></span></code></pre></div><p>설정이 변경되지 않은 상태에서의 reload는 &ldquo;커넥션 풀 재생성 → 기존 풀 graceful close&quot;를 거친다. 이 과정에서 기존 연결이 끊기지 않고 새 연결이 정상적으로 수립되는지가 핵심이다.</p>
<hr>
<h2 id="추가-테스트-7-cancelrequest">추가 테스트 7: CancelRequest</h2>
<p>PostgreSQL의 CancelRequest 프로토콜을 테스트한다. Go의 <code>context.WithTimeout</code>으로 컨텍스트를 취소하면, <code>database/sql</code> 드라이버가 CancelRequest를 보내고 pgmux가 이를 백엔드로 전달한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>ctx, cancel <span style="color:#ff79c6">:=</span> context.<span style="color:#50fa7b">WithTimeout</span>(context.<span style="color:#50fa7b">Background</span>(), <span style="color:#bd93f9">500</span><span style="color:#ff79c6">*</span>time.Millisecond)
</span></span><span style="display:flex;"><span><span style="color:#ff79c6">defer</span> <span style="color:#50fa7b">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>start <span style="color:#ff79c6">:=</span> time.<span style="color:#50fa7b">Now</span>()
</span></span><span style="display:flex;"><span>_, err <span style="color:#ff79c6">:=</span> db.<span style="color:#50fa7b">ExecContext</span>(ctx, <span style="color:#f1fa8c">&#34;SELECT pg_sleep(10)&#34;</span>)
</span></span><span style="display:flex;"><span>elapsed <span style="color:#ff79c6">:=</span> time.<span style="color:#50fa7b">Since</span>(start)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// err != nil (취소됨)</span>
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// elapsed &lt; 3s (10초가 아닌 ~500ms에 완료)</span>
</span></span></code></pre></div><p>취소 후에도 프록시가 정상 동작하는지 확인하는 서브테스트도 포함한다. CancelRequest는 별도 TCP 연결로 전송되므로, 기존 연결 상태에 영향을 주지 않아야 한다.</p>
<hr>
<h2 id="추가-테스트-8-mirror-stats-endpoint">추가 테스트 8: Mirror Stats Endpoint</h2>
<p>Query Mirroring 자체는 Shadow DB가 필요하지만, <code>/admin/mirror/stats</code> 엔드포인트는 mirror가 비활성 상태에서도 응답해야 한다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span>resp, err <span style="color:#ff79c6">:=</span> client.<span style="color:#50fa7b">Get</span>(e2eAdminURL <span style="color:#ff79c6">+</span> <span style="color:#f1fa8c">&#34;/admin/mirror/stats&#34;</span>)
</span></span><span style="display:flex;"><span><span style="color:#6272a4">// status == 200, JSON 파싱 가능</span>
</span></span></code></pre></div><p>mirror가 설정되지 않은 경우 zeroed 통계(sent=0, dropped=0, errors=0)를 반환하는지 확인하는 스모크 테스트다.</p>
<hr>
<h2 id="메트릭-추출-헬퍼">메트릭 추출 헬퍼</h2>
<p>여러 테스트에서 Prometheus 메트릭 값을 비교해야 하므로, 공통 헬퍼를 추가했다.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#8be9fd;font-style:italic">func</span> <span style="color:#50fa7b">getMetricValue</span>(t <span style="color:#ff79c6">*</span>testing.T, client <span style="color:#ff79c6">*</span>http.Client, metricName, labelFilter <span style="color:#8be9fd">string</span>) <span style="color:#8be9fd">float64</span> {
</span></span><span style="display:flex;"><span>    resp, err <span style="color:#ff79c6">:=</span> client.<span style="color:#50fa7b">Get</span>(e2eMetricURL)
</span></span><span style="display:flex;"><span>    <span style="color:#6272a4">// ...</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">for</span> _, line <span style="color:#ff79c6">:=</span> <span style="color:#ff79c6">range</span> strings.<span style="color:#50fa7b">Split</span>(<span style="color:#8be9fd;font-style:italic">string</span>(body), <span style="color:#f1fa8c">&#34;\n&#34;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> strings.<span style="color:#50fa7b">HasPrefix</span>(line, <span style="color:#f1fa8c">&#34;#&#34;</span>) <span style="color:#ff79c6">||</span> line <span style="color:#ff79c6">==</span> <span style="color:#f1fa8c">&#34;&#34;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#ff79c6">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> !strings.<span style="color:#50fa7b">Contains</span>(line, metricName) {
</span></span><span style="display:flex;"><span>            <span style="color:#ff79c6">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> labelFilter <span style="color:#ff79c6">!=</span> <span style="color:#f1fa8c">&#34;&#34;</span> <span style="color:#ff79c6">&amp;&amp;</span> !strings.<span style="color:#50fa7b">Contains</span>(line, labelFilter) {
</span></span><span style="display:flex;"><span>            <span style="color:#ff79c6">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        parts <span style="color:#ff79c6">:=</span> strings.<span style="color:#50fa7b">Fields</span>(line)
</span></span><span style="display:flex;"><span>        <span style="color:#ff79c6">if</span> <span style="color:#8be9fd;font-style:italic">len</span>(parts) <span style="color:#ff79c6">&gt;=</span> <span style="color:#bd93f9">2</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#8be9fd;font-style:italic">var</span> val <span style="color:#8be9fd">float64</span>
</span></span><span style="display:flex;"><span>            fmt.<span style="color:#50fa7b">Sscanf</span>(parts[<span style="color:#8be9fd;font-style:italic">len</span>(parts)<span style="color:#ff79c6">-</span><span style="color:#bd93f9">1</span>], <span style="color:#f1fa8c">&#34;%f&#34;</span>, <span style="color:#ff79c6">&amp;</span>val)
</span></span><span style="display:flex;"><span>            <span style="color:#ff79c6">return</span> val
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff79c6">return</span> <span style="color:#bd93f9">0</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Prometheus text exposition 형식을 한 줄씩 파싱하여 메트릭 이름과 레이블로 필터링한 뒤 수치를 반환한다.</p>
<hr>
<h2 id="테스트-현황-22개--30개">테스트 현황: 22개 → 30개</h2>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>테스트</th>
          <th style="text-align: center">신규</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1-22</td>
          <td>기존 테스트 (ProxyIntegration ~ AdminConnectionLimitStats)</td>
          <td style="text-align: center"></td>
      </tr>
      <tr>
          <td>23</td>
          <td><code>TestE2E_RouteHint</code> — route:writer/reader 힌트 + 메트릭 검증</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>24</td>
          <td><code>TestE2E_TimeoutHint</code> — timeout 힌트 기반 쿼리 취소</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>25</td>
          <td><code>TestE2E_DigestReset</code> — digest 통계 초기화</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>26</td>
          <td><code>TestE2E_SemanticCacheKey</code> — AST 정규화 캐시 키</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>27</td>
          <td><code>TestE2E_SlowQueryDetection</code> — slow query 메트릭</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>28</td>
          <td><code>TestE2E_ConfigReload</code> — 설정 리로드 + 사후 동작</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>29</td>
          <td><code>TestE2E_CancelRequest</code> — PG CancelRequest 프로토콜</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
      <tr>
          <td>30</td>
          <td><code>TestE2E_MirrorStatsEndpoint</code> — 미러 통계 스모크 테스트</td>
          <td style="text-align: center"><strong>NEW</strong></td>
      </tr>
  </tbody>
</table>
<h3 id="여전히-보류된-항목">여전히 보류된 항목</h3>
<table>
  <thead>
      <tr>
          <th>기능</th>
          <th>필요 인프라</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Query Mirroring</td>
          <td>Shadow DB PostgreSQL</td>
      </tr>
      <tr>
          <td>Circuit Breaker</td>
          <td>장애 주입 (reader 중단)</td>
      </tr>
      <tr>
          <td>Reader Failover</td>
          <td>reader 컨테이너 중단</td>
      </tr>
      <tr>
          <td>TLS</td>
          <td>인증서 + TLS 설정</td>
      </tr>
      <tr>
          <td>Redis Pub/Sub</td>
          <td>Redis 인스턴스</td>
      </tr>
      <tr>
          <td>Admin RBAC</td>
          <td>auth 활성화된 별도 설정</td>
      </tr>
      <tr>
          <td>Prepared Statement Multiplexing</td>
          <td>multiplex 모드 설정</td>
      </tr>
  </tbody>
</table>
<p>이들은 docker-compose에 Redis, Shadow DB를 추가하거나, 별도의 config.test-*.yaml을 준비하여 향후 확장할 수 있다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>&ldquo;테스트가 충분한가?&ldquo;에 대한 답은 기능 목록과 대조해야 나온다. 22개 테스트는 데이터 경로의 핵심을 커버했지만, 힌트 기반 기능(Route Hint, Timeout Hint)이나 관측 기능(Slow Query, Digest Reset)은 빠져 있었다. 이번 추가로 기존 인프라에서 테스트 가능한 범위는 거의 마무리되었다. 나머지는 테스트 인프라를 확장해야 하는 영역이다.</p>
]]></content:encoded></item></channel></rss>