<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>alirz-pixel.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 19 Apr 2026 06:33:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>alirz-pixel.log</title>
            <url>https://images.velog.io/images/alirz-pixel/profile/f760b289-21f3-4e0a-90f2-9e899fab2d6c/프로필.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. alirz-pixel.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/alirz-pixel" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[network] 윈도우 SSHD 설정하기]]></title>
            <link>https://velog.io/@alirz-pixel/network-%EC%9C%88%EB%8F%84%EC%9A%B0-SSHD-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@alirz-pixel/network-%EC%9C%88%EB%8F%84%EC%9A%B0-SSHD-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 19 Apr 2026 06:33:02 GMT</pubDate>
            <description><![CDATA[<p>해당 글은 리눅스의 <code>openssh-server</code>를 윈도우에 설치하는 법을 정리한다.</p>
<h3 id="윈도우-openssh-server-설치">윈도우 openssh-server 설치</h3>
<p>먼저,  <code>설정 -&gt; 시스템 -&gt; 선택적 기능 -&gt; 기능 보기</code>를 눌러 창을 띄운다.
이후, &quot;사용 가능한 기능보기&quot; 클릭 후, <code>OpenSSH 서버</code>를 검색하여 다운받는다.</p>
<p align=center><img src="https://velog.velcdn.com/images/alirz-pixel/post/01464553-28d4-4237-bec0-84e508e8a763/image.png" width=83%></p>
<p align=center><img src="https://velog.velcdn.com/images/alirz-pixel/post/3dd3918c-d7b9-43ba-9e32-e077b6fc5fe8/image.png" width=83%></p>
<p align=center><img src="https://velog.velcdn.com/images/alirz-pixel/post/f8dba9a3-f376-4c6b-8686-876b380ebefa/image.png" width=83%></p>


<h3 id="openssh-설정-및-서버-구동">OpenSSH 설정 및 서버 구동</h3>
<p>리눅스에서는 <code>systemctl sshd</code>를 이용하여 <code>sshd</code>를 구동시켰다면, 윈도우는 조금 다르다.
우선, Windows PowerShell을 관리자 권한으로 실행한다.</p>
<pre><code class="language-bash">&gt; Get-WindowsCapability -Online | ? Name -like &#39;*OpenSSH*&#39;</code></pre>
<p>해당 명령어를 입력하면, OpenSSH Client와 OpenSSH server 2개가 뜰 것이고, <code>State: Installed</code>로 표시되어 것이다. 또한, openSSH Client는 다른 Host로의 ssh 연결을 위해 기본으로 깔려있다.
(만약에 server가 <code>NotPresent</code>로 표시된다면 &quot;기능보기&quot;에서 다시 설치를 진행하자)</p>
<p>아래 명령어들은 <code>sshd (ssh-server)</code>를 시작 및 관리를 위한 명령어이다.</p>
<pre><code class="language-bash"># 현재 sshd의 상태 확인 (리눅스의 systemctl status 같은 느낌이라 보면 된다)
&gt; Get-Service sshd

# sshd 서비스 시작
&gt; Start-Service sshd (이후 Get-Service sshd을 하면 running으로 표기될 것이다)

# (선택) 컴퓨터 부팅 시 sshd가 자동 실행이 원할 경우 실행
&gt; Set-Service -Name sshd -StartupType &#39;Automatic&#39;</code></pre>
<p>여기까지 왔다면 이제 다른 host에서 해당 PC로의 ssh 연결이 가능해진 상태가 된다.
Windows는 username 및 host ip를 알기 위해선 다음의 명령어를 입력하면 된다.</p>
<pre><code class="language-bash">&gt; whoami
&lt;hostname&gt;\&lt;username&gt;

&gt; ipconfig

Windows IP 구성

이더넷 어댑터 이더넷:

   연결별 DNS 접미사. . . . :
   링크-로컬 IPv6 주소 . . . . : &lt;IPv6 
   IPv4 주소 . . . . . . . . . : &lt;host_ip&gt;
   서브넷 마스크 . . . . . . . : 255.255.255.0
   기본 게이트웨이 . . . . . . : 192.168.0.1</code></pre>
<p>공유기를 사용 중이라면 DHCP로 할당받은 &#39;사설(로컬) IP&#39;가 출력될 것이다. 
만약, 공유기의 공인 IP가 궁금하다면, 네이버에 &quot;내 아이피 주소 확인&quot;을 검색하면 된다.</p>
<h3 id="reference">reference</h3>
<p><a href="https://search.naver.com/search.naver?ie=UTF-8&amp;sm=whl_hty&amp;query=%EB%82%B4+%EC%95%84%EC%9D%B4%ED%94%BC+%EC%A3%BC%EC%86%8C+%ED%99%95%EC%9D%B8">네이버 - 내 아이피 주소 확인</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DS] SkipList]]></title>
            <link>https://velog.io/@alirz-pixel/DS-SkipList</link>
            <guid>https://velog.io/@alirz-pixel/DS-SkipList</guid>
            <pubDate>Mon, 06 Apr 2026 12:43:34 GMT</pubDate>
            <description><![CDATA[<p>Linked List의 유연한 insert의 장점을 살리면서 느린 search의 시간복잡도를 $O(\log N)$으로 보완한 자료구조인 SkipList를 소개한다.</p>
<hr>
<h1 id="skiplist">SkipList</h1>
<h2 id="node">Node</h2>
<p>Linked List는 각 Node당 <code>next</code> 포인터를 하나만 가진다.
반면, SkipList는 Level이 존재하며, 각 Node당 해당 노드의 최대 level만큼의 <code>next</code> 포인터를 가진다.</p>
<pre><code class="language-cpp">class Node {
public:
    int value;
    int level;
    Node** next;

    Node(int value, int level) : value(value), level(level) {
        next = new Node*[level + 1]();  // () 로 nullptr 초기화
    }

    ~Node() {
        delete[] next;
    }
};</code></pre>
<h3 id="level">Level</h3>
<p>SkipList의 Level 0은 Linked List와 같은 구조로 되어있으며, 해당 노드가 각 상위 레벨에도 있을 확률은 $p$에 의존한다. (일반적으로 $p=\frac{1}{2}$)</p>
<p>이에 따라 상위 레벨로 갈수록 Node가 점차 줄어들며, <code>Binary Search</code>와 비슷한 형태로 <code>Search</code>가 진행되는 구조로 변하게 된다.</p>
<h3 id="head-sentinel-node">Head (Sentinel Node)</h3>
<p>SkipList는 탐색의 시작점이 되는 <code>head</code> 노드를 별도로 유지한다.
<code>head</code> 노드는 유일하게 <code>MAX_LEVEL</code>만큼의 <code>next</code> 포인터를 전부 가지며, 초기에는 모두 <code>nullptr</code>로 초기화된다. 모든 탐색은 <code>head</code>의 최상위 레벨부터 시작한다.</p>
<pre><code>초기 상태:
level 3:  head → NULL
level 2:  head → NULL
level 1:  head → NULL
level 0:  head → NULL</code></pre><br>

<hr>
<h2 id="function">Function</h2>
<h3 id="_search">_Search</h3>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/641f7136-ebaa-4c05-a56b-a8b27c98d458/image.png" alt=""></p>
<pre><code class="language-cpp">bool search_node(int value) {
    Node* cur = head;

    for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
        while (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value &lt;= value) {
            cur = cur-&gt;next[c_level];
            if (cur-&gt;value == value)
                return true;
        }
    }

    return false;
}</code></pre>
<p><code>Search</code> 연산은 최상위 Level부터 시작하여 다음의 조건에 따라 진행된다.</p>
<ol>
<li><code>node-&gt;next[level] is null &amp;&amp; level &gt; 0</code> → <strong>Go to Lower Level</strong></li>
<li><code>node-&gt;next[level]-&gt;value &gt; search_value</code> → <strong>Go to Lower Level</strong></li>
<li><code>node-&gt;next[level]-&gt;value &lt;= search_value</code> → <strong>Go to Next</strong></li>
<li><code>node-&gt;next[level] is null &amp;&amp; level == 0</code> → <strong>return false</strong><ul>
<li>level 0까지 찾았으나, 찾고자 하는 node가 없음</li>
</ul>
</li>
</ol>
<hr>
<h3 id="_insert">_Insert</h3>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/c08e9efc-679e-4106-a937-5dcdf5831ec8/image.png" alt=""></p>
<h4 id="decide_level">[decide_level]</h4>
<pre><code class="language-cpp">int decide_level() {
    uniform_int_distribution&lt;uint32_t&gt; dist(0, UINT32_MAX);
    uint32_t random = dist(gen);

    int level = 0;
    while ((random &amp; 1) &amp;&amp; level &lt; MAX_LEVEL - 1) {
        random &gt;&gt;= 1;
        level++;
    }
    return level;
}</code></pre>
<p>각 Node의 level은 insert 하는 시점에서 정해지며 확률 $p$에 의존하도록 구성한다.</p>
<p>일반적으로 $p=\frac{1}{2}$이므로 랜덤 정수 값을 기준으로 이진수의 연속된 1의 개수로 레벨을 정하여 구현한다. 이 방식은 레벨 결정에 필요한 랜덤 생성을 1회 호출로 처리할 수 있어 반복 호출 방식보다 효율적이다.
(물론 $p$에 의존하는 랜덤 함수로 구현해도 된다)</p>
<br>

<h4 id="insert-node">[insert node]</h4>
<pre><code class="language-cpp">void insert(int value) {
    Node** update = new Node*[MAX_LEVEL];
    Node* cur = head;

    // 1. 정합성을 위해 insert할 위치 탐색
    for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
        while (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value &lt; value)
            cur = cur-&gt;next[c_level];

        // 중복 체크
        if (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value == value) {
            delete[] update;
            return;
        }

        update[c_level] = cur;
    }

    // 2. Node의 level 정한 뒤, 각 level에 해당하는 위치에 넣기
    int level = decide_level();
    Node* new_node = new Node(value, level);

    for (int c_level = level; c_level &gt;= 0; c_level--) {
        new_node-&gt;next[c_level] = update[c_level]-&gt;next[c_level];
        update[c_level]-&gt;next[c_level] = new_node;
    }

    delete[] update;
}</code></pre>
<p>Node의 insert는 정합성을 위해 각 level에 대해 먼저 insert할 위치를 탐색한다. (<code>update</code> 배열)</p>
<p>탐색과 동시에 삽입을 진행할 경우, 새로운 노드에 대해 일부 레벨만 연결시킨 불완전한 상태를 만들게 된다. 
이는 탐색 중 자기 자신을 다시 마주치거나, 멀티스레드 환경에서 불완전한 노드가 노출되는 문제로 이어질 수 있다.</p>
<p>따라서 <strong>탐색(update 배열 완성) → 연결(일괄 insert)</strong> 의 두 단계로 분리하여 구조 변경 시점을 최소화한다.</p>
<br>

<h4 id="큰-수의-법칙">[큰 수의 법칙]</h4>
<p>큰 수의 법칙에 의거하여 <code>level k</code>당 노드의 기대 개수는 $\frac{n}{2^k}$이다.
이로 인해 대부분의 경우 $O(\log N)$의 시간복잡도를 보장할 수 있으나, 상위 레벨이 앞쪽에 쏠리는 경우 최악 $O(N)$이 걸릴 수 있다. (편향 이진 트리와 흡사)</p>
<hr>
<h3 id="_delete">_Delete</h3>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/6e77b391-7820-4da4-87b9-86bed6e0b82e/image.png" alt=""></p>
<pre><code class="language-cpp">void delete_node(int value) {
    Node** update = new Node*[MAX_LEVEL]();
    Node* cur = head;

    // 1. 각 level에서 삭제할 node의 직전 node 탐색
    for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
        while (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value &lt; value)
            cur = cur-&gt;next[c_level];

        if (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value == value)
            update[c_level] = cur;
    }

    // 2. 각 level에서 연결 해제 후 메모리 해제
    Node* del_node = nullptr;
    for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
        if (update[c_level]) {
            // del_node 확정
            if (!del_node) del_node = update[c_level]-&gt;next[c_level];
            // 연결 끊기
            update[c_level]-&gt;next[c_level] = del_node-&gt;next[c_level];
        }
    }

    delete[] update;
    if (del_node) delete del_node;
}</code></pre>
<p><code>Delete</code> 연산도 insert와 동일하게 <strong>탐색 → 연결 해제</strong> 의 두 단계로 진행된다.</p>
<p><code>update</code> 배열에 각 level에서 삭제할 노드의 직전 노드를 기록한 뒤,
<code>del_node</code>를 먼저 확정하고 <code>del_node-&gt;next[c_level]</code>로 연결을 끊는다.
이는 <code>update[c_level]-&gt;next[c_level]-&gt;next[c_level]</code> 방식으로 접근하는 것보다 안전하다.
연결이 이미 끊긴 포인터를 다시 역참조하는 상황을 방지할 수 있기 때문이다.</p>
<p>마지막으로 <code>del_node</code>를 단 한 번만 <code>delete</code>하여 메모리를 해제한다.
(같은 노드 객체가 여러 레벨에 걸쳐 연결되어 있더라도, 노드 객체는 하나이기 때문이다.)</p>
<br>

<hr>
<h2 id="전체-코드">전체 코드</h2>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;random&gt;

using namespace std;

class Node {
public:
    int value;
    int level;
    Node** next;

    Node(int value, int level) : value(value), level(level) {
        next = new Node*[level + 1]();
    }

    ~Node() {
        delete[] next;
    }
};

