<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://dorumugs.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://dorumugs.github.io/" rel="alternate" type="text/html" /><updated>2026-06-01T19:02:07+09:00</updated><id>https://dorumugs.github.io/feed.xml</id><title type="html">KayserDocs</title><subtitle>LLM/LangChain, AI 코딩 에이전트(Claude Code·Cursor), SSH·Docker DevOps 실무 노트 — Kayser So(dorumugs)의 기술 블로그 KayserDocs</subtitle><author><name>Kayser So(dorumugs)</name></author><entry><title type="html">(4/4) Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지</title><link href="https://dorumugs.github.io/coding/AI_%EC%BD%94%EB%94%A9_CLI_3%EC%A2%85_%EB%B9%84%EA%B5%90/" rel="alternate" type="text/html" title="(4/4) Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지" /><published>2026-06-01T18:50:00+09:00</published><updated>2026-06-01T18:50:00+09:00</updated><id>https://dorumugs.github.io/coding/AI_%EC%BD%94%EB%94%A9_CLI_3%EC%A2%85_%EB%B9%84%EA%B5%90</id><content type="html" xml:base="https://dorumugs.github.io/coding/AI_%EC%BD%94%EB%94%A9_CLI_3%EC%A2%85_%EB%B9%84%EA%B5%90/"><![CDATA[<div class="notice--info">
  <p><strong>🛠️ AI 코딩 CLI 명령어 모음 시리즈 (전체 4편)</strong></p>
  <ol>
    <li><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지</a></li>
    <li><a href="/coding/Codex_CLI_자주_쓰는_명령어_모음/">Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</a></li>
    <li><a href="/coding/Gemini_CLI_자주_쓰는_명령어_모음/">Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</a></li>
    <li><strong>Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지</strong> ← <em>지금 글</em></li>
  </ol>
</div>

<h1 id="summary">Summary</h1>

<p><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">1편</a> ~ <a href="/coding/Gemini_CLI_자주_쓰는_명령어_모음/">3편</a> 에서 <strong>Claude Code · Codex · Gemini CLI</strong> 세 도구의 명령어를 각각 정리했어요. 4편은 시리즈 마무리로, 셋을 <strong>같은 축으로 나란히 놓고 비교</strong>해보는 글이에요. 명령어 카탈로그를 다시 나열하기보단, 실제로 도구를 고를 때 갈리는 포인트(권한 모드 이름 차이, 헤드리스 모드의 깊이, 세션 이어가기 방식, 컨텍스트 파일 컨벤션, MCP/확장 통합)를 한 표에 정리하고 작업 성격별로 어떤 도구가 어울리는지 정리합니다.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>한눈에 보는 세 도구 비교 표 (모드·플래그·컨텍스트 파일 이름)</li>
    <li>권한/승인 모델 비교 — <code class="language-plaintext highlighter-rouge">permission-mode</code> vs <code class="language-plaintext highlighter-rouge">approval-mode</code> vs <code class="language-plaintext highlighter-rouge">/permissions</code></li>
    <li>헤드리스 자동화 비교 — <code class="language-plaintext highlighter-rouge">-p</code> / <code class="language-plaintext highlighter-rouge">codex exec</code> / <code class="language-plaintext highlighter-rouge">-p -o stream-json</code></li>
    <li>세션 이어가기와 체크포인트 비교</li>
    <li>컨텍스트 파일: <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> / <code class="language-plaintext highlighter-rouge">AGENTS.md</code> / <code class="language-plaintext highlighter-rouge">GEMINI.md</code> 차이</li>
    <li>MCP·확장·스킬 통합 결의 차이</li>
    <li>작업 유형별 추천 매트릭스</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-한눈에-보는-차이">1. 한눈에 보는 차이</h2>

<p>먼저 세 도구를 같은 축으로 나란히 놓아볼게요. “같은 개념인데 이름만 다른 것” 이 많아서 이 표가 머릿속 매핑에 도움 돼요.</p>

