<?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://gayuna.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://gayuna.github.io/" rel="alternate" type="text/html" /><updated>2025-09-15T07:24:10+09:00</updated><id>https://gayuna.github.io/feed.xml</id><title type="html">hello.gayuna</title><subtitle>안녕가유나</subtitle><author><name>gayuna</name></author><entry><title type="html">Claude Code in Action</title><link href="https://gayuna.github.io/llm/claude-code/" rel="alternate" type="text/html" title="Claude Code in Action" /><published>2025-09-14T00:00:00+09:00</published><updated>2025-09-14T00:00:00+09:00</updated><id>https://gayuna.github.io/llm/claude-code</id><content type="html" xml:base="https://gayuna.github.io/llm/claude-code/"><![CDATA[<h1 id="what-is-claude-code">What is Claude Code?</h1>

<h2 id="what-is-a-coding-assistant">What is a coding assistant?</h2>

<p>‘이 에러를 고쳐주세요’ 같은 task를 받으면 LLM은</p>

<ol>
  <li>Context를 가져와서</li>
  <li>계획을 짜고</li>
  <li>이를 수행</li>
</ol>

<p>하는 절차를 거칩니다.</p>

<p><img src="https://everpath-course-content.s3-accelerate.amazonaws.com/instructor%2Fa46l9irobhg0f5webscixp0bs%2Fpublic%2F1750967940%2F002_-_What_is_a_Coding_Assistant%3F_02.1750967940100.png" alt="image" /></p>

<p>그리고 Context를 가져오고 계획을 수행하는 단계에서 (1,3)에서는 파일을 읽고, 수정하고, 커맨드를 날리고, 문서를 참고하느 등의 작업이 필요하게 됩니다. LLM에게는 이런 권한이 없기 때문에 아무 설정 없이 파일을 읽으라고 하면 “파일을 읽을 수 없습니다” 정도의 응답밖에 나오지 않을 것입니다.</p>

<p><img src="https://github.com/user-attachments/assets/2987f768-7cec-4397-8ff5-702411410551" alt="image" /></p>

<p>이 때 어떤 흐름을 만들어주게 되냐면 coding assistant가 LLM에 명령을 보낼 때 만약 파일을 읽어야 한다면 ‘특정 포맷으로 파일 이름을 알려줘’라고 명령을 합니다. 그러면 LLM은 해당 포맷으로 읽어야 하는 파일 이름을 보내고, coding assistant는 다시 해당 파일을 읽어서 내용을 LLM에게 보냅니다. LLM은 이 내용을 기반으로 기존에 받았던 task를 수행합니다. 이러한 작업을 <code class="language-plaintext highlighter-rouge">Tool use</code> 라고 표현합니다.</p>

<h4 id="tool-use의-장점">Tool use의 장점</h4>
<ul>
  <li>Tackles harder tasks - Claude can combine different tools to handle complex work and will use tools it hasn’t seen before</li>
  <li>Extensible platform - You can easily add new tools to Claude Code, and Claude will adapt to use them as your workflow evolves</li>
  <li>Better security - Claude Code can navigate codebases without requiring indexing, which often means not sending your entire codebase to external servers</li>
</ul>

<h1 id="getting-hands-on">Getting hands on</h1>

<h2 id="claude-code-setup"><a href="https://docs.anthropic.com/en/docs/claude-code/setup">Claude Code setup</a></h2>

<h2 id="project-setup">Project setup</h2>
<p>제공하는 프로젝트(uizen)를 다운로드받거나 본인의 프로젝트 준비하기</p>

<h2 id="adding-context">Adding context</h2>

<p>꼭 필요한 context는 넣으면서도, 상관이 없거나 잘못된 output을 만들 수 있는 context는 들어가지 않도록 하는 것이 중요.</p>

<p><code class="language-plaintext highlighter-rouge">/init</code> 커맨드를 먼저 날리면 claude가 전체 프로젝트의 아키텍쳐등을 파악, 요약해서 <code class="language-plaintext highlighter-rouge">claude.md</code> 파일을 자동으로 생성해줌. 이 <code class="language-plaintext highlighter-rouge">claude.md</code>는:</p>
<ul>
  <li>claude가 코드의 위치 등을 더 빠르게 파악하게 함</li>
  <li>유저가 claude에게 일반적인 guidance를 제공하는 위치가 됨.</li>
  <li>claude에서 <code class="language-plaintext highlighter-rouge">#</code>을 치고 프롬프트를 입력하면 자동으로 ‘메모리 모드’가 되면서 <code class="language-plaintext highlighter-rouge">claude.md</code>파일을 업데이트할 수 있음</li>
  <li>어느 파일을 참고해야하는지 이미 알고있다면 <code class="language-plaintext highlighter-rouge">@</code>를 입력하고 파일을 지정해서 참고하게 할 수 있음. 이는 <code class="language-plaintext highlighter-rouge">claude.md</code> 내에서도 동일하게 사용 가능.</li>
</ul>

<h3 id="claude파일의-층위">CLAUDE.파일의 층위</h3>
<ul>
  <li>CLAUDE.md - Generated with /init, committed to source control, shared with other engineers</li>
  <li>CLAUDE.local.md - Not shared with other engineers, contains personal instructions and customizations for Claude</li>
  <li>~/.claude/CLAUDE.md - Used with all projects on your machine, contains instructions that you want Claude to follow on all projects</li>
</ul>

<h2 id="making-changes">Making changes</h2>

<p>스크린샷을 찍어서 <code class="language-plaintext highlighter-rouge">ctrl + V</code> (맥에서 <code class="language-plaintext highlighter-rouge">cmd+v</code> 아님에 유의!)를 해서 클로드에게 이미지를 제공할 수 있음</p>

<p>복잡한 변경이 필요할 경우 <code class="language-plaintext highlighter-rouge">Shift+Tab</code>을 두번 눌러 <code class="language-plaintext highlighter-rouge">Plan mode</code>로 들어갈 수 있음.</p>

<p><img src="https://github.com/user-attachments/assets/ccc4f260-eec6-4647-91ba-c48dcb804399" alt="image" /></p>

<p>Plan mode가 실행되고 나면 claude가 계획을 제공하는데, 사용자는 이를 받아들여서 claude가 작업하도록 하거나 놓친 것이 있다면 추가적으로 가이드를 줄 수 있음.</p>

<p>또 다른 방법으로는 <code class="language-plaintext highlighter-rouge">Thinking mode</code>를 들어가게 할 수 있음.</p>

<ul>
  <li><strong>Think</strong> - Basic reasoning</li>
  <li><strong>Think more</strong> - Extended reasoning</li>
  <li><strong>Think a lot</strong> - Comprehensive reasoning</li>
  <li><strong>Think longer</strong> - Extended time reasoning</li>
  <li><strong>Ultrathink</strong> - Maximum reasoning capability</li>
</ul>

<p>두가지 모드는 token을 추가적으로 소비하니 유의할 것</p>

<p>Planning Mode is best for (넓이):</p>
<ul>
  <li>Tasks requiring broad understanding of your codebase</li>
  <li>Multi-step implementations</li>
  <li>Changes that affect multiple files or components</li>
</ul>

<p>Thinking Mode is best for(깊이):</p>
<ul>
  <li>Complex logic problems</li>
  <li>Debugging difficult issues</li>
  <li>Algorithmic challenges</li>
</ul>

<h2 id="controlling-context">Controlling context</h2>

<p><code class="language-plaintext highlighter-rouge">esc</code>를 누르면 claude의 동작을 끊을 수 있음. 무언가 의도한 방향과 다르게 가고 있을 때 이렇게 끊어주면 좋음.</p>

<p>같은 실수 (존재하지 않는 파일을 참조하려고 함)를 반복한다면 <code class="language-plaintext highlighter-rouge">esc</code>로 끊고 <code class="language-plaintext highlighter-rouge">#</code>의 메모리 모드를 사용해 <code class="language-plaintext highlighter-rouge">claude.md</code>에 올바른 파일 경로/이름을 알려주면 이와 같은 실수를 피할 수 있음.</p>

<p>A라는 task에 대해 a,b,c,d의 sub-task가 있었는데, b를 하다가 에러가 발생해서 이를 debug한 경우, 해당 에러를 해결하기 위한 내용이 context에 많아서 A와 c,d에 대해 집중하지 못할 수 있음. 이런 경우 <code class="language-plaintext highlighter-rouge">esc</code>를 두번 누르면 과거의 특정 시점이 대화로 돌아가고 그 사이의 대화를 무시할 수 있음.</p>

<p>이번에는 c를 하다가 에러가 나서 또 디버깅을 오랫동안 했다고 가정했을 때, A와 a,b,c,d에 대한 컨텍스트로 돌아가야 하지만 동시에 c를 디버깅하면서 알아냈고 다음 task를 개발하는데 사용할만한 정보가 많을 수도 있음. 이럴 때는 <code class="language-plaintext highlighter-rouge">/compact</code> 커맨드를 사용하면 됨.</p>

<p>완전 새로운 작업을 시작해서 이전 context를 비우고 싶을 때는 <code class="language-plaintext highlighter-rouge">/clear</code> 커맨드를 사용하면 됨.</p>

<h2 id="custom-commands">Custom commands</h2>

<p>claude code의 기본 커맨트 외에 자주 반복하는 작업이 있다면 나만의 custom command를 만들 수 있음.</p>

<ol>
  <li>Find the <code class="language-plaintext highlighter-rouge">.claude</code> folder in your project directory</li>
  <li>Create a new directory called <code class="language-plaintext highlighter-rouge">command</code>s` inside it</li>
  <li>Create a new <code class="language-plaintext highlighter-rouge">markdown file</code> with your desired command name (like audit.md)</li>
</ol>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Write comprehensive tests for: $ARGUMENTS

Testing conventions:
<span class="p">*</span> Use Vitests with React Testing Library
<span class="p">*</span> Place test files in a __tests__ directory in the same folder as the source file
<span class="p">*</span> Name test files as [filename].test.ts(x)
<span class="p">*</span> Use @/ prefix for imports

Coverage:
<span class="p">*</span> Test happy paths
<span class="p">*</span> Test edge cases
<span class="p">*</span> Test error states
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/write_tests the use-auth.ts file in the hooks directory </code> 와 같이 argument를 넣어서 사용 가능.</p>

<p>주의: 파일 만들고 나서 cluade를 다시 실행해야 함</p>

<h2 id="mcp-servers-with-claude-code">MCP servers with Claude Code</h2>

<p>MCP 서버를 연결해서 Claude Code의 기능을 확장할 수 있음.
예를 들어 <code class="language-plaintext highlighter-rouge">claude mcp add playwright npx @playwright/mcp@latest</code> 커맨드를 터미널에서 입력해서 playwright의 mcp 서버를 연결, 로컬에서 띄우고 claude로 돌아가서 ‘브라우저를 띄워서서 localhost:3000로 연결해라’ 같은 명령을 줄 수 있음. 이 때는 permission이 필요한데, <code class="language-plaintext highlighter-rouge">.claude/settings.local.json</code>에 아래와 같이 항상 권한을 줄 mcp를 지정할 수 있음. (<code class="language-plaintext highlighter-rouge">_</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">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"allow"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"mcp__playwright"</span><span class="p">],</span><span class="w">
    </span><span class="nl">"deny"</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>이전에 봤던 테크닉과 결합하여 다음과 같은 프롬프트가 가능함.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Navigate to localhost:3000, generate a basic component, review the styling, and update the generation prompt at @src/lib/prompts/generation.tsx to produce better components going forward.
</code></pre></div></div>

<h2 id="github-integration">GitHub integration</h2>

<p>GitHub Action을 통해 동작하는 공식 integration이 존재함. claude에 <code class="language-plaintext highlighter-rouge">/install-github-app</code> 명령어를 입력하면 1. GitHub에 Claude Code 앱을 설치하고 2. 나의 API key를 등록한 후 3.workflow 파일들과 함께 PR을 생성함.</p>

<h3 id="mention-action">Mention Action</h3>
<p>Issue를 등록할 때 <code class="language-plaintext highlighter-rouge">@claude</code>와 같은 식으로 claude를 부르면 claude가 해당 issue의 내용을 분석하고, 필요하다면 해당 codebase에 수정할 내용을 작성하여 해당 issue 에 답하거나 혹은 새로운 PR을 등록함.</p>

<h3 id="pull-request-action">Pull Request Action</h3>
<p>새로운 PR을 올릴 때 PR을 리뷰하고 이 변경의 영향을 체크해서 해당 레포트를 PR에 등록해줌</p>

<h3 id="custom-workflow">Custom workflow</h3>

<p>필요하면 본인의 레포만을 위한 workflow를 만들 수 있음.</p>

<ul>
  <li>Project Setup
```yml</li>
  <li>name: Project Setup
run: |
  npm run setup
  npm run dev:daemon