class SkipList {
    int MAX_LEVEL;
    Node* head;
    std::mt19937 gen;

public:
    SkipList(int MAX_LEVEL = 4) : MAX_LEVEL(MAX_LEVEL), gen(std::random_device{}()) {
        head = new Node(-INT_MAX, MAX_LEVEL - 1);
    }

    ~SkipList() {
        Node* cur = head-&gt;next[0];
        while (cur) {
            Node* temp = cur-&gt;next[0];
            delete cur;
            cur = temp;
        }
        delete head;
    }

    int decide_level() {
        uniform_int_distribution&lt;uint32_t&gt; dist(0, UINT32_MAX);
        uint32_t random = dist(gen);

        int level = 0;
        while ((random &amp; 1) &amp;&amp; level &lt; MAX_LEVEL - 1) {
            random &gt;&gt;= 1;
            level++;
        }
        return level;
    }

    void insert(int value) {
        Node** update = new Node*[MAX_LEVEL];
        Node* cur = head;

        for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
            while (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value &lt; value)
                cur = cur-&gt;next[c_level];

            if (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value == value) {
                delete[] update;
                return;
            }

            update[c_level] = cur;
        }

        int level = decide_level();
        Node* new_node = new Node(value, level);

        for (int c_level = level; c_level &gt;= 0; c_level--) {
            new_node-&gt;next[c_level] = update[c_level]-&gt;next[c_level];
            update[c_level]-&gt;next[c_level] = new_node;
        }

        delete[] update;
    }

    bool search_node(int value) {
        Node* cur = head;

        for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
            while (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value &lt;= value) {
                cur = cur-&gt;next[c_level];
                if (cur-&gt;value == value)
                    return true;
            }
        }

        return false;
    }

    void delete_node(int value) {
        Node** update = new Node*[MAX_LEVEL]();
        Node* cur = head;

        for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
            while (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value &lt; value)
                cur = cur-&gt;next[c_level];

            if (cur-&gt;next[c_level] &amp;&amp; cur-&gt;next[c_level]-&gt;value == value)
                update[c_level] = cur;
        }

        Node* del_node = nullptr;
        for (int c_level = MAX_LEVEL - 1; c_level &gt;= 0; c_level--) {
            if (update[c_level]) {
                if (!del_node) del_node = update[c_level]-&gt;next[c_level];
                update[c_level]-&gt;next[c_level] = del_node-&gt;next[c_level];
            }
        }

        delete[] update;
        if (del_node) delete del_node;
    }

    void print_each_level() {
        for (int level = MAX_LEVEL - 1; level &gt;= 0; level--) {
            Node* cur = head-&gt;next[level];
            cout &lt;&lt; &quot;level (&quot; &lt;&lt; level &lt;&lt; &quot;): &quot;;
            while (cur) {
                cout &lt;&lt; cur-&gt;value &lt;&lt; &quot; &quot;;
                cur = cur-&gt;next[level];
            }
            cout &lt;&lt; &quot;\n&quot;;
        }
    }
};</code></pre>
<br>

<hr>
<h2 id="사용처">사용처</h2>
<p>병렬 환경에서 B-Tree는 재균형이 일어날 때 관련된 Node들에 Lock을 걸어야 하기 때문에 동시성이 좋지 않다. 이러한 문제로 SingleStore의 rowstore (in-memory DB)에서는 Lock-free SkipList를 사용한다.</p>
<h3 id="lock-free-skiplist">[Lock-free SkipList]</h3>
<p>Lock-free SkipList는 Lock-free Linked List와 동일하게 CAS (Compare-And-Swap) 를 기반으로 동작한다. </p>
<p>삽입 과정에서는 level 0부터 CAS로 먼저 삽입하여 데이터 유실이 발생하지 않도록 진행하고, 각 상위 레벨도 하나씩 CAS를 진행해 나간다.</p>
<p>삭제 과정에서는 여러 레벨의 포인터를 동시에 수정할 수 없으므로 Tagged Pointer를 활용한 논리적 삭제를 먼저 진행한다. 삭제될 노드의 next 포인터 LSB에 마킹을 달아 다른 스레드가 해당 노드를 predecessor로 삼지 못하도록 하며, level 0 마킹 완료 시점을 논리적 삭제 완료로 본다.</p>
<pre><code>Thread A: level 0 마킹 → level 1 마킹 → level 2 마킹 중...

Thread B: 탐색 중 마킹된 노드 발견
          → 기다리지 않고 직접 물리적 연결 해제 (helping)
          → 그 다음 자기 작업 계속 진행</code></pre><p><a href="https://velog.io/@alirz-pixel/tagged-pointer">Tagged Pointer란?</a></p>
<hr>
<br>
Lock-free SkipList 설명까지 모두 포함하면 너무 길어지므로 간략하게 설명했으나, 이후 게시할 예정입니다.

