<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Bansanka</title>
    <link>https://eunchanee.tistory.com/</link>
    <description> </description>
    <language>ko</language>
    <pubDate>Thu, 6 Aug 2026 18:06:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Enchant&amp;eacute;e</managingEditor>
    <image>
      <title>Bansanka</title>
      <url>https://tistory1.daumcdn.net/tistory/3850911/attach/8332eb8cd34443498bc6dc055b91107e</url>
      <link>https://eunchanee.tistory.com</link>
    </image>
    <item>
      <title>C++ Copy Elision과 RVO/NRVO 제대로 이해하기</title>
      <link>https://eunchanee.tistory.com/776</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서 함수가 객체를 반환할 때마다 copy나 move가 발생한다고 생각하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 성능을 걱정한 나머지 return 문에 std::move를 붙이는 코드를 종종 보게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 C++17 이후에는 prvalue 반환에서 copy elision이 보장되는 경우가 있고, named local object 반환에서도 compiler가 NRVO를 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오히려 return std::move(local)처럼 작성하면 NRVO가 막히고 move가 강제로 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체를 값으로 반환할 때는 먼저 copy elision과 NRVO 가능성을 이해하고, local object에 습관적으로 std::move를 붙이지 않는 것이 중요합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bv0a5w/dJMcacqxNpy/v1E1okkvLhb1WoNGN3qZAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bv0a5w/dJMcacqxNpy/v1E1okkvLhb1WoNGN3qZAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bv0a5w/dJMcacqxNpy/v1E1okkvLhb1WoNGN3qZAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbv0a5w%2FdJMcacqxNpy%2Fv1E1okkvLhb1WoNGN3qZAK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;copy elision은 값 반환 코드를 더 단순하게 유지하면서도 불필요한 copy와 move를 줄여주는 C++의 중요한 최적화 규칙입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Modern C++에서는 객체를 값으로 반환하는 코드가 자연스럽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전 C++ 경험이 있으면 “큰 객체를 반환하면 copy가 비싸지 않을까?”라는 걱정을 먼저 할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 output parameter를 쓰거나, pointer를 반환하거나, return 문에 std::move를 붙여 최적화하려는 시도를 하기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이런 코드는 오히려 의도를 흐리고 compiler가 해줄 수 있는 최적화를 방해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 C++17 기준으로 copy elision, RVO, NRVO가 어떤 의미인지 정리하고, return std::move(local)이 왜 조심해야 할 코드인지 예제로 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;반환 형태&lt;/td&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;일반적인 결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prvalue 반환&lt;/td&gt;
&lt;td&gt;return Trace{&quot;x&quot;};&lt;/td&gt;
&lt;td&gt;C++17에서 copy elision 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;named local 반환&lt;/td&gt;
&lt;td&gt;Trace value; return value;&lt;/td&gt;
&lt;td&gt;NRVO가 적용될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local object를 std::move로 반환&lt;/td&gt;
&lt;td&gt;return std::move(value);&lt;/td&gt;
&lt;td&gt;NRVO가 막히고 move가 발생할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;member 반환&lt;/td&gt;
&lt;td&gt;return member_;&lt;/td&gt;
&lt;td&gt;보통 copy 또는 move 대상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 반환 값을 무조건 최적화하려고 손대기보다, compiler가 객체를 어디에 직접 생성할 수 있는지 보는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 반환은 API를 단순하게 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수가 객체를 만들어 반환하면 호출자는 auto result = make_value();처럼 자연스럽게 받을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 output parameter나 raw pointer 반환은 ownership과 lifetime을 더 어렵게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 copy elision을 이해하면 성능 때문에 불필요하게 복잡한 API를 만드는 일을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;값 반환 API를 더 자신 있게 사용할 수 있습니다.&lt;/li&gt;
&lt;li&gt;불필요한 std::move 때문에 NRVO를 막는 실수를 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;copy, move, destructor 호출 로그를 보고 객체 lifetime을 해석할 수 있습니다.&lt;/li&gt;
&lt;li&gt;성능 최적화와 코드 가독성 사이에서 더 나은 기본값을 선택할 수 있습니다.&lt;/li&gt;
&lt;li&gt;면접에서 move semantics를 단순 문법이 아니라 compiler 최적화와 연결해 설명할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 함수가 vector, string, DTO, configuration object, parser result 같은 값을 반환하는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 “return by value는 느리다”는 막연한 생각만 갖고 있으면 오히려 C++다운 코드를 쓰기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;copy elision은 copy나 move가 일어날 것처럼 보이는 상황에서 compiler가 그 중간 객체 생성을 생략하는 동작입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RVO는 Return Value Optimization의 약자로, 함수의 반환 객체를 caller 쪽 저장 공간에 직접 생성하는 최적화로 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NRVO는 Named Return Value Optimization입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수 안의 이름 있는 local object를 반환할 때 compiler가 그 object를 반환 위치에 직접 만들 수 있으면 copy나 move를 생략합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개념&lt;/td&gt;
&lt;td&gt;설명&lt;/td&gt;
&lt;td&gt;주의할 점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copy elision&lt;/td&gt;
&lt;td&gt;copy나 move 대상인 임시 객체 생성을 생략&lt;/td&gt;
&lt;td&gt;C++17에서 일부 상황은 최적화가 아니라 언어 규칙에 가까움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RVO&lt;/td&gt;
&lt;td&gt;반환 객체를 caller의 결과 저장 위치에 직접 생성&lt;/td&gt;
&lt;td&gt;return Type{...}; 같은 형태에서 이해하기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NRVO&lt;/td&gt;
&lt;td&gt;이름 있는 local object를 반환 위치에 직접 생성&lt;/td&gt;
&lt;td&gt;가능한 최적화지만 모든 형태에서 항상 보장되는 것은 아님&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::move&lt;/td&gt;
&lt;td&gt;객체를 xvalue로 cast해 move 대상임을 표현&lt;/td&gt;
&lt;td&gt;local return에 붙이면 NRVO 기회를 없앨 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++17에서 return Trace{&quot;x&quot;};처럼 prvalue를 반환하는 경우에는 반환 객체가 바로 만들어진다고 이해하는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Trace value; return value;처럼 이름 있는 local object를 반환하는 경우에는 compiler가 NRVO를 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 return std::move(value);를 쓰면 반환식이 더 이상 NRVO가 기대하는 단순한 local object 이름이 아니게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 move constructor가 호출될 수 있고, 그만큼 최적화 여지가 줄어듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는 생성자, copy constructor, move constructor, destructor가 언제 호출되는지 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;make_prvalue(), make_named(), make_forced_move() 세 함수를 비교하면 prvalue 반환, NRVO 후보, std::move로 NRVO를 막는 경우의 차이를 볼 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;utility&amp;gt;

class Trace {
public:
    explicit Trace(std::string name)
        : name_(std::move(name))
    {
        std::cout &amp;lt;&amp;lt; &quot;ctor &quot; &amp;lt;&amp;lt; name_ &amp;lt;&amp;lt; '\n';
    }

    Trace(const Trace&amp;amp; other)
        : name_(other.name_ + &quot;.copy&quot;)
    {
        std::cout &amp;lt;&amp;lt; &quot;copy &quot; &amp;lt;&amp;lt; other.name_ &amp;lt;&amp;lt; &quot; -&amp;gt; &quot; &amp;lt;&amp;lt; name_ &amp;lt;&amp;lt; '\n';
    }

    Trace(Trace&amp;amp;&amp;amp; other) noexcept
        : name_(std::move(other.name_))
    {
        std::cout &amp;lt;&amp;lt; &quot;move &quot; &amp;lt;&amp;lt; name_ &amp;lt;&amp;lt; '\n';
        other.name_ = &quot;moved-from&quot;;
    }

    ~Trace()
    {
        std::cout &amp;lt;&amp;lt; &quot;dtor &quot; &amp;lt;&amp;lt; name_ &amp;lt;&amp;lt; '\n';
    }

private:
    std::string name_;
};

Trace make_prvalue()
{
    std::cout &amp;lt;&amp;lt; &quot;make_prvalue\n&quot;;
    return Trace{&quot;prvalue&quot;};
}

Trace make_named()
{
    std::cout &amp;lt;&amp;lt; &quot;make_named\n&quot;;
    Trace value{&quot;named&quot;};
    return value;
}

Trace make_forced_move()
{
    std::cout &amp;lt;&amp;lt; &quot;make_forced_move\n&quot;;
    Trace value{&quot;forced&quot;};
    return std::move(value);
}