```</li>
  <li>Custom Instructions
    <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">custom_instructions</span><span class="pi">:</span> <span class="pi">|</span>
<span class="err">T</span><span class="s">he project is already set up with all dependencies installed.</span>
<span class="err">T</span><span class="s">he server is already running at localhost:3000. Logs from it</span>
<span class="err">a</span><span class="s">re being written to logs.txt. If needed, you can query the</span>
<span class="err">d</span><span class="s">b with the 'sqlite3' cli. If needed, use the mcp__playwright</span>
<span class="err">s</span><span class="s">et of tools to launch a browser and interact with the app.</span>
</code></pre></div>    </div>
  </li>
  <li>MCP Server Configuration
    <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">mcp_config</span><span class="pi">:</span> <span class="pi">|</span>
<span class="err">{</span>
  <span class="s">"mcpServers": {</span>
    <span class="s">"playwright": {</span>
      <span class="s">"command": "npx",</span>
      <span class="s">"args": [</span>
        <span class="s">"@playwright/mcp@latest",</span>
        <span class="s">"--allowed-origins",</span>
        <span class="s">"localhost:3000;cdn.tailwindcss.com;esm.sh"</span>
      <span class="s">]</span>
    <span class="s">}</span>
  <span class="s">}</span>
<span class="err">}</span>
</code></pre></div>    </div>
  </li>
  <li>Tool Permissions
    <div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">allowed_tools</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Bash(npm:*),Bash(sqlite3:*),mcp__playwright__browser_snapshot,mcp__playwright__browser_click,..."</span>
</code></pre></div>    </div>
  </li>
</ul>

<h1 id="hooks-and-the-sdk">Hooks and the SDK</h1>

<h2 id="introducing-hooks">Introducing hooks</h2>
<p>Hook은 claude code가 실행되기 전 혹은 후에 실행됨
예를들면 코드가 변경되고 나서 formatter를 돌린다든가, 테스트를 실행하도록 할 수 있음.</p>

<p><img src="https://everpath-course-content.s3-accelerate.amazonaws.com/instructor%2Fa46l9irobhg0f5webscixp0bs%2Fpublic%2F1752618158%2F010_-_Introducing_Hooks_06.1752618158162.png" alt="image" /></p>

<p><code class="language-plaintext highlighter-rouge">settings.json</code> 파일에 직접 작성하거나 /hooks 커맨드를 사용해 추가할 수 있음.</p>

<p><img src="https://everpath-course-content.s3-accelerate.amazonaws.com/instructor%2Fa46l9irobhg0f5webscixp0bs%2Fpublic%2F1752618160%2F010_-_Introducing_Hooks_15.1752618160073.png" alt="image" /></p>

<ul>
  <li>Code formatting - Automatically format files after Claude edits them</li>
  <li>Testing - Run tests automatically when files are changed</li>
  <li>Access control - Block Claude from reading or editing specific files</li>
  <li>Code quality - Run linters or type checkers and provide feedback to Claude</li>
  <li>Logging - Track what files Claude accesses or modifies</li>
  <li>Validation - Check naming conventions or coding standards</li>
</ul>

<h2 id="defining-hooks">Defining hooks</h2>
<p>아래와 같은 과정을 거쳐 hook을 생성하게 됨</p>
<ol>
  <li><strong>Decide on a PreToolUse or PostToolUse hook</strong> - PreToolUse hooks can prevent tool calls from executing, while PostToolUse hooks run after the tool has already been used</li>
  <li><strong>Determine which type of tool calls you want to watch for</strong> - You need to specify exactly which tools should trigger your hook</li>
  <li><strong>Write a command that will receive the tool call</strong> - This command gets JSON data about the proposed tool call via standard input</li>
  <li><strong>If needed, command should provide feedback to Claude</strong> - Your command’s exit code tells Claude whether to allow or block the operation</li>
</ol>

<p>예를들어, claude가 <code class="language-plaintext highlighter-rouge">.env</code> 파일의 내용을 읽지는 못하게 하고 싶다면, PreToolUse의 hook을 만들어야 함. 그리고 cluade가 <code class="language-plaintext highlighter-rouge">Read</code> 혹은 <code class="language-plaintext highlighter-rouge">Grep</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">"session_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2d6a1e4d-6..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"transcript_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/sg/..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"hook_event_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PreToolUse"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tool_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Read"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tool_input"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/code/queries/.env"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>위와 같이 <code class="language-plaintext highlighter-rouge">Read</code> 툴인데 읽어서는 안되는 파일을 요청하면 읽지 않고 블락하도록 해야 함.</p>

<h2 id="implementing-a-hook">Implementing a hook</h2>
<p>이제 위의 내용을 실제로 구현해보자면
먼저 구현할 <code class="language-plaintext highlighter-rouge">settings.json</code>파일을 결정함. (글에서는 <code class="language-plaintext highlighter-rouge">.claude/settings.local.json</code>로 해서 나만 쓸 로컬 hook으로 구현)</p>

<p><img src="https://github.com/user-attachments/assets/51e56fdf-9e31-408a-a774-030ea5c8c350" alt="i" /></p>

<p><code class="language-plaintext highlighter-rouge">settings.json</code>파일에 <code class="language-plaintext highlighter-rouge">hooks</code>항목 아래 중 <code class="language-plaintext highlighter-rouge">PreToolUse</code>에 하나를 추가함. <code class="language-plaintext highlighter-rouge">Read</code>와 <code class="language-plaintext highlighter-rouge">Grep</code> 명령어를 볼 것이기 때문에 <code class="language-plaintext highlighter-rouge">matcher</code>에 <code class="language-plaintext highlighter-rouge">Read|Grep</code>을 추가하고, command로 우리가 실행할 파일(hook script)을 만들어서 그 경로를 넣어주게 됨.</p>

<p>hook script 파일은 다음과 같은 내용을 가지게 됨:</p>
<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">chunks</span> <span class="o">=</span> <span class="p">[];</span>
  <span class="k">for</span> <span class="k">await</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">chunk</span> <span class="k">of</span> <span class="nx">process</span><span class="p">.</span><span class="nx">stdin</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">chunks</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">chunk</span><span class="p">);</span>
  <span class="p">}</span>
  
  <span class="kd">const</span> <span class="nx">toolArgs</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">Buffer</span><span class="p">.</span><span class="nx">concat</span><span class="p">(</span><span class="nx">chunks</span><span class="p">).</span><span class="nx">toString</span><span class="p">());</span>
  
  <span class="c1">// Extract the file path Claude is trying to read</span>
  <span class="kd">const</span> <span class="nx">readPath</span> <span class="o">=</span> 
    <span class="nx">toolArgs</span><span class="p">.</span><span class="nx">tool_input</span><span class="p">?.</span><span class="nx">file_path</span> <span class="o">||</span> <span class="nx">toolArgs</span><span class="p">.</span><span class="nx">tool_input</span><span class="p">?.</span><span class="nx">path</span> <span class="o">||</span> <span class="dl">""</span><span class="p">;</span>
  
  <span class="c1">// Check if Claude is trying to read the .env file</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">readPath</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="dl">'</span><span class="s1">.env</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">"</span><span class="s2">You cannot read the .env file</span><span class="dl">"</span><span class="p">);</span>
    <span class="nx">process</span><span class="p">.</span><span class="nx">exit</span><span class="p">(</span><span class="mi">2</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="gotchas-around-hooks">Gotchas around hooks</h2>

<p><strong>보안 실천 방법</strong> - 더 안전한 hook을 작성하기 위한 몇 가지 핵심적인 방법:</p>
<ul>
  <li>입력 값 검증 및 삭제 (Validate and sanitize inputs) - 입력 데이터를 맹목적으로 신뢰하지 마세요.</li>
  <li>항상 쉘 변수를 따옴표로 묶기 (Always quote shell variables) - <code class="language-plaintext highlighter-rouge">$VAR</code>가 아닌 <code class="language-plaintext highlighter-rouge">"$VAR"</code>를 사용하세요.</li>
  <li>경로 순회(Path Traversal) 차단 - 파일 경로에 <code class="language-plaintext highlighter-rouge">..</code>가 있는지 확인하세요.</li>
  <li>절대 경로 사용 (Use absolute paths) - 스크립트의 전체 경로를 명시하세요.</li>
  <li>민감한 파일 건너뛰기 (Skip sensitive files) - <code class="language-plaintext highlighter-rouge">.env</code>, <code class="language-plaintext highlighter-rouge">.git/</code>, <code class="language-plaintext highlighter-rouge">keys</code> 등은 피하세요.</li>
</ul>

<p>이 중 절대 경로를 사용하라는 것은 path interception 및 binary planting 공격을 완화하는 데 도움이 됨. 하지만 이는 <code class="language-plaintext highlighter-rouge">settings.json</code> 파일을 공유하는 것을 어렵게 만듬.</p>

<p>이 문제를 해결하기 위해,프로젝트에는 <code class="language-plaintext highlighter-rouge">settings.example.json</code> 파일이 있음. 그 안의 스크립트 참조에는 <code class="language-plaintext highlighter-rouge">$PWD placeholder</code>가 포함되어 있고, <strong>npm run setup</strong>을 실행하면, 일부 종속성이 설치될 뿐만 아니라 scripts 디렉터리 안에 있는 init-claude.js 스크립트도 실행됨. 이 스크립트는 해당 <code class="language-plaintext highlighter-rouge">$PWD placeholder</code>를 머신에 있는 프로젝트의 absolute path로 교체하고, <code class="language-plaintext highlighter-rouge">settings.example.json</code> 파일을 복사한 후 파일명을 <strong>settings.local.json</strong>으로 변경함.</p>

<p>이것으로 settings.json 파일을 공유하면서도 권장되는 absolute paths를 계속 사용할 수 있음!</p>

<h2 id="유용한-hook들">유용한 Hook들</h2>

<ul>
  <li>타입 checker: ts 파일이 수정될때마다 <code class="language-plaintext highlighter-rouge">tsc --noEmit</code>를 실행하게 만들고 -&gt; 에러가 뜨면 exit(2)</li>
  <li>Query Duplication Prevention: 새로운 쿼리를 만드려고 시도하기 전에 별도의 claude code를 띄워서 비슷한 쿼리가 존재하는지 질의. 존재한다면 exit(2)</li>
</ul>

<p>Pre/post tool use 외에 다음과 같은 Hook도 있음:</p>
<ul>
  <li>Notification - Claude Code가 알림을 보낼 때 실행. 이는 Claude가 도구를 사용하는 데 권한이 필요하거나, Claude Code가 60초 동안 유휴 상태일 때 발생</li>
  <li>Stop - Claude Code가 응답을 마쳤을 때 실행</li>
  <li>SubagentStop - subagent(UI에서는 “Task”로 표시됨)가 완료되었을 때 실행</li>
  <li>PreCompact - 수동 또는 자동으로 compact 작업이 발생하기 전에 실행</li>
</ul>

<p>hook을 만들 때 주의사항:</p>
<ul>
  <li>명령어로 들어오는 stdin 입력은 실행되는 hook의 유형(PreToolUse, PostToolUse, Notification 등)에 따라 변경됨</li>
  <li>tool_input에 포함된 내용은 호출된 도구에 따라 달라짐 (PreToolUse 및 PostToolUse hook의 경우)</li>
</ul>

<p>아래와 같은 hook을 사용해서 hook에 대한 입력을 post-log.json 파일에 기록하여 확인해보는 것이 권장됨:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"PostToolUse"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">또는</span><span class="w"> </span><span class="s2">"PreToolUse"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Stop"</span><span class="w"> </span><span class="err">등</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"matcher"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hooks"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"command"</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">"jq . &gt; post-log.json"</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>

<h2 id="claude-code-sdk">Claude Code SDK</h2>

<p>Python, TS, commandline에서 claude code SDK를 사용할 수 있음.
기본적으론 Read만 가능하고, settings.json이나 아니면 코드에 명시적으로 다른 tool을 주어야 다른 기능도 사용 가능.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">query</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@anthropic-ai/claude-code</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">prompt</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">Look for duplicate queries in the ./src/queries dir</span><span class="dl">"</span><span class="p">;</span>

<span class="k">for</span> <span class="k">await</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">message</span> <span class="k">of</span> <span class="nx">query</span><span class="p">({</span>
  <span class="nx">prompt</span><span class="p">,</span>
  <span class="na">options</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">allowedTools</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">Edit</span><span class="dl">"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}))</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">message</span><span class="p">,</span> <span class="kc">null</span><span class="p">,</span> <span class="mi">2</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>다음과 같은 상황에서 유용하게 사용할 수 있음:</p>
<ul>
  <li>Git hooks that automatically review code changes</li>
  <li>Build scripts that analyze and optimize code</li>
  <li>Helper commands for code maintenance tasks</li>
  <li>Automated documentation generation</li>
  <li>Code quality checks in CI/CD pipelines</li>
</ul>]]></content><author><name>gayuna</name></author><category term="LLM" /><category term="AI" /><category term="LLM" /><summary type="html"><![CDATA[What is Claude Code?]]></summary></entry><entry><title type="html">2025 이직 후기 feat.캐나다 워킹홀리데이로 개발자 취업한 썰 푼다 (2/2)</title><link href="https://gayuna.github.io/etc/self-reflect-trans-canada-2/" rel="alternate" type="text/html" title="2025 이직 후기 feat.캐나다 워킹홀리데이로 개발자 취업한 썰 푼다 (2/2)" /><published>2025-09-08T00:00:00+09:00</published><updated>2025-09-08T00:00:00+09:00</updated><id>https://gayuna.github.io/etc/self-reflect-trans-canada-2</id><content type="html" xml:base="https://gayuna.github.io/etc/self-reflect-trans-canada-2/"><![CDATA[<p>아무도 의견을 안주셔서 그냥 제맘대로 씁니다 ^_^;</p>

<p>여기 나오는 이야기는 기존에 사람들이 말하는 방법이랑 상당히 상충될 수 있습니다. 본인이 생각하시기에 맞다고 생각하는 방법을 따라가시면 됩니다. 반복해서 언급되겠지만, 포인트는 내 강점이 어디있는지를 찾고, 그 강점이 잘 적용될 방법으로 job searching을 하는 것이라고 생각합니다.</p>

<p>요새 북미쪽 취준 알아보라고 하면 다들 1000개씩 넣으라고 합니다. 그래도 될까말까라고요. 근데 이 방법 제가 1-2월에 해봤는데요, 일단 저한테는 안맞았습니다. 일단 지겹고요, 그렇게 대충 넣는 회사를 제가 열심히 보고 고를 수도 없어요. 그렇게 고르지 않은 회사가 저한테 맞는 회사일지도 알 수 없습니다.</p>

<p>그리고 크리티컬한건 이 부분인데, 한번 떨어진 회사는 가까운 시일 내에 재지원한 경우 바로 떨어질 수도 있습니다. 보통 인터뷰를 보고 떨어진 경우에는 명시적으로 쿨타임이 있는데, 서류만 넣고 떨어진 경우에도 쿨타임이 있는 경우가 있다고 해요. 저처럼 서비스 위주로 만들던 개발자가 갑자기 인프라쪽 포지션 넣다가 떨어졌는데, (애초에 가능성이 매우 낮았을거고) 이후에 완전 제가 기존에 하던거랑 똑같은 포지션이 떳는데 과거 지원 이력때문에 걸리면 억울하지 않겠어요?</p>

<p>또 많이 나오는게 레퍼럴인데, 레퍼럴은 당연히 있으면 좋습니다. 하지만 현재 상황은 레퍼럴이 있어도 면접이 잡히는 상황이 아닙니다. 그리고 저도 들은 이야기지만, 레퍼럴이 다 같은 레퍼럴이 아니라고 합니다. 회사마다 다르지만 strong한 Referral인지 선택하게 만드는 회사도 있다고 하고, 전 직장동료인지 등 관계를 입력하는 곳도 있다고 하고요. 그럼 당연히 친구보다는 전 직장동료의, weak보다는 strong referral이 낫겠죠? 이걸 많이 구할 수 있다면 당연히 좋겠지만, 일단 저는 아니었습니다. 빅텍은 오히려 레퍼럴 해주겠다는 분들이 계셨는데, 더 fit 맞아보이는 position 기다려보다가 넣지도 못했네요.</p>

<p>그래서 저는 어떻게 했냐면, 저는 리쿠르터가 저에게 먼저 접근하게 만드는게 젤 낫다고 생각했습니다. <a href="https://gayuna.github.io/etc/accidental-trans-1/">시장 좋던 시절에 이렇게 해서 이직</a>을 했었고요, 연말에 봤던 아마존 인터뷰도 리쿠르터의 InMail로 시작되었어요. 시장이 안좋으면 확률이 낮긴 하지만, 어쨋든 리쿠르터가 연락와서 시작하면 서류 단계를 스킵하게 되기 때문에 서류 통과가 힘든 현재 상태에서는 이쪽이나 저쪽이나라고 생각했습니다. 그래서 제가 한 일들은 대부분 어떻게 하면 링크드인에서 리쿠르터가 나에게 연락을 줄까?에 집중되어 있었고, 실제로 현재 합격한 회사도 직원으로부터 DM을 받아서 인터뷰를 보기 시작했습니다.</p>

<h1 id="캐나다에-오기-전에-갖추어야-할-것">캐나다에 오기 전에 갖추어야 할 것</h1>

<h2 id="경력">경력</h2>
<p>한줄요약: <strong>2025년 기준 경력 없으면 캐나다 오지 마세요…</strong></p>

<p>이유는 간단합니다. 신입 실업율이 최악인데, 우리나라에서 무경력인 사람이 캐나다 왔을 때는 캐나다에서 학교 나와서 entry level 노리는 사람보다도 경쟁력이 떨어집니다. 여기 와서 학생들 이야기를 듣고 알게된건데, 여기는 학교 다닐 때 부터 co-op이니 인턴이니 해서 2년정도 경력이 있는게 거의 기본이더라고요. 이게 없으면 졸업이 안되기 때문에 주를 넘어가서라도 하고 오는 분위기고, 나라에서도 이 포지션이 열리도록 장려하고 있습니다. 그리고 이 포지션들은 학교 재학생들 대상으로 열리기 때문에 졸업한 상태로 캐나다로 넘어와서 들어갈 수 있는 자리도 아니고요. 사실상 우리나라의 ‘경력 있는 신입’이 디폴트인 나라입니다.
시니어 포지션 넣을 수 있다면 도전해볼만 하겠습니다. 일단 시니어 포지션이 엔트리 레벨보다 많기도 하고, 시니어정도 되면 초반의 인턴 2년 정도는 희석될만한 연차니까요.</p>

<p>신입이라면 돈은 더 들겠지만 차라리 <a href="https://www.cs.ubc.ca/students/undergrad/degree-programs/bcs-program-second-degree">여기 대학의 2nd degree 프로그램</a>을 알아보는 것이 낫다고 생각합니다. 이 경우에도 바로 co-op/인턴 구할 수 있게 인터뷰 준비는 한국에서 하고 오는게 낫다고 생각해요. (요새 미국 석사도 들어가자마자 인턴 구할 수 있게 인터뷰 준비는 한국에서 다 하고간다죠?) 워홀 비자는 나중에 비자 이슈가 있을 때를 대비해서 남겨놓고요. 저같은 경우 워홀 막차로 왔기 때문에 죽이되든 밥이되든 이걸로 해결해야 했는데, 만약 제가 20대라면 이쪽으로 진지하게 알아볼 것 같습니다.</p>

<h2 id="링크드인-이력서-포트폴리오">링크드인, 이력서, 포트폴리오</h2>
<p>이 세가지-포트폴리오는 optional이니 빼면 두가지-는 무조건 완성 하고 와야한다고 생각합니다. 그리고 이것들을 만드는데 있어서 제가 신경쓴것은 ‘내 계정이 스캠으로 보이지 않도록’ 이었습니다. 저라는 사람이 엔지니어로서 실존해야 리쿠르터들이 연락 할 생각을 하겠죠?
고리님이 추천해주셨던 <a href="https://www.youtube.com/playlist?list=PLo-kPya_Ww2zqOZVXMNQCJeTNAaan8GcW">Jeff Su의 링크드인 플레이리스트</a>를 쭉 들으며 링크드인을 정리했습니다. 그리고 링크드인의 포스트를 영어로 작성하기 시작했습니다. 내가 캐나다에 사는 영어밖에 못하는 리쿠르터인데, 이 사람이 캐나다에 산다고는 하는데 프로필 보니까 외국어밖에 없어. 그럼 당연히 말걸지 않고 스킵할 것 같지 않나요?</p>

<p>이력서는 앞선 글에서도 언급했듯, <a href="https://discord.gg/wKdMvFpSDp">스터디클럽++</a>에서 했던 이력서 워크샵에도 참여해서 만든 버젼으로 지원했습니다. <a href="https://docs.google.com/document/d/1sdeyhAOx-hn4IkdCEzCOlXfhcIQ1mrfg7L86qlYMG5c">이 링크에서 보시면 알겠지만</a> 글자가 빽빽한 편도 아니고, 제가 했던 일이나 다뤘던 기술을 다 우겨넣으려고 노력하지 않았습니다. 워크샵에서 주안점을 둔 내용은 1.비전공자인 리쿠르터도 술술 읽히는 읽고싶은 이력서 만들기 2.내가 만들어낸 value 위주로 작성하기 입니다. 그리고 무엇보다 많이 덜어내기 위해 노력한 것 같습니다.
이력서 워크샵 거의 열리면 당일 마감되긴 하는데 정말 좋으니 우연히 시간 맞으신다면 들으시는거 강추합니다. 올해 하반기 워크샵은 완료되어서 아마 내년 상반기에나 열릴 것 같지만요.</p>

<p>또 한가지, 이력서를 냈는데 해외 전화번호가 써져있으면 당연히 전화 걸 생각도 안들고 패스되겠죠? 이 생각이 들어서 캐나다 전화번호를 넣을 수 있는 방법을 연구했습니다. 처음엔 미리 이심이든 뭐든 개통할 수 있나 알아봤는데 쉽지 않았고요, 결국은 <a href="https://blog.naver.com/ssooyyaa55/223738592848">인터넷 전화를 개통해서 해당 번호를 넣어두었습니다.</a>
깃허브로 만든 포트폴리오는 어느날 자려고 누웠다가 디자이너분들은 앱 리디자인 하는데 나도 시스템디자인 하면서 새로 디자인해서 포트폴리오 만들면 되는거 아냐? 하고 시작했습니다. 그래서 전체 포트폴리오는 깃허브에 자기소개 위주로 만들고, system redesign은 블로그에 쓰되 쓰고나서 매번 깃허브와 링크드인에도 링크를 걸었습니다. 링크드인에 올릴 때는 또 당연히 영어버젼을 만들어서 영어 소개글과 함께 링크했고요.</p>

<p>아 그리고 이 깃허브나 이력서에서 blog 링크를 눌렀을 때는 영어로 된 포스팅만 보이도록 했습니다. 저같은 경우는 영어 블로그를 따로 만들 기력까지는 없어서 모든 영어로 만든 글에 English 태그를 추가한 후, 해당 태그의 글만 보이는 화면으로 링크를 설정했습니다. 이거도 당연히 영어밖에 못하는 사람이 보기에는 영어 아티클이 보이는게 유의미할테니까요.</p>

<p>그리고 링크드인은 그냥 사용하는게 아니라 ‘캐나다 사람들의 네트워크 안에서’사용하는게 중요하다고 생각합니다. 한국에 있는 사람들 글에만 좋아요 누르는데 아무리 포스트를 영어로 써봤자 캐나다 사람들이 볼까요? 포지션 뜨는 회사들 페이지 들어가서 I’m interested 다 누르고 회사들 팔로우도 하고, 그 회사 임직원들 중에 링크드인 열심히 쓰는 사람들 팔로우 하고 좋아요도 했습니다. (이 시점까지는 친구신청까지는 잘 못했는데, NSA 하면서 보니 소개 글 잘 쓰면 모르는 사람 친추도 생각보다 잘 받아주더라구요.)</p>

<h2 id="hard-skill">Hard Skill</h2>
<p>열심히 링크드인을 하는 이유는 인터뷰를 잡기 위해서입니다. (이거는 1000개 이력서를 넣기로 결정하신 분도 동일할거라고 생각합니다.) 잡마켓이 힘들다는 것은 이력서는 넣기 쉽지만 면접을 잡기는 힘든 상황이죠. 그럼 이 소중한 먼접 기회를 어떻게 해야할까? 저는 높은 확률로 면접을 통과할 수 있는 준비는 미리 한국에서 하고 와야한다고 생각합니다. 이거는 학교다니면서 신입 취준 준비할 때 거기 코칭하시는 분이 인적성 공부하라고 하면서 하신 말씀이었어요. 다들 원서는 열심히 넣으면서 인적성 공부는 안해서 인적성에서 다 떨어진다고. 근데 인적성은 미리 공부할 수 있다. 미리미리 스터디 해서 인적성 합격률 100%를 만들어두면 면접이 몇배는 늘어날거고 그럼 합격을 얼마나 더 빨리 할 수 있을 것 같냐고.</p>

<p>이 인터뷰 말인데, 이거도 저는 ‘나에게 맞는 전형을 보는게 낫다’ 라고 생각합니다. 그리고 저는 과제보다는 인터뷰였어요. 그리고 인터뷰 중에서도 기술에 대해서 정답을 물어보는 한국식 인터뷰가 아니라 코딩, 시스템 디자인, behaviour 인터뷰가 더 적성에 맞을거라고 생각했습니다. 왜냐면 하나의 기술을 깊게 파는게 아니라 그때그때 회사에 필요한거면 다 배워서 쓰는 타입의 사람이었거든요.</p>

<p>과제가 싫은건 걍 동기부여가 너무 부족했습니다. 솔직히 인터뷰는 하고나면 잘봤다 못봤다라는 감이라도 오는데 과제는 뭘 원하는지도 모르겠고 (이거도 자주 해보신분들은 감이 오시겠죠?) 결과적으로 너무 꽁짜로 일해주는 느낌이라 의욕이 안나더라고요…ㅋㅋㅋ 하지만 분명 라이브 코딩 인터뷰보다 이게 맞으시는 분들도 있을거라 생각하고, 그러면 이런 과제 전형 있다고 하는 회사들을 집중적으로 노리시는게 낫다고 봅니다.</p>

<p>여튼 저는 적당히 인터뷰어와 티키타카하면서 인터뷰 이끌어가는데 스스로 장점이 있다고 생각했고, 이제와서 과제를 잘하기 위해 연구하는거보다 그냥 인터뷰를 뿌수고 1등해서 들어가는게 더 확률이 높다고 생각했습니다.</p>

<h3 id="코딩-인터뷰">코딩 인터뷰</h3>

<p>코딩인터뷰는 솔직히 2021년 이직 때 감을 잡았어서 인터뷰 운용 방식은 따로 공부하지 않았습니다. 대신 LeetCode 150 스터디 들어가서 일주일에 5문제씩 꾸준히 푸는걸 디폴트로, neetcode에서 평소에 좀 헷깔리는 테마들(예를들면 binary search라든가)를 집중적으로 풀면서 준비했습니다. 근데 진짜 리트코드 준비하지 않고 캐나다 넘어오시는거 진짜 의미 없다고 생각합니다.</p>

<h3 id="시스템-디자인-인터뷰">시스템 디자인 인터뷰</h3>

<p>저는 백엔드를 2021년에 쿠팡 가면서 시작했어서 시스템 디자인 면접이 가장 병목이 될거라 생각했습니다. (behaviour은 그 때 AWS에서도 feedback이 진짜 좋았어서 별로 걱정하지 않았어요. 쿠팡 3년 다니면서 쌓인 에피소드도 많았고요.) 작년에 <code class="language-plaintext highlighter-rouge">가상 면접 사례로 배우는 대규모 시스템 설계 기초</code> 책으로 시작해서 <code class="language-plaintext highlighter-rouge">데이터 중심 애플리케이션 설계</code>, 그리고 <code class="language-plaintext highlighter-rouge">Hello Interview</code>까지 1년 가까이 꾸준히 공부했습니다. 그리고 처음에는 각각의 테마도 별개같고, 나의 업무와도 별개같은데 하다보니까 어느순간 리트코드도 아 이런 문제때문에 저런 문제 타입이 생겼구나 하는 깨달음이 오는 것처럼, 아 회사에서 A를 했었는데 그거의 바탕에 B의 설계가 깔려있구나 하는 깨달음이 오는 상황이 발생하더라고요. 근데 진짜 시니어 레벨 준비하실건데 시스템 디자인 준비하지 않고 캐나다 넘어오시는거 진짜 의미 없다고 생각합니다222…</p>

<h2 id="영어-실력-얼마나-필요하냐구요">영어 실력 얼마나 필요하냐구요?</h2>

<p>이 질문을 많이 들었는데, 저같은 경우는 이번 취준에 있어서 영어 실력이 크게 걸림돌이 된다는 생각 까지는 들지 않았습니다. 딱 한번 특정 사이트 링크를 던져주고 거기서 무료 테스트를 보고 점수를 알려달라는 회사가 있긴 했는데, 거기도 그냥 그자리에서 보고 점수 주고 나니까 넘어갔구요.
어차피 한국 회사처럼 토익 몇점 이상 이런거 필요한게 아니고 실제 업무를 보는게 중요하니까, 제가 내린 현재의 기준은 ‘내가 준비한걸 100% 말하지 못해서 답답할 수는 있어도, 듣는 상대방이 무슨소린지 몰라서 답답하게 만들면 안된다’입니다. 제 생각에 맨 처음 리쿠르터의 Screening call은 이력서의 경력이 가짜가 아니고 이 영어에만 문제가 없다면 거의 통과할 수 있을거라고 생각합니다.</p>

<h1 id="캐나다-와서-할-일">캐나다 와서 할 일</h1>

<p>넘어가실 날짜 대충 각이 잡히시면 링크드인 위치를 갈 도시로 바꾸시는 것도 추천합니다. (대충 2-3개월 이내로 남았을 때.) 열심히 링크드인 하면서 가끔이라도 리크루터한테서 인터뷰 보자는 메세지가 오면 대충 준비가 된거라고 생각합니다. 아얘 없다면 저라면 캐나다 오는거 재고해볼 것 같습니다. (경력이 문제인지, 링크드인을 더 활발히 해야하는지 등) 물론 갯수는 적어도 인터뷰가 잡히기만 한다면 그 인터뷰 위에서 준비한걸로 깨고 들어가면 되는 문제니까요.</p>

<h2 id="인터뷰-준비---recruiter-call">인터뷰 준비 - Recruiter Call</h2>

<p>보통 맨 처음은 리크루터가 스크리닝을 하게될텐데, 위에서 언급했듯 경험에 대해서 잘 말할 수 있고, 그걸 영어로 하는데 크게 문제가 없으면 in mail로 시작할 경우 거의 패스할거라고 생각합니다.
이 단계에서 리쿠르터가 니 합법적으로 일할 수 있니? 혹은 비자 status가 어떻게 되니?라고 물어봅니다. 그리고 리쿠르터는 비자의 전문가가 아닙니다. Working holiday 비자라고 말해봤자 뭔소린지 모른다는 뜻입니다. 비자 최종 허가까지 받으신 분들은 알겠지만 Working holiday 비자는 open work permit의 형태로 나오니 그냥 ‘나 2년짜리 open work permit 있어’라고 설명하시면 충분합니다.</p>

<h2 id="online-assessment-준비">Online Assessment 준비</h2>

<p>이건 결국 leetcode 많이 준비했으면 별 문제 없을거라고 생각하는데… 빅텍에 한해서 한가지 팁이라면 빅텍은 워낙 응시자수가 많다보니 leetcode에 후기들이 모입니다. 근데 이 후기들이 생겼다 없어졌다 하더라고요? 아마존 기준 시험 잡히면 일주일정도 수시로 discuss 게시판 들어가보면서 다른사람들이 뭐 나왔다고 하는지, 유사한 문제 뭐였다고 하는지 보고 준비하시는걸 추천합니다. 이 방법으로 두 문제 중 한문제는 보자마자 풀 수 있었습니다.</p>

<h2 id="인터뷰-준비---technical-interview">인터뷰 준비 - Technical interview</h2>
<p>코딩인터뷰 어떻게 준비했는지는 지난 이직 후기에 자세하게 써놨으니 참고해주세요. 난번 이직 후기에서도 coding interview에서 Mock interview 꼭 해보라고 썻는데, 이번에도 동일합니다. 결국 문제 푸는 스킬 외에 인터뷰를 진행하는 스킬도 꼭 필요한거고, 그거에 익숙해지는거는 Mock Interview 만한게 없습니다. 실제로 작년에 봤던 면접중에는 처음 문제를 받았을 때는 어떻게 푸는지 모르겠다… 상태였는데 자연스럽게 제약사항 설정하고, edge case 설정하고 하다가 스스로 풀어낸 경험도 있었습니다. 그리고 어쨋든 한국에서 캐나다로 오는 경우에는 이걸 다 영어로 해야하니까 그 준비도 해야하고요. 리트코드 영상으로 설명해주는 것들에서 뭐라고 표현하는지 유심히 듣고 표현들을 줍줍해서 입에 붙게 하는 것도 중요하다고 생각했습니다. 그리고 한국에서 영어 인터뷰 봐보는 법은 국내 오피스 있는 외국 기업 - 단 국내 오피스가 크진 않아야 함 - 이나 한국에 진출하려고 초기 멤버를 뽑는 기업들의 경우에는 영어 인터뷰를 봐볼 수 있습니다.</p>

<p>그리고 올해 초 본 인터뷰에서도 없었던건데 최근 (지금 입사한 회사 말고도 합격 전에 진행하던 회사가 또 있었습니다.) 인터뷰들에서 느낀 경향은 AI를 어떻게 활용하는지도 면접의 대상에 포함시키고 있다는 점입니다. 예를들면 AI를 사용해도 되는데, 사용할거면 사용하는 화면을 screen share 해달라고 요청받았어요. 저같은 경우는 그걸 보여줄 준비는 안되어있어서 평소에 쓰긴 하는데 한국어로 써서 share 해도 너가 이해를 못할테니까 서로를 위해서 그냥 나는 안쓰겠다. 하고 넘겼는데, 만약 다시 이직을 준비한다면 잘 쓰는 사람으로 showing 하는 방법을 어느정도 연구해야겠다 싶었습니다.</p>

<h2 id="인터뷰-준비---behaviour-interview">인터뷰 준비 - Behaviour Interview</h2>

<p>Behaviour Interview는 저같은 경우에는 백엔드 경력은 3년이고 그 이전에는 다른 분야의 소프트웨어를 개발했기 때문에 이 시기의 에피소드를 이야기하냐 마냐를 좀 고민했습니다. 그런데 결론은 결국 상대방이 흥미롭게 들을 수 있도록 하는게 중요하다는거에요. 배경 설명을 장황하게 할 필요도 없고 내가 한 모든 일들을 A부터 Z까지 이야기 할 필요도 없습니다. 딱 상대방이 이해할 수 있는 단어와 레벨로 전달해서 모든 내용을 상대방이 이해해서 내가 problem solving skill이 충분하구나 하고 인지하게 하면 됩니다.</p>

<p>어떤 에피소드를 선정할지가 고민이 되었었는데요, 일단 소요 시간별 에피소드를 준비해두는게 낫습니다. 1시간동안 한가지 에피소드를 말해야하는 면접에서는 충분히 길고 말할거리가 많은 경험을 이야기 하는게 낫겠지요. 근데 같은 에피소드를 10-15분 안에 말해야 한다면 디테일을 많이 떨궈낸 버젼을 준비하든가 아니면 아얘 다른 이야기를 하는게 나을 것입니다. 짧은 시간에 우겨넣다가 상대방이 제대로 이해를 하지 못한다면 그게 더 참사거든요.</p>

<p>하나의 에피소드를 설명할 때 그 에피소드에 대해 A부터 Z까지 다 설명할 필요는 없고요, 그 과제를 하면서 내가 가장 적극적으로 임했던 부분, 가장 어려웠는데 해결해낸 부분 위주로 당시의 상황과 어떻게 해결했는지를 서술하고 Result는 과제 전체에 대해서 이야기해도 됩니다.</p>

<p>여기서 특히 여성들의 경우 자기검열을 할 수가 있는데 괜히 ‘별로 임팩트가 없었던 것 같아’ 하면서 자기검열을 할 필요는 없을 것 같고요. 비지니스 임팩트가 있었던 거라면 어떤 과제든 가져와서 정리할 수 있다고 생각합니다.</p>

<p>마지막으로 개인적인 감상은 technical interview도 포함해서, 질문하라고 했을 때나 대답할 때 좀 인간적인 모습을 보이거나 엔지니어라면 다 공감할만한 이야기를 섞어서 하는게 진짜같고 공감을 이끌어내는 것 같음. 예를들면 질문을 하다가 자연스럽게 ‘근데 backlog는 쌓여가는데 비지니스 요구사항들 쳐내다보면 테크 백로그를 해결할 리소스를 받기가 어려운데 이런 경우 어떻게 딜하셨어요?’ 하고 질문했었는데 반응이 좋았어요. 아니면 A 프로젝트에서 그 기능을 꼭 넣으려고 열심히 했던 이유는, 내가 담당자기 때문에 그 기능을 뺄겨면 빼야만 하는 이유도 내가 만들어야 했다. 넣든 빼든 일이 생기는건 똑같았기 때문에 내가 생각했을 때 더 옳은 방향으로 만들어나가고자 했다. 라거나.</p>

<h2 id="meet-up-참가하기">Meet up 참가하기</h2>

<p>캐나다 와서 가장 좋은건 밋업을 실제로 참가할 수 있고, 거기서 만난 사람들과는 링크드인 친구를 할 수 있다는 점입니다. 저는 실제로 캐나다 온지 3일 후에 나간걸 시작으로 시차적응도 하기 전에 2주 연속으로 밋업에 참가했어요. (근데 시차적응은 하고 나가는게 좋을 것 같습니다…) 어쨋든 현지 네트워크가 생기니까 링크드인에서 리크루터가 날 조회 한 횟수가 늘어나는게 느껴졌습니다. 동네마다 하다못해 리트코드 푸는 밋업이라도 있으니 틈틈히 나가면 좋을 것 같아요. 어차피 밋업 나가도 거의 구직자라서 거기서 만나서 레퍼럴을 받았어요 이럴 확률은 희박하긴 한데, 어쨋든 링크드인의 내 계정을 캐나다 알고리즘 안에 넣는다는 측면에서라도 중요하다 생각합니다.</p>

<h2 id="나에게-맞는-agency-도움을-받는-것도-좋음">나에게 맞는 Agency 도움을 받는 것도 좋음</h2>

<p>저같은 경우 링크드인에서 <a href="https://leopard.fyi">leopard.fyi</a>라는 단체에 등록해뒀었습니다. 여성, 논바이너리, 앨라이들을 대상으로 기업과 연결시켜주는 단체에요. 미국 위주인데 캐나다 remote도 가끔 들어와서 마지막에 인터뷰 보고 있었던 회사는 여기서 연결 된 회사였습니다. 커피챗도 잘 해서 합격 여부와 상관 없이 그 회사도 팔로우하고 인터뷰어분도 connect 했어요. 이런 곳을 통해서 Apply 하는 것도 좋다고 생각합니다. 가입은 하지 않았는데 알아봤던 단체로는 <a href="https://www.trytoast.ca/">TOAST</a>도 있는데, 여기는 JD에도 회사에 대한 자세한 정보는 가리고, 반대로 지원자에 대해서도 바이어스가 생길만한 정보는 가려서 약간 소개팅 같은 job board를 운영하더라고요. 오프라인 밋업 가면 유료 멤버쉽 할인 코드도 알려주니 시도해보시면 좋을 것 같습니다.</p>

<h1 id="기타-준비-과정의-tip">기타 준비 과정의 tip</h1>

<h2 id="나한테-맞는-멘토-제대로-찾기">나한테 맞는 멘토 제대로 찾기</h2>

<p>스터디클럽++에서 하는 이력서 워크샵은 누가 뭐래도 북미 대상, 영어 이력서 대상인데요, 1기 때 후기가 좋았더니 2기 때 한국에서 취준하시는 한국어 이력서를 들고 오시는 분들이 많아서 진행이 한번 꼬였습니다. 그런데 이거는 한번 더 생각해봤다면 신청을 하지 않으셨을거라고 생각해요. 한국과 북미에서 둘 다 잘 통하는 이력서같은건 없습니다. (굳이 따지자면 저는 한국에서도 영어이력서를 쓰고 다니긴 했는데, 귀찮으니 그냥 영어 이력서 받아주는 곳 아니면 안가겠다는 마인드였습니다.) AI도 똑같잖아요. 그냥 물어보는게 아니라 내 목적에 맞게 어떤 모델을 선택할지, 어떻게 context를 넣어줄지, 프롬프트는 어떻게 넣을지가 중요합니다. 근데 커피챗 / 멘토링은 아무한테나 받으면 당연히 유용한 정보를 얻을 수 없겠죠.</p>

<p>만약 이 글을 보시는 분이 캐나다에 올 준비를 하고 있다면, 여러분이 찾아야 할 사람은 여러분과 비슷한 경력에, 최근에 캐나다로 갔거나 캐나다 내에서 이직을 한 한국인이 베스트입니다. 이제 거기서 조건 몇개 뺄 수도 있겠죠. 경력이 조금 다른 사람이나, 한국인이 아니거나, 최근에 캐나다에 넘어간게 아니거나. 그래도 일단은 최대한 조건을 비슷하게 맞추세요. 예를들어 워홀비자로 취직한 사람이 4년제 대학을 캐나다에서 나가서 co-op을 구하고 있는 사람에게 주는 조언은 적당히 걸러들어야 한다고 생각합니다. 반대도 마찬가지구요. 같은 비자여도 2022년에 취직한 사람과 2025년에 취직한 사람을 말하는게 전혀 다를거에요.</p>

<h2 id="never-search-alone">Never Search Alone</h2>

<p>저는 결국 이 책에 나온걸 하기 전에 최종 인터뷰를 보긴 했는데, 이 책의 방법론에 크게 공감합니다. 원서 아무렇게나 넣고 하나만 걸려라 하고 기도하지 말고, 내가 잘 할 수 있는 포지션을 찾고, 그 포지션을 찾기 위해 네트워킹을 하라는거에요. 그 디테일한 액션 아이템은 책에 있구요.</p>

<p>어쨋든 캐나다 현재 상황은 한국보다도 힘든 것 같고, 이런 상황에서 캐나다 경력도 없는데 캐나다에서 취직을 하려면 남들보다 더 뾰족한 전략으로 임하는게 맞다고 생각합니다.</p>

<p>혹시 이 글을 보고 링크드인 친추하실 분은 최소한의 note는 남겨주세요 ^^;</p>]]></content><author><name>gayuna</name></author><category term="etc" /><category term="이직" /><category term="코딩테스트" /><summary type="html"><![CDATA[아무도 의견을 안주셔서 그냥 제맘대로 씁니다 ^_^;]]></summary></entry><entry><title type="html">2025 이직 후기 feat.캐나다 워킹홀리데이로 개발자 취업한 썰 푼다 (1/2)</title><link href="https://gayuna.github.io/etc/self-reflect-trans-canada-1/" rel="alternate" type="text/html" title="2025 이직 후기 feat.캐나다 워킹홀리데이로 개발자 취업한 썰 푼다 (1/2)" /><published>2025-09-05T00:00:00+09:00</published><updated>2025-09-05T00:00:00+09:00</updated><id>https://gayuna.github.io/etc/self-reflect-trans-canada-1</id><content type="html" xml:base="https://gayuna.github.io/etc/self-reflect-trans-canada-1/"><![CDATA[<h4 id="타임라인">타임라인</h4>

<p>그냥 시간순으로 쭉 써볼까요.</p>

<p>언제부터 외국에서 살고싶었냐고 한다면 중고등학교때부터라고 하겠습니다. 중학생즈음에 이미 나는 결혼하고 애기낳고 이런데 관심이 없구나도 깨달았고, 뭘할지도 모르겠지만 언젠가 외국에서 일해보고싶다고 생각했어요. 고2 중반부터 재수 끝날때까지 최소한 영어 모의고사는 틀린 적이 없는데, 그래도 난 외국에 언젠가 살꺼니까 하면서 계속 영어공부를 하긴 했습니다. 학부 졸업하고 취직하고 나서도 언젠가는 갈 수 있으니까… 하면서 영어는 계속 공부했어요. 물론 학교다닐 때처럼 열심히 한건 아니고, 회사에서 꽁짜로 수업 해준다고 하면 매번 찾아서 듣고, 코로나때는 전화영어 하고. 오픽 도전해서 AL 따보고 그랬습니다.</p>

<p><a href="https://gayuna.github.io/etc/accidental-trans-1/">지난번 이직 회고글</a>을 보면 알겠지만, 그 때도 미국으로 대학원을 가볼까 하는 생각이 있었습니다. 근데 결국 국내에서 이직했던거구요. 이제 대학원 가기도 좀 늦었나 싶기도 하고 (보니까 딱히 그렇지도 않음) 딱히 회사에 불만이 있었던 것은 아니기 때문에 그대로 회사 쭉 다니게 되겠거니…하고 생각했습니다. 기본적으로 전 이직을 그렇게 항시 준비하고 자주 생각하는 타입은 아니에요. 적응하는건 힘들고, 인터뷰 보는건 귀찮아요.</p>

<p>2023년 가을, 조직이동이 되고 매니저가 바뀝니다. 그 매니저가 뭐 썰풀려면 매우 긴데… 여튼 매니저랑 이슈가 있었어요. 그래서 사실 워홀 비자 신청은 어느날 새벽에 되게 충동적으로 했습니다. 나중에 찾아보니 네이버 카페같은데 가이드도 step by step으로 있던데, 그런거도 없고 그냥 직접 사이트 들어가서 하나하나 읽으면서 신청했어요. 왜 캐나다냐고 많이 질문 받았는데, ‘북미’라는 점이 없지는 않았지만, 솔직히 말하자면 가서도 개발자를 하고싶고. 그러면 너무 기간이 짧으면 안될 것 같고. 영어권이긴 해야겠고. 그러면 후보가 영국 캐나다 정도 남는데 영국은 사이트 들어가보니 영국에 오기 6개월 전에 신청하라고 쓰여있고, 캐나다는 추첨이라고 하더라고요? 그럼 캐나다 먼저 넣어보고 여름까지 당첨이 안되면 (당시는 전세계약이 3월에 끝나니 3월쯤에 넘어가면 되지 않을까 막연하게 생각함) 영국 넣어야지. 정도의 생각이었어요. 근데 인원이 늘어서 그런지 한번에 당첨됩니다. 근데 일본에서 교환학생 했던 이력때문에 이슈가 있어서 최종적으로 입국 허가 받은건 6월 중순이었습니다.</p>

<p>근데 사실 받긴 했어도 100퍼 넘어갈 생각은 아니었구요, 사실 사내이동을 먼저 알아봤어요. 근데 2024년 되면서 채용이 줄었다는건 내부 Head Count도 줄었다는거라 열린 포지션이 없더라고요. 고충 상담 같은 것도 해봤는데… 솔직한 후기는 차라리 외부 직장내 괴롭힘 상담을 받는게 나았을 것 같습니다 ^^… 삼X이었으면 팀이라도 바꿔줬을텐데 니가 알아서 팀 바꾸라는 식이고. (이 글 보는 사람이 있을 것 같지도 않지만) 쿠X 인사는 믿지 마세요.</p>

<p>여름쯤에는 한국 내 기업들 이직 면접을 봤습니다. 최종까지 간 곳도 몇군데 있었고요. 개인적으로 이 때 면접 경험이 가장 좋았던건 팔란티어. 그 때 사업모델 보고 좋아보였을 때 주식이라도 사놓을걸. 여튼 여기 면접은 제안 오시면 한번쯤 보시길 추천드립니다. 뻔하지 않고 재밌었어요. 리쿠르터가 좋았던 회사도 따로 있는데 리쿠르터분이 이직을 하셔가지고… 뭐 그렇습니다.</p>

<p>8월에 건강검진을 했는데 결과가 말도 안되는거에요. 스트레스로 인해 사람이 거의 걸어다니는 종합병원 수준? 정신과쪽은 물론이고, 말도 안되게 평생 한번도 문제 없었던 갑상선저하증 이런거까지 뜨는 것 보면서 아 그래 사람이 먼저지 하면서 퇴사 노티스를 내게 됩니다. 이때 뭐할거냐고 해서 워홀갈거라고 많이 하고 다녔는데 (그 전까지는 조금씩은 말은 했는데 이렇게 많이 말하고 다니진 않음) 말하고 다니면서 약간 자기예언적 상황이 되는건가 싶기도 했네요. 퇴사하고 한동안은 병원 열심히 다녔고요. 근데 갑상선쪽은 일단 약 최저치만 쓰고 부작용 없으면 늘리자고 했는데 퇴사하고 한달동안 자연치유…되어서 결국 최저치만 쓰고 끝났습니다. 역시 스트레스는 만악의 근원.</p>

<p>그리고 연말까지는 그냥 쉬었습니다. 사실 처음엔 쉰다고 하면서 자꾸 책읽고 공부하고 그랬는데… 여러분 노는 것도 버릇이고 경험이라서 할 수록 늡니다. 이제 이틀 후에 출근해야하는 지금은 더 놀 수 있을 것 같음.. 이때 한거는 동생 사업 관련해서 개발해주고 - 제 이력에 있는 ‘장문’이 사실 동생 회사입니다. 근데 학생들 대상으로 서비스 만드니까 바로바로 피드백 와서 재밌었습니다. 회사 일만 했으면 언제 OpenAI API, Gemini API 써보겠어 싶은데 바로 써봐서 좋았구요. 결과도 좋아서 결국 이력서에도 넣었네요 - 아마존 면접 제의받아서 그거 하다가 또 최종에서 떨어지고… 뭐 그정도였네요.</p>

<p>1-2월은 처음으로 링크드인 위치 바꾸고 열심히 이력서를 넣기 시작합니다. 이 때부터는 3월에 넘어갈 생각을 하고 있었어요. 뭐 100개 써야한다 500개 써야한다 말 나오는거 보고 일단 열심히 다 써봤습니다. 커버레터도 붙여보기도 해보고 빼보기도 해보고. 이래저래 하는데 당연히 인터뷰 잡히는 비율이 좋지는 않았습니다. 인터뷰가 좀 많이 잡혔으면 3월에 넘어갔을텐데, 딱히 그런건 아니어서 당장 넘어가진 않아도 되겠다는 생각이 들었습니다. 밀려서 6월에 최종적으로 허가 나왔던게 오히려 전화위복이 되었죠.</p>

<p>솔직히 이력서 많이 넣는거 질리기도 하고, 이게 맞나 하는 생각을 하고 있었는데 기용님 멘토링 통해서 캐나다에서 막 취뽀한 <a href="https://www.linkedin.com/in/becoming-cora/">Cora</a>님이랑 커피챗 해보고 아 역시 많이 넣는게 답이 아니구나 싶어서 이력서 넣는걸 중단했습니다. 그리고 이미 어느정도 꾸며놓은 상태였지만, 링크드인을 더 채우기 시작했어요. 그리고 어느날 디자이너분들은 포트폴리오를 만드는데, 개발자도 비슷한거 만들 수 있지 않을까? 싶어서 깃허브 페이지를 만들고 거기에 포트폴리오로서 System redesign 시리즈를 쓰기 시작합니다. 지금 블로그에 올라온 세개는 다분히 의도적으로 고른 토픽이었어요. write heavy한거 하게 하나는 runday, 고용량 파일 다루는거 하게 mybox 이런식이었어요. 배민이랑 멜론도 하겠다고 캡쳐 다 해놨는데 결국 아직 쓰지는 못했네요.</p>

<p>3월에는 <a href="https://discord.gg/wKdMvFpSDp">스터디클럽++</a>에서 했던 이력서 워크샵에도 참여했습니다. 여기서 만난 분들이랑은 이 이후에도 서로 첨삭해주고, 얼마 전에는 워크샵 2기가 열려서 저는 도우미로도 참가했습니다. 여기서는 이력서 빽빽하게 쓰는게 아니라 읽기 쉽게 덜어내는 작업을 많이 했구요. 원서 많이 넣는 것도 이력서 빽빽하게 쓰는것도 약간 일반론인데 저는 그렇게 해서 잘 풀리지 않았습니다. 생각해보면 제 인생에서 항상 남들 하는대로 안했을 때 더 잘 풀렸던 것 같아요 ^^;</p>

<p>지금 합격한 회사에서는 4월 초에 처음 인메일을 받았구요, 그리고 최종적으로 캐나다 가는걸 6월 중순으로 정합니다. 데드라인을 이틀 남기고 아슬아슬하게 가게 됐어요. 사유는 뭐 일찍 갈 이유가 없었음 (물가 비싼건 너무 잘 아니까 취업이 된게 아니라면 급하게 갈 것은 없다고 생각) + 좋아하는 가수 소극장 콘서트가 6월 15일까지로 잡혀서 그거는 다 보고 가자였습니다. 조금 주책일 수도 있는데 다 좋자고 하는 사회생활이지 않겠어요? 이거는 잘 한 결정이었다고 생각합니다. 나름 마지막으로 불태우고 오니까 아쉬움이 덜하더라고요.</p>

<p>4월에 리쿠르터콜이랑 코딩테스트, 5월에 1차 면접을 봤습니다. 1차 면접은 기술인터뷰는 없었고 여태 했던 프로젝트 중에 하나를 꼽아서 한시간동안 그거에 관련된 이야기만 하는 behaviour interview였습니다. 한동안 응답이 없다가 연락이 와서 최종 인터뷰는 여기 넘어와서 7월 중순에 봤어요. 코딩테스트가 2번, system design이 1번, behaviour이 1번 해서 45분씩 4시간 한번에 봤습니다. 다 보고 마지막에 리쿠르터가 2주 안에 연락준댓는데 연락 안와서 떨어진 줄 알다가 매니저랑 리크루터가 돌아가며 휴가가느라 좀 늦었다고 다음주까지 연락 줄게 해서 진짜 다음주에 다시 연락 받았네요. 이게 8월 중순 이야기였습니다. 되게 많이 지난 것 같았는데 아직 캐나다 온지 100일도 안됐더라고요.</p>

<p>사실 중간에 나 개발자 다시 할 수 있는거 맞나… 싶었던 시간도 있었고요. 근데 ‘사람이 먼저다’하고 나왔더니 그렇게 퇴사하는건 아니었나 하는 후회는 크게 들지는 않더라구요. 물론 뭐든 오퍼를 받고 이동하는게 안정적이진 하죠. 근데 안정 찾다가 평생 못움직이는 케이스도 있을 것이라고 생각해요. 근데 막 퇴사하라는건 아니고 움직일 준비는 다 해놔야 한다고 생각하는데, 그건 이제 다음 글에서 다뤄보도록 하겠습니다.</p>

<p>다음 글 내용 대충 아래정도로 써보려고 하는데 혹시 듣고싶은 이야기가 있으면 따로 디엠이나 스핀 주세요:</p>

<ul>
  <li>비자 설명 하기: 워킹 홀리데이 비자는 공식적으로 Open Work Permit으로 나옵니다.</li>
  <li>Hard Skill: 스터디. 나한테 맞는 놈을 패는게 낫다. 나는 과제보다 인터뷰였음.</li>
  <li>Don’t Spray and Pray: 리쿠르터가 먼저 접근하게 만드는게 젤 낫다</li>
  <li>멘토 제대로 찾기: 상담 대상을 정확하게 찾는게 중요하다. 비슷한 경력, 최근에 이직을 한 사람 등. 나랑 최대한 비슷하게.</li>
  <li>이력서: <a href="https://blog.naver.com/ssooyyaa55/223738592848">캐나다 전화번호 미리 만들 수 있으니</a> 만들어서 넣어두기</li>
  <li>Behaviour Interview: 결국 상대방이 흥미롭게 받아들이도록 하는게 중요. (배경설명 장황할 필요 없음.) / 소요 시간별 에피소드 준비 /  Do not 자기검열 / 임팩트가 있었던 것 위주로</li>
  <li>Interview with AI: 막판에는 앞으로 인터뷰가 변화할 것 같다는 인상을 좀 받음.</li>
  <li>Never Search Alone: 저는 결국 이거 제대로 하기 전에 최종 인터뷰를 봤지만 이 방법론에 크게 공감합니다.</li>
  <li>2025년 기준: 경력 없으면 캐나다 오지 마세요…. 차라리 2nd degree 같은건 ㅇㅋ</li>
</ul>]]></content><author><name>gayuna</name></author><category term="etc" /><category term="이직" /><category term="코딩테스트" /><summary type="html"><![CDATA[타임라인]]></summary></entry><entry><title type="html">Elasticsearch 엘라스틱서치 deep dive</title><link href="https://gayuna.github.io/study/hello-interview-elastic-search/" rel="alternate" type="text/html" title="Elasticsearch 엘라스틱서치 deep dive" /><published>2025-08-03T00:00:00+09:00</published><updated>2025-08-03T00:00:00+09:00</updated><id>https://gayuna.github.io/study/hello-interview-elastic-search</id><content type="html" xml:base="https://gayuna.github.io/study/hello-interview-elastic-search/"><![CDATA[<h2 id="elasticsearch-deep-dive">Elasticsearch Deep Dive</h2>

<h3 id="introduction">Introduction</h3>

<p>Elasticsearch란?</p>
<ul>
  <li>가장 인기 있고 강력한 오픈 소스 분산 검색 엔진</li>
  <li>수십 년간 구축된 방대한 프로젝트: 시스템 디자인 문제에서 검색 및 검색과 관련된 많은 문제를 해결하는 데 사용</li>
  <li>대부분의 데이터베이스 시스템(예: Postgres의 full text search)도 검색에 능숙하지만, 특정 규모나 정교함 수준에서는 목적에 맞게 구축된 시스템이 필요함.</li>
  <li>distributed, asynchronous, and concurrent</li>
  <li>정렬(sorting), 필터링(filtering), 랭킹(ranking), 패싯(faceting) 등 다양한 요구사항을 처리함.</li>
</ul>

<h3 id="검색의-기본-개념">검색의 기본 개념</h3>
<ul>
  <li>Criteria: 검색을 위해 지정하는 조건이나 규칙
    <ul>
      <li>제목에 특정 단어 포함</li>
      <li>가격 범위</li>
    </ul>
  </li>
  <li>Facet: 검색 결과를 더 세분화하고 정제할 수 있게 도와주는 옵션 - 처음 입력한 검색 기준을 바탕으로 나온 결과들을 특정 기준에 따라 분류하여 보여줌으로써, 사용자가 원하는 정보를 더 쉽게 찾을 수 있도록 돕는 기능</li>
  <li>Results: 검색의 결과 - Set of Documents</li>
</ul>

<h3 id="elasticsearch-기본-개념-및-사용법">Elasticsearch 기본 개념 및 사용법</h3>

<p>RESTful API를 제공하여 쉽게 작업을 수행할 수 있음.</p>

<h4 id="basic-concepts">Basic Concepts</h4>
<p><img src="https://github.com/user-attachments/assets/f09d352a-8753-4bf9-931f-887277687bb6" alt="image" /></p>
<ul>
  <li>문서 (Documents): 검색 대상이 되는 개별 데이터 단위. JSON 객체 같은 것.
    <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> 
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XYZ123"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The Great Gatsby"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"author"</span><span class="p">:</span><span class="w"> </span><span class="s2">"F. Scott Fitzgerald"</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"price"</span><span class="p">:</span><span class="w"> </span><span class="mf">10.99</span><span class="p">,</span><span class="w"> 
  </span><span class="nl">"createdAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-01-01T00:00:00.000Z"</span><span class="w"> 
</span><span class="p">}</span><span class="w">
</span></code></pre></div>    </div>
  </li>
  <li>인덱스: 문서의 컬렉션. 관계형 데이터베이스의 ‘데이터베이스 테이블’과 유사
    <ul>
      <li>각 문서는 고유한 ID와 검색할 데이터를 포함하는 키-값 쌍인 필드(fields)와 연결됨.</li>
      <li>검색은 인덱스를 대상으로 수행되며, 검색 기준과 일치하는 문서 결과를 반환합니다.</li>
    </ul>
  </li>
  <li>매핑 및 필드 (Mappings and Fields):
    <ul>
      <li>매핑(Mapping): 인덱스의 스키마. 인덱스가 가지는 필드와 각 필드의 데이터 유형, 그리고 필드가 처리되고 인덱싱되는 방식과 같은 다른 속성을 정의.</li>
      <li>매핑은 어떤 필드가 검색 가능하고 어떤 타입의 데이터를 포함하는지 Elasticsearch에 알려주므로 매우 중요. - 사진에서 <code class="language-plaintext highlighter-rouge">price: Float</code></li>
      <li>타입은 복잡할 수 있으며, 중첩 객체, 배열, 지리 공간 유형, 사용자 정의 분석기 또는 시맨틱 검색을 위한 임베딩을 사용할 수 있습니다.</li>
      <li><code class="language-plaintext highlighter-rouge">keyword</code> 타입은 전체 값으로 처리되어 효율적인 검색 및 정렬에 유용(hash table)하며, <code class="language-plaintext highlighter-rouge">text</code> 타입은 토큰화되어 텍스트 검색에 사용됨 (reverse index).</li>
      <li>매핑에 사용하지 않는 많은 필드를 포함하면 인덱스의 메모리 오버헤드가 증가하여 성능 문제 및 비용 증가.</li>
    </ul>
  </li>
</ul>

<h4 id="basic-use">Basic Use</h4>
<ul>
  <li>인덱스 생성: 간단한 <code class="language-plaintext highlighter-rouge">PUT</code> 요청으로 동적 매핑, 1개의 샤드, 1개의 복제본을 가진 인덱스를 생성.
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUT /books 
{
  "settings": { 
  "number_of_shards": 1, 
  "number_of_replicas": 1 
  } 
}
</code></pre></div>    </div>
  </li>
  <li>매핑 설정: 데이터의 대부분 필드가 검색 가능하지 않거나, Elasticsearch가 필드 유형을 정확히 추론하기 어렵다면 미리 매핑을 설정할 수 있음. (This lets Elasticsearch know that certain fields should be treated as searchable and what types to expect in those fields.) 이제 book을 추가하면 Elasticsearch가 정확한 값을 추출해서 index를 만들어 검색될 수 있게 할 것.
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PUT /books/_mapping 
{
  "properties": {
 "title": { "type": "text" },
 "author": { "type": "keyword" },
 "description": { "type": "text" },
 "price": { "type": "float" },
 "publish_date": { "type": "date" },
 "categories": { "type": "keyword" },
 "reviews": {
   "type": "nested",
   "properties": {
     "user": { "type": "keyword" },
     "rating": { "type": "integer" },
     "comment": { "type": "text" }
   }
 }
  }
}
</code></pre></div>    </div>
  </li>
  <li>중첩 필드(Nested Fields): <code class="language-plaintext highlighter-rouge">reviews</code>와 같이 중첩된 문서를 정의할 수 있음. 이는 데이터 업데이트 및 쿼리 패턴에 따라 결정다. (예: 리뷰가 자주 업데이트되지 않고 자주 쿼리되는 경우 중첩이 효율적).</li>
  <li>문서 추가 (Add Documents):
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// POST /books/_doc
{
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
"description": "A novel about the American Dream in the Jazz Age",
"price": 9.99,
"publish_date": "1925-04-10",
"categories": ["Classic", "Fiction"],
"reviews": [
  {
    "user": "reader1",
    "rating": 5,
    "comment": "A masterpiece!"
  },
  {
    "user": "reader2",
    "rating": 4,
    "comment": "Beautifully written, but a bit sad."
  }
]
}
</code></pre></div>    </div>
    <ul>
      <li>각 요청은 문서 ID와 클러스터에 문서가 유지된 방법에 대한 데이터를 반환
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"_index": "books",
"_id": "kLEHMYkBq7V9x4qGJOnh",
"_version": 1, // NOTE!
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
</code></pre></div>        </div>
      </li>
      <li><code class="language-plaintext highlighter-rouge">_version</code> 필드는 문서가 원자적으로 업데이트되도록 보장하는 데 사용됩니다.</li>
    </ul>
  </li>
  <li>문서 업데이트 (Updating Documents)
    <ul>
      <li>문서 ID를 URL에 지정하여 전체 문서를 <code class="language-plaintext highlighter-rouge">PUT</code>할 수 있지만, 동시성 문제가 발생할 수 있음: A가 읽음 -&gt; B가 읽음 -&gt; A가 씀 -&gt; B가 씀</li>
      <li><code class="language-plaintext highlighter-rouge">_version</code> 필드를 사용하여 버전이 일치하는 경우에만 업데이트하도록 지정하여 낙관적 동시성 제어(optimistic concurrency control)를 구현 가능. <code class="language-plaintext highlighter-rouge">PUT /books/_doc/kLEHMYkBq7V9x4qGJOnh?version=1</code></li>
      <li><code class="language-plaintext highlighter-rouge">_update</code> 엔드포인트(<code class="language-plaintext highlighter-rouge">POST</code>)를 사용하여 문서의 특정 필드만 업데이트
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// POST /books/_update/kLEHMYkBq7V9x4qGJOnh
{
"doc": {
"price": 14.99
}
}
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>검색 (Search)
    <ul>
      <li>Elasticsearch의 쿼리 문법은 SQL과 유사하며 JSON 기반</li>
      <li><code class="language-plaintext highlighter-rouge">GET /books/_search</code> 엔드포인트를 사용하며, <code class="language-plaintext highlighter-rouge">query</code> 객체 안에 다양한 쿼리 타입을 사용하여 필터링
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /books/_search
{
"query": {
"match": {
  "title": "Great"
}
}
}
</code></pre></div>        </div>
        <ul>
          <li>match: 특정 필드에 특정 단어가 포함된 문서.</li>
          <li>bool (must, should, filter, must_not 등): 여러 조건을 조합하여 검색.</li>
          <li>range: 숫자 또는 날짜 범위로 검색.</li>
          <li>nested: 중첩된 필드 내에서 검색.
            <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /books/_search
{
"query": {
"bool": {
"must": [
  { "match": { "title": "Great" } },
  { "range": { "price": { "lte": 15 } } }
]
}
}
}
</code></pre></div>            </div>
            <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /books/_search
{
"query": {
"nested": {
"path": "reviews",
"query": {
  "bool": {
    "must": [
      { "match": { "reviews.comment": "excellent" } },
      { "range": { "reviews.rating": { "gte": 4 } } }
    ]
  }
}
}
}
}
</code></pre></div>            </div>
          </li>
        </ul>
      </li>
      <li>검색 결과는 <code class="language-plaintext highlighter-rouge">took</code>, <code class="language-plaintext highlighter-rouge">timed_out</code>, <code class="language-plaintext highlighter-rouge">_shards</code>, <code class="language-plaintext highlighter-rouge">hits</code> 등의 정보를 포함하며, <code class="language-plaintext highlighter-rouge">hits</code> 배열에는 일치하는 문서와 <code class="language-plaintext highlighter-rouge">_score</code> (관련성 점수)가 포함.
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
"took": 7,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
  "value": 2,
  "relation": "eq"
},
"max_score": 2.1806526,
"hits": [
  {
    "_index": "books",
    "_type": "_doc",
    "_id": "1",
    "_score": 2.1806526,
    "_source": {
      "title": "The Great Gatsby",
      "author": "F. Scott Fitzgerald",
      "price": 12.99
    }
  },
  {
    "_index": "books",
    "_type": "_doc",
    "_id": "2",
    "_score": 1.9876543,
    "_source": {
      "title": "Great Expectations",
      "author": "Charles Dickens",
      "price": 10.50
    }
  }
]
}
}
</code></pre></div>        </div>
      </li>
    </ul>
  </li>
  <li>정렬 (Sort)
    <ul>
      <li>기본 정렬: <code class="language-plaintext highlighter-rouge">sort</code> 파라미터를 사용하여 검색 결과를 특정 필드를 기준으로 정렬. (다중 정렬도 가능)
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /books/_search
{
"sort": [
{ "price": "asc" },
{ "publish_date": "desc" }
],
"query": {
"match_all": {}
}
}
</code></pre></div>        </div>
      </li>
      <li>스크립트 기반 정렬: <a href="https://www.elastic.co/docs/explore-analyze/scripting/modules-scripting-painless">Painless Script Language</a>를 사용하여 계산된 값으로 정렬
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /books/_search
{
"sort": [
{
  "_script": {
    "type": "number",
    "script": {
      "source": "doc['price'].value * 0.9"
    },
    "order": "asc"
  }
}
],
"query": {
"match_all": {}
}
}
</code></pre></div>        </div>
      </li>
      <li>중첩 필드 (Nested Fields) 정렬: 중첩된 객체 내의 값으로 정렬할 때 <code class="language-plaintext highlighter-rouge">nested</code> 정렬을 사용.
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /books/_search
{
"sort": [
{
  "reviews.rating": {
    "order": "desc",
    "mode": "max",
    "nested": {
      "path": "reviews"
    }
  }
}
],
"query": {
"match_all": {}
}
}
</code></pre></div>        </div>
      </li>
      <li>관련성 기반 정렬: 정렬 순서를 지정하지 않으면 Elasticsearch는 관련성 점수(<code class="language-plaintext highlighter-rouge">_score</code>)에 따라 결과를 정렬. 기본 알고리즘은 <a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf">TF-IDF</a>와 밀접하게 관련되어 있음</li>
    </ul>
  </li>
  <li>페이지네이션 및 커서 (Pagination and Cursors)
    <ul>
      <li>From/Size 페이지네이션: 가장 간단한 형태, <code class="language-plaintext highlighter-rouge">from</code> (시작 인덱스)과 <code class="language-plaintext highlighter-rouge">size</code> (반환할 결과 수)를 지정. - 깊은 페이지네이션(예: 10,000개 이상)에는 비효율적 (due to the overhead of sorting and fetching all preceding documents. The cluster needs to retrieve and sort all these documents on each request, which can be prohibitively expensive.)
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /my_index/_search
{
"from": 0,
"size": 10,
"query": {
"match": {
  "title": "elasticsearch"
}
}
}
</code></pre></div>        </div>
      </li>
      <li>Search After: 깊은 페이지네이션에 더 효율적.</li>
      <li>이전 페이지의 마지막 결과의 정렬 값을 다음 페이지의 시작점으로 사용.
        <ul>
          <li>첫 쿼리는 그냥 보냄.</li>
          <li>쿼리의 결과에서 sort value를 저장.</li>
          <li>다음 쿼리를 보낼 때 search after 필드의 위 value들을 함께 보냄.</li>
          <li>그 사이에 새로운 document가 추가되어도 중복으로 데이터를 보여주지 않음. (처음부터 20개씩 보여주는 방식이면 중간에 데이터가 새로 생겨서 페이지가 넘어가는 document들은 두번 보여주게 됨)</li>
          <li>클라이언트 측에서 상태를 유지해야 하며, 페이지에 대한 무작위 접근은 허용되지 않음.
            <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /my_index/_search
{
"size": 10,
"query": {
"match": {
"title": "elasticsearch"
}
},
"sort": [
{"date": "desc"},
{"_id": "desc"}
],
"search_after": [1463538857, "654323"]  // 직전 페이지 마지막 document의 timestamp와 id
}
</code></pre></div>            </div>
            <blockquote>
              <p>[!NOTE]
데이터 변경으로 인한 누락: 만약 사용자가 한 페이지를 보고 다음 페이지를 요청하는 사이에 인덱스의 기본 데이터(문서)가 삭제되거나 업데이트되어 정렬 순서가 변경된다면, 이전에 반환되었어야 할 문서가 다음 페이지에서는 나타나지 않거나 아예 누락될 수 있습니다.
예를 들어, 책 목록을 정렬하여 첫 번째 페이지(1~10번)를 받았다고 가정해봅시다. 사용자가 두 번째 페이지를 요청하기 직전에, 7번째에 있던 책이 삭제되거나 <em>가격이 바뀌어</em> 정렬 순서가 크게 변경될 수 있습니다. 이 경우, 두 번째 페이지를 요청하면 11번째로 나타났어야 할 책이 갑자기 10번째로 당겨지거나, 심지어 11번째 이후에 있던 책이 10번째 자리를 채울 수 있어, 사용자는 일관된 순서로 모든 결과를 보지 못하고 특정 문서가 누락된 것처럼 느낄 수 있습니다. 이는 검색 결과가 결과적 일관성(eventual consistency) 모델을 따르기 때문에 발생할 수 있는 현상입니다.
       ▪ 첫 페이지 결과: [책 A (5달러), 책 B (7달러), 책 C (10달러)]
       ▪ 다음 페이지를 위한 search_after 값은 <strong>책 C의 정렬 값(10달러)</strong>이 됩니다.
   ◦ 이제 사용자가 다음 페이지를 요청하기 전에 ‘책 D (원래 12달러)’가 ‘8달러’로 가격이 변경되었다고 가정해 봅시다.
       ▪ 원래 정렬 순서: 책 A (5), 책 B (7), 책 C (10), 책 D (12)…
       ▪ 가격 변경 후 정렬 순서: 책 A (5), 책 B (7), 책 D (8), 책 C (10)…
이러한 문제를 해결하기 위해 Elasticsearch는 Point in Time (PIT) API와 search_after를 함께 사용하는 방식을 제공합니다. PIT는 특정 시점의 인덱스 스냅샷을 생성하여, 그 스냅샷을 기준으로 페이지네이션을 진행함으로써 데이터 변경에 관계없이 일관된 결과를 제공할 수 있습니다</p>
            </blockquote>
          </li>
        </ul>
      </li>
      <li>커서 (Cursors): <code class="language-plaintext highlighter-rouge">point in time</code> (PIT) API와 <code class="language-plaintext highlighter-rouge">search_after</code>를 함께 사용하여 페이지네이션 전반에 걸쳐 데이터의 일관된 뷰(consistent view)를 제공.
        <ul>
          <li>PIT ID를 생성(<code class="language-plaintext highlighter-rouge">POST /my_index/_pit?keep_alive=1m</code>)하고, 검색 쿼리에 PIT ID를 포함하며, <code class="language-plaintext highlighter-rouge">search_after</code>와 함께 사용.
            <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// GET /_search
{
"size": 10,
"query": {
"match": {
"title": "elasticsearch"
}
},
"pit": {
"id": "46To...",
"keep_alive": "1m"
},
"sort": [
{"_score": "desc"},
{"_id": "asc"}
]
}
</code></pre></div>            </div>
          </li>
          <li>작업 완료 시 PIT를 닫아야 함. 서버 자원 소모가 크기 때문에 <code class="language-plaintext highlighter-rouge">keep_alive</code>를 지정할 것.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// DELETE /_pit
{
  "id" : "46To..."
}
</code></pre></div></div>

<h3 id="elasticsearch-작동-원리">Elasticsearch 작동 원리</h3>

<p>Elasticsearch는 Apache Lucene이라는 고도로 최적화된 검색 라이브러리의 오케스트레이션 프레임워크. <a href="https://gayuna.github.io/system%20design/twitter-design-in-real/">트위터 같은 곳에서는 Lucene를 직접 사용합니다.</a></p>

<ul>
  <li>Elasticsearch의 역할: 클러스터 조정, API, 집계, 실시간 기능 등 분산 시스템 측면을 처리.</li>
  <li>Lucene의 역할: 검색 기능 자체를 담당. Lucene은 단일 노드에서 작동하며, Elasticsearch는 클러스터 수준에서 작동합니다.</li>
</ul>

<h4 id="클러스터-아키텍처">클러스터 아키텍처</h4>

<p>Elasticsearch는 분산 검색 엔진이며, 여러 노드를 실행. 하나의 물리적 머신이 여러 노드 유형의 책임을 맡을 수도 있음.</p>

<h5 id="노드-유형">노드 유형</h5>
<ul>
  <li>마스터 노드: 클러스터를 조정하는 역할. 노드 추가/제거, 인덱스 생성/삭제와 같은 클러스터 수준 작업을 수행할 수 있는 유일한 노드. 클러스터당 하나의 활성 마스터 노드만 존재.</li>
  <li>데이터 노드: 데이터를 저장하는 역할. 실제 데이터가 저장되는 곳. 큰 클러스터에서는 많은 데이터 노드를 가짐. 데이터 노드는 <code class="language-plaintext highlighter-rouge">hot</code>, <code class="language-plaintext highlighter-rouge">warm</code>, <code class="language-plaintext highlighter-rouge">cold</code>, <code class="language-plaintext highlighter-rouge">frozen</code>과 같이 데이터 접근 빈도에 따라 전문화될 수 있음.</li>
  <li>코디네이팅 노드: 클러스터 전반의 검색 요청을 조정하는 역할. 클라이언트로부터 검색 요청을 수신, 적절한 노드로 전송.</li>
  <li>인제스트 노드: 데이터 수집을 담당. 데이터가 색인 전에 변환되고 준비되는 곳.</li>
  <li>머신러닝 노드: 머신러닝 작업을 담당합니다.</li>
</ul>

<h5 id="노드-간-상호작용">노드 간 상호작용</h5>
<p><img src="https://github.com/user-attachments/assets/e2aef54e-0a85-42bd-9e7e-f222d66e62b1" alt="Image" />
인제스트 노드가 데이터 노드에 데이터를 로드하고, 코디네이팅 노드를 통해 쿼리하는 방식으로 작동.
각 노드 유형의 책임은 다른 하드웨어 요구 사항을 내포합니다 (예: 데이터 노드는 높은 디스크 I/O 또는 더 많은 메모리가 필요).
When the cluster starts, you’ll specify a list of seed nodes (these are master-eligible) which will perform a leader election algorithm to choose a master for the cluster. Only one node should be the active master at a time, while the other master-eligible nodes are on standby.</p>

<h4 id="문서-수집-및-검색-흐름">문서 수집 및 검색 흐름</h4>

<ol>
  <li>문서 수집:
    <ul>
      <li>클라이언트가 인제스트 노드에 문서를 보냅니다.</li>
      <li>인제스트 노드는 수집 파이프라인을 실행하여 문서를 처리한 후 데이터 노드로 전달합니다.</li>
      <li>데이터 노드가 문서를 수신하고 인덱싱하면 인제스트 노드에 확인 응답을 보내고, 이는 클라이언트에게 다시 확인됩니다.</li>
      <li>데이터 노드에서는 문서가 Lucene 인덱스에 추가되며, 대부분의 경우 새 세그먼트가 생성됩니다.</li>
    </ul>
  </li>
  <li>문서 검색:
    <ul>
      <li>클라이언트가 코디네이팅 노드에 연결합니다.</li>
      <li>코디네이팅 노드는 사용자가 제출한 쿼리를 구문 분석하고, 데이터 접근 계획(어떤 샤드에 상주하는지 등)을 세웁니다.</li>
      <li>계획에 따라 관련 데이터 노드에 쿼리를 제출합니다.</li>
      <li>데이터 노드는 쿼리를 Lucene 인덱스에 전달하며, Lucene 인덱스는 관련 세그먼트에 걸쳐 병렬로 실행됩니다.</li>
      <li>역인덱스와 Doc Values 같은 기능을 활용하여 결과를 필터링하고 정렬합니다.</li>
      <li>데이터 노드는 부분적인 결과를 코디네이팅 노드에 반환하고, 코디네이팅 노드는 이 결과들을 병합하여 사용자에게 반환합니다.</li>
      <li>병렬 처리: 이 과정은 여러 수준에서 병렬 처리가 이루어집니다 (예: 여러 샤드 동시 쿼리, 다른 복제본으로 요청 전송, 인덱스 내에서 여러 CPU에 걸쳐 작업 병렬화).</li>
    </ul>
  </li>
</ol>

<h4 id="데이터-노드">데이터 노드</h4>

<p>데이터 노드의 주요 기능은 문서를 저장하고 빠르게 검색 가능하게 하는 것.
<img src="https://github.com/user-attachments/assets/1b0016a0-c316-47a0-a476-1544d2684455" alt="image" /></p>
<ul>
  <li>_source 데이터 vs. Lucene 인덱스: Elasticsearch는 원본 <code class="language-plaintext highlighter-rouge">_source</code> 데이터와 검색에 사용되는 Lucene 인덱스를 분리.</li>
  <li>요청 처리 단계
    <ol>
      <li>쿼리 단계 (Query Phase): 최적화된 인덱스 데이터 구조를 사용하여 관련 문서를 식별</li>
      <li>가져오기 단계 (Fetch Phase): (선택적으로) 식별된 문서 ID를 노드에서 가져옴
        <ul>
          <li>이상적인 쿼리는 소스 문서를 전혀 건드리지 않고 답변할 수 있는 쿼리입니다.</li>
        </ul>
      </li>
    </ol>
  </li>
  <li>인덱스, 샤드, 복제본 (Indices, Shards, Replicas):
    <ul>
      <li>데이터 노드는 인덱스(문서의 컬렉션)를 포함, 인덱스는 샤드(shards)와 그 복제본(replicas)으로 구성.</li>
      <li>샤드:
        <ul>
          <li>Elasticsearch가 데이터(및 관련 인덱스)를 여러 호스트에 분할할 수 있게 해주는 기능</li>
          <li>문서와 해당 인덱스 구조를 클러스터 내 여러 노드에 분산시켜 성능과 확장성을 크게 향상</li>
          <li>문서는 상호 배타적으로 하나의 샤드에 할당되며, 검색은 모든 관련 샤드에서 병렬로 실행되고, 결과는 조정 노드에 의해 병합 및 정렬</li>
          <li>샤드에는 Lucene 인덱스가 캡슐화되어 있음</li>
        </ul>
      </li>
      <li>복제본 (Replica):
        <ul>
          <li>샤드의 정확한 복사본 (exact copy)</li>
          <li>Elasticsearch는 인덱스의 샤드 사본을 하나 이상 생성할 수 있으며, 이를 replica shards 또는 그냥 replicas이라고 함</li>
          <li>일반적으로 고가용성(high availability)과 처리량 증가(increased throughput)를 위해 도입</li>
          <li>코디네이팅 노드는 모든 사용 가능한 샤드 사본(기본 샤드 및 복제본)에 검색 요청을 분산하여 검색 성능을 향상시키고 검색 워크로드를 클러스터 전체에 효과적으로 분산</li>
        </ul>
      </li>
      <li>Elasticsearch shards are 1:1 with Lucene indexes</li>
    </ul>
  </li>
</ul>

<h4 id="lucene-세그먼트-crud-lucene-segment-crud">Lucene 세그먼트 CRUD (Lucene Segment CRUD)</h4>

<p>Lucene 인덱스는 검색 엔진의 기본 단위인 세그먼트(segments)로 구성.</p>

<ul>
  <li>불변성: 세그먼트는 색인된 데이터의 불변(immutable) 컨테이너.
    <ul>
      <li>쓰기 작업은 배치로 처리되며 세그먼트를 구성. 문서를 삽입할 때 즉시 인덱스에 저장되는 것이 아니라 세그먼트에 추가됨. 문서 배치가 모이면 세그먼트를 구성하고 디스크로 플러시.</li>
      <li>세그먼트 수가 너무 많아지면 병합할 수 있음. 병합하려는 세그먼트로부터 새 세그먼트를 생성하고 이전 세그먼트를 제거.</li>
      <li>삭제: 각 세그먼트에는 삭제된 식별자 집합이 있고, 삭제된 문서에 대한 데이터를 쿼리할 때 해당 데이터는 존재하지 않는 것처럼 처리되지만, 데이터는 여전히 세그먼트에 남아 있음. 병합 작업 중에 삭제된 문서가 정리됨.</li>
      <li>업데이트: 실제 세그먼트 자체를 업데이트하지 않음. 대신, 이전 문서를 soft delete하고 업데이트된 데이터로 새 문서를 삽입. 이전 문서는 나중에 세그먼트 병합 이벤트에서 정리.
        <ul>
          <li>이로 인해 삭제는 매우 빠르지만, 병합 및 정리 전까지는 일부 성능 저하가 발생할 수 있음.</li>
          <li>업데이트는 삽입보다 성능이 낮음. 이는 Elasticsearch가 빠르게 업데이트되는 데이터에 적합하지 않은 이유 중 하나.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>불변 아키텍처의 장점:
    <ul>
      <li>향상된 쓰기 성능: 새 문서를 기존 세그먼트 변경 없이 새 세그먼트에 빠르게 추가할 수 있음.</li>
      <li>효율적인 캐싱: 세그먼트는 불변이므로 일관성 문제 없이 메모리나 SSD에 안전하게 캐시될 수 있음</li>
      <li>단순화된 동시성: 읽기 작업은 쿼리 도중 데이터 변경에 대해 걱정할 필요가 없어 동시 접근이 단순화</li>
      <li>쉬운 복구: 충돌 시, 불변 세그먼트는 상태가 알려져 있고 일관성이 유지되므로 복구가 더 쉬움.</li>
      <li>최적화된 압축: 불변 데이터는 더 효과적으로 압축되어 디스크 공간을 절약.</li>
      <li>더 빠른 검색: 불변성으로 인해 검색을 위한 최적화된 데이터 구조와 알고리즘이 가능</li>
    </ul>
  </li>
  <li>주기적인 세그먼트 병합의 필요성 및 정리 작업 전 일시적으로 증가하는 저장 공간 요구사항과 같은 과제도 있음</li>
</ul>

<h4 id="lucene-세그먼트-특징">Lucene 세그먼트 특징</h4>

<p>세그먼트는 검색 작업에 관련된 고도로 최적화된 데이터 구조를 담고 있습니다.</p>

<ul>
  <li>역인덱스 (Inverted Index)
    <ul>
      <li>Lucene의 핵심은 역인덱스.</li>
      <li>콘텐츠(예: 단어 또는 숫자)에서 데이터베이스의 위치(문서)로의 매핑을 저장하는 데 사용되는 데이터 구조</li>
      <li>Elasticsearch를 사용한 키워드 검색을 빠르게 함.</li>
      <li>모든 문서에 나타나는 고유한 단어를 나열하고 각 단어가 나타나는 모든 문서를 식별.</li>
      <li>“great”이라는 단어를 찾는다 가정: 모든 문서를 스캔하는 대신, 역인덱스를 찾아 문서 ID를 O(1)에 찾을 수 있음.</li>
      <li>You can copy your data and organize the copy like (1) (?)</li>
    </ul>
  </li>
  <li>Doc Values
    <ul>
      <li>역인덱스가 토큰에서 문서로의 매핑을 제공한다면, Doc Values는 최종 정렬을 수행하는 데 필요한 데이터를 제공.</li>
      <li>on-disk</li>
      <li>정렬들을 수행할 때는 역인덱스와 달리 특정 컬럼의 값이 필요하게 됨.</li>
      <li>Sorting, aggregations, and access to field values in scripts requires a different data access pattern. Instead of looking up the term and finding documents, we need to be able to look up the document and find the terms that it has in a field.</li>
      <li>관계형 데이터베이스와 같은 행 지향 데이터베이스의 문제점인, 단일 컬럼에만 접근해야 할 때 전체 행을 읽어야 하는 비효율성을 해결함.</li>
      <li>The doc_values field is an on-disk data structure that is built at document index time and enables efficient data access. It stores the same values as _source, but in a columnar format that is more efficient for sorting and aggregation.</li>
      <li>Doc Values 구조는 세그먼트의 모든 문서에 대해 단일 필드의 컬럼형, 연속적인 표현(contiguous representation)을 가짐 -&gt; 빠른 정렬 및 필터링을 가능하게 합니다.</li>
    </ul>
  </li>
</ul>

<h4 id="코디네이팅-노드">코디네이팅 노드</h4>

<p>코디네이팅 노드는 클라이언트로부터 요청을 받아 클러스터 전반에 걸쳐 실행을 조정하는 역할을 합니다.</p>

<ul>
  <li>쿼리 플래너: 검색 쿼리를 실행하는 가장 효율적인 방법을 결정하는 알고리즘
    <ul>
      <li>쿼리 플래너는 쿼리 실행 계획을 평가하여 관련 문서를 검색하는 최상의 방법을 결정</li>
      <li>여기에는 역인덱스 사용 여부 결정, 쿼리 부분 실행 순서 결정, 여러 노드에서 온 결과 결합 조정 등이 포함</li>
      <li>예를 들어, “bill nye”를 검색할 때, “nye”가 포함된 문서 수가 “bill”보다 훨씬 적다면, “nye”가 포함된 문서를 먼저 찾아 교집합을 구하는 것이 훨씬 효율적</li>
      <li>Elasticsearch의 쿼리 플래너는 필드의 유형, 인기 있는 키워드, 문서 길이 등 통계를 유지하여 사용자에게 결과를 반환하는 데 걸리는 시간을 최소화하도록 최적의 옵션을 선택</li>
      <li>이러한 최적화는 데이터 세트의 크기와 복잡성이 증가함에 따라 성능을 유지하는 데 중요</li>
      <li>데이터 의존성(data dependence)을 처리할 수 있게 하여, 시스템이 인덱스의 데이터에 동적으로 반응할 수 있게 함</li>
    </ul>
  </li>
</ul>

<h3 id="인터뷰에서의-활용">인터뷰에서의 활용</h3>

<h4 id="elasticsearch-사용-시-고려사항">Elasticsearch 사용 시 고려사항</h4>

<ul>
  <li>주요 데이터베이스로 사용하지 말 것: Elasticsearch는 기본적으로 검색 엔진이며, 기존 데이터베이스를 대체하기 위한 것이 아님
    <ul>
      <li>일반적으로 PostgreSQL 또는 DynamoDB와 같은 권한 있는 데이터 스토어에 Change Data Capture (CDC)를 통해 연결</li>
    </ul>
  </li>
  <li>읽기 중심 워크로드에 최적화됨: 쓰기 중심 시스템을 다루는 경우 다른 옵션을 고려하거나 쓰기 버퍼를 구현
    <ul>
      <li>잦은 업데이트는 Elasticsearch의 성능을 저하시킴</li>
    </ul>
  </li>
  <li>최종 일관성 모델 허용: 결과는 지연될 수 있으며, 경우에 따라 상당히 오래 걸림
    <ul>
      <li>강력한 일관성이 필요한 경우에는 다른 데이터베이스를 사용해야 함</li>
    </ul>
  </li>
  <li>데이터 비정규화(Denormalization): 검색 쿼리를 효율적으로 만들기 위해 데이터를 최대한 비정규화해야 함
    <ul>
      <li>Elasticsearch는 관계형 데이터베이스가 아님: 조인(JOINs)을 지원하지 않으며, 단일 또는 두 개의 쿼리로 결과를 제공해야 함</li>
    </ul>
  </li>
  <li>항상 필요한 것은 아님: 데이터가 작거나(10만 문서 미만) 자주 변경되지 않는다면, 더 간단하고 빠른 솔루션
    <ul>
      <li>기본 데이터 스토어에 대한 간단한 쿼리가 충분한지 확인하고, 부족할 경우에만 Elasticsearch를 고려</li>
    </ul>
  </li>
  <li>데이터 동기화 유지: 기본 데이터와 Elasticsearch 간의 동기화를 주의 깊게 유지해야 함. 동기화 실패는 드리프트(drift)를 유발하고 일반적인 버그의 원인이 됨.</li>
  <li>선택 정당화: Elasticsearch가 강력한 도구이지만 만능 해결책은 아니므로, 다른 옵션보다 Elasticsearch를 선택하는 이유를 정당화하고 장점뿐만 아니라 한계도 논의할 준비를 해야 함.</li>
</ul>

<h4 id="elasticsearch에서-얻을-수-있는-시스템-디자인-팁">Elasticsearch에서 얻을 수 있는 시스템 디자인 팁</h4>

<ol>
  <li>불변성(Immutability)의 힘: 스택의 적절한 계층에서 불변성을 사용하면 캐싱, 압축 및 데이터 최적화 능력을 향상시킬 수 있습니다. 또한 가변 데이터에서 해결하기 훨씬 어려운 동기화 및 무결성 문제를 걱정할 필요가 없습니다.</li>
  <li>쿼리 실행과 데이터 저장 분리: 쿼리 실행을 데이터 저장소에서 분리함으로써 각 부분을 독립적으로 최적화할 수 있습니다. Elasticsearch의 데이터 노드와 코디네이팅 노드는 각 노드 유형의 책임에 집중함으로써 서로를 완벽하게 보완합니다.</li>
  <li>인덱싱 전략의 중요성: 인덱싱 전략은 검색 성능에 큰 영향을 미칩니다. Elasticsearch의 역인덱스 구조는 빠른 전문 텍스트 검색을 가능하게 하고, Doc Values는 효율적인 정렬 및 집계를 가능하게 합니다. 빠른 데이터 검색이 필요한 시스템을 설계할 때 가장 일반적인 쿼리 패턴을 지원하기 위해 데이터를 어떻게 구조화할지 고려해야 합니다.</li>
  <li>분산 시스템의 복잡성: 분산 시스템은 확장성과 내결함성을 제공하지만, 복잡성도 증가시킵니다. Elasticsearch의 클러스터 아키텍처는 대량의 데이터와 높은 쿼리 부하를 처리할 수 있지만, 데이터 일관성 및 네트워크 분할에 대한 신중한 고려도 필요합니다. 분산 시스템을 설계할 때 항상 일관성, 가용성, 분할 허용 오차(CAP 이론) 사이의 절충점을 고려해야 합니다.</li>
  <li>효율적인 데이터 구조의 중요성: Elasticsearch가 역인덱스를 위해 스킵 리스트(skip lists) 및 유한 상태 변환기(finite state transducers)와 같은 전문화된 데이터 구조를 사용하는 것은 특정 사용 사례에서 맞춤형 데이터 구조가 성능을 극적으로 향상시킬 수 있음을 보여줍니다. 데이터 구조를 선택하거나 설계할 때 항상 데이터의 접근 패턴을 고려해야 합니다.</li>
</ol>]]></content><author><name>gayuna</name></author><category term="study" /><category term="System Design" /><category term="elasticsearch" /><summary type="html"><![CDATA[Elasticsearch Deep Dive]]></summary></entry><entry><title type="html">Streaming Systems - Chapter 8 : Looking Forward: Toward Robust Streaming SQL</title><link href="https://gayuna.github.io/study/streaming-system-ch08/" rel="alternate" type="text/html" title="Streaming Systems - Chapter 8 : Looking Forward: Toward Robust Streaming SQL" /><published>2025-07-04T00:00:00+09:00</published><updated>2025-07-04T00:00:00+09:00</updated><id>https://gayuna.github.io/study/streaming-system-ch08</id><content type="html" xml:base="https://gayuna.github.io/study/streaming-system-ch08/"><![CDATA[<h1 id="핵심-개념-및-sql-확장">핵심 개념 및 SQL 확장</h1>
<ul>
  <li>SQL에 견고한 스트리밍을 지원하기 위한 핵심은 시간에 따라 변화하는 관계(Time-Varying Relations, TVRs)를 사용하는 것.</li>
  <li>SQL에 사용되는 방법을 그대로 적용할 수도 있지만, 이것이 실용적이지 않은 케이스들이 있음: table과 stream 두가지를 다 다룰 수 있어야 함.</li>
  <li>시간, 특히 event time에 대해서도 고려를 해야 함. : timestamp, windowing, triggering에 대한 고려 필요.</li>
</ul>

<h1 id="stream-and-table-selection">Stream and Table Selection</h1>
<p>시간에 따라 변화하는 관계(TVR)는 테이블(Table) 또는 스트림(Stream)의 두 가지 방식으로 물리적으로 표현.</p>

<ul>
  <li>TABLE 키워드: 특정 시점의 스냅샷 뷰를 반환합니다.</li>
  <li>STREAM 키워드: 시간 경과에 따른 데이터의 변화를 이벤트 단위로 캡처한 뷰를 반환합니다.</li>
</ul>

<p>TABLE과 STREAM중 무엇을 선택하는 것이 좋은가? - 좋은 기본값: 명시적인 키워드를 제공하지 않을 때 시스템이 채택해야 할 기본 동작에 따라.</p>

<ul>
  <li>모든 입력이 테이블일 경우: 출력은 기본적으로 테이블. 이는 기존 SQL 쿼리의 동작과 일치합니다.</li>
  <li>하나라도 입력이 스트림일 경우: 출력은 기본적으로 스트림.</li>
</ul>

<pre><code class="language-Text">Table이나 Stream과 같은 TVR의 물리적인 표현은 TVR을 (사용자가) 직접 보거나 특정 테이블/Stream으로 출력하려는 경우에만 필요하다.

테이블은 특정 시점만 포함하거나, 스트림은 기록 중 일부만 포함하게 됨. (리소스 절감을 위한 trade-off) 쿼리의 중간 단게에서 TVR을 이런식으로 랜더링하면 정보 손실/불필요한 오버헤드가 발생함. 스트리밍 SQL이 강력해지려면 이러한 손실이나 불필요한 변환 없이 데이터를 최대한 완전하게 다루는 것이 중요.
</code></pre>

<h1 id="시간-관련-연산자-temporal-operators">시간 관련 연산자 (Temporal Operators)</h1>
<p>견고한 순서 불일치 처리(out-of-order processing)의 기반은 이벤트 시간(event-time timestamp)
SQL의 세계에서 event time은 단순히 하나의 column. source data 자체에 있음.</p>

<p><img src="http://www.streamingbook.net/static/images/figures/stsy_0806.png" alt="8-6" />
앞에서 계속 사용한 시간별 점수 그림을 SQL 테이블로 나타내면 아래와 같이 표현됨:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:10&gt; SELECT TABLE <span class="k">*</span>, Sys.MTime as ProcTime
       FROM UserScores ORDER BY EventTime<span class="p">;</span>
<span class="nt">------------------------------------------------</span>
| Name  | Team  | Score | EventTime | ProcTime |
<span class="nt">------------------------------------------------</span>
| Julie | TeamX |     5 |  12:00:26 | 12:05:19 |
| Frank | TeamX |     9 |  12:01:26 | 12:08:19 |
| Ed    | TeamX |     7 |  12:02:26 | 12:05:39 |
| Julie | TeamX |     8 |  12:03:06 | 12:07:06 |
| Amy   | TeamX |     3 |  12:03:39 | 12:06:13 |
| Fred  | TeamX |     4 |  12:04:19 | 12:06:39 |
| Naomi | TeamX |     3 |  12:06:39 | 12:07:19 |
| Becky | TeamX |     8 |  12:07:26 | 12:08:39 |
| Naomi | TeamX |     1 |  12:07:46 | 12:09:00 |
<span class="nt">------------------------------------------------</span>
</code></pre></div></div>
<ul>
  <li>SQL로 랜더링함으로서 9개의 점수가 7명의 user에 의해 났다거나, 이들이 모든 같은 팀이라든가의 정보 확인 가능.</li>
  <li>각 기록에 대해 event time과 processing time을 모두 보여줌.</li>
  <li>간단하게 다른 방법으로 볼 수 있음 (ex. ORDER BY 키워드로 processing time 기준으로 정렬 가능)</li>
  <li>부분적으로 (특정 시점만 보여주니까) 충실한 View라고 할 수 있음.</li>
</ul>

<p>만약 SQL로 시점에 따른 변화를 그려주려면 n개의 table이 나와야 함. (너무 많다.) 이에 비해 stream은 비교적 심플하게 표현 가능.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:00&gt; SELECT STREAM Score, EventTime, Sys.MTime as ProcTime FROM UserScores<span class="p">;</span>
<span class="nt">--------------------------------</span>
| Score | EventTime | ProcTime |
<span class="nt">--------------------------------</span>
|     5 |  12:00:26 | 12:05:19 |
|     7 |  12:02:26 | 12:05:39 |
|     3 |  12:03:39 | 12:06:13 |
|     4 |  12:04:19 | 12:06:39 |
|     8 |  12:03:06 | 12:07:06 |
|     3 |  12:06:39 | 12:07:19 |
|     9 |  12:01:26 | 12:08:19 |
|     8 |  12:07:26 | 12:08:39 |
|     1 |  12:07:46 | 12:09:00 |
........ <span class="o">[</span>12:00, 12:10] ........
</code></pre></div></div>
<ul>
  <li>맨 아래의 trailing footer (…….. [12:00, 12:10] ……..)가 해당되는 기간을 나타냄.</li>
  <li>stream이 더 데이터가 들어오는걸 기다리고 있음에 유의.</li>
</ul>

<p>이제 이 단순한 stream을 조작하기 시작하면 더 재밌어짐.</p>
<pre><code class="language-Java">PCollection&lt;String&gt; raw = IO.read(...);
PCollection&lt;KV&lt;Team, Integer&gt;&gt; input = raw.apply(new ParseFn());
PCollection&lt;KV&lt;Team, Integer&gt;&gt; totals =
  input.apply(Sum.integersPerKey());  // input을 합치고 있음.
</code></pre>
<p>http://www.streamingbook.net/fig/8-7</p>

<p>이 예시에서 우리의 data가 bounded data기 때문에 전통적인 batch와 다를바 없고, table과 stream의 형태가 매우 유사함.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:10&gt; SELECT TABLE SUM<span class="o">(</span>Score<span class="o">)</span> as Total, MAX<span class="o">(</span>EventTime<span class="o">)</span>,
       MAX<span class="o">(</span>Sys.MTime<span class="o">)</span> as <span class="s2">"MAX(ProcTime)"</span> FROM UserScores GROUP BY Team<span class="p">;</span>
<span class="nt">------------------------------------------</span>
| Total | MAX<span class="o">(</span>EventTime<span class="o">)</span> | MAX<span class="o">(</span>ProcTime<span class="o">)</span> |
<span class="nt">------------------------------------------</span>
|    48 |       12:07:46 |      12:09:00 |
<span class="nt">------------------------------------------</span>

12:00&gt; SELECT STREAM SUM<span class="o">(</span>Score<span class="o">)</span> as Total, MAX<span class="o">(</span>EventTime<span class="o">)</span>,
       MAX<span class="o">(</span>Sys.MTime<span class="o">)</span> as <span class="s2">"MAX(ProcTime)"</span> FROM UserScores GROUP BY Team<span class="p">;</span>
<span class="nt">------------------------------------------</span>
| Total | MAX<span class="o">(</span>EventTime<span class="o">)</span> | MAX<span class="o">(</span>ProcTime<span class="o">)</span> |
<span class="nt">------------------------------------------</span>
|    48 |       12:07:46 |      12:09:00 |
<span class="nt">------</span> <span class="o">[</span>12:00, 12:10] END-OF-STREAM <span class="nt">------</span>
</code></pre></div></div>

<h1 id="윈도잉-windowing">윈도잉 (Windowing)</h1>

<pre><code class="language-Java">PCollection&lt;String&gt; raw = IO.read(...);
PCollection&lt;KV&lt;Team, Integer&gt;&gt; input = raw.apply(new ParseFn());
PCollection&lt;KV&lt;Team, Integer&gt;&gt; totals = input
  .apply(Window.into(FixedWindows.of(TWO_MINUTES))) // 요부분이 추가됨. 2분 간격으로 나눈 것.
  .apply(Sum.integersPerKey());
</code></pre>

<p>http://www.streamingbook.net/fig/8-8</p>

<p>8-7 대비 달라진 것: 하나의 sum이 아니라 2분 간격의 window로 나뉘어서 4개의 윈도우에 각각 sum이 존재하게 됨.
GROUP BY를 사용하거나, built-in windowing operation을 사용함으로서 이런식으로 윈도우를 나눌 수 있음.</p>

<h2 id="ad-hoc-windowing">ad hoc windowing</h2>
<p>group by를 사용해서 아래와 같이 표현 가능:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:10&gt; SELECT TABLE SUM<span class="o">(</span>Score<span class="o">)</span> as Total, 
         <span class="s2">"["</span> <span class="o">||</span> EventTime / INTERVAL <span class="s1">'2'</span> MINUTES <span class="o">||</span> <span class="s2">", "</span> <span class="o">||</span> 
           <span class="o">(</span>EventTime / INTERVAL <span class="s1">'2'</span> MINUTES<span class="o">)</span> + INTERVAL <span class="s1">'2'</span> MINUTES <span class="o">||</span>
           <span class="s2">")"</span> as Window, 
         MAX<span class="o">(</span>Sys.MTime<span class="o">)</span> as <span class="s2">"MAX(ProcTime)"</span>
       FROM UserScores
       GROUP BY Team, EventTime / INTERVAL <span class="s1">'2'</span> MINUTES<span class="p">;</span>
<span class="nt">------------------------------------------------</span>
| Total | Window               | MAX<span class="o">(</span>ProcTime<span class="o">)</span> |
<span class="nt">------------------------------------------------</span>
| 14    | <span class="o">[</span>12:00:00, 12:02:00<span class="o">)</span> | 12:08:19      |
| 18    | <span class="o">[</span>12:02:00, 12:04:00<span class="o">)</span> | 12:07:06      |
| 4     | <span class="o">[</span>12:04:00, 12:06:00<span class="o">)</span> | 12:06:39      |
| 12    | <span class="o">[</span>12:06:00, 12:08:00<span class="o">)</span> | 12:09:00      |
<span class="nt">------------------------------------------------</span>
</code></pre></div></div>

<h2 id="명시적-windowing">명시적 windowing</h2>
<p>Apache Calciter와 같이 windowing statement가 지원하는 경우도 있음:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:10&gt; SELECT TABLE SUM<span class="o">(</span>Score<span class="o">)</span> as Total,
         TUMBLE<span class="o">(</span>EventTime, INTERVAL <span class="s1">'2'</span> MINUTES<span class="o">)</span> as Window,
         MAX<span class="o">(</span>Sys.MTime<span class="o">)</span> as <span class="s1">'MAX(ProcTime)'</span> 
       FROM UserScores
       GROUP BY Team, TUMBLE<span class="o">(</span>EventTime, INTERVAL <span class="s1">'2'</span> MINUTES<span class="o">)</span><span class="p">;</span>
<span class="nt">------------------------------------------------</span>
| Total | Window               | MAX<span class="o">(</span>ProcTime<span class="o">)</span> |
<span class="nt">------------------------------------------------</span>
| 14    | <span class="o">[</span>12:00:00, 12:02:00<span class="o">)</span> | 12:08:19      |
| 18    | <span class="o">[</span>12:02:00, 12:04:00<span class="o">)</span> | 12:07:06      |
| 4     | <span class="o">[</span>12:04:00, 12:06:00<span class="o">)</span> | 12:06:39      |
| 12    | <span class="o">[</span>12:06:00, 12:08:00<span class="o">)</span> | 12:09:00      |
<span class="nt">------------------------------------------------</span>
</code></pre></div></div>

<p>group by로도 충분한데, 명시적인 windowing 생성을 지원하는 이유:</p>
<ul>
  <li>윈도우 계산의 자동화: 직접 윈도우 계산을 수행할 필요 없이, 너비 / 슬라이드와 같은 기본 파라미터를 지정하여 결과를 얻기 쉽다.</li>
  <li>복잡하고 동적인 그룹화를 간결하게 표현: 세션 윈도우, self join 등등…</li>
</ul>

<h1 id="워터마크-watermarks--트리거-triggering">워터마크 (Watermarks) / 트리거 (Triggering)</h1>
<p>windowing 까지는 data를 table처럼 소비하면서 기존의 batch나 관계형 데이터처럼 취급함. 하지만 Stream으로 된 data를 받아들이려면 ‘processing time 중 언제 결과를 materialize 해야 하는가’하는 질문이 떠오르게 됨. : 기존과 마찬가지로 트리거와 워터마크가 정답임.</p>

<h2 id="a-sql-ish-default-per-record-trigger">A SQL-ish default: per-record trigger</h2>
<p>데이터가 도착할 때마다 trigger 하는 경우의 장점:</p>
<ul>
  <li>단순함: 이해하기 편함. materialized view는 이런식으로 동작해왔음</li>
  <li>정확도: 원본 TVR이 가진 시간적 정보/변화를 스트림이 완벽하게 보존/표현 (앞선 table이 partial-fidelity 였던 것과 대조) - 뒤에 나올 다른 trigger들도 fidelity는 희생하게 됨.</li>
</ul>

<p>단점:</p>
<ul>
  <li>
    <p>비용</p>
  </li>
  <li>Trigger는 grouping 다음에 오게되는데, grouping은 특성상 data flow의 cardinality를 줄이고, triggering은 특성상 데이터의 빈도를 줄여 줌. -&gt; 비용 절감.</li>
  <li>비용이 중요하지 않다면, per-record trigger를 사용할 때의 단순함+정확도의 이점이 완벽하지 않은 트리거를 사용하는 인지적 복잡성보다 크다.</li>
</ul>

<p>http://www.streamingbook.net/fig/8-9</p>

<p>부작용: data가 안정화 되자마자 트리거에 의해 다시 바뀌게 되므로 안정화 되는(brought to rest)효과를 억제하게 됨.
그리고 매우 시끄러움(chatty). 우리가 큰 스케일의 애플리케이션을 만들고 있다면, 모든 upstream에서의 기록 발생때마다 downstream에서 계산을 하는 비용을 지불하고 싶지는 않을 것.</p>

<h2 id="watermark-triggers">Watermark triggers</h2>

<p>http://www.streamingbook.net/fig/8-10
(늦게 오는 숫자는 SUM에 숫자가 바뀌지만 그 아래 STREAM에는 반영되지 않음에 유의 -&gt; 데이터 유실)</p>

<p>늦게 도착한 data에 대해서는 즉시lateFiring 함으로서 보완할 수 있음.
http://www.streamingbook.net/fig/8-11</p>

<p>이 때 2개의 컬럼을 추가하면 유용하다.</p>
<ul>
  <li>Sys.EmitTiming: watermark 대비한 타이밍 (on-time / late)</li>
  <li>Sys.EmitIndex: revisions for a given row/window</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:00&gt; SELECT STREAM SUM<span class="o">(</span>Score<span class="o">)</span> as Total,
         TUMBLE<span class="o">(</span>EventTime, INTERVAL <span class="s1">'2'</span> MINUTES<span class="o">)</span> as Window,
         CURRENT_TIMESTAMP as EmitTime,
         Sys.EmitTiming, Sys.EmitIndex 
       FROM UserScores
       GROUP BY Team, TUMBLE<span class="o">(</span>EventTime, INTERVAL <span class="s1">'2'</span> MINUTES<span class="o">)</span>
       EMIT WHEN WATERMARK PAST WINDOW_END<span class="o">(</span>Window<span class="o">)</span>
         AND THEN AFTER 0 SECONDS<span class="p">;</span>
<span class="nt">----------------------------------------------------------------------------</span>
| Total | Window               | EmitTime | Sys.EmitTiming | Sys.EmitIndex |
<span class="nt">----------------------------------------------------------------------------</span>
| 5     | <span class="o">[</span>12:00:00, 12:02:00<span class="o">)</span> | 12:06:00 | on-time        | 0             |
| 18    | <span class="o">[</span>12:02:00, 12:04:00<span class="o">)</span> | 12:07:30 | on-time        | 0             |
| 4     | <span class="o">[</span>12:04:00, 12:06:00<span class="o">)</span> | 12:07:41 | on-time        | 0             |
| 14    | <span class="o">[</span>12:00:00, 12:02:00<span class="o">)</span> | 12:08:19 | late           | 1             |
| 12    | <span class="o">[</span>12:06:00, 12:08:00<span class="o">)</span> | 12:09:22 | on-time        | 0             |
.............................. <span class="o">[</span>12:00, 12:10] ..............................
</code></pre></div></div>
<p>(밑에서 두번째 열 보기)</p>

<h2 id="repeated-delay-triggers">Repeated delay triggers</h2>
<p>데이터가 도착한 후 프로세싱 타임 상 1분 후에 해당 window trigger 하기</p>
<ul>
  <li>로드를 고르게 분산하는데 도움이 됨.</li>
  <li>Watermark가 필요하지 않음</li>
</ul>

<p>http://www.streamingbook.net/fig/8-12
각 윈도우에 첫번째 데이터가 도착하고 1분 후 트리거 되어서 그 사이에 도착한 데이터를 함께 처리함. (이거때문데 per-record보단 덜 시끄러움) 트리거 완료 후 데이터가 도착하면 다시 그 이후로 1분 기다렸다가 트리거. 비용과 적시성(timeliness) 사이의 밸런스를 잡음.</p>

<h2 id="data-driven-triggers">Data-driven triggers</h2>
<p>score &gt; 10과 같이 데이터를 보고 trigger 하는 것은 결국 per-record trigger를 매번 하고 해당 조건을 만족할때만 downstream으로 보내주겠다는 것과 같다. 따라서 명시적으로 있을 필요가 없다.</p>

<h1 id="accumulation">Accumulation</h1>

<p>누적 방식은 단일 윈도우에 대해 여러 번 결과가 구체화될 때 결과들의 관계를 정의</p>

<p>지금까지 본 것: Accumulation 모드.
http://www.streamingbook.net/fig/8-13 
단점: Over Counting</p>

<p>Accumulation &amp; Retraction: 더해서 내보내면서 이전단계를 빼주는 것. -&gt; Down stream에서 over counting을 신경쓰지 않아도 됨.
http://www.streamingbook.net/fig/8-14</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>12:00&gt; SELECT STREAM SUM<span class="o">(</span>Score<span class="o">)</span> as Total,
         SESSION<span class="o">(</span>EventTime, INTERVAL <span class="s1">'1'</span> MINUTE<span class="o">)</span> as Window,
         CURRENT_TIMESTAMP as EmitTime,
         Sys.Undo as Undo
       FROM UserScoresForSessions
       GROUP BY Team, SESSION<span class="o">(</span>EventTime, INTERVAL <span class="s1">'1'</span> MINUTE<span class="o">)</span><span class="p">;</span>
<span class="nt">--------------------------------------------------</span>
| Total | Window               | EmitTime | Undo |
<span class="nt">--------------------------------------------------</span>
| 5     | <span class="o">[</span>12:00:26, 12:01:26<span class="o">)</span> | 12:05:19 |      |
| 7     | <span class="o">[</span>12:02:26, 12:03:26<span class="o">)</span> | 12:05:39 |      |
| 3     | <span class="o">[</span>12:03:39, 12:04:39<span class="o">)</span> | 12:06:13 |      |
| 3     | <span class="o">[</span>12:03:39, 12:04:39<span class="o">)</span> | 12:06:46 | undo |
| 7     | <span class="o">[</span>12:03:39, 12:05:19<span class="o">)</span> | 12:06:46 |      |
| 3     | <span class="o">[</span>12:06:39, 12:07:39<span class="o">)</span> | 12:07:19 |      |
| 7     | <span class="o">[</span>12:02:26, 12:03:26<span class="o">)</span> | 12:07:33 | undo |
| 7     | <span class="o">[</span>12:03:39, 12:05:19<span class="o">)</span> | 12:07:33 | undo |
| 22    | <span class="o">[</span>12:02:26, 12:05:19<span class="o">)</span> | 12:07:33 |      |
| 3     | <span class="o">[</span>12:06:39, 12:07:39<span class="o">)</span> | 12:08:13 | undo |
| 11    | <span class="o">[</span>12:06:39, 12:08:26<span class="o">)</span> | 12:08:13 |      |
| 5     | <span class="o">[</span>12:00:26, 12:01:26<span class="o">)</span> | 12:08:19 | undo |
| 22    | <span class="o">[</span>12:02:26, 12:05:19<span class="o">)</span> | 12:08:19 | undo |
| 36    | <span class="o">[</span>12:00:26, 12:05:19<span class="o">)</span> | 12:08:19 |      |
| 11    | <span class="o">[</span>12:06:39, 12:08:26<span class="o">)</span> | 12:09:00 | undo |
| 12    | <span class="o">[</span>12:06:39, 12:08:46<span class="o">)</span> | 12:09:00 |      |
................. <span class="o">[</span>12:00, 12:10] .................
</code></pre></div></div>

<p>다운스트림에서 하나의 그룹으로 단순하게 high-volumn 더하는 등의 특수한 유즈케이스가 아니라면 discarding 모드는 복잡하고 에러를 만들기 쉬움. (기본적으로 비추천)</p>]]></content><author><name>gayuna</name></author><category term="study" /><category term="Streaming Systems" /><summary type="html"><![CDATA[핵심 개념 및 SQL 확장 SQL에 견고한 스트리밍을 지원하기 위한 핵심은 시간에 따라 변화하는 관계(Time-Varying Relations, TVRs)를 사용하는 것. SQL에 사용되는 방법을 그대로 적용할 수도 있지만, 이것이 실용적이지 않은 케이스들이 있음: table과 stream 두가지를 다 다룰 수 있어야 함. 시간, 특히 event time에 대해서도 고려를 해야 함. : timestamp, windowing, triggering에 대한 고려 필요.]]></summary></entry><entry><title type="html">Hello Interview - Design Youtube</title><link href="https://gayuna.github.io/study/hello-interview-system-design-youtube/" rel="alternate" type="text/html" title="Hello Interview - Design Youtube" /><published>2025-06-24T00:00:00+09:00</published><updated>2025-06-24T00:00:00+09:00</updated><id>https://gayuna.github.io/study/hello-interview-system-design-youtube</id><content type="html" xml:base="https://gayuna.github.io/study/hello-interview-system-design-youtube/"><![CDATA[<h1 id="requirements">Requirements</h1>
<h2 id="functional-requirements">Functional Requirements</h2>
<ul>
  <li>Users should be able to upload videos</li>
  <li>Users should be able to watch/stream videos</li>
</ul>

<p>Scale:</p>
<ul>
  <li>~1M uploads/day</li>
  <li>100M DAU</li>
  <li>Max video size of 256GB / 12 hours</li>
</ul>

<h2 id="non-functional-requirements">Non-Functional Requirements</h2>
<ul>
  <li>Availability » consistency (업로드 되자마자 비디오를 볼 수 있지만 에러가 뜨기 vs 조금 기다려야 하지만 에러가 안뜨기)</li>
  <li>Support uploading/streaming for large videos (256GB)</li>
  <li>low latency streaming (&lt;500ms) true in low bandwidth</li>
  <li>scalability to scale to 1M uploads/day and 100M views</li>
</ul>

<h1 id="core-entities--apis">Core Entities &amp; APIs</h1>
<h2 id="core-entities">Core Entities</h2>
<ul>
  <li>Video (bytes)</li>
  <li>Video Metadata</li>
  <li>User</li>
</ul>

<h1 id="apis">APIs</h1>
<ul>
  <li>Upload a video
    <ul>
      <li>POST /videos (small-sized videos)
{
  Video,
  VideoMetadata
}</li>
      <li>POST /video (large videos) -&gt; pre-signed URL
{
  VideoMetadata (including size)
}</li>
    </ul>
  </li>
  <li>watch a video
    <ul>
      <li>GET /videos/{:videoId} -&gt; Video &amp; VideoMetadata</li>
    </ul>
  </li>
</ul>

<h1 id="high-level-design">High-level Design</h1>
<p><img src="https://github.com/user-attachments/assets/ae81ad50-e82c-4e26-8f5f-3c3f03b7f3b8" alt="Image" /></p>

<ul>
  <li><a href="https://gayuna.github.io/system%20design/system-design-mybox/">한국 서비스를 기반으로 시스템 디자인 공부하기 – MY BOX</a></li>
</ul>

<h1 id="deep-dives">Deep-dives</h1>
<h2 id="support-uploading--streaming-for-large-videos">support uploading / streaming for large videos</h2>
<p>기존 디자인: S3에서 바로 download -&gt; 1.시간이 오래 걸림 2. client에게 그만큼의 메모리 공간이 필요하고 연결도 stable 해야 함. 3. 다 다운이 되어야만 재생할 수 있음.
<img src="https://github.com/user-attachments/assets/465dfa26-8a74-411e-b6a5-574d784ee2ea" alt="Image" />
해결법: S3에서 동영상을 쪼개서 2-10초의 chunk로 가지고 있다가 다운로드 하는 방안
이 때 streaming을 위한 chunk는 업로드에 비해 잘게 쪼갠다. (ex. 2초)</p>

<p>기존 디자인: 인터넷 연결이 unstable한 환경에서 접속한다면 제대로 재생이 안될 것.
<img src="https://github.com/user-attachments/assets/f4c73e3b-b9a5-4f23-948e-4f7478889356" alt="Image" />
해결법: 여러가지 해상도/비트레이트로 미리 변환해두고 client에 맞는 파일을 내려 보내줌.
adaptive bitrate : client가 network condition을 체크. 주기적으로 체크하면서 그에 맞는 화질을 선택.</p>

<p>기존 디자인: hot video에 다량의 쿼리 발생 &amp; client는 유럽에 있고 S3는 미국 서부에 있다면 latency issue 발생
<img src="https://github.com/user-attachments/assets/bcc71d94-4891-48d8-8a4d-de244c5ef78a" alt="Image" />
해결법: DB sharding, metadata cache와 CDN 도입</p>]]></content><author><name>gayuna</name></author><category term="study" /><category term="System Design" /><summary type="html"><![CDATA[Requirements Functional Requirements Users should be able to upload videos Users should be able to watch/stream videos]]></summary></entry><entry><title type="html">Streaming Systems - Chapter 3</title><link href="https://gayuna.github.io/study/streaming-systems-ch03/" rel="alternate" type="text/html" title="Streaming Systems - Chapter 3" /><published>2025-05-22T00:00:00+09:00</published><updated>2025-05-22T00:00:00+09:00</updated><id>https://gayuna.github.io/study/streaming-systems-ch03</id><content type="html" xml:base="https://gayuna.github.io/study/streaming-systems-ch03/"><![CDATA[<h4 id="percentile-watermarks">Percentile watermarks</h4>

<p>이 전까지 watermark는 가장 빠른 event time을 기준으로 계산함. 이렇게 최소 점 대신에, 특정 퍼센트를 정해서, 그 퍼센트까지 데이터가 도착했으면 데이터가 도착했다고 간주함. (ex. 99%로 선정했다면, 빠른 99%가 도착했다면 데이터가 도착했다고 가정) outlier에 의한 지연을 제거할 수 있음. 100 -&gt; 66 -&gt; 33% 워터마크가 될 수록 점점 그림이 빨리 그려짐: latency는 개선되나 precision이 떨어짐.</p>

<h4 id="processing-time-watermarks">Processing-Time watermarks</h4>

<p>event time watermark로는 한시간 전의 데이터를 빠르게 처리하는 시스템과, 현제의 데이터를 한시간동안 걸려서 처리하는 시스템을 구분할 수 없다.</p>

<p>event time watermark와 대비되는 processing time watermark를 사용, data delay와 별개로 processing delay를 구분할 수 있게 된다.</p>

<p>그림 3-12, 3-13에서는 event-time watermark가 늘어나면서 동시에 processing time watermark가 늘어나고 있다. -&gt; system processing이 지연되고 있는 것.</p>

<p>처리 시간 워터마크 지연 = (현재 실제 처리 시간) - (가장 오래된 미완료 연산이 시작되었던 처리 시간 타임스탬프) : 실제로 처리를 시작하고 나서 지연되고 있다는 것 -&gt; 유저나 관리자가 처리해야하는 문제가 있을 확률이 높다.</p>

<p>그림 3-12, 3-13과 대비되게 3-14의 경우는 event-time watermark는 증가하지만 processing-time watermark는 그대로 있다 -&gt; 처리에 시간이 걸리고 있는게 아니라, 그 앞단에서 buffer에 쌓고 있거나 한 상황인 것.</p>

<p>그림 3-15의 경우 fixed window인데, window가 넘어갈 때마다 trigger가 되어서 이벤트들이 처리되면 watermark 값이 올라가고 그만큼 watermark delay의 값이 떨어져서 저런 모양이 나오는 것</p>

<h4 id="watermarks-in-google-cloud-dataflow">Watermarks in Google Cloud dataflow</h4>

<p>Dataflow는 key를 기반으로 데이터를 재분배해서 각각의 worker에 분배한다. 
앞에서 watermark는 여러개의 단계로 구분된다고 했는데, Dataflow에서는 각 단계에서도 key range에 따라 별개로 트래킹이 필요하다. 그럼 이 key range를 관통하는, 최소한의 watermark를 계산해야한다.
조건:</p>
<ul>
  <li>모든 range들은 watermark를 보고해야 한다.</li>
  <li>watermark는 증가하기만 해야한다. late data가 들어오더라도 후진해서는 안된다.
Google Cloud Dataflow는 중앙화된 aggregator에서 이를 계산한다. 이는 “single source of truth”로서 작동한다.</li>
</ul>

<h4 id="watermarks-in-apache-flink">Watermarks in Apache Flink</h4>

<p>플링크는 워터마크 추적과 집계를 인밴드(in-band) 방식으로 수행
source들이 중간중간에 워터마크 ‘체크포인트’들을 발생시키고, data stream에 함께 전달된다. 53이라는 타임스탬프가 발생하면 53 이전의 정시(nonlate) 데이터는 발생하지 않는다! (late data는 여전히 들어올 수 있겠지만…)</p>

<p>인밴드 워터마크의 장점:</p>
<ul>
  <li>워터마크 전파 지연 감소 및 매우 낮은 지연 시간의 워터마크: 워터마크 데이터가 여러 홉을 거치거나 중앙 집계를 기다릴 필요가 없기 때문에, 인밴드 방식은 매우 낮은 지연 시간을 더 쉽게 달성할 수 있습니다.</li>
  <li>워터마크 집계에 대한 단일 장애 지점(single point of failure) 없음: 중앙 워터마크 집계 에이전트의 장애는 전체 파이프라인의 워터마크 지연을 초래할 수 있지만, 인밴드 방식에서는 파이프라인 일부의 장애가 전체 파이프라인의 워터마크 지연을 유발하지 않습니다.</li>
  <li>내재된 확장성: 중앙 집중식 워터마크 집계 서비스로 확장성을 달성하려면 더 많은 복잡성이 필요하지만, 인밴드 워터마크는 암묵적인 확장성을 가집니다.</li>
</ul>

<p>아웃오브밴드 워터마크 집계의 장점:</p>
<ul>
  <li>“단일 진실 공급원(Single source of truth)”: 디버깅, 모니터링, 또는 파이프라인 진행 상황에 따른 입력 조절과 같은 애플리케이션을 위해, 시스템 각 구성요소가 부분적인 뷰를 갖는 것보다 워터마크 값을 제공할 수 있는 서비스가 있는 것이 유리합니다.</li>
  <li>소스 워터마크 생성: 일부 소스 워터마크는 전역적인 정보가 필요합니다. 예를 들어, 소스가 일시적으로 유휴 상태이거나, 데이터 전송률이 낮거나, 워터마크를 생성하기 위해 소스 또는 다른 시스템 구성 요소에 대한 아웃오브밴드 정보가 필요할 수 있습니다. 이는 중앙 서비스에서 더 쉽게 달성할 수 있습니다. (예: 구글 클라우드 Pub/Sub의 소스 워터마크 사례)</li>
</ul>

<h4 id="watermarks-for-google-cloud-pubsub">Watermarks for Google Cloud Pub/Sub</h4>

<p>각 client들은 구독을 하고 메세지를 pull 하는 방식. 최대한 old한 메세지부터 pull 되게 하지만, 강한 보장은 없다.
원본 데이터가 “Well behaved”라고 가정. 어느 정도의 순서 어긋남은 허용하지만, 그 범위는 제한. 현재 구현에서는 최소 10초의 재정렬을 허용하며, 이 범위를 벗어난 타임스탬프를 가진 데이터는 지연 데이터(late data)로 간주. 이 10초=추정 대역(estimation band).
기본구독/추적구독을 두고 메세지를 소비하는 기본구독 되에 빠르게 메타데이터만 소비하는 추적구독이 백로그 내 이벤트 타임스탬프의 최솟값을 가져옴.
추적구독이 기본구독보다 충분히 (최소 추정 대역만큼)앞서고 있고, 실제 시간에 가까우면 (백로그가 없으면) 이 추적구독의 데이터를 기반으로 워터마크를 추정함. -&gt; 기본 구독의 가장 오래된 미확인 메시지보다 새로운 게시 타임스탬프를 가진, 또는 추정 대역 너비만큼의 추적 구독에서 읽은 이벤트 타임스탬프 대역을 고려하여, 이 대역 내의 최소 이벤트 시간을 워터마크 값으로 계산.</p>

<p>그림 3-19에서 보면 pt=100인게 ack를 안보낸 것 중 가장 오래 된 메세지. (pub/sub에서는 처리를 하고 나면 ack를 보냄.) ack가 안된거는 전달이 안되었거나, 전달까진 되었으나 처리되지 않은것임. pt=101인거는 게시는 더 나중에 된 것으로 보이나 처리가 완료되어서 ack까지 간 상태인 것.</p>]]></content><author><name>gayuna</name></author><category term="study" /><category term="Streaming Systems" /><summary type="html"><![CDATA[Percentile watermarks]]></summary></entry><entry><title type="html">Streaming Systems - Chapter 2</title><link href="https://gayuna.github.io/study/streaming-system-chapter-2/" rel="alternate" type="text/html" title="Streaming Systems - Chapter 2" /><published>2025-05-08T00:00:00+09:00</published><updated>2025-05-08T00:00:00+09:00</updated><id>https://gayuna.github.io/study/streaming-system-chapter-2</id><content type="html" xml:base="https://gayuna.github.io/study/streaming-system-chapter-2/"><![CDATA[<h4 id="when-early--on-time--late-tiggers-ftw">When: Early / On-Time / Late Tiggers FTW!</h4>

<p>repeated update trigger + completeness/watermark trigger를 조합해서 사용하는 것이 이상적이다. -&gt; Bean은  데이터가 도착하는 시점에 따라 다르게 triggering 하는 방법을 가짐 -&gt; Early / On-Time / Late Tigger</p>

<ul>
  <li>Early Trigger
    <ul>
      <li>워터마크가 도달하기 전~watermark가 윈도우를 통과할 때까지 주기적으로 중간 결과를 내보냄.</li>
      <li>watermark가 너무 느릴 수 있다는 단점을 보완함.</li>
    </ul>
  </li>
  <li>On-Time Trigger
    <ul>
      <li>워터마크가 해당 윈도우에 도달했을 때, 즉 정시에 트리거 됨.</li>
      <li>시스템이 판단하기에 윈도우 데이터가 대부분 도착했다고 보는 시점.</li>
    </ul>
  </li>
  <li>Late Trigger
    <ul>
      <li>워터마크 이후에 도착한 늦은 데이터에 대해 다시 트리거.</li>
    </ul>
  </li>
</ul>

<pre><code class="language-Java">PCollection&lt;KV&lt;Team, Integer&gt;&gt; totals = input
  .apply(Window.into(FixedWindows.of(TWO_MINUTES))
               .triggering(AfterWatermark()
			     .withEarlyFirings(AlignedDelay(ONE_MINUTE))  // 1분마다 주기적으로 내보냄
			     .withLateFirings(AfterCount(1))))  // 지연된 데이터가 1개 도착할 때마다 trigger 됨
  .apply(Sum.integersPerKey());

/*
2분: window의 크기를 '이벤트 시간' 기준으로 정의 &gt; 결과를 2분 단위로 나누어 보겠다
1분: event trigger가 발동하는 주기를 '처리 시간' 기준으로 정의 &gt; 결과를 1분마다 중간집계 하겠다.
*/
</code></pre>

<p><a href="https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9781491983867/files/assets/stsy_0211.mp4">2-11 영상</a></p>

<p>2-9 대비 변화:</p>
<ul>
  <li>두번째 window[12:02,12:04)의 ‘watermark too slow’ 케이스에서 1분에 한번씩 주기적으로 업데이트 됨.</li>
  <li>첫번째 window[12:00, 12:02)의 ‘heuristic watermarks too fast’ 케이스에서 9 데이터가 나타났을 때 이를 반영함.</li>
  <li>2-10 대비 perfect watermark와 heuristic watermark의 차이가 줄어든 것을 볼 수 있음.</li>
</ul>

<p>heuristic watermark에서는 늦게 들어오는 데이터를 위해 일정 시간동안 window의 상태를 가지고 있어야 함. -&gt; allowed lateness 필요.</p>

<h4 id="when-allowed-lateness-ie-garbage-collection">When: Allowed Lateness (i.e., Garbage Collection)</h4>

<p>이상적으로는 위의 예시처럼 이전 window에 대한 상태 정보를 모두 저장하는 것이 좋겠으나, 현실적으로 끊임 없는 데이터를 처리한다면 이를 영원히 저장하는 것은 힘듦. -&gt; window의 수명을 정해줘야 함. : allowed lateness의 horizon을 지정.</p>

<p>현재시각: 12:10
watermark: 12:00 (event time이 12:00 인 것은 도착했을 것이라 가정)
allowed lateness: 5min (5분간은 더 허용해줌.)
(watermark를 기존 데이터의 표본평균, allowed lateness을 정규화 했을 때 원하는 %의 표준편차 범위 내로 하지 않을까 하는 생각)</p>

<pre><code class="language-Java">PCollection&lt;KV&lt;Team, Integer&gt;&gt; totals = input
  .apply(Window.into(FixedWindows.of(TWO_MINUTES))
               .triggering(
                 AfterWatermark()
                   .withEarlyFirings(AlignedDelay(ONE_MINUTE))
                   .withLateFirings(AfterCount(1))
               .withAllowedLateness(ONE_MINUTE))  // 1분의 lateness도 allow
 .apply(Sum.integersPerKey());
</code></pre>

<p><a href="https://learning.oreilly.com/api/v2/epubs/urn:orm:book:9781491983867/files/assets/stsy_0212.mp4">2-12 영상</a></p>

<p>[12:00, 12:02)의 lateness horizon이 12:03 임을 볼 수 있음.
processing time이 12:06일 때 watermark가 12:02에 도달.
그럼 12:06에 시스템은 12:02까지의 데이터는 다 들어왔을 것이라고 가정.
그런데 allow lateness가 1분임. -&gt; watermark가 12:03이 될 때 까지 도착한 data는 처리해주기로 함.
6 데이터는 watermark가 12:03이 되기 전에 도착함 -&gt; 처리
9 데이터는 watermark가 12:03이 되고 나서 도착함 -&gt; drop</p>

<h4 id="how-accumulation">How: Accumulation</h4>

<ul>
  <li>Discarding: 새로운 pane이 만들어 질 때마다 이전 값은 버림. downstream에서 별도로 처리를 할 때 유용.</li>
  <li>Accumulating: 새로운 pane이 만들어 질 때마다 이전 값을 누적.</li>
  <li>Acumulating and retracting: 새로운 pane이 만들어질 때마다 이전 값을 누적함과 동시에 빼는데 사용할 이전 값을 같이 보냄. “이전에 X라고 했는데 사실 Y야.” downstream에서 데이터를 다시 그루핑하거나 dynamic window를 사용할 때(새로운 값이 하나 이상의 이전 window를 대신할 때) 유용함.</li>
</ul>

<table>
  <thead>
    <tr>
      <th>Pane</th>
      <th>Discarding</th>
      <th>Accumulating</th>
      <th>Accumulating &amp; Retracting</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Pane 1: inputs=[3]</td>
      <td>3</td>
      <td>3</td>
      <td>3</td>
    </tr>
    <tr>
      <td>Pane 2: inputs=[8, 1]</td>
      <td>9</td>
      <td>12</td>
      <td>12, –3</td>
    </tr>
    <tr>
      <td>Value of final normal pane</td>
      <td>9</td>
      <td>12</td>
      <td>12</td>
    </tr>
    <tr>
      <td>Sum of all panes</td>
      <td>12</td>
      <td>15</td>
      <td>12</td>
    </tr>
  </tbody>
</table>]]></content><author><name>gayuna</name></author><category term="study" /><category term="Streaming Systems" /><summary type="html"><![CDATA[When: Early / On-Time / Late Tiggers FTW!]]></summary></entry><entry><title type="html">Think Better, Solve Smarter: Personalized AI Feedback for Math Education</title><link href="https://gayuna.github.io/eventrecap/kaggle-google-ai-capstone/" rel="alternate" type="text/html" title="Think Better, Solve Smarter: Personalized AI Feedback for Math Education" /><published>2025-04-16T00:00:00+09:00</published><updated>2025-04-16T00:00:00+09:00</updated><id>https://gayuna.github.io/eventrecap/kaggle-google-ai-capstone</id><content type="html" xml:base="https://gayuna.github.io/eventrecap/kaggle-google-ai-capstone/"><![CDATA[<h2 id="1-why-we-started-this-project">1. Why We Started This Project</h2>

<p>In South Korea, high school students take several official mock exams between March and November each year.
These exams are not just practice tests—they closely resemble the actual College Scholastic Ability Test (CSAT),
which plays a pivotal role in college admissions. Because they are created by national or regional education authorities,
these mock tests serve as crucial checkpoints for students to evaluate their progress and adjust their study strategy.</p>

<p>However, even after these critical assessments, students are typically left with only their scores and answer sheets.
There is no structured feedback to help them understand why they got a question wrong or how they can improve.</p>

<p>In this Kaggle Capstone project, we set out to build an AI-powered system that analyzes students’ thinking processes,
delivers personalized feedback, and recommends similar problems that align with the reasoning they need to strengthen.</p>

<p>We hope this system can empower students to take charge of their own learning direction—and ultimately,
help reduce educational inequality by making reflective feedback more accessible.</p>

<hr />

<h2 id="2-problem-statement">2. Problem Statement</h2>
<ul>
  <li>After an exam, students’ thought processes are lost—only correct or incorrect answers remain.</li>
  <li>There is no structured or quantifiable way to analyze reasoning patterns or weaknesses by topic.</li>
  <li>When students ask, “What kind of problems should I practice next?”, both teachers and students often respond in vague terms.</li>
</ul>

<hr />

<h2 id="3-solution-approach">3. Solution Approach</h2>

<p>We addressed the problem through the following three-stage pipeline:</p>

<ol>
  <li>Input
After completing a mock exam, students record their thought process for each question via a web interface or post-exam survey.
The input includes:
    <ul>
      <li>A written description of the student’s initial reasoning</li>
      <li>Whether the student guessed the answer</li>
      <li>The actual answer submitted (multiple-choice or open-ended)</li>
    </ul>
  </li>
  <li>
    <p>Processing
Using the Gemini API, we evaluate the student’s reasoning against the ideal approach—not just for correctness,
but for conceptual alignment, strategy, and direction.
We also search for semantically similar problems by comparing embeddings of ideal thinking patterns using cosine similarity.</p>
  </li>
  <li>Output
    <ul>
      <li>Personalized feedback per question, including a reasoning score and guidance</li>
      <li>A summary of the student’s performance and reasoning by topic</li>
      <li>Recommended problems that align with the student’s thinking gaps</li>
    </ul>
  </li>
</ol>

<hr />

<h2 id="4-gen-ai-capabilities-used">4. Gen AI Capabilities Used</h2>
<ul>
  <li>Structured Output / JSON: Consistent, machine-readable responses from Gemini in structured JSON format</li>
  <li>Few-shot Prompting: Carefully designed examples guide the model to assess reasoning with appropriate generosity</li>
  <li>Generative AI Evaluation: Instead of binary grading, the system evaluates the direction and conceptual accuracy of the student’s thinking</li>
  <li>Text Embedding + Vector Search: Embedding-based semantic search is used to recommend problems that reflect similar thought processes</li>
</ul>

<hr />

<h2 id="5-results">5. Results</h2>
<ul>
  <li>Visualized reasoning scores for each question
<img src="https://github.com/user-attachments/assets/cada221d-7428-45f2-8338-7b5fa2e992d3" alt="Image" /></li>
  <li>Aggregated reasoning scores and accuracy by chapter
<img src="https://github.com/user-attachments/assets/82408b04-b178-44e0-8e29-393f7c7cbe6e" alt="Image" /></li>
  <li>Highlighted weaknesses in high-priority concepts</li>
  <li>Semantically recommended follow-up problems based on student thinking gaps
<img src="https://github.com/user-attachments/assets/4ac8ba83-8e32-4734-805c-d11ee5ea8cf0" alt="Image" /></li>
</ul>

<hr />

<h2 id="6-conclusion--future-work">6. Conclusion &amp; Future Work</h2>

<p>This project was not just about “solving the questions you got wrong.”
It began with a more fundamental question:
“How can we help students improve the way they think?”</p>

<p>In the future, we plan to:</p>
<ul>
  <li>Deploy the system using real student data from our learning platform</li>
  <li>Automate feedback generation with LLM-powered scoring and recommendations</li>
  <li>Develop dashboards that visualize student thinking patterns and conceptual growth over time</li>
</ul>]]></content><author><name>gayuna</name></author><category term="EventRecap" /><category term="AI" /><category term="후기" /><category term="English" /><summary type="html"><![CDATA[1. Why We Started This Project]]></summary></entry><entry><title type="html">트위터(현 X)가 진짜로 타임라인을 구성하는 방식</title><link href="https://gayuna.github.io/system%20design/twitter-design-in-real/" rel="alternate" type="text/html" title="트위터(현 X)가 진짜로 타임라인을 구성하는 방식" /><published>2025-04-11T00:00:00+09:00</published><updated>2025-04-11T00:00:00+09:00</updated><id>https://gayuna.github.io/system%20design/twitter-design-in-real</id><content type="html" xml:base="https://gayuna.github.io/system%20design/twitter-design-in-real/"><![CDATA[<p>시스템 디자인을 공부하다보면 SNS에 글을 발행하고 이를 사람들의 타임라인에 뿌려주는 방법이 자주 등장합니다. 여러가지 기술이 언급되곤 하지만, 실제로 어떻게 구현되었는지 궁금하시진 않았나요? 이 글에서는 2020년에 발행된 <a href="https://blog.x.com/engineering/en_us/topics/infrastructure/2020/reducing-search-indexing-latency-to-one-second">Reducing search indexing latency to one second</a>, 2023년에 발행된 <a href="https://blog.x.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm">Twitter’s Recommendation Algorithm</a> 두 글의 내용을 따라가보면서 내가 올린 글을 친구가 타임라인에서 보게 되기까지 사용된 기술들을 살펴보겠습니다.</p>

<p>사용자가 포스트를 발행하면 이는 ingestion pipeline으로 전달되어 indexing이 일어납니다. 이 때 indexing이 빠르게 되어야 사용자는 발행된 포스트를 바로 확인할 수 있습니다. 이를 최대한 빠르게 하는데는 두가지 제약사항이 있었다고 합니다. 첫번째로는 사용자가 발행한 포스터의 내용이 모두 같은 속도/시점에 사용가능한게 아니라는 점입니다, 예를들어 단축된 url을 포함하고 있다면, 그 url의 원본 주소와 그 내용도 인덱싱을 해야 정확도가 올라갈 것입니다. 두번째는 포스트의 수신 순서가 생성 시간 순서가 아니라는 점입니다. 타임라인은 시간 역순으로 정렬되어있어야 하기 때문에 순서를 맞추기 위해 일정 시간동안 버퍼에 쌓아두었다가 정렬해서 indexing을 했다고 합니다. 이때문에 필연적으로 지연이 생겼던 것이죠. 이 문제들을 해결하고 빠르게 indexing을 하기 위해서 1.비동기 색인, 2.Skip list를 이용한 색인 두가지를 도입했다고 합니다.</p>

<h4 id="색인indexing-찍먹하기">색인(Indexing) 찍먹하기</h4>

<p>이 색인을 할 때 사용되는 것이 Lucene이라는 라이브러리 입니다. full-text search를 위한 Java 라이브러리로, Elastic Search가 내부적으로 이를 사용하고 있다고 합니다. Lucene은 문서에서 단어를 찾아낼 수 있도록 하는 역색인(inverted index) 구조를 제공합니다. 이 역색인은 <code class="language-plaintext highlighter-rouge">단어: [단어를 포함하고 있는 document의 id list]</code>를 할 수 있게 합니다. 예를들어</p>

<p>document 1:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>서울특별시발 토론토행
5월 5일 (월)
편도 · 성인 1명

오전 9:35 – 오전 9:55
대한항공 · 직항 · ICN–YYZ
₩1,137k
</code></pre></div></div>

<p>document 2:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>서울특별시발 토론토행
5월 5일 (월)
편도 · 성인 1명

오후 7:05 – 오후 7:30
에어캐나다 · 직항 · ICN–YYZ
₩1,228k
</code></pre></div></div>

<p>두개의 문서가 있다고 치면, 우리의 역색인은</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>서울특별시: [1, 2]
토론토: [1, 2]
대한항공: [1]
에어캐나다: [1]
</code></pre></div></div>

<p>이런식으로 만들어집니다. 나중에 만약 유저가 ‘토론토’로 검색을 한다면 이 역색인을 보고 1,2번 문서를 빠르게 리턴해줄 수 있습니다. 그리고 이를 시간 역순으로 유지하기 위해 트위터는 작성 시각을 사용해서 문서 ID(이후 docId)를 만들었습니다. 27비트는 글이 작성된 시각을 최대값에서 뺀 값으로 만들고, 나머지 4비트는 동일한 시간에 작성된 포스팅을 구분하기 위한 카운터값으로 사용합니다. 식으로 작성한다면 <code class="language-plaintext highlighter-rouge">docId = (max_possible_timestamp - current_timestamp) &lt;&lt; 4 | counter</code>과 같이 표현됩니다. 27비트로만 시간을 표현하기 때문에 상위 정보값이 사라져서 긴 기간을 저장한다면 충돌이 일어나겠지만, Lucene 자체가 segment별로 나눠서 관리하고 각 segment 안에서만 docId가 고유하면 되기 때문에 상관이 없습니다.</p>

<p>이제 역색인에 대해 파악했으니 저 역색인이 docId를 어떤식으로 저장하는지 알아야합니다. 그리고 이 구조의 개선이 2020년 블로그 포스트에서 말하는 색인에 15초가 걸리던걸 1초로 개선할 수 있었던 주요 포인트입니다.</p>

<h4 id="skip-list의-도입">Skip List의 도입</h4>

<p><img src="https://cdn.cms-twdigitalassets.com/content/dam/blog-twitter/engineering/en_us/infrastructure/2020/reducingsearchlatency/unrolled_linked.png.img.fullhd.medium.png" alt="Image" /></p>

<p><code class="language-plaintext highlighter-rouge">서울특별시: [1,2]</code> 예시에서 <code class="language-plaintext highlighter-rouge">[1,2]</code> 처럼 docId를 저장하고 있는 것을 Lucene에서는 <code class="language-plaintext highlighter-rouge">posting list</code>라고 부릅니다. 트위터에서는 2020년 이전에는 docId를 <code class="language-plaintext highlighter-rouge">Unrolled Linked List</code>로 저장했다고 합니다. 이는 Linked List 중에서도 각 노드가 값 여러 개를 배열로 저장하는 구조입니다. 새로운 값이 항상 Head에 저장되는 방식으로 매우 빠르게(O(1)) 새 노드를 추가할 수 있어 최신 포스팅을 먼저 보여주는 타임라인에 적합하다고 볼 수 있습니다. 하지만 중간에 삽입하는데는 비교적 비용이 많이 들기 때문에, 포스팅이 순서대로 오지 않는다면 적합하지 않은 자료구조였습니다.</p>

<p><img src="https://cdn.cms-twdigitalassets.com/content/dam/blog-twitter/engineering/en_us/infrastructure/2020/reducingsearchlatency/skiplist_index_pointers.png.img.fullhd.medium.png" alt="Image" /></p>

<p>이를 해결하기 위해 <code class="language-plaintext highlighter-rouge">Skip list</code>를 도입합니다. Skip List는 O(log n) 삽입/검색이 가능하고 정렬된 데이터에 유연한 장점을 가지고 있습니다. Lucene차원에서도 이 Skip list를 활용하고 있다고 합니다. Skip list는 여러 레벨의 Link List를 관리하는데, 가장 낮은 레벨에는 모든 경우에 삽입을 하고, 높은 레벨일수록 일부 값만 저장을 합니다. 그리고 탐색할 때는 상위 레벨에서부터 빠르게 탐색할 수 있습니다. 트위터에서는 이 Skip list를 도입할 때 이전 탐색 경로를 저장해 반복 탐색 최적화한다든가, primitive array를 사용해 JVM에 오버헤드를 최소화 하는 등의 최적화 작업도 함께 진행했다고 합니다.</p>

<h4 id="순서대로-받진-않지만-원자성은-유지하고-싶어">순서대로 받진 않지만 원자성은 유지하고 싶어</h4>

<p>Skip list를 도입하면서 중간에 노드를 쉽게 삽입할 수 있게 되었습니다. 그런데 이제 원자성을 유지할 수 없다는 문제가 생기게 되었습니다.검색에서 원자성이란 하나의 문서는 모든 인덱싱이 완료되거나 아얘 들어가지 않아야 한다는 것을 뜻합니다. 예를들어 아까의 예시에서 document 2가 인덱싱이 절반만 된 상태에서 원자성이 보장되지 않고 유저가 ‘서울특별시 토론토 -에어캐나다’로 검색했다고 합시다. 유저의 의도는 서울특별시, 토론토에 동시에 인덱싱이 되어 있으면서 에어캐나다에 인덱싱되지는 않은 결과 값을 원할 것입니다. 근데 만약 우리의 document 2의 인덱싱이 진행중이라 서울특별시, 토론토에는 2가 추가되었는데 에어캐나다에는 2가 추가되지 않았다면 유저는 두가지 document를 다 보게 될 것입니다. 유저가 원하던 결과가 아닌 것이지요.</p>

<p>docID가 항상 순서대로 배정됐기 때문에, searcher(검색 쓰레드)는 ‘지금부터 여기까지가 색인 완료된 문서’라는 최소 visible docID를 참조해서 그보다 작은 문서는 아예 건너뛰는 방식을 사용했습니다. 그런데 이제 중간에 삽입이 되니 단순하게 Id만 가지고는 이를 처리할 수 없게 된 것입니다.</p>

<p>이를 해결하기 위해서 docId 대신에 메모리 포인터를 사용했다고 합니다. 리스트라 메모리 위에서 연속적으로 위치하고, 새로운 node는 계속 아래쪽에 추가될 것이기 때문에, <code class="language-plaintext highlighter-rouge">published pointer</code>라는 개념을 도입하여 이 pointer보다 밖에 존재한다면 searcher는 해당 문서를 건너뛰게 했습니다.</p>

<h4 id="비동기로-색인하기">비동기로 색인하기</h4>

<p>이제 중간에 삽입할 준비를 완료했으니, 비동기로 색인을 해볼 수 있습니다. 여기서 비동기로 색인을 하는 것은 외부 서비스에 의존적인 것들입니다. 위에서 소개했듯 포함된 url 같은 것이지요. 혹은 사진이 포함되어있고, 이 사진이 나타내는 keyword도 인덱싱을 하고 싶다면 사진이 첨부된 포스팅도 해당할 것입니다. 여기서는 우리의 document 예시에서 이 티켓을 구매할 수 있는 url이 포함되었다고 가정해보겠습니다:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>서울특별시발 토론토행
5월 5일 (월)
편도 · 성인 1명

오전 9:35 – 오전 9:55
대한항공 · 직항 · ICN–YYZ
₩1,137k
http://some.url/xyz
</code></pre></div></div>

<p>url 말고는 기존과 동일하게 빠르게 인덱싱해서 사람들이 검색할 수 있도록 합니다. 그런데 url을 expand에서 인덱싱하는건 더 시간이 오래 걸립니다. 이는 별도 파이프라인을 타게 됩니다.</p>

<p><img src="https://cdn.cms-twdigitalassets.com/content/dam/blog-twitter/engineering/en_us/infrastructure/2020/reducingsearchlatency/new_ingestion_pipeline.jpg.img.fullhd.medium.jpg" alt="Image" /></p>

<p>이 url에 대한 정보를 얻고 나면 우리의 posting list에 docId를 추가합니다. 이 부분에서 외부 서비스에 대해서는 원자성이 깨지게 되는데요, 약간의 원자성이 깨지더라도 빠르게 할 수 있는 텍스트에 대해서는 빠르게 인덱싱을 하는게 고객에게 더 나은 경험을 제공할 수 있다고 판단한 것으로 보입니다. (We noticed that most Tweet data that we wanted to index was available as soon as the Tweet was created, and our clients often didn’t need the data in the delayed fields to be present in the index right away.)</p>

<h4 id="for-you추천-타임라인-구성하기-데이터의-시작">‘For You’(추천 타임라인) 구성하기: 데이터의 시작</h4>

<p>여기까지가 인덱싱에 관한 블로그 포스팅의 내용이었는데요, 타임라인을 구성하는데 왜이렇게 Indexing 이야기를 오래 했나 생각하셨죠? 그런데 이 인덱싱이 바로 타임라인을 구성하기 위해 필요한 데이터 중 하나이기 때문입니다. 위에서 <code class="language-plaintext highlighter-rouge">토론토: [1, 2]</code> 와 같이 keyword만 언급했는데, 사실 author, title, hashtag 같은 필드도 똑같이 인덱싱을 할 수 있다고 합니다. 예를들어 <code class="language-plaintext highlighter-rouge">title:"The Right Way" AND go</code> 같은 검색문은 제목은 ‘The Right Way’고 내용에 go를 검색하겠다는건데, 여기서 title도 인덱싱이 되고있다는 것이죠. since, until 등의 검색 방법도 다 동일하게 인덱싱이 기저에 깔려있을 것 같네요.</p>

<p><img src="https://cdn.cms-twdigitalassets.com/content/dam/blog-twitter/engineering/en_us/open-source/2023/twitter-recommendation-algorithm/open-algorithm.png.img.fullhd.medium.png" alt="Image" /></p>

<p>수많은 트윗 중 일부만 사용자의 For You 타임라인에 노출되게 되는데, 이를 위해 추천 알고리즘이 필요하고 추천을 하기 전에 추천 할 후보를 추출해야 합니다. For You 타임라인은 50%는 팔로우 중인 사용자, 나머지 50%는 팔로우 밖의 사용자에서 후보를 추출한다고 합니다. 프론트엔드에서 쿼리를 할 때 local social graph를 넘긴다고 하는데, 이 social graph를 기반으로 팔로하고 있는 사람들이 author인 포스트들을 가져올 것이라고 쉽게 짐작할 수 있습니다.</p>

<h4 id="팔로우-하지-않고-있는-사람들로부터-연관성-있는-포스트-찾기">팔로우 하지 않고 있는 사람들로부터 연관성 있는 포스트 찾기</h4>

<p>나머지 50%의 후보는 어떻게 선정할까요? 이에 대해서는 두가지 전략을 취한다고 합니다. 첫번째로는 <code class="language-plaintext highlighter-rouge">내가 팔로우하고 있는 사람이 engagement를 한 포스트는 나도 관심이 있을 것이다</code>라는 가정입니다. 이를 위해 유저가 한 <a href="https://github.com/twitter/the-algorithm/tree/main/user-signal-service">좋아요, 리트윗 등의 행위나 비디오 시청, 프로필 확인 등의 행위를 input으로 받습니다.</a></p>

<p><img src="https://github.com/user-attachments/assets/1e165eae-6e10-4a63-b2ee-db2a5af91ef2" alt="Image" /></p>

<p>그림처럼 A2가 B2를 팔로우하고 있고, B2가 C2를 팔로우 한다면, A2에게 C2를 팔로우 하라고 추천을 보낸다고 합니다. 그런데 팔로우 추천 뿐만 아니라 좋아요나 리트윗 등 모든 행위에 대해서 이런 추천 알고리즘을 적용한다는 것이지요.</p>

<p>두번째 방법은 임베딩을 활용하는 것입니다. 이는 LLM이 유행하면서 많은 분들이 익숙한 방식일거라고 생각합니다. 유저의 관심사와 기존 포스팅을 바탕으로 소속되는 커뮤니티를 만들고, 이 커뮤니티를 기반으로 추천을 하는 것입니다.</p>

<p><img src="https://cdn.cms-twdigitalassets.com/content/dam/blog-twitter/engineering/en_us/open-source/2023/twitter-recommendation-algorithm/simclusters.png.img.fullhd.medium.png" alt="Image" /></p>

<p><a href="https://github.com/twitter/the-algorithm/tree/main/src/scala/com/twitter/simclusters_v2">이 커뮤니티를 만들 때, 먼저 팔로워가 많은 인플루언서들간의 행동을 기반으로 묶어서 그룹을 만들고 다른 유저들을 여기에 배치시키는 방식으로 진행했다고 합니다.</a> 그리고 커뮤니티에 속한 사람들의 반응을 바탕으로 어떤 토픽, 포스트가 이 커뮤니티의 사람들에게 인기가 있는지를 판단해서 추천을 하는 것이죠.</p>

<h4 id="후보-중에서-진짜-당신만을-위한-트윗-선별하기">후보 중에서 진짜 ‘당신만을 위한’ 트윗 선별하기</h4>

<p>위와 같은 과정을 통해 1500개의 후보가 생긴다고 합니다. 그런데 한번 앱을 켰을 때 그 중에는 많아야 수십개만 노출되죠. 이 노출되는 것은 어떻게 선정할까요? 단순히 모두에게 인기가 많은 것이 선정되는 것이 아니라, ‘내가 관심을 표하고 행동을 할 확률이 높은’ 쪽을 선정한다고 합니다. 여기에는 좋아요, 리트윗, 답글은 물론, ‘첨부된 영상을 50% 이상 볼 확률’, ‘프로필을 확인할 확률’ 같은 것도 포함됩니다. 반대로 부정적인 행위(관심 없음을 표할 것 같다거나, 신고를 할 것 같다거나)를 할 확률도 계산해서 고려합니다. 이를 수행하는 머신 러닝 모델인 <a href="https://github.com/twitter/the-algorithm-ml/tree/main/projects/home/recap#heavy-ranker">Heavy Ranker</a>를 공개하고 있습니다.</p>

<p>이렇게 순위가 선정된 포스트등을 대상으로 다시 조정을 거치게 됩니다. (Heuristic &amp; Filter) 예를들어, 내가 뮤트를 해둔 사람의 글은 빠져야 합니다. 특정 유저의 트윗만 너무 많이 노출되지 않도록 갯수를 조정하기도 합니다. 여기까지 해서 선정된 트윗만이 프론트단으로 전송되고, 프론트는 광고, 팔로우 추천과 같은 컨텐츠와 이를 섞어서 우리의 화면에 보여주게 됩니다.</p>

<p><img src="https://cdn.cms-twdigitalassets.com/content/dam/blog-twitter/engineering/en_us/open-source/2023/twitter-recommendation-algorithm/phone-frame-padded.png.img.fullhd.medium.png" alt="Image" /></p>

<h4 id="정리">정리</h4>

<p>여기까지 트위터에서 포스트가 작성되고, 인덱싱되어 나중에 타임라인에 구성되기 까지의 흐름을 따라가보았습니다. 여기까지 소개한 것은 일부 뿐으로, 트위터에서 훨씬 많은 종류의 관계 데이터들을 사용하는 것을 알계되었습니다. 예를들어 같은 임베딩이어도 관심사/ 커뮤니티를 기반으로 out-of-network 추천에만 주로 쓰이는 SimClusters와 추천, 검색, 광고 등에 좀 더 범용으로 쓰이는 TwHIN가 있거나 하는 식입니다. 자료구조에서도 Lucene이 skip list를 사용함에도 불구하고 과거에는 삽입에 O(1)이 걸린다는 이점 때문에 Unrolled Linked List를 사용했다가 비동기를 위해 skip list로 이전한 것 같다는 인상을 받았습니다. 모든 기술이 그렇듯이 완벽한 한가지가 있다기 보다는 목적에 맞게, 적재적소에 쓰이는 것이 중요한 것 같습니다.</p>

<h4 id="참고자료">참고자료</h4>

<p><a href="https://blog.x.com/engineering/en_us/topics/infrastructure/2020/reducing-search-indexing-latency-to-one-second">Reducing search indexing latency to one second</a>
<a href="https://medium.com/@digle117/elasticsearch%EC%97%90%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-apache-lucene%EC%9D%98-%EA%B5%AC%EC%A1%B0-%EB%B0%8F-%EA%B0%9C%EB%85%90-22134906c96d">ElasticSearch에서 사용하는 Apache Lucene의 구조 및 개념</a>
<a href="https://lucene.apache.org/core/8_6_2/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Fields">Lucene Package org.apache.lucene.queryparser.classic</a>
<a href="https://alibaba-cloud.medium.com/analysis-of-lucene-basic-concepts-5ff5d8b90a53">Analysis of Lucene — Basic Concepts</a>
<a href="https://blog.x.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm">Twitter’s Recommendation Algorithm</a>
<a href="https://cs.uwaterloo.ca/~jimmylin/publications/Busch_etal_ICDE2012.pdf">Earlybird: Real-Time Search at Twitter</a>
<a href="https://www.ueo-workshop.com/wp-content/uploads/2014/04/sig-alternate.pdf">RealGraph: User Interaction Prediction at Twitter</a>
<a href="https://arxiv.org/pdf/2202.05387">TwHIN: Embedding the Twitter Heterogeneous Information
Network for Personalized Recommendation</a>
<a href="https://github.com/twitter/the-algorithm/tree/main">GitHub - the-algorithm</a>
<a href="https://github.com/twitter/the-algorithm-ml/tree/main">GitHub - the-algorithm-ml</a></p>]]></content><author><name>gayuna</name></author><category term="System Design" /><summary type="html"><![CDATA[시스템 디자인을 공부하다보면 SNS에 글을 발행하고 이를 사람들의 타임라인에 뿌려주는 방법이 자주 등장합니다. 여러가지 기술이 언급되곤 하지만, 실제로 어떻게 구현되었는지 궁금하시진 않았나요? 이 글에서는 2020년에 발행된 Reducing search indexing latency to one second, 2023년에 발행된 Twitter’s Recommendation Algorithm 두 글의 내용을 따라가보면서 내가 올린 글을 친구가 타임라인에서 보게 되기까지 사용된 기술들을 살펴보겠습니다.]]></summary></entry></feed>