<div todo="수학적 증명 넣어보기 _ 공간 복잡도 계산식">]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] tagged pointer]]></title>
            <link>https://velog.io/@alirz-pixel/tagged-pointer</link>
            <guid>https://velog.io/@alirz-pixel/tagged-pointer</guid>
            <pubDate>Wed, 11 Feb 2026 23:18:05 GMT</pubDate>
            <description><![CDATA[<p><strong>tagged pointer</strong>는 memory alignment를 이용해 포인터의 일부 비트에 추가 정보를 저장하는 메모리 최적화 기법이다.</p>
<p>예를 들어 32bit 환경에서 어떤 객체가 4바이트 단위로 정렬된다면, 그 객체의 주소 하위 2비트는 항상 <code>00</code>이 된다.
이처럼 aligment 등으로 인해 <strong>항상 일정한 값을 가지는 비트를 tag(플래그, 타입 정보 등) 용도로 활용</strong>하는 기법을 <strong>tagged pointer</strong>라고 한다.</p>
<h3 id="example">Example</h3>
<p>리눅스 커널에서 동적으로 할당된 포인터는 최소 4바이트로 정렬되므로, 하위 2비트는 항상 0이다. 리눅스의 <code>XArray</code>는 이 점을 활용해 포인터 값 자체에 작은 태그를 함께 인코딩한다. 사용 가능한 태그 값은 0, 1, 3이며, 2는 내부 엔트리(internal entry)용으로 예약되어 있다.</p>
<ul>
<li><code>xa_tag_pointer()</code>: 포인터의 하위 비트에 태그를 OR 연산으로 삽입한다.</li>
<li><code>xa_untag_pointer()</code>: 하위 2비트를 마스킹해 원래 포인터를 복원한다.</li>
<li><code>xa_pointer_tag()</code>: 하위 2비트만 추출해 저장된 태그 값을 반환한다.</li>
</ul>
<pre><code class="language-c">static inline void *xa_tag_pointer(void *p, unsigned long tag)
{
    return (void *)((unsigned long)p | tag);
}

static inline void *xa_untag_pointer(void *entry)
{
    return (void *)((unsigned long)entry &amp; ~3UL);
}

static inline unsigned int xa_pointer_tag(void *entry)
{
    return (unsigned long)entry &amp; 3UL;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Docker] container 간 완벽한 의존성 보장하기]]></title>
            <link>https://velog.io/@alirz-pixel/docker-healtycheck</link>
            <guid>https://velog.io/@alirz-pixel/docker-healtycheck</guid>
            <pubDate>Thu, 08 Jan 2026 11:02:08 GMT</pubDate>
            <description><![CDATA[<p>docker container 간의 의존성이 있는 경우에는 <strong>실행 순서</strong>와 <strong>준비 상태</strong>를 보장해야 한다.</p>
<blockquote>
<p>단순히 컨테이너를 순서대로 띄우고 싶다면 <code>depends_on</code>
서비스의 연결까지 보장하고 싶다면 <code>healthcheck</code>와 <code>condition: service_healthy</code>를 조합</p>
</blockquote>
<h2 id="depends_on">Depends_on</h2>
<p><code>depends_on</code>은 컨테이너 간의 <strong>실행 순서</strong>를 정의해주는 옵션이다.
특정 컨테이너가 다른 컨테이너보다 <strong>먼저 실행</strong>되어야 할 때 사용한다.</p>
<pre><code class="language-yaml">services:
  redis:
    image: redis:latest

  nginx:
    image: nginx:latest
    depends_on:
      - redis # redis 컨테이너가 먼저 실행된 후 nginx가 실행된다.</code></pre>
<p>docker compose는 의존 대상인 컨테이너의 프로세스가 시작되면 즉시 다음 컨테이너를 실행시킨다.
이 점으로 인해 데이터베이스와 같이 내부 초기화(데이터 로딩, 네트워크 연결 등) 시간이 필요한 프로세스는 <code>Connection Refused</code> 같은 에러가 발생될 수 있다.</p>
<p>(정말 단순하게 실행 순서만 보장한다)</p>
<h2 id="healthcheck">Healthcheck</h2>
<p><code>healthcheck</code>는 컨테이너 내부의 서비스가 실제로 요청을 처리할 준비가 되었는지 확인할 때 사용한다.
이러한 점으로 <code>depends_on</code>과 함께 사용하면, 컨테이너의 상태가 <code>healthy</code>로 변경된 시점을 확인하여 의존성을 완벽하게 보장할 수 있게된다.</p>
<h3 id="예시">예시</h3>
<pre><code class="language-yaml">services:
  redis:
    image: redis:latest
    healthcheck: # 5초마다 redis ping 테스트를 총 5번 진행  (timeout 3초)
      test: [&quot;CMD&quot;, &quot;redis-cli&quot;, &quot;ping&quot;]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 30s

  app:
    build: .
    depends_on:
      redis:
        condition: service_healthy # redis가 healthy 상태가 될 때까지 실행 대기</code></pre>
<h3 id="설정-옵션">설정 옵션</h3>
<p><code>healthcheck</code>에서 사용되는 옵션은 다음과 같다.</p>
<ul>
<li>test: 상태를 결정할 명렁어 (성공 시 0, 실패 시 1 반환)</li>
<li>interval: 검사를 수행할 주기</li>
<li>timeout: 설정한 시간 안에 응답이 없으면 실패로 간주</li>
<li>retries: 연속으로 실패한 경우 <code>unhealthy</code> 상태로 변경</li>
<li>start_period: 컨테이너 부팅 직후, 초기화를 위해 검사 실패를 무시하는 유예 기간
(이 시간동안 <code>retries</code>를 차감하지 않음)</li>
</ul>
<h2 id="condition">condition</h2>
<p><code>depends_on</code>에서 <code>condition</code>으로 설정할 수 있는 옵션은 크게 3가지로 구성되어 있다.</p>
<h3 id="service_started-기본값">service_started (기본값)</h3>
<p>의존하는 컨테이너의 프로세스가 <strong>시작</strong>되면 즉시 다음 컨테이너를 실행
이 옵션은 <code>condition</code>을 별도로 명시하지 않았을 때의 기본 동작이며, 실제 서비스가 준비되었는지 보장하지 않는다.</p>
<h3 id="service_healthy">service_healthy</h3>
<p>의존하는 컨테이너의 <code>healthcheck</code> 결과가 <strong><code>healthy</code> (준비 완료)가 될 때까지 기다린다</strong>.
이 옵션을 지정해야지만 <code>Connection Refused</code>와 같은 문제를 해결할 수 있다.</p>
<h3 id="service_completed_successfully">service_completed_successfully</h3>
<p>의존하는 컨테이너가 실행을 마치고 <strong>정상적으로 종료</strong> (exit code 0)될 때까지 기다린다.
일회성 작업 (초기 데이터 삽입, 빌드 스크립트 등)들이 완전히 끝난 후에 메인 앱을 실행해야 할 때 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[크롤링] GUI 없는 서버에서 Headful 크롤링하기 (Xvfb)]]></title>
            <link>https://velog.io/@alirz-pixel/%ED%81%AC%EB%A1%A4%EB%A7%81-GUI-%EC%97%86%EB%8A%94-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-Headful-%ED%81%AC%EB%A1%A4%EB%A7%81%ED%95%98%EA%B8%B0-Xvfb</link>
            <guid>https://velog.io/@alirz-pixel/%ED%81%AC%EB%A1%A4%EB%A7%81-GUI-%EC%97%86%EB%8A%94-%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-Headful-%ED%81%AC%EB%A1%A4%EB%A7%81%ED%95%98%EA%B8%B0-Xvfb</guid>
            <pubDate>Thu, 18 Dec 2025 20:52:24 GMT</pubDate>
            <description><![CDATA[<p>일부 웹 서비스는 <code>headless</code> 브라우저 접근을 차단한다.
이 때문에 서버 환경에서 크롤링을 진행하려면, GUI가 포함된 무겁고 비용이 높은 윈도우 서버를 사용하는 경우가 많았다.</p>
<p>하지만 리눅스 기반 <strong>CLI 환경에서도 가상 디스플레이를 구성하면, <code>headful</code> 브라우저를 실행해 크롤링을 수행할 수 있다.</strong></p>
<h2 id="xvfb">Xvfb</h2>
<p>Xvfb는 실제 모니터가 없어도 GUI 애플리케이션을 실행할 수 있게 해준다.</p>
<pre><code class="language-bash">apt install xvfb</code></pre>
<h3 id="가상-디스플레이-구축">가상 디스플레이 구축</h3>
<p>Xvfb는 아래의 명령어로 가상 디스플레이를 구축할 수 있다.</p>
<pre><code class="language-bash">xvfb :99 -screen 0 1920x1080x24 &amp;</code></pre>
<p>사용된 옵션 정보는 다음과 같다.</p>
<ul>
<li><code>:99</code> 가상 디스플레이 번호</li>
<li><code>-screen 0</code> 첫 번째 스크린</li>
<li><code>1920x1080x24</code> 해상도 / 색 깊이 (24bit)</li>
<li><code>&amp;</code> 백그라운드 실행</li>
</ul>
<h3 id="display-환경-변수-설정">DISPLAY 환경 변수 설정</h3>
<pre><code class="language-bash">export DISPLAY=99</code></pre>
<p><code>DISPLAY</code> 환경 변수는 리눅스가 사용하는 표준 환경 변수이다.
따라서 위에서 구축한 가상 디스플레이 번호로 환경 변수를 세팅해주면 된다.</p>
<h3 id="gui-애플리케이션-실행">GUI 애플리케이션 실행</h3>
<pre><code class="language-bash">python crwal.py</code></pre>
<p>위에서 설정한 디스플레이 환경 변수로 인해 추가적인 명령어 없이도 크롤러는 가상 디스플레이를 통해 Headful 크롤링을 수행하게 된다.
<br></p>
<hr>
<h3 id="xvfb-run">xvfb-run</h3>
<p><code>xvfb-run</code>는 위 과정을 명령어 한 줄로 처리하는 래퍼이다.</p>
<pre><code>xvfb-run -s &quot;-screen 0 1920x1080x24&quot; python crawl.py</code></pre><ul>
<li><code>-a</code>: 비어있는 display 번호를 탐색하여 실행</li>
<li><code>-s</code>: Xvfb에 전달할 인자
(<code>-screen &lt;screen_number&gt; &lt;resolution&gt;x&lt;depth&gt;</code>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Backend] Cron으로 주기적 작업 스케줄링하기]]></title>
            <link>https://velog.io/@alirz-pixel/Cron-%EC%A3%BC%EA%B8%B0%EC%A0%81-%EC%9E%91%EC%97%85-%EC%8A%A4%EC%BC%80%EC%A4%84</link>
            <guid>https://velog.io/@alirz-pixel/Cron-%EC%A3%BC%EA%B8%B0%EC%A0%81-%EC%9E%91%EC%97%85-%EC%8A%A4%EC%BC%80%EC%A4%84</guid>
            <pubDate>Wed, 17 Dec 2025 16:38:30 GMT</pubDate>
            <description><![CDATA[<p>Cron은 Unix 계열 운영체제에서 제공하는 작업 스케줄러(scheduler)이다.
특정 명령이나 스크립트를 정해진 시간 또는 주기마다 자동으로 실행하기 위해 사용된다.</p>
<h2 id="사용법">사용법</h2>
<p>Cron은 내부적으로  cron daemon (crond)이 백그라운드에서 실행되며,
<code>crontab</code>에 등록된 스케줄을 주기적으로 확인한다.</p>
<pre><code class="language-bash">crontab -l   # 목록 보기
crontab -r   # 등록된 모든 crontab 삭제 
crontab -e   # 현재 사용자 crontab 편집

# ex) 매 5분마다 실행
*/5 * * * * /usr/bin/python3 /app/job.py</code></pre>
<h2 id="문법-정리">문법 정리</h2>
<p><code>crontab</code>은 &quot;언제(스케줄) 무엇을(커맨드)&quot; 실행할지 적는 설정이며, 형식은 아래와 같다.</p>
<pre><code class="language-bash"># ┌─ 분(0-59)
# │ ┌─ 시(0-23)
# │ │ ┌─ 일(1-31)
# │ │ │ ┌─ 월(1-12)
# │ │ │ │ ┌─ 요일(0-7, 0과 7은 일요일)
# │ │ │ │ │
# * * * * *  실행할_명령어</code></pre>
<h2 id="필드값">필드값</h2>
<p>각 시간  필드는 다음 표현을 조합해서 사용이 가능하다.</p>
<h3 id="와일드카드-">와일드카드 *</h3>
<ul>
<li><code>*</code>: 와일드카드는 모든 값을 의미한다.
ex) <code>* * * * *</code> -&gt; 매 분마다 실행</li>
</ul>
<h3 id="단일값">단일값</h3>
<ul>
<li><code>&lt;value&gt;</code>: <code>value</code> 값일 때만 실행
ex) <code>0 5 * * *</code> -&gt; 매일 05:00에 실행</li>
</ul>
<h3 id="범위--">범위 -</h3>
<ul>
<li><code>&lt;value1&gt;-&lt;value2&gt;</code>: <code>value1</code>부터 <code>value2</code>까지 실행
ex) <code>0 5 * * 1-5</code> -&gt; 평일 05:00에 실행</li>
</ul>
<h3 id="리스트-">리스트 ,</h3>
<ul>
<li><code>&lt;value1&gt;,&lt;value2&gt;,..</code> -&gt; 여러 값 지정 가능
ex) <code>0 5 * * 1,3,5</code> -&gt; 월/수/금 05:00에 실행</li>
</ul>
<h3 id="간격">간격</h3>
<ul>
<li><p><code>*/&lt;value&gt;</code>: 매 <code>value</code>마다 실행
ex) <code>*/10 * * * *</code> -&gt; 매 10분마다 실행</p>
</li>
<li><p><code>&lt;value1&gt;/&lt;value2&gt;</code>: <code>value1</code>부터 시작해서 매 <code>value2</code>마다 실행
ex) <code>1-30/5 * * * *</code> -&gt; 매 시간 01,06,11,16,21,26분에 실행 </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Backend] pytest로 테스트 코드 작성하기]]></title>
            <link>https://velog.io/@alirz-pixel/pytest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@alirz-pixel/pytest%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 12 Dec 2025 12:19:43 GMT</pubDate>
            <description><![CDATA[<h2 id="fixture">Fixture</h2>
<p>pytest의 <code>fixture</code>는 테스트에 필요한 준비물(의존성/리소스) 을 함수에 정의해두고 테스트 함수의 인자를 주입하는 형태로 재사용하는 기능이다.
(중복되는 setup/teardown 코드를 줄이기 위해 사용)</p>
<h3 id="인자-주입">인자 주입</h3>
<p>pytest <code>@pytest.fixture</code>로 등록된 함수는 테스트 함수의 파라미터 이름을 기준으로 매칭된다.
즉, pytest는 <code>test_...</code> 함수의 인자 목록을 보고 동일한 이름의 fixture를 찾아 실행한 뒤 반환값(또는 yield 값)을 인자로 주입한다.</p>
<pre><code class="language-py">import pytest

@pytest.fixture
def user():
    return {&quot;id&quot;: 1, &quot;name&quot;: &quot;test_user&quot;}

def test_user_name(user):
    assert user[&quot;name&quot;] == &quot;test_user&quot;</code></pre>
<p>위 코드에서 <code>test_user_name</code>는 <code>user()</code>를 직접 호출하지 않아도 pytest가 <code>test_user_name(user)</code>의 <code>user</code> 파라미터를 보고 <code>user</code> fixture를 실행한 결과를 주입해준다.</p>
<h3 id="fixture-scope">Fixture Scope</h3>
<p>fixture는 scope 단위로 결과를 캐시하고, 같은 scope 안에서는 재사용한다.
(즉, <code>scope=&quot;function&quot;</code>이면 테스트 함수마다 새로 만들고, 그 함수 안에서는 동일 fixture를 여러 번 요청해도 재사용된다.)</p>
<pre><code class="language-py">@pytest.fixture(scope=&quot;function&quot;)
def fx():
    ...</code></pre>
<p>스코프의 종류는 다음과 같다</p>
<ul>
<li><code>function</code>: 테스트 함수마다 새로 생성</li>
<li><code>class</code>: 클래스 단위로 1번 생성</li>
<li><code>module</code>: 파일(모듈) 단위로 1번 생성</li>
<li><code>package</code>: 패키지 단위로 1번 생성</li>
<li><code>session</code>: 전체 테스트 실행 동안 1번 생성</li>
</ul>
<h3 id="teardown">teardown</h3>
<p>teardown은 테스트에서 사용한 리소스를 정리(cleanup)하기 위한 코드이며,
pytest fixture에서 <code>yield</code>를 기준으로 setup/teardown을 한 함수에서 구현할 수 있다.</p>
<pre><code class="language-py">@pytest.fixture(scope=&quot;function&quot;)
def db_session():
    session = SessionLocal() 
    yield session    # setup 완료 후 테스트에 session 전달
    session.close()  # teardown: fixture scope 종료 시 정리

def test_db_connection(db_session):
    ...</code></pre>
<p><code>test_db_connection</code> 함수가 호출되면 다음의 흐름으로 진행된다.</p>
<ol>
<li><code>yield</code> 이전: DB 세션 생성 (setup)</li>
<li><code>yield</code> 지점: <code>session</code>을 pytest가 받아서 테스트 함수 인자로 주입</li>
<li><code>test_db_connection</code> 실행 시작</li>
<li><code>test_db_connection</code> 실행 종료</li>
<li><code>yield</code> 이후: DB 세션 종료 (teardown)</li>
</ol>
<hr>
<h4 id="실제-구현">실제 구현</h4>
<p>pytest는 fixture 함수에 대해 generator 객체를 <code>next()</code>로 재개(resume) 해서 teardown을 수행하는 구조로 되어있다.</p>
<p><a href="https://github.com/pytest-dev/pytest/blob/004a96740fb990c7d6dc39bd305b08852635ec5d/src/_pytest/fixtures.py#L894">pytest 관련 코드 링크</a></p>
<pre><code class="language-py"># pytest/src/_pytest/fixtures.py
def call_fixture_func(
    fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs
) -&gt; FixtureValue:
    ...
    generator = fixturefunc(**kwargs)  # 제너레이터 객체 생성
    fixture_result = next(generator)   # yield 까지 실행: setup 수행

    # scope가 끝날 때 teardown을 호출하는 콜백 등록
    finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, generator)
    request.addfinalizer(finalizer)
    ...
    return fixture_result # yield 값 반환


def _teardown_yield_fixture(fixturefunc, it) -&gt; None:
    try:
        next(it)  # teardown 수행
    except StopIteration:
        pass  # 정상 종료 
    else:     
        fs, lineno = getfslineno(fixturefunc)  # yield가 2개 이상이라면 에러 반환 
        fail(
            f&quot;fixture function has more than one &#39;yield&#39;:\n\n&quot;
            f&quot;{Source(fixturefunc).indent()}\n&quot;
            f&quot;{fs}:{lineno + 1}&quot;,
            pytrace=False,
        )</code></pre>
<p>pytest는 첫 <code>next(generator)</code>로 setup을 수행하고 <code>yield</code> 값을 받아 테스트에 주입한다.
그리고 teardown을 즉시 실행하지 않고 <code>_teardown_yield_fixture(..., generator)</code>를 finalizer로 등록한다.
이후 테스트 scope가 끝나는 시점에 <code>next(generator)</code>를 다시 호출하여 teardown을 진행한다.
(teardown을 위한 fixture을 사용 시, <code>yield</code>를 정확히 한 번만 사용해야 된다는 것도 알 수 있다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[PS] Unique Length-3 Palindromic Subsequences]]></title>
            <link>https://velog.io/@alirz-pixel/PS-Unique-Length-3-Palindromic-Subsequences</link>
            <guid>https://velog.io/@alirz-pixel/PS-Unique-Length-3-Palindromic-Subsequences</guid>
            <pubDate>Fri, 21 Nov 2025 03:24:49 GMT</pubDate>
            <description><![CDATA[<p>(바로 풀이가 나오므로 문제를 풀어보실 분은 주의가 필요합니다.)</p>
<p>해당 문제는 <a href="https://leetcode.com/problems/unique-length-3-palindromic-subsequences/description/?envType=daily-question&amp;envId=2025-11-21">LeetCode의 Daily Question에서 2025.11.21.에 나온 문제</a>입니다.
(난이도: Medium)</p>
<p>참신한 풀이가 있어서 가져왔습니다.</p>
<h2 id="문제">문제</h2>
<p>문제 정의는 간단하게 서술하겠습니다. (자세한 건 위의 링크를 타고 들어가주세요)
문자열 <code>s</code>가 주어졌을 때, Palindrom이 되는 3글자로 된 부분 문자열의 종류 찾는 것입니다.
(중복 문자열 제외)</p>
<blockquote>
<p><strong>예시 데이터</strong>
s = &quot;asafa&quot;
answer = 3
asa  (<u>as</u>af<u>a</u>의 부분 문자열)
asa  (<u>a</u>s<u>a</u>f<u>a</u>의 부분 문자열)
afa  (<u>a</u>sa<u>fa</u>의 부분 문자열)</p>
</blockquote>
<h2 id="풀이">풀이</h2>
<p>문자열의 크기는 최대 $10^5$까지 들어오기 때문에 $O(N^3)$의 풀이로는 해결하지 못한다.
(단순히 반복문 3개를 이용하여 <code>first</code>, <code>middle</code>, <code>last</code>를 찍으며 푸는 방식)</p>
<h3 id="풀이1-내-풀이">풀이1 (내 풀이)</h3>
<p>각 알파벳마다 처음(<code>first</code>)과 끝(<code>last</code>)에 등장하는 인덱스를 기록하고,
$first &lt; i &lt; last$를 만족하는 $s[i] \notin temp_set$을 구하면 된다.</p>
<p>조금 쉽게 표현하면,
3글자로 된 팰린드롬은 1번째 글자와 3번째 글자는 같아야 하며,
그 가운데에 들어가는 알파벳의 종류만 구하면 된다는 뜻이다.</p>
<pre><code class="language-cpp">struct Postion {
    int first;
    int last;
};

class Solution {
public:
    int countPalindromicSubsequence(string s) {
        vector&lt;Postion&gt; letters_position(26, {-1, -1});
        for (int i = 0; i &lt; s.size(); i++) {
            char cur = s[i] - &#39;a&#39;;
            if (letters_position[cur].first == -1)
                letters_position[cur].first = i;
            letters_position[cur].last = i;
        }

        int answer = 0;
        for (auto position : letters_position) {
            int left = position.first;
            int right = position.last;
            if (left == -1)
                continue;

            // 가운데에 들어가는 알파벳 중에 유니크한 것들만 뽑아내기 위해 set을 사용
            unordered_set&lt;char&gt; temp_set;
            for (int i = left + 1; i &lt; right; i++) {
                temp_set.insert(s[i]);
            }
            answer += temp_set.size();
        }

        return answer;
    }
};</code></pre>
<h3 id="풀이2-시간-복잡도-개선">풀이2 (시간 복잡도 개선)</h3>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/39a2d253-a6fe-4071-8155-d6f0f88ef8db/image.png" alt=""></p>
<p>내 방식의 풀이는 $O(N + 26N)$ 이지만, 
지금 서술할 풀이는 $O(N + 27^2 * log(N))$으로 조금 더 최적화 된 풀이이다.
(상수값이 최적화 됨, 단 <code>push_back</code>으로 인한 연산은 배제)</p>
<p>먼저 각 알파벳의 등장 인덱스를 오름차순으로 저장한다.</p>
<p>그리고 팰린드롬의 가운데에 들어갈 알파벳(두 번째 글자)이
첫 번째 글자와 세 번째 글자의 인덱스 사이에 존재하는지를<code>upper_bound</code>를 이용해 검사하는 방식이다.
(<code>upper_bound</code>의 사용으로 상수값이 최적화됨)</p>
<pre><code class="language-cpp">class Solution {
public:
    int countPalindromicSubsequence(string s) {
        vector&lt;vector&lt;int&gt;&gt; pos(26);
        for (int i = 0; i &lt; s.size(); i++) {
            pos[s[i] - &#39;a&#39;].push_back(i);
        }

        int ans = 0;

        // 문자 X 선택
        for (int x = 0; x &lt; 26; x++) {
            auto &amp;vx = pos[x];
            // 팰린드롬이 만들어질 수 없는 케이스
            if (vx.size() &lt; 2) continue;   

            int left = vx.front();
            int right = vx.back();

            // 중간에 들어갈 문자 Y 검사
            for (int y = 0; y &lt; 26; y++) {
                auto &amp;vy = pos[y];

                // index1 보다 큰 첫 위치 찾기
                auto it = upper_bound(vy.begin(), vy.end(), left);

                // 중간 영역에 있는지 확인
                if (it != vy.end() &amp;&amp; *it &lt; right)
                    ans++;
            }
        }

        return ans;
    }
};</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Backend] 검색어 자동 완성 최적화 (Compressed Trie)]]></title>
            <link>https://velog.io/@alirz-pixel/Backend-%EA%B2%80%EC%83%89%EC%96%B4-%EC%9E%90%EB%8F%99-%EC%99%84%EC%84%B1-%EC%B5%9C%EC%A0%81%ED%99%94-Compressed-Trie</link>
            <guid>https://velog.io/@alirz-pixel/Backend-%EA%B2%80%EC%83%89%EC%96%B4-%EC%9E%90%EB%8F%99-%EC%99%84%EC%84%B1-%EC%B5%9C%EC%A0%81%ED%99%94-Compressed-Trie</guid>
            <pubDate>Thu, 20 Nov 2025 12:03:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p><strong>미리보는 최적화 결과</strong>