int main()
{
    std::cout &amp;lt;&amp;lt; &quot;[prvalue]\n&quot;;
    auto a = make_prvalue();

    std::cout &amp;lt;&amp;lt; &quot;[named local]\n&quot;;
    auto b = make_named();

    std::cout &amp;lt;&amp;lt; &quot;[return std::move(local)]\n&quot;;
    auto c = make_forced_move();

    std::cout &amp;lt;&amp;lt; &quot;end of main\n&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trace 객체는 생성, copy, move, 소멸 시점을 모두 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 서비스 코드에서는 이런 logging type을 만들 일은 거의 없지만, 객체 lifetime을 관찰하는 학습용 예제로는 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실행 결과 또는 예상 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 clang++ -std=c++17로 컴파일하고 실행하면 다음과 같은 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;[prvalue]
make_prvalue
ctor prvalue
[named local]
make_named
ctor named
[return std::move(local)]
make_forced_move
ctor forced
move forced
dtor moved-from
end of main
dtor forced
dtor named
dtor prvalue
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;make_prvalue()에서는 Trace{&quot;prvalue&quot;}가 반환 위치에 직접 생성되므로 copy나 move 출력이 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;make_named()에서는 이름 있는 local object를 반환하지만, 이 실행에서는 compiler가 NRVO를 적용해 copy와 move가 보이지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;make_forced_move()에서는 return std::move(value); 때문에 move가 실제로 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과만 봐도 local object 반환에 습관적으로 std::move를 붙이는 것이 항상 좋은 최적화가 아니라는 점을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;return Trace{&quot;prvalue&quot;}
반환 객체가 결과 저장 위치에 직접 생성됨
copy 없음
move 없음

Trace value{&quot;named&quot;}; return value;
compiler가 NRVO를 적용할 수 있음
적용되면 copy 없음
move 없음

Trace value{&quot;forced&quot;}; return std::move(value);
value를 xvalue로 cast
NRVO 기회가 사라짐
move constructor 호출 가능
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NRVO는 compiler 최적화이므로 조건과 compiler 설정에 따라 결과가 다를 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 C++17의 prvalue 반환과 return std::move(local)의 차이는 코드 리뷰에서 안정적으로 짚을 수 있는 포인트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그림으로 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 반환의 차이를 그림으로 보면 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 반환 객체가 caller의 저장 위치에 직접 생성될 수 있는지, 아니면 local object에서 결과 객체로 move해야 하는지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAy0qt/dJMcadQn5SA/AAhBiaFkeLHrzYwXbMcU2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAy0qt/dJMcadQn5SA/AAhBiaFkeLHrzYwXbMcU2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAy0qt/dJMcadQn5SA/AAhBiaFkeLHrzYwXbMcU2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAy0qt%2FdJMcadQn5SA%2FAAhBiaFkeLHrzYwXbMcU2k%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;prvalue 반환과 NRVO는 반환 객체를 결과 위치에 직접 만들 수 있지만, return std::move(local)은 local object에서 결과 객체로 move하는 경로를 만들 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림의 위쪽 PRVALUE 흐름은 return Trace{&quot;prvalue&quot;}처럼 임시 객체를 반환할 때 결과 객체가 caller의 저장 위치에 바로 만들어지는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가운데 NRVO 흐름은 Trace value{&quot;named&quot;}; return value;처럼 이름 있는 local object를 반환하지만, compiler가 local object와 result를 같은 저장 위치로 취급할 수 있는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래쪽 STD MOVE 흐름은 return std::move(value);처럼 local object를 xvalue로 바꾸면서 NRVO 기회를 잃고, 별도의 result 객체로 move가 발생할 수 있는 경우입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 그림에서 핵심은 좌우 방향이 아니라 세 줄의 차이입니다. 위쪽과 가운데는 move가 필요 없는 경로이고, 아래쪽만 실제 move 경로를 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;값으로 반환하면 항상 copy가 발생한다.&lt;/b&gt;&lt;br /&gt;C++17 이후 prvalue 반환에서는 copy elision이 보장되는 경우가 있습니다. named local 반환도 NRVO가 적용되면 copy와 move가 생략될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;return std::move(local)은 항상 더 빠르다.&lt;/b&gt;&lt;br /&gt;local object를 반환할 때 std::move를 붙이면 NRVO가 막혀 오히려 move가 추가될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;move는 비용이 0이다.&lt;/b&gt;&lt;br /&gt;move는 resource pointer 교환처럼 저렴한 경우가 많지만, 타입에 따라 상태 정리, 작은 buffer 처리, invariant 유지 비용이 있을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NRVO는 반드시 항상 일어난다.&lt;/b&gt;&lt;br /&gt;NRVO는 가능한 최적화입니다. 여러 local object 중 조건에 따라 반환하는 구조처럼 복잡한 흐름에서는 적용되지 않을 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최적화를 위해 output parameter를 기본으로 써야 한다.&lt;/b&gt;&lt;br /&gt;값 반환이 더 명확한 API가 되는 경우가 많고, compiler가 copy elision을 적용할 수 있으므로 먼저 값 반환을 기본값으로 검토하는 편이 좋습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 오해들은 대부분 std::move를 “성능을 올리는 함수”처럼 기억해서 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::move는 실제로 move를 수행하는 함수가 아니라, move 대상으로 취급하라는 cast라는 점을 다시 떠올려야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실무에서는 어떻게 볼까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 코드 리뷰에서 가장 자주 볼 패턴은 local object를 만든 뒤 마지막에 return std::move(result);를 쓰는 코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 경우 이 코드는 return result;로 두는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compiler가 NRVO를 적용할 수 있는 기회를 남겨두기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;권장 표현&lt;/td&gt;
&lt;td&gt;이유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;임시 객체를 바로 반환&lt;/td&gt;
&lt;td&gt;return Result{...};&lt;/td&gt;
&lt;td&gt;C++17에서 결과 위치에 직접 생성되는 형태가 명확함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local result 하나를 만들어 반환&lt;/td&gt;
&lt;td&gt;return result;&lt;/td&gt;
&lt;td&gt;NRVO 기회를 유지함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함수 parameter를 반환&lt;/td&gt;
&lt;td&gt;return std::move(param);&lt;/td&gt;
&lt;td&gt;parameter는 NRVO 대상 local object가 아니므로 move 의도를 표현할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;member를 반환&lt;/td&gt;
&lt;td&gt;상황에 따라 copy 또는 move 명확화&lt;/td&gt;
&lt;td&gt;객체 상태를 비우는 API인지 확인해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;여러 local object 중 하나 반환&lt;/td&gt;
&lt;td&gt;단순 구조로 재검토&lt;/td&gt;
&lt;td&gt;NRVO 적용 가능성이 낮아질 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, local object 반환에서는 std::move를 줄이고, parameter나 member처럼 실제로 ownership 이전 의도가 있는 곳에서만 조심해서 쓰는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 성능이 정말 중요하다면 추측하지 말고 compiler warning, benchmark, profiler를 함께 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Clang과 GCC는 return std::move(local) 같은 pessimized move를 경고해주는 경우가 있으므로 warning level을 높여두는 것도 도움이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++17의 prvalue 반환에서는 copy elision이 보장되는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 있는 local object를 반환할 때는 compiler가 NRVO를 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return std::move(local)은 NRVO를 막고 move를 발생시킬 수 있으므로 습관적으로 쓰지 않는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 반환은 성능 때문에 피해야 할 패턴이 아니라, Modern C++에서 자연스럽고 명확한 API 설계 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적화가 걱정된다면 코드 모양을 단순하게 유지하고, 실제 컴파일러 출력과 측정 결과를 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값으로 반환할 때의 기본 판단은 “return result;로 충분한가?”입니다. std::move는 자동으로 빨라지는 주문이 아니라, compiler의 선택지를 바꾸는 표현입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>C++/Concepts</category>
      <category>C++</category>
      <category>c++17</category>
      <category>CopyElision</category>
      <category>modernc++</category>
      <category>MoveSemantics</category>
      <category>nrvo</category>
      <category>RVO</category>
      <category>stdmove</category>
      <category>객체생명주기</category>
      <category>성능최적화</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/776</guid>
      <comments>https://eunchanee.tistory.com/776#entry776comment</comments>
      <pubDate>Thu, 30 Jul 2026 22:28:11 +0900</pubDate>
    </item>
    <item>
      <title>Google Pay의 Ask GPay, 금융 앱 AI는 어디까지 해야 할까?</title>
      <link>https://eunchanee.tistory.com/775</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 앱 안으로 AI assistant가 들어오는 흐름이 더 뚜렷해지고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 7월 29일 보도에 따르면 Google Pay는 인도에서 Ask GPay를 소개하고, 사용자가 자신의 소비 패턴과 금융 정보를 대화형으로 이해할 수 있도록 하는 기능을 확대하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능은 단순한 챗봇이라기보다 결제 앱 안의 transaction history, personalization control, AI-generated answer, privacy boundary가 만나는 사례입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 관점에서는 “금융 앱에 AI를 붙였다”보다 “AI가 민감한 금융 데이터에 접근할 때 어떤 안전 경계를 둬야 하는가”가 더 중요한 포인트입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ask GPay 뉴스의 핵심은 금융 앱의 AI assistant가 개인화된 분석을 제공하되, 결제 실행과 전문 금융 자문 같은 고위험 행동은 명확히 제한한다는 점입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhKQOq/dJMcaiYrbpr/VkQKLUbxSEBLQbYsiofWs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhKQOq/dJMcaiYrbpr/VkQKLUbxSEBLQbYsiofWs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhKQOq/dJMcaiYrbpr/VkQKLUbxSEBLQbYsiofWs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhKQOq%2FdJMcaiYrbpr%2FVkQKLUbxSEBLQbYsiofWs1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;금융 AI assistant는 편의성보다 privacy, consent, action boundary를 먼저 설계해야 하는 영역입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 세 줄 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 7월 29일 보도에 따르면 Google Pay는 인도에서 Gemini 기반 Ask GPay 기능을 소개했고, 일부 사용자에게 제한적으로 제공되는 AI finance assistant 흐름을 강화하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Pay 공식 도움말 기준으로 Ask GPay는 transaction history와 CIBIL report data 등을 바탕으로 소비 패턴, 절약 팁, 금융 개념, 카드 추천 같은 정보를 대화형으로 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Ask GPay는 결제 transaction을 직접 시작하거나 완료할 수 없고, 전문 금융 자문을 대체하지 않으며, personalization control과 privacy boundary가 핵심 설계 요소로 제시됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 무슨 일이 있었나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Times of India는 2026년 7월 29일 Google Pay가 Ask Google Pay, 즉 Ask GPay를 소개하고 Flex 관련 확장도 함께 진행한다고 보도했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보도에 따르면 Ask GPay는 Gemini 기반 conversational AI로, 사용자가 소비 습관을 분석하거나 금융 개념을 이해하는 데 도움을 주는 방향으로 제공됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 Google Pay Help 문서도 Ask GPay를 “limited number of users in India”에게 제공되는 기능으로 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서 확인할 수 있는 핵심은 기능보다 경계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ask GPay는 지출 패턴 분석, 절약 팁, 적합한 offer 탐색, SIP나 CIBIL score 같은 금융 개념 설명을 지원하지만, 결제를 대신 실행하지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;확인된 내용&lt;/td&gt;
&lt;td&gt;개발자 관점에서 보는 지점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제품&lt;/td&gt;
&lt;td&gt;Ask GPay&lt;/td&gt;
&lt;td&gt;금융 앱 내부 AI assistant 사례&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지역&lt;/td&gt;
&lt;td&gt;인도 일부 사용자 대상 제한 제공&lt;/td&gt;
&lt;td&gt;지역별 규제, 언어, 금융 데이터 정책이 중요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 기능&lt;/td&gt;
&lt;td&gt;소비 패턴 분석, 절약 팁, 금융 개념 설명, offer 탐색&lt;/td&gt;
&lt;td&gt;개인화 추천과 설명형 AI의 결합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;transaction history와 CIBIL report data 활용 가능&lt;/td&gt;
&lt;td&gt;consent, retention, data minimization 설계 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제한&lt;/td&gt;
&lt;td&gt;결제 시작 또는 완료 불가, 전문 금융 자문 아님&lt;/td&gt;
&lt;td&gt;AI action boundary를 명확히 둔 설계&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 뉴스는 Google Pay 한 제품의 기능 업데이트로만 보면 작아 보일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 결제 앱, 개인 금융 데이터, AI assistant가 한 화면 안에 들어오는 사례라는 점에서 fintech와 consumer AI 양쪽 모두에 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 앱은 사용자의 생활 패턴을 가장 직접적으로 보여주는 데이터가 모이는 곳입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카드 결제, 송금, 청구서, 대출, 신용 정보는 단순한 activity log가 아니라 매우 민감한 개인 데이터입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 금융 앱의 AI assistant는 일반 productivity assistant보다 훨씬 보수적인 설계가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;AI가 개인 금융 데이터를 분석하려면 명시적인 personalization control이 필요합니다.&lt;/li&gt;
&lt;li&gt;AI 답변은 유용할 수 있지만 금융 자문으로 오해되지 않도록 경계를 명확히 해야 합니다.&lt;/li&gt;
&lt;li&gt;결제 실행, 송금, 대출 신청 같은 action은 더 강한 confirmation과 별도 flow가 필요합니다.&lt;/li&gt;
&lt;li&gt;AI assistant의 hallucination은 금융 영역에서 사용자 손실이나 규제 리스크로 이어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;대화 기록과 feedback data를 어떻게 보관하고 개선에 활용하는지도 중요한 privacy 설계가 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google Pay Help 문서는 Ask GPay가 결제 transaction을 시작하거나 완료하지 못한다고 설명합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 제한은 불편함이 아니라 안전 장치입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 사용자의 돈을 직접 움직일 수 있는 순간부터 product risk와 compliance risk가 크게 올라가기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 개발자에게 미치는 영향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 fintech 개발자뿐 아니라 consumer app, SaaS, platform engineer에게도 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 데이터가 풍부한 앱일수록 AI assistant를 붙였을 때 “대답 품질”보다 “어떤 데이터를 읽을 수 있고, 어떤 행동은 못 하게 할 것인가”가 먼저 결정되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개발 영역&lt;/td&gt;
&lt;td&gt;이번 사례에서 배울 점&lt;/td&gt;
&lt;td&gt;실무 체크포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product engineering&lt;/td&gt;
&lt;td&gt;AI assistant를 핵심 탭 안에 넣을 때 user trust가 중요&lt;/td&gt;
&lt;td&gt;onboarding에서 데이터 사용 목적과 제한을 명확히 설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;AI query가 민감 데이터 조회와 연결됨&lt;/td&gt;
&lt;td&gt;query scope, audit log, consent state를 서버에서 강제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;AI가 계정 데이터와 연결될수록 prompt injection과 data leakage 위험 증가&lt;/td&gt;
&lt;td&gt;tool permission, output filtering, sensitive field masking 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data platform&lt;/td&gt;
&lt;td&gt;transaction history와 credit data가 answer context가 됨&lt;/td&gt;
&lt;td&gt;data minimization, retention period, anonymization 정책 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compliance&lt;/td&gt;
&lt;td&gt;금융 정보와 advice boundary가 중요&lt;/td&gt;
&lt;td&gt;교육용 정보와 regulated advice를 UI와 정책에서 구분&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자에게 특히 중요한 부분은 “AI가 무엇을 할 수 있는지”보다 “무엇을 할 수 없게 막았는지”입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ask GPay는 사용자의 금융 정보를 분석해 설명할 수 있지만, 결제를 직접 실행하지 못한다고 명시합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 action boundary는 앞으로 banking, commerce, healthcare, enterprise SaaS AI assistant에서도 반복될 가능성이 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 관련 개발자/IT 실무자 관점에서 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 AI assistant를 만들 때는 일반 챗봇처럼 “사용자 질문에 답한다”로 설계를 시작하면 위험합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 데이터 접근, consent, action permission, 설명 책임을 분리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ask GPay 공식 문서에서 눈에 띄는 부분도 바로 이 경계들입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설계 질문&lt;/td&gt;
&lt;td&gt;Ask GPay에서 보이는 방향&lt;/td&gt;
&lt;td&gt;다른 앱에 적용할 때&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터 사용은 opt-in인가?&lt;/td&gt;
&lt;td&gt;Personalization within Google Pay가 필요하고 기본은 off로 설명됨&lt;/td&gt;
&lt;td&gt;기능 활성화와 데이터 연결을 분리해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대화 기록은 얼마나 남기는가?&lt;/td&gt;
&lt;td&gt;공식 문서 기준 과거 42일 chat history 확인 가능&lt;/td&gt;
&lt;td&gt;사용자 삭제권과 retention policy를 명확히 해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 답변이 틀릴 때는?&lt;/td&gt;
&lt;td&gt;AI가 오류를 낼 수 있고 feedback을 받을 수 있다고 안내&lt;/td&gt;
&lt;td&gt;정정 요청, feedback loop, high-risk answer 차단 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부 공유는 어떻게 막는가?&lt;/td&gt;
&lt;td&gt;데이터가 Google Pay ecosystem 안에 머문다고 설명&lt;/td&gt;
&lt;td&gt;third-party tool call과 external integration을 별도 통제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;행동 실행은 어디까지 허용하는가?&lt;/td&gt;
&lt;td&gt;결제 시작 또는 완료 불가&lt;/td&gt;
&lt;td&gt;read-only assistant에서 confirmable action으로 단계적 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표는 금융 앱뿐 아니라 B2B SaaS에도 그대로 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 CRM AI assistant가 customer data를 읽을 수 있다면, email 발송이나 계약 조건 변경은 별도 confirmation과 policy check를 거쳐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI가 업무 흐름 안으로 들어올수록 read 권한과 write 권한을 같은 assistant experience 안에서도 나눠야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 앞으로 볼 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ask GPay가 더 넓게 배포된다면 앞으로 볼 지점은 기능 수보다 신뢰 설계입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 AI에게 금융 데이터를 맡기려면 답변 품질뿐 아니라 데이터가 어디로 가고, 무엇에 쓰이며, 어떤 행동은 차단되는지 이해할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;제한 제공에서 일반 제공으로 확대되는가?&lt;/b&gt;&lt;br /&gt;현재 공식 문서 기준 일부 인도 사용자 대상 제한 제공이므로, rollout 범위와 속도를 봐야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;언어와 지역별 금융 규제 대응이 충분한가?&lt;/b&gt;&lt;br /&gt;금융 정보는 국가별 규제와 product wording의 영향을 크게 받습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 답변의 책임 범위를 어떻게 표시하는가?&lt;/b&gt;&lt;br /&gt;교육용 정보와 자문, 추천, 실행의 경계를 UI에서 반복적으로 보여줘야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;거래 실행으로 확장될 가능성이 있는가?&lt;/b&gt;&lt;br /&gt;현재는 결제를 직접 실행하지 못하지만, 향후 confirmable action이 들어간다면 보안 설계가 더 중요해집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개인화 기능을 끈 사용자에게도 유용한가?&lt;/b&gt;&lt;br /&gt;privacy를 선택한 사용자가 기능 가치를 거의 잃는다면 adoption이 제한될 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발팀이 비슷한 기능을 만든다면, 처음부터 AI assistant를 “read-only insight layer”와 “action layer”로 나눠 설계하는 것이 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 금융, 의료, 법률, HR처럼 민감한 영역에서는 이 구분이 제품 품질의 일부가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 사견&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저는 Ask GPay에서 가장 흥미로운 부분이 Gemini 기반이라는 점보다 “결제를 직접 하지 못한다”는 제한이라고 봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI assistant 제품은 보통 더 많은 일을 할수록 좋아 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 금융 앱에서는 할 수 없는 일을 명확히 정하는 것이 사용자 신뢰를 만드는 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 입장에서도 이 사례는 좋은 기준이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;민감 데이터가 들어가는 AI 기능을 만들 때는 먼저 답변 가능한 질문, 접근 가능한 데이터, 실행 가능한 action, 보관되는 로그를 표로 정리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 모델 품질을 논의해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델이 똑똑해지는 속도보다, 제품이 안전한 경계를 만드는 속도가 느리면 실제 사용자는 불안해질 수밖에 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금융 AI assistant의 좋은 기본값은 “잘 대답하는 AI”가 아니라 “어디까지 읽고, 어디서 멈출지 분명한 AI”입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://timesofindia.indiatimes.com/technology/tech-news/google-pay-introduces-ask-google-pay-expands-flex-with-sbi-card/articleshow/132711794.cms&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Times of India - Google Pay introduces Ask Google Pay, expands Flex with SBI card&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/pay/india/answer/17034014?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Pay Help - Get started with Ask GPay&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/pay/india/answer/16597732?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Pay Help - Introducing Flex by Google Pay&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.google.com/pay/india/answer/9674221?hl=en&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Google Pay Help - Supported cards and payment modes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;

&lt;/div&gt;</description>
      <category>IT News</category>
      <category>ai assistant</category>
      <category>Ask GPay</category>
      <category>consent</category>
      <category>Fintech</category>
      <category>Gemini</category>
      <category>Google Pay</category>
      <category>IT News</category>
      <category>Privacy</category>
      <category>Product Security</category>
      <category>금융AI</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/775</guid>
      <comments>https://eunchanee.tistory.com/775#entry775comment</comments>
      <pubDate>Thu, 30 Jul 2026 22:14:57 +0900</pubDate>
    </item>
    <item>
      <title>DB 장애 복구를 이해하는 WAL 기본기</title>
      <link>https://eunchanee.tistory.com/774</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database는 매 요청마다 disk의 data page를 즉시 안전하게 고쳐 쓰지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 storage engine은 memory 안에서 page를 먼저 바꾸고, 변경 기록을 log에 남긴 뒤, 실제 data page flush는 나중에 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 장애가 나면 자연스럽게 질문이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;“COMMIT이 성공했다고 응답했는데 서버 전원이 꺼지면 데이터는 어디까지 살아남을까?”&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문의 핵심에 있는 개념이 WAL, 즉 Write-Ahead Log입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL의 핵심은 data page를 영구 저장소에 반영하기 전에, 그 변경을 복구할 수 있는 log record를 먼저 durable 하게 기록하는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tPPHO/dJMcaa7jgxs/ng6oiX7UoCn0LDaH3l3iy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tPPHO/dJMcaa7jgxs/ng6oiX7UoCn0LDaH3l3iy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tPPHO/dJMcaa7jgxs/ng6oiX7UoCn0LDaH3l3iy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPPHO%2FdJMcaa7jgxs%2Fng6oiX7UoCn0LDaH3l3iy1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;WAL은 commit durability와 crash recovery를 연결하는 database storage engine의 핵심 아이디어입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database를 쓰다 보면 COMMIT이라는 단어를 자연스럽게 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 COMMIT이 단순히 “메모리의 값을 바꿨다”는 뜻이라면 장애 상황에서 신뢰하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 프로세스가 죽거나, OS가 panic을 내거나, 장비 전원이 꺼졌을 때도 이미 성공으로 응답한 transaction은 복구되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 COMMIT되지 않은 transaction의 중간 결과가 data file에 남아 있으면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 이 문제를 풀기 위한 대표적인 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경 내용을 data page에 직접 반영하기 전에, 먼저 log file에 “무엇을 바꿨는지”를 순서대로 기록합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애가 난 뒤에는 이 log를 읽고 committed transaction은 다시 반영하고, 끝나지 않은 transaction은 되돌리거나 무시하는 방식으로 일관성을 회복합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세부 구현은 database마다 다르지만, log를 먼저 안전하게 남긴다는 원칙은 많은 storage engine에서 공통으로 등장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;관점&lt;/td&gt;
&lt;td&gt;WAL 없이 생각하면&lt;/td&gt;
&lt;td&gt;WAL을 사용하면&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능&lt;/td&gt;
&lt;td&gt;작은 변경마다 data page를 즉시 안전하게 flush해야 함&lt;/td&gt;
&lt;td&gt;순차 log append를 먼저 하고 page flush를 나중으로 미룰 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;복구&lt;/td&gt;
&lt;td&gt;장애 시 어떤 변경이 끝났는지 판단하기 어려움&lt;/td&gt;
&lt;td&gt;log record와 commit record를 기준으로 복구 지점을 판단함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일관성&lt;/td&gt;
&lt;td&gt;data file에 반쯤 반영된 변경이 남을 수 있음&lt;/td&gt;
&lt;td&gt;committed 변경과 incomplete 변경을 구분해 회복함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 WAL을 DB 내부 구현 세부사항이 아니라, 장애 복구를 이해하기 위한 CS 기본기로 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 backend 개발자가 직접 구현할 일이 많지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 database 장애, replication lag, backup, checkpoint, transaction durability를 다룰 때 계속 마주치는 개념입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 운영 중 장애가 났을 때 “DB가 올라오면서 recovery를 수행한다”는 로그를 본다면, 내부적으로 어떤 종류의 일이 일어나는지 알아야 상황을 해석할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;COMMIT 성공 응답이 무엇을 의미하는지 이해할 수 있습니다.&lt;/li&gt;
&lt;li&gt;장애 후 database가 바로 열리지 않고 recovery를 수행하는 이유를 설명할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Checkpoint가 왜 필요한지, checkpoint가 너무 오래 밀리면 어떤 비용이 생기는지 이해할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Replication이나 backup에서 log stream이 왜 중요한지 감을 잡을 수 있습니다.&lt;/li&gt;
&lt;li&gt;면접에서 transaction, durability, storage engine을 한 흐름으로 설명하기 좋습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL을 모르면 transaction을 단순히 SQL 문법으로만 이해하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 WAL을 이해하면 database가 성능과 안정성 사이에서 어떤 순서를 강제하는지 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 이름 그대로 “먼저 쓰는 log”입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 먼저 쓴다는 말은 data page를 먼저 고치는 것이 아니라, 복구 가능한 변경 기록을 먼저 durable 하게 저장한다는 뜻입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database는 보통 table이나 index 데이터를 page 단위로 관리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Transaction이 row를 수정하면 memory buffer 안의 page가 dirty page가 되고, 이 page는 나중에 disk로 flush될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개념&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;실무에서 보는 지점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data page&lt;/td&gt;
&lt;td&gt;table이나 index 데이터가 저장되는 page 단위&lt;/td&gt;
&lt;td&gt;buffer pool에 올라와 수정되고 나중에 flush됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dirty page&lt;/td&gt;
&lt;td&gt;memory에서는 바뀌었지만 disk data file에는 아직 반영되지 않은 page&lt;/td&gt;
&lt;td&gt;장애가 나면 disk file만 보고는 최신 상태를 알 수 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log record&lt;/td&gt;
&lt;td&gt;변경 내용을 복구할 수 있도록 순서대로 남긴 기록&lt;/td&gt;
&lt;td&gt;장애 후 redo나 undo 판단의 근거가 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commit record&lt;/td&gt;
&lt;td&gt;transaction이 완료되었음을 나타내는 log 기록&lt;/td&gt;
&lt;td&gt;durability 판단의 기준이 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint&lt;/td&gt;
&lt;td&gt;특정 시점까지의 page 상태와 log 적용 상태를 정리한 기준점&lt;/td&gt;
&lt;td&gt;recovery가 읽어야 하는 log 범위를 줄임&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 data page flush와 COMMIT이 항상 같은 순간에 일어나지 않는다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COMMIT 시점에는 변경된 page 전체가 disk에 내려가지 않았을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대신 WAL record가 durable 하게 기록되어 있으면, crash 이후에도 database는 그 변경을 다시 재생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그림으로 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL을 이해할 때 가장 중요한 순서는 log append, data page 변경, commit record, crash recovery입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 그림은 transaction이 data page를 바꾸기 전에 WAL을 먼저 durable 하게 남기고, 장애 후 recovery가 그 log를 기준으로 page를 복구하는 흐름을 단순화한 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMXkY3/dJMcagsQulH/d6m5q71XLVfJWHN8kCmKtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMXkY3/dJMcagsQulH/d6m5q71XLVfJWHN8kCmKtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMXkY3/dJMcagsQulH/d6m5q71XLVfJWHN8kCmKtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMXkY3%2FdJMcagsQulH%2Fd6m5q71XLVfJWHN8kCmKtK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;WAL에서는 durable log가 먼저 남기 때문에 data page flush가 늦어져도 crash recovery가 committed 변경을 다시 반영할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림을 볼 때 data page가 언제 disk에 실제 반영되는지보다, 복구 가능한 정보가 먼저 남았는지를 보는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Log가 먼저 durable 해져 있으면 page flush가 뒤로 밀려도 장애 복구가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 data page만 먼저 일부 반영되고 log가 없다면, database는 그 변경이 정상적으로 commit된 결과인지 판단하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;transaction 시작
row 변경 요청이 들어옴

WAL append
변경 내용을 log record로 먼저 기록

data page 수정
buffer pool의 page가 dirty page가 됨

COMMIT
commit record가 durable 해지면 성공 응답 가능

crash 발생
dirty page 일부는 disk에 내려갔을 수도 있고 아닐 수도 있음

recovery
WAL을 scan해서 committed transaction은 redo
incomplete transaction은 engine 정책에 따라 undo하거나 무시
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태 변화에서 핵심은 “log가 먼저다”입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL이라는 이름은 바로 이 순서 제약을 말합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실무 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계좌 이체처럼 두 row를 함께 바꾸는 transaction을 생각해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 계좌에서는 금액을 빼고, 다른 계좌에는 금액을 더합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 하나만 반영된 채로 장애가 나면 데이터 일관성이 깨집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;BEGIN;

UPDATE accounts
SET balance = balance - 100
WHERE id = 1;

UPDATE accounts
SET balance = balance + 100
WHERE id = 2;

COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;BEGIN 기록
transaction의 시작 정보가 남음

첫 번째 UPDATE log record 기록
id=1 balance 변경을 복구할 수 있는 정보가 WAL에 남음

두 번째 UPDATE log record 기록
id=2 balance 변경을 복구할 수 있는 정보가 WAL에 남음

COMMIT record 기록 및 flush
database가 사용자에게 commit 성공을 응답할 수 있음

장애 발생
data page 중 일부가 아직 flush되지 않았을 수 있음

recovery 수행
commit record가 있는 transaction의 update를 redo해서 두 계좌 변경을 일관되게 맞춤
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 WAL이 없으면 database는 장애 후에 두 UPDATE가 어디까지 안전하게 반영되었는지 확인하기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 WAL이 있으면 commit record를 기준으로 이 transaction이 완료되었는지 판단하고, 필요한 변경을 다시 적용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 database는 isolation, lock, MVCC, undo log, page LSN 같은 더 많은 장치를 함께 사용하지만, 장애 복구의 큰 흐름은 WAL을 기준으로 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;장애 시점&lt;/td&gt;
&lt;td&gt;복구 판단&lt;/td&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UPDATE log는 있지만 COMMIT 전&lt;/td&gt;
&lt;td&gt;완료되지 않은 transaction으로 봄&lt;/td&gt;
&lt;td&gt;engine 설계에 따라 undo하거나 최종 반영에서 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COMMIT record가 durable 함&lt;/td&gt;
&lt;td&gt;committed transaction으로 봄&lt;/td&gt;
&lt;td&gt;data page 반영 여부와 무관하게 redo 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint 이후 log가 짧음&lt;/td&gt;
&lt;td&gt;최근 log만 scan하면 됨&lt;/td&gt;
&lt;td&gt;recovery 시간이 줄어듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkpoint가 오래 밀림&lt;/td&gt;
&lt;td&gt;scan해야 하는 log 범위가 커짐&lt;/td&gt;
&lt;td&gt;재시작 recovery 시간이 길어질 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서 “DB restart가 오래 걸린다”는 문제는 단순히 프로세스가 느린 것이 아닐 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애 전 dirty page와 log 상태, checkpoint 진행 상황, storage 성능에 따라 recovery 비용이 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. C++ 또는 DB와 연결해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 DB 내부 기능이지만, C++로 storage component를 만든다고 생각하면 더 직관적으로 이해할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 key-value store가 memory table을 먼저 고치기 전에 append-only log에 변경을 남긴다고 해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 log append와 flush가 성공한 뒤 memory 상태를 바꾸면, 프로세스가 죽어도 log를 다시 읽어 memory table을 복원할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;단순 storage 구성 요소&lt;/td&gt;
&lt;td&gt;DB에서 대응되는 개념&lt;/td&gt;
&lt;td&gt;복구 관점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;append-only log file&lt;/td&gt;
&lt;td&gt;WAL segment&lt;/td&gt;
&lt;td&gt;장애 후 변경 이력을 순서대로 재생함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;in-memory map&lt;/td&gt;
&lt;td&gt;buffer pool 또는 memtable&lt;/td&gt;
&lt;td&gt;빠르게 읽고 쓰지만 단독으로는 durable 하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;snapshot file&lt;/td&gt;
&lt;td&gt;checkpoint 또는 flushed data file&lt;/td&gt;
&lt;td&gt;복구 시작 지점을 줄여줌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sequence number&lt;/td&gt;
&lt;td&gt;LSN&lt;/td&gt;
&lt;td&gt;어디까지 반영했는지 판단하는 기준이 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ 관점에서 보면 WAL은 특별한 문법이 아니라 순서와 실패 처리의 문제입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일에 썼다고 해서 항상 durable 한 것은 아니고, buffer, OS page cache, fsync, storage cache 같은 계층이 사이에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 “log를 먼저 쓴다”는 말은 단순히 write 함수를 호출한다는 의미보다 더 엄격합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자에게 성공을 응답하기 전에, 복구에 필요한 정보가 적절한 durability level로 남아 있어야 한다는 뜻에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예상 결과&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;정상 종료
snapshot 또는 data page가 최신 상태를 포함
WAL은 checkpoint 이후 필요한 범위만 유지 가능

COMMIT 직후 crash
data page가 오래된 상태여도 WAL에 commit record가 있으면 redo 가능

COMMIT 전 crash
commit record가 없으면 완료된 transaction으로 취급하지 않음
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 이해하면 database 설정도 더 현실적으로 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 durability를 강하게 잡으면 commit latency가 늘 수 있고, group commit처럼 여러 transaction의 log flush를 묶어 비용을 줄이는 방식도 등장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 WAL은 성능을 포기한 기능이 아니라, 순차 append와 비동기 page flush를 활용하면서도 crash recovery를 가능하게 만드는 타협점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;COMMIT이 성공하면 data page가 반드시 즉시 disk에 저장된다.&lt;/b&gt;&lt;br /&gt;일반적으로 핵심은 data page 전체가 즉시 flush되는 것이 아니라, recovery에 필요한 log가 durable 하게 남았는지입니다. 실제 page flush는 나중에 일어날 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAL은 backup과 같은 것이다.&lt;/b&gt;&lt;br /&gt;WAL은 변경 이력 기반의 복구 재료이고, backup은 특정 시점의 데이터 사본입니다. 둘은 함께 쓰일 수 있지만 같은 개념은 아닙니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Checkpoint가 있으면 WAL은 필요 없다.&lt;/b&gt;&lt;br /&gt;Checkpoint는 recovery가 시작할 기준점을 줄이는 역할입니다. checkpoint 이후 변경을 복구하려면 여전히 WAL이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Log file만 있으면 언제나 즉시 복구된다.&lt;/b&gt;&lt;br /&gt;Log 양, checkpoint 상태, storage 속도, page flush 상태에 따라 recovery 시간은 달라질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAL은 DB 관리자만 알면 된다.&lt;/b&gt;&lt;br /&gt;Backend 개발자도 장애 분석, transaction latency, replication, migration 계획을 이해하려면 WAL의 기본 흐름을 알고 있어야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서도 이 오해들을 풀어 설명하면 단순 암기보다 훨씬 좋은 답변이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 COMMIT과 data page flush를 같은 것으로 말하지 않는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 면접 질문 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 transaction의 ACID, storage engine, OS file system, 장애 복구를 함께 묻기 좋은 주제입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;WAL은 무엇이고 왜 data page보다 먼저 기록해야 하나요?&lt;/b&gt;&lt;br /&gt;WAL은 변경 내용을 복구 가능한 log record로 먼저 durable 하게 남기는 방식입니다. data page가 나중에 flush되거나 일부만 반영되더라도, crash recovery가 log를 기준으로 committed 변경을 redo할 수 있기 때문에 먼저 기록해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;COMMIT 성공 직후 crash가 나면 database는 어떻게 복구하나요?&lt;/b&gt;&lt;br /&gt;commit record가 durable 하게 남아 있다면 recovery 과정에서 해당 transaction을 committed로 보고 필요한 변경을 redo합니다. data page가 이미 flush되었는지 여부는 page의 반영 상태와 log sequence를 기준으로 판단합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Checkpoint는 WAL과 어떤 관계가 있나요?&lt;/b&gt;&lt;br /&gt;Checkpoint는 특정 시점까지의 반영 상태를 정리해 recovery가 읽어야 하는 log 범위를 줄입니다. WAL을 대체하는 것이 아니라, WAL scan 비용을 줄이기 위한 기준점입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 답변은 “log를 먼저 쓴다”에서 멈추지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Commit record, dirty page, redo, checkpoint, durability cost까지 연결해 말하면 실무 감각이 있는 답변이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL은 data page보다 변경 log를 먼저 durable 하게 기록하는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Database는 memory의 dirty page를 나중에 flush할 수 있고, crash가 나면 WAL을 기준으로 committed 변경을 복구합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;COMMIT 성공은 보통 복구 가능한 log 기록이 안전하게 남았다는 의미로 이해해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Checkpoint는 WAL을 없애는 기능이 아니라 recovery가 scan해야 하는 log 범위를 줄이는 기준점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL을 이해하면 transaction durability, database restart recovery, replication, backup 전략을 더 현실적으로 해석할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAL을 한 문장으로 정리하면, “먼저 복구할 수 있게 기록하고, 나중에 data page를 안전하게 따라오게 만드는 방식”입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;

&lt;/div&gt;</description>
      <category>CS</category>
      <category>Checkpoint</category>
      <category>CrashRecovery</category>
      <category>CS</category>
      <category>Database</category>
      <category>DB</category>
      <category>Durability</category>
      <category>StorageEngine</category>
      <category>transaction</category>
      <category>wal</category>
      <category>WriteAheadLog</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/774</guid>
      <comments>https://eunchanee.tistory.com/774#entry774comment</comments>
      <pubDate>Tue, 28 Jul 2026 08:40:55 +0900</pubDate>
    </item>
    <item>
      <title>std::vector reallocation과 noexcept move constructor</title>
      <link>https://eunchanee.tistory.com/773</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서 move constructor를 작성할 때 noexcept를 붙여야 하는지 고민되는 순간이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 “예외를 던지지 않으니까 붙인다” 정도로만 이해하면 std::vector 같은 container에서 중요한 성능 차이를 놓칠 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::vector는 capacity가 부족해져 reallocation이 발생하면 기존 element를 새 memory buffer로 옮겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 move constructor가 noexcept인지 여부는 기존 element를 move할지 copy할지 결정하는 중요한 힌트가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::vector reallocation에서 noexcept move constructor는 “이 객체는 안전하게 move해도 된다”는 신호가 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRaE5N/dJMcacD3qWS/kKmHVXqlZL9M3oLrQqfoK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRaE5N/dJMcacD3qWS/kKmHVXqlZL9M3oLrQqfoK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRaE5N/dJMcacD3qWS/kKmHVXqlZL9M3oLrQqfoK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRaE5N%2FdJMcacD3qWS%2FkKmHVXqlZL9M3oLrQqfoK0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;move constructor의 noexcept 여부는 std::vector가 reallocation 중 기존 element를 옮기는 방식에 영향을 줄 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::move를 배웠다면 move가 copy보다 항상 빠르다고 생각하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 standard container는 성능만 보지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container는 element를 옮기는 중 예외가 발생했을 때 기존 상태를 얼마나 안전하게 유지할 수 있는지도 고려해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 std::vector는 연속된 memory를 사용하기 때문에 capacity가 부족해지면 새 buffer를 만들고 기존 element를 새 buffer로 옮깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 move가 예외를 던질 수 있다면, container 입장에서는 copy가 더 안전한 선택일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;std::vector reallocation에서의 영향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copy constructor&lt;/td&gt;
&lt;td&gt;기존 객체를 보존한 채 새 객체를 만듦&lt;/td&gt;
&lt;td&gt;move가 위험하다고 판단되면 선택될 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;move constructor&lt;/td&gt;
&lt;td&gt;기존 객체의 resource를 새 객체로 이전&lt;/td&gt;
&lt;td&gt;빠를 수 있지만 예외 가능성이 있으면 container가 조심함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noexcept move constructor&lt;/td&gt;
&lt;td&gt;move 중 예외를 던지지 않는다고 선언&lt;/td&gt;
&lt;td&gt;기존 element 재배치에서 move가 선택되기 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reallocation&lt;/td&gt;
&lt;td&gt;capacity 부족으로 새 buffer를 할당하고 element를 옮기는 과정&lt;/td&gt;
&lt;td&gt;많은 element에서 copy/move 차이가 누적됨&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 C++17 기준으로 noexcept move constructor가 왜 중요한지, std::vector reallocation에서 어떤 차이를 만드는지 예제 코드로 확인해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 class가 string, vector, unique_ptr, file handle, socket 같은 resource를 들고 있다면 move constructor를 직접 작성하거나 compiler-generated move에 의존하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 move가 실제로 빠르게 사용되려면 container가 그 move를 믿고 사용할 수 있어야 합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;std::vector는 reallocation 때 기존 element를 새 buffer로 옮깁니다.&lt;/li&gt;
&lt;li&gt;move constructor가 noexcept이면 기존 element를 move해도 rollback 위험이 작습니다.&lt;/li&gt;
&lt;li&gt;move constructor가 noexcept가 아니고 copy가 가능하면 copy가 선택될 수 있습니다.&lt;/li&gt;
&lt;li&gt;element 수가 많고 copy 비용이 크면 reallocation 비용 차이가 커집니다.&lt;/li&gt;
&lt;li&gt;성능뿐 아니라 exception safety를 코드로 표현한다는 점에서도 중요합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 noexcept가 단순한 최적화 장식이 아니라는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noexcept는 library에게 “이 move는 실패하지 않는다”는 contract를 제공하고, container는 그 contract를 바탕으로 더 공격적인 move 전략을 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::vector는 element를 연속된 memory에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;capacity가 부족한 상태에서 새 element가 들어오면 더 큰 memory buffer를 만들고, 기존 element를 새 buffer로 옮긴 뒤 기존 buffer를 정리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 기존 element를 move하다가 예외가 발생하면 vector는 중간 상태를 복구해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Copy는 원본 element를 그대로 둔 채 새 객체를 만들기 때문에 rollback 관점에서 더 다루기 쉬운 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;조건&lt;/td&gt;
&lt;td&gt;container가 보기 쉬운 선택&lt;/td&gt;
&lt;td&gt;이유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;move constructor가 noexcept&lt;/td&gt;
&lt;td&gt;move&lt;/td&gt;
&lt;td&gt;move 중 예외가 없다고 선언되어 rollback 부담이 작음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;move constructor가 noexcept가 아님&lt;/td&gt;
&lt;td&gt;copy&lt;/td&gt;
&lt;td&gt;copy가 가능하면 기존 element를 보존하는 쪽이 안전할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copy constructor가 없음&lt;/td&gt;
&lt;td&gt;move&lt;/td&gt;
&lt;td&gt;copy 선택지가 없으므로 move를 사용할 수밖에 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;trivial type 또는 작은 type&lt;/td&gt;
&lt;td&gt;차이가 작을 수 있음&lt;/td&gt;
&lt;td&gt;copy/move 비용보다 allocation 비용이 더 클 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 standard library 구현은 이런 판단을 위해 std::move_if_noexcept 계열의 전략을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, move가 noexcept이면 move하고, 그렇지 않고 copy가 가능하면 copy를 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 move constructor를 직접 작성했다면 noexcept를 붙일 수 있는지 반드시 검토해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 예제는 move constructor에 noexcept가 없는 타입과 있는 타입을 비교합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::vector에 element 하나를 넣고 capacity를 1로 고정한 뒤, 두 번째 element를 넣어 reallocation을 강제로 발생시킵니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;utility&amp;gt;
#include &amp;lt;vector&amp;gt;

struct RiskyMove {
    static int copies;
    static int moves;

    std::string value;

    explicit RiskyMove(std::string text)
        : value(std::move(text))
    {
    }

    RiskyMove(const RiskyMove&amp;amp; other)
        : value(other.value)
    {
        ++copies;
    }

    RiskyMove(RiskyMove&amp;amp;&amp;amp; other)
        : value(std::move(other.value))
    {
        ++moves;
    }
};

int RiskyMove::copies = 0;
int RiskyMove::moves = 0;

struct SafeMove {
    static int copies;
    static int moves;

    std::string value;

    explicit SafeMove(std::string text)
        : value(std::move(text))
    {
    }

    SafeMove(const SafeMove&amp;amp; other)
        : value(other.value)
    {
        ++copies;
    }

    SafeMove(SafeMove&amp;amp;&amp;amp; other) noexcept
        : value(std::move(other.value))
    {
        ++moves;
    }
};

int SafeMove::copies = 0;
int SafeMove::moves = 0;

template &amp;lt;typename T&amp;gt;
void trigger_reallocation()
{
    std::vector&amp;lt;T&amp;gt; items;
    items.reserve(1);

    items.emplace_back(&quot;first&quot;);
    items.emplace_back(&quot;second&quot;);
}

int main()
{
    trigger_reallocation&amp;lt;RiskyMove&amp;gt;();
    std::cout &amp;lt;&amp;lt; &quot;RiskyMove copies: &quot; &amp;lt;&amp;lt; RiskyMove::copies
              &amp;lt;&amp;lt; &quot;, moves: &quot; &amp;lt;&amp;lt; RiskyMove::moves &amp;lt;&amp;lt; '\n';

    trigger_reallocation&amp;lt;SafeMove&amp;gt;();
    std::cout &amp;lt;&amp;lt; &quot;SafeMove copies: &quot; &amp;lt;&amp;lt; SafeMove::copies
              &amp;lt;&amp;lt; &quot;, moves: &quot; &amp;lt;&amp;lt; SafeMove::moves &amp;lt;&amp;lt; '\n';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;RiskyMove copies: 1, moves: 0
SafeMove copies: 0, moves: 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RiskyMove는 move constructor가 있지만 noexcept가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 reallocation 중 기존 element를 옮길 때 copy가 선택되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SafeMove는 move constructor가 noexcept이므로 기존 element가 copy되지 않고 move되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실행 결과에서 봐야 할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제의 핵심은 새로 들어오는 second element가 아니라 기존에 있던 first element입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;items.reserve(1) 이후 items.emplace_back(&quot;first&quot;)를 호출하면 capacity가 1인 vector에 element 하나가 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 items.emplace_back(&quot;second&quot;)에서 capacity가 부족해져 reallocation이 발생하고, 기존 first element를 새 buffer로 옮겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;reserve(1)
capacity 1 확보

emplace_back(&quot;first&quot;)
기존 buffer에 first 생성

emplace_back(&quot;second&quot;)
capacity 부족
새 buffer 할당
기존 first element를 새 buffer로 재배치

RiskyMove
move constructor가 noexcept가 아님
copy constructor가 있으므로 copy 선택

SafeMove
move constructor가 noexcept
기존 element를 move
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과가 보여주는 것은 std::move 문법 자체가 아니라 container의 선택입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;move constructor가 존재해도 noexcept가 아니면 vector reallocation에서 copy가 선택될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그림으로 이해하기&lt;/h2&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/m1Lwb/dJMcaixswPz/jSGK5sD9YT9bfgxXTC9hbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/m1Lwb/dJMcaixswPz/jSGK5sD9YT9bfgxXTC9hbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/m1Lwb/dJMcaixswPz/jSGK5sD9YT9bfgxXTC9hbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fm1Lwb%2FdJMcaixswPz%2FjSGK5sD9YT9bfgxXTC9hbk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;std::vector는 reallocation 중 기존 element를 새 buffer로 옮길 때 exception safety를 고려해 copy 또는 move를 선택할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 중요한 분기점은 move constructor가 noexcept인지 여부입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noexcept가 없다면 container는 move 중 예외 가능성을 고려해야 하고, copy가 가능하면 copy 경로를 선택할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noexcept가 있다면 기존 element를 move해도 실패하지 않는다는 contract가 생기므로 reallocation 비용을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;move constructor가 있으면 std::vector는 항상 move한다.&lt;/b&gt;&lt;br /&gt;move constructor가 있어도 noexcept가 아니고 copy가 가능하면 reallocation에서 copy가 선택될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;noexcept는 성능과 관계없는 예외 명세일 뿐이다.&lt;/b&gt;&lt;br /&gt;noexcept는 library가 안전한 move를 선택할 수 있게 만드는 중요한 정보입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;std::move를 쓰면 무조건 move가 발생한다.&lt;/b&gt;&lt;br /&gt;std::move는 cast입니다. 실제로 어떤 constructor가 호출되는지는 overload resolution과 container의 내부 전략에 따라 달라집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;move가 copy보다 항상 빠르다.&lt;/b&gt;&lt;br /&gt;작은 type이나 trivial type에서는 차이가 거의 없을 수 있습니다. 하지만 resource를 가진 큰 object에서는 차이가 커질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;noexcept는 아무 move constructor에나 붙이면 된다.&lt;/b&gt;&lt;br /&gt;정말 예외를 던지지 않을 때만 붙여야 합니다. noexcept 함수에서 예외가 밖으로 나가면 std::terminate가 호출됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noexcept는 compiler와 standard library에게 보내는 약속입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약속할 수 없는 경우에는 붙이면 안 되지만, 약속할 수 있는 move constructor라면 붙이지 않는 것도 비용이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실무에서는 어떻게 볼까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 custom type을 container에 많이 넣는지부터 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::vector&amp;lt;T&amp;gt;에 T가 많이 들어가고, T가 string, vector, unique_ptr, buffer 같은 resource를 갖고 있다면 move constructor의 noexcept 여부를 확인할 가치가 큽니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 성능 문제가 있는 code path에서 vector growth가 자주 발생한다면 copy/move count를 측정해보는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;코드 리뷰 질문&lt;/td&gt;
&lt;td&gt;확인할 내용&lt;/td&gt;
&lt;td&gt;권장 방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;move constructor를 직접 작성했는가?&lt;/td&gt;
&lt;td&gt;내부 member들의 move가 예외를 던질 수 있는지 확인&lt;/td&gt;
&lt;td&gt;가능하면 noexcept 명시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::vector에 많이 들어가는 type인가?&lt;/td&gt;
&lt;td&gt;reallocation이 자주 발생하는지 확인&lt;/td&gt;
&lt;td&gt;reserve와 noexcept move를 함께 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;copy 비용이 큰가?&lt;/td&gt;
&lt;td&gt;string, buffer, nested container, handle 보유 여부 확인&lt;/td&gt;
&lt;td&gt;move 경로가 실제로 사용되는지 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;member type의 move가 noexcept인가?&lt;/td&gt;
&lt;td&gt;직접 작성하지 않은 member들의 noexcept 상태 확인&lt;/td&gt;
&lt;td&gt;defaulted move constructor의 noexcept 추론 활용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;noexcept를 거짓으로 선언했는가?&lt;/td&gt;
&lt;td&gt;예외 가능성이 있는데 noexcept를 붙였는지 확인&lt;/td&gt;
&lt;td&gt;잘못된 noexcept는 std::terminate 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 기본 전략은 직접 move constructor를 작성하지 않아도 되는 구조를 먼저 만드는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compiler-generated move constructor가 member들의 noexcept 상태를 바탕으로 적절히 noexcept를 추론할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 작성해야 한다면 함수 body에서 예외가 나갈 수 없는지 확인하고 noexcept를 명시하는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::vector는 reallocation 때 기존 element를 새 buffer로 옮겨야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;move constructor가 noexcept이면 vector가 기존 element를 move하기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;move constructor가 noexcept가 아니고 copy가 가능하면 copy가 선택될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;noexcept는 성능 최적화 힌트이면서 exception safety contract입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container에 많이 들어가는 custom type이라면 move constructor의 noexcept 여부를 코드 리뷰에서 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;

&lt;/div&gt;</description>
      <category>C++/Concepts</category>
      <category>C++</category>
      <category>c++17</category>
      <category>modern C++</category>
      <category>move constructor</category>
      <category>move_if_noexcept</category>
      <category>noexcept</category>
      <category>RAII</category>
      <category>reallocation</category>
      <category>std::move</category>
      <category>std::vector</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/773</guid>
      <comments>https://eunchanee.tistory.com/773#entry773comment</comments>
      <pubDate>Mon, 27 Jul 2026 09:00:46 +0900</pubDate>
    </item>
    <item>
      <title>Consistent Hashing, cache sharding에서 key 이동을 줄이는 방법</title>
      <link>https://eunchanee.tistory.com/772</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 커지면 하나의 cache node나 storage node에 모든 key를 넣기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 여러 node로 key를 나누는 sharding을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 방식은 hash(key) % node_count이지만, node가 추가되거나 제거되는 순간 많은 key가 한꺼번에 이동하는 문제가 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing은 이 문제를 줄이기 위해 hash space를 ring처럼 보고, key가 이동해야 하는 범위를 제한하는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing의 핵심은 node 수가 바뀌어도 전체 key를 다시 나누지 않고, 바뀐 node 주변의 key만 이동시키는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lBlMx/dJMcacKOgAR/4ipxaLkwck9J2qH9fKWDt1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lBlMx/dJMcacKOgAR/4ipxaLkwck9J2qH9fKWDt1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lBlMx/dJMcacKOgAR/4ipxaLkwck9J2qH9fKWDt1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlBlMx%2FdJMcacKOgAR%2F4ipxaLkwck9J2qH9fKWDt1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;Consistent hashing은 cache sharding, distributed storage, load distribution에서 node 변화에 따른 key 이동을 줄이기 위해 사용됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sharding은 데이터를 여러 node에 나눠 담는 방법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 user session cache를 Redis 여러 대에 나눠 저장하거나, image metadata를 여러 storage partition에 나눠 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 node 수가 고정되어 있지 않다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 늘면 node를 추가하고, 장애가 나면 node를 제거해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 key 이동이 너무 많으면 cache miss가 폭증하거나 storage migration 비용이 커집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;td&gt;아이디어&lt;/td&gt;
&lt;td&gt;node 변화 시 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modulo sharding&lt;/td&gt;
&lt;td&gt;hash(key) % node_count&lt;/td&gt;
&lt;td&gt;node_count가 바뀌면 대부분 key의 위치가 바뀜&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Range sharding&lt;/td&gt;
&lt;td&gt;key range를 node별로 나눔&lt;/td&gt;
&lt;td&gt;hot range가 생기면 특정 node에 부하 집중&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistent hashing&lt;/td&gt;
&lt;td&gt;hash ring에서 key가 시계 방향으로 만나는 node에 배치&lt;/td&gt;
&lt;td&gt;ring 설계와 virtual node 수를 잘 잡아야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing은 모든 문제를 해결하는 magic은 아니지만, node 증감이 잦은 distributed system에서 중요한 기본기입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 key를 어디에 둘지 정하는 방식은 성능과 장애 복구에 직접 영향을 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잘못된 sharding 방식은 평소에는 문제가 없어 보이다가 scale-out이나 failover 순간에 큰 비용을 만듭니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;node를 추가할 때 이동해야 하는 key 수를 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;cache cluster에서 대규모 cache miss를 줄이는 데 도움이 됩니다.&lt;/li&gt;
&lt;li&gt;storage나 queue partition을 늘릴 때 migration 범위를 제한할 수 있습니다.&lt;/li&gt;
&lt;li&gt;장애 node가 빠졌을 때 영향을 받는 key range를 이해하기 쉽습니다.&lt;/li&gt;
&lt;li&gt;면접에서 sharding, hash table, distributed cache를 함께 설명하기 좋은 주제입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 cache에서는 key 이동이 곧 cache miss로 이어질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;node를 3대에서 4대로 늘렸는데 대부분 key가 다른 node로 이동하면, scale-out 직후 backend DB에 부하가 몰릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing은 hash value를 일직선이 아니라 원형 ring으로 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node와 key를 같은 hash space 위에 올려두고, key는 시계 방향으로 가장 먼저 만나는 node에 배치합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ring의 끝까지 갔는데 node가 없으면 처음 위치로 돌아갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;개념&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;실무에서 보는 지점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash space&lt;/td&gt;
&lt;td&gt;key와 node가 배치되는 전체 범위&lt;/td&gt;
&lt;td&gt;hash function의 분포가 고르게 나와야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ring&lt;/td&gt;
&lt;td&gt;hash space의 끝과 시작을 연결한 구조&lt;/td&gt;
&lt;td&gt;마지막 range는 첫 node로 wrap around됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Successor node&lt;/td&gt;
&lt;td&gt;key 위치에서 시계 방향으로 처음 만나는 node&lt;/td&gt;
&lt;td&gt;key의 담당 node가 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual node&lt;/td&gt;
&lt;td&gt;하나의 물리 node를 ring 위 여러 지점에 배치&lt;/td&gt;
&lt;td&gt;부하 분산을 더 고르게 만들기 위해 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node가 추가되면 새 node 바로 이전 range의 key만 새 node로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node가 제거되면 그 node가 담당하던 key만 다음 successor node로 이동합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성이 consistent hashing의 핵심입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그림으로 이해하기&lt;/h2&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5eajk/dJMcaaTF6Rk/mKoDi75fQ44hYAVWizXIak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5eajk/dJMcaaTF6Rk/mKoDi75fQ44hYAVWizXIak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5eajk/dJMcaaTF6Rk/mKoDi75fQ44hYAVWizXIak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5eajk%2FdJMcaaTF6Rk%2FmKoDi75fQ44hYAVWizXIak%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;Consistent hashing에서는 새 node가 추가되어도 전체 key가 다시 섞이지 않고, 새 node 앞쪽의 일부 key range만 이동합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 중요한 점은 key가 node 개수로 나눠지는 것이 아니라 ring 위의 위치로 배치된다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 node_count가 3에서 4로 바뀌어도 모든 key의 modulo 결과를 다시 계산하는 방식과 다르게 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영향 범위가 새 node 주변으로 제한되기 때문에 cache warm-up과 migration 비용을 예측하기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;기존 ring
node A, node B, node C가 hash ring 위에 배치됨
key는 자기 위치에서 시계 방향으로 처음 만나는 node에 저장됨

node D 추가
node D가 ring 위 특정 위치에 들어옴
node D 바로 앞 range의 key만 node D로 이동
나머지 key는 기존 담당 node를 유지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접에서는 이 상태 변화를 말로 설명할 수 있어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;“node가 추가되면 일부 key만 이동한다”에서 멈추지 말고, “새 node가 ring에서 차지하는 위치 이전 range만 이동한다”까지 설명하면 더 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실무 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 예시는 distributed cache입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application server가 user_id나 session_id를 key로 hash하고, consistent hashing ring에서 담당 cache node를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 cache node를 하나 추가해도 전체 session key가 이동하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;Modulo sharding&lt;/td&gt;
&lt;td&gt;Consistent hashing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache node 1대 추가&lt;/td&gt;
&lt;td&gt;node_count가 바뀌어 많은 key 재배치&lt;/td&gt;
&lt;td&gt;새 node 주변 key range만 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache node 1대 장애&lt;/td&gt;
&lt;td&gt;전체 modulo 기준이 흔들릴 수 있음&lt;/td&gt;
&lt;td&gt;장애 node 담당 key가 다음 node로 이동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부하가 고르지 않음&lt;/td&gt;
&lt;td&gt;hash 분포와 key 특성에 의존&lt;/td&gt;
&lt;td&gt;virtual node로 분산 균형 조정 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 복잡도&lt;/td&gt;
&lt;td&gt;구현은 단순하지만 scale 변화에 약함&lt;/td&gt;
&lt;td&gt;ring 관리가 필요하지만 변화 비용이 작음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 consistent hashing만으로 충분하지 않은 경우도 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual node 수, replication factor, hot key 대응, node weight, health check, rebalancing 속도까지 함께 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. C++와 연결해보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 C++17 예제는 단순화된 hash ring을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 시스템에서는 더 좋은 hash function과 virtual node가 필요하지만, key가 successor node로 배치된다는 핵심은 이 코드로 확인할 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;map&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;utility&amp;gt;
#include &amp;lt;vector&amp;gt;

class HashRing {
public:
    void add_node(int position, std::string name)
    {
        ring_[position] = std::move(name);
    }

    std::string route(int key_hash) const
    {
        auto it = ring_.lower_bound(key_hash);
        if (it == ring_.end()) {
            it = ring_.begin();
        }
        return it-&amp;gt;second;
    }

private:
    std::map&amp;lt;int, std::string&amp;gt; ring_;
};

int toy_hash(const std::string&amp;amp; key)
{
    int value = 0;
    for (unsigned char ch : key) {
        value = (value * 31 + ch) % 600;
    }
    return value;
}

void print_routes(const HashRing&amp;amp; ring,
                  const std::vector&amp;lt;std::string&amp;gt;&amp;amp; keys)
{
    for (const auto&amp;amp; key : keys) {
        const int hash = toy_hash(key);
        std::cout &amp;lt;&amp;lt; key &amp;lt;&amp;lt; &quot; hash=&quot; &amp;lt;&amp;lt; hash
                  &amp;lt;&amp;lt; &quot; node=&quot; &amp;lt;&amp;lt; ring.route(hash) &amp;lt;&amp;lt; '\n';
    }
}

int main()
{
    const std::vector&amp;lt;std::string&amp;gt; keys = {
        &quot;user:12&quot;, &quot;session:88&quot;, &quot;product:7&quot;, &quot;cart:45&quot;
    };

    HashRing ring;
    ring.add_node(100, &quot;A&quot;);
    ring.add_node(300, &quot;B&quot;);
    ring.add_node(500, &quot;C&quot;);

    std::cout &amp;lt;&amp;lt; &quot;before adding D\n&quot;;
    print_routes(ring, keys);

    ring.add_node(150, &quot;D&quot;);

    std::cout &amp;lt;&amp;lt; &quot;\nafter adding D\n&quot;;
    print_routes(ring, keys);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;before adding D
user:12 hash=544 node=A
session:88 hash=12 node=A
product:7 hash=132 node=B
cart:45 hash=19 node=A

after adding D
user:12 hash=544 node=A
session:88 hash=12 node=A
product:7 hash=132 node=D
cart:45 hash=19 node=A
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 node D는 ring position 150에 추가되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hash 값이 100보다 크고 150 이하인 key는 D로 이동하고, 다른 key는 기존 node를 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예제에서는 product:7만 B에서 D로 이동하고, user:12, session:88, cart:45는 기존 node를 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 면접 질문 예시&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;hash(key) % node_count 방식은 node를 추가할 때 왜 문제가 되나요?&lt;/b&gt;&lt;br /&gt;node_count가 바뀌면 modulo 결과가 바뀌어 많은 key가 다른 node로 이동합니다. Cache에서는 대량 cache miss로 이어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Consistent hashing에서 node가 추가되면 어떤 key가 이동하나요?&lt;/b&gt;&lt;br /&gt;새 node가 ring 위에 들어간 위치를 기준으로, 그 node 바로 앞 range에 있던 key만 새 node로 이동합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Virtual node는 왜 필요한가요?&lt;/b&gt;&lt;br /&gt;물리 node를 ring 위 여러 위치에 배치해 key 분포를 더 고르게 만들고, node별 capacity 차이를 weight로 반영하기 위해 사용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변할 때는 “key 이동이 적다”는 장점만 말하기보다, ring 구조, successor node, virtual node, hot key 한계까지 함께 언급하는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실무에서는 어떻게 볼까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing을 도입할 때는 algorithm 자체보다 운영 조건이 더 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hash ring이 잘 설계되어도 특정 key에 요청이 몰리는 hot key 문제는 별도로 해결해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 cache node가 서로 다른 성능을 갖는다면 virtual node 개수나 weight를 조절해야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;운영 질문&lt;/td&gt;
&lt;td&gt;확인할 내용&lt;/td&gt;
&lt;td&gt;대응 방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key 분포가 고른가?&lt;/td&gt;
&lt;td&gt;특정 node에 key나 traffic이 몰리는지 확인&lt;/td&gt;
&lt;td&gt;virtual node 수와 hash function 점검&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hot key가 있는가?&lt;/td&gt;
&lt;td&gt;일부 key에 요청이 집중되는지 확인&lt;/td&gt;
&lt;td&gt;replication, local cache, request coalescing 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node weight가 다른가?&lt;/td&gt;
&lt;td&gt;node별 CPU, memory, network capacity 차이 확인&lt;/td&gt;
&lt;td&gt;weighted virtual node 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장애 복구가 빠른가?&lt;/td&gt;
&lt;td&gt;node 제거 후 traffic이 어느 node로 몰리는지 확인&lt;/td&gt;
&lt;td&gt;replication factor와 health check 조정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration 속도를 제어하는가?&lt;/td&gt;
&lt;td&gt;새 node 추가 시 backend 부하가 증가하는지 확인&lt;/td&gt;
&lt;td&gt;gradual rollout과 rate limit 적용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing은 “key 이동을 줄이는 배치 전략”이지 “부하 문제를 자동으로 해결하는 시스템”은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 monitoring, replication, health check와 함께 설계해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consistent hashing은 node 변화가 있을 때 key 이동 범위를 줄이는 sharding 기법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key와 node를 같은 hash ring 위에 두고, key는 시계 방향 successor node에 배치됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node 추가 시 새 node 주변 range의 key만 이동하므로 cache miss와 migration 비용을 줄일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Virtual node는 key 분포를 고르게 만들고 node weight를 반영하는 데 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 hot key, replication, health check, gradual migration까지 함께 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>CS</category>
      <category>cache</category>
      <category>consistent hashing</category>
      <category>CS</category>
      <category>Distributed System</category>
      <category>Hash Ring</category>
      <category>Load Distribution</category>
      <category>REDIS</category>
      <category>sharding</category>
      <category>system design</category>
      <category>Virtual Node</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/772</guid>
      <comments>https://eunchanee.tistory.com/772#entry772comment</comments>
      <pubDate>Sat, 25 Jul 2026 21:30:49 +0900</pubDate>
    </item>
    <item>
      <title>C++ lock ownership 제대로 이해하기</title>
      <link>https://eunchanee.tistory.com/771</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++에서 mutex를 직접 lock하고 unlock하는 코드는 짧아 보이지만, 예외나 early return이 끼어드는 순간 실수하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 실무 C++에서는 mutex를 직접 관리하기보다 RAII object로 lock ownership을 관리하는 방식을 자주 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 도구가 std::lock_guard와 std::unique_lock입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 다 scope가 끝날 때 unlock을 보장하지만, 사용할 수 있는 상황과 표현하는 의도는 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard는 단순한 scope lock이고, std::unique_lock은 lock ownership을 상태로 들고 다니는 더 유연한 RAII lock object입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
 &lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMEpvH/dJMcabLVqax/oWbkJQE0yVqCh5GU0HiKik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMEpvH/dJMcabLVqax/oWbkJQE0yVqCh5GU0HiKik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMEpvH/dJMcabLVqax/oWbkJQE0yVqCh5GU0HiKik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMEpvH%2FdJMcabLVqax%2FoWbkJQE0yVqCh5GU0HiKik%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;lock_guard와 unique_lock의 차이는 성능보다 lock ownership을 얼마나 유연하게 다뤄야 하는지에서 시작됩니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mutex는 공유 자원에 동시에 접근하는 코드를 보호하기 위한 기본 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 mutex.lock()과 mutex.unlock()을 직접 호출하면 unlock을 빠뜨리는 실수가 생길 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예외가 발생하거나 함수가 중간에 return되면 더 위험합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++의 RAII lock object는 이런 문제를 줄이기 위해 lock과 unlock을 객체의 lifetime에 묶습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;std::lock_guard&lt;/td&gt;
&lt;td&gt;std::unique_lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기본 성격&lt;/td&gt;
&lt;td&gt;가볍고 단순한 scope lock&lt;/td&gt;
&lt;td&gt;lock ownership을 상태로 관리하는 lock object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생성 시 lock&lt;/td&gt;
&lt;td&gt;기본적으로 즉시 lock&lt;/td&gt;
&lt;td&gt;즉시 lock, 지연 lock, 이미 lock된 mutex 인수 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중간 unlock&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ownership 이동&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;td&gt;move 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 용도&lt;/td&gt;
&lt;td&gt;짧고 단순한 critical section&lt;/td&gt;
&lt;td&gt;condition_variable, try_lock, unlock/relock이 필요한 흐름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘 중 무엇을 써야 하는지는 “어느 쪽이 더 고급인가”가 아니라 “critical section이 단순한가”로 판단하는 편이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock 코드는 버그가 나면 재현이 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unlock 누락은 deadlock으로 이어질 수 있고, 너무 넓은 critical section은 성능 저하와 contention을 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 lock을 잡은 상태에서 logging, callback, I/O 같은 작업을 오래 수행하면 다른 thread가 불필요하게 대기할 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;lock 범위를 코드로 명확하게 표현할 수 있습니다.&lt;/li&gt;
&lt;li&gt;예외나 early return이 있어도 scope 종료 시 unlock을 보장할 수 있습니다.&lt;/li&gt;
&lt;li&gt;critical section을 짧게 유지하는 습관을 만들 수 있습니다.&lt;/li&gt;
&lt;li&gt;condition_variable처럼 lock ownership 이동이 필요한 API와 자연스럽게 연결할 수 있습니다.&lt;/li&gt;
&lt;li&gt;코드 리뷰에서 lock이 잡힌 구간과 풀린 구간을 더 쉽게 확인할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard는 “여기서부터 scope 끝까지 lock을 잡는다”는 의도를 가장 짧게 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::unique_lock은 “lock을 잡을 수도 있고, 나중에 잡거나 중간에 풀 수도 있다”는 상태 변화를 표현합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념: lock ownership&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 타입의 공통점은 destructor에서 unlock을 수행한다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이는 lock ownership을 얼마나 명시적으로 다룰 수 있는지입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard는 생성되면 lock을 잡고, scope가 끝날 때까지 ownership이 고정됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 std::unique_lock은 owns_lock()으로 현재 lock ownership을 확인할 수 있고, lock(), unlock(), try_lock()을 통해 상태를 바꿀 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;권장 타입&lt;/td&gt;
&lt;td&gt;이유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함수 시작부터 끝까지 같은 lock 보호&lt;/td&gt;
&lt;td&gt;std::lock_guard&lt;/td&gt;
&lt;td&gt;가장 단순하고 의도가 명확함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lock을 나중에 잡아야 함&lt;/td&gt;
&lt;td&gt;std::unique_lock&lt;/td&gt;
&lt;td&gt;std::defer_lock 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중간에 unlock 후 오래 걸리는 작업 수행&lt;/td&gt;
&lt;td&gt;std::unique_lock&lt;/td&gt;
&lt;td&gt;unlock()과 relock이 가능함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::condition_variable 사용&lt;/td&gt;
&lt;td&gt;std::unique_lock&lt;/td&gt;
&lt;td&gt;wait 중 lock release/reacquire가 필요함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lock object를 다른 함수로 넘겨야 함&lt;/td&gt;
&lt;td&gt;std::unique_lock&lt;/td&gt;
&lt;td&gt;move-only ownership 전달 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 먼저 std::lock_guard로 충분한지 확인하고, lock 상태를 바꿔야 하는 요구가 있을 때 std::unique_lock으로 넘어가는 순서가 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제는 같은 BankAccount 객체에서 std::lock_guard와 std::unique_lock을 각각 사용하는 모습을 보여줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard는 deposit처럼 lock 범위가 단순한 함수에 사용하고, std::unique_lock은 withdraw 후 audit을 lock 밖에서 수행하기 위해 중간 unlock을 사용합니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;mutex&amp;gt;
#include &amp;lt;string&amp;gt;

class BankAccount {
public:
    void deposit_with_lock_guard(int amount)
    {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
        balance_ += amount;
        std::cout &amp;lt;&amp;lt; &quot;lock_guard deposit: &quot; &amp;lt;&amp;lt; balance_ &amp;lt;&amp;lt; '\n';
    }

    bool withdraw_with_unique_lock(int amount)
    {
        std::unique_lock&amp;lt;std::mutex&amp;gt; lock(mutex_, std::defer_lock);

        std::cout &amp;lt;&amp;lt; std::boolalpha;
        std::cout &amp;lt;&amp;lt; &quot;unique_lock owns before lock: &quot;
                  &amp;lt;&amp;lt; lock.owns_lock() &amp;lt;&amp;lt; '\n';

        lock.lock();
        std::cout &amp;lt;&amp;lt; &quot;unique_lock owns after lock: &quot;
                  &amp;lt;&amp;lt; lock.owns_lock() &amp;lt;&amp;lt; '\n';

        if (balance_ &amp;lt; amount) {
            return false;
        }

        balance_ -= amount;

        lock.unlock();
        std::cout &amp;lt;&amp;lt; &quot;unique_lock owns after unlock: &quot;
                  &amp;lt;&amp;lt; lock.owns_lock() &amp;lt;&amp;lt; '\n';

        audit(&quot;withdraw finished&quot;);
        return true;
    }

    int balance() const
    {
        std::lock_guard&amp;lt;std::mutex&amp;gt; lock(mutex_);
        return balance_;
    }

private:
    void audit(const std::string&amp;amp; message) const
    {
        std::cout &amp;lt;&amp;lt; &quot;audit: &quot; &amp;lt;&amp;lt; message &amp;lt;&amp;lt; '\n';
    }

    mutable std::mutex mutex_;
    int balance_ = 0;
};

int main()
{
    BankAccount account;

    account.deposit_with_lock_guard(100);
    const bool ok = account.withdraw_with_unique_lock(40);

    std::cout &amp;lt;&amp;lt; &quot;withdraw result: &quot; &amp;lt;&amp;lt; ok &amp;lt;&amp;lt; '\n';
    std::cout &amp;lt;&amp;lt; &quot;final balance: &quot; &amp;lt;&amp;lt; account.balance() &amp;lt;&amp;lt; '\n';
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;lock_guard deposit: 100
unique_lock owns before lock: false
unique_lock owns after lock: true
unique_lock owns after unlock: false
audit: withdraw finished
withdraw result: true
final balance: 60
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;deposit_with_lock_guard()에서는 lock object가 만들어지는 순간 mutex를 잡고, 함수가 끝나면 자동으로 unlock됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;withdraw_with_unique_lock()에서는 std::defer_lock으로 lock을 미루고, lock.lock() 시점에 ownership을 얻습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;balance_를 수정한 뒤 lock.unlock()을 호출하므로 audit()은 mutex를 잡지 않은 상태에서 실행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실행 결과에서 봐야 할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출력에서 가장 중요한 부분은 owns_lock() 값의 변화입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::unique_lock은 현재 자신이 mutex ownership을 갖고 있는지 상태로 추적합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;std::unique_lock 생성
std::defer_lock 때문에 아직 mutex를 잡지 않음
owns_lock() == false

lock.lock() 호출
mutex ownership 획득
owns_lock() == true

balance_ 수정
lock.unlock() 호출
mutex ownership 해제
owns_lock() == false

audit() 실행
mutex를 잡지 않은 상태에서 오래 걸릴 수 있는 작업 수행
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard에는 이런 상태 변화 API가 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 제한이 단점만은 아닙니다. 오히려 단순한 critical section에서는 lock 범위를 바꿀 수 없다는 점이 코드의 의도를 더 분명하게 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그림으로 이해하기&lt;/h2&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FGp1Y/dJMcahefwYv/3lPfFXLdSrHJkSAgYTyNLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FGp1Y/dJMcahefwYv/3lPfFXLdSrHJkSAgYTyNLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FGp1Y/dJMcahefwYv/3lPfFXLdSrHJkSAgYTyNLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFGp1Y%2FdJMcahefwYv%2F3lPfFXLdSrHJkSAgYTyNLk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;std::lock_guard는 scope 시작부터 끝까지 단순하게 mutex를 잡고, std::unique_lock은 필요할 때 lock ownership 상태를 바꿀 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 봐야 할 차이는 unlock 시점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard는 scope가 끝날 때 unlock되는 구조라 중간에 critical section을 줄일 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::unique_lock은 필요한 작업만 mutex 안에서 수행하고, audit이나 I/O처럼 오래 걸릴 수 있는 작업은 unlock 이후로 뺄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;std::unique_lock이 항상 더 좋은 선택이다.&lt;/b&gt;&lt;br /&gt;더 유연한 도구일 뿐입니다. 단순한 scope lock에는 std::lock_guard가 더 읽기 쉽고 의도가 분명합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;std::lock_guard는 예외가 나면 unlock하지 못한다.&lt;/b&gt;&lt;br /&gt;std::lock_guard는 RAII object입니다. stack unwinding 중 destructor가 호출되면 mutex를 unlock합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;std::unique_lock을 쓰면 lock 범위가 자동으로 최적화된다.&lt;/b&gt;&lt;br /&gt;unlock 시점은 개발자가 직접 설계해야 합니다. unique_lock은 도구일 뿐 critical section을 줄여주지는 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;owns_lock()이 true이면 공유 데이터가 항상 안전하다.&lt;/b&gt;&lt;br /&gt;현재 lock object가 mutex를 소유한다는 뜻입니다. 보호해야 할 데이터와 mutex의 대응 관계가 코드에서 일관되어야 안전합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mutex를 오래 잡아도 correctness만 맞으면 괜찮다.&lt;/b&gt;&lt;br /&gt;correctness는 필요조건입니다. lock을 오래 잡으면 contention이 늘고 tail latency가 나빠질 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock 관련 코드는 “동작한다”보다 “어디까지가 critical section인지 분명한가”를 기준으로 읽어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실무에서는 어떻게 볼까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 코드 리뷰에서는 먼저 lock 범위를 봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공유 데이터 접근만 critical section 안에 있고, logging, callback, file I/O, network I/O 같은 작업이 lock 밖에 있는지 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 “thread-safe하다”는 말만으로는 부족합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;코드 리뷰 질문&lt;/td&gt;
&lt;td&gt;확인할 내용&lt;/td&gt;
&lt;td&gt;권장 방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lock 범위가 짧은가?&lt;/td&gt;
&lt;td&gt;공유 데이터 접근 외 작업이 critical section에 들어갔는지 확인&lt;/td&gt;
&lt;td&gt;필요하면 std::unique_lock으로 중간 unlock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;unlock 누락 가능성이 있는가?&lt;/td&gt;
&lt;td&gt;직접 mutex.lock(), mutex.unlock()을 호출하는지 확인&lt;/td&gt;
&lt;td&gt;RAII lock object 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;condition_variable을 쓰는가?&lt;/td&gt;
&lt;td&gt;wait 중 lock release/reacquire가 필요한지 확인&lt;/td&gt;
&lt;td&gt;std::unique_lock 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lock object를 함수 밖으로 넘기는가?&lt;/td&gt;
&lt;td&gt;ownership 이동이 필요한지 확인&lt;/td&gt;
&lt;td&gt;std::unique_lock을 move-only object로 다룸&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;성능 문제가 있는가?&lt;/td&gt;
&lt;td&gt;lock 횟수보다 lock을 잡는 시간과 contention을 먼저 측정&lt;/td&gt;
&lt;td&gt;critical section 축소, shard, lock-free 구조 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 원칙은 단순합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock을 잡고 바로 공유 데이터를 수정한 뒤 scope가 끝나면 std::lock_guard가 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lock을 늦게 잡거나, 중간에 풀거나, condition_variable과 함께 써야 한다면 std::unique_lock이 적합합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard와 std::unique_lock은 모두 mutex unlock을 RAII로 보장하는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::lock_guard는 짧고 단순한 critical section에 가장 잘 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::unique_lock은 lock ownership을 상태로 관리해야 할 때 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;condition_variable, defer_lock, try_lock, 중간 unlock이 필요하면 std::unique_lock을 검토합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 lock 타입보다 critical section 범위와 unlock 시점을 명확히 하는 것이 더 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>C++/Concepts</category>
      <category>C++</category>
      <category>c++17</category>
      <category>Concurrency</category>
      <category>critical section</category>
      <category>modern C++</category>
      <category>mutex</category>
      <category>RAII</category>
      <category>std::lock_guard</category>
      <category>std::unique_lock</category>
      <category>thread safety</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/771</guid>
      <comments>https://eunchanee.tistory.com/771#entry771comment</comments>
      <pubDate>Sat, 25 Jul 2026 21:26:38 +0900</pubDate>
    </item>
    <item>
      <title>GitHub Code Quality GA, AI 시대의 코드 리뷰는 어떻게 바뀔까?</title>
      <link>https://eunchanee.tistory.com/770</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI coding tool이 코드를 더 빠르게 만들어주는 시대가 되면서, 팀이 마주하는 질문도 조금 바뀌고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제는 “코드를 얼마나 빨리 만들 수 있는가”뿐 아니라 “빨리 만들어진 코드를 merge 전에 얼마나 안정적으로 검증할 수 있는가”가 중요해졌습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub는 2026년 7월 20일 GitHub Code Quality를 GitHub Enterprise Cloud와 GitHub Team에서 generally available로 전환했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 이번 발표의 핵심 내용, 개발팀에 미치는 영향, 그리고 실제 도입 전에 확인해야 할 지점을 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Code Quality GA의 핵심은 CodeQL 기반 deterministic analysis와 AI-assisted detection, Copilot Autofix를 pull request 품질 관리 흐름에 묶었다는 점입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lzbcg/dJMcaglYJLh/I4nzZExeiITkWDWLHnRC00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lzbcg/dJMcaglYJLh/I4nzZExeiITkWDWLHnRC00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lzbcg/dJMcaglYJLh/I4nzZExeiITkWDWLHnRC00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flzbcg%2FdJMcaglYJLh%2FI4nzZExeiITkWDWLHnRC00%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #666666; font-size: 14px; margin-top: 8px;&quot; data-ke-size=&quot;size14&quot;&gt;GitHub Code Quality는 pull request 단계에서 maintainability, reliability, coverage, AI-assisted fix 흐름을 함께 다루려는 제품입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 세 줄 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Code Quality가 2026년 7월 20일 GitHub Enterprise Cloud와 GitHub Team에서 GA가 되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CodeQL 기반 deterministic analysis, AI-assisted detection, Copilot Autofix, organization dashboard, code coverage metric, ruleset 기반 quality gate를 묶어 pull request 품질 관리를 강화합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 standalone paid product로 운영되며 active committer license, GitHub Actions minutes, AI credit usage가 함께 비용에 영향을 주므로 도입 전 repository scope와 billing boundary를 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 무슨 일이 있었나?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub는 Code Quality를 public preview에서 일반 제공 단계로 전환했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 발표에 따르면 Code Quality는 GitHub Enterprise Cloud와 GitHub Team에서 사용할 수 있고, GitHub Enterprise Server에서는 출시 시점에 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제품 방향은 단순 lint보다 넓습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CodeQL의 deterministic analysis로 maintainability와 reliability issue를 찾고, AI-assisted detection과 Copilot Autofix로 수정 제안을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub는 자사 engineering organization에서 Code Quality finding의 67.3%가 pull request merge 전에 해결된다고 밝혔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;항목&lt;/td&gt;
&lt;td&gt;내용&lt;/td&gt;
&lt;td&gt;개발팀에서 보는 지점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출시 상태&lt;/td&gt;
&lt;td&gt;Generally available&lt;/td&gt;
&lt;td&gt;Preview 실험 단계가 아니라 유료 제품 운영 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;지원 대상&lt;/td&gt;
&lt;td&gt;GitHub Enterprise Cloud, GitHub Team&lt;/td&gt;
&lt;td&gt;Enterprise Server 조직은 출시 시점 기준 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;분석 방식&lt;/td&gt;
&lt;td&gt;CodeQL deterministic analysis + AI-assisted detection&lt;/td&gt;
&lt;td&gt;규칙 기반 분석과 AI 보조 탐지를 함께 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;수정 지원&lt;/td&gt;
&lt;td&gt;Copilot Autofix suggestion&lt;/td&gt;
&lt;td&gt;제안은 merge 전 사람이 검토해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 기능&lt;/td&gt;
&lt;td&gt;Org dashboard, coverage metric, ruleset quality gate, API&lt;/td&gt;
&lt;td&gt;팀 단위 rollout과 정책화가 쉬워짐&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 발표는 “AI code review가 좋아졌다” 정도로만 보면 부족합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 AI가 코드를 많이 만들수록 품질 검증과 비용 관리도 product workflow 안으로 들어오고 있다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 개발 환경에서는 AI가 boilerplate, test, refactor, migration code를 빠르게 만들어줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 code output이 늘어나면 reviewer가 봐야 할 diff도 늘고, 사람이 놓칠 수 있는 maintainability issue도 늘어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Code Quality는 이 지점에서 pull request 단계의 guardrail 역할을 노립니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;PR에서 maintainability와 reliability finding을 바로 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Code coverage metric을 기존 Cobertura XML test report와 연결해 PR에 표시할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Ruleset을 통해 coverage threshold 같은 quality gate를 merge 정책에 포함할 수 있습니다.&lt;/li&gt;
&lt;li&gt;Organization-wide dashboard로 여러 repository의 품질 상태를 한 번에 볼 수 있습니다.&lt;/li&gt;
&lt;li&gt;AI-assisted finding과 Copilot Autofix를 통해 수정 후보를 빠르게 검토할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자 입장에서는 code review가 더 자동화된다는 기대가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 팀 리더나 platform engineer 입장에서는 “어디까지 자동 제안을 신뢰할 것인가”, “어떤 repository에 켤 것인가”, “비용을 어떻게 예측할 것인가”가 새로운 운영 문제가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 개발자에게 미치는 영향&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 직접적인 변화는 PR 품질 검증이 더 productized된다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에도 lint, test, CodeQL, custom CI rule을 조합할 수 있었지만, Code Quality는 그 결과를 GitHub pull request와 organization dashboard 안에 더 직접적으로 노출합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 변화는 개발자에게 다음과 같은 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;대상&lt;/td&gt;
&lt;td&gt;좋아질 수 있는 점&lt;/td&gt;
&lt;td&gt;주의할 점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개별 개발자&lt;/td&gt;
&lt;td&gt;PR에서 수정 후보를 빠르게 확인&lt;/td&gt;
&lt;td&gt;Autofix suggestion을 그대로 믿지 말고 의도와 side effect를 검토해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviewer&lt;/td&gt;
&lt;td&gt;반복적인 maintainability issue를 tool이 먼저 잡아줄 수 있음&lt;/td&gt;
&lt;td&gt;Tool finding과 실제 domain risk를 구분해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engineering Manager&lt;/td&gt;
&lt;td&gt;Repository별 품질 추세와 coverage gate를 관리 가능&lt;/td&gt;
&lt;td&gt;숫자가 좋아져도 설계 품질이 자동 보장되지는 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform Team&lt;/td&gt;
&lt;td&gt;Organization-wide enablement와 API로 rollout 자동화 가능&lt;/td&gt;
&lt;td&gt;Active committer 기준 비용과 AI credit usage를 추적해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 AI가 만든 코드가 많아지는 팀에서는 “AI가 만든 코드를 AI가 다시 검토한다”는 흐름이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 중요한 것은 자동화된 finding을 merge 조건으로 바로 강제하기보다, 먼저 evaluate mode나 제한된 repository에서 false positive와 비용을 확인하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 관련 개발자/IT 실무자 관점에서 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 뉴스는 GitHub를 쓰는 모든 팀에 똑같이 중요한 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영향이 큰 팀은 GitHub Enterprise Cloud나 Team을 쓰면서 PR 기반 개발, CodeQL, coverage report, organization policy를 운영하는 팀입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 개인 프로젝트나 GitHub Enterprise Server 중심 조직은 영향이 제한적일 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도입 전에 특히 봐야 할 질문은 다음과 같습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;어떤 repository에 Code Quality를 켤 것인가?&lt;/li&gt;
&lt;li&gt;Active committer 수가 실제 비용에 얼마나 영향을 주는가?&lt;/li&gt;
&lt;li&gt;AI-assisted detection과 Copilot Autofix 사용량을 어떻게 제한하거나 모니터링할 것인가?&lt;/li&gt;
&lt;li&gt;CodeQL analysis가 사용하는 GitHub Actions minutes는 어느 정도인가?&lt;/li&gt;
&lt;li&gt;Coverage gate를 바로 차단 모드로 둘지, evaluate mode로 먼저 관찰할지 결정했는가?&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비용 모델도 단순하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Docs 기준으로 Code Quality 비용에는 active committer license, GitHub Actions minutes, GitHub AI Credits가 함께 들어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 “repository 하나에 켜는 기능”이 아니라 “조직 단위 품질 정책과 비용 정책”으로 접근하는 편이 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 앞으로 볼 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 발표 이후에는 기능 자체보다 실제 운영 품질을 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI-assisted finding이 얼마나 정확한지, Copilot Autofix가 실제 codebase context를 얼마나 잘 반영하는지, ruleset 기반 quality gate가 개발 속도를 과도하게 막지 않는지가 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 다음 지표를 관찰하면 좋습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;관찰 포인트&lt;/td&gt;
&lt;td&gt;왜 중요한가&lt;/td&gt;
&lt;td&gt;볼 수 있는 신호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positive 비율&lt;/td&gt;
&lt;td&gt;많으면 reviewer 피로도가 올라감&lt;/td&gt;
&lt;td&gt;Dismiss finding 증가, rule disable 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autofix 채택률&lt;/td&gt;
&lt;td&gt;제안이 실제로 쓸 만한지 보여줌&lt;/td&gt;
&lt;td&gt;제안 commit 비율, 추가 수정량&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge 지연 시간&lt;/td&gt;
&lt;td&gt;Quality gate가 개발 흐름을 막는지 확인&lt;/td&gt;
&lt;td&gt;PR lead time, blocked PR 수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용 추세&lt;/td&gt;
&lt;td&gt;AI usage와 active committer 비용 관리 필요&lt;/td&gt;
&lt;td&gt;AI credit usage, Actions minutes, license count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실제 장애 감소&lt;/td&gt;
&lt;td&gt;품질 도구의 최종 목표&lt;/td&gt;
&lt;td&gt;Regression, rollback, hotfix 빈도&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Code Quality는 review를 대체한다기보다, 사람이 더 중요한 판단에 집중하도록 반복적인 품질 신호를 앞단에서 정리하는 도구에 가깝습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도입 효과는 finding 수보다 실제 defect 감소와 reviewer 시간 절감으로 판단해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 사견&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 발표는 AI coding 시대의 자연스러운 다음 단계로 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 생성이 빨라지면 review와 품질 정책도 같은 속도로 따라가야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 AI-assisted quality tool은 “더 많은 finding”을 만들어내는 것보다 “팀이 실제로 고칠 finding”을 정확히 골라내는 것이 더 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 rollout 방식은 모든 repository에 한 번에 켜는 것이 아니라, critical repository 몇 개에서 evaluate mode로 시작해 false positive, Autofix 품질, 비용을 먼저 확인하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 이미 자체 lint, test, coverage gate, CodeQL workflow를 운영하는 팀이라면 Code Quality가 기존 CI와 어떤 역할을 나눌지 먼저 정리해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.blog/changelog/2026-07-20-github-code-quality-is-now-generally-available/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Changelog: GitHub Code Quality is now generally available&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/billing/concepts/product-billing/github-code-quality&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Docs: GitHub Code Quality billing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/code-security/tutorials/improve-code-quality/catch-issues-before-merge&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;GitHub Docs: Preventing code quality issues from reaching your default branch&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>IT News</category>
      <category>AI Code Review</category>
      <category>code review</category>
      <category>CodeQL</category>
      <category>Copilot Autofix</category>
      <category>Developer tools</category>
      <category>DevOps</category>
      <category>github</category>
      <category>GitHub Code Quality</category>
      <category>IT News</category>
      <category>software quality</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/770</guid>
      <comments>https://eunchanee.tistory.com/770#entry770comment</comments>
      <pubDate>Wed, 22 Jul 2026 08:48:26 +0900</pubDate>
    </item>
    <item>
      <title>DNS Lookup 흐름, Recursive Resolver부터 Authoritative DNS까지</title>
      <link>https://eunchanee.tistory.com/769</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 주소창에 도메인을 입력하면 애플리케이션은 곧바로 서버에 연결하는 것처럼 보입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로는 먼저 도메인 이름을 IP address로 바꾸는 DNS Lookup 과정이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정은 보통 매우 빠르게 끝나지만, 장애가 나면 “서버는 살아 있는데 접속이 안 된다”, “일부 사용자만 예전 서버로 간다”, “배포 후 트래픽 전환이 예상보다 늦다” 같은 문제로 나타납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘은 DNS Lookup이 어떤 순서로 일어나는지, Cache TTL이 왜 중요한지, 실무에서 어떤 지점을 봐야 하는지 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS Lookup은 도메인 이름을 IP address로 바꾸는 과정이고, TTL은 그 결과를 resolver와 client가 얼마나 오래 cache할 수 있는지 정하는 값입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnXXWH/dJMcaf8od3N/qCWCWVj2WKHxJnF3c3eRw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnXXWH/dJMcaf8od3N/qCWCWVj2WKHxJnF3c3eRw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnXXWH/dJMcaf8od3N/qCWCWVj2WKHxJnF3c3eRw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnXXWH%2FdJMcaf8od3N%2FqCWCWVj2WKHxJnF3c3eRw1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;DNS는 사람이 읽기 쉬운 domain name과 network가 사용하는 IP address 사이를 연결하는 이름 해석 계층입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. DNS Lookup은 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS Lookup은 domain name을 IP address로 변환하는 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 example.com 같은 이름으로 서버에 접속하려면, 먼저 그 이름이 어떤 IP address를 가리키는지 알아야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 운영체제의 resolver, recursive resolver, root name server, TLD name server, authoritative name server가 순서대로 관여할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상 모든 단계가 매번 실행되는 것은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache에 이미 답이 있으면 중간 단계에서 바로 결과를 돌려줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구성 요소&lt;/td&gt;
&lt;td&gt;역할&lt;/td&gt;
&lt;td&gt;실무에서 보는 지점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stub Resolver&lt;/td&gt;
&lt;td&gt;Client OS나 runtime이 DNS 요청을 시작&lt;/td&gt;
&lt;td&gt;local cache, hosts file, resolver 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive Resolver&lt;/td&gt;
&lt;td&gt;Client 대신 DNS 계층을 따라가며 답을 찾음&lt;/td&gt;
&lt;td&gt;ISP DNS, public DNS, VPC DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Root Name Server&lt;/td&gt;
&lt;td&gt;TLD server 위치를 알려줌&lt;/td&gt;
&lt;td&gt;com, net, kr 같은 상위 영역으로 안내&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLD Name Server&lt;/td&gt;
&lt;td&gt;해당 domain의 authoritative server를 알려줌&lt;/td&gt;
&lt;td&gt;등록기관, zone delegation 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoritative Name Server&lt;/td&gt;
&lt;td&gt;실제 DNS record의 최종 답을 제공&lt;/td&gt;
&lt;td&gt;A, AAAA, CNAME, MX, TXT record 관리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 하나의 중앙 서버가 모든 domain을 알고 있는 구조가 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계층적으로 위임된 name server들이 협력해 최종 record를 찾아주는 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 application code 밖의 문제처럼 보이지만, 실제 장애와 성능에 자주 영향을 줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 요청 latency, CDN 전환, blue-green deployment, 장애 조치, multi-region routing, Kubernetes service discovery 모두 이름 해석과 연결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 TTL을 이해하지 못하면 DNS 변경이 즉시 반영되지 않는 이유를 설명하기 어렵습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DNS cache가 있으면 매 요청마다 authoritative server까지 가지 않습니다.&lt;/li&gt;
&lt;li&gt;TTL이 길면 query traffic은 줄지만 변경 전파가 늦어질 수 있습니다.&lt;/li&gt;
&lt;li&gt;TTL이 짧으면 전환은 빨라지지만 resolver query가 늘어날 수 있습니다.&lt;/li&gt;
&lt;li&gt;CNAME chain이 길면 lookup 단계와 장애 지점이 늘어날 수 있습니다.&lt;/li&gt;
&lt;li&gt;DNS 결과가 바뀌어도 이미 열려 있는 TCP connection은 자동으로 새 IP로 바뀌지 않습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 DNS record만 바꾸고 끝내면 안 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client cache, resolver cache, connection pool, retry, health check까지 함께 봐야 실제 트래픽이 어떻게 이동하는지 설명할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념: TTL과 Cache&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 Time To Live의 약자입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS record를 받은 resolver가 그 답을 cache해도 되는 시간을 뜻합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 A record의 TTL이 300 seconds라면, resolver는 해당 결과를 최대 300초 동안 재사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시간 안에는 authoritative name server에 다시 묻지 않고 cache된 IP address를 반환할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TTL 설정&lt;/td&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;주의할 점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;긴 TTL&lt;/td&gt;
&lt;td&gt;DNS query 감소, resolver cache 효율 증가&lt;/td&gt;
&lt;td&gt;IP 변경, 장애 조치, 배포 전환 반영이 늦을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;짧은 TTL&lt;/td&gt;
&lt;td&gt;record 변경 반영이 비교적 빠름&lt;/td&gt;
&lt;td&gt;DNS query 증가, resolver와 authoritative server 부담 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;너무 짧은 TTL&lt;/td&gt;
&lt;td&gt;빠른 전환을 기대할 수 있음&lt;/td&gt;
&lt;td&gt;일부 resolver가 최소 TTL을 적용하거나 application cache가 별도로 동작할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 “이 시간이 지나면 모든 사용자가 새 IP를 본다”는 보장이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resolver 구현, client runtime, browser cache, mobile network, enterprise proxy가 중간에 별도 cache 정책을 가질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 DNS 전환은 보통 TTL보다 조금 더 보수적으로 관찰해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 그림으로 이해하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS Lookup과 Cache TTL 흐름을 단순화하면 다음처럼 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMJPu2/dJMcaaMRMXa/vvTgKFUmW5e8ix9BJiUKhK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMJPu2/dJMcaaMRMXa/vvTgKFUmW5e8ix9BJiUKhK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMJPu2/dJMcaaMRMXa/vvTgKFUmW5e8ix9BJiUKhK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMJPu2%2FdJMcaaMRMXa%2FvvTgKFUmW5e8ix9BJiUKhK%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;처음 조회에서는 DNS 계층을 따라가며 authoritative answer를 찾고, 이후 TTL 안에서는 resolver cache가 같은 답을 빠르게 반환할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 lookup은 여러 단계를 거칠 수 있지만, 한 번 답을 얻은 뒤에는 cache가 큰 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client나 recursive resolver가 cache hit를 얻으면 root, TLD, authoritative server까지 가지 않아도 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 TTL이 만료되면 resolver는 다시 authoritative path를 통해 최신 답을 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 실무 예시: 배포 전 DNS 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 새 load balancer나 새 region으로 옮길 때 DNS record를 바꾸는 경우가 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 TTL이 길게 설정되어 있으면 일부 resolver는 기존 IP address를 계속 반환할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 중요한 전환 전에는 TTL을 미리 낮추고, 충분히 기다린 뒤 record를 바꾸는 절차를 잡기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;전환 1일 전
TTL을 3600 seconds에서 300 seconds로 낮춤

전환 시점
A record를 old IP에서 new IP로 변경

전환 직후
일부 resolver는 아직 old IP를 반환할 수 있음
새로 조회하거나 TTL이 만료된 resolver는 new IP를 반환

관찰 기간
old IP 트래픽이 충분히 줄어드는지 access log와 load balancer metric 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 DNS 변경과 실제 traffic migration이 같은 순간에 일어나지 않는다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 DNS 결과가 새 IP로 바뀌어도 기존 keep-alive connection이나 connection pool은 old server에 남아 있을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 DNS 전환 계획에는 old endpoint 유지 시간, draining, health check, rollback 기준이 함께 들어가야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 네트워크와 운영체제 관점에서 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS Lookup은 application, OS, network가 만나는 지점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Application은 보통 domain name만 넘기지만, 실제 이름 해석은 OS resolver 설정과 runtime library를 통해 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Container 환경에서는 host의 resolver 설정, Kubernetes DNS, VPC DNS, service mesh 설정도 함께 영향을 줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;계층&lt;/td&gt;
&lt;td&gt;확인할 것&lt;/td&gt;
&lt;td&gt;문제가 되는 경우&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;DNS cache, connection pool, retry 정책&lt;/td&gt;
&lt;td&gt;새 IP를 받아도 기존 connection을 계속 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS Resolver&lt;/td&gt;
&lt;td&gt;resolver 설정, hosts file, local cache&lt;/td&gt;
&lt;td&gt;예상과 다른 DNS server에 질의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recursive Resolver&lt;/td&gt;
&lt;td&gt;cache TTL, negative caching, response policy&lt;/td&gt;
&lt;td&gt;일부 사용자만 다른 답을 받음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authoritative DNS&lt;/td&gt;
&lt;td&gt;record, delegation, zone serial, health check&lt;/td&gt;
&lt;td&gt;최종 답 자체가 잘못되거나 region별 답이 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;UDP/TCP 53, firewall, DoH, split-horizon DNS&lt;/td&gt;
&lt;td&gt;사내망과 외부망에서 결과가 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 장애를 볼 때는 “도메인이 안 된다”에서 멈추면 원인을 좁히기 어렵습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 resolver에 물었는지, cache hit인지, authoritative answer가 맞는지, 실제 connection이 어느 IP로 열렸는지 단계별로 나눠 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;DNS record를 바꾸면 모든 사용자가 즉시 새 IP로 간다.&lt;/b&gt;&lt;br /&gt;Resolver와 client cache 때문에 일정 시간 동안 old IP와 new IP가 함께 관찰될 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL을 0으로 두면 cache 문제가 사라진다.&lt;/b&gt;&lt;br /&gt;일부 resolver는 최소 TTL을 적용할 수 있고, application이나 OS가 별도 cache를 가질 수 있습니다. Query 증가 비용도 고려해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNS Lookup이 끝나면 이후 요청은 항상 같은 IP로 간다.&lt;/b&gt;&lt;br /&gt;Load balancing, multiple A records, DNS-based routing, connection pool 정책에 따라 요청이 다른 IP로 갈 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CNAME은 단순한 별칭이라 비용이 없다.&lt;/b&gt;&lt;br /&gt;CNAME chain이 길면 추가 lookup이 필요하고, chain 중간의 record나 provider 장애가 영향을 줄 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버가 살아 있으면 DNS 문제는 아니다.&lt;/b&gt;&lt;br /&gt;Authoritative DNS, recursive resolver, delegation, firewall, local resolver 설정이 잘못되면 서버가 정상이어도 사용자는 접속하지 못할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 단순한 이름표가 아니라 cache와 위임이 결합된 distributed system입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 변경 전파와 장애 분석에서는 시간을 두고 관찰하는 태도가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 면접 질문 예시&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS는 네트워크 기초와 실무 장애 대응 감각을 함께 묻기 좋은 주제입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;브라우저에 domain을 입력했을 때 DNS Lookup은 어떤 순서로 일어나나요?&lt;/b&gt;&lt;br /&gt;Client cache, OS resolver, recursive resolver, root, TLD, authoritative name server 순서로 설명하고, cache hit가 있으면 일부 단계가 생략될 수 있다고 말하면 좋습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TTL은 무엇이고 왜 중요한가요?&lt;/b&gt;&lt;br /&gt;DNS 응답을 cache할 수 있는 시간을 뜻합니다. 긴 TTL은 query를 줄이지만 변경 반영이 늦고, 짧은 TTL은 전환은 빠르지만 query 부담이 늘 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DNS record를 바꿨는데 일부 사용자만 예전 서버로 접속하는 이유는 무엇일까요?&lt;/b&gt;&lt;br /&gt;Recursive resolver나 client cache가 old answer를 TTL 동안 유지할 수 있고, 이미 열린 connection pool이 old IP를 계속 사용할 수도 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 답변은 DNS 서버 이름을 나열하는 데서 끝나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache, TTL, connection reuse, 배포 전환, 장애 분석 순서까지 연결할 수 있어야 실무적인 답변이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS Lookup은 domain name을 IP address로 바꾸는 이름 해석 과정입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Recursive resolver는 client 대신 DNS 계층을 따라가며 authoritative answer를 찾습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL은 DNS 응답을 cache할 수 있는 시간이며, 성능과 변경 전파 속도 사이의 trade-off를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS record 변경은 즉시 모든 traffic 전환을 보장하지 않으며, cache와 connection pool을 함께 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 장애 분석에서는 client, resolver, authoritative DNS, 실제 connection IP를 단계별로 분리해 확인해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DNS 문제를 볼 때는 “record가 맞는가”보다 “누가, 언제, 어떤 cache를 기준으로 답을 받는가”를 먼저 확인해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>CS</category>
      <category>Authoritative DNS</category>
      <category>backend</category>
      <category>Cache TTL</category>
      <category>CS</category>
      <category>DNS</category>
      <category>DNS Lookup</category>
      <category>domain</category>
      <category>network</category>
      <category>Recursive Resolver</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/769</guid>
      <comments>https://eunchanee.tistory.com/769#entry769comment</comments>
      <pubDate>Tue, 21 Jul 2026 08:47:40 +0900</pubDate>
    </item>
    <item>
      <title>if constexpr는 일반 if와 무엇이 다를까?</title>
      <link>https://eunchanee.tistory.com/768</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Template 코드를 처음 작성하면 타입에 따라 다른 코드를 실행하고 싶어지는 순간이 자주 옵니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 숫자 타입은 std::to_string으로 처리하고, std::string은 size를 함께 출력하고, std::vector는 front 값을 읽고 싶은 식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++17 이전에는 이런 분기를 SFINAE, tag dispatch, partial specialization으로 풀어야 하는 경우가 많았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++17의 if constexpr는 이런 compile-time branch를 함수 본문 안에서 더 직접적으로 표현할 수 있게 해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr의 핵심은 조건이 compile time에 결정되면 선택되지 않은 branch가 해당 template instantiation에서 버려진다는 점입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/t5Jxw/dJMcaic6VWl/UwwJzDnWrwpnOD1gunkWvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/t5Jxw/dJMcaic6VWl/UwwJzDnWrwpnOD1gunkWvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/t5Jxw/dJMcaic6VWl/UwwJzDnWrwpnOD1gunkWvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ft5Jxw%2FdJMcaic6VWl%2FUwwJzDnWrwpnOD1gunkWvk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;if constexpr는 template 코드에서 타입 조건에 따라 필요한 branch만 instantiation하도록 도와줍니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. if constexpr는 무엇인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr는 C++17에 추가된 compile-time conditional statement입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 if와 문법은 비슷하지만, 조건식이 compile time에 평가될 수 있어야 한다는 점이 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 true이면 true branch가 사용되고, false이면 false branch가 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Template context에서는 선택되지 않은 branch가 해당 instantiation에서 discarded statement로 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 덕분에 어떤 타입에는 존재하지 않는 member function이나 operator를 다른 branch에 두고도 안전하게 코드를 구성할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;일반 if&lt;/td&gt;
&lt;td&gt;if constexpr&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조건 평가 시점&lt;/td&gt;
&lt;td&gt;runtime&lt;/td&gt;
&lt;td&gt;compile time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Template branch 처리&lt;/td&gt;
&lt;td&gt;두 branch가 모두 type check 대상이 되기 쉬움&lt;/td&gt;
&lt;td&gt;선택되지 않은 branch는 해당 instantiation에서 버려짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;주요 용도&lt;/td&gt;
&lt;td&gt;값에 따른 runtime 분기&lt;/td&gt;
&lt;td&gt;타입과 trait에 따른 compile-time 분기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 조합&lt;/td&gt;
&lt;td&gt;bool flag, runtime state&lt;/td&gt;
&lt;td&gt;type_traits, template parameter, constexpr value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr는 일반 if를 더 빠르게 만드는 문법이 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적은 runtime 최적화보다 template 코드의 표현력과 오류 범위를 줄이는 데 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Template 코드는 여러 타입에 대해 재사용되지만, 모든 타입이 같은 interface를 갖지는 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int에는 size member function이 없고, std::vector는 std::ostream에 바로 출력되지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이를 무시하고 하나의 함수 안에서 모든 코드를 일반 if로 처리하려고 하면 compile error가 쉽게 발생합니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;타입별 처리 코드를 함수 본문 안에서 읽기 쉽게 모을 수 있습니다.&lt;/li&gt;
&lt;li&gt;SFINAE나 overload set이 과하게 늘어나는 상황을 줄일 수 있습니다.&lt;/li&gt;
&lt;li&gt;선택되지 않은 branch의 타입 의존 코드를 해당 instantiation에서 제외할 수 있습니다.&lt;/li&gt;
&lt;li&gt;코드 리뷰에서 타입 조건과 실제 처리 흐름을 한 곳에서 확인하기 쉽습니다.&lt;/li&gt;
&lt;li&gt;Library code뿐 아니라 logging, serialization, debug helper 같은 실무 보조 코드에도 유용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 C++ 기초 문법은 알고 있지만 Modern C++ template 문법이 낯선 개발자에게 if constexpr는 좋은 진입점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 metaprogramming 기법을 쓰기 전에 타입 조건 분기를 명확하게 표현할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념: discarded statement&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr를 이해할 때 가장 중요한 표현은 discarded statement입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Template이 특정 타입으로 instantiation될 때, 조건에 맞지 않는 branch는 그 instantiation에서 버려집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 T가 int라면 std::vector 전용 branch가 버려질 수 있고, T가 std::vector&amp;lt;int&amp;gt;라면 stream 출력 branch가 버려질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 각 타입에 맞는 코드만 남아 compile됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;조건&lt;/td&gt;
&lt;td&gt;선택되는 branch&lt;/td&gt;
&lt;td&gt;버려지는 branch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T가 integral type&lt;/td&gt;
&lt;td&gt;std::to_string으로 숫자 처리&lt;/td&gt;
&lt;td&gt;std::string, std::vector 전용 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T가 std::string&lt;/td&gt;
&lt;td&gt;size와 문자열 내용 처리&lt;/td&gt;
&lt;td&gt;숫자 처리, std::vector 전용 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T가 std::vector&amp;lt;int&amp;gt;&lt;/td&gt;
&lt;td&gt;empty 확인 후 front 처리&lt;/td&gt;
&lt;td&gt;stream 출력 branch&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 선택되지 않은 branch가 완전히 아무 검사를 받지 않는다는 뜻은 아닙니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문법적으로 잘못된 코드는 여전히 문제가 될 수 있고, type-dependent expression인지 아닌지에 따라 error가 발생하는 시점이 달라질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 if constexpr는 “아무 코드나 숨기는 기능”이 아니라 “타입별로 의미 있는 branch를 안전하게 나누는 기능”으로 보는 편이 맞습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제는 타입에 따라 값을 설명하는 함수를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;숫자, std::string, std::vector&amp;lt;int&amp;gt;를 각각 다른 branch에서 처리합니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;type_traits&amp;gt;
#include &amp;lt;vector&amp;gt;

template &amp;lt;typename T&amp;gt;
void print_value(const T&amp;amp; value)
{
    if constexpr (std::is_integral_v&amp;lt;T&amp;gt;) {
        std::cout &amp;lt;&amp;lt; &quot;integral: &quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; '\n';
    } else if constexpr (std::is_floating_point_v&amp;lt;T&amp;gt;) {
        std::cout &amp;lt;&amp;lt; &quot;floating: &quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; '\n';
    } else if constexpr (std::is_same_v&amp;lt;T, std::string&amp;gt;) {
        std::cout &amp;lt;&amp;lt; &quot;string size: &quot; &amp;lt;&amp;lt; value.size()
                  &amp;lt;&amp;lt; &quot;, value: &quot; &amp;lt;&amp;lt; value &amp;lt;&amp;lt; '\n';
    } else if constexpr (std::is_same_v&amp;lt;T, std::vector&amp;lt;int&amp;gt;&amp;gt;) {
        if (value.empty()) {
            std::cout &amp;lt;&amp;lt; &quot;vector is empty\n&quot;;
        } else {
            std::cout &amp;lt;&amp;lt; &quot;vector first: &quot; &amp;lt;&amp;lt; value.front() &amp;lt;&amp;lt; '\n';
        }
    } else {
        std::cout &amp;lt;&amp;lt; &quot;unsupported type\n&quot;;
    }
}

int main()
{
    print_value(42);
    print_value(3.14);
    print_value(std::string{&quot;cache&quot;});
    print_value(std::vector&amp;lt;int&amp;gt;{10, 20, 30});
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;integral: 42
floating: 3.14
string size: 5, value: cache
vector first: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T가 int일 때는 integral branch만 선택됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T가 std::string일 때는 value.size()를 사용하는 branch가 선택됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T가 std::vector&amp;lt;int&amp;gt;일 때는 value.front()를 사용하는 branch가 선택됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 std::vector&amp;lt;int&amp;gt;를 std::cout에 직접 출력하는 branch가 instantiation되지 않는다는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 일반 if와 어떻게 다른가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 if는 runtime branch입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 runtime에 결정되므로 compiler는 기본적으로 두 branch의 코드가 모두 type-correct해야 한다고 봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 if constexpr는 조건이 compile time에 결정되므로, template instantiation에서 선택되지 않은 branch를 버릴 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이는 타입별로 서로 다른 interface를 호출할 때 크게 드러납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;print_value(42) 호출
T = int 로 template instantiation
std::is_integral_v&amp;lt;T&amp;gt; 조건이 true
integral branch만 남음
std::string branch와 std::vector&amp;lt;int&amp;gt; branch는 discarded statement
컴파일된 함수는 int 출력 코드만 포함
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름 덕분에 하나의 template 함수 안에 여러 타입 전용 처리를 모아둘 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 조건 자체가 runtime 값이면 if constexpr를 사용할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자 입력, 파일 내용, network response처럼 실행 중에만 알 수 있는 값은 일반 if의 영역입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그림으로 이해하기&lt;/h2&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVK9mR/dJMcadCE0JF/Bsfh6qZUIn8O7iYlqZR5Z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVK9mR/dJMcadCE0JF/Bsfh6qZUIn8O7iYlqZR5Z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVK9mR/dJMcadCE0JF/Bsfh6qZUIn8O7iYlqZR5Z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVK9mR%2FdJMcadCE0JF%2FBsfh6qZUIn8O7iYlqZR5Z0%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;if constexpr는 template parameter가 정해진 뒤 compile-time 조건에 맞는 branch만 남기고, 선택되지 않은 branch를 해당 instantiation에서 제외합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 중요한 지점은 branch 선택 시점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Runtime if는 program이 실행될 때 path가 갈라지지만, if constexpr는 compile time에 타입과 trait를 기준으로 path가 정리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 결과 binary에는 해당 instantiation에 필요한 코드만 남을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;if constexpr는 일반 if보다 항상 빠른 if다.&lt;/b&gt;&lt;br /&gt;핵심은 runtime 성능보다 compile-time branch입니다. 값이 runtime에 결정되는 상황에는 일반 if가 맞습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선택되지 않은 branch에는 아무 코드나 넣어도 된다.&lt;/b&gt;&lt;br /&gt;문법적으로 잘못된 코드는 여전히 문제가 될 수 있습니다. 또한 type-dependent expression인지 아닌지에 따라 compile error가 나는 시점이 달라집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;if constexpr를 쓰면 SFINAE가 필요 없다.&lt;/b&gt;&lt;br /&gt;간단한 함수 내부 분기에는 if constexpr가 읽기 쉽지만, overload resolution 단계에서 후보 자체를 제거해야 하는 경우에는 SFINAE나 concepts가 더 적합할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;type_traits를 쓰면 코드가 무조건 과해진다.&lt;/b&gt;&lt;br /&gt;타입별 처리가 실제로 필요하다면 type_traits와 if constexpr의 조합은 의도를 명확히 드러냅니다. 문제는 필요한 조건보다 복잡한 trait를 억지로 만드는 경우입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Unsupported type은 그냥 else에서 처리하면 충분하다.&lt;/b&gt;&lt;br /&gt;Library code라면 static_assert로 명확한 compile error를 내는 것이 더 좋을 수 있습니다. Debug helper라면 fallback 출력도 실용적입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr는 template metaprogramming을 숨기는 도구가 아니라, 필요한 compile-time 결정을 함수 본문 안에 드러내는 도구입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조건이 정말 compile-time 성격인지, 아니면 runtime state인지 먼저 구분해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실무에서는 어떻게 볼까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 if constexpr는 generic helper, serialization, logging, numeric utility, container utility에서 자주 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 타입별로 가능한 operation이 다를 때 branch를 함수 안에 모아둘 수 있어 코드가 읽기 쉬워집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 타입별 동작이 커지고 책임이 많아진다면 if constexpr chain이 길어지며 유지보수가 어려워질 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;if constexpr 적합도&lt;/td&gt;
&lt;td&gt;검토할 대안&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;함수 안에서 타입별로 짧게 처리&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;td&gt;type_traits 조합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overload 후보 자체를 제한해야 함&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;SFINAE, concepts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;타입별 구현이 크고 독립적임&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;overload, specialization, policy class&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;runtime 설정에 따라 분기&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;일반 if, strategy object&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 리뷰에서는 if constexpr가 들어간 조건이 type-level decision인지 확인하는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 branch가 많아진다면 “이 함수가 너무 많은 타입 정책을 알고 있는가”를 점검해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 helper라면 if constexpr가 깔끔하지만, domain logic이 branch마다 커진다면 overload나 별도 type으로 분리하는 편이 더 낫습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr는 C++17의 compile-time branch 문법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Template instantiation에서 조건이 결정되면 선택되지 않은 branch는 discarded statement로 처리됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타입마다 가능한 operation이 다를 때 SFINAE보다 읽기 쉬운 함수 내부 분기를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 if를 대체하는 성능 문법이 아니라 compile-time decision을 표현하는 문법으로 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Branch가 길어지면 overload, specialization, concepts 같은 다른 설계를 검토해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if constexpr를 잘 쓰는 기준은 “실행 중 상태”가 아니라 “타입이 정해지면 함께 정해지는 코드 경로”를 분기하는 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>C++/Concepts</category>
      <category>C++</category>
      <category>c++17</category>
      <category>Compile Time</category>
      <category>generic programming</category>
      <category>if constexpr</category>
      <category>modern C++</category>
      <category>SFINAE</category>
      <category>template</category>
      <category>type_traits</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/768</guid>
      <comments>https://eunchanee.tistory.com/768#entry768comment</comments>
      <pubDate>Mon, 20 Jul 2026 08:55:50 +0900</pubDate>
    </item>
    <item>
      <title>C++ Callback 코드에서 Lambda Capture를 안전하게 쓰는 법</title>
      <link>https://eunchanee.tistory.com/767</link>
      <description>&lt;div style=&quot;max-width: 100%; overflow-x: hidden;&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ lambda는 문법만 보면 짧고 편합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 lambda가 std::function에 저장되거나 callback queue, event handler, async task로 넘어가면 capture한 값의 lifetime이 훨씬 중요해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 reference capture는 코드가 짧아 보이지만, lambda가 local variable보다 오래 살아남는 순간 dangling reference를 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 C++17 기준으로 lambda capture의 lifetime을 예제 코드와 함께 정리해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda capture에서 중요한 질문은 &amp;ldquo;무엇을 capture했는가&amp;rdquo;보다 &amp;ldquo;lambda가 capture 대상보다 오래 살아남을 수 있는가&amp;rdquo;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpOJEZ/dJMcacqnOQX/IeRyVoJRTJBlbgE0xqqqk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpOJEZ/dJMcacqnOQX/IeRyVoJRTJBlbgE0xqqqk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpOJEZ/dJMcacqnOQX/IeRyVoJRTJBlbgE0xqqqk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpOJEZ%2FdJMcacqnOQX%2FIeRyVoJRTJBlbgE0xqqqk1%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;lambda가 즉시 실행되는지, 저장되어 나중에 실행되는지에 따라 capture 방식의 안전성이 달라집니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. lambda capture는 무엇을 저장하는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda는 주변 scope의 변수를 capture해서 나중에 함수처럼 호출할 수 있는 closure object를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;capture 방식은 크게 value capture와 reference capture로 나눌 수 있습니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;방식&lt;/td&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;td&gt;주요 위험&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;value capture&lt;/td&gt;
&lt;td&gt;[count]&lt;/td&gt;
&lt;td&gt;lambda 객체 안에 값을 복사해서 저장&lt;/td&gt;
&lt;td&gt;복사 비용, 오래된 snapshot 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reference capture&lt;/td&gt;
&lt;td&gt;[&amp;amp;count]&lt;/td&gt;
&lt;td&gt;외부 변수 자체를 참조&lt;/td&gt;
&lt;td&gt;외부 변수가 먼저 사라지면 dangling reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;default value capture&lt;/td&gt;
&lt;td&gt;[=]&lt;/td&gt;
&lt;td&gt;사용한 외부 값을 기본적으로 복사&lt;/td&gt;
&lt;td&gt;의도하지 않은 복사, this capture 혼동&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;default reference capture&lt;/td&gt;
&lt;td&gt;[&amp;amp;]&lt;/td&gt;
&lt;td&gt;사용한 외부 값을 기본적으로 참조&lt;/td&gt;
&lt;td&gt;저장형 callback에서 lifetime 추적이 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value capture는 lambda가 만들어지는 시점의 값을 lambda 객체 안에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reference capture는 값을 복사하지 않고 기존 변수를 바라봅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 reference capture는 참조 대상이 lambda 호출 시점까지 살아 있다는 보장이 있을 때만 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 중요한가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 lambda가 항상 그 자리에서 바로 실행되지 않는다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무 코드에서는 lambda를 std::function에 넣어 저장하거나, task queue에 등록하거나, UI event handler와 network callback으로 넘기는 일이 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 lambda의 실행 시점은 capture가 일어난 scope보다 늦을 수 있습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;함수 안에서 local variable을 만든다.&lt;/li&gt;
&lt;li&gt;lambda가 local variable을 reference로 capture한다.&lt;/li&gt;
&lt;li&gt;lambda를 std::function이나 queue에 저장한다.&lt;/li&gt;
&lt;li&gt;함수가 return되며 local variable의 lifetime이 끝난다.&lt;/li&gt;
&lt;li&gt;나중에 저장된 lambda를 호출하면 이미 사라진 객체를 참조한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름은 컴파일러가 항상 막아주지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 컴파일될 수 있지만, 실행 시점에는 undefined behavior가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 핵심 개념: closure object와 lifetime&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda expression은 호출 가능한 객체를 만듭니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 객체를 closure object라고 볼 수 있고, capture한 값들은 이 객체의 내부 상태가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value capture라면 내부 상태에 값이 복사됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reference capture라면 내부 상태에 참조가 저장된 것처럼 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 lambda 자체의 lifetime과 capture 대상의 lifetime을 따로 봐야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;상황&lt;/td&gt;
&lt;td&gt;reference capture 안전성&lt;/td&gt;
&lt;td&gt;권장 판단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lambda를 즉시 호출&lt;/td&gt;
&lt;td&gt;대체로 안전&lt;/td&gt;
&lt;td&gt;읽기 쉬운 범위에서 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;algorithm 호출에 잠깐 전달&lt;/td&gt;
&lt;td&gt;대체로 안전&lt;/td&gt;
&lt;td&gt;std::sort, std::for_each처럼 호출 범위가 명확하면 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;std::function에 저장&lt;/td&gt;
&lt;td&gt;위험 가능성 높음&lt;/td&gt;
&lt;td&gt;value capture 또는 명시적 ownership 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;async, event, callback으로 등록&lt;/td&gt;
&lt;td&gt;위험 가능성 높음&lt;/td&gt;
&lt;td&gt;lifetime 보장을 코드로 드러내야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reference capture는 짧은 synchronous scope에서는 유용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 callable이 저장되는 순간부터는 lifetime을 증명해야 하는 비용이 생깁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 예제 코드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 예제는 value capture와 reference capture의 차이, 그리고 std::function에 저장되는 lambda에서 value capture가 어떤 의미를 갖는지 보여줍니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;
#include &amp;lt;utility&amp;gt;
#include &amp;lt;vector&amp;gt;

class TaskQueue {
public:
    void push(std::function&amp;lt;void()&amp;gt; task)
    {
        tasks_.push_back(std::move(task));
    }

    void run_all()
    {
        for (const auto&amp;amp; task : tasks_) {
            task();
        }
    }

private:
    std::vector&amp;lt;std::function&amp;lt;void()&amp;gt;&amp;gt; tasks_;
};

int main()
{
    int retry_count = 1;

    auto by_value = [retry_count]() {
        std::cout &amp;lt;&amp;lt; &quot;by value: &quot; &amp;lt;&amp;lt; retry_count &amp;lt;&amp;lt; '\n';
    };

    auto by_reference = [&amp;amp;retry_count]() {
        std::cout &amp;lt;&amp;lt; &quot;by reference: &quot; &amp;lt;&amp;lt; retry_count &amp;lt;&amp;lt; '\n';
    };

    retry_count = 3;

    by_value();
    by_reference();

    TaskQueue queue;
    std::string message = &quot;upload finished&quot;;

    queue.push([message]() {
        std::cout &amp;lt;&amp;lt; &quot;stored task: &quot; &amp;lt;&amp;lt; message &amp;lt;&amp;lt; '\n';
    });

    message = &quot;changed after enqueue&quot;;

    queue.run_all();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행 결과&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;by value: 1
by reference: 3
stored task: upload finished
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;by_value는 lambda가 만들어진 시점의 retry_count 값인 1을 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;by_reference는 외부 변수 retry_count를 바라보므로 변경 후 값인 3을 출력합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;queue.push([message] { ... })는 message를 value로 capture했기 때문에, 원본 문자열이 나중에 바뀌어도 저장된 task는 처음 값을 유지합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 위험한 예제: 저장된 reference capture&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 컴파일될 수 있지만, 실제 코드에서는 사용하면 안 되는 패턴입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;함수가 끝난 뒤 사라질 local variable을 reference로 capture한 lambda를 반환하기 때문입니다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;#include &amp;lt;functional&amp;gt;
#include &amp;lt;iostream&amp;gt;
#include &amp;lt;string&amp;gt;

std::function&amp;lt;void()&amp;gt; make_bad_task()
{
    std::string message = &quot;temporary&quot;;
    return [&amp;amp;message]() {
        std::cout &amp;lt;&amp;lt; message &amp;lt;&amp;lt; '\n';
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상태 변화&lt;/h4&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;width: 100%; max-width: 100%; box-sizing: border-box; overflow-x: auto;&quot;&gt;&lt;code&gt;make_bad_task() 진입
message local object 생성
lambda가 message를 reference로 capture
std::function에 lambda 저장 후 return
make_bad_task() 종료
message local object 파괴
저장된 lambda는 더 이상 유효하지 않은 message를 바라봄
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 std::function이 lambda 객체를 저장할 수는 있어도, reference capture 대상의 lifetime을 자동으로 연장하지는 않는다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴은 테스트 환경에서는 우연히 정상처럼 보일 수 있지만, undefined behavior이므로 결과를 신뢰할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그림으로 이해하기&lt;/h2&gt;
&lt;figure style=&quot;margin: 0;&quot;&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; width=&quot;100%&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/etPod0/dJMcagM6CNO/yFyTTRiwkjktcyp4ps0BSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/etPod0/dJMcagM6CNO/yFyTTRiwkjktcyp4ps0BSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/etPod0/dJMcagM6CNO/yFyTTRiwkjktcyp4ps0BSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FetPod0%2FdJMcagM6CNO%2FyFyTTRiwkjktcyp4ps0BSk%2Fimg.png&quot; width=&quot;100%&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;figcaption&gt;value capture는 lambda 객체 안에 독립적인 값을 남기지만, reference capture는 원본 객체의 lifetime이 끝나면 더 이상 안전하지 않습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림에서 중요한 차이는 저장 위치가 아니라 ownership입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value capture는 lambda가 사용할 값을 closure object 안에 들고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reference capture는 외부 객체를 빌려 쓰는 구조이므로, 외부 객체가 먼저 사라지면 lambda 내부에는 유효하지 않은 연결만 남습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 자주 하는 오해&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;std::function에 넣으면 capture 대상도 함께 안전해진다.&lt;/b&gt;&lt;br /&gt;std::function은 callable 객체를 저장할 뿐입니다. reference capture 대상의 lifetime을 연장하지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[&amp;amp;]를 쓰면 불필요한 복사를 피하므로 항상 좋다.&lt;/b&gt;&lt;br /&gt;즉시 실행되는 작은 scope에서는 편할 수 있지만, 저장되는 callback에서는 lifetime 추적 비용이 커집니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;[=]는 모든 것을 value로 안전하게 복사한다.&lt;/b&gt;&lt;br /&gt;member function 안에서 this가 관련되면 생각보다 복잡해집니다. C++17에서는 [=]가 member 접근에 대해 this를 capture하는 흐름을 만들 수 있으므로 객체 lifetime을 별도로 봐야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;string literal처럼 보이는 값은 항상 안전하다.&lt;/b&gt;&lt;br /&gt;std::string_view나 reference가 local std::string을 가리키고 있다면 안전하지 않을 수 있습니다. 겉보기 타입보다 실제로 가리키는 대상의 lifetime이 중요합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴파일이 되면 lifetime도 검증된 것이다.&lt;/b&gt;&lt;br /&gt;dangling reference 문제는 컴파일이 통과해도 남을 수 있습니다. 저장형 callback에서는 코드 리뷰와 테스트 설계가 필요합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda capture는 간결한 문법 때문에 위험이 작아 보이기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 capture list는 작은 ownership 설계라고 보고 읽는 편이 더 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 실무에서는 어떻게 볼까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 lambda가 저장되는지를 먼저 봐야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장되지 않고 바로 실행되는 lambda라면 reference capture가 자연스러운 경우도 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 std::function, thread, timer, event loop, network callback, UI callback으로 넘어가는 lambda는 보수적으로 다뤄야 합니다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; table-layout: fixed; word-break: break-word;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;코드 리뷰 질문&lt;/td&gt;
&lt;td&gt;확인할 내용&lt;/td&gt;
&lt;td&gt;권장 방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lambda가 저장되는가?&lt;/td&gt;
&lt;td&gt;std::function, queue, callback registry에 들어가는지 확인&lt;/td&gt;
&lt;td&gt;저장된다면 reference capture를 의심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;capture 대상이 local variable인가?&lt;/td&gt;
&lt;td&gt;함수 종료 후 사라지는 객체인지 확인&lt;/td&gt;
&lt;td&gt;value capture 또는 소유권 있는 객체 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;member function에서 this를 capture하는가?&lt;/td&gt;
&lt;td&gt;객체가 callback보다 오래 사는지 확인&lt;/td&gt;
&lt;td&gt;shared_ptr, weak_ptr, 명시적 해제 흐름 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;큰 객체를 value capture하는가?&lt;/td&gt;
&lt;td&gt;복사 비용과 snapshot 의미를 확인&lt;/td&gt;
&lt;td&gt;필요한 데이터만 복사하거나 shared ownership 검토&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장형 callback에서 가장 단순하고 안전한 기본값은 필요한 데이터를 명시적으로 value capture하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큰 객체를 매번 복사하기 부담스럽다면 std::shared_ptr로 lifetime을 공유하거나, std::weak_ptr로 객체가 살아 있을 때만 작업하는 구조를 검토할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 capture list만 보고도 lifetime 의도가 드러나야 한다는 점입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda capture는 closure object가 외부 값을 어떻게 보관할지 정하는 문법입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value capture는 생성 시점의 값을 lambda 객체 안에 저장하고, reference capture는 외부 객체를 빌려 씁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lambda가 즉시 실행된다면 reference capture도 자연스러울 수 있지만, 저장형 callback에서는 위험해질 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;std::function은 callable을 저장할 뿐 reference capture 대상의 lifetime을 연장하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async, event, timer, callback 코드에서는 capture list를 ownership 설계로 보고 명시적으로 작성하는 편이 안전합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장되는 lambda라면 reference capture를 먼저 의심하고, 안전성을 설명할 수 있을 때만 사용하세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;/div&gt;</description>
      <category>C++/Concepts</category>
      <category>C++</category>
      <category>c++17</category>
      <category>callback</category>
      <category>Dangling Reference</category>
      <category>lambda</category>
      <category>Lambda Capture</category>
      <category>Lifetime</category>
      <category>modern C++</category>
      <category>std::function</category>
      <author>Enchant&amp;eacute;e</author>
      <guid isPermaLink="true">https://eunchanee.tistory.com/767</guid>
      <comments>https://eunchanee.tistory.com/767#entry767comment</comments>
      <pubDate>Thu, 16 Jul 2026 21:34:21 +0900</pubDate>
    </item>
  </channel>
</rss>