<?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://csj4032.github.io/atom.xml" rel="self" type="application/atom+xml" /><link href="https://csj4032.github.io/" rel="alternate" type="text/html" /><updated>2026-08-12T06:55:41+00:00</updated><id>https://csj4032.github.io/atom.xml</id><title type="html">MMIX Blog</title><subtitle>JVM 기반 백엔드와 데이터 플랫폼을 느리게 파고드는 개발자</subtitle><author><name>MMIX</name></author><entry><title type="html">Claude Code 통합 가이드</title><link href="https://csj4032.github.io/etc/2026/08/12/Claude-Code-%ED%86%B5%ED%95%A9-%EA%B0%80%EC%9D%B4%EB%93%9C/" rel="alternate" type="text/html" title="Claude Code 통합 가이드" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/etc/2026/08/12/Claude-Code-%ED%86%B5%ED%95%A9-%EA%B0%80%EC%9D%B4%EB%93%9C</id><content type="html" xml:base="https://csj4032.github.io/etc/2026/08/12/Claude-Code-%ED%86%B5%ED%95%A9-%EA%B0%80%EC%9D%B4%EB%93%9C/"><![CDATA[<p>하나의 Mac에서 2개의 Claude Code 인스턴스를 독립적으로 운영하면서, 각 계정별로 MCP 서버(Atlassian, Slack, GitHub, BigQuery, PostgreSQL, Google Workspace)와 Telegram 봇을 연결하는 방법을 정리합니다.</p>

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

<h3 id="mcpmodel-context-protocol란">MCP(Model Context Protocol)란?</h3>

<p>Claude Code가 외부 서비스(Jira, Confluence, Slack, GitHub, Gmail, Google Drive, Calendar, DB 등)와 통신할 수 있게 해주는 프로토콜입니다. MCP 서버를 설정하면 Claude가 직접 Jira 이슈를 조회하거나, Slack 메시지를 읽고, 이메일을 보내고, DB를 쿼리할 수 있습니다.</p>

<h3 id="왜-멀티-계정-분리가-필요한가">왜 멀티 계정 분리가 필요한가</h3>

<ul>
  <li>회사 계정과 개인 계정의 인증 정보(토큰, OAuth) 분리</li>
  <li>MCP 서버 포트/프로세스 충돌 방지</li>
  <li>보안: secrets 파일 격리</li>
  <li>업무용/개인용 등 목적별로 분리된 Telegram 봇 운영</li>
</ul>

<h3 id="핵심-원리">핵심 원리</h3>

<p>Claude Code는 <code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR</code> 환경변수로 설정 디렉토리를 지정할 수 있습니다. 이 변수를 다르게 설정하면 완전히 독립된 2개의 Claude Code 환경을 만들 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>계정 A (work)</th>
      <th>계정 B (genius)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>설정 디렉토리</td>
      <td><code class="language-plaintext highlighter-rouge">~/.claude-work/</code></td>
      <td><code class="language-plaintext highlighter-rouge">~/.claude-genius/</code></td>
    </tr>
    <tr>
      <td>환경변수 파일</td>
      <td><code class="language-plaintext highlighter-rouge">~/.secrets/work.env</code></td>
      <td><code class="language-plaintext highlighter-rouge">~/.secrets/genius.env</code></td>
    </tr>
    <tr>
      <td>Telegram 봇</td>
      <td>별도 봇 토큰 A</td>
      <td>별도 봇 토큰 B</td>
    </tr>
    <tr>
      <td>MCP 서버</td>
      <td>Atlassian, Slack, GitHub, BigQuery 등</td>
      <td>Atlassian 등</td>
    </tr>
  </tbody>
</table>

<h2 id="2-디렉토리-구조">2. 디렉토리 구조</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~
├── .claude-work/                     # 계정 A (회사) 설정
│   ├── .mcp.json                     # MCP 서버 설정
│   ├── settings.json                 # Claude Code 설정
│   ├── channels/
│   │   └── telegram/
│   │       ├── .env                  # 봇 토큰 A
│   │       └── access.json           # 접근 허용 목록
│   └── plugins/cache/                # 플러그인 캐시
├── .claude-genius/                   # 계정 B (개인) 설정
│   ├── .mcp.json
│   ├── settings.json
│   ├── channels/
│   │   └── telegram/
│   │       ├── .env                  # 봇 토큰 B
│   │       └── access.json
│   └── plugins/cache/
└── .secrets/
    ├── work.env                      # 계정 A 환경변수 (chmod 600)
    └── genius.env                    # 계정 B 환경변수 (chmod 600)
</code></pre></div></div>

<p><strong>핵심</strong>: <code class="language-plaintext highlighter-rouge">HOME</code>을 바꾸지 않고 <code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR</code>만 바꿔서 Claude 설정만 분리합니다. 공통 MCP는 <code class="language-plaintext highlighter-rouge">~/.claude-work/.mcp.json</code>에, 프로젝트별 MCP는 프로젝트 루트의 <code class="language-plaintext highlighter-rouge">.mcp.json</code>에 설정합니다.</p>

<h2 id="3-환경변수-파일-구성">3. 환경변수 파일 구성</h2>

<p>각 계정에서 사용할 환경변수(API 토큰, DB 접속정보 등)를 별도 파일로 관리합니다.</p>

<h3 id="secretsworkenv">~/.secrets/work.env</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Atlassian</span>
<span class="nb">export </span><span class="nv">ATLASSIAN_API_TOKEN</span><span class="o">=</span><span class="s2">"your-atlassian-api-token"</span>
<span class="c"># Slack</span>
<span class="nb">export </span><span class="nv">SLACK_BOT_TOKEN</span><span class="o">=</span><span class="s2">"xoxb-your-slack-bot-token"</span>
<span class="nb">export </span><span class="nv">SLACK_TEAM_ID</span><span class="o">=</span><span class="s2">"your-slack-team-id"</span>
<span class="c"># BigQuery</span>
<span class="nb">export </span><span class="nv">GOOGLE_CLOUD_PROJECT</span><span class="o">=</span><span class="s2">"your-gcp-project-id"</span>
<span class="c"># GitHub</span>
<span class="nb">export </span><span class="nv">GITHUB_PERSONAL_ACCESS_TOKEN</span><span class="o">=</span><span class="s2">"ghp_your-github-token"</span>
<span class="c"># PostgreSQL</span>
<span class="nb">export </span><span class="nv">PGHOST</span><span class="o">=</span><span class="s2">"db-host"</span>
<span class="nb">export </span><span class="nv">PGPORT</span><span class="o">=</span><span class="s2">"5432"</span>
<span class="nb">export </span><span class="nv">PGDATABASE</span><span class="o">=</span><span class="s2">"your_db"</span>
<span class="nb">export </span><span class="nv">PGUSER</span><span class="o">=</span><span class="s2">"readonly_user"</span>
<span class="nb">export </span><span class="nv">PGPASSWORD</span><span class="o">=</span><span class="s2">"your-password"</span>
<span class="c"># Google Workspace (OAuth)</span>
<span class="nb">export </span><span class="nv">GOOGLE_CLIENT_ID</span><span class="o">=</span><span class="s2">"your-client-id.apps.googleusercontent.com"</span>
<span class="nb">export </span><span class="nv">GOOGLE_CLIENT_SECRET</span><span class="o">=</span><span class="s2">"your-client-secret"</span>
</code></pre></div></div>

<h3 id="secretsgeniusenv">~/.secrets/genius.env</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">ATLASSIAN_API_TOKEN</span><span class="o">=</span><span class="s2">"your-other-token"</span>
<span class="c"># ... 필요한 환경변수 추가</span>
</code></pre></div></div>

<h3 id="보안-주의사항">보안 주의사항</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod </span>600 ~/.secrets/<span class="k">*</span>.env    <span class="c"># 본인만 읽기 가능</span>
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">.mcp.json</code>에 토큰을 직접 넣지 않고 <code class="language-plaintext highlighter-rouge">${ENV_VAR}</code> 형태로 참조</li>
  <li><code class="language-plaintext highlighter-rouge">source</code>로 주입된 환경변수는 MCP 자식 프로세스가 자동 상속</li>
  <li>1Password CLI(<code class="language-plaintext highlighter-rouge">op</code>) 연동 시 더 안전하게 관리 가능</li>
</ul>

<h2 id="4-shell-함수-설정-zshrc">4. Shell 함수 설정 (~/.zshrc)</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Claude Code 계정 분리 실행</span>
claude-work<span class="o">()</span> <span class="o">{</span>
  <span class="o">(</span>
    <span class="nb">source</span> ~/.secrets/work.env
    <span class="nb">export </span><span class="nv">CLAUDE_CONFIG_DIR</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.claude-work"</span>
    claude <span class="nt">--channels</span> plugin:telegram@claude-plugins-official <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
  <span class="o">)</span>
<span class="o">}</span>

claude-genius<span class="o">()</span> <span class="o">{</span>
  <span class="o">(</span>
    <span class="nb">source</span> ~/.secrets/genius.env
    <span class="nb">export </span><span class="nv">CLAUDE_CONFIG_DIR</span><span class="o">=</span><span class="s2">"</span><span class="nv">$HOME</span><span class="s2">/.claude-genius"</span>
    claude <span class="nt">--channels</span> plugin:telegram@claude-plugins-official <span class="s2">"</span><span class="nv">$@</span><span class="s2">"</span>
  <span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<p><strong>핵심 포인트</strong></p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">source ~/.secrets/xxx.env</code> — 해당 계정의 환경변수를 로드</li>
  <li><code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR=~/.claude-xxx</code> — 설정 디렉토리를 분리</li>
  <li><code class="language-plaintext highlighter-rouge">--channels plugin:telegram@claude-plugins-official</code> — Telegram 채널 플러그인 활성화</li>
  <li>서브셸 <code class="language-plaintext highlighter-rouge">( )</code>로 감싸서 환경변수가 현재 셸에 영향을 주지 않도록 격리</li>
  <li><code class="language-plaintext highlighter-rouge">"$@"</code>로 추가 인자 전달 가능 (alias 대신 함수를 사용하는 이유)</li>
</ul>

<h2 id="5-mcp-서버-설정">5. MCP 서버 설정</h2>

<h3 id="5-1-공통-mcp-설정-claude-workmcpjson">5-1. 공통 MCP 설정 (~/.claude-work/.mcp.json)</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"mcpServers"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"atlassian-local"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"uvx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"mcp-atlassian"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"--jira-url"</span><span class="p">,</span><span class="w"> </span><span class="s2">"https://your-org.atlassian.net"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"--jira-username"</span><span class="p">,</span><span class="w"> </span><span class="s2">"you@your-org.com"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"--jira-token"</span><span class="p">,</span><span class="w"> </span><span class="s2">"${ATLASSIAN_API_TOKEN}"</span><span class="w">
      </span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"ATLASSIAN_API_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${ATLASSIAN_API_TOKEN}"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"slack"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@modelcontextprotocol/server-slack"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"SLACK_BOT_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${SLACK_BOT_TOKEN}"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"SLACK_TEAM_ID"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${SLACK_TEAM_ID}"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"github"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@modelcontextprotocol/server-github"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"GITHUB_PERSONAL_ACCESS_TOKEN"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${GITHUB_PERSONAL_ACCESS_TOKEN}"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"postgres"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@modelcontextprotocol/server-postgres"</span><span class="p">,</span><span class="w">
        </span><span class="s2">"postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}:${PGPORT}/${PGDATABASE}?sslmode=require"</span><span class="w">
      </span><span class="p">]</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"google-workspace"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@dguido/google-workspace-mcp"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"GOOGLE_CLIENT_ID"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${GOOGLE_CLIENT_ID}"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"GOOGLE_CLIENT_SECRET"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${GOOGLE_CLIENT_SECRET}"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"GOOGLE_WORKSPACE_SERVICES"</span><span class="p">:</span><span class="w"> </span><span class="s2">"gmail,drive,calendar"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"bigquery"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"command"</span><span class="p">:</span><span class="w"> </span><span class="s2">"npx"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"-y"</span><span class="p">,</span><span class="w"> </span><span class="s2">"@modelcontextprotocol/server-bigquery"</span><span class="p">],</span><span class="w">
      </span><span class="nl">"env"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"GOOGLE_CLOUD_PROJECT"</span><span class="p">:</span><span class="w"> </span><span class="s2">"${GOOGLE_CLOUD_PROJECT}"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><strong>포인트</strong>: 비밀 값(토큰, 비밀번호)은 <code class="language-plaintext highlighter-rouge">.mcp.json</code>에 넣지 않고 <code class="language-plaintext highlighter-rouge">${ENV_VAR}</code>로 secrets 파일의 환경변수를 참조합니다. ADC를 사용하는 경우 BigQuery의 <code class="language-plaintext highlighter-rouge">GOOGLE_APPLICATION_CREDENTIALS</code>는 생략 가능합니다.</p>

<h3 id="5-2-프로젝트별-mcp-설정">5-2. 프로젝트별 MCP 설정</h3>

<table>
  <thead>
    <tr>
      <th>설정 위치</th>
      <th>적용 범위</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">~/.claude-work/.mcp.json</code></td>
      <td>모든 프로젝트 (공통)</td>
      <td>Slack, Google, Jira, GitHub, PostgreSQL, BigQuery</td>
    </tr>
    <tr>
      <td>프로젝트 루트 <code class="language-plaintext highlighter-rouge">.mcp.json</code></td>
      <td>해당 프로젝트만</td>
      <td>프로젝트 전용 MCP (특정 DB, 특정 API 등)</td>
    </tr>
  </tbody>
</table>

<h3 id="5-3-연동된-mcp-서버-목록">5-3. 연동된 MCP 서버 목록</h3>

<table>
  <thead>
    <tr>
      <th>MCP 서버</th>
      <th>용도</th>
      <th>패키지</th>
      <th>필요한 환경변수</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Atlassian</strong></td>
      <td>Jira 이슈 조회/생성, Confluence 페이지 읽기/쓰기</td>
      <td><code class="language-plaintext highlighter-rouge">mcp-atlassian</code> (uvx)</td>
      <td><code class="language-plaintext highlighter-rouge">ATLASSIAN_API_TOKEN</code></td>
    </tr>
    <tr>
      <td><strong>Slack</strong></td>
      <td>채널 메시지 읽기, 메시지 전송, 스레드 조회</td>
      <td><code class="language-plaintext highlighter-rouge">@modelcontextprotocol/server-slack</code></td>
      <td><code class="language-plaintext highlighter-rouge">SLACK_BOT_TOKEN</code></td>
    </tr>
    <tr>
      <td><strong>GitHub</strong></td>
      <td>코드 검색, PR 리뷰, 이슈 관리</td>
      <td><code class="language-plaintext highlighter-rouge">@modelcontextprotocol/server-github</code></td>
      <td><code class="language-plaintext highlighter-rouge">GITHUB_PERSONAL_ACCESS_TOKEN</code></td>
    </tr>
    <tr>
      <td><strong>PostgreSQL</strong></td>
      <td>DB 쿼리, 스키마 조회</td>
      <td><code class="language-plaintext highlighter-rouge">@modelcontextprotocol/server-postgres</code></td>
      <td><code class="language-plaintext highlighter-rouge">PGHOST</code>, <code class="language-plaintext highlighter-rouge">PGPORT</code>, <code class="language-plaintext highlighter-rouge">PGDATABASE</code>, <code class="language-plaintext highlighter-rouge">PGUSER</code>, <code class="language-plaintext highlighter-rouge">PGPASSWORD</code></td>
    </tr>
    <tr>
      <td><strong>BigQuery</strong></td>
      <td>테이블 조회, SELECT 쿼리 실행</td>
      <td><code class="language-plaintext highlighter-rouge">@modelcontextprotocol/server-bigquery</code></td>
      <td><code class="language-plaintext highlighter-rouge">GOOGLE_CLOUD_PROJECT</code></td>
    </tr>
    <tr>
      <td><strong>Google Workspace</strong></td>
      <td>Gmail 읽기/발송, Drive 파일 관리, Calendar 일정 조회</td>
      <td><code class="language-plaintext highlighter-rouge">@dguido/google-workspace-mcp</code></td>
      <td><code class="language-plaintext highlighter-rouge">GOOGLE_CLIENT_ID</code>, <code class="language-plaintext highlighter-rouge">GOOGLE_CLIENT_SECRET</code></td>
    </tr>
  </tbody>
</table>

<h2 id="6-bigquery-mcp-상세-설정">6. BigQuery MCP 상세 설정</h2>

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

<ul>
  <li>Google Cloud 프로젝트 및 BigQuery 접근 권한</li>
  <li>GCP 서비스 계정 키(JSON) 또는 Application Default Credentials(ADC) 설정</li>
  <li>Node.js 18 이상</li>
</ul>

<h3 id="gcp-인증-설정-adc-방식-권장">GCP 인증 설정 (ADC 방식 권장)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud auth application-default login
gcloud config <span class="nb">set </span>project &lt;YOUR_GCP_PROJECT_ID&gt;
</code></pre></div></div>

<h3 id="claude-code에서-사용">Claude Code에서 사용</h3>

<p>공통 <code class="language-plaintext highlighter-rouge">.mcp.json</code>에 bigquery 서버를 등록하면(5-1 참고) Claude Code 실행 시 자동으로 연결됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Claude Code 실행 후 MCP 연동 확인</span>
/mcp
</code></pre></div></div>

<h3 id="claude-app-claudeai에서-사용">Claude App (claude.ai)에서 사용</h3>

<p>Claude App은 브라우저/데스크탑 앱에서 MCP 서버를 원격으로 연결하는 방식을 지원합니다. 로컬에 MCP 프록시 서버를 띄우고 claude.ai에서 URL로 연결합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># mcp-proxy 설치</span>
npm <span class="nb">install</span> <span class="nt">-g</span> @modelcontextprotocol/proxy

<span class="c"># BigQuery MCP 서버를 HTTP 프록시로 실행</span>
mcp-proxy <span class="nt">--port</span> 3100 npx <span class="nt">-y</span> @modelcontextprotocol/server-bigquery
</code></pre></div></div>

<p>claude.ai에서 등록:</p>

<ol>
  <li><a href="https://claude.ai">claude.ai</a> 접속 → 좌측 하단 <strong>설정(Settings)</strong></li>
  <li><strong>Integrations</strong> 또는 <strong>MCP Servers</strong> 탭 선택</li>
  <li><strong>Add MCP Server</strong> → Name: <code class="language-plaintext highlighter-rouge">bigquery</code>, URL: <code class="language-plaintext highlighter-rouge">http://localhost:3100/sse</code></li>
  <li><strong>Save</strong> 후 연결 상태 확인</li>
</ol>

<h3 id="bigquery-제공-도구">BigQuery 제공 도구</h3>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>설명</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">list-tables</code></td>
      <td>전체 테이블 목록 조회</td>
      <td>“테이블 목록 보여줘”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">describe-table</code></td>
      <td>테이블 스키마 조회</td>
      <td>“orders 테이블 컬럼 알려줘”</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">execute-query</code></td>
      <td>SELECT 쿼리 실행 (읽기 전용)</td>
      <td>“최근 주문 건수 집계해줘”</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">execute-query</code>는 읽기 전용(SELECT)만 허용됩니다. INSERT/UPDATE/DELETE는 실행되지 않습니다.</p>
</blockquote>

<h2 id="7-telegram-봇-연결">7. Telegram 봇 연결</h2>

<h3 id="7-1-봇-생성">7-1. 봇 생성</h3>

<ol>
  <li>Telegram에서 <strong>@BotFather</strong>에게 <code class="language-plaintext highlighter-rouge">/newbot</code> 명령어를 보냅니다.</li>
  <li>봇 이름과 username을 지정합니다. (예: <code class="language-plaintext highlighter-rouge">WorkAssistantBot</code>, <code class="language-plaintext highlighter-rouge">GeniusAssistantBot</code>)</li>
  <li>각각의 <strong>Bot Token</strong>을 발급받아 안전하게 보관합니다.</li>
  <li>이 과정을 2번 반복하여 2개의 봇 토큰을 확보합니다.</li>
</ol>

<h3 id="7-2-봇-토큰-등록">7-2. 봇 토큰 등록</h3>

<p>각 계정별로 Claude Code를 실행한 뒤 Telegram 봇 토큰을 등록합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 계정 A 실행</span>
claude-work

<span class="c"># Claude Code 프롬프트에서:</span>
/telegram:configure
<span class="c"># → 봇 토큰 A를 입력</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 계정 B 실행 (별도 터미널)</span>
claude-genius

<span class="c"># Claude Code 프롬프트에서:</span>
/telegram:configure
<span class="c"># → 봇 토큰 B를 입력</span>
</code></pre></div></div>

<h3 id="7-3-접근-권한-설정">7-3. 접근 권한 설정</h3>

<ol>
  <li>각 봇에게 Telegram으로 메시지를 보냅니다.</li>
  <li>Claude Code에서 페어링 요청이 표시됩니다.</li>
  <li><code class="language-plaintext highlighter-rouge">/telegram:access</code> 명령으로 해당 사용자를 승인합니다.</li>
</ol>

<p><code class="language-plaintext highlighter-rouge">access.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">"dmPolicy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"allowlist"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"allowedUserIds"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"your-telegram-user-id"</span><span class="p">],</span><span class="w">
  </span><span class="nl">"groupPolicy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">@userinfobot</code>에게 메시지를 보내면 자신의 Telegram 사용자 ID를 확인할 수 있습니다.</p>

<h2 id="8-google-workspace-mcp-설정">8. Google Workspace MCP 설정</h2>

<p>Google Workspace MCP는 Gmail, Google Drive, Google Calendar에 접근할 수 있게 해줍니다. OAuth 2.0 인증이 필요하므로 초기 설정이 다른 MCP보다 복잡합니다.</p>

<h3 id="8-1-google-cloud-console-설정">8-1. Google Cloud Console 설정</h3>

<ol>
  <li><a href="https://console.cloud.google.com/">Google Cloud Console</a>에서 프로젝트 생성 또는 선택</li>
  <li><strong>API 및 서비스 &gt; 라이브러리</strong>에서 Gmail API, Google Drive API, Google Calendar API 활성화</li>
  <li><strong>API 및 서비스 &gt; 사용자 인증 정보</strong>에서 OAuth 2.0 클라이언트 ID 생성 (데스크톱 앱)</li>
  <li>Client ID와 Client Secret을 <code class="language-plaintext highlighter-rouge">~/.secrets/work.env</code>에 저장</li>