노드 감소량: 1,410,449개 감소
노드 감소율: 59.32% 감소
메모리 절감량: 0.897 GB
메모리 절감율: 57.5%</p>
</blockquote>
<p>해당 블로그 글에서 사용된 코드는 해당 <a href="https://github.com/alirz-pixel/auto_complelete">Github Repository</a>에서 확인하실 수 있습니다.
(코드까지 포함하면 글이 너무 길어져 Github 링크를 참조하게 된 점 양해 부탁드립니다.)</p>
<h2 id="자료구조-설명">자료구조 설명</h2>
<h3 id="trie">Trie</h3>
<p><code>Compressed Trie</code>를 보기 전에 아주 간단하게 <code>Trie</code>에 대해서 복기하자면 이렇다.</p>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/5ed1929e-8e53-423b-a2ea-46d10f8c1a5c/image.png" alt="">
<a href="https://en.wikipedia.org/wiki/Trie">자료 출처: WIKIPEDIA</a>
<code>Trie</code>는 문자열을 저장하고 효율적으로 탐색하기 위한 트리 형태의 자료구조이다.</p>
<p>각 노드마다 한 글자가 들어가며, 트리를 탐색하여 내려갈 때마다 글자가 조합되어 leaf node에 도달하면 단어가 완성이 되는 구조로 되어있다.
(사진에서 왼쪽으로 쭉 타고 내려가면 t 노드, o 노드를 지나 최종적으로 to 단어가 완성된다.)</p>
<h3 id="compressed-trie">Compressed Trie</h3>
<p><code>Trie</code>의 단점은 한 노드 당 하나의 글자만 저장하기 때문에 메모리 사용량이 크다는 단점이 있다. 이 때 단어들 간의 공통된 prefix는 하나의 노드로 묶고, 나머지 부분들은 분기하여 각각 하나의 노드로 묶는 식으로 구조를 변경하면 전체 노드 수를 크게 줄일 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/437faec0-21b5-4414-92bd-3b0f111789e4/image.png" alt="">
두 단어 <code>APPLE</code>과 <code>APT</code>가 주어졌다고 해보자.
두 단어의 공통된 부분(<code>AP</code>)을  하나의 노드로 합치고, 그 외의 겹치지 않는 글자(<code>PLE</code>, <code>T</code>)들을 각각 하나의 노드로 구성하면 <code>Compressed Trie</code>가 된다.</p>
<p><code>Trie</code>에서는 노드 6개 였던 것이 <code>Compressed Trie</code> 에서는 노드 3개로 압축된 것을 확인 할 수 있다.</p>
<h2 id="최적화-결과">최적화 결과</h2>
<p>데이터셋은 우리말샘 사전에서 가져왔으며, 총 단어는 1,190,944개로 테스트 데이터를 구축하였다.</p>
<h3 id="trie-구축-후">Trie 구축 후</h3>
<ul>
<li>총 노드의 개수: 2378541</li>
<li>노드 insert 시간: 6.08초 </li>
<li>노드 search 시간: 평균 0.002초 </li>
<li>메모리 사용량: 1.559 GB </li>
</ul>
<h3 id="compressed-trie-구축-후">Compressed Trie 구축 후</h3>
<ul>
<li>총 노드의 개수: 968092 </li>
<li>노드 insert 시간: 1분 16초 </li>
<li>노드 search 시간: 평균 0.003초</li>
<li>메모리 사용량: 662 MB </li>
</ul>
<h3 id="정리">정리</h3>
<p>사전 데이터를 토대로 <code>Compressed Trie</code>를 구축했을 때 결과는 다음과 같다</p>
<blockquote>
<p><strong>총 결과</strong>
노드 감소량: 1,410,449개 감소
노드 감소율: 59.32% 감소
메모리 절감량: 0.897 GB
메모리 절감율: 57.5%</p>
</blockquote>
<p>초기 데이터 로드에 12.5배 정도 늘어났지만 search 시간에는 큰 변동이 없으며,
<strong>메모리 절감율 57.5%</strong>면 <code>Compressed Trie</code>의 압축률은 훌륭하다고 볼 수 있다.</p>
<p>Insert 시간이 증가한 이유는 <code>Compressed Trie</code>에서는 단어 삽입 시 노드를 분할(split)하거나 병합(merge)해야 하는 경우가 생기기 때문이다.
(<code>Trie</code>는 Insert시 추가 연산이 없음)</p>
<hr>
<p>블로그 정리를 위해 <code>Compressed Trie</code> 자료를 다시 찾아보다보니 한국어 자료가 부족하다는 걸 느꼈습니다.
그럴리는 없겠지만 반응이 좋다면, <code>Compressed Trie</code>의 Insert, Search, Delete 연산에 대해서 코드와 함께 블로그 정리하겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] UUID 버전별 차이점 및 장단점]]></title>
            <link>https://velog.io/@alirz-pixel/DB-UUID-%EB%B2%84%EC%A0%84%EB%B3%84-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B0%8F-%EC%9E%A5%EB%8B%A8%EC%A0%90-ltz7xgma</link>
            <guid>https://velog.io/@alirz-pixel/DB-UUID-%EB%B2%84%EC%A0%84%EB%B3%84-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EB%B0%8F-%EC%9E%A5%EB%8B%A8%EC%A0%90-ltz7xgma</guid>
            <pubDate>Thu, 20 Nov 2025 08:14:52 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.toomanyafterthoughts.com/uuids-are-bad-for-database-index-performance-uuid7/">썸네일 출처: toomanyafterthoughts 사이트</a></p>