<table>
  <thead>
    <tr>
      <th>축</th>
      <th>Claude Code</th>
      <th>Codex</th>
      <th>Gemini CLI</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>인터랙티브 모드</strong></td>
      <td><code class="language-plaintext highlighter-rouge">claude</code></td>
      <td><code class="language-plaintext highlighter-rouge">codex</code></td>
      <td><code class="language-plaintext highlighter-rouge">gemini</code></td>
    </tr>
    <tr>
      <td><strong>비대화(헤드리스)</strong></td>
      <td><code class="language-plaintext highlighter-rouge">claude -p</code></td>
      <td><code class="language-plaintext highlighter-rouge">codex exec</code></td>
      <td><code class="language-plaintext highlighter-rouge">gemini -p</code></td>
    </tr>
    <tr>
      <td><strong>세션 이어가기</strong></td>
      <td><code class="language-plaintext highlighter-rouge">claude -c</code> / <code class="language-plaintext highlighter-rouge">-r</code></td>
      <td><code class="language-plaintext highlighter-rouge">codex resume --last</code> / <code class="language-plaintext highlighter-rouge">&lt;ID&gt;</code></td>
      <td><code class="language-plaintext highlighter-rouge">gemini -r</code> / <code class="language-plaintext highlighter-rouge">&lt;index&gt;</code> / <code class="language-plaintext highlighter-rouge">&lt;UUID&gt;</code></td>
    </tr>
    <tr>
      <td><strong>권한 모드 플래그</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--permission-mode</code></td>
      <td>(세션 내 <code class="language-plaintext highlighter-rouge">/permissions</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">--approval-mode</code></td>
    </tr>
    <tr>
      <td><strong>풀자동 모드 이름</strong></td>
      <td><code class="language-plaintext highlighter-rouge">bypassPermissions</code></td>
      <td><code class="language-plaintext highlighter-rouge">Full Access</code></td>
      <td><code class="language-plaintext highlighter-rouge">yolo</code></td>
    </tr>
    <tr>
      <td><strong>자동 편집 모드 이름</strong></td>
      <td><code class="language-plaintext highlighter-rouge">acceptEdits</code></td>
      <td><code class="language-plaintext highlighter-rouge">Auto</code> (기본)</td>
      <td><code class="language-plaintext highlighter-rouge">auto_edit</code></td>
    </tr>
    <tr>
      <td><strong>플랜(읽기) 모드</strong></td>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
      <td><code class="language-plaintext highlighter-rouge">Read-only</code></td>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
    </tr>
    <tr>
      <td><strong>worktree 격리</strong></td>
      <td><code class="language-plaintext highlighter-rouge">-w</code></td>
      <td>— (<code class="language-plaintext highlighter-rouge">--cd</code> + 외부 worktree)</td>
      <td><code class="language-plaintext highlighter-rouge">-w</code></td>
    </tr>
    <tr>
      <td><strong>추가 디렉토리</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--add-dir</code></td>
      <td><code class="language-plaintext highlighter-rouge">--add-dir</code></td>
      <td><code class="language-plaintext highlighter-rouge">--include-directories</code></td>
    </tr>
    <tr>
      <td><strong>컨텍스트 파일</strong></td>
      <td><code class="language-plaintext highlighter-rouge">CLAUDE.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">AGENTS.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">GEMINI.md</code></td>
    </tr>
    <tr>
      <td><strong>출력 포맷</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--output-format</code></td>
      <td><code class="language-plaintext highlighter-rouge">--json</code></td>
      <td><code class="language-plaintext highlighter-rouge">--output-format</code></td>
    </tr>
    <tr>
      <td><strong>세션 이름</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--name</code></td>
      <td>—</td>
      <td>—</td>
    </tr>
    <tr>
      <td><strong>이미지 첨부</strong></td>
      <td>— (<code class="language-plaintext highlighter-rouge">@</code>로 첨부)</td>
      <td><code class="language-plaintext highlighter-rouge">-i, --image</code></td>
      <td>—</td>
    </tr>
    <tr>
      <td><strong>외부 도구</strong></td>
      <td><code class="language-plaintext highlighter-rouge">/mcp</code>, <code class="language-plaintext highlighter-rouge">/agents</code></td>
      <td><code class="language-plaintext highlighter-rouge">/mcp</code>, 프로필</td>
      <td><code class="language-plaintext highlighter-rouge">gemini mcp</code>, <code class="language-plaintext highlighter-rouge">extensions</code>, <code class="language-plaintext highlighter-rouge">skills</code></td>
    </tr>
  </tbody>
</table>

<p>큰 그림은 정말 비슷한데, 모드 이름과 컨텍스트 파일명이 각자 따로 진화한 거예요. 셋 다 쓰다 보면 머릿속에서 자연스럽게 매핑이 잡힙니다.</p>

<p><br /></p>

<p><br /></p>

<h2 id="2-권한승인-모델--가장-헷갈리는-지점">2. 권한·승인 모델 — 가장 헷갈리는 지점</h2>

<p>세 도구가 모두 “위험한 액션은 사용자에게 묻는다” 는 같은 결인데, 모델 이름과 단계 수가 미묘하게 달라요.</p>

<h3 id="2-1-claude-code--5단계-플래그-중심">2-1. Claude Code — 5단계, 플래그 중심</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default → acceptEdits → auto → plan → bypassPermissions
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">default</code> 매번 묻기</li>
  <li><code class="language-plaintext highlighter-rouge">acceptEdits</code> 파일 편집 자동 / 셸은 묻기</li>
  <li><code class="language-plaintext highlighter-rouge">auto</code> 분류기 기반 자동 (위험한 것만 묻기) ← 일상에서 가장 자주 쓰는 모드</li>
  <li><code class="language-plaintext highlighter-rouge">plan</code> 코드 안 건드림</li>
  <li><code class="language-plaintext highlighter-rouge">bypassPermissions</code> 전부 자동 (위험)</li>
</ul>

<p>CLI 띄울 때 <code class="language-plaintext highlighter-rouge">--permission-mode auto</code> 같이 박거나, 세션 안에서 <code class="language-plaintext highlighter-rouge">Shift+Tab</code> 으로 순환.</p>

<h3 id="2-2-codex--3단계-세션-내-전환-중심">2-2. Codex — 3단계, 세션 내 전환 중심</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Read-only → Auto (기본) → Full Access
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Read-only</code> 파일/명령 손 못 댐</li>
  <li><code class="language-plaintext highlighter-rouge">Auto</code> 작업 폴더 내 자동, 외부는 묻기</li>
  <li><code class="language-plaintext highlighter-rouge">Full Access</code> 전 시스템·네트워크 자동</li>
</ul>

<p>대부분 인터랙티브 세션 안에서 <code class="language-plaintext highlighter-rouge">/permissions</code> 로 전환하는 흐름. CLI 플래그로 한 번에 박는 경로가 다른 둘보다 약해요.</p>

<h3 id="2-3-gemini-cli--4단계-플래그--alias">2-3. Gemini CLI — 4단계, 플래그 + alias</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>default → auto_edit → yolo → plan
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">default</code> 매번 묻기</li>
  <li><code class="language-plaintext highlighter-rouge">auto_edit</code> 파일 편집 자동 / 셸은 묻기 (= Claude 의 <code class="language-plaintext highlighter-rouge">acceptEdits</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">yolo</code> 전부 자동 (= Claude 의 <code class="language-plaintext highlighter-rouge">bypassPermissions</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">plan</code> 코드 안 건드림</li>
</ul>

<p>CLI 띄울 때 <code class="language-plaintext highlighter-rouge">--approval-mode yolo</code> 식으로 박는 게 가장 깔끔.</p>

<h3 id="2-4-같은-개념-매핑">2-4. 같은 개념 매핑</h3>

<table>
  <thead>
    <tr>
      <th>의미</th>
      <th>Claude</th>
      <th>Codex</th>
      <th>Gemini</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>매번 묻기</td>
      <td><code class="language-plaintext highlighter-rouge">default</code></td>
      <td>(없음)</td>
      <td><code class="language-plaintext highlighter-rouge">default</code></td>
    </tr>
    <tr>
      <td>읽기 전용</td>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
      <td><code class="language-plaintext highlighter-rouge">Read-only</code></td>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
    </tr>
    <tr>
      <td>편집 자동, 셸 묻기</td>
      <td><code class="language-plaintext highlighter-rouge">acceptEdits</code></td>
      <td>(없음)</td>
      <td><code class="language-plaintext highlighter-rouge">auto_edit</code></td>
    </tr>
    <tr>
      <td>작업 폴더 자동</td>
      <td><code class="language-plaintext highlighter-rouge">auto</code></td>
      <td><code class="language-plaintext highlighter-rouge">Auto</code></td>
      <td>(별도 없음)</td>
    </tr>
    <tr>
      <td>전부 자동</td>
      <td><code class="language-plaintext highlighter-rouge">bypassPermissions</code></td>
      <td><code class="language-plaintext highlighter-rouge">Full Access</code></td>
      <td><code class="language-plaintext highlighter-rouge">yolo</code></td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>🚨 풀자동 모드(<code class="language-plaintext highlighter-rouge">bypassPermissions</code> / <code class="language-plaintext highlighter-rouge">Full Access</code> / <code class="language-plaintext highlighter-rouge">yolo</code>) 는 호스트 파일·네트워크에 자유롭게 액션이 들어가서 한 번의 실수가 크게 번질 수 있어요. <strong>세 도구 모두 격리된 컨테이너/VM 안에서만 권장</strong>합니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="3-헤드리스-자동화-비교">3. 헤드리스 자동화 비교</h2>

<p>CI·스크립트에 끼워넣을 때의 사용감이 가장 갈리는 지점이에요.</p>

<h3 id="3-1-claude-code---p----output-format-json">3-1. Claude Code — <code class="language-plaintext highlighter-rouge">-p</code> + <code class="language-plaintext highlighter-rouge">--output-format json</code></h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-p</span> <span class="s2">"/review"</span> <span class="nt">--output-format</span> json
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/review</code> 같은 슬래시 명령을 <code class="language-plaintext highlighter-rouge">-p</code> 안에 그대로 박을 수 있는 게 특징. CI 에서 셀프 리뷰 봇 만들기 편해요.</p>

<h3 id="3-2-codex--codex-exec----json">3-2. Codex — <code class="language-plaintext highlighter-rouge">codex exec</code> + <code class="language-plaintext highlighter-rouge">--json</code></h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nb">exec</span> <span class="nt">--json</span> <span class="s2">"이 PR diff 요약"</span>
</code></pre></div></div>

<p>전용 서브커맨드(<code class="language-plaintext highlighter-rouge">exec</code>)로 분리돼 있어서 의도가 명확해요. <code class="language-plaintext highlighter-rouge">--json</code> 한 줄로 깔끔.</p>

<h3 id="3-3-gemini---p---o-stream-json">3-3. Gemini — <code class="language-plaintext highlighter-rouge">-p</code> + <code class="language-plaintext highlighter-rouge">-o stream-json</code></h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-p</span> <span class="s2">"긴 리팩토링"</span> <span class="nt">-o</span> stream-json | <span class="nb">tee </span>progress.jsonl
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">stream-json</code> 출력이 두드러져요. <strong>토큰 단위 스트리밍을 라인 단위로 받아서</strong> 파이프로 흘리면 진행 상황을 CI 로그에 실시간으로 박을 수 있어요. 셋 중 헤드리스 모드의 깊이가 가장 다듬어진 인상.</p>

<h3 id="3-4-매칭-매트릭스">3-4. 매칭 매트릭스</h3>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>추천</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>한 번에 결과 받고 끝</td>
      <td>셋 다 OK — <code class="language-plaintext highlighter-rouge">claude -p</code> / <code class="language-plaintext highlighter-rouge">codex exec</code> / <code class="language-plaintext highlighter-rouge">gemini -p</code></td>
    </tr>
    <tr>
      <td>진행 상황 실시간 스트리밍</td>
      <td><strong>Gemini</strong> (<code class="language-plaintext highlighter-rouge">-o stream-json</code>)</td>
    </tr>
    <tr>
      <td>슬래시 명령 그대로 자동화</td>
      <td><strong>Claude Code</strong> (<code class="language-plaintext highlighter-rouge">-p "/review"</code>)</td>
    </tr>
    <tr>
      <td>단순 JSON 파싱 후처리</td>
      <td>Codex (<code class="language-plaintext highlighter-rouge">exec --json</code>) 가 의도가 가장 명확</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p><br /></p>

<h2 id="4-세션-이어가기와-체크포인트">4. 세션 이어가기와 체크포인트</h2>

<p>작업 도중 끊겼다가 돌아올 때의 사용감 비교.</p>

<h3 id="4-1-claude-code">4-1. Claude Code</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-c</span>                <span class="c"># 현재 디렉토리 최근 세션</span>
claude <span class="nt">-r</span>                <span class="c"># 피커</span>
claude <span class="nt">-r</span> <span class="s2">"blog"</span>         <span class="c"># 이름 검색</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--name</code> 으로 세션에 이름 박아두면 <code class="language-plaintext highlighter-rouge">-r "blog"</code> 같이 검색해서 복귀 가능. <strong>세션 명명</strong> 이 강점.</p>

<h3 id="4-2-codex">4-2. Codex</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex resume             <span class="c"># 피커</span>
codex resume <span class="nt">--last</span>      <span class="c"># 최근</span>
codex resume &lt;SESSION_ID&gt;
</code></pre></div></div>

<p>이름 검색이 별도로 없고 ID 기반. 단, <strong><code class="language-plaintext highlighter-rouge">codex cloud</code></strong> 와 결합되면 클라우드 세션도 같은 결로 이어가기 가능해서 멀리 떨어진 환경 간 연속성에서 강해요.</p>

<h3 id="4-3-gemini-cli">4-3. Gemini CLI</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-r</span>                                 <span class="c"># 최근</span>
gemini <span class="nt">-r</span> 1                               <span class="c"># 인덱스</span>
gemini <span class="nt">-r</span> &lt;UUID&gt;                          <span class="c"># UUID</span>
gemini <span class="nt">--list-sessions</span>
gemini <span class="nt">--delete-session</span> 2
</code></pre></div></div>

<p>세션 <strong>삭제·목록</strong> 같은 관리 명령이 CLI 1급 시민이라는 게 특징. 거기에 세션 안에서 <code class="language-plaintext highlighter-rouge">/resume save &lt;이름&gt;</code> 으로 <strong>체크포인트</strong> 를 박을 수 있어서 한 세션 안에서 가지치기가 깔끔해요.</p>

<h3 id="4-4-매칭">4-4. 매칭</h3>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>추천</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>짧게 끊고 자주 복귀</td>
      <td>Claude (<code class="language-plaintext highlighter-rouge">-c</code>) 가 가장 빠름</td>
    </tr>
    <tr>
      <td>세션 안에서 분기·되돌리기</td>
      <td><strong>Gemini</strong> (<code class="language-plaintext highlighter-rouge">/resume save &lt;이름&gt;</code>)</td>
    </tr>
    <tr>
      <td>원격(클라우드) ↔ 로컬 연속성</td>
      <td><strong>Codex</strong> (<code class="language-plaintext highlighter-rouge">cloud</code> + <code class="language-plaintext highlighter-rouge">resume</code>)</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p><br /></p>

<h2 id="5-컨텍스트-파일--claudemd--agentsmd--geminimd">5. 컨텍스트 파일 — <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> / <code class="language-plaintext highlighter-rouge">AGENTS.md</code> / <code class="language-plaintext highlighter-rouge">GEMINI.md</code></h2>

<p>각 도구가 저장소를 처음 열 때 자동으로 읽는 컨텍스트 파일이에요. 이름만 다를 뿐 역할은 거의 같아요.</p>

<table>
  <thead>
    <tr>
      <th>파일</th>
      <th>도구</th>
      <th>위치</th>
      <th>자동 로드</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CLAUDE.md</code></td>
      <td>Claude Code</td>
      <td>저장소 루트, 사용자 홈</td>
      <td>✅</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AGENTS.md</code></td>
      <td>Codex</td>
      <td>저장소 루트, <code class="language-plaintext highlighter-rouge">~/.codex/AGENTS.md</code></td>
      <td>✅</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">GEMINI.md</code></td>
      <td>Gemini</td>
      <td>저장소 루트, <code class="language-plaintext highlighter-rouge">~/.gemini/GEMINI.md</code></td>
      <td>✅ (<code class="language-plaintext highlighter-rouge">/memory reload</code> 로 핫리로드)</td>
    </tr>
  </tbody>
</table>

<p><strong>같은 저장소에서 셋을 다 굴리고 싶다면</strong> 보통 두 가지 패턴이 있어요.</p>

<ol>
  <li><strong>세 파일 모두 같은 내용으로 유지</strong> — 가장 단순. 한쪽 수정하면 나머지에 복붙. 작은 저장소에서 OK.</li>
  <li><strong>한 파일을 마스터로 두고 나머지는 심볼릭 링크</strong> — 예: <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> 를 마스터로 하고 <code class="language-plaintext highlighter-rouge">AGENTS.md</code>, <code class="language-plaintext highlighter-rouge">GEMINI.md</code> 는 <code class="language-plaintext highlighter-rouge">ln -s CLAUDE.md ...</code> 로 링크. 한 곳만 고치면 셋 다 반영.</li>
</ol>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 마스터 = CLAUDE.md 전략</span>
<span class="nb">ln</span> <span class="nt">-sf</span> CLAUDE.md AGENTS.md
<span class="nb">ln</span> <span class="nt">-sf</span> CLAUDE.md GEMINI.md
</code></pre></div></div>

<blockquote>
  <p>💡 단, <strong>도구별로 다른 규칙이 있다면</strong> 심볼릭 링크는 깨져요. 그땐 공통부와 도구별 추가분을 섹션으로 나눠두고 각 파일에서 필요한 섹션만 포함하는 식이 안전합니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="6-mcp--확장--스킬--외부-도구-통합">6. MCP · 확장 · 스킬 — 외부 도구 통합</h2>

<p>세 도구 모두 외부 도구를 붙이는 통로를 가지고 있는데, 결이 조금씩 달라요.</p>

<h3 id="6-1-claude-code">6-1. Claude Code</h3>

<ul>
  <li><strong>MCP</strong>: <code class="language-plaintext highlighter-rouge">/mcp</code> 로 관리. 가장 표준에 가까움.</li>
  <li><strong>서브에이전트</strong>: <code class="language-plaintext highlighter-rouge">/agents</code> 로 페르소나 등록 (코드 리뷰 전용 등).</li>
  <li><strong>스킬/플러그인</strong>: <code class="language-plaintext highlighter-rouge">/skills</code> 로 외부 능력 모듈 호출. 표준화된 인터페이스.</li>
</ul>

<h3 id="6-2-codex">6-2. Codex</h3>

<ul>
  <li><strong>MCP</strong>: <code class="language-plaintext highlighter-rouge">codex mcp</code> 와 <code class="language-plaintext highlighter-rouge">~/.codex/config.toml</code> 의 <code class="language-plaintext highlighter-rouge">[mcp_servers]</code> 섹션.</li>
  <li><strong>프로필</strong>: <code class="language-plaintext highlighter-rouge">--profile &lt;name&gt;</code> 으로 모델·MCP·권한 조합을 통째로 묶어서 띄움. 작업 컨텍스트 전환이 한 글자로 끝나는 강점.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">codex cloud</code></strong> 와 결합되면 클라우드 환경 안에서도 같은 도구 셋업 유지.</li>
</ul>

<h3 id="6-3-gemini-cli">6-3. Gemini CLI</h3>

<ul>
  <li><strong>MCP</strong>: <code class="language-plaintext highlighter-rouge">gemini mcp</code> 로 관리.</li>
  <li><strong>확장(Extensions)</strong>: <code class="language-plaintext highlighter-rouge">gemini extensions</code> 로 더 큰 단위(파일·UI 통합 등) 의 확장 관리. <code class="language-plaintext highlighter-rouge">@github</code> 같은 mention 이 여기서 옴.</li>
  <li><strong>스킬(Skills)</strong>: <code class="language-plaintext highlighter-rouge">gemini skills</code> 로 명시적인 능력 모듈 시스템. 셋 중 가장 모듈화 단위가 세분돼 있어요.</li>
</ul>

<h3 id="6-4-매칭">6-4. 매칭</h3>

<table>
  <thead>
    <tr>
      <th>시나리오</th>
      <th>추천</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>표준 MCP 만 잘 쓰면 됨</td>
      <td>셋 다 OK</td>
    </tr>
    <tr>
      <td>작업별로 도구·모델 셋업 통째 전환</td>
      <td><strong>Codex</strong> (<code class="language-plaintext highlighter-rouge">--profile</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">@github</code> 처럼 확장이 mention 으로 자연스럽게 호출</td>
      <td><strong>Gemini</strong> (<code class="language-plaintext highlighter-rouge">@&lt;extension&gt;</code> mention)</td>
    </tr>
    <tr>
      <td>페르소나/역할 분리 (리뷰어·테스터)</td>
      <td><strong>Claude</strong> (<code class="language-plaintext highlighter-rouge">/agents</code>)</td>
    </tr>
  </tbody>
</table>

<p><br /></p>

<p><br /></p>

<h2 id="7-작업-유형별-추천-매트릭스">7. 작업 유형별 추천 매트릭스</h2>

<p>실제로 도구를 고를 때 가장 도움이 되는 표예요. “지금 하려는 작업” 을 기준으로 어떤 도구가 가장 매끄러운지 정리했어요.</p>

<table>
  <thead>
    <tr>
      <th>작업</th>
      <th>추천 도구</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>여러 세션을 이름으로 구분해 동시 작업</strong></td>
      <td>Claude Code</td>
      <td><code class="language-plaintext highlighter-rouge">--name</code> + <code class="language-plaintext highlighter-rouge">-r "&lt;name&gt;"</code> 검색</td>
    </tr>
    <tr>
      <td><strong>PR 셀프 리뷰 자동화</strong></td>
      <td>Claude Code</td>
      <td><code class="language-plaintext highlighter-rouge">-p "/review"</code> 가 가장 짧음</td>
    </tr>
    <tr>
      <td><strong>이미지 시안 보고 코드 만들기</strong></td>
      <td>Codex</td>
      <td><code class="language-plaintext highlighter-rouge">-i screenshot.png,mockup.png</code> 다중 이미지</td>
    </tr>
    <tr>
      <td><strong>모노레포 인접 패키지 같이 만지기</strong></td>
      <td>셋 다 OK</td>
      <td><code class="language-plaintext highlighter-rouge">--add-dir</code> / <code class="language-plaintext highlighter-rouge">--include-directories</code></td>
    </tr>
    <tr>
      <td><strong>CI 파이프라인에서 진행 상황 스트리밍</strong></td>
      <td>Gemini</td>
      <td><code class="language-plaintext highlighter-rouge">-p -o stream-json</code></td>
    </tr>
    <tr>
      <td><strong>격리 환경에서 풀자동 실행</strong></td>
      <td>Codex</td>
      <td><code class="language-plaintext highlighter-rouge">/permissions</code> Full Access + 컨테이너</td>
    </tr>
    <tr>
      <td><strong>읽기 전용으로 안전하게 훑기</strong></td>
      <td>Claude / Gemini</td>
      <td><code class="language-plaintext highlighter-rouge">plan</code> 모드</td>
    </tr>
    <tr>
      <td><strong>외부 시스템(GitHub·Linear 등) 호출</strong></td>
      <td>Gemini</td>
      <td><code class="language-plaintext highlighter-rouge">@github</code> 확장 mention 자연스러움</td>
    </tr>
    <tr>
      <td><strong>클라우드 환경에서 긴 작업 위임</strong></td>
      <td>Codex</td>
      <td><code class="language-plaintext highlighter-rouge">codex cloud exec --env --attempts</code></td>
    </tr>
    <tr>
      <td><strong>세션 안에서 분기·되돌리기</strong></td>
      <td>Gemini</td>
      <td><code class="language-plaintext highlighter-rouge">/resume save &lt;name&gt;</code> 체크포인트</td>
    </tr>
    <tr>
      <td><strong>새 저장소 처음 셋업</strong></td>
      <td>셋 다 OK</td>
      <td><code class="language-plaintext highlighter-rouge">/init</code> 또는 컨텍스트 파일 수동 작성</td>
    </tr>
  </tbody>
</table>

<p>물론 <strong>한 도구로 다 할 수 있는 작업이 대부분</strong> 이에요. 이 표는 “굳이 고른다면” 의 가이드이고, 실제 작업에선 본인이 손에 익은 도구를 그대로 쓰는 게 가장 빠릅니다.</p>

<p><br /></p>

<p><br /></p>

<h2 id="8-한-줄-결론">8. 한 줄 결론</h2>

<ul>
  <li><strong>Claude Code</strong>: 세션 명명·서브에이전트·플러그인 같은 <strong>워크플로우 인프라</strong> 가 가장 두텁다. 매일 여러 작업을 동시에 굴리는 사람에게 어울려요.</li>
  <li><strong>Codex</strong>: <strong>프로필</strong> 과 <strong>클라우드 통합</strong> 이 강점. 작업 컨텍스트 전환·원격 위임이 잦은 사람에게.</li>
  <li><strong>Gemini CLI</strong>: <strong>헤드리스 자동화</strong> 와 <strong>확장 mention</strong> 이 정돈됨. CI·스크립트 자동화 비중이 큰 사람에게.</li>
</ul>

<p>셋 다 무료/저렴한 티어로 깔아볼 수 있으니, 이 표를 가이드 삼아 본인 워크플로우에서 한 번씩 굴려보고 손에 가장 잘 붙는 걸 메인으로 잡는 걸 추천드려요. 저는 작업 종류에 따라 셋을 번갈아 쓰고 있는데, 이 시리즈를 정리하면서 “어떤 작업엔 어떤 도구가 더 빠른지” 가 머릿속에서 한층 정리됐어요.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 시리즈 후속으로 <strong><code class="language-plaintext highlighter-rouge">CLAUDE.md</code> / <code class="language-plaintext highlighter-rouge">AGENTS.md</code> / <code class="language-plaintext highlighter-rouge">GEMINI.md</code> 한 파일로 통합 관리하는 패턴</strong> 을 더 자세히 정리해볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/Gemini_CLI_자주_쓰는_명령어_모음/">(3/4) Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="ClaudeCode" /><category term="Codex" /><category term="GeminiCLI" /><category term="AI코딩" /><category term="CLI비교" /><category term="워크플로우" /><category term="권한모드" /><category term="컨텍스트" /><category term="개발도구" /><summary type="html"><![CDATA[Claude Code · Codex · Gemini CLI 세 도구를 권한 모드, 헤드리스 자동화, 세션 이어가기, 컨텍스트 파일, MCP·확장 측면에서 깊게 비교해봤어요.]]></summary></entry><entry><title type="html">(3/4) Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</title><link href="https://dorumugs.github.io/coding/Gemini_CLI_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C/" rel="alternate" type="text/html" title="(3/4) Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지" /><published>2026-06-01T18:30:00+09:00</published><updated>2026-06-01T18:30:00+09:00</updated><id>https://dorumugs.github.io/coding/Gemini_CLI_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C</id><content type="html" xml:base="https://dorumugs.github.io/coding/Gemini_CLI_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C/"><![CDATA[<div class="notice--info">
  <p><strong>🛠️ AI 코딩 CLI 명령어 모음 시리즈 (전체 4편)</strong></p>
  <ol>
    <li><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지</a></li>
    <li><a href="/coding/Codex_CLI_자주_쓰는_명령어_모음/">Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</a></li>
    <li><strong>Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/AI_코딩_CLI_3종_비교/">Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지</a></li>
  </ol>
</div>

<h1 id="summary">Summary</h1>

<p><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">1편 Claude Code</a>, <a href="/coding/Codex_CLI_자주_쓰는_명령어_모음/">2편 Codex CLI</a> 에 이어 시리즈 마지막 편은 <strong>Google Gemini CLI</strong> 예요. 셋 다 결은 비슷한데, Gemini 는 <code class="language-plaintext highlighter-rouge">--approval-mode</code> 와 <code class="language-plaintext highlighter-rouge">--prompt</code> 중심으로 헤드리스(비대화) 자동화 친화적인 색깔이 강해요. 이 글에선 자주 쓰는 CLI 플래그, 서브커맨드, 세션 슬래시 명령어, 그리고 헤드리스 모드 활용 예시까지 한 번에 훑어볼게요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>Gemini CLI 플래그 (<code class="language-plaintext highlighter-rouge">--model</code>, <code class="language-plaintext highlighter-rouge">-p</code>, <code class="language-plaintext highlighter-rouge">-i</code>, <code class="language-plaintext highlighter-rouge">--approval-mode</code>, <code class="language-plaintext highlighter-rouge">--sandbox</code>, <code class="language-plaintext highlighter-rouge">--worktree</code>, <code class="language-plaintext highlighter-rouge">--resume</code> 등)</li>
    <li>권한 모드(<code class="language-plaintext highlighter-rouge">default</code> / <code class="language-plaintext highlighter-rouge">auto_edit</code> / <code class="language-plaintext highlighter-rouge">yolo</code> / <code class="language-plaintext highlighter-rouge">plan</code>)</li>
    <li>서브커맨드 (<code class="language-plaintext highlighter-rouge">gemini mcp</code>, <code class="language-plaintext highlighter-rouge">gemini extensions</code>, <code class="language-plaintext highlighter-rouge">gemini skills</code>, <code class="language-plaintext highlighter-rouge">gemini update</code>)</li>
    <li>슬래시 명령어 (<code class="language-plaintext highlighter-rouge">/help</code>, <code class="language-plaintext highlighter-rouge">/quit</code>, <code class="language-plaintext highlighter-rouge">/resume</code>, <code class="language-plaintext highlighter-rouge">/chat</code>, <code class="language-plaintext highlighter-rouge">/memory reload</code>, <code class="language-plaintext highlighter-rouge">/mcp reload</code>, <code class="language-plaintext highlighter-rouge">/settings</code>, <code class="language-plaintext highlighter-rouge">/bug</code>)</li>
    <li>입력 prefix <code class="language-plaintext highlighter-rouge">@</code> 와 헤드리스 자동화 예시</li>
  </ul>
</blockquote>

<blockquote>
  <p>⚠️ Gemini CLI 도 업데이트 주기가 짧고 공식 docs 가 모듈별로 분산돼 있어요. 본문의 옵션이 안 보이면 <code class="language-plaintext highlighter-rouge">gemini --help</code> 와 <code class="language-plaintext highlighter-rouge">~/.gemini/</code> 설정 디렉토리를 먼저 확인하는 걸 추천드려요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-cli-플래그--자주-쓰는-것부터">1. CLI 플래그 — 자주 쓰는 것부터</h2>

<h3 id="1-1---model---m--모델-지정">1-1. <code class="language-plaintext highlighter-rouge">--model</code> / <code class="language-plaintext highlighter-rouge">-m</code> — 모델 지정</h3>

<p>세션 단위로 모델을 지정해요. alias 와 풀네임 둘 다 됩니다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-m</span> gemini-2.5-flash
gemini <span class="nt">--model</span> gemini-2.5-pro
</code></pre></div></div>

<p>용량 큰 추론은 Pro, 단순 대량 처리는 Flash 로 갈아끼우는 흐름이 일반적이에요.</p>

<h3 id="1-2---prompt---p--헤드리스-비대화-실행">1-2. <code class="language-plaintext highlighter-rouge">--prompt</code> / <code class="language-plaintext highlighter-rouge">-p</code> — 헤드리스 (비대화) 실행</h3>

<p>Claude Code 의 <code class="language-plaintext highlighter-rouge">-p</code>, Codex 의 <code class="language-plaintext highlighter-rouge">codex exec</code> 와 같은 결. 결과만 한 번 뱉고 끝나서 스크립트·파이프에서 쓰기 좋아요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-p</span> <span class="s2">"이 디렉토리 구조 한 단락으로 요약해줘"</span>
<span class="nb">cat </span>error.log | gemini <span class="nt">-p</span> <span class="s2">"이 로그가 의미하는 에러는?"</span>
</code></pre></div></div>

<p>stdin 입력이 있으면 거기에 프롬프트가 자동으로 덧붙어요. 파이프 친화적인 구조에요.</p>

<h3 id="1-3---prompt-interactive---i--한-줄-던지고-대화로-이어가기">1-3. <code class="language-plaintext highlighter-rouge">--prompt-interactive</code> / <code class="language-plaintext highlighter-rouge">-i</code> — 한 줄 던지고 대화로 이어가기</h3>

<p><code class="language-plaintext highlighter-rouge">-p</code> 와 비슷한데, 결과 뱉고 종료하지 않고 인터랙티브 세션으로 들어가요. “첫 메시지만 미리 박고 시작” 모드.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-i</span> <span class="s2">"이 저장소 구조부터 훑어줘"</span>
</code></pre></div></div>

<h3 id="1-4---approval-mode--권한-모드-핵심">1-4. <code class="language-plaintext highlighter-rouge">--approval-mode</code> — 권한 모드 (핵심)</h3>

<p>Gemini 의 권한 모델은 모드 네 가지로 표현돼요.</p>

<table>
  <thead>
    <tr>
      <th>모드</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">default</code></td>
      <td>모든 액션 묻기 (기본값)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">auto_edit</code></td>
      <td>파일 편집은 자동 승인, 쉘 명령은 묻기</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">yolo</code></td>
      <td>모든 액션 자동 승인</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
      <td>코드 안 건드리고 계획만 세움</td>
    </tr>
  </tbody>
</table>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">--approval-mode</span> auto_edit
gemini <span class="nt">--approval-mode</span> plan
gemini <span class="nt">--approval-mode</span> yolo   <span class="c"># 위험</span>
</code></pre></div></div>

<blockquote>
  <p>🚨 <code class="language-plaintext highlighter-rouge">yolo</code> 는 격리된 환경에서만 권장합니다. 호스트에 자유롭게 액션이 들어가서 한 번의 실수가 크게 번질 수 있어요. 일회용 컨테이너/VM 안에서만 쓰세요.</p>
</blockquote>

<blockquote>
  <p>💡 옛 버전의 <code class="language-plaintext highlighter-rouge">--yolo</code> / <code class="language-plaintext highlighter-rouge">-y</code> 플래그는 deprecate 됐어요. 같은 동작은 <code class="language-plaintext highlighter-rouge">--approval-mode=yolo</code> 로 표현합니다.</p>
</blockquote>

<h3 id="1-5---sandbox---s--샌드박스-실행">1-5. <code class="language-plaintext highlighter-rouge">--sandbox</code> / <code class="language-plaintext highlighter-rouge">-s</code> — 샌드박스 실행</h3>

<p>도구 호출을 격리된 샌드박스(컨테이너 등) 안에서 돌리는 모드. <code class="language-plaintext highlighter-rouge">yolo</code> 와 같이 박으면 안전성 + 자동화 균형이 잡혀요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-s</span>
gemini <span class="nt">-s</span> <span class="nt">--approval-mode</span> yolo <span class="s2">"이 마이그레이션 한 번에 끝내줘"</span>
</code></pre></div></div>

<h3 id="1-6---include-directories--추가-디렉토리-노출">1-6. <code class="language-plaintext highlighter-rouge">--include-directories</code> — 추가 디렉토리 노출</h3>

<p>작업 디렉토리 외에 다른 폴더도 컨텍스트에 같이 노출해요. 쉼표 또는 반복 지정 가능.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">--include-directories</span> ../lib,../docs
</code></pre></div></div>

<p>모노레포에서 인접 패키지 같이 만질 때 자주 씁니다.</p>

<h3 id="1-7---worktree---w--git-worktree-격리">1-7. <code class="language-plaintext highlighter-rouge">--worktree</code> / <code class="language-plaintext highlighter-rouge">-w</code> — git worktree 격리</h3>

<p>브랜치를 안 건드리고 별도 worktree 에서 실험할 때.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-w</span>                    <span class="c"># 자동 이름 (worktree-a1b2c3d4 같은 식)</span>
gemini <span class="nt">--worktree</span> feature-x  <span class="c"># 이름 지정 시 디렉토리·브랜치 이름 동일</span>
</code></pre></div></div>

<p>Claude Code 의 <code class="language-plaintext highlighter-rouge">-w</code> 와 동일한 결.</p>

<h3 id="1-8---resume---r--이전-세션-이어가기">1-8. <code class="language-plaintext highlighter-rouge">--resume</code> / <code class="language-plaintext highlighter-rouge">-r</code> — 이전 세션 이어가기</h3>

<p>이전 세션을 다시 열어요. 여러 방식 지원.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">--resume</span>                                           <span class="c"># 가장 최근 세션</span>
gemini <span class="nt">--resume</span> 1                                         <span class="c"># 인덱스로 지정</span>
gemini <span class="nt">--resume</span> a1b2c3d4-e5f6-7890-abcd-ef1234567890      <span class="c"># UUID 로 지정</span>
</code></pre></div></div>

<p>세션 목록·삭제도 같은 결로:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">--list-sessions</span>
gemini <span class="nt">--delete-session</span> 2
</code></pre></div></div>

<h3 id="1-9---output-format---o--출력-포맷">1-9. <code class="language-plaintext highlighter-rouge">--output-format</code> / <code class="language-plaintext highlighter-rouge">-o</code> — 출력 포맷</h3>

<p><code class="language-plaintext highlighter-rouge">-p</code> 와 같이 쓸 때 결과 포맷을 바꿔요. 후처리 파이프라인에 끼워넣기 편함.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-p</span> <span class="s2">"이 PR diff 요약"</span> <span class="nt">-o</span> json
gemini <span class="nt">-p</span> <span class="s2">"긴 작업 시작"</span> <span class="nt">-o</span> stream-json
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">stream-json</code> 은 결과를 토큰 단위로 스트리밍해서 실시간 진행 상황을 잡아낼 수 있어요.</p>

<h3 id="1-10---debug---d--디버그-모드">1-10. <code class="language-plaintext highlighter-rouge">--debug</code> / <code class="language-plaintext highlighter-rouge">-d</code> — 디버그 모드</h3>

<p>내부 호출·도구 실행 로그를 자세히 찍어요. 동작이 이상할 때 한 번씩 켭니다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-d</span>
</code></pre></div></div>

<h3 id="1-11---extensions---e--확장만-골라-활성">1-11. <code class="language-plaintext highlighter-rouge">--extensions</code> / <code class="language-plaintext highlighter-rouge">-e</code> — 확장만 골라 활성</h3>

<p>설치된 확장 중 일부만 켜고 싶을 때.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-e</span> github,linear
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="2-서브커맨드">2. 서브커맨드</h2>

<p><code class="language-plaintext highlighter-rouge">gemini</code> 뒤에 붙는 모드들. 자주 쓰는 것만 정리할게요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini mcp           <span class="c"># MCP 서버 설정/관리</span>
gemini extensions    <span class="c"># 확장 관리</span>
gemini skills        <span class="c"># 스킬 관리 (등록/조회/리로드)</span>
gemini update        <span class="c"># 최신 버전으로 업데이트</span>
</code></pre></div></div>

<p>MCP·extension·skill 모두 외부 도구·기능을 Gemini 에 붙이는 통로에요. 새 도구 붙일 때 가장 먼저 손이 가는 명령들.</p>

<p><br /></p>

<p><br /></p>

<h2 id="3-세션-안-슬래시-명령어">3. 세션 안 슬래시 명령어</h2>

<p>세션에서 <code class="language-plaintext highlighter-rouge">/</code> 로 시작하는 명령어들. 다른 두 CLI 보다 더 모듈화돼 있어서 “리로드” 류 명령이 카테고리별로 따로 있어요.</p>

<h3 id="3-1-help--quit">3-1. <code class="language-plaintext highlighter-rouge">/help</code> / <code class="language-plaintext highlighter-rouge">/quit</code></h3>

<p>기본 중의 기본. 도움말 보기, 세션 종료.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/help
/quit
</code></pre></div></div>

<h3 id="3-2-memory-reload--컨텍스트-파일-리로드">3-2. <code class="language-plaintext highlighter-rouge">/memory reload</code> — 컨텍스트 파일 리로드</h3>

<p><code class="language-plaintext highlighter-rouge">GEMINI.md</code> 같은 컨텍스트 파일을 세션 도중 다시 읽어와요. 파일 수정하고 바로 반영하고 싶을 때.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/memory reload
</code></pre></div></div>

<h3 id="3-3-mcp-reload--mcp-서버-재시작">3-3. <code class="language-plaintext highlighter-rouge">/mcp reload</code> — MCP 서버 재시작</h3>

<p>MCP 서버 설정을 바꿨거나 죽었을 때 한 번 리로드.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/mcp reload
</code></pre></div></div>

<h3 id="3-4-extensions-reload--skills-reload--agents-reload--commands-reload">3-4. <code class="language-plaintext highlighter-rouge">/extensions reload</code> / <code class="language-plaintext highlighter-rouge">/skills reload</code> / <code class="language-plaintext highlighter-rouge">/agents reload</code> / <code class="language-plaintext highlighter-rouge">/commands reload</code></h3>

<p>같은 결의 리로드 명령들. 확장, 스킬, 에이전트, 커스텀 슬래시 명령을 각각 따로 다시 불러옵니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/extensions reload
/skills reload
/agents reload
/commands reload
</code></pre></div></div>

<p>저는 새 확장이나 커스텀 명령 만들면서 테스트할 때 이 네 개를 자주 두드립니다.</p>

<h3 id="3-5-resume--chat--세션-브라우저와-체크포인트">3-5. <code class="language-plaintext highlighter-rouge">/resume</code> / <code class="language-plaintext highlighter-rouge">/chat</code> — 세션 브라우저와 체크포인트</h3>

<p><code class="language-plaintext highlighter-rouge">/resume</code> 은 세션 브라우저 UI 를 띄워서 이전 세션을 고르거나, 현재 세션에 이름 붙은 체크포인트를 만들어요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/resume                          # 세션 브라우저
/resume save decision-point      # 현재 시점에 체크포인트 저장
/resume list                     # 체크포인트 목록
/resume resume decision-point    # 특정 체크포인트로 복귀
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/chat</code> 은 <code class="language-plaintext highlighter-rouge">/resume</code> 의 호환 alias 에요. 작업 중간에 “여기서 가지치기 한 번 해볼까” 싶을 때 <code class="language-plaintext highlighter-rouge">save</code> 로 체크포인트 박아두는 흐름이 정말 편합니다.</p>

<h3 id="3-6-settings--세션-설정">3-6. <code class="language-plaintext highlighter-rouge">/settings</code> — 세션 설정</h3>

<p>세션 정책(권한·도구·로깅 등) 을 인터랙티브하게 바꿔요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/settings
</code></pre></div></div>

<h3 id="3-7-bug--이슈-리포트">3-7. <code class="language-plaintext highlighter-rouge">/bug</code> — 이슈 리포트</h3>

<p>문제 만났을 때 그 자리에서 이슈 리포트 양식을 띄워줘요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/bug
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="4-입력창-prefix--">4. 입력창 prefix — <code class="language-plaintext highlighter-rouge">@</code></h2>

<p>Gemini 의 입력 prefix 는 <code class="language-plaintext highlighter-rouge">@</code> 이 가장 두드러져요. 단순 파일 참조가 아니라 <strong>확장 기반 mention</strong> 으로 동작해서, 확장(<code class="language-plaintext highlighter-rouge">@github</code>, <code class="language-plaintext highlighter-rouge">@linear</code> 등) 을 통한 외부 자원 참조가 자연스러워요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@github 내 열린 PR 목록 보여줘
@src/api/handlers.py 이 파일 리팩토링 해줘
</code></pre></div></div>

<blockquote>
  <p>💡 Claude Code 의 <code class="language-plaintext highlighter-rouge">!</code> (쉘) / <code class="language-plaintext highlighter-rouge">#</code> (메모리) 같은 prefix 는 Gemini CLI 공식 레퍼런스에서 명시적으로 잡혀있진 않아요. 같은 결의 기능은 권한 모드(<code class="language-plaintext highlighter-rouge">auto_edit</code>, <code class="language-plaintext highlighter-rouge">yolo</code>) 와 컨텍스트 파일(<code class="language-plaintext highlighter-rouge">GEMINI.md</code>) + <code class="language-plaintext highlighter-rouge">/memory reload</code> 조합으로 풀어요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="5-헤드리스-자동화-예시-gemini-만의-색깔">5. 헤드리스 자동화 예시 (Gemini 만의 색깔)</h2>

<p>Gemini CLI 가 다른 두 CLI 대비 두드러지는 건 <strong>헤드리스 모드</strong> 의 깔끔함이에요. <code class="language-plaintext highlighter-rouge">-p</code> + <code class="language-plaintext highlighter-rouge">--output-format json</code> 조합으로 스크립트·CI 에 끼워넣기 쉬워요.</p>

<h3 id="5-1-json-출력으로-후처리">5-1. JSON 출력으로 후처리</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-p</span> <span class="s2">"이 저장소 라이선스 한 줄 요약"</span> <span class="nt">-o</span> json <span class="o">&gt;</span> out.json
jq <span class="s1">'.text'</span> out.json
</code></pre></div></div>

<h3 id="5-2-스트리밍-결과-받기">5-2. 스트리밍 결과 받기</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-p</span> <span class="s2">"긴 리팩토링 작업 시작"</span> <span class="nt">-o</span> stream-json | <span class="nb">tee </span>progress.jsonl
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">stream-json</code> 으로 받으면 진행 상황을 라인 단위로 받을 수 있어서 CI 로그에 그대로 흘릴 수 있어요.</p>

<h3 id="5-3-안전-모드로-첫-훑기">5-3. 안전 모드로 첫 훑기</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-i</span> <span class="s2">"이 저장소 구조 한 단락으로 요약"</span> <span class="nt">--approval-mode</span> plan
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">plan</code> 모드는 코드를 절대 안 건드리고 계획만 세워줘서, 처음 보는 저장소 훑을 때 부담 없이 시작 가능.</p>

<h3 id="5-4-격리된-컨테이너에서-풀자동">5-4. 격리된 컨테이너에서 풀자동</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">-s</span> <span class="nt">--approval-mode</span> yolo <span class="s2">"이 마이그레이션 끝까지 진행해줘"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-s</code> 로 샌드박스 켜고 <code class="language-plaintext highlighter-rouge">yolo</code> 로 묻지 않는 자동 실행. 호스트는 안전, 작업은 끝까지.</p>

<h3 id="5-5-모노레포-멀티-디렉토리">5-5. 모노레포 멀티 디렉토리</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gemini <span class="nt">--include-directories</span> ../shared,../types <span class="nt">-w</span> mono-experiment
</code></pre></div></div>

<p>인접 디렉토리 노출 + 새 worktree 까지 한 줄로.</p>

<p><br /></p>

<p><br /></p>

<h2 id="6-세-cli-비교-한-줄-정리">6. 세 CLI 비교 한 줄 정리</h2>

<p>시리즈 마지막인 만큼, 세 CLI 의 비슷한 결과 다른 결을 한 표로 정리할게요.</p>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>Claude Code</th>
      <th>Codex</th>
      <th>Gemini</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>비대화 실행</td>
      <td><code class="language-plaintext highlighter-rouge">-p</code></td>
      <td><code class="language-plaintext highlighter-rouge">codex exec</code></td>
      <td><code class="language-plaintext highlighter-rouge">-p</code></td>
    </tr>
    <tr>
      <td>세션 이어가기</td>
      <td><code class="language-plaintext highlighter-rouge">-c</code> / <code class="language-plaintext highlighter-rouge">-r</code></td>
      <td><code class="language-plaintext highlighter-rouge">codex resume --last</code></td>
      <td><code class="language-plaintext highlighter-rouge">-r</code></td>
    </tr>
    <tr>
      <td>권한 모드 플래그</td>
      <td><code class="language-plaintext highlighter-rouge">--permission-mode</code></td>
      <td>(세션 내 <code class="language-plaintext highlighter-rouge">/permissions</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">--approval-mode</code></td>
    </tr>
    <tr>
      <td>자동 편집 모드</td>
      <td><code class="language-plaintext highlighter-rouge">acceptEdits</code></td>
      <td><code class="language-plaintext highlighter-rouge">Auto</code> (기본)</td>
      <td><code class="language-plaintext highlighter-rouge">auto_edit</code></td>
    </tr>
    <tr>
      <td>풀자동 모드</td>
      <td><code class="language-plaintext highlighter-rouge">bypassPermissions</code></td>
      <td><code class="language-plaintext highlighter-rouge">Full Access</code></td>
      <td><code class="language-plaintext highlighter-rouge">yolo</code></td>
    </tr>
    <tr>
      <td>플랜 모드</td>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
      <td>—</td>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
    </tr>
    <tr>
      <td>worktree 격리</td>
      <td><code class="language-plaintext highlighter-rouge">-w</code></td>
      <td>— (대신 <code class="language-plaintext highlighter-rouge">--cd</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">-w</code></td>
    </tr>
    <tr>
      <td>디렉토리 추가</td>
      <td><code class="language-plaintext highlighter-rouge">--add-dir</code></td>
      <td><code class="language-plaintext highlighter-rouge">--add-dir</code></td>
      <td><code class="language-plaintext highlighter-rouge">--include-directories</code></td>
    </tr>
    <tr>
      <td>컨텍스트 파일</td>
      <td><code class="language-plaintext highlighter-rouge">CLAUDE.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">AGENTS.md</code></td>
      <td><code class="language-plaintext highlighter-rouge">GEMINI.md</code></td>
    </tr>
    <tr>
      <td>출력 포맷</td>
      <td><code class="language-plaintext highlighter-rouge">--output-format</code></td>
      <td><code class="language-plaintext highlighter-rouge">--json</code></td>
      <td><code class="language-plaintext highlighter-rouge">--output-format</code></td>
    </tr>
  </tbody>
</table>

<p>큰 그림은 정말 비슷한데, 모드 이름과 컨텍스트 파일명이 각자 다르다는 게 가장 헷갈리는 지점이에요. 셋 다 쓰다 보면 머릿속에 자연스럽게 매핑이 잡혀요.</p>

<p><br /></p>

<p><br /></p>

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

<p>여기까지 Gemini CLI 의 플래그·서브커맨드·슬래시 명령어·prefix 를 훑고, 시리즈 마무리로 세 CLI 의 비교 표까지 같이 정리해봤어요.</p>

<ul>
  <li><strong>자주 쓰는 플래그</strong>: <code class="language-plaintext highlighter-rouge">-m</code>, <code class="language-plaintext highlighter-rouge">-p</code>, <code class="language-plaintext highlighter-rouge">-i</code>, <code class="language-plaintext highlighter-rouge">--approval-mode</code>, <code class="language-plaintext highlighter-rouge">-s</code>, <code class="language-plaintext highlighter-rouge">-w</code>, <code class="language-plaintext highlighter-rouge">-r</code>, <code class="language-plaintext highlighter-rouge">--include-directories</code>, <code class="language-plaintext highlighter-rouge">-o</code></li>
  <li><strong>권한 모드</strong>: <code class="language-plaintext highlighter-rouge">default</code> / <code class="language-plaintext highlighter-rouge">auto_edit</code> / <code class="language-plaintext highlighter-rouge">yolo</code> / <code class="language-plaintext highlighter-rouge">plan</code> — <code class="language-plaintext highlighter-rouge">--approval-mode</code> 로 지정</li>
  <li><strong>서브커맨드</strong>: <code class="language-plaintext highlighter-rouge">gemini mcp</code>, <code class="language-plaintext highlighter-rouge">extensions</code>, <code class="language-plaintext highlighter-rouge">skills</code>, <code class="language-plaintext highlighter-rouge">update</code></li>
  <li><strong>슬래시 명령</strong>: <code class="language-plaintext highlighter-rouge">/help</code>, <code class="language-plaintext highlighter-rouge">/quit</code>, <code class="language-plaintext highlighter-rouge">/resume</code>, <code class="language-plaintext highlighter-rouge">/chat</code>, <code class="language-plaintext highlighter-rouge">/memory reload</code>, <code class="language-plaintext highlighter-rouge">/mcp reload</code>, <code class="language-plaintext highlighter-rouge">/extensions reload</code>, <code class="language-plaintext highlighter-rouge">/skills reload</code>, <code class="language-plaintext highlighter-rouge">/agents reload</code>, <code class="language-plaintext highlighter-rouge">/commands reload</code>, <code class="language-plaintext highlighter-rouge">/settings</code>, <code class="language-plaintext highlighter-rouge">/bug</code></li>
  <li><strong>입력 prefix</strong>: <code class="language-plaintext highlighter-rouge">@</code> (확장 기반 mention)</li>
  <li><strong>시그니처</strong>: <code class="language-plaintext highlighter-rouge">-p</code> + <code class="language-plaintext highlighter-rouge">-o stream-json</code> 조합의 깔끔한 헤드리스 모드</li>
</ul>

<p>셋 다 익혀두면 작업 성격에 맞는 도구를 골라서 띄울 수 있고, 모드/플래그가 헷갈릴 때마다 이 표만 한 번 보면 됩니다.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 시리즈 마무리로 세 CLI 를 <strong>권한 모드·헤드리스·세션 이어가기·컨텍스트 파일·MCP</strong> 축으로 깊게 비교해볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/Codex_CLI_자주_쓰는_명령어_모음/">(2/4) Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</a> ｜ <strong>다음 글 →</strong> <a href="/coding/AI_코딩_CLI_3종_비교/">(4/4) Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="GeminiCLI" /><category term="Google" /><category term="Gemini" /><category term="CLI" /><category term="AI코딩" /><category term="슬래시명령어" /><category term="워크플로우" /><category term="생산성" /><category term="개발도구" /><summary type="html"><![CDATA[Google Gemini CLI 의 자주 쓰는 플래그(--prompt, --approval-mode, --worktree, --resume)와 서브커맨드, 슬래시 명령어를 예시 위주로 정리했어요.]]></summary></entry><entry><title type="html">(2/4) Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</title><link href="https://dorumugs.github.io/coding/Codex_CLI_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C/" rel="alternate" type="text/html" title="(2/4) Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지" /><published>2026-06-01T10:30:00+09:00</published><updated>2026-06-01T10:30:00+09:00</updated><id>https://dorumugs.github.io/coding/Codex_CLI_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C</id><content type="html" xml:base="https://dorumugs.github.io/coding/Codex_CLI_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C/"><![CDATA[<div class="notice--info">
  <p><strong>🛠️ AI 코딩 CLI 명령어 모음 시리즈 (전체 4편)</strong></p>
  <ol>
    <li><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지</a></li>
    <li><strong>Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/Gemini_CLI_자주_쓰는_명령어_모음/">Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</a></li>
    <li><a href="/coding/AI_코딩_CLI_3종_비교/">Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지</a></li>
  </ol>
</div>

<h1 id="summary">Summary</h1>

<p><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">1편</a> 에서 Claude Code 의 CLI 플래그와 슬래시 명령어를 정리했는데, OpenAI 의 <strong>Codex CLI</strong> 도 비슷한 결로 자주 굴리고 있어서 같이 정리해두면 좋을 것 같았어요. Codex 는 Claude Code 와 닮은 점이 많지만 서브커맨드 구조, 권한 모드 이름, 슬래시 명령어 라인업이 미묘하게 달라서 한 번에 비교해두면 헷갈리지 않아요. 이 글에서는 자주 쓰는 서브커맨드, CLI 플래그, 세션 슬래시 명령어, 입력 prefix(<code class="language-plaintext highlighter-rouge">!</code>, <code class="language-plaintext highlighter-rouge">@</code>), 단축키까지 한 번에 훑어볼게요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>Codex CLI 서브커맨드 (<code class="language-plaintext highlighter-rouge">exec</code>, <code class="language-plaintext highlighter-rouge">resume</code>, <code class="language-plaintext highlighter-rouge">cloud</code>, <code class="language-plaintext highlighter-rouge">mcp</code>, <code class="language-plaintext highlighter-rouge">features</code>, <code class="language-plaintext highlighter-rouge">completion</code>)</li>
    <li>자주 쓰는 CLI 플래그 (<code class="language-plaintext highlighter-rouge">--model</code>, <code class="language-plaintext highlighter-rouge">-i</code>, <code class="language-plaintext highlighter-rouge">--cd</code>, <code class="language-plaintext highlighter-rouge">--add-dir</code>, <code class="language-plaintext highlighter-rouge">--profile</code>, <code class="language-plaintext highlighter-rouge">--search</code>, <code class="language-plaintext highlighter-rouge">--json</code>)</li>
    <li>세션 안 슬래시 명령어 (<code class="language-plaintext highlighter-rouge">/model</code>, <code class="language-plaintext highlighter-rouge">/review</code>, <code class="language-plaintext highlighter-rouge">/permissions</code>, <code class="language-plaintext highlighter-rouge">/status</code>, <code class="language-plaintext highlighter-rouge">/clear</code>, <code class="language-plaintext highlighter-rouge">/copy</code>, <code class="language-plaintext highlighter-rouge">/theme</code>)</li>
    <li>입력창 단축 prefix — <code class="language-plaintext highlighter-rouge">!</code> 쉘 실행, <code class="language-plaintext highlighter-rouge">@</code> 파일 참조</li>
    <li>권한 모드(Auto / Read-only / Full Access) 와 자주 쓰는 조합 예시</li>
  </ul>
</blockquote>

<blockquote>
  <p>⚠️ Codex CLI 는 업데이트 주기가 짧아서 옵션 이름·기본값이 자주 바뀌어요. 본문의 옵션이 안 보이면 <code class="language-plaintext highlighter-rouge">codex --help</code> 로 한 번 확인하는 걸 추천드려요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-서브커맨드--codex-뒤에-붙는-모드들">1. 서브커맨드 — <code class="language-plaintext highlighter-rouge">codex</code> 뒤에 붙는 모드들</h2>

<p>Codex 는 같은 바이너리 안에 모드가 여러 개 들어있어요. 가장 손이 자주 가는 것부터 정리할게요.</p>

<h3 id="1-1-codex--인터랙티브-tui">1-1. <code class="language-plaintext highlighter-rouge">codex</code> — 인터랙티브 TUI</h3>

<p>아무것도 안 붙이면 대화형 터미널 UI 가 떠요. 가장 기본 모드.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex
codex <span class="s2">"이 저장소 구조 한 줄로 요약해줘"</span>
</code></pre></div></div>

<p>뒤에 프롬프트를 바로 박으면 그 메시지로 세션이 시작됩니다.</p>

<h3 id="1-2-codex-exec--비대화-실행-파이프-친화">1-2. <code class="language-plaintext highlighter-rouge">codex exec</code> — 비대화 실행 (파이프 친화)</h3>

<p>Claude Code 의 <code class="language-plaintext highlighter-rouge">-p</code> 와 같은 결이에요. 결과만 한 번 뱉고 끝나서 스크립트나 파이프에서 쓰기 좋아요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nb">exec</span> <span class="s2">"CI 실패 원인 찾아서 패치 만들어줘"</span>
codex <span class="nb">exec</span> <span class="nt">--json</span> <span class="s2">"이 디렉토리에서 TODO 갯수만 세줘"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--json</code> 까지 같이 박으면 후처리 파이프라인에 끼워넣기 편합니다.</p>

<h3 id="1-3-codex-resume--이전-세션-이어가기">1-3. <code class="language-plaintext highlighter-rouge">codex resume</code> — 이전 세션 이어가기</h3>

<p>작업 중이던 세션을 다시 열어요. 옵션 세 가지가 있는데 손에 익는 순서대로:</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex resume               <span class="c"># 최근 세션 피커</span>
codex resume <span class="nt">--last</span>        <span class="c"># 가장 최근 세션으로 바로</span>
codex resume &lt;SESSION_ID&gt;  <span class="c"># 특정 세션 지정</span>
</code></pre></div></div>

<p>저는 <code class="language-plaintext highlighter-rouge">--last</code> 가 압도적으로 많아요. 잠깐 터미널 닫고 돌아올 때 한 줄로 복귀 가능.</p>

<h3 id="1-4-codex-cloud--클라우드-작업-관리">1-4. <code class="language-plaintext highlighter-rouge">codex cloud</code> — 클라우드 작업 관리</h3>

<p>로컬이 아니라 OpenAI 가 호스팅하는 환경에서 코덱스를 돌리는 모드. 사양 큰 환경/긴 작업 돌릴 때 유용해요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex cloud <span class="nb">exec</span> <span class="nt">--env</span> ENV_ID <span class="s2">"오픈된 버그 요약"</span>
codex cloud <span class="nb">exec</span> <span class="nt">--env</span> ENV_ID <span class="nt">--attempts</span> 3 <span class="s2">"리팩토링 PR 만들어줘"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--attempts 1~4</code> 로 best-of-N 실행을 요청하면 같은 작업을 여러 번 돌리고 가장 좋은 결과를 골라요.</p>

<h3 id="1-5-codex-mcp--mcp-서버-관리">1-5. <code class="language-plaintext highlighter-rouge">codex mcp</code> — MCP 서버 관리</h3>

<p>MCP(Model Context Protocol) 서버를 코덱스에 붙이고 떼는 명령. Notion, Linear, Gmail 같은 외부 도구를 코덱스가 직접 쓰게 해주는 통로에요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex mcp
</code></pre></div></div>

<h3 id="1-6-codex-features--피처-플래그">1-6. <code class="language-plaintext highlighter-rouge">codex features</code> — 피처 플래그</h3>

<p>베타·실험 기능을 켜고 끄는 토글. 새 기능이 나왔는데 안 보일 때 한 번씩 들여다봅니다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex features list
codex features <span class="nb">enable</span> &lt;feature&gt;
codex features disable &lt;feature&gt;
</code></pre></div></div>

<h3 id="1-7-codex-completion--쉘-자동완성">1-7. <code class="language-plaintext highlighter-rouge">codex completion</code> — 쉘 자동완성</h3>

<p>서브커맨드/플래그 자동완성을 셸에 박아주는 스크립트 생성기.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex completion zsh <span class="o">&gt;</span> ~/.zsh/completions/_codex
codex completion bash <span class="o">&gt;</span> ~/.local/share/bash-completion/completions/codex
</code></pre></div></div>

<p>한 번만 설정해두면 <code class="language-plaintext highlighter-rouge">codex --&lt;TAB&gt;</code> 으로 옵션 다 뜨니까 처음 설치하면 무조건 박아두는 걸 추천드려요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="2-cli-플래그--세션-동작-바꾸기">2. CLI 플래그 — 세션 동작 바꾸기</h2>

<p>서브커맨드와 같이 쓸 수 있는 플래그들이에요.</p>

<h3 id="2-1---model--모델-지정">2-1. <code class="language-plaintext highlighter-rouge">--model</code> — 모델 지정</h3>

<p>세션 단위로 모델을 지정해요. 기본값은 코덱스 추천 모델인데, 특정 작업에서는 갈아끼우는 게 나아요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--model</span> gpt-5.5
codex <span class="nb">exec</span> <span class="nt">--model</span> gpt-5.5 <span class="s2">"단순 포맷팅 작업"</span>
</code></pre></div></div>

<blockquote>
  <p>💡 사용 가능한 모델 alias 는 <code class="language-plaintext highlighter-rouge">codex --help</code> 또는 <code class="language-plaintext highlighter-rouge">~/.codex/config.toml</code> 의 프로필 설정으로 확인할 수 있어요. 버전마다 이름이 바뀌니까 직접 확인하는 게 정확합니다.</p>
</blockquote>

<p>세션 도중에는 <code class="language-plaintext highlighter-rouge">/model</code> 슬래시 명령으로 바꿀 수 있어요.</p>

<h3 id="2-2--i---image--이미지-첨부">2-2. <code class="language-plaintext highlighter-rouge">-i, --image</code> — 이미지 첨부</h3>

<p>스크린샷이나 디자인 시안을 코덱스에게 같이 보여줘요. 에러 화면 디버깅, UI 컴포넌트 만들 때 정말 유용.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">-i</span> screenshot.png <span class="s2">"이 에러 화면 의미하는 게 뭐야?"</span>
codex <span class="nt">-i</span> mockup.png,style.png <span class="s2">"이 디자인대로 React 컴포넌트 만들어줘"</span>
</code></pre></div></div>

<p>쉼표로 여러 이미지 한 번에 첨부 가능해요.</p>

<h3 id="2-3---cd--작업-디렉토리-변경">2-3. <code class="language-plaintext highlighter-rouge">--cd</code> — 작업 디렉토리 변경</h3>

<p><code class="language-plaintext highlighter-rouge">cd</code> 안 치고 코덱스에게 작업 디렉토리만 알려줘요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--cd</span> /path/to/project <span class="s2">"여기서 시작해줘"</span>
</code></pre></div></div>

<h3 id="2-4---add-dir--추가-디렉토리-노출">2-4. <code class="language-plaintext highlighter-rouge">--add-dir</code> — 추가 디렉토리 노출</h3>

<p>현재 디렉토리 외에 다른 폴더도 코덱스가 읽고 쓸 수 있게 해줘요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--add-dir</span> ../backend <span class="nt">--add-dir</span> ../shared
</code></pre></div></div>

<p>모노레포에서 인접 패키지 같이 만질 때 자주 씁니다.</p>

<h3 id="2-5---profile--설정-프로필-적용">2-5. <code class="language-plaintext highlighter-rouge">--profile</code> — 설정 프로필 적용</h3>

<p><code class="language-plaintext highlighter-rouge">$CODEX_HOME/&lt;profile&gt;.config.toml</code> 에 미리 박아둔 설정 묶음을 한 번에 적용해요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--profile</span> work
codex <span class="nt">--profile</span> sandbox-only
</code></pre></div></div>

<p>프로젝트별로 모델·권한·MCP 서버 조합이 달라질 때 프로필을 미리 만들어 두면 띄울 때 한 글자로 끝납니다.</p>

<h3 id="2-6---search--실시간-웹-검색">2-6. <code class="language-plaintext highlighter-rouge">--search</code> — 실시간 웹 검색</h3>

<p>기본은 캐시 결과를 쓰는데, 최신 정보가 필요할 때 이걸 박으면 실시간 검색을 같이 돌려요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--search</span> <span class="s2">"Node.js 24 의 최신 변경사항 정리해줘"</span>
</code></pre></div></div>

<h3 id="2-7---json--json-출력">2-7. <code class="language-plaintext highlighter-rouge">--json</code> — JSON 출력</h3>

<p><code class="language-plaintext highlighter-rouge">exec</code> 와 같이 쓰면 결과를 구조화된 JSON 으로 받을 수 있어요. 후처리 스크립트에서 파싱하기 편함.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nb">exec</span> <span class="nt">--json</span> <span class="s2">"이 PR diff 요약"</span> <span class="o">&gt;</span> review.json
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="3-권한-모드--auto--read-only--full-access">3. 권한 모드 — Auto / Read-only / Full Access</h2>

<p>코덱스의 권한 모델은 세 단계예요. 세션 안에서 <code class="language-plaintext highlighter-rouge">/permissions</code> 로 바꿀 수 있어요.</p>

<table>
  <thead>
    <tr>
      <th>모드</th>
      <th>파일 읽기</th>
      <th>파일 쓰기</th>
      <th>명령 실행</th>
      <th>외부 접근</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Read-only</strong></td>
      <td>✅</td>
      <td>❌</td>
      <td>❌</td>
      <td>❌</td>
    </tr>
    <tr>
      <td><strong>Auto</strong> (기본값)</td>
      <td>✅</td>
      <td>✅ (작업 폴더)</td>
      <td>✅ (작업 폴더)</td>
      <td>물어봄</td>
    </tr>
    <tr>
      <td><strong>Full Access</strong></td>
      <td>✅</td>
      <td>✅ (전 시스템)</td>
      <td>✅ (전 시스템)</td>
      <td>✅ (네트워크 포함)</td>
    </tr>
  </tbody>
</table>

<p>Auto 가 일상 작업에서 가장 균형 좋은 기본값이에요. Read-only 는 처음 보는 저장소 훑어볼 때, Full Access 는 일회용 샌드박스에서만.</p>

<p><br /></p>

<p><br /></p>

<h2 id="4-세션-안-슬래시-명령어">4. 세션 안 슬래시 명령어</h2>

<p>세션에서 <code class="language-plaintext highlighter-rouge">/</code> 로 시작하는 명령어들. <code class="language-plaintext highlighter-rouge">/</code> 만 쳐도 자동완성이 떠요.</p>

<h3 id="4-1-model--모델-바꾸기">4-1. <code class="language-plaintext highlighter-rouge">/model</code> — 모델 바꾸기</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/model gpt-5.5
</code></pre></div></div>

<h3 id="4-2-permissions--권한-모드-전환">4-2. <code class="language-plaintext highlighter-rouge">/permissions</code> — 권한 모드 전환</h3>

<p>위의 Read-only / Auto / Full Access 사이를 세션 도중 바꾸는 명령. 처음 훑을 땐 Read-only 로 두다가, 본격적으로 패치 만들기 시작하면 Auto 로 올리는 흐름.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/permissions
</code></pre></div></div>

<h3 id="4-3-review--코드-리뷰-프리셋">4-3. <code class="language-plaintext highlighter-rouge">/review</code> — 코드 리뷰 프리셋</h3>

<p>베이스 브랜치 대비 diff, 워킹트리 미커밋, 특정 커밋, 커스텀 지시 중에서 골라서 리뷰를 받을 수 있어요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/review
</code></pre></div></div>

<h3 id="4-4-status--세션-상태">4-4. <code class="language-plaintext highlighter-rouge">/status</code> — 세션 상태</h3>

<p>현재 모델, 권한 모드, 작업 디렉토리, MCP 서버 등을 한 페이지로 보여줘요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/status
</code></pre></div></div>

<h3 id="4-5-clear--화면컨텍스트-초기화">4-5. <code class="language-plaintext highlighter-rouge">/clear</code> — 화면+컨텍스트 초기화</h3>

<p>지금까지의 대화를 통째로 비우고 새로 시작.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/clear
</code></pre></div></div>

<h3 id="4-6-copy--마지막-출력-복사">4-6. <code class="language-plaintext highlighter-rouge">/copy</code> — 마지막 출력 복사</h3>

<p>마지막 코덱스 응답을 클립보드로. 셸 명령 결과 그대로 옮길 때 편해요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/copy
</code></pre></div></div>

<h3 id="4-7-theme--tui-테마-변경">4-7. <code class="language-plaintext highlighter-rouge">/theme</code> — TUI 테마 변경</h3>

<p>테마 미리보기 + 저장. 선택값은 <code class="language-plaintext highlighter-rouge">~/.codex/config.toml</code> 에 박힙니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/theme
</code></pre></div></div>

<h3 id="4-8-exit--세션-종료">4-8. <code class="language-plaintext highlighter-rouge">/exit</code> — 세션 종료</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/exit
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="5-입력창-단축-prefix---">5. 입력창 단축 prefix — <code class="language-plaintext highlighter-rouge">!</code>, <code class="language-plaintext highlighter-rouge">@</code></h2>

<p>Claude Code 와 거의 똑같은 결인데 한 가지가 빠져 있어요.</p>

<h3 id="5-1---쉘-명령어-즉시-실행">5-1. <code class="language-plaintext highlighter-rouge">!</code> — 쉘 명령어 즉시 실행</h3>

<p>입력 첫 글자가 <code class="language-plaintext highlighter-rouge">!</code> 면 그 줄을 쉘로 실행하고 결과가 대화에 들어와요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>!ls -la
!git status
</code></pre></div></div>

<h3 id="5-2---파일-퍼지-검색--참조">5-2. <code class="language-plaintext highlighter-rouge">@</code> — 파일 퍼지 검색 / 참조</h3>

<p><code class="language-plaintext highlighter-rouge">@</code> 를 치면 워크스페이스 안에서 파일 자동완성이 떠요. Tab 또는 Enter 로 경로 박힘.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>이 두 파일 비교해줘 @src/handlers.py @src/legacy_handlers.py
</code></pre></div></div>

<blockquote>
  <p>💡 Claude Code 의 <code class="language-plaintext highlighter-rouge">#</code> (메모리 추가) 같은 prefix 는 코덱스에는 없어요. 메모리/장기 컨텍스트는 <code class="language-plaintext highlighter-rouge">AGENTS.md</code> 와 <code class="language-plaintext highlighter-rouge">~/.codex/config.toml</code> 로 관리합니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="6-단축키">6. 단축키</h2>

<table>
  <thead>
    <tr>
      <th>단축키</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Tab</code></td>
      <td>다음 턴에 보낼 텍스트/슬래시/<code class="language-plaintext highlighter-rouge">!</code> 명령을 큐에 쌓기</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + L</code></td>
      <td>화면만 클리어 (대화 컨텍스트는 유지)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + O</code></td>
      <td>가장 최근 출력을 클립보드로 복사</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + R</code></td>
      <td>프롬프트 히스토리 검색</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + G</code></td>
      <td>외부 에디터(<code class="language-plaintext highlighter-rouge">$EDITOR</code> / <code class="language-plaintext highlighter-rouge">$VISUAL</code>)로 긴 프롬프트 편집</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + C</code></td>
      <td>세션 종료</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Esc Esc</code></td>
      <td>이전 사용자 메시지로 돌아가서 편집 (두 번 누르면 더 거슬러 올라감)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">↑ / ↓</code></td>
      <td>입력창에서 이전/다음 초안</td>
    </tr>
  </tbody>
</table>

<p>저는 <code class="language-plaintext highlighter-rouge">Ctrl + G</code> 가 진짜 편해요. 멀티라인 긴 지시 칠 때 <code class="language-plaintext highlighter-rouge">vim</code> 으로 바로 열려서 편집 쾌적합니다 (<code class="language-plaintext highlighter-rouge">$EDITOR</code> 가 잡혀 있어야 동작).</p>

<p><br /></p>

<p><br /></p>

<h2 id="7-실제로-쓰는-조합-예시">7. 실제로 쓰는 조합 예시</h2>

<h3 id="7-1-처음-보는-저장소-훑기-안전-모드">7-1. 처음 보는 저장소 훑기 (안전 모드)</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--cd</span> /path/to/unknown-repo
<span class="c"># 세션 안에서:</span>
/permissions   <span class="c"># Read-only 로 전환</span>
<span class="s2">"이 저장소 구조 한 단락으로 요약해줘"</span>
</code></pre></div></div>

<p>권한을 Read-only 로 내려두면 코덱스가 절대 파일/명령을 건드릴 수 없어서 위험 없이 구조 파악만 가능해요.</p>

<h3 id="7-2-pr-셀프-리뷰">7-2. PR 셀프 리뷰</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex
/review
</code></pre></div></div>

<p>베이스 브랜치 기준 diff 리뷰를 한 번 받고 보내는 흐름. CI 에 넣고 싶으면 <code class="language-plaintext highlighter-rouge">codex exec --json "/review"</code> 식으로도 가능.</p>

<h3 id="7-3-멀티-디렉토리-모노레포-작업">7-3. 멀티 디렉토리 모노레포 작업</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>codex <span class="nt">--cd</span> packages/api <span class="nt">--add-dir</span> ../shared <span class="nt">--add-dir</span> ../types <span class="nt">--profile</span> mono
</code></pre></div></div>

<p>작업 폴더를 <code class="language-plaintext highlighter-rouge">api</code> 로 두고 <code class="language-plaintext highlighter-rouge">shared</code>, <code class="language-plaintext highlighter-rouge">types</code> 까지 읽기·쓰기 허용. <code class="language-plaintext highlighter-rouge">--profile mono</code> 로 모노레포 전용 설정(모델·MCP 등) 한 번에 적용.</p>

<h3 id="7-4-일회용-컨테이너에서-풀자동">7-4. 일회용 컨테이너에서 풀자동</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="nt">-v</span> <span class="nv">$PWD</span>:/work codex-sandbox codex
<span class="c"># 세션 안에서:</span>
/permissions   <span class="c"># Full Access 로 전환</span>
<span class="s2">"이 마이그레이션 끝까지 진행해줘"</span>
</code></pre></div></div>

<p>격리 컨테이너 안에서 권한을 Full Access 로 올리면 묻지 않고 끝까지 자동 진행. 호스트는 안전.</p>

<blockquote>
  <p>🚨 Full Access 는 호스트 파일·네트워크에 자유롭게 접근할 수 있는 상태예요. 한 번의 실수가 크게 번질 수 있어서 일회용 컨테이너/VM/원격 격리 환경 안에서만 쓰는 걸 추천드립니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

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

<p>여기까지 코덱스 CLI 의 서브커맨드·플래그·슬래시 명령어·prefix·단축키를 한 번에 훑었어요. 정리하면 이렇게 기억해두면 좋아요.</p>

<ul>
  <li><strong>서브커맨드</strong>: <code class="language-plaintext highlighter-rouge">codex</code>, <code class="language-plaintext highlighter-rouge">codex exec</code>, <code class="language-plaintext highlighter-rouge">codex resume --last</code>, <code class="language-plaintext highlighter-rouge">codex cloud</code>, <code class="language-plaintext highlighter-rouge">codex mcp</code></li>
  <li><strong>자주 쓰는 플래그</strong>: <code class="language-plaintext highlighter-rouge">--model</code>, <code class="language-plaintext highlighter-rouge">-i</code>, <code class="language-plaintext highlighter-rouge">--cd</code>, <code class="language-plaintext highlighter-rouge">--add-dir</code>, <code class="language-plaintext highlighter-rouge">--profile</code>, <code class="language-plaintext highlighter-rouge">--search</code>, <code class="language-plaintext highlighter-rouge">--json</code></li>
  <li><strong>권한 모드</strong>: Read-only / Auto / Full Access — <code class="language-plaintext highlighter-rouge">/permissions</code> 로 전환</li>
  <li><strong>슬래시 명령</strong>: <code class="language-plaintext highlighter-rouge">/model</code>, <code class="language-plaintext highlighter-rouge">/review</code>, <code class="language-plaintext highlighter-rouge">/status</code>, <code class="language-plaintext highlighter-rouge">/clear</code>, <code class="language-plaintext highlighter-rouge">/copy</code>, <code class="language-plaintext highlighter-rouge">/theme</code>, <code class="language-plaintext highlighter-rouge">/permissions</code>, <code class="language-plaintext highlighter-rouge">/exit</code></li>
  <li><strong>입력 prefix</strong>: <code class="language-plaintext highlighter-rouge">!</code> (쉘), <code class="language-plaintext highlighter-rouge">@</code> (파일 퍼지 검색)</li>
  <li><strong>단축키</strong>: <code class="language-plaintext highlighter-rouge">Tab</code>(큐잉), <code class="language-plaintext highlighter-rouge">Ctrl+G</code>(외부 에디터), <code class="language-plaintext highlighter-rouge">Esc Esc</code>(이전 메시지 편집)</li>
</ul>

<p><a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">Claude Code 자주 쓰는 명령어 모음</a> 글과 같이 두고 비교해보면 두 도구가 얼마나 닮았고 얼마나 다른지 한눈에 들어와요. 큰 그림은 비슷한데 권한 모델 이름(<code class="language-plaintext highlighter-rouge">auto</code> vs <code class="language-plaintext highlighter-rouge">Auto</code>/<code class="language-plaintext highlighter-rouge">acceptEdits</code>), prefix 종류(<code class="language-plaintext highlighter-rouge">#</code> 없음), 워크트리 통합(코덱스는 <code class="language-plaintext highlighter-rouge">-w</code> 없음, 대신 <code class="language-plaintext highlighter-rouge">--cd</code> + 외부 worktree) 정도가 갈리는 포인트.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 시리즈 마지막 편으로 <strong>Google Gemini CLI</strong> 의 플래그·슬래시 명령어를 같은 결로 정리하고, 세 CLI 를 한 표로 비교해볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/Claude_Code_자주_쓰는_명령어_모음/">(1/4) Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지</a> ｜ <strong>다음 글 →</strong> <a href="/coding/Gemini_CLI_자주_쓰는_명령어_모음/">(3/4) Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="Codex" /><category term="OpenAI" /><category term="CLI" /><category term="AI코딩" /><category term="슬래시명령어" /><category term="워크플로우" /><category term="생산성" /><category term="개발도구" /><category term="입문" /><summary type="html"><![CDATA[OpenAI Codex CLI 를 일상적으로 굴리면서 손에 익은 서브커맨드, CLI 플래그, 세션 슬래시 명령어, 입력 prefix 와 단축키를 예시 위주로 정리했어요.]]></summary></entry><entry><title type="html">(1/4) Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지</title><link href="https://dorumugs.github.io/coding/Claude_Code_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C/" rel="alternate" type="text/html" title="(1/4) Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지" /><published>2026-05-31T11:20:00+09:00</published><updated>2026-05-31T11:20:00+09:00</updated><id>https://dorumugs.github.io/coding/Claude_Code_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C</id><content type="html" xml:base="https://dorumugs.github.io/coding/Claude_Code_%EC%9E%90%EC%A3%BC_%EC%93%B0%EB%8A%94_%EB%AA%85%EB%A0%B9%EC%96%B4_%EB%AA%A8%EC%9D%8C/"><![CDATA[<div class="notice--info">
  <p><strong>🛠️ AI 코딩 CLI 명령어 모음 시리즈 (전체 4편)</strong></p>
  <ol>
    <li><strong>Claude Code 자주 쓰는 명령어 모음 — CLI 플래그부터 슬래시 명령어까지</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/Codex_CLI_자주_쓰는_명령어_모음/">Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</a></li>
    <li><a href="/coding/Gemini_CLI_자주_쓰는_명령어_모음/">Gemini CLI 자주 쓰는 명령어 모음 — 헤드리스 모드와 슬래시 명령어까지</a></li>
    <li><a href="/coding/AI_코딩_CLI_3종_비교/">Claude Code · Codex · Gemini CLI 비교 — 권한 모드부터 컨텍스트 파일까지</a></li>
  </ol>
</div>

<h1 id="summary">Summary</h1>

<p>저는 작업 종류별로 <code class="language-plaintext highlighter-rouge">claude --enable-auto-mode --name "infra"</code> 같은 식으로 Claude Code 세션을 따로따로 띄워서 굴려요. 익숙해지면 정말 편해지는데, 처음에는 “이 옵션들이 다 뭐 하는 거지?” 싶을 거예요. 이 글에서는 제가 매일 쓰는 CLI 플래그, 세션 안에서 쓰는 슬래시 명령어, 그리고 입력창의 단축 prefix(<code class="language-plaintext highlighter-rouge">!</code>, <code class="language-plaintext highlighter-rouge">#</code>, <code class="language-plaintext highlighter-rouge">@</code>)까지 한 번에 정리해볼게요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>Claude Code 를 띄울 때 자주 쓰는 CLI 플래그 (<code class="language-plaintext highlighter-rouge">--name</code>, <code class="language-plaintext highlighter-rouge">--permission-mode</code>, <code class="language-plaintext highlighter-rouge">-c</code>, <code class="language-plaintext highlighter-rouge">-r</code>, <code class="language-plaintext highlighter-rouge">-p</code>, <code class="language-plaintext highlighter-rouge">-w</code> 등)</li>
    <li>세션 안에서 자주 두드리는 슬래시 명령어 (<code class="language-plaintext highlighter-rouge">/clear</code>, <code class="language-plaintext highlighter-rouge">/compact</code>, <code class="language-plaintext highlighter-rouge">/model</code>, <code class="language-plaintext highlighter-rouge">/agents</code>, <code class="language-plaintext highlighter-rouge">/mcp</code> 등)</li>
    <li>입력창의 단축 prefix — <code class="language-plaintext highlighter-rouge">!</code> 쉘 실행, <code class="language-plaintext highlighter-rouge">#</code> 메모리 추가, <code class="language-plaintext highlighter-rouge">@</code> 파일 참조</li>
    <li>제가 실제로 쓰는 조합 예시 (일상 모드, 리뷰 모드, 워크트리 격리 등)</li>
  </ul>
</blockquote>

<p>이 글의 기준 버전은 <code class="language-plaintext highlighter-rouge">claude --version</code> 기준 <strong>2.1.158</strong> 이에요. 버전이 올라가면 옵션이 추가/이름 변경될 수 있어서 <code class="language-plaintext highlighter-rouge">claude --help</code> 로 한 번 확인하는 걸 추천드려요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="1-세션-띄우기--cli-플래그">1. 세션 띄우기 — CLI 플래그</h2>

<p>가장 많이 쓰는 옵션부터 정리할게요. 셸에서 <code class="language-plaintext highlighter-rouge">claude</code> 뒤에 붙이면 그 세션 동안만 적용돼요.</p>

<h3 id="1-1---name---n--세션에-이름-붙이기">1-1. <code class="language-plaintext highlighter-rouge">--name</code> / <code class="language-plaintext highlighter-rouge">-n</code> — 세션에 이름 붙이기</h3>

<p>세션을 여러 개 띄울 때 가장 먼저 손이 가는 옵션이에요. 이름을 박아두면 프롬프트 박스, <code class="language-plaintext highlighter-rouge">/resume</code> 피커, 터미널 타이틀에 다 같이 노출돼서 “어떤 작업하던 창이지?” 헷갈리지 않아요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">--name</span> <span class="s2">"infra"</span>
claude <span class="nt">-n</span> <span class="s2">"review"</span>
</code></pre></div></div>

<p>저는 작업 종류별로 동시에 두세 개 띄우는 일이 많아서 거의 항상 박습니다.</p>

<h3 id="1-2---permission-mode--권한-모드-고르기">1-2. <code class="language-plaintext highlighter-rouge">--permission-mode</code> — 권한 모드 고르기</h3>

<p>Claude 가 파일 수정·셸 실행 같은 액션을 할 때 매번 물어볼지, 알아서 진행할지 정하는 옵션이에요. 모드는 다섯 가지가 있어요.</p>

<table>
  <thead>
    <tr>
      <th>모드</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">default</code></td>
      <td>매번 권한 묻기 (기본값)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">acceptEdits</code></td>
      <td>파일 편집은 자동 승인, 그 외는 물어봄</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">auto</code></td>
      <td>자동으로 진행. Claude 가 알아서 판단해서 위험한 액션만 물어봄</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">plan</code></td>
      <td>플랜 모드. 코드를 안 건드리고 계획만 세움</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bypassPermissions</code></td>
      <td>모든 권한 체크 무시 (위험)</td>
    </tr>
  </tbody>
</table>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">--permission-mode</span> auto
claude <span class="nt">--permission-mode</span> plan
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--enable-auto-mode</code> 같은 단축 플래그도 같이 쓸 수 있어요 (제가 글 상단에 적은 예시처럼). 결과적으로는 <code class="language-plaintext highlighter-rouge">--permission-mode auto</code> 와 같은 효과예요.</p>

<blockquote>
  <p>⚠️ <code class="language-plaintext highlighter-rouge">bypassPermissions</code> 와 <code class="language-plaintext highlighter-rouge">--dangerously-skip-permissions</code> 는 진짜 위험할 때 — 격리된 샌드박스/인터넷 차단 환경에서만 권장합니다. 일반 작업에는 쓰지 마세요.</p>
</blockquote>

<h3 id="1-3--c---continue---r---resume--이전-세션-이어가기">1-3. <code class="language-plaintext highlighter-rouge">-c, --continue</code> / <code class="language-plaintext highlighter-rouge">-r, --resume</code> — 이전 세션 이어가기</h3>

<p>작업하다가 터미널을 닫아도 세션은 디스크에 남아 있어요. 다시 켜고 싶을 때 두 가지 옵션이 있어요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-c</span>                       <span class="c"># 현재 디렉토리의 가장 최근 세션 그대로 이어가기</span>
claude <span class="nt">-r</span>                       <span class="c"># 세션 피커 띄워서 고르기</span>
claude <span class="nt">-r</span> <span class="s2">"blog"</span>                <span class="c"># 이름/검색어로 필터링해서 피커 띄우기</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-c</code> 가 정말 자주 손이 가요. 잠깐 다른 터미널로 빠졌다 돌아올 때 한 글자로 복귀 가능.</p>

<h3 id="1-4---model--모델-바꿔서-띄우기">1-4. <code class="language-plaintext highlighter-rouge">--model</code> — 모델 바꿔서 띄우기</h3>

<p>세션 단위로 모델을 지정하는 옵션이에요. alias(<code class="language-plaintext highlighter-rouge">sonnet</code>, <code class="language-plaintext highlighter-rouge">opus</code>, <code class="language-plaintext highlighter-rouge">haiku</code>) 또는 풀네임 둘 다 됩니다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">--model</span> opus
claude <span class="nt">--model</span> sonnet
claude <span class="nt">--model</span> claude-opus-4-7
</code></pre></div></div>

<p>저는 기본은 Opus 로 띄우고, 양이 많은 단순 변환 작업에만 Sonnet 으로 띄워요. 세션 도중에 바꾸고 싶으면 안에서 <code class="language-plaintext highlighter-rouge">/model</code> 슬래시 명령으로도 됩니다.</p>

<h3 id="1-5--p---print--비대화-모드-파이프-친화적">1-5. <code class="language-plaintext highlighter-rouge">-p, --print</code> — 비대화 모드 (파이프 친화적)</h3>

<p>인터랙티브 세션 대신 결과만 한 번 뱉고 끝나는 모드. 셸 파이프나 스크립트에서 쓰기 좋아요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-p</span> <span class="s2">"이 파일에서 TODO 가 몇 개 있나?"</span> &lt; src/main.py
<span class="nb">echo</span> <span class="s2">"log line"</span> | claude <span class="nt">-p</span> <span class="s2">"이 로그가 의미하는 에러는?"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--output-format json</code> 과 같이 쓰면 결과를 JSON 으로 받을 수 있어서 후처리 파이프라인에 끼워넣기도 좋습니다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-p</span> <span class="s2">"한 줄 요약해줘"</span> <span class="nt">--output-format</span> json &lt; README.md
</code></pre></div></div>

<h3 id="1-6--w---worktree--git-worktree-로-격리해서-띄우기">1-6. <code class="language-plaintext highlighter-rouge">-w, --worktree</code> — git worktree 로 격리해서 띄우기</h3>

<p>작업 중인 브랜치를 건드리지 않고 별도 worktree 에서 실험할 때 한 줄로 만들어 줘요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-w</span> feature-x
claude <span class="nt">-w</span> experiment-mig <span class="nt">--tmux</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--tmux</code> 까지 같이 박으면 tmux 세션을 자동으로 만들어 줘서 (iTerm2 면 네이티브 페인) 분리 작업이 정말 편해져요.</p>

<h3 id="1-7---add-dir--작업-디렉토리-추가하기">1-7. <code class="language-plaintext highlighter-rouge">--add-dir</code> — 작업 디렉토리 추가하기</h3>

<p>기본 동작은 현재 디렉토리만 도구로 접근 가능한데, 인접한 디렉토리도 같이 열어두고 싶을 때 씁니다.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">--add-dir</span> ../shared-lib ../other-repo
</code></pre></div></div>

<p>저는 모노레포 한쪽에서 작업하면서 옆 폴더의 노트를 같이 참조해야 할 때 자주 씁니다.</p>

<h3 id="1-8---allowedtools----disallowedtools--도구-화이트블랙리스트">1-8. <code class="language-plaintext highlighter-rouge">--allowedTools</code> / <code class="language-plaintext highlighter-rouge">--disallowedTools</code> — 도구 화이트/블랙리스트</h3>

<p>세션 단위로 어떤 도구를 쓸지/막을지 명시적으로 지정해요. CI 나 스크립트에서 안전하게 돌릴 때 좋아요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 읽기만 허용</span>
claude <span class="nt">-p</span> <span class="s2">"이 코드 베이스 구조 한 줄 요약"</span> <span class="se">\</span>
  <span class="nt">--allowedTools</span> <span class="s2">"Read,Grep,Glob"</span>

<span class="c"># Bash 는 git 명령만 허용</span>
claude <span class="nt">--allowedTools</span> <span class="s2">"Bash(git *) Edit Read"</span>

<span class="c"># 절대 쉘은 못 쓰게</span>
claude <span class="nt">--disallowedTools</span> <span class="s2">"Bash"</span>
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="2-세션-안에서--슬래시-명령어">2. 세션 안에서 — 슬래시 명령어</h2>

<p>세션 안에서 <code class="language-plaintext highlighter-rouge">/</code> 로 시작하는 명령어들이에요. 입력창에서 <code class="language-plaintext highlighter-rouge">/</code> 만 쳐도 자동완성 목록이 떠요. 손에 익는 순서대로 정리해볼게요.</p>

<h3 id="2-1-clear--컨텍스트-비우기">2-1. <code class="language-plaintext highlighter-rouge">/clear</code> — 컨텍스트 비우기</h3>

<p>지금까지의 대화를 통째로 비우고 새로 시작해요. 작업이 바뀌었을 때 컨텍스트 오염 막으려고 자주 누릅니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/clear
</code></pre></div></div>

<p>저는 한 세션 안에서 “이제 다른 글 쓸게” 라고 주제가 바뀌면 무조건 한 번 비워요. 안 그러면 직전 글의 톤/소재가 새 글에 묻어 나와요.</p>

<h3 id="2-2-compact--컨텍스트-압축">2-2. <code class="language-plaintext highlighter-rouge">/compact</code> — 컨텍스트 압축</h3>

<p><code class="language-plaintext highlighter-rouge">/clear</code> 처럼 완전히 비우지는 않고, 지금까지의 대화를 요약본으로 압축해서 계속 이어가요. “조금 길었지만 여기까지의 흐름은 살리고 싶을 때” 쓰는 절충안.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/compact
/compact 핵심 결정사항만 남기고 나머지는 정리해줘
</code></pre></div></div>

<p>뒤에 지시를 붙이면 어떤 관점으로 압축할지도 가이드 할 수 있어요.</p>

<h3 id="2-3-model--모델-바꾸기">2-3. <code class="language-plaintext highlighter-rouge">/model</code> — 모델 바꾸기</h3>

<p>세션 도중에 모델 갈아끼울 때. 토큰 많이 쓰는 작업이면 Opus, 단순 반복이면 Sonnet 으로 자주 옮겨 다닙니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/model sonnet
/model opus
</code></pre></div></div>

<h3 id="2-4-init--프로젝트에-claudemd-만들기">2-4. <code class="language-plaintext highlighter-rouge">/init</code> — 프로젝트에 CLAUDE.md 만들기</h3>

<p>새 저장소에서 처음 Claude Code 돌릴 때 한 번 치면, 코드베이스를 훑어보고 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> 초안을 만들어줘요. 이 파일은 이후 모든 세션의 컨텍스트로 자동 주입돼서 정말 강력해요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/init
</code></pre></div></div>

<p>저는 새 저장소 받을 때 거의 반사적으로 한 번 돌리고, 만들어진 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> 를 손으로 다듬어서 쓰는 패턴이에요.</p>

<h3 id="2-5-cost--토큰달러-비용-보기">2-5. <code class="language-plaintext highlighter-rouge">/cost</code> — 토큰/달러 비용 보기</h3>

<p>이번 세션에서 토큰 얼마 썼고 달러로 얼마나 됐는지 보여줘요. Opus 로 길게 작업할 때 한 번씩 확인합니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/cost
</code></pre></div></div>

<h3 id="2-6-status--세션-상태-한눈에">2-6. <code class="language-plaintext highlighter-rouge">/status</code> — 세션 상태 한눈에</h3>

<p>지금 어떤 모델, 어떤 권한 모드, 어떤 디렉토리, 어떤 MCP 서버 붙어있는지 한 페이지로 보여줘요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/status
</code></pre></div></div>

<h3 id="2-7-agents--커스텀-서브에이전트-관리">2-7. <code class="language-plaintext highlighter-rouge">/agents</code> — 커스텀 서브에이전트 관리</h3>

<p>특정 작업 전용 서브에이전트를 만들거나 관리하는 명령. “코드 리뷰 전용”, “테스트 작성 전용” 같은 식으로 페르소나를 따로 등록해두면 메인 컨텍스트 안 더럽히면서 위임이 가능해요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/agents
</code></pre></div></div>

<h3 id="2-8-mcp--mcp-서버-관리">2-8. <code class="language-plaintext highlighter-rouge">/mcp</code> — MCP 서버 관리</h3>

<p>MCP(Model Context Protocol) 서버를 붙이고 떼는 명령. Gmail, Google Drive, Notion, Linear 같은 외부 도구를 Claude 가 직접 쓰게 해주는 통로에요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/mcp
</code></pre></div></div>

<h3 id="2-9-review--현재-diff-코드-리뷰">2-9. <code class="language-plaintext highlighter-rouge">/review</code> — 현재 diff 코드 리뷰</h3>

<p>브랜치에 쌓인 변경분에 대해 한 번 훑어주는 리뷰 명령. PR 올리기 직전에 셀프 리뷰용으로 자주 씁니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/review
</code></pre></div></div>

<h3 id="2-10-help--단축키명령어-도움말">2-10. <code class="language-plaintext highlighter-rouge">/help</code> — 단축키/명령어 도움말</h3>

<p>까먹은 명령어/단축키를 빠르게 확인. <code class="language-plaintext highlighter-rouge">/</code> 자동완성도 좋지만 이쪽이 더 친절해요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/help
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="3-입력창-단축-prefix----">3. 입력창 단축 prefix — <code class="language-plaintext highlighter-rouge">!</code>, <code class="language-plaintext highlighter-rouge">#</code>, <code class="language-plaintext highlighter-rouge">@</code></h2>

<p>슬래시 명령어보다 더 가볍게 한 글자로 동작이 바뀌는 prefix 들이 있어요. 이게 진짜 손에 익으면 생산성이 다릅니다.</p>

<h3 id="3-1---쉘-명령어-즉시-실행">3-1. <code class="language-plaintext highlighter-rouge">!</code> — 쉘 명령어 즉시 실행</h3>

<p>입력 첫 글자가 <code class="language-plaintext highlighter-rouge">!</code> 면 그 줄을 쉘로 실행해요. 결과가 대화 안에 그대로 들어와서 Claude 가 바로 다음 턴부터 활용할 수 있어요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>! ls -la src/ | tail -10
! git status
! gcloud auth login
</code></pre></div></div>

<p>특히 인터랙티브 로그인(<code class="language-plaintext highlighter-rouge">gcloud auth login</code>, <code class="language-plaintext highlighter-rouge">aws sso login</code>)처럼 Claude 가 직접 못 돌리는 명령을 사용자가 직접 쳐야 할 때 이 prefix 가 답이에요.</p>

<h3 id="3-2---메모리에-추가">3-2. <code class="language-plaintext highlighter-rouge">#</code> — 메모리에 추가</h3>

<p>입력 첫 글자가 <code class="language-plaintext highlighter-rouge">#</code> 이면 그 내용이 메모리(또는 <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>)에 저장돼요. 다음 세션부터도 계속 기억해주길 바라는 사실/규칙을 한 줄로 던질 수 있어요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># 이 프로젝트에서 PR 만들 때는 영어 제목 + 한국어 본문으로 작성
# 빌드는 ./run.sh 로만 띄울 것 (bundle exec jekyll serve 직접 호출 X)
</code></pre></div></div>

<p>저는 같은 안내를 두 번 반복하게 됐다 싶으면 그 순간 <code class="language-plaintext highlighter-rouge">#</code> 으로 박아넣어요.</p>

<h3 id="3-3---파일디렉토리-참조">3-3. <code class="language-plaintext highlighter-rouge">@</code> — 파일/디렉토리 참조</h3>

<p>입력 도중에 <code class="language-plaintext highlighter-rouge">@</code> 를 치면 파일 자동완성이 떠요. 선택하면 그 파일이 컨텍스트로 첨부돼요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>이 두 파일 비교해줘 @src/api/handlers.py @src/api/legacy_handlers.py
</code></pre></div></div>

<p>긴 경로를 손으로 안 쳐도 돼서 진짜 편해요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="4-단축키-몇-가지">4. 단축키 몇 가지</h2>

<p>키보드만으로 자주 쓰는 단축키들도 같이 정리할게요.</p>

<table>
  <thead>
    <tr>
      <th>단축키</th>
      <th>동작</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Shift + Tab</code></td>
      <td>권한 모드 순환 (default → acceptEdits → plan → …)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Esc</code> (한 번)</td>
      <td>현재 응답 중단</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Esc Esc</code> (두 번)</td>
      <td>이전 사용자 메시지로 되돌아가서 다시 입력</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + R</code></td>
      <td>verbose 모드 토글 (도구 호출 인자 전체 보기)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Ctrl + L</code></td>
      <td>화면 클리어 (대화 컨텍스트는 유지)</td>
    </tr>
  </tbody>
</table>

<p>특히 <code class="language-plaintext highlighter-rouge">Esc Esc</code> 는 응답이 마음에 안 들 때 메시지 자체를 고쳐서 다시 보낼 수 있어서 정말 자주 씁니다.</p>

<p><br /></p>

<p><br /></p>

<h2 id="5-실제로-쓰는-조합-예시">5. 실제로 쓰는 조합 예시</h2>

<p>플래그들을 어떻게 조합해서 쓰는지 제 워크플로우 몇 개를 옮겨볼게요.</p>

<h3 id="5-1-일상-작업-모드">5-1. 일상 작업 모드</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">--enable-auto-mode</span> <span class="nt">--name</span> <span class="s2">"daily"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--name</code> 으로 어떤 창인지 명확히 하고, auto 모드로 띄워서 자잘한 파일 편집은 자동 진행. 매일 켜는 기본 조합이에요.</p>

<h3 id="5-2-pr-올리기-직전-셀프-리뷰">5-2. PR 올리기 직전 셀프 리뷰</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">--name</span> <span class="s2">"review"</span> <span class="nt">-p</span> <span class="s2">"/review"</span>
</code></pre></div></div>

<p>대화형으로 안 가도 되니까 <code class="language-plaintext highlighter-rouge">-p</code> 로 한 방에 리뷰만 받고 끝. CI 에 넣으면 자동 PR 리뷰 봇처럼도 쓸 수 있어요.</p>

<h3 id="5-3-위험한-실험은-worktree-에-격리">5-3. 위험한 실험은 worktree 에 격리</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-w</span> mig-experiment <span class="nt">--tmux</span> <span class="nt">--name</span> <span class="s2">"mig"</span>
</code></pre></div></div>

<p>브랜치 일은 별도 worktree 로 빼고, tmux 페인까지 자동으로 분리. 메인 작업 환경 안 망가지면서 실험 가능.</p>

<h3 id="5-4-읽기만-시키는-안전-모드">5-4. 읽기만 시키는 안전 모드</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>claude <span class="nt">-p</span> <span class="s2">"이 저장소 구조 한 단락으로 요약해줘"</span> <span class="se">\</span>
  <span class="nt">--allowedTools</span> <span class="s2">"Read,Grep,Glob"</span> <span class="se">\</span>
  <span class="nt">--model</span> sonnet
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Bash</code> / <code class="language-plaintext highlighter-rouge">Edit</code> 를 빼고 읽기 도구만 허용. 모델도 Sonnet 으로 내려서 비용도 절약. 처음 보는 저장소 훑을 때 이 조합을 자주 씁니다.</p>

<p><br /></p>

<p><br /></p>

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

<p>여기까지 자주 쓰는 CLI 플래그, 슬래시 명령어, 입력 prefix, 단축키를 한 번에 훑었어요. 정리하면 이런 흐름이에요.</p>

<ul>
  <li><strong>세션을 띄울 때</strong>: <code class="language-plaintext highlighter-rouge">--name</code>, <code class="language-plaintext highlighter-rouge">--permission-mode</code>, <code class="language-plaintext highlighter-rouge">-c</code>, <code class="language-plaintext highlighter-rouge">-r</code>, <code class="language-plaintext highlighter-rouge">--model</code>, <code class="language-plaintext highlighter-rouge">-w</code></li>
  <li><strong>세션 안에서</strong>: <code class="language-plaintext highlighter-rouge">/clear</code>, <code class="language-plaintext highlighter-rouge">/compact</code>, <code class="language-plaintext highlighter-rouge">/model</code>, <code class="language-plaintext highlighter-rouge">/status</code>, <code class="language-plaintext highlighter-rouge">/cost</code>, <code class="language-plaintext highlighter-rouge">/agents</code>, <code class="language-plaintext highlighter-rouge">/mcp</code>, <code class="language-plaintext highlighter-rouge">/review</code></li>
  <li><strong>입력창 prefix</strong>: <code class="language-plaintext highlighter-rouge">!</code> (쉘), <code class="language-plaintext highlighter-rouge">#</code> (메모리), <code class="language-plaintext highlighter-rouge">@</code> (파일)</li>
  <li><strong>단축키</strong>: <code class="language-plaintext highlighter-rouge">Shift+Tab</code>, <code class="language-plaintext highlighter-rouge">Esc Esc</code>, <code class="language-plaintext highlighter-rouge">Ctrl+R</code></li>
</ul>

<p>전부 외울 필요는 없고, <strong>본인 워크플로우에 자주 등장하는 두세 개부터 손에 익히는 걸 추천드려요.</strong> 저는 처음에 <code class="language-plaintext highlighter-rouge">--name</code> 하나만 박아 쓰다가, <code class="language-plaintext highlighter-rouge">/clear</code> 와 <code class="language-plaintext highlighter-rouge">#</code> 두 개를 추가로 익히면서 작업 속도가 눈에 띄게 빨라졌어요.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 같은 결로 <strong>OpenAI Codex CLI</strong> 의 서브커맨드와 슬래시 명령어를 정리해볼게요.</p>

<hr />

<p><strong>다음 글 →</strong> <a href="/coding/Codex_CLI_자주_쓰는_명령어_모음/">(2/4) Codex CLI 자주 쓰는 명령어 모음 — 서브커맨드부터 슬래시 명령어까지</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="ClaudeCode" /><category term="CLI" /><category term="AI코딩" /><category term="슬래시명령어" /><category term="워크플로우" /><category term="생산성" /><category term="개발도구" /><category term="입문" /><summary type="html"><![CDATA[매일 Claude Code 를 쓰면서 손에 익은 CLI 플래그와 세션 슬래시 명령어, 그리고 입력 prefix 단축을 예시 위주로 정리했어요.]]></summary></entry><entry><title type="html">(5/5) MSSQL 마이그레이션 정합성 트러블슈팅 — 케이스별로 풀어보는 8가지 함정</title><link href="https://dorumugs.github.io/coding/MSSQL_%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98_%EC%A0%95%ED%95%A9%EC%84%B1_%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85/" rel="alternate" type="text/html" title="(5/5) MSSQL 마이그레이션 정합성 트러블슈팅 — 케이스별로 풀어보는 8가지 함정" /><published>2026-05-30T10:43:00+09:00</published><updated>2026-05-30T10:43:00+09:00</updated><id>https://dorumugs.github.io/coding/MSSQL_%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98_%EC%A0%95%ED%95%A9%EC%84%B1_%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85</id><content type="html" xml:base="https://dorumugs.github.io/coding/MSSQL_%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98_%EC%A0%95%ED%95%A9%EC%84%B1_%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85/"><![CDATA[<div class="notice--info">
  <p><strong>📚 MSSQL → AWS RDS 마이그레이션 시리즈 (전체 5편)</strong></p>
  <ol>
    <li><a href="/coding/내부망_MSSQL_AWS_RDS_마이그레이션/">방법 비교 — 6가지 중에서 고르기</a></li>
    <li><a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</a></li>
    <li><a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</a></li>
    <li><a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</a></li>
    <li><strong>정합성 트러블슈팅 — 케이스별 8가지 함정</strong> ← <em>지금 글</em></li>
  </ol>
</div>

<p><a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">지난 글</a>에서 풀백업/복원 + DMS CDC-only 결합 패턴을 정리했어요. 흐름 자체는 깔끔한데, 실제로 운영해보면 데이터가 100% 1:1 로 떨어지지 않는 케이스를 자주 만나요. 이번 글은 그 결합 패턴(또는 DMS 단독)에서 <strong>실제로 자주 만나는 정합성 문제들</strong> 을 케이스별로 정리한 글이에요. 원인 → 증상 → 대응 순서로 풀게요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>중복 키 (Duplicate PK) — 풀로드와 CDC 시작 시점 겹침</li>
    <li>NOT NULL / DEFAULT 컬럼 — 풀로드는 OK, CDC 에서 깨지는 케이스</li>
    <li>IDENTITY 시드 어긋남 — 컷오버 후 첫 INSERT 가 PK 충돌</li>
    <li>외래키 적용 순서 — 풀로드 중/후의 cascade 문제</li>
    <li>Computed column / rowversion — 옮기지 않아야 하는 컬럼</li>
    <li>Collation / 데이터 타입 미스매치 — 비교 결과는 같아 보이는데 정합성 검증에서 다름</li>
    <li>트리거 fire-on-replication — 적용 측에서 데이터가 두 번 변형</li>
    <li>검증 체크리스트</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-큰-그림--정합성-문제가-어디서-생기나">1. 큰 그림 — 정합성 문제가 어디서 생기나</h2>

<p>DMS 든 백업/복원이든, <strong>데이터가 한 시점에 딱 고정되지 않는 한</strong> 정합성 문제는 어디서든 끼어들 수 있어요. 결합 패턴에서 정합성 문제가 잘 끼는 지점은 세 군데.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[원본]                          [타깃]
   │
   ├── A. 풀백업 시작 시점 (T1)
   │       풀백업이 도는 동안의 변경분이 CDC 와 겹침
   │
   ├── B. CDC 시작 시점 (T5)
   │       cdc-start-time 의 미세한 오차로 일부 변경 중복/누락
   │
   └── C. 컷오버 시점
           트리거/제약/IDENTITY 가 양쪽에서 동시에 발동
</code></pre></div></div>

<p>대부분의 함정은 이 세 지점에서 발생해요. 케이스별로 풀게요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="2-케이스-1--중복-키-duplicate-pk">2. 케이스 1 — 중복 키 (Duplicate PK)</h2>

<h3 id="증상">증상</h3>

<p>DMS Task 의 Table statistics 에 <code class="language-plaintext highlighter-rouge">Apply errors</code> 가 쌓이고, CloudWatch Logs 에 이런 메시지가 떨어져요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Violation of PRIMARY KEY constraint 'PK_Orders'. 
Cannot insert duplicate key in object 'dbo.Orders'. 
The duplicate key value is (12345).
</code></pre></div></div>

<h3 id="원인">원인</h3>

<p>풀백업 시점 ~ CDC 시작 시점 사이에 들어온 row 가 <strong>풀로드로 한 번, CDC INSERT 로 한 번</strong> 적용되면서 충돌해요. CDC start time 을 풀백업 <em>시작</em> 시각으로 잡으면 이게 흔히 일어나요(의도된 안전마진).</p>

<h3 id="대응">대응</h3>

<p>DMS Task settings 에서 INSERT 충돌을 흡수하도록 설정.</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">"ErrorBehavior"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ApplyErrorInsertPolicy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"INSERT_OR_UPDATE"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApplyErrorUpdatePolicy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOG_ERROR"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApplyErrorDeletePolicy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"IGNORE_RECORD"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ApplyErrorEscalationPolicy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOG_ERROR"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"FullLoadIgnoreConflicts"</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>

<table>
  <thead>
    <tr>
      <th>옵션</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">INSERT_OR_UPDATE</code></td>
      <td>PK 충돌 시 INSERT 대신 UPDATE 로 적용 (UPSERT)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">FullLoadIgnoreConflicts</code></td>
      <td>풀로드 중 충돌은 무시</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>⚠️ <code class="language-plaintext highlighter-rouge">INSERT_OR_UPDATE</code> 는 PK 가 있어야 동작해요. PK 없는 테이블엔 적용 안 됨. PK 없는 테이블은 별도 처리가 필요.</p>
</blockquote>

<h3 id="검증">검증</h3>

<p>컷오버 후 양쪽 row count + <code class="language-plaintext highlighter-rouge">CHECKSUM_AGG()</code> 비교.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">AS</span> <span class="n">cnt</span><span class="p">,</span> <span class="n">CHECKSUM_AGG</span><span class="p">(</span><span class="n">BINARY_CHECKSUM</span><span class="p">(</span><span class="o">*</span><span class="p">))</span> <span class="k">AS</span> <span class="n">chk</span>
<span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>
</code></pre></div></div>

<p>같은 값이 나와야 정합 OK.</p>

<p><br /></p>

<p><br /></p>

<h2 id="3-케이스-2--not-null--default-컬럼">3. 케이스 2 — NOT NULL / DEFAULT 컬럼</h2>

<h3 id="증상-1">증상</h3>

<p>풀로드는 정상인데, CDC 에서 특정 컬럼만 NULL 로 들어오면서 NOT NULL 제약 위반.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cannot insert the value NULL into column 'created_at', table 'dbo.Orders'. 
Column does not allow nulls.
</code></pre></div></div>

<h3 id="원인-1">원인</h3>

<p>두 가지가 흔해요.</p>

<ol>
  <li><strong>타깃에 DEFAULT 가 빠짐</strong> — 원본에는 <code class="language-plaintext highlighter-rouge">DEFAULT GETDATE()</code> 가 걸려있어서 NULL 이 안 들어오는데, 타깃 스키마를 새로 만들면서 DEFAULT 가 누락됨. CDC 가 <code class="language-plaintext highlighter-rouge">NULL</code> 을 그대로 INSERT 함.</li>
  <li><strong>컬럼 추가/변경이 원본에만 적용됨</strong> — 마이그레이션 도중 원본 스키마가 바뀜. DMS 는 기본적으로 DDL 을 안 따라가요.</li>
</ol>

<h3 id="대응-1">대응</h3>

<ul>
  <li><strong>DEFAULT 누락 점검</strong> — 풀로드 끝난 직후 스키마 비교 한 사이클 돌리기.</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 양쪽에서 동일 쿼리로 DEFAULT 비교</span>
<span class="k">SELECT</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">table_name</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">column_name</span><span class="p">,</span> <span class="n">dc</span><span class="p">.</span><span class="n">definition</span> <span class="k">AS</span> <span class="n">default_value</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">columns</span> <span class="k">c</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">default_constraints</span> <span class="n">dc</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">default_object_id</span> <span class="o">=</span> <span class="n">dc</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">WHERE</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s1">'Orders'</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="k">c</span><span class="p">.</span><span class="n">column_id</span><span class="p">;</span>
</code></pre></div></div>

<ul>
  <li><strong>마이그레이션 기간 동안 원본 스키마 freeze</strong> 가 가장 안전. 강제 못 하면 DMS Task settings 의 <code class="language-plaintext highlighter-rouge">HandleSourceTableAltered</code>, <code class="language-plaintext highlighter-rouge">HandleSourceTableDropped</code> 옵션으로 동작 명시.</li>
</ul>

<p><br /></p>

<p><br /></p>

<h2 id="4-케이스-3--identity-시드-어긋남">4. 케이스 3 — IDENTITY 시드 어긋남</h2>

<h3 id="증상-2">증상</h3>

<p>컷오버 후 앱이 첫 INSERT 를 치자마자 PK 충돌.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Violation of PRIMARY KEY constraint 'PK_Orders'. 
Cannot insert duplicate key in object 'dbo.Orders'. 
The duplicate key value is (10001).
</code></pre></div></div>

<h3 id="원인-2">원인</h3>

<p>복원/CDC 는 row 의 PK 값을 그대로 옮기지만, <strong>IDENTITY 의 “다음에 발급할 시드”</strong> 는 같이 안 따라와요. 타깃의 IDENTITY 다음 값이 1 부터 시작하는 채로 남아있다가, 앱이 INSERT 하면서 이미 존재하는 키를 다시 만들어요.</p>

<h3 id="대응-2">대응</h3>

<p>컷오버 직후 모든 IDENTITY 테이블에 대해 <code class="language-plaintext highlighter-rouge">DBCC CHECKIDENT(..., RESEED, ...)</code> 로 시드 보정.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 1. 원본의 현재 MAX 값 확인</span>
<span class="k">SELECT</span> <span class="k">MAX</span><span class="p">(</span><span class="n">id</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>
<span class="c1">-- =&gt; 10000</span>

<span class="c1">-- 2. 타깃에서 RESEED</span>
<span class="n">DBCC</span> <span class="n">CHECKIDENT</span><span class="p">(</span><span class="s1">'dbo.Orders'</span><span class="p">,</span> <span class="n">RESEED</span><span class="p">,</span> <span class="mi">10000</span><span class="p">);</span>

<span class="c1">-- 3. 검증</span>
<span class="n">DBCC</span> <span class="n">CHECKIDENT</span><span class="p">(</span><span class="s1">'dbo.Orders'</span><span class="p">,</span> <span class="n">NORESEED</span><span class="p">);</span>
<span class="c1">-- 'Checking identity information: current identity value '10000'.'</span>
</code></pre></div></div>

<h3 id="자동화-팁">자동화 팁</h3>

<p>IDENTITY 컬럼이 많으면 동적 SQL 로 한 번에.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">DECLARE</span> <span class="o">@</span><span class="k">sql</span> <span class="n">NVARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">)</span> <span class="o">=</span> <span class="n">N</span><span class="s1">''</span><span class="p">;</span>

<span class="k">SELECT</span> <span class="o">@</span><span class="k">sql</span> <span class="o">=</span> <span class="o">@</span><span class="k">sql</span> <span class="o">+</span> 
  <span class="n">N</span><span class="s1">'DECLARE @m BIGINT; SELECT @m = ISNULL(MAX('</span> <span class="o">+</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'),0) FROM '</span> 
  <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'.'</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'; '</span>
  <span class="o">+</span> <span class="n">N</span><span class="s1">'DBCC CHECKIDENT(</span><span class="se">''</span><span class="s1">'</span> <span class="o">+</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'.'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'</span><span class="se">''</span><span class="s1">, RESEED, @m);'</span> <span class="o">+</span> <span class="nb">CHAR</span><span class="p">(</span><span class="mi">13</span><span class="p">)</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">columns</span> <span class="k">c</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">WHERE</span> <span class="k">c</span><span class="p">.</span><span class="n">is_identity</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="k">EXEC</span> <span class="n">sp_executesql</span> <span class="o">@</span><span class="k">sql</span><span class="p">;</span>
</code></pre></div></div>

<blockquote>
  <p>💡 IDENTITY 시드 보정은 <strong>컷오버 절차의 필수 단계</strong> 예요. 이전 글 컷오버 체크리스트에도 들어있어요. 한 번이라도 빼먹으면 운영 첫 분에 사고.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="5-케이스-4--외래키-적용-순서">5. 케이스 4 — 외래키 적용 순서</h2>

<h3 id="증상-3">증상</h3>

<p>풀로드 중에 자식 테이블이 부모 테이블보다 먼저 들어가면서 외래키 위반.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The INSERT statement conflicted with the FOREIGN KEY constraint 'FK_OrderItems_Orders'. 
The conflict occurred in database 'MyDB', table 'dbo.Orders', column 'id'.
</code></pre></div></div>

<p>CDC 단계에서도 트랜잭션 경계가 어긋나면 비슷한 에러가 떨어져요.</p>

<h3 id="원인-3">원인</h3>

<p>DMS 의 풀로드는 테이블을 <strong>병렬로</strong> 로드해요. 부모/자식 의존성을 모르고 동시에 진행하니까, 자식이 먼저 끝나는 시점이 생겨요.</p>

<p>CDC 도 한 트랜잭션 안에 있던 변경을 항상 같은 트랜잭션 단위로 보장하진 않아요(<code class="language-plaintext highlighter-rouge">BatchApplyPreserveTransaction</code> 옵션이 켜져야).</p>

<h3 id="대응-3">대응</h3>

<p><strong>풀로드 전에 타깃의 외래키를 비활성화</strong>, 풀로드/CDC 끝나면 다시 활성화 + 검증.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 1. 타깃의 모든 FK 비활성화</span>
<span class="k">DECLARE</span> <span class="o">@</span><span class="k">sql</span> <span class="n">NVARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">)</span> <span class="o">=</span> <span class="n">N</span><span class="s1">''</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">@</span><span class="k">sql</span> <span class="o">=</span> <span class="o">@</span><span class="k">sql</span> <span class="o">+</span> 
  <span class="n">N</span><span class="s1">'ALTER TABLE '</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'.'</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
  <span class="o">+</span> <span class="n">N</span><span class="s1">' NOCHECK CONSTRAINT '</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">fk</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="o">+</span> <span class="n">N</span><span class="s1">';'</span> <span class="o">+</span> <span class="nb">CHAR</span><span class="p">(</span><span class="mi">13</span><span class="p">)</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">foreign_keys</span> <span class="n">fk</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span> <span class="k">ON</span> <span class="n">fk</span><span class="p">.</span><span class="n">parent_object_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span><span class="p">;</span>

<span class="k">EXEC</span> <span class="n">sp_executesql</span> <span class="o">@</span><span class="k">sql</span><span class="p">;</span>

<span class="c1">-- 2. 풀로드/CDC 진행</span>

<span class="c1">-- 3. 컷오버 후 FK 재활성화 + 검증 (WITH CHECK 가 핵심)</span>
<span class="k">SELECT</span> <span class="o">@</span><span class="k">sql</span> <span class="o">=</span> <span class="n">N</span><span class="s1">''</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">@</span><span class="k">sql</span> <span class="o">=</span> <span class="o">@</span><span class="k">sql</span> <span class="o">+</span> 
  <span class="n">N</span><span class="s1">'ALTER TABLE '</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">s</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="o">+</span> <span class="n">N</span><span class="s1">'.'</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">t</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
  <span class="o">+</span> <span class="n">N</span><span class="s1">' WITH CHECK CHECK CONSTRAINT '</span> <span class="o">+</span> <span class="n">QUOTENAME</span><span class="p">(</span><span class="n">fk</span><span class="p">.</span><span class="n">name</span><span class="p">)</span> <span class="o">+</span> <span class="n">N</span><span class="s1">';'</span> <span class="o">+</span> <span class="nb">CHAR</span><span class="p">(</span><span class="mi">13</span><span class="p">)</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">foreign_keys</span> <span class="n">fk</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span> <span class="k">ON</span> <span class="n">fk</span><span class="p">.</span><span class="n">parent_object_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span><span class="p">;</span>

<span class="k">EXEC</span> <span class="n">sp_executesql</span> <span class="o">@</span><span class="k">sql</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">WITH CHECK CHECK CONSTRAINT</code> 의 두 번째 <code class="language-plaintext highlighter-rouge">CHECK</code> 가 중요. <strong>기존 데이터까지 다시 검증</strong> 하라는 옵션이에요. 그냥 <code class="language-plaintext highlighter-rouge">CHECK CONSTRAINT</code> 만 쓰면 제약은 다시 켜지지만 기존 위반은 안 잡혀요(<code class="language-plaintext highlighter-rouge">is_not_trusted = 1</code> 로 남음).</p>

<h3 id="검증-1">검증</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="s1">'.'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">table_name</span><span class="p">,</span> <span class="n">fk</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="n">fk_name</span><span class="p">,</span> <span class="n">fk</span><span class="p">.</span><span class="n">is_not_trusted</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">foreign_keys</span> <span class="n">fk</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span> <span class="k">ON</span> <span class="n">fk</span><span class="p">.</span><span class="n">parent_object_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span>
<span class="k">WHERE</span> <span class="n">fk</span><span class="p">.</span><span class="n">is_not_trusted</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<p>위 쿼리 결과가 비어야 정합성 OK.</p>

<p><br /></p>

<p><br /></p>

<h2 id="6-케이스-5--computed-column--rowversion">6. 케이스 5 — Computed column / rowversion</h2>

<h3 id="증상-4">증상</h3>

<p>DMS 가 <code class="language-plaintext highlighter-rouge">computed column</code> 또는 <code class="language-plaintext highlighter-rouge">rowversion(timestamp)</code> 컬럼에 직접 값을 쓰려다 실패.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>The column 'total_amount' cannot be modified because it is either a computed column 
or is the result of a UNION operator.

Cannot insert an explicit value into a timestamp column.
</code></pre></div></div>

<h3 id="원인-4">원인</h3>

<p>이 두 종류 컬럼은 <strong>DB 가 자동 계산/발급</strong> 해요. 값을 직접 INSERT 하면 안 됨.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">computed column</code> (예: <code class="language-plaintext highlighter-rouge">total_amount AS price * quantity</code>)</li>
  <li><code class="language-plaintext highlighter-rouge">rowversion</code> / <code class="language-plaintext highlighter-rouge">timestamp</code> (자동 증가 8-byte 바이너리)</li>
</ul>

<h3 id="대응-4">대응</h3>

<p>DMS Table mapping 에서 <strong>transformation rule</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="nl">"rule-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"transformation"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"rule-id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"100"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"rule-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"exclude-computed"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"rule-target"</span><span class="p">:</span><span class="w"> </span><span class="s2">"column"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"object-locator"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"schema-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dbo"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"column-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"total_amount"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"rule-action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"remove-column"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">rowversion</code> 도 같은 식으로 <code class="language-plaintext highlighter-rouge">remove-column</code>. 타깃 테이블에는 동일 정의가 그대로 있으므로, INSERT 시점에 자동 계산됨.</p>

<h3 id="사전-검출">사전 검출</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- computed column 목록</span>
<span class="k">SELECT</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="s1">'.'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">table_name</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">column_name</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">computed_columns</span> <span class="k">c</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">object_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span><span class="p">;</span>

<span class="c1">-- rowversion/timestamp 컬럼 목록</span>
<span class="k">SELECT</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="s1">'.'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">table_name</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">column_name</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">columns</span> <span class="k">c</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">object_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">types</span> <span class="n">ty</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">user_type_id</span> <span class="o">=</span> <span class="n">ty</span><span class="p">.</span><span class="n">user_type_id</span>
<span class="k">WHERE</span> <span class="n">ty</span><span class="p">.</span><span class="n">name</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'timestamp'</span><span class="p">,</span> <span class="s1">'rowversion'</span><span class="p">);</span>
</code></pre></div></div>

<p>이 두 쿼리 결과를 사전에 다 뽑아서 transformation rule 로 다 빼두면 깔끔.</p>

<p><br /></p>

<p><br /></p>

<h2 id="7-케이스-6--collation--데이터-타입-미스매치">7. 케이스 6 — Collation / 데이터 타입 미스매치</h2>

<h3 id="증상-5">증상</h3>

<p>row count 는 같은데 <code class="language-plaintext highlighter-rouge">CHECKSUM_AGG</code> 가 안 맞음. 특정 문자열 컬럼을 비교하면 같은 글자처럼 보이는데 다름.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="k">source</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span> <span class="k">WHERE</span> <span class="n">customer_name</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'홍길동'</span><span class="p">;</span>   <span class="c1">-- 1건</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">target</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span> <span class="k">WHERE</span> <span class="n">customer_name</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'홍길동'</span><span class="p">;</span>   <span class="c1">-- 0건</span>
</code></pre></div></div>

<h3 id="원인-5">원인</h3>

<ul>
  <li><strong>Collation 차이</strong> — 원본 DB 가 <code class="language-plaintext highlighter-rouge">Korean_Wansung_CI_AS</code>, 타깃이 <code class="language-plaintext highlighter-rouge">SQL_Latin1_General_CP1_CI_AS</code> 같은 경우. 한글이 깨지거나, 비교가 case-insensitive 가 아니거나, trailing space 처리 다름.</li>
  <li><strong>타입 변경</strong> — DMS 가 자동으로 <code class="language-plaintext highlighter-rouge">VARCHAR(MAX) → NVARCHAR(MAX)</code> 같은 변환을 할 때 옵션에 따라 trailing space 가 잘림.</li>
</ul>

<h3 id="대응-5">대응</h3>

<ul>
  <li>타깃 DB 의 collation 을 <strong>원본과 동일하게</strong> 만들어두는 게 정공법.</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 새 DB 생성 시 collation 명시</span>
<span class="k">CREATE</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span> <span class="k">COLLATE</span> <span class="n">Korean_Wansung_CI_AS</span><span class="p">;</span>

<span class="c1">-- 기존 DB collation 변경 (다소 무거운 작업)</span>
<span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span> <span class="k">COLLATE</span> <span class="n">Korean_Wansung_CI_AS</span><span class="p">;</span>
</code></pre></div></div>

<ul>
  <li>이미 다른 collation 으로 만들어버렸으면 <strong>컬럼 단위 collation 변경</strong> 으로 보정.</li>
</ul>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span>
<span class="k">ALTER</span> <span class="k">COLUMN</span> <span class="n">customer_name</span> <span class="n">NVARCHAR</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="k">COLLATE</span> <span class="n">Korean_Wansung_CI_AS</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="검증-2">검증</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 의심되는 문자열 컬럼은 LEN, DATALENGTH 둘 다 비교</span>
<span class="k">SELECT</span> <span class="n">TOP</span> <span class="mi">10</span> <span class="n">id</span><span class="p">,</span> <span class="n">customer_name</span><span class="p">,</span> <span class="n">LEN</span><span class="p">(</span><span class="n">customer_name</span><span class="p">),</span> <span class="n">DATALENGTH</span><span class="p">(</span><span class="n">customer_name</span><span class="p">)</span> 
<span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">id</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">LEN</code> 은 같지만 <code class="language-plaintext highlighter-rouge">DATALENGTH</code> 가 다르면 trailing space 차이.</p>

<p><br /></p>

<p><br /></p>

<h2 id="8-케이스-7--트리거-fire-on-replication">8. 케이스 7 — 트리거 fire-on-replication</h2>

<h3 id="증상-6">증상</h3>

<p>CDC 가 INSERT 한 row 에 대해 타깃의 트리거가 발동되면서, 데이터가 한 번 더 변형되거나 다른 테이블에까지 영향이 번짐.</p>

<h3 id="원인-6">원인</h3>

<p>기본적으로 SQL Server 트리거는 <strong>모든 INSERT/UPDATE/DELETE 에 발동</strong> 해요. CDC 가 흘리는 INSERT 도 예외 없음. 원본에선 트랜잭션 한 번에 트리거 + 본 데이터가 같이 처리됐을 텐데, CDC 는 본 데이터만 흘려요 → 타깃에서 트리거가 또 발동 → 중복 처리.</p>

<h3 id="대응-6">대응</h3>

<p><strong>트리거를 <code class="language-plaintext highlighter-rouge">NOT FOR REPLICATION</code> 으로 정의</strong> 하면, replication/CDC 로 들어온 변경에는 발동 안 해요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TRIGGER</span> <span class="n">trg_Orders_AfterInsert</span>
<span class="k">ON</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span>
<span class="k">AFTER</span> <span class="k">INSERT</span>
<span class="k">NOT</span> <span class="k">FOR</span> <span class="n">REPLICATION</span>
<span class="k">AS</span>
<span class="k">BEGIN</span>
  <span class="c1">-- 트리거 로직</span>
<span class="k">END</span><span class="p">;</span>
</code></pre></div></div>

<p>기존 트리거 변경은 <code class="language-plaintext highlighter-rouge">ALTER TRIGGER</code> 로 똑같이.</p>

<blockquote>
  <p>⚠️ 이 옵션은 SQL Server 의 replication / CDC 기반 변경을 인식해서 skip 해요. DMS 가 일반 SQL INSERT 로 적용하는 경우엔 안 통할 수 있어요. 안 통하면 마이그레이션 기간 동안 트리거 자체를 <strong>DISABLE</strong> 해두는 게 가장 확실해요.</p>
</blockquote>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 컷오버 전: 트리거 다 끄기</span>
<span class="n">DISABLE</span> <span class="k">TRIGGER</span> <span class="k">ALL</span> <span class="k">ON</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>

<span class="c1">-- 컷오버 후: 다시 켜기</span>
<span class="n">ENABLE</span> <span class="k">TRIGGER</span> <span class="k">ALL</span> <span class="k">ON</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="9-케이스-8--lob--큰-컬럼">9. 케이스 8 — LOB / 큰 컬럼</h2>

<h3 id="증상-7">증상</h3>

<p><code class="language-plaintext highlighter-rouge">VARBINARY(MAX)</code>, <code class="language-plaintext highlighter-rouge">NVARCHAR(MAX)</code>, <code class="language-plaintext highlighter-rouge">XML</code>, <code class="language-plaintext highlighter-rouge">IMAGE</code> 같은 LOB 컬럼이 잘려서 들어오거나, 풀로드 자체가 느려짐.</p>

<h3 id="원인-7">원인</h3>

<p>DMS 의 기본 LOB 처리 모드가 <code class="language-plaintext highlighter-rouge">Limited LOB mode</code> 인 경우, LOB 컬럼을 일정 크기(기본 32KB) 로 잘라요. 큰 BLOB 가 있는 테이블에선 데이터 손실.</p>

<h3 id="대응-7">대응</h3>

<p>DMS Task settings 의 LOB 모드를 명시.</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">"TargetMetadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"SupportLobs"</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">"FullLobMode"</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">"LobChunkSize"</span><span class="p">:</span><span class="w"> </span><span class="mi">64</span><span class="p">,</span><span class="w">
    </span><span class="nl">"LimitedSizeLobMode"</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">"LobMaxSize"</span><span class="p">:</span><span class="w"> </span><span class="mi">1024</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>

<table>
  <thead>
    <tr>
      <th>모드</th>
      <th>의미</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Don’t include LOB</td>
      <td>LOB 컬럼 자체를 skip</td>
      <td>LOB 따로 마이그레이션할 때</td>
    </tr>
    <tr>
      <td>Limited LOB mode</td>
      <td><code class="language-plaintext highlighter-rouge">LobMaxSize</code> 까지만 옮김 (빠름)</td>
      <td>LOB 크기가 작고 일정한 경우</td>
    </tr>
    <tr>
      <td>Full LOB mode</td>
      <td>청크 단위로 전체 옮김 (느림)</td>
      <td>LOB 크기가 크고 가변인 경우</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>💡 Full LOB mode 는 속도가 크게 떨어져요. LOB 가 큰 테이블은 별도 task 로 분리해서 옮기는 게 일반적.</p>
</blockquote>

<h3 id="사전-검출-1">사전 검출</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 테이블별 LOB 컬럼 + 평균/최대 크기</span>
<span class="k">SELECT</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="s1">'.'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">table_name</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">column_name</span><span class="p">,</span>
       <span class="k">AVG</span><span class="p">(</span><span class="n">DATALENGTH</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">))</span> <span class="k">AS</span> <span class="n">avg_size</span><span class="p">,</span>
       <span class="k">MAX</span><span class="p">(</span><span class="n">DATALENGTH</span><span class="p">(</span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">))</span> <span class="k">AS</span> <span class="n">max_size</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">columns</span> <span class="k">c</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">object_id</span> <span class="o">=</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">types</span> <span class="n">ty</span> <span class="k">ON</span> <span class="k">c</span><span class="p">.</span><span class="n">user_type_id</span> <span class="o">=</span> <span class="n">ty</span><span class="p">.</span><span class="n">user_type_id</span>
<span class="k">WHERE</span> <span class="n">ty</span><span class="p">.</span><span class="n">name</span> <span class="k">IN</span> <span class="p">(</span><span class="s1">'varbinary'</span><span class="p">,</span> <span class="s1">'nvarchar'</span><span class="p">,</span> <span class="s1">'varchar'</span><span class="p">,</span> <span class="s1">'xml'</span><span class="p">,</span> <span class="s1">'image'</span><span class="p">,</span> <span class="s1">'text'</span><span class="p">,</span> <span class="s1">'ntext'</span><span class="p">)</span>
  <span class="k">AND</span> <span class="k">c</span><span class="p">.</span><span class="n">max_length</span> <span class="k">IN</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">8000</span><span class="p">)</span>  <span class="c1">-- MAX 타입</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span><span class="p">,</span> <span class="k">c</span><span class="p">.</span><span class="n">name</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">max_size</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="10-검증-체크리스트--컷오버-전후">10. 검증 체크리스트 — 컷오버 전후</h2>

<p>마이그레이션 마지막 단계에서 한 번에 돌리는 검증 모음이에요. 이 정도면 정합성에 대해 안심하고 컷오버할 수 있어요.</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>DBCC CHECKDB</strong> — 무결성 (타깃)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>테이블별 row count 비교</strong> — 양쪽 동일</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong><code class="language-plaintext highlighter-rouge">CHECKSUM_AGG(BINARY_CHECKSUM(*))</code></strong> 비교 — 핵심 테이블 전수</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>MAX(updated_at) 비교</strong> — CDC 따라잡힘 확인</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>DEFAULT 제약 비교</strong> — 누락된 DEFAULT 가 있는지</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>IDENTITY 시드 보정</strong> — 모든 IDENTITY 테이블</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>외래키 <code class="language-plaintext highlighter-rouge">is_not_trusted = 0</code></strong> — 전부 신뢰 상태</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>트리거 enable 상태</strong> — 컷오버용으로 disable 했던 것 복원</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>인덱스/통계 재구성</strong> — 풀로드 중 비활성화했다면 재생성 + <code class="language-plaintext highlighter-rouge">UPDATE STATISTICS</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>앱 권한/로그인 매핑</strong> — 고아 사용자 0</li>
</ul>

<p><br /></p>

<p><br /></p>

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

<p>데이터 정합성 문제는 <strong>사전 점검 → DMS 옵션 → 컷오버 후 보정</strong> 의 3단계로 잡히는 게 대부분이에요. 케이스별 핵심만 다시.</p>

<table>
  <thead>
    <tr>
      <th>케이스</th>
      <th>핵심 조치</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>중복 키</td>
      <td>DMS <code class="language-plaintext highlighter-rouge">INSERT_OR_UPDATE</code> + <code class="language-plaintext highlighter-rouge">FullLoadIgnoreConflicts</code></td>
    </tr>
    <tr>
      <td>NOT NULL/DEFAULT</td>
      <td>타깃 DEFAULT 누락 사전 검출</td>
    </tr>
    <tr>
      <td>IDENTITY 시드</td>
      <td>컷오버 직후 <code class="language-plaintext highlighter-rouge">DBCC CHECKIDENT ... RESEED</code></td>
    </tr>
    <tr>
      <td>외래키 순서</td>
      <td>풀로드 전 disable → 후 <code class="language-plaintext highlighter-rouge">WITH CHECK CHECK CONSTRAINT</code></td>
    </tr>
    <tr>
      <td>Computed/rowversion</td>
      <td>DMS transformation <code class="language-plaintext highlighter-rouge">remove-column</code></td>
    </tr>
    <tr>
      <td>Collation</td>
      <td>타깃 DB/컬럼 collation 원본과 일치</td>
    </tr>
    <tr>
      <td>트리거 fire</td>
      <td><code class="language-plaintext highlighter-rouge">NOT FOR REPLICATION</code> 또는 마이그레이션 동안 disable</td>
    </tr>
    <tr>
      <td>LOB</td>
      <td>LOB 모드 명시, 큰 LOB 는 별도 task</td>
    </tr>
  </tbody>
</table>

<p>저희 사내 마이그레이션에선 위 8가지 중 <strong>3 (IDENTITY) + 4 (외래키) + 7 (트리거)</strong> 가 가장 자주 발목을 잡았어요. 컷오버 절차의 표준 체크리스트에 넣어두면 사고가 거의 안 납니다.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 이 시리즈를 한 페이지로 정리하는 <strong>MSSQL → AWS RDS 마이그레이션 마스터 체크리스트</strong> 를 한 장으로 묶어볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">(4/5) 변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="mssql" /><category term="aws" /><category term="rds" /><category term="dms" /><category term="cdc" /><category term="troubleshooting" /><category term="data-integrity" /><category term="migration" /><category term="kayserdocs" /><summary type="html"><![CDATA[MSSQL → AWS RDS 마이그레이션에서 자주 만나는 데이터 정합성 문제 8가지를 케이스별로 정리했어요. 중복 키, NOT NULL, IDENTITY 시드, 외래키, 트리거, LOB 등.]]></summary></entry><entry><title type="html">(4/5) Native Backup/Restore 뒤에 변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</title><link href="https://dorumugs.github.io/coding/RDS_SQLServer_%EB%B3%B5%EC%9B%90_%EC%9D%B4%ED%9B%84_%EB%B3%80%EA%B2%BD%EB%B6%84_%EA%B3%84%EC%86%8D_%EC%8C%93%EA%B8%B0/" rel="alternate" type="text/html" title="(4/5) Native Backup/Restore 뒤에 변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합" /><published>2026-05-30T10:26:00+09:00</published><updated>2026-05-30T10:26:00+09:00</updated><id>https://dorumugs.github.io/coding/RDS_SQLServer_%EB%B3%B5%EC%9B%90_%EC%9D%B4%ED%9B%84_%EB%B3%80%EA%B2%BD%EB%B6%84_%EA%B3%84%EC%86%8D_%EC%8C%93%EA%B8%B0</id><content type="html" xml:base="https://dorumugs.github.io/coding/RDS_SQLServer_%EB%B3%B5%EC%9B%90_%EC%9D%B4%ED%9B%84_%EB%B3%80%EA%B2%BD%EB%B6%84_%EA%B3%84%EC%86%8D_%EC%8C%93%EA%B8%B0/"><![CDATA[<div class="notice--info">
  <p><strong>📚 MSSQL → AWS RDS 마이그레이션 시리즈 (전체 5편)</strong></p>
  <ol>
    <li><a href="/coding/내부망_MSSQL_AWS_RDS_마이그레이션/">방법 비교 — 6가지 중에서 고르기</a></li>
    <li><a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</a></li>
    <li><a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</a></li>
    <li><strong>변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/MSSQL_마이그레이션_정합성_트러블슈팅/">정합성 트러블슈팅 — 케이스별 8가지 함정</a></li>
  </ol>
</div>

<p><a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">Native Backup/Restore 실전편</a>을 따라 풀백업 복원까지 끝내고 나면, 다음 질문이 바로 나와요. <strong>“이제 운영 중인 원본에서 새로 들어오는 데이터는 어떻게 따라잡지?”</strong> 풀백업이 끝난 시점 이후 원본은 계속 INSERT/UPDATE 가 들어오는 중이니까요. 이번 글은 그 “복원 위에 변경분 계속 쌓기” 문제를 RDS for SQL Server 의 제약을 짚어가며 정리해볼게요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>왜 “복원이 끝난 DB 위에 다시 백업을 못 올리는가” — RECOVERY/NORECOVERY 차이</li>
    <li>옵션 A: 차등 + 트랜잭션 로그 백업 체인 (NORECOVERY 유지)</li>
    <li>옵션 B: 한 번 ONLINE 된 후에는 <strong>DMS CDC-only</strong> 로 흘리기</li>
    <li>옵션 C: 앱 레벨 dual-write / 쿼리 기반 증분 ETL</li>
    <li>베스트 패턴: 풀백업/복원 + CDC-only 결합으로 비용/속도 모두 잡기</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-먼저-짚어야-하는-제약--recovery-vs-norecovery">1. 먼저 짚어야 하는 제약 — RECOVERY vs NORECOVERY</h2>

<p>가장 많이 헷갈리는 부분부터 정리할게요. SQL Server 의 복원은 두 가지 상태로 끝낼 수 있어요.</p>

<table>
  <thead>
    <tr>
      <th>상태</th>
      <th>의미</th>
      <th>다음에 백업 더 적용 가능?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">RECOVERY</code> (기본)</td>
      <td>DB 가 ONLINE 으로 올라옴. 사용자/쿼리 접근 OK</td>
      <td>❌ 추가 백업 못 적용</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NORECOVERY</code></td>
      <td>DB 가 <code class="language-plaintext highlighter-rouge">RESTORING</code> 상태로 남음. 쿼리 접근 X</td>
      <td>✅ 차등/로그 백업 이어서 적용 가능</td>
    </tr>
  </tbody>
</table>

<p>🚨 <strong>여기가 함정입니다.</strong> 풀백업 복원을 <code class="language-plaintext highlighter-rouge">WITH RECOVERY</code> (기본값) 로 끝내면, <strong>그 위에 차등/로그 백업을 더 못 올려요.</strong> RDS for SQL Server 의 <code class="language-plaintext highlighter-rouge">rds_restore_database</code> 도 마찬가지로, 한 번 ONLINE 으로 올라온 DB 에 또 백업 적용을 시도하면 LSN 체인 에러가 떨어집니다.</p>

<blockquote>
  <p>💡 그래서 이전 글에서 풀백업 복원에 <code class="language-plaintext highlighter-rouge">@with_norecovery = 1</code> 을 강조했던 거예요. 차등 백업까지 다 적용하고, <strong>마지막 복원에만</strong> <code class="language-plaintext highlighter-rouge">@with_norecovery = 0</code> 으로 ONLINE 으로 올리는 패턴.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="2-옵션-a--차등--로그-백업-체인-norecovery-유지">2. 옵션 A — 차등 + 로그 백업 체인 (NORECOVERY 유지)</h2>

<p>가장 정공법. 컷오버 직전까지 타깃 DB 를 <code class="language-plaintext highlighter-rouge">RESTORING</code> 상태로 두고, 원본에서 떠오는 차등/로그 백업을 계속 적용해요.</p>

<h3 id="2-1-흐름">2-1. 흐름</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[원본]                                  [타깃 RDS]
  1) FULL backup --------- 업로드 ----&gt; rds_restore_database (NORECOVERY)
  2) LOG backup #1 ------- 업로드 ----&gt; rds_restore_log     (NORECOVERY)
  3) LOG backup #2 ------- 업로드 ----&gt; rds_restore_log     (NORECOVERY)
  ...
  N) 컷오버 시점 LOG ----- 업로드 ----&gt; rds_restore_log     (RECOVERY=ONLINE)
</code></pre></div></div>

<h3 id="2-2-원본--로그-백업-만들기">2-2. 원본 — 로그 백업 만들기</h3>

<p>차등은 이전 글에서 다뤘으니, 여기선 <strong>트랜잭션 로그 백업</strong> 위주로.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 원본 MSSQL 의 복구 모델이 FULL 또는 BULK_LOGGED 여야 로그 백업이 의미 있음</span>
<span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span> <span class="k">SET</span> <span class="n">RECOVERY</span> <span class="k">FULL</span><span class="p">;</span>

<span class="c1">-- 10분마다 로그 백업 (예시)</span>
<span class="n">BACKUP</span> <span class="n">LOG</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span>
<span class="k">TO</span> <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\l</span><span class="s1">og</span><span class="se">\M</span><span class="s1">yDB_log_20260530_1000.trn'</span>
<span class="k">WITH</span> <span class="n">COMPRESSION</span><span class="p">,</span> <span class="n">CHECKSUM</span><span class="p">,</span> <span class="n">STATS</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
</code></pre></div></div>

<blockquote>
  <p>⚠️ 복구 모델이 <code class="language-plaintext highlighter-rouge">SIMPLE</code> 이면 로그 백업 자체가 불가능해요. 운영 DB 가 SIMPLE 이면 옵션 A 는 사용 불가 → 옵션 B(CDC) 로.</p>
</blockquote>

<h3 id="2-3-타깃-rds--로그-백업-적용">2-3. 타깃 RDS — 로그 백업 적용</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_restore_log</span>
  <span class="o">@</span><span class="n">restore_db_name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">s3_arn_to_restore_from</span> <span class="o">=</span> <span class="s1">'arn:aws:s3:::my-mssql-migration/log/MyDB_log_20260530_1000.trn'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">with_norecovery</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<p>진행은 <code class="language-plaintext highlighter-rouge">rds_task_status</code> 로 확인. 로그 백업은 보통 풀백업보다 훨씬 작아서 1~5분 단위로 빠르게 흘릴 수 있어요.</p>

<h3 id="2-4-컷오버--마지막-로그만-recovery-로">2-4. 컷오버 — 마지막 로그만 RECOVERY 로</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 원본: 쓰기 차단</span>
<span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span> <span class="k">SET</span> <span class="n">READ_ONLY</span> <span class="k">WITH</span> <span class="k">ROLLBACK</span> <span class="k">IMMEDIATE</span><span class="p">;</span>

<span class="c1">-- 원본: 마지막 tail-log 백업</span>
<span class="n">BACKUP</span> <span class="n">LOG</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span>
<span class="k">TO</span> <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\l</span><span class="s1">og</span><span class="se">\M</span><span class="s1">yDB_tail.trn'</span>
<span class="k">WITH</span> <span class="n">NORECOVERY</span><span class="p">,</span> <span class="n">COMPRESSION</span><span class="p">,</span> <span class="n">CHECKSUM</span><span class="p">;</span>

<span class="c1">-- 타깃: 마지막 로그를 RECOVERY 로 적용 → DB ONLINE</span>
<span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_restore_log</span>
  <span class="o">@</span><span class="n">restore_db_name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">s3_arn_to_restore_from</span> <span class="o">=</span> <span class="s1">'arn:aws:s3:::my-mssql-migration/log/MyDB_tail.trn'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">with_norecovery</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</code></pre></div></div>

<p>🎉 이 시점에 타깃 DB 가 ONLINE 으로 올라오면서 컷오버 완료.</p>

<h3 id="2-5-옵션-a-의-장단점">2-5. 옵션 A 의 장단점</h3>

<p><strong>장점</strong></p>

<ul>
  <li><strong>순정 MSSQL 방식</strong>. 추가 매니지드 서비스 비용 없음</li>
  <li>데이터 손실 0 (트랜잭션 로그 단위로 정확히 따라잡힘)</li>
  <li>“그대로” 보존이 가장 잘 됨</li>
</ul>

<p><strong>단점</strong></p>

<ul>
  <li>타깃 DB 가 컷오버 전까지 <strong>계속 RESTORING 상태</strong> → 쿼리/검증 불가</li>
  <li>로그 백업 주기 / 업로드 / 복원이 모두 <strong>수동 파이프라인</strong>. 자동화 필요</li>
  <li>원본 복구 모델이 SIMPLE 이면 사용 불가</li>
</ul>

<p><br /></p>

<p><br /></p>

<h2 id="3-옵션-b--dms-cdc-only-모드-이미-online-된-db-에-흘리기">3. 옵션 B — DMS CDC-only 모드 (이미 ONLINE 된 DB 에 흘리기)</h2>

<p>만약 이미 <code class="language-plaintext highlighter-rouge">WITH RECOVERY</code> 로 ONLINE 시켜버렸거나, 검증을 위해 타깃에서 쿼리를 돌려보고 싶다면 옵션 A 는 쓸 수 없어요. 이때 쓰는 게 <strong>DMS 의 CDC-only 모드</strong>.</p>

<h3 id="3-1-핵심-아이디어">3-1. 핵심 아이디어</h3>

<ul>
  <li>풀로드는 이미 끝났다 (Native Backup/Restore 로)</li>
  <li>DMS 한테 <strong>“풀로드 건너뛰고 변경분만 잡아 흘려”</strong> 라고 시킴</li>
  <li>시작 시점(timestamp 또는 LSN)을 명시해서, 풀백업 시점 이후의 변경을 잡아냄</li>
</ul>

<h3 id="3-2-migration-type">3-2. Migration Type</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms create-replication-task <span class="se">\</span>
  <span class="nt">--replication-task-identifier</span> mssql-cdc-only <span class="se">\</span>
  <span class="nt">--source-endpoint-arn</span> arn:aws:dms:...:MSSQL-SOURCE <span class="se">\</span>
  <span class="nt">--target-endpoint-arn</span> arn:aws:dms:...:RDS-TARGET <span class="se">\</span>
  <span class="nt">--replication-instance-arn</span> arn:aws:dms:...:DMS-MSSQL-MIG <span class="se">\</span>
  <span class="nt">--migration-type</span> cdc <span class="se">\</span>
  <span class="nt">--cdc-start-time</span> 2026-05-30T10:00:00Z <span class="se">\</span>
  <span class="nt">--table-mappings</span> file://table-mappings.json <span class="se">\</span>
  <span class="nt">--replication-task-settings</span> file://task-settings.json
</code></pre></div></div>

<p>핵심 옵션 두 개.</p>

<table>
  <thead>
    <tr>
      <th>옵션</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--migration-type cdc</code></td>
      <td>풀로드 생략, 변경분만</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--cdc-start-time</code></td>
      <td>변경 캡처 시작 시점. <strong>풀백업 시작 시각</strong> 또는 그 직전으로</td>
    </tr>
  </tbody>
</table>

<h3 id="3-3-시작-시점을-어떻게-정하나">3-3. 시작 시점을 어떻게 정하나</h3>

<p>가장 안전한 방법은 <strong>풀백업을 시작하기 직전의 시각</strong> 을 기록해두는 거예요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 원본에서 풀백업 시작 전에</span>
<span class="k">SELECT</span> <span class="n">GETUTCDATE</span><span class="p">()</span> <span class="k">AS</span> <span class="n">backup_start_utc</span><span class="p">;</span>
<span class="c1">-- =&gt; 2026-05-30 09:55:00</span>

<span class="c1">-- 이 값을 cdc-start-time 으로 넣음. backup 진행 중 변경은 중복 적용되겠지만,</span>
<span class="c1">-- DMS 의 idempotent 동작과 PK 충돌 처리로 보통 안전하게 흡수됨.</span>
</code></pre></div></div>

<blockquote>
  <p>💡 LSN 기준으로 더 정확하게 잡고 싶다면 <code class="language-plaintext highlighter-rouge">--cdc-start-position</code> 에 <code class="language-plaintext highlighter-rouge">LSN:xxxxx:xxxxxxxx</code> 형태로 줄 수 있어요. 풀백업 직전 LSN 을 <code class="language-plaintext highlighter-rouge">sys.fn_dblog</code> 로 따와서 넣는 패턴.</p>
</blockquote>

<h3 id="3-4-옵션-b-의-장단점">3-4. 옵션 B 의 장단점</h3>

<p><strong>장점</strong></p>

<ul>
  <li>풀로드를 DMS 로 안 돌려도 됨 → <strong>비용 절감 + 시간 단축</strong></li>
  <li>타깃 DB 를 ONLINE 으로 두고도 변경 따라잡기 가능 → 검증/대시보드 미리 붙여볼 수 있음</li>
  <li>CDC latency 를 보면서 컷오버 타이밍을 잡을 수 있음 (이전 글의 7단계 컷오버 그대로 적용)</li>
</ul>

<p><strong>단점</strong></p>

<ul>
  <li>원본에 <strong>CDC 활성화</strong> 가 필요 (이전 DMS 글 참고)</li>
  <li>풀백업 시점 ~ CDC 시작 시점 사이의 변경이 <strong>중복 적용</strong> 될 수 있음. PK/UPSERT 동작 사전 점검</li>
  <li>DMS Replication Instance 비용은 발생</li>
</ul>

<p><br /></p>

<p><br /></p>

<h2 id="4-옵션-c--앱-레벨-dual-write--쿼리-기반-증분-etl">4. 옵션 C — 앱 레벨 dual-write / 쿼리 기반 증분 ETL</h2>

<p>DB 단 메커니즘(백업 체인, CDC) 을 못 쓰는 환경에선 마지막 카드로.</p>

<h3 id="4-1-dual-write">4-1. Dual-write</h3>

<p>애플리케이션이 INSERT/UPDATE 를 칠 때 <strong>원본 + 타깃 양쪽에 동시 기록</strong>.</p>

<ul>
  <li>장점: DB 메커니즘 의존 없음. 앱이 통제 가능</li>
  <li>단점: 코드 수정 필요. 실패/재시도 시 양쪽 일관성 보장이 까다로움. 두 DB 가 잠시라도 다르면 디버깅 지옥</li>
</ul>

<h3 id="4-2-쿼리-기반-증분-etl">4-2. 쿼리 기반 증분 ETL</h3>

<p>타임스탬프나 버전 컬럼이 있는 테이블에 한해, 주기적으로</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span> 
<span class="k">WHERE</span> <span class="n">updated_at</span> <span class="o">&gt;</span> <span class="o">@</span><span class="n">last_sync_ts</span><span class="p">;</span>
</code></pre></div></div>

<p>식으로 끌어와 타깃에 MERGE.</p>

<ul>
  <li>장점: 구현 간단. 추가 서비스 불필요</li>
  <li>단점: <strong>DELETE 를 못 잡음</strong>(soft delete 면 OK). <code class="language-plaintext highlighter-rouge">updated_at</code> 이 없는 테이블엔 적용 불가. PK 충돌 처리 직접 구현</li>
</ul>

<p>🚨 옵션 C 는 “다른 게 다 안 될 때” 의 카드예요. 실무에선 잔불 정도로만 쓰고, 본진은 A 또는 B 로.</p>

<p><br /></p>

<p><br /></p>

<h2 id="5-베스트-패턴--풀백업복원--cdc-only-결합">5. 베스트 패턴 — 풀백업/복원 + CDC-only 결합</h2>

<p>이게 <a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">이전 DMS 글</a> 마지막에 예고한 패턴이에요. 두 방법의 장점만 가져와요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>시점         원본                             타깃 RDS
─────────────────────────────────────────────────────────────
T0           ┌─ CDC 활성화                     
T1           │  GETUTCDATE() = T1 기록
T1+ε         │  FULL BACKUP 시작                                       
T2           │  FULL BACKUP 완료 → S3 업로드                            
T3           │                                rds_restore_database
T4           │                                (WITH RECOVERY=ONLINE) 🎉
T5           │  DMS CDC-only Task 시작
             │   --cdc-start-time = T1
T5+          │  CDC latency 안정화 추적
컷오버       │  앱 쓰기 차단 → latency 0 대기
             └─ 앱 connection string 전환
</code></pre></div></div>

<h3 id="5-1-왜-이게-좋은가">5-1. 왜 이게 좋은가</h3>

<table>
  <thead>
    <tr>
      <th>비교축</th>
      <th>옵션 A (백업 체인 단독)</th>
      <th>옵션 B (DMS Full+CDC)</th>
      <th><strong>A+B 결합</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>초기 풀로드 속도</td>
      <td>빠름 (백업/복원)</td>
      <td>느림 (DMS 풀로드)</td>
      <td><strong>빠름</strong></td>
    </tr>
    <tr>
      <td>초기 비용</td>
      <td>낮음</td>
      <td>높음 (Replication 시간 길어짐)</td>
      <td><strong>낮음</strong></td>
    </tr>
    <tr>
      <td>컷오버 다운타임</td>
      <td>짧음(분 단위)</td>
      <td>매우 짧음(초 단위)</td>
      <td><strong>매우 짧음</strong></td>
    </tr>
    <tr>
      <td>타깃 DB 사전 검증</td>
      <td>불가(RESTORING)</td>
      <td>가능</td>
      <td><strong>가능</strong></td>
    </tr>
    <tr>
      <td>운영 부담</td>
      <td>로그 백업 파이프라인</td>
      <td>DMS 모니터링</td>
      <td><strong>DMS 모니터링</strong></td>
    </tr>
  </tbody>
</table>

<p>핵심은 “<strong>풀카피의 무거운 일은 백업/복원 으로, 변경분 추적의 가벼운 일은 DMS 로</strong>” 분담시키는 것. DMS 가 풀로드까지 하면 시간/비용이 다 무거워지는데, 그 부분을 백업/복원으로 떼어내요.</p>

<h3 id="5-2-실전-순서-정리">5-2. 실전 순서 (정리)</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T0</strong>: 원본에 CDC 활성화 (DB + 모든 대상 테이블)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T1</strong>: 풀백업 시작 시각 기록 (<code class="language-plaintext highlighter-rouge">SELECT GETUTCDATE()</code>)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T1+ε</strong>: 원본 풀백업 → S3 업로드</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T3</strong>: RDS 에서 <code class="language-plaintext highlighter-rouge">rds_restore_database</code> (<code class="language-plaintext highlighter-rouge">@with_norecovery = 0</code> → ONLINE)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T4</strong>: 타깃 검증 (DBCC CHECKDB, row count 스냅샷)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T5</strong>: DMS CDC-only task 시작 (<code class="language-plaintext highlighter-rouge">--migration-type cdc</code>, <code class="language-plaintext highlighter-rouge">--cdc-start-time T1</code>)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>T5+</strong>: CloudWatch 의 <code class="language-plaintext highlighter-rouge">CDCLatencyTarget</code> 안정화 확인 (5~10분 연속 &lt; 3초)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>컷오버</strong>: 앱 쓰기 차단 → latency 0 수렴 대기 → 앱 connection string 전환</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>사후</strong>: DMS Task / Endpoints / Replication Instance 삭제, 원본 CDC 비활성화</li>
</ul>

<blockquote>
  <p>💡 풀백업 시작 ~ CDC 시작 사이의 변경은 <strong>재적용</strong> 돼요. PK 충돌 발생 시 DMS 가 어떻게 처리하는지 사전에 확인하세요. Task 설정의 <code class="language-plaintext highlighter-rouge">ErrorBehavior</code> 에서 duplicate key 를 <code class="language-plaintext highlighter-rouge">IGNORE_RECORD</code> 로 두면 보통 안전하게 흡수됩니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="6-자주-깨지는-함정">6. 자주 깨지는 함정</h2>

<p>이 패턴에서 한 번씩 다 밟아본 함정들이에요.</p>

<ul>
  <li>🚨 <strong>풀백업 복원을 <code class="language-plaintext highlighter-rouge">WITH RECOVERY</code> 로 끝낸 뒤 차등 백업 더 올리려고 시도</strong> → LSN 체인 에러. 옵션 A 를 끝까지 가려면 마지막까지 NORECOVERY 유지</li>
  <li>🚨 <strong>CDC 활성화 시점을 풀백업 시작 뒤로 미룸</strong> → 두 시각 사이의 변경이 캡처 안 됨 → 영원히 누락</li>
  <li>🚨 <strong><code class="language-plaintext highlighter-rouge">cdc-start-time</code> 을 풀백업 완료 시각으로 넣음</strong> → 풀백업 진행 도중의 변경 누락</li>
  <li>🚨 <strong>로그 백업과 차등 백업을 섞었는데 LSN 체인이 어긋남</strong> → 복원 거부. 차등을 적용했다면 그 뒤로는 로그 백업 체인만 쭉 가야 함</li>
  <li>🚨 <strong>타깃 DB 의 외래키/트리거가 활성화된 상태에서 CDC 적용 시작</strong> → 적용 실패. 컷오버 직전까지 트리거 disable 권장</li>
  <li>🚨 <strong>원본 트랜잭션 로그가 폭증</strong> → 로그 백업 주기가 너무 길거나 CDC 캡처 잡이 멈춤. <code class="language-plaintext highlighter-rouge">log_reuse_wait_desc</code> 확인</li>
</ul>

<p><br /></p>

<p><br /></p>

<h2 id="7-정리--상황별-선택-가이드">7. 정리 — 상황별 선택 가이드</h2>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>추천 옵션</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>운영 다운타임 1~2시간 OK, 단순함 우선</td>
      <td><strong>옵션 A 단독</strong> (NORECOVERY 체인 + 마지막 RECOVERY)</td>
    </tr>
    <tr>
      <td>다운타임 거의 0, 타깃 사전 검증 필요</td>
      <td><strong>옵션 A+B 결합 (베스트)</strong></td>
    </tr>
    <tr>
      <td>이미 타깃 DB 가 ONLINE 으로 올라가 버림</td>
      <td><strong>옵션 B 단독</strong> (CDC-only)</td>
    </tr>
    <tr>
      <td>원본이 SIMPLE 복구 모델이고 변경 못 함</td>
      <td><strong>옵션 B 또는 C</strong></td>
    </tr>
    <tr>
      <td>앱이 통제 가능하고 DB 메커니즘 못 씀</td>
      <td><strong>옵션 C</strong> (dual-write)</td>
    </tr>
  </tbody>
</table>

<p>저희 사내 케이스에서 가장 자주 쓰는 건 <strong>옵션 A+B 결합</strong> 이에요. 풀카피는 백업/복원으로 한 방에 끝내고, 변경분은 DMS CDC-only 로 가볍게 따라잡는 구성. 비용/속도/다운타임 세 마리 토끼를 잡아요.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 이 결합 패턴에서 <strong>실제로 자주 만나는 데이터 정합성 문제</strong> — 중복 키 처리, NULL 컬럼, IDENTITY 시드 어긋남, 외래키 적용 순서 — 를 케이스별로 풀어볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">(3/5) DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</a> ｜ <strong>다음 글 →:</strong> <a href="/coding/MSSQL_마이그레이션_정합성_트러블슈팅/">(5/5) 정합성 트러블슈팅 — 케이스별 8가지 함정</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="mssql" /><category term="aws" /><category term="rds" /><category term="backup" /><category term="restore" /><category term="dms" /><category term="cdc" /><category term="incremental" /><category term="kayserdocs" /><summary type="html"><![CDATA[Native Backup/Restore 로 풀카피한 뒤 원본의 변경분을 계속 따라잡는 방법. NORECOVERY 체인과 DMS CDC-only 결합 패턴을 RDS for SQL Server 제약과 함께 풀어봅니다.]]></summary></entry><entry><title type="html">(3/5) AWS DMS + CDC 로 MSSQL 무중단 컷오버 — 풀로드 후 변경분 따라잡기</title><link href="https://dorumugs.github.io/coding/MSSQL_AWS_DMS_CDC_%EB%AC%B4%EC%A4%91%EB%8B%A8_%EC%BB%B7%EC%98%A4%EB%B2%84/" rel="alternate" type="text/html" title="(3/5) AWS DMS + CDC 로 MSSQL 무중단 컷오버 — 풀로드 후 변경분 따라잡기" /><published>2026-05-30T09:16:00+09:00</published><updated>2026-05-30T09:16:00+09:00</updated><id>https://dorumugs.github.io/coding/MSSQL_AWS_DMS_CDC_%EB%AC%B4%EC%A4%91%EB%8B%A8_%EC%BB%B7%EC%98%A4%EB%B2%84</id><content type="html" xml:base="https://dorumugs.github.io/coding/MSSQL_AWS_DMS_CDC_%EB%AC%B4%EC%A4%91%EB%8B%A8_%EC%BB%B7%EC%98%A4%EB%B2%84/"><![CDATA[<div class="notice--info">
  <p><strong>📚 MSSQL → AWS RDS 마이그레이션 시리즈 (전체 5편)</strong></p>
  <ol>
    <li><a href="/coding/내부망_MSSQL_AWS_RDS_마이그레이션/">방법 비교 — 6가지 중에서 고르기</a></li>
    <li><a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</a></li>
    <li><strong>DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</a></li>
    <li><a href="/coding/MSSQL_마이그레이션_정합성_트러블슈팅/">정합성 트러블슈팅 — 케이스별 8가지 함정</a></li>
  </ol>
</div>

<p><a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">지난 글</a>에서 Native Backup/Restore 로 풀카피 → 컷오버 순서를 정리했어요. 그런데 운영 DB 라 <strong>단 몇 분의 다운타임도 허용 안 되는</strong> 케이스가 있죠. 이번 글은 그런 상황에서 쓰는 <strong>AWS DMS + CDC</strong> 구성법이에요. 원본을 운영 중 그대로 두고 풀로드 → 변경분 실시간 따라잡기 → 컷오버 순서로 다운타임을 분 단위 이하로 줄이는 흐름을 정리합니다.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>원본 MSSQL 에서 CDC 활성화 (DB 레벨 + 테이블 레벨)</li>
    <li>DMS Replication Instance / Endpoints / Task 세팅</li>
    <li>Full Load + CDC 동시 모드 vs 분리 모드 차이</li>
    <li>스키마/인덱스/IDENTITY 는 DMS 가 안 가져온다는 점과 대응</li>
    <li>CloudWatch 로 latency 추적해서 안전하게 컷오버하는 절차</li>
    <li>마이그레이션 끝난 뒤 CDC/DMS 정리(cleanup)</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-큰-그림--무엇이-어디서-도는가">1. 큰 그림 — 무엇이 어디서 도는가</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[내부망 MSSQL]
   │  (1) CDC 로 변경분 캡처 (트랜잭션 로그 기반)
   │
   ▼
[DMS Replication Instance (VPC)]
   │  (2) Full Load + 캡처된 변경분을 타깃에 적용
   │
   ▼
[AWS RDS for SQL Server]
</code></pre></div></div>

<p>DMS 는 <strong>Replication Instance</strong> 라는 EC2 같은 워커를 하나 띄워두고, 거기서 소스의 <strong>트랜잭션 로그</strong> 를 읽어와 타깃에 흘려줘요. 원본은 CDC 만 켜두면 되고, DMS 가 직접 원본을 폴링하면서 변경을 잡아갑니다.</p>

<blockquote>
  <p>💡 풀로드 + CDC 를 <strong>같은 task</strong> 에서 켜면 풀로드 진행 중에도 변경을 캐시해두고, 풀로드 끝나는 순간부터 따라잡기를 시작해요. “다운타임 거의 0” 의 핵심.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="2-원본-mssql-준비--cdc-활성화">2. 원본 MSSQL 준비 — CDC 활성화</h2>

<p>DMS 가 MSSQL 의 변경분을 잡아가려면 <strong>원본에서 CDC 가 켜져 있어야</strong> 해요. SQL Server 의 CDC 는 트랜잭션 로그를 읽어서 변경을 별도 시스템 테이블에 기록하는 기능이에요.</p>

<h3 id="2-1-db-레벨-cdc-켜기">2-1. DB 레벨 CDC 켜기</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">USE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">];</span>
<span class="k">EXEC</span> <span class="n">sys</span><span class="p">.</span><span class="n">sp_cdc_enable_db</span><span class="p">;</span>

<span class="c1">-- 확인</span>
<span class="k">SELECT</span> <span class="n">name</span><span class="p">,</span> <span class="n">is_cdc_enabled</span> 
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">databases</span> 
<span class="k">WHERE</span> <span class="n">name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">is_cdc_enabled = 1</code> 이면 OK.</p>

<h3 id="2-2-테이블-레벨-cdc-켜기">2-2. 테이블 레벨 CDC 켜기</h3>

<p>CDC 는 <strong>테이블 단위로 한 번 더 켜줘야</strong> 해요. 마이그레이션 대상 테이블 전부에 대해 돌립니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">USE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">];</span>

<span class="k">EXEC</span> <span class="n">sys</span><span class="p">.</span><span class="n">sp_cdc_enable_table</span>
  <span class="o">@</span><span class="n">source_schema</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'dbo'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">source_name</span>   <span class="o">=</span> <span class="n">N</span><span class="s1">'Orders'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">role_name</span>     <span class="o">=</span> <span class="k">NULL</span><span class="p">,</span>           <span class="c1">-- 별도 role 안 쓰면 NULL</span>
  <span class="o">@</span><span class="n">supports_net_changes</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</code></pre></div></div>

<p>테이블 많으면 동적 SQL 로 한 번에 돌려요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">DECLARE</span> <span class="o">@</span><span class="k">sql</span> <span class="n">NVARCHAR</span><span class="p">(</span><span class="k">MAX</span><span class="p">)</span> <span class="o">=</span> <span class="n">N</span><span class="s1">''</span><span class="p">;</span>
<span class="k">SELECT</span> <span class="o">@</span><span class="k">sql</span> <span class="o">=</span> <span class="o">@</span><span class="k">sql</span> <span class="o">+</span> 
  <span class="n">N</span><span class="s1">'EXEC sys.sp_cdc_enable_table @source_schema=N</span><span class="se">''</span><span class="s1">'</span> <span class="o">+</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> 
  <span class="o">+</span> <span class="n">N</span><span class="s1">'</span><span class="se">''</span><span class="s1">, @source_name=N</span><span class="se">''</span><span class="s1">'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> 
  <span class="o">+</span> <span class="n">N</span><span class="s1">'</span><span class="se">''</span><span class="s1">, @role_name=NULL, @supports_net_changes=1;'</span> <span class="o">+</span> <span class="nb">CHAR</span><span class="p">(</span><span class="mi">13</span><span class="p">)</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span>
<span class="k">WHERE</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="s1">'dbo'</span><span class="p">;</span>

<span class="k">EXEC</span> <span class="n">sp_executesql</span> <span class="o">@</span><span class="k">sql</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="2-3-sql-server-agent-가-떠-있어야-함">2-3. SQL Server Agent 가 떠 있어야 함</h3>

<p>CDC 캡처 잡(<code class="language-plaintext highlighter-rouge">cdc.MyDB_capture</code>) 은 SQL Server Agent 가 돌려요. Agent 가 죽어있으면 변경이 안 잡혀요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">sp_help_job</span> <span class="o">@</span><span class="n">job_name</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'cdc.MyDB_capture'</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">current_execution_status</code> 가 <code class="language-plaintext highlighter-rouge">1</code> (idle 아닌 실행 중) 또는 정상 스케줄로 도는지 확인.</p>

<blockquote>
  <p>⚠️ RDS for SQL Server <strong>소스</strong> 였다면 CDC 활성화 절차가 조금 달라요(<code class="language-plaintext highlighter-rouge">rds_cdc_enable_db</code>). 이 글은 <strong>온프레미스/EC2 소스</strong> 기준.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="3-aws-측-사전-세팅--replication-instance--endpoints">3. AWS 측 사전 세팅 — Replication Instance / Endpoints</h2>

<h3 id="3-1-replication-subnet-group--instance">3-1. Replication Subnet Group + Instance</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms create-replication-subnet-group <span class="se">\</span>
  <span class="nt">--replication-subnet-group-identifier</span> dms-subnet-grp <span class="se">\</span>
  <span class="nt">--replication-subnet-group-description</span> <span class="s2">"DMS subnet group"</span> <span class="se">\</span>
  <span class="nt">--subnet-ids</span> subnet-aaaa subnet-bbbb

aws dms create-replication-instance <span class="se">\</span>
  <span class="nt">--replication-instance-identifier</span> dms-mssql-mig <span class="se">\</span>
  <span class="nt">--replication-instance-class</span> dms.r5.large <span class="se">\</span>
  <span class="nt">--allocated-storage</span> 100 <span class="se">\</span>
  <span class="nt">--vpc-security-group-ids</span> sg-xxxxxxxx <span class="se">\</span>
  <span class="nt">--replication-subnet-group-identifier</span> dms-subnet-grp <span class="se">\</span>
  <span class="nt">--no-publicly-accessible</span> <span class="se">\</span>
  <span class="nt">--multi-az</span>
</code></pre></div></div>

<p>인스턴스 사이즈는 풀로드 데이터량/병렬도/CDC 부하 따라 골라요. 보통 <strong>dms.r5.large</strong> 부터 시작해서 latency 보면서 키워요.</p>

<h3 id="3-2-source--target-endpoints">3-2. Source / Target Endpoints</h3>

<p><strong>Source (내부망 MSSQL)</strong> — S2S VPN 으로 접근.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms create-endpoint <span class="se">\</span>
  <span class="nt">--endpoint-identifier</span> mssql-source <span class="se">\</span>
  <span class="nt">--endpoint-type</span> <span class="nb">source</span> <span class="se">\</span>
  <span class="nt">--engine-name</span> sqlserver <span class="se">\</span>
  <span class="nt">--server-name</span> 10.x.x.x <span class="se">\</span>
  <span class="nt">--port</span> 1433 <span class="se">\</span>
  <span class="nt">--database-name</span> MyDB <span class="se">\</span>
  <span class="nt">--username</span> dms_user <span class="se">\</span>
  <span class="nt">--password</span> <span class="s1">'&lt;DMS_USER_PASSWORD&gt;'</span>
</code></pre></div></div>

<p><strong>Target (AWS RDS for SQL Server)</strong></p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms create-endpoint <span class="se">\</span>
  <span class="nt">--endpoint-identifier</span> rds-target <span class="se">\</span>
  <span class="nt">--endpoint-type</span> target <span class="se">\</span>
  <span class="nt">--engine-name</span> sqlserver <span class="se">\</span>
  <span class="nt">--server-name</span> my-mssql-rds.xxxx.ap-northeast-2.rds.amazonaws.com <span class="se">\</span>
  <span class="nt">--port</span> 1433 <span class="se">\</span>
  <span class="nt">--database-name</span> MyDB <span class="se">\</span>
  <span class="nt">--username</span> admin <span class="se">\</span>
  <span class="nt">--password</span> <span class="s1">'&lt;RDS_ADMIN_PASSWORD&gt;'</span>
</code></pre></div></div>

<h3 id="3-3-connection-테스트">3-3. Connection 테스트</h3>

<p>엔드포인트를 만들면 반드시 한 번 테스트.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms test-connection <span class="se">\</span>
  <span class="nt">--replication-instance-arn</span> arn:aws:dms:ap-northeast-2:123456789012:rep:DMS-MSSQL-MIG <span class="se">\</span>
  <span class="nt">--endpoint-arn</span> arn:aws:dms:ap-northeast-2:123456789012:endpoint:MSSQL-SOURCE
</code></pre></div></div>

<p>✅ 결과가 <code class="language-plaintext highlighter-rouge">successful</code> 이어야 다음 단계로 갈 수 있어요. 실패 사유는 콘솔의 <code class="language-plaintext highlighter-rouge">Last failure message</code> 에 떨어져요.</p>

<h3 id="3-4-소스-계정-권한">3-4. 소스 계정 권한</h3>

<p><code class="language-plaintext highlighter-rouge">dms_user</code> 에는 최소 권한만. 흔히 깨지는 권한 세트는 다음 세 가지에요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">USE</span> <span class="n">master</span><span class="p">;</span>
<span class="k">GRANT</span> <span class="k">VIEW</span> <span class="n">SERVER</span> <span class="k">STATE</span> <span class="k">TO</span> <span class="n">dms_user</span><span class="p">;</span>

<span class="n">USE</span> <span class="n">MyDB</span><span class="p">;</span>
<span class="k">EXEC</span> <span class="n">sp_addrolemember</span> <span class="s1">'db_owner'</span><span class="p">,</span> <span class="s1">'dms_user'</span><span class="p">;</span>   
<span class="c1">-- 또는 SELECT + VIEW DATABASE STATE + db_datareader 조합</span>
</code></pre></div></div>

<p>DMS 가 CDC 함수(<code class="language-plaintext highlighter-rouge">fn_cdc_get_all_changes_*</code>) 를 호출하기 때문에 단순 <code class="language-plaintext highlighter-rouge">db_datareader</code> 만으론 부족해요. <code class="language-plaintext highlighter-rouge">db_owner</code> 가 가장 깔끔하지만, 보안상 줄이고 싶다면 <code class="language-plaintext highlighter-rouge">EXECUTE</code> 권한을 CDC 함수에 별도 부여하는 방식이 있어요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="4-스키마는-따로--dms-가-안-챙기는-것들">4. 스키마는 따로 — DMS 가 안 챙기는 것들</h2>

<p>🚨 <strong>여기서 가장 많이 헷갈려요.</strong> DMS 는 <strong>데이터 위주</strong> 로 옮기는 서비스라, 다음을 직접 챙겨야 해요.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>DMS 가 옮겨주나?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>테이블 구조 (CREATE TABLE)</td>
      <td>△ (기본 타입 변환만, 추천 X)</td>
    </tr>
    <tr>
      <td>인덱스 (clustered/nonclustered)</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>외래키 제약</td>
      <td>❌ (풀로드 중 자동 disable 권장)</td>
    </tr>
    <tr>
      <td>트리거 / 저장 프로시저 / 함수</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>시퀀스 / IDENTITY 시드</td>
      <td>❌</td>
    </tr>
    <tr>
      <td>사용자/로그인/권한</td>
      <td>❌</td>
    </tr>
  </tbody>
</table>

<p><strong>일반적인 대응 패턴</strong></p>

<ol>
  <li>원본에서 <code class="language-plaintext highlighter-rouge">SqlPackage /Action:Extract</code> 로 <strong>DACPAC</strong> 만들기 (스키마만, 데이터 X)</li>
  <li>타깃 RDS 에 <code class="language-plaintext highlighter-rouge">SqlPackage /Action:Publish</code> 로 스키마 먼저 적용</li>
  <li>그 다음 DMS 로 데이터만 채우기</li>
  <li>DMS 끝난 후 외래키 / 트리거 / 시퀀스 시드 보정</li>
</ol>

<p>또는 SSMS 의 “Generate Scripts” 로 <strong>스키마 + 보조 객체</strong> 만 추출해도 OK.</p>

<blockquote>
  <p>💡 <strong>풀로드 중에는 타깃의 외래키와 인덱스를 잠시 끄거나 비활성화</strong> 하는 게 빨라요. Task 설정의 <code class="language-plaintext highlighter-rouge">Target metadata</code> 에서 <code class="language-plaintext highlighter-rouge">BatchApplyEnabled</code> + 인덱스/제약 처리를 조정할 수 있어요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="5-migration-task-만들기">5. Migration Task 만들기</h2>

<p>이제 본 task. <strong>Full Load + CDC</strong> 모드로 만들고, table mapping 으로 옮길 테이블을 지정해요.</p>

<h3 id="5-1-table-mappings-table-mappingsjson">5-1. Table mappings (<code class="language-plaintext highlighter-rouge">table-mappings.json</code>)</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">"rules"</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">"rule-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"selection"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"rule-id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"rule-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"select-dbo"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"object-locator"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"schema-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dbo"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"table-name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"%"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"rule-action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"include"</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">dbo</code> 스키마의 모든 테이블을 선택. 일부만 옮길 거면 <code class="language-plaintext highlighter-rouge">table-name</code> 을 구체적으로.</p>

<h3 id="5-2-task-settings-task-settingsjson">5-2. Task settings (<code class="language-plaintext highlighter-rouge">task-settings.json</code>)</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">"TargetMetadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"BatchApplyEnabled"</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">"ParallelLoadThreads"</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ParallelLoadBufferSize"</span><span class="p">:</span><span class="w"> </span><span class="mi">500</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"FullLoadSettings"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"TargetTablePrepMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TRUNCATE_BEFORE_LOAD"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"MaxFullLoadSubTasks"</span><span class="p">:</span><span class="w"> </span><span class="mi">8</span><span class="p">,</span><span class="w">
    </span><span class="nl">"CommitRate"</span><span class="p">:</span><span class="w"> </span><span class="mi">10000</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"Logging"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"EnableLogging"</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">"LogComponents"</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">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SOURCE_CAPTURE"</span><span class="p">,</span><span class="w"> </span><span class="nl">"Severity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOGGER_SEVERITY_DEFAULT"</span><span class="w"> </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SOURCE_UNLOAD"</span><span class="p">,</span><span class="w">  </span><span class="nl">"Severity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOGGER_SEVERITY_DEFAULT"</span><span class="w"> </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TARGET_APPLY"</span><span class="p">,</span><span class="w">   </span><span class="nl">"Severity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOGGER_SEVERITY_DEFAULT"</span><span class="w"> </span><span class="p">},</span><span class="w">
      </span><span class="p">{</span><span class="w"> </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"TARGET_LOAD"</span><span class="p">,</span><span class="w">    </span><span class="nl">"Severity"</span><span class="p">:</span><span class="w"> </span><span class="s2">"LOGGER_SEVERITY_DEFAULT"</span><span class="w"> </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"ChangeProcessingTuning"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"BatchApplyPreserveTransaction"</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">"BatchApplyTimeoutMin"</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">"BatchApplyTimeoutMax"</span><span class="p">:</span><span class="w"> </span><span class="mi">30</span><span class="p">,</span><span class="w">
    </span><span class="nl">"MinTransactionSize"</span><span class="p">:</span><span class="w"> </span><span class="mi">1000</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">BatchApplyEnabled</code></td>
      <td>변경을 배치로 묶어 타깃에 적용. CDC 단계에서 처리량 ↑</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ParallelLoadThreads</code></td>
      <td>테이블당 병렬 스레드</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MaxFullLoadSubTasks</code></td>
      <td>동시에 풀로드 돌릴 테이블 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TargetTablePrepMode = TRUNCATE_BEFORE_LOAD</code></td>
      <td>풀로드 전 타깃 테이블 비우기. 단, 외래키 있으면 실패할 수 있어 사전 작업 필요</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CommitRate</code></td>
      <td>풀로드 커밋 단위. 트랜잭션 로그 부담 조절</td>
    </tr>
  </tbody>
</table>

<h3 id="5-3-task-생성">5-3. Task 생성</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms create-replication-task <span class="se">\</span>
  <span class="nt">--replication-task-identifier</span> mssql-fullload-cdc <span class="se">\</span>
  <span class="nt">--source-endpoint-arn</span> arn:aws:dms:...:MSSQL-SOURCE <span class="se">\</span>
  <span class="nt">--target-endpoint-arn</span> arn:aws:dms:...:RDS-TARGET <span class="se">\</span>
  <span class="nt">--replication-instance-arn</span> arn:aws:dms:...:DMS-MSSQL-MIG <span class="se">\</span>
  <span class="nt">--migration-type</span> full-load-and-cdc <span class="se">\</span>
  <span class="nt">--table-mappings</span> file://table-mappings.json <span class="se">\</span>
  <span class="nt">--replication-task-settings</span> file://task-settings.json

<span class="c"># 시작</span>
aws dms start-replication-task <span class="se">\</span>
  <span class="nt">--replication-task-arn</span> arn:aws:dms:...:task:MSSQL-FULLLOAD-CDC <span class="se">\</span>
  <span class="nt">--start-replication-task-type</span> start-replication
</code></pre></div></div>

<blockquote>
  <p>💡 <code class="language-plaintext highlighter-rouge">full-load-and-cdc</code> 가 핵심. <code class="language-plaintext highlighter-rouge">full-load</code> 만이면 풀로드 끝난 시점부터의 변경이 누락돼요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="6-진행-모니터링--무엇을-봐야-하는가">6. 진행 모니터링 — 무엇을 봐야 하는가</h2>

<h3 id="6-1-task-통계">6-1. Task 통계</h3>

<p>콘솔에서 Task 의 <strong>Table statistics</strong> 탭이 가장 직관적이에요. 테이블별로</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Full load rows</code> / <code class="language-plaintext highlighter-rouge">Total</code></li>
  <li><code class="language-plaintext highlighter-rouge">Inserts / Updates / Deletes</code> (CDC 단계)</li>
  <li><code class="language-plaintext highlighter-rouge">Load state</code> (<code class="language-plaintext highlighter-rouge">Table completed</code>, <code class="language-plaintext highlighter-rouge">Before load</code>, <code class="language-plaintext highlighter-rouge">Full load</code>, <code class="language-plaintext highlighter-rouge">Table error</code>)</li>
</ul>

<h3 id="6-2-cloudwatch-핵심-지표">6-2. CloudWatch 핵심 지표</h3>

<table>
  <thead>
    <tr>
      <th>지표</th>
      <th>의미</th>
      <th>컷오버 기준</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CDCLatencySource</code></td>
      <td>소스 트랜잭션 로그 → DMS 까지 지연 (초)</td>
      <td>1초 이내 안정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CDCLatencyTarget</code></td>
      <td>DMS → 타깃 반영까지 지연 (초)</td>
      <td>1~3초 이내 안정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CDCIncomingChanges</code></td>
      <td>캡처 중인 변경 수</td>
      <td>누적 안 쌓이고 평탄</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CDCChangesMemorySource</code></td>
      <td>메모리에 쌓인 변경</td>
      <td>안정적으로 낮게</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CDCChangesDiskSource</code></td>
      <td>디스크로 떨어진 변경</td>
      <td>0 근처 유지</td>
    </tr>
  </tbody>
</table>

<p>✅ 컷오버 직전 체크 — <code class="language-plaintext highlighter-rouge">CDCLatencyTarget</code> 이 <strong>연속 5~10분간 1~3초 이내</strong> 유지되면 따라잡기 안정 상태로 봐도 됩니다.</p>

<blockquote>
  <p>⚠️ <code class="language-plaintext highlighter-rouge">CDCLatencyTarget</code> 이 점점 커지면 DMS 인스턴스 사양 / 타깃 디스크 IOPS / 타깃 인덱스가 병목. 이 상태로 컷오버하면 안 됩니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="7-컷오버-절차--다운타임-분-단위-이하로">7. 컷오버 절차 — 다운타임 분 단위 이하로</h2>

<p>여기서부터가 본 게임이에요. 절차는 한 줄도 빼먹지 마세요.</p>

<h3 id="step-1-사전-정렬">Step 1. 사전 정렬</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />CDC latency 가 안정적으로 작음 (5~10분 연속)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Table statistics 의 에러 0</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />타깃에 인덱스/외래키 보조 객체 사전 생성 완료 (또는 사전 비활성)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />애플리케이션 측 새 접속 정보(RDS 엔드포인트) 준비 완료</li>
</ul>

<h3 id="step-2-원본-쓰기-차단">Step 2. 원본 쓰기 차단</h3>

<p>가장 안전한 방법은 애플리케이션 측에서 <strong>새 트랜잭션을 막는</strong> 거예요. DB 단에서 막으려면</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 원본 MSSQL</span>
<span class="k">ALTER</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span> <span class="k">SET</span> <span class="n">READ_ONLY</span> <span class="k">WITH</span> <span class="k">ROLLBACK</span> <span class="k">IMMEDIATE</span><span class="p">;</span>
</code></pre></div></div>

<p>또는 앱 계정의 권한을 일시적으로 회수.</p>

<h3 id="step-3-마지막-변경분-따라잡기-대기">Step 3. 마지막 변경분 따라잡기 대기</h3>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CDCLatencyTarget → 0 으로 수렴
CDCIncomingChanges → 0 으로 수렴
</code></pre></div></div>

<p>10~30초 정도면 보통 마지막 트랜잭션까지 다 따라잡아요.</p>

<h3 id="step-4-검증">Step 4. 검증</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 양쪽 row count 비교</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>       <span class="c1">-- 원본</span>
<span class="k">SELECT</span> <span class="k">COUNT</span><span class="p">(</span><span class="o">*</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>       <span class="c1">-- 타깃</span>

<span class="c1">-- 최신 데이터 비교</span>
<span class="k">SELECT</span> <span class="k">MAX</span><span class="p">(</span><span class="n">updated_at</span><span class="p">)</span> <span class="k">FROM</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span><span class="p">;</span>
</code></pre></div></div>

<p>핵심 테이블 몇 개만 빠르게 비교.</p>

<h3 id="step-5-외래키시퀀스identity-복구">Step 5. 외래키/시퀀스/IDENTITY 복구</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 외래키 다시 enable</span>
<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span> <span class="k">WITH</span> <span class="k">CHECK</span> <span class="k">CHECK</span> <span class="k">CONSTRAINT</span> <span class="k">ALL</span><span class="p">;</span>

<span class="c1">-- IDENTITY 시드 재설정 (원본의 최댓값으로)</span>
<span class="n">DBCC</span> <span class="n">CHECKIDENT</span><span class="p">(</span><span class="s1">'dbo.Orders'</span><span class="p">,</span> <span class="n">RESEED</span><span class="p">,</span> <span class="o">&lt;</span><span class="err">원본</span> <span class="k">MAX</span><span class="p">(</span><span class="n">id</span><span class="p">)</span><span class="o">&gt;</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="step-6-앱-connection-string-전환">Step 6. 앱 connection string 전환</h3>

<p>DNS 변경 or config 핫스왑. 새 쿼리는 RDS 로 흐름.</p>

<h3 id="step-7-task-중지">Step 7. Task 중지</h3>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws dms stop-replication-task <span class="nt">--replication-task-arn</span> arn:aws:dms:...:task:MSSQL-FULLLOAD-CDC
</code></pre></div></div>

<p>🎉 컷오버 완료. 다운타임은 보통 <strong>1~3분</strong> 안에 끝나요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="8-사후-정리--비싸게-두지-말기">8. 사후 정리 — 비싸게 두지 말기</h2>

<p>DMS Replication Instance 는 시간당 요금이 꾸준히 나와요. 컷오버 끝나면 빠르게 정리.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. Task 삭제</span>
aws dms delete-replication-task <span class="nt">--replication-task-arn</span> ...

<span class="c"># 2. Endpoints 삭제</span>
aws dms delete-endpoint <span class="nt">--endpoint-arn</span> arn:aws:dms:...:endpoint:MSSQL-SOURCE
aws dms delete-endpoint <span class="nt">--endpoint-arn</span> arn:aws:dms:...:endpoint:RDS-TARGET

<span class="c"># 3. Replication Instance 삭제</span>
aws dms delete-replication-instance <span class="nt">--replication-instance-arn</span> arn:aws:dms:...:rep:DMS-MSSQL-MIG
</code></pre></div></div>

<p>원본 MSSQL 의 CDC 도 더 이상 필요 없으면 끄세요. 트랜잭션 로그 보존 부담이 줄어요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">USE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">];</span>

<span class="c1">-- 테이블별 CDC 끄기</span>
<span class="k">EXEC</span> <span class="n">sys</span><span class="p">.</span><span class="n">sp_cdc_disable_table</span> 
  <span class="o">@</span><span class="n">source_schema</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'dbo'</span><span class="p">,</span> 
  <span class="o">@</span><span class="n">source_name</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'Orders'</span><span class="p">,</span> 
  <span class="o">@</span><span class="n">capture_instance</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'all'</span><span class="p">;</span>

<span class="c1">-- DB 레벨 CDC 끄기</span>
<span class="k">EXEC</span> <span class="n">sys</span><span class="p">.</span><span class="n">sp_cdc_disable_db</span><span class="p">;</span>
</code></pre></div></div>

<p><br /></p>

<p><br /></p>

<h2 id="9-자주-깨지는-함정">9. 자주 깨지는 함정</h2>

<p>저도 한 번씩 다 밟아본 함정들이에요.</p>

<ul>
  <li>🚨 <strong>CDC 안 켜고 task 시작</strong> → <code class="language-plaintext highlighter-rouge">full-load-and-cdc</code> 가 <code class="language-plaintext highlighter-rouge">CDC source endpoint not configured</code> 에러로 죽음</li>
  <li>🚨 <strong>SQL Server Agent 가 꺼져있음</strong> → CDC 캡처 잡이 안 돌아 변경이 누적되다 폭주</li>
  <li>🚨 <strong>타깃에 외래키가 켜진 채 풀로드</strong> → 부모/자식 로드 순서 때문에 실패. 풀로드 전 disable</li>
  <li>🚨 <strong>IDENTITY 시드 미보정</strong> → 컷오버 후 새 INSERT 가 키 충돌</li>
  <li>🚨 <strong>권한 부족</strong> (<code class="language-plaintext highlighter-rouge">VIEW SERVER STATE</code>, CDC 함수 <code class="language-plaintext highlighter-rouge">EXECUTE</code>) → CDC 단계에서 silent 하게 latency 만 늘어남</li>
  <li>🚨 <strong><code class="language-plaintext highlighter-rouge">CDCLatencyTarget</code> 이 점점 커짐</strong> → DMS 인스턴스 / 타깃 IOPS / 인덱스 병목. 이 상태로 컷오버 절대 금지</li>
  <li>🚨 <strong>트랜잭션 로그 폭증</strong> → CDC 가 안 따라잡고 있거나, 풀백업/로그백업 주기가 너무 길어서. log_reuse_wait_desc 확인</li>
</ul>

<p><br /></p>

<p><br /></p>

<h2 id="10-native-backuprestore-vs-dms--cdc--다시-정리">10. Native Backup/Restore vs DMS + CDC — 다시 정리</h2>

<p>이전 글과 함께 의사결정 표 한 장으로 정리해드릴게요.</p>

<table>
  <thead>
    <tr>
      <th>기준</th>
      <th>Native Backup/Restore</th>
      <th>DMS + CDC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>원본 그대로 보존(인덱스/제약/IDENTITY)</td>
      <td>⭐⭐⭐⭐⭐</td>
      <td>⭐⭐⭐ (수동 보정 필요)</td>
    </tr>
    <tr>
      <td>다운타임</td>
      <td>분~시간 단위</td>
      <td>분 단위 이하</td>
    </tr>
    <tr>
      <td>운영 부담/비용</td>
      <td>낮음 (일회성)</td>
      <td>중간 (Task 가동 시간 비용)</td>
    </tr>
    <tr>
      <td>셋업 난이도</td>
      <td>낮음</td>
      <td>중간~높음</td>
    </tr>
    <tr>
      <td>추천 시나리오</td>
      <td>“그대로 + 짧은 유지보수 창”</td>
      <td>“절대 다운 불가 + 운영 중 이전”</td>
    </tr>
  </tbody>
</table>

<p>저희 사내 케이스 기준으로는</p>

<ul>
  <li><strong>유지보수 창이 1~2시간 허락</strong> → Native Backup/Restore 단독</li>
  <li><strong>유지보수 창이 거의 없음</strong> → Native Backup/Restore 로 풀카피 후 <strong>DMS 의 CDC-only 모드</strong> 로 변경분만 흘려 컷오버</li>
</ul>

<p>두 번째 패턴이 사실상 베스트에요. 풀로드 비용은 백업/복원으로 절약하고, CDC 만 DMS 로 처리해서 latency 안정화 후 컷오버.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 이 두 번째 패턴 — <strong>백업/복원 + CDC-only DMS</strong> 의 결합을 단계별로 풀어볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">(2/5) Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</a> ｜ <strong>다음 글 →:</strong> <a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">(4/5) 변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="mssql" /><category term="aws" /><category term="dms" /><category term="cdc" /><category term="migration" /><category term="rds" /><category term="zero-downtime" /><category term="kayserdocs" /><summary type="html"><![CDATA[AWS DMS + CDC 로 MSSQL 을 다운타임 분 단위 이하로 RDS 에 옮기는 방법. 풀로드 + 변경분 실시간 따라잡기 + 컷오버 흐름을 단계별로 정리했어요.]]></summary></entry><entry><title type="html">(2/5) AWS RDS for SQL Server Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</title><link href="https://dorumugs.github.io/coding/RDS_SQLServer_Native_Backup_Restore_%EC%8B%A4%EC%A0%84/" rel="alternate" type="text/html" title="(2/5) AWS RDS for SQL Server Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지" /><published>2026-05-30T09:09:00+09:00</published><updated>2026-05-30T09:09:00+09:00</updated><id>https://dorumugs.github.io/coding/RDS_SQLServer_Native_Backup_Restore_%EC%8B%A4%EC%A0%84</id><content type="html" xml:base="https://dorumugs.github.io/coding/RDS_SQLServer_Native_Backup_Restore_%EC%8B%A4%EC%A0%84/"><![CDATA[<div class="notice--info">
  <p><strong>📚 MSSQL → AWS RDS 마이그레이션 시리즈 (전체 5편)</strong></p>
  <ol>
    <li><a href="/coding/내부망_MSSQL_AWS_RDS_마이그레이션/">방법 비교 — 6가지 중에서 고르기</a></li>
    <li><strong>Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</a></li>
    <li><a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</a></li>
    <li><a href="/coding/MSSQL_마이그레이션_정합성_트러블슈팅/">정합성 트러블슈팅 — 케이스별 8가지 함정</a></li>
  </ol>
</div>

<p><a href="/coding/내부망_MSSQL_AWS_RDS_마이그레이션/">지난 글</a>에서 내부망 MSSQL → AWS RDS for SQL Server 마이그레이션 방법 6가지를 비교하고, <strong>Native Backup/Restore</strong> 를 1순위로 추천드렸어요. 이번 글은 그 실전편이에요. RDS 옵션 그룹 세팅부터 시작해서, 풀백업 → S3 업로드 → 복원, 마지막으로 컷오버 시점의 <strong>차등 백업(differential)</strong> 으로 다운타임을 짧게 끊는 데까지 한 번에 따라가봅니다.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>RDS 옵션 그룹(<code class="language-plaintext highlighter-rouge">SQLSERVER_BACKUP_RESTORE</code>) + IAM Role + S3 버킷 사전 세팅</li>
    <li>원본에서 <code class="language-plaintext highlighter-rouge">BACKUP DATABASE</code> 로 풀백업 (STRIPE / COMPRESSION / CHECKSUM)</li>
    <li>S3 업로드 시 주의점 (KMS, 멀티파트, 리전 매칭)</li>
    <li>RDS 에서 <code class="language-plaintext highlighter-rouge">rds_restore_database</code> 로 복원하고 <code class="language-plaintext highlighter-rouge">rds_task_status</code> 로 진행 따라가기</li>
    <li>컷오버 시점의 차등 백업 + NORECOVERY 흐름</li>
    <li>복원 후 반드시 챙겨야 하는 로그인/사용자 매핑</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-사전-그림--무엇이-어디에-있어야-하나">1. 사전 그림 — 무엇이 어디에 있어야 하나</h2>

<p>먼저 그림 한 번 그려두면 머리가 정리돼요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[내부망 MSSQL]
    │
    │ (1) BACKUP DATABASE → .bak (stripe N개, 압축)
    ▼
[내부망 파일 서버 / jump 서버]
    │
    │ (2) aws s3 cp  (S2S VPN 또는 인터넷)
    ▼
[S3 버킷 (AWS, 같은 리전)]
    │
    │ (3) EXEC msdb.dbo.rds_restore_database
    ▼
[AWS RDS for SQL Server]
</code></pre></div></div>

<p>세 가지를 사전에 세팅해두면 본 작업은 사실상 명령어 몇 줄이에요.</p>

<ul>
  <li><strong>S3 버킷</strong> — RDS 인스턴스와 <strong>같은 리전</strong>.</li>
  <li><strong>IAM Role</strong> — RDS 가 S3 에 접근할 수 있도록.</li>
  <li><strong>RDS Option Group</strong> — <code class="language-plaintext highlighter-rouge">SQLSERVER_BACKUP_RESTORE</code> 옵션을 붙이고, 위 IAM Role 을 지정.</li>
</ul>

<p>이 셋 중 하나라도 어긋나면 <code class="language-plaintext highlighter-rouge">rds_restore_database</code> 가 곧장 권한 에러를 뱉어요. 순서대로 해두면 한 번에 통과합니다.</p>

<p><br /></p>

<p><br /></p>

<h2 id="2-s3-버킷--iam-role-만들기">2. S3 버킷 + IAM Role 만들기</h2>

<h3 id="2-1-s3-버킷">2-1. S3 버킷</h3>

<p>리전만 잘 맞춰주세요. 그리고 버킷 정책은 처음엔 <strong>굳이 손대지 않아도</strong> 동작해요. IAM Role 권한으로 처리되니까요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 mb s3://my-mssql-migration <span class="nt">--region</span> ap-northeast-2
</code></pre></div></div>

<blockquote>
  <p>⚠️ 인스턴스 리전과 버킷 리전이 다르면 RDS 가 접근 자체를 거부해요. 가장 흔히 깨지는 포인트 1위.</p>
</blockquote>

<h3 id="2-2-iam-role">2-2. IAM Role</h3>

<p>RDS 의 SQL Server 서비스가 S3 에 접근하도록 신뢰관계를 잡아줘야 해요. trust policy 와 권한 정책 둘 다 필요.</p>

<p><strong>Trust policy</strong> (<code class="language-plaintext highlighter-rouge">rds-s3-trust.json</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">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2012-10-17"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Statement"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w">
    </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"Principal"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"Service"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rds.amazonaws.com"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sts:AssumeRole"</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><strong>권한 정책</strong> (<code class="language-plaintext highlighter-rouge">rds-s3-policy.json</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">"Version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2012-10-17"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Statement"</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">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"s3:ListBucket"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"s3:GetBucketLocation"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:s3:::my-mssql-migration"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"Action"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"s3:GetObject"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"s3:PutObject"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"s3:ListMultipartUploadParts"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"s3:AbortMultipartUpload"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"Resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:s3:::my-mssql-migration/*"</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>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws iam create-role <span class="se">\</span>
  <span class="nt">--role-name</span> rds-sqlserver-s3-role <span class="se">\</span>
  <span class="nt">--assume-role-policy-document</span> file://rds-s3-trust.json

aws iam put-role-policy <span class="se">\</span>
  <span class="nt">--role-name</span> rds-sqlserver-s3-role <span class="se">\</span>
  <span class="nt">--policy-name</span> rds-sqlserver-s3-policy <span class="se">\</span>
  <span class="nt">--policy-document</span> file://rds-s3-policy.json
</code></pre></div></div>

<blockquote>
  <p>💡 KMS 로 암호화된 버킷이면 위 정책에 <code class="language-plaintext highlighter-rouge">kms:Decrypt</code>, <code class="language-plaintext highlighter-rouge">kms:GenerateDataKey</code> 권한도 추가해주세요. 이거 빼먹고 한참 헤매는 분 많아요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="3-rds-option-group-만들고-붙이기">3. RDS Option Group 만들고 붙이기</h2>

<p>이제 만든 IAM Role 을 RDS 인스턴스가 쓰도록 옵션 그룹을 만들고 attach 해요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 옵션 그룹 생성 (SQL Server 버전/에디션 맞춰서)</span>
aws rds create-option-group <span class="se">\</span>
  <span class="nt">--option-group-name</span> mssql-backup-restore-og <span class="se">\</span>
  <span class="nt">--engine-name</span> sqlserver-se <span class="se">\</span>
  <span class="nt">--major-engine-version</span> 15.00 <span class="se">\</span>
  <span class="nt">--option-group-description</span> <span class="s2">"Native backup/restore for migration"</span>

<span class="c"># 2. SQLSERVER_BACKUP_RESTORE 옵션 추가 + IAM Role 연결</span>
aws rds add-option-to-option-group <span class="se">\</span>
  <span class="nt">--option-group-name</span> mssql-backup-restore-og <span class="se">\</span>
  <span class="nt">--options</span> <span class="s2">"OptionName=SQLSERVER_BACKUP_RESTORE,OptionSettings=[{Name=IAM_ROLE_ARN,Value=arn:aws:iam::123456789012:role/rds-sqlserver-s3-role}]"</span> <span class="se">\</span>
  <span class="nt">--apply-immediately</span>

<span class="c"># 3. 인스턴스에 옵션 그룹 적용</span>
aws rds modify-db-instance <span class="se">\</span>
  <span class="nt">--db-instance-identifier</span> my-mssql-rds <span class="se">\</span>
  <span class="nt">--option-group-name</span> mssql-backup-restore-og <span class="se">\</span>
  <span class="nt">--apply-immediately</span>
</code></pre></div></div>

<p>옵션 그룹은 <strong>동적(dynamic)</strong> 옵션이라 인스턴스 재부팅 없이도 붙어요. 단, modify-db-instance 가 완전히 끝날 때까지(상태 <code class="language-plaintext highlighter-rouge">available</code>) 기다린 다음에 다음 단계로 가세요.</p>

<p>✅ 검증: SSMS / sqlcmd 로 인스턴스에 붙어서 아래 한 줄 실행해 보세요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_show_configuration</span><span class="p">;</span>
</code></pre></div></div>

<p>옵션이 잘 붙었으면 <code class="language-plaintext highlighter-rouge">S3 ARN access for backup/restore</code> 같은 라인이 보여요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="4-원본에서-풀백업-만들기-stripe--compression">4. 원본에서 풀백업 만들기 (STRIPE + COMPRESSION)</h2>

<p>이제 원본 MSSQL 에서 백업을 떠요. 한 파일로 통째로 떨어뜨리지 말고 <strong>STRIPE 으로 4~8 분할</strong> 하는 걸 추천드려요. 백업/복원 둘 다 병렬화돼서 훨씬 빠릅니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BACKUP</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span>
<span class="k">TO</span> <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_01.bak'</span><span class="p">,</span>
   <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_02.bak'</span><span class="p">,</span>
   <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_03.bak'</span><span class="p">,</span>
   <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_04.bak'</span>
<span class="k">WITH</span> 
  <span class="n">COMPRESSION</span><span class="p">,</span>
  <span class="n">CHECKSUM</span><span class="p">,</span>
  <span class="n">MAXTRANSFERSIZE</span> <span class="o">=</span> <span class="mi">4194304</span><span class="p">,</span>  <span class="c1">-- 4MB</span>
  <span class="n">BUFFERCOUNT</span> <span class="o">=</span> <span class="mi">50</span><span class="p">,</span>
  <span class="n">STATS</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
</code></pre></div></div>

<p>옵션 의미 한 줄씩.</p>

<table>
  <thead>
    <tr>
      <th>옵션</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">STRIPE (DISK 여러 개)</code></td>
      <td>병렬 IO. 보통 4~8 분할이 가성비 좋아요.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">COMPRESSION</code></td>
      <td>.bak 크기 1/4~1/8 로 축소. 전송 시간 단축.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CHECKSUM</code></td>
      <td>백업 시점에 페이지 체크섬 검증 + .bak 자체 체크섬 기록. 손상 사전 감지.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">MAXTRANSFERSIZE = 4MB</code></td>
      <td>IO 단위 크게 잡아서 처리량 ↑</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">BUFFERCOUNT = 50</code></td>
      <td>백업 IO 버퍼 수. 메모리 여유 있으면 늘려도 OK.</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">STATS = 5</code></td>
      <td>진행률 5% 단위로 출력. 모니터링 편함.</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>⚠️ <code class="language-plaintext highlighter-rouge">BUFFERCOUNT</code> 너무 크게 잡으면 백업 세션이 메모리를 많이 먹어요. 운영 DB 라면 50 정도가 무난.</p>
</blockquote>

<p>백업 끝나면 파일 크기와 무결성을 한 번 더 확인.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">RESTORE</span> <span class="n">VERIFYONLY</span> 
<span class="k">FROM</span> <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_01.bak'</span><span class="p">,</span>
     <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_02.bak'</span><span class="p">,</span>
     <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_03.bak'</span><span class="p">,</span>
     <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full_04.bak'</span>
<span class="k">WITH</span> <span class="n">CHECKSUM</span><span class="p">;</span>
</code></pre></div></div>

<p>🎉 <code class="language-plaintext highlighter-rouge">The backup set on file 1 is valid.</code> 가 떨어지면 OK.</p>

<p><br /></p>

<p><br /></p>

<h2 id="5-s3-로-업로드">5. S3 로 업로드</h2>

<p>내부망 → S3 업로드는 S2S VPN 으로 가도 되고, 인터넷 게이트웨이로 가도 돼요. <strong>VPC endpoint(S3 Gateway Endpoint)</strong> 가 있으면 RDS 가 복원할 때도 더 안정적이고 빨라요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 <span class="nb">cp </span>D:<span class="se">\b</span>ackup<span class="se">\ </span>s3://my-mssql-migration/mssql/ <span class="se">\</span>
  <span class="nt">--recursive</span> <span class="se">\</span>
  <span class="nt">--include</span> <span class="s2">"MyDB_full_*.bak"</span> <span class="se">\</span>
  <span class="nt">--expected-size</span> 50000000000  <span class="c"># 대용량이면 명시</span>
</code></pre></div></div>

<p>큰 파일은 CLI 가 자동으로 멀티파트 업로드로 끊어 올려요. 멀티파트 기본 chunk 가 작으면 업로드 객체 수가 너무 많아져서 느려질 수 있는데, 이때는 chunk 를 키워주세요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws configure <span class="nb">set </span>default.s3.multipart_chunksize 64MB
aws configure <span class="nb">set </span>default.s3.max_concurrent_requests 16
</code></pre></div></div>

<p>업로드 끝나면 객체 리스트 한 번 확인.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws s3 <span class="nb">ls </span>s3://my-mssql-migration/mssql/
</code></pre></div></div>

<blockquote>
  <p>💡 업로드 중간에 끊겨도 멀티파트는 <strong>이어 올라가요</strong>. <code class="language-plaintext highlighter-rouge">aws s3 cp</code> 를 다시 실행하면 이미 업로드된 파일은 skip 됩니다 (<code class="language-plaintext highlighter-rouge">--no-progress</code> 떼고 보면 보임).</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="6-rds-에서-복원--rds_restore_database">6. RDS 에서 복원 — <code class="language-plaintext highlighter-rouge">rds_restore_database</code></h2>

<p>이제 본 게임. RDS 의 <code class="language-plaintext highlighter-rouge">msdb.dbo.rds_restore_database</code> 저장 프로시저를 호출해요. STRIPE 백업은 콤마로 ARN 을 이어 붙여요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_restore_database</span>
  <span class="o">@</span><span class="n">restore_db_name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">s3_arn_to_restore_from</span> <span class="o">=</span> 
    <span class="s1">'arn:aws:s3:::my-mssql-migration/mssql/MyDB_full_01.bak,
     arn:aws:s3:::my-mssql-migration/mssql/MyDB_full_02.bak,
     arn:aws:s3:::my-mssql-migration/mssql/MyDB_full_03.bak,
     arn:aws:s3:::my-mssql-migration/mssql/MyDB_full_04.bak'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">with_norecovery</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>  <span class="c1">-- 차등 백업을 뒤이어 적용할 거라면 1</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@with_norecovery = 1</code> 로 두면 DB 가 <strong>RESTORING</strong> 상태로 남아서, 이어서 차등 백업을 추가 복원할 수 있어요. 마지막 복원에만 <code class="language-plaintext highlighter-rouge">0</code> 으로 두면 DB 가 ONLINE 으로 올라옵니다.</p>

<p>복원은 비동기로 큐에 들어가요. 진행 상황은 따로 조회.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_task_status</span> <span class="o">@</span><span class="n">db_name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">;</span>
</code></pre></div></div>

<p>진행 단계는 <code class="language-plaintext highlighter-rouge">lifecycle</code> 컬럼으로 봐요.</p>

<table>
  <thead>
    <tr>
      <th>lifecycle</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CREATED</code></td>
      <td>큐에 등록됨</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IN_PROGRESS</code></td>
      <td>복원 중</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SUCCESS</code></td>
      <td>성공</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ERROR</code></td>
      <td>실패 (에러 메시지는 <code class="language-plaintext highlighter-rouge">task_info</code> 컬럼)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">CANCEL_REQUESTED</code> / <code class="language-plaintext highlighter-rouge">CANCELLED</code></td>
      <td>취소 요청/완료</td>
    </tr>
  </tbody>
</table>

<p>✅ <code class="language-plaintext highlighter-rouge">SUCCESS</code> 가 뜨면 풀백업 복원 끝.</p>

<p><br /></p>

<p><br /></p>

<h2 id="7-컷오버--차등-백업으로-다운타임-줄이기">7. 컷오버 — 차등 백업으로 다운타임 줄이기</h2>

<p>풀백업이 큰 DB 라면, <strong>풀백업을 먼저 옮겨놓고</strong> + <strong>컷오버 직전에 차등 백업만 다시 옮기는</strong> 방식으로 다운타임을 분 단위로 줄일 수 있어요.</p>

<h3 id="7-1-원본에서-차등-백업">7-1. 원본에서 차등 백업</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">BACKUP</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span>
<span class="k">TO</span> <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_diff_01.bak'</span><span class="p">,</span>
   <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_diff_02.bak'</span>
<span class="k">WITH</span> 
  <span class="n">DIFFERENTIAL</span><span class="p">,</span>
  <span class="n">COMPRESSION</span><span class="p">,</span>
  <span class="n">CHECKSUM</span><span class="p">,</span>
  <span class="n">STATS</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
</code></pre></div></div>

<p>차등 백업은 풀백업 이후 변경된 페이지만 떠요. 보통 풀의 1~10% 크기로 끝납니다.</p>

<h3 id="7-2-s3-업로드-후-rds-에-차등-복원">7-2. S3 업로드 후 RDS 에 차등 복원</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_restore_database</span>
  <span class="o">@</span><span class="n">restore_db_name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">s3_arn_to_restore_from</span> <span class="o">=</span> 
    <span class="s1">'arn:aws:s3:::my-mssql-migration/mssql/MyDB_diff_01.bak,
     arn:aws:s3:::my-mssql-migration/mssql/MyDB_diff_02.bak'</span><span class="p">,</span>
  <span class="o">@</span><span class="k">type</span> <span class="o">=</span> <span class="s1">'DIFFERENTIAL'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">with_norecovery</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>  <span class="c1">-- 마지막 복원이므로 ONLINE 으로 올림</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@type = 'DIFFERENTIAL'</code> 가 핵심. <code class="language-plaintext highlighter-rouge">@with_norecovery = 0</code> 으로 끝내면 DB 가 <code class="language-plaintext highlighter-rouge">ONLINE</code> 으로 올라와요.</p>

<blockquote>
  <p>💡 RDS for SQL Server 의 차등 복원은 비교적 최근에 추가된 기능이에요. 인스턴스의 <strong>엔진 버전</strong> 이 차등 복원을 지원하는지 사전 확인하세요. 안 되면 풀백업만 반복하거나 컷오버 시간을 길게 잡아야 해요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="8-복원-후-반드시-챙길-것--로그인--사용자-매핑">8. 복원 후 반드시 챙길 것 — 로그인 / 사용자 매핑</h2>

<p><code class="language-plaintext highlighter-rouge">.bak</code> 으로 복원하면 <strong>DB 사용자(users)는 들어오지만</strong>, 서버 레벨의 <strong>로그인(logins)</strong> 은 안 들어와요. 원본 서버에 있던 로그인을 RDS 마스터에 똑같이 만들고, DB 사용자와 다시 매핑해줘야 해요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 1. RDS 에 로그인 생성 (원본에 있던 SQL 로그인 그대로)</span>
<span class="k">CREATE</span> <span class="n">LOGIN</span> <span class="p">[</span><span class="n">app_user</span><span class="p">]</span> <span class="k">WITH</span> <span class="n">PASSWORD</span> <span class="o">=</span> <span class="s1">'&lt;APP_USER_PASSWORD&gt;'</span><span class="p">;</span>

<span class="c1">-- 2. 복원된 DB 의 user 를 새 로그인에 다시 묶기 (orphaned user 정리)</span>
<span class="n">USE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">];</span>
<span class="k">EXEC</span> <span class="n">sp_change_users_login</span> <span class="s1">'Update_One'</span><span class="p">,</span> <span class="s1">'app_user'</span><span class="p">,</span> <span class="s1">'app_user'</span><span class="p">;</span>

<span class="c1">-- 또는 ALTER USER ... WITH LOGIN = ...</span>
<span class="k">ALTER</span> <span class="k">USER</span> <span class="p">[</span><span class="n">app_user</span><span class="p">]</span> <span class="k">WITH</span> <span class="n">LOGIN</span> <span class="o">=</span> <span class="p">[</span><span class="n">app_user</span><span class="p">];</span>
</code></pre></div></div>

<p>✅ 검증: <code class="language-plaintext highlighter-rouge">EXEC sp_change_users_login 'Report';</code> 로 고아 사용자가 남아있는지 한 번 더 확인.</p>

<blockquote>
  <p>⚠️ Windows 인증 기반 로그인은 RDS 가 Active Directory 연동(<code class="language-plaintext highlighter-rouge">SQLSERVER_AUDIT</code> / <code class="language-plaintext highlighter-rouge">SQLSERVER_AD</code> 옵션)되어 있어야 매핑 가능해요. 단순 SQL 로그인이면 위 절차로 충분.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="9-데이터-일관성-검증--한-번-더">9. 데이터 일관성 검증 — 한 번 더</h2>

<p>복원이 끝나면 마지막으로 데이터 검증 한 사이클 돌려요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 무결성 검사</span>
<span class="n">DBCC</span> <span class="n">CHECKDB</span> <span class="p">([</span><span class="n">MyDB</span><span class="p">])</span> <span class="k">WITH</span> <span class="n">NO_INFOMSGS</span><span class="p">;</span>

<span class="c1">-- 테이블별 row count 스냅샷 비교용</span>
<span class="k">SELECT</span> <span class="n">s</span><span class="p">.</span><span class="n">name</span> <span class="o">+</span> <span class="s1">'.'</span> <span class="o">+</span> <span class="n">t</span><span class="p">.</span><span class="n">name</span> <span class="k">AS</span> <span class="k">table_name</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="k">rows</span>
<span class="k">FROM</span> <span class="n">sys</span><span class="p">.</span><span class="n">tables</span> <span class="n">t</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">schemas</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">schema_id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">schema_id</span>
<span class="k">JOIN</span> <span class="n">sys</span><span class="p">.</span><span class="n">partitions</span> <span class="n">p</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">object_id</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">object_id</span>
<span class="k">WHERE</span> <span class="n">p</span><span class="p">.</span><span class="n">index_id</span> <span class="k">IN</span> <span class="p">(</span><span class="mi">0</span><span class="p">,</span><span class="mi">1</span><span class="p">)</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">p</span><span class="p">.</span><span class="k">rows</span> <span class="k">DESC</span><span class="p">;</span>
</code></pre></div></div>

<p>같은 쿼리를 원본 / 대상 양쪽에서 돌려서 결과를 비교해요. 큰 테이블 몇 개만이라도 row count 와 <code class="language-plaintext highlighter-rouge">MAX(updated_at)</code>, <code class="language-plaintext highlighter-rouge">CHECKSUM_AGG()</code> 정도는 맞춰보고 컷오버 결정.</p>

<p><br /></p>

<p><br /></p>

<h2 id="10-자주-깨지는-포인트--체크리스트">10. 자주 깨지는 포인트 — 체크리스트</h2>

<p>이번 작업하면서 한 번씩 다 밟아봤던 함정들이에요. 미리 알면 안 밟습니다.</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />S3 버킷 리전과 RDS 인스턴스 리전 <strong>일치</strong> 했는가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />IAM Role 의 trust principal 이 <code class="language-plaintext highlighter-rouge">rds.amazonaws.com</code> 인가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />KMS 암호화 버킷이면 IAM Role 에 <code class="language-plaintext highlighter-rouge">kms:Decrypt</code> 권한 있는가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />옵션 그룹의 <code class="language-plaintext highlighter-rouge">IAM_ROLE_ARN</code> 값이 위 Role 의 ARN 과 정확히 일치하는가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />STRIPE 백업의 모든 파일이 S3 에 다 올라갔는가 (하나라도 빠지면 복원 실패)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><code class="language-plaintext highlighter-rouge">rds_task_status</code> 의 <code class="language-plaintext highlighter-rouge">lifecycle</code> 이 <code class="language-plaintext highlighter-rouge">SUCCESS</code> 인지 확인했는가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />차등 복원 전에 풀 복원이 <code class="language-plaintext highlighter-rouge">NORECOVERY</code> 로 끝났는가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />복원 후 로그인 재생성 + 고아 사용자 매핑까지 했는가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><code class="language-plaintext highlighter-rouge">DBCC CHECKDB</code> 클린인가</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />원본/대상 row count 스냅샷 비교 완료했는가</li>
</ul>

<p><br /></p>

<p><br /></p>

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

<p>Native Backup/Restore 는 단계가 많아 보이지만, 한 번 셋업해두면 명령어 몇 줄로 끝나는 깔끔한 흐름이에요. 핵심만 다시 짚으면</p>

<ul>
  <li>옵션 그룹 + IAM Role + S3 버킷 → <strong>한 번만</strong> 세팅</li>
  <li>풀백업은 <strong>STRIPE + COMPRESSION + CHECKSUM</strong> 으로</li>
  <li>컷오버는 <strong>NORECOVERY 풀백업 → 차등 백업</strong> 패턴으로 다운타임 단축</li>
  <li>복원 후 <strong>로그인 재생성 + DBCC + row count 비교</strong> 까지 해야 끝난 거예요</li>
</ul>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 같은 마이그레이션을 <strong>AWS DMS + CDC</strong> 로 풀어서, “다운타임 거의 0” 컷오버를 어떻게 구성하는지 정리해볼게요.</p>

<hr />

<p><strong>← 이전 글:</strong> <a href="/coding/내부망_MSSQL_AWS_RDS_마이그레이션/">(1/5) 방법 비교 — 6가지 중에서 고르기</a> ｜ <strong>다음 글 →:</strong> <a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">(3/5) DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="mssql" /><category term="aws" /><category term="rds" /><category term="backup" /><category term="restore" /><category term="s3" /><category term="migration" /><category term="native-backup" /><category term="kayserdocs" /><summary type="html"><![CDATA[AWS RDS for SQL Server 의 Native Backup/Restore 를 옵션 그룹 세팅부터 S3 업로드, 복원, 컷오버 시 차등 백업까지 한 번에 따라가는 실전 가이드입니다.]]></summary></entry><entry><title type="html">(1/5) 내부망 MSSQL → AWS RDS for SQL Server 옮기기 — 부하 안 주고 그대로 넣는 법</title><link href="https://dorumugs.github.io/coding/%EB%82%B4%EB%B6%80%EB%A7%9D_MSSQL_AWS_RDS_%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98/" rel="alternate" type="text/html" title="(1/5) 내부망 MSSQL → AWS RDS for SQL Server 옮기기 — 부하 안 주고 그대로 넣는 법" /><published>2026-05-30T08:30:00+09:00</published><updated>2026-05-30T08:30:00+09:00</updated><id>https://dorumugs.github.io/coding/%EB%82%B4%EB%B6%80%EB%A7%9D_MSSQL_AWS_RDS_%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</id><content type="html" xml:base="https://dorumugs.github.io/coding/%EB%82%B4%EB%B6%80%EB%A7%9D_MSSQL_AWS_RDS_%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98/"><![CDATA[<div class="notice--info">
  <p><strong>📚 MSSQL → AWS RDS 마이그레이션 시리즈 (전체 5편)</strong></p>
  <ol>
    <li><strong>방법 비교 — 6가지 중에서 고르기</strong> ← <em>지금 글</em></li>
    <li><a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</a></li>
    <li><a href="/coding/MSSQL_AWS_DMS_CDC_무중단_컷오버/">DMS + CDC 무중단 컷오버 — 풀로드 후 변경분 따라잡기</a></li>
    <li><a href="/coding/RDS_SQLServer_복원_이후_변경분_계속_쌓기/">변경분 계속 쌓기 — NORECOVERY 체인과 CDC-only 결합</a></li>
    <li><a href="/coding/MSSQL_마이그레이션_정합성_트러블슈팅/">정합성 트러블슈팅 — 케이스별 8가지 함정</a></li>
  </ol>
</div>

<p>내부망에 있던 MSSQL DB 한 덩어리를 <strong>AWS RDS for SQL Server</strong> 로 그대로 옮겨야 했어요. 다행히 양쪽은 <strong>S2S VPN</strong> 으로 이미 통신이 되는 상태였고, 조건은 두 가지였어요. 첫째 <strong>원본을 그대로 옮길 것</strong>(스키마/식별자/인덱스 보존), 둘째 <strong>운영 중인 원본 DB에 부하를 주지 말 것</strong>. 이 글은 그때 검토했던 방법들과 최종 선택, 그리고 건수별 소요 시간 추정을 정리한 글이에요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>내부망 → AWS RDS for SQL Server 마이그레이션 방법 6가지 비교</li>
    <li>“부하 방지” 관점에서 각 방법이 원본에 주는 영향</li>
    <li>1만 건 / 100만 건 / 1억 건 기준 예상 소요 시간</li>
    <li>최종 추천: 어떤 상황엔 무엇을 골라야 하는가</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-전제--우리-환경-정리">1. 전제 — 우리 환경 정리</h2>

<p>먼저 작업 시작 전에 짚어둔 전제예요.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>원본</td>
      <td>내부망 MSSQL (온프레미스, SQL Server 2019 기준)</td>
    </tr>
    <tr>
      <td>대상</td>
      <td>AWS RDS for SQL Server (같은 메이저 버전 또는 상위)</td>
    </tr>
    <tr>
      <td>네트워크</td>
      <td>S2S VPN 으로 양방향 통신 가능 (대역폭은 보통 1Gbps 미만)</td>
    </tr>
    <tr>
      <td>목표</td>
      <td>원본 그대로 복제 (one-shot 풀 카피)</td>
    </tr>
    <tr>
      <td>제약</td>
      <td>원본 DB 부하 최소화, 서비스는 운영 중</td>
    </tr>
  </tbody>
</table>

<p>여기서 가장 중요한 두 가지는 <strong>“그대로”</strong> 와 <strong>“부하 없이”</strong> 예요. 이 두 단어가 방법 선택의 거의 모든 기준이 돼요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="2-옵션-정리--6가지-방법">2. 옵션 정리 — 6가지 방법</h2>

<p>먼저 후보를 다 펼쳐놓고 시작할게요. 표 한 번 보고 본문으로 들어가요.</p>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>방법</th>
      <th>그대로 보존</th>
      <th>원본 부하</th>
      <th>네트워크 부담</th>
      <th>작업 난이도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>Native Backup / Restore (.bak + S3)</td>
      <td>⭐⭐⭐⭐⭐</td>
      <td>⭐ (낮음)</td>
      <td>한 번에 큰 파일</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>2</td>
      <td>AWS DMS (Full Load)</td>
      <td>⭐⭐⭐</td>
      <td>⭐⭐</td>
      <td>균등 분산</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>3</td>
      <td>bcp (Bulk Copy)</td>
      <td>⭐⭐⭐⭐</td>
      <td>⭐⭐</td>
      <td>테이블 단위</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>4</td>
      <td>SSIS (Integration Services)</td>
      <td>⭐⭐⭐⭐</td>
      <td>⭐⭐⭐</td>
      <td>균등 분산</td>
      <td>중간~높음</td>
    </tr>
    <tr>
      <td>5</td>
      <td>SqlPackage (BACPAC)</td>
      <td>⭐⭐⭐⭐</td>
      <td>⭐⭐</td>
      <td>한 번에 큰 파일</td>
      <td>낮음</td>
    </tr>
    <tr>
      <td>6</td>
      <td>Linked Server <code class="language-plaintext highlighter-rouge">INSERT … SELECT</code></td>
      <td>⭐⭐</td>
      <td>⭐⭐⭐⭐⭐ (높음)</td>
      <td>가장 비효율</td>
      <td>가장 낮음</td>
    </tr>
  </tbody>
</table>

<p>⚠️ <strong>표의 별 개수 의미</strong> — “보존” 은 많을수록 좋음(원본 그대로), “부하” 와 “네트워크 부담” 은 많을수록 나쁨(원본/네트워크에 부담). 별 1개가 좋은 것일 수도, 나쁜 것일 수도 있어요. 헷갈리지 않게 ⭐ 옆에 (낮음)/(높음) 으로도 표시했어요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="3-방법-1--native-backuprestore-bak--s3--rds">3. 방법 1 — Native Backup/Restore (.bak → S3 → RDS)</h2>

<p><strong>MSSQL 의 정공법</strong>. 원본에서 <code class="language-plaintext highlighter-rouge">BACKUP DATABASE</code> 로 <code class="language-plaintext highlighter-rouge">.bak</code> 파일을 만들고, S3 에 올린 다음, RDS 의 저장 프로시저 <code class="language-plaintext highlighter-rouge">msdb.dbo.rds_restore_database</code> 로 복원하는 방법이에요.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 원본 (내부망 MSSQL)</span>
<span class="n">BACKUP</span> <span class="k">DATABASE</span> <span class="p">[</span><span class="n">MyDB</span><span class="p">]</span>
<span class="k">TO</span> <span class="n">DISK</span> <span class="o">=</span> <span class="n">N</span><span class="s1">'D:</span><span class="se">\b</span><span class="s1">ackup</span><span class="se">\M</span><span class="s1">yDB_full.bak'</span>
<span class="k">WITH</span> <span class="n">COMPRESSION</span><span class="p">,</span> <span class="n">CHECKSUM</span><span class="p">,</span> <span class="n">STATS</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span>
</code></pre></div></div>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># S3 업로드 (내부망 jump 서버에서 → S2S VPN 통해 S3 endpoint)</span>
aws s3 <span class="nb">cp </span>D:<span class="se">\b</span>ackup<span class="se">\M</span>yDB_full.bak s3://my-mssql-migration/MyDB_full.bak
</code></pre></div></div>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- AWS RDS for SQL Server 에서 실행</span>
<span class="k">EXEC</span> <span class="n">msdb</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">rds_restore_database</span>
  <span class="o">@</span><span class="n">restore_db_name</span> <span class="o">=</span> <span class="s1">'MyDB'</span><span class="p">,</span>
  <span class="o">@</span><span class="n">s3_arn_to_restore_from</span> <span class="o">=</span> <span class="s1">'arn:aws:s3:::my-mssql-migration/MyDB_full.bak'</span><span class="p">;</span>
</code></pre></div></div>

<p>진행상황은 <code class="language-plaintext highlighter-rouge">EXEC msdb.dbo.rds_task_status @db_name='MyDB';</code> 로 따라갈 수 있어요.</p>

<p><strong>장점</strong></p>

<ul>
  <li>스키마/인덱스/제약/식별자 시드/CLR 어셈블리 전부 <strong>그대로</strong> 보존돼요. “그대로 넣는다” 의 끝판왕.</li>
  <li>원본 부하는 <strong>풀백업 시점의 디스크 I/O 만</strong>. row-by-row 쿼리가 없으니 운영 쿼리에 영향이 작아요.</li>
  <li>큰 DB 도 압축 백업이면 보통 1/4~1/8 로 줄어들어 전송도 빨라요.</li>
</ul>

<p><strong>단점</strong></p>

<ul>
  <li>RDS 가 <strong>Native Backup/Restore 옵션 그룹</strong> 활성화 + S3 권한이 사전에 세팅되어 있어야 해요.</li>
  <li>AWS RDS for SQL Server 에서 <strong><code class="language-plaintext highlighter-rouge">differential</code> 복원은 일정 버전 이상부터</strong> 지원돼요. 풀백업 + 차등은 미리 RDS 버전 확인.</li>
  <li>한 파일이 큼 → 전송 중 끊기면 처음부터 다시 (멀티파트 업로드로 어느 정도 보완 가능).</li>
</ul>

<blockquote>
  <p>✅ “그대로” 가 가장 잘 지켜지는 방법. one-shot 마이그레이션이라면 1순위 후보.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="4-방법-2--aws-dms-database-migration-service">4. 방법 2 — AWS DMS (Database Migration Service)</h2>

<p>AWS 가 만든 <strong>마이그레이션 전용 매니지드 서비스</strong>. Replication Instance 를 띄우고, 소스(내부망 MSSQL) / 타깃(RDS for SQL Server) 엔드포인트를 등록한 뒤, <strong>Full Load</strong> 또는 <strong>Full Load + CDC</strong> 로 돌려요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[내부망 MSSQL] --(S2S VPN)--&gt; [DMS Replication Instance] --&gt; [AWS RDS for SQL Server]
</code></pre></div></div>

<p><strong>장점</strong></p>

<ul>
  <li>테이블/스키마 단위로 <strong>병렬 로드</strong> 가능. 큰 DB 를 시간 안에 끝내고 싶을 때 좋아요.</li>
  <li><strong>CDC(Change Data Capture)</strong> 를 켜면 풀 로드 후 변경분만 따라잡아 줘서 <strong>다운타임 거의 0</strong> 컷오버 가능.</li>
  <li>진행률/에러/지표가 콘솔에 다 보임 → 운영하기 편함.</li>
</ul>

<p><strong>단점</strong></p>

<ul>
  <li>보존 측면에서 <strong>인덱스/식별자 시드/제약을 별도 챙겨야</strong> 해요. DMS 는 데이터 위주로 옮기고, 보조 인덱스는 옵션으로 따로 만들거나 컷오버 후 생성하는 게 일반적이에요. “그대로” 라는 관점에선 백업/복원만 못해요.</li>
  <li>풀로드 도중 원본에 <strong>장기 SELECT</strong> 가 걸려요. 트랜잭션 격리 수준/락 옵션을 잘못 두면 오히려 원본에 부담.</li>
  <li>DMS Replication Instance 비용이 따로 들어요.</li>
</ul>

<blockquote>
  <p>💡 “운영 중인 DB 를 무중단에 가깝게 옮기고 싶다” 가 강한 요건이면 DMS + CDC 가 최선.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="5-방법-3--bcp-bulk-copy-program">5. 방법 3 — bcp (Bulk Copy Program)</h2>

<p>MSSQL 의 <strong>고전 명령어</strong>. 테이블 단위로 원본에서 파일로 추출(<code class="language-plaintext highlighter-rouge">bcp out</code>)하고, 대상에 일괄 적재(<code class="language-plaintext highlighter-rouge">bcp in</code> 또는 <code class="language-plaintext highlighter-rouge">BULK INSERT</code>)해요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 원본에서 추출 (네이티브 포맷이 가장 빠르고 안전)</span>
bcp MyDB.dbo.Orders out Orders.dat ^
  <span class="nt">-S</span> 192.168.x.x <span class="nt">-U</span> sa <span class="nt">-P</span> &lt;PASSWORD&gt; <span class="nt">-n</span> <span class="nt">-b</span> 10000

<span class="c"># AWS RDS 로 적재</span>
bcp MyDB.dbo.Orders <span class="k">in </span>Orders.dat ^
  <span class="nt">-S</span> mydb.xxxx.ap-northeast-2.rds.amazonaws.com <span class="nt">-U</span> admin <span class="nt">-P</span> &lt;PASSWORD&gt; <span class="nt">-n</span> <span class="nt">-b</span> 10000 <span class="nt">-h</span> <span class="s2">"TABLOCK"</span>
</code></pre></div></div>

<p>옵션 의미는 짧게.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-n</code> : 네이티브(바이너리) 포맷. 파싱 비용이 적어서 빠르고 타입 오차도 없음.</li>
  <li><code class="language-plaintext highlighter-rouge">-b 10000</code> : 배치 사이즈. 트랜잭션을 잘게 끊어서 로그 폭주를 막아요.</li>
  <li><code class="language-plaintext highlighter-rouge">-h "TABLOCK"</code> : 대상 테이블에 락을 잡고 적재 → minimally logged operation 으로 빨라져요.</li>
</ul>

<p><strong>장점</strong></p>

<ul>
  <li><strong>테이블별로 골라서</strong> 옮길 수 있음. “이 테이블만 빨리” 가 가능.</li>
  <li>배치 사이즈 / 병렬 처리(여러 bcp 동시 실행) 로 <strong>부하 조절</strong> 이 비교적 쉬워요.</li>
  <li>AWS 서비스 의존성 없음. 사내 jump 서버에서 그냥 돌리면 끝.</li>
</ul>

<p><strong>단점</strong></p>

<ul>
  <li>풀 DB 옮기려면 테이블/제약/인덱스 스크립트는 별도로 준비해야 함.</li>
  <li>외래키/식별자 시드/순서 의존성을 직접 관리해야 함 (적재 순서, IDENTITY_INSERT 등).</li>
</ul>

<blockquote>
  <p>⚠️ bcp 에서 부하 방지의 핵심은 <strong><code class="language-plaintext highlighter-rouge">-b</code> 배치 사이즈</strong> 와 <strong>동시 실행 수</strong> 둘이에요. 무작정 <code class="language-plaintext highlighter-rouge">-b 0</code> 으로 한 트랜잭션에 다 밀어넣지 마세요. 트랜잭션 로그가 폭주합니다.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="6-방법-4--ssis--방법-5--sqlpackagebacpac">6. 방법 4 — SSIS / 방법 5 — SqlPackage(BACPAC)</h2>

<p>두 개는 묶어서 짧게.</p>

<p><strong>SSIS (SQL Server Integration Services)</strong></p>

<ul>
  <li>ETL GUI 가 익숙하다면 좋아요. 패키지 한 번 만들어두면 재실행도 편함.</li>
  <li>내부적으로는 결국 bulk insert + 데이터 흐름이라 속도는 bcp 와 비슷한 급.</li>
  <li>단점: 패키지를 어디서 돌릴지(SSIS 카탈로그 서버) 가 필요. 일회성 마이그레이션엔 좀 무거워요.</li>
</ul>

<p><strong>SqlPackage (BACPAC / DACPAC)</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">SqlPackage.exe /Action:Export ...</code> → <code class="language-plaintext highlighter-rouge">.bacpac</code> 파일 생성 → S3 업로드 → 대상에 Import.</li>
  <li>스키마 + 데이터를 한 덩어리로 들고 다니기에 좋아요. 작은 DB 에 잘 맞음.</li>
  <li>큰 DB(수십 GB 이상)는 내보내기/들여오기 둘 다 느려서 <code class="language-plaintext highlighter-rouge">.bak</code> 보다 불리해요.</li>
</ul>

<p>이 둘은 <strong>원본 부하</strong> 관점에선 bcp 와 비슷하거나 살짝 더 무거워요(스키마 메타데이터 추출이 추가됨).</p>

<p><br /></p>

<p><br /></p>

<h2 id="7-방법-6--linked-server-insert--select-비추">7. 방법 6 — Linked Server <code class="language-plaintext highlighter-rouge">INSERT ... SELECT</code> (비추)</h2>

<p>가장 쉬워 보이지만, 실제론 <strong>운영 환경에서 거의 안 쓰는 방법</strong>.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 대상(RDS) 에서 원본을 Linked Server 로 등록 후</span>
<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">MyDB</span><span class="p">.</span><span class="n">dbo</span><span class="p">.</span><span class="n">Orders</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="p">[</span><span class="n">SRC_LINK</span><span class="p">].[</span><span class="n">MyDB</span><span class="p">].[</span><span class="n">dbo</span><span class="p">].[</span><span class="n">Orders</span><span class="p">];</span>
</code></pre></div></div>

<p><strong>왜 비추인가</strong></p>

<ul>
  <li>원본에서 <strong>한 줄 한 줄 네트워크로 끌어옴</strong>. S2S VPN 왕복 지연이 매 row 마다 발생해요.</li>
  <li>원본 트랜잭션 로그/락 부담이 큼. 부하 방지가 요건인데 정반대로 가요.</li>
  <li>도중에 끊기면 일관성 회복도 까다로움.</li>
</ul>

<p>🚨 이 방법은 <strong>테스트용 작은 테이블</strong> 외엔 쓰지 마세요. 100만 건만 돼도 시간이 끝없이 늘어집니다.</p>

<p><br /></p>

<p><br /></p>

<h2 id="8-추천--우리-케이스에서의-베스트">8. 추천 — 우리 케이스에서의 베스트</h2>

<p>요건이 “그대로” + “부하 최소” + “S2S VPN 있음” 이라면 결론은 명확해요.</p>

<p><strong>🎉 1순위 — Native Backup/Restore (.bak → S3 → RDS)</strong></p>

<p>이유.</p>

<ul>
  <li>원본에서 발생하는 부하는 <strong>백업 작업 한 번뿐</strong>. 그것도 디스크 I/O 라 운영 쿼리(메모리/CPU 위주)와 겹치는 영역이 작아요.</li>
  <li>풀백업 시점에 원본을 <strong>점적(point-in-time)</strong> 으로 떠오므로 데이터 일관성 보장.</li>
  <li>인덱스/제약/IDENTITY/통계까지 <strong>그대로</strong> 복원. 사용자 요건 그대로.</li>
</ul>

<p><strong>🎉 2순위 — DMS Full Load + CDC (다운타임 거의 0 이 필요한 경우)</strong></p>

<ul>
  <li>컷오버 시간을 분 단위로 줄여야 한다면 DMS.</li>
  <li>단, “그대로” 가 아니라 “내용은 같지만 인덱스 등은 새로 만들어도 됨” 이 허용돼야 가성비가 좋아요.</li>
</ul>

<p><strong>🎉 3순위 — bcp (특정 테이블만 빠르게)</strong></p>

<ul>
  <li>“이 테이블 하나만 옮기면 돼요” 같은 부분 마이그레이션에 최적.</li>
</ul>

<blockquote>
  <p>✅ 사내 케이스에서 저는 1순위(Native Backup/Restore) 로 풀카피 → 컷오버 시점에 차등 백업 한 번 더 로 정리하는 패턴을 가장 선호해요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="9-소요-시간-추정--1만-건--100만-건--1억-건">9. 소요 시간 추정 — 1만 건 / 100만 건 / 1억 건</h2>

<p>이 부분은 <strong>환경마다 차이가 큰</strong> 영역이에요. 그래도 기준선이 있어야 계획이 가능하니, <strong>평균 행 크기 1KB, 인덱스 보통 수준, S2S VPN 실효 200~500Mbps</strong> 가정으로 정리할게요.</p>

<h3 id="9-1-방법별-처리량throughput-감">9-1. 방법별 처리량(throughput) 감</h3>

<table>
  <thead>
    <tr>
      <th>방법</th>
      <th>평균 처리량 (1KB 행 기준)</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Native Backup/Restore</td>
      <td><strong>네트워크 대역폭에 수렴</strong></td>
      <td>row 단위 처리량 개념이 아님</td>
    </tr>
    <tr>
      <td>AWS DMS (Full Load, 단일 task)</td>
      <td>~5,000 ~ 20,000 rows/sec</td>
      <td>병렬 task 로 N배 확장</td>
    </tr>
    <tr>
      <td>bcp (네이티브 + TABLOCK + 적절한 배치)</td>
      <td>~20,000 ~ 100,000 rows/sec</td>
      <td>LAN 이면 더 빨라짐</td>
    </tr>
    <tr>
      <td>SSIS</td>
      <td>~10,000 ~ 50,000 rows/sec</td>
      <td>패키지 튜닝 의존</td>
    </tr>
    <tr>
      <td>BACPAC (Import)</td>
      <td>~3,000 ~ 15,000 rows/sec</td>
      <td>인덱스 재생성 비용 큼</td>
    </tr>
    <tr>
      <td>Linked Server</td>
      <td><strong>~100 ~ 1,000 rows/sec</strong></td>
      <td>WAN 왕복 지연으로 매우 느림</td>
    </tr>
  </tbody>
</table>

<h3 id="9-2-건수별-예상-시간">9-2. 건수별 예상 시간</h3>

<p>⚠️ 아래 숫자는 <strong>단일 task / 단일 connection</strong> 기준 추정치예요. 실제로는 병렬화로 단축돼요.</p>

<table>
  <thead>
    <tr>
      <th>방법</th>
      <th>1만 건</th>
      <th>100만 건</th>
      <th>1억 건</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Native Backup/Restore</td>
      <td>의미 없음 (한 번에 처리)</td>
      <td>약 1~3분 (.bak 1~5GB)</td>
      <td>약 30분~2시간 (.bak 100~500GB, 압축 시)</td>
    </tr>
    <tr>
      <td>AWS DMS (단일)</td>
      <td>~1~3초</td>
      <td><strong>~1~3분</strong></td>
      <td>~3~10시간 (병렬 시 1/N)</td>
    </tr>
    <tr>
      <td>bcp (튜닝 후)</td>
      <td>&lt;1초</td>
      <td><strong>~10~50초</strong></td>
      <td>~30분~2시간</td>
    </tr>
    <tr>
      <td>SSIS</td>
      <td>~1초</td>
      <td>~30초~2분</td>
      <td>~1~3시간</td>
    </tr>
    <tr>
      <td>BACPAC Import</td>
      <td>~2~5초</td>
      <td>~1~5분</td>
      <td>~3~10시간</td>
    </tr>
    <tr>
      <td>Linked Server</td>
      <td>~10~60초</td>
      <td><strong>~30분~3시간</strong></td>
      <td>❌ 사실상 비현실적</td>
    </tr>
  </tbody>
</table>

<p><strong>핵심 포인트</strong></p>

<ul>
  <li>1만 건 수준은 <strong>어떤 방법이든 1분 안에 끝남</strong>. 시간 차이가 거의 없음.</li>
  <li>100만 건부터는 방법별로 <strong>분~시간 단위</strong> 차이가 벌어져요. bcp 와 .bak 가 압도적으로 빠름.</li>
  <li>1억 건 이상은 단일 task 로 끌고 가지 말고, <strong>테이블/파티션 단위 병렬화</strong> 가 필수.</li>
</ul>

<blockquote>
  <p>💡 정확한 수치는 결국 행 크기, 인덱스 개수, 트랜잭션 로그 설정, 네트워크 실효 대역폭에 좌우돼요. 본격 작업 전엔 <strong>샘플 테이블 100만 건으로 한 번 실측</strong> 해두는 걸 추천해요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="10-부하-방지-체크리스트">10. 부하 방지 체크리스트</h2>

<p>방법을 골랐다면, 실행 단계에서 챙겨야 할 부하 방지 포인트들이에요.</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />작업 시간대를 <strong>운영 트래픽 적은 시간</strong> 으로 잡기 (보통 새벽 2~5시)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />원본 쿼리는 가능하면 <strong><code class="language-plaintext highlighter-rouge">READ UNCOMMITTED</code></strong> 또는 <strong>스냅샷 격리</strong> 로 잡아서 락 충돌 회피</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />bcp/DMS 사용 시 <strong>배치 사이즈</strong> 를 작게 시작해서 점진적으로 키우기</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />트랜잭션 로그 백업 주기 짧게 유지 (단순 복구 모델이면 더 좋음)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" /><strong>Resource Governor</strong> 로 마이그레이션 세션 CPU/IO 상한 걸기</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />백업 파일은 <strong>원본 디스크와 다른 볼륨</strong> 에 떨어뜨리기 (IO 분리)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />작업 중 원본 DB <strong>모니터링 대시보드</strong> 띄워두기 (CPU, IO, lock waits)</li>
</ul>

<p><br /></p>

<p><br /></p>

<h2 id="11-정리--어떤-상황엔-무엇">11. 정리 — 어떤 상황엔 무엇</h2>

<p>마지막으로 상황별 추천을 한 번 더 정리해드릴게요.</p>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>추천 방법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>운영 중지 시간을 1~2시간 줄 수 있다 + 그대로 옮기고 싶다</td>
      <td><strong>Native Backup/Restore</strong></td>
    </tr>
    <tr>
      <td>다운타임을 거의 0 으로 가야 한다</td>
      <td><strong>DMS Full Load + CDC</strong></td>
    </tr>
    <tr>
      <td>특정 큰 테이블 몇 개만 빠르게 옮기면 된다</td>
      <td><strong>bcp</strong></td>
    </tr>
    <tr>
      <td>DB 가 작고(수 GB) 한 덩어리로 들고 다니고 싶다</td>
      <td><strong>BACPAC</strong></td>
    </tr>
    <tr>
      <td>GUI ETL 익숙하고 재실행 자주 한다</td>
      <td><strong>SSIS</strong></td>
    </tr>
    <tr>
      <td>Linked Server INSERT…SELECT</td>
      <td><strong>하지 마세요</strong></td>
    </tr>
  </tbody>
</table>

<p>저희 사내 케이스처럼 <strong>“그대로” + “부하 최소” + “S2S 가능”</strong> 이면 <strong>Native Backup/Restore</strong> 가 거의 항상 1순위예요. DMS 는 컷오버 단축이 필요할 때 더해서 쓰는 식.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 실제로 Native Backup/Restore 를 RDS 옵션 그룹 세팅부터 끝까지 따라가는 실전편을 정리해볼게요.</p>

<hr />

<p><strong>다음 글 →</strong> <a href="/coding/RDS_SQLServer_Native_Backup_Restore_실전/">(2/5) Native Backup/Restore 실전 — 옵션 그룹부터 컷오버까지</a></p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="mssql" /><category term="aws" /><category term="rds" /><category term="dms" /><category term="bcp" /><category term="migration" /><category term="s2s-vpn" /><category term="database" /><category term="kayserdocs" /><summary type="html"><![CDATA[내부망 MSSQL 을 AWS RDS for SQL Server 로 옮기는 6가지 방법을 비교하고, 원본 부하 최소화 관점에서 Native Backup/Restore 를 1순위로 추천하는 글이에요.]]></summary></entry><entry><title type="html">GitHub 잔디가 안 심어지던 이유 — Fork repo 에서 Standalone 으로 옮기기</title><link href="https://dorumugs.github.io/coding/GitHub_%EC%9E%94%EB%94%94_Fork_repo_Standalone_%EC%A0%84%ED%99%98/" rel="alternate" type="text/html" title="GitHub 잔디가 안 심어지던 이유 — Fork repo 에서 Standalone 으로 옮기기" /><published>2026-05-29T01:14:00+09:00</published><updated>2026-05-29T01:14:00+09:00</updated><id>https://dorumugs.github.io/coding/GitHub_%EC%9E%94%EB%94%94_Fork_repo_Standalone_%EC%A0%84%ED%99%98</id><content type="html" xml:base="https://dorumugs.github.io/coding/GitHub_%EC%9E%94%EB%94%94_Fork_repo_Standalone_%EC%A0%84%ED%99%98/"><![CDATA[<p>블로그 글을 꾸준히 쓰고 있는데, 정작 GitHub 프로필의 잔디(컨트리뷰션 그래프)는 휑한 상태였어요. “내가 잘못 푸시했나?” 하고 며칠 무시했는데, 알고 보니 <strong>저장소 자체가 fork</strong> 라서 커밋이 잔디로 카운트가 안 되고 있던 거였어요. 이 글은 그 원인을 찾고 standalone repo 로 옮기기까지의 전 과정을 정리한 글이에요.</p>

<blockquote>
  <p>💡 이 글에서 다루는 것</p>
  <ul>
    <li>GitHub 잔디(컨트리뷰션 그래프) 카운팅의 4가지 조건</li>
    <li>왜 fork repo 의 커밋은 잔디로 안 잡히는지</li>
    <li>GitHub Support 에 “fork 분리” 요청을 보냈더니 거절당한 이야기</li>
    <li>안전하게 standalone repo 로 옮기는 실제 절차 (rename → new repo → push → Pages)</li>
  </ul>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="1-증상--잔디가-휑한데-커밋은-매일-쌓이고-있다">1. 증상 — 잔디가 휑한데 커밋은 매일 쌓이고 있다</h2>

<p>상황은 단순했어요.</p>

<ul>
  <li>블로그(<code class="language-plaintext highlighter-rouge">https://dorumugs.github.io</code>)에 글을 거의 매일 쓰고 push 도 잘 됨</li>
  <li>repo <code class="language-plaintext highlighter-rouge">dorumugs/dorumugs.github.io</code> 의 commit history 에는 제 커밋이 줄줄이 찍힘 (author: <code class="language-plaintext highlighter-rouge">Jaehyun So &lt;do***@gmail.com&gt;</code>)</li>
  <li>그런데 <code class="language-plaintext highlighter-rouge">https://github.com/dorumugs</code> 프로필의 잔디는 <strong>하나도 안 채워짐</strong></li>
</ul>

<p>뭔가 한 가지가 어긋났다는 신호인데, 그 한 가지를 정확히 찍어내려면 GitHub 가 잔디 카운팅을 어떤 조건으로 하는지부터 알아야 했어요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="2-github-잔디-카운팅의-4가지-조건">2. GitHub 잔디 카운팅의 4가지 조건</h2>

<p>GitHub 공식 문서에 따르면 커밋이 컨트리뷰션 그래프에 잡히려면 <strong>네 조건 전부</strong> 만족해야 해요. 하나라도 어긋나면 무시당해요.</p>

<table>
  <thead>
    <tr>
      <th>조건</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1️⃣ 최근 1년 이내</td>
      <td>1년 넘게 묵은 커밋은 안 잡힘</td>
    </tr>
    <tr>
      <td>2️⃣ author 이메일이 GitHub 계정에 verified</td>
      <td>commit author email 이 본인 계정의 verified email 목록에 있어야 함</td>
    </tr>
    <tr>
      <td>3️⃣ standalone 저장소</td>
      <td><strong>fork 면 안 잡힘</strong></td>
    </tr>
    <tr>
      <td>4️⃣ default branch (또는 <code class="language-plaintext highlighter-rouge">gh-pages</code> Pages 브랜치)</td>
      <td>그 외 feature 브랜치 커밋은 잔디로 안 잡힘</td>
    </tr>
  </tbody>
</table>

<p>제 케이스에서 1, 2, 4 는 다 통과했는데, 3 이 문제였어요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="3-원인-확인--내-repo-가-fork-였다">3. 원인 확인 — 내 repo 가 fork 였다</h2>

<p>GitHub REST API 로 한 줄 확인해봤어요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> https://api.github.com/repos/dorumugs/dorumugs.github.io | <span class="se">\</span>
  <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'"(fork|parent|default_branch)"'</span>
</code></pre></div></div>

<p>결과는 이렇게 떨어졌어요.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  "fork": true,
  "parent": { "full_name": "mmistakes/minimal-mistakes" }
  "default_branch": "gh-pages",
</code></pre></div></div>

<p>✅ 범인 확인 — <code class="language-plaintext highlighter-rouge">"fork": true</code>.</p>

<p>블로그는 <a href="https://github.com/mmistakes/minimal-mistakes">minimal-mistakes</a> 테마를 기반으로 시작했는데, 그때 GitHub 의 “Fork” 버튼으로 시작해서 그대로 콘텐츠를 채워나간 거였어요. 시간이 지나면서 테마는 거의 통째로 갈아엎고 내 글이 99% 인 상태가 됐지만, GitHub 가 보기엔 여전히 mmistakes/minimal-mistakes 의 fork 였어요. 그리고 fork 의 커밋은 잔디 정책상 무조건 카운트 제외.</p>

<blockquote>
  <p>⚠️ fork 여부는 repo 페이지 상단의 “forked from mmistakes/minimal-mistakes” 표기로도 확인할 수 있어요. 평소에는 안 보고 지나치게 되니까 본인 repo 가 fork 인지 모르고 있는 경우가 의외로 많아요.</p>
</blockquote>

<p><br /></p>

<p><br /></p>

<h2 id="4-해결-시도-1--github-support-에-detach-요청-실패">4. 해결 시도 1 — GitHub Support 에 detach 요청 (실패)</h2>

<p>옛날에는 GitHub Support 에 “이 repo 를 parent 와 detach 해주세요” 라고 요청하면 처리해줬다는 글들을 봐서 support 폼을 열었어요. 라우팅은 이런 흐름이에요.</p>

<p><code class="language-plaintext highlighter-rouge">Contributions</code> → <code class="language-plaintext highlighter-rouge">Missing contributions</code> → <code class="language-plaintext highlighter-rouge">Commit</code> → 상세 폼</p>

<p>제목/본문 다 채우고 제출 직전, GitHub 가 폼 아래에 자동 응답을 띄웠어요.</p>

<blockquote>
  <p>🚨 <strong>Forked repositories don’t count toward contribution graphs.</strong>
This behavior is expected and can’t be overridden by Support.
GitHub Support also can’t manually “detach” a repository from its parent fork.</p>

  <p>To have future commits count toward your contributions graph, you’ll need to move your site to a standalone repository.</p>
</blockquote>

<p>요약하면 <strong>정책이 바뀌어서 GitHub 가 더 이상 fork detach 안 해줘요</strong>. 유일한 해결책은 <strong>새 standalone repo 만들어서 옮기기</strong>.</p>

<p><br /></p>

<p><br /></p>

<h2 id="5-해결-시도-2--rename--새-repo--push">5. 해결 시도 2 — Rename + 새 repo + push</h2>

<p>문제는 제 사이트가 <strong>GitHub Pages user site</strong> 라서 repo 이름이 정확히 <code class="language-plaintext highlighter-rouge">dorumugs.github.io</code> 여야 한다는 거였어요. 즉 같은 이름을 비워주고 새로 만들어야 하는데, 기존 repo 를 그냥 삭제하면 백업이 없을 때 너무 위험해요.</p>

<p>그래서 좀 더 안전한 순서로 진행했어요.</p>

<ol>
  <li>기존 repo 를 다른 이름으로 <strong>rename</strong> 해서 살려둠 (아카이브)</li>
  <li>같은 이름 <code class="language-plaintext highlighter-rouge">dorumugs.github.io</code> 로 새 standalone repo 생성</li>
  <li>로컬에서 origin 갈아끼우고 push</li>
  <li>새 repo 에서 Pages 활성화</li>
  <li>이메일 verified 확인</li>
</ol>

<p>URL 도 보존되고 (<code class="language-plaintext highlighter-rouge">https://dorumugs.github.io</code> 그대로), 잘못되면 archive 로 복구 가능. 실제 진행한 과정 정리할게요.</p>

<p><br /></p>

<p><br /></p>

<h3 id="5-0-로컬-백업--사고-났을-때-살아남는-보험">5-0. 로컬 백업 — 사고 났을 때 살아남는 보험</h3>

<p>뭐든 시작하기 전에 로컬에서 한 번 더 백업.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git fetch <span class="nt">--all</span> <span class="nt">--tags</span>
git bundle create ~/dorumugs.github.io-backup-<span class="si">$(</span><span class="nb">date</span> +%Y%m%d<span class="si">)</span>.bundle <span class="se">\</span>
  gh-pages <span class="se">\</span>
  refs/remotes/origin/gh-pages <span class="se">\</span>
  refs/remotes/origin/master
git bundle verify ~/dorumugs.github.io-backup-<span class="si">$(</span><span class="nb">date</span> +%Y%m%d<span class="si">)</span>.bundle
</code></pre></div></div>

<p>번들 한 파일에 전체 히스토리가 들어가요. 복구할 일이 생기면 <code class="language-plaintext highlighter-rouge">git clone &lt;bundle파일&gt; restored-repo</code> 한 줄로 되돌릴 수 있어요.</p>

<p>만든 다음에 <code class="language-plaintext highlighter-rouge">verify</code> 까지 통과시켜놓으면 안심.</p>

<p><br /></p>

<p><br /></p>

<h3 id="5-1-기존-repo-를-rename--archive-로-살려두기">5-1. 기존 repo 를 rename — archive 로 살려두기</h3>

<p>GitHub 웹에서:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Settings</code> → <code class="language-plaintext highlighter-rouge">General</code> → <code class="language-plaintext highlighter-rouge">Repository name</code></li>
  <li><code class="language-plaintext highlighter-rouge">dorumugs.github.io</code> → <code class="language-plaintext highlighter-rouge">dorumugs.github.io-archive</code> 로 변경</li>
  <li><code class="language-plaintext highlighter-rouge">Rename</code> 클릭</li>
</ul>

<blockquote>
  <p>⚠️ rename 직후 <code class="language-plaintext highlighter-rouge">https://dorumugs.github.io</code> 가 잠시 404 가 될 수 있어요. 다음 단계들이 끝나면 다시 살아나요.</p>
</blockquote>

<p>이 시점에 원래 URL <code class="language-plaintext highlighter-rouge">github.com/dorumugs/dorumugs.github.io</code> 는 GitHub 가 자동으로 archive 로 리다이렉트해줘요. 새 repo 를 만드는 순간 그 리다이렉트는 끊기고 새 repo 가 그 이름을 차지.</p>

<p><br /></p>

<p><br /></p>

<h3 id="5-2-새-standalone-repo-생성">5-2. 새 standalone repo 생성</h3>

<p>GitHub 웹 <code class="language-plaintext highlighter-rouge">https://github.com/new</code> 에서 새 repo 생성.</p>

<ul>
  <li><strong>Owner</strong>: <code class="language-plaintext highlighter-rouge">dorumugs</code></li>
  <li><strong>Repository name</strong>: <code class="language-plaintext highlighter-rouge">dorumugs.github.io</code> ← 정확히 같은 이름</li>
  <li><strong>Public</strong></li>
  <li>README / .gitignore / license <strong>추가 안 함</strong> (빈 repo 여야 push 가 깔끔)</li>
  <li>⚠️ template / fork 옵션 절대 건드리지 말기</li>
</ul>

<p>생성된 repo 페이지가 비어있고 “Quick setup” 가이드만 보이면 정상.</p>

<p><br /></p>

<p><br /></p>

<h3 id="5-3-로컬-remote-갈아끼우고-push">5-3. 로컬 remote 갈아끼우고 push</h3>

<p>여기서부터는 로컬 터미널에서 진행.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># origin URL 명시적으로 새 repo 가리키게</span>
git remote set-url origin git@github.com:dorumugs/dorumugs.github.io.git

<span class="c"># mmistakes 가리키던 upstream 정리</span>
git remote remove upstream

<span class="c"># 확인</span>
git remote <span class="nt">-v</span>
</code></pre></div></div>

<p>브랜치 push.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git push <span class="nt">-u</span> origin gh-pages
git push origin refs/remotes/origin/master:refs/heads/master
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">gh-pages</code> 는 현재 사이트의 본체 브랜치. 이게 본문임</li>
  <li><code class="language-plaintext highlighter-rouge">master</code> 는 옛날 워크플로 잔재인데 일부 콘텐츠(이미지 등)가 살아있을 수 있어서 같이 보존</li>
  <li>tags 는 mmistakes 테마 태그라 푸시 안 함 (<code class="language-plaintext highlighter-rouge">git push --tags</code> 했다가는 mmistakes 의 4.x.x 릴리스 태그들이 통째로 따라옴)</li>
</ul>

<p>✅ 푸시 후 GitHub API 로 확인했을 때 <strong>커밋 SHA 가 그대로 보존돼야</strong> 해요. 예를 들어 가장 최근 커밋 <code class="language-plaintext highlighter-rouge">798c73fa</code> 가 새 repo 에서도 동일한 <code class="language-plaintext highlighter-rouge">798c73fa</code> 로 들어가있어야 GitHub 가 그 커밋을 잔디로 인식해줘요.</p>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> https://api.github.com/repos/dorumugs/dorumugs.github.io | <span class="se">\</span>
  <span class="nb">grep</span> <span class="nt">-E</span> <span class="s1">'"(fork|default_branch)"'</span>
<span class="c"># "fork": false,</span>
<span class="c"># "default_branch": "gh-pages",</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">fork: false</code> 가 나오면 standalone 전환 성공.</p>

<p><br /></p>

<p><br /></p>

<h3 id="5-4-pages-활성화">5-4. Pages 활성화</h3>

<p>새 repo 의 GitHub 웹에서:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Settings</code> → <code class="language-plaintext highlighter-rouge">Pages</code></li>
  <li><strong>Source</strong>: <code class="language-plaintext highlighter-rouge">Deploy from a branch</code></li>
  <li><strong>Branch</strong>: <code class="language-plaintext highlighter-rouge">gh-pages</code> / <code class="language-plaintext highlighter-rouge">/(root)</code></li>
  <li><code class="language-plaintext highlighter-rouge">Save</code></li>
</ul>

<p>1~5분 뒤 Actions 탭에서 “pages build and deployment” 가 success 로 끝나면 <code class="language-plaintext highlighter-rouge">https://dorumugs.github.io</code> 가 다시 살아나요.</p>

<p>확인 포인트.</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">https://dorumugs.github.io</code> HTTP 200</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />글 상세 페이지 한두 개 정상 렌더링</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />커스텀 도메인 쓰고 있었다면 도메인 설정 + <code class="language-plaintext highlighter-rouge">CNAME</code> 파일 재확인</li>
</ul>

<p><br /></p>

<p><br /></p>

<h3 id="5-5-이메일-verified-확인--자주-빼먹는-단계">5-5. 이메일 verified 확인 — 자주 빼먹는 단계</h3>

<p><code class="language-plaintext highlighter-rouge">Settings</code> → <code class="language-plaintext highlighter-rouge">Emails</code> 에서 commit author 로 쓰는 이메일이 verified 상태인지 확인.</p>

<blockquote>
  <p>⚠️ “Keep my email addresses private” 가 켜져 있으면 push 할 때 실제 이메일 대신 <code class="language-plaintext highlighter-rouge">*****+dorumugs@users.noreply.github.com</code> 으로 마스킹돼서 들어가요. 그러면 잔디 안 잡혀요. 끄거나, git config 를 noreply 이메일로 통일하거나, 둘 중 하나로 정리해두는 게 좋아요.</p>
</blockquote>

<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 내 최근 커밋의 author email 확인</span>
git log <span class="nt">--format</span><span class="o">=</span><span class="s1">'%ae'</span> <span class="nt">-10</span>
</code></pre></div></div>

<p>여기서 나오는 이메일이 GitHub <code class="language-plaintext highlighter-rouge">Settings</code> → <code class="language-plaintext highlighter-rouge">Emails</code> 의 verified 목록에 있어야 해요.</p>

<p><br /></p>

<p><br /></p>

<h2 id="6-검증--잔디가-다시-자라기까지">6. 검증 — 잔디가 다시 자라기까지</h2>

<p>push 끝나고 Pages 도 잘 떴다면 이제 GitHub 가 백필을 돌려요.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>반영 시점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>새 push 커밋</td>
      <td>거의 즉시 ~ 몇 분</td>
    </tr>
    <tr>
      <td>과거 커밋 백필</td>
      <td>보통 24시간 이내</td>
    </tr>
    <tr>
      <td>프로필 페이지 캐시</td>
      <td>새로고침 / 강제 리로드로 빠르게 갱신 가능</td>
    </tr>
  </tbody>
</table>

<p>🎉 fork 가 아닌 standalone repo 가 되는 순간부터 과거 커밋도 사후 카운팅 대상으로 풀려요.</p>

<p>만약 48시간 지나도 잔디가 안 잡힌다면 이 두 가지를 다시 확인.</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />commit author email 이 정말 verified email 인지</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />repo 가 정말 <code class="language-plaintext highlighter-rouge">fork: false</code> 인지</li>
</ul>

<p><br /></p>

<p><br /></p>

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

<ul>
  <li>GitHub 잔디는 <strong>fork repo 의 커밋은 무조건 제외</strong>. 정책이고, 정책상 detach 도 더 이상 안 해줘요.</li>
  <li>해결책은 standalone repo 로 옮기는 것 한 가지뿐.</li>
  <li>user site (<code class="language-plaintext highlighter-rouge">&lt;username&gt;.github.io</code>) 인 경우 rename → new repo (같은 이름) → push 순서로 가면 URL 도 보존돼요.</li>
  <li>가장 자주 빼먹는 함정은 <strong>commit author email 이 verified email 인지</strong>, <strong>Keep email private 설정 여부</strong>.</li>
</ul>

<p>블로그 시작할 때 “Fork” 버튼을 누른 게 1년 넘게 잔디를 가렸던 셈이에요. 새 프로젝트 시작할 때는 가능하면 <code class="language-plaintext highlighter-rouge">Use this template</code> 가 있으면 그걸 쓰거나, 그게 없으면 fork 후 가능한 빨리 standalone 으로 옮겨두는 게 좋아요.</p>

<p>일단 오늘은 여기까지….. <br />
다음 글에서는 새 repo 환경에서 이어가는 minimal-mistakes 커스터마이즈 작업 정리해볼게요.</p>]]></content><author><name>Kayser So(dorumugs)</name></author><category term="coding" /><category term="github" /><category term="github-pages" /><category term="jekyll" /><category term="fork" /><category term="contributions" /><category term="troubleshooting" /><category term="kayserdocs" /><summary type="html"><![CDATA[블로그 글을 꾸준히 쓰는데 GitHub 잔디가 안 심어지던 이유 — fork 저장소 라서였어요. 카운팅 조건 4가지를 짚고 standalone repo 로 옮기기까지 정리했어요.]]></summary></entry></feed>