</ol>

<h3 id="8-2-첫-실행-시-oauth-인증">8-2. 첫 실행 시 OAuth 인증</h3>

<p>Claude Code에서 Google Workspace MCP를 처음 사용하면 브라우저가 열리며 Google 계정 로그인 및 권한 동의를 요청합니다. 인증 완료 후 토큰이 <code class="language-plaintext highlighter-rouge">~/.config/google-workspace-mcp/tokens.json</code>에 저장됩니다.</p>

<h3 id="8-3-사용-가능한-기능">8-3. 사용 가능한 기능</h3>

<ul>
  <li><strong>Gmail</strong>: 메일 검색, 읽기, 발송, 삭제</li>
  <li><strong>Drive</strong>: 파일 목록 조회, 파일 다운로드/업로드, 폴더 관리</li>
  <li><strong>Calendar</strong>: 일정 조회, 일정 생성/수정/삭제, 빈 시간 찾기</li>
</ul>

<h2 id="9-실행-및-확인">9. 실행 및 확인</h2>

<p>2개의 터미널을 열고 각각 실행합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 터미널 1</span>
claude-work

<span class="c"># 터미널 2</span>
claude-genius
</code></pre></div></div>

<p>각 Telegram 봇에게 메시지를 보내면 해당 Claude Code 인스턴스가 독립적으로 응답합니다.</p>

<h3 id="활용-예시">활용 예시</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># Jira + Slack 연동
"슬랙 장애 채널 메시지 + 관련 Jira 이슈 요약해줘"

# 데이터 파이프라인 점검
"이 DAG 파일 분석하고, DB 스키마 확인해서 문제점 알려줘"

# BigQuery 데이터 조회
"BigQuery 테이블 목록 보여줘"
"orders 테이블 스키마 확인해줘"
"최근 7일간 주문 건수를 날짜별로 집계하는 쿼리 실행해줘"

# 이메일 + 캘린더 관리
"읽지 않은 메일 확인하고, 오늘 일정 알려줘"

# 업무 자동화
"오늘 Jira 스프린트 상태 요약해서 Slack에 공유해줘"
</code></pre></div></div>

<h2 id="10-트러블슈팅">10. 트러블슈팅</h2>

<h3 id="재시작-시-claude-기본-경로를-읽는-문제">재시작 시 <code class="language-plaintext highlighter-rouge">~/.claude/</code> 기본 경로를 읽는 문제</h3>

<ul>
  <li>원인: <code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR</code>가 제대로 export 되지 않음</li>
  <li>해결: alias 대신 함수 사용 + subshell 내에서 <code class="language-plaintext highlighter-rouge">export</code> 명시</li>
</ul>

<h3 id="mcp-서버-연결-실패">MCP 서버 연결 실패</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># MCP 서버 상태 확인 (Claude Code 내에서)</span>
/mcp
</code></pre></div></div>

<h3 id="google-workspace-api-활성화-안-됨">Google Workspace API 활성화 안 됨</h3>

<ul>
  <li>에러: <code class="language-plaintext highlighter-rouge">Gmail API has not been used in project XXX before or it is disabled</code></li>
  <li>해결: Google Cloud Console에서 해당 API(Gmail, Calendar, Drive)를 각각 활성화</li>
  <li>활성화 후 전파에 몇 분 소요될 수 있음</li>
</ul>

<h3 id="브라우저-oauth-꼬임">브라우저 OAuth 꼬임</h3>

<ul>
  <li>회사/개인 계정을 같은 브라우저에서 로그인하면 OAuth가 섞일 수 있음</li>
  <li>해결: Chrome 프로필 분리 또는 브라우저 분리</li>
</ul>

<h3 id="mcp-명령에서-mcp-목록이-안-보이는-경우"><code class="language-plaintext highlighter-rouge">/mcp</code> 명령에서 MCP 목록이 안 보이는 경우</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/mcp</code> UI가 <code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR</code>를 무시하고 기본 경로를 볼 수 있음</li>
  <li>실제 MCP 동작은 정상 — 도구 호출로 확인 가능</li>
</ul>

<h2 id="11-claude-code-vs-claude-app">11. Claude Code vs Claude App</h2>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Claude Code</th>
      <th>Claude App</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>실행 환경</td>
      <td>터미널(CLI)</td>
      <td>브라우저 / 데스크탑 앱</td>
    </tr>
    <tr>
      <td>설정 방식</td>
      <td><code class="language-plaintext highlighter-rouge">.mcp.json</code> 또는 전역 config</td>
      <td>claude.ai 설정에서 URL 등록</td>
    </tr>
    <tr>
      <td>인증</td>
      <td>로컬 GCP 인증 사용</td>
      <td>로컬 MCP 프록시 서버 필요</td>
    </tr>
    <tr>
      <td>적합한 용도</td>
      <td>코드 작성 중 데이터 확인, 쿼리 자동화</td>
      <td>비개발자 포함 팀원 데이터 조회 및 분석</td>
    </tr>
    <tr>
      <td>추가 설치</td>
      <td><code class="language-plaintext highlighter-rouge">claude-code</code> CLI</td>
      <td><code class="language-plaintext highlighter-rouge">mcp-proxy</code></td>
    </tr>
  </tbody>
</table>

<h2 id="12-주의사항">12. 주의사항</h2>

<ul>
  <li><strong>동시 실행 가능</strong>: 2개의 인스턴스를 동시에 실행할 수 있으며, 각각 독립적으로 동작합니다.</li>
  <li><strong>봇 토큰 보안</strong>: <code class="language-plaintext highlighter-rouge">~/.secrets/</code> 디렉토리의 파일 권한을 600으로 제한하세요.</li>
  <li><strong>계정 로그인</strong>: 각 <code class="language-plaintext highlighter-rouge">CLAUDE_CONFIG_DIR</code>별로 별도의 Claude Code 계정 로그인이 필요합니다. 즉, 2개의 Claude 계정(구독)이 필요합니다.</li>
  <li><strong>프로젝트별 MCP</strong>: 프로젝트 루트의 <code class="language-plaintext highlighter-rouge">.mcp.json</code>은 글로벌 설정과 별개로, 해당 프로젝트에서만 적용되는 MCP 서버를 추가로 설정할 수 있습니다.</li>
</ul>

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

<ul>
  <li><a href="https://modelcontextprotocol.io">MCP 공식 문서</a></li>
  <li><a href="https://docs.anthropic.com/claude-code">Claude Code 공식 문서</a></li>
  <li><a href="https://www.npmjs.com/package/@modelcontextprotocol/server-bigquery">BigQuery MCP 서버 (npm)</a></li>
  <li><a href="https://github.com/atlassian/mcp-atlassian">Atlassian MCP 서버</a></li>
  <li><a href="https://github.com/dguido/google-workspace-mcp">Google Workspace MCP 서버</a></li>
  <li><a href="https://cloud.google.com/docs/authentication/application-default-credentials">Google Cloud ADC 설정</a></li>