<blockquote>
<p><strong>미리보는 결론</strong>
단일 DB의 환경이라면, 인덱스 크기를 고려하여 AUTO_INCREMENT를 사용하는 것이 좋다.
그러나 보안을 신경써야 하며 여러 노드가 동시에 ID를 생성하는 환경이라면, UUID가 좋다.
또한, UUID 버전 중 순수 랜덤이 필요 없는 경우에는 성능을 고려하여 v6 또는 v7을 사용하는 것이 효율적이다.</p>
</blockquote>
<h2 id="uuid란">UUID란?</h2>
<p>UUID(Universally Unique IDentifier)는 간단하게 겹치지 않는 ID라고 보면 된다.
일반적으로 32개의 16진수(HEX)로 구성되어있으며, 하이폰으로 구분해서 총 36글자로 되어있다.</p>
<pre><code>UUID 예시) 
d6e74418-2081-4256-8c44-d8922d26dece</code></pre><h3 id="uuid-장점">UUID 장점</h3>
<p><code>AUTO_INCREMENT</code>를 사용하는 ID 값과 대비하여 UUID가 가지는 장점이 있다.</p>
<ol>
<li>보안적 이점: 순차 ID(1,2,..)는 쉽게 추측되어 예측이 가능하지만, UUID는 랜덤/비예측적이라 위험도가 낮다.</li>
<li>동시성 확장성 향상: <code>AUTO_INCREMENT</code>는 DB가 시퀀스를 관리해 병목이 생기고 샤딩이 어렵다. UUID는 애플리케이션에서 독립 생성이 가능해 여러 서버나 리전에서 충돌없이 생성 가능하다.</li>
</ol>
<hr>
<h3 id="uuid-버전별-차이점">UUID 버전별 차이점</h3>
<table>
<thead>
<tr>
<th>버전</th>
<th>방식 / 기반</th>
<th>비트 순서</th>
<th>특징</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><strong>v1</strong></td>
<td>시간 기반</td>
<td>timestamp → MAC 주소</td>
<td>시간 기반으로 저장</td>
<td>개인정보 노출 위험, 정확한 시간 순이 아님</td>
</tr>
<tr>
<td><strong>v2</strong></td>
<td>DCE Security</td>
<td>v1 기반 + UID/GID</td>
<td>권한/보안용</td>
<td>거의 사용되지 않음</td>
</tr>
<tr>
<td><strong>v3</strong></td>
<td>이름 기반 + MD5</td>
<td>MD5 해시로 결정적 생성</td>
<td>동일 입력 → 동일 UUID</td>
<td>충돌 가능, 순서 없음</td>
</tr>
<tr>
<td><strong>v4</strong></td>
<td>랜덤</td>
<td>완전 랜덤</td>
<td>충돌 거의 없음, 보안적 안전</td>
<td>순서 없음 → DB 인덱스 비효율</td>
</tr>
<tr>
<td><strong>v5</strong></td>
<td>이름 기반 + SHA1</td>
<td>SHA1 해시로 결정적 생성</td>
<td>동일 입력 → 동일 UUID</td>
<td>순서 없음, SHA1 → 느림</td>
</tr>
<tr>
<td><strong>v6</strong></td>
<td>시간 기반 개선</td>
<td>timestamp → random/node</td>
<td>시간 순 정렬 최적화, MAC 제거</td>
<td>아직 표준 확정 전, 일부 라이브러리 미지원</td>
</tr>
<tr>
<td><strong>v7</strong></td>
<td>Unix timestamp + 랜덤</td>
<td>timestamp → random</td>
<td>최신 표준, 시간 순 정렬 최적화, 보안적 안전</td>
<td>최신 표준 → 일부 라이브러리 미지원</td>
</tr>
</tbody></table>
<h3 id="버전별-사용처">버전별 사용처</h3>
<h4 id="이름-기반">이름 기반</h4>
<p>버전 <code>v3, v5</code>는 입력값에 따라 항상 동일한 UUID가 생성된다.
또한, <code>v5</code>는 MD5 대비 충돌 확률이 낮다.</p>
<p>즉, 입력값에 따른 고정 ID값이 필요한 경우에 <code>v5</code> 사용이 권장된다.</p>
<h4 id="랜덤-기반">랜덤 기반</h4>
<p>버전 <code>v4</code>는 완전히 랜덤하게 생성된다.
따라서 충돌 가능성이 거의 없으며, 보안적으로도 안전하다.</p>
<p>범용적으로 사용되는 버전이며, 유니크한 ID 값이 필요할 때 사용이 권장된다.</p>
<h4 id="시간-기반">시간 기반</h4>
<p>버전 <code>v1, v6, v7</code>은 생성 시, 시간 정보를 포함하므로 순차적으로 생성된다.
덕분에 DB B-Tree 인덱스 삽입 시 성능이 최적화된다.</p>
<p>그 중에서 <code>v1</code>은 완벽한 순차적 정렬이 보장되지 않으며, MAC 주소로 인해 개인정보 노출의 위험이 있어 최근에는 사용되는 기법이 아니라고 한다.
즉, DB의 효율을 생각한다면 <code>v6, v7</code>이 권장된다.</p>
<hr>
<h3 id="근거">근거</h3>
<p>v4는 완전 랜덤이기 때문에 데이터를 삽입할수록 인덱스 조각화가 심해진다. 이는 I/O 비용이 증가하고, 인덱스 조회 성능이 저하된다는 것을 의미한다. 
<img src="https://velog.velcdn.com/images/alirz-pixel/post/37549e06-251a-4fa1-8aaf-383e160c8e0c/image.png" alt="">
<a href="https://www.toomanyafterthoughts.com/uuids-are-bad-for-database-index-performance-uuid7/">사진 출처: toomanyafterthoughts 사이트</a>
위의 자료는 데이터 삽입 대비 디스크 사용량을 나타낸 자료이다. 시간 기반인 v6, v7은 데이터가 많이 삽입되도 디스크 페이지를 순차적으로 사용하므로 디스크 쓰기량이 최소화됨을 확인할 수 있다. (v1도 시간 기반이지만 정확한 시간 순서로 정렬되지 않아 v6, v7에 비해 디스크 사용 최적화가 덜 효율적임도 볼 수 있다.)</p>
<hr>
<p>게시글 작성 순서를 &#39;결론 -&gt; 배경 -&gt; 설명 -&gt; 근거&#39; 순으로 작성해 보았는데, 독자 입장에서 어떤 방식이 더 친화적인지 고민된다..</p>
<h3 id="참고자료">참고자료</h3>
<ul>
<li><a href="https://www.toomanyafterthoughts.com/uuids-are-bad-for-database-index-performance-uuid7/">https://www.toomanyafterthoughts.com/uuids-are-bad-for-database-index-performance-uuid7/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[게시글 이미지 업로드 (Delayed upload)]]></title>
            <link>https://velog.io/@alirz-pixel/delayedupload</link>
            <guid>https://velog.io/@alirz-pixel/delayedupload</guid>
            <pubDate>Fri, 14 Nov 2025 04:36:39 GMT</pubDate>
            <description><![CDATA[<h2 id="이미지-업로드-방식">이미지 업로드 방식</h2>
<p>찾아보고 고민해본 결과 총 3가지 방식으로 이미지 업로드 기능을 구현가능해 보인다.</p>
<ol>
<li>Delayed image upload</li>
<li>임시 이미지 업로드 </li>
<li>이미지 업로드 후 스케줄링 기반 정리</li>
</ol>
<p>해당 글에서는 delayed image upload에 대해서 작성한다.
구현 방식에는 정답이 없으므로 장단점을 고려해 선택하는 것을 추천합니다.</p>
<h2 id="delayed-image-upload">Delayed image upload</h2>
<h3 id="흐름">흐름</h3>
<ol>
<li>사용자가 이미지를 입력</li>
<li>웹에서는 해당 이미지를 BASE64로 보여줌 (서버로 저장 X)</li>
<li>게시글 저장 시,
 3-1. 서버에 이미지들을 업로드한 뒤, URL을 반환 받음
 3-2. BASE64 -&gt; URL 치환 -&gt; 게시글 HTML 저장</li>
</ol>
<h3 id="장점">장점</h3>
<ol>
<li>서버 트래픽 최소화: 게시글 업로드 전까지 트래픽을 사용하지 않음</li>
<li>저장 공간 최소화: 게시글에 저장될 이미지만 서버에 저장됨 (임시 이미지가 저장되지 않음)</li>
</ol>
<h3 id="단점">단점</h3>
<ol>
<li>브라우저 메모리 부담 증가: BASE64는 원본보다 용량이 약 33% 증가 </li>
<li>업로드 시 트래픽 집중: 이미지가 많은 게시글에서는 서버로 한 번에 트래픽이 몰릴 수 있음</li>
</ol>
<br>

<h2 id="구현">구현</h2>
<h3 id="1-이미지-미리보기-및-pendingimage">1) 이미지 미리보기 및 pendingImage</h3>
<p>해당 코드에서는 서버로 업로드 될 이미지를 담는 <code>pendingImages</code>와<code>BASE64</code>로 된 이미지 출력을 메인으로 보면 된다.
또한, 이후 BASE64에서 치환할 file을 트래킹하기 위해 <code>data-index</code>에는 <code>pendingImages</code>의 인덱스도 같이 저장한다.</p>
<pre><code class="language-js">const pendingImages = []; // 서버 업로드 예정 이미지 저장
const previewContainer = document.getElementById(&#39;preview&#39;);
const input = document.getElementById(&#39;imageInput&#39;);

input.addEventListener(&#39;change&#39;, (e) =&gt; {
    const files = Array.from(e.target.files);

    files.forEach((file) =&gt; {
        // 1) pendingImages 배열에 저장
        const index = pendingImages.length;
        pendingImages.push(file);

        // 2) FileReader로 Base64 변환
        const reader = new FileReader();
        reader.onload = function(event) {
            // 3) 이미지 요소 생성 및 미리보기
            const img = document.createElement(&#39;img&#39;);
            img.src = event.target.result; // Base64
            // BASE64에서 치환할 file을 트래킹하기 위한 index 저장
            img.setAttribute(&#39;data-index&#39;, index);

            previewContainer.appendChild(img);
        };
        reader.readAsDataURL(file);
    });
});</code></pre>
<h3 id="2-pendingimages에-저장된-이미지-서버에-저장">2) pendingImages에 저장된 이미지 서버에 저장</h3>
<p>해당 코드에서는 pendingImages에 쌓인 이미지들을 서버에 저장하고,
반환받은 URL로 HTML에 치환하여 게시글을 저장하는 방식이다.
(간결한 코드를 위해 따로 예외처리는 되어있지 않습니다.)</p>
<pre><code class="language-js">// 게시글 제출 시 서버 업로드 + BASE64 → URL 치환
submitBtn.addEventListener(&#39;click&#39;, async () =&gt; {
    // 1) FormData로 서버 업로드
    const formData = new FormData();
    pendingImages.forEach(file =&gt; formData.append(&#39;files&#39;, file));

    // 서버에 이미지 업로드 (예: /api/upload-images)
    const res = await fetch(&#39;/api/upload-images&#39;, {
        method: &#39;POST&#39;,
        body: formData
    });
    const uploadedUrls = await res.json(); // [&quot;image_url_1.png&quot;, &quot;image_url_2.png&quot;, ...]

    // 2) Base64 → 서버 URL 치환
    const imgs = previewContainer.querySelectorAll(&#39;img&#39;);
    imgs.forEach(img =&gt; {
        const index = parseInt(img.getAttribute(&#39;data-index&#39;));
        img.src = uploadedUrls[index];
    });

    // 3) 게시글 내용 생성 (HTML 포함)
    const title = document.getElementById(&#39;title&#39;).value;
    const contentHTML = previewContainer.innerHTML;

    // 4) 게시글 서버 전송
    await fetch(&#39;/api/post&#39;, {
        method: &#39;POST&#39;,
        headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },
        body: JSON.stringify({
            title,
            content: contentHTML
        })
    });

    alert(&#39;게시글 작성 완료!&#39;);
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[FastAPI] 이메일 인증 구현하기]]></title>
            <link>https://velog.io/@alirz-pixel/email-verify</link>
            <guid>https://velog.io/@alirz-pixel/email-verify</guid>
            <pubDate>Mon, 27 Oct 2025 05:31:49 GMT</pubDate>
            <description><![CDATA[<p><code>fastapi_mail</code>은 FastAPI에서 이메일을 보내기 쉽게 만든 라이브러리이다.
이를 사용하여 이메일 인증, 사용자 알람 등등을 구현할 수 있다.</p>
<p>해당 게시글에서는 <code>fastapi_mail</code>을 이용하여 이메일 인증을 구현해볼 것이다.</p>
<h2 id="방식">방식</h2>
<p>이메일 인증의 전체적인 흐름은 다음과 같다.</p>
<blockquote>
<ol>
<li>유저가 회원가입을 진행 → FastAPI가 인증 토큰 생성</li>
<li>FastAPI가 Redis에 토큰 저장 (만료시간 포함)
key: email_verify:{token},  value: user_id</li>
<li>FastAPI가 인증 링크을 포함한 메일 보내기 
인증 링크에는 get parameter로 token 값을 저장한다.</li>
<li>유저가 메일 링크 클릭 → FastAPI가 토큰 검증 → 인증 완료 처리</li>
</ol>
</blockquote>
<p>필자는 사용자 편의성을 고려하여 &quot;인증 링크&quot;를 발송하도록 하였지만, &quot;인증 번호&quot; 방식도 이와 비슷하게 진행하면 된다. </p>
<h3 id="이메일-인증-구현">이메일 인증 구현</h3>
<p>아래는 이해를 돕기 위한 코드로 redis <code>value</code>에 user 정보를 모두 넣어 작성하였다.
실제 프로젝트에서는 DB에 유저 정보를 저장 (<code>email_verified</code>=false)하고, redis value에 <code>user_id</code>를 넣어 메일 인증이 완료되었을 때 <code>email_verified=true</code>를 해주면 된다.
(로그인 과정에서는 <code>email_verified=false</code>면, 로그인 실패 + 메일 인증 요구를 해주면 된다)</p>
<pre><code class="language-py">import json
import secrets
import uvicorn
from typing import Optional
from pydantic import BaseModel, EmailStr

import redis
from fastapi import FastAPI, HTTPException
from fastapi_mail import FastMail, MessageSchema, ConnectionConfig

APP_BASE_URL = &quot;http://127.0.0.1:5000&quot;
VERIFY_TTL_SEC = 60 * 10  # 10분
conf = ConnectionConfig(
    MAIL_USERNAME=&quot;&lt;USERNAME&gt;&quot;,
    MAIL_PASSWORD=&quot;&lt;APP_PASSWORD&gt;&quot;, # Gamil의 경우, App 패스워드를 발급받아서 사용하면 된다.
    MAIL_FROM=&quot;&lt;FROM_EMAIL&gt;&quot;,
    MAIL_SERVER=&quot;smtp.gmail.com&quot;,
    MAIL_PORT=587,
    MAIL_STARTTLS=True,
    MAIL_SSL_TLS=False,
)

app = FastAPI()
r = redis.Redis(host=&quot;127.0.0.1&quot;, port=6379, decode_responses=True)

class User(BaseModel):
    name: Optional[str] = None
    email: EmailStr
    password: str


def redis_verify_key(token: str) -&gt; str:
    return f&quot;email_verify:{token}&quot;


async def send_email(title: str, email: str, context: str):
    message = MessageSchema(
        subject=title,
        recipients=[email],
        body=context,
        subtype=&quot;plain&quot;
    )
    fm = FastMail(conf)
    await fm.send_message(message)


@app.post(&quot;/signup&quot;)
async def signup_submit(user: User):
    # 1) 토큰 생성
    token = secrets.token_urlsafe(32)

    # 2) Redis에 저장
    #    key: email_verify:{token}
    #    value: 회원가입 정보
    payload = user.model_dump_json()
    r.setex(redis_verify_key(token), VERIFY_TTL_SEC, payload)

    # 3) 인증 링크 생성
    verify_link = f&quot;{APP_BASE_URL}/verify-email?token={token}&quot;

    # 4) 메일 발송
    title = &quot;[TEST] 이메일 인증&quot;
    content = f&quot;아래 링크를 클릭해서 이메일 인증을 완료하세요:\n\n{verify_link}\n\n(만료: {VERIFY_TTL_SEC//60}분)&quot;
    await send_email(title, user.email, content)
    return {&quot;ok&quot;: True, &quot;message&quot;: &quot;verification email sent&quot;}


@app.get(&quot;/verify-email&quot;)
async def verify_email(token: str):
    key = redis_verify_key(token)
    verify_value = r.get(key)
    if not verify_value:
        raise HTTPException(status_code=400, detail=&quot;invalid_or_expired_token&quot;)

    r.delete(key)
    user = json.loads(verify_value)
    return {&quot;ok&quot;: True, &quot;verified_email&quot;: user[&quot;email&quot;]}


if __name__ == &#39;__main__&#39;:
    uvicorn.run(app, host=&quot;0.0.0.0&quot;, port=5000)</code></pre>
<h3 id="테스트-코드">테스트 코드</h3>
<p>원래는 회원가입 페이지를 만들어야 하지만, 간단하게 테스트 코드만 작성해두었다.</p>
<pre><code class="language-py">import requests

user = {
    &quot;email&quot;: &quot;zhemfpdlf@gmail.com&quot;,
    &quot;password&quot;: &quot;&lt;PASSWORD&gt;&quot;,
    &quot;name&quot;: &quot;&lt;name&gt;&quot;
}

r = requests.post(&quot;http://127.0.0.1:5000/signup&quot;, json=user)
r.raise_for_status()
print(r.text)</code></pre>
<p>위의 코드를 실행시켰을 때, 다음과 같이 메일이 온다.
<img src="https://velog.velcdn.com/images/alirz-pixel/post/cc2a7b24-6fef-4e93-a9b4-3aed37a28858/image.png" alt=""></p>
<p>또한, 해당 링크를 클릭하면, 메일 인증까지 완료된다.
(인증 완료 페이지도 만들고, login 페이지로 리다이렉션까지 진행하면 완벽할 것이다.)
<img src="https://velog.velcdn.com/images/alirz-pixel/post/0f3ecdcc-5f04-47df-af7e-5d7fc4882a6c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] Composition Event (IME 문제)]]></title>
            <link>https://velog.io/@alirz-pixel/html-Composition-Event-IME-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@alirz-pixel/html-Composition-Event-IME-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 10 Jul 2025 12:05:18 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-배경">문제 배경</h2>
<p>가끔 사이트 이용 중에 한국어 입력 시, 마지막 글자가 2번 입력이 되는 모습을 본 적이 있을 것이다.
(아래 짤은 한국어 입력 후 Enter를 눌렀을 때, 마지막 글자가 한번 더 입력되는 모습)
<img src="https://velog.velcdn.com/images/alirz-pixel/post/08894dab-05df-4c23-8df7-4544b85b56ed/image.gif"/></p>
<h3 id="해결법">해결법</h3>
<p>해당 문제는 조합 중인 글자에서 발생하는 IME 문제이므로 <code>compositionstart</code> (조합시작), <code>compositionend</code> (조합 완성) 이벤트를 이용하여 <strong>&#39;글자 조합 확정 이벤트&#39;를 예외처리</strong> 해주면 된다. 
<img src="https://velog.velcdn.com/images/alirz-pixel/post/5d767f3d-aa4d-477a-bcf0-13102fdf204c/image.png" width=700px /></p>
<p>IME에 대해 좀 더 알아보고 싶다면, 아래의 코드를 실행하여 영어로도 입력해보길 추천
(영어는 조합형 글자가 아니기 때문에 composition evnet가 발생하지 않음)</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;title&gt;IME 이벤트 디버깅&lt;/title&gt;
    &lt;style&gt;
        body {
            font-family: sans-serif;
            padding: 20px;
        }

        #input {
            font-size: 1.2em;
            padding: 8px 16px;
            width: 300px;
        }

        #stack {
            margin-top: 20px;
            border: 1px solid #ccc;
            padding: 10px;
            overflow-y: auto;
            background-color: #f9f9f9;
            font-family: monospace;
        }

        .event-line {
            padding: 4px;
            border-bottom: 1px dotted #ddd;
        }

        #list {
            margin-top: 30px;
            padding-left: 20px;
        }

        #list li {
            font-size: 1.1em;
            margin-bottom: 4px;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;h2&gt;IME 이벤트 디버깅&lt;/h2&gt;

&lt;input type=&quot;text&quot; id=&quot;input&quot;/&gt;
&lt;div id=&quot;stack&quot;&gt;&lt;/div&gt;

&lt;h3&gt;📋 Todo List&lt;/h3&gt;
&lt;ul id=&quot;list&quot;&gt;&lt;/ul&gt;

&lt;script&gt;
    const input = document.getElementById(&quot;input&quot;);
    const stack = document.getElementById(&quot;stack&quot;);
    const list = document.getElementById(&quot;list&quot;);

    function pushToStack(message) {
        const time = new Date().toLocaleTimeString();
        const div = document.createElement(&quot;div&quot;);
        div.className = &quot;event-line&quot;;
        div.textContent = `[${time}] ${message}`;
        stack.prepend(div); // 최신 이벤트를 위로 쌓기
        console.log(`[${time}] ${message}`);
    }

    function addTodo() {
        const value = input.value;
        const li = document.createElement(&quot;li&quot;);
        li.textContent = value;
        list.appendChild(li);
        input.value = &quot;&quot;;
        pushToStack(`[addTodo] &#39;${value}&#39; 추가됨`);
    }

    let isComposing = false;
    input.addEventListener(&quot;compositionstart&quot;, () =&gt; {
        isComposing = true;
        pushToStack(&quot;compositionstart&quot;);
    });

    input.addEventListener(&quot;compositionend&quot;, (e) =&gt; {
        isComposing = false;
        pushToStack(`compositionend → data: &#39;${e.data}&#39;`);
    });

    input.addEventListener(&quot;compositionupdate&quot;, (e) =&gt; {
        pushToStack(`compositionupdate → data: &#39;${e.data}&#39;`);
    })


    input.addEventListener(&quot;keydown&quot;, (e) =&gt; {
        if (e.key === &quot;Enter&quot;) {
            if (isComposing === true) {
                pushToStack(`OS가 조합 중인 글자를 확정 짓는 이벤트는 예외처리: isComposing = ${isComposing}`);
                return;
            }
            pushToStack(`조합 완료된 글자 (${input.value})만 Todo에 반영: isComposing = ${isComposing}`);
            addTodo();
        }
    });
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<hr>
<h2 id="문제-원인-자세히-살펴보기">문제 원인 자세히 살펴보기</h2>
<h3 id="ime-문제">IME 문제</h3>
<p>한글 (정확히는 조합형 글자)을 입력하고 Enter 키를 누르면 <strong>마지막 글자가 한 번 더 입력되는 문제</strong>가 발생한다.</p>
<p><code>Enter</code> 키로 입력을 완료하는 기능 구현 시, 글자 조합 과정과 맞물려 2가지의 이벤트가 수행된다.</p>
<ol>
<li><strong>Keydown 이벤트</strong>: js로 등록해둔 keydown 이벤트가 수행된다.</li>
<li><strong>글자 조합 확정 이벤트</strong>: IME는 확정된 문자열을 커밋(commit)한다.</li>
</ol>
<p>&quot;사용자가 입력한 문자열&quot;과 &quot;확정된 문자열 커밋&quot;이라는 두 이벤트로 인해 위 예시처럼 (한국어) + (어)가 발생하게 된 것이다.</p>
<h3 id="조합-중인-글자">조합 중인 글자?</h3>
<p>한국어는 자모 (字母)를 조합해서 하나의 글자로 만드는 &quot;조합형 글자&quot;이다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>입력</th>
<th>자모 구성</th>
<th>조합 중 글자 상태</th>
<th>IME 이벤트</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>ㅎ 입력</td>
<td>초성: ㅇ</td>
<td>ㅎ (조합 시작)</td>
<td><code>compositionstart + update</code></td>
</tr>
<tr>
<td>2</td>
<td>ㅏ 입력</td>
<td>중성: ㅏ</td>
<td>하 (ㅎ + ㅏ)</td>
<td><code>compositionupdate</code></td>
</tr>
<tr>
<td>3</td>
<td>ㄴ 입력</td>
<td>종성: ㄴ</td>
<td>한 (하 + ㄴ)</td>
<td><code>compositionupdate</code></td>
</tr>
<tr>
<td>4-1</td>
<td>ㄱ 입력</td>
<td>초성: ㄱ</td>
<td>한 (조합 완성)</td>
<td><code>compositionend</code></td>
</tr>
<tr>
<td>4-2</td>
<td>-----</td>
<td>초성: ㄱ</td>
<td>ㄱ (조합 시작)</td>
<td><code>compositionstart + update</code></td>
</tr>
</tbody></table>
<p>조합 중인 글자는 사용자의 키 입력 순서대로 compositionstart -&gt; compositionupdate -&gt; compositionend 순으로 이벤트가 발생한다.</p>
<p>그리고 조합된 글자는 일반적으로 텍스트 커서(Text Cursor)가 다음 글자로 넘어가는 순간이라고 이해하면 된다 (위 표에서 4-1,4-2). 위 상황 외에도 <code>Enter</code>키를 누르면 OS가 내부적으로 해당 문자를 확정 짓는다 (이로인해 IME 문제가 발생하게 된다).</p>
<h4 id="실제-웹에서-확인해보기">실제 웹에서 확인해보기</h4>
<p><img src="https://velog.velcdn.com/images/alirz-pixel/post/88aaf6e2-9637-4a3b-8b3d-9f0a7cabed7b/image.gif" alt=""></p>
<h2 id="reference">reference</h2>
<ul>
<li><strong>MDN Web Docs</strong>: CompositionEvent 설명 (<a href="https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent">Link</a>)</li>
<li><strong>중국어 IME 조합</strong>: 조합 과정에서 onChange가 발생하는 문제 (<a href="https://github.com/facebook/react/issues/3926">Link</a>)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[DB] GraphQL]]></title>
            <link>https://velog.io/@alirz-pixel/GraphQL</link>
            <guid>https://velog.io/@alirz-pixel/GraphQL</guid>
            <pubDate>Tue, 29 Apr 2025 04:06:54 GMT</pubDate>
            <description><![CDATA[<p>GraphQL은 필요한 데이터만 가져오겠다는 접근 방식을 채택한 데이터 질의 언어이다.</p>
<h2 id="기존-rest-api의-문제점">기존 REST API의 문제점</h2>
<h3 id="오버패치-over-fetching">오버패치 (over-fetching)</h3>
<p>오버 패치는 영어 문장 그대로 불필요한 데이터까지 같이 가져오는 문제를 말한다.
어느 부분에서는 <code>name</code>, <code>email</code> 값만 필요하더라도 <code>/users/1/</code>을 요청하면 &quot;주소, 생일, 가입일&quot; 등의 불필요한 데이터까지 가져오게 된다.</p>
<h4 id="문제점">문제점</h4>
<ol>
<li>네트워크 낭비: 불필요한 데이터까지 전송하기 때문에 데이터 양이 커지게 되고, 이로 인해 네트워크 성능이 느려질 가능성이 있다</li>
<li>메모리 낭비: 전송 받은 데이터는 우선 메모리에 저장되기 때문에 메모리를 차지한다.</li>
<li>클라이언트 리소스 낭비: 받아온 데이터 중 필요한 데이터를 추출하는 과정에서의 렌더링 비용 (CPU 연산, 메모리 사용) 증가 가능성이 있다.</li>
</ol>
<p>받아온 데이터 중 필요한 것만 추려야 해서 메모리 사용량 증가, 렌더링 비용 증가 가능성이 있음</p>
<h3 id="언더패치-under-fetching">언더패치 (under-fetching)</h3>
<p>언더 패치도 문장 그대로 여러 종류의 데이터가 필요한데, <code>/users/1</code>, <code>/users/1/friends</code>처럼 여러 번 요청 해야 필요한 정보를 다 채울 수 있는 문제를 말한다.</p>
<h4 id="문제점-1">문제점</h4>
<ol>
<li>다수의 HTTP 요청: 하나의 기능을 위해 여러 번의 API 요청을 보내야 한다. 이로 인해 지연 (latency) 발생 가능성이 있음.</li>
<li>요청 순서 의존성 발생: API 요청 시, 요청 순서에 의존된다면 비동기 흐름의 복잡성이 증가될 수 있다. <ul>
<li><code>/posts/123 (123번 게시글 가져오기)</code></li>
<li><code>/users/45 (45번 작성자 가져오기)</code> 
123번 게시글이 로드되어야 45번 작성자 정보를 사용할 수 있는 상황과 같이 순서 의존이 발생할 수 있음</li>
</ul>
</li>
</ol>
<h2 id="graph-ql">Graph QL</h2>
<h3 id="쿼리">쿼리</h3>
<p>기존 RestAPI 방식과 다르게 GraphQL은 하나의 엔드포인트에서 요청마다 원하는 필드만 선택적으로 받아올 수 있다.</p>
<pre><code class="language-graphql">qeury {  # User 타입에서 name만 가져옴
    user {
        name
    }
}

query {  # User 타입에서 name과 friends 필드의 name 필드를 가져옴
    user {
        name
          friends {
            name
        }
    }
}</code></pre>
<h3 id="리졸버">리졸버</h3>
<p>리졸버는 GraphQL 스키마의 각 필드에 데이터를 채워주는 함수이다.
(default resolver가 사전에 정의되어 있으며, 가공이 필요한 경우엔 커스텀 리졸버를 제작하여 사용 가능)</p>
<pre><code class="language-graphql">// 커스텀 리졸버
const resolvers = {
    Query: {
        numberSix() {
            return 6;
        }
    }
}

// 쿼리
query {
    numberSix
}

// 반환 결과
{
    &quot;data&quot;: {
        &quot;numberSix&quot;: 6
    }
}</code></pre>
<p>위의 예시는 <code>numberSix</code> 필드에 대해 요청이 들어온 경우, 커스텀 리졸버를 통해 6을 반환받도록 한 예시이다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Chrome extension] 새 창에 데이터 보내기 ]]></title>
            <link>https://velog.io/@alirz-pixel/43rth</link>
            <guid>https://velog.io/@alirz-pixel/43rth</guid>
            <pubDate>Thu, 03 Apr 2025 00:33:42 GMT</pubDate>
            <description><![CDATA[<h3 id="알고리즘">알고리즘</h3>
<p>새 창에 데이터를 보내는 로직은 아래와 같다 (get 파라미터, chrome storage 사용 X)</p>
<ol>
<li>새 창을 만든 후, 해당 탭의 id값을 가져온다. (<code>chrome.tabs.create</code>)</li>
<li>1번에서 가져온 탭의 id 값을 통해 웹 페이지가 로드 되었는지 확인한다. (<code>chrome.tabs.onUpdated</code>)</li>
<li>웹 페이지가 로드 되었다면, 해당 탭으로 데이터를 보낸다. (<code>chrome.tabs.sendMessage</code>)</li>
<li><code>onMessage</code> 함수를 통해 데이터를 받아온다. (<code>chrome.runtime.onMessage</code>)</li>
</ol>
<blockquote>
<p>2번은 꼭 필요한 과정은 아니지만, 타이밍 이슈가 있을 수 있기 때문에 해주면 안정성이 좋아진다.</p>
</blockquote>
<h3 id="예시-코드">예시 코드</h3>
<pre><code class="language-js">// background.js

// 1. 새창 열기
chrome.tabs.create({ url: &quot;https://www.naver.com/&quot; }, (tab) =&gt; {  
    const tabId = tab.id;  

    // 2. 웹 페이지 로드 확인
    chrome.tabs.onUpdated.addListener(function listener(updatedTabId, info) {  
        if (updatedTabId === tabId &amp;&amp; info.status === &quot;complete&quot;) {  
            // 3. 데이터 전송
            chrome.tabs.sendMessage(tabId, {  
                data: &quot;데이터 전송&quot;,
                payload: &quot;payload 전송&quot;,  
            });  

            // 업데이트를 계속 감지하지 않도록 방지
            chrome.tabs.onUpdated.removeListener(listener);  
        }  
    });  
});</code></pre>
<pre><code class="language-js">// content script

// 4. 데이터 받아오기
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) =&gt; {  
    console.log(msg.data)
    console.log(msg.payload)
});</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[linux] 실험 스크립트]]></title>
            <link>https://velog.io/@alirz-pixel/linux-%EC%8B%A4%ED%97%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</link>
            <guid>https://velog.io/@alirz-pixel/linux-%EC%8B%A4%ED%97%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8</guid>
            <pubDate>Mon, 02 Dec 2024 16:34:01 GMT</pubDate>
            <description><![CDATA[<p>benchmark 툴에서 실험 결과에 들어갈 정보를 주지 않을 때, dstat에 찍히는 bandwidth를 측정해주는 스크립트이다.</p>
<pre><code>#!/bin/bash

SESSION_NAME=&quot;&lt;session_name&gt;&quot;

compare_version=&quot;&lt;kernel_version&gt;&quot;
kernel_version=$(uname -r)

result_path=&quot;dstat&quot;
file_name=&quot;&lt;result_filename&gt;&quot;

generate_unique_filename() {
        local base_name=&quot;$1&quot;
        local extension=&quot;csv&quot;
        local count=1

        local new_filename=&quot;${result_path}/${base_name}_${count}.${extension}&quot;
        while [ -e &quot;$new_filename&quot; ]; do
                count=$((count + 1))
                new_filename=&quot;${result_path}/${base_name}_${count}.${extension}&quot;
        done

        echo $new_filename
}

if [[ &quot;$kernel_version&quot; == *&quot;$compare_version&quot;* ]]; then
        result_path+=&quot;/proposed&quot;
else
        result_path+=&quot;/original&quot;
fi
mkdir -p ${result_path}

unique_filename=$(generate_unique_filename $file_name)
echo $unique_filename


cur_path=$(pwd)
tmux new-session -d -s $SESSION_NAME &quot;dstat --output ${cur_path}/${unique_filename}&quot;

# test code
# do something
sleep 2

tmux send-keys -t $SESSION_NAME C-c
sleep 1

tmux kill-session -t $SESSION_NAME # 혹시 모르니 한번 더 종료</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[linux] docker]]></title>
            <link>https://velog.io/@alirz-pixel/linux-docker</link>
            <guid>https://velog.io/@alirz-pixel/linux-docker</guid>
            <pubDate>Wed, 06 Nov 2024 15:48:42 GMT</pubDate>
            <description><![CDATA[<p>실험 환경 구축을 위해 docker container 환경을 만들어야 하는 일이 생겼다.</p>
<h2 id="command">command</h2>
<h3 id="환경-구축">환경 구축</h3>
<p>환경 구축을 위한 command 목록은 아래와 같다.</p>
<pre><code class="language-bash"># docker ubuntu 22.04 만들기
$ docker image pull ubuntu:22.04

# docker 실행
$ docker run -dit --name &lt;container_name&gt; &lt;docker_image&gt;

# 파일 전송
$ docker cp &lt;host_path&gt; &lt;container_name&gt;:/&lt;container_path&gt;

# container를 이미지로 만들기
$ docker commit &lt;container_name&gt; &lt;repository:tag&gt;

# 외부에서 명령어 실행
$ docker exec -it &lt;container_name&gt; &lt;command&gt;</code></pre>
<h3 id="container-상태-정보-보기">container 상태 정보 보기</h3>
<pre><code class="language-bash"># docker image 확인
$ docker images

# container 확인
$ docker ps -a

# container 내부 접속
$ docker exec -it /bin/bash</code></pre>
<h2 id="script">script</h2>
<h3 id="create_containerssh">create_containers.sh</h3>
<p>아래의 스크립트는 docker container를 원하는 개수만큼 만들어주는 스크립트이다.</p>
<p>use case는 다음과 같다.</p>
<ol>
<li>container가 이미 만들어져 있으며 running 중인 경우, 무시</li>
<li>container가 이미 만들어져 있으나 running이 아닌 경우, container start</li>
<li>conatiner가 만들어져 있지 않은 경우, <code>&lt;docker_image&gt;</code>에 대해 container를 만든 뒤 start</li>
</ol>
<p><code>usgae: ./create_containers &lt;container_num&gt;</code></p>
<pre><code class="language-bash">#!/bin/bash

# 제작할 container 개수
MAX_INDEX=$1
IMAGE_NAME=&quot;&lt;docker_image&gt;&quot;  # 사용하려는 Docker 이미지 이름 설정

declare -A pid_array # 백그라운드 프로세스의 pid를 담는 배열

for i in $(seq 1 &quot;$MAX_INDEX&quot;); do
    CONTAINER_NAME=&quot;&lt;container_name&gt;$i&quot; # $i는 지우지 말 것

    # 컨테이너가 존재하는지 확인
    CONTAINER_ID=$(docker ps -a --filter &quot;name=^/${CONTAINER_NAME}$&quot; --format &quot;{{.ID}}&quot;)

    if [ -n &quot;$CONTAINER_ID&quot; ]; then
        if [ &quot;$(docker ps --filter &quot;name=^/${CONTAINER_NAME}$&quot; --format &quot;{{.ID}}&quot;)&quot; ]; then
            # case1. 컨테이너가 이미 실행 중
            echo &quot;컨테이너 $CONTAINER_NAME 가 이미 실행 중입니다.&quot;
        else
               # case2. 컨테이너가 존재하지만 실행 중이지 않다면 시작
            echo &quot;컨테이너 $CONTAINER_NAME 이 존재하지만 실행 중이 아닙니다. 실행 시키겠습니다.&quot;
            docker start &quot;$CONTAINER_NAME&quot; &amp;                                                                                                                                                                                                                                             
        fi  
    else
        # case3. 컨테이너가 존재하지 않으므로 새로 생성
        # background 프로세스로 진행되게 하여 제작 속도를 높임
        echo &quot;컨테이너 $CONTAINER_NAME 이 없습니다. 새로 생성합니다.&quot;
        docker run -dit --name &quot;$CONTAINER_NAME&quot; &quot;$IMAGE_NAME&quot; /bin/bash &amp;
        pid_array[$CONTAINER_NAME]=$!
    fi  
done

for CONTAINER_NAME in &quot;${!pid_array[@]}&quot;; do
        PID=${pid_array[$CONTAINER_NAME]}

        wait &quot;$PID&quot;
        echo &quot;컨테이너 $CONTAINER_NAME의 생성이 완료되었습니다. &quot;
        docker start &quot;$CONTAINER_NAME&quot; &amp;
done

wait</code></pre>
<h3 id="exec_containerssh">exec_containers.sh</h3>
<p>해당 script는 만들어진 container들에 대해 명령을 순차적으로 보내는 script이다.</p>
<p>tmux는 dstat 결과를 저장하기 위해 추가한 것으로 dstat 결과가 필요하지 않은 경우엔 tmux 관련된 부분을 지우면 된다.</p>
<p><code>usage: ./exec_containers.sh &lt;container_num&gt;</code></p>
<pre><code class="language-bash">#!/bin/bash

SESSION_NAME=&quot;docker_stress&quot;
RUN_CONTAINER=$1

echo &quot;== ${RUN_CONTAINER} container ==&quot;

        tmux new-session -d -s $SESSION_NAME &quot;dstat -D total --output &lt;dstat_result_path&gt;&quot;
        for i in $(seq 1 $RUN_CONTAINER); do
                docker exec &lt;container_name&gt;$i &lt;some_work&gt; &amp; 
        done
        wait

        tmux send-keys -t $SESSION_NAME C-c 
        sleep 1

        tmux kill-session -t $SESSION_NAME
echo &quot;========= [done] ==========&quot;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] 스펙터 버그]]></title>
            <link>https://velog.io/@alirz-pixel/CS-%EC%8A%A4%ED%8E%99%ED%84%B0-%EB%B2%84%EA%B7%B8</link>
            <guid>https://velog.io/@alirz-pixel/CS-%EC%8A%A4%ED%8E%99%ED%84%B0-%EB%B2%84%EA%B7%B8</guid>
            <pubDate>Fri, 16 Aug 2024 05:35:17 GMT</pubDate>
            <description><![CDATA[<h2 id="등장-배경">등장 배경</h2>
<p><strong>분기 예측</strong> (branch prediction)은 CPU의 성능을 높이기 위해 등장한 것으로 조건문의 결과를 예측하는 식으로 진행된다.</p>
<p>이해를 돕기위해 아래와 같은 코드가 있다고 해보자</p>
<pre><code class="language-cpp">if (condition) {
    A() // condition의 결과가 True일 경우, A() 실행
} else {
    B() // condition의 결과가 False일 경우, B() 실행
}</code></pre>
<p>이러한 코드에 대해 분기 예측이 적용된 CPU는 <code>condition</code>의 결과를 예측하여 <code>A()</code> 또는 <code>B()</code>를 실행시킨다. 만약 예측이 맞으면 성능은 크게 향상될 것이다. </p>
<p align=center>
<img src='https://velog.velcdn.com/images/alirz-pixel/post/f000d3e9-6510-4373-b26e-6b032a7f8bd1/image.png' width=75%>
</p>

<p>그러나 예측이 틀린 경우엔 해당 <strong>결과를 되돌리고 (undo), 올바른 경로를 다시 실행</strong>하게 된다.</p>
<p align=center>
<img src='https://velog.velcdn.com/images/alirz-pixel/post/1b9bfba2-9ed9-43cb-82f1-5af60441822b/image.png' width=75%>
</p>

<h2 id="스펙터-버그">스펙터 버그</h2>
<p>스펙터 버그는 이러한 분기 예측의 취약점을 이용한 공격이다. 등장 배경에서 예측이 틀린 경우엔 결과를 되돌린다고 했었지만, 실제 코드가 실행된 데이터는 캐시에 남게 된다.</p>
<p>이에 대해 공격자는 아래와 같은 방식으로 취약점 공격을 할 수 있다. (스펙터 버그)</p>
<blockquote>
<p>스펙터 취약점 공격 방법</p>
</blockquote>
<ol>
<li>조건문의 결과를 참으로 예측하도록 유도한다. (조건이 참인 상황을 여러 번 반복한다.)</li>
<li>공격자가 알아내고자 하는 값에 접근한다. </li>
<li>분기 예측으로 해당 코드가 실행이 되고, 조건이 false라면 명령이 취소된다.</li>
<li>명령이 취소가 되었을지라도 그 데이터는 캐시에 남게 된다.</li>
<li>공격자는 여러 데이터에 접근하면서 접근 시간을 확인한다. 그 중 접근 시간이 짧은 데이터는 캐시에 저장되어 있다는 뜻이므로 공격자가 알아내고자 한 데이터임을 알 수 있다.</li>
</ol>
<h4 id="reference">reference</h4>
<ul>
<li><a href="https://parksb.github.io/article/31.html">https://parksb.github.io/article/31.html</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[CS] False sharing]]></title>
            <link>https://velog.io/@alirz-pixel/false-cache-line-sharing-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@alirz-pixel/false-cache-line-sharing-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 01 Aug 2024 21:37:20 GMT</pubDate>
            <description><![CDATA[<h2 id="cpu-cache">CPU cache</h2>
<p>CPU가 매번 메모리에 접근하여 데이터를 가져오는 것은 느리기 때문에 <strong>지역성</strong>의 특성을 이용해 CPU 캐시에 데이터를 추가로 가져온다.
(추가로 가져오는 데이터의 단위는 <strong>캐시 라인</strong>이며, 대부분의 CPU는 64 bytes로 구성되어 있다.)</p>
<p>이와 관련한 그림으로 아래의 그림을 많이 보았을 것이다.</p>
<p alian="center">
<img src="https://velog.velcdn.com/images/alirz-pixel/post/85043173-b7c7-473b-a8c2-df5ec400b4fa/image.png" width=50%></p>

<p>그러나 병렬 처리 환경에서는 CPU cache으로 인해 오히려 성능이 낮아질 때도 있다. (<code>false sharing</code> 문제)</p>
<h3 id="false-sharing">False sharing</h3>
<p><code>False sharing</code>은 &quot;거짓 공유&quot;의 문제로 실제로 쓰레드간 공유되지 않은 데이터이지만, <strong>동일한 캐시 라인의 데이터</strong>를 마치 공유하는 것처럼 인식하여 성능 저하를 일으키는 문제를 말한다.</p>
<p>이에 대하여 병렬처리 환경에서 <code>false sharing</code> 문제가 어떻게 발생되는지 그림으로 확인하면 이해가 쉽다.</p>
<p alian="center">
<img src="https://velog.velcdn.com/images/alirz-pixel/post/190596ed-0267-46aa-b831-1f8a07a2e63b/image.png" width=85%>

<img src="https://velog.velcdn.com/images/alirz-pixel/post/4399d1da-a251-4ee5-bad6-5bcab25f18b7/image.png" width=85%>

<img src="https://velog.velcdn.com/images/alirz-pixel/post/edd6451c-5e38-4b2b-a71d-f9e0f061684d/image.png" width=85%>
</p>


<p><code>False sharing</code> 문제는 메모리가 연속적 (정확히는 한 캐시 라인에 두 데이터가 포함) 일 때, 나타나는 문제이기 때문에 <strong>배열</strong>, <strong>구조체</strong> 등에서 자주 발생된다.</p>
<h4 id="false-sharing으로-인한-성능-저하-테스트">False sharing으로 인한 성능 저하 테스트</h4>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;thread&gt;
#include &lt;chrono&gt;

#define FALSE_SHARING
// #define RNAD_TEST
// #define __DEBUG

#define TEST_CNT 100
#define ITER_CNT 1000000

#ifdef FALSE_SHARING
struct Info {
    volatile int num1;
    volatile int num2;
} info;
volatile long long num3 = 0;
#else
struct Info {
    volatile long long num1 = 0;
    alignas(64) volatile long long num2 = 0; // cache line의 범위를 벗어나도록 64 bytes 만큼 padding을 추가함 (align)
} info;
alignas(64) volatile long long num3 = 0;
#endif

void fun1() {
    for (long long i = 0; i &lt; ITER_CNT / 2; i++)
#ifndef RNAD_TEST
        info.num1 += 1;
#else
        info.num1 += rand();
#endif
}

void fun2() {
    for (long long i = 0; i &lt; ITER_CNT / 2; i++)
#ifndef RNAD_TEST
        info.num2 += 1;
#else
        info.num2 += rand();
#endif
}

void fun3() {
    for (long long i = 0; i &lt; ITER_CNT; i++)
#ifndef RNAD_TEST
        num3 += 1;
#else
        num3 += rand();
#endif
}

std::chrono::duration&lt;double&gt; test(bool is_multi_test) {
    auto beginTime = std::chrono::high_resolution_clock::now();

    if (is_multi_test) {
        std::thread t1(fun1);
        std::thread t2(fun2);

        t1.join(); t2.join();
    }
    else {
        fun3(); //Single Thread 실행
    }

    auto endTime = std::chrono::high_resolution_clock::now();
    std::chrono::duration&lt;double&gt; resultTime = endTime - beginTime;

#ifdef __DEBUG
    std::cout &lt;&lt; &quot;-------[single]-------\n&quot;;
    std::cout &lt;&lt; &quot;total value: &quot; &lt;&lt; num3 &lt;&lt; std::endl;
    std::cout &lt;&lt; &quot;excution time: &quot; &lt;&lt; resultTime.count() &lt;&lt; std::endl;
#endif
    return resultTime;
}

int main() {
    double single_total = 0, multi_total = 0;

    for (int i = 0; i &lt; TEST_CNT; i++) {
        info.num1 = 0;
        info.num2 = 0;
        num3 = 0;
        single_total += test(false).count() / TEST_CNT;
        multi_total += test(true).count() / TEST_CNT;
#ifdef __DEBUG
        std::cout &lt;&lt; &quot;---------------------\n&quot;;
        std::cout &lt;&lt; std::endl;;
#endif
    }

    std::cout &lt;&lt; &quot;single test excution time: &quot; &lt;&lt; single_total &lt;&lt; std::endl;
    std::cout &lt;&lt; &quot;multi test excution time:  &quot; &lt;&lt; multi_total &lt;&lt; std::endl;
}</code></pre>
<pre><code>=== [False sharing test] ===
single test excution time: 0.00161374
multi test excution time:  0.00260395 (false sharing으로 인해 병렬 처리 환경임에도 더 오래걸림)

=== [ False sharing Mitigated ] ===
single test excution time: 0.00177046 (false sharing 문제를 해결하면 single thread가 더 느림을 확인 할 수 있음)
multi test excution time:  0.00119301</code></pre><h3 id="example">example</h3>
<h4 id="in-linux">in linux</h4>
<p>리눅스에서도 <code>false sharing</code> 문제를 회피하기 위한 코드들을 확인할 수 있다.</p>
<h5 id="in-structure">in structure</h5>
<pre><code class="language-c">struct page_counter {
    /*
     * Make sure &#39;usage&#39; does not share cacheline with any other field. The
     * memcg-&gt;memory.usage is a hot member of struct mem_cgroup.
     */
    atomic_long_t usage;
    CACHELINE_PADDING(_pad1_);

    /* effective memory.min and memory.min usage tracking */
    unsigned long emin;
    atomic_long_t min_usage;
    atomic_long_t children_min_usage;

    /* effective memory.low and memory.low usage tracking */
    unsigned long elow;
    atomic_long_t low_usage;
    atomic_long_t children_low_usage;

    unsigned long watermark;
    unsigned long failcnt;

    /* Keep all the read most fields in a separete cacheline. */
    CACHELINE_PADDING(_pad2_);

    unsigned long min;
    unsigned long low;
    unsigned long high;
    unsigned long max;
    struct page_counter *parent;
} ____cacheline_internodealigned_in_smp;</code></pre>
<p>위는 리눅스에서 사용되는 <code>page_counter</code> 구조체이다. 주석을 보면 <code>page_counter</code> 구조체의 <code>usage</code> 멤버 변수가 hot member (자주 사용되는 변수)라고 설명되어 있다. 이 <code>usage</code> 변수가 다른 멤버 변수들과 같은 캐시 라인에 위치하게 되면, <strong>false sharing</strong> 문제로 인해 성능이 심각하게 저하될 수 있다. 그래서 이를 방지하기 위해 <code>CACHELINE_PADDING</code>을 추가하여 다른 멤버 변수들과 같은 캐시라인에 들어오지 않도록 하고 있다.</p>
<h2 id="reference">reference</h2>
<ul>
<li><a href="https://smallake.kr/?p=2271">https://smallake.kr/?p=2271</a></li>
<li><a href="https://elky84.github.io/2010/08/19/false_sharing/">https://elky84.github.io/2010/08/19/false_sharing/</a></li>
<li><a href="https://ypangtrouble.tistory.com/entry/%EC%BA%90%EC%8B%9C-%EC%9D%BC%EA%B4%80%EC%84%B1%EA%B3%BC-%EA%B1%B0%EC%A7%93-%EA%B3%B5%EC%9C%A0">https://ypangtrouble.tistory.com/entry/%EC%BA%90%EC%8B%9C-%EC%9D%BC%EA%B4%80%EC%84%B1%EA%B3%BC-%EA%B1%B0%EC%A7%93-%EA%B3%B5%EC%9C%A0</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SWEA] 최대 상금 (정렬)]]></title>
            <link>https://velog.io/@alirz-pixel/SWEA-%EC%B5%9C%EB%8C%80-%EC%83%81%EA%B8%88-%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@alirz-pixel/SWEA-%EC%B5%9C%EB%8C%80-%EC%83%81%EA%B8%88-%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Sun, 02 Jun 2024 22:17:44 GMT</pubDate>
            <description><![CDATA[<p><a href="https://swexpertacademy.com/main/code/problem/problemDetail.do?contestProbId=AV15Khn6AN0CFAYD">문제 바로가기</a></p>
<table>
  <tr>
    <td>정답률</td>
    <td>35.78</td>
  </tr>
  <tr>
    <td>시간제한</td>
    <td>10초</td>
  </tr>
  <tr>
    <td>메모리 제한</td>
    <td>힙, 정적: 256MB, 스택: 1MB</td>
  </tr>
</table>

<h2 id="📚-해설-및-코드">📚 해설 및 코드</h2>
<h3 id="✏️-문제-접근">✏️ 문제 접근</h3>
<p>해당 문제는 테스트 케이스마다 주어지는 입력값의 크기가 크지 않으므로 (최대 자릿수: 6, 최대 교환 횟수: 10) DFS를 이용한 브루트 포스로 해결할 수 있다.</p>
<p>하지만 문제를 그대로 해석하여 <strong>정렬</strong>로도 풀 수 있다.</p>
<h3 id="정렬-풀이">정렬 풀이</h3>
<p>해당 문제를 정렬로 풀 때에는 <strong>정렬의 한 step의 결과가 최대</strong>가 되도록 하는 것이 중요하다</p>
<h3 id="정렬-풀이-예외-케이스">정렬 풀이 예외 케이스</h3>
<p>정렬 풀이로 풀게 되면 예외 케이스가 발생하게 된다.
(output를 보면 알겠지만, 문제의 답이 정렬의 결과랑 다르기 때문이다)</p>
<h4 id="1-숫자-카드에-중복된-값이-있을-경우">1) 숫자 카드에 중복된 값이 있을 경우</h4>
<p>해당 예외 케이스는 정렬의 한 step 당 결과가 최대가 되도록 하는 것에 집중한다면 문제가 되지 않을 수 있다.</p>
<p>문제의 예제를 보도록 하자</p>
<blockquote>
<p>input
숫자 카드: 3, 2, 8, 8, 8 &amp;nbsp&amp;nbsp | &amp;nbsp&amp;nbsp교환 횟수: 2</p>
<p>step1: 8, 2, 8, 8, 3
step2: 8, 8, 8, 2, 3
answer: 8, 8, 8, 3, 2 (wrong!)</p>
</blockquote>
<p>위의 예제처럼 그리디 시점으로 봤을 때 정렬의 각 step이 최대가 되기 위해선 
8 중에서도 제일 마지막에 위치한 값과 맨 앞의 카드를 바꾸면 된다.
그러나 answer의 값을 보면 그렇지 않을 것을 확인할 수 있다.</p>
<p>그렇기 때문에 정렬 풀이로 진행할 때는 다음 문장의 차이를 주의해야 한다.</p>
<blockquote>
<p>각 step에 대해 <strong>최선</strong>을 고르는 것이 아닌, step의 <strong>결과</strong>가 최대가 나오도록 해야 한다.</p>
</blockquote>
<h4 id="2-정렬을-수행하고도-교환-횟수가-남았을-경우">2) 정렬을 수행하고도 교환 횟수가 남았을 경우</h4>
<p>해당 예외 케이스는 정렬 수행 후의 남은 교환 횟수에 대해
어쩔 수 없이 끝의 두 자리를 swapping을 해야 하기 때문에 발생하는 예외 케이스이다.</p>
<p>남은 교환 횟수가 홀수라면: 끝의 두 자리를 교환해야만 한다.
남은 교환 횟수가 짝수라면: 정렬 수행 결과를 반환한다.</p>
<p>추가로 주의해야 할 점이 있다.
숫자 카드 중에 중복된 값이 있다면, 중복된 값끼리 교환하면 되므로 해당 예외를 고려하지 않아도 된다.</p>
<h2 id="📑-코드">📑 코드</h2>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;
#include &lt;string&gt;
#include &lt;set&gt;

using namespace std;


int main() {
    int T;
    cin &gt;&gt; T;

    for (int i = 1; i &lt;= T; i++) {
        string num;
        int max_change_cnt;
        cin &gt;&gt; num &gt;&gt; max_change_cnt;

        // 1. 선택 정렬 수행
        int cur_change_cnt = 0;
        bool exist_same_val = false;
        vector&lt;int&gt; is_changed(num.length()); // 어느 숫자 카드로 인해 스왑되었는지를 저장
        for (int i = 0; i &lt; num.length() - 1; i++) {
            int max_idx = i;

            for (int j = i + 1; j &lt; num.length(); j++) {
                if (num[max_idx] &lt;= num[j]) {
                    max_idx = j;
                }

                if (num[i] == num[j]) {
                    exist_same_val = true;
                }
            }

            // 현재 step에서 이미 최대값인 경우
            if (max_idx == i)
                continue;

            cur_change_cnt++;
            is_changed[max_idx] = num[max_idx]; // swap 된 결과를 기준으로 max_idx번 째는 num[i]의 값에 의해 swap 되었음을 의미
            swap(num[max_idx], num[i]);

            // 정렬 한 step의 결과가 최대가 되도록 하기 위해 추가 정렬 수행
            //  -&gt; 추가 정렬시 is_changed 고려
            for (int j = i; j &lt; num.length() - 1; j++) {
                if (!is_changed[j]) // swap 된 적이 없는 숫자 카드는, 추가 정렬을 수행해선 안됨
                    continue;

                max_idx = j;
                for (int k = i + 1; k &lt; num.length(); k++) {
                    // 예외 1) 숫자 카드에 같은 값이 있을 경우에 문제가 됨
                    //   =&gt; 이를 판단하기 위해 is_changed를 사용
                    if (is_changed[j] == is_changed[k] &amp;&amp; num[max_idx] &lt; num[k]) {
                        max_idx = k;
                    }
                }

                if (max_idx == j)
                    continue;
                swap(num[max_idx], num[j]);
            }

            // 정렬 횟수를 모두 소모함
            if (cur_change_cnt == max_change_cnt)
                break;
        }

        // 2. 숫자카드 중 중복된 값이 없다면,
        //  2-1. 남은 횟수가 홀수라면 맨 끝 2개의 숫자 swap
        //  2-2. 남은 횟수가 짝수라면 무시
        if (!exist_same_val &amp;&amp; (max_change_cnt - cur_change_cnt) % 2) {
            swap(num[num.length() - 1], num[num.length() - 2]);
        }
        cout &lt;&lt; &quot;#&quot; &lt;&lt; i &lt;&lt; &quot; &quot; &lt;&lt; stoi(num) &lt;&lt; &quot;\n&quot;;
    }

    return 0;
}
</code></pre>
]]></description>
        </item>
    </channel>
</rss>