</ul>]]></content><author><name>MMIX</name></author><category term="etc" /><category term="claude-code" /><category term="mcp" /><category term="telegram" /><category term="bigquery" /><summary type="html"><![CDATA[하나의 Mac에서 2개의 Claude Code 인스턴스를 독립적으로 운영하면서, 각 계정별로 MCP 서버와 Telegram 봇을 연결하는 방법을 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — AWS</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-AWS/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — AWS" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-AWS</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-AWS/"><![CDATA[<h2 id="s3">S3</h2>

<h3 id="s3의-일관성-모델은-어떻게-되나요">S3의 일관성 모델은 어떻게 되나요</h3>

<p>2020년 12월부터 <strong>강한 일관성(strong read-after-write consistency)</strong>을 제공합니다. PUT 직후 GET하면 항상 최신 객체가 보이고, 목록 조회에도 즉시 반영됩니다.</p>

<p>그 이전에는 최종 일관성이라 방금 쓴 파일이 목록에 안 보이는 문제가 있었고, EMRFS consistent view 같은 우회 장치가 필요했습니다. 지금은 필요 없습니다.</p>

<p>다만 <strong>원자적 rename이 없다</strong>는 점은 그대로입니다. S3의 “이동”은 복사 후 삭제이고, 디렉터리 개념도 없습니다(키 접두사일 뿐). Spark의 파일 커밋 프로토콜이 S3에서 느리고 위험한 이유이며, Iceberg·Delta 같은 테이블 포맷이 필요한 배경이기도 합니다.</p>

<h3 id="스토리지-클래스를-어떻게-고르나요">스토리지 클래스를 어떻게 고르나요</h3>

<table>
  <thead>
    <tr>
      <th>클래스</th>
      <th>용도</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Standard</td>
      <td>자주 접근</td>
      <td>기본</td>
    </tr>
    <tr>
      <td>Intelligent-Tiering</td>
      <td>접근 패턴이 불규칙</td>
      <td>자동으로 계층 이동, 모니터링 비용 소량</td>
    </tr>
    <tr>
      <td>Standard-IA / One Zone-IA</td>
      <td>가끔 접근</td>
      <td>저장 저렴, <strong>조회 요금 발생</strong>, 최소 보관 30일</td>
    </tr>
    <tr>
      <td>Glacier Instant / Flexible / Deep Archive</td>
      <td>보관</td>
      <td>매우 저렴, 복원 시간·비용</td>
    </tr>
  </tbody>
</table>

<p>IA 계열은 <strong>최소 보관 기간과 최소 객체 크기</strong>가 있어, 작은 파일을 자주 바꾸면 오히려 비쌉니다. 라이프사이클 정책으로 자동 전환을 걸어두는 것이 일반적입니다.</p>

<h3 id="s3-성능을-높이려면-어떻게-하나요">S3 성능을 높이려면 어떻게 하나요</h3>

<p>접두사(prefix) 단위로 초당 요청 한도가 적용되므로, <strong>키를 여러 접두사에 분산</strong>하면 처리량이 올라갑니다. 예전에는 해시를 앞에 붙이라고 했는데, 지금은 S3가 자동으로 파티션을 나눠서 날짜 기반 접두사도 대체로 괜찮습니다.</p>

<p>더 중요한 것은 <strong>파일 크기</strong>입니다. 작은 파일이 많으면 요청 수가 폭증합니다. 요청 자체가 과금 대상이고 지연도 붙습니다. 128MB~1GB 정도로 맞추는 것이 일반적인 권장입니다.</p>

<p>대용량 업로드는 멀티파트로 병렬화하고, 실패한 멀티파트 업로드는 라이프사이클 규칙으로 정리해야 합니다. 안 그러면 <strong>보이지 않는 저장 비용</strong>이 계속 쌓입니다.</p>

<h3 id="s3-비용은-어디서-발생하나요">S3 비용은 어디서 발생하나요</h3>

<ul>
  <li><strong>저장 용량</strong> — 클래스별 단가</li>
  <li><strong>요청 수</strong> — PUT/GET/LIST. 작은 파일이 많으면 여기가 커집니다</li>
  <li><strong>데이터 전송</strong> — 리전 밖으로 나가는 트래픽. 같은 리전 내 S3↔EC2는 무료</li>
  <li><strong>조회 요금</strong> — IA·Glacier 계열</li>
  <li><strong>버전 관리</strong> — 버저닝을 켜두고 정리 안 하면 옛 버전이 계속 쌓입니다</li>
</ul>

<p>크로스 리전 전송이 예상보다 비싼 경우가 많습니다. 컴퓨트와 스토리지를 같은 리전에 두는 것이 기본입니다.</p>

<h2 id="처리-엔진">처리 엔진</h2>

<h3 id="glue와-emr은-어떻게-다른가요">Glue와 EMR은 어떻게 다른가요</h3>

<p><strong>Glue</strong>는 서버리스 ETL입니다. 클러스터를 관리하지 않고 DPU 단위로 과금됩니다. 짧은 배치, 간헐적 작업에 적합합니다. 대신 Spark 버전과 설정 자유도가 제한적이고, 시작 지연(콜드 스타트)이 있습니다.</p>

<p><strong>EMR</strong>은 관리형 Hadoop/Spark 클러스터입니다. 인스턴스 타입, Spark 설정, 부트스트랩 스크립트를 자유롭게 다룰 수 있고 스팟 인스턴스로 비용을 크게 줄일 수 있습니다. 대신 클러스터 수명주기를 직접 관리해야 합니다.</p>

<p><strong>EMR Serverless</strong>가 그 중간입니다. 클러스터 관리 없이 EMR의 엔진을 쓰는 형태입니다.</p>

<p>기준을 단순화하면 — 오래 돌고 튜닝이 필요한 대용량 배치는 EMR, 가볍고 자주 도는 작업은 Glue입니다.</p>

<h3 id="glue-data-catalog는-무엇인가요">Glue Data Catalog는 무엇인가요</h3>

<p>Hive Metastore 호환 메타데이터 저장소입니다. 테이블 스키마·파티션 정보를 두고, Athena·EMR·Redshift Spectrum·Glue Job이 공유합니다.</p>

<p><strong>Glue Crawler</strong>가 S3를 스캔해 스키마를 추론하고 카탈로그에 등록합니다. 편하지만 추론이 틀리는 경우가 있고(숫자를 문자열로, 날짜를 문자열로), 파티션이 많으면 크롤링이 느리고 비쌉니다. 스키마가 안정적이라면 DDL로 직접 정의하고 파티션만 <code class="language-plaintext highlighter-rouge">ALTER TABLE ADD PARTITION</code> 또는 <code class="language-plaintext highlighter-rouge">MSCK REPAIR TABLE</code>로 추가하는 편이 예측 가능합니다.</p>

<h3 id="athena를-쓸-때-비용을-줄이려면">Athena를 쓸 때 비용을 줄이려면</h3>

<p>Athena는 <strong>스캔한 데이터 양</strong>으로 과금합니다. 그래서 줄이는 방법이 명확합니다.</p>

<ul>
  <li><strong>컬럼너 포맷</strong>(Parquet/ORC) — 필요한 컬럼만 읽음</li>
  <li><strong>파티셔닝</strong> — 조건에 맞는 파티션만 스캔. 파티션 컬럼을 WHERE에 반드시 포함</li>
  <li><strong>압축</strong> — 스캔량 자체가 줄어듦</li>
  <li><strong><code class="language-plaintext highlighter-rouge">SELECT *</code> 지양</strong></li>
</ul>

<p>파티션 프루닝이 안 되면 전체를 스캔하므로, 파티션 컬럼을 가공한 조건(<code class="language-plaintext highlighter-rouge">WHERE substr(dt,1,7) = '2026-08'</code>)은 피해야 합니다.</p>

<h2 id="redshift">Redshift</h2>

<h3 id="분산-키distkey와-정렬-키sortkey를-설명해보세요">분산 키(DISTKEY)와 정렬 키(SORTKEY)를 설명해보세요</h3>

<p>Redshift는 데이터를 여러 노드·슬라이스에 나눠 저장합니다.</p>

<p><strong>DISTSTYLE / DISTKEY</strong>는 어떻게 나눌지를 정합니다.</p>

<table>
  <thead>
    <tr>
      <th>스타일</th>
      <th>방식</th>
      <th>적합한 경우</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>KEY</td>
      <td>지정 컬럼 해시로 분산</td>
      <td>조인 키. 같은 값이 같은 슬라이스에 모여 셔플 감소</td>
    </tr>
    <tr>
      <td>ALL</td>
      <td>모든 노드에 복제</td>
      <td>작은 디멘전 테이블</td>
    </tr>
    <tr>
      <td>EVEN</td>
      <td>라운드로빈</td>
      <td>조인에 안 쓰이는 테이블</td>
    </tr>
    <tr>
      <td>AUTO</td>
      <td>Redshift가 판단</td>
      <td>기본값</td>
    </tr>
  </tbody>
</table>

<p><strong>SORTKEY</strong>는 블록 내 정렬 순서입니다. 정렬되어 있으면 존 맵(블록별 min/max)으로 불필요한 블록을 건너뜁니다. 날짜 컬럼을 SORTKEY로 두는 경우가 많습니다.</p>

<h3 id="vacuum과-analyze는-무엇인가요">VACUUM과 ANALYZE는 무엇인가요</h3>

<p><strong>VACUUM</strong>은 삭제된 행의 공간을 회수하고 정렬 순서를 복원합니다. Redshift는 UPDATE를 “삭제 표시 + 삽입”으로 처리하므로 갱신이 많으면 공간이 낭비되고 정렬이 흐트러집니다.</p>

<p><strong>ANALYZE</strong>는 통계를 갱신합니다. 통계가 낡으면 옵티마이저가 잘못된 조인 순서를 고릅니다.</p>

<p>최근에는 자동 VACUUM·ANALYZE가 동작하지만, 대량 적재 직후에는 수동으로 돌리는 것이 안전합니다.</p>

<h3 id="대량-적재는-어떻게-하나요">대량 적재는 어떻게 하나요</h3>

<p><code class="language-plaintext highlighter-rouge">INSERT</code>를 반복하면 매우 느립니다. <strong><code class="language-plaintext highlighter-rouge">COPY</code> 명령으로 S3에서 병렬 적재</strong>하는 것이 정석입니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">COPY</span> <span class="n">events</span> <span class="k">FROM</span> <span class="s1">'s3://bucket/prefix/'</span>
<span class="n">IAM_ROLE</span> <span class="s1">'arn:aws:iam::...:role/...'</span>
<span class="n">FORMAT</span> <span class="k">AS</span> <span class="n">PARQUET</span><span class="p">;</span>
</code></pre></div></div>

<p>파일을 슬라이스 수의 배수로 나눠두면 병렬도가 최대가 됩니다. 파일 하나만 있으면 슬라이스 하나만 일합니다.</p>

<h3 id="redshift-spectrum은-무엇인가요">Redshift Spectrum은 무엇인가요</h3>

<p>S3의 외부 테이블을 Redshift에서 직접 쿼리하는 기능입니다. 자주 쓰는 데이터는 Redshift 안에, 오래된 데이터는 S3에 두고 필요할 때만 조인하는 구조를 만들 수 있습니다.</p>

<p>Athena처럼 스캔량 기준 과금이라 파티셔닝과 컬럼너 포맷이 그대로 중요합니다.</p>

<h2 id="스트리밍">스트리밍</h2>

<h3 id="kinesis-data-streams와-msk를-비교해보세요">Kinesis Data Streams와 MSK를 비교해보세요</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Kinesis Data Streams</th>
      <th>MSK (관리형 Kafka)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>프로토콜</td>
      <td>AWS 고유</td>
      <td>Kafka 호환</td>
    </tr>
    <tr>
      <td>확장 단위</td>
      <td>샤드</td>
      <td>파티션</td>
    </tr>
    <tr>
      <td>보관</td>
      <td>기본 24시간, 최대 365일</td>
      <td>설정에 따라</td>
    </tr>
    <tr>
      <td>생태계</td>
      <td>AWS 서비스 통합이 쉬움</td>
      <td>Kafka 커넥터·도구 그대로</td>
    </tr>
    <tr>
      <td>운영</td>
      <td>완전 관리형(온디맨드 모드)</td>
      <td>브로커·토픽 관리 필요</td>
    </tr>
  </tbody>
</table>

<p>이미 Kafka 생태계를 쓰고 있거나 멀티 클라우드를 고려한다면 MSK, AWS 안에서 단순하게 가려면 Kinesis가 자연스럽습니다.</p>

<h3 id="샤드와-파티션의-병렬도-개념은-같나요">샤드와 파티션의 병렬도 개념은 같나요</h3>

<p>비슷합니다. Kinesis에서 샤드 하나는 쓰기 1MB/s·1000 records/s, 읽기 2MB/s의 한도를 갖고, 컨슈머 병렬도의 단위이기도 합니다.</p>

<p>차이는 <strong>재파티셔닝 방식</strong>입니다. Kinesis는 샤드를 분할(split)·병합(merge)할 수 있어 줄이는 것도 가능한 반면, Kafka 파티션은 늘릴 수만 있습니다.</p>

<h2 id="iam과-비용">IAM과 비용</h2>

<h3 id="iam-역할과-사용자의-차이는-무엇인가요">IAM 역할과 사용자의 차이는 무엇인가요</h3>

<p><strong>사용자</strong>는 장기 자격 증명(액세스 키)을 가진 주체이고, <strong>역할</strong>은 임시 자격 증명을 발급받아 위임(assume)하는 방식입니다.</p>

<p>애플리케이션과 서비스는 <strong>역할</strong>을 써야 합니다. EC2 인스턴스 프로파일, EKS의 IRSA(ServiceAccount에 역할 연결), Lambda 실행 역할이 모두 이 방식입니다. 액세스 키를 코드나 환경변수에 넣는 것은 유출 시 회수가 어렵습니다.</p>

<p>권한 평가는 <strong>명시적 Deny &gt; 명시적 Allow &gt; 암묵적 Deny</strong> 순입니다. S3 버킷 정책, IAM 정책, SCP, VPC 엔드포인트 정책이 모두 겹쳐 적용되므로, 접근이 안 될 때는 어느 계층에서 막혔는지 순서대로 봐야 합니다.</p>

<h3 id="크로스-계정으로-s3에-접근하려면">크로스 계정으로 S3에 접근하려면</h3>

<p>버킷 정책에서 상대 계정·역할을 허용하고, 접근하는 쪽 역할에도 해당 버킷 권한을 줍니다. <strong>양쪽 모두</strong> 필요합니다.</p>

<p>한 가지 흔한 함정이 <strong>객체 소유권</strong>입니다. 다른 계정이 버킷에 객체를 쓰면 기본적으로 그 계정이 객체 소유자가 되어, 버킷 소유자가 읽지 못하는 상황이 생깁니다. <code class="language-plaintext highlighter-rouge">bucket-owner-full-control</code> ACL을 지정하거나 Bucket Ownership Enforced 설정을 켜서 해결합니다.</p>

<h3 id="데이터-파이프라인-비용을-줄이는-방법은-무엇인가요">데이터 파이프라인 비용을 줄이는 방법은 무엇인가요</h3>

<ul>
  <li><strong>EMR 스팟 인스턴스</strong> — Core 노드는 온디맨드, Task 노드는 스팟으로. 중단돼도 재계산 가능한 부분에만 스팟을 씁니다</li>
  <li><strong>클러스터 수명 단축</strong> — 상시 클러스터 대신 잡마다 뜨고 지는 transient 클러스터</li>
  <li><strong>S3 라이프사이클</strong> — 오래된 파티션을 IA·Glacier로 이동</li>
  <li><strong>스캔량 감소</strong> — 파티셔닝과 Parquet. Athena·Spectrum 비용에 직접 영향</li>
  <li><strong>작은 파일 정리</strong> — 요청 수와 태스크 오버헤드를 동시에 줄임</li>
  <li><strong>크로스 리전 전송 제거</strong> — 컴퓨트와 데이터를 같은 리전에</li>
</ul>

<p>비용 원인을 찾을 때는 Cost Explorer에서 서비스별로 나눈 뒤, S3라면 요청 수인지 저장인지 전송인지까지 내려가 봐야 합니다.</p>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="aws" /><category term="s3" /><category term="emr" /><category term="redshift" /><category term="interview" /><summary type="html"><![CDATA[S3 스토리지 클래스와 성능, Glue와 EMR, Redshift 분산 설계, Kinesis와 MSK, IAM 권한 모델, 비용 최적화를 데이터 엔지니어 관점에서 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — Airflow</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Airflow/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — Airflow" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Airflow</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Airflow/"><![CDATA[<h2 id="기본-구조">기본 구조</h2>

<h3 id="airflow의-구성-요소를-설명해보세요">Airflow의 구성 요소를 설명해보세요</h3>

<ul>
  <li><strong>Scheduler</strong> — DAG를 파싱하고 실행할 태스크를 판단해 큐에 넣습니다</li>
  <li><strong>Executor</strong> — 태스크를 실제로 실행할 방식을 결정합니다</li>
  <li><strong>Worker</strong> — 태스크를 수행합니다(실행자에 따라 존재 여부가 다름)</li>
  <li><strong>Metadata DB</strong> — DAG·태스크 상태, 실행 이력, 연결 정보를 저장합니다</li>
  <li><strong>Webserver</strong> — UI</li>
</ul>

<p>핵심은 <strong>메타데이터 DB가 단일 진실 공급원</strong>이라는 점입니다. 스케줄러도 워커도 여기를 보고 판단하므로, DB 성능이 전체 처리량을 좌우합니다.</p>

<h3 id="dag는-무엇이고-왜-순환이-없어야-하나요">DAG는 무엇이고 왜 순환이 없어야 하나요</h3>

<p>방향성 비순환 그래프(Directed Acyclic Graph)입니다. 태스크가 노드, 의존 관계가 엣지입니다.</p>

<p>순환이 있으면 <strong>어느 태스크부터 실행할지 결정할 수 없습니다.</strong> A가 B를 기다리고 B가 A를 기다리면 영원히 시작하지 못합니다. Airflow는 DAG 파싱 시 순환을 감지해 오류를 냅니다.</p>

<h3 id="dag-파일이-파싱되는-방식을-설명해보세요">DAG 파일이 파싱되는 방식을 설명해보세요</h3>

<p>스케줄러가 DAG 폴더의 파이썬 파일을 <strong>주기적으로 실행</strong>해 DAG 객체를 수집합니다. 이게 몇 가지 함정을 만듭니다.</p>

<p><strong>최상위 코드는 파싱할 때마다 실행됩니다.</strong> DAG 파일 상단에서 DB를 조회하거나 API를 호출하면 수십 초마다 그 호출이 일어납니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 나쁨 — 파싱마다 실행됨
</span><span class="n">rows</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="s">"SELECT ..."</span><span class="p">)</span>

<span class="k">with</span> <span class="n">DAG</span><span class="p">(...)</span> <span class="k">as</span> <span class="n">dag</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">r</span> <span class="ow">in</span> <span class="n">rows</span><span class="p">:</span>
        <span class="n">PythonOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="sa">f</span><span class="s">"t_</span><span class="si">{</span><span class="n">r</span><span class="p">.</span><span class="nb">id</span><span class="si">}</span><span class="s">"</span><span class="p">,</span> <span class="p">...)</span>

<span class="c1"># 좋음 — 실행 시점에만 동작
</span><span class="k">def</span> <span class="nf">fetch_and_process</span><span class="p">(</span><span class="o">**</span><span class="n">context</span><span class="p">):</span>
    <span class="n">rows</span> <span class="o">=</span> <span class="n">db</span><span class="p">.</span><span class="n">query</span><span class="p">(</span><span class="s">"SELECT ..."</span><span class="p">)</span>
    <span class="p">...</span>

<span class="k">with</span> <span class="n">DAG</span><span class="p">(...)</span> <span class="k">as</span> <span class="n">dag</span><span class="p">:</span>
    <span class="n">PythonOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"process"</span><span class="p">,</span> <span class="n">python_callable</span><span class="o">=</span><span class="n">fetch_and_process</span><span class="p">)</span>
</code></pre></div></div>

<p>파싱 시간이 길어지면 스케줄러 전체가 느려집니다. <code class="language-plaintext highlighter-rouge">dagbag_import_timeout</code>을 넘기면 아예 DAG가 사라집니다.</p>

<h2 id="스케줄링">스케줄링</h2>

<h3 id="execution_datelogical-date는-무엇인가요">execution_date(logical date)는 무엇인가요</h3>

<p>Airflow에서 가장 헷갈리는 개념입니다. <strong>처리 대상 기간의 시작점</strong>을 가리키며, 실제 실행 시각이 아닙니다.</p>

<p><code class="language-plaintext highlighter-rouge">schedule="@daily"</code>인 DAG에서 <code class="language-plaintext highlighter-rouge">2026-08-11</code> 실행은 <strong>2026-08-12 00:00에 시작</strong>됩니다. 8월 11일 하루치 데이터가 다 모인 뒤에 처리하는 것이 자연스럽기 때문입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>데이터 구간: [2026-08-11 00:00, 2026-08-12 00:00)
logical date: 2026-08-11
실제 실행:    2026-08-12 00:00
</code></pre></div></div>

<p>이 설계 덕분에 <strong>백필과 정기 실행이 같은 코드로 동작</strong>합니다. 날짜를 파라미터로 받으니 과거 어느 날짜든 똑같이 돌릴 수 있습니다.</p>

<p>Airflow 2.2부터는 <code class="language-plaintext highlighter-rouge">data_interval_start</code> / <code class="language-plaintext highlighter-rouge">data_interval_end</code>라는 더 명확한 이름이 생겼습니다.</p>

<h3 id="코드에-current_date를-쓰면-왜-안-되나요">코드에 CURRENT_DATE를 쓰면 왜 안 되나요</h3>

<p>백필이 불가능해지기 때문입니다. 과거 날짜로 실행해도 쿼리는 항상 오늘을 보게 됩니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 나쁨 — 언제 돌려도 오늘 기준</span>
<span class="k">WHERE</span> <span class="n">dt</span> <span class="o">=</span> <span class="k">CURRENT_DATE</span> <span class="o">-</span> <span class="mi">1</span>

<span class="c1">-- 좋음 — 실행 컨텍스트의 날짜를 주입</span>
<span class="k">WHERE</span> <span class="n">dt</span> <span class="o">=</span> <span class="s1">'{{ ds }}'</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">{{ ds }}</code></td>
      <td>logical date (YYYY-MM-DD)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{{ ds_nodash }}</code></td>
      <td>YYYYMMDD</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{{ data_interval_start }}</code></td>
      <td>데이터 구간 시작</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{{ data_interval_end }}</code></td>
      <td>데이터 구간 끝</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">{{ prev_ds }}</code></td>
      <td>이전 실행의 ds</td>
    </tr>
  </tbody>
</table>

<h3 id="catchup은-무엇인가요">catchup은 무엇인가요</h3>

<p><code class="language-plaintext highlighter-rouge">start_date</code>부터 현재까지 밀린 실행을 자동으로 채우는 기능입니다. 기본값이 <code class="language-plaintext highlighter-rouge">True</code>라, 과거 날짜로 <code class="language-plaintext highlighter-rouge">start_date</code>를 잡고 DAG를 켜면 <strong>수백 개 실행이 한꺼번에 시작됩니다.</strong></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">DAG</span><span class="p">(</span>
    <span class="n">dag_id</span><span class="o">=</span><span class="s">"daily_agg"</span><span class="p">,</span>
    <span class="n">start_date</span><span class="o">=</span><span class="n">datetime</span><span class="p">(</span><span class="mi">2024</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
    <span class="n">schedule</span><span class="o">=</span><span class="s">"@daily"</span><span class="p">,</span>
    <span class="n">catchup</span><span class="o">=</span><span class="bp">False</span><span class="p">,</span>          <span class="c1"># 밀린 것 무시, 다음 주기부터
</span>    <span class="n">max_active_runs</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span>      <span class="c1"># 백필할 때도 동시 실행 제한
</span><span class="p">):</span>
</code></pre></div></div>

<p>과거 데이터를 채워야 한다면 <code class="language-plaintext highlighter-rouge">catchup=True</code>로 두되 <code class="language-plaintext highlighter-rouge">max_active_runs</code>로 동시성을 제한해야 소스 시스템이 버팁니다.</p>

<h3 id="dag-간-의존성은-어떻게-표현하나요">DAG 간 의존성은 어떻게 표현하나요</h3>

<ul>
  <li><strong>TriggerDagRunOperator</strong> — 다른 DAG를 직접 트리거. 밀어내는(push) 방식</li>
  <li><strong>ExternalTaskSensor</strong> — 다른 DAG의 태스크 완료를 기다림. 당기는(pull) 방식. <strong>스케줄 주기가 다르면 시각 매칭이 까다롭습니다</strong></li>
  <li><strong>Dataset(데이터 인식 스케줄링)</strong> — 업스트림이 데이터셋을 갱신하면 다운스트림이 자동 실행. Airflow 2.4+</li>
</ul>

<p>Dataset 방식이 가장 깔끔합니다. 시각이 아니라 <strong>데이터 준비 여부</strong>로 트리거되므로 스케줄 간 불일치 문제가 없습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">orders</span> <span class="o">=</span> <span class="n">Dataset</span><span class="p">(</span><span class="s">"s3://warehouse/orders"</span><span class="p">)</span>

<span class="c1"># 생산자
</span><span class="k">with</span> <span class="n">DAG</span><span class="p">(</span><span class="s">"load_orders"</span><span class="p">,</span> <span class="p">...):</span>
    <span class="n">PythonOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"load"</span><span class="p">,</span> <span class="n">outlets</span><span class="o">=</span><span class="p">[</span><span class="n">orders</span><span class="p">],</span> <span class="p">...)</span>

<span class="c1"># 소비자 — orders 가 갱신되면 실행
</span><span class="k">with</span> <span class="n">DAG</span><span class="p">(</span><span class="s">"build_mart"</span><span class="p">,</span> <span class="n">schedule</span><span class="o">=</span><span class="p">[</span><span class="n">orders</span><span class="p">],</span> <span class="p">...):</span>
    <span class="p">...</span>
</code></pre></div></div>

<h2 id="태스크">태스크</h2>

<h3 id="xcom은-무엇이고-무엇을-주의해야-하나요">XCom은 무엇이고 무엇을 주의해야 하나요</h3>

<p>태스크 간에 작은 값을 주고받는 메커니즘입니다. <strong>메타데이터 DB에 저장</strong>되므로 큰 데이터를 넣으면 안 됩니다.</p>

<p>파일 경로나 식별자만 넘기고 실제 데이터는 오브젝트 스토리지에 두는 것이 원칙입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 나쁨 — DataFrame 을 XCom 에
</span><span class="k">return</span> <span class="n">df</span>

<span class="c1"># 좋음 — 경로만 넘김
</span><span class="n">df</span><span class="p">.</span><span class="n">to_parquet</span><span class="p">(</span><span class="n">path</span><span class="p">)</span>
<span class="k">return</span> <span class="n">path</span>
</code></pre></div></div>

<h3 id="operator-sensor-hook을-구분해보세요">Operator, Sensor, Hook을 구분해보세요</h3>

<ul>
  <li><strong>Operator</strong> — 하나의 작업 단위. <code class="language-plaintext highlighter-rouge">PythonOperator</code>, <code class="language-plaintext highlighter-rouge">BashOperator</code>, <code class="language-plaintext highlighter-rouge">SparkSubmitOperator</code></li>
  <li><strong>Sensor</strong> — 조건이 충족될 때까지 기다리는 특수한 Operator. 파일 도착, 파티션 생성, 외부 태스크 완료</li>
  <li><strong>Hook</strong> — 외부 시스템 접속을 감싼 인터페이스. Operator 내부에서 씁니다. <code class="language-plaintext highlighter-rouge">PostgresHook</code>, <code class="language-plaintext highlighter-rouge">S3Hook</code></li>
</ul>

<h3 id="sensor가-워커를-잡아먹는-문제는-어떻게-해결하나요">Sensor가 워커를 잡아먹는 문제는 어떻게 해결하나요</h3>

<p>기본 모드(<code class="language-plaintext highlighter-rouge">poke</code>)의 Sensor는 대기하는 동안 <strong>워커 슬롯을 계속 점유</strong>합니다. 센서가 많으면 실제 작업이 실행되지 못하고 데드락처럼 보이는 상황이 생깁니다.</p>

<p><code class="language-plaintext highlighter-rouge">mode="reschedule"</code>로 바꾸면 poke 사이에 슬롯을 반납합니다. 대기 시간이 길면 반드시 이 모드를 써야 합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">S3KeySensor</span><span class="p">(</span>
    <span class="n">task_id</span><span class="o">=</span><span class="s">"wait_file"</span><span class="p">,</span>
    <span class="n">bucket_key</span><span class="o">=</span><span class="s">"s3://bucket/data/{{ ds }}/_SUCCESS"</span><span class="p">,</span>
    <span class="n">mode</span><span class="o">=</span><span class="s">"reschedule"</span><span class="p">,</span>        <span class="c1"># 대기 중 슬롯 반납
</span>    <span class="n">poke_interval</span><span class="o">=</span><span class="mi">300</span><span class="p">,</span>
    <span class="n">timeout</span><span class="o">=</span><span class="mi">60</span> <span class="o">*</span> <span class="mi">60</span> <span class="o">*</span> <span class="mi">6</span><span class="p">,</span>      <span class="c1"># 타임아웃 필수
</span><span class="p">)</span>
</code></pre></div></div>

<p>Airflow 2.2+의 <strong>Deferrable Operator</strong>는 더 나아가, 대기를 Triggerer 프로세스에 넘기고 워커를 완전히 비웁니다. 대기가 많은 파이프라인에서 자원 효율이 크게 좋아집니다.</p>

<p><code class="language-plaintext highlighter-rouge">timeout</code>을 안 걸면 조건이 영영 충족되지 않을 때 무한정 대기합니다.</p>

<h3 id="실행자executor-선택-기준은-무엇인가요">실행자(Executor) 선택 기준은 무엇인가요</h3>

<table>
  <thead>
    <tr>
      <th>Executor</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sequential</td>
      <td>하나씩. 개발용</td>
    </tr>
    <tr>
      <td>Local</td>
      <td>단일 머신 멀티프로세스. 소규모</td>
    </tr>
    <tr>
      <td>Celery</td>
      <td>워커 풀 + 메시지 브로커. 시작 빠름, 환경 고정</td>
    </tr>
    <tr>
      <td>Kubernetes</td>
      <td>태스크마다 파드. 격리·유연하지만 시작 지연</td>
    </tr>
    <tr>
      <td>CeleryKubernetes</td>
      <td>둘 혼용</td>
    </tr>
  </tbody>
</table>

<p>짧은 태스크가 많으면 Celery, 태스크마다 다른 의존성·리소스가 필요하면 Kubernetes가 맞습니다. Kubernetes 실행자는 파드 시작에 수 초~수십 초가 걸려서, 1초짜리 태스크 1000개에는 부적합합니다.</p>

<h2 id="운영">운영</h2>

<h3 id="태스크를-멱등하게-만들어야-하는-이유는-무엇인가요">태스크를 멱등하게 만들어야 하는 이유는 무엇인가요</h3>

<p>재시도, 백필, 수동 재실행이 일상적으로 일어나기 때문입니다. 같은 <code class="language-plaintext highlighter-rouge">logical date</code>로 여러 번 돌려도 결과가 같아야 안심하고 재실행할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 나쁨 — 재실행하면 중복 적재
</span><span class="n">INSERT</span> <span class="n">INTO</span> <span class="n">target</span> <span class="n">SELECT</span> <span class="o">*</span> <span class="n">FROM</span> <span class="n">source</span> <span class="n">WHERE</span> <span class="n">dt</span> <span class="o">=</span> <span class="s">'{{ ds }}'</span>

<span class="c1"># 좋음 — 해당 파티션을 지우고 다시 씀
</span><span class="n">DELETE</span> <span class="n">FROM</span> <span class="n">target</span> <span class="n">WHERE</span> <span class="n">dt</span> <span class="o">=</span> <span class="s">'{{ ds }}'</span><span class="p">;</span>
<span class="n">INSERT</span> <span class="n">INTO</span> <span class="n">target</span> <span class="n">SELECT</span> <span class="o">*</span> <span class="n">FROM</span> <span class="n">source</span> <span class="n">WHERE</span> <span class="n">dt</span> <span class="o">=</span> <span class="s">'{{ ds }}'</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="재시도는-어떻게-설정하나요">재시도는 어떻게 설정하나요</h3>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">default_args</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"retries"</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
    <span class="s">"retry_delay"</span><span class="p">:</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">minutes</span><span class="o">=</span><span class="mi">5</span><span class="p">),</span>
    <span class="s">"retry_exponential_backoff"</span><span class="p">:</span> <span class="bp">True</span><span class="p">,</span>
    <span class="s">"max_retry_delay"</span><span class="p">:</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">1</span><span class="p">),</span>
    <span class="s">"execution_timeout"</span><span class="p">:</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">hours</span><span class="o">=</span><span class="mi">2</span><span class="p">),</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong><code class="language-plaintext highlighter-rouge">execution_timeout</code>이 중요합니다.</strong> 없으면 멈춘 태스크가 영원히 running 상태로 남아 슬롯을 잡습니다.</p>

<p>그리고 <strong>재시도가 의미 있는 실패인지</strong> 구분해야 합니다. 일시적 네트워크 오류는 재시도가 맞지만, 잘못된 스키마나 권한 문제는 몇 번을 돌려도 실패합니다. 이런 경우 재시도는 알림만 늦출 뿐입니다.</p>

<h3 id="동시성을-제어하는-설정에는-무엇이-있나요">동시성을 제어하는 설정에는 무엇이 있나요</h3>

<table>
  <thead>
    <tr>
      <th>설정</th>
      <th>범위</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">parallelism</code></td>
      <td>전체 클러스터의 동시 태스크 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max_active_runs</code></td>
      <td>DAG 하나의 동시 실행 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max_active_tasks</code> (구 <code class="language-plaintext highlighter-rouge">concurrency</code>)</td>
      <td>DAG 하나의 동시 태스크 수</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">max_active_tis_per_dag</code></td>
      <td>특정 태스크의 동시 실행 수</td>
    </tr>
    <tr>
      <td><strong>Pool</strong></td>
      <td>자원별 슬롯 제한</td>
    </tr>
  </tbody>
</table>

<p><strong>Pool</strong>이 실무에서 특히 유용합니다. DB 커넥션이 10개뿐이라면 그 DB를 쓰는 태스크들을 슬롯 10짜리 풀에 넣어 과부하를 막습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PythonOperator</span><span class="p">(</span><span class="n">task_id</span><span class="o">=</span><span class="s">"query_db"</span><span class="p">,</span> <span class="n">pool</span><span class="o">=</span><span class="s">"warehouse_pool"</span><span class="p">,</span> <span class="p">...)</span>
</code></pre></div></div>

<h3 id="스케줄러가-느려지는-원인은-무엇인가요">스케줄러가 느려지는 원인은 무엇인가요</h3>

<ul>
  <li><strong>DAG 파싱 시간</strong> — 최상위 코드에서 무거운 작업을 하는 경우</li>
  <li><strong>DAG 파일 수</strong> — 파일이 수천 개면 파싱 주기가 길어집니다</li>
  <li><strong>메타데이터 DB 부하</strong> — 태스크 인스턴스 기록이 수백만 건 쌓이면 쿼리가 느려집니다</li>
  <li><strong>동적 DAG 생성 남용</strong> — 실행 때마다 구조가 바뀌면 부담이 큽니다</li>
</ul>

<p>메타데이터는 <code class="language-plaintext highlighter-rouge">airflow db clean</code>으로 주기적으로 정리해야 합니다. 로그도 함께 쌓이므로 보관 정책이 필요합니다.</p>

<h3 id="태스크가-계속-queued-상태입니다-원인이-무엇일까요">태스크가 계속 queued 상태입니다. 원인이 무엇일까요</h3>

<p>스케줄러는 실행을 결정했는데 워커가 못 집어가는 상태입니다.</p>

<ol>
  <li><strong>워커 부족</strong> — 워커가 죽었거나 개수가 모자람</li>
  <li><strong>Pool 슬롯 소진</strong> — 해당 풀이 꽉 참</li>
  <li><strong>동시성 상한 도달</strong> — <code class="language-plaintext highlighter-rouge">parallelism</code>, <code class="language-plaintext highlighter-rouge">max_active_tasks</code></li>
  <li><strong>큐 불일치</strong> — 태스크가 지정한 큐를 처리하는 워커가 없음</li>
  <li><strong>K8s 실행자에서 파드 Pending</strong> — 노드 자원 부족</li>
</ol>

<p>UI의 Pool 화면과 워커 로그를 함께 봐야 원인이 좁혀집니다.</p>

<h3 id="dag를-어떻게-테스트하나요">DAG를 어떻게 테스트하나요</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 파싱 오류 확인 — 가장 먼저</span>
python dags/my_dag.py

<span class="c"># DAG 구조 확인</span>
airflow dags list
airflow tasks list my_dag <span class="nt">--tree</span>

<span class="c"># 태스크 하나만 실행 (의존성 무시)</span>
airflow tasks <span class="nb">test </span>my_dag my_task 2026-08-11

<span class="c"># DAG 전체를 특정 날짜로 실행</span>
airflow dags <span class="nb">test </span>my_dag 2026-08-11
</code></pre></div></div>

<p>CI에서는 최소한 <strong>모든 DAG 파일의 임포트 검증과 순환 검사</strong>를 돌리는 것이 좋습니다. 배포 후에 파싱 오류를 발견하면 그 DAG가 UI에서 사라져버립니다.</p>

<h3 id="airflow가-적합하지-않은-경우는-언제인가요">Airflow가 적합하지 않은 경우는 언제인가요</h3>

<ul>
  <li><strong>초 단위 지연이 필요한 스트리밍</strong> — Airflow는 배치 오케스트레이터입니다</li>
  <li><strong>아주 잦은 실행</strong>(분 단위 이하) — 스케줄러 오버헤드가 큽니다</li>
  <li><strong>단순 크론 하나</strong> — 관리 부담만 늘어납니다. Cloud Scheduler 같은 것으로 충분합니다</li>
  <li><strong>데이터 처리 자체</strong> — Airflow는 <strong>조율</strong>하는 도구입니다. 워커에서 대용량을 직접 처리하지 말고 Spark·BigQuery에 넘겨야 합니다</li>
</ul>

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

<ul>
  <li><a href="/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A7%88%ED%8A%B8-%EA%B5%AC%EC%B6%95/">데이터 엔지니어 인터뷰 — 데이터 마트 구축</a></li>
</ul>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="airflow" /><category term="orchestration" /><category term="interview" /><summary type="html"><![CDATA[DAG와 스케줄링 모델, execution_date와 백필, 태스크 간 데이터 전달, 실행자 선택, 멱등성과 재시도, 센서와 동시성 제어를 질문과 핵심 답변으로 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — BigQuery</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-BigQuery/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — BigQuery" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-BigQuery</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-BigQuery/"><![CDATA[<h2 id="구조">구조</h2>

<h3 id="bigquery의-아키텍처를-설명해보세요">BigQuery의 아키텍처를 설명해보세요</h3>

<p><strong>저장과 연산이 완전히 분리</strong>되어 있습니다. 데이터는 Colossus(분산 파일시스템)에 컬럼너 포맷(Capacitor)으로 저장되고, 쿼리는 Dremel 엔진이 처리하며, 그 사이를 Jupiter 네트워크가 연결합니다.</p>

<p>이 구조 덕분에 클러스터를 미리 띄워둘 필요가 없고, 저장 비용과 연산 비용이 독립적으로 과금됩니다. 반대로 <strong>연산 노드에 데이터를 붙여두는 캐시 효과가 없어서</strong>, 매번 스토리지에서 읽어옵니다. 그래서 스캔량 관리가 성능이자 비용입니다.</p>

<h3 id="슬롯slot은-무엇인가요">슬롯(slot)은 무엇인가요</h3>

<p>쿼리를 실행하는 연산 단위입니다. 쿼리가 실행 계획으로 쪼개지면 각 단계가 슬롯에 배정되어 병렬 처리됩니다.</p>

<p>과금 모델이 둘입니다.</p>

<table>
  <thead>
    <tr>
      <th>모델</th>
      <th>과금 기준</th>
      <th>적합한 경우</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>On-demand</td>
      <td>스캔한 바이트</td>
      <td>사용량이 불규칙, 소규모</td>
    </tr>
    <tr>
      <td>Capacity (Editions)</td>
      <td>예약한 슬롯 수 × 시간</td>
      <td>사용량이 크고 예측 가능</td>
    </tr>
  </tbody>
</table>

<p>Capacity 모델에서는 스캔량이 아무리 커도 추가 비용이 없는 대신, 슬롯이 부족하면 쿼리가 대기합니다. 예약(reservation)을 팀·워크로드별로 나눠 중요한 배치가 애드혹 쿼리에 밀리지 않게 하는 것이 운영 포인트입니다.</p>

<h2 id="파티셔닝과-클러스터링">파티셔닝과 클러스터링</h2>

<h3 id="파티셔닝과-클러스터링의-차이는-무엇인가요">파티셔닝과 클러스터링의 차이는 무엇인가요</h3>

<p><strong>파티셔닝</strong>은 테이블을 물리적으로 나눕니다. 조건에 맞지 않는 파티션은 아예 읽지 않습니다(파티션 프루닝).</p>

<ul>
  <li>시간 단위(일/시간/월/연)</li>
  <li>정수 범위</li>
  <li>인제스트 시각(<code class="language-plaintext highlighter-rouge">_PARTITIONTIME</code>)</li>
</ul>

<p><strong>클러스터링</strong>은 파티션 안에서 지정한 컬럼 순으로 데이터를 정렬해 블록을 만듭니다. 조건에 맞는 블록만 읽어 스캔량을 줄입니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">ds</span><span class="p">.</span><span class="n">events</span> <span class="p">(</span>
  <span class="n">event_time</span> <span class="nb">TIMESTAMP</span><span class="p">,</span>
  <span class="n">user_id</span>    <span class="n">STRING</span><span class="p">,</span>
  <span class="n">country</span>    <span class="n">STRING</span><span class="p">,</span>
  <span class="n">payload</span>    <span class="n">JSON</span>
<span class="p">)</span>
<span class="k">PARTITION</span> <span class="k">BY</span> <span class="nb">DATE</span><span class="p">(</span><span class="n">event_time</span><span class="p">)</span>
<span class="k">CLUSTER</span> <span class="k">BY</span> <span class="n">country</span><span class="p">,</span> <span class="n">user_id</span><span class="p">;</span>
</code></pre></div></div>

<p>파티션은 <strong>개수 제한</strong>(테이블당 4,000개)이 있고 카디널리티가 낮아야 하지만, 클러스터링은 카디널리티가 높은 컬럼에 적합합니다. 보통 <strong>날짜로 파티션, 자주 필터링하는 컬럼으로 클러스터링</strong>하는 조합을 씁니다.</p>

<h3 id="클러스터링-컬럼의-순서가-왜-중요한가요">클러스터링 컬럼의 순서가 왜 중요한가요</h3>

<p>정렬 순서대로 프루닝이 적용되므로, <strong>선행 컬럼 없이는 효과가 약합니다.</strong> 복합 인덱스와 같은 원리입니다.</p>

<p><code class="language-plaintext highlighter-rouge">CLUSTER BY country, user_id</code>라면 <code class="language-plaintext highlighter-rouge">country</code> 조건만으로도 효과가 있지만, <code class="language-plaintext highlighter-rouge">user_id</code>만으로 필터링하면 효과가 크게 떨어집니다. 자주 쓰는 조건을 앞에 둬야 합니다.</p>

<h3 id="파티션-필터를-강제할-수-있나요">파티션 필터를 강제할 수 있나요</h3>

<p><code class="language-plaintext highlighter-rouge">require_partition_filter</code>를 켜면 파티션 조건 없는 쿼리를 거부합니다. 실수로 전체 스캔이 도는 것을 막는 안전장치입니다.</p>

<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">ds</span><span class="p">.</span><span class="n">events</span> <span class="k">SET</span> <span class="k">OPTIONS</span> <span class="p">(</span><span class="n">require_partition_filter</span> <span class="o">=</span> <span class="k">true</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="비용">비용</h2>

<h3 id="쿼리-비용을-줄이는-방법을-설명해보세요">쿼리 비용을 줄이는 방법을 설명해보세요</h3>

<p>On-demand 모델에서는 <strong>읽은 컬럼의 바이트 수</strong>로 과금됩니다. 행 수가 아니라 컬럼 크기라는 점이 중요합니다.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">SELECT *</code> 금지</strong> — 필요한 컬럼만. 컬럼너 저장이라 안 읽은 컬럼은 과금되지 않습니다</li>
  <li><strong>파티션 조건 필수</strong> — 파티션 컬럼을 가공하면 프루닝이 안 됩니다</li>
  <li><strong><code class="language-plaintext highlighter-rouge">LIMIT</code>은 비용을 줄이지 않습니다</strong> — 스캔은 이미 다 하고 결과만 자릅니다</li>
  <li><strong>미리보기 활용</strong> — 테이블 내용을 볼 때는 <code class="language-plaintext highlighter-rouge">tabledata.list</code>(미리보기)나 콘솔 프리뷰가 무료입니다</li>
  <li><strong>결과 캐시</strong> — 동일 쿼리는 24시간 캐시되어 무료. 단 <code class="language-plaintext highlighter-rouge">CURRENT_TIMESTAMP()</code> 같은 비결정적 함수가 있으면 캐시되지 않습니다</li>
  <li><strong>물리화(materialized view)</strong> — 반복되는 집계를 미리 계산</li>
</ul>

<p>실행 전에 스캔량을 확인하는 습관이 중요합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bq query <span class="nt">--dry_run</span> <span class="nt">--use_legacy_sql</span><span class="o">=</span><span class="nb">false</span> <span class="s1">'SELECT ...'</span>
</code></pre></div></div>

<p>콘솔에서도 쿼리 입력 시 오른쪽 위에 예상 스캔량이 표시됩니다.</p>

<h3 id="스토리지-비용은-어떻게-줄이나요">스토리지 비용은 어떻게 줄이나요</h3>

<p>90일 이상 수정되지 않은 파티션은 자동으로 <strong>long-term storage</strong> 요금(약 절반)이 적용됩니다. 파티션 단위로 판정되므로, 오래된 파티션을 굳이 건드리지 않는 것이 유리합니다.</p>

<p>전체 테이블을 매일 덮어쓰는 방식은 모든 파티션의 수정 시각을 갱신해 long-term 할인을 날립니다. <strong>증분 적재</strong>가 비용 면에서도 유리한 이유입니다.</p>

<p>파티션 만료(<code class="language-plaintext highlighter-rouge">partition_expiration_days</code>)를 걸어 오래된 데이터를 자동 삭제하는 것도 방법입니다.</p>

<h3 id="비용이-갑자기-늘었습니다-어떻게-원인을-찾나요">비용이 갑자기 늘었습니다. 어떻게 원인을 찾나요</h3>

<p><code class="language-plaintext highlighter-rouge">INFORMATION_SCHEMA.JOBS</code>를 조회하면 사용자·쿼리별 스캔량을 볼 수 있습니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span>
  <span class="n">user_email</span><span class="p">,</span>
  <span class="n">job_id</span><span class="p">,</span>
  <span class="n">total_bytes_processed</span> <span class="o">/</span> <span class="n">POW</span><span class="p">(</span><span class="mi">1024</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="k">AS</span> <span class="n">tb_processed</span><span class="p">,</span>
  <span class="n">query</span>
<span class="k">FROM</span> <span class="nv">`region-us`</span><span class="p">.</span><span class="n">INFORMATION_SCHEMA</span><span class="p">.</span><span class="n">JOBS_BY_PROJECT</span>
<span class="k">WHERE</span> <span class="n">creation_time</span> <span class="o">&gt;=</span> <span class="n">TIMESTAMP_SUB</span><span class="p">(</span><span class="k">CURRENT_TIMESTAMP</span><span class="p">(),</span> <span class="n">INTERVAL</span> <span class="mi">7</span> <span class="k">DAY</span><span class="p">)</span>
  <span class="k">AND</span> <span class="n">job_type</span> <span class="o">=</span> <span class="s1">'QUERY'</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="n">total_bytes_processed</span> <span class="k">DESC</span>
<span class="k">LIMIT</span> <span class="mi">20</span><span class="p">;</span>
</code></pre></div></div>

<p>흔한 원인은 대시보드의 자동 새로고침, 파티션 조건 없는 애드혹 쿼리, 그리고 <strong>CDC 업서트</strong>입니다. MERGE는 대상 파티션 전체를 다시 쓰므로 잦은 소량 업서트가 비용을 크게 키웁니다.</p>

<h2 id="적재">적재</h2>

<h3 id="데이터를-적재하는-방법에는-무엇이-있나요">데이터를 적재하는 방법에는 무엇이 있나요</h3>

<table>
  <thead>
    <tr>
      <th>방법</th>
      <th>특징</th>
      <th>비용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Load job (배치)</td>
      <td>GCS에서 배치 적재</td>
      <td><strong>무료</strong></td>
    </tr>
    <tr>
      <td>Storage Write API</td>
      <td>스트리밍, 정확히 한 번 지원</td>
      <td>유료(처리량 기준)</td>
    </tr>
    <tr>
      <td>Legacy streaming insert</td>
      <td>구 방식</td>
      <td>유료, 권장하지 않음</td>
    </tr>
    <tr>
      <td>Federated query</td>
      <td>GCS·외부 소스를 외부 테이블로 조회</td>
      <td>스캔량</td>
    </tr>
    <tr>
      <td>Datastream</td>
      <td>CDC 복제</td>
      <td>별도</td>
    </tr>
  </tbody>
</table>

<p><strong>배치 로드가 무료</strong>라는 점이 중요합니다. 지연 요구가 분 단위라면 GCS에 모았다가 배치로 넣는 편이 스트리밍보다 훨씬 저렴합니다.</p>

<h3 id="storage-write-api와-legacy-streaming-insert의-차이는-무엇인가요">Storage Write API와 legacy streaming insert의 차이는 무엇인가요</h3>

<p>Storage Write API가 후속 버전이고 여러 면에서 낫습니다.</p>

<ul>
  <li><strong>정확히 한 번</strong> 시맨틱 지원(스트림 오프셋 기반)</li>
  <li>더 저렴하고 처리량이 큼</li>
  <li>커밋 전까지 데이터가 보이지 않는 <strong>pending 모드</strong> 지원 — 배치처럼 원자적으로 반영 가능</li>
</ul>

<p>legacy streaming insert는 스트리밍 버퍼에 들어간 데이터가 일정 시간 동안 DML로 수정되지 않는 제약이 있었습니다.</p>

<h3 id="cdc를-bigquery에-반영할-때-무엇을-고려하나요">CDC를 BigQuery에 반영할 때 무엇을 고려하나요</h3>

<p>MERGE로 업서트하는 것이 일반적인데, 주의점이 있습니다.</p>

<p><strong>소스에 같은 키가 중복되면 MERGE가 실패</strong>합니다. CDC 피드에는 한 키의 여러 변경이 들어오므로 키별 최신 1건만 남겨야 합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MERGE</span> <span class="k">INTO</span> <span class="n">target</span> <span class="n">t</span>
<span class="k">USING</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="o">*</span> <span class="k">EXCEPT</span><span class="p">(</span><span class="n">rn</span><span class="p">)</span> <span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="o">*</span><span class="p">,</span> <span class="n">ROW_NUMBER</span><span class="p">()</span> <span class="n">OVER</span> <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">id</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">ts</span> <span class="k">DESC</span><span class="p">)</span> <span class="k">AS</span> <span class="n">rn</span>
    <span class="k">FROM</span> <span class="n">staging_changes</span>
  <span class="p">)</span> <span class="k">WHERE</span> <span class="n">rn</span> <span class="o">=</span> <span class="mi">1</span>
<span class="p">)</span> <span class="n">s</span>
<span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">id</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">op</span> <span class="o">=</span> <span class="s1">'DELETE'</span> <span class="k">THEN</span> <span class="k">DELETE</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">THEN</span> <span class="k">UPDATE</span> <span class="k">SET</span> <span class="p">...</span>
<span class="k">WHEN</span> <span class="k">NOT</span> <span class="n">MATCHED</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">op</span> <span class="o">!=</span> <span class="s1">'DELETE'</span> <span class="k">THEN</span> <span class="k">INSERT</span> <span class="p">...;</span>
</code></pre></div></div>

<p><strong>비용 측면</strong>에서는 MERGE가 대상 파티션을 재작성하므로, 파티션을 잘 잡아 영향 범위를 좁히는 것이 중요합니다. 파티션 없이 큰 테이블에 MERGE를 자주 돌리면 비용이 급격히 늘어납니다.</p>

<p>지연 요구가 느슨하다면 변경분을 append로만 쌓고, 조회 시점에 최신 상태를 뽑는 방식(또는 주기적으로 스냅샷 생성)이 훨씬 저렴합니다.</p>

<h2 id="쿼리-작성">쿼리 작성</h2>

<h3 id="중첩반복-필드struct-array를-어떻게-다루나요">중첩·반복 필드(STRUCT, ARRAY)를 어떻게 다루나요</h3>

<p>BigQuery는 중첩 구조를 네이티브로 지원합니다. 정규화해서 조인하는 대신 배열로 품고 있는 편이 성능·비용에 유리한 경우가 많습니다.</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">o</span><span class="p">.</span><span class="n">order_id</span><span class="p">,</span> <span class="n">item</span><span class="p">.</span><span class="n">sku</span><span class="p">,</span> <span class="n">item</span><span class="p">.</span><span class="n">qty</span>
<span class="k">FROM</span> <span class="n">orders</span> <span class="n">o</span><span class="p">,</span> <span class="k">UNNEST</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">items</span><span class="p">)</span> <span class="k">AS</span> <span class="n">item</span><span class="p">;</span>

<span class="c1">-- 배열 안에서 집계</span>
<span class="k">SELECT</span> <span class="n">order_id</span><span class="p">,</span>
       <span class="p">(</span><span class="k">SELECT</span> <span class="k">SUM</span><span class="p">(</span><span class="n">qty</span><span class="p">)</span> <span class="k">FROM</span> <span class="k">UNNEST</span><span class="p">(</span><span class="n">items</span><span class="p">))</span> <span class="k">AS</span> <span class="n">total_qty</span>
<span class="k">FROM</span> <span class="n">orders</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">UNNEST</code> 없이 배열 컬럼을 그대로 두면 스캔 대상에서 제외되므로, 필요할 때만 펼치면 비용을 아낄 수 있습니다.</p>

<h3 id="성능이-안-나오는-쿼리는-어떻게-진단하나요">성능이 안 나오는 쿼리는 어떻게 진단하나요</h3>

<p>실행 세부정보(Execution details)에서 단계별 소요 시간과 처리량을 봅니다. 특히 확인할 것들입니다.</p>

<ul>
  <li><strong>슬롯 대기 시간</strong> — 예약 슬롯 부족</li>
  <li><strong>셔플 바이트</strong> — 조인·집계에서 데이터 이동량이 큰지</li>
  <li><strong>특정 단계의 max vs avg 시간 차이</strong> — 크게 벌어지면 <strong>데이터 skew</strong></li>
</ul>

<p>skew는 조인 키에 특정 값(NULL 포함)이 몰릴 때 생깁니다. 미리 걸러내거나 키를 분산시켜야 합니다.</p>

<p><code class="language-plaintext highlighter-rouge">APPROX_COUNT_DISTINCT</code> 같은 근사 함수도 큰 도움이 됩니다. 정확한 값이 꼭 필요하지 않다면 <code class="language-plaintext highlighter-rouge">COUNT(DISTINCT)</code>보다 훨씬 가볍습니다.</p>

<h3 id="뷰-물리화-뷰-테이블을-어떻게-구분해-쓰나요">뷰, 물리화 뷰, 테이블을 어떻게 구분해 쓰나요</h3>

<ul>
  <li><strong>뷰</strong> — 쿼리를 저장한 것. 조회할 때마다 원본을 스캔합니다. 비용 절감 효과 없음</li>
  <li><strong>물리화 뷰(materialized view)</strong> — 결과를 미리 계산해 저장하고 증분 갱신. 원본이 바뀌면 자동 반영. 집계 패턴이 고정적일 때 효과적</li>
  <li><strong>테이블</strong> — 배치로 만들어 두는 것. 자유롭지만 갱신을 직접 관리</li>
</ul>

<p>물리화 뷰는 지원하는 쿼리 형태에 제약이 있습니다(집계 함수 종류, 조인 제한). 복잡한 변환은 예약 쿼리로 테이블을 만드는 편이 현실적입니다.</p>

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

<ul>
  <li><a href="/data-engineering/spark/2026/08/11/MSK-%EC%BB%A4%EB%84%A5%ED%84%B0-vs-Spark-Streaming-%EB%B9%84%EA%B5%90/">MSK 커넥터 vs Spark Streaming 비교</a></li>
</ul>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="bigquery" /><category term="gcp" /><category term="dwh" /><category term="interview" /><summary type="html"><![CDATA[BigQuery의 아키텍처와 슬롯, 파티셔닝과 클러스터링, 비용 모델과 최적화, 스트리밍 적재와 Storage Write API, MERGE와 CDC 처리를 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — Flink</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Flink/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — Flink" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Flink</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Flink/"><![CDATA[<h2 id="기본-개념">기본 개념</h2>

<h3 id="flink와-spark-streaming의-근본적인-차이는-무엇인가요">Flink와 Spark Streaming의 근본적인 차이는 무엇인가요</h3>

<p>처리 모델이 다릅니다. Spark Structured Streaming은 <strong>마이크로배치</strong>가 기본이라 작은 배치를 반복 실행합니다. Flink는 <strong>레코드 단위 연속 처리</strong>가 기본이라 이벤트가 도착하면 곧바로 흘려보냅니다.</p>

<p>그래서 지연 특성이 다릅니다. Flink는 밀리초 단위 지연이 가능하고, Spark는 배치 주기에 묶입니다. 대신 Spark는 배치 처리와 코드·생태계를 공유한다는 이점이 있습니다.</p>

<p>Flink는 <strong>배치를 스트림의 특수한 경우</strong>(유한한 스트림)로 봅니다. Spark는 반대로 스트림을 배치의 반복으로 봅니다.</p>

<h3 id="jobmanager와-taskmanager의-역할을-설명해보세요">JobManager와 TaskManager의 역할을 설명해보세요</h3>

<p>JobManager는 잡을 스케줄링하고, 체크포인트를 조율하며, 실패 시 복구를 지휘합니다. TaskManager는 실제 연산을 수행하는 워커이고, 내부에 <strong>Task Slot</strong>을 두어 병렬 실행 단위를 나눕니다.</p>

<p>슬롯 수가 그 TaskManager가 동시에 맡을 수 있는 파이프라인 수를 정합니다. 잡의 병렬도(parallelism)는 전체 슬롯 수를 넘을 수 없습니다.</p>

<h3 id="연산자-체이닝operator-chaining은-무엇인가요">연산자 체이닝(operator chaining)은 무엇인가요</h3>

<p>인접한 연산자를 같은 스레드에서 실행해 직렬화와 네트워크 전송을 없애는 최적화입니다. <code class="language-plaintext highlighter-rouge">map → filter</code>처럼 파티셔닝이 바뀌지 않는 구간이 하나로 묶입니다.</p>

<p>프로파일링할 때는 체인이 묶여 있어 어느 연산자가 느린지 안 보일 수 있습니다. <code class="language-plaintext highlighter-rouge">disableChaining()</code>으로 끊어 확인합니다.</p>

<h2 id="시간과-윈도우">시간과 윈도우</h2>

<h3 id="이벤트-시간-처리-시간-인제스트-시간을-구분해보세요">이벤트 시간, 처리 시간, 인제스트 시간을 구분해보세요</h3>

<table>
  <thead>
    <tr>
      <th>시간</th>
      <th>기준</th>
      <th>특성</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Event Time</td>
      <td>이벤트가 실제 발생한 시각</td>
      <td>결과가 재현 가능. 지연·순서 뒤바뀜 처리 필요</td>
    </tr>
    <tr>
      <td>Ingestion Time</td>
      <td>Flink에 들어온 시각</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>Processing Time</td>
      <td>연산자가 처리하는 시각</td>
      <td>가장 빠르지만 재실행 시 결과가 달라짐</td>
    </tr>
  </tbody>
</table>

<p>과거 데이터를 재처리해도 같은 결과가 나와야 한다면 <strong>이벤트 시간</strong>을 써야 합니다.</p>

<h3 id="워터마크는-무엇이고-어떻게-정하나요">워터마크는 무엇이고 어떻게 정하나요</h3>

<p>“이 시각 이전의 이벤트는 더 이상 오지 않는다고 간주한다”는 신호입니다. 윈도우를 언제 닫을지 결정합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">WatermarkStrategy</span>
    <span class="o">.&lt;</span><span class="nc">Event</span><span class="o">&gt;</span><span class="n">forBoundedOutOfOrderness</span><span class="o">(</span><span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">10</span><span class="o">))</span>
    <span class="o">.</span><span class="na">withTimestampAssigner</span><span class="o">((</span><span class="n">e</span><span class="o">,</span> <span class="n">ts</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="n">e</span><span class="o">.</span><span class="na">getEventTime</span><span class="o">());</span>
</code></pre></div></div>

<p>지연 허용을 크게 잡으면 늦은 데이터를 더 많이 담지만 <strong>결과가 그만큼 늦게 나오고 상태도 오래 유지</strong>됩니다. 실제 지연 분포를 측정해서 정하는 것이 맞습니다.</p>

<p>워터마크는 <strong>여러 입력 중 가장 느린 것</strong>을 따릅니다. 파티션 하나가 데이터를 안 보내면 워터마크가 멈춰 윈도우가 영영 닫히지 않습니다. <code class="language-plaintext highlighter-rouge">withIdleness()</code>로 유휴 파티션을 제외할 수 있습니다.</p>

<h3 id="윈도우-종류를-설명해보세요">윈도우 종류를 설명해보세요</h3>

<ul>
  <li><strong>Tumbling</strong> — 겹치지 않는 고정 크기. 5분마다 집계</li>
  <li><strong>Sliding</strong> — 겹치는 고정 크기. 5분 윈도우를 1분마다 (한 이벤트가 여러 윈도우에 속함)</li>
  <li><strong>Session</strong> — 활동 사이 간격(gap)으로 구분. 사용자 세션 분석</li>
  <li><strong>Global</strong> — 자동으로 닫히지 않음. 커스텀 트리거 필요</li>
</ul>

<p>Sliding은 겹치는 만큼 상태와 계산이 배로 늘어납니다. 5분/1분이면 이벤트 하나가 5개 윈도우에 들어갑니다.</p>

<h3 id="늦게-도착한-데이터는-어떻게-처리하나요">늦게 도착한 데이터는 어떻게 처리하나요</h3>

<p>워터마크가 지난 뒤 온 데이터입니다. 세 가지 선택지가 있습니다.</p>

<ol>
  <li><strong>버림</strong> (기본)</li>
  <li><strong>allowedLateness</strong> — 윈도우를 일정 시간 더 열어두고 늦게 온 것을 반영해 재발행</li>
  <li><strong>side output</strong> — 별도 스트림으로 빼서 따로 처리</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">stream</span><span class="o">.</span><span class="na">keyBy</span><span class="o">(...)</span>
      <span class="o">.</span><span class="na">window</span><span class="o">(</span><span class="nc">TumblingEventTimeWindows</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="nc">Time</span><span class="o">.</span><span class="na">minutes</span><span class="o">(</span><span class="mi">5</span><span class="o">)))</span>
      <span class="o">.</span><span class="na">allowedLateness</span><span class="o">(</span><span class="nc">Time</span><span class="o">.</span><span class="na">minutes</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>
      <span class="o">.</span><span class="na">sideOutputLateData</span><span class="o">(</span><span class="n">lateTag</span><span class="o">)</span>
      <span class="o">.</span><span class="na">aggregate</span><span class="o">(...);</span>
</code></pre></div></div>

<p>집계 결과를 이미 다운스트림에 보냈다면, 재발행이 중복인지 갱신인지 싱크가 구분할 수 있어야 합니다.</p>

<h2 id="상태와-체크포인트">상태와 체크포인트</h2>

<h3 id="상태state의-종류를-설명해보세요">상태(state)의 종류를 설명해보세요</h3>

<p><strong>Keyed State</strong>는 키별로 격리된 상태입니다(<code class="language-plaintext highlighter-rouge">ValueState</code>, <code class="language-plaintext highlighter-rouge">ListState</code>, <code class="language-plaintext highlighter-rouge">MapState</code>, <code class="language-plaintext highlighter-rouge">ReducingState</code>). <code class="language-plaintext highlighter-rouge">keyBy</code> 이후에만 쓸 수 있고, 같은 키의 이벤트는 항상 같은 태스크로 갑니다.</p>

<p><strong>Operator State</strong>는 키와 무관하게 연산자 인스턴스가 갖는 상태입니다. Kafka 소스의 오프셋 같은 것이 여기 해당합니다.</p>

<h3 id="체크포인트와-세이브포인트의-차이는-무엇인가요">체크포인트와 세이브포인트의 차이는 무엇인가요</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>체크포인트</th>
      <th>세이브포인트</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>목적</td>
      <td>장애 복구 (자동)</td>
      <td>의도적 중단·재시작 (수동)</td>
    </tr>
    <tr>
      <td>주기</td>
      <td>설정된 간격마다</td>
      <td>사람이 트리거</td>
    </tr>
    <tr>
      <td>수명</td>
      <td>보통 최신 것만 유지</td>
      <td>명시적으로 지울 때까지</td>
    </tr>
    <tr>
      <td>용도</td>
      <td>실패 시 자동 재개</td>
      <td>버전 업그레이드, 병렬도 변경, 코드 배포</td>
    </tr>
  </tbody>
</table>

<p>잡 코드를 바꿔 배포할 때는 세이브포인트를 떠서 중단하고, 새 코드로 그 세이브포인트에서 재개합니다. 상태 스키마가 호환되지 않으면 복원에 실패하므로, 상태를 쓰는 연산자에는 <strong>UID를 고정</strong>해 두는 것이 중요합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">stream</span><span class="o">.</span><span class="na">keyBy</span><span class="o">(...).</span><span class="na">process</span><span class="o">(...).</span><span class="na">uid</span><span class="o">(</span><span class="s">"order-state-processor"</span><span class="o">);</span>
</code></pre></div></div>

<p>UID를 안 주면 자동 생성되는데, 코드가 조금만 바뀌어도 값이 달라져 세이브포인트에서 상태를 못 찾습니다.</p>

<h3 id="exactly-once는-어떻게-달성하나요">exactly-once는 어떻게 달성하나요</h3>

<p>체크포인트 알고리즘(Chandy-Lamport 변형)으로 스트림에 <strong>배리어</strong>를 흘려보내 일관된 스냅샷을 만듭니다. 실패하면 마지막 스냅샷으로 되돌리고 소스 오프셋도 함께 되감습니다.</p>

<p>여기까지는 Flink 내부 상태에 대한 보장입니다. <strong>싱크까지 exactly-once가 되려면</strong> 싱크가 트랜잭션을 지원하고 2PC에 참여해야 합니다(<code class="language-plaintext highlighter-rouge">TwoPhaseCommitSinkFunction</code>). Kafka 싱크는 트랜잭션을 지원하고, 파일 싱크는 커밋 시점에 파일을 이동하는 방식으로 구현합니다.</p>

<p>싱크가 트랜잭션을 지원하지 않으면 at-least-once가 한계이고, 멱등 쓰기로 보완해야 합니다.</p>

<h3 id="상태-백엔드는-어떤-것을-고르나요">상태 백엔드는 어떤 것을 고르나요</h3>

<ul>
  <li><strong>HashMapStateBackend</strong> — 상태를 JVM 힙에 둠. 빠르지만 힙 크기가 상한</li>
  <li><strong>EmbeddedRocksDBStateBackend</strong> — 상태를 로컬 디스크의 RocksDB에 둠. 힙보다 훨씬 큰 상태를 다룰 수 있고 증분 체크포인트 가능. 직렬화 비용으로 느림</li>
</ul>

<p>상태가 GB 단위를 넘어가면 RocksDB가 사실상 유일한 선택입니다. 증분 체크포인트를 켜면 변경분만 저장해 체크포인트 시간이 크게 줄어듭니다.</p>

<h2 id="운영">운영</h2>

<h3 id="백프레셔backpressure는-무엇이고-어떻게-진단하나요">백프레셔(backpressure)는 무엇이고 어떻게 진단하나요</h3>

<p>다운스트림이 처리 속도를 못 따라가면 업스트림으로 압력이 전파되어 소스까지 느려지는 현상입니다. 버퍼가 무한정 쌓여 터지는 대신 자연스럽게 속도를 맞추는 메커니즘입니다.</p>

<p>Flink Web UI의 Backpressure 탭에서 어느 연산자가 원인인지 볼 수 있습니다. <strong>압력을 받는 연산자가 아니라 그 아래에서 막고 있는 연산자</strong>가 진짜 원인입니다.</p>

<p>흔한 원인은 외부 시스템 호출(DB, API) 지연, skew로 인한 특정 서브태스크 과부하, 직렬화 비용입니다.</p>

<h3 id="데이터-skew는-어떻게-다루나요">데이터 skew는 어떻게 다루나요</h3>

<p><code class="language-plaintext highlighter-rouge">keyBy</code>의 키 분포가 치우치면 특정 서브태스크만 바쁩니다. 접근은 Spark와 비슷합니다.</p>

<ul>
  <li>키에 임의 접미사를 붙여 분산한 뒤 2단계 집계</li>
  <li>사전 집계(local aggregation)로 네트워크로 보내는 양을 줄임</li>
  <li>병렬도를 키의 카디널리티에 맞춰 조정</li>
</ul>

<h3 id="잡-재시작-전략에는-무엇이-있나요">잡 재시작 전략에는 무엇이 있나요</h3>

<p><code class="language-plaintext highlighter-rouge">fixed-delay</code>(고정 횟수·간격 재시도), <code class="language-plaintext highlighter-rouge">failure-rate</code>(일정 시간 내 실패율 초과 시 중단), <code class="language-plaintext highlighter-rouge">exponential-delay</code>, <code class="language-plaintext highlighter-rouge">none</code>이 있습니다.</p>

<p>무한 재시도로 두면 근본 원인(잘못된 데이터, 권한 문제)이 있을 때 조용히 계속 실패합니다. 실패율 기반으로 두고 알림을 거는 편이 낫습니다.</p>

<h3 id="flink-sql은-언제-쓰나요">Flink SQL은 언제 쓰나요</h3>

<p>집계·조인 위주의 스트림 처리를 선언적으로 표현할 때 유용합니다. Table API/SQL은 배치와 스트림에서 <strong>같은 쿼리</strong>를 쓸 수 있다는 점이 큰 장점입니다.</p>

<p>다만 복잡한 상태 로직이나 세밀한 제어가 필요하면 DataStream API로 내려가야 합니다. 그리고 SQL로 짠 잡은 계획이 바뀌면 상태 호환성이 깨질 수 있어 업그레이드가 까다롭습니다.</p>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="flink" /><category term="streaming" /><category term="interview" /><summary type="html"><![CDATA[Flink의 스트림 우선 모델, 이벤트 시간과 워터마크, 체크포인트와 정확히 한 번 처리, 상태 관리와 백프레셔를 질문과 핵심 답변으로 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — GCP</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-GCP/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — GCP" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-GCP</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-GCP/"><![CDATA[<h2 id="스토리지">스토리지</h2>

<h3 id="gcs-스토리지-클래스를-설명해보세요">GCS 스토리지 클래스를 설명해보세요</h3>

<table>
  <thead>
    <tr>
      <th>클래스</th>
      <th>최소 보관</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Standard</td>
      <td>없음</td>
      <td>자주 접근</td>
    </tr>
    <tr>
      <td>Nearline</td>
      <td>30일</td>
      <td>월 1회 정도</td>
    </tr>
    <tr>
      <td>Coldline</td>
      <td>90일</td>
      <td>분기 1회 정도</td>
    </tr>
    <tr>
      <td>Archive</td>
      <td>365일</td>
      <td>연 1회 이하, 백업</td>
    </tr>
  </tbody>
</table>

<p>S3와 달리 <strong>모든 클래스가 밀리초 단위 접근</strong>을 제공합니다. Archive도 복원 대기 없이 바로 읽힙니다. 대신 조회 요금과 최소 보관 기간이 붙습니다.</p>

<p>최소 보관 기간 전에 삭제·변경하면 남은 기간만큼 조기 삭제 요금이 부과됩니다. 자주 바뀌는 데이터를 Coldline에 두면 오히려 손해입니다.</p>

<h3 id="버킷-위치는-어떻게-고르나요">버킷 위치는 어떻게 고르나요</h3>

<ul>
  <li><strong>Region</strong> — 단일 리전. 가장 저렴하고 지연이 낮음</li>
  <li><strong>Dual-region</strong> — 두 리전에 복제. 고가용성 + 낮은 지연</li>
  <li><strong>Multi-region</strong> — 대륙 단위. 전역 배포용</li>
</ul>

<p>데이터 처리 파이프라인은 <strong>컴퓨트와 같은 리전</strong>에 두는 것이 기본입니다. 리전이 다르면 전송 비용과 지연이 붙고, BigQuery는 아예 <strong>다른 리전의 GCS에서 로드할 수 없습니다.</strong></p>

<h3 id="gcs와-s3의-차이-중-실무에서-체감되는-것은-무엇인가요">GCS와 S3의 차이 중 실무에서 체감되는 것은 무엇인가요</h3>

<p>가장 큰 차이는 <strong>강한 일관성</strong>입니다. GCS는 처음부터 객체 생성·삭제·목록 조회에 강한 일관성을 제공했습니다.</p>

<p>그리고 <strong>객체 컴포지션</strong>(여러 객체를 이어 붙이기)이나 <strong>재개 가능 업로드</strong>가 네이티브로 지원됩니다.</p>

<p>공통점도 많습니다. 디렉터리가 없고 접두사만 있다는 점, 원자적 rename이 없다는 점은 같아서, 파일 커밋 문제와 테이블 포맷의 필요성은 동일합니다.</p>

<h2 id="처리-엔진">처리 엔진</h2>

<h3 id="dataflow와-dataproc은-어떻게-다른가요">Dataflow와 Dataproc은 어떻게 다른가요</h3>

<p><strong>Dataflow</strong>는 Apache Beam 기반 서버리스 처리 서비스입니다. 클러스터를 관리하지 않고, 오토스케일링과 동적 작업 재분배(dynamic work rebalancing)를 제공합니다. 배치와 스트리밍을 <strong>같은 코드</strong>로 작성할 수 있는 것이 Beam의 특징입니다.</p>

<p><strong>Dataproc</strong>은 관리형 Hadoop/Spark 클러스터입니다. 기존 Spark 코드를 그대로 옮길 수 있고 세밀한 튜닝이 가능합니다.</p>

<p>기준을 단순화하면 — 기존 Spark 자산이 있으면 Dataproc, 새로 만들고 운영 부담을 줄이려면 Dataflow입니다. Dataflow는 특히 <strong>스트리밍에서 오토스케일링과 워터마크 처리가 잘 되어 있습니다.</strong></p>

<h3 id="beam의-핵심-개념을-설명해보세요">Beam의 핵심 개념을 설명해보세요</h3>

<ul>
  <li><strong>PCollection</strong> — 분산 데이터셋. 유한(배치)일 수도 무한(스트림)일 수도 있음</li>
  <li><strong>PTransform</strong> — 변환 연산</li>
  <li><strong>Pipeline</strong> — 전체 그래프</li>
  <li><strong>Window / Trigger / Watermark</strong> — 무한 스트림을 유한 단위로 자르고 언제 결과를 낼지 정함</li>
</ul>

<p>배치와 스트리밍을 통합하는 지점이 <strong>윈도우</strong>입니다. 배치는 “전체가 하나의 윈도우”인 특수한 경우로 취급됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">Runner</code>를 바꾸면 같은 코드가 Dataflow, Flink, Spark 위에서 돌 수 있습니다. 다만 러너별로 지원 기능에 차이가 있습니다.</p>

<h3 id="dataflow의-오토스케일링이-잘-안-되는-경우는-언제인가요">Dataflow의 오토스케일링이 잘 안 되는 경우는 언제인가요</h3>

<ul>
  <li><strong>키 개수가 적을 때</strong> — <code class="language-plaintext highlighter-rouge">GroupByKey</code> 이후 병렬도는 키 수에 묶입니다. 키가 10개면 워커를 늘려도 소용없습니다</li>
  <li><strong>외부 시스템이 병목일 때</strong> — DB 커넥션 한도에 걸리면 워커를 늘려도 대기만 늘어납니다</li>
  <li><strong>핫 키(skew)</strong> — 특정 키에 데이터가 몰리면 그 키를 맡은 워커만 바쁩니다</li>
</ul>

<p>스트리밍에서는 Streaming Engine을 켜면 상태 관리가 워커에서 분리되어 스케일링이 유연해집니다.</p>

<h2 id="메시징">메시징</h2>

<h3 id="pubsub과-kafka의-차이는-무엇인가요">Pub/Sub과 Kafka의 차이는 무엇인가요</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Pub/Sub</th>
      <th>Kafka</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>모델</td>
      <td>완전 관리형 큐/토픽</td>
      <td>로그 기반</td>
    </tr>
    <tr>
      <td>순서 보장</td>
      <td>ordering key 지정 시</td>
      <td>파티션 내</td>
    </tr>
    <tr>
      <td>재처리</td>
      <td>seek(스냅샷·타임스탬프)</td>
      <td>오프셋 되감기</td>
    </tr>
    <tr>
      <td>확장</td>
      <td>자동</td>
      <td>파티션 수 관리</td>
    </tr>
    <tr>
      <td>보관</td>
      <td>기본 7일</td>
      <td>설정</td>
    </tr>
  </tbody>
</table>

<p>Pub/Sub은 <strong>파티션 개념이 노출되지 않습니다.</strong> 확장을 신경 쓸 필요가 없는 대신, 파티션 단위로 병렬도를 통제하는 방식은 쓸 수 없습니다.</p>

<p>기본은 <strong>at-least-once</strong>입니다. exactly-once 전달 옵션이 있지만 리전 내에서만 보장되고, 결국 소비자 멱등성이 중요합니다.</p>

<h3 id="pubsub에서-메시지-중복과-순서를-어떻게-다루나요">Pub/Sub에서 메시지 중복과 순서를 어떻게 다루나요</h3>

<p><strong>중복</strong>은 기본적으로 발생할 수 있습니다. <code class="language-plaintext highlighter-rouge">message_id</code>나 비즈니스 키로 멱등 처리를 해야 합니다. ack 기한(<code class="language-plaintext highlighter-rouge">ackDeadline</code>) 안에 처리를 못 끝내면 재전송되므로, 처리 시간이 길면 기한을 늘리거나 <code class="language-plaintext highlighter-rouge">modifyAckDeadline</code>으로 연장해야 합니다.</p>

<p><strong>순서</strong>는 <code class="language-plaintext highlighter-rouge">orderingKey</code>를 지정하면 같은 키끼리 순서가 보장됩니다. 대신 그 키의 메시지는 직렬 처리되므로 처리량이 떨어지고, 하나가 막히면 뒤가 밀립니다.</p>

<h3 id="데드-레터-토픽은-왜-필요한가요">데드 레터 토픽은 왜 필요한가요</h3>

<p>처리에 계속 실패하는 메시지가 무한 재전송되면 파이프라인 전체가 막힙니다. 최대 전달 횟수를 넘긴 메시지를 별도 토픽으로 보내 격리하고, 나중에 따로 분석·재처리합니다.</p>

<h2 id="오케스트레이션과-cdc">오케스트레이션과 CDC</h2>

<h3 id="cloud-composer는-무엇인가요">Cloud Composer는 무엇인가요</h3>

<p>관리형 Airflow입니다. GKE 위에서 동작하며 스케줄러·웹서버·워커를 GCP가 관리합니다.</p>

<p>주의할 점은 <strong>비용이 상시 발생</strong>한다는 것입니다. DAG가 없어도 환경이 떠 있으면 과금됩니다. 가벼운 스케줄링만 필요하다면 Cloud Scheduler + Cloud Run/Functions 조합이 훨씬 저렴합니다.</p>

<p>DAG는 GCS 버킷에 올리면 자동 동기화됩니다.</p>

<h3 id="datastream은-무엇인가요">Datastream은 무엇인가요</h3>

<p>관리형 CDC 서비스입니다. MySQL·PostgreSQL·Oracle의 변경 로그를 읽어 BigQuery나 GCS로 복제합니다.</p>

<p>BigQuery 대상일 경우 스트리밍 업서트로 반영되는데, 여기서 <strong>비용이 크게 나올 수 있습니다.</strong> 업서트가 대상 파티션을 재작성하므로, 파티셔닝 설계가 잘못되면 요금이 급증합니다. 소스 테이블에 갱신이 잦다면 append-only로 받아서 주기적으로 정리하는 구조도 검토할 만합니다.</p>

<p>소스 DB에 로그 보관 설정(binlog retention, WAL)이 충분해야 하고, 복제 지연이 그 보관 기간을 넘기면 초기 스냅샷부터 다시 해야 합니다.</p>

<h2 id="iam과-네트워크">IAM과 네트워크</h2>

<h3 id="gcp의-리소스-계층과-iam-상속을-설명해보세요">GCP의 리소스 계층과 IAM 상속을 설명해보세요</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Organization
  └── Folder
        └── Project
              └── Resource (버킷, 데이터셋, 인스턴스 …)
</code></pre></div></div>

<p>상위에서 부여한 권한은 <strong>하위로 상속</strong>됩니다. 조직 수준에서 준 역할은 모든 프로젝트에 적용되므로, 넓은 범위에 강한 역할을 주지 않는 것이 중요합니다.</p>

<p>역할은 세 종류입니다.</p>

<ul>
  <li><strong>기본 역할</strong>(Owner/Editor/Viewer) — 너무 광범위해서 운영에서는 지양</li>
  <li><strong>사전 정의 역할</strong> — 서비스별로 세분화됨(<code class="language-plaintext highlighter-rouge">roles/bigquery.dataViewer</code> 등)</li>
  <li><strong>커스텀 역할</strong> — 필요한 권한만 조합</li>
</ul>

<h3 id="서비스-계정을-안전하게-쓰려면">서비스 계정을 안전하게 쓰려면</h3>

<p><strong>키 파일(JSON)을 만들지 않는 것</strong>이 원칙입니다. 유출되면 회수가 어렵고 만료도 없습니다.</p>

<p>대신 쓰는 방법들입니다.</p>

<ul>
  <li><strong>Workload Identity</strong> (GKE) — 쿠버네티스 ServiceAccount를 GCP 서비스 계정에 연결</li>
  <li><strong>Workload Identity Federation</strong> — AWS·GitHub Actions 등 외부 워크로드가 키 없이 GCP 자격 증명을 얻음</li>
  <li><strong>Attached service account</strong> — GCE·Cloud Run에 서비스 계정을 붙이면 메타데이터 서버에서 토큰을 받음</li>
  <li><strong>ADC</strong> — 로컬 개발에서는 <code class="language-plaintext highlighter-rouge">gcloud auth application-default login</code></li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud auth application-default login
gcloud config <span class="nb">set </span>project &lt;PROJECT_ID&gt;
</code></pre></div></div>

<h3 id="vpc-service-controls는-무엇인가요">VPC Service Controls는 무엇인가요</h3>

<p>프로젝트 경계를 넘는 데이터 반출을 막는 경계(perimeter)를 설정하는 기능입니다. IAM 권한이 있어도 경계 밖으로는 데이터를 가져갈 수 없게 합니다.</p>

<p>내부자 위협이나 자격 증명 유출에 대비하는 계층인데, 설정이 까다로워서 <strong>정상 파이프라인까지 막히는 경우</strong>가 많습니다. 도입할 때는 dry-run 모드로 영향을 먼저 확인해야 합니다.</p>

<h2 id="비용">비용</h2>

<h3 id="gcp-데이터-파이프라인-비용은-어디서-나오나요">GCP 데이터 파이프라인 비용은 어디서 나오나요</h3>

<ul>
  <li><strong>BigQuery</strong> — 스캔량(on-demand) 또는 슬롯 예약. 대개 여기가 가장 큼</li>
  <li><strong>GCS</strong> — 저장, 클래스별 조회 요금, 리전 간 전송</li>
  <li><strong>Dataflow</strong> — 워커 vCPU·메모리 × 시간, Streaming Engine, Shuffle</li>
  <li><strong>Composer</strong> — 환경 상시 비용</li>
  <li><strong>Datastream</strong> — 처리 바이트</li>
</ul>

<h3 id="비용을-통제하는-방법은-무엇인가요">비용을 통제하는 방법은 무엇인가요</h3>

<ul>
  <li><strong>예산과 알림</strong> — 프로젝트별 예산을 걸고 임계치 알림</li>
  <li><strong>커스텀 쿼터</strong> — BigQuery는 사용자·프로젝트별 일일 쿼리 바이트 상한을 걸 수 있습니다. 실수로 큰 쿼리를 돌리는 사고를 막는 가장 확실한 장치입니다</li>
  <li><strong>라벨링</strong> — 리소스에 팀·용도 라벨을 붙여 청구서를 분해</li>
  <li><strong>커밋 할인</strong> — 예측 가능한 사용량에 CUD 적용</li>
  <li><strong>preemptible / Spot VM</strong> — Dataproc의 보조 워커에 적용</li>
</ul>

<p>청구 데이터를 BigQuery로 내보내면 세부 분석이 가능합니다. 서비스·라벨·SKU 단위로 어디서 늘었는지 추적할 수 있습니다.</p>

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

<ul>
  <li><a href="/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-BigQuery/">데이터 엔지니어 인터뷰 — BigQuery</a></li>
</ul>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="gcp" /><category term="dataflow" /><category term="pubsub" /><category term="interview" /><summary type="html"><![CDATA[GCS 스토리지 클래스, Dataflow와 Dataproc, Pub/Sub, Composer, Datastream, IAM 계층과 서비스 계정, 네트워크와 비용 관리를 데이터 엔지니어 관점에서 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — Hadoop</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Hadoop/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — Hadoop" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Hadoop</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Hadoop/"><![CDATA[<h2 id="hdfs">HDFS</h2>

<h3 id="hdfs의-설계-전제는-무엇인가요">HDFS의 설계 전제는 무엇인가요</h3>

<p><strong>대용량 파일을 한 번 쓰고 여러 번 읽는(write-once, read-many)</strong> 패턴을 가정합니다. 그래서 임의 위치 수정이 안 되고, 순차 읽기 처리량에 최적화되어 있습니다.</p>

<p>또 하나의 전제는 <strong>하드웨어는 고장 난다</strong>는 것입니다. 값싼 장비를 여럿 쓰되 복제로 내구성을 확보하고, 장애를 예외가 아니라 정상 상황으로 다룹니다.</p>

<p>작은 파일이 많거나, 낮은 지연의 임의 접근이 필요하거나, 잦은 수정이 필요한 워크로드에는 맞지 않습니다.</p>

<h3 id="블록-크기가-큰-이유는-무엇인가요">블록 크기가 큰 이유는 무엇인가요</h3>

<p>기본 128MB입니다. 일반 파일시스템(4KB)보다 훨씬 큰데, 이유는 <strong>탐색 시간 대비 전송 시간의 비율</strong>을 높이기 위해서입니다. 블록이 작으면 디스크 탐색 오버헤드가 전체 시간에서 차지하는 비중이 커집니다.</p>

<p>부수 효과로 NameNode의 메타데이터 부담도 줄어듭니다. 블록 개수가 적어지기 때문입니다.</p>

<h3 id="namenode와-datanode의-역할을-설명해보세요">NameNode와 DataNode의 역할을 설명해보세요</h3>

<p>NameNode는 <strong>메타데이터</strong>를 관리합니다 — 디렉터리 구조, 파일과 블록의 매핑, 블록이 어느 DataNode에 있는지. 이 정보를 메모리에 올려두므로 NameNode 메모리가 클러스터가 담을 수 있는 파일 수의 상한이 됩니다.</p>

<p>DataNode는 실제 블록을 저장하고, 주기적으로 NameNode에 블록 리포트와 하트비트를 보냅니다.</p>

<p><strong>블록 위치 정보는 디스크에 영속화하지 않습니다.</strong> DataNode들이 시작 시 보고하는 것으로 재구성합니다. 그래서 NameNode 재시작 후 안전 모드(safe mode)에서 보고를 기다립니다.</p>

<h3 id="secondary-namenode는-백업인가요">Secondary NameNode는 백업인가요</h3>

<p>아닙니다. 이름 때문에 오해하기 쉬운데, <strong>체크포인트 역할</strong>입니다.</p>

<p>NameNode는 변경 이력을 <code class="language-plaintext highlighter-rouge">edits</code> 로그에 append하고 전체 스냅샷은 <code class="language-plaintext highlighter-rouge">fsimage</code>에 둡니다. <code class="language-plaintext highlighter-rouge">edits</code>가 계속 커지면 재시작 시 재생 시간이 길어지므로, Secondary NameNode가 주기적으로 <code class="language-plaintext highlighter-rouge">fsimage + edits</code>를 병합해 새 <code class="language-plaintext highlighter-rouge">fsimage</code>를 만듭니다.</p>

<p>고가용성은 별도 기능입니다. Active/Standby NameNode를 두고 JournalNode로 edits를 공유하며, ZooKeeper 기반 자동 페일오버를 씁니다.</p>

<h3 id="복제-정책은-어떻게-되나요">복제 정책은 어떻게 되나요</h3>

<p>기본 복제 계수는 3이고, 배치 정책은 이렇습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1번째 — 쓰기를 수행하는 노드(또는 임의 노드)
2번째 — 다른 랙의 노드
3번째 — 2번째와 같은 랙의 다른 노드
</code></pre></div></div>

<p>랙 하나가 통째로 죽어도 데이터가 살아남고, 동시에 모든 복제본을 다른 랙에 두는 것보다 네트워크 비용이 적습니다. 이 판단을 위해 <strong>rack awareness</strong> 설정이 필요합니다.</p>

<h3 id="erasure-coding은-무엇인가요">Erasure Coding은 무엇인가요</h3>

<p>복제 대신 패리티로 내구성을 확보하는 방식입니다. 3배 복제는 저장 오버헤드가 200%인데, RS(6,3)은 50%로 비슷한 내구성을 얻습니다.</p>

<p>대신 복구 시 여러 노드에서 데이터를 읽어 계산해야 하므로 <strong>읽기·복구 비용이 큽니다.</strong> 자주 안 읽는 콜드 데이터에 적합합니다.</p>

<h2 id="mapreduce">MapReduce</h2>

<h3 id="mapreduce의-처리-흐름을-설명해보세요">MapReduce의 처리 흐름을 설명해보세요</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Input Split → Map → (Combine) → Partition → Shuffle &amp; Sort → Reduce → Output
</code></pre></div></div>

<p>Map은 입력 스플릿마다 하나씩 실행되어 key-value를 내보내고, 파티셔너가 키를 기준으로 어느 Reducer로 보낼지 정합니다. Shuffle 단계에서 네트워크 전송과 정렬이 일어나고, Reducer는 같은 키의 값들을 모아 처리합니다.</p>

<p><strong>Reducer 입력은 키로 정렬되어 있다는 점</strong>이 중요합니다. 정렬이 공짜로 되는 것이 아니라 Shuffle의 비용에 포함되어 있습니다.</p>

<h3 id="combiner는-무엇이고-언제-쓸-수-있나요">Combiner는 무엇이고 언제 쓸 수 있나요</h3>

<p>Map 쪽에서 미리 부분 집계를 해서 네트워크로 보내는 양을 줄이는 최적화입니다.</p>

<p><strong>교환법칙과 결합법칙이 성립하는 연산에만</strong> 쓸 수 있습니다. <code class="language-plaintext highlighter-rouge">sum</code>, <code class="language-plaintext highlighter-rouge">max</code>, <code class="language-plaintext highlighter-rouge">count</code>는 되지만 <code class="language-plaintext highlighter-rouge">average</code>는 안 됩니다. 부분 평균의 평균은 전체 평균이 아니기 때문입니다. 평균이 필요하면 <code class="language-plaintext highlighter-rouge">(합, 개수)</code> 쌍을 내보내고 마지막에 나누면 됩니다.</p>

<p>Combiner는 실행이 <strong>보장되지 않습니다.</strong> 0번, 1번, 여러 번 실행될 수 있으므로 결과가 그에 무관해야 합니다.</p>

<h3 id="데이터-지역성data-locality은-무엇인가요">데이터 지역성(data locality)은 무엇인가요</h3>

<p>계산을 데이터가 있는 노드로 보내는 원칙입니다. 대용량에서는 데이터를 옮기는 비용이 코드를 옮기는 비용보다 훨씬 큽니다.</p>

<p>스케줄러는 node-local → rack-local → off-rack 순으로 배치를 시도합니다. 오브젝트 스토리지(S3, GCS)를 쓰는 요즘 구조에서는 지역성 개념이 사실상 사라졌고, 대신 네트워크 대역폭과 요청 병렬도가 중요해졌습니다.</p>

<h3 id="speculative-execution은-무엇인가요">Speculative execution은 무엇인가요</h3>

<p>느린 태스크(straggler)를 감지해 <strong>같은 태스크를 다른 노드에서 중복 실행</strong>하고, 먼저 끝난 쪽을 채택하는 기능입니다.</p>

<p>하드웨어 문제로 느린 경우에는 도움이 되지만, <strong>데이터 skew가 원인이면 소용이 없습니다.</strong> 복제본도 똑같이 많은 데이터를 처리해야 하므로 리소스만 낭비합니다.</p>

<h2 id="yarn">YARN</h2>

<h3 id="yarn의-구성-요소를-설명해보세요">YARN의 구성 요소를 설명해보세요</h3>

<ul>
  <li><strong>ResourceManager</strong> — 클러스터 전체 리소스를 관리하고 애플리케이션에 컨테이너를 할당</li>
  <li><strong>NodeManager</strong> — 각 노드에서 컨테이너를 실행하고 상태를 보고</li>
  <li><strong>ApplicationMaster</strong> — 애플리케이션마다 하나씩 뜨는 관리자. 필요한 컨테이너를 요청하고 태스크를 조율</li>
</ul>

<p>핵심은 <strong>자원 관리와 잡 관리를 분리</strong>했다는 점입니다. 덕분에 MapReduce 외에 Spark, Flink 같은 다른 프레임워크도 같은 클러스터에서 돌 수 있게 됐습니다.</p>

<h3 id="스케줄러-종류와-차이는-무엇인가요">스케줄러 종류와 차이는 무엇인가요</h3>

<table>
  <thead>
    <tr>
      <th>스케줄러</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FIFO</td>
      <td>먼저 온 잡이 자원을 다 씀. 큰 잡이 뒤를 막음</td>
    </tr>
    <tr>
      <td>Capacity</td>
      <td>큐별로 용량을 보장. 조직 단위 분리에 적합</td>
    </tr>
    <tr>
      <td>Fair</td>
      <td>실행 중인 잡들에 자원을 균등 배분</td>
    </tr>
  </tbody>
</table>

<p>운영에서는 Capacity Scheduler로 팀·용도별 큐를 나누고, 큐마다 최소 보장과 최대 상한을 두는 구성이 흔합니다.</p>

<h3 id="컨테이너-메모리-설정에서-자주-나는-문제는-무엇인가요">컨테이너 메모리 설정에서 자주 나는 문제는 무엇인가요</h3>

<p><code class="language-plaintext highlighter-rouge">yarn.nodemanager.resource.memory-mb</code>(노드가 제공하는 총량), <code class="language-plaintext highlighter-rouge">yarn.scheduler.maximum-allocation-mb</code>(컨테이너 하나의 최대), 그리고 애플리케이션이 요청하는 값이 어긋나는 경우입니다.</p>

<p>Spark에서는 <code class="language-plaintext highlighter-rouge">spark.executor.memory</code> 외에 <code class="language-plaintext highlighter-rouge">spark.executor.memoryOverhead</code>가 따로 붙어 실제 컨테이너 요청량이 됩니다. 힙은 여유 있는데 컨테이너가 YARN에 의해 kill되는 경우, 대개 오버헤드가 부족한 것입니다.</p>

<h2 id="운영과-포맷">운영과 포맷</h2>

<h3 id="small-files-문제는-왜-심각한가요">small files 문제는 왜 심각한가요</h3>

<p>두 방향으로 문제가 됩니다.</p>

<p><strong>NameNode 메모리</strong> — 파일·블록마다 메타데이터가 메모리를 차지합니다. 1MB 파일 100만 개는 128MB 파일 8천 개보다 훨씬 많은 메모리를 씁니다.</p>

<p><strong>태스크 오버헤드</strong> — 스플릿이 파일 단위로 생기므로 작은 파일 100만 개는 태스크 100만 개가 됩니다. 태스크 시작 비용이 실제 처리 시간보다 커집니다.</p>

<p>대응은 적재 시점에 파일 크기를 맞추거나(compaction), <code class="language-plaintext highlighter-rouge">CombineFileInputFormat</code>으로 여러 파일을 한 스플릿에 묶거나, HAR·시퀀스 파일로 묶는 것입니다.</p>

<h3 id="parquet-orc-avro를-비교해보세요">Parquet, ORC, Avro를 비교해보세요</h3>

<table>
  <thead>
    <tr>
      <th>포맷</th>
      <th>구조</th>
      <th>강점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Parquet</td>
      <td>컬럼너</td>
      <td>분석 쿼리. 컬럼 선택 읽기, 압축률</td>
    </tr>
    <tr>
      <td>ORC</td>
      <td>컬럼너</td>
      <td>Parquet과 유사. Hive 계열에서 강함, 경량 인덱스</td>
    </tr>
    <tr>
      <td>Avro</td>
      <td>행 기반</td>
      <td>전체 행을 읽고 쓰는 워크로드, 스키마 진화</td>
    </tr>
  </tbody>
</table>

<p>분석용 테이블은 Parquet/ORC, 스트리밍 메시지나 원본 적재는 Avro가 일반적인 선택입니다.</p>

<h3 id="파티셔닝과-버케팅의-차이는-무엇인가요">파티셔닝과 버케팅의 차이는 무엇인가요</h3>

<p><strong>파티셔닝</strong>은 디렉터리를 나눕니다. 조회 조건에 파티션 컬럼이 있으면 디렉터리째로 건너뜁니다(partition pruning). 카디널리티가 높으면 디렉터리가 폭발합니다.</p>

<p><strong>버케팅</strong>은 해시로 고정 개수의 파일에 나눕니다. 같은 키가 같은 버킷에 모이므로 <strong>조인 시 셔플을 줄일 수 있습니다</strong>(같은 버킷 수·같은 키로 버케팅된 테이블끼리). 카디널리티가 높은 컬럼(user_id 등)에 적합합니다.</p>

<h3 id="요즘도-hadoop을-쓰나요">요즘도 Hadoop을 쓰나요</h3>

<p>HDFS + MapReduce 조합은 많이 줄었습니다. 저장은 오브젝트 스토리지(S3, GCS)로, 연산은 Spark/Flink로, 자원 관리는 Kubernetes로 옮겨가는 흐름입니다.</p>

<p>다만 <strong>개념은 그대로 유효합니다.</strong> 블록·스플릿·파티션, 셔플, 데이터 지역성, small files, 컬럼너 포맷은 지금 쓰는 도구들의 바탕에 그대로 남아 있습니다. 면접에서 Hadoop을 묻는 이유도 대개 이 개념 이해를 확인하려는 것입니다.</p>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="hadoop" /><category term="hdfs" /><category term="yarn" /><category term="interview" /><summary type="html"><![CDATA[HDFS의 블록과 복제, NameNode 구조, MapReduce 동작 원리, YARN 리소스 관리, small files 문제와 파일 포맷 선택을 질문과 핵심 답변으로 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — Apache Iceberg</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Iceberg/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — Apache Iceberg" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Iceberg</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Iceberg/"><![CDATA[<h2 id="기본-개념">기본 개념</h2>

<h3 id="테이블-포맷이-왜-필요한가요">테이블 포맷이 왜 필요한가요</h3>

<p>오브젝트 스토리지에 Parquet 파일만 쌓아두면 여러 문제가 생깁니다.</p>

<ul>
  <li>파일 목록으로 테이블을 정의하므로 <strong>디렉터리 리스팅</strong>이 병목이 됩니다. S3에서 수만 개 파일을 나열하는 것은 느리고 비쌉니다</li>
  <li><strong>원자성이 없습니다.</strong> 쓰는 도중에 읽으면 반쯤 쓰인 상태가 보입니다</li>
  <li>스키마 변경, 삭제·갱신이 어렵습니다</li>
  <li>동시 쓰기 시 충돌을 감지할 방법이 없습니다</li>
</ul>

<p>Iceberg는 <strong>메타데이터로 파일 목록을 관리</strong>해 이 문제들을 해결합니다. 테이블이 “이 디렉터리 아래 파일들”이 아니라 “이 스냅샷이 가리키는 파일들”이 됩니다.</p>

<h3 id="iceberg의-메타데이터-구조를-설명해보세요">Iceberg의 메타데이터 구조를 설명해보세요</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>카탈로그
  └── metadata.json          현재 스냅샷 포인터, 스키마, 파티션 스펙 이력
        └── manifest list    스냅샷 하나가 참조하는 매니페스트 목록 (+ 파티션 범위 통계)
              └── manifest   데이터 파일 목록 (+ 파일별 컬럼 min/max, null count)
                    └── data files (Parquet/ORC/Avro)
</code></pre></div></div>

<p>쿼리 플래닝은 이 계층을 <strong>위에서 아래로 가지치기</strong>하며 내려갑니다. 매니페스트 리스트의 파티션 범위로 매니페스트를 걸러내고, 매니페스트의 컬럼 통계로 데이터 파일을 걸러냅니다. 파일을 열지 않고도 대부분을 건너뛸 수 있습니다.</p>

<h3 id="스냅샷과-원자적-커밋은-어떻게-동작하나요">스냅샷과 원자적 커밋은 어떻게 동작하나요</h3>

<p>쓰기는 새 데이터 파일을 쓰고, 새 매니페스트와 매니페스트 리스트를 만든 뒤, 마지막에 <strong>카탈로그의 현재 metadata 포인터를 원자적으로 교체</strong>합니다. 이 교체가 성공해야 새 스냅샷이 보입니다.</p>

<p>읽는 쪽은 시작 시점의 스냅샷을 고정해서 읽으므로, 쓰기가 진행 중이어도 일관된 결과를 봅니다(snapshot isolation).</p>

<p>동시 쓰기는 <strong>낙관적 동시성 제어</strong>로 처리합니다. 커밋 시점에 기반 스냅샷이 바뀌었으면 실패하고 재시도합니다. 서로 다른 파티션을 건드리는 쓰기는 대개 재시도로 해결되지만, 같은 파일을 건드리면 충돌이 반복될 수 있습니다.</p>

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

<h3 id="히든-파티셔닝hidden-partitioning이란-무엇인가요">히든 파티셔닝(hidden partitioning)이란 무엇인가요</h3>

<p>Hive 방식에서는 파티션 컬럼이 실제 컬럼으로 존재하고, 쿼리에서 <strong>그 컬럼을 명시해야만</strong> 프루닝이 됩니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Hive: dt 컬럼을 따로 만들어 두고 조건에 넣어야 함</span>
<span class="k">WHERE</span> <span class="n">dt</span> <span class="o">=</span> <span class="s1">'2026-08-12'</span> <span class="k">AND</span> <span class="n">event_time</span> <span class="o">&gt;=</span> <span class="s1">'2026-08-12 00:00:00'</span>
</code></pre></div></div>

<p>Iceberg는 <strong>변환식을 파티션 스펙에 저장</strong>합니다. <code class="language-plaintext highlighter-rouge">days(event_time)</code>으로 파티셔닝했다면, 사용자가 <code class="language-plaintext highlighter-rouge">event_time</code>으로 조건을 걸어도 엔진이 알아서 파티션을 프루닝합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Iceberg: 원본 컬럼 조건만으로 프루닝됨</span>
<span class="k">WHERE</span> <span class="n">event_time</span> <span class="o">&gt;=</span> <span class="s1">'2026-08-12'</span>
</code></pre></div></div>

<p>파티션 컬럼을 별도로 만들지 않아도 되고, <strong>사용자가 파티션 구조를 몰라도</strong> 됩니다.</p>

<h3 id="파티션-진화partition-evolution는-무엇인가요">파티션 진화(partition evolution)는 무엇인가요</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">ALTER</span> <span class="k">TABLE</span> <span class="n">events</span> <span class="k">REPLACE</span> <span class="k">PARTITION</span> <span class="n">FIELD</span> <span class="n">days</span><span class="p">(</span><span class="n">event_time</span><span class="p">)</span> <span class="k">WITH</span> <span class="n">hours</span><span class="p">(</span><span class="n">event_time</span><span class="p">);</span>
</code></pre></div></div>

<p>Hive에서는 파티션 구조를 바꾸려면 테이블을 다시 만들고 전체를 재적재해야 했습니다.</p>

<h3 id="스키마-진화는-어떻게-안전한가요">스키마 진화는 어떻게 안전한가요</h3>

<p>Iceberg는 컬럼을 <strong>이름이 아니라 ID로</strong> 추적합니다. 그래서 이름을 바꿔도 데이터가 그대로 매핑되고, 컬럼 순서를 바꿔도 안전합니다.</p>

<p>지원되는 변경은 추가, 삭제, 이름 변경, 순서 변경, 타입 확대(int → long 등)입니다. 삭제한 컬럼과 같은 이름으로 새 컬럼을 추가해도 <strong>다른 ID를 받으므로</strong> 옛 데이터가 되살아나지 않습니다.</p>

<p>Parquet 파일에 컬럼 이름으로 매핑하는 방식(Hive)에서는 이 보장이 없어서, 컬럼을 지웠다 같은 이름으로 추가하면 옛 값이 섞여 나오는 사고가 생깁니다.</p>

<h3 id="타임-트래블은-어떻게-쓰나요">타임 트래블은 어떻게 쓰나요</h3>

<p>스냅샷이 남아 있는 한 과거 시점의 테이블을 그대로 읽을 수 있습니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 스냅샷 ID 로</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">events</span> <span class="k">VERSION</span> <span class="k">AS</span> <span class="k">OF</span> <span class="mi">3821550127947089612</span><span class="p">;</span>

<span class="c1">-- 시각으로</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">events</span> <span class="nb">TIMESTAMP</span> <span class="k">AS</span> <span class="k">OF</span> <span class="s1">'2026-08-11 00:00:00'</span><span class="p">;</span>

<span class="c1">-- 스냅샷 목록 조회</span>
<span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">events</span><span class="p">.</span><span class="n">snapshots</span><span class="p">;</span>
</code></pre></div></div>

<p>잘못된 배치가 돌았을 때 이전 스냅샷으로 되돌리는 것도 가능합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rollback_to_snapshot</span><span class="p">(</span><span class="s1">'db.events'</span><span class="p">,</span> <span class="mi">3821550127947089612</span><span class="p">);</span>
</code></pre></div></div>

<p>다만 <strong>스냅샷 만료 정책</strong>과 충돌합니다. 오래된 스냅샷을 정리하면 그 시점으로는 못 돌아갑니다.</p>

<h2 id="삭제와-갱신">삭제와 갱신</h2>

<h3 id="행-수준-삭제는-어떻게-구현되어-있나요">행 수준 삭제는 어떻게 구현되어 있나요</h3>

<p>두 가지 방식이 있습니다.</p>

<p><strong>Copy-on-Write (CoW)</strong> — 삭제 대상이 포함된 데이터 파일을 통째로 다시 씁니다. 읽기는 빠르지만(추가 처리 없음) 쓰기가 비쌉니다. 조회가 많고 갱신이 드문 테이블에 적합합니다.</p>

<p><strong>Merge-on-Read (MoR)</strong> — 삭제 정보를 별도 delete 파일에 기록하고, 읽을 때 병합합니다. 쓰기는 빠르지만 읽을 때 비용이 붙습니다. CDC처럼 갱신이 잦은 테이블에 적합합니다.</p>

<p>delete 파일에도 두 종류가 있습니다. <strong>position delete</strong>는 “이 파일의 N번째 행”을 가리키고, <strong>equality delete</strong>는 “이 키를 가진 행”을 가리킵니다. 후자는 스트리밍 upsert에서 쓰이는데, 읽을 때 조인 비용이 커서 주기적 컴팩션이 필수입니다.</p>

<h3 id="merge-into는-어떻게-쓰나요">MERGE INTO는 어떻게 쓰나요</h3>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">MERGE</span> <span class="k">INTO</span> <span class="n">target</span> <span class="n">t</span>
<span class="k">USING</span> <span class="k">source</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">id</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">AND</span> <span class="n">s</span><span class="p">.</span><span class="n">op</span> <span class="o">=</span> <span class="s1">'D'</span> <span class="k">THEN</span> <span class="k">DELETE</span>
<span class="k">WHEN</span> <span class="n">MATCHED</span> <span class="k">THEN</span> <span class="k">UPDATE</span> <span class="k">SET</span> <span class="o">*</span>
<span class="k">WHEN</span> <span class="k">NOT</span> <span class="n">MATCHED</span> <span class="k">THEN</span> <span class="k">INSERT</span> <span class="o">*</span><span class="p">;</span>
</code></pre></div></div>

<p>소스에 <strong>같은 키가 두 번 이상 있으면 실패</strong>합니다. CDC 피드를 그대로 MERGE에 넣으면 한 키의 여러 변경이 들어오므로, 미리 키별 최신 1건만 남겨야 합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 키별 최신 변경만 추리기</span>
<span class="k">WITH</span> <span class="n">latest</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="p">(</span>
    <span class="k">SELECT</span> <span class="o">*</span><span class="p">,</span> <span class="n">ROW_NUMBER</span><span class="p">()</span> <span class="n">OVER</span> <span class="p">(</span><span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">id</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">ts</span> <span class="k">DESC</span><span class="p">)</span> <span class="n">rn</span> <span class="k">FROM</span> <span class="n">changes</span>
  <span class="p">)</span> <span class="k">WHERE</span> <span class="n">rn</span> <span class="o">=</span> <span class="mi">1</span>
<span class="p">)</span>
<span class="n">MERGE</span> <span class="k">INTO</span> <span class="n">target</span> <span class="n">t</span> <span class="k">USING</span> <span class="n">latest</span> <span class="n">s</span> <span class="k">ON</span> <span class="n">t</span><span class="p">.</span><span class="n">id</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">id</span> <span class="p">...</span>
</code></pre></div></div>

<h2 id="운영">운영</h2>

<h3 id="유지보수-작업에는-어떤-것들이-있나요">유지보수 작업에는 어떤 것들이 있나요</h3>

<p>Iceberg는 <strong>자동으로 정리하지 않습니다.</strong> 주기적으로 돌려야 하는 작업이 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>작업</th>
      <th>내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rewrite_data_files</code></td>
      <td>작은 파일을 합침(컴팩션). MoR delete 파일도 병합</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">expire_snapshots</code></td>
      <td>오래된 스냅샷과 그 스냅샷만 참조하던 파일 삭제</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">remove_orphan_files</code></td>
      <td>실패한 쓰기가 남긴, 어떤 메타데이터도 참조하지 않는 파일 삭제</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">rewrite_manifests</code></td>
      <td>매니페스트를 재구성해 플래닝 속도 개선</td>
    </tr>
  </tbody>
</table>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">rewrite_data_files</span><span class="p">(</span><span class="k">table</span> <span class="o">=&gt;</span> <span class="s1">'db.events'</span><span class="p">);</span>
<span class="k">CALL</span> <span class="k">catalog</span><span class="p">.</span><span class="k">system</span><span class="p">.</span><span class="n">expire_snapshots</span><span class="p">(</span><span class="k">table</span> <span class="o">=&gt;</span> <span class="s1">'db.events'</span><span class="p">,</span> <span class="n">older_than</span> <span class="o">=&gt;</span> <span class="nb">TIMESTAMP</span> <span class="s1">'2026-08-01 00:00:00'</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">remove_orphan_files</code>는 <strong>동시에 쓰기가 진행 중이면 위험합니다.</strong> 아직 커밋되지 않은 파일을 고아로 판단할 수 있으므로 충분한 유예 시간을 둬야 합니다.</p>

<h3 id="스토리지-비용이-계속-늘어납니다-왜일까요">스토리지 비용이 계속 늘어납니다. 왜일까요</h3>

<p>스냅샷이 만료되지 않아 옛 데이터 파일이 계속 남아 있는 경우가 대부분입니다. CoW 테이블이면 갱신할 때마다 파일이 통째로 새로 쓰이므로 누적이 빠릅니다.</p>

<p><code class="language-plaintext highlighter-rouge">expire_snapshots</code>를 돌리되, 타임 트래블 요구사항과 균형을 맞춰야 합니다. 보통 7~30일 정도로 둡니다.</p>

<h3 id="카탈로그는-어떤-것을-쓰나요">카탈로그는 어떤 것을 쓰나요</h3>

<p>테이블의 현재 metadata 위치를 원자적으로 갱신해주는 주체가 필요합니다.</p>

<ul>
  <li><strong>Hive Metastore</strong> — 기존 자산과 호환. 널리 쓰임</li>
  <li><strong>AWS Glue</strong> — AWS 환경에서 관리형</li>
  <li><strong>REST Catalog</strong> — 표준 인터페이스. 엔진 중립적이라 최근 선호됨</li>
  <li><strong>Nessie</strong> — 브랜치·태그 개념으로 데이터 버전 관리</li>
  <li><strong>JDBC / Hadoop</strong> — 단순하지만 운영 기능이 약함</li>
</ul>

<p><code class="language-plaintext highlighter-rouge">Hadoop catalog</code>(파일시스템 기반)는 S3에서 원자적 rename이 보장되지 않아 <strong>동시 쓰기에 안전하지 않습니다.</strong> 운영에는 권장되지 않습니다.</p>

<h3 id="iceberg-delta-lake-hudi를-비교해보세요">Iceberg, Delta Lake, Hudi를 비교해보세요</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Iceberg</th>
      <th>Delta Lake</th>
      <th>Hudi</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>출신</td>
      <td>Netflix</td>
      <td>Databricks</td>
      <td>Uber</td>
    </tr>
    <tr>
      <td>파티션 진화</td>
      <td>지원</td>
      <td>미지원</td>
      <td>미지원</td>
    </tr>
    <tr>
      <td>히든 파티셔닝</td>
      <td>지원</td>
      <td>미지원</td>
      <td>미지원</td>
    </tr>
    <tr>
      <td>엔진 중립성</td>
      <td>높음</td>
      <td>Spark 중심(개선 중)</td>
      <td>Spark/Flink 중심</td>
    </tr>
    <tr>
      <td>강점</td>
      <td>대규모 테이블, 스키마·파티션 진화</td>
      <td>Spark 생태계 통합, 사용 편의</td>
      <td>인덱스 기반 빠른 upsert</td>
    </tr>
  </tbody>
</table>

<p>셋 다 ACID, 타임 트래블, 스키마 진화를 제공합니다. 선택은 대개 <strong>쓰는 엔진과 클라우드 환경</strong>에 따라 갈립니다.</p>

<h3 id="언제-iceberg가-과한-선택일까요">언제 Iceberg가 과한 선택일까요</h3>

<p>테이블이 작고, 갱신이 없고, 하루 한 번 전체를 덮어쓰는 정도라면 Parquet + 파티션 디렉터리로 충분합니다. Iceberg는 메타데이터 관리와 유지보수 잡이라는 운영 부담을 함께 가져옵니다.</p>

<p><strong>갱신·삭제가 필요하거나, 파일 수가 많아 리스팅이 느려지거나, 동시 쓰기가 있거나, 스키마가 자주 바뀌는</strong> 상황에서 값을 합니다.</p>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="iceberg" /><category term="lakehouse" /><category term="table-format" /><category term="interview" /><summary type="html"><![CDATA[Iceberg의 메타데이터 계층과 스냅샷, 히든 파티셔닝, 스키마 진화, 타임 트래블, 컴팩션과 유지보수, Delta·Hudi와의 비교를 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — Kafka</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Kafka/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — Kafka" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Kafka</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Kafka/"><![CDATA[<h2 id="기본-구조">기본 구조</h2>

<h3 id="토픽-파티션-오프셋의-관계를-설명해보세요">토픽, 파티션, 오프셋의 관계를 설명해보세요</h3>

<p>토픽은 논리적인 메시지 분류이고, 파티션은 그 토픽을 물리적으로 나눈 <strong>append-only 로그</strong>입니다. 오프셋은 파티션 안에서 메시지의 위치를 가리키는 단조 증가 정수입니다.</p>

<p>중요한 점은 <strong>오프셋이 파티션 단위</strong>라는 것입니다. 토픽 전체에 걸친 전역 순번은 없습니다. 그래서 순서 보장도 파티션 안에서만 성립합니다.</p>

<h3 id="파티션-수는-어떤-기준으로-정하나요">파티션 수는 어떤 기준으로 정하나요</h3>

<p>가장 큰 제약은 <strong>컨슈머 병렬도</strong>입니다. 한 컨슈머 그룹에서 파티션 하나는 컨슈머 하나에만 배정되므로, 파티션 수가 그룹 내 최대 병렬도가 됩니다. 컨슈머를 파티션 수보다 많이 띄우면 남는 컨슈머는 놉니다.</p>

<p>늘리기는 쉽지만 <strong>줄일 수 없다는 점</strong>이 중요합니다. 그리고 파티션을 늘리면 키 해싱 결과가 바뀌어 <strong>같은 키가 다른 파티션으로 갈 수 있습니다</strong> — 순서 보장이 필요한 토픽이라면 이 시점에 이력이 꼬입니다.</p>

<p>브로커 입장에서는 파티션마다 파일 핸들과 메모리를 쓰고, 리더 선출·복제 대상이 늘어납니다. 무작정 크게 잡을 이유가 없습니다.</p>

<h3 id="메시지-순서는-어디까지-보장되나요">메시지 순서는 어디까지 보장되나요</h3>

<p><strong>같은 파티션 안에서만</strong> 보장됩니다. 순서가 중요한 단위(주문 ID, 디바이스 ID 등)를 키로 지정해 같은 파티션에 떨어지게 만드는 것이 일반적인 방법입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>key = order_id  →  hash(key) % partition_count  →  항상 같은 파티션
</code></pre></div></div>

<p>다만 프로듀서 설정에 따라 순서가 깨질 수 있습니다. <code class="language-plaintext highlighter-rouge">max.in.flight.requests.per.connection</code>이 1보다 크고 재시도가 켜져 있으면, 앞 배치가 실패하고 재전송되는 사이 뒤 배치가 먼저 커밋될 수 있습니다. <code class="language-plaintext highlighter-rouge">enable.idempotence=true</code>를 켜면 브로커가 시퀀스 번호로 순서를 잡아주므로 in-flight를 5까지 두고도 순서가 유지됩니다.</p>

<h2 id="전달-보장">전달 보장</h2>

<h3 id="at-most-once-at-least-once-exactly-once를-구분해보세요">at-most-once, at-least-once, exactly-once를 구분해보세요</h3>

<table>
  <thead>
    <tr>
      <th>시맨틱</th>
      <th>의미</th>
      <th>언제 생기나</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>at-most-once</td>
      <td>유실 가능, 중복 없음</td>
      <td>처리 전에 오프셋을 먼저 커밋</td>
    </tr>
    <tr>
      <td>at-least-once</td>
      <td>유실 없음, 중복 가능</td>
      <td>처리 후에 오프셋 커밋 (기본)</td>
    </tr>
    <tr>
      <td>exactly-once</td>
      <td>유실도 중복도 없음</td>
      <td>트랜잭션 또는 멱등 처리</td>
    </tr>
  </tbody>
</table>

<p>실무에서 대부분은 <strong>at-least-once + 멱등한 소비자</strong>로 갑니다. exactly-once는 Kafka 안에서 끝나는 처리(read-process-write)에서는 트랜잭션으로 가능하지만, 외부 시스템에 쓰는 순간 그 시스템이 트랜잭션에 참여하지 않으면 성립하지 않습니다.</p>

<h3 id="컨슈머에서-중복을-어떻게-막나요">컨슈머에서 중복을 어떻게 막나요</h3>

<p>싱크 쪽을 멱등하게 만드는 것이 정석입니다.</p>

<ul>
  <li>메시지에 고유 키가 있으면 그 키로 UPSERT</li>
  <li>없으면 <code class="language-plaintext highlighter-rouge">topic-partition-offset</code> 조합을 키로 써서 중복 판정</li>
  <li>집계라면 원본을 그대로 적재하고 조회 시점에 중복 제거</li>
</ul>

<p>오프셋 커밋 시점도 함께 봐야 합니다. <code class="language-plaintext highlighter-rouge">enable.auto.commit=true</code>는 처리 완료와 무관하게 주기적으로 커밋하므로, 처리 중 죽으면 <strong>처리하지 않은 메시지의 오프셋이 이미 커밋된</strong> 상태가 될 수 있습니다.</p>

<h3 id="acks-설정은-어떤-의미인가요">acks 설정은 어떤 의미인가요</h3>

<p>프로듀서가 “썼다”고 판단하는 기준입니다.</p>

<table>
  <thead>
    <tr>
      <th>값</th>
      <th>의미</th>
      <th>유실 위험</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">0</code></td>
      <td>보내고 확인 안 함</td>
      <td>큼</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">1</code></td>
      <td>리더만 기록하면 성공</td>
      <td>리더 장애 시 유실</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">all</code> (<code class="language-plaintext highlighter-rouge">-1</code>)</td>
      <td>ISR 전체가 기록해야 성공</td>
      <td>가장 안전</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">acks=all</code>만으로는 부족하고 <code class="language-plaintext highlighter-rouge">min.insync.replicas</code>를 함께 봐야 합니다. 복제본이 3인데 <code class="language-plaintext highlighter-rouge">min.insync.replicas=1</code>이면 ISR이 1로 줄어든 상태에서도 쓰기가 성공하므로, 그 브로커가 죽으면 유실됩니다. 보통 복제 3 / <code class="language-plaintext highlighter-rouge">min.insync.replicas=2</code> 조합을 씁니다.</p>

<h2 id="컨슈머-그룹">컨슈머 그룹</h2>

<h3 id="리밸런싱은-언제-일어나고-무엇이-문제인가요">리밸런싱은 언제 일어나고 무엇이 문제인가요</h3>

<p>컨슈머가 들어오거나 나갈 때, 파티션 수가 바뀔 때, 그리고 <strong>컨슈머가 살아 있다는 신호를 못 보낼 때</strong> 일어납니다.</p>

<p>기본 방식(eager)에서는 리밸런싱 동안 <strong>그룹 전체가 처리를 멈춥니다</strong>(stop-the-world). 컨슈머 하나가 잠깐 느려져서 리밸런싱이 촉발되면 전체가 영향을 받습니다.</p>

<p>이걸 줄이는 방법이 몇 가지 있습니다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">partition.assignment.strategy</code>를 <strong>CooperativeStickyAssignor</strong>로 — 영향받는 파티션만 옮기고 나머지는 계속 처리</li>
  <li><code class="language-plaintext highlighter-rouge">group.instance.id</code>를 지정해 <strong>static membership</strong> 사용 — 재시작해도 같은 파티션을 유지, 배포 때 리밸런싱 회피</li>
  <li><code class="language-plaintext highlighter-rouge">max.poll.interval.ms</code>를 처리 시간에 맞게 조정</li>
</ul>

<h3 id="poll-관련-설정에서-자주-나는-사고는-무엇인가요">poll 관련 설정에서 자주 나는 사고는 무엇인가요</h3>

<p><code class="language-plaintext highlighter-rouge">max.poll.records</code>로 한 번에 많이 가져와 놓고 처리가 오래 걸려 <code class="language-plaintext highlighter-rouge">max.poll.interval.ms</code>를 넘기는 경우입니다. 브로커는 그 컨슈머가 죽었다고 판단해 리밸런싱을 일으키고, 컨슈머는 처리를 끝낸 뒤 커밋하려다 실패합니다. 그러면 다시 같은 메시지를 받아 무한 반복에 빠집니다.</p>

<p>해결은 둘 중 하나입니다. <code class="language-plaintext highlighter-rouge">max.poll.records</code>를 줄이거나, <code class="language-plaintext highlighter-rouge">max.poll.interval.ms</code>를 실제 처리 시간보다 넉넉히 잡는 것입니다.</p>

<p><code class="language-plaintext highlighter-rouge">session.timeout.ms</code>와 헷갈리기 쉬운데, 이쪽은 백그라운드 하트비트 기준이고 <code class="language-plaintext highlighter-rouge">max.poll.interval.ms</code>는 <strong>처리 루프가 돌아오는 주기</strong> 기준입니다.</p>

<h3 id="컨슈머-랙lag이-계속-늘어납니다-어떻게-접근하나요">컨슈머 랙(lag)이 계속 늘어납니다. 어떻게 접근하나요</h3>

<p>먼저 어디가 병목인지 나눕니다.</p>

<ol>
  <li><strong>파티션별 랙 분포를 봅니다.</strong> 특정 파티션만 밀리면 키 쏠림(skew)입니다. 전체가 고르게 밀리면 처리량 부족입니다.</li>
  <li>처리량 부족이면 컨슈머를 늘립니다. 다만 <strong>파티션 수가 상한</strong>이므로 그 이상은 효과가 없습니다.</li>
  <li>컨슈머 내부가 느리면(외부 API, DB 왕복) 배치로 묶거나 비동기화합니다.</li>
  <li>키 쏠림이면 키 설계를 바꾸거나 파티션 수를 조정합니다.</li>
</ol>

<h2 id="kafka-connect">Kafka Connect</h2>

<h3 id="kafka-connect를-쓰는-이유는-무엇인가요">Kafka Connect를 쓰는 이유는 무엇인가요</h3>

<p>소스/싱크 연동을 <strong>설정으로</strong> 처리하기 위해서입니다. 컨슈머를 직접 짜면 오프셋 관리, 재시도, 스케일링, 모니터링을 전부 구현해야 하는데 Connect는 그걸 프레임워크가 담당합니다.</p>

<p>대신 자유도가 낮습니다. 복잡한 변환이 필요하면 커스텀 SMT를 만들거나, 아예 스트리밍 처리(Spark, Flink)로 가는 편이 낫습니다.</p>

<h3 id="smtsingle-message-transform는-무엇인가요">SMT(Single Message Transform)는 무엇인가요</h3>

<p>Connect 파이프라인 중간에서 <strong>메시지 하나 단위로</strong> 적용하는 변환입니다. 필드 이름 변경, 타입 캐스팅, 특정 필드 마스킹, 토픽 라우팅 같은 가벼운 작업에 씁니다.</p>

<p>메시지 단위라는 점이 한계입니다. 조인, 집계, 여러 메시지에 걸친 상태 처리는 SMT로 할 수 없습니다.</p>

<h3 id="standalone과-distributed-모드의-차이는-무엇인가요">standalone과 distributed 모드의 차이는 무엇인가요</h3>

<p>standalone은 단일 프로세스에서 돌고 오프셋을 로컬 파일에 저장합니다. 개발이나 검증용입니다.</p>

<p>distributed는 여러 워커가 클러스터를 이루고 커넥터의 설정·오프셋·상태를 <strong>Kafka 내부 토픽</strong>에 저장합니다. 워커가 죽으면 태스크가 다른 워커로 재배치됩니다. 운영에서는 워커가 하나여도 distributed로 띄우는 편이 낫습니다.</p>

<h2 id="운영">운영</h2>

<h3 id="로그-컴팩션과-삭제-정책의-차이는-무엇인가요">로그 컴팩션과 삭제 정책의 차이는 무엇인가요</h3>

<p><code class="language-plaintext highlighter-rouge">cleanup.policy=delete</code>는 시간(<code class="language-plaintext highlighter-rouge">retention.ms</code>)이나 크기(<code class="language-plaintext highlighter-rouge">retention.bytes</code>) 기준으로 오래된 세그먼트를 통째로 지웁니다. 이벤트 스트림에 적합합니다.</p>

<p><code class="language-plaintext highlighter-rouge">cleanup.policy=compact</code>는 <strong>키별 최신 값만 남깁니다.</strong> 테이블의 현재 상태를 표현하는 토픽(CDC 스냅샷, 설정 정보)에 적합합니다. 삭제를 표현하려면 값이 null인 <strong>툼스톤(tombstone)</strong> 메시지를 보냅니다.</p>

<p>둘을 동시에 지정(<code class="language-plaintext highlighter-rouge">compact,delete</code>)할 수도 있습니다.</p>

<h3 id="리텐션이-지나-유실된-메시지를-다시-받고-싶다면">리텐션이 지나 유실된 메시지를 다시 받고 싶다면</h3>

<p>받을 수 없습니다. 이게 Kafka를 원본 저장소로 쓰면 안 되는 이유입니다. 재처리가 필요한 파이프라인이라면 원본을 오브젝트 스토리지에 적재해 두고, 재처리는 거기서 하는 구조가 안전합니다.</p>

<p><code class="language-plaintext highlighter-rouge">auto.offset.reset</code>이 <code class="language-plaintext highlighter-rouge">latest</code>인 컨슈머가 오랫동안 죽어 있다가 살아나면, 커밋된 오프셋이 리텐션 밖으로 밀려 조용히 최신부터 읽기 시작합니다. <strong>유실을 인지하지 못한 채 넘어가는</strong> 대표적인 사고입니다.</p>

<h3 id="파티션-리더와-isr을-설명해보세요">파티션 리더와 ISR을 설명해보세요</h3>

<p>파티션마다 리더 하나와 팔로워 여럿이 있고, 읽기·쓰기는 리더가 처리합니다. ISR(In-Sync Replicas)은 리더를 충분히 따라잡고 있는 복제본 집합입니다.</p>

<p>리더가 죽으면 ISR 안에서 새 리더를 뽑습니다. ISR이 비면 선택지가 둘입니다 — 기다리거나(가용성 손해), ISR 밖 복제본을 리더로 올리거나(<code class="language-plaintext highlighter-rouge">unclean.leader.election.enable=true</code>, <strong>데이터 유실</strong>). 기본값은 안전한 쪽입니다.</p>

<h3 id="스키마-관리는-어떻게-하나요">스키마 관리는 어떻게 하나요</h3>

<p>JSON을 스키마 없이 흘리면 프로듀서가 필드를 바꿀 때 컨슈머가 조용히 깨집니다. Schema Registry에 Avro/Protobuf 스키마를 등록하고 호환성 규칙(BACKWARD, FORWARD, FULL)을 걸어 두면 <strong>호환되지 않는 변경을 등록 시점에 거부</strong>합니다.</p>

<p>호환성 방향을 이해하는 것이 중요합니다. BACKWARD는 새 스키마로 옛 데이터를 읽을 수 있다는 뜻이라 <strong>컨슈머를 먼저</strong> 배포해야 하고, FORWARD는 반대입니다.</p>

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

<ul>
  <li><a href="/data-engineering/spark/2026/08/11/MSK-%EC%BB%A4%EB%84%A5%ED%84%B0-vs-Spark-Streaming-%EB%B9%84%EA%B5%90/">MSK 커넥터 vs Spark Streaming 비교</a></li>
</ul>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="kafka" /><category term="streaming" /><category term="interview" /><summary type="html"><![CDATA[Kafka 파티션과 순서 보장, 전달 시맨틱, 컨슈머 그룹 리밸런싱, Kafka Connect와 SMT, 운영 중 자주 만나는 문제를 질문과 핵심 답변으로 정리합니다.]]></summary></entry><entry><title type="html">데이터 엔지니어 인터뷰 — Kubernetes</title><link href="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Kubernetes/" rel="alternate" type="text/html" title="데이터 엔지니어 인터뷰 — Kubernetes" /><published>2026-08-12T00:00:00+00:00</published><updated>2026-08-12T00:00:00+00:00</updated><id>https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Kubernetes</id><content type="html" xml:base="https://csj4032.github.io/interview/2026/08/12/%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4-%EC%9D%B8%ED%84%B0%EB%B7%B0-Kubernetes/"><![CDATA[<h2 id="기본-개념">기본 개념</h2>

<h3 id="파드pod란-무엇인가요">파드(Pod)란 무엇인가요</h3>

<p>배포의 최소 단위입니다. 컨테이너 하나 이상을 묶어 <strong>네트워크 네임스페이스와 볼륨을 공유</strong>하게 합니다. 같은 파드 안의 컨테이너는 <code class="language-plaintext highlighter-rouge">localhost</code>로 통신합니다.</p>

<p>파드는 일회용입니다. 죽으면 되살아나는 것이 아니라 <strong>새 파드가 생깁니다.</strong> IP도 바뀝니다. 그래서 파드 IP를 직접 참조하면 안 되고 Service를 거쳐야 합니다.</p>

<p>사이드카 패턴(로그 수집기, 프록시)이 파드 개념을 쓰는 대표적인 예입니다.</p>

<h3 id="deployment-statefulset-daemonset-job의-차이는-무엇인가요">Deployment, StatefulSet, DaemonSet, Job의 차이는 무엇인가요</h3>

<table>
  <thead>
    <tr>
      <th>컨트롤러</th>
      <th>용도</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Deployment</td>
      <td>무상태 앱</td>
      <td>파드가 동등하게 취급됨, 롤링 업데이트</td>
    </tr>
    <tr>
      <td>StatefulSet</td>
      <td>상태를 가진 앱</td>
      <td>안정적인 이름(<code class="language-plaintext highlighter-rouge">app-0</code>, <code class="language-plaintext highlighter-rouge">app-1</code>)과 전용 볼륨, 순서 보장</td>
    </tr>
    <tr>
      <td>DaemonSet</td>
      <td>노드마다 하나</td>
      <td>로그 수집, 모니터링 에이전트</td>
    </tr>
    <tr>
      <td>Job / CronJob</td>
      <td>완료되면 끝나는 작업</td>
      <td>배치 잡, 스케줄 실행</td>
    </tr>
  </tbody>
</table>

<p>데이터 쪽에서는 Kafka·Zookeeper·데이터베이스가 StatefulSet, 배치 파이프라인이 Job 형태로 돕니다.</p>

<h3 id="service와-ingress는-어떻게-다른가요">Service와 Ingress는 어떻게 다른가요</h3>

<p><strong>Service</strong>는 파드 집합에 안정적인 접근점을 제공합니다. 타입에 따라 클러스터 내부(ClusterIP), 노드 포트(NodePort), 클라우드 로드밸런서(LoadBalancer)로 노출됩니다.</p>

<p><strong>Ingress</strong>는 L7(HTTP) 라우팅입니다. 호스트·경로 기준으로 여러 Service에 분배하고 TLS를 종료합니다. Ingress Controller가 실제로 이 규칙을 구현합니다.</p>

<h3 id="configmap과-secret의-차이는-무엇인가요">ConfigMap과 Secret의 차이는 무엇인가요</h3>

<p>둘 다 설정을 파드에 주입하는 리소스이고, 사용법도 거의 같습니다(환경변수 또는 볼륨 마운트).</p>

<p>Secret은 base64로 인코딩되어 저장되는데, <strong>이건 암호화가 아닙니다.</strong> 실제 보안을 위해서는 etcd 저장 시 암호화를 켜고, RBAC로 접근을 제한하고, 외부 시크릿 관리자(Vault, AWS Secrets Manager, External Secrets Operator)를 쓰는 것이 일반적입니다.</p>

<h2 id="리소스-관리">리소스 관리</h2>

<h3 id="requests와-limits의-차이를-설명해보세요">requests와 limits의 차이를 설명해보세요</h3>

<p><strong>requests</strong>는 스케줄러가 노드를 고를 때 쓰는 값입니다. “이만큼은 보장받아야 한다”는 뜻이고, 노드의 남은 용량 계산에 들어갑니다.</p>

<p><strong>limits</strong>는 런타임 상한입니다. 이 이상 쓰면 제재를 받습니다.</p>

<p>제재 방식이 CPU와 메모리에서 다릅니다.</p>

<ul>
  <li><strong>CPU 초과</strong> — throttling. 느려질 뿐 죽지 않습니다</li>
  <li><strong>메모리 초과</strong> — <strong>OOMKilled</strong>. 즉시 종료됩니다</li>
</ul>

<p>메모리는 압축 불가능한(incompressible) 자원이라 회수할 방법이 없기 때문입니다.</p>

<h3 id="qos-클래스는-무엇인가요">QoS 클래스는 무엇인가요</h3>

<p>노드에 자원이 부족할 때 어떤 파드를 먼저 쫓아낼지 정하는 등급입니다.</p>

<table>
  <thead>
    <tr>
      <th>클래스</th>
      <th>조건</th>
      <th>축출 우선순위</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Guaranteed</td>
      <td>모든 컨테이너에 requests == limits</td>
      <td>가장 나중</td>
    </tr>
    <tr>
      <td>Burstable</td>
      <td>requests &lt; limits</td>
      <td>중간</td>
    </tr>
    <tr>
      <td>BestEffort</td>
      <td>아무것도 지정 안 함</td>
      <td>가장 먼저</td>
    </tr>
  </tbody>
</table>

<p>중요한 배치 잡이라면 Guaranteed로 두는 편이 안전합니다.</p>

<h3 id="파드가-oomkilled-되었습니다-어떻게-접근하나요">파드가 OOMKilled 되었습니다. 어떻게 접근하나요</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe pod &lt;pod&gt;          <span class="c"># Last State: Terminated, Reason: OOMKilled</span>
kubectl get pod &lt;pod&gt; <span class="nt">-o</span> <span class="nv">jsonpath</span><span class="o">=</span><span class="s1">'{.status.containerStatuses[0].lastState}'</span>
</code></pre></div></div>

<p>JVM 애플리케이션에서 특히 자주 겪습니다. 컨테이너 메모리 limit과 <strong>JVM 힙 설정이 따로 놀기 때문</strong>입니다. 힙 외에 메타스페이스, 스레드 스택, 네이티브 버퍼가 추가로 필요한데 힙을 limit과 같게 잡으면 반드시 넘칩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>컨테이너 limit = JVM 힙 + 메타스페이스 + 스택 + 네이티브 + 여유
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">-XX:MaxRAMPercentage</code>로 힙을 컨테이너 limit의 비율(예: 75%)로 잡는 방식이 안전합니다.</p>

<p>PySpark처럼 파이썬 프로세스가 따로 뜨는 경우도 마찬가지입니다. JVM 힙 밖에서 메모리를 쓰므로 오버헤드를 별도로 잡아야 합니다.</p>

<h3 id="파드가-pending-상태입니다-원인은-무엇일까요">파드가 Pending 상태입니다. 원인은 무엇일까요</h3>

<p>스케줄링이 안 된 상태입니다. <code class="language-plaintext highlighter-rouge">kubectl describe pod</code>의 Events를 보면 이유가 나옵니다.</p>

<ul>
  <li><strong>Insufficient cpu/memory</strong> — 요청량을 만족하는 노드가 없음. requests를 줄이거나 노드를 늘림</li>
  <li><strong>node(s) had taint</strong> — toleration이 없어 배치 불가</li>
  <li><strong>PVC 바인딩 대기</strong> — 볼륨이 준비 안 됨</li>
  <li><strong>node affinity 불일치</strong> — 조건에 맞는 노드 없음</li>
</ul>

<h2 id="스토리지와-스케줄링">스토리지와 스케줄링</h2>

<h3 id="pv-pvc-storageclass의-관계를-설명해보세요">PV, PVC, StorageClass의 관계를 설명해보세요</h3>

<p><strong>PV(PersistentVolume)</strong>는 실제 스토리지 자원, <strong>PVC(PersistentVolumeClaim)</strong>는 그에 대한 요청, <strong>StorageClass</strong>는 동적 프로비저닝 방식의 정의입니다.</p>

<p>파드는 PVC를 참조하고, StorageClass가 있으면 PVC 생성 시 PV가 자동으로 만들어집니다.</p>

<p>접근 모드가 중요합니다. <code class="language-plaintext highlighter-rouge">ReadWriteOnce</code>는 노드 하나에서만 마운트 가능해서, 여러 파드가 공유해야 하면 <code class="language-plaintext highlighter-rouge">ReadWriteMany</code>를 지원하는 스토리지(NFS, EFS)가 필요합니다. EBS 같은 블록 스토리지는 RWO만 됩니다.</p>

<h3 id="taint와-toleration-nodeselector-affinity를-구분해보세요">taint와 toleration, nodeSelector, affinity를 구분해보세요</h3>

<ul>
  <li><strong>taint / toleration</strong> — 노드가 “나한테 아무거나 오지 마”라고 표시하고, 파드가 toleration으로 예외를 얻습니다. GPU 노드나 전용 노드 격리에 씁니다</li>
  <li><strong>nodeSelector / nodeAffinity</strong> — 파드가 “이런 노드로 가고 싶다”고 요청합니다</li>
  <li><strong>podAffinity / podAntiAffinity</strong> — 다른 파드와 같이/따로 배치합니다. 복제본을 서로 다른 노드에 흩뜨릴 때 antiAffinity를 씁니다</li>
</ul>

<p>taint는 <strong>노드가 미는 것</strong>, affinity는 <strong>파드가 당기는 것</strong>으로 기억하면 구분이 쉽습니다.</p>

<h3 id="hpa와-cluster-autoscaler는-무엇이-다른가요">HPA와 Cluster Autoscaler는 무엇이 다른가요</h3>

<p><strong>HPA(Horizontal Pod Autoscaler)</strong>는 지표(CPU, 메모리, 커스텀)에 따라 <strong>파드 수</strong>를 조절합니다.</p>

<p><strong>Cluster Autoscaler</strong>는 스케줄되지 못한 파드가 있으면 <strong>노드 수</strong>를 늘립니다.</p>

<p>둘은 함께 동작합니다. HPA가 파드를 늘렸는데 자리가 없으면 Pending이 되고, 그걸 보고 Cluster Autoscaler가 노드를 추가합니다.</p>

<p>Karpenter는 Cluster Autoscaler보다 유연하게(인스턴스 타입을 파드 요구에 맞춰 즉석에서 선택) 노드를 프로비저닝합니다.</p>

<h2 id="데이터-워크로드">데이터 워크로드</h2>

<h3 id="spark-on-kubernetes는-어떻게-동작하나요">Spark on Kubernetes는 어떻게 동작하나요</h3>

<p><code class="language-plaintext highlighter-rouge">spark-submit</code>이 Driver 파드를 만들고, Driver가 Kubernetes API를 통해 Executor 파드를 직접 생성합니다. YARN 대신 K8s가 자원 관리를 맡습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>spark-submit <span class="se">\</span>
  <span class="nt">--master</span> k8s://https://&lt;api-server&gt; <span class="se">\</span>
  <span class="nt">--deploy-mode</span> cluster <span class="se">\</span>
  <span class="nt">--conf</span> spark.kubernetes.container.image<span class="o">=</span>&lt;image&gt; <span class="se">\</span>
  <span class="nt">--conf</span> spark.executor.instances<span class="o">=</span>10 <span class="se">\</span>
  ...
</code></pre></div></div>

<p>YARN 대비 장점은 컨테이너 이미지로 의존성을 고정할 수 있다는 점, 다른 워크로드와 클러스터를 공유할 수 있다는 점입니다.</p>

<p>주의할 점도 있습니다.</p>

<ul>
  <li><strong>셔플 데이터</strong>가 Executor 파드의 로컬 디스크에 쌓입니다. 파드가 죽으면 재계산이 필요합니다. 동적 할당을 쓰려면 외부 셔플 서비스나 셔플 데이터 보존 설정이 필요합니다</li>
  <li><code class="language-plaintext highlighter-rouge">spark.executor.memory</code>와 <code class="language-plaintext highlighter-rouge">memoryOverhead</code>의 합이 파드 메모리 limit이 되므로, 이걸 넘기면 OOMKilled입니다</li>
  <li>노드 로컬 디스크(<code class="language-plaintext highlighter-rouge">emptyDir</code>) 용량이 부족하면 셔플 중 실패합니다</li>
</ul>

<h3 id="airflow를-k8s에서-운영할-때-무엇을-고려하나요">Airflow를 K8s에서 운영할 때 무엇을 고려하나요</h3>

<p><strong>KubernetesExecutor</strong>는 태스크마다 파드를 띄웁니다. 태스크별로 다른 이미지·리소스를 쓸 수 있고 격리가 좋지만, 파드 시작 지연(수 초~수십 초)이 붙습니다. 짧은 태스크가 많으면 오버헤드가 큽니다.</p>

<p><strong>CeleryExecutor</strong>는 워커를 미리 띄워두고 큐로 분배합니다. 시작이 빠르지만 워커 환경이 고정됩니다.</p>

<p><code class="language-plaintext highlighter-rouge">KubernetesPodOperator</code>는 실행자와 무관하게 특정 태스크만 파드로 띄우는 방식입니다. 무거운 의존성을 가진 작업을 격리할 때 유용합니다.</p>

<p><strong>DAG 파일 배포 방식</strong>도 정해야 합니다 — 이미지에 굽거나(재배포 필요), git-sync 사이드카를 쓰거나, 공유 볼륨을 마운트하거나.</p>

<h3 id="배치-잡에-cronjob을-쓸-때-주의할-점은-무엇인가요">배치 잡에 CronJob을 쓸 때 주의할 점은 무엇인가요</h3>

<ul>
  <li><strong>동시 실행 정책</strong> — <code class="language-plaintext highlighter-rouge">concurrencyPolicy: Forbid</code>로 두지 않으면 이전 실행이 안 끝났는데 다음이 시작됩니다</li>
  <li><strong><code class="language-plaintext highlighter-rouge">startingDeadlineSeconds</code></strong> — 컨트롤러가 잠시 멈춰 실행 시점을 놓쳤을 때 얼마나 늦게까지 실행할지</li>
  <li><strong>히스토리 제한</strong> — <code class="language-plaintext highlighter-rouge">successfulJobsHistoryLimit</code>을 안 두면 완료된 Job 오브젝트가 계속 쌓입니다</li>
  <li><strong>재시도</strong> — <code class="language-plaintext highlighter-rouge">backoffLimit</code>을 넘으면 실패로 확정됩니다. 멱등하지 않은 작업이면 재시도가 오히려 위험합니다</li>
</ul>

<p>복잡한 의존 관계가 있는 파이프라인은 CronJob보다 Airflow 같은 오케스트레이터가 맞습니다.</p>

<h3 id="문제가-생겼을-때-어떤-순서로-확인하나요">문제가 생겼을 때 어떤 순서로 확인하나요</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods                       <span class="c"># 상태 확인 (Pending / CrashLoopBackOff / OOMKilled)</span>
kubectl describe pod &lt;pod&gt;             <span class="c"># Events — 스케줄링·이미지·볼륨 문제</span>
kubectl logs &lt;pod&gt;                     <span class="c"># 현재 컨테이너 로그</span>
kubectl logs &lt;pod&gt; <span class="nt">--previous</span>          <span class="c"># 재시작 전 로그 (크래시 원인)</span>
kubectl get events <span class="nt">--sort-by</span><span class="o">=</span>.lastTimestamp
kubectl top pod                        <span class="c"># 실제 사용량</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">CrashLoopBackOff</code>는 원인이 아니라 결과입니다. <code class="language-plaintext highlighter-rouge">--previous</code> 로그를 봐야 실제 이유가 나옵니다.</p>]]></content><author><name>MMIX</name></author><category term="Interview" /><category term="kubernetes" /><category term="k8s" /><category term="devops" /><category term="interview" /><summary type="html"><![CDATA[파드와 컨트롤러, 리소스 requests/limits와 OOMKilled, 스토리지와 스테이트풀 워크로드, Spark on K8s와 Airflow 실행 환경을 데이터 엔지니어 관점에서 정리합니다.]]></summary></entry></feed>