<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>DevNinja</title>
    <link>https://yscho03.tistory.com/</link>
    <description>평범한 직장인의 개발 관련 관련 개인 블로그입니다. </description>
    <language>ko</language>
    <pubDate>Mon, 11 May 2026 02:37:36 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>DevNinja</managingEditor>
    <image>
      <title>DevNinja</title>
      <url>https://tistory1.daumcdn.net/tistory/2860681/attach/c4dea73f2bb44355b3d767f27788729a</url>
      <link>https://yscho03.tistory.com</link>
    </image>
    <item>
      <title>Claude Superpowers Skills 브레인스토밍, 코드 설계, 코드 리뷰까지</title>
      <link>https://yscho03.tistory.com/420</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결론부터 말하자면, Claude Code를 사용하는 사람이 obra(Jesse Vincent)가 만든 &lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;Superpowers&lt;/a&gt; 플러그인을 설치하지 않으면 진정한 의미의 손해다.&lt;/b&gt; 이 플러그인을 설치하기 전에는 Claude에게 &quot;기능을 만들어 달라&quot;고 하면 곧바로 코드부터 작성했지만, 설치한 이후에는 알아서 브레인스토밍부터 시작해 계획을 세우고, TDD로 구현하며, 발행 직전에는 코드 리뷰까지 수행한다. 이것이 Claude Superpowers 스킬의 핵심이다.&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;이 글에서는 IT에 익숙하지 않은 사람도 이해할 수 있도록, &quot;스킬&quot;이라는 개념이 무엇인지부터 obra superpowers가 다른 도구와 어떻게 다른지, 실제 비포/애프터 차이가 어떠했는지를 모두 풀어보고자 한다. AI 코딩 자동화를 한다며 ChatGPT 같은 것만 사용하던 사람이라면 끝까지 읽어보기를 권한다.&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;&lt;a href=&quot;https://github.com/obra/superpowers&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/obra/superpowers&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1778296799274&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - obra/superpowers: An agentic skills framework &amp;amp; software development methodology that works.&quot; data-og-description=&quot;An agentic skills framework &amp;amp; software development methodology that works. - obra/superpowers&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/obra/superpowers&quot; data-og-url=&quot;https://github.com/obra/superpowers&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/vwKCF/dJMb84qcX4n/6g52IPJdqHXIeC64KQ0ZqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bG0MGY/dJMb9frJJT8/K1qZd2XufrsNYCrtBkmB8K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600&quot;&gt;&lt;a href=&quot;https://github.com/obra/superpowers&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/obra/superpowers&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/vwKCF/dJMb84qcX4n/6g52IPJdqHXIeC64KQ0ZqK/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/bG0MGY/dJMb9frJJT8/K1qZd2XufrsNYCrtBkmB8K/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - obra/superpowers: An agentic skills framework &amp;amp; software development methodology that works.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;An agentic skills framework &amp;amp; software development methodology that works. - obra/superpowers&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Claude Superpowers 스킬, 도대체 무엇이기에 모두가 설치하라고 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude Code의 &quot;스킬&quot;이라는 개념부터 정리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Claude의 &quot;스킬(Skill)&quot;이 무엇인지 살펴보자. 비유하자면 &lt;b&gt;Claude에게 미리 입력해 둔 작업 매뉴얼&lt;/b&gt;이다. 회사에 신입이 들어오면 업무 매뉴얼을 제공하지 않는가? 그것과 유사하다. 다만 Claude는 사용자가 시키지 않아도 상황을 보고 알아서 매뉴얼을 꺼내 본다는 점이 다르다.&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;Anthropic 공식 &lt;a href=&quot;https://docs.claude.com/&quot;&gt;Claude Code 문서&lt;/a&gt;를 보면 Skill이라는 도구가 있는데, 이는 일종의 &quot;이 상황에서는 이렇게 일하라&quot;는 지시서를 호출하는 도구다. 예를 들어 디버깅 스킬이 있다면, 사용자가 &quot;버그가 발생했다&quot;고 말하는 순간 Claude가 디버깅 매뉴얼을 펴고 그 절차대로 움직인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;obra(Jesse Vincent)가 만든 플러그인 &amp;mdash; 단순한 프롬프트 모음이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obra는 Perl 커뮤니티에서 RT를 만든 것으로 유명한 베테랑 개발자다. 이 사람이 Claude Code용으로 만든 것이 superpowers이며, 깃허브에서 &lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;github.com/obra/superpowers&lt;/a&gt;를 통해 코드를 모두 확인할 수 있다.&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;그런데 이것은 단순히 &quot;프롬프트 모음&quot;이 아니다. 진정한 차이는 &lt;b&gt;자동 호출 메커니즘&lt;/b&gt;이다. 일반 프롬프트 모음은 사용자가 &quot;이것을 사용해 달라&quot;고 해야 동작하는데, superpowers는 &quot;1%라도 적용 가능성이 있으면 무조건 스킬을 호출하라&quot;는 강제 규약을 박아 놓았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심은 using-superpowers 진입 스킬이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 시작하면 가장 먼저 로드되는 것이 using-superpowers라는 진입 스킬이다. 이것이 무엇이냐 하면 &lt;b&gt;Claude에게 &quot;다른 스킬을 호출하지 않고 답하면 안 된다&quot;고 박아 놓는 가이드&lt;/b&gt;다. 영어 원문에는 태그 안에 &quot;이것은 협상 불가, 선택사항도 아니다&quot;라고 명시되어 있다. 거의 군대 명령 수준이다.&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;쉽게 말하자면 Claude가 &quot;이 정도는 그냥 작성해도 되겠지&quot;라고 자기 마음대로 스킵하는 것을 차단하는 장치다. 이것이 obra superpowers의 진정한 무기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사용하지 않으면 어떻게 되는가 &amp;mdash; 비포/애프터 시나리오&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 Claude Code: 지시하면 곧바로 코드부터 작성한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Superpowers가 설치되지 않은 상태에서 &quot;결제 모듈을 만들어 달라&quot;고 하면 보통 다음과 같이 흘러간다.&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;Claude가 즉시 파일을 만들기 시작한다&lt;/li&gt;
&lt;li&gt;의존성을 설치한다&lt;/li&gt;
&lt;li&gt;함수를 약 50줄가량 쭉 작성한다&lt;/li&gt;
&lt;li&gt;&quot;완료되었습니다&quot;라며 종료한다&lt;/li&gt;
&lt;/ol&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;b&gt;계획 단계가 통째로 빠져 있어서&lt;/b&gt; 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Superpowers 설치된 Claude: 알아서 단계별로 진행한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 요청을 superpowers가 설치된 상태에서 하면 흐름이 완전히 달라진다.&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;brainstorming 스킬 자동 호출 &amp;rarr; &quot;결제 모듈이라고 하셨는데 어떤 PG사를 사용할 것인가? 환불도 포함하는가? 멤버십 결제도 처리하는가?&quot; 식으로 질문을 던진다&lt;/li&gt;
&lt;li&gt;writing-plans 스킬 &amp;rarr; 계획서를 마크다운 파일로 작성하고 사용자에게 컨펌을 요청한다&lt;/li&gt;
&lt;li&gt;test-driven-development 스킬 &amp;rarr; 테스트부터 작성하고, 그다음 구현한다&lt;/li&gt;
&lt;li&gt;verification-before-completion 스킬 &amp;rarr; 실제로 통과했는지 명령어를 실행하여 확인한다&lt;/li&gt;
&lt;li&gt;requesting-code-review 스킬 &amp;rarr; 리뷰를 받는다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직역하자면 &quot;AI 페어 프로그래머&quot;가 자신의 직무 매뉴얼대로 일하는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 워크플로우 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체감되는 차이는 커밋 히스토리에서 가장 잘 드러난다. 일반 Claude Code로 작업하면 커밋 메시지 한 줄에 변경사항 100개가 박힌 경우가 자주 나타난다. Superpowers를 사용하면 의도 단위로 분리된 커밋이 자연스럽게 생성된다. 왜일까? 계획 단계에서 작업 단위를 미리 나눠 두었기 때문이다.&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;PR 품질 또한 다르다. 평소에는 &quot;이 부분을 좀 변경해 달라&quot;는 코멘트가 5~6개씩 달리는데, superpowers를 거친 PR은 코멘트가 0~2개로 머지되는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진정한 무기 1: 브레인스토밍을 강제로 수행하게 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;brainstorming 스킬은 어떤 트리거로 호출되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스킬의 정의를 보면 트리거 조건이 다음과 같이 박혀 있다. &quot;기능 만들기, 컴포넌트 작성, 동작 변경 같은 &lt;b&gt;창작 작업 전에는 무조건 호출&lt;/b&gt;할 것.&quot;&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;쉽게 말하자면 Claude가 코드 한 줄을 작성하기 전에 &lt;b&gt;사용자 의도부터 캐묻게&lt;/b&gt; 만드는 장치다. &quot;그것을 만들어 달라&quot;고 하면 곧바로 시작하지 못하고, &quot;잠시만, 진정으로 원하는 것이 무엇인지 함께 정리해보자&quot;부터 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;그냥 만들어 달라&quot;고 했을 때 Claude가 갑자기 질문을 던지는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 다소 짜증이 날 수도 있다. &quot;그냥 빨리 작성해 달라&quot;고 하는데 자꾸 질문을 던지기 때문이다. 그러나 이것이 단점이 아니라 장점인 이유는, &lt;b&gt;사람이 머릿속에서만 굴리던 요구사항을 강제로 끄집어내게 만든다&lt;/b&gt;는 점이다. 결과적으로 한 번에 제대로 만들 확률이 훨씬 높아진다.&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;브레인스토밍을 거치면 사용자가 미처 생각하지 못한 케이스도 함께 발굴된다. &quot;환불 정책은 어떻게 할 것인가?&quot;, &quot;결제 실패 시 재시도 정책이 있는가?&quot;, &quot;이 로직을 다른 페이지에서도 사용할 일이 있는가?&quot; &amp;mdash; 이러한 것들을 미리 정리하고 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;결과물 비교&lt;/h3&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;b&gt;요구사항 누락률&lt;/b&gt;에서 가장 크게 드러난다. 거치지 않으면 평균 30~40% 누락되는 것이, 거치면 5~10% 수준으로 떨어진다. 이것은 단순한 체감이 아니라 실제 PR 변경량을 비교해 보아도 그렇게 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진정한 무기 2: Superpowers + 동반 플러그인 묶음으로 퍼블리싱&amp;middot;디자인까지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obra/superpowers 본체에 들어 있는 스킬은 코드 작업 위주다(브레인스토밍, 계획, TDD, 디버깅, 검증, 코드 리뷰, git worktree 등). 다만 같은 Claude Code 마켓플레이스에 frontend-design, to-prd, to-issues 같은 &lt;b&gt;동반 플러그인이 별도로 존재하기 때문에&lt;/b&gt; 함께 설치하면 기획&amp;middot;디자인 단계까지 자연스럽게 하나의 흐름으로 묶인다. 아래의 스킬들은 obra/superpowers 코어가 아니라 별도 플러그인이지만, 워낙 짝꿍처럼 함께 사용되므로 묶어서 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;frontend-design 스킬 &amp;mdash; 차별화된 UI를 뽑아내도록 강제한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI에게 UI를 만들어 달라고 하면 모두 비슷하게 나오는 것을 알 것이다. 회색 박스 + 둥근 모서리 + 파란 버튼. 이른바 &quot;AI 디폴트 미감&quot;이다.&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;frontend-design 스킬은 이러한 generic AI 디자인을 &lt;b&gt;금지&lt;/b&gt;하고 차별화된 인터페이스를 뽑아내도록 만든다. 톤, 그리드, 컬러 시스템부터 잡고 들어가도록 강제한다. Claude가 &quot;이것은 너무 뻔하다, 다른 컨셉을 시도해 보자&quot;라고 자기 검열하는 모습이 신기할 정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;to-prd, to-issues 스킬 &amp;mdash; 대화 맥락을 PRD/이슈로 자동 변환한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화로 &quot;이런 기능을 만들고 싶다&quot;고 하다 보면 어느새 요구사항이 산만해진다. 이때 to-prd 스킬을 호출하면 &lt;b&gt;여태까지 나눈 대화 맥락을 PRD 문서로 자동 변환&lt;/b&gt;한다. 이슈 트래커에 곧바로 올릴 수 있도록 말이다.&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;to-issues 스킬은 한발 더 나아가 PRD를 &lt;b&gt;독립적으로 잡아갈 수 있는 작은 이슈들로 분할해 준다&lt;/b&gt;. 이를 트레이서 불릿(tracer-bullet) 방식이라고 부르는데, 비유하자면 큰 프로젝트를 작게 자른 다음 끝에서 끝까지 한 번 관통하는 가는 줄기부터 만드는 방식이다. 그래야 진척도가 보이고 백로그가 막히지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;requesting-code-review, receiving-code-review 콤보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행 직전 검증 루프다. requesting-code-review는 자신이 작성한 코드를 누군가에게 리뷰받게 하고, receiving-code-review는 받은 피드백을 어떻게 반영할지 가이드한다.&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;여기서 중요한 점은 receiving-code-review 스킬이 &quot;퍼포먼스성 동의&quot;를 막아 준다는 것이다. 즉 리뷰어가 &quot;이것은 별로인 것 같다&quot;고 하면 무조건 &quot;네 알겠습니다&quot;라며 수정하는 것이 아니라, &lt;b&gt;기술적으로 검증부터 하라&lt;/b&gt;고 박아 놓았다. 이것은 진정으로 영리한 설계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘텐츠 파이프라인 관점&lt;/h3&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;b&gt;기획 &amp;rarr; 설계 &amp;rarr; 구현 &amp;rarr; 검증 &amp;rarr; 발행이 하나의 흐름&lt;/b&gt;으로 진행된다. 평소에는 단계마다 사람이 &quot;다음에는 이것을 하라&quot;고 시켜야 했는데, superpowers를 설치하면 Claude가 알아서 다음 단계로 넘어간다. 콘텐츠 파이프라인을 자동화하는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진정한 무기 3: TDD/디버깅/검증을 우회하지 못하게 막는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rigid skill vs Flexible skill 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Superpowers의 스킬은 두 종류로 나뉜다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Rigid 스킬&lt;/b&gt;: 절대 우회할 수 없다. 정해진 순서를 그대로 따라야 한다. (예: TDD, debugging)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Flexible 스킬&lt;/b&gt;: 원칙은 따르되 상황에 맞게 변형 가능하다. (예: 패턴 가이드)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비유하자면 Rigid는 &lt;b&gt;운전면허 시험 코스&lt;/b&gt;이고, Flexible은 &lt;b&gt;운전 매너&lt;/b&gt;다. 코스는 거치지 않으면 안 되지만, 매너는 상황을 보고 적용 강도를 조절할 수 있다.&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;test-driven-development가 Rigid라는 것은, Claude가 &quot;그냥 빨리 작성하고 테스트는 나중에 붙이자&quot; 같은 시도를 할 수 없다는 뜻이다. 무조건 빨강(실패하는 테스트) &amp;rarr; 초록(통과시키기) &amp;rarr; 리팩터링 순서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;verification-before-completion &amp;mdash; &quot;완료되었다&quot;는 거짓말을 방지한다&lt;/h3&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;b&gt;숙제를 다 했다고 말하기 전에 실제로 풀어 봤는지 확인 도장을 받는 것&lt;/b&gt;이다.&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;Claude가 &quot;구현 완료&quot;를 외치기 전에, 실제로 검증 명령어를 실행하고 그 출력을 보아야 한다고 강제한다. 테스트가 통과했다고 말하기 전에 npm test 결과를 첨부해야 하고, 빌드되었다고 말하기 전에 tsc --noEmit 결과를 보여 주어야 한다. &lt;b&gt;증거 없이는 성공 주장 금지&lt;/b&gt;다.&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;이것이 빠지면 진정으로 자주 발생하는 것이 &quot;다 고쳤다&quot; &amp;rarr; 실행하면 또 같은 에러가 나는 상황이다. Superpowers가 설치된 Claude는 이러한 행위가 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;systematic-debugging &amp;mdash; 버그를 만나면 일단 가설부터 세운다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스킬도 Rigid다. 버그를 만나면 &lt;b&gt;재현 &amp;rarr; 최소화 &amp;rarr; 가설 &amp;rarr; 계측 &amp;rarr; 수정 &amp;rarr; 회귀 테스트&lt;/b&gt; 순서로 진행한다.&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;평소에 Claude는 버그를 보면 어딘가 한 줄을 고쳐서 &quot;되었을 것이다&quot;라고 말하는 경우가 많은데, superpowers가 설치되면 가설을 세우지 않고는 코드를 만지지 못한다. &quot;이것이 왜 발생하는지&quot;를 먼저 정리하고, 그 가설을 검증하는 방식으로 움직인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 및 사용 방법 + 솔직한 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치는 한 줄 명령어 수준이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;obra의 &lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;superpowers 깃허브 레포&lt;/a&gt; README를 보면 설치 명령어가 친절하게 적혀 있다. Claude Code의 플러그인 시스템에 등록하는 방식이라 한 줄로 끝난다. 자세한 내용은 README를 보면 된다 &amp;mdash; 이미 잘 정리되어 있어서 여기서 다시 옮겨 적을 필요는 없다.&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;설치 후에는 세션을 시작할 때 자동으로 using-superpowers 스킬이 로드되면서 다른 스킬들의 트리거가 활성화된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Skill 도구 + 슬래시 커맨드로 호출한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수동으로 스킬을 호출하고 싶을 때는 /&amp;lt;스킬이름&amp;gt; 형태의 슬래시 커맨드로도 호출할 수 있다. 예를 들어 /brainstorming 같은 식이다.&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;다만 superpowers의 진정한 매력은 자동 호출이므로, 슬래시 커맨드는 거의 사용하지 않게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점도 솔직하게 까보자면&lt;/h3&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;b&gt;1. 토큰 소모가 다소 더 많다.&lt;/b&gt; 매 세션마다 진입 스킬이 로드되고, 작업할 때마다 관련 스킬 매뉴얼을 읽기 때문에 입력 토큰이 늘어난다. 체감상 약 10~20% 정도다.&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;&lt;b&gt;2. 학습 곡선이 존재한다.&lt;/b&gt; 어떤 스킬이 언제 호출되는지 감을 잡기까지 며칠이 걸린다. 처음에는 &quot;왜 자꾸 질문을 던지는가?&quot;라며 답답할 수 있다.&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;&lt;b&gt;3. &quot;그냥 빨리 작성하고 싶다&quot;에는 오버스펙이다.&lt;/b&gt; 한 줄짜리 함수 하나를 추가하는데 브레인스토밍을 시키면 진정으로 짜증이 난다. 이런 경우에는 superpowers를 끄거나 스킬 트리거를 우회하는 방식으로 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼에도 사용하는 것이 이득인 이유&lt;/h3&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;b&gt;코드 품질 + 페어 프로그래머급 동반자&lt;/b&gt;가 생긴다는 점이 압도적으로 크다. 특히 큰 프로젝트나 장기 프로젝트일수록 차이가 벌어진다. 단기 프로젝트에서는 오버스펙처럼 보이지만, 한두 달 가는 프로젝트라면 무조건 켜 두는 것이 이득이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 Claude Superpowers 스킬, 왜 무조건 설치해야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심을 3줄로 요약하면 다음과 같다.&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;Claude Superpowers 스킬은 단순한 프롬프트 모음이 아니라, Claude가 자기 마음대로 우회하지 못하게 만드는 강제 규약이다.&lt;/b&gt; using-superpowers 진입 스킬이 1%라도 적용되면 무조건 호출하도록 박아 놓았다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;브레인스토밍 &amp;rarr; 계획 &amp;rarr; TDD &amp;rarr; 디버깅 &amp;rarr; 검증 &amp;rarr; 코드 리뷰 &amp;rarr; 발행/디자인까지 전 단계가 묶여 있다.&lt;/b&gt; 사람이 &quot;다음에는 이것을 하라&quot;고 시키지 않아도 알아서 다음 단계로 넘어간다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;토큰을 더 사용하고 학습 곡선이 존재한다는 단점은 있으나, 장기 프로젝트라면 무조건 이득이다.&lt;/b&gt; 평소 PR 코멘트 5~6개가 달리던 것이 0~2개로 감소한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 액션 &amp;mdash; &lt;a href=&quot;https://github.com/obra/superpowers&quot;&gt;obra superpowers 깃허브&lt;/a&gt;에 들어가 README를 한 번 읽어 보고, 자기 프로젝트에 설치해 보기를 권한다. 한 시간 안에 셋업이 끝난다.&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;후속 글로 &lt;b&gt;&quot;superpowers 스킬 직접 만들어 보기 (writing-skills)&quot;&lt;/b&gt;를 다룰 예정이니, 자기만의 워크플로우를 스킬로 박제하고 싶은 사람은 기다려 보기를 바란다.&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;Claude Code를 처음 사용해 보는 사람이라면 &lt;a href=&quot;http://localhost:20000/blog/claude-code-getting-started&quot;&gt;Claude Code 입문 가이드&lt;/a&gt;부터 보면 좋고, TDD 자동화 흐름이 궁금하다면 &lt;a href=&quot;http://localhost:20000/blog/ai-coding-tdd-workflow&quot;&gt;AI 코딩 TDD 워크플로우&lt;/a&gt; 글도 함께 보면 된다. Plan 모드가 헷갈린다면 &lt;a href=&quot;http://localhost:20000/blog/claude-plan-mode&quot;&gt;Claude Plan 모드 활용&lt;/a&gt; 글도 추천한다.&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;이 글을 한 줄로 요약하자면 다음과 같다 &amp;mdash; &lt;b&gt;Claude Superpowers 스킬을 사용하지 않는 것, 진정한 손해다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>Claude Code 스킬</category>
      <category>Claude 브레인스토밍</category>
      <category>Claude 퍼블리싱 디자인</category>
      <category>Claude 플러그인</category>
      <category>obra superpowers</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/420</guid>
      <comments>https://yscho03.tistory.com/420#entry420comment</comments>
      <pubDate>Sat, 9 May 2026 12:20:20 +0900</pubDate>
    </item>
    <item>
      <title>LLM을 코드를 다른 언어로 컨버팅 어려운 이유 (학계 논문 인용)</title>
      <link>https://yscho03.tistory.com/419</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;결론부터 쎄게 박고 시작하자&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;LLM이 다른 언어로 컨버팅 한번에 &quot;딸깍&quot; 안된다.&amp;nbsp;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;이 글은 실험 후기가 아니라 &lt;b&gt;학계 통계 정리글&lt;/b&gt;이다. 누가 LLM에게 &quot;이 Python 코드 Go로 바꿔줘&quot; 한 줄을 던져 깨진 적이 있다면 &amp;mdash; 그것은 본인의 프롬프트 문제가 아니라 &lt;b&gt;모델이 본질적으로 이 작업을 잘 하지 못한다&lt;/b&gt;는 사실이며, ICSE 2024 학회 논문에서 1700 샘플로 측정된 결과이다. 한 번에 끝내는 번역(one-shot translation)은 평균 정확도가 절반에도 미치지 못한다. 이를 모른 채 계속 시키기 때문에 매번 깨지는 것이다. 비유하자면, 외국어 통역사에게 사전 없이 즉석으로 의학 논문을 통역시키는 것과 비슷하다. 단어는 옮겨지지만 문맥이 박살난다.&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;이 글에서 풀어낼 4가지 질문이다. &lt;b&gt;(1)&lt;/b&gt; 왜 본질적으로 어려운가, &lt;b&gt;(2)&lt;/b&gt; 어떤 종류로 깨지는가, &lt;b&gt;(3)&lt;/b&gt; 어떻게 막을 수 있는가, &lt;b&gt;(4)&lt;/b&gt; 자동화는 어디까지 가능한가. 마지막에 Python agent 파이프라인을 Go로 옮기는 절차서를 적용 사례로 붙인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 번에 시킨 번역 정확도 47.3% &amp;mdash; ICSE 2024가 측정한 LLM 코드 번역의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ICSE 2024(소프트웨어공학 분야 최상위 학회)에 발표된 &lt;a href=&quot;https://arxiv.org/abs/2308.03109&quot;&gt;Pan et al. &quot;Lost in Translation&quot;&lt;/a&gt; 논문이 이 분야의 가장 단단한 실측 자료다. &lt;b&gt;7개 LLM &amp;times; 5개 언어(C&amp;middot;C++&amp;middot;Go&amp;middot;Java&amp;middot;Python, 총 31개 번역 페어) &amp;times; 1700 샘플&lt;/b&gt; 실측에서 한 번에 시킨 번역의 정확도는 &lt;b&gt;2.1%~47.3%&lt;/b&gt; 범위로 나타났다.&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;47.3%는 좋아 보이지만 이것이 &lt;b&gt;최고치&lt;/b&gt;다. 같은 측정에서 어떤 페어는 &lt;b&gt;2.1%&lt;/b&gt;까지 떨어졌다. 평균을 내도 절반을 넘기지 못한다. 즉, &quot;한 줄 프롬프트로 코드 번역&quot; 자체가 이미 &lt;b&gt;fail-by-design&lt;/b&gt;인 셈이다. 모델 크기의 문제도 아니고 프롬프트 엔지니어링의 문제도 아니다. GPT-4&amp;middot;StarCoder&amp;middot;CodeGen&amp;middot;Llama 2 모두 비슷하게 깨진다.&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;왜일까? 보통 개발자들은 &quot;LLM이 코드를 잘 짠다더라&quot;는 말을 듣고 번역도 잘할 것이라 가정한다. 그러나 코드 생성(generation)과 코드 번역(translation)은 &lt;b&gt;다른 문제&lt;/b&gt;다. 생성은 &quot;이런 기능을 만들어달라&quot;는 요구이고 정답이 여러 개다. 번역은 &quot;이 코드의 의미를 보존해 옮겨달라&quot;는 요구이고 &lt;b&gt;정답이 정확히 하나&lt;/b&gt;다. LLM은 후자에 약하다.&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;여기서 &quot;LLM 코드 번역&quot;이라는 키워드 자체가 검색 의도를 둘로 쪼갠다. 하나는 &quot;왜 안 되는가&quot;, 다른 하나는 &quot;그럼 어떻게 해야 하는가&quot;이다. 본문은 두 질문에 모두 답한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동적 &amp;rarr; 정적 변환이 가장 어렵다 &amp;mdash; Python을 Go로 옮길 때 깨지는 진짜 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pan et al. 논문이 또 하나 짚은 발견은 &lt;b&gt;동적 타입 &amp;rarr; 정적 타입&lt;/b&gt; 번역이 제일 어렵다는 점이다. Python&amp;middot;JavaScript처럼 런타임에 타입이 결정되는 언어를, Java&amp;middot;Go처럼 컴파일 타임에 타입이 박혀야 하는 언어로 옮길 때 정확도가 가장 낮게 나타났다.&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;이를 일상 비유로 풀면 다음과 같다. 한국어는 &quot;주어 생략&quot;이 자연스러운데 영어로 옮길 때는 주어를 채워 넣어야 한다. 누가 한 행동인지 모르는 채로 옮기면 영어 문장이 어색해진다. 동적 &amp;rarr; 정적 변환에서 LLM이 헷갈리는 지점도 이와 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM이 동적 &amp;rarr; 정적 번역에서 자꾸 망치는 4가지 지점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;타입 추론&lt;/b&gt;: Python의 def add(a, b): return a + b를 Go로 옮길 때 a, b가 int인지 string인지 generic인지 LLM이 추측한다. 추측이 틀리면 컴파일이 깨진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nil/None 처리&lt;/b&gt;: Python의 None 체크는 단순히 if x is None이지만 Go는 zero value, nil pointer, empty interface가 모두 다르다. LLM이 무지성으로 nil로 옮기다가 panic을 터뜨린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;generic vs interface{}&lt;/b&gt;: Python의 duck typing을 Go 1.18 이전 코드처럼 interface{}로 박아버리면 타입 안전성이 박살난다. 1.18 이후 generic이 있는데도 LLM이 옛날 패턴으로 출력하는 빈도가 높다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;error 반환 패턴&lt;/b&gt;: Python은 try/except, Go는 if err != nil이다. LLM이 try/except를 panic/recover로 옮기는 실수가 잦다. Go 컨벤션상 panic은 거의 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 4가지 지점은 단순한 syntax 변환이 아니라 &lt;b&gt;언어 철학&lt;/b&gt;의 차이다. &lt;a href=&quot;https://arxiv.org/abs/2308.08961&quot;&gt;&quot;Neural Code Translation Taxonomy&quot; 논문&lt;/a&gt;(ASE 2023)이 코드 번역을 4가지 타입(token-level, syntactic-level, library-level, algorithm-level)으로 분류했는데, &lt;b&gt;type-3 라이브러리 레벨&lt;/b&gt;과 &lt;b&gt;type-4 알고리즘 레벨&lt;/b&gt; 같은 지식 의존적 번역이 LLM이 가장 취약한 영역이다. Python &amp;rarr; Go는 type-3에서 type-4 사이에 걸쳐 있어 어려운 것이다. HumanEval-X 같은 멀티언어 벤치마크가 Go를 포함하는 것도 이 어려움 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;깨지는 패턴은 무작위가 아니다 &amp;mdash; 학계가 분류한 LLM 코드 번역 버그 15종&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pan et al.는 1700 샘플의 실패를 분석하여 &lt;b&gt;15개 버그 카테고리&lt;/b&gt;로 분류했다. 무작위가 아니라 패턴이 있다는 점이 이 분류의 의의다. 일부만 추리면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Syntax 직역&lt;/b&gt;: 소스 언어 문법을 그대로 옮기다가 타겟 언어에서 무효한 문법이 된다. Python [x for x in xs]를 Go에 그대로 옮기는 경우다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 의미 mismatch&lt;/b&gt;: 함수 이름은 비슷하지만 동작이 다른 API로 매핑한다. Python list.append를 Go slice = append(slice, x) 대신 다른 패턴으로 옮기다가 깨진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Target language requirement 위반&lt;/b&gt;: 타겟 언어가 요구하는 패턴(Go의 명시적 error 반환 등)을 누락한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dead code 생성&lt;/b&gt;: 의미가 없는 코드 블록을 끼워 넣는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Incorrect output format&lt;/b&gt;: 출력 형식이 미묘하게 다르다.&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://arxiv.org/abs/2509.12087&quot;&gt;TransLibEval&lt;/a&gt;이 200 task(Python&amp;middot;Java&amp;middot;C++ 라이브러리 의존 코드)로 측정한 syntactic 에러 내부 비율은 더 구체적이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;syntactic 에러 하위 카테고리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Syntax structure&lt;/td&gt;
&lt;td&gt;38.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language feature&lt;/td&gt;
&lt;td&gt;21.86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object definition&lt;/td&gt;
&lt;td&gt;39.73%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;Object definition 에러 39.73%&lt;/b&gt;가 핵심 시사점이다(syntactic 에러 카테고리 내부 비중 기준). numpy&amp;middot;pandas처럼 외부 라이브러리에 의존하는 코드를 옮길 때, LLM이 타겟 언어의 동등한 객체&amp;middot;자료구조 정의를 잘못 만들어내는 비중이 syntactic 실패 중 약 40%로 가장 크다. Python pandas.DataFrame을 Go로 옮길 때 동등한 추상화가 없기 때문에 LLM이 어설프게 struct로 흉내 내다가 의미가 깨지는 것이다.&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;&lt;a href=&quot;https://arxiv.org/abs/2506.00894&quot;&gt;CodeMEnv&lt;/a&gt;는 한 발 더 들어가서, 같은 언어 안에서 라이브러리 &lt;b&gt;버전만 바뀌어도&lt;/b&gt; LLM이 약하다는 점을 보였다. 19개 Python&amp;middot;Java 패키지의 922개 실제 함수 변경 데이터에서, LLM이 신&amp;middot;구 버전 API 호출을 정확히 매핑하는 비율이 낮게 나타났다. 즉, 언어 페어 번역 이전에 &lt;b&gt;의존성 관리 자체&lt;/b&gt;가 LLM의 약점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반전 &amp;mdash; Go가 사실은 코드 번역의 lingua franca다&lt;/h2&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;a href=&quot;https://arxiv.org/abs/2410.09812&quot;&gt;&quot;Unraveling the Potential of LLMs in Code Translation: How Far Are We?&quot; 논문&lt;/a&gt;이 &lt;b&gt;14개 언어 &amp;times; 4 LLM 패밀리&lt;/b&gt;로 측정한 결과, source &amp;rarr; target 직접 번역보다 &lt;b&gt;source &amp;rarr; Go &amp;rarr; target&lt;/b&gt; 2단 번역의 정확도가 더 높게 나타났다.&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;이것이 lingua franca 발견이다. 라틴어가 중세 유럽 학술어로 쓰였던 것처럼, Go가 LLM 코드 번역의 중간 허브 역할을 더 잘 수행한다는 의미다. 직관적으로는 이상하다 &amp;mdash; 단계가 늘면 오차도 늘 것 같다. 그러나 측정 결과는 반대였다.&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;논문이 제시한 설명은 다음과 같다. Go는 &lt;b&gt;문법이 단순하고 패턴이 일관적&lt;/b&gt;이다. 키워드 25개, 강제되는 컨벤션, 명시적 error 처리, 늦게 들어온 generic으로 인해 얕은 추상화 레이어를 갖는다. LLM이 Go로 옮긴 중간 결과물은 &lt;b&gt;의미가 명확하게 정렬된 상태&lt;/b&gt;가 된다. 그 상태에서 다른 언어로 한 번 더 옮기면 모호함이 줄어든 채로 출발하므로 정확도가 올라간다.&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;실무 시사점은 분명하다. Java를 Rust로 옮기고 싶다면, Java &amp;rarr; Rust로 직접 가는 대신 Java &amp;rarr; Go &amp;rarr; Rust로 가는 것이 통계적으로 유리하다. Python을 TypeScript로 옮길 때도 마찬가지다. &lt;b&gt;Go로 일단 갈아두면 다른 데로 옮기기가 쉬워진다&lt;/b&gt; &amp;mdash; 이것이 14개 언어 측정으로 확인된 사실이다.&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;이 발견은 글 후반의 &quot;Python &amp;rarr; Go 적용 사례&quot;가 단순한 예시가 아니라 &lt;b&gt;권장 전략의 첫 단계&lt;/b&gt;라는 의미이기도 하다. Python agent 파이프라인을 Go로 옮기는 작업은 그 자체로 가치가 있고, 이후 Rust로 또 옮기든 어디로 옮기든 출발점으로 쓸 만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Google 12개월 사내 사례 &amp;mdash; 환각율 25.55%를 막은 6단 validation gate&lt;/h2&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;a href=&quot;https://arxiv.org/abs/2504.09691&quot;&gt;&lt;b&gt;Google 2025 논문 &quot;Migrating Code At Scale With LLMs At Google&quot;&lt;/b&gt;&lt;/a&gt;(FSE 2025)는 그래도 어떻게 운영하는지를 보여주는 사례다. 12개월 사내 운영, &lt;b&gt;595개 변경&lt;/b&gt;, 그중 &lt;b&gt;74.45%가 LLM 자동 생성&lt;/b&gt;, 환각율 &lt;b&gt;25.55%&lt;/b&gt;, 파일당 &lt;b&gt;3회 시도&lt;/b&gt; 예산, 시간 &lt;b&gt;50% 절감&lt;/b&gt;이다.&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;여기서 핵심은 환각율이 25.55%인데도 시간 50%를 줄였다는 점이다. 4번에 1번꼴로 깨지는 모델로도 운영이 가능했던 이유가 &lt;b&gt;6단 validation gate&lt;/b&gt;다. 게이트 하나하나가 환각을 거른다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;게이트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;무엇을 검증하나&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;통과 못하면&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1. Success&lt;/td&gt;
&lt;td&gt;LLM이 응답 자체를 줬나 (timeout, 빈 응답)&lt;/td&gt;
&lt;td&gt;재요청&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Whitespace&lt;/td&gt;
&lt;td&gt;공백 외에 실질 변경이 있나&lt;/td&gt;
&lt;td&gt;스킵 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. AST parser&lt;/td&gt;
&lt;td&gt;결과물이 파싱 가능한가 (혹은 AST 동일)&lt;/td&gt;
&lt;td&gt;재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Punt (LLM self-check)&lt;/td&gt;
&lt;td&gt;모델이 변경 자체가 필요했는지 직접 판정&lt;/td&gt;
&lt;td&gt;재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Build&lt;/td&gt;
&lt;td&gt;빌드 시스템이 통과하는가&lt;/td&gt;
&lt;td&gt;재시도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Test&lt;/td&gt;
&lt;td&gt;기존 테스트가 통과하는가&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 게이트의 핵심은 &lt;b&gt;계단식&lt;/b&gt;이라는 점이다. 1단에서 떨어지면 2단을 보지 않는다. 6단까지 모두 통과하면 사람이 더 보지 않고 머지한다. 6단에서 떨어지면 사람에게 넘긴다. 이렇게 하면 LLM이 환각을 일으켜도 &amp;mdash; 25.55%는 환각하지만 &amp;mdash; 머지되기 전에 잡힌다.&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;파일당 &lt;b&gt;3회 시도&lt;/b&gt; 예산을 박은 것도 중요하다(논문 그대로 인용하면 &quot;three modification attempts are made per file&quot;). 1회로 끝낸다고 가정하면 정확도가 47%에서 멈추지만, 시도를 늘리고 게이트로 거르면 통과율이 더 올라간다. &lt;b&gt;재시도가 공짜가 아니라 예산&lt;/b&gt;이라는 발상이 운영의 핵심이다.&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;요약하면 Google이 한 일은 &quot;LLM을 똑똑하게 만들기&quot;가 아니라 &lt;b&gt;&quot;LLM은 어차피 깨진다고 가정하고 절차로 거르기&quot;&lt;/b&gt;다. 이 발상의 차이가 사내 593건을 돌릴 수 있게 한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자동화는 어디까지 &amp;mdash; translate-then-fix 패러다임의 등장&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google 게이트를 사람이 운영했다면, 학계는 이를 자동화하려고 한다. 그 흐름이 &lt;b&gt;translate-then-fix&lt;/b&gt; 패러다임이다.&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;대표 논문이 &lt;a href=&quot;https://arxiv.org/abs/2409.19894&quot;&gt;&lt;b&gt;TransAgent&lt;/b&gt;&lt;/a&gt;다. fine-grained execution alignment라는 발상으로, LLM이 일단 옮긴 결과물을 &lt;b&gt;컴파일러에 직접 넣어서&lt;/b&gt; syntax error를 받고, 그 에러 메시지를 다시 LLM에게 던져 iterative하게 패치한다. &quot;한 번에 끝내는 번역 vs 반복 검증&quot;의 정량 비교에서, 반복 검증이 단번 번역보다 측정 정확도를 유의미하게 올렸다는 결과를 보고한다.&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;비슷한 결의 후속 작업이 여럿이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://arxiv.org/abs/2312.13010&quot;&gt;&lt;b&gt;AgentCoder&lt;/b&gt;&lt;/a&gt;: generator + tester + executor 3개 에이전트를 분리한다. generator가 만들고, tester가 케이스를 만들고, executor가 돌린다. 한 모델이 다 하는 것보다 분리했을 때 정확도가 올라간다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2410.02089&quot;&gt;&lt;b&gt;RLEF&lt;/b&gt;&lt;/a&gt;: 실행 결과를 강화학습 보상으로 써서 코드 LLM SOTA를 갱신한다. 컴파일&amp;middot;테스트 통과 여부를 RL signal로 사용하는 것이다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/2510.16809&quot;&gt;&lt;b&gt;Many-Shot Fails 보고&lt;/b&gt;&lt;/a&gt;: 흥미로운 반전인데, &lt;b&gt;코드 번역에서는 many-shot prompting이 오히려 역효과&lt;/b&gt;라는 측정 결과다. 일반 자연어 태스크에서는 예시를 많이 주면 잘하는데, 코드 번역은 예시가 늘면 정확도가 떨어졌다. 즉 &quot;프롬프트에 예시 잔뜩 박기&quot; 전략이 이 도메인에서는 통하지 않는다.&lt;/li&gt;
&lt;/ul&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;b&gt;&quot;LLM 출력 &amp;rarr; 외부 검증 도구 &amp;rarr; 다시 LLM&quot;&lt;/b&gt; 루프다. 게이트를 자동화하든 RL signal로 박든, 외부 도구의 ground truth를 LLM 안으로 가져오는 것이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Python agent 파이프라인을 Go로 옮기는 절차서&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 일반론이다. 이제 Python agent 파이프라인을 Go로 옮긴다고 가정하고 위 학계 발견 6개를 stage-by-stage 절차로 박아본다. 어떤 언어 페어든 응용 가능한 일반 절차이고, Python &amp;rarr; Go는 한 토막 사례다.&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;&lt;b&gt;1단계 &amp;mdash; 인터페이스&amp;middot;struct 먼저 정의&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번에 옮기지 말고, 타겟 언어가 요구하는 구조부터 손으로 박는다. Pan et al.가 분류한 &quot;target language requirement 위반&quot; 버그 카테고리를 회피한다. agent 파이프라인이라면 Stage interface, Pipeline struct, Result struct를 사람이 먼저 정의한다. 이것이 LLM의 자유도를 줄여서 환각 면적을 좁힌다.&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;&lt;b&gt;2단계 &amp;mdash; stage 단위로 분리 번역&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일을 통째로 옮기지 말고 함수&amp;middot;메서드 단위로 쪼갠다. Google 사례에서 파일 단위가 평균 3회 재시도였던 점을 떠올리면, 더 작은 단위는 1~2회로 끝낼 수 있다. 함수 &amp;lt; 모듈 &amp;lt; 패키지 순서로 작을수록 환각 노출 면적이 작다.&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;&lt;b&gt;3단계 &amp;mdash; 각 stage마다 6단 게이트 적용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google 6단 게이트를 stage마다 강제한다. AST 파싱(3단)은 go/parser 패키지로 자동화가 가능하고, Build(5단)는 go build, Test(6단)는 go test로 자동화한다. Punt 셀프 체크(4단)만 LLM 호출이 또 들어가는데, 이는 비용 대비 효과가 크므로 유지한다.&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;&lt;b&gt;4단계 &amp;mdash; AST diff 검증&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransAgent 패러다임을 차용하여, 옮긴 Go 코드의 AST와 원본 Python AST의 구조적 동등성을 diff로 확인한다. 함수 시그니처 개수, 분기 깊이, 반복 구조가 맞는지 자동 비교한다. 이 단계에서 dead code나 incorrect output format 버그가 잡힌다.&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;&lt;b&gt;5단계 &amp;mdash; Go가 끝점이 아니라 lingua franca임을 의식&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unraveling 발견을 떠올린다. 지금 Go로 옮기는 작업은 그 자체로 가치 있지만, 동시에 &lt;b&gt;이후 Rust나 다른 언어로 또 옮길 때 출발점&lt;/b&gt;으로 쓸 수 있게 코드를 정돈한다. 즉 Go 컨벤션에 100% 맞춰 두기 &amp;mdash; generic, error 처리, package 분리 &amp;mdash; 그것이 다음 단계 번역의 정확도까지 올린다.&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;&lt;b&gt;6단계 &amp;mdash; 라이브러리 의존부는 별도 처리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransLibEval의 39.73% Object definition 에러를 의식하여, numpy&amp;middot;pandas 같은 의존 코드는 LLM에 한 번에 던지지 않는다. 동등한 Go 추상화(gonum, 자체 struct 등)를 사람이 먼저 결정한 뒤, &lt;b&gt;그 결정을 프롬프트에 박아서&lt;/b&gt; LLM이 추측하지 않도록 한다. 의존성 매핑은 LLM의 가장 약한 영역이므로 인간의 결정이 들어가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 옮기지 말고 절차로 환각을 거른다 &amp;mdash; 학계 12개월 실측이 확정한 사실&lt;/h2&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;&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;ICSE 2024 1700 샘플 실측에서 한 번에 시킨 LLM 코드 번역의 정확도는 &lt;b&gt;2.1~47.3%&lt;/b&gt;다. 절반도 맞히지 못한다. 본인의 프롬프트 문제가 아니라 모델의 본질적 한계다.&lt;/li&gt;
&lt;li&gt;깨지는 양상은 무작위가 아니라 &lt;b&gt;15개 버그 카테고리&lt;/b&gt;로 분류된다. TransLibEval은 Object definition 에러만 39.73%로 측정했다.&lt;/li&gt;
&lt;li&gt;Google 12개월 사내 사례(595 변경)는 환각율 25.55%인데도 운영을 굴렸다. 비결은 &lt;b&gt;6단 validation gate(Success &amp;rarr; Whitespace &amp;rarr; AST &amp;rarr; Punt &amp;rarr; Build &amp;rarr; Test) + 파일당 3회 시도 예산&lt;/b&gt;이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Go가 lingua franca&lt;/b&gt;라는 반전 발견(Unraveling, 14언어 &amp;times; 4 LLM)이 있다. source &amp;rarr; Go &amp;rarr; target 2단이 직접 번역보다 정확하다.&lt;/li&gt;
&lt;li&gt;자동화는 &lt;b&gt;translate-then-fix 패러다임&lt;/b&gt;(TransAgent, AgentCoder, RLEF)으로 진행 중이지만, &lt;b&gt;many-shot prompting은 오히려 역효과&lt;/b&gt;다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각설하고, LLM 코드 번역에서 깨지지 않으려면 결국 &lt;b&gt;&quot;한 번에 시키지 말 것&quot;&lt;/b&gt; 한 줄로 요약된다. Pan et al., Google, Unraveling, TransAgent, TransLibEval, CodeMEnv까지 학계 6편이 12개월 넘게 실측한 사실이다. 직접 옮기지 말고 절차로 환각을 거른다 &amp;mdash; 이것이 LLM 코드 마이그레이션의 운영 원칙이다.&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;다음 글에서는 위 6단 게이트를 실제 CI 파이프라인(GitHub Actions + Go 빌드 + 자동 LLM 재시도)에 박는 코드를 풀어볼 예정이다. 이 글은 학계 정리, 다음 글은 구현 &amp;mdash; 이렇게 한 세트로 묶어 시리즈로 진행할 것이다.&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>LLM 코드 마이그레이션</category>
      <category>Python Go 변환</category>
      <category>translate-then-fix</category>
      <category>validation gate 코드</category>
      <category>코드 번역 환각</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/419</guid>
      <comments>https://yscho03.tistory.com/419#entry419comment</comments>
      <pubDate>Sat, 9 May 2026 00:21:31 +0900</pubDate>
    </item>
    <item>
      <title>504 Gateway Timeout 진짜 누가 끊었는가 &amp;mdash; backend&amp;middot;nginx&amp;middot;ALB&amp;middot;Cloudflare 4단 스택을 직접 띄워 헤더로 귀속하다</title>
      <link>https://yscho03.tistory.com/418</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중에 504가 떴을 때 일단 nginx의 proxy_read_timeout부터 늘리고 보는 사람이 많은데, 그것이 답이 아닐 수 있다. 진짜로 끊은 주체가 nginx가 아니라 ALB(Amazon 로드밸런서)나 Cloudflare인 경우가 더 자주 있으며, 근거 없이 nginx만 만지면 같은 에러가 또 발생한다.&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;각설하고 결론부터 박는다. &lt;b&gt;504 Gateway Timeout 원인을 nginx에만 떠넘기면 ALB가 끊은 케이스를 놓친다.&lt;/b&gt; 4단 스택(backend &amp;rarr; nginx &amp;rarr; ALB &amp;rarr; Cloudflare)에서는 누가 먼저 timeout을 만료시키느냐에 따라 응답 status 한 줄과 식별자 헤더 3개(X-Nginx-Trace, X-Amzn-Trace-Id, CF-Ray)의 존재&amp;middot;부재 조합이 결정론적으로 갈린다. 그 조합만 보고 30초 안에 어느 레이어가 끊었는지 귀속할 수 있다.&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;비유하면 이렇다. 택배가 한국 &amp;rarr; 일본 &amp;rarr; 미국 &amp;rarr; 우리집 4단을 경유하는데, 어디서 멈췄는지 알려면 각 거점이 찍은 도장 3개를 보면 된다. 도장이 어디서부터 안 찍혔는지가 곧 누가 끊었는지의 증거다. 이 글은 그 도장을 직접 만들어 5개 시나리오로 실측한 결과를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;504와 524는 같지 않다 &amp;mdash; 코드부터 다른 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색해보면 504와 524를 묶어 &quot;둘 다 timeout이니 같은 것&quot;으로 설명하는 글이 더러 있는데, 진단 측면에서는 완전히 다른 신호다. 코드가 다르다는 것은 발급한 주체가 다르다는 뜻이며, 발급 주체가 다르면 fix해야 할 위치도 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;504(Gateway Timeout)는 RFC 7231 표준이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;504는 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7231#section-6.6.5&quot;&gt;RFC 7231 표준 status code&lt;/a&gt;이다. 게이트웨이나 프록시 역할을 하는 어떤 서버든 발급할 수 있다. 즉 nginx, ALB, HAProxy, Cloudflare 모두 504를 던질 권한이 있다. 그래서 504만 보고는 누가 끊었는지 단정할 수 없다. 헤더 추가 분기가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;524는 Cloudflare 전용이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;524는 표준이 아니라 &lt;a href=&quot;https://developers.cloudflare.com/support/troubleshooting/cloudflare-errors/troubleshooting-cloudflare-5xx-errors/#error-524-a-timeout-occurred&quot;&gt;Cloudflare 자체 상태 코드&lt;/a&gt;다. Cloudflare가 origin(원본 서버)과 TCP 연결은 했는데 100초 안에 HTTP 응답을 받지 못하면 발급한다. 즉 524가 보이면 origin이 살아 있긴 하지만 너무 느린 것이다. 백엔드 처리가 느리다는 신호이지 네트워크 끊김 신호가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;504/524 분기 자체가 진단 단서다&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;status 524 &amp;rarr; 무조건 Cloudflare가 끊은 것. 백엔드 처리 시간 점검부터&lt;/li&gt;
&lt;li&gt;status 504 &amp;rarr; nginx 또는 ALB가 끊었음. 헤더로 추가 분기 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 중에 524가 떴는데 &quot;Cloudflare 끄면 되겠지&quot; 하고 끄는 것은 헛삽질이다. 524는 origin이 느리다는 신호일 뿐, 끄면 원인이 보이지 않을 뿐 사라지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;504 Gateway Timeout 원인을 가르는 식별자 헤더 3개&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;504 Gateway Timeout 디버깅에 쓸 수 있는 식별자 헤더가 정확히 3개 있다. 이 3개가 결정론적으로 어느 레이어가 응답을 만졌는지 알려준다. 없는 헤더는 곧 그 레이어를 통과하지 못했다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;X-Nginx-Trace &amp;mdash; nginx가 응답을 만졌는지 확인용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx가 직접 발급한 504라면 이 헤더가 살아 있다. nginx는 자신이 만든 응답에 자신의 식별자를 부착한다. 만약 504 응답에 X-Nginx-Trace가 없다면 nginx보다 위 레이어(ALB나 Cloudflare)가 nginx 응답이 도달하기 전에 먼저 끊은 것이다.&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;함정이 있다. nginx 설정에서 add_header X-Nginx-Trace $request_id;만 쓰고 always를 붙이지 않으면 200 응답에만 헤더가 붙고 504에는 붙지 않는다. 그러면 진단이 무용지물이 된다. 무조건 always를 붙여야 한다. 이는 글 마지막에 다시 박는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;X-Amzn-Trace-Id &amp;mdash; ALB가 연결을 받았는지 증거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ALB는 통과한 모든 요청&amp;middot;응답에 X-Amzn-Trace-Id를 자동 부착한다. 별도 설정이 필요 없다. 즉 status 504 + X-Nginx-Trace 부재 + X-Amzn-Trace-Id 존재 조합이라면 &quot;ALB가 nginx 응답이 도착하기 전에 idle timeout으로 끊었다&quot;는 명백한 증거다.&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;이것이 가장 흔한 오진 케이스다. nginx의 proxy_read_timeout을 60초로 두고 ALB도 60초인 채로 두면 ALB가 5~10ms 차이로 먼저 끊는 경우가 많다. 그 상황에서 nginx만 늘리면 효과가 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CF-Ray &amp;mdash; Cloudflare가 끊었는지 최종 분기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CF-Ray는 Cloudflare가 처리한 모든 요청&amp;middot;응답에 자동 부착된다. 524 응답에는 무조건 존재한다. 504 응답에서도 origin 응답을 그대로 relay했을 때 CF-Ray가 함께 따라온다.&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;즉 status 504 + X-Nginx-Trace 부재 + X-Amzn-Trace-Id 부재 + CF-Ray 존재라면 매우 드문 케이스인데, Cloudflare가 504를 직접 발급한 것일 수 있다. 이때는 origin 연결 자체가 안 됐을 가능성도 있어 origin health부터 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 띄워 검증한 실험 &amp;mdash; 4단 스택을 단일 파이썬 프로세스로&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 이론이다. 다만 이론만 보면 &quot;그래서 진짜 그렇게 동작하는가?&quot;라는 의심이 남는다. 그래서 직접 띄워 확인해봤다. 운영 환경을 만지지 말고 단일 파이썬 프로세스 안에 4단 스택을 in-process simulator로 재현한 것이다.&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;실험명은 504-gateway-timeout-layers이며, python:3.12-slim 컨테이너에서 메모리 512MB, CPU 1.0, 네트워크 deny 환경으로 돌렸다. 외부 의존 없이 단일 프로세스만으로 결정론적 재현이 가능하도록 만든 것이다. 전체 실험은 66.64초 만에 끝났고 exit 0으로 PASS했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 in-process 시뮬레이터로 했는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경 timeout 60초를 그대로 쓰면 한 시나리오 검증에만 1분 이상 걸린다. 5개 시나리오 + race 6회 반복이면 11분+ 소요되며, 결정론적 재현도 어렵다(외부 네트워크 변동의 영향). 그래서 시간 스케일을 0.1배로 압축했다. 운영 60초 &amp;rarr; 실험 6초로 줄이니 전체 실험이 60초 안에 끝나면서도 race condition 같은 미세 시간차 현상은 그대로 재현된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구성 &amp;mdash; ThreadingHTTPServer 4개 + 식별자 헤더 relay&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backend, nginx-sim, alb-sim, cloudflare-sim 네 개의 http.server.ThreadingHTTPServer 인스턴스를 단일 프로세스 안에 띄운다. 각 레이어는 upstream 응답을 그대로 흘려보내되 자기 식별자 헤더만 덮어쓴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;nginx-sim &amp;rarr; X-Nginx-Trace 부착&lt;/li&gt;
&lt;li&gt;alb-sim &amp;rarr; X-Amzn-Trace-Id 부착&lt;/li&gt;
&lt;li&gt;cloudflare-sim &amp;rarr; CF-Ray + Server: cloudflare 부착&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 레이어가 자기 timeout을 만료시키면 nginx와 ALB는 504를, Cloudflare는 524를 발급하도록 구현했다. http.client.HTTPConnection의 timeout만 사용해 redis&amp;middot;실제 nginx&amp;middot;ALB&amp;middot;Cloudflare 부재 환경에서도 그대로 재현 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;축 4개로 시나리오 5종 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;축은 backend_delay &amp;times; nginx_timeout &amp;times; alb_timeout &amp;times; cloudflare_timeout이다. 이 네 변수 조합으로 시나리오 5종을 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;healthy&lt;/b&gt;: 모든 timeout이 backend_delay보다 큼 &amp;rarr; 정상 통과&lt;/li&gt;
&lt;li&gt;&lt;b&gt;nginx_kills&lt;/b&gt;: nginx_timeout이 가장 짧음 &amp;rarr; nginx가 자체 cut&lt;/li&gt;
&lt;li&gt;&lt;b&gt;alb_kills&lt;/b&gt;: alb_timeout이 nginx보다 짧음 &amp;rarr; ALB가 nginx 응답 전에 cut&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cloudflare_kills&lt;/b&gt;: cf_timeout이 가장 짧음 &amp;rarr; Cloudflare가 524 발급&lt;/li&gt;
&lt;li&gt;&lt;b&gt;race&lt;/b&gt;: nginx_timeout = alb_timeout (동일) &amp;rarr; 경합 발생, 6회 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5개 시나리오 실측 결과 매트릭스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;504 Gateway Timeout 원인별 응답 헤더 패턴을 매트릭스로 정리하면 다음과 같다. 진단할 때 이 표 한 장만 옆에 두고 보면 된다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;status&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;elapsed (s)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X-Nginx-Trace&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X-Amzn-Trace-Id&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CF-Ray&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;누가 끊었나&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;healthy&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;2.008&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;정상 통과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nginx_kills&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;3.020&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;nginx (proxy_read_timeout 만료)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alb_kills&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;4.007&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;ALB (idle_timeout 만료, nginx 응답 전 cut)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudflare_kills&lt;/td&gt;
&lt;td&gt;524&lt;/td&gt;
&lt;td&gt;5.007&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;Cloudflare (hard ceiling 만료)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;race&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.012&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;ALB (6/6 모두 ALB가 먼저 끊음)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;backend_delay&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;nginx_timeout&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;alb_timeout&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cf_timeout&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;body_preview&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;healthy&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;`hello-from-backend`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nginx_kills&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;`nginx: upstream cut (TimeoutError), status=504`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alb_kills&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;`alb: upstream cut (TimeoutError), status=504`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cloudflare_kills&lt;/td&gt;
&lt;td&gt;15.0&lt;/td&gt;
&lt;td&gt;20.0&lt;/td&gt;
&lt;td&gt;20.0&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;`cloudflare: upstream cut (TimeoutError), status=524`&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결정론적 시나리오 4종은 예상한 status code와 헤더 매트릭스에 100% 일치한다. 즉 헤더 부재&amp;middot;존재 조합으로 발급 주체를 귀속하는 결정 트리가 실제로 결정론적임을 확인한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;race 케이스가 가장 헷갈리는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;race 시나리오는 의도적으로 nginx_timeout = alb_timeout = 6.0s로 동일하게 두고 6회 반복했다. backend_delay는 10.0s, cf_timeout은 12.0s다. 결과는 다음과 같이 나왔다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;trial&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;status&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;elapsed (s)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X-Nginx-Trace&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;X-Amzn-Trace-Id&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CF-Ray&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;진단&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.010&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;alb_cut_before_nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.010&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;alb_cut_before_nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.019&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;alb_cut_before_nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.010&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;alb_cut_before_nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.009&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;alb_cut_before_nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;504&lt;/td&gt;
&lt;td&gt;6.013&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;alb_cut_before_nginx&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6회 모두 ALB가 먼저 끊은 것으로 귀속됐고, 평균 elapsed는 6.012초다. 이 환경에서는 ALB가 nginx보다 항상 미세하게 먼저 끊은 것이다. 다만 운영 환경에서는 백엔드 부하&amp;middot;네트워크 jitter 때문에 분포가 갈릴 수 있다. 그래서 nginx와 ALB의 timeout을 동일하게 두면 안 된다. 운영 권장은 &quot;nginx &amp;lt; ALB&quot; 구성이다 (예: nginx 60s, ALB 65s). 이렇게 하면 항상 nginx가 먼저 끊어 X-Nginx-Trace로 깔끔하게 귀속된다.&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;stdout 핵심 라인을 그대로 인용하면 다음과 같이 출력됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778250951666&quot; class=&quot;asciidoc&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;================================================================================================
4-layer HTTP stack cut-off attribution sweep (time scale 0.1x)
================================================================================================
scenario               status   elapsed  nginx  alb  cf  diagnosis                    match
------------------------------------------------------------------------------------------------
healthy                   200     2.008      Y    Y   Y  healthy_full_relay           OK
nginx_kills               504     3.020      Y    Y   Y  nginx_cut                    OK
alb_kills                 504     4.007      .    Y   Y  alb_cut_before_nginx         OK
cloudflare_kills          524     5.007      .    .   Y  cloudflare_cut               OK
------------------------------------------------------------------------------------------------
race_nginx_vs_alb (n=6, nginx=alb=6.0s, backend=10.0s, cf=12.0s):
  alb_cut_before_nginx         6/6
  avg elapsed: 6.012s
================================================================================================

deterministic scenario validation: ALL MATCH EXPECTED
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;30초 안에 끝나는 504 Gateway Timeout 진단 결정 트리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;504 Gateway Timeout 빠른 디버깅 절차다. 응답 한 번만 까보면 바로 결론이 나온다. 시간을 측정해봤는데 헤더 보고 분기 따라가면 진짜로 30초 컷이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778250951666&quot; class=&quot;markdown&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;1. 응답 status 확인
   - 524 &amp;rarr; Cloudflare가 끊음. backend 응답 시간 점검 (origin이 살아있는데 느림)
   - 504 &amp;rarr; 2번으로
2. X-Nginx-Trace 헤더 존재?
   - O &amp;rarr; nginx가 자체 cut. proxy_read_timeout / proxy_connect_timeout 점검
   - X &amp;rarr; 3번으로
3. X-Amzn-Trace-Id 헤더 존재?
   - O &amp;rarr; ALB가 nginx 응답 전에 cut. ALB idle_timeout &amp;gt; nginx 전체 응답 시간으로 늘려라
   - X &amp;rarr; 매우 드문 케이스. Cloudflare가 504 직접 발급한 것일 수 있음. CF-Ray로 재확인
&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;실험 stdout이 같은 트리를 그대로 출력했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778250951666&quot; class=&quot;coq&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;status 524                                                 -&amp;gt; Cloudflare self-cut
status 504  +  X-Nginx-Trace present                       -&amp;gt; nginx self-cut (proxy_read_timeout)
status 504  +  X-Nginx-Trace absent  +  X-Amzn-Trace-Id ON -&amp;gt; ALB cut before nginx responded
status 200  +  all 3 IDs present                           -&amp;gt; healthy full relay
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;흔한 오진 패턴&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;일단 nginx 늘려보자&quot;&lt;/b&gt; &amp;rarr; ALB가 끊은 것이라면 효과가 0이다. 실험의 alb_kills 케이스가 정확히 이 상황이다. nginx_timeout이 8.0s, alb_timeout이 4.0s인데 nginx를 아무리 늘려도 ALB가 4초에서 자른다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;Cloudflare 비활성화하면 되겠지&quot;&lt;/b&gt; &amp;rarr; 524는 origin 느림 신호다. Cloudflare를 끄면 원인이 보이지 않을 뿐 사라지지 않는다. 같은 백엔드는 ALB&amp;middot;nginx 단계에서 똑같이 timeout을 만료시킬 것이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영에서 바로 쓸 수 있는 timeout 권장값 + 헤더 노출 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 왔으면 이제 운영에서 적용할 차례다. 권장 timeout 순서와 nginx 설정 한 줄만 박으면 다음번 504가 떴을 때 즉시 진단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 레이어 timeout 권장 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;backend 처리 시간 SLO 30초 가정 시 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;backend 처리 SLO&lt;/b&gt;: 30s&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;a href=&quot;https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout&quot;&gt;nginx proxy_read_timeout&lt;/a&gt;&lt;/b&gt;: 60s (= SLO &amp;times; 2)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;a href=&quot;https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout&quot;&gt;ALB idle_timeout&lt;/a&gt;&lt;/b&gt;: 65s (nginx보다 5초 위)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cloudflare hard ceiling&lt;/b&gt;: 100s (Enterprise면 6000s까지 늘릴 수 있음)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두면 race가 나지 않고 항상 nginx가 먼저 끊어 진단이 깔끔해진다. ALB가 5초 여유가 있어 nginx 응답이 도달할 시간이 보장된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;X-Nginx-Trace 헤더 활성화 &amp;mdash; nginx에서 직접 발급&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nginx 설정에서 한 줄만 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1778250951667&quot; class=&quot;nginx&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;nginx&quot;&gt;&lt;code&gt;add_header X-Nginx-Trace $request_id always;
&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;always가 핵심이다. 이를 빠뜨리면 200 응답에만 헤더가 붙고 504 응답에는 붙지 않아 진단이 무용지물이 된다. nginx 기본 동작은 2xx&amp;middot;3xx만 헤더를 부착하므로 4xx&amp;middot;5xx는 명시적으로 always 키워드를 줘야 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ALB&amp;middot;Cloudflare 헤더는 자동이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X-Amzn-Trace-Id는 ALB가 자동 부착한다. CF-Ray도 Cloudflare가 자동 부착한다. 별도 설정 없이 그대로 받으면 된다. 즉 운영자가 직접 만져야 하는 것은 nginx의 add_header X-Nginx-Trace $request_id always; 한 줄뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마지막 정리 &amp;mdash; 504 Gateway Timeout 원인 추적은 헤더 한 줄에서 시작된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길게 썼는데 핵심은 5문장으로 압축된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;504와 524는 같은 것이 아니다. 524가 보이면 origin이 느린 신호다&lt;/li&gt;
&lt;li&gt;X-Nginx-Trace, X-Amzn-Trace-Id, CF-Ray 헤더 3개 + status 한 줄로 어느 레이어가 끊었는지 즉시 알 수 있다&lt;/li&gt;
&lt;li&gt;nginx와 ALB가 동일 timeout이면 race가 발생한다 &amp;rarr; ALB &amp;gt; nginx로 5초 갭을 두라 (실험에서 6/6 모두 ALB가 먼저 끊는 결과 확인)&lt;/li&gt;
&lt;li&gt;진단 결정 트리는 30초 안에 끝난다 &amp;rarr; status &amp;rarr; X-Nginx-Trace &amp;rarr; X-Amzn-Trace-Id 순으로 분기&lt;/li&gt;
&lt;li&gt;직접 in-process 시뮬레이터를 띄워 결정론적으로 재현 가능하다. 0.1배 시간 스케일 압축으로 60초 안에 5개 시나리오 + race 6회를 모두 검증했다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 운영 환경에서 nginx의 add_header X-Nginx-Trace $request_id always; 한 줄부터 추가하라. 다음번 504가 떴을 때 응답 헤더 한 번만 까보면 누가 끊었는지 30초 안에 나온다. 504 Gateway Timeout 원인 추적은 결국 식별자 헤더 설정 한 줄에서 시작된다. 운영 60초 timeout을 그대로 만지지 않고도 단일 파이썬 프로세스에서 결정론적으로 재현 가능한 디버깅 환경을 만들 수 있다는 것, 이번 실험으로 직접 확인했다.&lt;/p&gt;</description>
      <category>Backend</category>
      <category>504 vs 524 차이</category>
      <category>ALB idle timeout</category>
      <category>Cloudflare 524 error</category>
      <category>nginx proxy_read_timeout</category>
      <category>X-Amzn-Trace-Id 디버깅</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/418</guid>
      <comments>https://yscho03.tistory.com/418#entry418comment</comments>
      <pubDate>Fri, 8 May 2026 23:36:09 +0900</pubDate>
    </item>
    <item>
      <title>MySQL REPEATABLE READ가 정말로 phantom read를 막는지 18셀 실험으로 직접 검증하다</title>
      <link>https://yscho03.tistory.com/417</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 표준 표를 보면 REPEATABLE READ 칸에 phantom read &quot;허용&quot;이라고 명확히 적혀 있다. 학교에서도, 책에서도, 인터넷 블로그 99%가 그 표를 그대로 베껴 옮긴다. 그러나 정작 MySQL InnoDB에서 직접 돌려보면 phantom은 발생하지 않는다. 표가 거짓말을 하는 것인가 싶을 정도이다.&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;면접에서 &quot;MySQL 기본 격리 수준이 무엇이며 phantom read를 막아주는가?&quot;라는 질문이 나오면 답이 갈린다. 표를 외운 사람은 &quot;RR이고 phantom은 막지 못한다&quot;라고 답하고, MySQL을 실제로 다뤄본 사람은 &quot;RR이고 phantom까지 막는다&quot;라고 답한다. 둘 다 맞을 수 있는 미묘한 영역이지만 정확히 짚어주는 한국어 자료는 거의 없었다.&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;그래서 이번에 mysql 격리 수준 동작을 18셀(3 격리수준 &amp;times; 2 읽기 변형 &amp;times; 3 이상현상)의 매트릭스로 짜서 직접 돌려봤다. MariaDB를 띄워 두 트랜잭션을 동시에 굴리며 어디에서 막히고 어디에서 깨지는지 정량적으로 측정했다. SQL 표준 명세표와 InnoDB 실측이 어디에서 일치하고 어디에서 갈라지는지 차트로 함께 제시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;Phantom Read란?&lt;/h1&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시)&lt;/p&gt;
&lt;pre id=&quot;code_1777990610611&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;트랜잭션 A (당신)
1) SELECT * FROM users WHERE age &amp;gt; 20;
   결과: 3명 (이름: A, B, C)

2) [다른 트랜잭션이 age &amp;gt; 20인 사용자 D를 추가]

3) SELECT * FROM users WHERE age &amp;gt; 20;
   결과: 4명 (이름: A, B, C, D) &amp;larr; D가 새로 나타남!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SQL 표준이 정의한 격리 수준 4가지부터 복습한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dirty read, non-repeatable read, phantom read, lost update란 각각 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리 수준은 &quot;어떤 이상 현상을 막느냐&quot;로 정의된다. 막아야 할 이상 현상은 네 종류이다.&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;dirty read는 다른 트랜잭션이 commit하지 않은 데이터를 읽어버리는 현상이다. 매우 위험하다. non-repeatable read는 같은 row를 동일 트랜잭션 안에서 두 번 읽었는데 값이 달라지는 현상이다. 그 사이에 다른 트랜잭션이 UPDATE 후 commit한 경우에 발생한다. phantom read는 같은 조건으로 SELECT를 두 번 했는데 row 개수가 달라지는 현상이다. 다른 트랜잭션이 매칭 row를 INSERT하고 commit하면 일어난다. 마지막으로 lost update는 두 트랜잭션이 같은 값을 동시에 읽고 각자 갱신해 commit하면 한쪽 갱신이 통째로 사라지는 현상이다. 잔액 +100을 두 번 했는데 +100만 반영되는 그 시나리오다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;623&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bWMHbx/dJMcacbVCyZ/XlXakAADuDylmAcxLCgdw0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bWMHbx/dJMcacbVCyZ/XlXakAADuDylmAcxLCgdw0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bWMHbx/dJMcacbVCyZ/XlXakAADuDylmAcxLCgdw0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbWMHbx%2FdJMcacbVCyZ%2FXlXakAADuDylmAcxLCgdw0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1132&quot; height=&quot;623&quot; data-origin-width=&quot;1132&quot; data-origin-height=&quot;623&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://medium.com/@abhi18632/transaction-isolation-updated-eef62fb2c5bf&quot;&gt;Medium (29KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 표준 명세표 (lost update 포함 확장 버전)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI SQL 1992 원문은 dirty read&amp;middot;non-repeatable read&amp;middot;phantom read 세 가지 read phenomena만 정의한다. lost update는 1995년 Berenson 외 4인의 &quot;A Critique of ANSI SQL Isolation Levels&quot; 논문에서 P4로 추가된 항목이다. 인터넷에 가장 많이 도는 4열짜리 매트릭스는 그 둘을 합친 확장판이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;dirty read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;non-repeatable read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;phantom read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;lost update&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ UNCOMMITTED&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&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;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 RR 칸의 phantom read는 &quot;허용&quot;이다. 표만 보면 &quot;RR에서 phantom이 일어난다&quot;가 답인데, MySQL InnoDB에서 정말 그러한지가 이 글의 본론이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다만 MySQL InnoDB는 표대로 움직이지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;InnoDB 기본 격리 수준은 RR이고 표준보다 한 단계 엄격하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL/MariaDB InnoDB의 기본 격리 수준은 REPEATABLE READ이다. PostgreSQL이 READ COMMITTED를 기본으로 두는 것과는 다르다. 그리고 InnoDB의 RR은 SQL 표준이 정의한 RR보다 한 단계 더 엄격하게 동작한다. &lt;b&gt;phantom read도 차단하고, lost update 또한 일반 SELECT 기준으로는 차단해버린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MVCC 스냅샷과 gap lock이 함께 동작한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB가 RR에서 phantom까지 막는 비결은 두 메커니즘이 함께 동작하기 때문이다.&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;첫 번째는 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html&quot;&gt;MVCC consistent read&lt;/a&gt;, 즉 스냅샷 읽기이다. 트랜잭션이 첫 SELECT를 던지는 순간 그 시점의 read view를 고정한다. 일반 SELECT는 이 스냅샷에서만 읽으므로 다른 트랜잭션이 INSERT/UPDATE/commit을 아무리 수행해도 보이지 않는다. 두 번째는 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html&quot;&gt;gap lock과 next-key lock&lt;/a&gt;이다. SELECT ... FOR UPDATE나 UPDATE처럼 잠금을 거는 읽기는 인덱스 range를 잠가버린다. 그 range 안으로 INSERT가 들어오려 하면 물리적으로 블로킹된다.&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;이 두 메커니즘이 같은 RR 안에서 따로 돌아가는 것이 사람들이 헷갈리는 부분이다. &lt;b&gt;consistent read는 phantom을 &quot;보이지 않게&quot; 하고, gap lock은 phantom INSERT 자체를 &quot;물리적으로 못 들어오게&quot; 한다.&lt;/b&gt; 결과는 둘 다 phantom 차단이지만 메커니즘은 완전히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 18셀 매트릭스를 돌려보았다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스키마와 두 트랜잭션 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블은 users(id, balance, age) 한 장이다. 초기 상태는 row 9건, 모두 age &amp;le; 30, 한 row의 balance = 1000이다.&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;각 셀마다 두 트랜잭션 Tx1&amp;middot;Tx2를 동시에 띄워 다음 세 시나리오를 굴렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777990610612&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- phantom_read 시나리오
-- Tx1
START TRANSACTION;
SELECT COUNT(*) FROM users WHERE age &amp;gt; 30;  -- 첫 번째
-- (잠깐 대기)
SELECT COUNT(*) FROM users WHERE age &amp;gt; 30;  -- 두 번째
COMMIT;

-- Tx2 (Tx1의 두 SELECT 사이에 끼어듦)
START TRANSACTION;
INSERT INTO users (id, balance, age) VALUES (10, 500, 35);
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777990610612&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- non_repeatable_read 시나리오
-- Tx1
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;  -- 첫 번째
-- (잠깐 대기)
SELECT balance FROM users WHERE id = 1;  -- 두 번째
COMMIT;

-- Tx2
START TRANSACTION;
UPDATE users SET balance = 9999 WHERE id = 1;
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777990610612&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- lost_update 시나리오 (Tx1&amp;middot;Tx2가 동시에 +100)
-- Tx1과 Tx2 둘 다
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;
-- (계산: balance + 100)
UPDATE users SET balance = ? WHERE id = 1;
COMMIT;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 SELECT vs SELECT FOR UPDATE 두 변형을 모두 측정한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 격리 수준이라도 일반 SELECT(consistent snapshot read)와 SELECT ... FOR UPDATE(locking read)는 동작이 다르다. 갭 락이 켜지는 지점이 어디인지를 자연스럽게 드러내려면 두 변형을 한 번에 비교해야 한다. 표만 보고 &quot;RR에서 phantom이 일어나지 않는다&quot;라고 외우면 잠금 읽기 동작을 놓치게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경 (MariaDB on python:3.12-slim, 68초 안에 종료)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;베이스 이미지&lt;/td&gt;
&lt;td&gt;python:3.12-slim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB 엔진&lt;/td&gt;
&lt;td&gt;InnoDB (MariaDB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;innodb_lock_wait_timeout&lt;/td&gt;
&lt;td&gt;5초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 한도&lt;/td&gt;
&lt;td&gt;1024 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU 한도&lt;/td&gt;
&lt;td&gt;2.0 vCPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스윕 규모&lt;/td&gt;
&lt;td&gt;18 셀 (3 &amp;times; 2 &amp;times; 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실측 소요&lt;/td&gt;
&lt;td&gt;68,175 ms (약 68.2초)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;종료 코드&lt;/td&gt;
&lt;td&gt;0 (PASS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 컨테이너에 MariaDB를 띄우고 PyMySQL로 두 세션을 동시 운용했다. MariaDB 기준이지만 vanilla InnoDB와 동일한 RR 동작이라고 보면 된다. Aurora나 TiDB 같은 호환 DB는 또 다를 수 있다. 이 부분은 본문 끝에서 한 줄 더 짚는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결과 매트릭스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;18셀 한 장 차트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 18셀의 결과를 한 장으로 정리하면 다음과 같다. 1 = ALLOWED(현상 발생), 0 = PREVENTED(차단)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djiNad/dJMcageliWP/Qk7gjEW5eDEIdlusQCqMw1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djiNad/dJMcageliWP/Qk7gjEW5eDEIdlusQCqMw1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djiNad/dJMcageliWP/Qk7gjEW5eDEIdlusQCqMw1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjiNad%2FdJMcageliWP%2FQk7gjEW5eDEIdlusQCqMw1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;420&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;420&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;변형&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;phantom_read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;non_repeatable_read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;lost_update&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;snapshot&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;for_update&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;snapshot&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 (`err_1020`)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;for_update&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;snapshot&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 (deadlock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;for_update&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0 (`err_1020`)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RC + 일반 SELECT는 phantom&amp;middot;non-repeatable&amp;middot;lost 셋 모두 발생한다. 표준 표와 일치하는 결과이다. RR + 일반 SELECT는 셋 모두 발생하지 않는다. 이 지점이 표준 표와 갈리는 부분인데, 표는 phantom&amp;middot;lost를 허용으로 적어두었다. RR + FOR UPDATE 또한 셋 모두 발생하지 않는다. 이번에는 갭 락이 INSERT를 블로킹하여 차단된다. SERIALIZABLE은 어떤 변형이든 셋 모두 차단된다. 다만 동시성 비용이 가장 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQL 표준과 InnoDB 실측이 갈라지는 지점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 표준 명세와 InnoDB 실측을 셀 단위로 비교하면 RR 줄에서 두 칸이 갈라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;305&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/REEoW/dJMcajhMufw/11wzaTjVf0uqlop9g3Aro1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/REEoW/dJMcajhMufw/11wzaTjVf0uqlop9g3Aro1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/REEoW/dJMcajhMufw/11wzaTjVf0uqlop9g3Aro1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FREEoW%2FdJMcajhMufw%2F11wzaTjVf0uqlop9g3Aro1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;305&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;305&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이상 현상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;SQL 표준 명세&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;InnoDB 실측&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;판정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;phantom_read&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;non_repeatable_read&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;lost_update&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;phantom_read&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;DIVERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;non_repeatable_read&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;lost_update&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;DIVERGE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;phantom_read&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;non_repeatable_read&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;lost_update&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;block&lt;/td&gt;
&lt;td&gt;MATCH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stdout으로 찍은 비교 결과 또한 동일한 이야기이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777990610615&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;text&quot;&gt;&lt;code&gt;=== Spec vs InnoDB (snapshot reads) ===
  [MATCH ] READ COMMITTED     phantom_read           spec=allow  innodb=allow
  [MATCH ] READ COMMITTED     non_repeatable_read    spec=allow  innodb=allow
  [MATCH ] READ COMMITTED     lost_update            spec=allow  innodb=allow
  [DIVERGE] REPEATABLE READ    phantom_read           spec=allow  innodb=block
  [MATCH ] REPEATABLE READ    non_repeatable_read    spec=block  innodb=block
  [DIVERGE] REPEATABLE READ    lost_update            spec=allow  innodb=block
  [MATCH ] SERIALIZABLE       phantom_read           spec=block  innodb=block
  [MATCH ] SERIALIZABLE       non_repeatable_read    spec=block  innodb=block
  [MATCH ] SERIALIZABLE       lost_update            spec=block  innodb=block
&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;표준은 RR에서 phantom&amp;middot;lost를 허용한다고 적었으나 &lt;b&gt;InnoDB는 둘 다 차단한다.&lt;/b&gt; 9개 셀 중 7개는 일치하고 RR에서 두 셀만 갈라지는 형태이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RC가 만드는 사고들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RC에서 일반 SELECT는 매 SELECT마다 새 read view를 잡는다. 따라서 다른 트랜잭션이 commit한 직후에는 그것이 곧바로 보인다. 결과적으로 phantom_read 시나리오에서는 Tx1의 첫 SELECT가 9, 두 번째 SELECT가 10으로 나온다. INSERT가 그대로 보이는 것이다. non_repeatable_read 시나리오에서는 Tx1이 1000을 보았다가 9999를 보게 된다. lost_update 시나리오에서는 최종 잔액이 1100으로 찍힌다. 기댓값은 1200인데 한쪽 +100이 통째로 사라진 것이다.&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;이는 SQL 표준이 정확히 예측한 그대로이다. RC + 일반 SELECT 조합을 실전 OLTP의 잔액&amp;middot;재고 관리 로직에 그대로 사용하면 망한다.&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;다만 RC에서도 SELECT FOR UPDATE로 바꾸면 non_repeatable&amp;middot;lost는 차단된다. 두 번째 SELECT 시점에 락이 잡혀 있어 Tx2 UPDATE가 1807ms 동안 블로킹되었다가 Tx1 commit 후 진행된다. 그러나 phantom은 여전히 발생한다. 갭 락이 RC에서는 켜지지 않으므로 INSERT는 그대로 들어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RR에서 일반 SELECT는 phantom까지 차단한다 (표와 정반대)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RR + 일반 SELECT의 결과 셀을 보면 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Tx1 첫 SELECT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Tx1 둘째 SELECT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비고&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;phantom_read&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Tx2 INSERT는 commit되었으나 Tx1엔 보이지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;non_repeatable_read&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;Tx2 UPDATE도 보이지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lost_update&lt;/td&gt;
&lt;td&gt;1100 / 1200&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;단 `err_1020`로 한쪽이 막혀 결과적으로 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;phantom 사이즈가 9 &amp;rarr; 9로 그대로 유지된 것이 묘미이다. Tx2가 분명 INSERT 후 commit까지 끝냈는데 Tx1의 두 번째 SELECT에서는 그것이 보이지 않는다. &lt;b&gt;MVCC가 트랜잭션 첫 SELECT 시점의 스냅샷을 고정해두기 때문이다.&lt;/b&gt; SQL 표준 표는 이 칸을 &quot;phantom 허용&quot;으로 적어두었지만 InnoDB는 그렇게 동작하지 않는다.&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;lost_update 셀의 err_1020 에러는 InnoDB가 &quot;row가 다른 트랜잭션에 의해 변경됨&quot;을 감지하고 한 트랜잭션을 강제로 실패시킨 케이스이다. 결과적으로 lost update 자체는 발생하지 않았으며, 애플리케이션이 retry 로직을 깔아둔다면 잔액 1200을 정확히 맞출 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RR에서 phantom이 차단되는 진짜 이유는 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;consistent read 경로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RR + 일반 SELECT가 phantom을 차단하는 메커니즘은 MVCC consistent read이다. 트랜잭션이 첫 SELECT를 던지는 순간 InnoDB가 read view, 즉 트랜잭션 ID 리스트를 만든다. 이후의 모든 SELECT는 그 시점에 commit된 row만 보고, 그 이후 commit된 row는 보지 않는다.&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;이 메커니즘은 Tx2의 INSERT를 막지 않는다. Tx2는 자기 갈 길을 가서 commit까지 한다. 단지 Tx1의 두 번째 SELECT는 자신의 read view에 없는 trx_id가 만든 row이므로 무시할 뿐이다. &lt;b&gt;&quot;안 보이게&quot; 하는 것이 핵심이다. &quot;못 들어오게&quot; 하는 것이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;locking read 경로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RR + SELECT ... FOR UPDATE는 메커니즘이 완전히 다르다. 여기서는 gap lock과 next-key lock이 켜진다.&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;SELECT COUNT(*) WHERE age &amp;gt; 30 FOR UPDATE를 던지면 InnoDB는 age 인덱스에서 30보다 큰 값 range를 통째로 잠가버린다. Tx2가 INSERT (age=35)를 시도하면 그 range 안에 들어가므로 락 대기로 빠진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;395&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IGJQJ/dJMb99TNFRC/XoNYHLkRKa6sLQHYW1HUo0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IGJQJ/dJMb99TNFRC/XoNYHLkRKa6sLQHYW1HUo0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IGJQJ/dJMb99TNFRC/XoNYHLkRKa6sLQHYW1HUo0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIGJQJ%2FdJMb99TNFRC%2FXoNYHLkRKa6sLQHYW1HUo0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;395&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;실측 대기 시간을 보면 RR + snapshot + phantom_read 셀에서 Tx2 wait는 1ms이다. Tx2가 자유롭게 INSERT/commit을 수행하고, 단지 Tx1엔 보이지 않을 뿐이다. 반면 RR + for_update + phantom_read 셀의 Tx2 wait는 1803ms이다. Tx2의 INSERT가 갭 락에 막혀 Tx1 commit 직전까지 기다린 것이다. &lt;b&gt;같은 RR 격리 수준이지만 변형에 따라 락 메커니즘 작동 여부가 정반대로 갈린다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 RR 안에서 두 메커니즘이 동시에 작동한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 지점이 사람들이 자주 틀리는 부분이다. RR이라고 해서 phantom 차단 메커니즘이 한 가지로 통일된 것이 아니다. 어떤 SELECT를 사용하느냐에 따라 다른 메커니즘이 작동한다. 일반 SELECT는 MVCC consistent read 경로로 가서 &quot;안 보이게&quot; 차단하고, SELECT FOR UPDATE나 UPDATE/DELETE는 next-key lock으로 &quot;못 들어오게&quot; 차단한다.&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;따라서 면접에서 &quot;RR에서 phantom을 어떻게 막는가?&quot;라는 질문을 받으면 &quot;&lt;b&gt;MVCC로 막거나 갭 락으로 막으며, 둘 다 RR에서 작동한다&lt;/b&gt;&quot; 이렇게 답해야 정확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SERIALIZABLE은 어디에서 비싸지는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모든 SELECT가 사실상 share lock으로 승격된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SERIALIZABLE 격리 수준에서는 일반 SELECT조차도 share lock을 잡는다. Tx1이 SELECT하면 그 row(또는 range)에 LOCK IN SHARE MODE 락이 걸리고, Tx2가 UPDATE/DELETE/INSERT를 시도하면 막힌다.&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;실측에서도 SERIALIZABLE + snapshot + phantom_read 셀에서 Tx2 wait가 1808ms로 잡힌다. 일반 SELECT인데도 RR + FOR UPDATE와 거의 같은 대기 시간이 나온다. SERIALIZABLE은 사실상 모든 읽기를 잠금 읽기로 승격시키는 모드라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락 대기 시간 차트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 18셀의 Tx2 대기 시간을 막대 차트로 그리면 동시성 비용이 어디에서 폭발하는지 한눈에 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckWL9i/dJMcageliWO/OJGgld5WvnroKv2JxEu0L1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckWL9i/dJMcageliWO/OJGgld5WvnroKv2JxEu0L1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckWL9i/dJMcageliWO/OJGgld5WvnroKv2JxEu0L1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FckWL9i%2FdJMcageliWO%2FOJGgld5WvnroKv2JxEu0L1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;360&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;변형&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;phantom_read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;non_repeatable_read&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;lost_update&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;snapshot&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;for_update&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1,807&lt;/td&gt;
&lt;td&gt;427&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;snapshot&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;for_update&lt;/td&gt;
&lt;td&gt;1,803&lt;/td&gt;
&lt;td&gt;1,807&lt;/td&gt;
&lt;td&gt;430&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;snapshot&lt;/td&gt;
&lt;td&gt;1,808&lt;/td&gt;
&lt;td&gt;1,806&lt;/td&gt;
&lt;td&gt;252&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;for_update&lt;/td&gt;
&lt;td&gt;1,807&lt;/td&gt;
&lt;td&gt;1,807&lt;/td&gt;
&lt;td&gt;172&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 SELECT만 사용할 때 락 대기는 거의 0이다. RC와 RR snapshot phantom_read 셀이 1~2ms로 박혀 있다. 잠금이 켜지는 순간 1800ms대로 점프한다. Tx1이 sleep 후 commit하는 1.8초가 그대로 반영된 결과이다. SERIALIZABLE은 일반 SELECT인데도 1800ms 대기가 잡힌다. 이것이 &lt;b&gt;&quot;모든 읽기를 잠금으로 승격&quot;의 비용&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SERIALIZABLE까지 가야 하는 경우는 거의 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 차트가 답한다. RR + FOR UPDATE는 잠금 읽기 셀에서만 1800ms 대기 비용이 들고 일반 SELECT는 빠르게 통과한다. SERIALIZABLE은 모든 SELECT가 비용을 짊어진다.&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;한 트랜잭션 안에서 잠가야 할 row만 명시적으로 FOR UPDATE를 거는 RR이 SERIALIZABLE보다 거의 항상 더 낫다. SERIALIZABLE은 &quot;모든 읽기가 잠금이어야 한다&quot;가 진짜 요구사항인 회계&amp;middot;금융 일부 시나리오에서나 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전에서 격리 수준을 어떻게 선택해야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 OLTP 서비스는 RR 기본값을 그대로 사용한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 웹 서비스는 InnoDB의 RR 기본값을 그대로 사용하면 된다. phantom&amp;middot;non-repeatable이 일반 SELECT에서는 차단된다는 점은 위 실험으로 검증되었으며, 명시적 잠금이 필요한 부분만 SELECT ... FOR UPDATE로 처리하면 된다. READ COMMITTED로 내릴 동기는 보통 락을 풀어주려는 것이지만, RR도 일반 SELECT는 락을 잡지 않으므로 굳이 내릴 이유가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잔액 증감 같은 lost-update 위험 코드는 RR + SELECT FOR UPDATE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잔액에 +100 더하기, 재고 -1 차감하기 같은 read-modify-write 패턴은 RR 기본값만으로는 부족할 수 있다. 위 실험에서 RR + snapshot lost_update 셀을 보면 err_1020이 떴는데, 이는 한쪽 트랜잭션이 retry 가능한 에러로 빠진 결과이다. 애플리케이션 retry 로직이 없다면 한쪽 갱신이 그대로 사라진다.&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;따라서 그러한 코드는 명시적으로 다음과 같이 작성하는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777990610616&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;START TRANSACTION;
SELECT balance FROM users WHERE id = 1 FOR UPDATE;
-- 계산
UPDATE users SET balance = ? WHERE id = 1;
COMMIT;
&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;FOR UPDATE로 row를 잠가서 lost_update 자체를 발생하지 못하게 막는 것이다. 실측에서 RR + for_update + lost_update 셀은 최종 잔액이 정확히 1200 / 1200으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보고서&amp;middot;통계는 RC로 낮춰 락을 풀어주는 것이 맞다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 분석 쿼리, 즉 수십 분 도는 보고서 같은 경우는 RC로 낮추는 것이 나을 수 있다. RR 스냅샷이 트랜잭션 끝까지 유지되면 undo log가 쌓여 다른 트랜잭션이 느려질 수 있기 때문이다. 어차피 보고서는 약간의 non-repeatable이 큰 문제가 되지 않으므로 RC로 풀어주는 편이 시스템 전체에 이롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SERIALIZABLE은 마지막 수단이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 대기 시간 차트가 보여주듯 SERIALIZABLE은 일반 SELECT까지 잠금을 걸어버린다. 이를 감수할 만한 시나리오는 거의 없다. 정합성이 절대적으로 중요한 회계 마감 작업, 금융 결제 정산처럼 동시성을 포기해도 되는 영역에서만 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 틀리는 면접 질문&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;MySQL 기본 격리 수준은 무엇인가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REPEATABLE READ이다. PostgreSQL은 READ COMMITTED를 기본으로 둔다는 점에서 다르다. 헷갈리는 사람이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;RR에서 phantom read가 일어나는가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL 표준의 답과 InnoDB의 답이 다르다. SQL 표준은 &quot;허용&quot;이고 InnoDB는 &quot;차단&quot;이다. 면접관이 어떤 답을 원하는지에 따라 갈리므로 둘 다 알고 있어야 한다. &quot;&lt;b&gt;표준 표대로라면 허용이지만 InnoDB는 MVCC + 갭 락으로 phantom까지 차단한다&lt;/b&gt;&quot; 이렇게 답하는 것이 정확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;갭 락이란 무엇인가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 range를 잠그는 락이다. 특정 row가 아니라 row 사이의 빈 공간(gap)을 잠가 그 range로 INSERT가 들어오는 것을 차단한다. RR에서 phantom을 막는 핵심 메커니즘 중 하나이다. RC에서는 켜지지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;MVCC consistent read와 locking read의 차이는 무엇인가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;consistent read는 일반 SELECT가 사용하는 경로로, 트랜잭션 시작 시점 스냅샷에서 읽는다. 락을 잡지 않는다. locking read는 SELECT FOR UPDATE나 LOCK IN SHARE MODE 같은 것으로 row(또는 range)에 락을 걸고 읽는다. 같은 RR 안에서 두 경로가 동시에 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;표를 베끼지 말고 직접 돌려보라&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;격리 수준을 공부할 때 표를 외우고 끝내면 면접에서 위 세 번째 질문에서 무너진다. 정확히 답하려면 SQL 표준과 InnoDB 구현이 어디에서 갈라지는지 알아야 하고, 그것을 가장 확실히 아는 방법은 직접 돌려보는 것이다.&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;가장 중요한 사실 하나만 가져간다면, &lt;b&gt;InnoDB의 RR은 SQL 표준 RR보다 한 단계 엄격하여 phantom&amp;middot;lost_update까지 사실상 차단한다&lt;/b&gt;는 것이다. 표만 보면 잡히지 않는 차이이며, 한국어 자료가 잘 짚지 않는 부분이기도 하다. 거기 더해, 같은 RR 안에서도 일반 SELECT는 MVCC로 막고 FOR UPDATE는 갭 락으로 막는다는 점, 그리고 SERIALIZABLE은 동시성 비용이 너무 커서 RR + FOR UPDATE 조합이 거의 항상 더 낫다는 점까지 함께 기억해두면 된다.&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;마지막으로 한 줄 경고한다. 위 결과는 vanilla InnoDB(MySQL 8.0+, MariaDB 10+) 기준이다. Aurora는 RR 동작이 미묘하게 다를 수 있고, TiDB나 Spanner 같은 분산 DB는 또 다르다. 본인이 사용하는 DB가 진짜 InnoDB인지부터 확인하고, 의심스럽다면 위 18셀 매트릭스를 그대로 본인 환경에 돌려보면 된다. 표만 베끼는 자료보다 직접 돌린 결과가 항상 답이다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>innodb mvcc</category>
      <category>innodb 팬텀 리드</category>
      <category>mysql 격리 수준</category>
      <category>repeatable read serializable 차이</category>
      <category>select for update mysql</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/417</guid>
      <comments>https://yscho03.tistory.com/417#entry417comment</comments>
      <pubDate>Tue, 5 May 2026 23:18:19 +0900</pubDate>
    </item>
    <item>
      <title>Bloom Filter로 DB 호출을 99% 차단해보았다 - set&amp;middot;dict&amp;middot;SQLite와 메모리 58MB&amp;rarr;1.2MB 직접 비교</title>
      <link>https://yscho03.tistory.com/416</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;if user_id in user_ids 한 줄을 쓰려고 set에 100만 건을 올렸다가 메모리 60MB 가까이 날린 적이 있는가? 아니면 캐시 미스 때마다 DB로 가서 &quot;없음&quot; 답만 받아오는 네거티브 캐시 페널티 때문에 골치 아팠던 적은? 블룸 필터(Bloom Filter)는 이런 상황에서 메모리 1MB 남짓으로 DB 호출의 99%를 잘라내는 확률적 자료구조이다. 다만 인터넷에 떠도는 글들은 대부분 &quot;1% false positive로 설정한다&quot; 한 줄짜리 튜토리얼이거나, 위키백과식 원리 설명에 그치고 있어서 실제로 얼마나 이득인지 감이 잡히지 않는다.&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;각설하고, 직접 100만 사용자 ID 기준으로 set&amp;middot;dict&amp;middot;SQLite(:memory: PK 인덱스)&amp;middot;Bloom Filter 4종을 같은 조건에 박아놓고 빌드 시간, RSS 메모리, hit/miss 지연(p50&amp;middot;p99), FPR 이론값 vs 실측값, DB 차단량까지 모두 측정했다. python:3.12-slim 컨테이너에서 32.6초 만에 종료되었고 결과는 PASS이다. 이 글은 그 raw 데이터를 차트와 함께 풀어놓고, 언제 블룸 필터가 답이고 언제 그냥 set을 쓰는 편이 나은지 정리한 결과이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸 필터가 무엇이길래 모두 캐시 앞단에 깔자고 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfcpZX/dJMcafmcyCr/51jhcoMQKXL1dYQ8dKP9hk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfcpZX/dJMcafmcyCr/51jhcoMQKXL1dYQ8dKP9hk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfcpZX/dJMcafmcyCr/51jhcoMQKXL1dYQ8dKP9hk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfcpZX%2FdJMcafmcyCr%2F51jhcoMQKXL1dYQ8dKP9hk%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;810&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://systemdesign.one/bloom-filters-explained/&quot;&gt;System Design (131KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄 정의는 &quot;있을 수도 있음 / 확실히 없음&quot; 두 답만 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블룸 필터는 1970년 Burton H. Bloom이 논문 &quot;Space/Time Trade-offs in Hash Coding with Allowable Errors&quot;에서 제안한 확률적 자료구조이다. 핵심은 두 가지 답만 한다는 점이다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;있을 수도 있음 (probably present)&lt;/b&gt; &amp;mdash; false positive 가능성이 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확실히 없음 (definitely not present)&lt;/b&gt; &amp;mdash; 100% 신뢰 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;False positive(오탐, 없는데 있다고 답함)는 발생할 수 있지만 false negative(있는데 없다고 답함)는 절대 발생하지 않는다. 이것이 캐시 앞단 가드로 쓰기 좋은 이유이다. &quot;없다&quot;는 답은 무조건 신뢰하고 DB 호출을 스킵하면 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비트 배열 + k개 해시가 동작 방식의 전부이다&lt;/h3&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;&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;크기 m의 비트 배열을 0으로 초기화한다&lt;/li&gt;
&lt;li&gt;키를 추가할 때 k개의 해시 함수로 인덱스 k개를 뽑아 해당 비트들을 1로 세팅한다&lt;/li&gt;
&lt;li&gt;키를 조회할 때 같은 k개 해시를 돌려 모두 1이면 &quot;있을 수도 있음&quot;, 하나라도 0이면 &quot;확실히 없음&quot;으로 판정한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 충돌 때문에 다른 키들이 같은 비트를 1로 세팅했을 수 있어 false positive가 생기는 것이다. 다만 false negative는 불가능하다 &amp;mdash; 한 번 1로 세팅된 비트는 절대 0으로 돌아가지 않기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디에 사용하는가&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Cassandra SSTable&lt;/b&gt; &amp;mdash; 디스크 IO를 일으키지 않고 &quot;이 SSTable에 키가 있는가?&quot;를 빠르게 체크한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Google BigTable / LevelDB / RocksDB&lt;/b&gt; &amp;mdash; LSM 트리 read amplification을 줄인다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Chrome Safe Browsing&lt;/b&gt; &amp;mdash; 위험한 URL 리스트를 클라이언트에서 1차 필터링한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Medium 추천 dedup&lt;/b&gt; &amp;mdash; 이미 읽은 글을 다시 추천하지 않으려고 사용한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PostgreSQL bloom 인덱스&lt;/b&gt; &amp;mdash; 다중 컬럼 동치 조건을 가속한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 한국어권에서는 위 사례를 &quot;어디서 들어봤다&quot; 수준으로만 인용할 뿐, 직접 돌려서 &quot;내 워크로드에서 얼마나 이득인지&quot; 보여주는 글이 없어 이번 실험을 돌리게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bloom Filter 사이즈는 어떻게 잡는가? 공식부터 보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btMt5c/dJMcaaSJExL/3TJkVo3LV8MKG1PfEXhWNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btMt5c/dJMcaaSJExL/3TJkVo3LV8MKG1PfEXhWNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btMt5c/dJMcaaSJExL/3TJkVo3LV8MKG1PfEXhWNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtMt5c%2FdJMcaaSJExL%2F3TJkVo3LV8MKG1PfEXhWNk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;432&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.javamex.com/tutorials/collections/bloom_filter_false_positives_graph.shtml&quot;&gt;javamex.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 공식은 두 줄이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 false positive rate를 $p$, 키 개수를 $n$이라 두면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777976359124&quot; class=&quot;ini&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;m = -n * ln(p) / (ln 2)^2     # 비트 배열 크기 (비트)
k = (m / n) * ln 2            # 해시 함수 개수
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;100만 키, FPR 1%를 대입하면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$n = 1{,}000{,}000$&lt;/li&gt;
&lt;li&gt;$p = 0.01$&lt;/li&gt;
&lt;li&gt;$m \approx 9{,}585{,}059$ 비트 &amp;asymp; 1.14 MiB &amp;asymp; 1,170 KB&lt;/li&gt;
&lt;li&gt;$k \approx 7$&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 키 한 건당 약 9.6비트를 쓰고 7번 해시한다. 100만 키를 1.2MB 안에 모두 욱여넣는 것이 가능한 이유이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;7번 해싱하는데 dict보다 빠를 수 있는가?&quot; &amp;mdash; 솔직히 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 자주 오해되는데, 블룸 필터가 dict보다 in-memory 조회는 빠르지 않다. dict는 한 번만 해시하면 되지만 블룸은 7번 해싱과 7번 비트 체크를 수행한다. 실험에서도 dict p50 hit 0.4&amp;mu;s vs Bloom p50 hit 3.1&amp;mu;s로 약 8배 느렸다.&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;블룸 필터의 진짜 이점은 &lt;b&gt;메모리&lt;/b&gt;와 &lt;b&gt;DB 왕복 차단&lt;/b&gt;이다. 7번 해시해서 3&amp;mu;s를 쓰는 것 vs DB로 가서 5ms를 날리는 것을 비교하면 1,500배 이득이다. 핫 패스 in-memory 조회만 보면 set/dict 승, 캐시&amp;middot;DB 앞단이면 Bloom 승이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 설계 - 100만 사용자 ID, 4종 자료구조를 동일 조건으로 비교한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;런타임과 리소스를 모두 공개한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;런타임&lt;/td&gt;
&lt;td&gt;python:3.12-slim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스&lt;/td&gt;
&lt;td&gt;메모리 1024MB, CPU 2.0, 타임아웃 480s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;td&gt;PASS, exit 0, 32.60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터셋&lt;/td&gt;
&lt;td&gt;정수 사용자 ID 100만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회 횟수&lt;/td&gt;
&lt;td&gt;hit 1만 + miss 1만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교 대상 4종 + Bloom sweep 4단계&lt;/h3&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;&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;Python set&lt;/b&gt; &amp;mdash; 정확한 멤버십, 빠른 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Python dict&lt;/b&gt; &amp;mdash; set과 비슷하나 value 슬롯이 들어가서 더 크다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQLite(:memory: PK 인덱스)&lt;/b&gt; &amp;mdash; 디스크를 쓰지 않는 메모리 모드, id 컬럼에 PRIMARY KEY 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bloom Filter (목표 FPR 1%)&lt;/b&gt; &amp;mdash; 비트 배열 + 7개 해시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Bloom Filter는 0.1% / 1% / 5% / 10% 4단계로 sweep을 돌려 메모리-정확도 트레이드오프 곡선까지 추출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 항목&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 시간 (wall-clock 초)&lt;/li&gt;
&lt;li&gt;RSS 메모리 증가량 (MB)&lt;/li&gt;
&lt;li&gt;hit 1만 건 조회 p50 / p99 지연 (&amp;mu;s)&lt;/li&gt;
&lt;li&gt;miss 1만 건 조회 p50 / p99 지연 (&amp;mu;s)&lt;/li&gt;
&lt;li&gt;Bloom Filter false positive 건수 + 실측 FPR&lt;/li&gt;
&lt;li&gt;DB 왕복 차단량 (= 1만 - false positive)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stdout 마지막에 깔끔하게 정리되어 출력된다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777976359125&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;name             build_s    mem_MB  p50_hit_us  p99_hit_us  p50_mis_us  p99_mis_us     FPs
set                0.154     32.44        0.30        1.00        0.20        0.80       0
dict               0.281     57.62        0.40        1.30        0.30        1.00       0
sqlite             1.179     20.00        2.30        4.80        1.50        1.70       0
bloom_1pct         3.352      0.00        3.10        6.30        1.70        3.40     106
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 결과: dict 58MB &amp;rarr; Bloom 1.2MB로 약 50배 줄였다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nRvCJ/dJMcafmcyCb/GJR7CoCTPGbNhttemTWiH1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nRvCJ/dJMcafmcyCb/GJR7CoCTPGbNhttemTWiH1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nRvCJ/dJMcafmcyCb/GJR7CoCTPGbNhttemTWiH1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnRvCJ%2FdJMcafmcyCb%2FGJR7CoCTPGbNhttemTWiH1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;500&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;100만 정수 ID 기준 RSS 증가량은 다음과 같다:&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자료구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;빌드 시간&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;32.44 MB&lt;/td&gt;
&lt;td&gt;0.154초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dict&lt;/td&gt;
&lt;td&gt;57.62 MB&lt;/td&gt;
&lt;td&gt;0.281초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite (:memory: PK)&lt;/td&gt;
&lt;td&gt;20.00 MB&lt;/td&gt;
&lt;td&gt;1.179초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom Filter (FPR 1%)&lt;/td&gt;
&lt;td&gt;**1.14 MB**&lt;/td&gt;
&lt;td&gt;3.352초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브리프에는 &quot;80MB &amp;rarr; 1.2MB&quot;라고 적혀 있었으나 실제 측정은 dict 기준 58MB이다. 그래도 Bloom 대비 약 50배 차이라 인상은 그대로이다. 이것이 의미하는 바는, 운영 메모리 1GB짜리 컨테이너 워커 기준 dict는 약 5.7%를 차지하지만 Bloom은 0.1%로 떨어진다는 점이다. 워커 100개를 띄우면 5.6GB &amp;rarr; 120MB로 딱 그만큼 메모리 여유가 생기는 것이다.&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;⚠️ 표에 Bloom_1pct RSS가 0.00 MB로 찍힌 이유는 다음과 같다. 비트 배열 자체는 1,198,133 바이트 (약 1.14 MiB)인데 인터프리터가 이미 잡아놓은 페이지 안에 흡수되어 RSS 측정 단위(MB) 미만으로 잡혔기 때문이다. result.json의 extra.size_bytes 값이 실제 자료구조 크기이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 시간은 Bloom이 가장 느리다 (3.35초 vs set 0.15초). 키 한 건당 7번 해싱하니 당연한 결과이다. 한 번 빌드해서 오래 쓰는 시나리오라면 무시해도 되는 비용이고, 매 요청마다 빌드해야 한다면 Bloom을 쓰지 않는 편이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지연 시간 결과: Bloom이 set보다 8배 느린데도 캐시 앞단으로는 압승이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 차트는 hit/miss 1만 건씩 조회한 p50&amp;middot;p99 마이크로초 분포이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BJbRk/dJMcafmcyCc/HLzzGgAFICKCqaYhyW7NK1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BJbRk/dJMcafmcyCc/HLzzGgAFICKCqaYhyW7NK1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BJbRk/dJMcafmcyCc/HLzzGgAFICKCqaYhyW7NK1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBJbRk%2FdJMcafmcyCc%2FHLzzGgAFICKCqaYhyW7NK1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;400&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자료구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 hit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 hit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 miss&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 miss&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;0.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.00&amp;mu;s&lt;/td&gt;
&lt;td&gt;0.20&amp;mu;s&lt;/td&gt;
&lt;td&gt;0.80&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dict&lt;/td&gt;
&lt;td&gt;0.40&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;0.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.00&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;2.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;4.80&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.50&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.70&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom (FPR 1%)&lt;/td&gt;
&lt;td&gt;3.10&amp;mu;s&lt;/td&gt;
&lt;td&gt;6.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.70&amp;mu;s&lt;/td&gt;
&lt;td&gt;3.40&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set/dict가 가장 빠르다. nanosecond 단위에 거의 들어간다. SQLite는 메모리 모드라도 PK 인덱스 lookup + Python &amp;lt;-&amp;gt; C 경계 비용으로 &amp;mu;s 단위가 된다. Bloom은 7번 해시 비용 때문에 set 대비 약 8배 느리다.&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;여기서 자주 나오는 잘못된 결론이 &quot;그러면 Bloom을 쓰지 않는 편이 빠르다&quot;인데, &lt;b&gt;블룸 필터의 비교 대상은 set이 아니라 DB 왕복&lt;/b&gt;이다. 같은 워크로드에서:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in-memory set 조회: ~0.3&amp;mu;s&lt;/li&gt;
&lt;li&gt;in-memory Bloom 조회: ~3&amp;mu;s&lt;/li&gt;
&lt;li&gt;DB(Postgres/MySQL) 단순 PK 조회: 1~10ms = 1,000~10,000&amp;mu;s&lt;/li&gt;
&lt;li&gt;네트워크 캐시(Redis) 조회: 100~500&amp;mu;s&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미존재 키를 Bloom이 차단하여 DB로 가지 않게 만들면 1만 &amp;mu;s &amp;rarr; 3&amp;mu;s로 약 3,000배 빨라진다. &lt;b&gt;블룸 필터는 in-memory 자료구조 사이의 경쟁이 아니라 &quot;in-memory vs network/disk&quot; 경쟁에서 의미가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸 필터 FPR 이론 vs 실측, DB 호출 99% 차단까지 검증한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FPR 4단계 sweep 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 FPR 0.1% / 1% / 5% / 10% 4단계로 비트 배열 크기를 다르게 잡고 돌렸다. 공식이 실제로 맞는지 확인하는 것이 목적이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;목표 FPR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;m(비트)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;k(해시)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;크기(KB)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이론 FPR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;실측 FPR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;DB 차단(/10,000)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;차단율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.10%&lt;/td&gt;
&lt;td&gt;14,377,588&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1,755.1&lt;/td&gt;
&lt;td&gt;0.100%&lt;/td&gt;
&lt;td&gt;0.090%&lt;/td&gt;
&lt;td&gt;9,991&lt;/td&gt;
&lt;td&gt;99.91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00%&lt;/td&gt;
&lt;td&gt;9,585,059&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1,170.1&lt;/td&gt;
&lt;td&gt;1.004%&lt;/td&gt;
&lt;td&gt;1.060%&lt;/td&gt;
&lt;td&gt;9,894&lt;/td&gt;
&lt;td&gt;98.94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.00%&lt;/td&gt;
&lt;td&gt;6,235,225&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;761.1&lt;/td&gt;
&lt;td&gt;5.027%&lt;/td&gt;
&lt;td&gt;5.040%&lt;/td&gt;
&lt;td&gt;9,496&lt;/td&gt;
&lt;td&gt;94.96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.00%&lt;/td&gt;
&lt;td&gt;4,792,530&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;585.0&lt;/td&gt;
&lt;td&gt;10.071%&lt;/td&gt;
&lt;td&gt;10.240%&lt;/td&gt;
&lt;td&gt;8,976&lt;/td&gt;
&lt;td&gt;89.76%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론 FPR과 실측 FPR이 거의 일치한다. 공식대로 사이즈를 잡으면 예측대로 동작한다는 뜻이라 운영 관점에서 매우 중요하다. SLA를 잡고 가도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGvkop/dJMcafmcyCd/Hl1I5TkkCuTMFgvACmxsTK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGvkop/dJMcafmcyCd/Hl1I5TkkCuTMFgvACmxsTK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGvkop/dJMcafmcyCd/Hl1I5TkkCuTMFgvACmxsTK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGvkop%2FdJMcafmcyCd%2FHl1I5TkkCuTMFgvACmxsTK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;500&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB 호출 차단량 시각화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미존재 키 1만 건을 조회했을 때 Bloom이 &quot;확실히 없음&quot; 답을 하여 DB 호출 자체를 일으키지 않게 만든 건수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k3XhY/dJMcab49BNB/SBfllkdCZSKFaXcXTIkIl1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k3XhY/dJMcab49BNB/SBfllkdCZSKFaXcXTIkIl1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k3XhY/dJMcab49BNB/SBfllkdCZSKFaXcXTIkIl1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk3XhY%2FdJMcab49BNB%2FSBfllkdCZSKFaXcXTIkIl1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;500&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;FPR 1% 설정으로 메모리 1.17MB만 쓰고도 1만 건 미존재 조회 중 9,894건을 차단하여 &lt;b&gt;DB 호출을 98.94% 잘라낸다.&lt;/b&gt; 0.1% 설정이면 9,991건을 차단하여 99.91%까지 올라가고, 메모리는 1.76MB 정도이다. 트레이드오프 곡선이 거의 정확하게 공식을 따라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트레이드오프 정리: 메모리 vs 차단율&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차단율 99.9%를 원하면: 메모리 ~1.8MB, 해시 10번&lt;/li&gt;
&lt;li&gt;차단율 99%를 원하면: 메모리 ~1.2MB, 해시 7번&lt;/li&gt;
&lt;li&gt;차단율 95%를 원하면: 메모리 ~750KB, 해시 4번&lt;/li&gt;
&lt;li&gt;차단율 90%를 원하면: 메모리 ~600KB, 해시 3번&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 sweep마다 빌드 시간과 hit p99도 함께 측정했는데, 해시 개수 k가 줄수록(10&amp;rarr;3) 둘 다 단조 감소한다. k=10일 때 빌드 4.5초&amp;middot;p99 hit 15&amp;mu;s, k=3일 때 빌드 2.1초&amp;middot;p99 hit 4.1&amp;mu;s이다. 키 한 건당 k번 해시하니 그저 비례 관계이다. 본인 워크로드에서 SLA가 빡빡하다면 FPR 5% 정도까지 풀어 hit 지연을 줄이는 것이 합리적인 선택일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그러면 언제 Bloom을 쓰고 언제 set을 쓰면 되는가?&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;set/dict가 답인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정확한 멤버십이 절대적으로 필요하다 (false positive 0)&lt;/li&gt;
&lt;li&gt;메모리에 여유가 있다 (수십 MB를 써도 무방)&lt;/li&gt;
&lt;li&gt;키 개수가 ~수십만 ~ 백만 단위이다&lt;/li&gt;
&lt;li&gt;핫 패스 in-memory 조회이다 (&amp;mu;s도 아까운 상황)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQLite/RDB 인덱스가 답인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정확성과 영구 저장 둘 다 필요하다&lt;/li&gt;
&lt;li&gt;키마다 부가 정보(value)도 함께 보관해야 한다&lt;/li&gt;
&lt;li&gt;메모리 모드 SQLite는 set보다 느리고 무거우니 굳이 쓰지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bloom Filter가 답인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;없음&quot; 답만 빠르게 받으면 되는 시나리오이다 (멤버십 게이트)&lt;/li&gt;
&lt;li&gt;메모리가 빡빡하다 (워커 수백 개 / 엣지 노드 / 모바일 클라이언트)&lt;/li&gt;
&lt;li&gt;키 개수가 수백만 ~ 수억 단위이다&lt;/li&gt;
&lt;li&gt;캐시&amp;middot;DB 앞단의 네거티브 캐시 가드이다&lt;/li&gt;
&lt;li&gt;가끔 false positive가 떠도 &quot;한 번 더 DB로 확인&quot; 정도는 허용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안티 패턴: &quot;있음&quot; 답을 그대로 신뢰하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블룸 필터에서 가장 자주 나오는 사고가 이것이다. &quot;있음&quot; 답은 false positive 가능성이 있어 100% 신뢰할 수 없다. 반드시 다음 단계가 있어야 한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777976359129&quot; class=&quot;excel&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;key in bloom?
├─ 확실히 없음 &amp;rarr; 즉시 &quot;없음&quot; 응답 (DB 안 감)
└─ 있을 수도 있음 &amp;rarr; 캐시/DB로 가서 실제 확인
&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;이 구조로 짜면 &quot;없는 키&quot;는 99% 차단되고 &quot;있는 키&quot;는 항상 정확하게 처리된다. 반대로 &quot;있음&quot; 답을 그대로 응답으로 쓰면 1% 확률로 잘못된 데이터가 내려가는 사고가 터진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python 라이브러리 추천&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;pybloom-live&lt;/b&gt; &amp;mdash; 순수 파이썬 구현, scalable bloom filter도 지원한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pybloomfiltermmap3&lt;/b&gt; &amp;mdash; C 구현, mmap 기반이라 더 빠르다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;redisbloom&lt;/b&gt; &amp;mdash; Redis 모듈, 분산 환경에서 공유 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청량이 많다면 RedisBloom으로 가서 워커 간 필터를 공유하는 패턴이 정석이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸 필터 실험 데이터 정리: 캐시 앞단에 깔면 거의 무조건 이득이다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리&lt;/b&gt;: dict 58MB &amp;rarr; Bloom 1.2MB, 약 50배 절감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;차단율&lt;/b&gt;: FPR 1% 설정으로 미존재 키 98.94% 차단, 0.1% 설정이면 99.91%&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이론 vs 실측&lt;/b&gt;: 공식대로 잡으면 거의 정확하게 일치한다 &amp;rarr; SLA를 잡고 가도 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연&lt;/b&gt;: in-memory에서 set 대비 8배 느리지만 DB 왕복 대비 1,000배 이상 빠르다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트레이드오프&lt;/b&gt;: 메모리를 600KB까지 줄이면 차단율은 90%로 떨어진다, 본인 워크로드에 맞춰 선택한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 앞단에 블룸 필터 한 줄을 깔면 백엔드 DB 호출의 90~99%가 잘린다. 메모리는 MB 단위로 끝나고, 코드 한 줄짜리 트레이드오프이다. 핫 패스 in-memory 자료구조 자리에는 쓰지 마라 &amp;mdash; 그곳은 set이 답이다. 다만 미존재 키 차단이 목적이라면 블룸 필터가 거의 무조건 이득이다.&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;result.json과 차트 4종을 모두 첨부했으니 본인 워크로드에서 키 개수와 허용 FPR을 바꿔 그대로 sweep을 돌려보면 된다. 공식 신뢰도까지 검증한 1차 데이터라 의사결정 자료로 그대로 활용해도 무방하다.&lt;/p&gt;
&lt;h1&gt;Bloom Filter로 DB 호출을 99% 차단해보았다 - set&amp;middot;dict&amp;middot;SQLite와 메모리 58MB&amp;rarr;1.2MB 직접 비교&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;if user_id in user_ids 한 줄을 쓰려고 set에 100만 건을 올렸다가 메모리 60MB 가까이 날린 적이 있는가? 아니면 캐시 미스 때마다 DB로 가서 &quot;없음&quot; 답만 받아오는 네거티브 캐시 페널티 때문에 골치 아팠던 적은? 블룸 필터(Bloom Filter)는 이런 상황에서 메모리 1MB 남짓으로 DB 호출의 99%를 잘라내는 확률적 자료구조이다. 다만 인터넷에 떠도는 글들은 대부분 &quot;1% false positive로 설정한다&quot; 한 줄짜리 튜토리얼이거나, 위키백과식 원리 설명에 그치고 있어서 실제로 얼마나 이득인지 감이 잡히지 않는다.&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;각설하고, 직접 100만 사용자 ID 기준으로 set&amp;middot;dict&amp;middot;SQLite(:memory: PK 인덱스)&amp;middot;Bloom Filter 4종을 같은 조건에 박아놓고 빌드 시간, RSS 메모리, hit/miss 지연(p50&amp;middot;p99), FPR 이론값 vs 실측값, DB 차단량까지 모두 측정했다. python:3.12-slim 컨테이너에서 32.6초 만에 종료되었고 결과는 PASS이다. 이 글은 그 raw 데이터를 차트와 함께 풀어놓고, 언제 블룸 필터가 답이고 언제 그냥 set을 쓰는 편이 나은지 정리한 결과이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸 필터가 무엇이길래 모두 캐시 앞단에 깔자고 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;810&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9Q7VS/dJMcabKTNoQ/G0A92Ov6lKXNd5pF3KeFak/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9Q7VS/dJMcabKTNoQ/G0A92Ov6lKXNd5pF3KeFak/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9Q7VS/dJMcabKTNoQ/G0A92Ov6lKXNd5pF3KeFak/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9Q7VS%2FdJMcabKTNoQ%2FG0A92Ov6lKXNd5pF3KeFak%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;810&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;810&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://systemdesign.one/bloom-filters-explained/&quot;&gt;System Design (131KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄 정의는 &quot;있을 수도 있음 / 확실히 없음&quot; 두 답만 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블룸 필터는 1970년 Burton H. Bloom이 논문 &quot;Space/Time Trade-offs in Hash Coding with Allowable Errors&quot;에서 제안한 확률적 자료구조이다. 핵심은 두 가지 답만 한다는 점이다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;있을 수도 있음 (probably present)&lt;/b&gt; &amp;mdash; false positive 가능성이 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확실히 없음 (definitely not present)&lt;/b&gt; &amp;mdash; 100% 신뢰 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;False positive(오탐, 없는데 있다고 답함)는 발생할 수 있지만 false negative(있는데 없다고 답함)는 절대 발생하지 않는다. 이것이 캐시 앞단 가드로 쓰기 좋은 이유이다. &quot;없다&quot;는 답은 무조건 신뢰하고 DB 호출을 스킵하면 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비트 배열 + k개 해시가 동작 방식의 전부이다&lt;/h3&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;&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;크기 m의 비트 배열을 0으로 초기화한다&lt;/li&gt;
&lt;li&gt;키를 추가할 때 k개의 해시 함수로 인덱스 k개를 뽑아 해당 비트들을 1로 세팅한다&lt;/li&gt;
&lt;li&gt;키를 조회할 때 같은 k개 해시를 돌려 모두 1이면 &quot;있을 수도 있음&quot;, 하나라도 0이면 &quot;확실히 없음&quot;으로 판정한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 충돌 때문에 다른 키들이 같은 비트를 1로 세팅했을 수 있어 false positive가 생기는 것이다. 다만 false negative는 불가능하다 &amp;mdash; 한 번 1로 세팅된 비트는 절대 0으로 돌아가지 않기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디에 사용하는가&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Cassandra SSTable&lt;/b&gt; &amp;mdash; 디스크 IO를 일으키지 않고 &quot;이 SSTable에 키가 있는가?&quot;를 빠르게 체크한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Google BigTable / LevelDB / RocksDB&lt;/b&gt; &amp;mdash; LSM 트리 read amplification을 줄인다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Chrome Safe Browsing&lt;/b&gt; &amp;mdash; 위험한 URL 리스트를 클라이언트에서 1차 필터링한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Medium 추천 dedup&lt;/b&gt; &amp;mdash; 이미 읽은 글을 다시 추천하지 않으려고 사용한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PostgreSQL bloom 인덱스&lt;/b&gt; &amp;mdash; 다중 컬럼 동치 조건을 가속한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 한국어권에서는 위 사례를 &quot;어디서 들어봤다&quot; 수준으로만 인용할 뿐, 직접 돌려서 &quot;내 워크로드에서 얼마나 이득인지&quot; 보여주는 글이 없어 이번 실험을 돌리게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bloom Filter 사이즈는 어떻게 잡는가? 공식부터 보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOnynk/dJMcafmcyCy/8xvmon1ClntMIjhO9g7GHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOnynk/dJMcafmcyCy/8xvmon1ClntMIjhO9g7GHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOnynk/dJMcafmcyCy/8xvmon1ClntMIjhO9g7GHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOnynk%2FdJMcafmcyCy%2F8xvmon1ClntMIjhO9g7GHK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;700&quot; height=&quot;432&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.javamex.com/tutorials/collections/bloom_filter_false_positives_graph.shtml&quot;&gt;javamex.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 공식은 두 줄이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 false positive rate를 $p$, 키 개수를 $n$이라 두면:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777976352840&quot; class=&quot;ini&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;m = -n * ln(p) / (ln 2)^2     # 비트 배열 크기 (비트)
k = (m / n) * ln 2            # 해시 함수 개수
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;100만 키, FPR 1%를 대입하면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;$n = 1{,}000{,}000$&lt;/li&gt;
&lt;li&gt;$p = 0.01$&lt;/li&gt;
&lt;li&gt;$m \approx 9{,}585{,}059$ 비트 &amp;asymp; 1.14 MiB &amp;asymp; 1,170 KB&lt;/li&gt;
&lt;li&gt;$k \approx 7$&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 키 한 건당 약 9.6비트를 쓰고 7번 해시한다. 100만 키를 1.2MB 안에 모두 욱여넣는 것이 가능한 이유이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;7번 해싱하는데 dict보다 빠를 수 있는가?&quot; &amp;mdash; 솔직히 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 자주 오해되는데, 블룸 필터가 dict보다 in-memory 조회는 빠르지 않다. dict는 한 번만 해시하면 되지만 블룸은 7번 해싱과 7번 비트 체크를 수행한다. 실험에서도 dict p50 hit 0.4&amp;mu;s vs Bloom p50 hit 3.1&amp;mu;s로 약 8배 느렸다.&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;블룸 필터의 진짜 이점은 &lt;b&gt;메모리&lt;/b&gt;와 &lt;b&gt;DB 왕복 차단&lt;/b&gt;이다. 7번 해시해서 3&amp;mu;s를 쓰는 것 vs DB로 가서 5ms를 날리는 것을 비교하면 1,500배 이득이다. 핫 패스 in-memory 조회만 보면 set/dict 승, 캐시&amp;middot;DB 앞단이면 Bloom 승이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 설계 - 100만 사용자 ID, 4종 자료구조를 동일 조건으로 비교한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;런타임과 리소스를 모두 공개한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;런타임&lt;/td&gt;
&lt;td&gt;python:3.12-slim&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스&lt;/td&gt;
&lt;td&gt;메모리 1024MB, CPU 2.0, 타임아웃 480s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크&lt;/td&gt;
&lt;td&gt;allow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;td&gt;PASS, exit 0, 32.60s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터셋&lt;/td&gt;
&lt;td&gt;정수 사용자 ID 100만 건&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;조회 횟수&lt;/td&gt;
&lt;td&gt;hit 1만 + miss 1만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교 대상 4종 + Bloom sweep 4단계&lt;/h3&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;&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;Python set&lt;/b&gt; &amp;mdash; 정확한 멤버십, 빠른 조회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Python dict&lt;/b&gt; &amp;mdash; set과 비슷하나 value 슬롯이 들어가서 더 크다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SQLite(:memory: PK 인덱스)&lt;/b&gt; &amp;mdash; 디스크를 쓰지 않는 메모리 모드, id 컬럼에 PRIMARY KEY 적용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bloom Filter (목표 FPR 1%)&lt;/b&gt; &amp;mdash; 비트 배열 + 7개 해시&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Bloom Filter는 0.1% / 1% / 5% / 10% 4단계로 sweep을 돌려 메모리-정확도 트레이드오프 곡선까지 추출했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 항목&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빌드 시간 (wall-clock 초)&lt;/li&gt;
&lt;li&gt;RSS 메모리 증가량 (MB)&lt;/li&gt;
&lt;li&gt;hit 1만 건 조회 p50 / p99 지연 (&amp;mu;s)&lt;/li&gt;
&lt;li&gt;miss 1만 건 조회 p50 / p99 지연 (&amp;mu;s)&lt;/li&gt;
&lt;li&gt;Bloom Filter false positive 건수 + 실측 FPR&lt;/li&gt;
&lt;li&gt;DB 왕복 차단량 (= 1만 - false positive)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stdout 마지막에 깔끔하게 정리되어 출력된다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777976352842&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;name             build_s    mem_MB  p50_hit_us  p99_hit_us  p50_mis_us  p99_mis_us     FPs
set                0.154     32.44        0.30        1.00        0.20        0.80       0
dict               0.281     57.62        0.40        1.30        0.30        1.00       0
sqlite             1.179     20.00        2.30        4.80        1.50        1.70       0
bloom_1pct         3.352      0.00        3.10        6.30        1.70        3.40     106
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리 결과: dict 58MB &amp;rarr; Bloom 1.2MB로 약 50배 줄였다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lx22B/dJMcab49BNj/GT3xFSYKKMC4FNBKjWmINk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lx22B/dJMcab49BNj/GT3xFSYKKMC4FNBKjWmINk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lx22B/dJMcab49BNj/GT3xFSYKKMC4FNBKjWmINk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flx22B%2FdJMcab49BNj%2FGT3xFSYKKMC4FNBKjWmINk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;500&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;100만 정수 ID 기준 RSS 증가량은 다음과 같다:&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자료구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;빌드 시간&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;32.44 MB&lt;/td&gt;
&lt;td&gt;0.154초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dict&lt;/td&gt;
&lt;td&gt;57.62 MB&lt;/td&gt;
&lt;td&gt;0.281초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite (:memory: PK)&lt;/td&gt;
&lt;td&gt;20.00 MB&lt;/td&gt;
&lt;td&gt;1.179초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom Filter (FPR 1%)&lt;/td&gt;
&lt;td&gt;**1.14 MB**&lt;/td&gt;
&lt;td&gt;3.352초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브리프에는 &quot;80MB &amp;rarr; 1.2MB&quot;라고 적혀 있었으나 실제 측정은 dict 기준 58MB이다. 그래도 Bloom 대비 약 50배 차이라 인상은 그대로이다. 이것이 의미하는 바는, 운영 메모리 1GB짜리 컨테이너 워커 기준 dict는 약 5.7%를 차지하지만 Bloom은 0.1%로 떨어진다는 점이다. 워커 100개를 띄우면 5.6GB &amp;rarr; 120MB로 딱 그만큼 메모리 여유가 생기는 것이다.&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;⚠️ 표에 Bloom_1pct RSS가 0.00 MB로 찍힌 이유는 다음과 같다. 비트 배열 자체는 1,198,133 바이트 (약 1.14 MiB)인데 인터프리터가 이미 잡아놓은 페이지 안에 흡수되어 RSS 측정 단위(MB) 미만으로 잡혔기 때문이다. result.json의 extra.size_bytes 값이 실제 자료구조 크기이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드 시간은 Bloom이 가장 느리다 (3.35초 vs set 0.15초). 키 한 건당 7번 해싱하니 당연한 결과이다. 한 번 빌드해서 오래 쓰는 시나리오라면 무시해도 되는 비용이고, 매 요청마다 빌드해야 한다면 Bloom을 쓰지 않는 편이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;지연 시간 결과: Bloom이 set보다 8배 느린데도 캐시 앞단으로는 압승이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 차트는 hit/miss 1만 건씩 조회한 p50&amp;middot;p99 마이크로초 분포이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IeRTh/dJMcabYo3Fp/86urjFUl6LjTSGIyK0M22K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IeRTh/dJMcabYo3Fp/86urjFUl6LjTSGIyK0M22K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IeRTh/dJMcabYo3Fp/86urjFUl6LjTSGIyK0M22K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIeRTh%2FdJMcabYo3Fp%2F86urjFUl6LjTSGIyK0M22K%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;400&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;자료구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 hit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 hit&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 miss&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 miss&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;set&lt;/td&gt;
&lt;td&gt;0.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.00&amp;mu;s&lt;/td&gt;
&lt;td&gt;0.20&amp;mu;s&lt;/td&gt;
&lt;td&gt;0.80&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dict&lt;/td&gt;
&lt;td&gt;0.40&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;0.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.00&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;2.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;4.80&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.50&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.70&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom (FPR 1%)&lt;/td&gt;
&lt;td&gt;3.10&amp;mu;s&lt;/td&gt;
&lt;td&gt;6.30&amp;mu;s&lt;/td&gt;
&lt;td&gt;1.70&amp;mu;s&lt;/td&gt;
&lt;td&gt;3.40&amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;set/dict가 가장 빠르다. nanosecond 단위에 거의 들어간다. SQLite는 메모리 모드라도 PK 인덱스 lookup + Python &amp;lt;-&amp;gt; C 경계 비용으로 &amp;mu;s 단위가 된다. Bloom은 7번 해시 비용 때문에 set 대비 약 8배 느리다.&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;여기서 자주 나오는 잘못된 결론이 &quot;그러면 Bloom을 쓰지 않는 편이 빠르다&quot;인데, &lt;b&gt;블룸 필터의 비교 대상은 set이 아니라 DB 왕복&lt;/b&gt;이다. 같은 워크로드에서:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;in-memory set 조회: ~0.3&amp;mu;s&lt;/li&gt;
&lt;li&gt;in-memory Bloom 조회: ~3&amp;mu;s&lt;/li&gt;
&lt;li&gt;DB(Postgres/MySQL) 단순 PK 조회: 1~10ms = 1,000~10,000&amp;mu;s&lt;/li&gt;
&lt;li&gt;네트워크 캐시(Redis) 조회: 100~500&amp;mu;s&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미존재 키를 Bloom이 차단하여 DB로 가지 않게 만들면 1만 &amp;mu;s &amp;rarr; 3&amp;mu;s로 약 3,000배 빨라진다. &lt;b&gt;블룸 필터는 in-memory 자료구조 사이의 경쟁이 아니라 &quot;in-memory vs network/disk&quot; 경쟁에서 의미가 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸 필터 FPR 이론 vs 실측, DB 호출 99% 차단까지 검증한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FPR 4단계 sweep 결과&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목표 FPR 0.1% / 1% / 5% / 10% 4단계로 비트 배열 크기를 다르게 잡고 돌렸다. 공식이 실제로 맞는지 확인하는 것이 목적이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;목표 FPR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;m(비트)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;k(해시)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;크기(KB)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이론 FPR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;실측 FPR&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;DB 차단(/10,000)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;차단율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.10%&lt;/td&gt;
&lt;td&gt;14,377,588&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1,755.1&lt;/td&gt;
&lt;td&gt;0.100%&lt;/td&gt;
&lt;td&gt;0.090%&lt;/td&gt;
&lt;td&gt;9,991&lt;/td&gt;
&lt;td&gt;99.91%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00%&lt;/td&gt;
&lt;td&gt;9,585,059&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1,170.1&lt;/td&gt;
&lt;td&gt;1.004%&lt;/td&gt;
&lt;td&gt;1.060%&lt;/td&gt;
&lt;td&gt;9,894&lt;/td&gt;
&lt;td&gt;98.94%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5.00%&lt;/td&gt;
&lt;td&gt;6,235,225&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;761.1&lt;/td&gt;
&lt;td&gt;5.027%&lt;/td&gt;
&lt;td&gt;5.040%&lt;/td&gt;
&lt;td&gt;9,496&lt;/td&gt;
&lt;td&gt;94.96%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.00%&lt;/td&gt;
&lt;td&gt;4,792,530&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;585.0&lt;/td&gt;
&lt;td&gt;10.071%&lt;/td&gt;
&lt;td&gt;10.240%&lt;/td&gt;
&lt;td&gt;8,976&lt;/td&gt;
&lt;td&gt;89.76%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론 FPR과 실측 FPR이 거의 일치한다. 공식대로 사이즈를 잡으면 예측대로 동작한다는 뜻이라 운영 관점에서 매우 중요하다. SLA를 잡고 가도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxeCW0/dJMcabKTNoN/kwYLwXlYGkNykNqVl4VDU0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxeCW0/dJMcabKTNoN/kwYLwXlYGkNykNqVl4VDU0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxeCW0/dJMcabKTNoN/kwYLwXlYGkNykNqVl4VDU0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxeCW0%2FdJMcabKTNoN%2FkwYLwXlYGkNykNqVl4VDU0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;500&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB 호출 차단량 시각화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미존재 키 1만 건을 조회했을 때 Bloom이 &quot;확실히 없음&quot; 답을 하여 DB 호출 자체를 일으키지 않게 만든 건수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4PTFj/dJMcadaLN67/2Ht3JjVSvAn3AfHoyrH74K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4PTFj/dJMcadaLN67/2Ht3JjVSvAn3AfHoyrH74K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4PTFj/dJMcadaLN67/2Ht3JjVSvAn3AfHoyrH74K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4PTFj%2FdJMcadaLN67%2F2Ht3JjVSvAn3AfHoyrH74K%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;500&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;FPR 1% 설정으로 메모리 1.17MB만 쓰고도 1만 건 미존재 조회 중 9,894건을 차단하여 &lt;b&gt;DB 호출을 98.94% 잘라낸다.&lt;/b&gt; 0.1% 설정이면 9,991건을 차단하여 99.91%까지 올라가고, 메모리는 1.76MB 정도이다. 트레이드오프 곡선이 거의 정확하게 공식을 따라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트레이드오프 정리: 메모리 vs 차단율&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차단율 99.9%를 원하면: 메모리 ~1.8MB, 해시 10번&lt;/li&gt;
&lt;li&gt;차단율 99%를 원하면: 메모리 ~1.2MB, 해시 7번&lt;/li&gt;
&lt;li&gt;차단율 95%를 원하면: 메모리 ~750KB, 해시 4번&lt;/li&gt;
&lt;li&gt;차단율 90%를 원하면: 메모리 ~600KB, 해시 3번&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 sweep마다 빌드 시간과 hit p99도 함께 측정했는데, 해시 개수 k가 줄수록(10&amp;rarr;3) 둘 다 단조 감소한다. k=10일 때 빌드 4.5초&amp;middot;p99 hit 15&amp;mu;s, k=3일 때 빌드 2.1초&amp;middot;p99 hit 4.1&amp;mu;s이다. 키 한 건당 k번 해시하니 그저 비례 관계이다. 본인 워크로드에서 SLA가 빡빡하다면 FPR 5% 정도까지 풀어 hit 지연을 줄이는 것이 합리적인 선택일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그러면 언제 Bloom을 쓰고 언제 set을 쓰면 되는가?&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;set/dict가 답인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정확한 멤버십이 절대적으로 필요하다 (false positive 0)&lt;/li&gt;
&lt;li&gt;메모리에 여유가 있다 (수십 MB를 써도 무방)&lt;/li&gt;
&lt;li&gt;키 개수가 ~수십만 ~ 백만 단위이다&lt;/li&gt;
&lt;li&gt;핫 패스 in-memory 조회이다 (&amp;mu;s도 아까운 상황)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SQLite/RDB 인덱스가 답인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정확성과 영구 저장 둘 다 필요하다&lt;/li&gt;
&lt;li&gt;키마다 부가 정보(value)도 함께 보관해야 한다&lt;/li&gt;
&lt;li&gt;메모리 모드 SQLite는 set보다 느리고 무거우니 굳이 쓰지 않는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Bloom Filter가 답인 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;없음&quot; 답만 빠르게 받으면 되는 시나리오이다 (멤버십 게이트)&lt;/li&gt;
&lt;li&gt;메모리가 빡빡하다 (워커 수백 개 / 엣지 노드 / 모바일 클라이언트)&lt;/li&gt;
&lt;li&gt;키 개수가 수백만 ~ 수억 단위이다&lt;/li&gt;
&lt;li&gt;캐시&amp;middot;DB 앞단의 네거티브 캐시 가드이다&lt;/li&gt;
&lt;li&gt;가끔 false positive가 떠도 &quot;한 번 더 DB로 확인&quot; 정도는 허용된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안티 패턴: &quot;있음&quot; 답을 그대로 신뢰하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블룸 필터에서 가장 자주 나오는 사고가 이것이다. &quot;있음&quot; 답은 false positive 가능성이 있어 100% 신뢰할 수 없다. 반드시 다음 단계가 있어야 한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777976352845&quot; class=&quot;excel&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;key in bloom?
├─ 확실히 없음 &amp;rarr; 즉시 &quot;없음&quot; 응답 (DB 안 감)
└─ 있을 수도 있음 &amp;rarr; 캐시/DB로 가서 실제 확인
&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;이 구조로 짜면 &quot;없는 키&quot;는 99% 차단되고 &quot;있는 키&quot;는 항상 정확하게 처리된다. 반대로 &quot;있음&quot; 답을 그대로 응답으로 쓰면 1% 확률로 잘못된 데이터가 내려가는 사고가 터진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python 라이브러리 추천&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;pybloom-live&lt;/b&gt; &amp;mdash; 순수 파이썬 구현, scalable bloom filter도 지원한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pybloomfiltermmap3&lt;/b&gt; &amp;mdash; C 구현, mmap 기반이라 더 빠르다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;redisbloom&lt;/b&gt; &amp;mdash; Redis 모듈, 분산 환경에서 공유 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청량이 많다면 RedisBloom으로 가서 워커 간 필터를 공유하는 패턴이 정석이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸 필터 실험 데이터 정리: 캐시 앞단에 깔면 거의 무조건 이득이다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리&lt;/b&gt;: dict 58MB &amp;rarr; Bloom 1.2MB, 약 50배 절감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;차단율&lt;/b&gt;: FPR 1% 설정으로 미존재 키 98.94% 차단, 0.1% 설정이면 99.91%&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이론 vs 실측&lt;/b&gt;: 공식대로 잡으면 거의 정확하게 일치한다 &amp;rarr; SLA를 잡고 가도 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연&lt;/b&gt;: in-memory에서 set 대비 8배 느리지만 DB 왕복 대비 1,000배 이상 빠르다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;트레이드오프&lt;/b&gt;: 메모리를 600KB까지 줄이면 차단율은 90%로 떨어진다, 본인 워크로드에 맞춰 선택한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 앞단에 블룸 필터 한 줄을 깔면 백엔드 DB 호출의 90~99%가 잘린다. 메모리는 MB 단위로 끝나고, 코드 한 줄짜리 트레이드오프이다. 핫 패스 in-memory 자료구조 자리에는 쓰지 마라 &amp;mdash; 그곳은 set이 답이다. 다만 미존재 키 차단이 목적이라면 블룸 필터가 거의 무조건 이득이다.&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;result.json과 차트 4종을 모두 첨부했으니 본인 워크로드에서 키 개수와 허용 FPR을 바꿔 그대로 sweep을 돌려보면 된다. 공식 신뢰도까지 검증한 1차 데이터라 의사결정 자료로 그대로 활용해도 무방하다.&lt;/p&gt;</description>
      <category>Backend</category>
      <category>Bloom Filter Python</category>
      <category>DB 호출 줄이기</category>
      <category>블룸 필터 메모리</category>
      <category>블룸 필터 사용 사례</category>
      <category>캐시 앞단 필터</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/416</guid>
      <comments>https://yscho03.tistory.com/416#entry416comment</comments>
      <pubDate>Tue, 5 May 2026 19:19:53 +0900</pubDate>
    </item>
    <item>
      <title>자신보다 뛰어난 사람을 채용해야 하는 이유 (그리고 어떻게 선발할 것인가)</title>
      <link>https://yscho03.tistory.com/415</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사가 좀 굴러간다 싶을 때 누구나 한 번씩 부딪히는 벽이 있다. &quot;내가 영업도 제일 잘하고 개발도 제일 잘하는데 왜 직원을 뽑아야 하는가?&quot; &lt;b&gt;이 본능적인 거부감을 깨지 못하면 회사는 절대 성장하지 못한다.&lt;/b&gt; 자기보다 뛰어난 사람을 채용하는 것이 왜 합리적인 결정인지, 어떻게 알아보는지, 한국 현실에서 무엇을 조심해야 하는지 짚어본다. 면접에서 바로 활용할 질문도 함께 정리했다.&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;&lt;a href=&quot;https://www.youtube.com/watch?v=_ZqtQ2ZbYIQ&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.youtube.com/watch?v=_ZqtQ2ZbYIQ&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=_ZqtQ2ZbYIQ&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/dOaXug/dJMb9fZy5tw/rpGuNlYjKyAXdb5Sa1BFZk/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=514_194_820_526,https://scrap.kakaocdn.net/dn/cccjYI/dJMb9frJdcO/zbLu9w2M4O3w0k8NvrGdQK/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=514_194_820_526,https://scrap.kakaocdn.net/dn/QF4n3/dJMb9g5eS9O/7EJ7mknAXeYbfT9RirNwK1/img.jpg?width=1280&amp;amp;height=720&amp;amp;face=514_194_820_526&quot; data-video-width=&quot;860&quot; data-video-height=&quot;484&quot; data-video-origin-width=&quot;860&quot; data-video-origin-height=&quot;484&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;EO Korea&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/_ZqtQ2ZbYIQ&quot; width=&quot;860&quot; height=&quot;484&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사장이 제일 잘하는 회사는 100명이 되어도 그대로다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이비드 오길비가 임원 책상마다 마트료시카 인형을 올려두고 메모를 끼워뒀다는 일화는 채용 이야기가 나올 때마다 등장한다. 메모 내용은 대략 이러했다. &quot;각자 자기보다 작은 사람을 뽑으면 우리는 난쟁이 회사가 된다. 자기보다 큰 사람을 뽑으면 거인 회사가 된다.&quot; 처음 들으면 광고 카피처럼 들리지만, 곱씹을수록 무서운 이야기다. &lt;b&gt;사장이 모든 영역의 최고라면 회사 의사결정의 천장은 사장의 키 높이에서 멈춰버린다.&lt;/b&gt; 직원이 100명이 되든 1000명이 되든 인원만 늘어날 뿐, 회사가 다룰 수 있는 문제의 크기는 변하지 않는다.&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;각설하고, 이 글에서 풀어낼 것은 두 갈래다. 사람들이 자기보다 뛰어난 사람을 뽑는 것을 왜 망설이는지, 그리고 일단 뽑기로 마음먹었을 때 어떻게 알아보고 한국 환경에 어떻게 맞춰야 하는지다. 도덕론은 일부러 배제했고 실용 관점으로만 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자기보다 뛰어난 사람을 망설이게 만드는 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접을 보다가 &quot;이 사람 나보다 잘하는구나&quot; 싶은 순간이 오면, 대부분 본능적으로 단점부터 찾기 시작한다. 이력서 공백을 트집 잡거나 우리 도메인 경험이 없다는 식으로 합리적 평가처럼 포장한다. 솔직히 말하면 &lt;b&gt;자존심 방어&lt;/b&gt;다. 이를 인정하는 것이 출발점이다.&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;자존심 못지않게 무서운 것이 통제 욕구다. 잘 모르는 영역을 위임하면 검증할 방법이 없다. 마케팅을 한 번도 제대로 해보지 않은 사장이 마케팅 전문가의 &quot;이건 이렇게 가야 한다&quot; 같은 의견을 들으면 옳고 그름을 판단할 수 없다. 그래서 결국 자기가 어설프게라도 아는 영역의 사람만 뽑는 패턴이 굳어진다.&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;A급은 비싸다는 고정관념도 한몫한다. 연봉 한 명 분으로 B급 둘셋을 굴리는 것이 싸 보이지만, &lt;b&gt;결과물의 차이는 머릿수로 메워지지 않는다.&lt;/b&gt; 디자이너나 엔지니어처럼 결과물 편차가 큰 직군일수록 격차는 더 벌어진다. 잡스가 &quot;평균과 최고의 결과 차이가 50배에서 100배까지 난다&quot;고 본 이유가 여기에 있다.&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;여기에 한국 특유의 위계 압박이 더해진다. &quot;후배가 선배보다 잘해서는 안 된다&quot;는 무의식이 채용 자리에서 의외로 자주 작동한다. 신입을 뽑을 때는 부담이 적지만, 사장보다 경력이 길고 연봉도 높은 사람을 뽑는 것은 다른 결단이다. 이를 넘어가지 못하면 조직은 사장의 그릇 안에서만 성장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래도 뽑아야 하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A급의 진짜 가치는 시간 단축이 아니라 &lt;b&gt;평범한 팀이라면 손도 대지 못할 문제를 풀 수 있다는 데 있다.&lt;/b&gt; &quot;왜 이 방향으로 가야 하는지&quot;를 아는 사람과 지시받은 대로만 움직이는 사람의 차이는 한두 달 단위로는 잘 보이지 않는다. 1년쯤 누적되면 결과물이 완전히 갈라진다.&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;사장 시간 관점에서도 그렇다. 사장의 시간은 회사에서 가장 비싼 자원인데, 자기보다 못한 사람만 뽑으면 매번 검수하고 수정하느라 시간을 모두 소진한다. 정작 사장이 해야 할 의사결정과 자본 배분은 손도 대지 못한다. 자기보다 뛰어난 사람을 뽑아야 그 영역에서 손을 뗄 수 있고, 그것이 진짜 위임이다.&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;또 하나는 인재 밀도 효과다. 넷플릭스에서 강조하는 talent density 개념인데, A급은 다른 A급과 일하고 싶어 한다. 회사에 B급, C급이 많으면 A급 지원자는 면접 한 번 보고 거절한 뒤 사라진다. 반대로 첫 A급을 잘 뽑으면 그 사람이 자기 네트워크에서 다음 A급을 끌고 온다. &lt;b&gt;첫 한 명이 결정적인 이유&lt;/b&gt;가 여기에 있다.&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;시대 변화도 함께 작용한다. AI가 IC 영역의 작업을 빠르게 잠식하는 시대에는 자기보다 뛰어난 사람을 뽑아 결과를 만드는 능력이 사실상 장기 커리어 자체가 된다. 혼자 잘하는 사람보다 뛰어난 사람들을 모아 더 큰 결과를 내는 사람이 살아남는 구조로 가고 있다.&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;투자를 받거나 매각할 때도 채용의 흔적이 모두 드러난다. 투자자나 인수자가 가장 먼저 보는 것은 &quot;사장이 빠지면 회사가 굴러가는가?&quot;다. 사장 의존도가 높으면 같은 매출이라도 밸류에이션이 깎인다. &lt;b&gt;채용은 인건비 항목이 아니라 회사 가격표에 붙는 숫자를 직접 바꾸는 작업이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자기보다 뛰어난 사람을 어떻게 알아보는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터는 면접 자리에서 바로 적용할 수 있는 것들이다. WP Engine 창업자 &lt;a href=&quot;https://news.hada.io/topic?id=28923&quot;&gt;Jason Cohen이 정리한 평가 기준&lt;/a&gt;에 한국 맥락을 얹어 풀어본다. 자기 전문 영역 밖의 사람을 뽑을 때 특히 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;면접이 끝나고 사장이 메모장을 켜는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;면접이 끝났을 때 사장의 머릿속에 &quot;이 사람이 한 말의 절반은 당장 실행해야겠다&quot;는 충동이 드는지가 첫 신호다. A급은 답변을 잘하는 것이 아니라 &lt;b&gt;회사를 어떻게 바꿀지에 대한 그림&lt;/b&gt;을 들고 온다. 추상적 원론만 읊는 사람은 컨설턴트지 실행자가 아니다. 반대로 면접 30분을 듣다가 갑자기 메모를 시작하게 만드는 사람이 있는데, 그것이 신호다. 내가 직접 겪었던 케이스로는 콘텐츠 마케터 면접에서 &quot;지금 랜딩에서 두 번째 섹션을 빼고 가격표만 위로 올려도 가입 전환율이 올라갈 것 같습니다&quot;라는 답변을 들은 적이 있다. 입사 둘째 주에 실제로 그렇게 바꿨고 전환이 올라갔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사장이 그 사람 밑에서 일할 수 있겠는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저커버그가 인용되는 채용 원칙 중에 이런 것이 있다. &quot;내 밑에서 일할 사람은, 내가 그 사람 밑에서 일할 의향이 있을 때만 뽑는다.&quot; 직역이지만 의미는 명확하다. &lt;b&gt;사장이 그 사람에게서 무언가 배울 수 있어야 한다&lt;/b&gt;는 뜻이다. 배울 것이 없는 사람은 정의상 자기보다 뛰어난 사람이 아니다.&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;이 기준과 함께 봐야 할 것이 자기 직무 너머에 영향을 미치는지다. 자기 부서 일만 잘하는 사람은 한계가 있다. 회의에서 의사결정 질을 높이고, 부서 간 갈등을 풀고, 다른 팀과 협업할 때 결과를 만드는 사람이 진짜 A급이다. &quot;지금까지 가장 어려웠던 부서 간 협업 경험&quot;을 물으면 답변에서 그 결이 모두 드러난다. 자기 부서 이야기만 하면 IC급, 조직 전반을 이야기하면 리더급으로 봐도 큰 무리가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회사가 진짜 겪는 문제를 면접 테이블에 올리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사가 지금 실제로 풀고 있는 문제를 면접 자리에 올려놓는 것이다. SaaS라면 이탈률을 어떻게 줄일지, 마케팅이라면 제한된 예산으로 신규 유저를 어떻게 확보할지 같은 질문이다. 답이 정답인지 아닌지보다 &lt;b&gt;사고 구조&lt;/b&gt;를 봐야 한다. 정보가 부족할 때 어떤 질문을 던지는지, 가설을 어떻게 세우는지, 트레이드오프를 어떻게 다루는지가 답하는 5분 안에 모두 드러난다. 여기서 &quot;더 알려주실 수 있는 데이터가 있나요?&quot; 같은 역질문이 빨리 나오는 사람일수록 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레퍼런스 체크는 질문이 절반이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이력서 검증보다 훨씬 강력한 것이 전 직장 레퍼런스 체크인데, &quot;어떤 사람이었나요?&quot;처럼 두루뭉술하게 물으면 &quot;좋은 사람이었어요&quot;라는 답변밖에 나오지 않는다. 다음 질문이 진짜 답을 끌어낸다. &quot;이 분이 가장 빛났던 환경은 어떤 환경이었나요?&quot;는 어떤 조건에서 성과를 내는 사람인지를 드러내고, &quot;이 분이 무너지는 시나리오가 있다면 어떤 상황인가요?&quot;는 약점을 명확히 보여준다. 마지막으로 &quot;본인이 잘 모르는 본인의 강점이 있다면 무엇이라고 말해주겠나요?&quot;를 물으면 메타인지 수준이 보인다. 통화 5분이면 끝나지만, 이력서 두 시간 검토보다 정보가 더 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한국에서 채용할 때 추가로 조심할 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미국 채용 글들은 &quot;잘못 뽑으면 빠르게 해고&quot;가 전제다. 다만 한국은 노동법이 다르므로 그대로 따라가면 다친다. &lt;a href=&quot;https://news.hada.io/topic?id=28923&quot;&gt;hada.io의 Jason Cohen 글 댓글&lt;/a&gt;에서도 &quot;한국에서는 채용 후 해고가 어렵다&quot;는 지적이 있었는데, 채용 설계 전체를 흔드는 변수다. &lt;a href=&quot;https://www.law.go.kr/%EB%B2%95%EB%A0%B9/%EA%B7%BC%EB%A1%9C%EA%B8%B0%EC%A4%80%EB%B2%95&quot;&gt;근로기준법 제23조&lt;/a&gt;는 정당한 사유 없는 해고를 금지하고 있어 미국식 at-will employment를 그대로 가져오면 부당해고 분쟁으로 직행한다.&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;해고가 어려운 만큼 채용이 더 빡빡해야 한다. 근로기준법상 정당한 사유 없는 해고는 부당해고 구제신청 대상이라, 한국에서는 &lt;b&gt;수습 기간 3개월을 진짜로 활용해야 한다.&lt;/b&gt; 입사 계약서에 수습 기간을 명시하고 30일&amp;middot;60일&amp;middot;90일 시점의 평가 기준을 사전에 합의하는 것이 필수다. 명목상 수습으로만 두고 자동 패스시키는 것이 가장 비싼 실수가 된다.&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;연봉 협상에서도 시장가 위로 줘야 사람이 온다. 한국 스타트업이 자주 실패하는 지점인데, &quot;초기라서 예산이 빡빡하다&quot;라며 시장가보다 낮게 부르는 케이스가 많다. 왜일까? A급은 그 가격에 오지 않기 때문이다. 굳이 그 자리에 가지 않아도 다른 곳에서 시장가로 받을 수 있다. 자기보다 뛰어난 사람을 뽑고 싶다면 &lt;b&gt;시장가 +&amp;alpha;&lt;/b&gt;를 줄 각오를 해야 하고, 그것이 안 되면 차라리 채용을 미루는 것이 낫다. 현금이 부족하면 스톡옵션이나 RSU로 보완할 수 있는데, 옵션 vesting 구조와 행사 조건을 처음부터 명확히 짜놓지 않으면 나중에 분쟁이 생긴다.&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;위계 문화도 깨야 한다. 후배 직군인데 사장보다 더 잘하는 상황이 정상이라는 분위기가 팀에 자리잡아야 A급이 편하게 일한다. 사장이 회의에서 &quot;이건 박OO이 결정해주세요&quot; 같은 식으로 권한을 명시적으로 위임하는 시그널이 자주 나와야 한다. 그렇지 않으면 A급은 &quot;여기서 내 결정권은 어디까지인가?&quot;를 계속 의심하다가 결국 떠난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;잘못 뽑았을 때가 진짜 비용&lt;/h2&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;b&gt;잘못 뽑은 것을 1년 동안 방치하는 것이 진짜 비용&lt;/b&gt;이다. 저성과자를 1년 방치하면 옆에서 일하는 A급들이 먼저 떠난다. &quot;이 회사는 기준이 낮구나&quot;로 인식되는 순간이 문제다. 한 명을 자르지 못하다가 다섯 명을 잃는 셈이 된다.&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;수습 기간 동안 매월 점검할 것이 있다. 첫 30일에는 회사 핵심 시스템과 도메인을 이해했는지, 첫 결과물을 어떤 형태로든 내놨는지를 본다. 60일쯤에는 자기 영역에서 자율적으로 의사결정을 시작했는지, 동료 평판이 어떻게 형성되고 있는지가 드러난다. 90일에는 사장이 검수하지 않아도 결과물이 기준치 이상 나오는지, 다음 6개월의 임팩트가 그려지는지를 판단한다. 어느 단계에서든 명확히 미달이면 다음 달까지 끌지 말고 결단해야 한다.&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;법적으로 정당한 해고가 어렵기 때문에 한국에서는 보통 권고사직(합의해지)으로 간다. 절차는 면담에서 평가 사실관계를 공유하면서 기록을 남기고, 권고사직 의사를 명시하면서 위로금을 협의하고, 사직서와 합의서를 동시에 작성&amp;middot;서명하고, 4대보험과 실업급여 처리를 안내하는 흐름이다. 디테일한 사항은 고용노동부 사이트나 노무사 상담을 받는 것이 안전하다. 절차에서 실수하면 부당해고로 인정돼 더 큰 비용이 발생한다.&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;조심할 것이 하나 더 있다. 한 번 잘못 뽑고 나면 &quot;역시 사람 뽑는 것은 아니다&quot;라며 다시 1인 사장 모드로 돌아가는 사례가 흔하다. 다만 이는 시스템 문제로 봐야지 사람 자체를 포기할 일은 아니다. 평가 기준을 다듬고 레퍼런스 체크를 강화하고 수습 기간을 진짜로 굴리면 다음에는 더 잘 뽑게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1인 사업자도 첫 직원은 자기보다 뛰어나야 한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1인 사업자나 1~2명짜리 작은 팀일수록 첫 직원의 무게가 크다. 본인이 영업&amp;middot;마케팅&amp;middot;개발을 모두 하다가 한계가 보이는 영역이 있을 것이다. 그 영역을 본인보다 잘하는 사람을 뽑는 것이 정답이다. &lt;b&gt;본인이 그나마 잘하는 영역을 채울 사람이 아니라, 본인이 약한 영역을 채울 사람을 뽑는 것이다.&lt;/b&gt;&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;지금까지 짚은 평가 기준을 면접에서 한 번에 적용해보고 싶다면, 회사가 지금 진짜 겪고 있는 문제 한 개를 면접 테이블에 던져보면 된다. 그 답변이 자기보다 뛰어난 사람인지 아닌지를 절반 이상 알려준다. 채용에서 자존심이 깨지는 순간이 있는데, 그 자존심은 좀 흘려보내도 된다. 그래야 회사가 성장한다.&lt;/p&gt;</description>
      <category>회사생활</category>
      <category>A급 인재 채용</category>
      <category>리더의 채용 기준</category>
      <category>마크 저커버그 채용 원칙</category>
      <category>스티브 잡스 A플레이어</category>
      <category>우수 인재 채용 방법</category>
      <category>채용 실패 대응</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/415</guid>
      <comments>https://yscho03.tistory.com/415#entry415comment</comments>
      <pubDate>Tue, 5 May 2026 00:07:13 +0900</pubDate>
    </item>
    <item>
      <title>Redis 캐시 stampede를 진짜로 막는 법 &amp;mdash; single-flight, XFetch, 분산 lock 직접 돌려본 결과</title>
      <link>https://yscho03.tistory.com/414</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;TTL 만료 1ms 사이에 DB로 1000건이 몰리는 redis 캐시 stampede 상황은 누구나 한 번쯤 겪어봤을 것이다. 이를 막는 패턴은 단일 프로세스용 single-flight, 확률적 사전 갱신 XFetch, Redis SET NX EX 기반 분산 lock 이렇게 셋이 표준인데, 실제로 어느 것이 얼마나 효과가 있는지 한국어 자료에는 숫자가 거의 없다. 그래서 직접 golang 1.22 환경에서 동시성 100/500/1000으로 10라운드씩 네 패턴을 돌려서 p99/p999 꼬리 지연까지 비교했다. 결론부터 말하면 DB 호출 수렴 효과는 셋 다 거의 동일하지만, &lt;b&gt;꼬리 지연과 lock 누적 대기 시간에서 갈림길이 명확하게 나뉜다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Redis Stampede가 무엇이며 왜 그렇게 큰 문제인가&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1721&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctXbjU/dJMcahxxPvc/9UlRa3ek5FznZBNQNVSIr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctXbjU/dJMcahxxPvc/9UlRa3ek5FznZBNQNVSIr0/img.png&quot; data-alt=&quot;Redis Stampede 현상&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctXbjU/dJMcahxxPvc/9UlRa3ek5FznZBNQNVSIr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctXbjU%2FdJMcahxxPvc%2F9UlRa3ek5FznZBNQNVSIr0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1721&quot; height=&quot;498&quot; data-origin-width=&quot;1721&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Redis Stampede 현상&lt;/figcaption&gt;
&lt;/figure&gt;
&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;캐시 stampede(쇄도 현상, thundering herd라고도 부른다)는 인기 키의 TTL이 만료되는 순간 수백~수천 건의 동시 요청이 한꺼번에 백엔드로 직행해서 짧은 시간에 시스템 전체를 압박하는 패턴이다. 평소에는 캐시 히트율 99%로 잘 돌아가다가 TTL 만료 직후 1ms 사이에 DB로 1000건이 그대로 꽂히는 것 &amp;mdash; 이것이 redis 캐시 stampede의 본질이다.&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;문제는 단순히 &quot;DB가 한 번 더 일하는&quot; 수준이 아니다. 백엔드 응답이 100ms 걸린다면 그 100ms 동안 들어온 요청 전부가 캐시 미스를 보고 또 백엔드를 때린다. 결과적으로 DB가 같은 키를 1000번 조회하고, 1000개의 응답이 한꺼번에 캐시에 set 되며, 그 사이 들어온 요청은 죄다 미스 상태로 누적된다. 인기 키가 여러 개라면 그것이 동시 다발로 일어나면서 DB 커넥션 풀이 즉시 고갈된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTL 만료 1ms에 DB로 1000건이 몰리는 진짜 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 단순히 &quot;운 나쁜 타이밍&quot;이 아닌 이유는, &lt;b&gt;TTL이 백엔드 응답시간보다 짧으면 매 라운드마다 미스가 보장되기 때문이다&lt;/b&gt;. 본 실험에서도 의도적으로 TTL을 80ms, 백엔드 응답을 100ms로 잡았다. 첫 요청이 백엔드를 때리는 동안(100ms) 후속 요청 999개가 같은 키로 들어오는데, 캐시는 아직 비어있다(첫 요청이 set 하기 전까지). 따라서 999개도 모두 미스 &amp;rarr; 모두 백엔드 직행 &amp;rarr; 1000개의 동시 백엔드 호출이 cliff처럼 발생한다.&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;이를 막는 핵심 아이디어는 단 하나다: &lt;b&gt;&quot;같은 키에 대한 미스를 어떻게 1건으로 줄일 것인가&quot;&lt;/b&gt;. 패턴별로 이 문제에 답하는 방식이 다를 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 돌려본 실험 세팅 &amp;mdash; 4패턴, 동시성 1000, 10라운드&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777880387206&quot; class=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;yaml&quot;&gt;&lt;code&gt;config:
  ttl_ms: 80
  slow_backend_ms: 100
  rounds: 10
  concurrencies: [100, 500, 1000]
  patterns: [naive, singleflight, xfetch, distlock]
  lock_poll_ms: 2
  lock_ttl_sec: 5
  xfetch_beta: 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;Redis와 백엔드는 in-process 시뮬레이터로 돌려서 네트워크 지연 변수를 제거했다. 따라서 측정값에 보이는 지연은 순수하게 백엔드 처리 시간(100ms)과 lock 폴링 / single-flight 대기 같은 &quot;동기화 비용&quot;으로 해석하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TTL을 백엔드 응답시간보다 짧게 잡은 이유는 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 의도적인 설계다. TTL이 백엔드 응답보다 길면 첫 요청이 set 하는 동안 후속 요청은 그냥 캐시 히트로 빠진다 &amp;rarr; stampede가 일어나지 않는다. 반대로 TTL이 응답시간보다 짧으면(80ms &amp;lt; 100ms) 매 라운드마다 반드시 cliff가 발생한다. 이렇게 만들어야 패턴 간 차이가 명확하게 드러난다. 실전에서도 인기 캐시 키는 TTL이 짧고 백엔드는 느리기 때문에 이 시나리오가 가장 현실적인 worst case다.&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;측정 지표는 여섯 축으로 잡았다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;지표&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의미&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;봐야 하는 이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db_calls&lt;/td&gt;
&lt;td&gt;백엔드 도달한 요청 수&lt;/td&gt;
&lt;td&gt;stampede 본질 &amp;mdash; 적을수록 좋다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stampede%&lt;/td&gt;
&lt;td&gt;미스 시 동시 백엔드 호출 비율&lt;/td&gt;
&lt;td&gt;패턴 효과 직접 지표&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p50_ms&lt;/td&gt;
&lt;td&gt;응답 지연 중앙값&lt;/td&gt;
&lt;td&gt;정상 상태 응답성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99/p999_ms&lt;/td&gt;
&lt;td&gt;꼬리 지연 분위&lt;/td&gt;
&lt;td&gt;패턴 차별화 핵심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rps&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_contention_ms&lt;/td&gt;
&lt;td&gt;누적 lock 대기 시간&lt;/td&gt;
&lt;td&gt;distlock 숨은 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패턴 1: 아무것도 하지 않는 naive 캐시 &amp;mdash; 베이스라인&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 케이스다. 캐시 미스면 그냥 백엔드 호출 &amp;rarr; 응답 받으면 캐시에 set. 동시성 제어 같은 것은 없다. 결과는 처참하다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;pattern&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;reqs&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;db_calls&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;stampede%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p999_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;rps&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;100.00%&lt;/td&gt;
&lt;td&gt;101.86&lt;/td&gt;
&lt;td&gt;101.96&lt;/td&gt;
&lt;td&gt;494.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;100.00%&lt;/td&gt;
&lt;td&gt;101.39&lt;/td&gt;
&lt;td&gt;101.43&lt;/td&gt;
&lt;td&gt;2467.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;100.00%&lt;/td&gt;
&lt;td&gt;108.14&lt;/td&gt;
&lt;td&gt;108.28&lt;/td&gt;
&lt;td&gt;4903.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 1000에서 10라운드 돌렸더니 DB 호출이 정확히 10,000건 &amp;mdash; &lt;b&gt;요청의 100%가 백엔드까지 직행했다&lt;/b&gt;. 캐시가 사실상 무의미해진 것이다. 동시성에 비례해 DB 호출이 1,000 &amp;rarr; 5,000 &amp;rarr; 10,000으로 그대로 폭증한다. 이것이 cliff 시나리오의 디폴트 결과다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IcjR9/dJMcaiwpwUZ/Qh5Jclk2qxAl2KR3WlLprk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IcjR9/dJMcaiwpwUZ/Qh5Jclk2qxAl2KR3WlLprk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IcjR9/dJMcaiwpwUZ/Qh5Jclk2qxAl2KR3WlLprk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIcjR9%2FdJMcaiwpwUZ%2FQh5Jclk2qxAl2KR3WlLprk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;360&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;위 그래프에서 naive만 동시성에 비례해 솟구치고 나머지 셋은 바닥에 깔리는 것이 보인다. 차이가 두 자리 수 수준이 아니라 세 자리 수 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패턴 2: single-flight &amp;mdash; 단일 프로세스의 정답&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;golang &lt;a href=&quot;https://pkg.go.dev/golang.org/x/sync/singleflight&quot;&gt;golang.org/x/sync/singleflight&lt;/a&gt; 패키지가 표준 구현체다. 같은 키에 대한 인플라이트 호출을 자동으로 병합해준다. 100개 고루틴이 동시에 같은 키를 조회해도 백엔드는 1번만 때리고 나머지 99개는 그 결과를 공유한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777880387209&quot; class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;import &quot;golang.org/x/sync/singleflight&quot;

var sfg singleflight.Group

func GetUser(ctx context.Context, id string) (*User, error) {
    v, err, _ := sfg.Do(id, func() (interface{}, error) {
        // 캐시 확인 &amp;rarr; 미스면 DB 호출 &amp;rarr; 캐시 set
        return loadFromDB(ctx, id)
    })
    if err != nil {
        return nil, err
    }
    return v.(*User), nil
}
&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;실험 결과는 깔끔하다. 동시성 1000에서도 라운드당 1건씩 10라운드 = &lt;b&gt;DB 호출 정확히 10건&lt;/b&gt;. naive 대비 1000배 감소다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;pattern&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;db_calls&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;stampede%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p999_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;rps&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;singleflight&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1.00%&lt;/td&gt;
&lt;td&gt;104.12&lt;/td&gt;
&lt;td&gt;104.15&lt;/td&gt;
&lt;td&gt;493.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;singleflight&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.20%&lt;/td&gt;
&lt;td&gt;101.56&lt;/td&gt;
&lt;td&gt;101.59&lt;/td&gt;
&lt;td&gt;2476.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;singleflight&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.10%&lt;/td&gt;
&lt;td&gt;101.92&lt;/td&gt;
&lt;td&gt;102.00&lt;/td&gt;
&lt;td&gt;4942.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stampede 비율(요청 중 실제 DB까지 도달한 비율)이 동시성 1000에서 0.10%까지 떨어졌다. 이것이 single-flight의 위력이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rlkhk/dJMcaiiS8DY/Xor8GatelyVXBPdLwhRCMK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rlkhk/dJMcaiiS8DY/Xor8GatelyVXBPdLwhRCMK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rlkhk/dJMcaiiS8DY/Xor8GatelyVXBPdLwhRCMK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Frlkhk%2FdJMcaiiS8DY%2FXor8GatelyVXBPdLwhRCMK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;360&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;p99도 101~104ms 수준으로 백엔드 응답시간(100ms)과 거의 동일하다. 추가 오버헤드가 거의 없다. 코드 변경량도 최소다 &amp;mdash; 함수 한 번 감싸면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한계: 멀티 인스턴스에서는 그냥 무력하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;single-flight의 본질적 한계는 &lt;b&gt;프로세스 경계를 넘지 못한다는 것이다&lt;/b&gt;. 같은 서비스 인스턴스 10개가 떠 있으면 각 인스턴스가 자기 안에서만 인플라이트 병합을 한다. 인스턴스 10개 &amp;times; 라운드 10번 = DB 호출 100건이 되는 것이다. 인스턴스 100개라면 1000건이다. 결국 단일 프로세스(또는 정말 적은 수의 인스턴스)에서만 답이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패턴 3: XFetch &amp;mdash; 만료 직전에 미리 새로고침하는 확률적 트릭&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XFetch는 Vattani et al.의 &lt;a href=&quot;https://www.vldb.org/pvldb/vol8/p886-vattani.pdf&quot;&gt;&quot;Optimal Probabilistic Cache Stampede Prevention&quot;&lt;/a&gt; 논문(VLDB 2015)에서 제안된 알고리즘이다. 핵심 아이디어는 &lt;b&gt;&quot;TTL이 만료되기 전에 일부 요청이 확률적으로 미리 갱신하게 만들어서 만료 순간의 cliff를 평탄화한다&quot;&lt;/b&gt;이다.&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;수식 한 줄로 요약하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777880387210&quot; class=&quot;lisp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;should_refresh = (now - delta * beta * ln(rand())) &amp;gt;= expiry
&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;여기서 delta는 백엔드 응답시간 추정치, beta는 적극성 계수(보통 1.0)다. 만료 직전일수록 갱신 확률이 높아져서 한 요청이 &quot;내가 미리 갱신하겠다&quot; 하고 백엔드를 때린다. 다른 요청은 아직 살아있는 캐시값을 받는다.&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;이는 정상 분포 트래픽에서 진짜로 강력하다. 만료 순간에 1000건이 동시에 cliff를 만나는 것이 아니라, 만료 50ms 전부터 분산된 시점에 1건이 미리 갱신해주기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장점은 부드러운 부하 분산, 단점은 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 본 실험은 cliff 시나리오다 &amp;mdash; 워커가 한꺼번에 동시 조회하는 상황. 이런 환경에서는 XFetch의 진가가 드러나지 않는다. 결과는 실질적으로 single-flight와 동일했다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;pattern&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;db_calls&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;stampede%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p999_ms&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xfetch&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1.00%&lt;/td&gt;
&lt;td&gt;101.53&lt;/td&gt;
&lt;td&gt;101.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xfetch&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.20%&lt;/td&gt;
&lt;td&gt;101.63&lt;/td&gt;
&lt;td&gt;101.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xfetch&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.10%&lt;/td&gt;
&lt;td&gt;102.10&lt;/td&gt;
&lt;td&gt;102.18&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p99가 101~102ms로 single-flight보다 살짝 더 낫다(미세한 차이지만 일관된다). cliff에서는 미스 경로의 dedupe 효과가 single-flight과 거의 같이 나오기 때문이다. &lt;b&gt;XFetch의 본 가치는 &quot;만료 전에 도착하는 정상 트래픽을 흡수하는 것&quot;인데 cliff 시나리오에는 그런 트래픽이 없어서 차별화 포인트가 사라진다&lt;/b&gt;. 이는 단점이라기보다 적용 환경이 다른 것이다 &amp;mdash; 정상 분포 트래픽이라면 XFetch가 가장 부드럽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;패턴 4: 분산 lock(SET NX EX) &amp;mdash; 다중 인스턴스의 표준 카드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 인스턴스가 같은 Redis를 공유하는 환경에서는 single-flight가 막지 못한다. 그래서 Redis 자체에 분산 lock을 만드는 것이 표준 카드다. &lt;a href=&quot;https://redis.io/docs/latest/commands/set/&quot;&gt;Redis SET 명령의 NX/EX 옵션&lt;/a&gt;을 붙여서 lock을 잡고, 잡지 못한 워커는 짧은 주기로 폴링하다가 lock을 잡은 워커가 캐시를 채우면 그 결과를 받아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777880387210&quot; class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;// pseudo-code
func GetUserDistLock(ctx context.Context, id string) (*User, error) {
    if v, ok := cache.Get(id); ok {
        return v, nil
    }
    lockKey := &quot;lock:&quot; + id
    // SET lockKey 1 NX EX 5
    locked, _ := redis.SetNX(ctx, lockKey, &quot;1&quot;, 5*time.Second).Result()
    if locked {
        defer redis.Del(ctx, lockKey)
        u, err := loadFromDB(ctx, id)
        if err != nil { return nil, err }
        cache.Set(id, u, 80*time.Millisecond)
        return u, nil
    }
    // lock 못 잡았음 &amp;rarr; 2ms 폴링
    for {
        time.Sleep(2 * time.Millisecond)
        if v, ok := cache.Get(id); ok {
            return v, nil
        }
    }
}
&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;DB 호출 결과는 single-flight&amp;middot;XFetch와 동일하게 라운드당 1건으로 수렴한다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;pattern&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;db_calls&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;stampede%&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p999_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;lock_contention_ms&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distlock&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;1.00%&lt;/td&gt;
&lt;td&gt;103.94&lt;/td&gt;
&lt;td&gt;103.98&lt;/td&gt;
&lt;td&gt;101,453.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distlock&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.20%&lt;/td&gt;
&lt;td&gt;104.40&lt;/td&gt;
&lt;td&gt;104.81&lt;/td&gt;
&lt;td&gt;505,192.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;distlock&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.10%&lt;/td&gt;
&lt;td&gt;104.99&lt;/td&gt;
&lt;td&gt;105.23&lt;/td&gt;
&lt;td&gt;1,007,795.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 p99가 single-flight 대비 3ms, p999는 5ms 더 크다. 별 것 아닌 듯 보이지만 동시성 1000에서 일관되게 더 나쁘다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c2QXTq/dJMcadPmZqP/e09YtgTshoWwkaDUb1Mw7K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c2QXTq/dJMcadPmZqP/e09YtgTshoWwkaDUb1Mw7K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c2QXTq/dJMcadPmZqP/e09YtgTshoWwkaDUb1Mw7K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc2QXTq%2FdJMcadPmZqP%2Fe09YtgTshoWwkaDUb1Mw7K%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;360&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숨은 비용: 누적 lock 대기 시간이 꼬리 지연을 만든다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;distlock 줄의 마지막 컬럼 lock_contention_ms가 진짜 핵심이다. 동시성 1000에서 &lt;b&gt;누적 1,007,795ms&lt;/b&gt; = &lt;b&gt;약 1,007초&lt;/b&gt;가 lock 대기로 깔린 것이다. 이것이 individual 요청별로 분산되면서 p99/p999를 살살 끌어올린다.&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;원리는 단순하다 &amp;mdash; lock을 잡지 못한 999개 워커가 2ms 폴링 주기로 대기한다. 첫 폴링에서 캐시가 채워졌으면 즉시 반환되지만, 폴링 타이밍에 따라 4ms, 6ms, ... 식으로 누적된다. 동시성이 늘수록 누적 lock 대기가 선형으로 증가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Pz7wj/dJMcaiiS8DZ/lcBIXArysDmtioQNlF08Hk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Pz7wj/dJMcaiiS8DZ/lcBIXArysDmtioQNlF08Hk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Pz7wj/dJMcaiiS8DZ/lcBIXArysDmtioQNlF08Hk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPz7wj%2FdJMcaiiS8DZ%2FlcBIXArysDmtioQNlF08Hk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;650&quot; height=&quot;360&quot; data-origin-width=&quot;650&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;위 그래프를 보면 distlock만 막대가 솟아오르고 나머지는 0이다. &lt;b&gt;이것이 분산 lock의 진짜 비용이다&lt;/b&gt; &amp;mdash; DB는 보호되지만 사용자 요청은 lock 대기 시간만큼 느려진다. 폴링 주기를 1ms로 줄이면 꼬리 지연은 줄지만 Redis 부하가 늘어나고, 5ms로 늘리면 부하는 줄지만 꼬리 지연이 더 커진다. 트레이드오프다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 무엇을 써야 하는가? 환경별 의사결정 트리&lt;/h2&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;b&gt;없다&lt;/b&gt;. 환경에 따라 정답이 갈린다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;운영 환경&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천 패턴&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단일 프로세스 (모놀리스)&lt;/td&gt;
&lt;td&gt;single-flight&lt;/td&gt;
&lt;td&gt;코드 변경 최소, 오버헤드 없음, p99 가장 깔끔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다중 인스턴스 (k8s 다수 pod)&lt;/td&gt;
&lt;td&gt;분산 lock (SETNX EX)&lt;/td&gt;
&lt;td&gt;프로세스 경계를 넘는 유일한 옵션, 꼬리 지연 비용 감수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;정상 분포 트래픽 (cliff 아님)&lt;/td&gt;
&lt;td&gt;XFetch&lt;/td&gt;
&lt;td&gt;만료 전 트래픽 흡수 효과 극대화, 부하 평탄화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인기 키 cliff + 멀티 인스턴스&lt;/td&gt;
&lt;td&gt;distlock + 인스턴스 내 single-flight&lt;/td&gt;
&lt;td&gt;두 층 결합 &amp;mdash; 인스턴스 안에서는 sf, 인스턴스 간에는 lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTL &amp;ge; 백엔드 응답시간&lt;/td&gt;
&lt;td&gt;그냥 naive로도 충분&lt;/td&gt;
&lt;td&gt;stampede 자체가 잘 일어나지 않는다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;2단계 전략&lt;/b&gt;을 많이 쓴다. 인스턴스 내부에서 single-flight으로 1차 dedupe &amp;rarr; 그래도 인스턴스마다 1건씩 나오는 호출은 분산 lock으로 2차 dedupe. 이렇게 하면 distlock의 lock_contention_ms도 인스턴스 수만큼만 누적되고 single-flight의 멀티 인스턴스 한계도 보완된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;캐시 stampede 패턴 적용 전 챙겨야 할 운영 디테일&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;lock TTL 길이&lt;/b&gt;: 너무 짧으면 백엔드 응답이 늦을 때 lock이 만료돼서 두 워커가 동시에 호출한다. 너무 길면 워커가 죽었을 때 다른 워커가 5초 동안 손가락을 빨고 기다린다. &lt;b&gt;백엔드 p99 + 여유분&lt;/b&gt; 정도가 적정하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;stale-while-revalidate&lt;/b&gt;: 만료된 캐시값을 잠시 내주면서 백그라운드로 갱신하는 패턴이다. cliff 자체를 우회하는 강력한 옵션이지만 stale 데이터가 허용 가능한 도메인에서만 쓸 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;베타값(XFetch)&lt;/b&gt;: 1.0이 표준이지만 트래픽 특성에 따라 0.5~2.0 범위에서 튜닝의 여지가 있다. 베타를 키우면 더 일찍 갱신해서 더 부드러워지지만 백엔드 호출이 늘어난다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;폴링 주기 vs pub/sub&lt;/b&gt;: 분산 lock 폴링 대신 lock 해제 시 Redis pub/sub으로 알리는 변형이 있다. 꼬리 지연은 줄지만 구현 복잡도 + Redis pub/sub 부하의 트레이드오프다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;circuit breaker&lt;/b&gt;: stampede 패턴들이 다 막혀도 백엔드가 진짜 죽으면 그 1건의 호출도 timeout을 누적시킨다. 외곽에 circuit breaker 한 겹을 더 두는 것이 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB 수렴은 다 되는데, 꼬리 지연이 갈림길이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 실험 조건(TTL 80ms, 백엔드 100ms, 동시성 1000, 10라운드)에서 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DB 호출 보호&lt;/b&gt;: single-flight, XFetch, distlock 셋 다 라운드당 1건으로 수렴한다. 차이가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;꼬리 지연(p99)&lt;/b&gt;: single-flight &amp;asymp; XFetch(101~102ms) &amp;lt; distlock(104~105ms).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;숨은 비용&lt;/b&gt;: distlock의 lock_contention_ms가 동시성 1000에서 누적 약 1,007초 &amp;mdash; single-flight&amp;middot;XFetch는 0이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;적용 환경 차이&lt;/b&gt;: 단일 프로세스라면 single-flight, 멀티 인스턴스라면 distlock, 정상 트래픽이라면 XFetch가 진가를 발휘한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;redis 캐시 stampede를 막는 첫 단계는 &quot;내가 처한 환경이 어디에 해당하는지&quot; 정확히 진단하는 것이다. 모놀리스이면서 distlock을 박는 것은 오버엔지니어링이고, 멀티 인스턴스인데 single-flight만 믿는 것은 진짜 위험하다. 본 실험의 숫자는 출발점일 뿐이고, 본인 서비스의 TTL/백엔드 응답시간 비율에 맞춰 베타값&amp;middot;폴링 주기&amp;middot;lock TTL을 다시 튜닝해봐야 진짜 결론이 나온다.&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;마지막으로 한 번 더 강조하면 &amp;mdash; &lt;b&gt;이 실험은 cliff 시나리오다&lt;/b&gt;. 정상 분포 트래픽에서는 XFetch가 더 빛날 수 있고, 백엔드가 200ms를 넘어가면 lock 폴링 누적이 훨씬 더 큰 비중을 차지한다. 본인 서비스의 부하 패턴을 한 번 캡처해서 동일 코드로 돌려보면 어느 패턴이 답인지 5분 안에 답이 나온다. 일반화하지 말고 직접 측정하는 것이 redis 캐시 stampede 대응의 진짜 첫 단계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;그런데 Key가 서로 다르다면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지의 비교는 동일한 키 하나에 1,000개의 동시 요청이 몰리는 cliff 시나리오를 가정한 결과이다. single-flight 는&amp;nbsp;&lt;b&gt;같은 키&lt;/b&gt;에&amp;nbsp;동시에&amp;nbsp;미스난&amp;nbsp;요청&amp;nbsp;N개를&amp;nbsp;1개로&amp;nbsp;합치는&amp;nbsp;도구이므로,&amp;nbsp;1,000개의&amp;nbsp;키가&amp;nbsp;모두&amp;nbsp;다른&amp;nbsp;경우&amp;nbsp;1,000번의&amp;nbsp;DB&amp;nbsp;호출은&amp;nbsp;그대로&amp;nbsp;발생한다.&amp;nbsp;즉&amp;nbsp;single-flight&amp;nbsp;가&amp;nbsp;책임지는&amp;nbsp;영역은&amp;nbsp;캐시&amp;nbsp;stampede&amp;nbsp;의&amp;nbsp;한&amp;nbsp;갈래일&amp;nbsp;뿐이며,&amp;nbsp;키가&amp;nbsp;분산되었음에도&amp;nbsp;백엔드가&amp;nbsp;동일한&amp;nbsp;압박을&amp;nbsp;받는&amp;nbsp;패턴은&amp;nbsp;별도의&amp;nbsp;도구로&amp;nbsp;다뤄야&amp;nbsp;한다. &lt;br /&gt;&lt;br /&gt;키가 다른데도 stampede 와 동일한 효과가 나타나는 가장 흔한 형태는 &lt;b&gt;동시 만료(snowstorm)&lt;/b&gt; 이다. 배포 시점에 1,000개 키를 한꺼번에 SET 하면 동일한 TTL 이 곱해져 한 시간 뒤 1,000개가 같은 순간에 만료된다. 키는 다르지만 타이밍이 같아 백엔드는 동일 키 stampede 와 구분되지 않는 압박을 받는다. 이 경우의 가장 저렴한 해결책은 &lt;b&gt;TTL 지터링&lt;/b&gt;이다. SET 시점에 TTL 에 &amp;plusmn;N% 의 난수 오프셋을 더해 만료 시각을 시간축 위에 분산시키면 한 시점에 몰리던 미스가 자연스럽게 평탄화된다. 코드 한 줄 변경으로 끝나는 작업이지만 효과는 본 실험의 어떤 패턴보다 직접적이다. &lt;br /&gt;&lt;br /&gt;XFetch 가 본래의 강점을 드러내는 영역도 여기다. 본 실험의 cliff 시나리오에서는 single-flight 와 사실상 동일하게 동작했지만, 정상 트래픽이 TTL 만료 이전부터 꾸준히 도달하는 환경에서는 일부 요청이 만료 직전에 확률적으로 사전 재계산을 트리거하므로 cliff 자체가 만들어지지 않는다. TTL 지터링이 *만료 시각*을 시간축에 흩뿌리는 정적 기법이라면, XFetch 는&amp;nbsp;&lt;b&gt;재계산 시각&lt;/b&gt;을&amp;nbsp;동적으로&amp;nbsp;흩뿌리는&amp;nbsp;기법이다. &lt;br /&gt;&lt;br /&gt;조금 더 넓게 보면 &lt;b&gt;Stale-While-Revalidate(SWR)&lt;/b&gt;가 있다. 만료 직전&amp;middot;직후의 짧은 grace 윈도 동안에는 stale 값을 즉시 반환하고, 백그라운드에서 단 한 명만(single-flight 적용) 재계산하도록 한다. 사용자는 항상 즉시 응답을 받으며 미스 경로 자체가 동기 호출이 되지 않는다. Cloudflare Workers, AWS CloudFront 같은 엣지 캐시가 기본으로 탑재한 패턴이며, 애플리케이션 캐시에서도 동일한 원리를 적용할 수 있다. &lt;br /&gt;&lt;br /&gt;마지막으로, 서로 다른 키 N개가 동시에 hot 해지는 &lt;b&gt;hot-set&lt;/b&gt; 패턴은 엄밀히 말하면 stampede 라기보다는 단순히 백엔드 호출량 자체가 많은 상황이다. 이 경우의 표준 도구는 &lt;b&gt;batch fetch&lt;/b&gt; 이다. 같은 tick 안에서 발생한 N개 키 조회를 `IN (id1, id2, ...)` 같은 단일 쿼리로 묶어 DB 왕복 횟수를 N 에서 1 로 줄인다. JavaScript 진영의 `dataloader`, Java 의 `org.dataloader` 같은 라이브러리가 이 패턴을 표준화해 두었으므로 GraphQL &amp;middot; REST 어느 형태의 백엔드든 그대로 적용할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;국내 IT 기업에서도 매번 다뤄지는 주제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;국내 IT 기업에서도 매번 다뤄지는 주제라서 한번쯤 읽어볼만하다.&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;&lt;a href=&quot;https://toss.tech/article/cache-traffic-tip&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.tech/article/cache-traffic-tip&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777884418683&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;캐시 문제 해결 가이드 - DB 과부하 방지 실전 팁&quot; data-og-description=&quot;대용량 트래픽 환경에서 캐시를 사용할 때 주의해야할 위험 상황과 예방법을 소개합니다.&quot; data-og-host=&quot;toss.tech&quot; data-og-source-url=&quot;https://toss.tech/article/cache-traffic-tip&quot; data-og-url=&quot;https://toss.tech/article/cache-traffic-tip&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/b0FN6g/dJMb8U8XlkX/GUnnOFLZCfC2Ljl5ObWji1/img.jpg?width=1500&amp;amp;height=767&amp;amp;face=0_0_1500_767,https://scrap.kakaocdn.net/dn/bnzWGv/dJMb8XR9tXK/ZKO6sNELacgYvxeldeWzrK/img.jpg?width=1500&amp;amp;height=767&amp;amp;face=0_0_1500_767,https://scrap.kakaocdn.net/dn/iHbIm/dJMb8Xki79n/2BxmF0qdS3UAg1e0Al5E81/img.png?width=1492&amp;amp;height=1243&amp;amp;face=0_0_1492_1243&quot;&gt;&lt;a href=&quot;https://toss.tech/article/cache-traffic-tip&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://toss.tech/article/cache-traffic-tip&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/b0FN6g/dJMb8U8XlkX/GUnnOFLZCfC2Ljl5ObWji1/img.jpg?width=1500&amp;amp;height=767&amp;amp;face=0_0_1500_767,https://scrap.kakaocdn.net/dn/bnzWGv/dJMb8XR9tXK/ZKO6sNELacgYvxeldeWzrK/img.jpg?width=1500&amp;amp;height=767&amp;amp;face=0_0_1500_767,https://scrap.kakaocdn.net/dn/iHbIm/dJMb8Xki79n/2BxmF0qdS3UAg1e0Al5E81/img.png?width=1492&amp;amp;height=1243&amp;amp;face=0_0_1492_1243');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;캐시 문제 해결 가이드 - DB 과부하 방지 실전 팁&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;대용량 트래픽 환경에서 캐시를 사용할 때 주의해야할 위험 상황과 예방법을 소개합니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;toss.tech&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;&lt;a href=&quot;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777884525304&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Distributed Cache 도입기 (1): Redis Pub/Sub을 이용한 인터페이스 설계&quot; data-og-description=&quot;채널톡의 분산 캐시 일관성 문제 해결법&quot; data-og-host=&quot;channel.io&quot; data-og-source-url=&quot;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&quot; data-og-url=&quot;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cNN4wT/dJMb86n1lnr/HWKQ79XRFM6yHKSi6gx6N1/img.jpg?width=1400&amp;amp;height=732&amp;amp;face=0_0_1400_732,https://scrap.kakaocdn.net/dn/vX1Ic/dJMb89yhleq/ZaSh86MhERrOzsjdRUS2mK/img.png?width=1212&amp;amp;height=1012&amp;amp;face=0_0_1212_1012,https://scrap.kakaocdn.net/dn/dzvHrB/dJMb9frJaY5/QrOQOcgzilVOBwbNbKxKHk/img.png?width=1276&amp;amp;height=906&amp;amp;face=0_0_1276_906&quot;&gt;&lt;a href=&quot;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://channel.io/ko/team/blog/articles/tech-distributed-cache-1-67a392c5&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cNN4wT/dJMb86n1lnr/HWKQ79XRFM6yHKSi6gx6N1/img.jpg?width=1400&amp;amp;height=732&amp;amp;face=0_0_1400_732,https://scrap.kakaocdn.net/dn/vX1Ic/dJMb89yhleq/ZaSh86MhERrOzsjdRUS2mK/img.png?width=1212&amp;amp;height=1012&amp;amp;face=0_0_1212_1012,https://scrap.kakaocdn.net/dn/dzvHrB/dJMb9frJaY5/QrOQOcgzilVOBwbNbKxKHk/img.png?width=1276&amp;amp;height=906&amp;amp;face=0_0_1276_906');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Distributed Cache 도입기 (1): Redis Pub/Sub을 이용한 인터페이스 설계&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;채널톡의 분산 캐시 일관성 문제 해결법&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;channel.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <category>single flight 패턴</category>
      <category>thundering herd 해결</category>
      <category>XFetch probabilistic early expiration</category>
      <category>분산 lock SETNX 캐시</category>
      <category>캐시 stampede 방지</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/414</guid>
      <comments>https://yscho03.tistory.com/414#entry414comment</comments>
      <pubDate>Mon, 4 May 2026 16:47:35 +0900</pubDate>
    </item>
    <item>
      <title>PgBouncer를 정말 무조건 써야 하는가 &amp;mdash; 직접 연결과 session/transaction/statement 3가지 모드 직접 비교</title>
      <link>https://yscho03.tistory.com/413</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL을 운영하는 사람들이 거의 자동으로 설치하고 가는 것이 PgBouncer다. 다만 &quot;PgBouncer 모드 비교&quot;라고 검색해 보면 대부분 공식문서 번역이거나 &quot;transaction 모드를 무조건 켜라&quot;는 통념의 반복이다. 정말 맞는 말인지 의심스러워, 백엔드 슬롯 20개를 고정하고 클라이언트를 40개&amp;middot;200개로 스윕하면서 직접 연결과 session/transaction/statement 3가지 풀링 모드를 한 번에 측정했다. 결론부터 말하면 &quot;무조건 정답&quot;은 없으며, &lt;b&gt;트래픽 패턴과 세션 기능 의존도에 따라 답이 완전히 달라진다&lt;/b&gt;. 이 글에서는 실측 TPS&amp;middot;p99 지연&amp;middot;거절률&amp;middot;hop 오버헤드&amp;middot;세션 기능 호환성 데이터를 그대로 제시하고, 어떤 워크로드에 어떤 모드가 맞는지 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh1w5Q/dJMcagZEDH6/6nFfmpLB9R7y8CnKTIuDr1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh1w5Q/dJMcagZEDH6/6nFfmpLB9R7y8CnKTIuDr1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh1w5Q/dJMcagZEDH6/6nFfmpLB9R7y8CnKTIuDr1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh1w5Q%2FdJMcagZEDH6%2F6nFfmpLB9R7y8CnKTIuDr1%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;985&quot; height=&quot;489&quot; data-origin-width=&quot;985&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://scalegrid.io/blog/postgresql-connection-pooling-part-2-pgbouncer/&quot;&gt;scalegrid.io&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PgBouncer 통념부터 먼저 정리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PgBouncer 관련 글을 보면 당연한 것처럼 전제하고 가는 통념이 몇 가지 있다. 우선 이것부터 깨고 시작해야 데이터를 제대로 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;session 모드는 다중화(multiplexing)가 없다&quot;는 정말인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반은 맞고 반은 틀리다. session 모드가 클라이언트의 연결이 끊길 때까지 백엔드 슬롯을 점유하는 것은 사실이다. 다만 이것이 의미하는 바는 &quot;동일한 동시성 천장(클라이언트 수가 백엔드 슬롯을 넘지 못함)&quot;이지, 다중화가 0이라는 뜻은 아니다. &lt;b&gt;연결 폭풍(connection storm)을 흡수해 거절률을 0으로 만드는 효과&lt;/b&gt;는 그대로 존재한다. 클라이언트가 연결을 끊었다가 다시 붙는 패턴에서는 백엔드 슬롯의 재사용이 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;transaction 모드가 사실상 표준&quot;이라는 말에 깔린 가정은 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 통념은 두 가지 가정 위에 서 있다. 첫째, 트랜잭션이 짧아야 한다. 둘째, 세션 기능에 의존하지 않아야 한다. 두 가정이 모두 깨지면 transaction 풀링은 오히려 함정이다. 트랜잭션이 길면 multiplexing의 이득이 사라지며, prepared statement나 advisory lock을 쓰면 그대로 깨진다. 무조건 표준이라 할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;statement 모드는 너무 엄격해 쓰지 않는다&quot;는 말, 정확히 무엇이 안 되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement 모드는 매 statement마다 백엔드 슬롯을 회수한다. 그래서 트랜잭션 자체가 깨진다. BEGIN-COMMIT 묶음이 같은 슬롯에서 처리된다는 보장이 없다. PgBouncer 공식문서가 명시한 타깃은 PL/Proxy이며, 그 외에는 극단적인 read-only burst 워크로드에서나 가끔 보이는 정도이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 환경 &amp;mdash; 백엔드 20슬롯, 클라이언트 40&amp;middot;200 스윕&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 지표 4종&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리량(TPS), p50/p99 지연(ms), 거절 연결 수(refused), 트랜잭션당 누적 hop 오버헤드(ms)이다. 여기에 더해 세션 기능 3종(prepared statement, advisory lock, 세션 SET)의 호환성도 별도로 점검했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오별 모형 파라미터&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;파라미터&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;backend_slots&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;stmts_per_txn&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;txns_per_session&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;query_us&lt;/td&gt;
&lt;td&gt;300 &amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hop_us&lt;/td&gt;
&lt;td&gt;25 &amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;think_us&lt;/td&gt;
&lt;td&gt;800 &amp;mu;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;client_sweep&lt;/td&gt;
&lt;td&gt;40, 200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이 숫자들이 현실적인 부하 모델인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement 처리 시간 300&amp;mu;s는 인덱스를 잘 탄 단순 SELECT의 평균값이다. hop 25&amp;mu;s는 PgBouncer가 동일 호스트의 unix socket으로 붙어 있을 때 흔히 나오는 수치이다. think time 800&amp;mu;s는 애플리케이션이 결과를 받고 다음 쿼리를 만들기까지 걸리는 시간으로, 짧은 트랜잭션 워크로드의 보편적 모델이다. 백엔드 20슬롯은 &lt;b&gt;클라이언트 &amp;lt; 백엔드(40개)&lt;/b&gt;와 &lt;b&gt;클라이언트 &amp;gt; 백엔드(200개)&lt;/b&gt; 양쪽 시나리오를 만들기 위한 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 연결과 PgBouncer 4모드 처리량 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 핵심이다. 4가지 모드를 동일 백엔드&amp;middot;동일 부하로 돌린 결과를 표로 정리했다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;클라이언트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;ok_txns&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conn_refused&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;wall_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;TPS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99_ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;hop_ms/txn&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;27.368&lt;/td&gt;
&lt;td&gt;2,192.3&lt;/td&gt;
&lt;td&gt;7.66&lt;/td&gt;
&lt;td&gt;7.75&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;56.975&lt;/td&gt;
&lt;td&gt;2,106.2&lt;/td&gt;
&lt;td&gt;7.45&lt;/td&gt;
&lt;td&gt;9.09&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transaction&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;53.461&lt;/td&gt;
&lt;td&gt;2,244.6&lt;/td&gt;
&lt;td&gt;15.41&lt;/td&gt;
&lt;td&gt;22.00&lt;/td&gt;
&lt;td&gt;0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;statement&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;27.237&lt;/td&gt;
&lt;td&gt;4,405.6&lt;/td&gt;
&lt;td&gt;7.83&lt;/td&gt;
&lt;td&gt;10.19&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;27.551&lt;/td&gt;
&lt;td&gt;2,177.7&lt;/td&gt;
&lt;td&gt;7.72&lt;/td&gt;
&lt;td&gt;8.04&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;301.810&lt;/td&gt;
&lt;td&gt;1,988.0&lt;/td&gt;
&lt;td&gt;8.24&lt;/td&gt;
&lt;td&gt;12.01&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transaction&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;235.620&lt;/td&gt;
&lt;td&gt;2,546.5&lt;/td&gt;
&lt;td&gt;76.14&lt;/td&gt;
&lt;td&gt;79.85&lt;/td&gt;
&lt;td&gt;0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;statement&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;121.495&lt;/td&gt;
&lt;td&gt;4,938.5&lt;/td&gt;
&lt;td&gt;34.67&lt;/td&gt;
&lt;td&gt;52.65&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 &amp;lt; 백엔드 슬롯 (40 클라이언트) &amp;mdash; 직접 연결도 충분하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;40 클라이언트 케이스부터 보면 다소 의외이다. direct 연결이 TPS 2,192를 기록하는데, 이는 session 모드(2,106)보다 미세하게 빠르다. 이유는 단순하다. &lt;b&gt;클라이언트 수가 백엔드 슬롯보다 적다면 풀러 hop은 그저 손해&lt;/b&gt;다. direct는 hop 오버헤드가 0ms이고, session은 트랜잭션당 0.3ms를 추가로 소모한다. 다만 direct 역시 60건만 처리하고 20건은 거절되었는데(클라이언트 40 vs 슬롯 20), 풀러를 끼지 않으면 burst 흡수가 되지 않는다는 뜻이다. 이것이 &lt;b&gt;session 모드의 진짜 가치, 즉 multiplexing이 아닌 connection storm 흡수&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 &amp;gt; 백엔드 슬롯 (200 클라이언트) &amp;mdash; 거절률이 폭발한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200 클라이언트 시나리오가 진짜 갈리는 구간이다. direct는 60건만 처리하고 &lt;b&gt;180건이 거절된다&lt;/b&gt;. 풀러가 없다면 max_connections를 넘는 순간 그대로 끝이다. 반면 session/transaction/statement 모드는 모두 거절 0건이다. 풀러가 connection storm을 흡수해 버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;transaction 모드가 throughput을 2배 끌어올리는 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;200 클라이언트에서 transaction 모드의 TPS는 2,546으로 session(1,988) 대비 28% 더 높다. statement 모드는 4,938로 session 대비 약 2.5배이다. 짧은 트랜잭션에서 클라이언트 think time(800&amp;mu;s) 동안 백엔드를 다른 트랜잭션에 넘겨주는 다중화가 throughput을 직접 끌어올리는 것이다. &lt;b&gt;다만 함정이 있다.&lt;/b&gt; transaction 모드의 p99 지연이 79.85ms로 session(12ms) 대비 6배 이상 튄다. 큐잉이 발생하는 것이다. throughput은 벌었지만 꼬리 지연(tail latency)을 잃은 셈이다.&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;stdout 원문에도 이 트레이드오프가 그대로 드러나 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777859186791&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[Throughput sweep]
mode         clients  ok_txns   refused   wall_ms   tps       p50_ms   p99_ms   hop_ms
direct       200      60        180       27.6      2177.7    7.72     8.04     0.00
session      200      600       0         301.8     1988.0    8.24     12.01    0.30
transaction  200      600       0         235.6     2546.5    76.14    79.85    0.35
statement    200      600       0         121.5     4938.5    34.67    52.65    0.30
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;hop 오버헤드는 무시해도 되는 수준인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PgBouncer 관련 글이 자주 얼버무리는 부분이 hop 비용이다. &quot;거의 무시할 만하다&quot;로 넘기는 경우가 많지만, 짧은 statement 워크로드에서는 의외로 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션당 누적 hop 시간 계산&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 보면 session/statement 모드는 트랜잭션당 0.30ms, transaction 모드는 0.35ms의 hop 오버헤드를 소모한다. 25&amp;mu;s hop이 statement 6회씩 트랜잭션마다 들어가므로 누적되는 것이다. statement당 백엔드 처리 시간이 300&amp;mu;s인데, 여기에 25&amp;mu;s hop이 붙으면 &lt;b&gt;per-statement 오버헤드가 약 8% 추가&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;짧은 statement에서는 hop 비중이 의외로 크다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OLTP 워크로드처럼 인덱스를 잘 탄 1ms 미만 쿼리가 대다수라면 hop 비중은 5~10% 수준이다. 무시할 수 없는 숫자이다. 반대로 백엔드 처리 시간이 50ms를 넘는 분석성 쿼리에서는 hop 25&amp;mu;s가 그야말로 노이즈 수준이라 의미가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 연결로 빠지는 편이 나은 워크로드의 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PgBouncer 성능 데이터를 종합하면 다음 조건을 모두 만족할 때 &lt;b&gt;풀러를 끼지 않는 편이 더 빠르다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 클라이언트 수 &amp;lt; max_connections&lt;/li&gt;
&lt;li&gt;워커가 connection lifetime을 길게 유지(connection storm 없음)&lt;/li&gt;
&lt;li&gt;statement당 백엔드 처리 시간이 짧음(100~500&amp;mu;s)&lt;/li&gt;
&lt;li&gt;세션 기능(prepared statement 등) 적극 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 마이크로서비스 간 통신처럼 클라이언트 수가 통제되는 환경이 여기에 해당한다. 외부 트래픽을 받는 API 게이트웨이 뒤편이라면 그냥 풀러를 깔아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세션 기능 호환성 &amp;mdash; 여기서 모드가 갈린다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리량이 전부가 아니다. 이 부분이 PgBouncer 모드 선택의 진짜 분기점이다. transaction과 statement 모드는 &lt;b&gt;백엔드 연결을 트랜잭션&amp;middot;statement 경계에서 회수&lt;/b&gt;하므로, 세션 상태가 통째로 깨진다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;prepared_stmt&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;advisory_lock&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;session_setting&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;session&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;td&gt;OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transaction&lt;/td&gt;
&lt;td&gt;BROKEN&lt;/td&gt;
&lt;td&gt;BROKEN&lt;/td&gt;
&lt;td&gt;BROKEN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;statement&lt;/td&gt;
&lt;td&gt;BROKEN&lt;/td&gt;
&lt;td&gt;BROKEN&lt;/td&gt;
&lt;td&gt;BROKEN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;prepared statement &amp;mdash; transaction/statement 모드에서 깨지지만 우회법은 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 측 prepared statement는 백엔드 세션에 캐시되는데, transaction 모드에서는 다음 트랜잭션이 다른 백엔드 슬롯에 붙을 수 있어 캐시가 날아간다. &lt;b&gt;우회법은 두 가지&lt;/b&gt;다. 첫째, 클라이언트 측 prepared statement 캐시를 끄는 것이다(예: JDBC prepareThreshold=0). 둘째, PgBouncer 1.21부터 지원되는 protocol-level prepared statements 옵션을 켜는 것이다. 후자가 더 깔끔하지만 클라이언트 드라이버 호환성 확인은 필수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;advisory lock &amp;mdash; session 모드를 강제한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_advisory_lock은 세션이 살아 있는 동안에만 유효하다. transaction 모드에서는 트랜잭션이 끝나는 순간 백엔드가 풀에 반환되면서 락이 사라진다. &lt;b&gt;분산 잠금을 advisory lock으로 구현했다면 session 모드 외에는 답이 없다&lt;/b&gt;. 굳이 transaction 풀링으로 가고 싶다면 pg_advisory_xact_lock(트랜잭션 스코프) 변형으로 갈아타야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세션 단위 SET (search_path, timezone 등) 동작 매트릭스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SET search_path TO myschema 같은 세션 변수도 transaction 모드에서는 다음 트랜잭션에 보장되지 않는다. 멀티테넌시 SaaS에서 스키마 분리를 search_path에 의존했다면 그대로는 transaction 모드로 갈 수 없다. SET LOCAL로 트랜잭션 스코프로 바꾸거나, 매 트랜잭션 시작 시 search_path를 명시적으로 설정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 어떤 모드를 쓸 것인가 &amp;mdash; 의사결정 트리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 데이터를 모두 보았다면 이제 자기 워크로드에 매핑할 수 있다. session과 transaction 사이에서 고민하는 사람들 대부분이 이 트리를 그려 두지 않고 시작해 헛수고를 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 &amp;lt; 백엔드 슬롯 + connection storm 없음 &amp;rarr; 직접 연결 또는 session&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내부 서비스 간 통신, 워커 풀 사이즈가 통제됨&lt;/li&gt;
&lt;li&gt;워크로드 특성상 connection lifetime이 길게 유지됨&lt;/li&gt;
&lt;li&gt;&amp;rarr; 직접 연결로 hop을 절약하거나, 안전망으로 session 모드를 채택한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트 &amp;gt; 백엔드 슬롯 + 세션 기능 미사용 &amp;rarr; transaction&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;외부 API 트래픽이나 서버리스 함수처럼 클라이언트 수가 폭발한다&lt;/li&gt;
&lt;li&gt;prepared statement&amp;middot;advisory lock&amp;middot;세션 SET 의존이 0이다&lt;/li&gt;
&lt;li&gt;&amp;rarr; transaction 풀링이 throughput과 거절률을 동시에 해결한다&lt;/li&gt;
&lt;li&gt;단, p99 지연이 튀는 부분은 반드시 모니터링해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;prepared statement 의존 + transaction 사용 희망 &amp;rarr; 클라이언트 측 캐시 비활성화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트 prepared cache를 끄거나 protocol-level prepared 옵션을 활성화한다&lt;/li&gt;
&lt;li&gt;ORM이라면 prepareThreshold=0 같은 옵션을 찾아본다&lt;/li&gt;
&lt;li&gt;트레이드오프: prepared의 이점 일부를 포기하고 transaction 풀링의 throughput 이득을 취한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;advisory lock + 분산 락 필요 &amp;rarr; session 강제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;session 모드 외에는 답이 없다&lt;/li&gt;
&lt;li&gt;또는 PostgreSQL 락 대신 Redis Redlock 같은 외부 락으로의 전환을 검토한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;statement 모드를 정말 써야 하는 희귀 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;statement 모드는 거의 사용하지 않는다. PgBouncer 공식문서가 지목하는 PL/Proxy 호환성을 맞추거나, 트랜잭션 자체가 없는 read-only burst 워크로드(예: 캐시 무효화용 단발 SELECT 폭주)에서나 가끔 보인다. 일반 OLTP에서는 트랜잭션이 깨지므로 답이 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 &amp;mdash; PgBouncer 모드 선택은 통념이 아닌 트레이드오프이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PgBouncer 모드 비교 결과를 4가지로 압축하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&quot;무조건 transaction&quot;은 거짓말이다.&lt;/b&gt; 짧은 트랜잭션과 세션 기능 미사용 조건일 때만 throughput에서 압승이며, 세션 기능을 쓰면 그대로 깨진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트가 백엔드보다 적으면 직접 연결도 무방하다.&lt;/b&gt; 풀러 hop이 손해이므로 굳이 끼지 않는 편이 빠르다. 다만 burst 흡수는 되지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;세션 기능 의존도가 모드를 강제한다.&lt;/b&gt; prepared statement&amp;middot;advisory lock&amp;middot;세션 SET 셋 모두 transaction/statement 모드에서 깨진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;statement 모드는 거의 사용하지 않는다.&lt;/b&gt; PL/Proxy 호환과 극단적 burst에 한정된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인 환경에서 동일하게 4모드를 돌려 보는 것을 강력히 권한다. 클라이언트 수, 백엔드 슬롯, 트랜잭션 길이, 세션 기능 의존도 &amp;mdash; 이 네 변수만 자신의 숫자로 맞춰 시뮬레이션을 돌려 보면, 어떤 PgBouncer 모드가 자기 워크로드에 맞는지 30분 안에 답이 나온다. &quot;남들이 transaction을 쓰니 나도 transaction&quot; 식으로 가지 말고, 데이터로 결정해야 한다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>pgbouncer session vs transaction</category>
      <category>pgbouncer statement mode</category>
      <category>pgbouncer transaction pooling</category>
      <category>pgbouncer 성능</category>
      <category>postgres connection pooler 추천</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/413</guid>
      <comments>https://yscho03.tistory.com/413#entry413comment</comments>
      <pubDate>Mon, 4 May 2026 10:46:44 +0900</pubDate>
    </item>
    <item>
      <title>HTTP Keep-Alive는 정말 효과가 있는가? 1000회 호출로 직접 비교한다</title>
      <link>https://yscho03.tistory.com/412</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP Keep-Alive를 켜라는 글은 도처에 널려 있다. 다만 정작 &quot;얼마나 차이가 나는지&quot; 한국어로 수치를 제시한 자료는 거의 없다. 이론 글만 읽다가 답답해서 직접 실행했다. 평문 HTTP, HTTPS, mTLS 세 시나리오 &amp;times; fresh/keepalive 두 모드 &amp;times; 동시성 1/8/32 = &lt;b&gt;18케이스 매트릭스&lt;/b&gt;로 1000회씩 호출했고, 결론부터 말하면 mTLS에서 keepalive를 사용하지 않으면 치명적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ecjhkE/dJMcahddvL3/KtswXPdJSYymTkymUJM7P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ecjhkE/dJMcahddvL3/KtswXPdJSYymTkymUJM7P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ecjhkE/dJMcahddvL3/KtswXPdJSYymTkymUJM7P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FecjhkE%2FdJMcahddvL3%2FKtswXPdJSYymTkymUJM7P0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1138&quot; height=&quot;694&quot; data-origin-width=&quot;1138&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론부터 제시한다&lt;/h2&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;b&gt;conc=1 기준 keepalive가 fresh 대비 RPS 비율은 평문 HTTP가 2.80배, HTTPS가 7.65배, mTLS가 9.60배다.&lt;/b&gt; 시나리오가 무거워질수록 keep-alive의 이득은 기하급수로 벌어진다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;fresh RPS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;keepalive RPS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;keepalive/fresh&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;2,504.8&lt;/td&gt;
&lt;td&gt;7,000.5&lt;/td&gt;
&lt;td&gt;**2.80&amp;times;**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;https&lt;/td&gt;
&lt;td&gt;787.6&lt;/td&gt;
&lt;td&gt;6,025.4&lt;/td&gt;
&lt;td&gt;**7.65&amp;times;**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mtls&lt;/td&gt;
&lt;td&gt;538.7&lt;/td&gt;
&lt;td&gt;5,172.1&lt;/td&gt;
&lt;td&gt;**9.60&amp;times;**&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mTLS fresh는 conc=32에서 &lt;b&gt;RPS가 1,053&lt;/b&gt;까지 떨어진다. 동일한 mTLS에서 keepalive를 켜면 conc=8에서 27,285 RPS를 기록한다. 25배가 넘는 차이다. 이것이 핵심이다. HTTP Keep-Alive는 평문에서도 의미가 있고, TLS가 들어가는 순간 필수가 되며, mTLS라면 켜지 않는 것 자체가 잘못이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;측정을 시작한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mTLS를 운영하다가 RPS가 도무지 나오지 않아 의심하기 시작했다. &quot;TLS handshake가 비싸다&quot;는 말은 모두가 하지만, 정확히 얼마나 비싼지, 클라이언트 인증서까지 더해지면 또 얼마나 더 비싼지 한국어로 깔끔하게 정리된 데이터가 없었다. 영어권에서는 Cloudflare 블로그나 Stripe 엔지니어링 글이 잘 정리되어 있는 반면, 한국어 자료는 입문 수준에서 멈춰 있다.&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;각설하고, 변수를 모두 고정하고 핸드셰이크 비용만 분리해서 측정하기로 했다. 동일 컨테이너 내 loopback이므로 네트워크 RTT 변수는 제거되고, HTTP/1.1을 강제하여 HTTP/2 multiplexing 효과도 분리된다. 순수하게 &lt;b&gt;TCP 3-way + TLS handshake + 인증서 검증&lt;/b&gt; 비용만 보는 셋업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핸드셰이크 비용이 누적되는 메커니즘&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TCP 3-way handshake 비용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평문 HTTP라도 매번 새 connection을 열면 SYN &amp;rarr; SYN-ACK &amp;rarr; ACK 왕복이 한 번 발생한다. loopback이라 RTT 자체는 작지만, conc가 올라가면 ephemeral port를 할당하고 해제하는 비용이 누적된다. fresh 모드에서 800 요청에 accept 카운트가 정확히 800인 점이 그 증거다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TLS 1.2 / 1.3 handshake가 더해지는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS 1.2는 2 RTT, TLS 1.3은 1 RTT가 추가된다. 더 무거운 부분은 RTT 자체가 아니라 &lt;b&gt;인증서 파싱과 키 교환 CPU&lt;/b&gt;다. fresh 모드 https는 동일한 conc=1에서도 평문 대비 RPS가 1/3로 떨어진다(2,504 &amp;rarr; 787).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;mTLS 클라이언트 인증서 검증까지 포함되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mTLS는 서버가 클라이언트 인증서까지 검증한다. 양측 모두 인증서 파싱 + 체인 검증 + 서명 검증이 발생한다. fresh 모드 mtls의 conc=32에서 &lt;b&gt;CPU user+sys가 3.62초&lt;/b&gt;를 기록한다. 동일한 800 요청을 keepalive로 처리하면 0.387초다. CPU만 9배 가까운 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 환경 &amp;mdash; 변수를 모두 고정하고 핸드셰이크만 측정한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실험 ID&lt;/td&gt;
&lt;td&gt;http-keepalive-effect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;런타임&lt;/td&gt;
&lt;td&gt;golang:1.22-bookworm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리&lt;/td&gt;
&lt;td&gt;512 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크&lt;/td&gt;
&lt;td&gt;deny (loopback only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;타임아웃&lt;/td&gt;
&lt;td&gt;300 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실행 시각&lt;/td&gt;
&lt;td&gt;2026-05-04T01:16:21 &amp;rarr; 2026-05-04T01:16:43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;총 실행 시간&lt;/td&gt;
&lt;td&gt;22.16s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;td&gt;PASS &amp;middot; exit 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;산출물&lt;/td&gt;
&lt;td&gt;result.json (8282B)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단일 컨테이너 loopback&lt;/b&gt;: 네트워크 RTT 변수 제거. 순수 핸드셰이크 비용만 본다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3 시나리오&lt;/b&gt;: http(평문), https(서버 TLS), mtls(상호 TLS).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2 모드&lt;/b&gt;: fresh(Transport.DisableKeepAlives=true)와 keepalive(pooled connection).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;3 동시성&lt;/b&gt;: conc=1, 8, 32.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;케이스당 800 요청 + 워밍업 50&lt;/b&gt;: 워밍업 노이즈 제거.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP/1.1 강제&lt;/b&gt;: HTTP/2 connection coalescing 효과와 분리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go net/http 클라이언트의 핵심 부분만 발췌하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777857975104&quot; class=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;// fresh 모드
tr := &amp;amp;http.Transport{
    DisableKeepAlives: true,
    TLSClientConfig:   tlsCfg,
    ForceAttemptHTTP2: false,
}

// keepalive 모드
tr := &amp;amp;http.Transport{
    MaxIdleConns:        100,
    MaxIdleConnsPerHost: 100,
    IdleConnTimeout:     90 * time.Second,
    TLSClientConfig:     tlsCfg,
    ForceAttemptHTTP2:   false,
}
&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;DisableKeepAlives=true 이 한 줄이 매 요청마다 새 connection을 열게 만드는 스위치다. 실서비스에서 모르고 켜 두면 그대로 무너진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;평문 HTTP 결과 &amp;mdash; 차이는 존재하지만 예상보다 작다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평문 HTTP는 TLS가 없으므로 TCP handshake만 절약하면 끝이다. 차이가 작을 것이라 예상했지만, 실측해 보니 conc=1에서도 2.8배의 차이가 나타났다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;mode&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;rps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99.9 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;accepts&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;2,504.8&lt;/td&gt;
&lt;td&gt;0.370&lt;/td&gt;
&lt;td&gt;0.731&lt;/td&gt;
&lt;td&gt;1.110&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;13,633.2&lt;/td&gt;
&lt;td&gt;0.386&lt;/td&gt;
&lt;td&gt;3.241&lt;/td&gt;
&lt;td&gt;4.442&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;7,314.9&lt;/td&gt;
&lt;td&gt;2.122&lt;/td&gt;
&lt;td&gt;**45.863**&lt;/td&gt;
&lt;td&gt;46.799&lt;/td&gt;
&lt;td&gt;800&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7,000.5&lt;/td&gt;
&lt;td&gt;0.127&lt;/td&gt;
&lt;td&gt;0.327&lt;/td&gt;
&lt;td&gt;0.668&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;56,862.6&lt;/td&gt;
&lt;td&gt;0.089&lt;/td&gt;
&lt;td&gt;1.348&lt;/td&gt;
&lt;td&gt;1.594&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;46,648.0&lt;/td&gt;
&lt;td&gt;0.370&lt;/td&gt;
&lt;td&gt;3.302&lt;/td&gt;
&lt;td&gt;3.864&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&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;conc=32 fresh의 p99 폭주&lt;/b&gt;: 평균은 4.27ms 수준으로 보이지만 p99에서 45.8ms로 튄다. ephemeral port 회전과 backlog queue 때문이다. 평균만 보면 절대로 잡아낼 수 없는 꼬리지연이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;accepts 카운트가 결정적 증거&lt;/b&gt;: fresh는 모든 conc에서 800(=요청수)이다. keepalive는 1/8/32 &amp;rarr; 0/8/39로 동시성 수준에 수렴한다. connection이 풀에서 재사용되고 있다는 직접적인 증거다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평문에서도 conc가 올라가면 fresh는 &lt;b&gt;TIME_WAIT 누적으로 인한 ephemeral port 고갈&lt;/b&gt; 위험까지 떠안는다. loopback이라 터지지 않았을 뿐, 실서비스에서는 실제로 사고가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTPS 결과 &amp;mdash; 여기서부터 차이가 명확해진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS handshake가 들어가면 fresh 비용이 본격적으로 폭발한다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;mode&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;rps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99.9 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cpu (user+sys, s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;787.6&lt;/td&gt;
&lt;td&gt;1.193&lt;/td&gt;
&lt;td&gt;2.244&lt;/td&gt;
&lt;td&gt;2.600&lt;/td&gt;
&lt;td&gt;1.214&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;2,071.6&lt;/td&gt;
&lt;td&gt;3.407&lt;/td&gt;
&lt;td&gt;15.730&lt;/td&gt;
&lt;td&gt;25.888&lt;/td&gt;
&lt;td&gt;2.042&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;2,071.6&lt;/td&gt;
&lt;td&gt;13.842&lt;/td&gt;
&lt;td&gt;38.460&lt;/td&gt;
&lt;td&gt;43.258&lt;/td&gt;
&lt;td&gt;2.103&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;6,025.4&lt;/td&gt;
&lt;td&gt;0.155&lt;/td&gt;
&lt;td&gt;0.378&lt;/td&gt;
&lt;td&gt;0.835&lt;/td&gt;
&lt;td&gt;0.192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;26,831.7&lt;/td&gt;
&lt;td&gt;0.128&lt;/td&gt;
&lt;td&gt;3.535&lt;/td&gt;
&lt;td&gt;6.915&lt;/td&gt;
&lt;td&gt;0.176&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;9,106.1&lt;/td&gt;
&lt;td&gt;0.585&lt;/td&gt;
&lt;td&gt;24.448&lt;/td&gt;
&lt;td&gt;32.222&lt;/td&gt;
&lt;td&gt;0.469&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 conc=1에서 fresh 787 RPS &amp;rarr; keepalive 6,025 RPS, 무려 &lt;b&gt;7.65배&lt;/b&gt;다. p50도 1.193ms &amp;rarr; 0.155ms로 8배 단축된다. handshake 한 번이 평문 대비 얼마나 무거운지 그대로 드러난다.&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;p99.9 꼬리지연을 보면 fresh conc=8이 25.8ms를 기록하는데, keepalive conc=8은 6.9ms다. 평균 지연만 보면 둘 다 한 자릿수 ms처럼 보여 &quot;비슷한가?&quot; 싶지만 꼬리에서 4배의 차이가 난다. &lt;b&gt;평균만 보면 속는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;mTLS 결과 &amp;mdash; keepalive를 사용하지 않으면 치명적이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 진정한 하이라이트다. mTLS는 서버 인증서 + 클라이언트 인증서 양쪽 모두를 검증해야 한다. 매 요청마다 인증서 체인 파싱과 서명 검증이 풀로 돌아간다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;mode&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;conc&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;rps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99.9 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cpu (user+sys, s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;538.7&lt;/td&gt;
&lt;td&gt;1.749&lt;/td&gt;
&lt;td&gt;3.419&lt;/td&gt;
&lt;td&gt;4.505&lt;/td&gt;
&lt;td&gt;2.004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1,804.8&lt;/td&gt;
&lt;td&gt;4.018&lt;/td&gt;
&lt;td&gt;10.616&lt;/td&gt;
&lt;td&gt;12.248&lt;/td&gt;
&lt;td&gt;2.468&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fresh&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;**1,053.0**&lt;/td&gt;
&lt;td&gt;26.320&lt;/td&gt;
&lt;td&gt;85.840&lt;/td&gt;
&lt;td&gt;**130.521**&lt;/td&gt;
&lt;td&gt;**3.617**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;5,172.1&lt;/td&gt;
&lt;td&gt;0.186&lt;/td&gt;
&lt;td&gt;0.367&lt;/td&gt;
&lt;td&gt;0.600&lt;/td&gt;
&lt;td&gt;0.226&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;27,285.5&lt;/td&gt;
&lt;td&gt;0.121&lt;/td&gt;
&lt;td&gt;3.216&lt;/td&gt;
&lt;td&gt;5.191&lt;/td&gt;
&lt;td&gt;0.181&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keepalive&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;11,469.9&lt;/td&gt;
&lt;td&gt;0.567&lt;/td&gt;
&lt;td&gt;28.485&lt;/td&gt;
&lt;td&gt;37.941&lt;/td&gt;
&lt;td&gt;0.387&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mtls fresh conc=32의 RPS가 &lt;b&gt;1,053&lt;/b&gt;이다. 동일 인프라에서 keepalive로 가면 11,469다. &lt;b&gt;약 11배의 차이&lt;/b&gt;다. 그리고 p99.9가 130.5ms다. 응답 시간이 100ms를 넘는 사용자가 0.1% 발생한다는 의미인데, 트래픽이 1만 RPS라면 매초 10명씩 100ms를 넘는 응답을 받는다.&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;stderr에는 다음과 같은 로그가 다수 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777857975108&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;2026/05/03 16:16:40 http: TLS handshake error from 127.0.0.1:45426: EOF
2026/05/03 16:16:40 http: TLS handshake error from 127.0.0.1:45518: EOF
2026/05/03 16:16:40 http: TLS handshake error from 127.0.0.1:45534: read: connection reset by peer
&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;fresh 모드에서 동시성이 32까지 올라가면서 서버 backlog가 넘쳐 TLS handshake 도중 끊기는 현상이다. errors 카운트는 0이지만, 서버 로그에는 핸드셰이크 실패가 연달아 기록된다. 실서비스라면 클라이언트 retry 폭주로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Keep-Alive는 CPU 비용도 함께 절감한다&lt;/h2&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;b&gt;CPU도 함께 소진된다&lt;/b&gt;. conc=8, 800 요청 처리분 기준 CPU 누적 비교다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;fresh CPU (s)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;keepalive CPU (s)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;절감 비율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;http&lt;/td&gt;
&lt;td&gt;0.343&lt;/td&gt;
&lt;td&gt;0.091&lt;/td&gt;
&lt;td&gt;**3.77&amp;times;** 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;https&lt;/td&gt;
&lt;td&gt;2.042&lt;/td&gt;
&lt;td&gt;0.176&lt;/td&gt;
&lt;td&gt;**11.6&amp;times;** 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mtls&lt;/td&gt;
&lt;td&gt;2.468&lt;/td&gt;
&lt;td&gt;0.181&lt;/td&gt;
&lt;td&gt;**13.6&amp;times;** 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mTLS fresh는 평문 fresh 대비 CPU를 7배 더 사용한다. keepalive를 켜면 평문 0.091초 &amp;rarr; mTLS 0.181초로 2배만 더 사용한다. 즉, &lt;b&gt;인증서 검증 CPU는 handshake 단계에서 거의 모두 발생하며, 데이터 전송 자체는 그다지 비싸지 않다&lt;/b&gt;. handshake만 재사용으로 처리하면 mTLS도 거의 평문 수준의 CPU로 운용할 수 있다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 언제 keep-alive를 켜야 하는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;단발성 요청 (스크립트, cron)&lt;/b&gt;: 그래도 켜 두는 것이 맞다. 동일 도메인에 두 번 이상 호출하면 이득을 본다. 손해 볼 일은 거의 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지속적 클라이언트 (백엔드 &amp;rarr; 백엔드 호출)&lt;/b&gt;: 무조건 켠다. connection pool 크기는 동시성 수준의 1.5배 정도로 잡으면 안전하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTPS 호출 (외부 API 콜)&lt;/b&gt;: 무조건 켠다. TLS handshake 한 번을 절약하는 것이 RPS 7배의 차이를 만든다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mTLS 서비스 메시 (Istio, Linkerd 등)&lt;/b&gt;: 켜는 것 외에 답이 없다. 켜지 않으면 RPS가 한 자릿수로 떨어진다. 사이드카 설정 점검이 필수다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP Keep-Alive 사용 시 흔한 함정 5가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;keep-alive를 켰다고 끝이 아니다. 함정이 몇 가지 있다.&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;pool 크기 미스매치&lt;/b&gt;: MaxIdleConnsPerHost의 기본값은 Go에서 2다. 동시성이 32인데 pool이 2라면 효과가 없다. 30개는 여전히 fresh로 열려야 한다. &lt;b&gt;conc 수준에 맞춰 명시적으로 키워야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;idle timeout 미스매치&lt;/b&gt;: 서버 idle timeout이 클라이언트보다 짧으면, 클라이언트가 살아 있다고 믿는 connection이 서버에서는 이미 닫혀 있다. 다음 요청에서 RST를 받고 재시도해야 한다. &lt;b&gt;클라이언트 idle timeout &amp;lt; 서버 idle timeout&lt;/b&gt; 으로 설정해야 안전하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTP/2로 가면 또 다른 이야기다&lt;/b&gt;: HTTP/2는 multiplexing이므로 connection 하나로 여러 stream을 동시에 처리한다. 이때 connection 수가 적기 때문에 fresh/keepalive의 차이가 더 극적으로 벌어진다. 다음 글에서 다룰 예정이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TIME_WAIT와 CLOSE_WAIT를 혼동하지 말 것&lt;/b&gt;: TIME_WAIT는 능동적으로 닫은 쪽에서 60초 정도 대기한다. ephemeral port를 잡아먹는 것은 TIME_WAIT다. fresh 모드는 클라이언트가 매번 능동 close를 수행하므로 TIME_WAIT가 쌓인다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LB 앞단의 keepalive 설정&lt;/b&gt;: L7 LB(NGINX, HAProxy, Envoy)를 앞에 두면 LB &amp;harr; upstream 사이의 keep-alive를 별도로 설정해야 한다. 클라이언트 &amp;harr; LB에만 keep-alive를 켜고 LB &amp;harr; upstream이 fresh라면 LB 뒤에서 다시 무너진다. NGINX의 upstream { keepalive 64; }와 같은 설정을 빠뜨리지 말 것.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론 &amp;mdash; 숫자가 명확하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 takeaway를 다섯 가지로 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;HTTP Keep-Alive는 평문에서도 RPS가 2.8배 차이가 난다.&lt;/b&gt; 단발 호출이 아니라면 무조건 켜야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HTTPS에서는 7.65배, mTLS에서는 9.60배의 차이.&lt;/b&gt; TLS가 들어갈수록 keep-alive는 필수가 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mTLS fresh conc=32는 RPS가 1,053까지 떨어진다.&lt;/b&gt; 동일 인프라가 keepalive에서는 11,469를 기록한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99.9 꼬리지연은 fresh에서 폭주한다.&lt;/b&gt; 평균만 보면 속는다. mTLS fresh conc=32는 130ms를 기록한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CPU 비용도 최대 13.6배까지 차이가 난다.&lt;/b&gt; 지연만 늘어나는 것이 아니다. 서버 비용도 그대로 늘어난다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론 글은 도처에 널려 있지만 한국어로 mTLS 핸드셰이크 비용을 직접 측정한 자료는 거의 없어 18케이스를 모두 실행했다. &lt;b&gt;HTTP Keep-Alive는 옵션이 아니라 디폴트다.&lt;/b&gt; 끄는 것이 의식적인 결정이 되어야 하며, 켜는 것이 결정이 되어서는 안 된다.&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;다음 글에서는 HTTP/2 ALPN과 connection coalescing이 이 그림을 어떻게 바꾸는지 비교할 예정이다. 동일한 mTLS라도 HTTP/2로 가면 또 다른 트레이드오프가 발생하는데, 이는 별도 측정으로 다뤄야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://datatracker.ietf.org/doc/html/rfc7230#section-6&quot;&gt;RFC 7230 &amp;mdash; HTTP/1.1 Message Syntax and Routing (Connection Management)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.cloudflare.com/introducing-0-rtt/&quot;&gt;Cloudflare blog &amp;mdash; TLS handshake latency와 0-RTT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pkg.go.dev/net/http#Transport&quot;&gt;Go net/http Transport 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Backend</category>
      <category>Connection Pool</category>
      <category>Go net/http Transport</category>
      <category>HTTP/1.1 keepalive</category>
      <category>Keep-Alive 효과</category>
      <category>mTLS 성능</category>
      <category>TCP connection 재사용</category>
      <category>TLS handshake 비용</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/412</guid>
      <comments>https://yscho03.tistory.com/412#entry412comment</comments>
      <pubDate>Mon, 4 May 2026 10:33:42 +0900</pubDate>
    </item>
    <item>
      <title>DB max_connections=100인데 pool 200을 잡으면 어떻게 되는가 &amp;mdash; 응급처치 3가지 직접 비교</title>
      <link>https://yscho03.tistory.com/411</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;새벽 3시에 알람 폭탄을 맞아본 적이 있는가? FATAL: sorry, too many clients already 메시지가 로그에 도배되는 그 광경 말이다. 이는 거의 다 &lt;b&gt;DB max_connections=100임에도 애플리케이션 풀을 200, 300으로 박아둔&lt;/b&gt; 상황에서 발생한다. 풀을 키우면 처리량이 늘어날 것이라는 직관 때문에 다들 한 번씩 밟는 함정인데, 실제로 어떻게 깨지는지 실측 데이터로 본 적은 거의 없을 것이다.&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;그래서 직접 측정했다. golang:1.22-bookworm 컨테이너에서 max_connections=100 천장을 고정해둔 채, &lt;b&gt;pool 200을 넘기는 raw_saturate부터 pool=1 직렬화, pgbouncer 트랜잭션 풀링, 클라이언트 축소까지 5가지 풀 전략을 같은 부하로 줄세웠다&lt;/b&gt;. 처리량, 에러율, p99 지연, 꼬리 회복 시간까지 모두 측정했다.&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;각설하고, 이 글에서 다룰 내용은 다음과 같다. &lt;b&gt;pool &amp;gt; max_connections일 때 왜 즉시 깨지는가&lt;/b&gt;, 새벽에 5분 안에 살려야 할 때 쓰는 &lt;b&gt;응급처치 3가지&lt;/b&gt;, 그리고 셋 중 무엇을 골라야 하는지 정리한 의사결정표까지. 운영 중인 풀 사이즈를 점검하는 데 그대로 써먹을 수 있는 숫자 위주로 정리했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwD2JI/dJMcadhAckm/5GQNirxxrXjS27ilZemnR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwD2JI/dJMcadhAckm/5GQNirxxrXjS27ilZemnR0/img.png&quot; data-alt=&quot;https://www.postgresql.org/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwD2JI/dJMcadhAckm/5GQNirxxrXjS27ilZemnR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwD2JI%2FdJMcadhAckm%2F5GQNirxxrXjS27ilZemnR0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;692&quot; height=&quot;328&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.postgresql.org/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;max_connections는 천장이고 pool은 사다리다 &amp;mdash; 둘이 충돌하면 벌어지는 일&lt;/h2&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;b&gt;max_connections는 DB가 받아들일 수 있는 동시 커넥션의 절대 천장&lt;/b&gt;이다. PostgreSQL 기본값이 100이고, 이를 늘리려면 메모리도 같이 잡아먹는다. 커넥션 하나당 work_mem, temp_buffers, 백엔드 프로세스 fork 비용까지 모두 따라붙는다. 그래서 무작정 늘릴 수 없다.&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;반면 &lt;b&gt;pool은 애플리케이션이 미리 잡아두는 사다리와 같다&lt;/b&gt;. 클라이언트 코드에서 maxPoolSize=200을 박으면 우리 앱 인스턴스 하나가 DB로 200개 커넥션을 열려고 시도한다. 인스턴스가 5대면 1000개다. 천장이 100인데도 그렇다.&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;여기서 충돌이 발생한다. 풀이 100을 넘는 순간 DB는 새 커넥션 요청에 대고 FATAL: sorry, too many clients already를 뱉는다. 클라이언트 라이브러리가 fail_fast로 설정돼 있으면 즉시 에러를 던지고, 그렇지 않으면 200ms든 5초든 풀 큐에서 대기하다가 타임아웃이 발생한다.&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;&quot;풀을 키우면 처리량이 늘어나겠지&quot;라는 것이 가장 흔한 오해다. 실측해보면 &lt;b&gt;정반대&lt;/b&gt;다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 풀=300으로 잡고 부하를 돌리면 어떻게 되는가? 천장에 닿는 순간부터 38%가 즉시 실패한다. 처리량은 22,000 QPS가 찍히긴 하는데, 그중 84,439건이 db_full 에러다. 즉 &quot;처리량처럼 보이는 숫자&quot;의 절반 가까이가 실은 에러 응답이다. 사용자에게는 500 에러 폭탄이 떨어진 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 세팅 &amp;mdash; golang 컨테이너에서 5개 전략을 줄세웠다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;측정은 2026-05-03에 진행했다. 환경은 다음과 같다.&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;부하 패턴은 4단계 10초로 구성했다. &lt;/span&gt;&lt;b&gt;워밍업 1초(버림) &amp;rarr; 정상 3초(120 클라이언트) &amp;rarr; 버스트 2초(300 클라이언트) &amp;rarr; 회복 4초(120 클라이언트)&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;. 쿼리 모델은 균등 3~7ms에 4% 확률로 0~25ms 롱테일을 더한 형태다. 실제 운영 트래픽과 비슷하게 구성했다.&lt;/span&gt;&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;루프 모델은 클로즈드 루프 고루틴이다. 성공 시 300&amp;micro;s sleep, 에러 시 5ms sleep. 즉 에러가 발생하면 살짝 백오프가 걸리도록 했다.&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;5가지 전략의 표는 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전략&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;풀 크기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;fail_fast&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;클라이언트 배수&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;타임아웃&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;의도&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;1.0&amp;times;&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;td&gt;천장 아래로 안전하게&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;raw_saturate&lt;/td&gt;
&lt;td&gt;**300**&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;1.0&amp;times;&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;천장 뚫고 즉사 보기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pool_1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;1.0&amp;times;&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;td&gt;응급처치 ① 직렬화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgbouncer&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;1.0&amp;times;&lt;/td&gt;
&lt;td&gt;200ms&lt;/td&gt;
&lt;td&gt;응급처치 ② 멀티플렉싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reduced_clients&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;**0.25&amp;times;**&lt;/td&gt;
&lt;td&gt;200ms&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종합 결과부터 제시한다. stdout 그대로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841387&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;strategy           samples    err%      qps   p50_stdy   p99_stdy   p99_brst   p99_recv
------------------------------------------------------------------------------------------
baseline            118286   0.00%    11825       7.51      26.10      39.80      26.67
raw_saturate        221086  38.19%    22106       5.57      24.51      23.57      24.22
pool_1                7134  78.60%      713     202.54     220.65     220.17     215.99
pgbouncer            58797   0.00%     5879      16.64      35.44      63.13      36.18
reduced_clients      48773   0.00%     4876       5.64      25.38      23.23      24.14

--- error breakdown by type ---
raw_saturate      db_full=84439
pool_1            pool_timeout=5607
&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;raw_saturate는 풀=300이라 천장이 뚫리자마자 84,439건이 db_full로 즉사한 것이 보인다. 클라이언트 측에서 fail_fast=true로 두었으니 대기조차 하지 않고 바로 에러가 떨어진다. &lt;b&gt;풀을 키우면 처리량이 늘어난다는 직관이 정반대로 작동한다는 증거다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;응급처치 ① &amp;mdash; pool=1로 일단 직렬화하기&lt;/h2&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;b&gt;언제 쓰는가&lt;/b&gt;: 새벽 장애가 발생하여 &quot;5분 안에 살려라&quot; 상황. 일단 죽지만 않게 하면 되는 배치 작업이나 백오피스 시스템.&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;&lt;b&gt;동작 원리&lt;/b&gt;: 모든 요청이 단일 커넥션 큐에 줄을 선다. 동시성=1로 강제 직렬화를 거는 셈이다. DB 천장이고 뭐고 일단 1개만 쓰니까 충돌 자체가 없다.&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;&lt;b&gt;실측 결과&lt;/b&gt;: pool_1 결과를 다시 보면 처리량은 713 QPS다. baseline(11,825)의 6%에 불과하다. 다만 &lt;b&gt;p99 지연은 220ms로 거의 일정&lt;/b&gt;하다. steady에서도 220.65ms, 버스트에서도 220.17ms, 회복에서도 215.99ms로 변동이 거의 없다.&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;에러율 78.6%는 다소 충격적으로 보일 수 있는데, 이는 클로즈드 루프에서 200ms 타임아웃이 빡빡한 탓에 큐 대기가 길어진 요청들이 잘려나간 결과다. 즉 &lt;b&gt;pool_1로 살리면 들어온 요청 일부는 떨궈낸다. 다만 떨군 것은 떨군 것이고, 받아낸 것은 안정적으로 처리한다&lt;/b&gt;. db_full로 캐스케이딩 실패하는 raw_saturate와는 결이 완전히 다르다.&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;자바 진영 HikariCP 설정이라면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841388&quot; class=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  datasource:
    hikari:
      maximum-pool-size: 1
      connection-timeout: 200
      validation-timeout: 100
&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;Go pgx라면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841388&quot; class=&quot;arduino&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;config, _ := pgxpool.ParseConfig(dsn)
config.MaxConns = 1
config.MinConns = 1
pool, _ := pgxpool.NewWithConfig(ctx, config)
&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;b&gt;한계&lt;/b&gt;: 처리량 절벽이다. 트래픽이 큰 사용자 대면 서비스에는 임시방편 이상으로 쓸 수 없다. 큐가 길어지면서 사용자 응답 시간이 200ms 근처로 고정된다. 응답이 늦더라도 죽지만 않으면 되는 배치성 워크로드에서나 쓸 수 있는 응급조치다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;응급처치 ② &amp;mdash; pgbouncer 트랜잭션 풀링으로 다중화하기&lt;/h2&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;b&gt;언제 쓰는가&lt;/b&gt;: 클라이언트 인스턴스는 많은데 DB 천장은 늘릴 수 없을 때. 마이크로서비스 환경에서 인스턴스를 10대씩 굴리면 풀 사이즈 합산이 금방 100을 넘어선다. 이때의 정답이다.&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;&lt;b&gt;동작 원리&lt;/b&gt;: pgbouncer가 클라이언트 풀과 DB 풀을 분리한다. 클라이언트는 pgbouncer로 1000개 커넥션을 열어도, pgbouncer는 DB로 40개만 유지한다. &lt;b&gt;pool_mode=transaction&lt;/b&gt;으로 두면 트랜잭션 단위로 DB 커넥션을 빌려주고 반납한다. 동시성 비대칭이 흡수되는 구조다.&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;&lt;b&gt;실측 결과&lt;/b&gt;: pgbouncer 전략은 DB 커넥션 40개로 baseline(80)에 근접한 처리량 5,879 QPS를 기록했다. 에러율은 0%다. p99도 steady 35.44ms, burst 63.13ms로 안정적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841388&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;pgbouncer            58797   0.00%     5879      16.64      35.44      63.13      36.18
&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;물론 baseline 11,825 QPS에는 미치지 못한다. 트랜잭션 풀 라우팅 오버헤드가 다소 존재한다. 다만 &lt;b&gt;DB 커넥션을 절반(80 &amp;rarr; 40)으로 줄이면서도 에러 없이 받아냈다는 점이 핵심&lt;/b&gt;이다. 클라이언트가 늘어날수록 이 비대칭 흡수 효과는 커진다.&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;설정 예시는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841388&quot; class=&quot;ini&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;ini&quot;&gt;&lt;code&gt;[databases]
mydb = host=db-primary port=5432 dbname=mydb

[pgbouncer]
pool_mode = transaction
default_pool_size = 40
max_client_conn = 1000
reserve_pool_size = 5
reserve_pool_timeout = 3
server_idle_timeout = 600
&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;default_pool_size는 DB max_connections 대비 여유를 두고 잡아야 한다. 천장이 100이면 40~60 사이가 무난하다. 다른 직접 접속(관리자 콘솔, 백업 등)이 있으므로 100을 다 써서는 안 된다.&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;&lt;b&gt;도입 함정&lt;/b&gt;: 트랜잭션 풀링은 &lt;a href=&quot;https://www.pgbouncer.org/config.html&quot;&gt;PgBouncer 공식 문서&lt;/a&gt;에도 나와 있듯 다음 기능들이 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;prepared statement&lt;/b&gt; 캐시: 같은 세션이 보장되지 않으므로 pgx든 jdbc든 prepared statement를 쓰지 않도록 꺼야 한다. JDBC라면 prepareThreshold=0, pgx라면 default_query_exec_mode=exec 또는 cache_describe.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SET LOCAL&lt;/b&gt; 외 SET: 트랜잭션이 끝나면 다른 세션으로 넘어가므로 세션 변수가 모두 날아간다. SET LOCAL만 안전하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;advisory lock&lt;/b&gt;: 세션 단위 락이라 트랜잭션 풀링과 맞지 않는다. 트랜잭션 단위 advisory lock(pg_advisory_xact_lock)으로 바꿔야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LISTEN/NOTIFY&lt;/b&gt;: 세션 종속이라 사용할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 챙기지 않고 도입하면 더 큰 사고가 발생한다. 도입 전에 코드베이스 grep을 한 번 돌리고 가는 것을 강력히 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;응급처치 ③ &amp;mdash; 클라이언트 수 자체를 줄이기&lt;/h2&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;b&gt;언제 쓰는가&lt;/b&gt;: 응답 시간 SLA가 빡빡한 사용자 트래픽. p95, p99 지연이 가장 중요한 서비스.&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;&lt;b&gt;동작 원리&lt;/b&gt;: 풀 사이즈는 그대로 두고, &lt;b&gt;동시 in-flight 요청 수 자체를 1/4로 깎는다&lt;/b&gt;. 큐 길이가 짧아지므로 대기 시간이 줄어든다. 처리량을 포기하는 대신 지연 분포를 짭짤하게 가져가는 전략이다.&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;&lt;b&gt;실측 결과&lt;/b&gt;: reduced_clients(client_scale=0.25)는 4,876 QPS, 에러율 0%, p99 25.38ms를 기록했다. 단계별로 보면 더 깔끔하다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;단계&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p95&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99.9&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;steady&lt;/td&gt;
&lt;td&gt;5.500ms&lt;/td&gt;
&lt;td&gt;7.918ms&lt;/td&gt;
&lt;td&gt;24.135ms&lt;/td&gt;
&lt;td&gt;30.076ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;burst&lt;/td&gt;
&lt;td&gt;5.608ms&lt;/td&gt;
&lt;td&gt;7.931ms&lt;/td&gt;
&lt;td&gt;23.226ms&lt;/td&gt;
&lt;td&gt;30.536ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recovery&lt;/td&gt;
&lt;td&gt;5.500ms&lt;/td&gt;
&lt;td&gt;7.918ms&lt;/td&gt;
&lt;td&gt;24.135ms&lt;/td&gt;
&lt;td&gt;30.076ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버스트 구간에서도 p99가 23ms대로 떨어진다. baseline 버스트 p99가 39.80ms인 것과 비교하면 &lt;b&gt;p99 지연이 절반에 가깝다&lt;/b&gt;. 사용자 경험 측면에서는 가장 깔끔하다.&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;&lt;b&gt;실전 적용&lt;/b&gt;: 큐 깊이, 워커 수, rate limiter, semaphore 패턴으로 깎는다. Go라면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841390&quot; class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;sem := make(chan struct{}, 30)  // 동시 in-flight 30개로 제한

func handle(ctx context.Context, q string) error {
    select {
    case sem &amp;lt;- struct{}{}:
        defer func() { &amp;lt;-sem }()
    case &amp;lt;-ctx.Done():
        return ctx.Err()
    }
    return db.Exec(ctx, q)
}
&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;스프링이라면 Bulkhead 패턴(resilience4j)을 써서 동시 호출 수 제한을 걸면 된다.&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;&lt;b&gt;한계&lt;/b&gt;: 처리량이 1/4로 떨어지므로 트래픽 자체를 줄일 수 없는 서비스(공개 API, 광고 서버 등)에서는 쓸 수 없다. 다만 사내 도구나 관리자 페이지처럼 사용자 수가 한정된 곳에서는 정말 잘 먹힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;셋 중 무엇을 골라야 하는가 &amp;mdash; 의사결정표&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응급처치 3가지를 한 축에서 비교한 매트릭스다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천 처치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;즉시 죽지만 않으면 됨 (배치, 백오피스)&lt;/td&gt;
&lt;td&gt;**pool_1**&lt;/td&gt;
&lt;td&gt;직렬화로 안정성 확보, 처리량은 포기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;응답 시간 SLA 빡빡함 (사용자 트래픽)&lt;/td&gt;
&lt;td&gt;**reduced_clients**&lt;/td&gt;
&lt;td&gt;p99 가장 안정, 큐 짧게 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동시 클라이언트 수가 본질적으로 많음 (마이크로서비스)&lt;/td&gt;
&lt;td&gt;**pgbouncer**&lt;/td&gt;
&lt;td&gt;천장 아래에서 다중화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;절대 하면 안 되는 것&lt;/td&gt;
&lt;td&gt;~~raw_saturate~~&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;&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;&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; &amp;rarr; pool_1로 일단 살린다. 처리량이 떨어져도 죽지는 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 응답 시간이 깎이는 중인가?&lt;/b&gt; &amp;rarr; reduced_clients로 큐를 깎는다. p99가 회복된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;마이크로서비스 / 멀티 인스턴스라 클라이언트가 본질적으로 많은가?&lt;/b&gt; &amp;rarr; pgbouncer로 영구 해결한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;풀을 무작정 키우기?&lt;/b&gt; &amp;rarr; 절대 안 된다. raw_saturate 결과를 다시 보라.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;raw_saturate 데이터를 다시 제시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841390&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;strategy           samples    err%      qps
raw_saturate        221086  38.19%    22106
  err: db_full=84439
&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;22,106 QPS는 함정 숫자다. 38.19%가 에러다. 실제 처리량은 13,665 QPS인데, 그마저도 천장을 뚫고 들어가 &lt;b&gt;다른 정상 클라이언트 커넥션까지 잡아먹는 connection storm&lt;/b&gt;을 만들어낸다. 이것이 새벽에 캐스케이딩 장애를 일으키는 전형적 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;응급 끝나면 해야 할 것 &amp;mdash; 천장 자체를 다시 보기&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;max_connections를 늘리는 것이 능사가 아닌 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL은 커넥션 하나당 백엔드 프로세스 하나를 fork한다. 메모리 비용이 직선으로 늘어난다. work_mem 기본 4MB에 추가 메모리까지 합치면 커넥션 1개당 10~20MB는 잡는다. &lt;b&gt;max_connections를 100에서 500으로 늘리면 메모리만 4~8GB 더 필요&lt;/b&gt;하다.&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;게다가 컨텍스트 스위칭 비용도 함께 늘어난다. CPU 코어가 8개인데 active 커넥션이 200개라면 스케줄러가 헛돌기 시작한다. &lt;a href=&quot;https://wiki.postgresql.org/wiki/Number_Of_Database_Connections&quot;&gt;PostgreSQL 공식 위키 가이드&lt;/a&gt; 역시 connections == ((core_count * 2) + effective_spindle_count) 공식을 권장한다. 8코어 SSD 환경이라면 17개 정도가 이론 최적이다. 100도 사실 많은 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pgbouncer를 영구 도입할 때 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[ ] &lt;b&gt;prepared statement 끄기&lt;/b&gt; 또는 protocol level prepared로 전환&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;SET 사용처 grep&lt;/b&gt; &amp;mdash; SET LOCAL로 바꾸거나 제거&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;advisory lock 점검&lt;/b&gt; &amp;mdash; pg_advisory_xact_lock으로 마이그레이션&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;LISTEN/NOTIFY 제거&lt;/b&gt; &amp;mdash; Redis pub/sub 등으로 대체&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;default_pool_size 산정&lt;/b&gt; &amp;mdash; DB max_connections &amp;times; 0.5 정도로 시작&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;모니터링&lt;/b&gt; &amp;mdash; pgbouncer SHOW POOLS, SHOW STATS 대시보드 구축&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;HA 구성&lt;/b&gt; &amp;mdash; pgbouncer 자체가 SPOF가 되지 않도록 다중화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모니터링 지표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_stat_activity에서 확인해야 할 것은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777819841391&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = 'mydb'
GROUP BY state;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;idle_in_transaction&lt;/b&gt;: 이 값이 늘어나면 클라이언트가 트랜잭션을 열어둔 채 일을 하지 않고 있는 것이다. statement_timeout을 짧게 두고, idle_in_transaction_session_timeout도 걸어두는 편이 좋다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;application_name&lt;/b&gt;: 커넥션마다 application_name을 분리해두면 어떤 서비스가 풀을 잡아먹는지 보인다. 무조건 설정하라.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HikariCP 풀 사이즈 산정&lt;/h3&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;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot;&gt;HikariCP wiki &amp;ndash; About Pool Sizing&lt;/a&gt;이 자바 진영의 성경인데, 거기서 권장하는 값은 의외로 작다. &lt;b&gt;&quot;작은 풀에 스레드가 대기 줄을 서는 구조&quot;&lt;/b&gt;가 핵심 메시지다. 권장 공식은 ((core_count * 2) + effective_spindle_count)로 PostgreSQL wiki와 동일하다. 코어 수의 2배 + 디스크 수 정도면 충분하다는 결론이다. 다만 우리처럼 max_connections 천장이 낮은 경우라면 이 권장값보다 더 작게 잡는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리 &amp;mdash; pool은 max_connections 천장 아래로, raw_saturate는 절대 금지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5가지 전략을 한 줄씩 다시 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;baseline (pool=80)&lt;/b&gt;: 천장 아래에서 안정. 11,825 QPS, 에러 0%. 정상 운영 모드.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;raw_saturate (pool=300)&lt;/b&gt;: 천장 뚫고 즉사. 38% 에러. &lt;b&gt;절대 해서는 안 되는 짓&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pool_1&lt;/b&gt;: 직렬화 응급처치. 713 QPS이지만 죽지 않는다. 5분 안에 살려야 할 때.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;pgbouncer&lt;/b&gt;: 트랜잭션 풀링으로 다중화. 5,879 QPS, 에러 0%. &lt;b&gt;영구 해결책으로 가장 추천&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;reduced_clients&lt;/b&gt;: 클라이언트 1/4. 4,876 QPS, p99 가장 안정. SLA가 빡빡할 때.&lt;/li&gt;
&lt;/ul&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;b&gt;pool은 DB max_connections 천장 아래로 둘 것. 천장이 뚫리면 즉사한다&lt;/b&gt;. 풀을 키운다고 처리량이 늘어나지 않는다. 오히려 connection storm을 만나서 다 같이 죽는다.&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;응급처치 3가지 &amp;mdash; pool=1 직렬화, pgbouncer, 클라이언트 축소 &amp;mdash; 중에서 지금 상황에 맞는 것을 골라 적용하면 된다. 근본 해결은 pgbouncer를 영구 도입하고 max_connections 산정 공식을 다시 살펴보는 것이다.&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;지금 운영 중인 서비스의 풀 사이즈를 한 번 점검해보라. 인스턴스 수 &amp;times; pool size를 계산해보고 max_connections 천장과 비교하면 정말 큰일 날 뻔한 부분을 발견할 수도 있다. 새벽 알람 폭탄을 맞기 전에 미리 손보는 편이 훨씬 싸게 먹힌다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>DB 커넥션 풀 사이즈</category>
      <category>FATAL sorry too many clients already</category>
      <category>pgbouncer transaction pooling</category>
      <category>PostgreSQL too many connections</category>
      <category>커넥션 풀 max_connections 초과</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/411</guid>
      <comments>https://yscho03.tistory.com/411#entry411comment</comments>
      <pubDate>Sun, 3 May 2026 23:51:43 +0900</pubDate>
    </item>
    <item>
      <title>DB Connection Pool은 클수록 좋다는 거짓말 &amp;mdash; PostgreSQL 풀을 1부터 256까지 늘려본 결과</title>
      <link>https://yscho03.tistory.com/410</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;서버 응답이 느려졌다고 max_connections=200을 박아두었더니 오히려 더 느려진 경험이 있는가? 흔하게 마주치는 케이스다. PostgreSQL 커넥션 풀 사이즈를 무작정 키우면 처리량이 같이 올라간다는 명제가 한국 개발자들 사이에 거의 신앙처럼 박혀 있지만, 실제로 측정해 보면 정반대 곡선이 나타난다. 풀이 커질수록 평균 지연은 줄어드나 p99는 오히려 폭발하고, 어떤 구간부터는 처리량 자체가 떨어진다.&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;이 글은 단순한 위키 번역이 아니다. 풀 크기를 1부터 256까지 9개 구간으로 직접 스윕한 벤치마크 raw 데이터(2026-05-03 측정)를 그대로 실었다. (cores*2)+spindles 공식이 어디까지 진실이고 어디서부터 거짓말인지, 풀이 너무 작을 때와 너무 클 때의 죽는 모드가 어떻게 다른지를 곡선으로 제시한다. 마지막에는 자신의 환경에서 5분 안에 적정 풀 크기를 결정할 수 있는 체크리스트까지 정리해 두었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bX1Rfg/dJMcaad6JOJ/jlqku2IzVn1K1nfIgnTwG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bX1Rfg/dJMcaad6JOJ/jlqku2IzVn1K1nfIgnTwG1/img.png&quot; data-alt=&quot;https://www.postgresql.org/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bX1Rfg/dJMcaad6JOJ/jlqku2IzVn1K1nfIgnTwG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbX1Rfg%2FdJMcaad6JOJ%2Fjlqku2IzVn1K1nfIgnTwG1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;696&quot; height=&quot;334&quot; data-origin-width=&quot;696&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.postgresql.org/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀 크기 미신: &quot;크게 잡으면 빨라진다&quot;는 어디서 비롯되었는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL 공식 위키의 (cores*2)+spindles 공식&lt;/h3&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;a href=&quot;https://wiki.postgresql.org/wiki/Number_of_Database_Connections&quot;&gt;PostgreSQL 공식 위키&lt;/a&gt;의 &quot;Number of Database Connections&quot; 문서에는 다음 공식이 박혀 있다.&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;connections = ((core_count * 2) + effective_spindle_count)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코어 수에 2를 곱한 값에 디스크 스핀들 수를 더한 값이 적정 커넥션 수라는 것이다. 8코어 + SSD 1개라면 풀 17개 정도가 권장값이 된다. 한국 블로그 9할이 인용하는 출처가 바로 이것이다. 다만 이 공식의 진짜 의미는 &quot;이걸 무조건 사용하라&quot;가 아니라 &quot;이 정도부터 시작해서 측정하라&quot;는 가이드라인이다. 원본 위키에도 &quot;starting point&quot;라는 단어가 박혀 있는데, 한국어 번역본은 그 부분을 빼먹고 숫자만 가져왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HikariCP 위키가 Oracle 실험 영상을 끌어온 이유&lt;/h3&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;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot;&gt;HikariCP 풀 사이징 문서&lt;/a&gt;는 더 강하게 주장한다. Brett Wooldridge가 Oracle Real-World Performance 그룹의 시연 영상까지 끌어와 &quot;풀이 작을수록 빨라진다&quot;는 카운터 인튜이션을 제시한다. 그 영상은 풀을 2048에서 96으로 줄였더니 응답 시간이 ~100ms에서 ~2ms로 50배 이상 좋아지는 데모다. 핵심은 큐잉 이론이다. 처리해야 할 작업이 코어 수보다 많아지면 컨텍스트 스위치 비용이 처리량을 갉아먹기 시작한다. 코어가 8개라면 동시에 진짜로 일하는 백엔드는 8개여야 최적이지만, 풀을 100개 열어 두면 92개는 그저 락 대기 상태로 자원만 차지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국 개발자들이 max_connections=200을 디폴트로 박는 관습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 현실에서는 어떻게 하는가. 그저 max_connections=200을 박는다. 풀러를 사용하지 않는 곳도 많고, 사용하더라도 HikariCP 디폴트 10을 100으로 늘린다. &quot;큰 것이 안전하다&quot;는 보험 심리 때문이다. 메모리만 충분하면 큰 풀이 손해 볼 것이 없다고 생각하지만, 이것이 정확히 거짓말이다. 메모리뿐 아니라 락, 컨텍스트 스위치, 스케줄링 오버헤드, work_mem 곱셈이 모두 비용으로 박힌다. 측정해 보지 않으면 모르는 함정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 측정했다: 풀 크기 1~256 스윕 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로만 풀어 놓으면 또 위키 번역과 똑같은 글이 된다. 그래서 직접 돌려 보았다. 실험명 postgres-pool-size-sweep, golang:1.22-bookworm 컨테이너, 메모리 512MB, CPU 2.0코어, 네트워크 deny, 타임아웃 180초. 200개 동시 클라이언트가 풀 크기별로 3초씩 부하를 인가했고, 각 쿼리는 CPU 스핀 + 공유 락 + I/O 슬립의 3단계를 거치도록 설계했다. 실제 PostgreSQL 백엔드의 자원 경합 패턴을 단순화하여 모방한 것이다. 풀 크기는 1, 2, 4, 8, 16, 32, 64, 128, 256으로 코어의 0.125배에서 32배까지 스윕했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리량 곡선 &amp;mdash; 풀 8~32에서 평탄, 그 이상은 평지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각설하고 raw 데이터부터 박는다. 직접 측정한 1~256 풀 스윕(2026-05-03) 결과다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;pool&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;qps&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p50 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p95 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99 (ms)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;peak_active&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;wait_mean (ms)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;797&lt;/td&gt;
&lt;td&gt;275.77&lt;/td&gt;
&lt;td&gt;284.73&lt;/td&gt;
&lt;td&gt;287.84&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;260.63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1,508&lt;/td&gt;
&lt;td&gt;139.57&lt;/td&gt;
&lt;td&gt;145.39&lt;/td&gt;
&lt;td&gt;147.25&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;134.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;2,957&lt;/td&gt;
&lt;td&gt;69.51&lt;/td&gt;
&lt;td&gt;75.34&lt;/td&gt;
&lt;td&gt;77.09&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;66.96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;5,867&lt;/td&gt;
&lt;td&gt;34.60&lt;/td&gt;
&lt;td&gt;38.07&lt;/td&gt;
&lt;td&gt;39.64&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;32.89&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;13,612&lt;/td&gt;
&lt;td&gt;15.49&lt;/td&gt;
&lt;td&gt;19.06&lt;/td&gt;
&lt;td&gt;21.03&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;13.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;33,415&lt;/td&gt;
&lt;td&gt;5.91&lt;/td&gt;
&lt;td&gt;8.94&lt;/td&gt;
&lt;td&gt;10.11&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;5.04&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;109,342&lt;/td&gt;
&lt;td&gt;1.70&lt;/td&gt;
&lt;td&gt;2.73&lt;/td&gt;
&lt;td&gt;3.67&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;1.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;120,866&lt;/td&gt;
&lt;td&gt;1.23&lt;/td&gt;
&lt;td&gt;3.77&lt;/td&gt;
&lt;td&gt;6.64&lt;/td&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;0.62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;110,199&lt;/td&gt;
&lt;td&gt;0.72&lt;/td&gt;
&lt;td&gt;6.08&lt;/td&gt;
&lt;td&gt;11.72&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 두 가지다. 첫째, 풀 1에서 64까지는 처리량이 거의 비례하여 늘어난다. 797 qps에서 109,342 qps까지 약 137배 뛴다. 둘째, 풀 64를 넘어서면 곡선이 평탄해진다. 풀 128에서 120,866 qps로 살짝 더 오르긴 하지만, 풀 256에서는 오히려 110,199 qps로 떨어진다. &lt;b&gt;클수록 빨라진다는 명제가 거짓임이 여기서 그대로 드러난다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지연 분포 &amp;mdash; p50은 얌전한데 p99가 폭발하는 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 처리량만 보면 왜 풀 256이 망하는지 잘 보이지 않는다. 진짜 무서운 것은 지연 분포다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777817390516&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;peak throughput @ pool=128  (120866 qps)
interpretation: throughput typically rises with pool until ~(2*cores),
then plateaus or drops as p95/p99 inflate from contention.
&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;p50(중간값)은 풀 256에서 0.72ms로 가장 작다. 평균만 보면 &quot;풀이 클수록 좋다&quot;는 결론에 도달하기 쉽다. 그러나 p99를 보면 이야기가 달라진다. 풀 64에서 3.67ms였던 p99가 풀 128에서 6.64ms, 풀 256에서 11.72ms로 뛴다. p50 대비 p99 비율로 환산하면 풀 64에서는 2.16배, 풀 256에서는 16.28배다. &lt;b&gt;꼬리 지연이 7~8배로 부풀어 오른 것이다.&lt;/b&gt; 평균만 보고 &quot;풀을 키워도 멀쩡하다&quot;고 판단했다가 사용자 1%가 16배 느려지는 사태가 이런 식으로 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀이 너무 작을 때는 무슨 일이 벌어지는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐 대기(queue_wait_mean_ms)가 어떻게 폭증하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 1일 때 queue_wait_mean_ms가 260.63ms이다. 평균 지연 261.99ms 중에 99.5%가 큐 대기다. 실제 쿼리 처리는 1.36ms뿐이지만, 클라이언트 200명이 1개의 풀을 두고 줄을 서니 99% 시간을 그저 기다리는 셈이다. 풀 2면 134.32ms, 풀 4면 66.96ms로 깔끔하게 절반씩 줄어드는 양상이 보인다.&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;이것이 풀이 너무 작을 때의 죽는 모드다. 백엔드는 한가한데 클라이언트는 큐에서 죽는다. CPU 사용률은 멀쩡한데 응답은 느린 미스터리한 상황이 정확히 이 경우다. &quot;DB 커넥션이 너무 많으면&quot;이라고 검색하는 사람들 중 절반은 사실 정반대 문제를 겪고 있다. 풀이 너무 작아서 죽고 있는데 풀을 더 줄이려고 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Little's Law로 본 1~4 풀의 한계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐잉 이론의 Little's Law는 단순하다. &lt;b&gt;L = &amp;lambda; &amp;times; W&lt;/b&gt; (큐 길이 = 도착률 &amp;times; 평균 체류시간). 풀 1일 때 도착률 200 req/s에 처리시간 1.25ms라면 이론 큐 길이는 0.25지만, 풀이 1뿐이니 클라이언트 199명이 줄을 선다. 200 클라이언트 / 1 서버 = 큐 평균 199.5. 이것이 260ms 지연으로 나타난다.&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;풀 4면 큐 길이가 49까지 떨어지고, 풀 8이면 24까지 떨어진다. 코어 8개에서 풀을 8 이하로 박으면 코어를 채울 수 없다. CPU가 놀고 있는데 클라이언트는 죽는, 가장 비효율적인 케이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리량이 코어를 못 따라가는 구간 분석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코어 8개 환경에서 풀이 4 이하라면 절대로 코어를 다 사용하지 못한다. peak_active 칼럼이 그대로 풀 사이즈와 똑같이 나오는 것이 그 증거다. 풀 4 &amp;rarr; peak_active 4 &amp;rarr; 코어 8개 중 4개만 작동한다. CPU의 50%만 쓰고 처리량은 코어 절반 분량밖에 나오지 않는다. 이때 늘려야 할 것은 풀 사이즈가 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀이 너무 클 때는 더 끔찍하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;peak_active 백엔드 수와 락 경합 상관관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 64까지는 peak_active가 풀 사이즈와 같이 비례한다. 풀 64라면 peak_active 64다. 그러나 풀 256에서는 peak_active가 200으로 떨어진다. 클라이언트 수가 200이라 그 이상 늘어날 수 없는 것이다. 이것이 의미하는 바는 200개 백엔드가 동시에 락을 두고 싸우고 있다는 뜻이다.&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;PostgreSQL 내부 락에는 ProcArrayLock, LWLock, BufferContent lock 같은 것이 있는데, 동시 백엔드 수가 늘면 이 락들의 contention이 superlinear하게 증가한다. M/M/c 큐 모델로 보아도 c가 너무 커지면 락 자체가 새로운 병목이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;p99/max가 풀 64 이상에서 가파르게 솟는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max_latency 칼럼이 결정적이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;pool&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;max_latency (ms)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;12.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;14.37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;22.05&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;30.40&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;풀 32 대비 풀 256에서 max는 2.46배 뛴다. 평균은 거의 같은데 max만 폭발하는 것이 락 경합의 시그니처 패턴이다. 일부 트랜잭션이 다른 200개와 락 경합에 들어가며 길게 묶이는 것이다. p99 지연도 풀 64에서 3.67ms였다가 풀 256에서 11.72ms로 3.19배 뛴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PostgreSQL의 ProcArray, LWLock 등 내부 락이 병목이 되는 지점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 백엔드는 트랜잭션 시작/커밋마다 ProcArrayLock을 획득한다. 백엔드 100개가 동시 실행되면 이 락 대기열이 그대로 100칸짜리 줄이 된다. autovacuum이라도 돌아가면 더 심해진다. 풀을 너무 키운 결과 외부 큐(클라이언트 측)가 비는 대신 내부 큐(DB 측)에서 줄을 서서 죽는다. 진짜 도움이 되지 않는 트레이드오프다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 PostgreSQL 커넥션 풀 적정 크기는 얼마인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 커넥션 풀 사이즈의 적정값은 &lt;b&gt;CPU 코어 수의 4~16배&lt;/b&gt; 구간이다. CPU 바운드 워크로드라면 코어의 1~4배, I/O 비중이 큰 OLTP라면 6~16배가 안전하다. 위키 공식 (cores*2)+spindles는 하한선이며, 실제 최적값은 부하 측정으로 잡아야 한다. 풀을 너무 크게 잡으면 p99 지연이 평균 대비 16배까지 폭발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코어 수 기준 4~16배 구간이 안전권&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 표를 다시 보면 답이 나온다. 8코어 환경에서:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;풀 32 (코어의 4배): qps 33,415 / p99 10.11ms / wait 5.04ms&lt;/li&gt;
&lt;li&gt;풀 64 (코어의 8배): qps 109,342 / p99 3.67ms / wait 1.25ms&lt;/li&gt;
&lt;li&gt;풀 128 (코어의 16배): qps 120,866 / p99 6.64ms / wait 0.62ms&lt;/li&gt;
&lt;/ul&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;b&gt;코어의 4~16배&lt;/b&gt; 구간이다. (cores*2)+spindles 공식대로면 풀 17~18이지만 실측은 그것보다 한참 큰 64가 최고다. 공식은 하한선이다. CPU 바운드한 워크로드라면 공식값에 가깝고, I/O 비중이 큰 워크로드라면 그것의 4~8배까지 늘려도 무방하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;I/O 비중 큰 워크로드 vs CPU 바운드 워크로드 보정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론은 단순하다. 한 쿼리가 CPU 시간 X, I/O/wait 시간 Y라면 적정 풀 크기는 대략 cores * (1 + Y/X)다. CPU 바운드(Y&amp;asymp;0)라면 풀 = 코어. I/O 비중이 큰 OLTP(Y/X &amp;asymp; 5~10)라면 풀 = 코어의 6~11배. 이것이 풀 64가 최적이었던 이유다. 본 실험은 CPU+락+I/O 혼합이라 Y/X가 7 근처다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PgBouncer/HikariCP 같은 풀러 도입 시 양쪽 풀 크기 합산 주의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PgBouncer 커넥션 풀을 도입할 때 흔한 실수가 있다. 애플리케이션 측 HikariCP 풀 100, PgBouncer transaction pooling 풀 50, PostgreSQL max_connections 200을 함께 박는 것이다. 양쪽 풀이 맞지 않으면 PgBouncer가 병목이 되거나 PostgreSQL이 터진다. 권장사항은 &lt;b&gt;앱 풀 &amp;le; PgBouncer 풀 &amp;le; max_connections&lt;/b&gt; 부등식을 만족시키는 것이다. PgBouncer 공식 문서도 transaction mode에서는 DB 측 풀을 코어의 2~4배로 작게 박고 앱 측에서 길게 큐를 잡으라고 안내한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀 크기만 만져서는 안 된다: 함께 보아야 할 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;max_connections와 work_mem의 곱셈 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;max_connections=200을 박았는데 work_mem=64MB로 잡았다면? 최악의 경우 200 &amp;times; 64MB = 12.8GB 메모리가 정렬/해시에 묶인다. PostgreSQL 16GB 인스턴스라면 운영체제 캐시와 shared_buffers를 사용하지 못하고 OOM 직전까지 가는 케이스다. work_mem은 커넥션당 곱해진다는 점이 핵심이다. 풀 키우기 &amp;rarr; max_connections 키우기 &amp;rarr; work_mem 곱셈으로 메모리 폭발 &amp;rarr; 페이지 캐시 죽음 &amp;rarr; 디스크 I/O 폭증 순서로 망가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shared_buffers, effective_cache_size 함께 튜닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PostgreSQL 추천값은 shared_buffers = RAM의 25%, effective_cache_size = RAM의 75%이다. 다만 max_connections &amp;times; work_mem이 너무 크면 shared_buffers를 늘릴 수 없다. 풀 크기를 정할 때 이 곱셈을 항상 함께 보아야 한다. 8GB RAM에 풀 100, work_mem 4MB가 풀 50, work_mem 8MB보다 더 위험할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;autovacuum이 풀을 갉아먹는 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;autovacuum_max_workers의 디폴트는 3개다. 이것이 풀의 슬롯을 잠식한다. max_connections=100이라면 실제 사용 가능한 풀은 97 정도다. replication slot을 사용하면 거기서도 빠진다. superuser_reserved_connections(디폴트 3)를 제외하면 또 -3. 실제 앱이 사용 가능한 커넥션은 max_connections - autovacuum - superuser - replication이다. 풀 크기를 정할 때 이 잠식분을 빼야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;풀 크기를 정하는 5분 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;길게 서술했지만, 결국 실전에서 5분 안에 끝낼 수 있는 절차는 다음과 같다. PostgreSQL 커넥션 풀 사이즈를 정할 때 이 순서대로 가면 된다.&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;: nproc 또는 SHOW max_parallel_workers. DB 인스턴스 vCPU 기준이다. 컨테이너라면 cgroup limit도 함께 보아야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;워크로드 분류&lt;/b&gt;: 한 트랜잭션의 CPU vs I/O 비율. OLTP라면 I/O 비중이 크고(풀 6~16배), OLAP나 계산 위주라면 CPU 바운드(풀 1~4배)이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;시작값 박기&lt;/b&gt;: cores &amp;times; 4부터 시작한다. 8코어라면 풀 32. 이것이 한국 개발자들이 흔히 하는 max_connections=200을 박는 것보다 거의 항상 안전하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부하 측정&lt;/b&gt;: pgbench나 sysbench로 풀 사이즈 &amp;plusmn;2배를 스윕한다. 본 글의 실험처럼 1, 2, 4, 8, 16, 32, 64 스윕을 권장한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 확인&lt;/b&gt;: 평균 지연이 아니라 p99가 평탄해지는 마지막 구간이 진짜 스위트 스폿이다. p50만 보면 풀 256이 좋아 보이지만 p99가 16배 폭발하는 것을 알 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 5단계를 거치면 (cores*2)+spindles 공식만 박은 사람보다 항상 빠른 시스템을 얻는다. 직접 측정한 1~256 풀 스윕(2026-05-03) 결과가 그것을 증명한다. 풀이 너무 작으면 큐에서 죽고, 풀이 너무 크면 락에서 죽는다. 둘 사이의 평탄 구간이 자신의 환경에서 어디에 있는지는 결국 측정해야 알 수 있다. 위키 번역 글 100개를 읽는 것보다 자신의 DB에 pgbench를 한 번 돌리는 편이 훨씬 가치 있다. PostgreSQL 커넥션 풀 사이즈 튜닝은 신앙이 아니라 실험이다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>HikariCP 풀 사이즈</category>
      <category>max_connections 200</category>
      <category>PgBouncer 커넥션 풀</category>
      <category>postgres connection pool size</category>
      <category>PostgreSQL 커넥션 풀 적정 크기</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/410</guid>
      <comments>https://yscho03.tistory.com/410#entry410comment</comments>
      <pubDate>Sun, 3 May 2026 23:10:49 +0900</pubDate>
    </item>
    <item>
      <title>로그 적재에 큐(Queue)가 진정 필요한가? 5만 건으로 비교했다</title>
      <link>https://yscho03.tistory.com/409</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 로그는 카프카나 래빗MQ로 받아야 한다는 식의 주장이 통용된다. 그러나 정말 그러한가? &lt;b&gt;로그 큐 필요성&lt;/b&gt;을 한 번이라도 의심해본 사람이라면 &quot;이 계층을 빼도 되지 않을까&quot; 하는 의문을 품어봤을 것이다. 큐 클러스터를 깔고 운영하는 비용이 만만치 않음에도, 제거 가능 여부에 대한 정량 근거를 찾기는 의외로 어렵다.&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;그래서 직접 측정했다. Go 1.22로 동일한 sink 앞에 direct goroutine 방식과 bounded queue + 워커 풀 방식을 두고 각각 5만 건을 흘렸다. 처리량, P99 지연, heap 피크, goroutine 수까지 모두 측정했다. 결과는 예상보다 명확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewjjpw/dJMcaib9iYy/B72a9EThPEK1twDbYunwxk/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewjjpw/dJMcaib9iYy/B72a9EThPEK1twDbYunwxk/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewjjpw/dJMcaib9iYy/B72a9EThPEK1twDbYunwxk/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fewjjpw%2FdJMcaib9iYy%2FB72a9EThPEK1twDbYunwxk%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;301&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.geeksforgeeks.org/system-design/message-queues-system-design/&quot;&gt;geeksforgeeks&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론부터 정리한다&lt;/h2&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;b&gt;항상&lt;/b&gt; 필요한 것은 아니다. 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sink 지연이 평탄하고 트래픽이 튀지 않으면 direct goroutine만으로도 운용 가능하다&lt;/li&gt;
&lt;li&gt;다만 &lt;b&gt;버스트 + sink 스파이크 + 손실 불허&lt;/b&gt; 조합이라면 큐 + 배치가 압도적이다&lt;/li&gt;
&lt;li&gt;큐를 도입한다고 자동으로 빨라지지 않는다. &lt;b&gt;본질은 배치 처리에 있다.&lt;/b&gt; batch=1 큐는 direct와 큰 차이가 없었다&lt;/li&gt;
&lt;li&gt;운영 안정성 관점에서는 bounded queue가 무조건 유리하다. goroutine 폭주를 차단해주기 때문이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 수치만 짚으면, direct_bg 방식은 P99 e2e 지연이 16,109,864&amp;micro;s(약 16초)였고, queue + cap 65,536 + batch 64 조합은 동일한 5만 건을 무손실로 처리하면서도 P99가 262,820&amp;micro;s(약 0.26초)로 &lt;b&gt;약 60배 차이&lt;/b&gt;가 났다. heap_max 또한 31.6MB vs 24.1MB, goroutine_max는 48,704개 vs 6개로 마무리됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 가지 패턴, 무엇이 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;direct_bg &amp;mdash; 요청마다 goroutine을 던지는 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 단순한 fire-and-forget 구조이다. 핸들러가 요청을 받으면 그저 go func() { sink.Write(log) }()를 던지고 응답한다. 인프라 추가 비용이 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777792569116&quot; class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;// 의사 코드
func handler(w http.ResponseWriter, r *http.Request) {
    log := buildLog(r)
    go sink.Write(log) // fire-and-forget
    w.WriteHeader(200)
}
&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;b&gt;sink가 막히는 순간 goroutine이 무제한으로 누적된다.&lt;/b&gt; heap이 따라 올라가고, GC 압박이 가중되며, 종국에는 OOM 위험까지 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;queue &amp;mdash; bounded 채널 + 워커 풀 + 배치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;producer-consumer 패턴이다. 핸들러는 채널에 enqueue만 하고 반환된다. 워커 N개가 채널에서 항목을 빼내 배치로 묶어 sink에 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777792569116&quot; class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;// 의사 코드
ch := make(chan Log, cap)  // bounded
for i := 0; i &amp;lt; workers; i++ {
    go worker(ch, batchSize)
}
func handler(...) {
    select {
    case ch &amp;lt;- log: // 인큐
    default: // 풀 차면 드롭
    }
}
&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;장점은 백프레셔가 자연스럽게 형성된다는 점이다. cap이 한계로 작용하므로 메모리도 폭주하지 않으며, 배치 인서트로 sink 호출 횟수도 감소한다. 단점은 cap이 차면 드롭이 발생한다는 것과, 인큐 지연이 약간 추가된다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실험 환경 &amp;mdash; 동일 조건으로 통제했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비교의 핵심은 &quot;동일한 sink 앞에서 구조 차이만 관찰한다&quot;는 데 있다. 따라서 환경을 엄격히 통제했다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;값&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;런타임&lt;/td&gt;
&lt;td&gt;golang:1.22-bookworm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GOMAXPROCS&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리&lt;/td&gt;
&lt;td&gt;1024MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;네트워크&lt;/td&gt;
&lt;td&gt;deny (인프로세스 시뮬레이션)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sink 커넥션 풀&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sink 기본 쓰기&lt;/td&gt;
&lt;td&gt;200&amp;micro;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sink 스파이크&lt;/td&gt;
&lt;td&gt;1% 확률로 5,000&amp;micro;s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;발행 건수&lt;/td&gt;
&lt;td&gt;모드별 50,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비교 변수&lt;/td&gt;
&lt;td&gt;cap &amp;isin; {256, 4096, 65536} &amp;times; batch &amp;isin; {1, 64}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜일까? 인프로세스 sink로 시뮬레이션한 이유는 네트워크 변동성을 제거하고 &lt;b&gt;구조 차이만 노출시키기 위함&lt;/b&gt;이다. 실제 ClickHouse나 RabbitMQ로 돌리면 네트워크 RTT 편차가 너무 커서, 어느 부분이 구조 비용이고 어느 부분이 네트워크 비용인지 분리할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;측정 결과 &amp;mdash; 수치가 모든 것을 말한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리량 비교 &amp;mdash; 본질은 배치다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stdout 출력을 그대로 옮기면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777792569117&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;mode           cap batch  wkr    prod    drop   wrote     thr/s
direct_bg        0     1    0   50000       0   50000      3048
queue          256     1    4     276   49724     276    538813
queue          256    64    4    1152   48848    1152   5923397
queue         4096     1    4    4112   45888    4112     36645
queue         4096    64    4    5376   44624    5376   1961769
queue        65536     1    4   50000       0   50000      3056
queue        65536    64    4   50000       0   50000    184306
&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;b&gt;batch=1짜리 큐(cap=65,536, 무손실)는 처리량이 3,056/s로 direct_bg(3,048/s)와 사실상 동일하다.&lt;/b&gt; 큐를 도입한다고 자동으로 빨라지지 않는다는 의미다.&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;그러나 동일한 cap=65,536에 batch=64를 적용하면 처리량이 184,306/s로 &lt;b&gt;약 60배&lt;/b&gt; 도약한다. 이는 sink 호출 횟수를 1/64로 축소했음을 의미한다. ClickHouse나 BigQuery 같은 컬럼 스토어 적재 시 진정 체감되는 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;e2e 지연 분포 &amp;mdash; direct_bg가 무너지는 지점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P50/P95/P99/Max만 별도로 추출하여 비교하면 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;mode&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;batch&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;e2e P50 &amp;micro;s&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;e2e P95 &amp;micro;s&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;e2e P99 &amp;micro;s&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;e2e Max &amp;micro;s&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct_bg&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8,114,189.8&lt;/td&gt;
&lt;td&gt;15,434,066.8&lt;/td&gt;
&lt;td&gt;16,109,864.0&lt;/td&gt;
&lt;td&gt;16,279,736.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;2,415.8&lt;/td&gt;
&lt;td&gt;6,486.7&lt;/td&gt;
&lt;td&gt;6,491.0&lt;/td&gt;
&lt;td&gt;6,492.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;13,111.3&lt;/td&gt;
&lt;td&gt;20,608.4&lt;/td&gt;
&lt;td&gt;20,838.2&lt;/td&gt;
&lt;td&gt;20,855.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8,048,918.6&lt;/td&gt;
&lt;td&gt;15,510,242.5&lt;/td&gt;
&lt;td&gt;16,196,630.0&lt;/td&gt;
&lt;td&gt;16,355,595.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;126,913.8&lt;/td&gt;
&lt;td&gt;250,858.5&lt;/td&gt;
&lt;td&gt;262,820.4&lt;/td&gt;
&lt;td&gt;266,307.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;direct_bg는 P50조차 8초를 넘는다. sink가 4-conn 풀로 묶여 있어 in-flight goroutine 4만 8천 개가 그대로 줄 서 있는 상황이다. 1% 5ms 스파이크가 누적되면서 tail이 16초까지 끌려간다.&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;반면 queue + cap 65,536 + batch 64는 동일한 무손실 조건에서 P99가 262,820&amp;micro;s에 머문다. &lt;b&gt;약 60배 빠른 셈이다.&lt;/b&gt;&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;다만 흥미로운 지점이 또 있다. queue + cap 65,536 + batch 1은 direct_bg와 거의 동일한 P99(16,196,630&amp;micro;s)를 보인다. 배치가 없으면 큐 자체로는 sink 처리속도 한계를 돌파하지 못한다는 의미다. &lt;b&gt;cap만 키워도 batch를 묶지 않으면 무의미하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;*중요* 해석&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;줄별&amp;nbsp;해석 &lt;br /&gt;&lt;br /&gt;①&amp;nbsp;direct_bg&amp;nbsp;&amp;mdash;&amp;nbsp;무너진&amp;nbsp;케이스 &lt;br /&gt;&lt;br /&gt;P50조차&amp;nbsp;8초다.&amp;nbsp;절반의&amp;nbsp;요청이&amp;nbsp;8초&amp;nbsp;이상&amp;nbsp;걸렸다는&amp;nbsp;뜻이다.&amp;nbsp;sink는&amp;nbsp;4-conn&amp;nbsp;풀인데&amp;nbsp;그&amp;nbsp;앞에&amp;nbsp;in-flight &lt;br /&gt;goroutine이&amp;nbsp;4만&amp;nbsp;8천&amp;nbsp;개&amp;nbsp;줄&amp;nbsp;서&amp;nbsp;있는&amp;nbsp;상황이라,&amp;nbsp;사실상&amp;nbsp;sink&amp;nbsp;처리율&amp;nbsp;한계에&amp;nbsp;그대로&amp;nbsp;묶인다.&amp;nbsp;P95에서&amp;nbsp;Max까지 &lt;br /&gt;차이가&amp;nbsp;작은&amp;nbsp;이유는&amp;nbsp;안정적이어서가&amp;nbsp;아니다.&amp;nbsp;거의&amp;nbsp;모든&amp;nbsp;요청이&amp;nbsp;똑같이&amp;nbsp;막혀서&amp;nbsp;똑같이&amp;nbsp;늦어진&amp;nbsp;결과다. &lt;br /&gt;&lt;br /&gt;②&amp;nbsp;queue&amp;nbsp;cap=256,&amp;nbsp;batch=64&amp;nbsp;&amp;mdash;&amp;nbsp;가장&amp;nbsp;빠른&amp;nbsp;케이스 &lt;br /&gt;&lt;br /&gt;P99&amp;nbsp;6.5ms로&amp;nbsp;표&amp;nbsp;안에서&amp;nbsp;압도적&amp;nbsp;1등이다.&amp;nbsp;다만&amp;nbsp;cap이&amp;nbsp;256으로&amp;nbsp;작은&amp;nbsp;만큼&amp;nbsp;드롭률이&amp;nbsp;따로&amp;nbsp;기록되는데, &lt;br /&gt;본문에서는&amp;nbsp;4만&amp;nbsp;9,724건이&amp;nbsp;drop된다고&amp;nbsp;명시한다.&amp;nbsp;빠른&amp;nbsp;대신&amp;nbsp;받지&amp;nbsp;못한&amp;nbsp;요청은&amp;nbsp;통째로&amp;nbsp;버려지는&amp;nbsp;셈이라, &lt;br /&gt;&quot;속도&amp;nbsp;vs&amp;nbsp;손실&quot;&amp;nbsp;트레이드오프가&amp;nbsp;극단으로&amp;nbsp;기운&amp;nbsp;케이스다. &lt;br /&gt;&lt;br /&gt;③&amp;nbsp;queue&amp;nbsp;cap=4,096,&amp;nbsp;batch=64&amp;nbsp;&amp;mdash;&amp;nbsp;균형점 &lt;br /&gt;&lt;br /&gt;P99&amp;nbsp;20.8ms.&amp;nbsp;cap을&amp;nbsp;16배&amp;nbsp;키운&amp;nbsp;덕분에&amp;nbsp;드롭이&amp;nbsp;줄어드는&amp;nbsp;대신&amp;nbsp;큐&amp;nbsp;대기&amp;nbsp;시간이&amp;nbsp;약간&amp;nbsp;더&amp;nbsp;붙는다.&amp;nbsp;배치&amp;nbsp;효과는 &lt;br /&gt;그대로&amp;nbsp;살아&amp;nbsp;있어서&amp;nbsp;절대&amp;nbsp;지연은&amp;nbsp;여전히&amp;nbsp;매우&amp;nbsp;짧다. &lt;br /&gt;&lt;br /&gt;④&amp;nbsp;queue&amp;nbsp;cap=65,536,&amp;nbsp;batch=1&amp;nbsp;&amp;mdash;&amp;nbsp;&quot;큐만&amp;nbsp;키워도&amp;nbsp;소용&amp;nbsp;없다&quot;의&amp;nbsp;증거 &lt;br /&gt;&lt;br /&gt;P50&amp;nbsp;8초.&amp;nbsp;direct_bg와&amp;nbsp;거의&amp;nbsp;동일한&amp;nbsp;결과다.&amp;nbsp;큐가&amp;nbsp;무한히&amp;nbsp;받아내더라도&amp;nbsp;워커가&amp;nbsp;sink에&amp;nbsp;단건씩&amp;nbsp;호출하면&amp;nbsp;sink &lt;br /&gt;처리율(약&amp;nbsp;3,000건/s)이&amp;nbsp;그대로&amp;nbsp;천장이&amp;nbsp;된다.&amp;nbsp;본문이&amp;nbsp;강조하는&amp;nbsp;핵심도&amp;nbsp;여기에&amp;nbsp;있다.&amp;nbsp;cap만&amp;nbsp;키우고&amp;nbsp;batch를 &lt;br /&gt;묶지&amp;nbsp;않으면&amp;nbsp;의미가&amp;nbsp;없다. &lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;⑤&amp;nbsp;queue&amp;nbsp;cap=65,536,&amp;nbsp;batch=64&amp;nbsp;&amp;mdash;&amp;nbsp;드롭&amp;nbsp;0의&amp;nbsp;견실한&amp;nbsp;케이스&lt;/b&gt; &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;P99&amp;nbsp;263ms.&amp;nbsp;②④와&amp;nbsp;비교하면&amp;nbsp;&quot;드롭&amp;nbsp;없이&amp;nbsp;모두&amp;nbsp;받으면서&amp;nbsp;sink&amp;nbsp;호출은&amp;nbsp;1/64로&amp;nbsp;줄임&quot;&amp;nbsp;조합이다.&amp;nbsp;절대값은&amp;nbsp;②③보다 &lt;br /&gt;느리지만&amp;nbsp;단&amp;nbsp;한&amp;nbsp;건도&amp;nbsp;버리지&amp;nbsp;않는다는&amp;nbsp;점이&amp;nbsp;핵심&amp;nbsp;가치다.&amp;nbsp;처리량으로는&amp;nbsp;184,306건/s로&amp;nbsp;본문에서&amp;nbsp;1등을 &lt;br /&gt;차지한다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;한&amp;nbsp;문장&amp;nbsp;요약&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;직접&amp;nbsp;던지면&amp;nbsp;sink&amp;nbsp;풀이&amp;nbsp;병목이&amp;nbsp;되어&amp;nbsp;16초까지&amp;nbsp;끌리고,&amp;nbsp;큐만&amp;nbsp;키우면&amp;nbsp;sink&amp;nbsp;호출&amp;nbsp;빈도가&amp;nbsp;새&amp;nbsp;병목이&amp;nbsp;되며, &lt;br /&gt;batch=64로&amp;nbsp;묶어야&amp;nbsp;비로소&amp;nbsp;60배&amp;nbsp;처리량과&amp;nbsp;안정적인&amp;nbsp;tail이&amp;nbsp;같이&amp;nbsp;나온다.&amp;nbsp;그게&amp;nbsp;②③⑤의&amp;nbsp;본질이다. &lt;br /&gt;&lt;br /&gt;&lt;b&gt;곁다리:&amp;nbsp;P95-P99-Max가&amp;nbsp;거의&amp;nbsp;같은&amp;nbsp;케이스의&amp;nbsp;의미&lt;/b&gt; &lt;br /&gt;&lt;br /&gt;queue+batch&amp;nbsp;케이스(②③⑤)는&amp;nbsp;P95에서&amp;nbsp;Max까지&amp;nbsp;차이가&amp;nbsp;1~6%에&amp;nbsp;불과하다.&amp;nbsp;이는&amp;nbsp;tail이&amp;nbsp;평탄하다는&amp;nbsp;뜻으로,&amp;nbsp;운 &lt;br /&gt;나쁜&amp;nbsp;1%&amp;nbsp;요청도&amp;nbsp;평소&amp;nbsp;요청과&amp;nbsp;큰&amp;nbsp;차이&amp;nbsp;없이&amp;nbsp;처리됐음을&amp;nbsp;의미한다.&amp;nbsp;운영&amp;nbsp;관점에서&amp;nbsp;매우&amp;nbsp;좋은&amp;nbsp;신호다.&amp;nbsp;예측 &lt;br /&gt;가능한&amp;nbsp;지연은&amp;nbsp;곧&amp;nbsp;SLO&amp;nbsp;잡기가&amp;nbsp;수월하다는&amp;nbsp;뜻이기도&amp;nbsp;하다. &lt;br /&gt;&lt;br /&gt;반면&amp;nbsp;direct_bg와&amp;nbsp;④도&amp;nbsp;P95에서&amp;nbsp;Max까지&amp;nbsp;가깝지만,&amp;nbsp;이쪽은&amp;nbsp;정반대&amp;nbsp;이유다.&amp;nbsp;모두&amp;nbsp;골고루&amp;nbsp;망한&amp;nbsp;결과다.&amp;nbsp;같은 &lt;br /&gt;수치라도&amp;nbsp;의미가&amp;nbsp;정반대인&amp;nbsp;경우가&amp;nbsp;있으니,&amp;nbsp;분포를&amp;nbsp;볼&amp;nbsp;때는&amp;nbsp;항상&amp;nbsp;절대값과&amp;nbsp;함께&amp;nbsp;읽어야&amp;nbsp;한다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;heap_max_mb / goroutine_max &amp;mdash; 운영 안정성의 진짜 차이&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;mode&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;cap&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;batch&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;dropped&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;heap_max_mb&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;goroutine_max&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;direct_bg&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;31.59&lt;/td&gt;
&lt;td&gt;48,704&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;49,724&lt;/td&gt;
&lt;td&gt;22.11&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;44,624&lt;/td&gt;
&lt;td&gt;22.24&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;24.11&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queue&lt;/td&gt;
&lt;td&gt;65,536&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;24.11&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;direct_bg는 goroutine_max가 &lt;b&gt;48,704개&lt;/b&gt;까지 치솟는다. 5만 건을 처리하는 동안 거의 전부가 살아 있었다는 뜻이다. heap도 31.59MB로 가장 컸다. 부하가 더 커지거나 sink가 더 느렸다면 그대로 OOM으로 직행했을 가능성이 크다.&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;queue 모드는 워커 4개에 cap이 상한으로 작용해 goroutine_max가 &lt;b&gt;6개&lt;/b&gt;로 평탄하다. heap도 22~24MB대로 안정적이다. 운영 환경에서 &quot;갑작스러운 메모리 폭증으로 컨테이너 재시작&quot; 같은 이슈가 발생하지 않는다는 의미다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인큐 지연 (큐 모드 한정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐 모드에서 enqueue P99는 모든 조합에서 &lt;b&gt;0.1&amp;micro;s&lt;/b&gt;다. 사실상 공짜에 가깝다. enqueue Max도 17.4~141.9&amp;micro;s로 무시할 만한 수준이다.&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;이 수치가 의미하는 바는 &quot;큐에 넣는 비용이 부담스러워 못 쓴다&quot;는 우려가 거의 근거 없다는 것이다. 채널은 진정 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;cap 설정의 의미 &amp;mdash; 흡수량과 메모리, 드롭의 트레이드오프&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실험에서 cap을 256 &amp;rarr; 4,096 &amp;rarr; 65,536으로 변경하며 관찰한 결과, cap의 의미가 명확해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;cap 256, batch 1&lt;/b&gt;: 49,724건 드롭. 풀이 너무 작아 producer가 거의 잘려나간다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cap 4,096, batch 64&lt;/b&gt;: 44,624건 드롭. 여전히 풀이 부족하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;cap 65,536, batch 64&lt;/b&gt;: 0건 드롭. 5만 건을 모두 흡수한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;cap은 &lt;b&gt;&quot;버스트를 얼마나 흡수할지의 단위&quot;&lt;/b&gt;다. 너무 작으면 드롭으로 직결되고, 너무 크면 메모리 낭비 + 큐가 길어질수록 e2e 지연도 함께 증가한다. cap=65,536 + batch=1처럼 누적만 되고 빠지지 않으면 P99가 16초까지 치솟는 양상을 그대로 확인했다.&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;실무 룰은 단순하다. &lt;b&gt;cap = 예상 burst peak rate &amp;times; drain time&lt;/b&gt;이다. drain time은 워커수 &amp;times; 배치당 처리시간으로 산출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 큐, 도입해야 하는가 말아야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;로그 적재에 큐가 반드시 필요한 것은 아니다. sink 지연이 안정적이고 트래픽이 평탄하면 direct goroutine만으로 충분하다. 다만 버스트가 크고 손실이 허용되지 않으면 bounded queue + 워커 풀 + 배치 인서트가 압도적으로 유리하다.&lt;/blockquote&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐를 빼도 되는 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sink 지연이 안정적이다 (P99/P50 비율이 3 이하)&lt;/li&gt;
&lt;li&gt;트래픽이 평탄하다 (피크/평균 비율이 작고, 버스트가 거의 없음)&lt;/li&gt;
&lt;li&gt;약간의 로그 손실이 허용된다 (혹은 sink 자체가 신뢰성을 보장함)&lt;/li&gt;
&lt;li&gt;운영 인력이 적고 인프라를 단순하게 유지하고자 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 조건이라면 direct_bg + 적절한 goroutine 제한 패턴(semaphore로 동시 in-flight 묶기)이 진정 충분하다. 인프라를 절약하고 코드를 단순하게 가져가는 편이 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;큐를 도입해야 하는 조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sink 스파이크가 자주 발생한다 (DB 락, 외부 API rate limit, GC 등)&lt;/li&gt;
&lt;li&gt;트래픽 버스트가 크다 (피크/평균 &amp;gt; 5)&lt;/li&gt;
&lt;li&gt;로그 손실이 허용되지 않는다 (감사 로그, 결제 로그, 에러 추적 로그)&lt;/li&gt;
&lt;li&gt;배치 인서트로 sink 비용을 절감해야 한다 (ClickHouse, BigQuery 같은 컬럼 스토어나 S3 객체 스토리지에 Parquet 적재)&lt;/li&gt;
&lt;li&gt;메모리&amp;middot;goroutine 안정성이 중요하다 (장기 실행 컨테이너)&lt;/li&gt;
&lt;/ul&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;b&gt;다만 반드시 카프카&amp;middot;래빗MQ까지 갈 필요는 없다.&lt;/b&gt; 다음 절을 보아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;중간 지점 &amp;mdash; 가벼운 인메모리 큐로 80% 효과&lt;/h3&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;b&gt;굳이 외부 큐 시스템을 도입하지 않아도 채널 + 워커만으로 80% 효과를 본다는 점&lt;/b&gt;이다. 동일 프로세스 안의 Go 채널이 cap 65,536 + batch 64 조건에서 5만 건 무손실 처리에 P99 262ms를 기록했다. RabbitMQ 클러스터 운영 비용을 빼고 산출한 수치다.&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;물론 단점은 있다. 프로세스가 죽으면 큐에 있던 데이터는 모두 소실된다. 따라서 손실이 절대 허용되지 않는 결제&amp;middot;감사 로그라면 외부 영속 큐가 필요하다. 그러나 트래픽 메트릭, 일반 액세스 로그, 추적용 이벤트 로그라면 인메모리 큐로 충분하다.&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;비용 관점에서 보면 카프카 클러스터는 최소 3노드를 띄워야 안정적이고, 그 운영 부담 + 인프라 비용이 만만치 않다. 인메모리 큐는 코드 200줄 미만으로 마무리된다. ROI를 따져보면 작은 서비스 입장에서는 인메모리가 답인 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서 어떻게 결정할 것인가 &amp;mdash; 체크리스트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 결정하기 전에 다음 4가지를 점검하면 된다.&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;/li&gt;
&lt;li&gt;&lt;b&gt;sink P99/P50 비율을 잰 적이 있는가?&lt;/b&gt; 3 이하라면 direct로 충분하고, 5 이상이라면 큐가 필요하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그 한 건이 유실됐을 때의 비즈니스 영향이 정의되어 있는가?&lt;/b&gt; 정량적으로 답할 수 있어야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영 인력이 카프카&amp;middot;래빗MQ 클러스터를 감당할 수 있는가?&lt;/b&gt; 솔직하게 답해야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4개 모두 &quot;글쎄&quot;라면 우선 direct_bg에 semaphore를 걸어 시작하고, 모니터링을 붙여 실제로 문제가 발생하면 채널 기반 큐로 이행하는 편이 합리적이다. 처음부터 카프카부터 도입하는 것은 90% 케이스에서 오버킬이다.&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;4개 중 3개 이상이 명확하게 &quot;큐가 필요하다&quot;라면 그때 설계에 들어가면 된다. 그것도 외부 큐로 가기 전에 인메모리 큐로 먼저 검증해보는 편이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리 &amp;mdash; 로그 큐 필요성, 측정으로 답한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심을 3줄로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐는 도구이지 신앙이 아니다. 조건이 부합하면 direct goroutine으로도 충분히 운용된다&lt;/li&gt;
&lt;li&gt;큐의 진정한 가치는 처리량 자체보다 &lt;b&gt;백프레셔 + 메모리 안정성 + 배치 인서트&lt;/b&gt; 효과에 있다&lt;/li&gt;
&lt;li&gt;외부 큐 시스템으로 가기 전에 &lt;b&gt;인메모리 채널 + 워커 풀 + 배치&lt;/b&gt;로 충분한지 먼저 검증해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 운영 중인 로그 파이프라인이 있다면 P99를 한 번 측정해보길 권한다. &lt;b&gt;로그 큐 필요성&lt;/b&gt;은 추상적인 베스트 프랙티스가 아니라 측정 가능한 트래픽 특성에서 답이 도출되는 문제다. 5만 건을 돌려본 데이터가 보여주는 바가 그것이다. 큐가 답인지 아닌지는 본인 트래픽 측정 데이터가 결정한다.&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;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Backend</category>
      <category>Go goroutine 로그 전송</category>
      <category>Kafka vs 직접 호출</category>
      <category>RabbitMQ 로그 수집</category>
      <category>배치 인서트</category>
      <category>백프레셔</category>
      <category>비동기 로그 처리</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/409</guid>
      <comments>https://yscho03.tistory.com/409#entry409comment</comments>
      <pubDate>Sun, 3 May 2026 16:16:41 +0900</pubDate>
    </item>
    <item>
      <title>회사 다니기 싫은 것이 번아웃인가 보어아웃인가? 직장인 자가진단 정리</title>
      <link>https://yscho03.tistory.com/408</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;469&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tPL6k/dJMcagk3CuA/53JPk95VYJLS4Z90Caao6K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tPL6k/dJMcagk3CuA/53JPk95VYJLS4Z90Caao6K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tPL6k/dJMcagk3CuA/53JPk95VYJLS4Z90Caao6K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtPL6k%2FdJMcagk3CuA%2F53JPk95VYJLS4Z90Caao6K%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;704&quot; height=&quot;469&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;469&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.jiffy.com/design/exhausted-office-worker-collapses-on-desk-in-burnout-19345809.html&quot;&gt;jiffy.com&lt;/a&gt;&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;월요일 아침에 발이 떨어지지 않는 직장인은 매우 많다. 그러나 그 이유가 모두 같지는 않다. 누군가는 일이 너무 많아서 타들어가고 있고, 누군가는 일이 너무 없어서 굳어가고 있다. 전자가 번아웃이고 후자가 &lt;b&gt;보어아웃&lt;/b&gt;이다. 이 둘의 차이를 모른 채 그저 &quot;회사 가기 싫다&quot;는 한마디로 뭉뚱그리면 처방이 정반대로 나가서 상태가 더 악화된다.&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;요즘 직장인 사이에서 번아웃은 누구나 알지만, 보어아웃은 처음 듣는 사람도 적지 않다. 한국 대기업, 공무원, 정년 보장 직군에서는 보어아웃이 번아웃보다 더 흔한 경우도 많다. 둘 다 회사를 싫어지게 만드는 점은 동일하지만 해법은 정반대로 갈린다. 이 글에서는 &lt;b&gt;번아웃 보어아웃 차이&lt;/b&gt;부터 짚어보고, 직장인 시나리오 몇 가지로 본인 상태를 가늠해 보며, 자가진단 체크리스트와 단계별 대처법까지 한 번에 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일단 번아웃과 보어아웃이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;용어를 헷갈리는 사람이 많은데, 출발점부터 다르다. 한쪽은 1970년대에 명명되었고 다른 쪽은 2007년에 정의되었다. 둘 다 학술 용어이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;번아웃: 일이 너무 많아서 타버리는 현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번아웃(burnout)이라는 말은 1974년에 미국 심리학자 헤르베르트 프로이덴버거가 처음 붙인 명칭이다. 마약중독 클리닉에서 일하던 의료진들이 환자에게 무관심해지고 냉소적으로 변하는 모습을 보고, &quot;이 사람들이 모두 타버렸다&quot;는 의미에서 번아웃이라고 이름 붙였다.&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;세계보건기구(WHO)는 2019년 ICD-11(국제질병분류 11판)에 번아웃을 정식으로 등재했다. 다만 &quot;질병&quot;이 아니라 &quot;직업 관련 현상(occupational phenomenon)&quot;으로 분류한다. 정신질환은 아니지만 직업 환경에서 발생하는 명확한 건강 문제로 인정한 것이다.&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;WHO가 정의한 번아웃은 크게 정서적 고갈, 냉소와 거리두기, 성취감 저하의 세 갈래로 나뉜다. 정서적 고갈은 에너지가 모두 빠진 느낌에 만성 피로가 동반되는 상태이고, 냉소와 거리두기는 일이나 동료에 대한 감정이 메말라가는 단계이다. 성취감 저하는 무엇을 해도 의미 없게 느껴지는 것이다.&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;쉽게 말해 일이 너무 많고 너무 빡세서 양초처럼 다 타버린 상태다. 코르티솔(스트레스 호르몬) 수치가 만성적으로 올라가고 면역력이 떨어지며 잠도 오지 않는다. IT 개발자, 의료진, 광고 기획자, 스타트업 직원들에게 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;보어아웃: 일이 없어서 굳어가는 현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZfd3M/dJMcagk3CuB/mNXNamTolP0358FO6JVzkk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZfd3M/dJMcagk3CuB/mNXNamTolP0358FO6JVzkk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZfd3M/dJMcagk3CuB/mNXNamTolP0358FO6JVzkk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZfd3M%2FdJMcagk3CuB%2FmNXNamTolP0358FO6JVzkk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;608&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://unsplash.com/photos/man-with-dreadlocks-holding-head-at-desk-with-laptop-PsV8ypwsd-0&quot;&gt;https://unsplash.com/photos/man-with-dreadlocks-holding-head-at-desk-with-laptop-PsV8ypwsd-0&lt;/a&gt;&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;보어아웃(boreout)은 2007년 스위스 컨설턴트 페터 베르더(Peter Werder)와 필리프 로틀린(Philippe Rothlin)이 책 &quot;Diagnose Boreout&quot;에서 처음 정의한 개념이다. 번아웃의 정확히 반대 개념이다.&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;베르더가 제시한 보어아웃의 구성 요소는 권태(Boredom), 무관심(Disinterest), 저요구(Underchallenge)이다. 할 일이 없거나 의미 없을 때 권태가 오고, 일에 흥미가 생기지 않으면서 무관심이 깊어지며, 본인 능력 대비 너무 시시한 업무만 하다 보면 저요구 상태로 굳어간다.&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;증상은 번아웃과 비슷해 보이지만 원인은 정반대다. 일이 너무 적거나, 너무 단순하거나, 본인 능력에 비해 너무 시시할 때 발생한다. 흥미로운 점은 겉으로는 바빠 보이려고 가짜 야근을 하고 메일을 천천히 답하며 회의를 늘리는 행동이 동반된다는 사실이다. 본인조차 자신이 보어아웃인 줄 모르고 &quot;내가 우울증인가&quot; 의심하는 경우도 많다.&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;한국에서 최근 보어아웃이 부상하는 데에는 이유가 있다. 정년 보장 직군(공무원, 공기업), 대기업 잉여 부서, 자동화로 업무량이 줄어든 사무직 등에서 늘고 있다. &quot;회사는 좋은데 일이 없어서 미치겠다&quot;는 호소가 보어아웃의 전형적 증상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;둘 다 회사 가기 싫은 것인데 무엇이 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번아웃과 보어아웃은 둘 다 결과적으로 &quot;월요일 출근이 싫다&quot;, &quot;내가 왜 이 일을 하고 있는가&quot;라는 생각으로 이어진다. 그러나 원인과 처방이 정반대다. 잘못 진단하면 증상이 더 심해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;번아웃 vs 보어아웃 한눈에 비교표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;번아웃&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;보어아웃&lt;/b&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;일이 너무 적거나 시시함&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;두통, 불면, 위장 장애&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;시간이 너무 안 감&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;IT, 의료, 광고, 스타트업&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;똑같이 텅 빈 느낌, 허무함&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;/tbody&gt;
&lt;/table&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;b&gt;번아웃은 일을 빼야 회복되고, 보어아웃은 일을 채워야 회복된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일을 줄여야 하는가, 늘려야 하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번아웃 환자에게 &quot;더 의미 있는 일을 찾아야 한다&quot;고 조언하면 상태는 더욱 악화된다. 이미 과부하 상태인데 일을 더 던지는 셈이라 코르티솔이 폭발할 뿐이다. 진짜 번아웃은 일단 빠져 쉬어야 한다.&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;반대로 보어아웃 환자에게 &quot;일이 많아서 그런 것이니 좀 쉬어라&quot;라고 말하면 그 또한 망친다. 보어아웃은 안 쉬어서 생기는 것이 아니라 안 쓰여서 생기는 것이다. 더 쉬게 하면 무기력만 깊어진다. 의미 있는 도전이나 새 프로젝트가 들어와야 풀린다.&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;본인이 어느 쪽인지 모르면 정반대 처방을 스스로에게 내릴 수 있다. 그래서 자가진단부터 해 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직장인 시나리오로 본인 상태 가늠해 보기&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 1, 월요일 출근길에 발이 떨어지지 않을 때&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보어아웃 쪽이라면 막상 회사에 가도 할 일이 없다는 것을 알면서도 가기 싫다. 의미 없는 8시간을 어떻게 또 때울지 답이 보이지 않아 발이 움직이지 않는다.&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;둘 다 발이 떨어지지 않지만 머릿속 시뮬레이션은 다르다. 번아웃은 &quot;또 시작이다&quot;, 보어아웃은 &quot;또 무엇을 하지&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 2, 오후 3시에 멍하니 시간만 볼 때&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보어아웃 쪽은 할 일을 진작 끝냈거나 애초에 없었다. 시계만 보면서 &quot;6시까지 어떻게 버틸 것인가&quot;를 생각한다. 죄책감보다 권태감이 더 크다.&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;오후 3시의 멍 때림이 번아웃이라면 에너지 고갈이고, 보어아웃이라면 자극 부재이다. 같은 행동이지만 원인은 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 3, 회식 다녀온 다음 날 아침&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번아웃 쪽은 안 그래도 피곤한데 회식까지 다녀와서 영혼이 분리된다. 출근하기 싫은 정도가 100배가 된다. 보어아웃 쪽은 회식 자리에서는 그래도 사람들과 떠들면서 살아 있다는 느낌을 받았는데, 다음 날 사무실에 들어오면 그 대비 때문에 더 무기력해진다.&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;번아웃은 회식이 추가 부담이고, 보어아웃은 회식이 짧은 도파민이었음을 자각하는 차이이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 4, 일요일 저녁의 묘한 우울감&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번아웃 쪽이라면 일요일 저녁 8시쯤 가슴이 조이는 느낌이 온다. 내일 출근을 생각하면 심장 박동이 빨라진다. 흔히 말하는 샌데이 블루(Sunday Blue)의 전형적 패턴이다.&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;보어아웃 쪽도 일요일 저녁이 우울한 점은 같지만 결이 좀 다르다. &quot;내일 또 시간을 죽이러 가야 한다&quot;는 허무함이 깔린다. 분노보다 체념이 크다.&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;번아웃의 일요일 저녁은 공포에 가깝고, 보어아웃의 일요일 저녁은 허무에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번아웃 자가진단 체크리스트 (10문항)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매슬랙 번아웃 척도(MBI: Maslach Burnout Inventory)를 단순화한 버전이다. 최근 2주 기준으로 답하면 된다. 각 문항을 &quot;해당됨(2점) / 가끔(1점) / 아님(0점)&quot;으로 채점한다.&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;/li&gt;
&lt;li&gt;일을 마치고 나면 완전히 진이 빠짐&lt;/li&gt;
&lt;li&gt;하루 종일 일하는 게 정말 힘들게 느껴짐&lt;/li&gt;
&lt;li&gt;직장 동료나 고객을 대할 때 감정이 메말라 있음&lt;/li&gt;
&lt;li&gt;업무 때문에 좌절감을 자주 느낌&lt;/li&gt;
&lt;li&gt;내가 너무 열심히 일하고 있다는 생각이 듦&lt;/li&gt;
&lt;li&gt;일에 대해 점점 더 무관심해지고 있음&lt;/li&gt;
&lt;li&gt;내가 하는 일이 누군가에게 도움이 되는지 의심됨&lt;/li&gt;
&lt;li&gt;업무 중 두통, 위장 장애, 불면이 자주 생김&lt;/li&gt;
&lt;li&gt;휴일에도 일 생각으로 쉬지 못함&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;점수 해석은 단순하다. 0~6점이면 안전, 평범한 직장 스트레스 수준이다. 7~13점이면 주의 단계로 번아웃 초기 신호이므로 휴식 패턴 점검이 필요하다. 14~20점이면 위험 단계로 본격적인 번아웃 가능성이 크며, 의료 상담을 고려해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;보어아웃 자가진단 체크리스트 (10문항)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;베르더의 보어아웃 척도(Boreout Index)를 단순화한 버전이다. 마찬가지로 최근 2주가 기준이다.&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;/li&gt;
&lt;li&gt;회사에서 일부러 일 처리 속도를 늦추는 편임&lt;/li&gt;
&lt;li&gt;사적인 인터넷 서핑/SNS 시간이 업무 시간의 30% 이상임&lt;/li&gt;
&lt;li&gt;가짜 야근(할 일 없는데 사무실 남기) 한 적이 있음&lt;/li&gt;
&lt;li&gt;본인이 가진 능력의 절반도 못 쓰고 있다고 느낌&lt;/li&gt;
&lt;li&gt;직장 동료한테 &quot;나 바쁜 척&quot; 하는 걸 의식적으로 함&lt;/li&gt;
&lt;li&gt;업무에서 의미나 보람을 거의 못 느낌&lt;/li&gt;
&lt;li&gt;회의 시간에 안 가도 될 회의 가서 그냥 앉아만 있음&lt;/li&gt;
&lt;li&gt;일에 대한 호기심이 거의 사라짐&lt;/li&gt;
&lt;li&gt;&quot;내가 사라져도 회사 별일 없을 듯&quot; 생각해본 적 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;채점은 번아웃 표와 동일하다. 0~6점이면 안전이며, 일이 시시할 때도 있지만 보어아웃은 아니다. 7~13점이면 보어아웃 초기로 잡 크래프팅 시도가 필요한 시점이다. 14~20점이면 본격적인 보어아웃이며 부서 이동이나 이직 검토 단계로 보아야 한다.&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;둘 다 14점을 넘으면 복합형으로 본다. 일은 너무 많은데 의미가 없는 상태(예: 단순 반복 야근)이다. 이 경우는 환경 문제가 가장 크기 때문에 개인 회복으로는 풀리지 않는다. 부서 이동이나 이직이 가장 빠른 답일 수 있다.&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;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 보어아웃이라면 어떻게 해야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보어아웃 진단이 나왔다면 참는 것이 답이 아니다. 보어아웃을 방치하면 진짜 우울증으로 넘어간다. 베르더의 책에서도 보어아웃이 6개월 이상 지속되면 임상 우울증의 위험이 유의하게 올라간다고 지적한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1주일 안에 본인 상태를 인정하고 기록 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 해야 할 일은 부정의 멈춤이다. &quot;내가 게을러서 그렇다&quot;가 아니다. 환경이 본인을 쓰지 않고 있는 것이다. 매일 30분이라도 일지를 써 보자. 오늘 의미 있다고 느낀 일과 시간을 죽인 일을 구분해 기록하면 패턴이 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1개월 안에 잡 크래프팅과 사이드 프로젝트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잡 크래프팅(Job Crafting)은 본인이 본인 일에 의미를 추가하는 작업이다. 같은 업무라도 새 도구로 자동화해 보거나, 시키지 않은 분석 보고서를 만들어 가져가거나, 후배 멘토링을 자청하는 식이다. 회사가 주지 않으면 본인이 만드는 것이다.&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;병행으로 사이드 프로젝트 시작도 효과가 좋다. 블로그, 유튜브, 작은 코딩 프로젝트 무엇이든 가능하다. 회사 밖에서 의미를 찾으면 회사 안에서 받는 데미지가 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3개월 안에 부서 이동, 직무 변경, 이직 검토&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개월 동안 잡 크래프팅을 하고 사이드 프로젝트를 돌려도 풀리지 않는다면 환경이 진짜 문제다. 부서 이동을 신청하거나, 직무 변경을 요청하거나, 이직을 시작해야 한다. 보어아웃은 본인의 잘못이 아니라 일자리가 본인 능력을 따라오지 못하는 미스매치 문제이다.&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;길게 끌면 무기력이 만성화되어 회복도 더 오래 걸린다. 보어아웃을 오래 버틴 사람들을 보면 다음 직장에 가서도 한동안 적응하지 못하는 경우가 종종 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;진짜 번아웃이라면 어떻게 해야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMYd0k/dJMcai4cn8p/BoIXINjxRCyw7tAwAY6eDK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMYd0k/dJMcai4cn8p/BoIXINjxRCyw7tAwAY6eDK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMYd0k/dJMcai4cn8p/BoIXINjxRCyw7tAwAY6eDK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMYd0k%2FdJMcai4cn8p%2FBoIXINjxRCyw7tAwAY6eDK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;876&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.pinterest.com/pin/5348093304473846/&quot;&gt;https://www.pinterest.com/pin/5348093304473846/&lt;/a&gt;&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;번아웃은 보어아웃과 처방이 정반대이다. 일을 줄이고 본인을 빼야 회복된다. 보어아웃 회복이 시간순(1주/1개월/3개월) 단계라면, 번아웃은 우선순위 순으로 가는 것이 맞다. 가장 위급한 것이 신체 회복이고, 그다음이 환경 조정, 마지막이 경계 재설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최우선, 일단 빠져서 쉬는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번아웃 14점 이상이라면 일주일이라도 연차나 병가를 쓰는 것이 우선이다. 그리고 &quot;잠시 충전&quot;이 아니라 정말 아무것도 하지 않는 시간이 필요하다. 만성 코르티솔 상태는 며칠 쉰다고 풀리지 않는다. 심하면 정신건강의학과 상담을 받아야 한다. 약물 도움이 필요한 단계일 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째, 업무 분담 요청과 거절 연습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복귀하면 업무량 자체를 줄이는 협상이 필요하다. 팀장과 솔직히 이야기하고, 안 되면 인사팀까지 가야 한다. 거절 연습도 필수다. 새 업무가 떨어질 때 &quot;이것을 받으면 기존 업무가 늦어집니다&quot;라는 식으로 명확히 선을 긋는다.&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;이것이 약해 보일까 봐 못 하는 사람이 많은데, 번아웃이 한 번 오면 회복에 평균 6개월에서 1년이 걸린다. 그동안의 회사 손해가 더 크다. 솔직히 까놓고 말하는 것이 모두에게 이득이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마지막, 워크-라이프 경계 다시 쌓기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저녁 6시 이후 업무 메일이나 슬랙을 보지 않고, 주말에는 아예 노트북을 켜지 않으며, 휴가는 한 번에 길게 쓰는 식의 새 규칙을 만들어야 한다. 한 번 무너진 경계는 다시 쌓는 데 시간이 걸린다. 처음에는 죄책감이 들지만 몇 달이 지나면 적응된다.&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;내가 빠진다고 회사가 망하지 않는다. 일단 살고 봐야 한다. 본인을 망가뜨려서까지 지킬 만한 회사는 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회사가 싫은 것이 아니라 몸이 신호를 보내는 것이다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 다룬 것을 줄여 정리하면, &lt;b&gt;번아웃 보어아웃 차이&lt;/b&gt;는 결국 과부하인가 자극 부족인가의 차이이다. 번아웃은 일이 너무 많아서 타버린 상태이고, 보어아웃은 일이 너무 없어서 굳어버린 상태이다. 둘 다 &quot;회사 가기 싫다&quot;로 보이지만 처방은 정반대로 갈린다. 자가진단 두 개를 돌려 본인 점수를 확인해 보고, 보어아웃이라면 시간순으로 잡 크래프팅부터, 번아웃이라면 우선순위 순으로 휴식부터 잡으면 된다. 자가진단은 자가진단일 뿐이고, 심하다면 정신건강의학과 상담이 답이다.&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;WHO가 번아웃을 직업 관련 현상으로 정식 인정한 것이 2019년이다. 보어아웃은 아직 ICD에 들어가지는 않았으나 베르더 이후 후속 연구가 계속 나오고 있다. 둘 다 엄살이 아니라 진짜 건강 문제이다.&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;같은 사무실에 앉아 있는 동료 중에도 본인과 같은 진단이 나오는 사람이 분명 있을 것이다. 일단 본인 상태부터 솔직하게 인정하는 것이 시작이다.&lt;/p&gt;</description>
      <category>끄적끄적</category>
      <category>번아웃</category>
      <category>번아웃 증상</category>
      <category>번아웃 증후군 극복</category>
      <category>보어아웃</category>
      <category>보어아웃 자가진단</category>
      <category>의욕 없음 무기력</category>
      <category>자가진단</category>
      <category>직장인 우울증</category>
      <category>직장인스트레스</category>
      <category>회사 다니기 싫을 때</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/408</guid>
      <comments>https://yscho03.tistory.com/408#entry408comment</comments>
      <pubDate>Sun, 3 May 2026 13:58:42 +0900</pubDate>
    </item>
    <item>
      <title>SSE 스트림은 어떤 언어가 안정적인가? Go&amp;middot;Python&amp;middot;Node&amp;middot;Rust를 User 1000명까지 굴려본 결과</title>
      <link>https://yscho03.tistory.com/407</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT 응답 스트리밍을 보고 &quot;이것은 우리도 만들어야 한다&quot;는 분위기가 형성된 지 1년이 넘었다. 그래서 SSE(Server-Sent Events)가 갑자기 다시 부상했고, 백엔드를 설계할 때 &quot;Go가 좋다더라&quot;, &quot;FastAPI가 빠르다더라&quot;, &quot;Rust를 쓰면 끝장난다더라&quot; 식의 잡담만 무성하다. 다만 &lt;b&gt;SSE 스트림 언어 비교&lt;/b&gt; 글을 살펴보면 벤치마크 없이 일반론만 나열하거나, 단일 언어 튜토리얼에서 끝나버린다.&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;진짜 중요한 질문은 &quot;누가 빠른가&quot;가 아니다. 운영에 들어가면 답은 달라진다. &lt;b&gt;&quot;구독자 200명을 넘어도 죽지 않는가, 1000명에서는 손실률이 얼마나 누적되는가&quot;&lt;/b&gt; 가 실제 SLA를 깨먹는 변수이다.&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;그래서 직접 돌려봤다. 50/200/500/1000명 구독자 구간에서 동일 하드웨어&amp;middot;동일 런타임을 고정하고 동시성 패턴만 바꿔 측정했다. 결론부터 말하면 직렬 패턴(Python/Node)은 50명 구간부터 손실률 56%를 찍었다. 고루틴 병렬(Go)은 1000명까지 드롭 0건이었다. 워커 풀(Rust 패턴)은 가장 평탄한 p99 분포를 보여줬다. 수치를 모두 공개하니 본인 환경에 대입해 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bu5QfT/dJMcaiJULtI/1AzSSkCwfKbMiJEnbVrJg0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bu5QfT/dJMcaiJULtI/1AzSSkCwfKbMiJEnbVrJg0/img.png&quot; data-alt=&quot;출처: https://goframe.org/en/articles/go-sse-implementation-guide&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bu5QfT/dJMcaiJULtI/1AzSSkCwfKbMiJEnbVrJg0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbu5QfT%2FdJMcaiJULtI%2F1AzSSkCwfKbMiJEnbVrJg0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;926&quot; height=&quot;547&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://goframe.org/en/articles/go-sse-implementation-guide&lt;/figcaption&gt;
&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;SSE란 무엇이며 언어 선택이 왜 중요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSE는 HTTP 위에서 서버 &amp;rarr; 클라이언트 단방향 스트리밍을 보내는 표준이다(&lt;a href=&quot;https://html.spec.whatwg.org/multipage/server-sent-events.html&quot;&gt;WHATWG HTML spec의 EventSource 정의&lt;/a&gt; 참고). WebSocket보다 가볍고, 재연결이 자동이며, 프록시&amp;middot;CDN 호환성이 좋다. &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events&quot;&gt;MDN의 Server-sent events 문서&lt;/a&gt;에 브라우저 호환성&amp;middot;이벤트 포맷이 모두 정리돼 있다. LLM 토큰 단위 응답, 실시간 알림, 라이브 대시보드는 거의 다 SSE로 굴러간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebSocket과는 무엇이 다른가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 양방향이고 풀 듀플렉스이다. 채팅처럼 클라이언트도 자주 보내야 한다면 그쪽을 써야 한다. 다만 SSE는 단방향이라 서버 구현이 훨씬 단순하다. HTTP를 그대로 쓰니 인증&amp;middot;CORS&amp;middot;로드밸런서 같은 기존 인프라를 그대로 재활용할 수 있다. ChatGPT류 응답 스트리밍은 클라이언트가 한 번 요청을 보내고 서버가 토큰을 줄줄이 흘려주는 구조라 SSE와 결이 정확히 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM 시대에 SSE가 다시 부상한 이유는 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI API, Anthropic API를 비롯한 거의 모든 LLM 게이트웨이가 응답을 SSE로 흘려준다. 토큰 100개를 한 번에 받는 것이 아니라 1개씩 받아 화면에 흘리는 UX가 표준이 됐다. 이는 백엔드 입장에서는 골치 아픈 변화이다. 한 사용자 요청이 30초~5분간 연결을 유지하면서 토큰 수백 개를 푸시해야 한다. 동시 사용자 100명만 돼도 살아 있는 SSE 커넥션이 100개 떠 있는 셈이다.&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;이때 &lt;b&gt;SSE 서버 전송 이벤트&lt;/b&gt;를 처리하는 동시성 모델이 비싼 변수가 된다. 단일 이벤트 루프로 모두 돌리면 한 사용자가 느려질 때 다른 사용자도 함께 느려진다. 그래서 &lt;b&gt;SSE 스트림 언어 비교&lt;/b&gt;가 단순 hello world 벤치가 아니라 운영 안정성 기준으로 다시 보게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비교 후보 4종 &amp;mdash; Go, Python(FastAPI), Node.js, Rust&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Go 고루틴 패턴 &amp;mdash; 구독자별 비차단 팬아웃&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go는 이벤트가 발생할 때마다 구독자별로 고루틴을 띄워 동시 전송한다. 채널이 가득 차면 그 구독자만 드롭 처리하고 다른 구독자는 영향을 받지 않는다. 고루틴이 워낙 가벼우니(스택 2KB부터 시작) 수천 개를 띄워도 메모리 부담이 적다. &lt;b&gt;Go SSE 구현&lt;/b&gt;을 검색하면 99% 이 패턴이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777783026954&quot; class=&quot;go&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;go&quot;&gt;&lt;code&gt;for _, sub := range subs {
    go func(s *Sub) {
        select {
        case s.ch &amp;lt;- evt:
        default:
            // backpressure: 이 구독자만 드롭
        }
    }(sub)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Python(FastAPI) asyncio 직렬 패턴 &amp;mdash; sse-starlette EventSourceResponse&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI는 sse-starlette 패키지의 EventSourceResponse를 쓰는 것이 표준이다(Starlette 본체에는 들어 있지 않고 별도 설치가 필요하다). 단일 이벤트 루프가 구독자 목록을 순차로 돌면서 await sub.send()를 호출한다. &lt;b&gt;FastAPI SSE&lt;/b&gt; 코드 예제를 보면 모두 비슷하게 생겼다. 직관적이고 짜기 쉽다. 다만 한 구독자가 느리면 뒤에 줄 선 구독자들이 모두 함께 느려진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777783026954&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;async def event_stream():
    async for evt in queue:
        for sub in subscribers:
            try:
                await sub.send(evt)
            except QueueFull:
                # 뒤 구독자도 같이 밀림
                pass
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node.js 이벤트 루프 &amp;mdash; 단일 스레드 순회의 한계&lt;/h3&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.js도 본질적으로 같다. 단일 스레드 이벤트 루프 위에서 비동기 콜백을 돌리는 구조라 패턴이 Python asyncio와 거의 동치이다. &lt;b&gt;Node.js SSE 안정성&lt;/b&gt;을 검증할 때 worker_threads로 멀티 코어를 활용하는 트릭을 쓰지만, 기본형은 직렬 팬아웃이다. CPU 코어 1개만 쓰니 구독자가 늘어나면 처리량 천장이 뚜렷해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rust tokio 워커 풀 &amp;mdash; 바운드 작업 채널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Rust는 tokio 위에서 고정 크기 워커 풀(예: 16개)을 만들고 공유 작업 채널을 비우는 패턴이 일반적이다. 고루틴 무한 생성도 하지 않고, 직렬 처리도 하지 않는다. 그 중간이다. &lt;b&gt;Rust tokio SSE&lt;/b&gt; 구현을 보면 mpsc::channel로 잡 큐를 깔고 워커가 recv().await로 빨아들인다. 워커 수가 CPU 코어 수에 매핑되니 컨텍스트 스위치 비용이 최소화된다. 참고로 tokio mpsc::Receiver는 단일 소비자 설계라 clone()이 되지 않으므로, 워커가 함께 빼먹게 하려면 Arc&amp;gt;로 감싸거나 MPMC를 지원하는 async-channel 같은 외부 크레이트를 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777783026954&quot; class=&quot;rust&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;rust&quot;&gt;&lt;code&gt;use std::sync::Arc;
use tokio::sync::Mutex;

let (tx, rx) = mpsc::channel::&amp;lt;Event&amp;gt;(64);
let rx = Arc::new(Mutex::new(rx));
for _ in 0..16 {
    let rx = Arc::clone(&amp;amp;rx);
    tokio::spawn(async move {
        while let Some(evt) = rx.lock().await.recv().await {
            deliver(evt).await;
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 돌려본 벤치마크 &amp;mdash; sse-fanout-bench 실험 결과&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각설하고 숫자를 보자. 단일 컨테이너 제약 때문에 4개 런타임을 동시에 띄울 수는 없었다. 그래서 동일 하드웨어&amp;middot;동일 런타임(Go 1.22)을 고정하고 위 세 가지 동시성 패턴만 Go로 시뮬레이션해 측정했다. 런타임 자체의 차이가 아니라 &lt;b&gt;동시성 패턴 변수만 분리&lt;/b&gt;해 보는 것이 목적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실험 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루프백 HTTP 비용을 배제하기 위해 인프로세스로 측정했다. 실제 운영에서는 네트워크 오버헤드가 더 붙겠지만, 패턴 간 상대 비교에는 이쪽이 더 깨끗하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;50명 구독자 &amp;mdash; 모두 비슷하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;50명 구간에서는 어떤 패턴을 골라도 큰 차이가 없다. 직렬 패턴조차 처리율이 높아 보일 수 있다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;전달&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;드롭&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;처리율(eps)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99(&amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-parallel&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1,715,751&lt;/td&gt;
&lt;td&gt;52.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-serial&lt;/td&gt;
&lt;td&gt;8,693&lt;/td&gt;
&lt;td&gt;**11,307**&lt;/td&gt;
&lt;td&gt;12,937,937&lt;/td&gt;
&lt;td&gt;305.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker-pool&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;4,260,394&lt;/td&gt;
&lt;td&gt;57.8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 잘 봐야 한다. fanout-serial은 처리율 1300만 eps를 찍었지만 &lt;b&gt;드롭이 11,307건&lt;/b&gt;이다. 즉, 시도한 2만 건 중 56.5%가 손실됐다. 처리율 숫자만 보고 &quot;직렬이 빠르네?&quot;라고 판단해서는 안 된다. 보낼 수 없는 것을 빠르게 버린 것이지 빠르게 전달한 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;200~500명 &amp;mdash; 직렬 패턴이 무너지는 구간&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;구독자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;드롭&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;손실률&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99(&amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-parallel&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;342.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-serial&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;47,727&lt;/td&gt;
&lt;td&gt;**59.7%**&lt;/td&gt;
&lt;td&gt;1,001.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker-pool&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;272.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-parallel&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;559.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-serial&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;116,337&lt;/td&gt;
&lt;td&gt;**58.2%**&lt;/td&gt;
&lt;td&gt;2,679.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker-pool&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;218.2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬 패턴은 200명 구간에서 60% 가까이 드롭한다. p99 지연도 1ms를 넘는다. 이는 &lt;b&gt;SSE 백프레셔&lt;/b&gt;가 발생해 채널 버퍼(64)가 가득 차자마자 이벤트가 버려진 결과이다. asyncio 단일 루프가 구독자 200명을 순회하는 동안 새 이벤트가 계속 쌓이는데 소비 속도가 따라가지 못해 누락이 발생한다.&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;반면 fanout-parallel(Go 패턴)은 구독자 500명에서도 드롭 0건이다. 처리율은 약 193만 eps로 직렬 대비 낮아 보이지만, &lt;b&gt;이는 손실률 0% 기준 처리율&lt;/b&gt;이다. 진짜 비교 대상이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1000명 &amp;mdash; 워커 풀의 진가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;구독자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;전달&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;드롭&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;p99(&amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;max(&amp;mu;s)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-parallel&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;400,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;954.7&lt;/td&gt;
&lt;td&gt;1,548.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fanout-serial&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;181,445&lt;/td&gt;
&lt;td&gt;**218,555**&lt;/td&gt;
&lt;td&gt;4,591.6&lt;/td&gt;
&lt;td&gt;4,728.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worker-pool&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;400,000&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;**346.4**&lt;/td&gt;
&lt;td&gt;853.9&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1000명 구간에서 진짜 결판이 난다. worker-pool은 p99 346&amp;mu;s로 fanout-parallel(954&amp;mu;s)의 3분의 1 수준이다. 최댓값(max)도 853&amp;mu;s로 평탄하다. 고루틴을 무한 생성하는 fanout-parallel은 컨텍스트 스위치 부하가 누적되면서 꼬리 지연이 늘어났다.&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;직렬 패턴(fanout-serial)은 그대로 무너졌다. 218,555건 드롭, 손실률 54.6%, p99 4.5ms. SLA 0.1%를 보장하라는 운영팀이 있다면 즉시 알람 폭격을 받을 수치이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777783026957&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;=== SSE-style fanout benchmark ===
model              subs  delivered  dropped     thr_eps    p99_us  gc_pause_ms
fanout-parallel    1000     400000        0     1778822    954.70         0.85
fanout-serial      1000     181445   218555    16352135   4591.60         0.05
worker-pool        1000     400000        0     3782315    346.40         0.15
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GC 일시정지는 진짜 문제인가 &amp;mdash; Go GC pause 수치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Go는 GC 때문에 SSE에서는 못 쓴다&quot;는 옛날 글이 아직 돌아다닌다. 다만 측정 결과는 다르다. fanout-parallel 1000명 구간에서 GC 일시정지 누적은 0.85ms이다. 224ms 측정 구간 중 0.4% 비중에 불과하다. p99 지연 954&amp;mu;s에 GC pause가 직접 영향을 준 흔적은 없다.&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;Go 1.22는 동시 GC가 잘 다듬어져 sub-millisecond pause가 일반적이다. &lt;a href=&quot;https://go.dev/doc/gc-guide&quot;&gt;Go 공식 GC 가이드&lt;/a&gt;에도 GC가 대부분 동시(concurrent)로 돌고 STW 구간은 mark/sweep 전환 시점에 짧게만 발생하며 힙 크기에 비례해 늘어나지 않는다고 명시돼 있다(가이드는 특정 마이크로초 수치를 못 박지는 않는다). 적어도 SSE 팬아웃 워크로드에서는 GC가 병목이 되지 않는다. &lt;b&gt;GC 일시정지&lt;/b&gt;가 무서워 Rust로 갈아탈 명분으로는 부족하다. &lt;b&gt;SSE 스트림 언어 비교&lt;/b&gt;를 할 때 GC FUD에 휘둘리지 말고 본인 워크로드에서 직접 측정해 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;손실률(dropped)이 처리량보다 중요한 이유&lt;/h2&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;b&gt;SSE 팬아웃 벤치마크&lt;/b&gt; 글은 거의 다 처리율(throughput) 비교에서 끝난다. 다만 SSE 운영자에게는 처리율보다 손실률이 훨씬 중요한 지표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SSE는 재전송이 안 된다 &amp;mdash; 잃으면 끝이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 메시지가 실패하면 재전송 로직을 넣을 수 있다. Kafka는 오프셋을 커밋해 재처리할 수 있다. 다만 SSE는 그런 것이 없다. 클라이언트가 받지 못하면 그 이벤트는 영영 사라진다. Last-Event-ID 헤더로 재연결 시 일부 보충이 가능하지만, 서버가 이벤트 히스토리를 들고 있어야 작동한다. 대부분의 구현은 이를 들고 있지 않다.&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;LLM 토큰 스트리밍에서 토큰 1개를 잃으면 어떻게 되는가? 사용자에게 깨진 단어가 보인다. 알림 시스템에서 알림 1개를 잃으면? 그저 그 알림은 없는 것이 된다. 손실률이 1%만 돼도 운영 후폭풍은 무섭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백프레셔 처리 방식별 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 실험에서 직렬 패턴의 손실률 56~60%는 채널 버퍼 64가 가득 찼을 때 즉시 드롭하는 정책에 따른 것이다. 운영에서 더 흔한 선택지는 다음과 같다.&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;블로킹 (block until consumer catches up)&lt;/b&gt;: 누락은 없지만 느린 구독자 한 명이 전체를 멈춘다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;드롭 (즉시 버림)&lt;/b&gt;: 빠르지만 손실이 발생한다 &amp;mdash; 이번 실험의 fanout-serial&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버퍼 확장 (큐 무한 확장)&lt;/b&gt;: 메모리 폭발 위험이 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Go 고루틴 패턴은 구독자별 채널을 따로 들고 있어 한 명만 드롭된다. 직렬 패턴은 공유 큐라 드롭이 누적된다. 이것이 손실률 차이의 본질이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;채널 버퍼 64는 충분한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 실험은 채널 버퍼를 64로 고정했다. 운영에서는 워크로드별 튜닝이 필요하다. 토큰 1개당 5ms를 소비하는 LLM 응답이라면 버퍼 64는 약 320ms 분량이다. 클라이언트가 0.5초만 느려져도 버퍼가 가득 찬다. 일반적으로 256~1024 사이가 무난하다.&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;다만 버퍼를 늘려도 직렬 패턴의 본질적 한계는 바뀌지 않는다. 단일 루프가 구독자 N명을 순회하는 동안 새 이벤트가 들어오면 그만큼 백프레셔가 누적된다. 패턴 자체를 바꿔야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 어떤 언어를 골라야 하는가 &amp;mdash; SSE 스트림 언어 비교 최종 권장&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;빠른 프로토타입, 구독자 ~200명&lt;/td&gt;
&lt;td&gt;Python(FastAPI)&lt;/td&gt;
&lt;td&gt;짜기 빠름, 손실 감수 가능 단계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 운영, 구독자 ~수천명&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;안정성&amp;middot;생산성 균형, GC 문제 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;극한 처리량, 꼬리 지연 민감&lt;/td&gt;
&lt;td&gt;Rust tokio&lt;/td&gt;
&lt;td&gt;p99 평탄, 학습곡선 감수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;프론트 인프라와 통합&lt;/td&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;TypeScript 공유, 패턴은 Python과 동치&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빠른 프로토타입 &amp;mdash; Python(FastAPI), 단 구독자 200 이하&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POC 단계에서는 FastAPI가 압도적으로 빠르게 짜진다. EventSourceResponse 한 줄이면 끝난다. 다만 사용자가 200명을 넘는 순간 손실이 시작된다. 베타를 풀고 트래픽이 들어오면 갈아엎어야 한다. &quot;MVP는 FastAPI, 본격 운영은 Go&quot; 패턴이 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 운영 &amp;mdash; Go, 안정성&amp;middot;생산성 균형&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 케이스에서 답이다. 고루틴 모델이 SSE 팬아웃에 거의 자연스럽게 매핑된다. 1000명까지 드롭 0건 데이터를 확인했다. p99는 1ms 안쪽이고 GC pause도 무시할 수 있는 수준이다. 채용 시장도 넓고 라이브러리도 풍부하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;극한 처리량 &amp;mdash; Rust tokio, 학습곡선을 감수해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처리율 4배, p99 평탄함이 진짜 필요한 환경(트레이딩, 게임, 실시간 광고 입찰)에서는 Rust가 답이다. 다만 lifetime, async trait, mpsc 패턴을 익히는 데 3~6개월을 잡아야 한다. 팀 전부가 Rust를 다룰 수 있어야 운영이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node.js는 언제 쓰는가 &amp;mdash; 프론트 인프라와 묶어 운영할 때만&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기술적으로는 Python asyncio와 동치라 안정성 측면의 메리트는 없다. 다만 프론트가 Next.js이고 백엔드도 TypeScript로 통일하면 타입 공유&amp;middot;코드 공유 메리트가 크다. SSE 단독 성능보다 풀스택 일체화가 중요한 팀에서는 선택지로 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 적용 체크리스트&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;채널/큐 버퍼 크기&lt;/b&gt;: 토큰 1개당 처리 시간 &amp;times; 256~1024로 잡고 시작, 운영 데이터를 보면서 튜닝&lt;/li&gt;
&lt;li&gt;&lt;b&gt;드롭 발생 시 알림&lt;/b&gt;: dropped 카운터 메트릭을 노출, 0이 아니면 즉시 알람&lt;/li&gt;
&lt;li&gt;&lt;b&gt;p99 지연 모니터링&lt;/b&gt;: 처리율 대시보드 대신 p99&amp;middot;p999 히스토그램이 필수&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GC 일시정지 측정&lt;/b&gt;: Go는 runtime/metrics, Rust는 jemalloc stats, Node는 --trace-gc&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재연결 시 보충 정책&lt;/b&gt;: Last-Event-ID를 받아 처리할지 결정, 그러지 않을 경우 명시적으로 문서화&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백프레셔 정책 명문화&lt;/b&gt;: 블로킹/드롭/확장 중 무엇을 쓸지 코드 주석에 박아두기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 3줄 요약&lt;/h2&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;&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;직렬 패턴(Python asyncio, Node.js 이벤트 루프)은 구독자 200명을 넘으면 손실률 60%를 찍는다&lt;/b&gt;. 프로토타입까지는 OK이지만, 운영 진입 전 갈아타야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고루틴 병렬(Go)이 안전한 기본값&lt;/b&gt;이다. 1000명까지 드롭 0건, GC pause 1ms 미만, 작성 또한 직관적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;워커 풀(Rust tokio)이 극한 환경에서 최강&lt;/b&gt;이다. p99 346&amp;mu;s로 가장 평탄하다. 다만 학습곡선&amp;middot;인력 비용이 크다.&lt;/li&gt;
&lt;/ol&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;b&gt;SSE 스트림 언어 비교&lt;/b&gt;의 결론을 한 줄로 줄이면, &quot;언어 자체보다 동시성 패턴이 더 큰 변수이다&quot;. 같은 Python이라도 직렬 루프를 쓰지 않고 워커 프로세스 풀을 깔면 다른 결과가 나온다. 같은 Go라도 고루틴 무한 생성을 하지 않고 워커 풀을 깔면 Rust와 비슷해진다.&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;본인 환경에서 직접 측정하는 것을 강력히 권한다. 실험 코드는 인프로세스 Go 시뮬레이션이지만 패턴 자체는 어떤 언어로든 옮길 수 있다. 채널 버퍼&amp;middot;워커 수&amp;middot;구독자 수 변수를 바꿔가며 본인 워크로드의 특성을 찾는 것이 진짜 답이다. 운영에 들어가서 새벽 3시에 알람을 받는 것보다 백배 낫다.&lt;/p&gt;</description>
      <category>Backend</category>
      <category>FastAPI SSE</category>
      <category>Go SSE 구현</category>
      <category>Node.js SSE 안정성</category>
      <category>Rust tokio SSE</category>
      <category>SSE 백프레셔</category>
      <category>SSE 서버 전송 이벤트</category>
      <category>SSE 팬아웃 벤치마크</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/407</guid>
      <comments>https://yscho03.tistory.com/407#entry407comment</comments>
      <pubDate>Sun, 3 May 2026 13:49:20 +0900</pubDate>
    </item>
    <item>
      <title>블룸버그 터미널 연 4천만원? 오픈소스로 비슷하게 쓸 수 있다</title>
      <link>https://yscho03.tistory.com/406</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;블룸버그 터미널 한 자리에 연 $26,000~$32,000을 지불해야 한다는 사실을 알고 있는가? 환산하면 한국 돈으로 약 3,500만~4,200만원이다. 헤지펀드, IB, 자산운용사에서나 사용하는 도구이지 개인 투자자가 손댈 수 있는 가격이 아니다. 다만 최근 &lt;b&gt;오픈소스 금융 분석 플랫폼&lt;/b&gt;들이 빠르게 성장하면서 분위기가 다소 바뀌고 있다. 그중에서도 &lt;b&gt;Fincept Terminal&lt;/b&gt;이라는 프로젝트가 GitHub 스타 18.8k를 기록하며 &quot;오픈소스 블룸버그 터미널&quot; 포지션을 노리고 있다.&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;이 글에서는 Fincept Terminal이 진정한 블룸버그 대안이 될 수 있는지, 유사한 오픈소스 도구들과 어떻게 다른지, 한국 개인 투자자 입장에서 활용할 만한지를 정리한다. 결론부터 말하자면 &quot;완벽한 대체는 아니지만 보완재로는 매우 강력하다&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bsXSRj/dJMcaaypikg/Demj8hWZPjPIVWHO2UTLJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bsXSRj/dJMcaaypikg/Demj8hWZPjPIVWHO2UTLJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bsXSRj/dJMcaaypikg/Demj8hWZPjPIVWHO2UTLJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbsXSRj%2FdJMcaaypikg%2FDemj8hWZPjPIVWHO2UTLJ0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2172&quot; height=&quot;724&quot; data-origin-width=&quot;2172&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://github.com/Fincept-Corporation/FinceptTerminal&quot;&gt;GitHub (148KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블룸버그 터미널은 무엇이고 왜 개인이 사용하지 못하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블룸버그 터미널을 모르는 독자를 위해 잠시 설명한다. 1980년대부터 월스트리트 트레이더들이 데스크에 설치해놓고 사용하는 그 검정 화면이다. 실시간 시장 데이터, 뉴스, 채팅, 분석 모듈, 주문 집행까지 한 박스에 모두 담겨 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;연 구독료 약 $30,000의 충격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격이 매우 부담스럽다. 2024년 기준 싱글 터미널은 연 약 $31,980, 멀티 터미널의 경우 자리당 연 약 $26,580이다. 2025년부터는 6.5% 인상되어 멀티 자리당 $28,320으로 올라간다. 대량 구매하는 큰 기관이라면 자리당 연 $20,000~$22,000까지 할인되기도 하지만 그래도 만만치 않다. 한국 돈으로 환산하면 자리 하나에 연 약 3,500만~4,200만원이 들어간다. 회사가 비용을 부담해주는 경우가 아니라면 개인이 결제하기 거의 불가능한 수준이다.&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;다만 더 흥미로운 점은 이것이 &quot;단말기&quot; 가격이라는 사실이다. 데이터 커버리지를 추가하거나 추가 마켓 라이선스를 붙이면 비용은 더 늘어난다. 개인 투자자에게는 진입장벽 자체가 안드로메다급이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기관급 데이터 소스의 진입장벽&lt;/h3&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;b&gt;데이터다&lt;/b&gt;. 글로벌 주식, 채권, FX, 파생상품, 매크로 지표, 기업 펀더멘털, 실시간 뉴스, 애널리스트 리포트가 통일된 인터페이스로 들어오는 점이 핵심이다.&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;개인이 동일한 워크플로우를 구축하려면 yfinance로 주가, FRED로 매크로, Polygon으로 옵션, IMF로 글로벌 데이터를 따로 수집해 직접 통합해야 한다. 라이브러리 5~6개를 설치하고 데이터프레임을 머지하다 보면 분석은커녕 ETL만 하다가 시간이 다 간다. 이것이 폐쇄형 터미널과 오픈소스 금융 분석 플랫폼의 핵심 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 금융 분석 플랫폼은 무엇이 다른가&lt;/h2&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;b&gt;오픈소스 금융 분석 플랫폼&lt;/b&gt;은 블룸버그와 같은 폐쇄형 터미널의 데이터/분석 기능을 공개 코드로 제공하는 도구다. 사용자가 자체 호스팅하고 데이터 소스를 자유롭게 추가할 수 있어 비용 부담 없이 기관급 워크플로우를 구현할 수 있다. 대표 사례가 Fincept Terminal과 OpenBB Terminal이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;폐쇄형 vs 오픈소스 데이터 모델&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별것 아닌 것처럼 보여도 실무에서는 큰 차이다. 예컨대 한국 시장 데이터(KRX) 같은 것은 글로벌 벤더에서는 커버리지가 약하지만, 오픈소스 플랫폼이라면 pykrx 어댑터를 직접 붙이면 끝이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;듀얼 라이선스(AGPL+상용)가 의미하는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fincept Terminal은 &lt;b&gt;AGPL-3.0 + 상용 라이선스 듀얼 모델&lt;/b&gt;을 사용한다. 개인 학습/연구용으로 사용할 때는 무료지만, 회사 서비스에 탑재하여 외부에 노출시키면 AGPL 조항에 의해 소스 공개 의무가 발생한다. 그것이 부담된다면 상용 라이선스를 별도로 구매하는 구조다.&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;이러한 모델은 MongoDB, Grafana, Elastic 등에서도 채택한 비즈니스 모델이다. 오픈소스 정신과 회사 운영 자금을 동시에 확보하려는 전략이다. 개인이 사용할 경우에는 신경 쓸 필요가 없으나, 핀테크 스타트업이 제품에 통합할 계획이라면 라이선스 검토가 필수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자체 호스팅 가능성 = 데이터 주권&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Fincept Terminal 핵심 특징 5가지&lt;/h2&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;b&gt;오픈소스 금융 분석 플랫폼&lt;/b&gt;이 왜 &quot;오픈소스 블룸버그 터미널&quot; 포지션을 노릴 수 있는지, Fincept Terminal의 핵심 5가지 특징을 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;C++20 네이티브 + 임베디드 Python 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 오픈소스 금융 도구가 순수 파이썬으로 만들어진 것과 달리, Fincept Terminal은 &lt;b&gt;C++20 + Qt6&lt;/b&gt;로 구현된 네이티브 데스크톱 앱이다. 그리고 그 안에 &lt;b&gt;Python 3.11이 임베디드&lt;/b&gt; 형태로 탑재되어 있다.&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;왜일까? GUI 반응성과 실시간 데이터 처리 성능 때문이다. 차트 수십 개를 띄우고 실시간 틱 데이터를 받으면서 스크립트를 돌리는 워크로드를 순수 파이썬으로 처리하면 GIL 때문에 헬게이트가 열린다. C++로 코어를 만들고 분석 로직만 파이썬에 위임하는 구조가 합리적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 커넥터 100개 이상 (FRED/IMF/Polygon 등)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 커버리지가 매우 광범위하다. 공식 README 기준 &lt;b&gt;100개 이상의 데이터 커넥터&lt;/b&gt;가 내장되어 있다. 주요 소스만 추리면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;주가/암호화폐&lt;/b&gt;: Yahoo Finance, Polygon, Kraken, HyperLiquid&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매크로/경제&lt;/b&gt;: FRED, DBnomics, IMF, World Bank&lt;/li&gt;
&lt;li&gt;&lt;b&gt;뉴스/심리&lt;/b&gt;: 다양한 RSS, 소셜 피드 어댑터&lt;/li&gt;
&lt;li&gt;&lt;b&gt;펀더멘털&lt;/b&gt;: 다양한 SEC/공시 어댑터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블룸버그처럼 한 곳에서 모두 받는 방식은 아니고 각 소스별 API 키를 등록해야 하는 경우가 많지만, 통합 인터페이스로 모두 노출되므로 ETL을 따로 작성할 필요가 없어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI 에이전트 37개 (멀티 LLM 백엔드)&lt;/h3&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;b&gt;AI 에이전트 37개&lt;/b&gt;가 빌트인으로 포함되어 있다. 트레이더, 가치투자자, 매크로 분석가, 지정학 리스크 분석가 같은 역할별 에이전트 프레임워크가 있고, 각자 다른 프롬프트와 도구를 보유한다.&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;LLM 백엔드는 폐쇄형과 오픈형을 모두 지원한다. &lt;b&gt;OpenAI, Anthropic, Gemini, Groq, DeepSeek&lt;/b&gt;에 더해 &lt;b&gt;로컬 모델&lt;/b&gt;도 지원한다. 로컬 모델을 구동할 수 있다는 것은 데이터 외부 유출 없이 분석이 가능하다는 의미다. 헤지펀드 입장에서는 이것이 결정적 장점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브로커 16개 통합 + 페이퍼 트레이딩&lt;/h3&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;b&gt;16개 브로커가 통합&lt;/b&gt;되어 있어 분석 &amp;rarr; 백테스팅 &amp;rarr; 페이퍼 트레이딩 &amp;rarr; 실거래까지 하나의 앱에서 모두 처리할 수 있다. 글로벌 메이저 브로커 위주이며 한국 증권사는 직접 지원되지 않는다(이 부분은 뒤에서 별도로 다룬다).&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;페이퍼 트레이딩(paper trading), 즉 모의 거래도 내장되어 있어 전략 검증 시 별도의 페이퍼 트레이딩 플랫폼에 가입할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QuantLib 기반 정량 모듈 18개&lt;/h3&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;b&gt;QuantLib 기반 정량 모듈 18개&lt;/b&gt;가 포함되어 있다. QuantLib은 금융공학 업계에서 사실상 표준인 오픈소스 라이브러리다. DCF 모델, 옵션 가격 결정(Black-Scholes, 바이노미얼 트리, 몬테카를로), 변동성 분석, 채권 듀레이션, 포트폴리오 최적화(Markowitz, Black-Litterman) 등이 모두 포함된다.&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;학부 수준의 퀀트 강의에 등장하는 모델은 거의 모두 클릭 한 번에 구동할 수 있다고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다른 오픈소스 금융 분석 플랫폼과 비교해본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fincept Terminal만 존재하는 것은 아니다. &lt;b&gt;오픈소스 트레이딩 플랫폼&lt;/b&gt; 또는 &lt;b&gt;무료 퀀트 분석 도구&lt;/b&gt; 카테고리에는 꽤 여러 도구가 있다. 어떤 것이 어떤 포지션인지 정리해본다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;도구&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;포지셔닝&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;강점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**Fincept Terminal**&lt;/td&gt;
&lt;td&gt;통합 데스크톱 터미널&lt;/td&gt;
&lt;td&gt;GUI + AI 에이전트 + 데이터 100+ 통합&lt;/td&gt;
&lt;td&gt;한국 데이터 직접 지원 약함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**OpenBB Terminal**&lt;/td&gt;
&lt;td&gt;파이썬 SDK + 웹&lt;/td&gt;
&lt;td&gt;커뮤니티 크고 데이터 풍부&lt;/td&gt;
&lt;td&gt;GUI 약하고 AI 통합 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**QuantConnect LEAN**&lt;/td&gt;
&lt;td&gt;알고 트레이딩 엔진&lt;/td&gt;
&lt;td&gt;백테스팅 강력&lt;/td&gt;
&lt;td&gt;분석/리서치 GUI 부재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**Backtrader/Zipline**&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;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OpenBB Terminal과 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenBB는 본래 CLI 기반이었으나 최근 웹 SaaS + 파이썬 SDK 모델로 전환했다. 데이터 커버리지는 비슷한 수준이지만 &lt;b&gt;GUI가 약하고 AI 에이전트 통합이 얕다&lt;/b&gt;. 차트를 띄우고 인터랙티브하게 분석하는 워크플로우에는 Fincept이 더 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;QuantConnect LEAN과 포지션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LEAN은 알고리즘 백테스팅과 라이브 트레이딩 엔진이다. 강력하지만 &lt;b&gt;리서치/분석 GUI가 없다&lt;/b&gt;. 그래서 보통 LEAN을 사용하는 사람은 별도의 노트북 환경(JupyterLab)에서 분석하고 LEAN으로 실행만 돌린다. Fincept은 분석과 실행을 한 박스에 담은 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Backtrader/Zipline은 왜 부족한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Backtrader, Zipline은 단순한 &lt;b&gt;백테스팅 라이브러리&lt;/b&gt;다. 학습용으로는 좋지만, 데이터 수집을 직접 작성해야 하고 GUI도 없으며 AI 통합도 없다. 통합 플랫폼이라기보다 부품에 가깝다.&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;&amp;rarr; 결론적으로 다른 오픈소스 금융 분석 플랫폼 대비 Fincept의 차별점은 &quot;OpenBB의 데이터 통합 + LEAN의 알고 + GUI + AI 에이전트&quot;를 &lt;b&gt;하나의 바이너리에 묶었다&lt;/b&gt;는 점이다. 무료 퀀트 분석 도구 중에 이 정도 통합도를 가진 도구는 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한국 투자자 관점에서 쓸만한지 검토&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국 데이터(KRX/네이버 금융) 통합 가능 여부&lt;/h3&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;b&gt;KRX 데이터 직접 지원은 약하다&lt;/b&gt;. 빌트인 커넥터에 한국 거래소가 1차 시민으로 들어가 있지는 않다. 다만 임베디드 파이썬이 있으므로 &lt;b&gt;pykrx, FinanceDataReader&lt;/b&gt; 같은 한국 라이브러리를 직접 import해서 사용하면 된다. 어댑터를 직접 만들어 기존 데이터 모델에 끼워넣는 것도 가능하다.&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;이 점이 폐쇄형 터미널 대비 오픈소스 금융 분석 플랫폼의 진짜 장점이다. 블룸버그는 KRX 데이터를 제공하지 않으면 그대로 못 쓰지만, 오픈소스는 직접 붙이면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;키움/한투 API 연동 vs 글로벌 브로커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커 통합 16개도 마찬가지다. &lt;b&gt;키움증권, 한국투자증권 같은 한국 증권사 직접 지원은 없다&lt;/b&gt;. 빌트인은 IBKR, Alpaca, Binance 같은 글로벌 브로커 위주다.&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;다만 한국 증권사 API는 자체 공식 SDK를 제공하므로 임베디드 파이썬에서 import해서 주문을 보내는 워크플로우는 가능하다. 다만 통합된 UI에서 사용하는 방식이 아니고 스크립트로 우회하는 형태라 매끄러움은 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환율/세금 관점에서 글로벌 트레이딩 현실성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 브로커(IBKR 등)로 직접 트레이딩하면 환전 수수료, 양도세 신고 부담이 추가된다. 한국 거주자가 IBKR로 미국 주식을 매매하면 양도세를 자진 신고해야 하고, 환차익도 계산해야 한다.&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;&amp;rarr; 결론: &lt;b&gt;한국 투자자에게는 &quot;글로벌 시장 분석/리서치 도구&quot;로 사용하는 것이 가장 합리적이다&lt;/b&gt;. 실거래는 한국 증권사를 거치고, Fincept Terminal은 분석+백테스팅+AI 리서치 용도로 활용하는 조합이 현실적이다. &lt;b&gt;파이썬 금융 분석&lt;/b&gt; 환경을 풍부한 데이터, GUI, AI로 업그레이드하는 도구로 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;설치 후 시작하는 방법&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시스템 요구사항 (Win/Mac/Linux)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Windows 10/11&lt;/b&gt;, &lt;b&gt;macOS 12+&lt;/b&gt;, 주요 &lt;b&gt;Linux 배포판&lt;/b&gt; 지원&lt;/li&gt;
&lt;li&gt;RAM 최소 8GB, 권장 16GB (멀티 차트 + AI 에이전트 동시 구동 시)&lt;/li&gt;
&lt;li&gt;디스크 5GB+ (데이터 캐시 포함)&lt;/li&gt;
&lt;li&gt;인터넷 연결 (실시간 데이터 수신 시 필수)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빌드 vs 사전 빌드 바이너리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub 릴리즈 페이지에 사전 빌드된 바이너리가 제공된다. 다운로드해서 실행하면 된다. C++ 소스부터 빌드하고 싶다면 CMake + Qt6 + Python 3.11 개발 환경을 세팅해야 하는데, 처음 사용해본다면 그냥 바이너리가 정답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처음 30분에 해볼 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 후 30분 동안 시도해볼 만한 항목을 정리한다.&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; &amp;mdash; 보유 종목을 입력해 대시보드 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;차트 띄우기&lt;/b&gt; &amp;mdash; 관심 종목을 검색해 인터랙티브 차트 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 에이전트 채팅&lt;/b&gt; &amp;mdash; &quot;AAPL 매크로 리스크 분석해줘&quot; 같은 명령 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;백테스팅 1개 돌려보기&lt;/b&gt; &amp;mdash; 단순 이동평균 크로스오버 전략&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 커넥터 등록&lt;/b&gt; &amp;mdash; FRED API 키를 받아 매크로 지표 수신&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상용으로 쓸 거면 라이선스 유의점&lt;/h3&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;b&gt;회사 제품에 통합해서 외부 사용자에게 서비스로 제공할 경우 AGPL-3.0 의무가 발생&lt;/b&gt;한다. 소스 공개 없이 상용으로 사용하려면 &lt;b&gt;상용 라이선스 별도 구매&lt;/b&gt;가 필요하다. 핀테크 스타트업이라면 법무 검토가 필수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론적으로 누구에게 추천하는가&lt;/h2&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;b&gt;오픈소스 금융 분석 플랫폼&lt;/b&gt;, 특히 Fincept Terminal은 블룸버그를 1:1로 대체하는 도구가 아니다. 블룸버그가 보유한 기관 전용 데이터(딜 플로우, 채팅 네트워크 효과, 일부 폐쇄형 데이터 소스)는 오픈소스가 따라잡을 수 없다. 그 점은 인정해야 한다.&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;다만 &lt;b&gt;개인 투자자, 퀀트 입문자, 핀테크 개발자&lt;/b&gt;에게는 매우 강력한 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개인 투자자&lt;/b&gt;: 글로벌 시장 분석을 GUI + AI로 한 번에 처리하고 싶다면 추천&lt;/li&gt;
&lt;li&gt;&lt;b&gt;퀀트 입문자&lt;/b&gt;: QuantLib 모듈 + 백테스팅 + 페이퍼 트레이딩 통합 환경&lt;/li&gt;
&lt;li&gt;&lt;b&gt;핀테크 개발자&lt;/b&gt;: 데이터 커넥터 100개 + 임베디드 파이썬 = 빠른 프로토타이핑&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오픈소스 컨트리뷰터&lt;/b&gt;: C++20 + Qt6 코드베이스 + 활발한 커밋&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;한국 주식 위주로 분석할 거라면 pykrx + FinanceDataReader를 직접 사용하는 편이 낫다&lt;/li&gt;
&lt;li&gt;가벼운 백테스팅만 필요하다면 Backtrader가 충분하다&lt;/li&gt;
&lt;li&gt;노코드만 원한다면 차라리 TradingView가 적합하다&lt;/li&gt;
&lt;/ul&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;b&gt;오픈소스 금융 분석 플랫폼&lt;/b&gt;은 블룸버그 대체가 아니라 보완재로 보고 접근하면 가성비가 압도적인 도구다. 연 3,500만원+ 비용 없이 유사 워크플로우의 90%는 구현할 수 있다. GitHub에서 직접 README를 보고 결정하는 편이 빠르다.&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;&amp;rarr; &lt;a href=&quot;https://github.com/Fincept-Corporation/FinceptTerminal&quot;&gt;Fincept Terminal GitHub&lt;/a&gt;에서 직접 코드를 확인하고 릴리즈를 받을 수 있다. &lt;a href=&quot;https://github.com/OpenBB-finance/OpenBBTerminal&quot;&gt;OpenBB Terminal&lt;/a&gt;이나 &lt;a href=&quot;https://github.com/QuantConnect/Lean&quot;&gt;QuantConnect LEAN&lt;/a&gt;도 함께 비교해보면 자신에게 맞는 도구를 고르기 쉽다. &lt;b&gt;오픈소스 금융 분석 플랫폼&lt;/b&gt; 생태계가 빠르게 성장하고 있어 6개월 뒤에는 또 다른 그림이 펼쳐질 수도 있다.&lt;/p&gt;</description>
      <category>Tools</category>
      <category>Fincept Terminal</category>
      <category>무료 퀀트 분석 도구</category>
      <category>오픈소스 블룸버그 터미널</category>
      <category>오픈소스 트레이딩 플랫폼</category>
      <category>파이썬 금융 분석</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/406</guid>
      <comments>https://yscho03.tistory.com/406#entry406comment</comments>
      <pubDate>Sun, 3 May 2026 10:58:55 +0900</pubDate>
    </item>
    <item>
      <title>Matt Pocock이 TDD 스킬에서 Mock 대신 실제 연결을 좋은 테스트라고 하였을까?</title>
      <link>https://yscho03.tistory.com/405</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Matt Pocock이 본인 .claude 디렉토리를 통째로 GitHub에 공개했다. 그 안의 TDD 스킬 하나가 며칠 동안 개발자 트위터에서 회자됐는데, &lt;b&gt;좋은 테스트는 Mock이 아니라 실제 연결&lt;/b&gt;이라고 못 박은 것이 핵심이었다. 본인 X 계정에서는 &quot;Before: dozens of shit tests, coupled to implementation. After: only the tests required, validating real behavior&quot;라고 결과를 요약했다. 한마디로 &quot;내가 통제하는 코드는 mock 하지 마라&quot;이다.&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;또 하나의 흔한 TDD 글로 넘기기 쉽지만, AI 코딩 시대에는 얘기가 좀 다르다. Claude Code 같은 LLM이 한 번에 테스트 10개를 갈겨쓰는 환경에서 mock 남용이 만드는 가짜 신뢰는 예전보다 훨씬 위험하다. 이 글에서는 Pocock 스킬이 진짜로 무슨 말을 하는지, mock이 LLM 시대에 왜 더 큰 함정이 되는지, 그리고 한국 개발자가 오늘 당장 적용할 만한 부분까지 다뤄본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Matt Pocock이 누구이기에 이것이 화제인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Matt Pocock은 TypeScript 강의로 유명한 영국 개발자다. Total TypeScript라는 강의 시리즈로 이름값을 쌓았고, X(Twitter) 팔로워 10만+를 끌어모았다. ts 트릭이나 타입 레벨 프로그래밍 영상을 한 번이라도 본 사람이라면 얼굴이 익을 것이다. 단순 인플루언서가 아니라 실제로 OSS에 기여하고 강의도 깊이 있게 만드는 쪽이다.&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;2026년 2월, Pocock이 본인이 Claude Code를 쓸 때 사용하는 .claude 디렉토리를 통째로 GitHub에 올렸다. 저장소 이름은 mattpocock/skills. 그 안에는 engineering, productivity, personal, misc 카테고리의 스킬이 들어 있는데, 그중 &lt;a href=&quot;https://github.com/mattpocock/skills/tree/main/skills/engineering/tdd&quot;&gt;engineering/tdd&lt;/a&gt; 폴더가 특히 화제였다. 스킬이라는 것은 결국 Claude Code에게 &quot;이런 식으로 일해라&quot;라고 가르치는 마크다운 파일 묶음인데, Pocock은 이것을 TDD에 한정해 굉장히 빡세게 짰다. tests.md, mocking.md, interface-design.md 같은 파일들이 각자 한 가지 주제만 깊게 다루는 구조다.&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;다른 스킬은 각 분야 사람들만 보겠지만, TDD는 모든 개발자에게 해당된다. Pocock이 평소에는 TypeScript 타입 마법사 이미지였는데 갑자기 &quot;테스트를 어떻게 짤 것인가&quot;에 대해 말하기 시작하니, 사람들이 &quot;어, 이 사람도 이런 의견이 있었구나&quot; 하면서 들여다본 것이다. 막상 열어 보니 의견이 굉장히 강했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Matt Pocock TDD 스킬이 말하는 핵심 한 줄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bn3v7I/dJMcabqx2to/wAFYclaR2ZOCOklGkBmRcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bn3v7I/dJMcabqx2to/wAFYclaR2ZOCOklGkBmRcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bn3v7I/dJMcabqx2to/wAFYclaR2ZOCOklGkBmRcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbn3v7I%2FdJMcabqx2to%2FwAFYclaR2ZOCOklGkBmRcK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1080&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://github.com/microsoft/skills&quot;&gt;GitHub (1.1MB)&lt;/a&gt;&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;스킬 안의 &lt;a href=&quot;https://github.com/mattpocock/skills/blob/main/skills/engineering/tdd/SKILL.md&quot;&gt;SKILL.md&lt;/a&gt; Philosophy 섹션 첫 줄이 &quot;Tests should verify behavior through public interfaces, not implementation details&quot;이다. 한국어로 풀면 &lt;b&gt;&quot;테스트는 사용자나 호출자가 실제로 보는 결과를 검증해야지, 내부에서 함수가 어떻게 호출됐는지 같은 구현 디테일을 검증하면 안 된다&quot;&lt;/b&gt;이다. 별 것 아닌 것 같지만 실제 코드에 적용하면 결과가 꽤 다르다. 내가 짠 모듈 안에서 다른 함수가 몇 번 호출됐는지 검증하는 테스트, 내부 메서드 시그니처가 바뀌면 깨지는 테스트, 이런 것이 다 &quot;구현 결합&quot; 테스트다.&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;mocking.md 파일에는 더 강한 룰이 박혀 있다. &quot;Don't mock: Your own classes/modules, Internal collaborators, Anything you control&quot;이다. 풀어 쓰면 자기가 만든 클래스나 모듈, 그리고 내부 collaborator처럼 자기 통제 안에 있는 모든 것에는 mock을 쓰지 말라는 것이다. mock의 적정 자리는 &lt;b&gt;&quot;내가 통제하지 못하는 것&quot;&lt;/b&gt;, 즉 시스템 경계뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Pocock이 직접 정리한 Before/After&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pocock 본인이 X에서 결과를 이렇게 요약했다.&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;&quot;Before: dozens of shit tests, coupled to implementation. After: only the tests required, validating real behavior.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;번역하면 &quot;전에는 구현에 결합된 쓰레기 테스트가 수십 개. 지금은 진짜 동작을 검증하는 필요한 테스트만 남았다&quot;이다. 테스트 갯수가 줄었는데 신뢰도는 올라간 것이다. 양보다 질이라는 흔한 말 같지만, 코드베이스에 박힌 테스트의 절반이 사라지는 것을 보면 좀 다른 얘기처럼 느껴진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 Mock이 왜 나쁘다는 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;795&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VQDSd/dJMcabYncGf/gPsC5DyX0k9nHP8DXbilp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VQDSd/dJMcabYncGf/gPsC5DyX0k9nHP8DXbilp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VQDSd/dJMcabYncGf/gPsC5DyX0k9nHP8DXbilp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVQDSd%2FdJMcabYncGf%2FgPsC5DyX0k9nHP8DXbilp0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;795&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;795&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://matteomanferdini.com/ios-unit-testing/&quot;&gt;Matteo Manferdini (532KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구현에 결합되는 순간 리팩토링이 불가능해진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pocock이 SKILL.md에서 한 말이 직설적이다. &quot;If you rename an internal function and tests fail, those tests were testing implementation, not behavior.&quot; 내부 함수 이름만 바꿨는데 테스트가 깨지면, 그 테스트는 동작이 아니라 구현을 검증하던 것이다.&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;이것이 회사에서 어떻게 나타나는가? 누가 좀 큰 리팩토링을 하려고 보면 테스트 30개가 빨갛게 변한다. 동작은 안 바뀌었는데도 그렇다. 테스트 코드를 같이 30개 고쳐야 하고, 그러다 보면 리팩토링 자체를 포기하게 된다. 코드는 점점 썩어 간다. mock 떡칠한 코드베이스의 전형적인 운명이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Mock이 통과해도 진짜 코드 경로는 한 번도 돌지 않은 가짜 신뢰&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 더 무서운 부분이다. mock으로 의존성을 다 갈아끼우면 테스트는 초록색이 뜨지만, 진짜 코드 경로는 한 번도 실행되지 않았을 수 있다. 결제 서비스 내부에 if문 분기가 있는데, mock이 그냥 &quot;결제 성공&quot; 객체만 리턴하면 그 분기는 타지 않고 통과돼 버리는 식이다. 프로덕션에서 터지고 나서 &quot;근데 테스트는 통과했는데요?&quot;라고 말하는 상황이 이래서 생긴다. 카카오페이 기술블로그에서도 &lt;a href=&quot;https://tech.kakaopay.com/post/mock-test-code/&quot;&gt;Mock Test 잘 쓰는 법&lt;/a&gt;을 다루면서 비슷한 문제를 지적했다. mock이 거짓 안전망이 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LLM이 테스트 10개를 갈겨쓸 때 가장 큰 피해가 발생한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 핵심 포인트다. AI 코딩 도구는 한 번에 테스트 여러 개를 한꺼번에 생성한다. 프롬프트 한 줄에 &quot;테스트 추가해줘&quot;라고 하면 10개짜리 테스트 파일이 한 방에 나온다. 그런데 이것을 통과시키는 가장 쉬운 방법이 mock이다. LLM 입장에서는 mock으로 의존성을 다 갈아끼우면 테스트 짜기가 훨씬 쉽다. 진짜 동작을 분석할 필요도 없고, 그냥 &quot;이 함수가 호출됐는지&quot; 검증하면 된다. LLM이 만든 테스트는 거의 다 mock 떡칠인 경우가 많고, 위에서 말한 &lt;b&gt;&quot;구현 결합 + 가짜 신뢰&quot;&lt;/b&gt; 안티패턴이 극단적으로 증폭된다.&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;Pocock이 TDD 스킬을 만든 진짜 이유가 이것인 듯하다. 사람에게 가르치려는 것이 아니라, Claude Code가 코드를 짤 때 이런 함정에 빠지지 않게 하려고 만든 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mock을 써도 되는 자리는 어디인가? 시스템 경계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;917&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvkB0H/dJMcabqx2tp/wOvNiftswEV6Bo8SNKzYr0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvkB0H/dJMcabqx2tp/wOvNiftswEV6Bo8SNKzYr0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvkB0H/dJMcabqx2tp/wOvNiftswEV6Bo8SNKzYr0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdvkB0H%2FdJMcabqx2tp%2FwOvNiftswEV6Bo8SNKzYr0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;917&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;917&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.docker.com/blog/testcontainers-testing-with-real-dependencies/&quot;&gt;docker.com&lt;/a&gt;&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;Pocock 룰에서 mock이 허용되는 자리는 시스템 경계뿐이다. 시스템 경계가 무엇인가? &lt;b&gt;내가 통제할 수 없는 영역&lt;/b&gt;이다. 외부 결제 API(Stripe, Toss), 이메일 발송(SendGrid), 외부 webhook, 푸시 알림 서비스 같은 것이다. 이런 것은 테스트마다 진짜로 호출하면 돈도 들고 속도도 느리며, 진짜 사람에게 메일이 가서 사고가 난다.&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;다만 DB는 좀 미묘하다. 옛날에는 DB도 무조건 mock 했지만, 요즘은 &lt;a href=&quot;https://testcontainers.com/&quot;&gt;Testcontainers&lt;/a&gt; 같은 도구로 진짜 DB를 띄워 놓고 테스트하는 것이 표준이 됐다. Pocock도 가급적 진짜 DB를 쓰라고 한다. 트랜잭션, 인덱스, constraint 같은 것은 mock으로는 절대 잡지 못한다. 여기에 한 마디 더 붙이면, 인메모리 SQLite 같은 다른 종류의 DB로 mock하는 것도 위험하다. 프로덕션이 PostgreSQL인데 테스트는 SQLite라면 똑같지 않다. 같은 DB의 도커 컨테이너가 정답이다.&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;Date.now(), Math.random(), 파일 읽기/쓰기도 시스템 경계다. 이 자리에서는 mock 혹은 stub을 허용한다. 안 그러면 테스트가 시간에 따라 결과가 바뀌는 flaky test가 된다. Pocock의 시스템 경계 정의를 표로 정리하면 이런 모양이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;카테고리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;mock 가능?&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부 서비스&lt;/td&gt;
&lt;td&gt;Stripe, SendGrid, Slack API&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외부 DB(이상적)&lt;/td&gt;
&lt;td&gt;Testcontainers PostgreSQL&lt;/td&gt;
&lt;td&gt;mock 안 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시간/난수&lt;/td&gt;
&lt;td&gt;Date, Math.random&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;파일 시스템&lt;/td&gt;
&lt;td&gt;fs.readFile&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내가 만든 클래스&lt;/td&gt;
&lt;td&gt;UserService, OrderRepository&lt;/td&gt;
&lt;td&gt;**금지**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;내부 헬퍼 함수&lt;/td&gt;
&lt;td&gt;calculateTax, formatDate&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 연결 테스트는 어떤 코드인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mock으로 떡칠된 나쁜 테스트는 보통 이렇게 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777688103713&quot; class=&quot;lisp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;test('주문 결제 처리', () =&amp;gt; {
  const paymentService = { charge: vi.fn() };
  const orderService = new OrderService(paymentService);

  orderService.placeOrder({ amount: 1000 });

  expect(paymentService.charge).toHaveBeenCalledWith(1000);
});
&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;이 테스트가 검증하는 것은 &quot;OrderService가 paymentService.charge를 1000으로 호출했음&quot;이다. 즉, 내부 호출 시퀀스를 검증하는 것이다. orderService를 리팩토링해서 charge 대신 chargeAsync로 바꾸면, 동작이 똑같아도 테스트가 깨진다.&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;Pocock 식 좋은 테스트는 이렇게 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777688103714&quot; class=&quot;lisp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;test('주문 결제 처리', async () =&amp;gt; {
  const orderService = new OrderService(realPaymentGatewayStub);

  const result = await orderService.placeOrder({ amount: 1000 });

  expect(result.status).toBe('paid');
  expect(result.amount).toBe(1000);
});
&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;여기에서는 외부 결제 게이트웨이만 stub으로 두고, OrderService 자체는 진짜 인스턴스다. 검증하는 것도 &lt;b&gt;&quot;결과가 paid로 나왔는가&quot;&lt;/b&gt;라는 외부에서 보이는 동작이다. 내부에서 charge를 호출하든 chargeAsync를 호출하든, 결과만 같으면 통과한다. 리팩토링을 해도 잘 깨지지 않는다.&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;이상하게 들리지만, 통합 스타일 테스트가 단위 테스트보다 중복이 적은 경우가 많다. 단위 테스트는 모듈마다 mock을 깔고 테스트하니까 같은 동작을 여러 곳에서 검증하게 된다. 반면 통합 테스트는 &quot;사용자가 주문하면 결제가 된다&quot; 한 줄로 여러 컴포넌트가 같이 검증된다. &lt;a href=&quot;https://techblog.woowahan.com/19509/&quot;&gt;우아한형제들 기술블로그의 프론트엔드 통합 테스트 글&lt;/a&gt;도 비슷한 결론에 도달했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;인터페이스 설계만 잘 해도 mock이 거의 필요 없어진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pocock의 interface-design.md에 박힌 룰 중 하나가 &quot;Accept dependencies, don't create them&quot;이다. 의존성을 함수나 클래스 안에서 직접 만들지 말고 인자로 받으라는 것이다. 이것이 곧 의존성 주입(Dependency Injection)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777688103714&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;typescript&quot;&gt;&lt;code&gt;// Bad: 안에서 만들면 테스트할 때 mock 강제됨
class OrderService {
  private payment = new StripePaymentGateway();
}

// Good: 받으면 테스트에서 진짜 객체든 stub이든 자유롭게 넣을 수 있음
class OrderService {
  constructor(private payment: PaymentGateway) {}
}
&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;DI를 적용하면 시스템 경계만 stub으로 갈아끼우고 나머지는 다 진짜 객체로 통합 테스트가 가능해진다.&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;또 다른 룰은 &quot;Return results, don't produce side effects&quot;이다. 함수가 안에서 DB에 쓰고 메일 보내고 다 하지 말고, 가능한 한 값만 리턴하고 부수효과는 바깥에서 처리하라는 것이다. &lt;b&gt;순수 함수는 mock이 필요 없다.&lt;/b&gt; 입력 넣고 출력만 검증하면 끝이다. 마지막은 좁은 인터페이스 설계다. &quot;이 모듈이 무슨 기능을 하는가&quot;를 SDK처럼 명확하게 정의해 두면, 테스트할 때 그 인터페이스만 검증하면 된다. 내부 구현은 알 필요가 없다. 이것은 &lt;a href=&quot;https://martinfowler.com/articles/mocksArentStubs.html&quot;&gt;Martin Fowler의 Mocks Aren't Stubs&lt;/a&gt; 글에서 말한 Classicist 진영의 입장과 잘 맞는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TDD 트레이서 불릿: 한 번에 하나씩 RED, GREEN, REFACTOR&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S8ZzU/dJMcabD3VSe/Q1CujlEHnT5O4jskdI1eJK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S8ZzU/dJMcabD3VSe/Q1CujlEHnT5O4jskdI1eJK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S8ZzU/dJMcabD3VSe/Q1CujlEHnT5O4jskdI1eJK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS8ZzU%2FdJMcabD3VSe%2FQ1CujlEHnT5O4jskdI1eJK%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;421&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://dev.to/mungaben/the-tdd-cycle-red-green-refactor-1aaf&quot;&gt;dev.to&lt;/a&gt;&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;Pocock이 강조하는 또 한 가지가 트레이서 불릿(tracer bullet) 방식이다. 테스트 한 개만 빨갛게 만들고, 그 한 개만 통과시키고, 그 다음 한 개로 넘어가는 식이다. 한 번에 10개를 빨갛게 만들면 어디부터 손대야 할지 길을 잃는다. LLM에게 시킬 때도 똑같다.&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;스킬에 박혀 있는 강력한 룰이 &quot;Never refactor while RED. Get to GREEN first.&quot;이다. &lt;b&gt;빨간 상태에서 리팩토링하면 안 된다.&lt;/b&gt; 일단 통과시키고, 통과된 상태에서 코드를 정리하라는 것이다. Kent Beck의 원래 TDD 룰 그대로인데, LLM이 자주 어기는 룰이기도 하다. AI는 자꾸 &quot;이왕 바꾸는 김에 이것도...&quot; 하면서 RED 상태에서 코드를 다 갈아엎으려 한다.&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;수직 슬라이스(vertical slice)는 한 기능을 UI에서 DB까지 얇게 한 번 관통하는 구현이다. 트레이서 불릿이 그 슬라이스 하나하나다. &quot;주문 생성&quot;이라는 큰 기능을 한 번에 다 짜지 말고, 일단 주문 생성 후 ID 반환만 통과시킨 다음 amount 저장으로 넘어가고, 그 다음에 주문 상태 pending 저장 식으로 한 줄씩 박는 것이다. 각 단계마다 통합 테스트 하나만 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한국 개발자가 오늘 당장 적용하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 코드베이스에 mock으로 떡칠된 테스트가 수백 개 있다면, 한 번에 다 갈아엎는 것은 비현실적이다. 새로 짜는 코드부터 Pocock 룰을 적용해 보고, 리팩토링하다 깨지는 mock 테스트는 살리지 말고 통합 테스트로 대체하는 식이 현실적이다. 핵심 비즈니스 로직 위주로 통합 테스트 비율을 늘리고, 외부 의존이 진짜 외부 의존인지 한 번 더 확인해 보면 어느 정도 정리된다.&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;JS/TS 생태계에서는 Vitest나 Jest를 쓰면 된다. Testcontainers 노드 클라이언트도 잘 돼 있다. 한국어 자료는 카카오페이, 우아한형제들, 토스 기술블로그에 단편적으로 있는데, Pocock 룰의 프레임으로 다시 읽으면 새롭게 보일 것이다.&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;Claude Code를 쓰는 사람이라면 더 쉽다. mattpocock/skills 저장소를 클론해서 본인 .claude/skills/ 폴더에 복사하면 된다. 그 다음부터 Claude Code가 테스트를 짤 때 자동으로 이 룰을 따른다. &lt;a href=&quot;https://www.aihero.dev/skill-test-driven-development-claude-code&quot;&gt;Pocock 본인이 정리한 가이드&lt;/a&gt;에 셋업 방법이 자세히 나와 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Mock은 도구이지 종교가 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Matt Pocock TDD 스킬의 핵심은 mock을 무조건 쓰지 말라는 것이 아니다. &lt;b&gt;mock을 어디에 쓸 것인가&lt;/b&gt;가 중요하다는 것이다. 좋은 테스트는 public 인터페이스로 외부에서 보이는 동작을 검증하고, 내가 통제하는 코드에는 mock을 쓰지 않으며, mock은 시스템 경계인 외부 API나 시간, 난수 영역에서만 허용된다. 거기에 RED 상태에서 리팩토링하지 말고 트레이서 불릿으로 한 줄씩 박으라는 룰, 그리고 인터페이스 설계만 잘 하면(DI, 순수 함수) mock이 거의 필요 없어진다는 룰까지 묶이면 끝이다.&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;AI 시대에 이것이 더 중요해진 이유는 LLM이 mock으로 가짜 통과를 만들기가 너무 쉽기 때문이다. Claude Code 같은 도구를 쓸수록 사람이 더 빡세게 룰을 박아 둬야 한다.&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;다만 mock 자체가 악은 아니다. 외부 결제 시스템을 진짜로 호출하는 테스트는 짜면 안 된다. 그러나 자기가 짠 OrderService를 mock하는 테스트는 거의 항상 잘못 짠 것이다. 다음에 본인 PR을 열 때, 새로 추가된 mock을 한 번씩만 들여다보고 자문해 보았다. &lt;b&gt;&quot;이 mock, 진짜 시스템 경계인가? 아니면 그냥 내가 게을러서 갈아끼운 것인가?&quot;&lt;/b&gt; 답이 후자라면 그 mock은 빼고 통합 테스트로 다시 짜는 것이 맞다.&lt;/p&gt;</description>
      <category>개발 방법론</category>
      <category>Claude Code TDD</category>
      <category>mock 남용</category>
      <category>TDD mock 안티패턴</category>
      <category>테스트 더블</category>
      <category>통합 테스트 vs 단위 테스트</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/405</guid>
      <comments>https://yscho03.tistory.com/405#entry405comment</comments>
      <pubDate>Sat, 2 May 2026 11:15:30 +0900</pubDate>
    </item>
    <item>
      <title>검색을 제대로 수행하는 방법: BM25, 시맨틱, 하이브리드 검색</title>
      <link>https://yscho03.tistory.com/404</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 답변이 엉뚱한 결과를 내놓는다면 90%는 retrieval이 잘못된 경우다. 모델을 탓하기 전에 검색부터 의심해야 한다. 그리고 검색이라는 것이 임베딩 한 번 박아 넣는다고 끝나는 일이 아니라는 사실은, RAG를 한 번이라도 돌려본 사람이라면 모두 안다.&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;처음에는 다들 비슷하게 시작한다. OpenAI 임베딩을 뽑아 pgvector나 Qdrant에 넣고, 코사인 유사도로 top-k를 뽑으면 알아서 잘 동작할 것이라 기대한다. 그러나 막상 운영에 들어가면 사용자가 &quot;PRD-2024-031 문서를 찾아달라&quot;고 했는데 엉뚱한 결과가 나오고, &quot;환불 정책&quot;을 검색했는데 정확한 정책 문서 대신 환불 관련 잡담 글이 1등을 차지한다. 이것이 바로 시맨틱 검색만 사용할 때 무너지는 전형적 사례다.&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;이 글에서는 BM25, 시맨틱 검색, 하이브리드 검색의 차이를 실전 관점에서 정리하고, 왜 AI 시대에 retrieval이 LLM 자체보다 중요해지는지까지 풀어본다. 2026년 기준 최신 기법(contextual retrieval, GraphRAG, late-interaction)도 함께 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;검색 엔진이 갑자기 다시 중요해진 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;449&quot;&gt;&lt;span data-url=&quot;https://cdn.prod.website-files.com/65cbae0a3956181cf7a74c75/66d1b783f6b8d0d2478dc0f9_RAG-architecture-simple.png&quot; data-phocus=&quot;https://cdn.prod.website-files.com/65cbae0a3956181cf7a74c75/66d1b783f6b8d0d2478dc0f9_RAG-architecture-simple.png&quot;&gt;&lt;img src=&quot;https://cdn.prod.website-files.com/65cbae0a3956181cf7a74c75/66d1b783f6b8d0d2478dc0f9_RAG-architecture-simple.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fcdn.prod.website-files.com%2F65cbae0a3956181cf7a74c75%2F66d1b783f6b8d0d2478dc0f9_RAG-architecture-simple.png&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; loading=&quot;lazy&quot; width=&quot;1192&quot; height=&quot;449&quot; data-origin-width=&quot;1192&quot; data-origin-height=&quot;449&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.pryon.com/landing/what-is-retrieval-augmented-generation&quot;&gt;pryon.com&lt;/a&gt;&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;검색이 다시 주목받게 된 것은 LLM이 등장한 이후부터다. 정확히 말하면 RAG (Retrieval-Augmented Generation) 패턴이 표준이 되면서부터 retrieval 품질이 곧 답변 품질이 되었다.&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;원리는 단순하다. LLM은 자기 가중치(weights) 안에 학습된 지식과 프롬프트로 들어온 컨텍스트만으로 답한다. 사내 문서, 최신 뉴스, 고객 데이터 같은 정보는 학습에 포함되어 있지 않으므로 retrieval로 떠먹여 주어야 한다. 떠먹여 준 자료가 잘못되면 어떻게 되는가? 모델이 아무리 똑똑해도 답이 망가진다. 결국 옛날의 &quot;garbage in, garbage out&quot; 원칙이 LLM 시대에 부활한 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색이 부실하면 LLM이 아무리 좋아도 답이 망가진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-4든 Claude Opus든 Gemini Ultra든 컨텍스트가 잘못 들어가면 모두 똑같이 헛소리를 한다. 실제로 RAG 실패를 분석해 보면 대부분의 잘못된 답변이 &quot;관련 문서가 retrieval 단계에서 아예 잡히지 않은 경우&quot;다. 모델이 답을 지어낸 것이 아니라 답을 만들 재료 자체가 없었던 것이다.&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;흔한 실패 패턴은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사용자가 &quot;환불 정책&quot;을 물었는데 retrieval이 &quot;환불 후기&quot; 글을 1등으로 뽑는다 &amp;rarr; 모델이 후기 기준으로 답변한다&lt;/li&gt;
&lt;li&gt;코드 스니펫에서 함수명을 정확히 검색해야 하는데 시맨틱 검색이 &quot;비슷한 함수&quot;를 더 위에 올린다&lt;/li&gt;
&lt;li&gt;한국어 형태소 분석을 하지 않아 &quot;삼성전자&quot;와 &quot;삼성&quot;이 다른 토큰으로 처리된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;hallucination을 줄이는 가장 빠른 방법은 retrieval 개선이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hallucination을 줄이려고 모델 fine-tuning부터 알아보는 사람이 많은데, 비용 대비 효과로 보면 retrieval을 손보는 쪽이 훨씬 빠르다. 정답이 들어 있는 문서 청크가 LLM 컨텍스트에 들어가기만 하면, 웬만한 모델은 그것을 잘 인용해 답한다. 정답 청크를 retrieval이 찾지 못하면 무슨 짓을 해도 답이 나오지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키워드 검색(BM25)이 아직도 살아있는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://substackcdn.com/image/fetch/$s_!Bhxe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d71335e-5177-4228-87bf-dde550e55e79_1936x1200.png&quot; data-phocus=&quot;https://substackcdn.com/image/fetch/$s_!Bhxe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d71335e-5177-4228-87bf-dde550e55e79_1936x1200.png&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!Bhxe!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4d71335e-5177-4228-87bf-dde550e55e79_1936x1200.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21Bhxe%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F4d71335e-5177-4228-87bf-dde550e55e79_1936x1200.png&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; loading=&quot;lazy&quot; width=&quot;1936&quot; height=&quot;1200&quot; data-origin-width=&quot;1936&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://blog.dailydoseofds.com/p/how-does-bm25-ranking-algorithm-work&quot;&gt;Daily Dose of Data Science (217KB)&lt;/a&gt;&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;BM25는 1994년에 등장한 알고리즘이다. 그런데 2026년 지금도 Elasticsearch, OpenSearch, Vespa, Tantivy 같은 거의 모든 검색 엔진의 기본값이다. 30년 된 알고리즘이 왜 사라지지 않았을까? 답은 단순하다. 키워드 매칭에서 BM25를 이기는 알고리즘이 좀처럼 없다.&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;BM25는 TF-IDF의 진화형이다. 한 문서에 쿼리 단어가 자주 나오면 점수가 올라가고(term frequency), 그 단어가 전체 코퍼스에서 희귀하면 점수가 더 올라가며(inverse document frequency), 문서 길이로 정규화도 한다. 이렇게 단순한데도 정확한 단어 매칭 시나리오에서는 임베딩 기반 검색을 자주 이긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BM25 수식을 외우지 않아도 된다, 직관만 잡으면 된다&lt;/h3&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;&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;: &quot;신청서 양식 PRD-2024-031&quot; 같은 쿼리에서 &quot;PRD-2024-031&quot;은 코퍼스에 거의 등장하지 않는 토큰이다. BM25는 이 단어가 매치되면 점수를 크게 부여한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;쿼리 단어가 여러 번 나와도 점수가 무한정 오르지 않는다&lt;/b&gt;: log 스케일과 비슷하게 saturate된다. &quot;고양이 고양이 고양이&quot;가 &quot;고양이&quot;보다 약간만 점수가 높다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;긴 문서가 단순히 길다는 이유로 유리해지지 않도록 정규화&lt;/b&gt;: 기본 파라미터 b=0.75 정도가 평균적이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Elasticsearch / OpenSearch / Tantivy 같은 엔진들은 모두 BM25 기반이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용 중인 검색 인프라가 무엇이든 BM25는 무료로 깔려 있다. Elasticsearch의 기본 similarity가 BM25다. PostgreSQL의 tsvector 기본 ts_rank는 BM25가 아니라 단순 TF 기반이라 IDF/문서길이 정규화가 빠져 있다. BM25를 제대로 쓰려면 ParadeDB나 VectorChord-BM25 같은 익스텐션을 박아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국어에서 BM25를 쓸 때 형태소 분석기를 박지 않으면 망한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영어 BM25를 그대로 한국어에 가져다 쓰면 그야말로 망한다. 영어는 띄어쓰기가 단어 경계라 토크나이징이 거의 자동이지만, 한국어는 조사가 붙고 어미가 붙으며 합성어가 마구 만들어진다.&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;예를 들어 &quot;삼성전자가 반도체를 만든다&quot;라는 문서가 있을 때, 단순 띄어쓰기 토크나이저는 &quot;삼성전자가&quot;, &quot;반도체를&quot;, &quot;만든다&quot;를 토큰으로 만든다. 사용자가 &quot;삼성전자&quot;로 검색하면 &quot;삼성전자가&quot;와 매치되지 않는다.&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;해결책은 형태소 분석기를 박는 것이다. mecab-ko, nori (Elasticsearch 한국어 분석기), khaiii, kiwi 같은 것을 골라 쓰면 된다. nori는 Elasticsearch에 공식 플러그인으로 들어가 있어 가장 무난하다. 이것을 박지 않고 한국어 BM25를 한다는 것은 그저 검색을 망치겠다는 선언이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시맨틱 검색(Semantic / Dense Retrieval)이 잘하는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시맨틱 검색은 텍스트를 벡터(보통 768 ~ 3072 차원)로 임베딩하고, 쿼리 벡터와 문서 벡터들 사이의 코사인 유사도(또는 내적)로 가까운 결과를 뽑는 방식이다. dense retrieval이라고도 부른다.&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;강점은 명확하다. 단어가 달라도 의미가 비슷하면 잡아낸다. &quot;자동차 보험&quot;으로 검색해도 &quot;차량 보험&quot; 문서가 함께 잡힌다. &quot;환불&quot;로 검색하면 &quot;취소 후 입금&quot; 문서까지 잡힌다. BM25는 이것이 절대 되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임베딩 모델 고르는 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 기준으로 임베딩 모델 선택지가 너무 많다. 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OpenAI text-embedding-3-small / large&lt;/b&gt;: 무난한 기본값. multilingual이 잘 되고 한국어도 쓸 만하다. 비용도 적당하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BGE (BAAI General Embedding) M3&lt;/b&gt;: 오픈소스 중 최강 라인. 자체 호스팅 가능, multilingual 지원, MTEB 상위권이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;E5-mistral-7b-instruct&lt;/b&gt;: 큰 모델 기반이라 정확도는 좋으나 추론 비용이 크다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cohere embed-multilingual-v3&lt;/b&gt;: 한국어 포함 100개 이상의 언어를 지원하며, API 비용도 합리적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한국어 특화 모델&lt;/b&gt;: KoSimCSE, ko-sbert 같은 모델 &amp;mdash; 도메인이 맞으면 OpenAI보다 좋은 결과가 나오기도 한다.&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://huggingface.co/spaces/mteb/leaderboard&quot;&gt;MTEB Leaderboard&lt;/a&gt;에서 한국어(또는 multilingual) 카테고리를 보면 된다. 다만 벤치마크 1등이 자기 데이터에서도 1등이라는 보장은 없으므로, 실제 자기 코퍼스 일부로 검증을 한 번 돌려보는 것이 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;청킹 전략이 잘못되면 시맨틱 검색도 망한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩 모델을 잘 골랐다고 끝이 아니다. 청킹(chunking)이 잘못되면 검색이 모두 망가진다. 왜일까? 임베딩은 청크 단위로 만들어지는데, 청크가 너무 크면 의미가 희석되고 너무 작으면 컨텍스트가 끊기기 때문이다.&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;흔한 청킹 함정은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;고정 길이로 자르기&lt;/b&gt;: 500 토큰씩 단순 분할 &amp;rarr; 문장 중간에서 잘림&lt;/li&gt;
&lt;li&gt;&lt;b&gt;너무 작은 청크&lt;/b&gt;: 100 토큰씩 자르면 표/리스트 단위로 의미가 파괴됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메타데이터 누락&lt;/b&gt;: 청크에 출처 문서명, 섹션 제목, 날짜 같은 것을 박지 않으면 검색 결과 정렬/필터가 모두 동작하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권장은 의미 단위 청킹(semantic chunking) 또는 문단/섹션 단위 청킹이다. 그리고 청크에 헤더를 prepend하는 것이 거의 항상 도움이 된다. Anthropic이 발표한 &lt;a href=&quot;https://www.anthropic.com/news/contextual-retrieval&quot;&gt;contextual retrieval&lt;/a&gt; 기법이 바로 이 아이디어인데, 청크 앞에 50~100 토큰짜리 문서 컨텍스트 요약을 붙이면 검색 실패율이 35% 정도 줄어든다고 한다 (BM25까지 결합하면 49%, 리랭커까지 얹으면 67%).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벡터 DB 종류 (pgvector, Qdrant, Weaviate, Pinecone, Milvus)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터 DB 선택은 데이터 규모와 운영 인력에 따라 갈린다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;약점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pgvector&lt;/td&gt;
&lt;td&gt;이미 PostgreSQL을 쓰면 0설정, 트랜잭션 OK&lt;/td&gt;
&lt;td&gt;1000만 벡터를 넘어가면 느려짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qdrant&lt;/td&gt;
&lt;td&gt;빠르고 자체 호스팅이 무난하며 필터가 강함&lt;/td&gt;
&lt;td&gt;운영 학습 곡선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weaviate&lt;/td&gt;
&lt;td&gt;하이브리드 검색 내장, GraphQL API&lt;/td&gt;
&lt;td&gt;메모리를 많이 먹음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinecone&lt;/td&gt;
&lt;td&gt;매니지드 서비스, 운영 부담 0&lt;/td&gt;
&lt;td&gt;비싸고 락인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Milvus&lt;/td&gt;
&lt;td&gt;대규모 (10억 벡터) 가능, 오픈소스&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;100만 벡터 이하라면 pgvector로 시작해도 충분하다. 그 이상으로 가면 Qdrant나 Weaviate를 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;하이브리드 검색(Hybrid Search)이 답인 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;285&quot;&gt;&lt;span data-url=&quot;https://opensearch.org/wp-content/uploads/2025/02/RRFFinalScoreCalculations-300x285.png&quot; data-phocus=&quot;https://opensearch.org/wp-content/uploads/2025/02/RRFFinalScoreCalculations-300x285.png&quot;&gt;&lt;img src=&quot;https://opensearch.org/wp-content/uploads/2025/02/RRFFinalScoreCalculations-300x285.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fopensearch.org%2Fwp-content%2Fuploads%2F2025%2F02%2FRRFFinalScoreCalculations-300x285.png&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; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;285&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;285&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://opensearch.org/blog/introducing-reciprocal-rank-fusion-hybrid-search/&quot;&gt;opensearch.org&lt;/a&gt;&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;BM25는 단어 매칭에 강하고, 시맨틱은 의미 매칭에 강하다. 이 둘의 약점이 정확히 반대다. 그래서 둘을 합치면 거의 항상 둘 중 하나만 쓰는 것보다 결과가 좋다. 이것이 하이브리드 검색이다.&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;&lt;a href=&quot;https://github.com/beir-cellar/beir&quot;&gt;BEIR 벤치마크&lt;/a&gt; 결과를 보면 거의 모든 도메인에서 하이브리드 검색이 BM25 단독, dense 단독을 모두 이긴다. 차이가 작은 도메인도 있고 큰 도메인도 있는데, 평균적으로 nDCG@10 기준 5~15% 정도의 향상이 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RRF가 가성비 좋은 기본값이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하이브리드 결합 방법 중 가장 무난한 것이 RRF (Reciprocal Rank Fusion)다. 수식은 진짜 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777683318046&quot; class=&quot;lisp&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;RRF_score(d) = sum( 1 / (k + rank_i(d)) )
&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;각 검색 시스템(BM25, dense)에서 문서 d의 순위(rank)를 가져와 1/(k+rank) 형태로 합치는 방식이다. k는 보통 60이다. 핵심 장점은 &lt;b&gt;점수 정규화를 신경 쓰지 않아도 된다&lt;/b&gt;는 점이다. BM25 점수와 코사인 유사도 점수는 스케일이 완전히 달라서 단순 가중합을 하면 한쪽이 지배해 버리는데, RRF는 순위만 사용하므로 그 문제가 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777683318046&quot; class=&quot;xquery&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def rrf_combine(results_bm25, results_dense, k=60):
    scores = {}
    for rank, doc in enumerate(results_bm25, start=1):
        scores[doc.id] = scores.get(doc.id, 0) + 1.0 / (k + rank)
    for rank, doc in enumerate(results_dense, start=1):
        scores[doc.id] = scores.get(doc.id, 0) + 1.0 / (k + rank)
    return sorted(scores.items(), key=lambda x: -x[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;이 함수 하나만 짜면 하이브리드 검색이 일단 굴러간다. 막 만든 것치고는 성능이 깜짝 놀랄 정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;점수 정규화를 하지 않으면 sparse가 dense를 짓밟는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RRF 말고 점수 가중합 (Convex Combination) 방식도 많이 쓴다. score = alpha dense + (1-alpha) sparse 식인데, 이때 두 점수의 스케일이 맞지 않으면 망한다.&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;BM25 점수는 보통 0~50 범위이고, 코사인 유사도는 -1~1 범위다. 이것을 그냥 더하면 BM25의 영향이 압도적이다. 그래서 min-max 정규화나 z-score 정규화를 한 다음에 합쳐야 한다. 이것이 귀찮아서 그냥 RRF를 쓰는 사람이 많다.&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;alpha 값은 0.5에서 시작해 자기 데이터에 맞게 튜닝한다. 키워드 검색이 더 중요한 도메인(법률, 의료, 코드)은 alpha를 낮추고, 의미 매칭이 더 중요한 도메인(고객 지원, 자연어 Q&amp;amp;A)은 alpha를 높인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;하이브리드를 만들기 가장 쉬운 스택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 기준 하이브리드 검색을 가장 쉽게 만드는 옵션은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Elasticsearch 8+&lt;/b&gt;: BM25와 dense vector 검색이 모두 네이티브로 지원된다. RRF 쿼리도 8.8부터 들어왔다. _search/hybrid 엔드포인트 한 방이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Weaviate&lt;/b&gt;: hybrid search가 기본 API다. alpha 파라미터 한 줄로 조정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OpenSearch&lt;/b&gt;: 2.10+에서 hybrid search 파이프라인을 지원한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Qdrant + 별도 BM25&lt;/b&gt;: Qdrant는 dense만 다루므로 BM25는 Tantivy나 Elasticsearch로 따로 운영하고 결과를 합치는 형태다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Vespa&lt;/b&gt;: 처음부터 하이브리드를 염두에 두고 만들어진 엔진이다. 러닝 커브가 빡세지만 강력하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;889&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bswW0U/dJMcacXfSxA/9P8tumckwkD7Xg2HTa64rK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bswW0U/dJMcacXfSxA/9P8tumckwkD7Xg2HTa64rK/img.png&quot; data-alt=&quot;출처: https://milvus.io/docs/ko/milvus_hybrid_search_retriever.md&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bswW0U/dJMcacXfSxA/9P8tumckwkD7Xg2HTa64rK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbswW0U%2FdJMcacXfSxA%2F9P8tumckwkD7Xg2HTa64rK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1698&quot; height=&quot;889&quot; data-origin-width=&quot;1698&quot; data-origin-height=&quot;889&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://milvus.io/docs/ko/milvus_hybrid_search_retriever.md&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 요즘 검색은 어디까지 발전했는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Reranking&lt;/b&gt;: top-k (보통 50~100) 결과를 cross-encoder 리랭커로 다시 정렬한다. Cohere Rerank, BGE reranker, Jina reranker 같은 것이 있다. retrieval에서 50개를 뽑고 리랭커로 top-5를 뽑는 패턴이 정석이다. 정확도가 크게 오르지만 latency가 100~500ms 추가된다. 중요한 쿼리에만 쓰는 것이 답이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Late interaction (ColBERT, ColBERTv2)&lt;/b&gt;: 쿼리와 문서를 토큰 단위로 매칭한다. dense retrieval보다 정밀하고 cross-encoder보다 빠르다. 인덱스 크기가 큰 것이 단점이다. 큰 코퍼스에서는 부담스럽다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SPLADE (Sparse 임베딩)&lt;/b&gt;: BM25처럼 sparse 벡터지만 학습된 가중치를 쓴다. BM25보다 좋고 dense보다 해석이 가능하다. Naver Labs Europe에서 만들었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Contextual Retrieval&lt;/b&gt;: Anthropic이 띄운 기법이다. 청크 앞에 &quot;이 청크는 X 문서의 Y 섹션이고 Z를 다룬다&quot; 식의 컨텍스트를 LLM으로 만들어 prepend한다. 검색 실패율 35% 감소가 보고되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GraphRAG&lt;/b&gt;: 지식 그래프와 벡터 검색을 결합한 방식이다. Microsoft가 띄웠다. 엔티티 간 관계까지 활용해 multi-hop 질문에 강하다. 인덱싱 비용이 크다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Agentic Search&lt;/b&gt;: LLM이 직접 쿼리를 reformulate하고, 검색 결과를 보고 추가 쿼리를 날리며, 자가평가까지 한다. 비용은 크지만 어려운 질문에 강하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다 하면 좋지만 다 하면 비싸다, 우선순위를 정리한다&lt;/h3&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;&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;BM25 한 줄 추가&lt;/b&gt; (이미 dense만 쓰고 있다면 가장 빠른 win)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하이브리드 결합 (RRF)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;청킹 전략 개선 + 메타데이터 보강&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리랭커 도입 (top-k에만)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;contextual retrieval (인덱싱 비용을 감수할 수 있다면)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;ColBERT, GraphRAG, agentic search는 그 다음 순서다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2026년 기준 가장 가성비 좋은 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은~중간 규모 RAG라면 다음 조합이 거의 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;형태소 분석기를 박은 BM25&lt;/li&gt;
&lt;li&gt;OpenAI text-embedding-3-small 또는 BGE-M3 임베딩&lt;/li&gt;
&lt;li&gt;RRF로 하이브리드 결합&lt;/li&gt;
&lt;li&gt;top-50을 뽑고 리랭커로 top-5 추출&lt;/li&gt;
&lt;li&gt;컨텍스트 윈도우에 들어갈 청크에 출처 메타데이터 함께 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것만 해도 어지간한 RAG는 무너지지 않는다. 더 잘하려면 contextual retrieval을 추가한다. 그 위는 도메인 특수성에 따라 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 시대, 검색이 LLM보다 중요해지는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;1242&quot;&gt;&lt;span data-url=&quot;https://assets.ngc.nvidia.com/products/api-catalog/build-an-enterprise-rag-pipeline/diagram.jpg&quot; data-phocus=&quot;https://assets.ngc.nvidia.com/products/api-catalog/build-an-enterprise-rag-pipeline/diagram.jpg&quot;&gt;&lt;img src=&quot;https://assets.ngc.nvidia.com/products/api-catalog/build-an-enterprise-rag-pipeline/diagram.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fassets.ngc.nvidia.com%2Fproducts%2Fapi-catalog%2Fbuild-an-enterprise-rag-pipeline%2Fdiagram.jpg&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; loading=&quot;lazy&quot; width=&quot;1036&quot; height=&quot;1242&quot; data-origin-width=&quot;1036&quot; data-origin-height=&quot;1242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://build.nvidia.com/nvidia/build-an-enterprise-rag-pipeline&quot;&gt;NVIDIA Build (194KB)&lt;/a&gt;&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;요즘 모델 품질이 평준화되는 중이다. Claude 4.7, GPT-5, Gemini 3 Pro가 모두 비슷비슷하다. 어떤 벤치마크는 A가 이기고 어떤 것은 B가 이긴다. 이것이 무엇을 뜻하는가 하면, &lt;b&gt;모델 자체로는 차별화가 점점 어려워진다는 것이다&lt;/b&gt;. 차별화는 어디서 나오는가? 컨텍스트에서 나온다. 그리고 컨텍스트는 retrieval에서 나온다.&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;비즈니스 관점에서도 동일하다. LLM API 비용은 입력 토큰 수에 비례한다. 검색이 정확하면 진짜 필요한 청크 5개만 보내면 된다. 검색이 부실하면 안전마진을 두려고 50개를 보내야 한다. 비용이 10배 차이가 난다. 같은 답변 품질을 더 적은 토큰으로 만드는 회사가 그대로 이긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;더 큰 모델&quot;보다 &quot;더 좋은 검색&quot;이 ROI가 크다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-5 Opus 대신 GPT-5 Sonnet을 쓰면 비용이 1/5이다. 답변 품질은 어떨까? retrieval이 좋으면 거의 차이가 없다. retrieval이 부실하면 큰 모델로도 살리지 못한다. 결국 같은 비용 예산 안에서 retrieval에 쓰는 것이 모델 업그레이드보다 ROI가 크다.&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;평가 지표도 정착되는 중이다. retrieval 자체는 nDCG@10, MRR, recall@k 같은 지표다. 답변 품질은 faithfulness (인용 정확도), answer relevance (질문 관련성)다. &lt;a href=&quot;https://github.com/explodinggradients/ragas&quot;&gt;Ragas&lt;/a&gt; 같은 프레임워크의 표준화가 진행 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검색이 곧 제품 차별화의 해자(moat)가 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM API는 누구나 부를 수 있다. 시맨틱 검색도 라이브러리만 있으면 누구나 깔 수 있다. 차이를 만드는 것은 자기 도메인 데이터에 맞는 retrieval 튜닝이다. 청킹 전략, 메타데이터 설계, 가중치 튜닝, 리랭커 학습 데이터 &amp;mdash; 이것들이 모두 노하우이고 데이터이며 결국 해자다.&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;옛날에 구글이 검색으로 해자를 만들었던 것처럼, 2026년 AI 제품에서는 도메인 특화 retrieval이 새로운 해자다. 모델은 갈아끼울 수 있어도 잘 튜닝된 retrieval은 베끼지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄 요약: 하이브리드 검색부터 챙겨라&lt;/h2&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;&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;BM25는 단어 매칭, 시맨틱은 의미 매칭이다&lt;/b&gt;. 둘 다 약점이 정확히 반대다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;하이브리드 검색이 거의 항상 둘 다보다 잘 나온다&lt;/b&gt;. RRF가 시작점이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 시대 차별화는 LLM이 아니라 retrieval에서 나온다&lt;/b&gt;. 같은 모델로도 retrieval이 다르면 결과는 천지 차이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 RAG를 돌리고 있다면 가장 빠른 개선 순서는 이렇다. 첫째, dense만 쓰고 있다면 BM25를 추가하고 RRF로 결합한다. 둘째, 한국어 코퍼스라면 형태소 분석기(nori, mecab-ko, kiwi)를 박았는지 확인한다. 셋째, 청크 앞에 문서 컨텍스트를 prepend해 본다 (Anthropic contextual retrieval). 넷째, 중요한 쿼리에만 리랭커를 얹는다.&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;이 네 가지만 해도 답변 품질의 체감 차이가 크다. 모델 업그레이드를 검토하기 전에 하이브리드 검색부터 손봐야 한다. 결국 AI 시대 RAG의 ROI는 더 큰 LLM이 아니라 더 좋은 검색에서 나온다. 그것이 답이다.&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>ai 검색 엔진</category>
      <category>BM25</category>
      <category>RAG 검색</category>
      <category>리랭킹</category>
      <category>벡터 검색</category>
      <category>시맨틱 검색</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/404</guid>
      <comments>https://yscho03.tistory.com/404#entry404comment</comments>
      <pubDate>Sat, 2 May 2026 09:58:04 +0900</pubDate>
    </item>
    <item>
      <title>Anthropic이 지급한 $200 보상 크레딧, 90일 안에 사용하지 않으면 소멸된다</title>
      <link>https://yscho03.tistory.com/403</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;4월에 Anthropic으로부터 &quot;Extra Usage Credit이 지급되었다&quot;는 알림을 받고 console.anthropic.com을 뒤져보다가 잔액에 $200이 보이지 않아 당황한 사람이 분명 있을 것이다. 나도 그랬다. 결론부터 말하면 이 보상 크레딧은 console.anthropic.com이 아니라 &lt;b&gt;claude.ai 사용량 페이지&lt;/b&gt;에 표시되며, &lt;b&gt;Extra Usage(추가 사용량)&lt;/b&gt; 토글을 켜고 배너에서 Claim까지 눌러야 비로소 사용할 수 있다. 클레임한 날부터 &lt;b&gt;90일&lt;/b&gt; 안에 사용하지 않으면 그대로 소멸된다. 환불도 되지 않는다.&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;이 글에서는 Anthropic 크레딧 사용방법을 진입 경로부터 차감 순서, 만료일 확인까지 한 번에 정리한다. 영어 docs만 보고 헤매던 독자라면 이 글을 읽고 claude.ai에서 바로 처리하면 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tFaIn/dJMcajotpnC/8mA4ynn52Rk5kx9UGW6BA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tFaIn/dJMcajotpnC/8mA4ynn52Rk5kx9UGW6BA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tFaIn/dJMcajotpnC/8mA4ynn52Rk5kx9UGW6BA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtFaIn%2FdJMcajotpnC%2F8mA4ynn52Rk5kx9UGW6BA1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1001&quot; height=&quot;320&quot; data-origin-width=&quot;1001&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4월에 배포된 $200 크레딧의 정체는 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic이 4월 초 일부 구독자에게 배포한 보상 성격의 크레딧이다. 정식 명칭은 &lt;b&gt;&quot;Extra Usage Credit&quot;&lt;/b&gt; 이다. 수령 대상자는 다음 네 가지 케이스 중 하나에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pro 구독자&lt;/b&gt; - $20 지급&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Max 5x 구독자&lt;/b&gt; - $100 지급&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Max 20x 구독자&lt;/b&gt; - $200 지급&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Team 플랜 구독자&lt;/b&gt; - $200 지급 (계정당)&lt;/li&gt;
&lt;/ul&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;b&gt;2026년 4월 3일 오전 9시(PT) 이전에 활성 구독을 보유하고 있던 계정&lt;/b&gt;이며, 클레임 마감은 &lt;b&gt;2026년 4월 17일(PT)&lt;/b&gt; 까지였다. 마감이 지난 사용자는 이번 크레딧을 받을 수 없다. &lt;b&gt;Enterprise 플랜과 Console(API) 계정은 처음부터 제외&lt;/b&gt;되었기 때문에 콘솔에 들어가서 아무리 찾아봐도 나오지 않는다.&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;claude.ai 사용량 페이지에 표시될 때는 &lt;b&gt;&quot;Extra Usage Credit&quot;&lt;/b&gt; 또는 &lt;b&gt;&quot;Promotional credit&quot;&lt;/b&gt; 으로 적혀 있다. 일반 구독 한도와 다른 점은 단 하나다. &lt;b&gt;유효기간이 있다는 것&lt;/b&gt;이다. 일반 한도는 매달 리셋되지만, 보상 크레딧은 클레임한 날부터 90일이 지나면 그대로 사라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;claude.ai 어디로 들어가야 잔액이 보이는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계 - 올바른 사이트로 접속&lt;/h3&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;a href=&quot;https://claude.ai/&quot;&gt;claude.ai&lt;/a&gt; 에 접속한다. 주의할 점이 있다. &lt;b&gt;claude.ai&lt;/b&gt;와 &lt;b&gt;console.anthropic.com&lt;/b&gt;은 별개의 사이트다. API 콘솔을 사용하던 사람들은 console에서 크레딧을 찾는데, &lt;b&gt;이번 보상 크레딧은 console에 표시되지 않는다&lt;/b&gt;. claude.ai 측 구독자에게 지급되는 것이라 그곳에서만 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계 - 사용량(Usage) 메뉴로 진입&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pro / Max 구독자&lt;/b&gt;: Settings &amp;rarr; Usage&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Team 플랜&lt;/b&gt;: Organization settings &amp;rarr; Usage&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI는 가끔 변경되지만 어쨌든 사용량 관련 메뉴다. 또한 &lt;b&gt;모바일 앱(iOS/Android)에서는 이 토글이 보이지 않는다&lt;/b&gt;. 반드시 웹 브라우저에서 처리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계 - Extra Usage Credit 배너 / 잔액 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Usage 페이지 상단에 &lt;b&gt;&quot;Extra Usage Credit&quot;&lt;/b&gt; 배너가 떠 있는 것이 정상이다. 4월 17일까지였던 클레임 기간에는 &lt;b&gt;&quot;Claim&quot;&lt;/b&gt; 버튼이 그곳에 있었고, 누르면 잔액에 추가되었다. 클레임 후에는 다음 정보가 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;잔액&lt;/b&gt;: 남은 크레딧 금액 (예: 사용량만큼 차감되고 남은 잔액)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;만료일&lt;/b&gt;: &quot;Expires on YYYY-MM-DD&quot; 형식으로 클레임한 날 기준 90일 후 날짜&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgTX7q/dJMcaib8yUp/1rEJyd5Mpy7VuRwDqzqFk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgTX7q/dJMcaib8yUp/1rEJyd5Mpy7VuRwDqzqFk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgTX7q/dJMcaib8yUp/1rEJyd5Mpy7VuRwDqzqFk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgTX7q%2FdJMcaib8yUp%2F1rEJyd5Mpy7VuRwDqzqFk1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;310&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.reddit.com/r/ClaudeAI/comments/1sbsul8/thanks_anthropic_200_credit_extra/?tl=ko&quot;&gt;Reddit (35KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잔액이 0으로 보이거나 줄이 표시되지 않는 흔한 원인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;claude.ai에 들어갔는데 Extra Usage Credit 줄이 아예 보이지 않는다면 다음 중 하나다.&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; - 4월 17일까지 누르지 않은 사용자는 이번 크레딧을 받을 수 없다. 메일을 다시 봐도 소용없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 organization으로 진입&lt;/b&gt; - Team 플랜이라면 좌측 상단 organization 셀렉터를 확인한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;다른 계정으로 로그인됨&lt;/b&gt; - 메일을 받은 계정과 현재 로그인된 계정이 다를 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모바일 앱에서 조회 중&lt;/b&gt; - 웹 브라우저로 다시 진입해야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 케이스는 어쩔 수 없고, 2~4번이라면 계정이나 디바이스를 바꿔서 다시 진입하면 된다. 그래도 잔액이 표시되지 않는다면 &lt;a href=&quot;https://support.anthropic.com/&quot;&gt;Anthropic Support&lt;/a&gt; 에 메일 캡처를 첨부해 문의하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Extra Usage를 켜야 크레딧이 차감된다 (이것이 핵심이다)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 진짜로 헷갈리는 부분이다. 잔액에 $200이 멀쩡히 표시되어 있어도 &lt;b&gt;Extra Usage 토글이 OFF라면 크레딧이 단 한 푼도 차감되지 않는다&lt;/b&gt;. 사용량이 그냥 구독 기본 한도에서 막혀버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Extra Usage란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic Pro / Max / Team 플랜은 매달 사용량 한도(메시지 / 토큰)가 정해져 있는데, 이를 초과한 분량에 대해 &lt;b&gt;유료로 추가 사용을 허용할지 여부&lt;/b&gt;를 결정하는 옵션이 Extra Usage다. 토글이 OFF면 한도 도달 시 그대로 차단된다.&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;기본값이 OFF로 설정된 계정도 있다. 보상 크레딧을 받았다고 자동으로 켜지지 않는다. 사용자가 직접 켜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활성화 방법 단계별 설명&lt;/h3&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;&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;claude.ai 로그인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Settings&lt;/b&gt; &amp;rarr; &lt;b&gt;Usage&lt;/b&gt; (Team은 &lt;b&gt;Organization settings&lt;/b&gt; &amp;rarr; &lt;b&gt;Usage&lt;/b&gt;) 진입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&quot;Enable Extra Usage&quot;&lt;/b&gt; 또는 &lt;b&gt;&quot;Extra usage&quot;&lt;/b&gt; 토글을 ON&lt;/li&gt;
&lt;li&gt;&lt;b&gt;월별 한도 금액(monthly cap)&lt;/b&gt; 설정 - 카드값 폭탄 방지용이다. 보상 크레딧만 사용할 계획이라면 $200 정도로 설정하면 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;저장(Save)&lt;/b&gt; 클릭&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;활성화 시 결제 차감 순서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 가장 중요하다. Extra Usage를 켜고 사용하면 결제는 다음 순서로 차감된다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;우선순위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;차감 대상&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비고&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1순위&lt;/td&gt;
&lt;td&gt;구독 플랜 기본 한도&lt;/td&gt;
&lt;td&gt;매달 리셋, 한도 도달 시 다음 단계로&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2순위&lt;/td&gt;
&lt;td&gt;Extra Usage Credit (보상 $200)&lt;/td&gt;
&lt;td&gt;90일 만료, 한도 초과분 자동 차감&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3순위&lt;/td&gt;
&lt;td&gt;등록된 신용카드 / 결제수단&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;b&gt;기본 한도 &amp;rarr; 보상 크레딧 &amp;rarr; 카드&lt;/b&gt; 순이다. 보상 크레딧을 다 사용하기 전에는 절대 카드에서 빠질 일이 없으므로 안심하고 켜도 된다. 다만 월별 한도(monthly cap)를 지나치게 크게 설정하면, 크레딧 소진 후에 사용 패턴 그대로 가다가 카드값이 청구될 수 있으므로 한도는 보수적으로 잡을 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Anthropic 크레딧 90일 만료 카운트는 언제부터 시작되는가&lt;/h2&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;b&gt;클레임한 날 기준 90일&lt;/b&gt;이다. 받은 날도, 사용 시작일도 아닌 &lt;b&gt;Claim 버튼을 누른 날 기준&lt;/b&gt;이다. claude.ai 사용량 페이지에 표시되는 &quot;Expires on YYYY-MM-DD&quot; 날짜가 본인의 데드라인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4월에 클레임한 사용자의 만료 시점&lt;/h3&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;b&gt;2026년 4월 3일 ~ 4월 17일(PT)&lt;/b&gt; 이었으므로 만료는 다음 범위에 들어온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;4월 3일 클레임&lt;/b&gt;: 2026년 7월 2일 만료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4월 10일 클레임&lt;/b&gt;: 2026년 7월 9일 만료&lt;/li&gt;
&lt;li&gt;&lt;b&gt;4월 17일 클레임 (마지막 날)&lt;/b&gt;: 2026년 7월 16일 만료&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만료 임박 전에 알림 메일이 오는지는 다소 불확실하다. 오는 사람도 있고 오지 않는 사람도 있다. 메일에 의존하지 말고 사용량 페이지에 직접 들어가 확인하는 것이 안전하다. 한 번에 다 사용하는 것이 부담스럽다면 만료 2주 전쯤 캘린더에 알람을 설정해 두는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;$200을 다 사용하려면 얼마나 사용해야 하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 콘솔 단가 기준이긴 하지만 토큰 부피 감을 잡기에는 좋다. 2026년 5월 기준 가격이다 (정확한 최신 가격은 &lt;a href=&quot;https://www.anthropic.com/pricing&quot;&gt;Claude Pricing&lt;/a&gt; 페이지를 확인하라).&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;입력 단가 (1M 토큰)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;출력 단가 (1M 토큰)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;$200으로 처리 가능 (입력+출력 50:50 가정)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;$1&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;약 6,600만 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;$3&lt;/td&gt;
&lt;td&gt;$15&lt;/td&gt;
&lt;td&gt;약 2,200만 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.7&lt;/td&gt;
&lt;td&gt;$5&lt;/td&gt;
&lt;td&gt;$25&lt;/td&gt;
&lt;td&gt;약 1,300만 토큰&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;22,000,000 토큰이면 책 70권 분량이다. 평소 가볍게 사용하는 사람은 90일 안에 절대 다 쓸 수 없다. 그래서 활용처를 미리 정해두는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Anthropic 크레딧 사용방법, 다 사용할 자신이 없다면 이렇게 활용한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;90일 안에 $200을 다 사용하려면 평소 하지 않던 큰 작업을 의도적으로 돌려야 한다. 추천 활용처는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;평소 미뤄둔 큰 작업을 한 번에 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;대용량 코드베이스 리뷰&lt;/b&gt;: 회사 레포 전체 코드 PR 리뷰, 보안 감사&lt;/li&gt;
&lt;li&gt;&lt;b&gt;책 한 권 분량 번역 / 요약&lt;/b&gt;: 영문 기술서적을 통째로 한국어로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지 일괄 분석&lt;/b&gt;: Vision으로 사진 수천 장을 분류 / 캡션 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;회의록 / 인터뷰 트랜스크립트 정리&lt;/b&gt;: 몇 시간 분량을 한 번에&lt;/li&gt;
&lt;li&gt;&lt;b&gt;개인 데이터 정리&lt;/b&gt;: 메모, 노션, 구글 드라이브 문서를 일괄 요약&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude Code에 적극적으로 투입&lt;/h3&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;a href=&quot;https://docs.claude.com/en/docs/claude-code&quot;&gt;Claude Code&lt;/a&gt; 사용자라면 토큰이 정말 빠르게 소진된다. 코드 한 번 분석시키면 입출력 합쳐서 100K 토큰은 우습게 넘어간다. Opus 4.7로 본인의 사이드프로젝트 리팩토링을 한 번 돌리면 며칠 만에 $50씩 빠진다.&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;이번 보상 크레딧은 &lt;b&gt;claude.ai 구독 인증으로 동작하는 Claude Code에 적용&lt;/b&gt;된다. API 키로 따로 동작시키면 console.anthropic.com 쪽에서 차감되기 때문에 이번 크레딧과는 무관하다. Claude Code를 처음 켤 때 &quot;Sign in with Anthropic&quot; 같은 구독 로그인 옵션을 선택해야 보상 크레딧이 차감된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude Cowork / 서드파티 통합도 포함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic 공식 안내에 따르면 이번 크레딧은 다음 범위에 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Claude (claude.ai 웹/모바일 앱 채팅)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Claude Code&lt;/b&gt; (구독 로그인 모드)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Claude Cowork&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;써드파티 통합&lt;/b&gt; (구독 인증 기반)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 채팅을 평소보다 길게 / 깊게 사용하는 것만으로도 $200을 소진할 수 있다. 한도 초과분이 자동으로 보상 크레딧에서 차감되는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 헷갈리는 점을 한 번에 정리&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구독료에서 자동 차감되는가?&lt;/h3&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;b&gt;No.&lt;/b&gt; Claude Pro / Max / Team 구독료는 별도다. 매달 정해진 금액이 카드에서 빠지고, 채팅 기본 한도는 거기서 커버된다. 보상 크레딧은 &lt;b&gt;기본 한도를 초과한 분량&lt;/b&gt;에만 적용되는 별도 잔액이다. 즉 평소처럼 한도 안에서 사용하면 보상 크레딧은 줄어들지 않고 그대로 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude.ai 채팅에 적용되는가?&lt;/h3&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;b&gt;Yes.&lt;/b&gt; 이번 크레딧은 처음부터 claude.ai 구독자에게 지급되는 것이라 채팅이 메인 적용처다. 메시지 한도 초과분이 자동으로 보상 크레딧에서 차감된다. 평소 한도를 끝까지 사용하는 사람이라면 실제로 가장 빠르게 소진하는 경로다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API Console에서도 사용할 수 있는가?&lt;/h3&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;b&gt;No.&lt;/b&gt; console.anthropic.com 측 사용량은 별도 결제 라인이며, 이번 보상 크레딧은 처음부터 Console / API 계정을 제외하고 발급된 것이다. API 호출 / Batch API는 콘솔 자체 잔액으로만 결제된다. 같은 Anthropic 계정이라도 양쪽 잔액은 완전히 분리되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;팀 워크스페이스에서는 어드민에게만 보이는가?&lt;/h3&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;b&gt;워크스페이스 권한 설정에 따라 다르다.&lt;/b&gt; Team 플랜은 Owner / Admin이 Organization settings &amp;rarr; Usage에서 잔액과 토글을 관리한다. 일반 멤버에게는 사용량만 보이는 것이 보통이다. 팀 보상 크레딧이 들어왔다면 어드민이 Extra Usage 토글을 켜야 멤버들이 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다 사용하지 못한 잔액은 환불 가능한가?&lt;/h3&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;b&gt;No.&lt;/b&gt; 만료되면 그대로 0으로 리셋된다. 환불, 이월, 다른 계정 양도 모두 불가능하다. 안내 문구에도 &quot;non-refundable, non-transferable&quot;로 명시되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;만료 임박 시 알림 메일이 오는가?&lt;/h3&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;b&gt;불확실하다.&lt;/b&gt; 받은 사람도 있고 받지 못한 사람도 있다. Anthropic이 공식적으로 보장하지 않는다. 직접 사용량 페이지를 캘린더에 일정으로 잡고 챙기는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 묻는 질문 (FAQ)&lt;/h2&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;b&gt;Q. Extra Usage를 켜면 무조건 카드값이 청구되는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. No. 차감 순서는 기본 한도 &amp;rarr; 보상 크레딧 &amp;rarr; 카드다. 크레딧을 다 사용하기 전에는 카드에서 빠지지 않는다. 월별 한도(monthly cap)도 함께 설정해 두면 더 안전하다.&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;&lt;b&gt;Q. claude.ai에 Extra Usage Credit 줄 자체가 보이지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 4월 17일 클레임 기한을 놓쳤거나, 다른 계정 / organization으로 로그인된 상태다. 모바일 앱에서는 표시되지 않으니 웹에서 확인할 것. 그래도 표시되지 않는다면 메일에 적힌 발송자 주소로 회신하거나 &lt;a href=&quot;https://support.anthropic.com/&quot;&gt;Support&lt;/a&gt;에 문의해야 한다.&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;&lt;b&gt;Q. 90일이 지나기 전에 일부만 사용하면 나머지는 어떻게 되는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 만료일이 되면 잔액이 그대로 0이 된다. 부분 사용해도 만료일은 연장되지 않는다.&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;&lt;b&gt;Q. Claude Code를 API 키 모드로 동작시키면 보상 크레딧에서 차감되는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. No. API 키 모드는 console.anthropic.com 측 결제라 이번 보상 크레딧과는 무관하다. 구독 로그인 모드로 동작시켜야 차감된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약 3줄&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 Anthropic 크레딧 사용방법을 정리했다. 핵심만 3줄로 압축한다.&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;claude.ai 사용량 페이지에 있다&lt;/b&gt; - console.anthropic.com이 아니다. Settings &amp;rarr; Usage(또는 Organization settings &amp;rarr; Usage)에서 잔액을 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Extra Usage 토글을 켜라&lt;/b&gt; - 켜지 않으면 크레딧이 있어도 사용할 수 없다. 같은 페이지에서 토글을 ON으로 변경하고 monthly cap을 적정 수준으로 설정할 것.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;90일 카운트는 직접 챙겨라&lt;/b&gt; - 알림 메일이 오지 않을 수도 있다. Expires on 날짜를 캘린더에 등록하고 큰 작업으로 다 소진할 것.&lt;/li&gt;
&lt;/ol&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;a href=&quot;https://claude.ai/&quot;&gt;claude.ai&lt;/a&gt;에 들어가 Settings &amp;rarr; Usage에서 잔액부터 확인하고, Extra Usage가 OFF라면 켜라. 90일은 생각보다 빠르게 흘러간다. 7월 만료 직전에 부랴부랴 사용하려 하지 말고 미리 큰 작업을 하나씩 돌려서 $200을 모두 소진하는 것이 이득이다.&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;크레딧 환불이 되지 않는다는 점은 진짜로 No Refund다. 그냥 사용하지 않으면 Anthropic이 다시 회수해 가는 셈이다. 이를 모르고 90일을 흘려보내는 사람이 너무 많아서 글로 정리했다. 도움이 되었기를 바란다.&lt;/p&gt;</description>
      <category>개발 지식in</category>
      <category>Anthropic Extra Usage</category>
      <category>Anthropic200 크레딧</category>
      <category>Claude Pro Max Team 크레딧</category>
      <category>Claude Usage 크레딧 확인</category>
      <category>Claude 보상 크레딧 90일</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/403</guid>
      <comments>https://yscho03.tistory.com/403#entry403comment</comments>
      <pubDate>Fri, 1 May 2026 16:59:19 +0900</pubDate>
    </item>
    <item>
      <title>TypeScript 끝판왕 Matt Pocock, 본인 .claude/skills 폴더를 통째로 공개하다</title>
      <link>https://yscho03.tistory.com/402</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Total TypeScript를 만든 Matt Pocock이 본인 .claude/skills 폴더를 깃허브에 올렸다. 레포 이름은 mattpocock/skills이고, 설명은 &quot;Skills for Real Engineers. Straight from my .claude directory.&quot;로 박아두었다. &lt;b&gt;본인이 매일 쓰던 것을 그대로 던졌다는 뜻이다.&lt;/b&gt;&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;영어권 TypeScript 교육 시장에서 워낙 인지도가 높은 사람이라, 본인 AI 코딩 워크플로우를 통째로 공개하는 일은 흔치 않은 케이스다. 2026년 5월 1일 기준 별 4만 8천 개, 포크 3,900개 가까이 찍혔다. 2026년 2월 초에 공개됐으니 약 석 달 만에 이 정도면 상당한 화제다.&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;각설하고, Matt Pocock skills 레포 안을 카테고리별로 뜯어보고, 한국 개발자 입장에서 그대로 쓸 수 있는 스킬과 손봐야 하는 스킬을 가렸다. 5분 안에 따라 깔아보는 셋업도 마지막에 붙였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RUM0u/dJMcaiDacE1/kGkFuXryZVQ2xmE4M1Hh11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RUM0u/dJMcaiDacE1/kGkFuXryZVQ2xmE4M1Hh11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RUM0u/dJMcaiDacE1/kGkFuXryZVQ2xmE4M1Hh11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRUM0u%2FdJMcaiDacE1%2FkGkFuXryZVQ2xmE4M1Hh11%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;462&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;&lt;a href=&quot;https://github.com/mattpocock/skills&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/mattpocock/skills&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777682645171&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;object&quot; data-og-title=&quot;GitHub - mattpocock/skills: Skills for Real Engineers. Straight from my .claude directory.&quot; data-og-description=&quot;Skills for Real Engineers. Straight from my .claude directory. - mattpocock/skills&quot; data-og-host=&quot;github.com&quot; data-og-source-url=&quot;https://github.com/mattpocock/skills&quot; data-og-url=&quot;https://github.com/mattpocock/skills&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/HNEJI/dJMb89yg42r/m48Oh17iuky6kA0QQ4Y7xK/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_129_1053_219,https://scrap.kakaocdn.net/dn/mPp0H/dJMb9jOqoMd/oxHk6Kp20U67NqyqGMmlf1/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_129_1053_219,https://scrap.kakaocdn.net/dn/7dywL/dJMb9c9BmUa/SbjZEcY0fEoSvg61WHGWhk/img.png?width=738&amp;amp;height=388&amp;amp;face=491_81_572_169&quot;&gt;&lt;a href=&quot;https://github.com/mattpocock/skills&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://github.com/mattpocock/skills&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/HNEJI/dJMb89yg42r/m48Oh17iuky6kA0QQ4Y7xK/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_129_1053_219,https://scrap.kakaocdn.net/dn/mPp0H/dJMb9jOqoMd/oxHk6Kp20U67NqyqGMmlf1/img.png?width=1200&amp;amp;height=600&amp;amp;face=971_129_1053_219,https://scrap.kakaocdn.net/dn/7dywL/dJMb9c9BmUa/SbjZEcY0fEoSvg61WHGWhk/img.png?width=738&amp;amp;height=388&amp;amp;face=491_81_572_169');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;GitHub - mattpocock/skills: Skills for Real Engineers. Straight from my .claude directory.&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Skills for Real Engineers. Straight from my .claude directory. - mattpocock/skills&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Matt Pocock은 누구인가, Total TypeScript의 그 사람&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름만 들어본 사람도 있을 테니 짧게 정리한다. Matt Pocock은 &lt;b&gt;Total TypeScript라는 유료 워크샵 시리즈를 만든 사람&lt;/b&gt;이다. TypeScript Pro Essentials, Type Transformations, Advanced TypeScript Patterns 같은 워크샵이 모두 이 사람 작품이고, 트위터에서도 TypeScript 팁을 짧게 던지는 것으로 유명하다.&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;이 사람 워크플로우를 신뢰할 만한 이유는 두 가지로 압축된다. 본업이 콘텐츠 제작이라 코드 짜고 강의 만들고 글 쓰는 사이클을 매일 돌리고, 그 와중에 AI 도구를 진심으로 깎아 쓴다는 것이 첫째다. 둘째는 도메인 자체의 궁합이다. TypeScript는 정적 타입 체커가 잘 잡힌 언어라서, 타입 에러가 곧 빠른 피드백 루프가 되고, TDD나 디버깅 자동화와 잘 맞는다. 이 사람 스킬들도 그 강점 위에서 설계됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;.claude/skills 폴더가 무엇이길래 이렇게 난리인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code skills 시스템 자체를 모르는 사람들을 위해 30초 요약한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude Code skills 시스템 30초 요약&lt;/h3&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;b&gt;스킬은 Claude Code가 특정 상황을 만나면 자동으로 호출하는 재사용 가능한 프롬프트 묶음이다.&lt;/b&gt; 구조는 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777621945031&quot; class=&quot;crystal&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;~/.claude/skills/
└── 스킬이름/
    ├── SKILL.md          # 메인 진입점 (frontmatter + 본문)
    └── 보조파일.md        # 필요하면 더 추가
&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;SKILL.md의 YAML 프론트매터에 name과 description이 들어가는데, 모델이 사용자 메시지를 읽고 이 디스크립션과 매칭되면 알아서 해당 스킬을 로드해 따라간다. 슬래시 커맨드처럼 명시적으로 부를 수도 있고, 디스크립션 매칭으로 알아서 발동될 수도 있다.&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;한 번 만들어두면 이 컴퓨터, 저 프로젝트에서 모두 쓸 수 있다는 점이 좋다. dotfiles처럼 자기 워크플로우가 자산이 되는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자기 스킬 공개 트렌드가 생긴 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 후반부터 GitHub에 .claude/skills를 공개하는 사람이 늘기 시작했다. &lt;b&gt;dotfiles 공개 트렌드의 AI 버전인 셈이다.&lt;/b&gt; 본인 워크플로우를 박제해두면 다른 사람이 베껴 쓰기 좋고, 자기 머릿속도 정리되며, 이 사람이 일하는 방식이 그대로 드러나니 채용/세일즈 측면의 자기 PR도 된다. Matt Pocock 정도의 이름값 있는 사람이 던지면 파급력은 상상 이상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Matt Pocock skills 레포 안에 정확히 무엇이 있는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777621945032&quot; class=&quot;reasonml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;mattpocock/skills/
├── .claude-plugin/
├── .out-of-scope/
├── docs/adr/
├── scripts/
├── skills/
│   ├── deprecated/
│   ├── engineering/
│   ├── misc/
│   ├── personal/
│   └── productivity/
├── CLAUDE.md
├── CONTEXT.md
├── LICENSE       # MIT
└── README.md
&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;skills/ 안에 카테고리는 5개다. deprecated, personal을 빼고 보면 핵심은 &lt;b&gt;engineering, productivity, misc 세 폴더&lt;/b&gt;다. 각각에 들어 있는 스킬을 풀어본다.&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;엔지니어링 폴더가 9개로 가장 두꺼우니 이것이 본진이다. 본진은 표로 정리한다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;스킬&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;한 줄 설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`tdd`&lt;/td&gt;
&lt;td&gt;Red-Green-Refactor 루프 자동화. 사용자가 &quot;TDD로&quot;, &quot;테스트 먼저&quot; 같은 말을 꺼내면 자동 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`diagnose`&lt;/td&gt;
&lt;td&gt;어려운 버그/성능 회귀를 6단계(피드백 루프 구축&amp;rarr;재현&amp;rarr;가설&amp;rarr;계측&amp;rarr;수정+회귀테스트&amp;rarr;정리/사후분석)로 풀어가는 루프&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`triage`&lt;/td&gt;
&lt;td&gt;들어온 이슈/버그 분류&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`to-issues`&lt;/td&gt;
&lt;td&gt;대화 내용을 깃허브 이슈 형태로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`to-prd`&lt;/td&gt;
&lt;td&gt;대화 내용을 PRD(제품 기획 문서)로 변환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`grill-with-docs`&lt;/td&gt;
&lt;td&gt;문서 업데이트와 함께 가는 플래닝 세션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`zoom-out`&lt;/td&gt;
&lt;td&gt;디테일에 빠져 있을 때 한 발 빼서 전체 그림 보기&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`improve-codebase-architecture`&lt;/td&gt;
&lt;td&gt;코드베이스 아키텍처 개선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`setup-matt-pocock-skills`&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흥미로운 것은 이 폴더에 &lt;b&gt;TypeScript 전용 스킬이 따로 없다는 점&lt;/b&gt;이다. Matt Pocock = TypeScript 사람이라고 생각하고 들어가면 약간 김이 빠진다. 대신 TDD, 디버깅, PRD 같은 일반 엔지니어링 워크플로우 스킬에 집중되어 있는데, AI 시대에도 기본기가 답이라는 신호로 읽힌다.&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;생산성 폴더는 3개다. grill-me는 사용자 계획이나 디자인을 끝까지 추궁(grill)해서 합의에 도달할 때까지 인터뷰하는 스킬이고, caveman은 토큰 75%를 줄이는 초압축 커뮤니케이션 모드다. 관사고 군더더기고 모두 빼고 핵심만 말하라는 것이다. 마지막으로 write-a-skill이 있는데, 새 스킬을 만들 때 쓰는 스킬, 즉 &lt;b&gt;스킬을 만드는 스킬&lt;/b&gt;이다.&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;caveman은 디스크립션이 진짜 웃긴데, &quot;talk like caveman&quot;, &quot;less tokens&quot; 같은 말이 트리거다. 글자 그대로 원시인처럼 말하라는 것이다. 토큰 비용에 신경 쓰는 사람한테는 꽤 실용적이다.&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;grill-me는 개인적으로 가장 인상 깊었던 스킬이다. 사용자가 무언가 만들어달라고 하면 바로 코드를 짜는 것이 아니라, 의사결정 트리의 모든 가지를 하나씩 질문해서 합의를 본 뒤에 진행한다. AI가 미리 추측하고 코드를 박아놨다가 갈아엎는 비용을 줄이려는 의도다.&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;미스크 폴더에는 4개가 들어 있다. git-guardrails-claude-code는 git의 위험한 명령(force push, hard reset 같은 것)에 가드레일을 거는 스킬, migrate-to-shoehorn은 shoehorn이라는 라이브러리로 테스트 마이그레이션을 도와주는 것이다. scaffold-exercises는 강의/튜토리얼용 연습문제 스캐폴딩이라서 Matt Pocock이 본인 강의 만들 때 쓰던 것을 그대로 공개한 모양인데, 일반 개발자에게는 큰 의미가 없을 수 있다. setup-pre-commit은 pre-commit 훅 셋업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가장 흥미로웠던 스킬 3개를 꼽다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. tdd: 진짜로 동작하는 TDD 루프&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크립션은 &quot;Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD...&quot; 이런 식이다.&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;핵심은 본문에 박힌 안티 패턴 경고다. &lt;b&gt;&quot;절대 테스트를 다 쓰고 나서 구현 다 하지 말라(horizontal slicing 금지)&quot;&lt;/b&gt;라고 못 박아두었다. 즉 &quot;테스트 10개 &amp;rarr; 구현 10개&quot; 식이 아니라, 한 슬라이스씩 vertical로 가야 한다는 것이다. 이걸 강제하는 스킬은 흔치 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. caveman: 토큰 비용 75% 컷&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 비용을 줄이려고 만든 게 아니라 &lt;b&gt;사고의 군더더기를 줄이려고 만든 느낌&lt;/b&gt;이 강하다. 며칠 caveman 모드로 작업해보니 모델 답변이 진짜 짧아지는데, 짧은 답이 더 정확하게 느껴지는 것이 신기하다. 자기 합리화하는 텍스트가 줄어드니 결정에 필요한 정보만 남는 효과로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. grill-me: AI에게 인터뷰 당하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 스킬을 켜고 쓰면 진짜 귀찮다. 한 번에 하나씩 질문을 던지고, 추천 답변까지 같이 줘서 결정하라고 강요한다. 며칠 전 단순한 캐싱 레이어 하나 붙이려다 grill-me로 12개 질문을 받았는데, 그 중 &quot;캐시 무효화 기준이 mtime이냐 컨텐츠 해시냐&quot;라는 질문에서 결정이 뒤집혔다. 처음에는 그냥 mtime으로 박을 생각이었는데 인터뷰하면서 mtime이 git checkout으로 바뀌는 케이스를 놓쳤다는 것을 깨달았다. &lt;b&gt;잘못된 방향으로 5시간 코드 짜는 비용보다 12분 인터뷰가 훨씬 쌌다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;솔직히 평범했던 스킬들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 얘기만 하면 균형이 안 맞으니 평범한 영역도 짚어둔다. 가장 큰 것은 TypeScript 전용 스킬이 없다는 점이다. Total TypeScript를 만든 사람 레포라는 기대를 갖고 들어가면 약간 김이 빠진다. 자기 도메인 전문성은 따로 박지 않았고, 일반 엔지니어링 스킬이 더 중요하다고 본 모양이다.&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;misc/migrate-to-shoehorn이나 scaffold-exercises 같은 것은 본인 컨텍스트에 너무 의존적이라 다른 사람이 그대로 쓸 일이 없다. personal/, deprecated/ 폴더도 비슷한 결이다. 또 루트에 있는 CLAUDE.md, CONTEXT.md 같은 문서는 본인 프로젝트 컨벤션이 박혀 있어서 그대로 베끼면 안 되는 참고용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한국 개발자 입장에서 그대로 쓸 수 있는 것 vs 수정해야 하는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스킬 15개를 OK / 손봐야 함 / 안 가져와도 됨으로 분류해봤다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;스킬&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;그대로 OK?&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비고&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`tdd`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;언어 무관, 한국어 환경에서도 그대로 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`diagnose`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;디버깅 루프는 만국 공통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`grill-me`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;영어로 질문 와도 한국어로 답해도 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`caveman`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;한글로 써도 토큰 절약됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`triage`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;일반 워크플로우라 그대로 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`zoom-out`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;그대로 사용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`git-guardrails-claude-code`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;git 위험 명령은 만국 공통&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`setup-pre-commit`&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;pre-commit 훅도 그대로 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`to-issues`&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;깃허브/지라 등 본인 트래커에 맞게 살짝 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`to-prd`&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;PRD 템플릿이 한국 회사 양식과 다를 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`improve-codebase-architecture`&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;한국어 코멘트/네이밍 컨벤션 반영하면 더 좋음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`grill-with-docs`&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;사내 문서 시스템(노션, 컨플루언스)에 맞게 손봐야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`setup-matt-pocock-skills`&lt;/td&gt;
&lt;td&gt;△&lt;/td&gt;
&lt;td&gt;초기 셋업용. 한국 환경(노션, 슬랙 등)으로 답하면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`migrate-to-shoehorn`&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;Matt 본인 라이브러리 마이그레이션용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`scaffold-exercises`&lt;/td&gt;
&lt;td&gt;X&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표를 합쳐 보면 &lt;b&gt;8개는 그대로 베껴 써도 되고, 5개는 살짝 손보면 되며, 2개는 굳이 안 가져와도 된다.&lt;/b&gt; 시작점으로는 충분히 든든한 기본 셋이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Matt Pocock skills 따라하기 5분 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 5분 안에 끝난다.&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;먼저 ~/.claude/ 폴더가 이미 있는지 확인한다. 없으면 Claude Code부터 깔아야 한다. 그다음 레포 README에 박아둔 공식 셋업 명령어 한 줄을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777621945035&quot; class=&quot;dockerfile&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npx skills@latest add mattpocock/skills
&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;이거 한 방이면 ~/.claude/skills/mattpocock-skills/ 같은 경로에 깔린다(정확한 경로는 명령어 출력에서 확인). npx skills는 별도 CLI인데, 여러 사람의 스킬 레포를 합쳐 쓸 수 있게 만든 도구다.&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;설치가 끝나면 Claude Code를 켜고 셋업 스킬을 돌리면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777621945035&quot; class=&quot;arduino&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;/setup-matt-pocock-skills
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트는 코딩 작업 하나 시켜보고 /grill-me나 /tdd 같은 슬래시 커맨드를 직접 쳐보면 된다. 자동 매칭이 동작하는지 보려면 &quot;이거 TDD로 만들어줘&quot; 같이 디스크립션과 겹치는 단어를 던지면 된다.&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;라이선스는 MIT라서 그대로 베껴서 사내 또는 본인 환경에서 써도 OK다. 어디 공개할 거면 출처/원작자 표기는 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;628&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/br1sk7/dJMcaiDacE0/yAGom3bAw30yuDsHRMjvek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/br1sk7/dJMcaiDacE0/yAGom3bAw30yuDsHRMjvek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/br1sk7/dJMcaiDacE0/yAGom3bAw30yuDsHRMjvek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbr1sk7%2FdJMcaiDacE0%2FyAGom3bAw30yuDsHRMjvek%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;628&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;628&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.totaltypescript.com/tutorials&quot;&gt;totaltypescript.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 이거 진짜 쓸 만한가, 솔직 평가&lt;/h2&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;b&gt;빈말이 없는 실전 검증 프롬프트&lt;/b&gt;라는 점, MIT 라이선스라 그대로 가져다 써도 되는 점, 엔지니어링 스킬 9개가 워낙 잘 짜여 있어서 본인 첫 스킬 셋업의 베이스로 쓰기 좋다는 점이 강점이다. caveman이나 grill-me 같은 창의적인 스킬은 영감 자극용으로도 쓸 만하다.&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;다만 아쉬운 점도 있다. 전부 영어 베이스라 디스크립션 자동 매칭을 쓸 거면 본인이 영어로 프롬프트 던질 때 가장 정확하게 발동한다. TypeScript 전용 스킬이 없다 보니 Matt Pocock 도메인 전문성을 직접 베끼는 것은 아니고, personal/이나 deprecated/처럼 본인 컨텍스트 의존 폴더가 섞여 있어서 그대로 다 깔면 군더더기가 생긴다. migrate-to-shoehorn 같은 본인 라이브러리 의존 스킬은 클론할 때 빼는 것이 정신 건강에 좋다.&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;추천 대상은 이미 Claude Code를 어느 정도 써본 사람, 본인 스킬 셋업을 시작하는 사람, TDD나 디버깅 워크플로우를 강제하고 싶은 사람이다. 비추천 대상은 Claude Code를 처음 깐 사람(먼저 기본부터 익히는 것이 낫다), 그리고 한국어로만 작업하는 사람(영어 디스크립션 매칭 한계가 있어서 본인 워크플로우를 한국어로 다시 짜는 것이 나을 수도 있다)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Matt Pocock skills가 던지는 진짜 교훈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Matt Pocock skills를 뜯어보면서 가장 강하게 남는 것은 &lt;b&gt;결국 기본기 위에 짓는 워크플로우라는 점&lt;/b&gt;이다. TypeScript 끝판왕인 사람이 정작 자기 스킬 폴더에 박아둔 것은 TDD, 디버깅, 코드 리뷰 같은 일반 엔지니어링 도구였다. AI가 발전한다고 기본기를 패스하면 안 된다는 신호처럼 읽혔다.&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;워크플로우 자체가 공유 가능한 자산이 된 점도 인상적이다. dotfiles 공개하던 시대가 .claude 폴더 공개하는 시대로 넘어왔다는 것이다. 본인 Claude Code skills 셋업을 깃허브에 올려두면 자기 PR이자 커뮤니티 기여가 된다.&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;그래서 권장 순서는 이것이다. Matt Pocock skills 레포를 그대로 베껴 깔고, 검증된 9개 엔지니어링 스킬을 베이스로 쓰면서, 자기 도메인(한국어 글쓰기, 사내 컨벤션 등)에 맞는 스킬을 위에 얹어가는 식이다. 처음부터 스킬을 짜려고 하면 막막한데, 누가 만들어둔 베이스 위에서 추가하는 것은 훨씬 쉽다. 다음 단계는 본인 ~/.claude/skills/ 폴더를 공개하는 것이다. &lt;b&gt;본인 워크플로우를 박제해두면 6개월 뒤 자기 자신이 가장 고마워하게 된다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>Claude Code skills</category>
      <category>claude skills 폴더</category>
      <category>Claude 스킬</category>
      <category>Matt Pocock TypeScript</category>
      <category>클로드 코드 skills</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/402</guid>
      <comments>https://yscho03.tistory.com/402#entry402comment</comments>
      <pubDate>Fri, 1 May 2026 16:52:43 +0900</pubDate>
    </item>
    <item>
      <title>마틴 파울러가 짚은 LLM 부채 3가지, 정말 위험하다</title>
      <link>https://yscho03.tistory.com/401</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 에게 코드를 맡긴 지 6개월쯤 지나면, 어느 날 갑자기 그 코드가 폭탄으로 변하는 경험을 하게 된다. 처음에는 &quot;빠르다&quot; 싶다가도 시간이 지나면 &quot;이 코드는 누가 짠 것인가, 왜 이렇게 짰는가&quot; 하면서 머리를 쥐어뜯게 된다. 이를 그저 LLM 기술 부채라는 한 단어로 뭉뚱그려 부르고 있었으나, 마틴 파울러가 &lt;a href=&quot;https://martinfowler.com/fragments/2026-04-02.html&quot;&gt;2026-04-02 fragments 섹션에 올린 글&lt;/a&gt;에서 이것이 한 종류가 아니라고 짚었다. 빅토리아대 마거릿-앤 스토리(Margaret-Anne Storey) 교수가 &lt;a href=&quot;https://arxiv.org/abs/2603.22106&quot;&gt;arXiv 논문&lt;/a&gt;에서 제안한 분류이며, 파울러가 fragments 에서 공유하면서 알려졌다. &lt;b&gt;기술 부채, 인지 부채, 의도 부채.&lt;/b&gt; 세 갈래로 쪼개서 봐야 한다는 것이다.&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;이번 글에서는 스토리 교수가 나눈 세 부채가 각각 무엇인지, 어디서 나온 개념인지, 어떻게 서로 강화하면서 팀을 망가뜨리는지, 그리고 어떻게 막을지까지 살펴본다. LLM 도입 이후 무언가 이상한데 정확히 무엇이 이상한지 모르겠다면 참고할 만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;부채 비유는 어디서 왔는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부채(debt) 비유 자체는 LLM 시대 이전부터 있었다. 1992년 OOPSLA 컨퍼런스에서 워드 커닝햄(Ward Cunningham)이 처음 제안한 것이다. 당시 커닝햄이 한 말의 핵심은 &lt;b&gt;&quot;지금 빨리 가려고 짠 코드는 미래에서 끌어다 쓴 빚과 같다&quot;&lt;/b&gt; 였다. 빚 자체는 나쁜 것이 아니다. 집을 살 때 대출을 끼는 것과 비슷하다. 다만, 이자가 붙는다. 이자를 갚을 줄 모르면 결국 망한다.&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;마틴 파울러가 2003년 &lt;a href=&quot;https://martinfowler.com/bliki/TechnicalDebt.html&quot;&gt;Technical Debt bliki 글&lt;/a&gt;에서 이 비유를 다듬어 대중화시켰고, 그 뒤로 오랫동안 소프트웨어 업계의 표준 어휘가 되었다. 그런데 이 비유가 LLM 시대에 다시 소환된 이유는 단순하다. &lt;b&gt;부채 발생 속도가 미친 듯이 빨라졌기 때문이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 LLM 시대에 이 비유가 다시 소환되었는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전에는 사람이 직접 타이핑했으니 부채가 쌓이는 속도에도 한계가 있었다. 지금은 클로드 코드(Claude Code), 커서(Cursor), 깃허브 코파일럿(GitHub Copilot) 같은 도구로 하루에 5천 줄을 찍어내는 일이 가능하다. 문제는 그 5천 줄이 이해되지 않은 채로 쌓인다는 점이다. 부채 액수는 10배인데 갚을 능력은 그대로거나 오히려 줄어드는 상황이다. 스토리 교수가 한 갈래로 보던 부채를 세 갈래로 쪼개고, 파울러가 그것을 짚은 이유가 여기에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;첫 번째 &amp;mdash; 기술 부채(Technical Debt)는 좀 다르게 쌓인다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 이 만들어내는 기술 부채는 사람이 만드는 기술 부채와 결이 다르다. 사람은 의식적으로 &quot;지금 시간 없으니 임시로 이렇게 짜고 나중에 고치자&quot; 하면서 부채를 진다. 즉, 부채가 어디 있는지 본인이 안다. LLM 은 그렇지 않다. 일단 돌아가는 코드를 뽑아내는데, 그 코드의 어느 부분이 엉성한지 LLM 도 사람도 잘 알지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동작은 하는데 구조가 엉성한 코드의 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔한 패턴이 있다. LLM 에게 &quot;이것을 만들어달라&quot; 하면 만들어준다. 테스트도 통과한다. 그런데 코드를 열어보면 같은 로직이 여러 군데에 복붙되어 있고, 함수 하나가 200줄을 넘으며, 변수명이 일관성 없이 userId, user_id, uid 가 한 파일에 다 들어가 있다. 에러 핸들링도 들쭉날쭉하고, 추상화가 어설프게 되어 있어서 변경 한 번을 하려면 다섯 군데를 손대야 한다.&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;여기서 무서운 점이 하나 있다. 사람이 의도적으로 짠 부채는 TODO 코멘트라도 남겨두는데 LLM 은 그런 것을 남기지 않는다. &lt;b&gt;LLM 은 &quot;이건 임시야&quot;라고 생각하지 않는다.&lt;/b&gt; 그저 동작하는 코드를 뽑을 뿐이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;바이브 코딩으로 만든 MVP 가 6개월 뒤에 어떻게 되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이브 코딩(vibe coding)은 안드레이 카파시(Andrej Karpathy)가 2025년 초에 트윗으로 처음 사용한 용어다. 코드를 직접 읽지 않고 LLM 에게 &quot;느낌적인 느낌&quot;으로 시켜서 만드는 방식이다. 처음에는 &quot;이렇게도 코드가 만들어지는구나&quot; 하면서 신기해했으나, 6개월쯤 지나니 후폭풍이 시작되었다.&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;&lt;a href=&quot;https://www.gitclear.com/coding_on_copilot_data_shows_ais_downward_pressure_on_code_quality&quot;&gt;GitClear 가 발표한 2024 코드 품질 리포트&lt;/a&gt;에 따르면, AI 코딩 도구 도입 이후 코드 churn(2주 안에 다시 손대는 비율)이 2020년 3.1% 에서 2024년에는 두 배 가까이로 늘었다. 한 번 짜고 끝나는 것이 아니라 짜고, 다시 짜고, 또 다시 짜는 패턴이 생긴 것이다. 처음에는 빨라 보이지만 6개월 뒤에는 같은 자리를 계속 맴돌고 있다. 이것이 기술 부채의 이자다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 번째 &amp;mdash; 인지 부채(Cognitive Debt), 이것이 가장 위험하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인지 부채는 2025년에 MIT 미디어랩(MIT Media Lab)에서 발표한 &lt;a href=&quot;https://www.media.mit.edu/projects/your-brain-on-chatgpt/overview/&quot;&gt;&quot;Your Brain on ChatGPT&quot; 연구&lt;/a&gt;에서 나온 용어다. 참가자 54명에게 EEG(뇌파) 측정 장비를 붙이고 글쓰기 작업을 시켰는데, 결과가 다소 충격적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MIT 미디어랩 2025년 연구가 밝힌 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연구진은 참가자를 세 그룹으로 나눴다. 첫 그룹은 ChatGPT 에게 모두 시키고 결과만 받는 조건, 두 번째는 구글 검색은 가능하지만 LLM 은 사용할 수 없는 조건, 세 번째는 아무 도움 없이 본인 머리로만 쓰는 노툴 조건이었다.&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;EEG 로 측정한 뇌 연결성(brain connectivity) 결과는 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;그룹&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;뇌 활성도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;본인 글 기억 정도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;결과물 품질&lt;/b&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;거의 다 기억함&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;대부분 기억함&lt;/td&gt;
&lt;td&gt;무난함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT 그룹&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;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT 그룹은 본인이 방금 제출한 글의 핵심 문장조차 인용하지 못 했다. 이것이 인지 부채의 정체다. &lt;b&gt;LLM 에게 사고를 떠넘기다 보니, 그 결과물이 본인 머릿속에 들어오지 않는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;내가 쓴 코드인데 내가 모른다&quot; 현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 코딩에 그대로 적용하면 어떻게 될지 짐작이 간다. PR 을 올릴 때 본인이 짠 코드인데 리뷰어가 &quot;이 부분은 왜 이렇게 했는가&quot; 물으면 &quot;어&amp;hellip; LLM 이 그렇게 줬다&quot;라는 답이 나오기 시작한다. 처음에는 부끄러워하다가 점점 당연시된다. 무서운 지점이 여기다.&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;ChatGPT 코딩 문제점 가운데 가장 치명적인 것은 &lt;b&gt;결과물은 있는데 학습이 없다&lt;/b&gt;는 점이다. 옛날에는 어려운 문제 하나를 풀면 그 과정에서 머리가 단단해졌다. 지금은 어려운 문제 하나를 풀어도 머리는 그대로다. LLM 이 풀었기 때문이다. 주변에서 &quot;5년차인데 신입 때보다 디버깅이 안 된다&quot; 같은 이야기가 종종 들리는데, 같은 메커니즘이라고 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주니어 개발자에게 가장 치명적인 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인지 부채가 무서운 이유는 주니어에게 압도적으로 치명적이라는 점이다. 시니어는 어차피 머릿속에 패턴이 다 있으니 LLM 결과물을 보고 &quot;이건 이상하다&quot; 하고 잡아낸다. 주니어는 그것이 되지 않는다. 비교 대상이 없으니 LLM 이 주는 코드를 그대로 받아들이고, 본인이 직접 고민해야 할 단계를 건너뛴다.&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;결과적으로 5년차가 되었는데 3년차 수준의 판단력을 가진 사람이 만들어진다. 한국처럼 시니어 인력 풀이 얇은 SI/스타트업 환경에서 위험한 점은, 다음 시니어가 자라지 않으면 팀 전체가 LLM 의존도에서 빠져나오지 못한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 번째 &amp;mdash; 의도 부채(Intent Debt), 스토리 교수가 새로 제안한 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 스토리 교수 분류의 핵심이고, 파울러가 fragments 글에서 가장 길게 인용한 부분이다. 의도 부채(Intent Debt)는 코드는 남는데 그 코드를 왜 그렇게 짰는지가 같이 남지 않는 부채다. 기술 부채는 코드 품질 문제이고, 인지 부채는 사람의 능력 문제인데, 의도 부채는 좀 더 미묘하다. &lt;b&gt;결과물 자체는 멀쩡한데 그 결과물의 컨텍스트가 사라지는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드는 남는데 &quot;왜&quot;가 사라지는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람이 직접 코드를 짜면 머릿속에 자연스럽게 &quot;왜&quot;가 같이 남는다. &quot;여기서 왜 try-catch 를 두 번 감쌌느냐면, 외부 API 가 가끔 부분 실패를 던지는데 그것을 따로 처리해야 했기 때문이다&quot; 같은 식이다. 그 자리에서 즉시 코멘트로 남기 어려워도 사람 기억에는 남고, 다음 번 그 코드를 만질 때 자연스럽게 끌려 나온다.&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;LLM 이 짜면 이것이 남지 않는다. LLM 에게 &quot;이러이러한 상황이라 이렇게 처리해달라&quot;라고 시킨 사람조차도, 며칠 지나면 그 의도를 잊는다. 코드 유지보수 AI 시대의 가장 큰 함정이 여기에 있다. &lt;b&gt;결과물은 영구 보존되는데 의도는 휘발된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의도 부채가 기술 부채보다 더 무서운 이유&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도 부채는 다르다. 코드를 봐도 왜 그렇게 짰는지 모르니 함부로 고치지 못한다. &quot;이거 지워도 되는가&quot; 했는데 알고 보니 6개월 전에 특정 엣지 케이스 때문에 일부러 그렇게 한 것이었고, 지웠더니 프로덕션이 터지는 식이다. &lt;b&gt;의도 부채는 코드를 못 만지게 만드는 부채다.&lt;/b&gt; 일종의 결빙이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시니어 개발자에게 가장 치명적인 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인지 부채가 주니어를 망가뜨린다면, 의도 부채는 시니어를 망가뜨린다. 시니어는 시스템 전체를 머릿속에 모델링해서 의사결정을 내리는 사람들이다. 그런데 LLM 이 짠 코드가 코드베이스의 30~40% 를 차지하기 시작하면, 시니어가 본인이 책임지는 시스템의 의도조차 다 알지 못하는 상황이 온다.&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;이것은 문서화로도 잘 막히지 않는다. LLM 이 코드를 짤 때마다 ADR(Architecture Decision Record)까지 같이 작성하는 사람은 거의 없기 때문이다. 그저 &quot;잘 되었다&quot; 하고 머지하고 끝난다. 의도는 그 순간 휘발된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LLM 기술 부채 세 갈래는 서로 강화하는 무한루프다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM 에게 코드를 시키면 일단 기술 부채가 쌓인다. 본인이 직접 짜지 않았으니 의도가 남지 않고, 거기서 의도 부채가 더해진다. 시간이 지나서 그 코드를 봐도 이해가 되지 않으니 또 LLM 에게 묻는다. 사고를 자꾸 LLM 에게 떠넘기게 되고, 결국 인지 부채까지 같이 쌓인다. 인지 능력이 떨어지면 LLM 없이는 코드를 손도 대지 못한다. 그러면 다시 첫 단계로 돌아간다.&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;이 루프의 무서운 점은 본인이 그 안에 있는 줄도 모른다는 것이다. AI 의존 개발자가 되어가는 과정은 점진적이라서, 어느 순간 돌아보면 이미 빠져나오기 어려운 상태에 와 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;팀 단위로 번지는 메커니즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 단위면 그래도 본인이 알아서 조절할 수 있는데, 팀 단위로 이 루프가 돌면 위험하다. 코드 리뷰 단계에서 리뷰어조차 LLM 에게 &quot;이 PR 을 리뷰해달라&quot; 한다고 생각해보면 된다. &lt;b&gt;부채를 만드는 단계도 LLM, 검증하는 단계도 LLM. 사람은 어디에도 없다.&lt;/b&gt; 그런데 책임은 사람이 진다.&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;한국 SI 환경에서 빠르게 폭발하는 이유는 인력 회전이 빠르기 때문이다. 의도를 가진 사람이 6개월 뒤에 퇴사하면, 그 사람이 LLM 과 둘이서 만든 코드는 영원히 미궁이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 이것을 어떻게 막을 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KZxmW/dJMcaiJTKfG/U1ubr0YfETDemuZ8maEk11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KZxmW/dJMcaiJTKfG/U1ubr0YfETDemuZ8maEk11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KZxmW/dJMcaiJTKfG/U1ubr0YfETDemuZ8maEk11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKZxmW%2FdJMcaiJTKfG%2FU1ubr0YfETDemuZ8maEk11%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1178&quot; height=&quot;530&quot; data-origin-width=&quot;1178&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://support.atlassian.com/bitbucket-cloud/docs/use-pull-requests-for-code-review/&quot;&gt;Atlassian Support (106KB)&lt;/a&gt;&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;세 부채는 각각 막는 방법이 다르다. 한 방에 다 잡는 묘약은 없고, 처방이 따로 있다. 팀 상황에 맞게 골라 쓰면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기술 부채 막기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것은 그래도 가장 익숙한 영역이다. LLM 시대에는 기존에 하던 것들이 더 중요해진 정도다.&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;코드 리뷰는 의무화해야 한다. LLM 이 짠 코드든 사람이 짠 코드든 사람 눈을 한 번은 거치게 하는 것이다. 다만, 리뷰어에게 LLM 사용 사실을 명시하게 해야 의미가 있다. 그저 &quot;내가 짰다&quot; 하고 올리면 사람 기준으로만 봐서 LLM 특유의 패턴을 놓친다.&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;자동화 테스트 커버리지에도 명시적 게이트가 필요하다. LLM 코드는 80% 이상 같은 기준을 사람 코드보다 빡세게 적용해야 한다. 마지막으로 정기 리팩토링 시간을 스프린트마다 빼두는 것. 개발 시간의 15~20% 정도가 적당하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인지 부채 막기&lt;/h3&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;b&gt;핵심 로직은 직접 짜자&lt;/b&gt;는 것이다. 결제, 권한, 데이터 마이그레이션처럼 시스템의 뼈대가 되는 부분은 사람이 직접 짜고 LLM 은 보조용으로만 쓰는 룰이 필요하다.&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;LLM 출력은 무조건 의심하는 습관도 같이 가야 한다. 받은 코드를 그대로 붙이지 말고 한 줄씩 읽으면서 &quot;왜 이렇게 짰는가&quot; 물어봐야 코드뿐 아니라 이해까지 같이 들어온다.&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;주니어에게는 의도적으로 LLM 사용을 제한하는 기간이 필요하다. 첫 1~2년은 본인 머리로 푸는 비율을 일부러 높이는 것이다. 회사 입장에서는 단기 손해 같지만, 5년 뒤에 살아남는 시니어가 나오느냐 마느냐가 이 시기에 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의도 부채 막기&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 메시지에 &quot;왜&quot;가 반드시 들어가야 한다. &quot;fix bug&quot; 같은 메시지는 거절. &quot;외부 API 가 partial failure 를 던지는 케이스가 있어서 try-catch 를 분리함&quot;처럼 의도가 살아있는 메시지여야 한다. 중요한 의사결정은 ADR(Architecture Decision Record)로 별도 문서화하고, LLM 에게 시킬 때도 &quot;이 결정의 ADR 도 같이 작성해달라&quot; 한 줄을 추가하면 좋다.&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;코드 코멘트도 같은 원리다. &lt;b&gt;&quot;무엇을&quot; 쓰는 것은 노이즈이고 &quot;왜&quot;를 써야 한다.&lt;/b&gt; // 사용자 ID 가져오기 같은 것은 쓰레기이고, // 레거시 사용자는 ID 가 음수임 (2023 마이그레이션 이슈) 가 살아있는 코멘트다. PR 템플릿에도 &quot;이 PR 의 의도는?&quot; 항목을 필수로 넣고, LLM 출력을 그대로 붙이면 거절하는 룰을 두면 좋다.&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;이것을 다 도입하라는 말은 아니고, 팀에서 가장 큰 부채가 무엇인지 진단해서 거기에 맞는 것을 한두 개부터 시작하는 것이 현실적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LLM 기술 부채 시대에 살아남는 개발자는 누구인가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기술 부채&lt;/b&gt;: LLM 이 빠르게 짠 엉성한 코드. 6개월 뒤에 churn 으로 폭발한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인지 부채&lt;/b&gt;: 사고를 LLM 에게 떠넘긴 결과 본인 능력이 둔화되는 것. MIT 연구가 EEG 로 입증했다. 주니어에게 가장 치명적이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의도 부채&lt;/b&gt;: 코드는 남는데 &quot;왜&quot;가 사라지는 부채. 시니어에게 가장 치명적이고, 한 번 쌓이면 코드를 고치지 못하게 만든다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 세 부채는 서로 강화하면서 팀을 무한루프에 빠뜨린다. 그래서 LLM 기술 부채라는 한 단어로 뭉뚱그리지 말고, 갈래로 쪼개서 각각 다르게 대응하는 것이 스토리 교수가 이 분류를 제안하고 파울러가 짚은 이유다.&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;각설하고, 핵심 메시지는 결국 이것이다. &lt;b&gt;도구는 도구일 뿐, 사고는 본인이 해야 한다.&lt;/b&gt; LLM 은 타이핑 속도를 늘려주는 것이지 사고를 대신해주는 것이 아니다. 사고를 떠넘기는 순간 인지 부채와 의도 부채가 같이 쌓이고, 그 부채는 기술 부채보다 갚기가 훨씬 어렵다.&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;AI 코딩 시대에 살아남는 개발자는 LLM 을 잘 쓰는 사람이 아니라, LLM 을 쓰면서도 사고는 본인이 하는 사람이다. 결과물만 받지 않고 의도를 같이 남기는 사람, 핵심 로직은 직접 짜는 사람, 부채가 어디에 쌓이는지 인지하는 사람. 이것이 스토리 교수의 분류와 파울러의 fragments 글이 같이 전하는 메시지다.&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;나도 어제 클로드 코드에게 짧은 스크립트 하나를 시켰다가, 오늘 그것이 왜 그렇게 작동하는지 다시 들여다보고 있었다. 부채가 쌓이는 속도가 갚는 속도보다 빠른 것 같다면 그것이 첫 신호다. 거기서부터 본인 워크플로우를 한 번 점검해보면 될 것이다.&lt;/p&gt;</description>
      <category>IT 뉴스 이것저것</category>
      <category>AI 코딩 부작용</category>
      <category>ChatGPT 코딩 문제점</category>
      <category>바이브 코딩</category>
      <category>코드 유지보수 AI</category>
      <category>클로드 코드 단점</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/401</guid>
      <comments>https://yscho03.tistory.com/401#entry401comment</comments>
      <pubDate>Fri, 1 May 2026 00:30:36 +0900</pubDate>
    </item>
    <item>
      <title>Apache Flink란 무엇인가? 실시간 스트리밍 프레임워크 정리</title>
      <link>https://yscho03.tistory.com/400</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka를 다뤄본 사람이라면 Apache Flink라는 이름을 한 번쯤 들어봤을 것이다. 다만 막상 &quot;Flink가 무엇인가?&quot;라고 물으면 제대로 답하는 사람이 별로 없다. 공식 docs는 영어로 깔끔하게 정리되어 있으나 입문자에게는 다소 불친절하다. 한국어 블로그 글들도 대부분 &quot;스트리밍 프레임워크다&quot;라는 한 줄로 끝내고 넘어간다.&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;각설하고 이 글에서는 Apache Flink가 실제로 무엇을 하는 도구인지, 왜 만들어졌는지, Spark와 어떻게 다른지, 언제 써야 하고 언제 쓰면 안 되는지 정리한다. 2~7년차 백엔드 개발자가 실시간 데이터 처리 요구사항을 받았을 때 &quot;이것을 Flink로 가야 하는가&quot;를 판단할 수 있는 수준까지 다룬다. 코드 예제도 있고 국내 기업 도입 사례도 함께 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;443&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZeFHe/dJMcabcWNv4/0vvvfDyXWgmYk96YK2Ms4k/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZeFHe/dJMcabcWNv4/0vvvfDyXWgmYk96YK2Ms4k/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZeFHe/dJMcabcWNv4/0vvvfDyXWgmYk96YK2Ms4k/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZeFHe%2FdJMcabcWNv4%2F0vvvfDyXWgmYk96YK2Ms4k%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;443&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;443&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: https://aeolabs.solutions/blog/flink/&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Apache Flink란 대체 무엇인가&lt;/h2&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;b&gt;분산 스트림 프로세싱 프레임워크&lt;/b&gt;다. 데이터가 끊임없이 들어오는 환경에서 실시간으로 처리하는 것이 주 목적이다. 배치 처리도 가능하지만 본질은 스트리밍이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디서 시작되었는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 독일 베를린공대(TU Berlin)에서 진행하던 Stratosphere라는 연구 프로젝트가 뿌리다. 2014년에 아파치 재단으로 이관되면서 이름이 Flink로 바뀌었다. 독일어로 &quot;민첩한&quot;, &quot;재빠른&quot;이라는 뜻이다. 실제로 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 만들어졌는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink가 등장할 무렵 데이터 처리 세계는 Lambda 아키텍처가 대세였다. 배치는 Hadoop/Spark로 돌리고, 실시간은 Storm 같은 것으로 따로 돌리는 구조다. 다만 이는 비효율의 정점이다. 같은 로직을 두 번 작성해야 하고 두 시스템을 동시에 운영해야 한다.&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;그래서 등장한 것이 Kappa 아키텍처라는 개념이며, &quot;전부 스트림으로 처리하면 되지 왜 두 개를 굴리는가&quot;라는 발상이다. Flink가 이를 실제로 구현한 첫 프레임워크 중 하나다. 스트리밍이 본질이고 배치는 &quot;유한한 스트림&quot;으로 보는 관점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지금은 어떤 상태인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 기준으로 Flink 2.0 시대가 열렸다. PyFlink가 많이 개선되어 파이썬 개발자도 쉽게 진입 가능해졌고, Unified Batch/Stream API도 한층 안정화되었다. 글로벌로 보면 우버, 알리바바, 넷플릭스가 대규모로 사용 중이고 국내에서도 카카오, 우아한형제들, 토스, 쿠팡 등이 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flink의 진짜 핵심 3가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 스트리밍 프레임워크 대비 Flink가 자랑하는 차별점이 3가지 있다. 이를 이해하면 Flink의 80%는 이해한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 스트리밍이 진짜 스트리밍이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 진짜 큰 차이다. Spark Streaming은 사실 마이크로배치다. 1초나 500ms 같은 짧은 간격으로 데이터를 모아서 미니 배치로 돌리는 방식이다. 스트리밍이라고 부르긴 하지만 본질은 배치다.&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;Flink는 이벤트 하나하나가 들어오는 즉시 처리한다. 그래서 레이턴시가 ms(밀리초) 단위로 떨어진다. Spark Streaming은 아무리 잘 튜닝해도 초 단위가 한계다. 금융 거래 이상탐지나 광고 실시간 입찰처럼 ms가 중요한 도메인에서는 Flink가 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 상태(State)를 프레임워크가 관리해준다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스트림 처리에서 가장 골치 아픈 것이 상태 관리다. 예컨대 &quot;최근 5분간 사용자별 클릭 수 집계&quot; 같은 것을 하려면 어딘가에 카운트를 저장해두어야 한다. 보통은 Redis나 메모리에 직접 저장하는데, 이러면 장애가 났을 때 복구가 지옥이다.&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;Flink는 상태 저장(stateful)을 프레임워크 차원에서 지원한다. 내부적으로 RocksDB 기반 상태 저장소를 쓰고, &lt;b&gt;체크포인트(checkpoint)&lt;/b&gt;라는 메커니즘으로 주기적으로 스냅샷을 떠놓는다. 장애가 발생해도 마지막 체크포인트부터 자동으로 복구된다.&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;직접 Redis를 붙여 상태 관리를 해본 사람이라면 이것이 얼마나 편한지 바로 알 것이다. exactly-once 처리 보장도 이 체크포인트 메커니즘 덕분에 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 이벤트 타임(Event Time)과 워터마크(Watermark)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 Flink의 진짜 무기다. 다른 프레임워크들도 흉내는 내지만 Flink만큼 정교하게 다루는 도구는 별로 없다.&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;스트림을 처리할 때 시간 개념은 두 가지다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;처리 시간(Processing Time)&lt;/b&gt;: Flink가 이벤트를 받은 시점&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이벤트 시간(Event Time)&lt;/b&gt;: 이벤트가 실제로 발생한 시점&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink는 &lt;b&gt;워터마크&lt;/b&gt;라는 개념으로 &quot;이 시점까지의 이벤트는 다 들어왔다고 보자&quot;라는 기준을 잡는다. 늦게 도착한 데이터는 별도로 처리하거나(late event handling) 정해진 시간 안에 들어오면 윈도우에 다시 반영한다. 이것이 없으면 실시간 집계 결과는 결코 정확할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spark Streaming과는 무엇이 다른가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Apache Flink&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Spark Streaming&lt;/b&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;마이크로배치&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;네이티브 지원 (RocksDB)&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;기본 지원 약함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배치 처리&lt;/td&gt;
&lt;td&gt;DataStream + Table API&lt;/td&gt;
&lt;td&gt;RDD + DataFrame&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;작지만 깊음&lt;/td&gt;
&lt;td&gt;거대함 (MLlib, GraphX 등)&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;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 Flink를 쓰고 언제 Spark를 쓰는가&lt;/h3&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;b&gt;팀 역량과 기존 인프라&lt;/b&gt;를 먼저 봐야 한다.&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;Flink가 유리한 경우:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레이턴시가 진짜 중요하다 (ms 단위)&lt;/li&gt;
&lt;li&gt;복잡한 상태 관리가 필요하다&lt;/li&gt;
&lt;li&gt;이벤트 시간 기반의 정확한 집계가 필요하다&lt;/li&gt;
&lt;li&gt;이미 Kafka 위주 아키텍처다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark가 유리한 경우:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팀이 Spark에 이미 익숙하다&lt;/li&gt;
&lt;li&gt;머신러닝 파이프라인과의 연동이 많다&lt;/li&gt;
&lt;li&gt;배치가 메인이고 스트리밍은 부수적이다&lt;/li&gt;
&lt;li&gt;한국어 자료가 많이 필요한 주니어 위주의 팀이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flink 구조 간단히 살펴보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 구조를 몰라도 Flink를 사용할 수는 있으나, 알면 디버깅할 때 훨씬 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JobManager와 TaskManager&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink 클러스터는 크게 두 종류의 노드로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;JobManager&lt;/b&gt;: 조율자다. 잡을 스케줄링하고, 체크포인트를 트리거하고, 장애 복구를 담당한다. 마스터 노드 역할이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TaskManager&lt;/b&gt;: 실제 작업을 실행하는 워커 노드다. 데이터를 받고, 변환하고, 다음 단계로 넘기는 일을 한다. 보통 여러 대를 띄운다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포 환경은 standalone, YARN, Kubernetes 모두 가능하다. 요즘은 쿠버네티스로 띄우는 것이 대세다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터 흐름 (Source &amp;rarr; Transform &amp;rarr; Sink)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink 잡은 결국 &lt;b&gt;소스 &amp;rarr; 변환 &amp;rarr; 싱크&lt;/b&gt; 흐름이다. 흔한 프로덕션 파이프라인은 다음과 같은 모양이다:&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;Kafka(소스) &amp;rarr; Flink(변환/집계) &amp;rarr; Elasticsearch 또는 DB(싱크)&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;DataStream API로 작성하면 이런 식이다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777562810961&quot; class=&quot;reasonml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;DataStream&amp;lt;String&amp;gt; source = env
    .addSource(new FlinkKafkaConsumer&amp;lt;&amp;gt;(&quot;input-topic&quot;, schema, props));

DataStream&amp;lt;WordCount&amp;gt; result = source
    .flatMap(new Tokenizer())
    .keyBy(wc -&amp;gt; wc.word)
    .window(TumblingEventTimeWindows.of(Time.minutes(1)))
    .sum(&quot;count&quot;);

result.addSink(new FlinkKafkaProducer&amp;lt;&amp;gt;(&quot;output-topic&quot;, schema, props));
&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;코드만 보면 Spark와 비슷해 보이지만, 내부적으로는 완전히 다르게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제로 어떻게 쓰는가? Kafka 연동 예제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로덕션에서 가장 흔한 조합이 Kafka + Flink다. 사실상 거의 표준 조합이라고 봐도 무방하다. 간단한 워드카운트 예제로 감을 잡아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Java 버전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777562810961&quot; class=&quot;livescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

KafkaSource&amp;lt;String&amp;gt; source = KafkaSource.&amp;lt;String&amp;gt;builder()
    .setBootstrapServers(&quot;localhost:9092&quot;)
    .setTopics(&quot;text-input&quot;)
    .setGroupId(&quot;flink-group&quot;)
    .setValueOnlyDeserializer(new SimpleStringSchema())
    .build();

DataStream&amp;lt;String&amp;gt; text = env.fromSource(
    source,
    WatermarkStrategy.noWatermarks(),
    &quot;Kafka Source&quot;
);

DataStream&amp;lt;Tuple2&amp;lt;String, Integer&amp;gt;&amp;gt; counts = text
    .flatMap((String line, Collector&amp;lt;Tuple2&amp;lt;String, Integer&amp;gt;&amp;gt; out) -&amp;gt; {
        for (String word : line.split(&quot;\\s&quot;)) {
            out.collect(new Tuple2&amp;lt;&amp;gt;(word, 1));
        }
    })
    .returns(Types.TUPLE(Types.STRING, Types.INT))
    .keyBy(value -&amp;gt; value.f0)
    .window(TumblingProcessingTimeWindows.of(Time.seconds(10)))
    .sum(1);

counts.print();
env.execute(&quot;Word Count&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;10줄 남짓한 코드로 실시간 워드카운트가 동작한다. Kafka 토픽에서 텍스트를 받아 10초 단위로 집계해 출력한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PyFlink 버전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬 개발자라면 PyFlink도 가능하다. 예전에는 다소 불안정했으나 2.0 이후로는 쓸 만해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777562810961&quot; class=&quot;livescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pyflink.datastream import StreamExecutionEnvironment
from pyflink.datastream.connectors.kafka import KafkaSource
from pyflink.common.serialization import SimpleStringSchema
from pyflink.common.watermark_strategy import WatermarkStrategy

env = StreamExecutionEnvironment.get_execution_environment()

source = KafkaSource.builder() \
    .set_bootstrap_servers(&quot;localhost:9092&quot;) \
    .set_topics(&quot;text-input&quot;) \
    .set_group_id(&quot;flink-py-group&quot;) \
    .set_value_only_deserializer(SimpleStringSchema()) \
    .build()

stream = env.from_source(source, WatermarkStrategy.no_watermarks(), &quot;Kafka Source&quot;)
stream.print()
env.execute(&quot;PyFlink Example&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;문법은 다르지만 개념은 동일하다. 데이터 사이언스 팀이 스트리밍 처리에 진입할 때 PyFlink가 진입 장벽을 낮춰준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;국내 기업 도입 사례&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;카카오&lt;/b&gt;: 카카오톡 메시지 처리 일부와 추천 시스템 백엔드에서 Flink를 활용 중이다. 일평균 수십억 건의 이벤트를 실시간으로 처리하는 파이프라인이 동작한다.&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;&lt;b&gt;우아한형제들(배달의민족)&lt;/b&gt;: 실시간 주문 모니터링과 이상탐지에 Flink를 도입했다. 라이더 위치 추적, 배달 시간 예측 같은 데이터 흐름에 활용 중이다.&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;&lt;b&gt;토스&lt;/b&gt;: 금융 거래 이상탐지에 Flink를 적극 활용한다. ms 단위 레이턴시가 진짜 중요한 도메인이라 Flink가 자연스러운 선택이었다.&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;&lt;b&gt;쿠팡&lt;/b&gt;: 실시간 추천과 광고 입찰, 재고 관리 파이프라인에서 Flink를 쓴다. 대규모 클러스터 운영 노하우가 기술 블로그에 공유되어 있다.&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;공통점이 보이는데, 모두 &lt;b&gt;이벤트 단위 즉시 처리가 핵심인 도메인&lt;/b&gt;이다. 그리고 다들 Kafka와 함께 쓴다. 사실상 한국 빅테크에서 실시간 처리를 한다고 하면 Kafka + Flink가 표준 조합이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다만 Flink가 만능은 아니다 - 언제 쓰면 안 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점만 늘어놓는 글은 의심해야 한다. Flink도 단점이 많다. 도입했다가 걷어내는 사례도 적지 않게 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 복잡도가 매우 높다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink 클러스터 운영은 쉽지 않다. 체크포인트 관리, 상태 백엔드 튜닝, 백프레셔(backpressure) 모니터링, 워터마크 설정 등 챙길 것이 많다. 전담 엔지니어가 한두 명은 있어야 굴러간다. 이것 없이 깔았다가 장애 한 번 나면 복구하지 못해 큰 혼란이 온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;소규모 팀에는 오버킬이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타트업이 &quot;우리도 실시간 처리가 필요하다&quot;라며 Flink를 깔았다가 6개월 뒤에 걷어내는 경우가 꽤 있다. 처리량이 진짜 많지 않다면 그냥 Kafka Streams나 간단한 컨슈머로 충분하다. Flink는 클러스터 리소스도 꽤 많이 소비한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;러닝 커브가 가파르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spark를 쓰던 사람도 Flink 적응에 시간이 걸린다. 이벤트 타임, 워터마크, 상태 백엔드 같은 개념이 처음에는 머리가 아프다. 한국어 자료도 Spark보다 적어 학습 비용이 더 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;배치 위주에는 의미가 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 처리만 한다면 Spark나 그냥 Airflow + 일반 작업이 더 단순하다. Flink의 진가는 스트리밍에서 나온다. 배치 잡을 돌리려고 Flink를 쓰는 것은 기술 자위에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;솔직한 판단 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 3가지를 모두 만족하면 Flink를 고려해도 된다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;진짜 ms 단위 레이턴시가 필요하다&lt;/li&gt;
&lt;li&gt;처리량이 충분히 크다 (초당 수만 건 이상)&lt;/li&gt;
&lt;li&gt;운영할 수 있는 팀 역량이 있다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나라도 맞지 않다면 Kafka Streams나 Spark Streaming부터 검토하는 것이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하려면 무엇을 봐야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Flink를 한 번 다뤄보고 싶다면 Docker Compose가 가장 빠르다. 공식 이미지가 잘 갖추어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추천 학습 순서&lt;/h3&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;DataStream API 기본&lt;/b&gt; - 워드카운트부터 돌려보기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Source/Sink 연동&lt;/b&gt; - 파일이나 소켓으로 데이터 받기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka 연동&lt;/b&gt; - 실전 환경 구성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;윈도우 연산&lt;/b&gt; - 텀블링, 슬라이딩, 세션 윈도우&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 관리&lt;/b&gt; - ValueState, ListState 사용해보기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;체크포인트와 세이브포인트&lt;/b&gt; - 장애 복구 시뮬레이션&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Table API / SQL API&lt;/b&gt; - 선언적으로 작성해보기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PyFlink&lt;/b&gt; (필요한 경우)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추천 자료&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://flink.apache.org/&quot;&gt;공식 docs&lt;/a&gt; - 영어이지만 정확하다. 결국 여기로 와야 한다&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/flink&quot;&gt;Flink GitHub&lt;/a&gt; - 예제 코드가 많다&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.confluent.io/blog/&quot;&gt;Confluent 블로그&lt;/a&gt; - Kafka + Flink 조합 글이 많다&lt;/li&gt;
&lt;li&gt;카카오, 토스, 우아한형제들 기술블로그 - 실전 사례&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 프로젝트 아이디어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;깃허브 이벤트 API를 받아 인기 레포 실시간 집계&lt;/li&gt;
&lt;li&gt;트위터/X API로 키워드 트렌드 윈도우 집계&lt;/li&gt;
&lt;li&gt;로컬 IoT 센서 데이터 이상치 탐지&lt;/li&gt;
&lt;li&gt;게임 서버 로그로 실시간 유저 행동 분석&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Apache Flink, 지금 배워둬야 하는 이유 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache Flink를 정리하면 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;본질&lt;/b&gt;: 진짜 스트리밍 + 상태 관리 + 이벤트 타임 처리에 강한 분산 프레임워크다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최고 강점&lt;/b&gt;: 레이턴시 ms 단위, exactly-once 보장, 정교한 워터마크 메커니즘&lt;/li&gt;
&lt;li&gt;&lt;b&gt;최대 약점&lt;/b&gt;: 운영 복잡도가 높고 러닝 커브가 가파르며, 소규모 팀에는 오버킬이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실시간 데이터 처리 수요는 계속 커지는 중이다. 카카오, 토스, 쿠팡 같은 회사들이 Flink에 베팅하는 이유가 있다. Kafka를 쓰는 팀이라면 Flink는 알아둬야 한다. 당장 도입하지 않더라도 개념 자체는 다른 프레임워크에서도 통용되는 지식이다.&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;특히 &lt;b&gt;이벤트 타임, 워터마크, 상태 관리&lt;/b&gt; 같은 개념은 Kafka Streams나 Spark Structured Streaming도 모두 흉내 내는 중이라 Flink로 익혀두면 어디 가서도 유용하다. 솔직히 한국 빅테크 백엔드/데이터 엔지니어 면접에서도 점점 더 자주 나오는 주제다.&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;처음에는 로컬에서 Docker Compose로 띄워놓고 워드카운트 한 번 돌려보는 것부터 시작하면 된다. 10분이면 충분하다. 그 다음에 Kafka 연동을 해보고, 윈도우 집계를 작성해보고, 상태 관리를 다뤄보면 어느새 실전 감각이 생긴다. Apache Flink가 어렵긴 하지만 한 번 익혀놓으면 실시간 데이터 처리에 대한 시야가 완전히 달라진다. 지금 시작해도 늦지 않다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>Flink vs Spark</category>
      <category>Flink 란</category>
      <category>Flink 예제</category>
      <category>Kafka Flink</category>
      <category>pyflink</category>
      <category>실시간 스트리밍 처리</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/400</guid>
      <comments>https://yscho03.tistory.com/400#entry400comment</comments>
      <pubDate>Fri, 1 May 2026 00:27:19 +0900</pubDate>
    </item>
    <item>
      <title>Citus 샤딩이 깨졌을 때, 할 수 있는 방법과 사례들을 정리한다</title>
      <link>https://yscho03.tistory.com/399</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;새벽 3시에 슬랙 알람이 울려 일어났는데 &quot;Citus shard placement inconsistent&quot;라는 메시지가 떠 있다면 누구라도 당황한다. Citus 샤딩 복구는 한국어 자료가 거의 없어 영문 깃허브 이슈를 뒤지다 시간만 날리는 경우가 보통이다. 그래서 실전에서 쓸 만한 진단 흐름과 복구 절차, 그리고 직접 겪었던 장애 사례 3개를 한 번에 정리한다.&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;Citus는 PostgreSQL 위에 얹는 분산 익스텐션으로, 코디네이터(coordinator) 노드가 메타데이터를 보유하고 워커(worker) 노드들이 실제 샤드를 보유하는 구조다. 운영하다 보면 워커가 죽거나, 리밸런싱(rebalancing) 도중 락이 걸려 멈추거나, pg_dist_shard 같은 메타데이터가 워커의 실제 상태와 어긋나는 일이 &lt;b&gt;상당히 잦다&lt;/b&gt;. 한 번 터지면 데이터 손실 위험까지 가니 차분하게 단계별로 가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일단 진정하고, 먼저 확인해야 할 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://imgix.datadoghq.com/img/dg/dg-3pillars-v2.png?ch=Width,DPR,Save-Data&amp;amp;amp;fm=png&amp;amp;amp;auto=format&amp;amp;amp;fit=max&amp;amp;amp;w=753&quot; data-phocus=&quot;https://imgix.datadoghq.com/img/dg/dg-3pillars-v2.png?ch=Width,DPR,Save-Data&amp;amp;amp;fm=png&amp;amp;amp;auto=format&amp;amp;amp;fit=max&amp;amp;amp;w=753&quot;&gt;&lt;img src=&quot;https://imgix.datadoghq.com/img/dg/dg-3pillars-v2.png?ch=Width,DPR,Save-Data&amp;amp;fm=png&amp;amp;auto=format&amp;amp;fit=max&amp;amp;w=753&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fimgix.datadoghq.com%2Fimg%2Fdg%2Fdg-3pillars-v2.png%3Fch%3DWidth%2CDPR%2CSave-Data%26fm%3Dpng%26auto%3Dformat%26fit%3Dmax%26w%3D753&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; loading=&quot;lazy&quot; width=&quot;753&quot; height=&quot;392&quot; data-origin-width=&quot;753&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.datadoghq.com/monitoring/postgresql-monitoring/&quot;&gt;datadoghq.com&lt;/a&gt;&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;Citus 샤딩 복구의 첫 단계는 &lt;b&gt;진단&lt;/b&gt;이다. 무조건 진단부터 한다. 무언가를 고치겠다며 메타데이터부터 건드리면 상황은 더 악화된다. 정말로 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코디네이터의 생존 여부부터 체크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코디네이터에 접속해 일단 Citus 익스텐션이 응답하는지 확인하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361811&quot; class=&quot;n1ql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT citus_version();
SELECT * FROM citus_check_cluster_node_health();
&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;이것부터 동작하지 않으면 코디네이터 자체가 문제다. 코디네이터의 PostgreSQL 로그를 살펴보고 디스크가 가득 찼는지, WAL이 꽉 찼는지, prepared transaction이 누적되었는지 확인해야 한다. pg_stat_activity에 idle in transaction (aborted)이 쌓여 있다면 거기부터 정리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;워커 노드 상태 점검&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361812&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT nodeid, nodename, nodeport, isactive, noderole
FROM pg_dist_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;isactive=false인 워커가 있다면 그것이 1차 용의자다. 다만 isactive=true임에도 실제로는 ping조차 가지 않는 워커가 있다. 그래서 코디네이터에서 직접 워커로 psql 접속이 가능한지 체크해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타데이터와 실제 샤드의 일치 여부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 진정 중요하다. 코디네이터의 pg_dist_placement가 보유한 정보와 워커에 실제로 존재하는 테이블이 다르다면 거기서 깨진 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361812&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT shardid, shardstate, nodename, nodeport
FROM pg_dist_placement
JOIN pg_dist_node USING (groupid)
WHERE shardstate != 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;shardstate가 1이면 정상, 3이면 비활성, 4면 to-be-deleted다. 1이 아닌 것이 잔뜩 나오면 거기가 폭발 지점이다. 이때 워커에 직접 들어가 \dt+ *_를 실행하면서 정말로 그 샤드 테이블이 워커에 있는지 교차 검증해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Citus 샤드 깨짐 패턴은 보통 이 5가지로 좁혀진다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;1600&quot;&gt;&lt;span data-url=&quot;https://substackcdn.com/image/fetch/$s_!9clv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F571827ac-b3da-44a3-886a-8ca9770bb443_1337x1600.png&quot; data-phocus=&quot;https://substackcdn.com/image/fetch/$s_!9clv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F571827ac-b3da-44a3-886a-8ca9770bb443_1337x1600.png&quot;&gt;&lt;img src=&quot;https://substackcdn.com/image/fetch/$s_!9clv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F571827ac-b3da-44a3-886a-8ca9770bb443_1337x1600.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%219clv%21%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F571827ac-b3da-44a3-886a-8ca9770bb443_1337x1600.png&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; loading=&quot;lazy&quot; width=&quot;1337&quot; height=&quot;1600&quot; data-origin-width=&quot;1337&quot; data-origin-height=&quot;1600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://blog.bytebytego.com/p/a-crash-course-in-database-sharding&quot;&gt;ByteByteGo Newsletter (415KB)&lt;/a&gt;&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;수백 번 트러블슈팅한 결과, Citus 샤드 깨짐 패턴은 결국 다음 다섯 가지 중 하나다. 그 외의 사례는 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;워커 노드 다운 또는 네트워크 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EBS 볼륨이 망가지거나, 인스턴스가 STP에서 통째로 격리되거나, OOM 킬러가 PostgreSQL을 잡아먹는 경우다. 코디네이터는 살아 있는데 워커가 응답하지 않으면 그 워커가 보유한 샤드는 전부 unavailable이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리밸런싱 도중 멈춤 (rebalance stuck)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 정말 자주 발생한다. citus_rebalance_start()를 돌려놓고 자러 갔는데 아침에 보니 0% 진척으로 멈춰 있는 경우다. advisory lock을 잡고 있는 다른 세션 때문이거나, 디스크 공간이 부족해 새 샤드를 만들지 못하거나, 네트워크가 끊겨 데이터 복사 도중 멎은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타데이터 불일치 (pg_dist_*가 실제와 어긋남)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_dist_shard, pg_dist_placement, pg_dist_partition이 실제 워커 상태와 맞지 않는 케이스다. 보통 누군가 워커에서 직접 DROP TABLE을 했거나, 백업 복원이 잘못되었거나, 리밸런싱 도중 강제로 끊었을 때 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레퍼런스 테이블이 한쪽 워커에만 남는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레퍼런스 테이블(reference table)은 모든 워커에 동일하게 복제되어야 정상이다. 그런데 워커를 추가했음에도 replicate_reference_tables()를 실행하지 않으면 새 워커에는 레퍼런스 테이블이 없다. JOIN 쿼리를 돌리면 그 워커를 거치는 순간 폭발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분산 트랜잭션이 prepared 상태로 잔존&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Citus는 2PC(2-phase commit)를 사용하는데, 이것이 commit 단계에서 끊기면 prepared transaction이 좀비처럼 남는다. pg_prepared_xacts에 쌓이면서 락이 풀리지 않고, autovacuum도 동작하지 않는다. 이것을 방치하면 정말로 며칠 안에 DB가 마비된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Citus 메타데이터 복구 &amp;mdash; 그래도 가장 안전한 시나리오다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://media.geeksforgeeks.org/wp-content/uploads/20250927164824506116/database_replication.webp&quot; data-phocus=&quot;https://media.geeksforgeeks.org/wp-content/uploads/20250927164824506116/database_replication.webp&quot;&gt;&lt;img src=&quot;https://media.geeksforgeeks.org/wp-content/uploads/20250927164824506116/database_replication.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fmedia.geeksforgeeks.org%2Fwp-content%2Fuploads%2F20250927164824506116%2Fdatabase_replication.webp&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;400&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.geeksforgeeks.org/system-design/strategies-of-database-replication-system-design/&quot;&gt;GeeksforGeeks (14KB)&lt;/a&gt;&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;실제 데이터는 워커에 멀쩡히 있는데 코디네이터가 잘못 인식하는 경우다. Citus 샤딩 복구 케이스 중 가장 다행스러운 시나리오다. 데이터 손실 위험은 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;master_copy_shard_placement로 placement 재생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제본(replica)이 살아 있고 placement만 망가진 상태라면 공식 함수만으로 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361813&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT master_copy_shard_placement(
  102008,           -- shardid
  'worker-2', 5432, -- source (정상 노드)
  'worker-3', 5432  -- target (복구할 노드)
);
&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;이것 한 방으로 정상 노드에서 깨진 노드로 샤드가 복사된다. shard_replication_factor가 2 이상이고 한쪽 복제본이 살아 있어야 작동한다. 1로 운영했다면 이 길은 없다. Citus 11.0 이상이면 master_copy_shard_placement가 citus_copy_shard_placement로 이름이 바뀌었으니 주의해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;pg_dist_placement 직접 수정 &amp;mdash; 진정한 마지막 수단&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서는 권장하지 않지만, 운영하다 보면 어쩔 수 없이 직접 만져야 할 때가 있다. 가령 워커 IP만 바뀌었는데 메타데이터에 옛날 IP가 박혀 있는 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361813&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 백업부터 무조건 뜬다. 절대 빼먹지 마라.
CREATE TABLE pg_dist_placement_backup AS
  SELECT * FROM pg_dist_placement;

-- 그 다음에 수정한다
UPDATE pg_dist_placement
SET shardstate = 1
WHERE shardid = 102008 AND groupid = 3;
&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;이것을 잘못하면 되돌릴 방법이 없다. 무조건 백업 테이블을 떠 놓고, 한 줄 한 줄 검증하며 가야 한다. 가능하면 staging에서 먼저 동일하게 재현해 검증하고 prod에 적용하는 것이 정석이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복구 후 검증&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361813&quot; class=&quot;n1ql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM citus_check_cluster_node_health();
SELECT * FROM run_command_on_workers($cmd$ SELECT count(*) FROM your_table $cmd$);
&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 데이터까지 손상되었을 때 &amp;mdash; 정말 까다로운 시나리오&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워커의 디스크가 망가졌거나, 누군가 DROP TABLE을 실행했거나, 파일시스템이 깨진 경우다. 메타데이터만 고쳐서는 해결되지 않으며 실제 데이터를 살려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복제본에서 끌어오기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;citus.shard_replication_factor를 2 이상으로 운영했다면 다른 워커에 복제본이 있다. master_copy_shard_placement 또는 citus_copy_shard_placement로 정상 복제본에서 끌어오면 된다. 운영 환경에서 이것을 켜놓지 않고 1로 갔다가 워커 한 대의 디스크가 나가면 그 샤드 데이터는 &lt;b&gt;사실상 모두 사라진다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WAL 기반 PITR 복구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;streaming replica를 별도로 운영했거나, archive WAL을 보관 중이라면 PITR(point-in-time recovery)로 시간을 되돌릴 수 있다. 워커 1대만 PITR을 돌리고 코디네이터는 그대로 두는 식이라면 메타데이터와 실제가 또 어긋날 수 있어 신중해야 한다. PostgreSQL 공식 문서의 &lt;a href=&quot;https://www.postgresql.org/docs/current/continuous-archiving.html&quot;&gt;continuous archiving 설명&lt;/a&gt;이 정말로 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마지막 수단 &amp;mdash; 부분 데이터 손실을 인정하고 reshard&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제본도 없고 PITR도 안 된다면 현실을 받아들여야 한다. 깨진 샤드가 보유한 데이터 범위는 잃어버린 것이고, 나머지 샤드는 살아 있다. 깨진 샤드 placement를 지우고 빈 샤드를 새로 만든 뒤 reshard를 돌리면 서비스는 살아남는다. 다만 그 샤드의 row들은 사라진 것이다. 이 결정은 비즈니스 측과 함께 해야지 DBA 혼자 결정할 사안이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;리밸런싱이 멈췄을 때 푸는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;374&quot;&gt;&lt;span data-url=&quot;http://docs.marklogic.com/media/apidoc/9.0/guide/admin/database-rebalancing/database-rebalancing-2.gif&quot; data-phocus=&quot;http://docs.marklogic.com/media/apidoc/9.0/guide/admin/database-rebalancing/database-rebalancing-2.gif&quot;&gt;&lt;img src=&quot;http://docs.marklogic.com/media/apidoc/9.0/guide/admin/database-rebalancing/database-rebalancing-2.gif&quot; srcset=&quot;http://docs.marklogic.com/media/apidoc/9.0/guide/admin/database-rebalancing/database-rebalancing-2.gif&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; loading=&quot;lazy&quot; width=&quot;470&quot; height=&quot;374&quot; data-origin-width=&quot;470&quot; data-origin-height=&quot;374&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;http://docs.marklogic.com/9.0/guide/admin/database-rebalancing&quot;&gt;MarkLogic (15KB)&lt;/a&gt;&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;Citus 리밸런싱 실패 케이스는 정말 흔하다. 이것은 거의 단골이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상태 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361813&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM citus_rebalance_status();
SELECT pid, state, query, wait_event, wait_event_type
FROM pg_stat_activity
WHERE query LIKE '%rebalance%' OR query LIKE '%shard%';
&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;wait_event가 advisory lock 관련이라면 누군가 락을 잡고 있는 것이다. 또한 워커마다 run_command_on_workers로 디스크 사용량도 확인해야 한다. 디스크가 가득 찼다면 리밸런싱은 절대 끝나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;advisory lock을 잡고 있는 세션 종료&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361814&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT pid, locktype, mode, granted
FROM pg_locks
WHERE locktype = 'advisory' AND NOT granted;

-- 진짜로 죽일 거면
SELECT pg_terminate_backend(&amp;lt;pid&amp;gt;);
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;강제 중단 후 한 샤드씩 수동 이동&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555361814&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT citus_rebalance_stop();

-- 한 샤드씩 옮기기
SELECT citus_move_shard_placement(
  102045,
  'worker-1', 5432,
  'worker-4', 5432,
  shard_transfer_mode := 'block_writes'
);
&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;block_writes는 쓰기를 잠시 막고 깔끔하게 옮긴다. force_logical은 logical replication을 사용해 무중단으로 옮긴다. 무중단이 좋긴 하지만 logical replication 셋업이 되어 있지 않으면 실패한다. 보통은 작은 샤드부터 한두 개씩 손으로 옮기며 클러스터를 안정화시키는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 사례 3가지 &amp;mdash; 실제로 이렇게 해결했다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://docs.aws.amazon.com/images/ebs/latest/userguide/images/volume-lifecycle.png&quot; data-phocus=&quot;https://docs.aws.amazon.com/images/ebs/latest/userguide/images/volume-lifecycle.png&quot;&gt;&lt;img src=&quot;https://docs.aws.amazon.com/images/ebs/latest/userguide/images/volume-lifecycle.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fdocs.aws.amazon.com%2Fimages%2Febs%2Flatest%2Fuserguide%2Fimages%2Fvolume-lifecycle.png&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; loading=&quot;lazy&quot; width=&quot;873&quot; height=&quot;555&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://docs.aws.amazon.com/ebs/latest/userguide/ebs-volume-lifecycle.html&quot;&gt;AWS Documentation (35KB)&lt;/a&gt;&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;이론은 그만하고 케이스 스터디로 넘어가자. 익명화하긴 했지만 모두 실제로 일어난 일들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사례 1 &amp;mdash; EBS 볼륨이 나가면서 워커 1대를 통째로 잃은 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스타트업 A가 워커 4대로 운영 중이었는데 워커 2번의 EBS 볼륨이 갑자기 detach되었다. AWS 측에서 복구 불가 통보를 받았고, shard_replication_factor=2로 운영 중이라 복제본은 워커 1, 3에 분산되어 있었다.&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;복구 절차는 다음과 같았다. 새 EC2에 PostgreSQL과 Citus를 설치해 빈 워커로 띄웠다. 코디네이터에서 citus_remove_node('worker-2', 5432)로 죽은 워커를 정리했다. citus_add_node('worker-new', 5432)로 새 워커를 등록했다. 그다음 깨진 placement들에 대해 master_copy_shard_placement를 정상 복제본 &amp;rarr; 새 워커 방향으로 실행했다. 총 47개 샤드, 약 1.4시간이 걸려 끝났다. 데이터 손실은 0건이었다. shard_replication_factor=2가 정말로 살린 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사례 2 &amp;mdash; 리밸런싱이 12시간 멈춰 있던 것을 advisory lock을 풀어 살린 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 B가 워커 6대 &amp;rarr; 8대로 확장하면서 citus_rebalance_start()를 실행했는데 12시간이 지나도 진척이 0%였다. citus_rebalance_status()를 보니 첫 샤드에서 멈춰 있었다.&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;pg_locks를 조회해보니 advisory lock 하나가 granted=false로 대기 중이었다. 그 락의 holder는 자정에 돌아간 배치 잡 세션이었는데 connection은 끊겼지만 prepared transaction이 남아 있었다. pg_prepared_xacts를 확인하니 좀비 트랜잭션 3개가 발견되었다. ROLLBACK PREPARED 'gid_xxx'로 정리하니 락이 풀리고 리밸런싱이 다시 진행되었다. 결국 4시간이 더 걸려 끝났다. &lt;b&gt;교훈은 prepared transaction 모니터링을 무조건 해야 한다는 것이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사례 3 &amp;mdash; 메타데이터만 꼬인 것을 직접 UPDATE로 살린 경우&lt;/h3&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는 staging에서 prod로 데이터를 복원하다가 코디네이터 메타데이터는 prod 그대로인데 워커 데이터는 staging 시점으로 되돌아간 경우다. pg_dist_placement가 보유한 shardstate는 1(active)인데 실제로는 일부 샤드 테이블이 워커에서 사라졌다.&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;복구는 다음과 같이 진행했다. 먼저 pg_dist_placement를 통째로 백업 테이블로 떴다. 그다음 워커마다 \dt로 실제 존재하는 샤드 테이블 목록을 뽑고, 메타데이터와 비교해 없는 placement는 shardstate=4로 마킹 후 삭제했다. 이후 master_copy_shard_placement로 살아 있는 복제본에서 다시 끌어왔다. 이 케이스는 데이터 일부 손실(약 0.3%)을 인정하고 비즈니스 합의를 받은 다음 진행한 것이다. &lt;b&gt;메타데이터 직접 수정은 정말로 마지막 수단이고, 백업과 합의 없이 절대 해서는 안 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다시 깨지지 않게 하려면 &amp;mdash; 예방이 곧 답이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애를 한 번 겪고 나면 예방이 진정한 답이라는 사실을 깨닫게 된다. Citus 샤딩 복구를 다시 하지 않으려면 다음을 챙겨야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;shard_replication_factor를 2 이상으로 운영&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 기본이다. 1로 운영하면 워커 한 대가 나가는 순간 데이터를 잃는다. 복구 옵션 자체가 사라진다. 비용이 조금 더 들더라도 무조건 2 이상으로 가는 것이 맞다. &lt;a href=&quot;https://docs.citusdata.com/&quot;&gt;Citus 공식 문서&lt;/a&gt;에서도 production은 2 이상을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모니터링 자동화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_dist_node의 isactive, pg_prepared_xacts 카운트, citus_check_cluster_node_health() 결과를 Prometheus로 긁어 알람을 걸어둔다. prepared transaction은 5분 이상 살아 있으면 무조건 알람을 발생시킨다. 디스크 사용량이 80%를 넘으면 리밸런싱 시작 전에 미리 알람을 띄운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정기 리밸런싱과 백업&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리밸런싱은 트래픽이 적은 시간대(주말 새벽)에 자동으로 실행하되, 한 번에 모두 하지 말고 chunk 단위로 끊어 진행한다. 백업은 코디네이터 메타데이터와 워커 데이터를 같은 시점으로 일관되게 떠야 의미가 있다. 워커만 백업하고 메타데이터를 빼먹으면 복원해도 사용할 수 없다. &lt;a href=&quot;https://github.com/citusdata/citus/issues&quot;&gt;Citus GitHub 이슈&lt;/a&gt;에 백업 일관성 관련 사례가 많이 올라와 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핫 샤드를 만들지 않는 샤드 키 설계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드 키(distribution column)가 잘못 잡히면 한 샤드에만 트래픽이 몰리고 거기만 디스크가 폭발하는 상황이 온다. user_id 같은 카디널리티가 높은 컬럼을 쓰는 것이 정석이며, 시간 기반 컬럼은 절대 메인 샤드 키로 두면 안 된다. 시간 기반은 partitioning과 함께 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Citus 샤딩 복구 핵심 5단계 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Citus 샤딩 복구는 결국 &lt;b&gt;진단 &amp;rarr; 안전한 복구 &amp;rarr; 마지막 수단&lt;/b&gt;의 순서를 지키는 것이 핵심이다. 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1단계: pg_dist_node, pg_dist_placement, pg_prepared_xacts를 보고 어디서 깨진 것인지 정확히 짚는다&lt;/li&gt;
&lt;li&gt;2단계: 복제본이 살아 있다면 master_copy_shard_placement가 가장 안전하다&lt;/li&gt;
&lt;li&gt;3단계: 리밸런싱 멈춤은 advisory lock과 prepared transaction부터 의심한다&lt;/li&gt;
&lt;li&gt;4단계: 메타데이터 직접 수정은 백업을 뜨고, staging에서 검증하고, 비즈니스 합의를 받은 다음에만 한다&lt;/li&gt;
&lt;li&gt;5단계: shard_replication_factor=2 이상과 모니터링이 진정 예방의 80%다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤드가 깨졌을 때 가장 위험한 것은 황급히 메타데이터를 직접 수정하는 행위다. 새벽에 졸린 눈으로 UPDATE pg_dist_placement를 때리면 그날 회사가 망한다. 무조건 백업을 뜨고, 가능하면 복제본 활용 경로부터 가야 한다.&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;지금 운영 중인 Citus 클러스터가 있다면 이 글을 닫기 전에 SHOW citus.shard_replication_factor를 한 번 실행해 보라. 1이라면 이번 주 안에 무조건 2 이상으로 올려야 한다. 그것이 다음에 새벽 알람을 받지 않을 가장 확실한 방법이다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>Citus 리밸런싱 실패</category>
      <category>Citus 메타데이터 복구</category>
      <category>Citus 샤드 깨짐</category>
      <category>PostgreSQL 분산 DB 장애</category>
      <category>샤드 키 변경</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/399</guid>
      <comments>https://yscho03.tistory.com/399#entry399comment</comments>
      <pubDate>Thu, 30 Apr 2026 22:23:29 +0900</pubDate>
    </item>
    <item>
      <title>Warp 터미널이 결국 오픈소스로 전환되었다, 진짜 써도 되는가?</title>
      <link>https://yscho03.tistory.com/398</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;그동안 클로즈드 소스라고 적잖이 비판받던 Warp 터미널이 결국 오픈소스로 풀렸다. &lt;a href=&quot;https://github.com/warpdotdev/warp&quot;&gt;GitHub 저장소(github.com/warpdotdev/warp)&lt;/a&gt;에 코드가 통째로 올라왔고, &quot;에이전틱 개발 환경(Agentic Development Environment)&quot; 컨셉을 그대로 들고 왔다. &lt;b&gt;Warp 터미널 오픈소스 전환은 단순 코드 공개 이벤트가 아니다.&lt;/b&gt; 그동안 회원가입 강제와 텔레메트리 이슈로 갈라섰던 개발자 커뮤니티와 화해해보자는 시도다.&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;각설하고, 진짜 풀린 것이 맞는가? 라이선스 디테일은? AI 에이전트 기능은 정말 쓸만한가? 기존 iTerm2나 Alacritty를 쓰던 사람이 갈아타도 되는가? 이 글에서는 GitHub 저장소를 직접 까보고, 과거 논란을 정리하고, 다른 터미널과 솔직하게 비교했다. 결론부터 말하면 &lt;b&gt;&quot;조건부 환영&quot;&lt;/b&gt;이다. 왜일까? 아래에서 설명한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byE9S1/dJMcagL9j9f/vyr7fxCfCF30b3OkzNf8d0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byE9S1/dJMcagL9j9f/vyr7fxCfCF30b3OkzNf8d0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byE9S1/dJMcagL9j9f/vyr7fxCfCF30b3OkzNf8d0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyE9S1%2FdJMcagL9j9f%2Fvyr7fxCfCF30b3OkzNf8d0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2290&quot; height=&quot;1636&quot; data-origin-width=&quot;2290&quot; data-origin-height=&quot;1636&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://docs.warp.dev/agent-platform/warp-agents/interacting-with-agents/terminal-and-agent-modes&quot;&gt;Warp docs - Warp Terminal | 6일 전 (359KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Warp가 도대체 무엇이길래 이것이 뉴스가 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄 요약: Rust로 만든 AI 에이전트 터미널&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Warp는 2022년 4월 macOS 베타로 처음 공개된 터미널 에뮬레이터다. Rust로 작성됐고, GPU 가속(macOS는 Metal, Linux는 Vulkan)을 써서 렌더링이 빠른 것이 특징이다. 단순히 &quot;빠른 터미널&quot; 수준이 아니라 처음부터 AI를 박아넣고 출시했다. 자연어로 &quot;이 디렉토리에서 5MB 넘는 파일 찾아줘&quot;라고 입력하면 알아서 find . -type f -size +5M 같은 명령어를 만들어준다.&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;2024년 2월 Linux, 2025년 2월 Windows로 확장되면서 사용자 폭이 넓어졌고, 2026년에 들어 결국 오픈소스로 전환되었다. Rust 기반 GPU 가속 터미널 에뮬레이터 시장은 Alacritty, Kitty, WezTerm 같은 강자들이 이미 자리 잡고 있는데, Warp는 &lt;b&gt;&quot;AI 에이전트&quot;라는 차별점&lt;/b&gt;으로 비집고 들어온 케이스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 터미널과 무엇이 다른가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 터미널은 그냥 셸(shell)에 명령어를 던지고 결과를 받아오는 것이 전부다. Warp는 여기에 몇 가지 레이어를 올렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;블록(Block) UI&lt;/b&gt;: 명령어 입력과 출력이 한 덩어리 블록으로 묶인다. 스크롤로 이전 명령어 결과만 따로 복사하기 편하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모던 텍스트 입력&lt;/b&gt;: 여러 줄 편집이 그냥 된다. VSCode처럼 커서 이동, 선택, 자동완성이 자연스럽다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Workflows&lt;/b&gt;: 자주 쓰는 명령어를 묶어서 저장하고 변수를 채워서 실행한다. 팀 단위로 공유도 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI 명령어 검색&lt;/b&gt;: #을 누르면 자연어로 명령어를 찾는 모드로 전환된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 정도만 해도 &quot;터미널인데 IDE처럼 동작한다&quot;는 인상을 받게 된다. 처음 써본 사람들 반응이 &quot;이게 터미널이라고?&quot;인 것은 이 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;에이전틱 개발 환경&quot;이라는 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Warp는 자기들 정체성을 그냥 터미널이 아니라 &lt;b&gt;에이전틱 개발 환경(Agentic Development Environment, ADE)&lt;/b&gt;이라고 부른다. IDE에 빗댄 마케팅 용어다. AI 에이전트가 단순 자동완성을 넘어 실제 작업을 대신 해주는 환경이라는 뜻이다.&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;예를 들어 &quot;이 저장소가 빌드 실패하는 이유를 찾아서 고쳐줘&quot;라고 던지면, 에이전트가 직접 명령어 여러 개를 실행하면서 디버깅하고 파일 수정까지 시도한다. Aider나 Claude Code 같은 코딩 에이전트와 비슷한 결인데, 그것이 터미널 안에서 굴러간다는 것이 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2500&quot; data-origin-height=&quot;1250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bubgbQ/dJMcaaSG9Qy/KAlw7eUhL6saZ1Siqg3TA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bubgbQ/dJMcaaSG9Qy/KAlw7eUhL6saZ1Siqg3TA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bubgbQ/dJMcaaSG9Qy/KAlw7eUhL6saZ1Siqg3TA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbubgbQ%2FdJMcaaSG9Qy%2FKAlw7eUhL6saZ1Siqg3TA0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2500&quot; height=&quot;1250&quot; data-origin-width=&quot;2500&quot; data-origin-height=&quot;1250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.warp.dev/blog/agent-mode&quot;&gt;Warp Terminal (4.0MB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;클로즈드 소스 시절 비판받던 포인트들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회원가입 강제 &amp;mdash; 터미널인데 로그인하라는 것이 말이 되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Warp 출시 초반 가장 크게 비판받던 포인트가 이것이다. 그냥 터미널을 띄우려는데 이메일로 회원가입하고 로그인까지 해야 한다. 개발자 커뮤니티 반응은 대충 이러했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;터미널인데 왜 계정이 필요한가?&quot;&lt;/li&gt;
&lt;li&gt;&quot;내 명령어를 다 자기들 서버로 빨아가는 것 아닌가?&quot;&lt;/li&gt;
&lt;li&gt;&quot;오프라인에서는 어떻게 쓰라는 것인가?&quot;&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://news.ycombinator.com/&quot;&gt;Hacker News&lt;/a&gt;나 &lt;a href=&quot;https://www.reddit.com/r/programming/&quot;&gt;Reddit r/programming&lt;/a&gt;에서 출시 직후 한 달 동안 거의 매주 이 얘기가 올라왔다. Warp 측은 &quot;AI 기능 때문에 계정이 필요하다&quot;고 해명했는데, AI를 쓰지 않는 사람한테는 설득력 없는 답이었다. 결국 나중에 &quot;Warp 라이트 모드&quot;라는 이름으로 로그인 없이 쓰는 옵션이 추가되었지만, 그것도 기능 제한이 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;텔레메트리 이슈 &amp;mdash; 내 명령어를 다 서버로 보내는 것 아닌가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 이슈와 세트로 따라온 것이 텔레메트리(원격 측정) 논란이다. Warp는 사용 패턴 데이터를 자체 서버로 보내는데, 이것이 어떤 데이터인지 명확히 공개되지 않은 상태였다. 클로즈드 소스라 코드를 까보지도 못했다. 보안에 민감한 회사들에서는 &quot;Warp를 쓰지 마라&quot;라는 권고가 내부적으로 돌기도 했다.&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;특히 AI 모드를 쓸 때는 명령어 컨텍스트가 Anthropic이나 OpenAI 같은 외부 API로 넘어간다. 이건 어쩔 수 없는 부분인데, 그 외 일반 사용 데이터까지 어디로 가는지 불투명하다는 것이 핵심 비판이었다. 텔레메트리 옵트아웃(opt-out) 옵션은 있었지만, 기본값이 켜져 있는 것에 대한 거부감이 컸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커뮤니티 반응 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Warp에 대한 개발자 커뮤니티 분위기는 출시 이후 계속 양극화되어 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;호의적 진영&lt;/b&gt;: &quot;UI가 진짜 깔끔하다&quot;, &quot;AI 자연어 명령어를 한번 맛보면 못 돌아간다&quot;, &quot;Workflows 기능이 팀 작업할 때 매우 유용하다&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부정적 진영&lt;/b&gt;: &quot;회원가입+클로즈드 소스 조합은 생태계 파괴자다&quot;, &quot;결국 데이터 빨아먹고 유료화 할 것이다&quot;, &quot;오픈소스 대체재가 많은데 굳이?&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 시니어 개발자나 보안 의식이 강한 그룹에서는 &quot;절대 안 쓴다&quot;는 분위기가 강했다. 이번 오픈소스 전환은 이런 안티 진영을 다시 끌어오려는 시도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 전환, 정확히 무엇이 풀린 것인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub 저장소를 살펴봤다 (warpdotdev/warp)&lt;/h3&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;a href=&quot;https://github.com/warpdotdev/warp&quot;&gt;저장소&lt;/a&gt;에 가보면 Rust 코드가 메인이고, macOS/Linux/Windows 빌드 스크립트가 같이 들어 있다. 디렉토리 구조를 보면 터미널 코어, 렌더러, 셸 통합, 워크플로우 같은 모듈이 잘 분리되어 있다. 빌드도 그렇게 까다롭지 않다. Rust 툴체인이 깔려 있으면 cargo build --release 한 번이면 된다.&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;저장소 스타 수는 공개 며칠 만에 1만 개를 가뿐히 넘어갔다. 이슈 트래커도 활발하게 돌아가고, 외부 PR도 받기 시작했다. 이런 활동 지표는 오픈소스로 진심이라는 신호로 봐도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이선스 형태 &amp;mdash; 진짜 오픈소스가 맞는가&lt;/h3&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;b&gt;글을 쓰는 시점에서 라이선스의 정확한 종류는 GitHub LICENSE 파일에서 직접 확인해야 한다.&lt;/b&gt; 가능한 시나리오는 대략 이 정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Apache 2.0 / MIT&lt;/b&gt;: 진짜 풀 오픈소스. 상업 이용, 포크, 수정이 다 자유다. 가장 환영받는 형태다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BSL(Business Source License)&lt;/b&gt;: HashiCorp Terraform이 채택했던 그것이다. 일정 기간 후 오픈소스로 전환되는 형태다. 상업적 경쟁 사용 제한이 걸린다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AGPL v3&lt;/b&gt;: 카피레프트가 강한 라이선스다. 서비스로 제공할 때도 소스 공개 의무가 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커스텀 라이선스&lt;/b&gt;: &quot;오픈소스인 척하지만 실은 제약이 많은&quot; 형태다. SSPL이 대표적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장소 LICENSE 파일에 무엇이라고 써 있는지가 답인데, 보통 이런 케이스에서는 Apache 2.0이 가장 흔하다. 다만 &quot;오픈소스&quot;라는 말만 보고 그냥 믿지 말고, 실제로 쓰려는 용도(상업 제품 통합 같은)에 맞는지 본인이 직접 확인하는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어디까지 공개되었고 어디는 비공개인가&lt;/h3&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;b&gt;AI 백엔드(에이전트 추론, 모델 호출 부분)&lt;/b&gt;는 클라우드 서비스로 남겨뒀을 가능성이 크다. 즉 클라이언트는 오픈소스인데 서버는 그대로 Warp 회사가 운영하는 구조다.&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;이것은 Cursor, Continue 같은 다른 AI 코딩 도구들도 비슷한 모델이다. 클라이언트 오픈, 서버 클로즈드. 비즈니스 모델을 유지하면서 커뮤니티 신뢰를 일부 회복하는 절충안이다. 다만 &lt;b&gt;&quot;오픈소스 = 회원가입 없이 다 된다&quot;가 절대 아니다.&lt;/b&gt; AI 기능을 풀로 쓰려면 여전히 계정이 필요할 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Warp 터미널 오픈소스의 AI 에이전트 기능, 진짜 쓸만한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자연어 &amp;rarr; 명령어 변환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 자주 쓰게 되는 기능이다. 명령어를 잘 모를 때 #을 치고 한국어든 영어든 자연어로 던지면 된다. 예를 들어 &quot;지난 7일 동안 수정된 .py 파일 찾아줘&quot;를 입력하면 find . -name &quot;*.py&quot; -mtime -7이 후보로 뜬다. 실행 전에 검토할 수 있어서 안전하다.&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;ChatGPT를 띄우고 복붙하던 작업이 한 단계 줄어드는 효과다. 작은 차이 같지만 하루에 수십 번 검색하는 명령어를 한 번에 줄여주니 누적 효과가 크다. 특히 Docker, kubectl, awk, sed처럼 옵션이 많은 명령어를 다룰 때 진짜 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에이전트 모드 (Warp Agent Mode)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 ADE 컨셉의 핵심이다. 단순 명령어 추천이 아니라 &lt;b&gt;여러 단계 작업을 자율적으로 수행한다&lt;/b&gt;. &quot;이 Node.js 프로젝트 의존성을 업데이트하고 테스트가 통과하는지 확인해줘&quot; 같은 작업을 던지면 다음과 같이 동작한다.&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;npm outdated 실행해서 오래된 패키지 확인&lt;/li&gt;
&lt;li&gt;npm update 실행&lt;/li&gt;
&lt;li&gt;npm test 돌려서 결과 확인&lt;/li&gt;
&lt;li&gt;실패하면 어떤 테스트가 깨졌는지 분석&lt;/li&gt;
&lt;li&gt;가능한 수정 방안 제안&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 흐름을 사람의 개입 없이 돌릴 수 있다. 다만 실제로 써보면 100% 신뢰는 아직 어렵다. 위험한 명령어(rm -rf 같은) 실행 직전에는 확인을 받게 되어 있는데, 그 외에도 사람이 한 번씩 끼어들어야 할 때가 있다. Claude Code, Aider와 비슷한 수준의 신뢰도라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다른 AI 터미널과 비교하면 어떠한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GitHub Copilot CLI&lt;/b&gt;: GitHub 생태계에 강하게 묶여 있다. 명령어 추천 위주다. 에이전트 자동 실행은 약하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Aider&lt;/b&gt;: 터미널 기반 코딩 에이전트다. AI 페어 프로그래밍에 특화됐다. 터미널 자체를 대체하는 것은 아니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Claude Code&lt;/b&gt;: Anthropic 공식. 코드 작성/리팩토링이 강하다. 터미널 안에서 동작하지만 터미널 에뮬레이터는 아니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Warp&lt;/b&gt;: 터미널 에뮬레이터 + 에이전트 통합. 위 도구들이 &quot;터미널 위에서 돌아가는 도구&quot;라면, Warp는 &quot;터미널 자체&quot;다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포지셔닝이 다르다. Warp는 &lt;b&gt;터미널을 새로 만들면서 AI를 박아넣은 케이스&lt;/b&gt;고, 다른 도구들은 &lt;b&gt;기존 터미널에 얹어 쓰는 도구&lt;/b&gt;다. 평소 터미널 자체에 만족하던 사람은 Aider/Claude Code만으로 충분할 수 있고, 터미널 UX 자체를 바꾸고 싶은 사람은 Warp로 가는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 터미널 vs Warp 솔직 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;iTerm2 (맥 기본 대안)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iTerm2는 macOS에서 사실상 기본값처럼 쓰이는 터미널이다. 안정성, 호환성, 커스터마이징이 다 검증됐고, 무료에 오픈소스다. 단점은 UI가 2010년대 느낌 그대로라는 것이다. AI 기능은 당연히 없다.&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;iTerm2를 쓰던 사람이 Warp로 갈 때 느끼는 차이는 &quot;UI 현대화 + AI&quot; 두 가지다. 단축키 같은 것은 어차피 학습이 필요하니 큰 장벽은 아니다. 다만 iTerm2의 빵빵한 커스터마이징(트리거, 프로파일 분리 등)에 익숙한 사람은 Warp가 살짝 답답할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Alacritty / Kitty / WezTerm (Rust 계열)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쪽은 &quot;빠른 터미널&quot; 카테고리다. Alacritty는 미니멀리즘 끝판왕, Kitty는 그래픽 프로토콜까지 지원하는 풀 기능형, WezTerm은 그 중간쯤이다. 셋 다 GPU 가속 + Rust(Alacritty/WezTerm) 또는 C/Python(Kitty) 기반이다.&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;Warp와 비교하면 이쪽은 &lt;b&gt;순수 터미널 에뮬레이터에 충실한&lt;/b&gt; 진영이다. AI 기능 없음, 회원가입 없음, 텔레메트리 없음. tmux/zellij 같은 멀티플렉서와 조합해서 직접 워크플로우를 짜는 사용자에게는 Warp가 오히려 거추장스럽게 느껴질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ghostty (요즘 뜨는 신상)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ghostty는 2024년 말 공개된 신상 터미널이다. Mitchell Hashimoto(HashiCorp 창업자)가 만들었고, Zig 언어로 작성됐다. 처음부터 오픈소스(MIT)였고, 성능이 진짜 빠르다. AI 기능은 없는 대신 &quot;터미널 본질에 충실하면서 현대적 UX&quot;라는 포지션이다.&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;Warp 오픈소스 전환 이슈가 뜨면서 Ghostty와 자주 비교된다. 결론은 &lt;b&gt;목적이 다르다는 것&lt;/b&gt;이다. AI 에이전트 워크플로우를 원하면 Warp, 빠르고 깔끔한 순수 터미널을 원하면 Ghostty.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한눈에 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;터미널&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;라이선스&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;AI 기능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;회원가입&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;강점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Warp&lt;/td&gt;
&lt;td&gt;오픈소스(라이선스 확인 필수)&lt;/td&gt;
&lt;td&gt;강함 (에이전트 모드)&lt;/td&gt;
&lt;td&gt;AI 사용 시 필요&lt;/td&gt;
&lt;td&gt;AI 통합, 모던 UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iTerm2&lt;/td&gt;
&lt;td&gt;GPL&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;안정성, macOS 통합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alacritty&lt;/td&gt;
&lt;td&gt;Apache 2.0&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;Kitty&lt;/td&gt;
&lt;td&gt;GPLv3&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;WezTerm&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;크로스플랫폼, Lua 설정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ghostty&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;빠름, 모던 UX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 갈아타야 하는가? 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이런 사람은 써봐라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AI 에이전트 워크플로우를 자주 쓰는 사람&lt;/b&gt;: ChatGPT/Claude를 띄워놓고 명령어를 물어보는 패턴을 반복하는 사람에게는 게임 체인저다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀에서 명령어 워크플로우를 공유하고 싶은 사람&lt;/b&gt;: Workflows 기능이 진짜 잘 만들어졌다. README에 명령어를 박아두는 것보다 훨씬 편하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;터미널 UX의 답답함에 지친 사람&lt;/b&gt;: 멀티라인 편집, 블록 UI, 자동완성을 한번 맛보면 기존 터미널로 돌아가지 못한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클로즈드 소스라서 안 쓰던 사람&lt;/b&gt;: 이번 오픈소스 전환이 그 장벽을 어느 정도 풀어줬다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이런 사람은 그냥 기존 것을 써라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;tmux/zsh 커스터마이징 끝판왕&lt;/b&gt;: 이미 본인 셋업이 최적화되어 있다면 Warp가 오히려 답답하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안이 빡센 환경&lt;/b&gt;: 정부, 금융, 의료 쪽은 텔레메트리/AI 외부 API 호출 자체가 컴플라이언스 위반일 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;터미널은 그냥 셸이면 된다는 사람&lt;/b&gt;: AI 기능을 쓰지 않는데 굳이 무거운 것을 깔 이유가 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AGPL 같은 강한 카피레프트에 거부감이 있는 사람&lt;/b&gt;: 라이선스가 그쪽이라면(아닐 가능성이 높지만) 상업 통합 시 문제가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 한 줄 가이드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;macOS는 Homebrew로 설치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555150143&quot; class=&quot;mipsasm&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;brew install --cask warp
&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;Linux는 공식 저장소에서 .deb/.rpm을 받거나, 직접 빌드하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777555150143&quot; class=&quot;crmsh&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;git clone https://github.com/warpdotdev/warp
cd warp &amp;amp;&amp;amp; cargo build --release
&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;Windows는 공식 .msi 인스톨러를 받으면 된다. 빌드를 직접 하기 싫으면 GitHub Releases에서 바이너리를 받는 것이 제일 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 3줄 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Warp 터미널 오픈소스 전환은 진짜고, GitHub에 코드가 다 올라와 있다. AI 에이전트 기능은 강력하고 ADE 컨셉은 신선하다. 다만 라이선스 종류와 어디까지 공개됐는지(특히 AI 백엔드)는 본인이 직접 LICENSE와 README를 까보고 판단해야 한다.&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;오픈소스 전환은 일단 환영이다. 그동안 클로즈드 소스라고 거부하던 진영도 이제 코드를 직접 보고 판단할 수 있게 됐다. 다만 회원가입이나 텔레메트리 같은 운영 측면 이슈가 완전히 풀린 것은 아니라서, 갈아타기 전에 본인 환경에서 한번 테스트해보는 것이 맞다. 글을 보고 끝내지 말고 직접 GitHub 저장소에 가서 LICENSE 파일을 확인하고 빌드를 한번 해보시라. AI 에이전트 터미널 시대가 진짜 시작된 것 같다.&lt;/p&gt;</description>
      <category>IT 뉴스 이것저것</category>
      <category>iTerm2 대안</category>
      <category>Rust 터미널 에뮬레이터</category>
      <category>Warp AI 에이전트</category>
      <category>에이전틱 개발 환경</category>
      <category>터미널 추천 2026</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/398</guid>
      <comments>https://yscho03.tistory.com/398#entry398comment</comments>
      <pubDate>Thu, 30 Apr 2026 22:20:33 +0900</pubDate>
    </item>
    <item>
      <title>비관적 락 vs 낙관적 락, 언제 어느 것을 써야 하는가</title>
      <link>https://yscho03.tistory.com/397</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DB 락이 두 종류로 나뉘는 근본 이유는 설명하지 않고 코드만 던지는 글이 너무 많다. 그래서 &quot;비관적 락 낙관적 락&quot; 차이를 검색해도 결국 신입은 개념만, 주니어는 판단 기준만 따로 보다가 둘 다 어정쩡하게 알게 된다. 이 글에서는 두 락의 차이부터 시작해 실무에서 어느 도메인에 어느 락을 써야 하는지, 그리고 데드락이나 OptimisticLockException 같은 함정까지 한 번에 정리한다. JPA를 쓰든 raw SQL을 쓰든 모두 적용되도록 케이스 위주로 풀었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;876&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwKEix/dJMcagyywT9/M7qkEdZJFNteRAzl46QH0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwKEix/dJMcagyywT9/M7qkEdZJFNteRAzl46QH0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwKEix/dJMcagyywT9/M7qkEdZJFNteRAzl46QH0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwKEix%2FdJMcagyywT9%2FM7qkEdZJFNteRAzl46QH0K%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1654&quot; height=&quot;876&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;876&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://vladmihalcea.com/optimistic-vs-pessimistic-locking/&quot;&gt;vladmihalcea.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;락이 두 종류로 갈라진 근본 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동시성 문제란 무엇인가 &amp;mdash; 30초 요약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 동시성 문제는 결국 두 트랜잭션이 같은 행을 동시에 건드릴 때 발생한다. 대표적인 사례가 &lt;b&gt;Lost Update&lt;/b&gt;다. A가 잔고 1만 원에서 5천 원을 빼는 동안 B가 동시에 3천 원을 빼면 누군가의 차감이 사라진다. &lt;b&gt;Dirty Read&lt;/b&gt;는 커밋되지 않은 데이터를 읽는 것이고, &lt;b&gt;Phantom Read&lt;/b&gt;는 같은 쿼리를 두 번 날렸는데 행 개수가 달라지는 현상이다.&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;은행 잔고가 가장 직관적인 예시다. 통장에 10만 원이 있는데 ATM에서 7만 원을 출금하면서 동시에 폰뱅킹으로 5만 원을 송금한다고 해 보자. 락이 없으면 둘 다 성공해 잔고가 -2만 원이 되거나, 한쪽 차감이 그대로 증발한다. 이를 막기 위해 락이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락이 해결하려는 것은 정확히 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 트랜잭션이 같은 데이터를 동시에 건드릴 때 일관성을 유지하는 것이 락의 목적이다. 다만 트랜잭션 격리 수준(Isolation Level)만으로는 부족한 경우가 많다. READ COMMITTED 수준에서는 Lost Update가 그대로 발생하고, REPEATABLE READ로 올려도 Phantom 문제가 남는다. SERIALIZABLE까지 가면 처리량이 박살난다.&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;그래서 격리 수준은 적당히(보통 READ COMMITTED) 두고, 동시성 충돌 가능성이 있는 부분만 골라 락으로 보호하는 것이 일반적인 패턴이다. 락 전략이 비관적/낙관적 두 갈래로 나뉘는 이유도 여기서 시작된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락(Pessimistic Lock)이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄로 정리하면&lt;/h3&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;b&gt;충돌이 일어날 것이라고 미리 가정하고 데이터에 자물쇠를 채우는 방식&lt;/b&gt;이다. 누군가 먼저 잡으면 다른 트랜잭션은 락이 풀릴 때까지 기다려야 한다. SQL로는 SELECT ... FOR UPDATE가 그 자체다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280376&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 비관적 락 (배타 락)
SELECT * FROM stock WHERE product_id = 100 FOR UPDATE;

-- 비관적 공유 락
SELECT * FROM stock WHERE product_id = 100 FOR SHARE;
&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;FOR UPDATE를 잡으면 트랜잭션이 커밋되거나 롤백될 때까지 다른 트랜잭션은 그 행을 건드리지 못한다. 읽기조차 막힐 수 있다(DB마다 다르다). &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html&quot;&gt;MySQL InnoDB는 기본 row-level 락&lt;/a&gt;이라 행 단위로 잠그며, PostgreSQL도 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA에서 비관적 락 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280376&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface StockRepository extends JpaRepository&amp;lt;Stock, Long&amp;gt; {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query(&quot;select s from Stock s where s.productId = :productId&quot;)
    Optional&amp;lt;Stock&amp;gt; findByProductIdForUpdate(@Param(&quot;productId&quot;) Long productId);
}
&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;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/jpa/locking.html&quot;&gt;@Lock 어노테이션&lt;/a&gt; 하나면 끝난다. 위 메서드를 호출하면 자동으로 SELECT ... FOR UPDATE가 나간다.&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;PESSIMISTIC_READ vs PESSIMISTIC_WRITE의 차이는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PESSIMISTIC_WRITE: 배타 락. FOR UPDATE. 다른 트랜잭션은 읽기/쓰기 모두 불가(DB에 따라 읽기는 가능)&lt;/li&gt;
&lt;li&gt;PESSIMISTIC_READ: 공유 락. FOR SHARE. 여러 트랜잭션이 동시에 읽을 수는 있지만 쓰기는 모두 막힌다&lt;/li&gt;
&lt;li&gt;PESSIMISTIC_FORCE_INCREMENT: 비관적 락 + @Version 강제 증가. 자식 엔티티가 바뀔 때 부모 버전도 함께 올리고 싶을 때 사용한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비관적 락의 함정 &amp;mdash; 이를 챙기지 않으면 운영에서 문제가 발생한다&lt;/h3&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;b&gt;1. 데드락(Deadlock)&lt;/b&gt;. 가장 자주 만나는 문제다. 트랜잭션 A가 행1을 잡고 행2를 기다리는데, 트랜잭션 B가 행2를 잡고 행1을 기다리면 둘 다 영원히 대기한다. DB가 데드락을 감지해 한쪽을 죽이긴 하지만, 죽은 쪽 트랜잭션은 롤백되고 사용자는 에러를 보게 된다.&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;&lt;b&gt;2. 락 타임아웃&lt;/b&gt;. 락을 잡지 못하고 무한 대기해서는 안 되니 타임아웃을 거는데, 이 값이 짧으면 정상 처리도 죽고 길면 사용자 경험이 망가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280377&quot; class=&quot;less&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@QueryHints({
    @QueryHint(name = &quot;javax.persistence.lock.timeout&quot;, value = &quot;3000&quot;)
})
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;select s from Stock s where s.id = :id&quot;)
Stock findByIdWithLock(@Param(&quot;id&quot;) Long id);
&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;b&gt;3. 처리량 저하&lt;/b&gt;. 한 트랜잭션이 락을 잡고 있으면 그 행을 건드리려는 다른 트랜잭션은 모두 줄을 서야 한다. 트래픽이 몰리는 시점에 락 경합이 심해지면 응답 시간이 폭발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;낙관적 락(Optimistic Lock)이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄로 정리하면&lt;/h3&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;b&gt;충돌이 거의 일어나지 않을 것이라 가정하고 일단 진행한 다음, 커밋 시점에 &quot;내가 읽어 온 것과 지금의 것이 동일한가&quot;를 검증하는 방식&lt;/b&gt;이다. 락 자체를 잡지 않는다. 그래서 동시 처리량은 비관적 락보다 훨씬 좋다.&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;검증은 보통 버전 컬럼으로 한다. 트랜잭션 시작 시 version=3으로 읽어 왔다면, UPDATE를 칠 때 WHERE version = 3 조건을 박고 동시에 version을 4로 올린다. 다른 트랜잭션이 먼저 커밋해 version이 4로 올라갔다면 내 UPDATE는 0건 영향 &amp;rarr; 충돌 감지 &amp;rarr; 예외 발생이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280377&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 낙관적 락 동작 원리 (직접 짠다면 이런 식)
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 100 AND version = 3;
-- affected rows = 0 이면 충돌
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JPA에서 낙관적 락 사용법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔티티에 @Version을 박으면 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280377&quot; class=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
public class Product {

    @Id
    private Long id;

    private String name;
    private int stock;

    @Version
    private Long version;
}
&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;JPA가 알아서 매 UPDATE마다 version 검증을 박고, 맞지 않으면 OptimisticLockException을 던진다. 추가로 @Lock으로 명시할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280377&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Lock(LockModeType.OPTIMISTIC)
Optional&amp;lt;Product&amp;gt; findById(Long id);

@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
Optional&amp;lt;Product&amp;gt; findByIdWithIncrement(Long id);
&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;OPTIMISTIC_FORCE_INCREMENT는 엔티티 자체가 바뀌지 않아도 강제로 버전을 올린다. 자식이 바뀌었는데 부모 엔티티 버전도 올려야 할 때 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낙관적 락의 함정 &amp;mdash; 재시도를 구현하지 않으면 시한폭탄이다&lt;/h3&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;b&gt;1. OptimisticLockException 처리&lt;/b&gt;. 충돌이 나면 RuntimeException 계열이 터지면서 트랜잭션이 롤백된다. 사용자에게 그대로 에러를 보내면 망한다. &lt;b&gt;재시도 로직이 반드시 필요하다&lt;/b&gt;.&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;&lt;b&gt;2. 재시도 로직 필수&lt;/b&gt;. 낙관적 락은 거의 항상 재시도가 세트다. 짜지 않으면 사용자가 &quot;왜 또 실패했지&quot; 하며 새로고침을 누르는 비극이 펼쳐진다.&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;&lt;b&gt;3. 비즈니스 로직이 무거우면 재시도 비용이 크다&lt;/b&gt;. 트랜잭션 안에서 외부 API를 호출하거나 무거운 계산을 하는데 충돌로 롤백되고 재시도하면 그 비용이 모두 두 배가 된다. 외부 API는 트랜잭션 밖으로 빼는 것이 정석이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;331&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chGzko/dJMcaaynAi6/9XiKFAhGO99LGt4s4N5kk0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chGzko/dJMcaaynAi6/9XiKFAhGO99LGt4s4N5kk0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chGzko/dJMcaaynAi6/9XiKFAhGO99LGt4s4N5kk0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchGzko%2FdJMcaaynAi6%2F9XiKFAhGO99LGt4s4N5kk0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;421&quot; height=&quot;331&quot; data-origin-width=&quot;421&quot; data-origin-height=&quot;331&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.baeldung.com/cs/concurrency-control-lost-update-problem&quot;&gt;Baeldung (18KB)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;비관적 락과 낙관적 락, 언제 써야 하는가 (이것이 핵심이다)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;충돌 빈도로 판단하는 1차 기준&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;도메인&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;충돌 빈도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천 락&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이유&lt;/b&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;낙관적&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;비관적&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;비관적&lt;/td&gt;
&lt;td&gt;인기 공연은 사실상 동시 1만 명&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;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;비관적&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;낙관적&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;상황별&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;비관적 또는 별도 처리&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 원칙은 단 하나다. &lt;b&gt;&quot;같은 행을 동시에 건드릴 확률이 어느 정도냐&quot;&lt;/b&gt;이다. 사용자가 자기 것만 만지는 도메인은 무조건 낙관적, 모두가 같은 행을 노리는 도메인(재고/예매)은 무조건 비관적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 길이로 판단하는 2차 기준&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;짧은 트랜잭션(수 ms 이내)&lt;/b&gt;: 비관적도 무방하다. 락을 잡고 있는 시간이 짧아 다른 트랜잭션에 미치는 영향이 적다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;긴 트랜잭션(외부 API, 복잡한 비즈니스 로직 포함)&lt;/b&gt;: 낙관적을 권장한다. 비관적으로 락을 잡고 있으면 그 사이 들어오는 트랜잭션이 모두 줄을 서다 죽는다&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;재시도 가능 여부로 판단하는 3차 기준&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;재시도 가능&lt;/b&gt;: 단순 카운터, 좋아요, 단순 데이터 수정 &amp;rarr; 낙관적 OK&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재시도 불가&lt;/b&gt;: 사용자가 폼을 다시 채워야 하는 경우, 외부 API로 이미 결제가 완료된 상황 &amp;rarr; 비관적 권장&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실무 의사결정 플로우차트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280379&quot; class=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;충돌 빈도 높음? &amp;rarr; YES &amp;rarr; 비관적 락
                 &amp;rarr; NO &amp;rarr; 트랜잭션 김?
                          &amp;rarr; YES &amp;rarr; 낙관적 락
                          &amp;rarr; NO &amp;rarr; 재시도 가능?
                                   &amp;rarr; YES &amp;rarr; 낙관적 락
                                   &amp;rarr; NO &amp;rarr; 비관적 락
&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;이 흐름만 외워 두면 90%의 케이스는 답이 나온다. 나머지 10%는 분산 환경 같은 특수 케이스인데 뒤에서 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 발생하는 함정과 해결법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비관적 락의 데드락을 어떻게 막을 것인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데드락 100% 예방은 불가능하지만 빈도는 확연히 줄일 수 있다.&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;&lt;b&gt;1. 락 획득 순서 통일&lt;/b&gt;. 트랜잭션 A가 행1 &amp;rarr; 행2 순서, 트랜잭션 B가 행2 &amp;rarr; 행1 순서로 잡으면 데드락은 확정이다. 모든 트랜잭션이 같은 순서(예: ID 오름차순)로 락을 잡도록 코드를 짜면 데드락이 거의 사라진다.&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;&lt;b&gt;2. 타임아웃을 짧게&lt;/b&gt;. 위에 적은 javax.persistence.lock.timeout 같은 값으로 락 대기 시간을 제한해 두면 데드락을 감지하지 못한 케이스도 결국 풀린다.&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;&lt;b&gt;3. SKIP LOCKED 활용&lt;/b&gt;. 큐 형태로 작업을 분배할 때 매우 유용하다. 락이 걸린 행은 건너뛰고 다음 행을 가져온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280379&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT * FROM job_queue
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;
&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;워커 여러 개가 큐를 처리할 때 이를 쓰지 않으면 모두 첫 번째 행에서 줄을 선다. SKIP LOCKED를 박으면 워커마다 다른 작업을 잡고 병렬로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;낙관적 락 재시도 로직 작성법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이라면 @Retryable이 깔끔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280379&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Retryable(
    value = {ObjectOptimisticLockingFailureException.class},
    maxAttempts = 3,
    backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional
public void decreaseStock(Long productId, int quantity) {
    Product product = productRepository.findById(productId)
        .orElseThrow();
    product.decrease(quantity);
}

@Recover
public void recover(ObjectOptimisticLockingFailureException e,
                    Long productId, int quantity) {
    log.error(&quot;재시도가 모두 실패했다. productId={}&quot;, productId);
    throw new BusinessException(&quot;일시적 오류이다. 다시 시도해 달라&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;&lt;b&gt;백오프 전략&lt;/b&gt;. 첫 재시도는 100ms 뒤, 그다음은 200ms, 400ms&amp;hellip; 점점 늘리는 지수 백오프(exponential backoff)다. 모두 같은 시점에 재시도하면 또 충돌이 나므로 시간차를 두는 것이다.&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;&lt;b&gt;최대 재시도 횟수&lt;/b&gt;. 보통 3~5회 정도가 적정하다. 그 이상 실패한다면 진짜 시스템 문제이거나 트래픽 폭증이라 그냥 사용자에게 에러를 보내는 편이 낫다.&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;&lt;b&gt;주의: @Retryable은 메서드 단위 재시도다&lt;/b&gt;. 그 메서드가 @Transactional이면 재시도할 때마다 트랜잭션이 새로 시작된다. 트랜잭션 안에서 수동 재시도를 짜면 같은 트랜잭션 안에서 또 충돌이 나니 의미가 없다. 반드시 트랜잭션 밖에서 재시도가 도는 구조여야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB 락만으로는 부족한 분산 환경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 인스턴스가 여러 개 떠 있는 환경에서 DB 락은 한계가 명확하다. 왜일까? DB 락은 DB 단에서 동작하니 인스턴스가 몇 개든 같은 DB만 본다면 동작은 한다. 다만 캐시 무효화나 외부 API 호출 직렬화 같은 &quot;DB 행 외의 자원&quot; 동시성 제어는 DB 락으로 할 수 없다.&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;이럴 땐 Redis 분산 락(&lt;a href=&quot;https://github.com/redisson/redisson&quot;&gt;Redisson 라이브러리&lt;/a&gt;)을 쓴다. RLock을 잡고 비즈니스 로직을 돌린 뒤 풀어 주는 식이다. 자세한 내용은 분산 락을 따로 정리한 글에서 다룰 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777524280380&quot; class=&quot;cs&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;RLock lock = redissonClient.getLock(&quot;payment:&quot; + userId);
try {
    if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
        // 비즈니스 로직
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}
&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;a href=&quot;https://www.postgresql.org/docs/current/mvcc.html&quot;&gt;PostgreSQL은 MVCC&lt;/a&gt;라 일반 SELECT는 락을 잡지 않는다는 점도 자주 헷갈리는 포인트다. PG에서 동시성을 제어하려면 명시적으로 FOR UPDATE를 박거나 SERIALIZABLE 격리 수준을 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/upVXc/dJMcaaynAjc/QM2ETLHOpkcCCWFrgrmWqK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/upVXc/dJMcaaynAjc/QM2ETLHOpkcCCWFrgrmWqK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/upVXc/dJMcaaynAjc/QM2ETLHOpkcCCWFrgrmWqK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FupVXc%2FdJMcaaynAjc%2FQM2ETLHOpkcCCWFrgrmWqK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;713&quot; height=&quot;620&quot; data-origin-width=&quot;713&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://sqlskull.com/2021/06/27/lost-update-problem-in-concurrent-transations/&quot;&gt;sqlskull.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;면접 단골 질문 정리&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q. 낙관적 락과 비관적 락의 차이는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 비관적 락은 충돌을 가정하고 미리 락을 잡는 방식, 낙관적 락은 충돌이 나지 않는다고 가정하고 커밋 시점에 버전으로 검증하는 방식이다. 비관적은 SELECT FOR UPDATE, 낙관적은 @Version 컬럼 기반이다.&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;&lt;b&gt;Q. 언제 어느 것을 쓰는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 충돌 빈도가 높으면 비관적, 낮으면 낙관적이다. 트랜잭션이 길면 낙관적이 처리량 면에서 좋다. 결제처럼 재시도가 위험한 도메인은 비관적이 안전하다.&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;&lt;b&gt;Q. @Version 동작 원리는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. UPDATE할 때 WHERE version = ? 조건이 자동으로 박히고, 함께 version을 +1 한다. 다른 트랜잭션이 먼저 커밋해 버전을 올렸다면 affected rows가 0이 되고, JPA가 그것을 감지해 OptimisticLockException을 던진다.&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;&lt;b&gt;Q. OptimisticLockException을 어떻게 처리하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. 트랜잭션 밖에서 재시도 로직을 둔다. 스프링이라면 @Retryable + 지수 백오프다. 최대 3~5회 정도 시도하고, 모두 실패하면 사용자에게 에러를 보낸다.&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;&lt;b&gt;Q. PESSIMISTIC_WRITE와 PESSIMISTIC_READ의 차이는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;A. WRITE는 배타 락(FOR UPDATE)이라 다른 트랜잭션이 읽기/쓰기 모두 불가하다. READ는 공유 락(FOR SHARE)이라 다른 트랜잭션도 읽을 수는 있지만 쓰기는 막힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 3줄 요약&lt;/h2&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; &amp;mdash; 같은 행을 동시에 건드릴 확률이 높으면 비관적, 낮으면 낙관적이다. 재고/예매는 비관적, 좋아요/프로필은 낙관적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재시도 로직이 없으면 낙관적 락은 의미가 없다&lt;/b&gt; &amp;mdash; OptimisticLockException을 그대로 사용자에게 보내면 욕먹는다. @Retryable + 지수 백오프가 정석이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 락이 만능은 아니다&lt;/b&gt; &amp;mdash; 분산 환경에서는 Redis 분산 락이 필요한 케이스가 있다. DB 락만 믿고 가면 멀티 인스턴스 환경에서 구멍이 난다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락과 낙관적 락의 차이는 결국 &quot;충돌을 사전에 막을 것인가, 사후에 감지할 것인가&quot;의 트레이드오프다. 정답은 도메인이 결정한다. 위 표를 한 번 보고 자기 도메인에 매칭해 보면 답이 거의 나온다. 다음 단계로 가고 싶다면 분산 락(Redisson), MVCC 동작 원리, SKIP LOCKED를 활용한 큐 패턴 정도가 자연스러운 학습 루트다.&lt;/p&gt;</description>
      <category>개발 방법론</category>
      <category>DB 락 종류</category>
      <category>JPA 낙관적 락</category>
      <category>낙관적 락 비관적 락 차이</category>
      <category>동시성 제어 방법</category>
      <category>비관적 락 사용 시기</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/397</guid>
      <comments>https://yscho03.tistory.com/397#entry397comment</comments>
      <pubDate>Thu, 30 Apr 2026 13:45:06 +0900</pubDate>
    </item>
    <item>
      <title>RabbitMQ vs Kafka, 언제 무엇을 사용해야 하는지 정리한다</title>
      <link>https://yscho03.tistory.com/396</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ vs Kafka를 검색하면 비슷한 글이 200개쯤 쏟아지지만, 다 읽어봐도 결국 &quot;둘 다 메시징 시스템인데 다르다&quot;는 한 줄로 귀결된다. 그러나 현업에서 두 시스템을 모두 운영해본 입장에서는 그렇게 단순히 정리될 사안이 아니다. 같은 카테고리의 도구가 아닌데도 자꾸 비교 대상이 되는 이유, 그리고 진정 언제 무엇을 사용해야 하는지를 본 글에서 정리한다. 2025년 기준 최신 정보를 모두 반영했고, 옛 글에서 자주 언급되는 잘못된 통설도 함께 짚는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ vs Kafka, 두 시스템은 같은 카테고리의 도구가 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y0ZGZ/dJMcacppKWJ/XYjcBrKTbdYqybpYTIUcrK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y0ZGZ/dJMcacppKWJ/XYjcBrKTbdYqybpYTIUcrK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y0ZGZ/dJMcacppKWJ/XYjcBrKTbdYqybpYTIUcrK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy0ZGZ%2FdJMcacppKWJ%2FXYjcBrKTbdYqybpYTIUcrK%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.slideshare.net/slideshow/message-queue-architecture/98264838&quot;&gt;slideshare.net&lt;/a&gt;&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;먼저 못 박고 시작하자. RabbitMQ는 &lt;b&gt;메시지 큐(Message Queue)&lt;/b&gt;이고, Kafka는 &lt;b&gt;분산 로그(Distributed Log)&lt;/b&gt; 또는 &lt;b&gt;스트리밍 플랫폼&lt;/b&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;메시지를 &quot;전달하고 잊을&quot; 것이라면 RabbitMQ, 메시지를 &quot;쌓아두고 다시 읽을&quot; 것이라면 Kafka이다.&lt;/blockquote&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;b&gt;비동기 메시징&lt;/b&gt;이라는 큰 카테고리 안에 속하고, 사용처가 일부 겹치기 때문이다. 예컨대 &quot;주문이 들어왔을 때 알림을 발송&quot;하는 케이스는 양쪽 모두로 구현 가능하다. 그래서 입문자 입장에서는 두 시스템이 비슷해 보인다.&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;그러나 조금만 들여다보면 완전히 다르다. RabbitMQ는 라우팅 로직이 정교한 우체국과 같고, Kafka는 모든 사건을 시간 순으로 적어두는 거대한 로그북에 가깝다. 우체국에 &quot;지난주 화요일 편지를 다시 보여달라&quot;고 요청하면 불가능하다. 그러나 Kafka에서는 가능하다.&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;이 차이를 모른 채 잘못된 도구를 고르면 6개월 뒤에 갈아엎게 된다. 실제 사례가 적지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ란 무엇인가 - 핵심 구조를 빠르게 살펴본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1VZAT/dJMcacppKWL/Qu2SSQg2j989hWwrKKaxRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1VZAT/dJMcacppKWL/Qu2SSQg2j989hWwrKKaxRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1VZAT/dJMcacppKWL/Qu2SSQg2j989hWwrKKaxRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1VZAT%2FdJMcacppKWL%2FQu2SSQg2j989hWwrKKaxRk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;741&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.cloudamqp.com/blog/exchange-to-exchange-binding-in-rabbitmq.html&quot;&gt;cloudamqp.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AMQP 프로토콜과 Erlang 기반 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 &lt;b&gt;AMQP 0-9-1&lt;/b&gt;(Advanced Message Queuing Protocol) 표준을 구현한 메시지 브로커이다. 2007년에 등장했고 현재 4.x 버전이다. Erlang/OTP로 구현되어 있는데, 이 언어가 분산 시스템에 강한 특성을 지녀 메시징에 매우 잘 어울린다. 결함 격리(fault isolation)가 언어 차원에서 지원된다.&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;&lt;a href=&quot;https://www.rabbitmq.com/&quot;&gt;RabbitMQ 공식 문서&lt;/a&gt;에 따르면 전 세계 수만 개 회사에서 사용 중이며, 한국에서도 카카오, 토스, 우아한형제들 같은 곳에서 운영한다. &quot;스타트업이나 작은 서비스용&quot;이라는 오해가 있으나 사실이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;익스체인지-큐-바인딩 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ의 가장 큰 특징은 &lt;b&gt;라우팅 유연성&lt;/b&gt;이다. 메시지가 큐로 직접 들어가는 것이 아니라 &lt;b&gt;익스체인지(Exchange)&lt;/b&gt;를 거쳐 라우팅 룰에 따라 큐로 분배된다. 익스체인지는 4가지 타입을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Direct&lt;/b&gt;: 라우팅 키가 정확히 일치하는 큐로 전송한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Topic&lt;/b&gt;: 와일드카드 패턴(order.*.created)으로 매칭한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Fanout&lt;/b&gt;: 바인딩된 모든 큐로 복사한다 (브로드캐스트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Headers&lt;/b&gt;: 헤더 값으로 매칭한다 (거의 사용되지 않는다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조 덕분에 &quot;주문 생성 이벤트는 결제 큐와 알림 큐로 가고, 결제 실패는 재시도 큐로 가고&amp;hellip;&quot; 같은 복잡한 라우팅을 코드 한 줄 작성하지 않고 설정만으로 구현할 수 있다. Kafka는 이것이 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.0에서 변경된 점 - Khepri 도입과 Quorum Queue&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛 글들은 모두 Mnesia 데이터베이스를 다루지만, 분위기가 바뀌고 있다. RabbitMQ 4.0(2024년 9월 출시)부터 &lt;b&gt;Khepri&lt;/b&gt;라는 새로운 데이터스토어가 정식 지원된다. Raft 합의 알고리즘 기반이라 클러스터 분할(network partition) 상황에서 훨씬 안정적이다. 다만 4.0/4.1은 여전히 Mnesia가 기본값이며, 4.2부터 신규 배포의 디폴트가 Khepri로 바뀌었다.&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;또한 &lt;b&gt;Quorum Queue&lt;/b&gt;가 4.0부터 기본 큐 타입으로 자리 잡았다. 기존 Classic Mirrored Queue는 3.9에서 deprecated된 후 4.0에서 완전히 제거되었다. Quorum Queue는 Raft 기반 복제로 데이터 손실 위험이 거의 없다. 처리량은 Classic보다 다소 낮지만 내구성이 훨씬 뛰어나다.&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;스트림(Streams) 기능도 추가되었는데, 이는 Kafka를 모방한 것이다. 로그 기반 메시지 보관이 가능하다. 다만 Kafka급 성능은 아니므로 진정한 스트리밍 워크로드에는 그냥 Kafka를 쓰는 편이 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka란 무엇인가 - 분산 로그라는 발상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b9PHbo/dJMcacppKWK/mxRsVktXuTvTgACRycvqnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b9PHbo/dJMcacppKWK/mxRsVktXuTvTgACRycvqnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b9PHbo/dJMcacppKWK/mxRsVktXuTvTgACRycvqnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb9PHbo%2FdJMcacppKWK%2FmxRsVktXuTvTgACRycvqnK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;667&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://storage.googleapis.com/dpwrkyffymiqxe/partition-kafka-offset.html&quot;&gt;storage.googleapis.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토픽-파티션-오프셋 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 LinkedIn이 2011년에 만들어 오픈소스화한 &lt;b&gt;분산 커밋 로그&lt;/b&gt;이다. &lt;a href=&quot;https://kafka.apache.org/documentation/&quot;&gt;Apache Kafka 공식 문서&lt;/a&gt;에 따르면 발상이 RabbitMQ와 완전히 다르다. 메시지를 큐에서 빼는 것이 아니라 &lt;b&gt;로그 파일에 append만&lt;/b&gt; 한다. 한 번 기록되면 보관 기간(retention) 동안 그대로 남는다.&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;핵심 개념 3가지는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;토픽(Topic)&lt;/b&gt;: 로그 카테고리이다. RabbitMQ의 큐와 비슷한 위치를 차지한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;파티션(Partition)&lt;/b&gt;: 토픽을 여러 개로 쪼갠 단위이다. 병렬 처리의 핵심이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;오프셋(Offset)&lt;/b&gt;: 파티션 내부에서 메시지 위치를 가리키는 숫자이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머는 자신이 어디까지 읽었는지를 오프셋으로 기억한다. 그래서 &quot;어제 9시부터 다시 읽어달라&quot; 같은 요청이 가능하다. 이것이 Kafka의 진정 강력한 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Zookeeper는 더 이상 사용하지 않는다 (KRaft 모드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛 Kafka 문서를 보면 무조건 Zookeeper를 함께 설치하라고 안내한다. 그러나 이 또한 옛이야기이다. Kafka 2.8부터 &lt;b&gt;KRaft(Kafka Raft) 모드&lt;/b&gt;가 도입되었고, 3.3에서 production-ready 상태가 되었다. &lt;b&gt;Kafka 4.0(2025년 출시)부터는 Zookeeper가 완전히 제거되었다.&lt;/b&gt;&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;KRaft를 사용하면 운영 복잡도가 확연히 낮아진다. 의존성이 하나 줄어들기 때문이다. 새로 시작하는 프로젝트라면 무조건 KRaft로 가는 것이 옳다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨슈머 그룹과 리밸런싱&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 또 다른 중요한 개념이 &lt;b&gt;컨슈머 그룹&lt;/b&gt;이다. 같은 그룹에 속한 컨슈머들이 파티션을 나누어 가진다. 파티션이 4개이고 컨슈머가 2개라면 컨슈머당 2개씩 가져간다. 컨슈머를 추가하면 자동으로 재분배(리밸런싱)된다.&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;이것이 Kafka의 수평 확장 핵심이다. 처리량이 부족하면 컨슈머를 추가하기만 해도 알아서 분배된다. RabbitMQ도 같은 큐에 컨슈머 여러 개를 붙일 수는 있으나, Kafka처럼 파티션 단위로 명확하게 나뉘지는 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ Kafka 차이 핵심 8가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Vc0J8/dJMcacppKWI/keyMbYWD6a0pPjha9XTYr1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Vc0J8/dJMcacppKWI/keyMbYWD6a0pPjha9XTYr1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Vc0J8/dJMcacppKWI/keyMbYWD6a0pPjha9XTYr1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVc0J8%2FdJMcacppKWI%2FkeyMbYWD6a0pPjha9XTYr1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;450&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.linkedin.com/posts/anton-putra_kafka-vs-rabbitmq-performance-latency-activity-7254795262995415040-sh39&quot;&gt;linkedin.com&lt;/a&gt;&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;각설하고 진정한 차이점을 정리한다. 표를 먼저 박고 자세한 설명으로 들어간다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Kafka&lt;/b&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;보관 기간 동안 유지된다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리량&lt;/td&gt;
&lt;td&gt;~50K msg/sec&lt;/td&gt;
&lt;td&gt;~1M msg/sec&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;Exchange로 정교하다&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;파티션 단위&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;완만하다&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;대규모&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 보관 방식 - 큐는 소비되면 사라지고, 로그는 사라지지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 본질적인 차이이다. RabbitMQ는 컨슈머가 메시지 ack를 보내면 큐에서 삭제된다. 다시 볼 수 없다. Kafka는 보관 기간(기본 7일, 무한대로도 설정 가능) 동안 그대로 남고, 여러 컨슈머가 같은 메시지를 각자 읽을 수 있다.&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;이 때문에 이벤트 소싱(Event Sourcing)이나 CQRS 패턴은 거의 무조건 Kafka로 가야 한다. 과거 이벤트를 다시 재생할 일이 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처리량 vs 지연시간 - 누가 진정 빠른가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 흔한 오해가 있다. &quot;Kafka가 무조건 빠르다&quot;는 말은 틀렸다. &lt;b&gt;상황에 따라 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;처리량(throughput)&lt;/b&gt;: Kafka가 압도적이다. 초당 100만 건도 가능하다. RabbitMQ는 잘 튜닝해도 5만~10만 건 정도이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지연시간(latency)&lt;/b&gt;: 작은 메시지라면 RabbitMQ가 더 빠르다. 마이크로초 단위 응답이 가능하다. Kafka는 배치로 묶어서 보내는 구조라 ms 단위이다.&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://www.confluent.io/&quot;&gt;Confluent 벤치마크&lt;/a&gt;에 따르면 큰 메시지를 대량으로 처리할 때 Kafka가 RabbitMQ보다 약 15배 빠르다고 한다. 그러나 작은 메시지 1건을 빨리 보내야 하는 케이스라면 RabbitMQ가 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라우팅 유연성 - RabbitMQ가 압도적이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 Topic Exchange로 order.kr.seoul, order.us.la 같은 식으로 라우팅 키 패턴 매칭이 가능하다. Kafka는 토픽 단위로만 분리되며, 그 안에서 어디로 갈지는 컨슈머가 알아서 처리해야 한다.&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;복잡한 라우팅 룰이 필요하다면 RabbitMQ가 정답이다. 코드 양 자체가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 순서 보장 - 파티션 단위 vs 큐 단위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 큐 단위로 FIFO가 보장된다. 컨슈머가 1개라면 완벽한 순서이다. 여러 개라면 순서가 깨질 수 있다.&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;Kafka는 &lt;b&gt;파티션 단위&lt;/b&gt;로 순서가 보장된다. 같은 키를 가진 메시지는 같은 파티션으로 가므로, 그 키 단위의 순서가 보장된다. 예컨대 같은 user_id의 이벤트 순서를 보장하려면 user_id를 파티션 키로 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;장애 복구 - HA Queue vs Replication Factor&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 Quorum Queue로 노드 장애 시 자동 페일오버된다. Kafka는 토픽을 만들 때 replication.factor=3 같이 설정하면 3개 브로커에 복제된다. 한 대가 죽어도 멈추지 않는다.&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;설정을 잘못하면 양쪽 다 데이터 손실이 가능하다. 특히 Kafka에서 acks=0이나 acks=1로 두면 메시지가 사라질 수 있다. &quot;Kafka는 메시지 손실이 없다&quot;는 말은 거짓이다. 설정을 제대로 해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습 곡선 - 어느 쪽이 더 까다로운가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 입문이 매우 쉽다. Docker로 띄우고 5분이면 메시지를 보낼 수 있다. 관리 콘솔(Management UI)도 친절하다.&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;Kafka는 처음 시작하면 답답하다. 토픽을 만들고, 파티션을 정하고, 컨슈머 그룹을 설정하고, 오프셋을 관리하고&amp;hellip; 운영 노하우를 쌓으려면 최소 6개월은 잡아야 한다. 그리고 한 번 클러스터를 잘못 설계하면 나중에 갈아엎기가 매우 까다롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 비용 - CPU/메모리/디스크 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메모리&lt;/b&gt;: RabbitMQ가 메모리를 많이 쓴다. 큐가 메모리에 올라가기 때문이다. Kafka는 디스크 위주라 메모리를 덜 쓴다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;디스크&lt;/b&gt;: Kafka가 디스크를 많이 쓴다. 로그를 보관하기 때문이다. SSD를 추천한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CPU&lt;/b&gt;: 양쪽 다 비슷하다. 트래픽에 따라 다르다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규모가 작다면 RabbitMQ가 한 대로 충분하지만, Kafka는 최소 3대 클러스터가 권장된다. 초기 운영 비용은 Kafka가 더 비싸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;생태계 - 클라이언트 라이브러리, 플러그인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 거의 모든 언어에 공식/비공식 클라이언트가 있다. Kafka도 마찬가지인데, JVM 친화적이라 Java/Scala/Kotlin 쪽이 가장 잘 갖춰져 있다.&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;플러그인은 RabbitMQ가 풍부하다. Shovel, Federation, MQTT, STOMP 모두 지원한다. Kafka는 Kafka Connect, Kafka Streams, ksqlDB 같은 자체 생태계가 강력하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 상황에서 RabbitMQ가 정답인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작업 큐 (백그라운드 잡, 이메일 발송 등)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 발송, 썸네일 생성, PDF 변환 같은 백그라운드 작업은 RabbitMQ가 정답이다. 한 번 처리되면 끝이므로 로그로 보관할 필요가 없다. Celery, Sidekiq 같은 작업 큐 라이브러리도 RabbitMQ를 잘 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;복잡한 라우팅이 필요할 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트 종류별로 다른 컨슈머에게 보내야 하고, 조건부 라우팅 룰이 많고, 우선순위 큐가 필요하다면 RabbitMQ이다. Kafka로 이를 흉내 내려면 코드를 매우 많이 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RPC 패턴이나 요청-응답 워크플로우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 RPC 패턴을 기본 지원한다. reply-to 큐를 만들어서 응답을 받는 구조가 자연스럽다. Kafka로는 이 패턴이 매우 번거롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지가 즉시 처리되어야 하는 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지연시간을 ms 이하로 보장해야 한다면 RabbitMQ가 낫다. Kafka는 배치 처리 구조라 약간의 지연이 불가피하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어떤 상황에서 Kafka가 정답인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 소싱과 CQRS&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 영구 보관하고 다시 재생할 수 있어야 하는 패턴이라면 무조건 Kafka이다. 과거 이벤트를 모아서 read model을 다시 생성할 수 있어야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그 수집, 메트릭, 분석 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹서버 로그, 사용자 행동 데이터, IoT 센서 데이터 같은 것을 모을 때 Kafka가 적합하다. 초당 수십만 건이 들어와도 끄떡없고, Spark, Flink, Kafka Streams로 실시간 분석도 가능하다. 우아한형제들 기술블로그에도 이러한 사례가 많이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 컨슈머가 같은 데이터를 다시 읽어야 할 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석팀, 추천팀, 알림팀이 같은 주문 데이터를 각자 다른 방식으로 처리해야 하는 상황이라면 Kafka 컨슈머 그룹으로 깔끔하게 분리된다. 같은 메시지를 여러 그룹이 각자 읽는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;초당 10만 건 이상 처리해야 할 때&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진정 큰 트래픽이라면 RabbitMQ로는 한계이다. Kafka는 파티션을 늘려 수평 확장이 가능하다. 카카오 광고 플랫폼이나 토스 결제 이벤트 같은 곳에서 Kafka를 쓰는 이유가 여기에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ vs Kafka 비용 비교 - 매니지드 서비스 기준&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;1520&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q7VPg/dJMcagyyjwC/KoWdFrMRDmxwN4FKBUwIJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q7VPg/dJMcagyyjwC/KoWdFrMRDmxwN4FKBUwIJk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q7VPg/dJMcagyyjwC/KoWdFrMRDmxwN4FKBUwIJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ7VPg%2FdJMcagyyjwC%2FKoWdFrMRDmxwN4FKBUwIJk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;2540&quot; height=&quot;1520&quot; data-origin-width=&quot;2540&quot; data-origin-height=&quot;1520&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.vantage.sh/blog/amazon-msk-confluent-pricing-comparison&quot;&gt;vantage.sh&lt;/a&gt;&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;자체 운영을 하면 인건비가 매우 크다. 그래서 매니지드 서비스 기준으로 비교한다. 2025년 기준 가격이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS MSK vs Amazon MQ 가격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Amazon MQ for RabbitMQ&lt;/b&gt;: mq.t3.micro 기준 시간당 약 $0.04이다. 월 $30 수준이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AWS MSK&lt;/b&gt;: kafka.t3.small 기준 시간당 약 $0.05 + 스토리지이다. 월 $40~$50 + 데이터 비용이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작은 규모라면 비슷하고, 커질수록 Kafka가 더 비싸진다. 클러스터 최소 3대이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Confluent Cloud vs CloudAMQP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Confluent Cloud&lt;/b&gt;: Basic 클러스터는 베이스 요금이 없는 사용량 기반 과금이다. 첫 eCKU는 무료이고 이후 eCKU당 시간당 $0.14, 데이터 인/아웃 $0.05/GB, 스토리지 $0.08/GB이다. 트래픽이 어느 정도 있으면 한 달 수백~수천 달러가 우습게 나온다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CloudAMQP&lt;/b&gt;: Little Lemur는 무료이다. 공유형 Tough Tiger는 $19/월이다. 전용 Big Bunny는 $99/월부터이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;소규모는 CloudAMQP가 압도적으로 저렴하다. 정말로 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국 클라우드 옵션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 클라우드, KT 클라우드, NHN 클라우드 모두 자체 매니지드 메시지 큐를 보유한다. 네이버 클라우드는 &lt;b&gt;Cloud Hadoop&lt;/b&gt; 안에 Kafka 옵션이 있고, NHN 클라우드는 &lt;b&gt;CloudPub/Sub&lt;/b&gt; 같은 서비스를 제공한다.&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;다만 한국 클라우드의 매니지드 메시징은 AWS, GCP에 비해 기능이 빈약하다. 큰 회사들도 결국 AWS MSK나 Confluent Cloud를 쓰는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자체 운영 시 인건비 포함한 진정한 비용&lt;/h3&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;b&gt;운영 인력 1명의 인건비&lt;/b&gt;(연 6000만원~1억) 추가하면 매니지드가 훨씬 저렴하다. Kafka 클러스터를 운영하다 새벽에 Zookeeper가 죽어 깨어나본 사람만이 이해한다.&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;규모가 진정 큰 회사가 아니라면 매니지드를 쓰는 것이 정답이다. 시간 vs 돈의 트레이드오프이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ vs Kafka 의사결정 체크리스트 - 그래서 무엇을 선택해야 하는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의사결정 체크리스트 5문항&lt;/b&gt;&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;메시지를 다시 읽어야 하는가? &amp;rarr; YES면 Kafka, NO면 RabbitMQ&lt;/li&gt;
&lt;li&gt;초당 처리량이 5만 건을 넘는가? &amp;rarr; YES면 Kafka&lt;/li&gt;
&lt;li&gt;라우팅 룰이 복잡한가? &amp;rarr; YES면 RabbitMQ&lt;/li&gt;
&lt;li&gt;지연시간 ms 이하가 필요한가? &amp;rarr; YES면 RabbitMQ&lt;/li&gt;
&lt;li&gt;운영 인력이 충분한가? &amp;rarr; NO면 RabbitMQ (또는 매니지드)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5개 중 3개 이상이 Kafka 쪽이라면 Kafka, 그 반대라면 RabbitMQ이다.&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;또한 두 가지를 모두 사용하는 케이스도 매우 흔하다. 배민이나 토스도 Kafka로 큰 이벤트 스트림을 처리하고, RabbitMQ로 작업 큐를 돌린다. 도구 하나로 모든 것을 해결하려 하지 말고, 적재적소에 사용하는 것이 옳다.&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;마지막으로 한 번 더 강조한다. RabbitMQ vs Kafka는 &quot;어느 것이 더 좋은가&quot;가 아니라 &quot;어떤 문제를 풀 것인가&quot;의 문제이다. 망치와 드라이버를 비교하는 것과 비슷하다. 못을 박을 것이라면 망치를, 나사를 돌릴 것이라면 드라이버를 쓰는 것이지, 어느 것이 더 좋다고 단언해서는 안 된다.&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;설계 단계에서 이를 잘 정해두면 1~2년 뒤에 갈아엎는 일이 발생하지 않는다. 신중하게 선택하기 바란다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>Kafka 아키텍처</category>
      <category>RabbitMQ Kafka 차이</category>
      <category>RabbitMQ 사용 사례</category>
      <category>메시지 큐 추천</category>
      <category>카프카 래빗MQ 비교</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/396</guid>
      <comments>https://yscho03.tistory.com/396#entry396comment</comments>
      <pubDate>Thu, 30 Apr 2026 11:12:28 +0900</pubDate>
    </item>
    <item>
      <title>도커를 만든 사람을 알면 더 흥미롭다, 솔로몬 하익스 이야기</title>
      <link>https://yscho03.tistory.com/395</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxjFmc/dJMcajhIX9i/8RIOQJr3bD42DLw6YDz8PK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxjFmc/dJMcajhIX9i/8RIOQJr3bD42DLw6YDz8PK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxjFmc/dJMcajhIX9i/8RIOQJr3bD42DLw6YDz8PK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbxjFmc%2FdJMcajhIX9i%2F8RIOQJr3bD42DLw6YDz8PK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;648&quot; height=&quot;452&quot; data-origin-width=&quot;648&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://thenewstack.io/solomon-hykes-leader-open-source-world-needs/&quot;&gt;thenewstack.io&lt;/a&gt;&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;요즘 개발자 면접에 가서 &quot;도커(Docker)는 써본 적이 없다&quot;고 답하면 분위기가 어색해진다. 백엔드, 데브옵스를 가리지 않고 도커는 거의 기본 소양으로 취급되는 시대다. 다들 docker run은 매일 치면서, 정작 도커 개발자가 누구인지 아는 사람은 드물다.&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;이 점이 다소 의아하다. 리누스 토르발즈의 이름은 누구나 알면서, 도커를 만든 사람의 이름은 잘 떠올리지 못한다. 도커는 등장 1년 만에 깃허브 스타 1만 개를 찍고 인프라 시장의 표준이 된 도구임에도 그렇다.&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;면접 준비를 하다가 문득 궁금해져서 찾아봤다. &lt;b&gt;솔로몬 하익스(Solomon Hykes)&lt;/b&gt;가 누구인지, dotCloud라는 PaaS 스타트업에서 도커가 어떻게 튀어나왔는지, 2013년 PyCon에서 5분짜리 라이트닝 토크 한 방에 어떻게 업계가 뒤집어졌는지를 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도커를 만든 사람은 솔로몬 하익스(Solomon Hykes)다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프랑스 개발자가 미국에서 PaaS 스타트업을 차린 배경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔로몬 하익스는 처음부터 도커를 만들겠다고 회사를 세운 사람이 아니다. 2008년에 dotCloud라는 PaaS(Platform as a Service) 스타트업을 공동 창업한 것이 시작이다. PaaS란, 개발자가 코드만 푸시하면 알아서 서버에 배포해주는 클라우드 서비스를 말한다. 헤로쿠(Heroku)와 비슷한 포지션이라고 보면 된다.&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;2008년 당시는 헤로쿠가 막 부상하던 시기였고, AWS도 EC2 정도로만 알려져 있던 시절이다. 그 틈에 프랑스 개발자가 &quot;우리도 PaaS를 만들어보겠다&quot;고 뛰어든 것이다. dotCloud는 &lt;a href=&quot;https://www.ycombinator.com/companies/docker&quot;&gt;와이콤비네이터(Y Combinator)&lt;/a&gt;에도 들어갔고 어느 정도 투자도 받았으나, 헤로쿠나 AWS 빔스토크(Beanstalk) 같은 거인들 사이에서 차별화가 쉽지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dotCloud라는 회사 안에서 도커가 태어난 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotCloud는 내부적으로 Ruby, Python, Node.js 같은 여러 런타임을 한 플랫폼에서 돌려야 했다. 이를 한 서버에 깔끔하게 격리해서 운용하는 것이 진짜 골치 아픈 문제였다.&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;그래서 솔로몬 하익스가 사내 도구로 컨테이너(Container) 기반의 격리 시스템을 만들기 시작했다. 이것이 도커의 시작이다. 즉, 도커는 처음부터 &quot;세상을 바꿀 인프라 도구&quot;로 기획된 것이 아니라, PaaS 운영 효율을 조금 높이기 위해 만든 사이드 프로젝트였다.&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;이 디테일이 중요한 이유는, 임팩트 있는 도구들이 종종 이런 식으로 태어나기 때문이다. 깃(Git)도 리누스 토르발즈가 리눅스 커널을 관리하기 위해 만든 사이드 프로젝트였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;사실 도커는 &quot;사이드 프로젝트&quot;였다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;803&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bJWnaZ/dJMcaiC8Svj/OuXAsm8ZxTEmYidB6qyTtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bJWnaZ/dJMcaiC8Svj/OuXAsm8ZxTEmYidB6qyTtk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bJWnaZ/dJMcaiC8Svj/OuXAsm8ZxTEmYidB6qyTtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbJWnaZ%2FdJMcaiC8Svj%2FOuXAsm8ZxTEmYidB6qyTtk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1574&quot; height=&quot;803&quot; data-origin-width=&quot;1574&quot; data-origin-height=&quot;803&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://chrisna2.github.io/docker/01-Docker01/&quot;&gt;chrisna2.github.io&lt;/a&gt;&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;도커는 처음에는 &quot;도커&quot;라는 이름조차 없었다. 그저 dotCloud 인프라팀의 격리 도구일 뿐이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dotCloud 내부에서 배포 자동화를 시도하다 만든 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dotCloud는 사용자가 푸시한 앱을 격리된 환경에서 돌려야 했다. VM(가상머신)으로 격리하면 너무 무겁고, 한 OS 안에서 그냥 돌리면 의존성 충돌이 미친 듯이 발생한다. 그래서 솔로몬 하익스 팀은 리눅스 커널이 제공하는 cgroups, namespaces 같은 기능을 활용해 &quot;VM보다 가벼운 격리&quot;를 구현하기 시작했다.&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;이쯤에서 그냥 LXC(Linux Containers)를 쓰면 되지 않느냐는 의문이 들 수 있다. 다만 LXC는 당시에 진짜 거칠었다. 명령어가 복잡하고, 이미지 관리 개념도 없었으며, 개발자가 일상적으로 쓸 만한 UX가 아니었다. 솔로몬 하익스는 LXC 위에 개발자가 바로 쓸 수 있는 추상화 레이어를 얹은 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;처음에는 LXC 래퍼에 가까웠다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 초기 버전은 LXC 위에 얇은 래퍼를 씌운 수준이었다. 그럼에도 LXC와 다른 점이 두 가지 있었다.&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;첫째, &lt;b&gt;이미지(Image)&lt;/b&gt;라는 개념을 도입해 환경을 통째로 패키징하고 공유할 수 있게 만들었다. 둘째, Dockerfile이라는 선언적 빌드 문법을 만들어 &quot;이 환경을 어떻게 만드는지&quot;를 코드로 표현하게 했다. 이 두 가지가 LXC가 해내지 못한 일이다.&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;2013년 즈음에는 이 사이드 프로젝트가 dotCloud 본업보다 더 흥미로워 보이기 시작했다. 솔로몬 하익스는 도커를 오픈소스로 풀자고 결심하게 되는데, 이것이 회사의 운명을 바꾼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2013년 PyCon, 5분짜리 데모가 모든 것을 바꿨다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 역사에서 가장 결정적인 순간은 2013년 3월 PyCon US다. 솔로몬 하익스가 라이트닝 토크 세션에 올라 5분짜리 데모를 진행했다. 제목은 &quot;The future of Linux containers&quot;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;The Future of Linux Containers&quot; 라이트닝 토크&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라이트닝 토크는 보통 5분짜리 짧은 발표인데, 이 짧은 발표가 인프라 업계를 흔들었다. 솔로몬 하익스가 무대에서 한 일은 단순했다. 터미널을 열고 docker run 명령어 몇 개를 쳐서 컨테이너를 띄우고, 이미지를 공유하고, 격리된 환경에서 앱이 돌아가는 모습을 보여줬다.&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;다만 그 흐름이 너무 매끄러웠다. 그때까지 컨테이너라는 개념을 쓰려면 chroot, jail, LXC 같은 것을 직접 다뤄야 했고, 명령어가 누더기처럼 길었다. 솔로몬 하익스가 보여준 것은 &quot;명령어 한 줄로 격리된 환경을 띄우고, 그 환경을 그대로 다른 사람에게 보낼 수 있다&quot;는 사실이었다. PyCon 2013 라이트닝 토크 영상을 보면, 발표가 끝난 뒤 박수가 길게 이어지는 장면이 그대로 잡혀 있다.&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;데모 직후 도커 깃허브 저장소에는 트래픽이 폭주했다. PyCon 발표 당일에만 깃허브 스타가 수백 개 붙었고, 한 달 안에 수천 개를 찍었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;발표가 끝나자마자 깃허브 스타가 폭발한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 이렇게 폭발했는가. 솔로몬 하익스가 천재였기 때문일까? 그것도 영향은 있겠지만, 진짜 이유는 타이밍이다. 2013년은 클라우드, 마이크로서비스, 데브옵스라는 단어가 막 보편화되던 시점이다. 개발자들은 모두 같은 고통을 공유하고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;내 노트북에서는 되는데 서버에서는 안 된다&quot;&lt;/li&gt;
&lt;li&gt;&quot;스테이징 서버와 프로덕션 서버 환경이 미묘하게 다르다&quot;&lt;/li&gt;
&lt;li&gt;&quot;라이브러리 버전 충돌로 배포가 실패한다&quot;&lt;/li&gt;
&lt;li&gt;&quot;VM은 너무 무거워 마이크로서비스 수십 개를 돌리기 힘들다&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 모든 고통을 한 방에 해결할 수 있어 보이는 도구가 5분 데모로 등장했다. 깃허브 스타가 안 붙는 것이 더 이상한 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 도커가 왜 센세이션이었는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 개발자가 누구인가보다 더 중요한 것은 &quot;왜 이것이 그렇게 큰일이었나&quot;이다. 네 가지로 압축된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;내 컴퓨터에서는 된다&quot; 문제를 진짜로 해결해버렸다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발자라면 누구나 한 번쯤 들어본 그 문장, &quot;It works on my machine.&quot; 도커는 이를 진심으로 끝내버린 도구다. 이미지에 OS, 런타임, 의존성, 코드, 환경변수까지 통째로 패키징하니, 내 노트북에서 만든 이미지가 동료 노트북, 스테이징 서버, 프로덕션 서버에서 똑같이 돌아간다.&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;말은 쉬운데 그전까지는 진짜 안 되던 일이다. Vagrant 같은 도구로 비슷하게 흉내는 냈지만 무거웠고, Chef/Puppet 같은 구성 관리 도구는 환경 차이를 완전히 막지 못했다. 도커는 이를 이미지 한 덩어리로 해결했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;VM 대비 부팅은 초 단위, 디스크는 메가 단위&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VM(Virtual Machine)은 게스트 OS 전체를 띄우니 부팅에 분 단위가 걸리고, 디스크도 기가바이트 단위로 잡아먹는다. 도커 컨테이너는 호스트 OS 커널을 공유하므로 수 초 안에 뜨고, 디스크도 수십 메가바이트면 충분하다.&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;이 차이가 어떤 의미인가. 마이크로서비스 100개를 한 머신에 띄우는 것이 가능해진다는 뜻이다. VM으로는 못 했던 일이다. 클라우드 비용 구조 자체가 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile이라는 작성 경험이 너무 직관적이었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777511181019&quot; class=&quot;dockerfile&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;FROM python:3.11
COPY app.py /app/
RUN pip install flask
CMD [&quot;python&quot;, &quot;/app/app.py&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;이것이 전부다. 5줄짜리 텍스트 파일이 곧 환경 정의이자 빌드 스크립트다. 누가 봐도 무슨 뜻인지 알 수 있다. 이를 깃에 함께 커밋하면 환경이 코드로 버전 관리된다.&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;그전까지 환경 구성은 위키 문서나 README에 적어두고 신입 개발자가 며칠씩 삽질하면서 따라가는 방식이었다. Dockerfile은 그 작업을 5분짜리로 줄여버렸다. 솔로몬 하익스가 이 인터페이스를 깔끔하게 디자인한 것이 도커 성공의 절반은 차지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker Hub로 이미지 공유가 npm처럼 쉬워졌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔로몬 하익스가 잘한 또 한 가지가 Docker Hub 같은 공개 레지스트리를 빠르게 띄운 것이다. docker pull nginx를 치면 누군가 만들어둔 nginx 이미지가 그대로 내려온다. docker push를 하면 내 이미지를 공유할 수 있다.&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;이는 npm, PyPI 같은 패키지 생태계와 똑같은 효과를 냈다. 재사용 가능한 인프라 컴포넌트의 표준 유통 채널이 생긴 것이다. 한 번 이런 생태계가 자리 잡으면 후발주자가 뒤집기는 진짜 어려워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;회사 이름까지 바꾸게 만든 사이드 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUZti/dJMcaiC8Svq/H7zXmhWhl3XZLmHik8bTH0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUZti/dJMcaiC8Svq/H7zXmhWhl3XZLmHik8bTH0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUZti/dJMcaiC8Svq/H7zXmhWhl3XZLmHik8bTH0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUZti%2FdJMcaiC8Svq%2FH7zXmhWhl3XZLmHik8bTH0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;1133&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;1133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://velog.io/@chrishan/docker-summary&quot;&gt;velog.io&lt;/a&gt;&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;도커가 PyCon에서 폭발한 뒤, dotCloud는 진짜 어려운 결정을 내려야 했다. 본업인 PaaS는 헤로쿠 같은 경쟁자에게 밀리고 있었고, 사이드 프로젝트였던 도커는 매일 깃허브 스타가 수백 개씩 붙고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;dotCloud &amp;rarr; Docker, Inc.로 사명 변경&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2013년 말, dotCloud는 회사 이름을 Docker, Inc.로 바꿨다. 동시에 dotCloud의 PaaS 사업 자체를 다른 회사에 매각했다. 본업을 팔고 사이드 프로젝트에 올인한 것이다. 이런 결정은 결코 쉽지 않다.&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;스타트업 회고를 보면 보통 &quot;피벗(pivot)&quot;이라는 단어를 쓰는데, 도커의 경우는 피벗을 넘어 거의 환골탈태 수준이었다. 솔로몬 하익스가 본인의 회사를 갈아엎으면서까지 도커에 베팅한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PaaS 본업을 팔고 도커에 올인한 결정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결정이 옳았는지는 결과가 말해준다. Docker, Inc.는 그 후로 수억 달러의 투자를 받았고, 한때 기업 가치가 10억 달러를 넘었다(소위 유니콘). dotCloud로 계속 갔다면 절대 만들 수 없는 규모다.&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;다만 한편으로는 &quot;왜 도커가 진짜 큰돈은 못 벌었나&quot;라는 비판도 따라온다. 도커는 오픈소스라 회사 자체가 수익화하기는 어려웠고, 결국 엔터프라이즈 사업부는 미란티스(Mirantis)에 매각하게 된다. 이는 또 다른 이야기이긴 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;도커가 만든 도미노, 쿠버네티스와 클라우드 네이티브&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨테이너 표준화(OCI)의 시작점이 되었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커가 컨테이너 표준이 되자 다른 회사들도 자기 컨테이너 런타임을 만들기 시작했다. 이것이 호환성 문제를 일으킬까봐 2015년에 &lt;a href=&quot;https://opencontainers.org/&quot;&gt;OCI(Open Container Initiative)&lt;/a&gt;라는 표준 단체가 만들어졌다. 도커, 구글, 레드햇, IBM 등이 모두 들어갔다. 컨테이너 이미지 포맷, 런타임 스펙이 여기서 표준화된다.&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;OCI가 없었다면 컨테이너 시장은 분열됐을 것이다. 도커 개발자가 만든 도구가 산업 표준의 시발점이 된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;구글의 쿠버네티스(2014)로 이어졌다&lt;/h3&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;a href=&quot;https://kubernetes.io/&quot;&gt;쿠버네티스(Kubernetes)&lt;/a&gt;는 컨테이너 수천 개를 클러스터로 묶어 돌리는 문제를 풀었다. 구글이 2014년에 쿠버네티스를 오픈소스로 공개했는데, 이는 도커가 없었다면 나올 수가 없는 도구였다.&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;쿠버네티스는 처음에 도커 컨테이너를 직접 실행했고(이후 containerd로 분리), 도커 이미지 포맷을 그대로 썼다. 도커가 컨테이너의 알파벳을 만들었고, 쿠버네티스가 그 알파벳으로 단어를 만들었다고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;지금 우리가 쓰는 클라우드 인프라의 뼈대가 여기서 시작되었다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 클라우드에서 쓰는 매니지드 서비스 대부분이 컨테이너 기반이다. AWS ECS, AWS Fargate, Google Cloud Run, Azure Container Instances, GitHub Actions의 러너 모두 컨테이너 위에서 돈다. &lt;a href=&quot;https://www.cncf.io/&quot;&gt;CNCF(Cloud Native Computing Foundation)&lt;/a&gt;라는 재단도 도커가 만든 흐름 위에서 결성됐다.&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;요약하면 솔로몬 하익스가 2013년 PyCon에서 친 5분 데모 한 번이 10년 넘게 클라우드 인프라 산업의 방향을 바꿨다. 도커 개발자라는 타이틀이 가벼운 것이 아닌 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;솔로몬 하익스는 지금 무엇을 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;849&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwtMHq/dJMcaiC8Svk/yjjVM2tuhIPyYk8qdMuXYk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwtMHq/dJMcaiC8Svk/yjjVM2tuhIPyYk8qdMuXYk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwtMHq/dJMcaiC8Svk/yjjVM2tuhIPyYk8qdMuXYk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwtMHq%2FdJMcaiC8Svk%2FyjjVM2tuhIPyYk8qdMuXYk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;849&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;849&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.geekwire.com/2018/docker-co-founder-solomon-hykes-leaving-company-cites-need-enterprise-focused-cto/&quot;&gt;GeekWire&lt;/a&gt;&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;도커 개발자가 지금 무엇을 하고 사는지 궁금한 사람도 있을 것이다. 솔로몬 하익스는 더 이상 도커에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2018년 Docker, Inc. 공식 퇴사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2018년 3월, 솔로몬 하익스는 Docker, Inc.에서 공식적으로 떠났다. 본인 트위터에 &quot;다음 챕터로 간다&quot;는 식으로 발표했다. 도커 회사 자체는 그 후로도 계속 운영됐지만, 창업자 본인은 손을 뗀 것이다.&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;이 시점 즈음에 Docker, Inc.는 사업 모델 고민이 깊었다. 오픈소스 도커는 폭발적으로 쓰이는데 회사가 어떻게 돈을 벌지가 명확하지 않았다. 솔로몬 하익스가 그 짐까지 모두 짊어지고 가지는 않은 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Native 창업, 여전히 개발자 도구를 만드는 중&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;퇴사 후 솔로몬 하익스는 Native라는 새 스타트업을 차렸다. 여전히 개발자 도구 영역에서 일하고 있다. 도커처럼 또 한 번 업계 판도를 바꿀 도구를 만들지는 두고 봐야 알겠지만, 한 번 큰 것을 만든 사람이 그 한 번으로 끝나는 경우는 드문 편이다.&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;도커 자체는 OCI 표준으로 분리됐고, 컨테이너 런타임은 containerd, CRI-O 같은 것으로 다양해졌다. 솔로몬 하익스가 처음 만든 형태는 진화했지만, 그 출발점은 여전히 그가 PyCon 무대에서 친 그 5분짜리 데모다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 줄 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커 개발자는 프랑스 출신 솔로몬 하익스(Solomon Hykes)다. dotCloud라는 PaaS 회사의 사이드 프로젝트로 도커가 태어났다.&lt;/li&gt;
&lt;li&gt;2013년 PyCon US에서 5분짜리 라이트닝 토크로 공개됐고, 그 한 방으로 깃허브 스타가 폭발하면서 인프라 업계 표준이 됐다.&lt;/li&gt;
&lt;li&gt;도커가 OCI, 쿠버네티스, 클라우드 네이티브 생태계로 이어지는 도미노의 첫 패였다. 솔로몬 하익스는 2018년 회사를 떠났고 지금은 Native라는 새 스타트업을 운영 중이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커가 그저 운 좋게 뜬 도구라고 생각하기 쉽지만, 알고 보면 개발자가 진짜로 아파하던 문제를 정확히 짚어낸 결과물이다. 솔로몬 하익스가 잘한 것은 기술 자체보다 Dockerfile이라는 인터페이스와 Docker Hub라는 생태계를 한 번에 깔아준 점이다. 다음에 docker run을 칠 때 이 사람의 이름을 한 번쯤 떠올려봐도 좋을 듯하다.&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;다음 글에서는 컨테이너 내부 동작 원리(cgroups, namespaces, overlayfs)를 직접 손으로 돌려보는 실습을 정리해볼 생각이다. 도커가 어떻게 격리를 구현하는지 코드 레벨로 따라가면 클라우드 네이티브 전반이 훨씬 또렷하게 잡힌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://www.docker.com/&quot;&gt;Docker 공식 사이트&lt;/a&gt; &amp;mdash; 도커 회사 및 제품 정보&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Docker_(software&quot;&gt;Wikipedia: Docker (software)&lt;/a&gt;) &amp;mdash; 도커 역사 정리&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://opencontainers.org/&quot;&gt;Open Container Initiative&lt;/a&gt; &amp;mdash; 컨테이너 표준화 단체&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cncf.io/&quot;&gt;Cloud Native Computing Foundation&lt;/a&gt; &amp;mdash; 클라우드 네이티브 생태계 재단&lt;/li&gt;
&lt;li&gt;솔로몬 하익스 PyCon 2013 라이트닝 토크 영상 (YouTube에서 &quot;Solomon Hykes Docker PyCon 2013&quot; 검색)&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>DevOps</category>
      <category>dotCloud</category>
      <category>Solomon Hykes</category>
      <category>도커 만든 사람</category>
      <category>도커 역사</category>
      <category>솔로몬 하익스</category>
      <category>컨테이너 기술</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/395</guid>
      <comments>https://yscho03.tistory.com/395#entry395comment</comments>
      <pubDate>Thu, 30 Apr 2026 10:06:44 +0900</pubDate>
    </item>
    <item>
      <title>개발자가 PRD 잘 쓰는 법 정리 (작성하지 않으면 왜 실패하는가)</title>
      <link>https://yscho03.tistory.com/394</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PRD 작성 방법을 검색해 보면 대부분 PM이 PM에게 쓴 글이다. 그러나 정작 부실한 PRD 때문에 야근하는 쪽은 개발자다. 모호한 한 줄짜리 요구사항을 받고 &quot;이것이 어떻게 동작해야 하는가&quot;를 슬랙으로 50번 물어본 경험은 누구에게나 있을 것이다.&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;이 글은 개발자 입장에서 PRD(Product Requirements Document)를 어떻게 바라보아야 하는지, 직접 작성하게 되었을 때 어떻게 써야 하는지를 정리한다. 2026년 현재 Cursor, Claude Code 같은 AI 코딩 도구가 PRD를 직접 읽어서 코드를 생성하는 시대이며, PRD 품질이 곧 코드 품질로 직결되는 흐름까지 함께 다룬다. 형식적인 SW공학 교과서 톤은 배제하고 현장에서 실제로 쓸 수 있는 내용만 추렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;347&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cz1INo/dJMcacCXELQ/Cxog24ZSrkaXAf3Q3adu30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cz1INo/dJMcacCXELQ/Cxog24ZSrkaXAf3Q3adu30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cz1INo/dJMcacCXELQ/Cxog24ZSrkaXAf3Q3adu30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcz1INo%2FdJMcacCXELQ%2FCxog24ZSrkaXAf3Q3adu30%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;347&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;347&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.smartsheet.com/content/free-product-requirements-document-template&quot;&gt;www.smartsheet.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PRD란 무엇인가부터 다시 짚어 보자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRD는 Product Requirements Document의 약자다. 한국어로는 &quot;제품 요구사항 문서&quot;이다. 무엇을 만들 것인지, 왜 만드는지, 어떻게 동작해야 하는지를 한 문서에 정리한 결과물이다. 기획서와 비슷해 보이지만 다소 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PRD vs 기획서 vs 요구사항 명세서의 차이&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;문서 종류&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주 작성자&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;강조점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;분량&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**PRD**&lt;/td&gt;
&lt;td&gt;PM/PO&lt;/td&gt;
&lt;td&gt;&quot;왜&quot; + &quot;무엇을&quot;&lt;/td&gt;
&lt;td&gt;1~10페이지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**기획서**&lt;/td&gt;
&lt;td&gt;기획자&lt;/td&gt;
&lt;td&gt;UI/UX 흐름 위주&lt;/td&gt;
&lt;td&gt;보통 두꺼운 편이다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**요구사항 명세서(SRS)**&lt;/td&gt;
&lt;td&gt;SI 분석가&lt;/td&gt;
&lt;td&gt;기능 단위 형식 완결&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRD는 &quot;왜 이것을 만들어야 하는가&quot;부터 시작한다. 기획서는 화면 흐름을 그린다. 요구사항 명세서(IEEE 830 같은 것)는 기능 ID를 매기고 추적성을 따지는 데 집중한다. 모던 스타트업 및 IT 회사에서 개발자가 가장 자주 마주치는 문서가 바로 PRD다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PM이 작성하는 PRD와 개발자가 작성하는 PRD는 다소 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM이 작성하는 PRD는 &quot;왜&quot;에 무게가 실린다. 시장, 사용자, 비즈니스 임팩트가 중심이다. 반대로 개발자가 직접 작성하는 PRD는 &quot;어떻게 동작하는가&quot;에 더 깊이 들어간다. API 계약, 데이터 모델, 엣지 케이스, 에러 메시지까지 모두 들어가야 한다.&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;다만 양쪽 모두에서 빠지면 안 되는 요소가 있다. &lt;b&gt;문제 정의&lt;/b&gt;, &lt;b&gt;사용자 시나리오&lt;/b&gt;, &lt;b&gt;성공 지표&lt;/b&gt;, &lt;b&gt;Out of Scope&lt;/b&gt;가 그것이다. PM이든 개발자든 PRD에 반드시 포함되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PRD가 부실하면 개발자가 가장 큰 피해를 본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRD가 부실하면 결국 개발자가 손해를 본다. PM은 위에 보고만 잘하면 되지만 코드는 결국 개발자가 작성하기 때문이다. 모호한 PRD를 받으면 추측으로 구현하고, 추측이 틀리면 갈아엎어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모호한 PRD가 부르는 5가지 재앙&lt;/h3&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;&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;: &quot;이것이 다른 의미였는가?&quot; &amp;rarr; 코드 절반을 갈아엎는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재작업 폭탄&lt;/b&gt;: QA 단계에서 &quot;이것은 의도한 동작이 아니다&quot;가 발견된다 &amp;rarr; 다시 작성해야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엣지 케이스 누락&lt;/b&gt;: 비밀번호가 틀렸을 때 어떻게 표시할지 적혀 있지 않아 출시 직전에 발견된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팀원 간 해석 충돌&lt;/b&gt;: 백엔드는 A로, 프론트는 B로, 디자이너는 C로 받아들인다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리뷰어의 분노&lt;/b&gt;: PR을 올리니 &quot;이렇게 동작하면 안 된다&quot;는 코멘트가 달린다 &amp;rarr; PRD에는 그런 언급이 한마디도 없었다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 가지가 함께 터지면 1주일 일정이 3주가 된다. 실제로 그렇다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;이것이 어떻게 동작하는가&quot;라는 질문 무한 루프를 만드는 PRD 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부실한 PRD의 전형적인 증상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;사용자가 로그인할 수 있게 한다&quot; 같은 한 줄짜리 명세&lt;/li&gt;
&lt;li&gt;&quot;직관적이고 빠르게&quot; 같은 형용사 위주 명세&lt;/li&gt;
&lt;li&gt;입력값/출력값 예시 부재&lt;/li&gt;
&lt;li&gt;에러 케이스에 대한 언급이 한 줄도 없다&lt;/li&gt;
&lt;li&gt;&quot;필요시 추후 협의&quot; 떡밥만 잔뜩 붙어 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 슬랙 멘션이 무한 루프를 돈다. 답이 나오지 않으니 PM 본인도 헷갈려 &quot;팀장에게 물어보겠다&quot;라며 떠넘긴다. 그동안 개발자는 손을 놓고 기다려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄 PRD 때문에 3주를 잃은 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매우 흔한 사례다. &quot;결제 실패 시 사용자에게 알림을 보낸다&quot;라는 한 줄을 보고 개발자가 푸시 알림으로 구현했다. 출시 직전에 &quot;이것은 이메일이어야 하고, 카드사별로 메시지가 달라야 한다&quot;는 코멘트가 붙는다. 푸시 인프라를 모두 구축해 두었는데 이메일 큐 시스템을 새로 붙이고 카드사별 분기 처리를 추가해야 했다. 결국 3주가 더 소요되었다. PRD에 한 줄만 더 있었다면 발생하지 않았을 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;좋은 PRD가 갖춰야 할 핵심 구조&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좋은 PRD는 정해진 형식보다 &lt;b&gt;빠진 것이 없다는 점&lt;/b&gt;이 핵심이다. 아래 6개 블록이 모두 포함되어 있다면 형식은 무엇이든 상관없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 정의 (Why) - 이것이 빠지면 모든 것이 무너진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;왜 이것을 만드는가&quot;가 첫 줄에 나와야 한다. 이것이 없으면 개발자가 만들면서도 &quot;왜 만드는가&quot;라는 의문 때문에 의욕이 떨어진다. 더 나쁜 것은 PM조차 &quot;왜&quot;를 모르고 위에서 시켜서 작성하는 PRD다. 그런 PRD는 90% 갈아엎는다.&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;좋은 예: &quot;현재 결제 실패율이 8%이며 그중 60%가 카드 한도 초과다. 사용자가 다른 카드로 즉시 재시도할 수 있게 하면 결제 성공률이 5%p 상승할 것으로 추정된다.&quot;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 스토리 / 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;누가, 언제, 무엇을, 왜&quot;를 한 문장으로 표현한다. 이 한 줄의 유무가 만드는 차이가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133859&quot; class=&quot;prolog&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[사용자 스토리]
프리미엄 사용자가 결제 실패 시
다른 카드로 1탭 안에 재시도해서
이탈 없이 구독을 완료할 수 있어야 한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기능 명세 (What) - 동사 단위로 쪼개라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;~할 수 있다&quot;가 아니라 동사 단위로 쪼개야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;❌ &quot;결제 재시도 기능을 추가한다&quot; (이것이 한 줄 PRD다)&lt;/li&gt;
&lt;li&gt;✅ 다음과 같이 쪼개야 한다:&lt;/li&gt;
&lt;/ul&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;- &quot;다른 카드로 결제&quot; 버튼을 노출한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 버튼을 누르면 등록된 카드 목록 모달이 뜬다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 카드 선택 후 결제 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;h3 data-ke-size=&quot;size23&quot;&gt;Out of Scope - 만들지 않을 것을 명시하는 일이 더 중요하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이번에는 만들지 않는다&quot; 항목이 PRD에 반드시 있어야 한다. 이것이 빠지면 나중에 &quot;이것도 함께 들어가는 것으로 알고 있었다&quot;가 100% 발생한다. 만들지 않을 것을 적어 두면 분쟁이 한 번에 종결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133859&quot; class=&quot;asciidoc&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[Out of Scope]
- 신규 카드 등록 플로우 (별도 PRD에서 처리)
- 카드 한도 사전 조회 기능
- 페이팔/간편결제 재시도
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성공 지표 (Success Metrics)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들고 난 뒤 &quot;잘 되었는지&quot;를 어떻게 판단할 것인가. 숫자로 적어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결제 실패 후 재시도 성공률 30% 이상&lt;/li&gt;
&lt;li&gt;결제 실패에 따른 이탈률 4%p 감소&lt;/li&gt;
&lt;li&gt;재시도 추가에 따른 결제 평균 응답 시간 +500ms 이내&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;엣지 케이스와 예외 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 PRD 품질이 갈린다. 행복 경로(happy path)만 있는 PRD는 반드시 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 카드가 한도 초과라면? &amp;rarr; 안내 메시지 + 고객센터 링크&lt;/li&gt;
&lt;li&gt;네트워크가 끊긴다면? &amp;rarr; 재시도 버튼 + 30초 후 자동 재시도&lt;/li&gt;
&lt;li&gt;사용자가 동일 카드로 5회 이상 재시도한다면? &amp;rarr; 차단 + 다른 결제수단 안내&lt;/li&gt;
&lt;li&gt;결제 진행 중 앱이 종료된다면? &amp;rarr; 다음 진입 시 결과 확인 모달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자가 PRD를 작성할 때 자주 빠뜨리는 항목들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PM이 작성한 PRD에는 사용자 관점이 있으나 기술 디테일이 부족하다. 반대로 개발자가 작성한 PRD에는 기술이 있으나 다른 디테일이 빠진다. 개발자가 자주 누락하는 항목을 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 계약 / 데이터 스키마&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청/응답 예시 JSON을 PRD에 직접 박아 두면 협업이 매우 수월해진다. 백엔드, 프론트, QA 셋 모두가 동일한 그림을 보게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133859&quot; class=&quot;dts&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;json&quot;&gt;&lt;code&gt;POST /api/payments/retry
{
  &quot;payment_id&quot;: &quot;pay_xxx&quot;,
  &quot;card_id&quot;: &quot;card_yyy&quot;
}

200 OK
{
  &quot;status&quot;: &quot;success&quot;,
  &quot;transaction_id&quot;: &quot;tx_zzz&quot;
}

402 Payment Required
{
  &quot;status&quot;: &quot;failed&quot;,
  &quot;reason_code&quot;: &quot;LIMIT_EXCEEDED&quot;,
  &quot;reason_message&quot;: &quot;카드 한도가 초과됨&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;에러 핸들링과 사용자 메시지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러 케이스마다 어떤 메시지를 보여줄지 PRD에 적어 두어야 한다. 프론트가 임의로 만들면 톤도 어긋나고 CS 대응도 꼬인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;권한과 인증 시나리오&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비로그인 사용자가 접근하면 어떻게 되는가?&lt;/li&gt;
&lt;li&gt;권한 없는 사용자가 호출하면 어떻게 되는가?&lt;/li&gt;
&lt;li&gt;토큰 만료 중에 액션을 시도하면 어떻게 되는가?&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 / 부하 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;빠르게&quot;가 아니라 숫자로 표현한다. &quot;P95 응답 시간 300ms 이내&quot;, &quot;동시 1만 요청까지 무중단&quot; 같은 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마이그레이션과 롤백 계획&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 데이터에 영향을 주는 변경이라면 마이그레이션 스크립트 계획과 롤백 시나리오까지 PRD에 들어가야 한다. 이것을 빠뜨리면 배포 직전에 SRE에게 크게 혼난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 코딩 시대의 PRD - Cursor, Claude Code에 잘 먹히는 작성법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 진짜 중요한 파트다. 2026년 현재 PRD는 사람만 읽는 문서가 아니다. Cursor, Claude Code, GitHub Copilot Workspace 같은 AI 도구가 PRD를 직접 읽고 코드를 생성한다. 즉, PRD 품질이 곧 코드 품질이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;709&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d4dE5V/dJMcai4aqTn/EZtfGnHHdZYgg7lMKCTng1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d4dE5V/dJMcai4aqTn/EZtfGnHHdZYgg7lMKCTng1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d4dE5V/dJMcai4aqTn/EZtfGnHHdZYgg7lMKCTng1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd4dE5V%2FdJMcai4aqTn%2FEZtfGnHHdZYgg7lMKCTng1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;709&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;709&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.sentisight.ai/cursor-ai-code-editor-in-hot-demand/&quot;&gt;sentisight.ai&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI는 모호한 PRD를 받으면 마음대로 만든다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람은 모호한 PRD를 받으면 PM에게 다시 묻는다. 그러나 LLM은 묻지 않는다. 스스로 그럴듯한 내용으로 채워 넣는다. 그래서 결과물이 사양과 미세하게 다른 지점이 수십 군데 발생한다. 그것을 다시 잡아내는 일에 더 많은 시간이 든다.&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;해법은 단순하다. &lt;b&gt;AI가 추측할 여지를 0으로 만드는 것이 좋은 PRD다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 생성용 PRD에 반드시 포함해야 할 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM이 코드를 작성할 때 결정적으로 필요한 정보다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;입출력 예시&lt;/b&gt;: 함수 시그니처와 호출 예시 1~3개&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자료 구조&lt;/b&gt;: 핵심 객체의 필드와 타입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에러 케이스 분기&lt;/b&gt;: 어떤 조건에서 어떤 에러를 반환할지&lt;/li&gt;
&lt;li&gt;&lt;b&gt;테스트 케이스&lt;/b&gt;: 통과해야 할 케이스 명시&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명시적 제약&lt;/b&gt;: &quot;절대 이렇게 하지 말 것&quot; 항목&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 가지가 들어간 PRD를 LLM에 주면 결과물이 완전히 달라진다. 넣지 않고 돌리면 LLM이 스스로 패턴을 만들어 코드베이스의 일관성이 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PRFAQ 스타일이 AI에 잘 먹히는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마존이 만든 PRFAQ(Press Release + FAQ)가 최근 다시 주목받는다. 이유는 &quot;Why &amp;rarr; What &amp;rarr; How&quot; 흐름이 LLM의 추론 패턴과 잘 맞기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫 부분에 &quot;왜&quot;가 명확히 있어 LLM이 의도를 파악한다&lt;/li&gt;
&lt;li&gt;FAQ 형식이 엣지 케이스를 자연스럽게 다룬다&lt;/li&gt;
&lt;li&gt;결정 사항이 문장으로 박혀 있어 추측의 여지가 적다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRD를 짧게 작성할 것이라면 PRFAQ 포맷을 한번 시도해 볼 만하다. 마티 케이건이 정리한 PM 원칙이나 Lenny's Newsletter 같은 글로벌 자료에서도 PRFAQ 부활 흐름을 자주 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;망한 PRD vs 잘 쓴 PRD 비교 (Before/After)&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Before - &quot;로그인 기능 추가&quot; 한 줄&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133860&quot; class=&quot;prolog&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[기능] 로그인 기능을 추가한다.
&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;농담 같지만 실제로 이런 PRD를 받는다. 받은 개발자의 머릿속을 들여다보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일인가, 소셜인가, OTP인가, 매직링크인가?&lt;/li&gt;
&lt;li&gt;비밀번호 정책은 무엇인가?&lt;/li&gt;
&lt;li&gt;자동 로그인 유지 기간은 얼마인가?&lt;/li&gt;
&lt;li&gt;비밀번호가 틀렸을 때 메시지는 무엇인가?&lt;/li&gt;
&lt;li&gt;로그인 5회 실패 시 차단할 것인가?&lt;/li&gt;
&lt;li&gt;로그아웃은? 다른 기기 로그아웃은?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문이 30개가 나온다. 그 30개를 PM에게 모두 묻거나 추측으로 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;After - 동일한 기능을 30줄로 작성한 PRD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133860&quot; class=&quot;clean&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;## 로그인 기능

### Why
회원가입 후 재방문 시 사용자가 자기 데이터에 접근할 수 있어야 한다.
초기에는 이메일+비밀번호만 지원하며 소셜은 후속 PRD에서 다룬다.

### 사용자 스토리
회원가입한 사용자가 이메일/비밀번호로 로그인하여
대시보드에 진입할 수 있어야 한다.

### 기능 명세
- 로그인 화면: 이메일, 비밀번호, &quot;로그인&quot; 버튼, &quot;비밀번호 찾기&quot; 링크
- 이메일은 RFC 5322 형식으로 검증한다
- 비밀번호는 8자 이상, 대소문자+숫자를 포함한다
- 성공 시 JWT 토큰 발급 (만료 24시간), HttpOnly 쿠키에 저장한다
- 실패 시 에러 메시지: &quot;이메일 또는 비밀번호가 일치하지 않습니다&quot;
  (이메일 존재 여부 노출 금지 - 보안)
- 5회 연속 실패 시 30분간 잠금
- &quot;자동 로그인&quot; 체크박스: 30일 유지 (refresh token)

### Out of Scope
- 소셜 로그인 (별도 PRD)
- 2FA
- 비밀번호 재설정 플로우 (별도 PRD)

### 성공 지표
- 로그인 성공률 95% 이상
- 로그인 평균 응답시간 P95 200ms 이내

### 엣지 케이스
- 잠금 상태에서 시도: &quot;30분 후 다시 시도&quot; 안내
- 토큰 만료 중 API 호출: 401 + 자동 재로그인 시도
- 다른 기기 로그인 감지: 알림 메일 발송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;차이가 만드는 결과물의 차이&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Before (한 줄)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;After (30줄)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;질문 메시지 횟수&lt;/td&gt;
&lt;td&gt;30개+&lt;/td&gt;
&lt;td&gt;2~3개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;개발 완료 후 재작업&lt;/td&gt;
&lt;td&gt;거의 100%&lt;/td&gt;
&lt;td&gt;거의 0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QA 추가 발견 이슈&lt;/td&gt;
&lt;td&gt;5~10개&lt;/td&gt;
&lt;td&gt;0~1개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI 코딩 결과물 품질&lt;/td&gt;
&lt;td&gt;추측 50% 혼입&lt;/td&gt;
&lt;td&gt;95% 의도와 일치&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일정 추정 정확도&lt;/td&gt;
&lt;td&gt;50% 빗나감&lt;/td&gt;
&lt;td&gt;&amp;plusmn;10% 이내&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;바로 써먹는 PRD 템플릿과 작성 팁&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1페이지 PRD 템플릿 (작은 기능용)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133861&quot; class=&quot;vala&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;# [기능명] PRD

## Why
- 문제 1줄
- 가설 1줄

## 사용자 스토리
[누가] [언제] [무엇을] 할 수 있어야 한다.

## 기능 명세
- 동사 단위로 쪼갠 항목 5~10개

## Out of Scope
- 만들지 않을 항목 명시

## 엣지 케이스 / 에러
- 케이스별 동작 정의

## 성공 지표
- 숫자로 표현
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;풀 PRD 템플릿 (큰 기능 / 제품용)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777508133861&quot; class=&quot;vala&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;# [제품/기능명] PRD

## 1. 배경 및 문제 정의
## 2. 목표와 비목표 (Goals / Non-goals)
## 3. 사용자 페르소나와 시나리오
## 4. 기능 요구사항
   - 4.1 화면별 상세
   - 4.2 API 계약 / 데이터 스키마
   - 4.3 권한 / 인증
## 5. 엣지 케이스와 에러 처리
## 6. 성능 / 보안 / 접근성 요구사항
## 7. 마이그레이션 / 롤백 계획
## 8. Out of Scope
## 9. 성공 지표 및 측정 방법
## 10. 일정 / 마일스톤
## 11. 오픈 이슈
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PRD 작성 시간을 줄이는 5가지 팁&lt;/h3&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;PRFAQ로 시작한다&lt;/b&gt;: 1페이지 보도자료 형식으로 작성하면 핵심부터 잡힌다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;사용자 스토리를 먼저 쓴다&lt;/b&gt;: 한 문장을 만들고 거기서 기능을 뽑아내면 누락이 없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Out of Scope를 먼저 쓴다&lt;/b&gt;: &quot;만들지 않을 것&quot;부터 적으면 스코프가 자동으로 정리된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 응답 예시를 박아 넣는다&lt;/b&gt;: JSON 한 덩어리가 글 30줄보다 명확하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI에 한 번 돌려본다&lt;/b&gt;: Cursor나 Claude Code에 PRD를 던져 &quot;어떻게 구현하겠는가&quot;라고 물어보면 모호한 부분이 즉시 드러난다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PRD 리뷰 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막에 본인 PRD를 검수할 때 이 다섯 가지만 확인하면 80점은 확보된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[ ] &quot;왜&quot;에 한 줄로 답할 수 있는가?&lt;/li&gt;
&lt;li&gt;[ ] 사용자 스토리가 한 문장으로 표현되는가?&lt;/li&gt;
&lt;li&gt;[ ] 기능 명세가 동사 단위로 쪼개져 있는가?&lt;/li&gt;
&lt;li&gt;[ ] Out of Scope가 명시되어 있는가?&lt;/li&gt;
&lt;li&gt;[ ] 엣지 케이스 / 에러 케이스가 최소 5개 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PRD 작성 방법은 개발자에게 무기다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRD는 PM이 작성하는 행정 문서가 아니다. 개발자에게 PRD는 &lt;b&gt;분쟁을 막는 무기&lt;/b&gt;이며 &lt;b&gt;재작업을 0으로 만드는 보험&lt;/b&gt;이다. 좋은 PRD 한 페이지가 1주일의 야근을 막아 준다.&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;다시 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모호한 PRD는 개발자가 가장 큰 손해를 본다&lt;/li&gt;
&lt;li&gt;좋은 PRD에는 Why, 사용자 스토리, 동사 단위 명세, Out of Scope, 엣지 케이스, 성공 지표가 모두 들어간다&lt;/li&gt;
&lt;li&gt;개발자가 직접 작성할 때는 API 계약, 에러 메시지, 권한, 성능, 롤백까지 챙겨야 한다&lt;/li&gt;
&lt;li&gt;AI 코딩 시대에는 PRD가 추측의 여지를 0으로 만들어야 LLM이 제대로 코드를 작성한다&lt;/li&gt;
&lt;li&gt;PRFAQ 같은 짧은 포맷이 AI에 더 잘 먹힌다&lt;/li&gt;
&lt;li&gt;한 줄 PRD와 30줄 PRD는 결과물이 완전히 다르다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PRD 작성 방법을 익혀 두면 PM이 부실하게 던졌을 때 직접 보강할 수도 있고, 본인이 사이드 프로젝트를 할 때도 AI에 잘 먹히는 문서를 만들 수 있다. 개발자 PRD를 한번 제대로 작성해 보면 다음부터는 작성하지 않으면 불안할 정도가 된다. 오늘 하나 만들어 보기를 추천한다.&lt;/p&gt;</description>
      <category>개발 방법론</category>
      <category>PRD 예시</category>
      <category>PRD 템플릿</category>
      <category>PRD란</category>
      <category>Product Requirements Document</category>
      <category>개발자 PRD</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/394</guid>
      <comments>https://yscho03.tistory.com/394#entry394comment</comments>
      <pubDate>Thu, 30 Apr 2026 09:16:19 +0900</pubDate>
    </item>
    <item>
      <title>RocksDB란 무엇이며 왜 다들 사용하는가</title>
      <link>https://yscho03.tistory.com/393</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 내부 구조를 살펴보면 RocksDB가 등장한다. TiKV 문서를 보면 또다시 RocksDB가 나온다. CockroachDB의 옛 버전도 RocksDB를 썼고, MyRocks라는 MySQL 변종 역시 RocksDB 기반이다. 도대체 이 컴포넌트가 무엇이길래 이렇게 광범위하게 채택되는 것인가?&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;다만 한국어로 RocksDB를 제대로 정리한 글은 많지 않다. 위키백과는 너무 짧고, 개발 블로그들은 LSM-Tree만 깊게 파고들거나 코드만 던져둔다. &quot;이게 무엇이고 왜 쓰며 어디서 쓰이는지&quot;가 한 번에 잡히는 글이 부족하다고 느껴 직접 정리한다.&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;각설하고 한 줄로 요약하면, RocksDB는 데이터베이스라기보다 &quot;데이터베이스의 부품&quot;에 가깝다. 그래서 직접 사용할 일은 거의 없지만, 그럼에도 알아 두어야 백엔드 시스템 내부를 제대로 이해할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RocksDB는 도대체 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eKMLT5/dJMcaib7dhj/kkq4dV5XQjqEJ2fXQJ3V30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eKMLT5/dJMcaib7dhj/kkq4dV5XQjqEJ2fXQJ3V30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eKMLT5/dJMcaib7dhj/kkq4dV5XQjqEJ2fXQJ3V30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeKMLT5%2FdJMcaib7dhj%2Fkkq4dV5XQjqEJ2fXQJ3V30%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.logo.wine/logo/RocksDB&quot;&gt;logo.wine&lt;/a&gt;&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;한 줄로 정의하면 &lt;b&gt;페이스북(현 Meta)이 만든 임베디드 키-밸류 스토어&lt;/b&gt;다. C++로 작성되었고 Java, Python, Go, Rust 바인딩이 모두 제공된다. 라이선스는 GPLv2와 Apache 2.0 듀얼 라이선스이므로 상용 프로젝트에서도 부담 없이 도입할 수 있다.&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;핵심 키워드는 &quot;임베디드&quot;와 &quot;키-밸류&quot;이다. 이 두 단어가 RocksDB의 정체성을 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;임베디드 DB란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베디드(embedded)는 &quot;박혀 있다&quot;는 뜻이다. MySQL이나 PostgreSQL처럼 별도 서버 프로세스로 띄우는 것이 아니라, &lt;b&gt;애플리케이션 안에 라이브러리 형태로 박혀서 함께 동작하는&lt;/b&gt; DB를 말한다. 네트워크 통신도 없고 별도 데몬도 없다. 그저 프로세스 메모리 안에서 함수 호출하듯 데이터를 읽고 쓴다.&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;비슷한 사례로 SQLite가 있다. SQLite도 임베디드이지만 SQL을 지원한다. 반면 RocksDB는 SQL을 사용하지 않고 Put(key, value), Get(key) 같은 단순 KV 인터페이스만 제공한다.&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;서버형 DB(MySQL, PostgreSQL)는 별도 데몬으로 떠 있고 TCP나 Unix Socket으로 통신하므로 다중 클라이언트가 붙을 수 있는 대신 모니터링이나 백업 등 운영 부담이 크다. 반면 임베디드 DB는 라이브러리로 링크해 함수 호출로 마무리되므로 단일 프로세스에서만 사용하지만 운영 부담이 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;키-밸류 스토어란&lt;/h3&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를 던지면 value를 돌려주는 단순한 모델이다. SQL의 JOIN이나 복잡한 쿼리는 존재하지 않는다. user:123 키에 JSON 값 하나를 박아 두고 꺼내 쓰는 방식이다. 단순한 만큼 빠르고, 단순한 만큼 분산 시스템에 끼워 넣기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 만들어졌는가: 페이스북이 LevelDB를 포크한 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;625&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eJOAMV/dJMcafGrPGl/GAPUlZv8lcxE74bxksmTkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eJOAMV/dJMcafGrPGl/GAPUlZv8lcxE74bxksmTkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eJOAMV/dJMcafGrPGl/GAPUlZv8lcxE74bxksmTkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeJOAMV%2FdJMcafGrPGl%2FGAPUlZv8lcxE74bxksmTkk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;770&quot; height=&quot;625&quot; data-origin-width=&quot;770&quot; data-origin-height=&quot;625&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.influxdata.com/blog/benchmarking-leveldb-vs-rocksdb-vs-hyperleveldb-vs-lmdb-performance-for-influxdb/&quot;&gt;influxdata.com&lt;/a&gt;&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;RocksDB는 2012년 페이스북에서 출발했다. 새로 작성된 것이 아니라 &lt;b&gt;구글이 만든 LevelDB를 포크한 결과물&lt;/b&gt;이다. LevelDB는 2011년 구글의 Jeff Dean과 Sanjay Ghemawat(BigTable을 만든 그 두 사람)이 오픈소스로 공개한 임베디드 KV 라이브러리다.&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;다만 LevelDB가 페이스북 워크로드에서 한계를 드러냈다. 가장 큰 문제는 LevelDB가 HDD 시절의 디자인 그대로였다는 점이다. 2011년이면 SSD가 막 보급되던 시기였는데 LevelDB는 그 변화를 반영하지 못했다. 거기에 싱글 스레드 위주여서 멀티코어 서버에서 CPU를 놀리고 있었고, 페이스북 규모의 쓰기 부하를 받아내기에는 write throughput이 부족했다. 옵션도 빈약해 워크로드별 튜닝이 거의 불가능한 수준이었다.&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;페이스북이 이 코드를 포크해 SSD 최적화, 멀티스레드, 풍부한 튜닝 옵션을 더해 RocksDB를 만들었다. 현재는 Meta 내부에서만 수백 개 서비스가 사용 중이며, 외부에서도 사실상 임베디드 KV의 디폴트 선택지로 자리 잡았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LevelDB와 무엇이 다른가&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;LevelDB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;RocksDB&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;출시&lt;/td&gt;
&lt;td&gt;2011 (Google)&lt;/td&gt;
&lt;td&gt;2012 (Facebook)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;동시성&lt;/td&gt;
&lt;td&gt;단일 스레드 위주&lt;/td&gt;
&lt;td&gt;멀티스레드 Compaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Column Family&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;지원 (네임스페이스 분리)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bloom Filter&lt;/td&gt;
&lt;td&gt;기본만&lt;/td&gt;
&lt;td&gt;다양한 옵션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;Snappy 정도&lt;/td&gt;
&lt;td&gt;Snappy, Zlib, LZ4, ZSTD 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트랜잭션&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;Optimistic/Pessimistic 지원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backup/Checkpoint&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;적음&lt;/td&gt;
&lt;td&gt;100개 가까이&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RocksDB는 LevelDB에 SSD 시대의 옷을 입히고 멀티코어 활용을 더한 강화판이라고 보면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LSM-Tree 구조: RocksDB의 진짜 핵심&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QGTYL/dJMcafGrPGn/z6kmx4p2wnYTiHzNEKLLa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QGTYL/dJMcafGrPGn/z6kmx4p2wnYTiHzNEKLLa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QGTYL/dJMcafGrPGn/z6kmx4p2wnYTiHzNEKLLa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQGTYL%2FdJMcafGrPGn%2Fz6kmx4p2wnYTiHzNEKLLa1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;407&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;407&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.scylladb.com/glossary/sstable/&quot;&gt;scylladb.com&lt;/a&gt;&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;RocksDB가 빠른 이유는 &lt;b&gt;LSM-Tree(Log-Structured Merge-Tree)&lt;/b&gt; 구조 때문이다. 이 구조를 모르면 RocksDB를 안다고 할 수 없다. 그래서 다소 자세히 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터가 어떻게 흐르는가&lt;/h3&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(Write-Ahead Log)에 append로 기록한다. 크래시 복구용이다. 그다음 메모리의 MemTable에 정렬된 자료구조(보통 Skip List) 형태로 보관한다. MemTable이 가득 차면 Immutable MemTable로 전환해 더 이상 쓰기를 받지 않고, 백그라운드 스레드가 이를 SSTable로 디스크에 flush한다. SSTable이 여러 개 쌓이면 다시 백그라운드에서 Compaction이 동작하면서 레벨별로 병합한다.&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;읽기는 그 반대다. MemTable을 먼저 보고, 없으면 Immutable MemTable, 그래도 없으면 디스크 SSTable을 Level 0부터 차례로 뒤진다. 그래서 읽기가 느려질 수 있는데, Bloom Filter로 &quot;이 SSTable에 키가 없음&quot;을 빠르게 걸러 내고 Block Cache로 자주 사용되는 블록을 메모리에 유지하면서 어느 정도 보완한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜일까? Write가 빠른 이유&lt;/h3&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;b&gt;Sequential Write&lt;/b&gt;이다. B-Tree 기반 DB(MySQL InnoDB 등)는 데이터를 디스크의 정해진 위치에 직접 써야 한다. 그러면 디스크 헤드가 여기저기 점프해야 하는 &lt;b&gt;Random Write&lt;/b&gt;가 발생한다. HDD에서는 이것이 치명적으로 느렸고, SSD에서도 부담이다.&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;LSM-Tree는 일단 메모리에 쓰고, 디스크에는 큰 덩어리로 한 번에 sequential하게 떨어뜨린다. write-heavy 워크로드에서 차이가 크게 벌어지는 이유가 여기에 있다. RocksDB 공식 벤치마크 기준으로 random write 처리량이 LevelDB의 수 배 수준에 이른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;B-Tree(MySQL InnoDB)와 비교하면&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-Tree 계열(InnoDB, PostgreSQL)은 한 번의 lookup으로 데이터 위치를 바로 찾기 때문에 read가 빠르고, range query도 트리 구조 덕분에 강하다. 다만 write가 들어올 때마다 정해진 페이지를 갱신해야 해서 random IO가 발생하고, 페이지 단위 빈 공간 때문에 공간 효율은 떨어진다. 그래서 read-heavy OLTP에 적합하다.&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;LSM-Tree(RocksDB)는 정반대 성향이다. write는 sequential로 떨어뜨리므로 빠르지만, read는 여러 레벨의 SSTable을 뒤져야 하므로 상대적으로 느리다. range query는 약점이 더 두드러진다. 대신 정렬된 채로 압축되므로 공간 효율이 좋고, write-heavy나 시계열 데이터에 잘 맞는다. 운영 측면에서는 Compaction 튜닝이라는 골치 아픈 포인트가 추가된다.&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;쉽게 말하면 읽기 위주는 B-Tree, 쓰기 위주는 LSM-Tree다. 다만 SSD가 보편화되고 Bloom Filter나 Block Cache 같은 보조 장치가 발달하면서 LSM-Tree의 read 약점은 상당히 좁혀졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Compaction이란 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LSM-Tree에서 가장 골치 아픈 부분이자 핵심 메커니즘이다. SSTable이 레벨별로 쌓이는데, 시간이 지나면 같은 키에 대한 여러 버전이 여러 SSTable에 흩어져 있을 수 있다. 이를 백그라운드에서 병합하면서 오래된 버전을 제거하고 정리하는 작업이 Compaction이다.&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;문제는 이 작업이 공짜가 아니라는 점이다. CPU와 IO를 동시에 잡아먹고, 같은 데이터를 여러 번 다시 쓰게 된다. 이를 &lt;b&gt;Write Amplification&lt;/b&gt;이라 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Write Amplification이란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 1MB를 썼는데 Compaction 때문에 디스크에는 10MB가 쓰일 수 있다. 이 비율(10배)이 Write Amplification이다. SSD 입장에서는 수명에 영향을 주는 부분이다. 다만 SSD가 워낙 빨라서 trade-off로 받아들이는 추세이다.&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;RocksDB는 이를 줄이기 위해 Universal Compaction, Tiered Compaction 같은 여러 전략을 옵션으로 제공한다. 워크로드에 따라 골라 쓸 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;어디서 RocksDB를 사용하는가: 실제 사용 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;444&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vmRgO/dJMcaaLUe7q/IpTCwg95kUH45WyYkyskMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vmRgO/dJMcaaLUe7q/IpTCwg95kUH45WyYkyskMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vmRgO/dJMcaaLUe7q/IpTCwg95kUH45WyYkyskMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvmRgO%2FdJMcaaLUe7q%2FIpTCwg95kUH45WyYkyskMk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;975&quot; height=&quot;444&quot; data-origin-width=&quot;975&quot; data-origin-height=&quot;444&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://tikv.org/blog/rocksdb-in-tikv/&quot;&gt;tikv.org&lt;/a&gt;&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;이 부분이 진짜 중요하다. RocksDB는 그 자체로 사용되기보다 &lt;b&gt;다른 시스템의 부품으로 박혀 있는&lt;/b&gt; 경우가 압도적으로 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Apache Kafka (Kafka Streams)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka Streams의 state store 기본 구현이 RocksDB다. 스트림 처리 도중 중간 상태(예: 윈도우 집계, 조인 테이블)를 저장해야 하는데, 이를 메모리에만 두면 OOM이 나고 외부 DB에 두면 느리다. 따라서 로컬 디스크에 임베디드 KV로 박는 것이 정답이고, 그 자리를 RocksDB가 차지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TiKV / TiDB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PingCAP이 만든 분산 DB다. TiKV가 RocksDB를 노드별 스토리지 엔진으로 사용한다. 그 위에 Raft 합의 알고리즘을 얹어 분산 트랜잭션을 구현한다. 즉 RocksDB는 단일 노드 KV 역할만 하고, 분산 부분은 위 레이어에서 처리하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CockroachDB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CockroachDB도 초기에는 RocksDB를 사용했다. 그러나 2020년 무렵 자체 스토리지 엔진인 &lt;b&gt;Pebble&lt;/b&gt;(Go로 작성)로 갈아탔다. 이유는 GC 호환성, Go 생태계 일관성, Cgo 오버헤드 제거 등이었다. RocksDB가 뛰어나도 만능은 아니라는 좋은 사례이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MyRocks (MySQL)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이스북이 직접 만든 MySQL 스토리지 엔진이다. InnoDB(B-Tree) 자리를 RocksDB(LSM-Tree)로 갈아 끼운 것이다. 페이스북 내부에서 UDB(User Database)에 실제로 운영 중이다. 압축률이 InnoDB 대비 2~3배 좋아 SSD 비용을 크게 절감했다는 점이 도입 사유였다. 자세한 도입기는 &lt;a href=&quot;https://engineering.fb.com/2016/08/31/core-data/myrocks-a-space-and-write-optimized-mysql-database/&quot;&gt;Meta Engineering 블로그의 MyRocks 포스트&lt;/a&gt;에 잘 정리되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그 외&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;YugabyteDB&lt;/b&gt;: TiKV처럼 RocksDB 기반 분산 DB&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dgraph&lt;/b&gt;: 그래프 DB. 초기에 RocksDB를 썼다가 자체 Go 구현인 Badger로 완전히 이전&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Bitcoin Core&lt;/b&gt;: 블록 인덱스/UTXO 저장에 LevelDB 사용(RocksDB는 미사용. 다만 외부 BTC 인덱서들은 RocksDB 채택)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Apache Cassandra&lt;/b&gt;: Instagram이 RocksDB 백엔드(Rocksandra) 프로젝트로 실험&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Ceph&lt;/b&gt;: BlueStore 메타데이터 저장에 RocksDB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 다들 RocksDB를 채택하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 크게 네 가지로 압축된다. 첫째, 검증된 임베디드 KV가 별로 없다. 대안이 LevelDB(낡음), LMDB(read-heavy 위주), BadgerDB(Go 전용) 정도여서 선택지가 좁다. 둘째, 페이스북이 운영하면서 단련된 코드라는 점이며, 버그가 생길 만한 부분은 거의 잡혔다고 보아도 무방하다. 셋째, C++ 성능에 풍부한 튜닝 옵션이 더해져 워크로드에 맞춰 최적화가 가능하다. 마지막으로 커뮤니티와 업데이트가 활발해 GitHub 이슈/PR이 빠르게 회전한다는 점도 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RocksDB의 단점도 솔직하게&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Range query가 약하다는 점이 가장 자주 지적된다. SELECT * FROM table WHERE id BETWEEN 100 AND 200 같은 범위 쿼리는 여러 SSTable을 머지하면서 읽어야 하므로 B-Tree 대비 느리다. Bloom Filter도 point lookup에는 도움이 되지만 range scan에는 별 효과가 없다.&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;Compaction 비용도 만만치 않다. 위에서 언급한 Write Amplification 외에도, Compaction이 IO와 CPU를 백그라운드에서 잡아먹기 때문에 트래픽 피크 시간에 Compaction이 겹치면 latency spike가 발생할 수 있다. 이를 통제하는 것이 운영의 핵심이다.&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;튜닝이 복잡하다는 점도 입문 장벽이다. RocksDB의 옵션은 100개에 가깝다. write_buffer_size, max_write_buffer_number, level0_file_num_compaction_trigger, target_file_size_base 등이 끝없이 등장한다. 잘못 설정하면 성능이 급락한다. RocksDB Wiki에 튜닝 가이드가 있긴 하지만 입문자에게는 부담스럽다.&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;메모리 사용량도 무시할 수 없다. MemTable, Block Cache, Bloom Filter, Index/Filter Block 등이 모두 메모리를 잡아먹어, 임베디드라 해서 메모리를 적게 쓰는 것이 아니다. 오히려 제대로 동작시키려면 충분한 RAM이 필요하다.&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;마지막으로 트랜잭션 지원도 제한적이다. Optimistic/Pessimistic 트랜잭션은 있지만 MySQL InnoDB 같은 풀스펙 ACID는 아니다. 분산 트랜잭션은 당연히 지원하지 않는다(그것은 위 레이어에서 처리해야 한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 RocksDB를 직접 사용해야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;1260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/purei/dJMcagehJjk/Ovpx5I9axBYQXj5PmLhbe1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/purei/dJMcagehJjk/Ovpx5I9axBYQXj5PmLhbe1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/purei/dJMcagehJjk/Ovpx5I9axBYQXj5PmLhbe1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpurei%2FdJMcagehJjk%2FOvpx5I9axBYQXj5PmLhbe1%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;2240&quot; height=&quot;1260&quot; data-origin-width=&quot;2240&quot; data-origin-height=&quot;1260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://themythicalengineer.com/list-of-most-used-key-value-stores-and-databases.html&quot;&gt;themythicalengineer.com&lt;/a&gt;&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;대부분의 개발자는 RocksDB를 사용하는 시스템(Kafka, TiKV, MyRocks 등)을 사용하는 입장이지, 직접 RocksDB API를 호출할 일은 드물다. 직접 손댈 일이 생기는 케이스는 보통 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;로컬 캐시&lt;/b&gt;: Redis보다 가볍게, 디스크 기반 KV 캐시가 필요할 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;임베디드 시스템&lt;/b&gt;: 모바일/IoT에서 작은 KV가 필요할 때&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로그/메트릭 인덱스&lt;/b&gt;: write-heavy인 시계열 데이터 저장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;새로운 DB/시스템 제작&lt;/b&gt;: 스토리지 엔진으로 박아 넣기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대안도 존재한다. Read-heavy 워크로드라면 메모리 매핑 기반의 LMDB가 더 빠를 수 있고, Go 생태계라면 CockroachDB가 만든 RocksDB 호환 엔진 Pebble이 합리적이다. 같은 Go에서 키/값 분리 저장 모델이 필요하다면 BadgerDB가 있고, SQL이 반드시 필요하다면 SQLite가 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽었다면 RocksDB가 무엇인지 대략 감이 잡혔을 것이다. 한 줄씩 박아 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RocksDB는 페이스북이 2012년에 LevelDB를 포크해 만든 임베디드 키-밸류 스토어이다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;C++로 작성되었고 SSD 최적화, 멀티스레드, 풍부한 튜닝 옵션이 강점이다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LSM-Tree 구조라 write가 빠르고 압축률이 좋다. 다만 read와 range query는 상대적으로 약하다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka, TiKV, MyRocks, YugabyteDB 등 수많은 시스템이 부품으로 채택한다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;단점은 Compaction 비용, Write Amplification, 튜닝 복잡성, 메모리 사용량이다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 메시지를 한 줄로 다시 박으면, RocksDB는 데이터베이스라기보다 데이터베이스의 부품이다. 그래서 직접 사용할 일은 적지만, 백엔드 시스템 내부 구조를 이해하려면 반드시 알아야 하는 컴포넌트이다.&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;내부 동작을 더 깊이 파보고 싶다면 &lt;a href=&quot;http://localhost:20000/project/0bedd0f2-3f5a-4bd7-897d-760ced9826bc#&quot;&gt;LSM-Tree 구조 완전 정복&lt;/a&gt; 글이나 &lt;a href=&quot;http://localhost:20000/project/0bedd0f2-3f5a-4bd7-897d-760ced9826bc#&quot;&gt;B-Tree vs LSM-Tree 어느 것이 더 빠른가&lt;/a&gt; 글을 따라 읽어 보길 권한다. 실전 사례가 궁금하다면 &lt;a href=&quot;http://localhost:20000/project/0bedd0f2-3f5a-4bd7-897d-760ced9826bc#&quot;&gt;Kafka Streams가 RocksDB를 사용하는 이유&lt;/a&gt; 글도 추천한다. 공식 자료는 &lt;a href=&quot;https://rocksdb.org/&quot;&gt;rocksdb.org&lt;/a&gt;와 &lt;a href=&quot;https://github.com/facebook/rocksdb&quot;&gt;GitHub facebook/rocksdb&lt;/a&gt;에 모두 정리되어 있다.&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;운영하다 보면 결국 마주치는 것은 Compaction 튜닝과 Write Amplification 모니터링이다. 거기서부터 RocksDB는 진짜로 시작된다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>leveldb</category>
      <category>LSM-Tree</category>
      <category>RocksDB vs LevelDB</category>
      <category>임베디드 DB</category>
      <category>키-밸류 스토어</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/393</guid>
      <comments>https://yscho03.tistory.com/393#entry393comment</comments>
      <pubDate>Thu, 30 Apr 2026 09:09:20 +0900</pubDate>
    </item>
    <item>
      <title>gRPC vs REST, 내부 통신에는 왜 모두 gRPC를 쓰는가</title>
      <link>https://yscho03.tistory.com/392</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스를 도입한 회사에 들어가보면 흥미로운 패턴이 있다. 외부 클라이언트에는 모두 REST API로 응답하면서, 내부 서비스끼리는 모두 gRPC로 통신한다. 토스도 그렇고 우아한형제들도 그렇고 넷플릭스도 그렇다. 왜 이렇게 안과 밖을 갈라놓는 것인가?&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;REST를 잘 쓰고 있었는데 왜 굳이 gRPC를 또 배워야 하는지 궁금했다. 그래서 gRPC vs REST 비교는 결국 어디서 갈라지는지, gRPC가 왜 만들어졌고 HTTP와 무엇이 다른지, 어디에 써야 하고 어디에 쓰지 말아야 하는지 직접 파봤다. 결론부터 말하면 &quot;둘 중 무엇이 더 좋다&quot;가 아니라 &lt;b&gt;&quot;각자 잘하는 영역이 다르다&quot;&lt;/b&gt; 이며, 그 차이를 만드는 것은 결국 HTTP/2와 Protocol Buffer라는 두 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFjOMe/dJMcajhHTMC/Gj1sFvUM2Ttb08KW4z7sqK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFjOMe/dJMcajhHTMC/Gj1sFvUM2Ttb08KW4z7sqK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFjOMe/dJMcajhHTMC/Gj1sFvUM2Ttb08KW4z7sqK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFjOMe%2FdJMcajhHTMC%2FGj1sFvUM2Ttb08KW4z7sqK%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;960&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.slideshare.net/slideshow/grpc-76880771/76880771&quot;&gt;slideshare.net&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RPC란 무엇인가, 네트워크 너머로 이루어지는 함수 호출이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC를 이해하려면 먼저 RPC(Remote Procedure Call)가 무엇인지부터 살펴봐야 한다. 이름 그대로 &quot;원격 프로시저 호출&quot;이다. 풀어 말하면 &lt;b&gt;&quot;내 코드에서 함수를 부르듯 다른 서버 함수도 부르고 싶다&quot;&lt;/b&gt; 는 욕망에서 나온 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로컬 함수 호출과 같은 인터페이스로 원격 호출하는 것이 RPC다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 getUser(id) 같은 함수를 부르면 같은 프로세스 안에서 결과를 받아온다. 그런데 그 함수가 다른 서버에 있다면? REST 스타일이라면 fetch(&quot;&lt;a href=&quot;https://api.example.com/users/123&quot;&gt;https://api.example.com/users/123&lt;/a&gt;&quot;) 식으로 URL을 만들고 HTTP 메서드를 정하고 JSON을 파싱해야 한다. 사람이 매번 신경 써야 할 것이 너무 많다.&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;RPC는 이를 추상화하여 그저 userService.getUser(123) 처럼 부르면 알아서 네트워크 너머로 넘어가 결과를 받아오는 모델이다. 클라이언트가 보기에는 단순한 함수 호출이고 네트워크는 보이지 않는 척한다. 이것이 RPC의 핵심 철학이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;gRPC는 구글이 만든 RPC 프레임워크다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC 개념 자체는 1980년대부터 있었다. CORBA, SOAP, Java RMI, Apache Thrift(페이스북 출신) 같은 것이 모두 RPC 계보다. 다만 다들 너무 무겁거나(SOAP은 XML 지옥), 특정 언어에 묶여 있거나(RMI는 자바 전용), 표준화가 약했다.&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;gRPC는 2015년 구글이 &lt;a href=&quot;https://grpc.io/about/&quot;&gt;오픈소스로 공개했다&lt;/a&gt;. &quot;g&quot;가 무엇의 약자인지는 매 릴리스마다 바뀌는데(green, glorious, good 등) 사실상 &quot;google&quot;이라 봐도 무방하다. 구글이 사내에서 쓰던 Stubby라는 RPC 프레임워크를 외부에 다듬어 낸 것이다. 핵심은 &lt;b&gt;HTTP/2 위에서 Protocol Buffer로 직렬화하는 RPC&lt;/b&gt; 라는 점이다. 이 두 가지가 gRPC vs REST 차이의 거의 전부다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;HTTP REST는 무엇이 부족했기에 gRPC가 등장했는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST가 잘 굴러가는데 왜 또 새로 만들었는가. REST가 못하는 것이 있었기 때문이다. REST API의 단점 몇 가지를 짚고 가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;REST는 사람이 읽으라고 만든 것이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST의 기본 페이로드는 JSON이다. JSON은 사람이 읽기 좋게 만들어진 텍스트 포맷이다. {&quot;user_id&quot;: 123, &quot;name&quot;: &quot;홍길동&quot;} 식으로 키 이름이 모두 박혀 있고 들여쓰기까지 한다. 사람에게는 친절하지만 기계 입장에서는 낭비가 심하다.&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;같은 데이터를 Protocol Buffer로 보내면 키 이름이 숫자 태그(1, 2)로 압축되고, 정수는 가변 길이 인코딩으로 들어간다. 결과적으로 같은 정보가 &lt;b&gt;약 3~10배 작은 바이트&lt;/b&gt; 로 줄어든다. 트래픽이 많은 서비스에서는 이것이 그대로 비용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JSON 직렬화 비용은 생각보다 크다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON은 텍스트 파싱이 필요하다. 문자열을 읽고, 따옴표를 찾고, 콜론을 찾고, 타입을 추론한다. 한 번이라면 무시할 수 있지만 초당 수만 건을 처리하는 백엔드에서는 CPU 점유율로 잡힌다. Protocol Buffer는 바이너리 스트림을 스키마대로 그저 읽으면 된다. 파싱이 아니라 디코딩에 가깝다. 직렬화/역직렬화 속도가 보통 &lt;b&gt;2~5배 빠른 것으로 측정된다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP/1.1의 헤드 오브 라인 블로킹 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 보통 HTTP/1.1 위에서 도는데, HTTP/1.1은 하나의 TCP 커넥션에서 한 번에 한 요청만 처리한다. 앞 요청이 끝나지 않으면 뒤 요청은 줄을 서야 한다. 이를 &quot;헤드 오브 라인 블로킹(Head-of-Line Blocking)&quot;이라 부른다. 우회하려고 브라우저는 도메인당 6개씩 커넥션을 열고, 백엔드는 커넥션 풀을 키우는 등 별의별 짓을 다 하지만 근본적인 해결은 아니다.&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;여기에 매 요청마다 헤더가 같은 내용을 또 실어 보낸다. Authorization: Bearer ..., User-Agent: ..., Accept: ... 같은 것이 매 요청마다 평문으로 다시 가는 것이다. 작은 API 호출인데 헤더가 페이로드보다 큰 경우도 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmh6Zz/dJMcagL7gSL/3Y3RxfdJlfqIBijsDV9Pb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmh6Zz/dJMcagL7gSL/3Y3RxfdJlfqIBijsDV9Pb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmh6Zz/dJMcagL7gSL/3Y3RxfdJlfqIBijsDV9Pb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbmh6Zz%2FdJMcagL7gSL%2F3Y3RxfdJlfqIBijsDV9Pb0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;419&quot; height=&quot;416&quot; data-origin-width=&quot;419&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://stackoverflow.com/questions/36517829/what-does-multiplexing-mean-in-http-2&quot;&gt;Stack Overflow&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gRPC가 빠른 진짜 이유는 HTTP/2와 Protocol Buffers다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 gRPC와 REST 차이의 핵심으로 들어간다. gRPC는 위에서 말한 REST의 약점 두 가지를 정확히 정조준하여 풀어버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HTTP/2 멀티플렉싱이 RTT를 어떻게 줄이는가&lt;/h3&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;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7540&quot;&gt;HTTP/2 표준(RFC 7540)&lt;/a&gt;은 한 TCP 커넥션 안에 여러 &quot;스트림&quot;을 동시에 흘려보낼 수 있게 한다. 요청 A, B, C가 동시에 출발하고 응답도 도착하는 대로 받는다. 줄을 서지 않아도 된다. 헤드 오브 라인 블로킹이 TCP 레이어에는 남아 있지만 HTTP 레이어에서는 사라진다.&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;거기에 &lt;b&gt;HPACK 헤더 압축&lt;/b&gt; 이 붙는다. 첫 요청에서 본 헤더는 양쪽 모두 테이블에 저장하고, 다음 요청부터는 인덱스 번호만 보낸다. Authorization: Bearer eyJhbGc... 같은 길쭉한 토큰이 두 번째 요청부터는 62 같은 숫자 한 개로 줄어든다. 마이크로서비스끼리 초당 수천 번 호출할 때 이 절감 효과가 진짜 크다.&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;또 하나는 &lt;b&gt;양방향 스트리밍(Bidirectional Streaming)&lt;/b&gt; 이다. 클라이언트와 서버가 동시에 메시지를 주고받을 수 있다. 채팅 서비스, 실시간 알림, 대용량 파일 업로드 같은 시나리오를 REST는 어색하게 풀어야 하는데 gRPC는 그저 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Protocol Buffer는 바이너리이자 스키마 기반이라 작고 빠르다&lt;/h3&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;a href=&quot;https://protobuf.dev/&quot;&gt;Protocol Buffer&lt;/a&gt;(줄여서 protobuf)는 구글이 만든 직렬화 포맷이다. .proto 파일에 스키마를 정의하면 컴파일러(protoc)가 각 언어용 코드를 자동 생성해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777386200434&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;proto&quot;&gt;&lt;code&gt;message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}
&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;이 스키마로 User{id: 123, name: &quot;홍길동&quot;} 을 직렬화하면 키 이름은 사라지고 태그 번호(1, 2)와 값만 바이너리로 남는다. 같은 데이터의 JSON 페이로드 대비 보통 &lt;b&gt;30~70% 작아진다&lt;/b&gt;. 거기에 정수 가변 길이 인코딩, 패킹 최적화 같은 것이 들어가면 더 줄어들기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 제너레이션으로 타입 안정성을 확보한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마가 있으므로 클라이언트와 서버 양쪽 모두 같은 타입을 공유한다. REST에서 흔히 보이는 &quot;API 응답 필드 이름이 바뀌었는데 클라이언트는 모르고 NPE가 터지는&quot; 사고가 거의 발생하지 않는다. 컴파일 타임에 막힌다. 자바, 고, 파이썬, 코틀린, 러스트, C++을 모두 지원하므로 &lt;b&gt;언어가 다양한 마이크로서비스 환경에서 진짜 강력하다&lt;/b&gt;.&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;여기에 인터셉터(Interceptor) 개념이 있어서 인증&amp;middot;로깅&amp;middot;재시도&amp;middot;메트릭 같은 공통 로직을 한 군데에 박아놓고 모든 RPC 호출에 적용 가능하다. REST에서는 미들웨어로 비슷하게 처리하기는 하지만 클라이언트와 서버 쪽의 통일된 추상화가 없어서 매번 다시 짜야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwayhy/dJMcajhHTMG/hnC2K8aXTObKPtKVsPZGr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwayhy/dJMcajhHTMG/hnC2K8aXTObKPtKVsPZGr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwayhy/dJMcajhHTMG/hnC2K8aXTObKPtKVsPZGr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcwayhy%2FdJMcajhHTMG%2FhnC2K8aXTObKPtKVsPZGr1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1500&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.youtube.com/playlist?list=PLmIhXONtG4WvKcGB8Rg8Yrz9BF_rHYCoD&quot;&gt;YouTube&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 왜 외부 API에는 gRPC를 쓰지 않는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터가 진짜 중요한 부분이다. gRPC가 그토록 좋다면 왜 다들 외부 API는 여전히 REST로 쓰는 것인가. gRPC 장단점 중 단점이 명확하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;브라우저는 HTTP/2 트레일러를 읽지 못한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gRPC는 응답의 메타데이터 일부(특히 상태 코드)를 HTTP/2 trailer 헤더에 실어 보낸다. 그런데 브라우저의 fetch API는 trailer를 읽지 못한다. 그래서 브라우저에서 gRPC를 직접 호출할 수 없다. &lt;b&gt;gRPC-Web&lt;/b&gt; 이라는 우회 프로토콜이 나오기는 했지만 양방향 스트리밍이 빠지고 프록시 한 단계가 더 필요하다. 외부 클라이언트가 브라우저라면 gRPC는 그저 손해다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;디버깅이 어렵다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 curl &lt;a href=&quot;https://api.example.com/users/123&quot;&gt;https://api.example.com/users/123&lt;/a&gt; 한 줄로 테스트된다. Postman, 브라우저 개발자 도구도 모두 가능하다. 페이로드는 JSON이라 사람이 그대로 읽는다.&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;gRPC는 바이너리라 페이로드를 사람이 읽지 못한다. 디버깅하려면 grpcurl 같은 전용 도구가 필요하고, 그것도 서버가 reflection을 활성화해두지 않았다면 .proto 파일을 직접 들고 다녀야 한다. 외부 개발자가 우리 API에 붙이려고 할 때 진입장벽이 너무 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;클라이언트가 스키마(.proto)를 알아야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REST는 OpenAPI 스펙이 없어도 일단 호출은 된다. 응답을 받아서 dict를 까보면 끝이다. gRPC는 스키마가 없으면 호출 자체가 되지 않는다. 외부 파트너에게 .proto 파일을 보내고, 컴파일하여 쓰라고 하는 것은 보통 부담스러운 일이 아니다. &lt;b&gt;외부 API의 본질은 자유도&lt;/b&gt; 인데 gRPC는 그 자유도를 줄여서 성능을 사는 거래다.&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;그래서 결론은 명확하다. &lt;b&gt;외부에 노출되는 API는 클라이언트 환경을 통제하지 못하므로 REST(혹은 GraphQL)로 가는 것이 맞고, 내부 마이크로서비스끼리는 환경을 통제할 수 있으므로 gRPC가 압도적으로 유리하다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 언제 gRPC를 쓰면 되는가, 의사결정 가이드&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마이크로서비스 간 내부 통신&lt;/td&gt;
&lt;td&gt;gRPC&lt;/td&gt;
&lt;td&gt;페이로드가 작고, 타입이 안전하고, 스트리밍이 가능하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모바일 앱 &amp;rarr; 백엔드&lt;/td&gt;
&lt;td&gt;트래픽 많으면 gRPC&lt;/td&gt;
&lt;td&gt;데이터 사용량&amp;middot;배터리 절감, 그 외에는 REST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;브라우저 &amp;rarr; 백엔드&lt;/td&gt;
&lt;td&gt;REST 또는 GraphQL&lt;/td&gt;
&lt;td&gt;gRPC-Web 한계, 디버깅 편의성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;공개 API, 외부 통합&lt;/td&gt;
&lt;td&gt;REST&lt;/td&gt;
&lt;td&gt;진입장벽이 낮고 도구가 풍부하다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실시간 채팅&amp;middot;푸시&lt;/td&gt;
&lt;td&gt;gRPC 양방향 스트리밍 또는 WebSocket&lt;/td&gt;
&lt;td&gt;REST는 polling을 해야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단순 CRUD 백오피스&lt;/td&gt;
&lt;td&gt;REST&lt;/td&gt;
&lt;td&gt;gRPC 도입 비용이 효익을 따라가지 못한다&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마이크로서비스 간 내부 통신은 거의 무조건 gRPC다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 서비스끼리는 환경을 모두 통제 가능하다. 모든 서비스가 .proto를 공유하고, 같은 사내 도구로 모니터링하고, 디버깅도 통합 트레이싱으로 한다. 이 환경에서는 gRPC의 단점(브라우저 호환성, 디버깅 도구)은 거의 의미가 없고 장점(성능, 타입 안정성, 스트리밍)만 남는다. 우아한형제들이 배달 시스템 내부 통신에 gRPC를 쓴다고 공개한 적이 있고, 토스도 코어 서비스 내부 통신을 gRPC로 통일했다고 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트래픽이 적으면 쓰지 않아도 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 솔직히 트래픽이 1초에 수십 건도 안 되는 작은 서비스라면 그저 REST를 써도 된다. gRPC를 도입하면 빌드 파이프라인에 protoc 추가, 스키마 버저닝 정책 수립, 디버깅 도구 셋업, 팀원 온보딩 비용까지 모두 들어간다. 성능 차이가 비즈니스에 보이지 않는 수준이라면 단순함이 이긴다. &lt;b&gt;&quot;구글이 만든 것이니 좋겠지&quot;라는 이유로 도입하면 절반은 후회한다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;팀이 작고 언어가 단일하면 효익이 줄어든다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바만 쓰는 10명 팀이라면 그저 자바 RPC 라이브러리나 REST를 써도 충분하다. gRPC의 진가는 &lt;b&gt;언어가 다양할수록&lt;/b&gt;, &lt;b&gt;서비스가 많을수록&lt;/b&gt; 커진다. 자바&amp;middot;고&amp;middot;파이썬&amp;middot;코틀린이 섞여 있고 서비스가 30개를 넘으면 그때부터는 gRPC를 쓰지 않는 것이 손해다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;gRPC vs REST 선택, 누가 클라이언트인지가 모든 것을 결정한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 gRPC vs REST 논쟁은 결국 &lt;b&gt;&quot;누가 클라이언트인가&quot;&lt;/b&gt; 의 문제다. 클라이언트가 우리 통제 안에 있다면(=내부 서비스) gRPC가 거의 무조건 좋다. 클라이언트가 우리 통제 밖에 있다면(=외부 개발자, 브라우저) REST가 안전하다.&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;핵심만 추려보면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;gRPC가 빠른 이유는 마법이 아니다&lt;/b&gt;: HTTP/2 멀티플렉싱&amp;middot;HPACK 헤더 압축과 Protocol Buffer 바이너리 직렬화 두 가지의 합작품이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REST가 느린 이유도 명확하다&lt;/b&gt;: JSON 텍스트 파싱 비용과 HTTP/1.1 헤드 오브 라인 블로킹, 매 요청 헤더 중복&lt;/li&gt;
&lt;li&gt;&lt;b&gt;gRPC를 외부에 쓰지 않는 것은 단점 때문이다&lt;/b&gt;: 브라우저 비호환, 디버깅 어려움, 스키마 공유 부담&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의사결정 기준&lt;/b&gt;: 내부 마이크로서비스 통신이면 gRPC, 외부 공개 API면 REST, 둘 다 필요하면 둘 다 쓴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 도입을 검토 중이라면 처음부터 모든 통신을 gRPC로 통일하려 하지 말고, 내부 서비스 간 통신부터 gRPC로 점진적으로 옮기는 것이 안전하다. 외부 API는 REST로 유지하고 그 앞단에 BFF(Backend for Frontend) 같은 레이어를 두어 내부에서는 gRPC로 호출하는 패턴이 요즘 표준이다. &lt;b&gt;결국 gRPC vs REST 선택은 누가 클라이언트인지의 문제이며, 그래서 회사들은 둘 다 쓰는 것이다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>Backend</category>
      <category>gRPC 장단점</category>
      <category>HTTP/2</category>
      <category>Protocol Buffers</category>
      <category>rest api 단점</category>
      <category>마이크로서비스 통신</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/392</guid>
      <comments>https://yscho03.tistory.com/392#entry392comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:24:03 +0900</pubDate>
    </item>
    <item>
      <title>Vitess가 갑자기 부상하는 이유</title>
      <link>https://yscho03.tistory.com/391</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PlanetScale 무료티어가 사라진 이후 개발자 커뮤니티에서 &quot;Vitess를 직접 띄우자&quot;는 글이 슬슬 보이기 시작했다. 다만 Vitess가 무엇이냐고 물으면 대부분 &quot;MySQL 샤딩 도구?&quot; 정도로만 알고 있는 듯하다. 사실 Vitess는 단순한 샤딩 도구가 아니라, 유튜브가 14년째 운영하고 있는 &lt;b&gt;MySQL 클러스터 매니저&lt;/b&gt;이다. 슬랙, Square, GitHub, HubSpot 같은 곳들이 핵심 인프라에서 사용하고 있으며, CNCF에서 2019년에 졸업한 8번째 프로젝트이기도 하다.&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;이번 글에서는 Vitess가 정확히 무엇인지, 어떤 회사들이 실제 프로덕션에서 사용하고 있는지, 왜 2024~2025년에 갑자기 다시 화제가 되었는지를 한 번에 정리한다. 마지막에는 &quot;그래서 우리 회사도 도입해야 하는가?&quot;에 대한 솔직한 답까지 적어두었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vitess란 무엇인가, 한 줄로 정리하면 MySQL 클러스터 매니저&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjGChE/dJMcabxebay/uLzKkRsekbVWSVnMdbznD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjGChE/dJMcabxebay/uLzKkRsekbVWSVnMdbznD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjGChE/dJMcabxebay/uLzKkRsekbVWSVnMdbznD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjGChE%2FdJMcabxebay%2FuLzKkRsekbVWSVnMdbznD0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;380&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://vitess.io/docs/24.0/user-guides/configuration-basic/vtgate/&quot;&gt;vitess.io&lt;/a&gt;&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;Vitess는 한마디로 &lt;b&gt;MySQL을 수평확장 가능한 분산 데이터베이스로 바꿔주는 시스템&lt;/b&gt;이다. MySQL 위에 얇은 레이어를 깔아 여러 대의 MySQL 인스턴스를 마치 한 덩어리처럼 다룰 수 있게 해준다.&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;핵심 컴포넌트는 네 가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;VTGate&lt;/b&gt;: 쿼리 라우터. 애플리케이션이 MySQL 프로토콜로 여기에 접속한다. 어느 샤드에 쿼리를 보낼지 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VTTablet&lt;/b&gt;: 각 MySQL 인스턴스 앞에 붙는 프록시. 쿼리 보호, 풀링, 백업 등을 담당한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VTctld&lt;/b&gt;: 컨트롤 플레인. 샤드 분할, 마이그레이션 같은 운영 작업을 처리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Topology Service&lt;/b&gt;: etcd, Consul, ZooKeeper 중 하나를 사용해 클러스터 메타데이터를 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 입장에서는 그저 MySQL에 접속하는 것처럼 보인다. 드라이버를 바꿀 필요도, ORM을 교체할 필요도 없다. 이것이 Vitess의 가장 큰 무기이다. CockroachDB나 TiDB 같은 NewSQL은 결국 새로운 DB로 갈아타야 하지만, Vitess는 기존 MySQL 생태계를 통째로 살려준다.&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;CNCF 졸업 프로젝트라는 사실은 단순한 인증 마크가 아니라, 운영 안정성, 거버넌스, 보안, 커뮤니티 활성도가 모두 검증되었다는 의미이다. 쿠버네티스, 프로메테우스, 헬름과 동일한 등급에 속한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;유튜브가 만들고 14년째 운영 중인 프로젝트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfdzBy/dJMcabxebaw/M1rdFv9GgCPihKUumoggb0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfdzBy/dJMcabxebaw/M1rdFv9GgCPihKUumoggb0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfdzBy/dJMcabxebaw/M1rdFv9GgCPihKUumoggb0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfdzBy%2FdJMcabxebaw%2FM1rdFv9GgCPihKUumoggb0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.youtube.com/watch?v=zDAYZU4A3w0&quot;&gt;YouTube &amp;gt; Google Cloud Tech&lt;/a&gt;&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;Vitess의 시작은 2010년이다. 유튜브가 폭발적으로 성장하면서 MySQL 단일 인스턴스로는 도저히 버틸 수 없는 상황이 왔다. 당시 선택지는 두 가지였다. 하나는 NoSQL로 갈아타는 것이고, 다른 하나는 MySQL을 직접 샤딩하는 것이었다.&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;유튜브 팀은 후자를 선택했다. 이유는 단순했다. 이미 트랜잭션, 정합성, SQL 쿼리에 의존하는 코드가 산더미였고, NoSQL로 전환하면 그것을 모두 다시 작성해야 했다. 그렇게 만들어진 것이 Vitess의 전신이다.&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;타임라인을 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2010년&lt;/b&gt;: 유튜브 내부 프로젝트로 시작&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2012년&lt;/b&gt;: 오픈소스로 공개&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2018년 2월&lt;/b&gt;: CNCF 인큐베이션 진입&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2019년 11월&lt;/b&gt;: CNCF 졸업 (8번째 졸업 프로젝트)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2024년&lt;/b&gt;: v20.x 안정화, MySQL 8.0 호환성 강화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;창시자 Sugu Sougoumarane는 PlanetScale을 공동 창업하면서 Vitess 상용화를 본격적으로 추진하기 시작했다. 구글이 자사 인프라를 오픈소스로 공개한 이유는 명확하다. 클라우드 네이티브 시대에 자사 표준을 깔아두면 인재 풀이 넓어지고 생태계가 커지므로 결국 구글에 이익이 된다. 쿠버네티스와 동일한 전략이다.&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;흥미로운 점은 유튜브가 여전히 Vitess를 핵심 인프라에서 운영하고 있다는 사실이다. 수천 개의 샤드, exabyte급 데이터를 처리하는 인프라가 14년째 동일한 시스템 위에서 돌아가고 있다. 이것이야말로 진정한 검증이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실제 도입 기업들, Vitess 성공 사례&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kMvSF/dJMcadogBrr/WvpS0fmE0od8xciwj5sVHk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kMvSF/dJMcadogBrr/WvpS0fmE0od8xciwj5sVHk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kMvSF/dJMcadogBrr/WvpS0fmE0od8xciwj5sVHk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkMvSF%2FdJMcadogBrr%2FWvpS0fmE0od8xciwj5sVHk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://ecosystem.hubspot.com/marketplace/listing/slack&quot;&gt;ecosystem.hubspot.com&lt;/a&gt;&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;이론보다 실제로 누가 사용하는지를 보면 그 기술이 진짜인지 아닌지 답이 나온다. Vitess 도입 기업 리스트는 GitHub 리포지토리의 ADOPTERS.md에 정리되어 있는데, 이름을 보면 다소 놀라게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;슬랙, 수백 TB MySQL을 무중단으로 이전한 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;슬랙은 2017년부터 Vitess 도입을 시작해 2020년에 99% 마이그레이션을 완료했다. 현재 슬랙은 &lt;b&gt;3000개가 넘는 Vitess 샤드, 1PB가 넘는 데이터, 1조+ row&lt;/b&gt;를 운영하고 있다. 이는 슬랙 메시지 데이터베이스 전체를 거의 무중단으로 이전했다는 의미이다.&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;슬랙 엔지니어링 블로그에는 &quot;Scaling Datastores at Slack with Vitess&quot;라는 글이 게시되어 있는데, 마이그레이션을 실시간 트래픽 분산으로 처리한 과정이 상세히 기술되어 있다. VReplication 기능을 사용해 기존 MySQL에서 Vitess 클러스터로 데이터를 흘려보내고, 점진적으로 트래픽을 이전해갔다. 다운타임 없이 이를 해낸 것이야말로 Vitess의 진가를 보여준 사례이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Square (Block), Cash App 인프라에 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Square(현 Block)는 2017년 11월 Cash App에 Vitess의 첫 샤드 분할을 적용했고, 그 이후 Cash App의 핵심 데이터스토어로 사용하고 있다. P2P 결제는 절대 다운타임이 발생해서는 안 되고 데이터 정합성도 100% 보장되어야 하는 영역인데, 거기에 Vitess를 도입했다는 사실은 신뢰도가 충분히 검증되었음을 의미한다. Square 엔지니어링 팀은 Vitess 컨트리뷰터로 활동하면서 Online DDL, 스키마 마이그레이션 같은 기능 개선에 직접 기여했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HubSpot, 1000+ MySQL 클러스터 통합 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HubSpot은 2017년부터 Vitess 얼리 어답터로 도입을 시작했다. 이 사례가 다소 특이한데, &lt;b&gt;각 환경에서 운영 중인 1000개가 넘는 MySQL 클러스터를 통합&lt;/b&gt;하는 것이 목적이었다. 기존에는 클러스터마다 별도의 운영팀이 필요했고, 백업, 모니터링, 장애 대응이 모두 분산되어 있어 운영 비용이 어마어마했다. Vitess로 묶은 결과 운영이 표준화되고 인프라 비용도 절감되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub, Etsy, Pinterest 등의 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GitHub&lt;/b&gt;: 메타데이터 일부에 Vitess를 사용. 2018년부터 적용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Etsy&lt;/b&gt;: 검색 카탈로그 인프라에 Vitess를 적용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Pinterest&lt;/b&gt;: 일부 서비스 데이터에 Vitess를 도입.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JD.com&lt;/b&gt;: 중국 이커머스 거대 기업, 대규모 트랜잭션 처리에 사용.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Stitch Labs, Nozzle 등&lt;/b&gt;: 중소 SaaS들도 다수 도입.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;PlanetScale, Vitess로 창업한 사례&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PlanetScale은 다소 특별한 위치에 있다. Vitess 창시자가 만든 회사이며, Vitess를 매니지드 서비스로 판매하는 것이 사업 모델이다. 2018년에 창업해 개발자 커뮤니티에서 폭발적으로 화제가 되었다. 무엇보다 &quot;MySQL이지만 분산 가능&quot;이라는 포지션이 통했다.&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;다만 PlanetScale이 2024년 초에 무료티어를 종료하면서 사용자들이 대거 이탈했고, 그 사용자들이 &quot;직접 Vitess를 띄우자&quot;는 방향으로 움직이면서 오히려 Vitess 본체에 대한 관심이 더 커졌다. 아이러니한 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2024~2025년에 Vitess가 다시 부상하는 이유는 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;409&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/y1hgt/dJMcaiwlxeW/6WoPVM9XMeWkRowe9yKVIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/y1hgt/dJMcaiwlxeW/6WoPVM9XMeWkRowe9yKVIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/y1hgt/dJMcaiwlxeW/6WoPVM9XMeWkRowe9yKVIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fy1hgt%2FdJMcaiwlxeW%2F6WoPVM9XMeWkRowe9yKVIK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;409&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;409&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://dzone.com/articles/let-the-oracle-database-operator-for-kubernetes-do&quot;&gt;dzone.com&lt;/a&gt;&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;Vitess는 사실 14년 된 기술인데, 왜 갑자기 지금 다시 화제인가? 네 가지 이유가 겹쳤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. PlanetScale 무료티어 종료 사건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 초에 PlanetScale이 무료티어를 종료하면서 개발자 커뮤니티가 다소 시끄러워졌다. 그동안 PlanetScale의 마케팅 효과로 Vitess 인지도가 올라간 상태였는데, 무료티어가 사라지자 &quot;그렇다면 직접 Vitess를 깔자&quot;는 흐름이 생긴 것이다. Hacker News, 긱뉴스에 관련 글이 자주 올라왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. AI/SaaS 붐으로 인한 데이터 폭증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2023~2024년 AI SaaS 스타트업들이 우후죽순 등장하면서 멀티테넌시와 스케일을 동시에 해결해야 하는 상황이 많아졌다. 테넌트별로 데이터를 격리하면서 전체 인프라는 효율적으로 운영해야 한다. Vitess의 샤딩 모델은 이 케이스에 거의 정확히 부합한다. 테넌트 ID로 샤딩 키를 잡으면 자동으로 분산되고, 큰 테넌트는 별도 샤드로 분리할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Aurora, RDS 한계에 부딪힌 기업들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Aurora가 단일 라이터 한계로 인해 버티지 못하는 케이스가 늘어났다. Aurora도 훌륭한 DB이지만 결국 single primary 구조이기 때문에 쓰기 처리량에 천장이 존재한다. 그 한계에 부딪힌 회사들이 대안을 찾다가 Vitess를 만나는 흐름이 형성되었다. 비용 측면에서도 Aurora는 데이터가 커질수록 비싸지는 구조이므로, 큰 테이블을 가진 곳은 Vitess가 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 쿠버네티스 환경에서의 stateful 표준화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠버네티스는 stateless 워크로드는 잘 처리해왔지만 stateful은 오랫동안 약점이었다. 2023년부터 Vitess Operator가 GA 수준으로 안정화되면서, 쿠버네티스 위에서 데이터베이스를 운영하는 표준이 점점 자리잡고 있다. 컨테이너 환경에서 MySQL 클러스터를 운영하려면 Vitess가 거의 유일한 현실적 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vitess vs TiDB vs Aurora, 분산 DB 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbWVgX/dJMcaiwlxeX/77nG0qxt1ILkYnsKzIV9xK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbWVgX/dJMcaiwlxeX/77nG0qxt1ILkYnsKzIV9xK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbWVgX/dJMcaiwlxeX/77nG0qxt1ILkYnsKzIV9xK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbWVgX%2FdJMcaiwlxeX%2F77nG0qxt1ILkYnsKzIV9xK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;600&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.mcobject.com/distributed-database/&quot;&gt;mcobject.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vs TiDB / CockroachDB, MySQL/PostgreSQL 호환 깊이의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TiDB와 CockroachDB는 처음부터 분산을 가정하고 만든 NewSQL이다. SQL 호환성을 제공하지만 결국 별개의 DB이다. 기존 MySQL 코드, ORM, 마이그레이션 도구를 모두 검증해야 한다. 일부 기능은 동작하지 않기도 한다.&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;Vitess는 &quot;진짜 MySQL&quot;이다. 인스턴스 자체가 MySQL이며, Vitess는 그 위의 라우팅/관리 레이어이다. 따라서 ORM 호환성이 압도적으로 우수하고, 운영 노하우도 그대로 사용할 수 있다. 단점은 본격적인 분산 트랜잭션이 약하다는 점이다. cross-shard 트랜잭션은 사실상 불가능하다고 봐도 무방하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vs Aurora, 멀티 라이터와 비용 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aurora는 매니지드 서비스이므로 운영이 편하다. 다만 단일 라이터 구조여서 쓰기 처리량에 천장이 있고, 데이터가 커지면 비용이 폭증한다. Vitess는 운영 부담이 크지만 멀티 라이터 구조라 쓰기 스케일이 자유롭다. 자체 호스팅하면 비용도 훨씬 저렴하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;vs MongoDB / NoSQL, 트랜잭션 보존 여부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL은 스케일이 용이하지만 트랜잭션, JOIN, 정합성을 모두 양보해야 한다. Vitess는 &quot;MySQL을 그대로 사용하면서 스케일까지 확보하는&quot; 포지션이므로 트랜잭션, 외래키 일부, ACID가 모두 살아있다. 결제, 주문 같은 정합성이 중요한 도메인에서 NoSQL을 사용할 수 없는 곳은 Vitess가 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 우리도 도입해야 하는가, 솔직한 답변&lt;/h2&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;b&gt;대부분의 한국 스타트업은 도입하지 않아도 된다&lt;/b&gt;. 다만 어떤 케이스에서는 정확한 답이 되기도 한다. 케이스별로 나누어 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도입해야 하는 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 MySQL 인스턴스로는 도저히 버틸 수 없는 데이터량 (수십 TB 이상)&lt;/li&gt;
&lt;li&gt;멀티테넌트 SaaS이면서 테넌트별 격리가 필요한 경우&lt;/li&gt;
&lt;li&gt;Aurora, RDS 비용이 월 수천만 원을 넘어가서 자체 호스팅으로 전환해야 하는 상황&lt;/li&gt;
&lt;li&gt;이미 MySQL 생태계에 깊이 묶여 있어 NewSQL로 갈아타기가 부담스러운 경우&lt;/li&gt;
&lt;li&gt;쿠버네티스 환경에서 DB까지 통합 운영하려는 경우&lt;/li&gt;
&lt;li&gt;운영 인력이 충분한 곳 (DBA, SRE 풀타임 최소 2~3명)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도입하지 않아도 되는 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 1TB 이하: MySQL 한 대로 충분하다. 인덱스를 잘 설계하고 슬로우 쿼리를 잡으면 된다.&lt;/li&gt;
&lt;li&gt;인력이 부족한 스타트업: Vitess 운영은 진짜 까다롭다. 컴포넌트만 4개이고, 토폴로지 서비스까지 합치면 5개이다. 장애가 발생했을 때 디버깅이 가능한 인력이 없으면 위험하다.&lt;/li&gt;
&lt;li&gt;트래픽이 읽기 위주인 서비스: 읽기 스케일은 MySQL 리플리카로도 충분히 가능하다.&lt;/li&gt;
&lt;li&gt;쓰기가 그렇게 많지 않은 곳: 단일 MySQL의 쓰기 처리량은 생각보다 크다. 실제 한계에 부딪힌 후에 도입을 고민해도 늦지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도입 비용과 학습 곡선의 솔직한 평가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 복잡도는 매우 높다. VTGate, VTTablet, VTctld, Topology Service를 모두 이해해야 하고, VReplication, Reshard 같은 워크플로우에도 익숙해져야 한다. 한국에는 사례도 적고 전문가도 거의 없어 영문 문서를 읽고 GitHub 이슈를 뒤져야 한다.&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;마이그레이션 자체도 가볍지 않다. 슬랙은 3년이 걸렸고, HubSpot도 2년 넘게 소요되었다. 작은 회사 기준으로도 최소 6개월~1년은 잡아야 한다.&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;다만 일단 자리잡고 나면 운영이 표준화되고 스케일이 자유로워진다는 점은 확실하다. PlanetScale 같은 매니지드 옵션도 존재하고, 자체 호스팅 가이드도 점점 개선되고 있다. 결국 &quot;지금 우리에게 필요한가&quot;의 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Vitess가 분산 DB의 표준이 되는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 다음과 같다. Vitess는 갑자기 부상한 것이 아니라 14년 동안 묵묵히 검증된 기술이며, 2024년에는 PlanetScale 사건, AI SaaS 붐, Aurora 한계, 쿠버네티스 stateful 표준화 같은 외부 요인이 동시에 터지면서 다시 주목받고 있다. 핵심을 정리하면 다섯 줄이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유튜브가 14년째 운영 중인 검증된 시스템이다&lt;/li&gt;
&lt;li&gt;MySQL 호환성 덕분에 기존 생태계를 그대로 활용할 수 있다&lt;/li&gt;
&lt;li&gt;슬랙, Square, HubSpot 같은 거대 기업들이 핵심 인프라에 도입했다&lt;/li&gt;
&lt;li&gt;쿠버네티스 + AI SaaS 시대의 데이터 스케일 요구에 거의 정확히 부합한다&lt;/li&gt;
&lt;li&gt;운영 복잡도가 높아 작은 팀에는 오버스펙이지만, 일정 규모를 넘으면 다른 선택지가 거의 없다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 Vitess는 &lt;b&gt;MySQL을 버릴 수 없는 회사들의 답&lt;/b&gt;이다. NewSQL로 갈아타기에는 코드가 너무 묶여 있고, NoSQL로 가기에는 트랜잭션이 필요하며, Aurora는 비싸거나 한계에 부딪힌 곳들. 이 자리를 정확히 채워주는 시스템이 Vitess이며, 그래서 시간이 갈수록 더 표준이 되어가는 흐름이다.&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;도입 결정은 진짜 신중하게 내려야 한다. 일단 데이터가 1TB를 넘고 운영 인력이 갖춰져 있다면 한 번 검토할 가치가 있다. 그 이하라면 MySQL을 잘 튜닝해서 사용하는 것이 답이다. 다음 글에서는 Vitess를 로컬에서 직접 띄우고 샤딩 테스트를 하는 방법을 정리할 예정이다. 관심이 있다면 기다려주길 바란다.&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;&lt;b&gt;참고 자료&lt;/b&gt;:&lt;/p&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://vitess.io/&quot;&gt;Vitess 공식 사이트&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cncf.io/announcements/2019/11/05/cloud-native-computing-foundation-announces-vitess-graduation/&quot;&gt;CNCF Vitess Graduation 발표 (2019)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://slack.engineering/scaling-datastores-at-slack-with-vitess/&quot;&gt;Slack Engineering &amp;mdash; Scaling Datastores at Slack with Vitess&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/vitessio/vitess/blob/main/ADOPTERS.md&quot;&gt;GitHub vitessio/vitess ADOPTERS.md&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Database</category>
      <category>MySQL 샤딩</category>
      <category>planetscale</category>
      <category>Vitess DB</category>
      <category>분산 데이터베이스</category>
      <category>쿠버네티스 데이터베이스</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/391</guid>
      <comments>https://yscho03.tistory.com/391#entry391comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:22:32 +0900</pubDate>
    </item>
    <item>
      <title>Anthropic이 Claude Code에 굳이 터미널을 박은 이유, 그 안에 숨은 철학</title>
      <link>https://yscho03.tistory.com/390</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-4 시대에 IDE 플러그인도 아니고, 화려한 GUI도 아니고, 굳이 터미널이라니. 2025년 2월 Anthropic이 &lt;b&gt;Claude Code&lt;/b&gt;를 발표했을 때 다들 의아했다. Cursor가 IDE 시장을 잡고 GitHub Copilot이 에디터에 박혀 잘 굴러가는 마당에, claude 한 줄짜리 CLI를 들고 나온 것이다.&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;다만 1년 남짓 지나고 보니 이것은 그냥 옛날 사람들의 취향이 아니었다. 작정하고 깐 디자인 결정이었고, 그 안에는 4가지 명확한 철학이 박혀 있었다. &lt;b&gt;claude code 터미널&lt;/b&gt; 인터페이스가 왜 정답이었는지, 보리스 체르니(Boris Cherny)의 발언과 실제 사용 패턴을 함께 살피며 정리한다.&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;이 글을 끝까지 읽으면 &quot;아, 이래서 터미널이었구나&quot;라는 답이 나온다. &lt;b&gt;claude code 철학&lt;/b&gt; 자체가 LLM 시대의 인터페이스 본질을 건드리는 이야기라, 알아두면 다른 도구를 평가할 때도 쓸모가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일단 사실 정리 - Claude Code가 무엇이고 왜 터미널인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B5lh3/dJMcaib59Pl/EjInPhIDVFsa4RoMR6LCjK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B5lh3/dJMcaib59Pl/EjInPhIDVFsa4RoMR6LCjK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B5lh3/dJMcaib59Pl/EjInPhIDVFsa4RoMR6LCjK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB5lh3%2FdJMcaib59Pl%2FEjInPhIDVFsa4RoMR6LCjK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.claude.com/product/claude-code&quot;&gt;Claude&lt;/a&gt;&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;Claude Code는 Anthropic이 2025년 2월 24일에 리서치 프리뷰로 공개한 코딩 에이전트다. 5월에 정식 GA가 됐고, 인터페이스는 진짜로 단순하다. 터미널에서 claude 한 단어를 치면 끝이다.&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;IDE 익스텐션도 없다. 웹 GUI도 없다. 데스크탑 앱도 없다. 그저 npm으로 깔고 셸에서 실행하는 CLI 하나다. 처음 본 사람은 &quot;이게 다인가?&quot;라고 느낄 정도로 미니멀하다.&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;다만 Cursor와 GitHub Copilot 같은 경쟁 제품과 비교하면 이 선택이 얼마나 역방향인지 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude Code 터미널이 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 터미널은 Anthropic이 만든 CLI 기반 코딩 에이전트다. IDE나 GUI 없이 셸(shell)에서 claude 명령어 한 줄로 실행한다. npm으로 설치하고 어떤 에디터&amp;middot;운영체제 위에든 얹어 쓸 수 있는 unopinionated 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cursor&amp;middot;Copilot과 무엇이 다른가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;도구&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;인터페이스&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;통합 방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;종속성&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;VS Code 포크&lt;/td&gt;
&lt;td&gt;IDE 자체가 제품이다&lt;/td&gt;
&lt;td&gt;에디터를 갈아타야 한다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Copilot&lt;/td&gt;
&lt;td&gt;IDE 플러그인&lt;/td&gt;
&lt;td&gt;에디터에 종속된다&lt;/td&gt;
&lt;td&gt;VS Code&amp;middot;JetBrains가 필수다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;터미널 CLI&lt;/td&gt;
&lt;td&gt;어디든 붙는다&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cursor는 VS Code를 통째로 포크해서 만든 별도의 IDE다. Copilot은 에디터 안에서 동작한다. 두 제품 모두 &quot;IDE가 곧 워크플로우&quot;라는 전제 위에 서 있다.&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;Claude Code는 그 전제를 깨버린다. &quot;에디터는 알아서 골라라. 우리는 그저 터미널에 살겠다&quot;는 입장이다. 같은 AI 코딩 에이전트인데 시작점이 완전히 다르다.&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;이는 단순한 기술적 차이가 아니라 &lt;b&gt;claude code 철학&lt;/b&gt; 자체의 차이다. 그 철학을 4가지로 뜯어본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;철학 1: Unix 철학을 그대로 박았다 - &quot;한 가지만 잘 하는 도구&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;530&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mKGo2/dJMcaib59Po/75tYNNk73ERJfRD5BbJF80/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mKGo2/dJMcaib59Po/75tYNNk73ERJfRD5BbJF80/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mKGo2/dJMcaib59Po/75tYNNk73ERJfRD5BbJF80/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmKGo2%2FdJMcaib59Po%2F75tYNNk73ERJfRD5BbJF80%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1140&quot; height=&quot;530&quot; data-origin-width=&quot;1140&quot; data-origin-height=&quot;530&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://storage.googleapis.com/djnbwvucfmelke/pipe-unix-command.html&quot;&gt;storage.googleapis.com&lt;/a&gt;&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;1978년 Bell Labs의 Doug McIlroy가 정리한 Unix 철학에는 이런 말이 있다. &quot;Make each program do one thing well.&quot; 한 프로그램은 한 가지만 잘 하면 된다는 것이다. 50년 가까이 지났는데도 이 원칙은 여전히 유효하다.&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;Claude Code는 이 원칙을 그대로 따른다. &quot;코딩 에이전트&quot;라는 한 가지에만 집중한다. 에디터를 만들지 않는다. 디버거도 만들지 않는다. 터미널 에뮬레이터도 만들지 않는다. 자기 영역 외에는 모두 외부 도구에 맡긴다.&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;보리스 체르니(Claude Code 리드 엔지니어)가 Latent Space 팟캐스트에서 한 말이 핵심이다. &quot;We think of it as like a Unix utility... the same way that you would compose, you know, grep or cat.&quot; Unix 유틸리티처럼 grep&amp;middot;cat과 조합되는 저수준 도구로 봤다는 의미다.&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;여기서 핵심 단어가 &quot;low-level primitive&quot;다. 고수준 통합 환경(IDE)을 만드는 것이 아니라 &lt;b&gt;저수준 빌딩 블록&lt;/b&gt;을 만드는 것이다. 사용자가 자기 환경에 갖다 끼우면 그만이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;composability(조합성)를 살리려면 텍스트가 답이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조합성을 살리려면 인터페이스가 단순해야 한다. 가장 단순한 인터페이스는 무엇인가? 텍스트 스트림이다. stdin으로 받고 stdout으로 뱉는다. 50년 된 패턴이지만 아직도 이보다 단순하고 강력한 것은 없다.&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;예시를 한 번 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777386037779&quot; class=&quot;1c&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;git diff | claude &quot;이 변경사항 코드 리뷰해줘&quot; &amp;gt; review.md
&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;한 줄이다. git diff 결과를 Claude Code에 파이프로 넘기고, 결과를 마크다운 파일로 저장한다. IDE 플러그인이었다면 이 시나리오는 절대로 불가능하다. 마우스로 diff 영역을 선택하고, 우클릭 메뉴를 열고, AI 리뷰 버튼을 누르고, 결과창에서 복사해 파일에 붙여넣고&amp;hellip; 이 모든 과정이 &quot;한 줄 파이프&quot;보다 비효율적이다.&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;GUI를 만들면 그 GUI가 곧 제약이 된다. 텍스트 인터페이스라면 사용자 상상력만큼 확장된다. 이것이 Unix 철학이 LLM 에이전트와 만났을 때 나오는 시너지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;철학 2: &quot;Unopinionated&quot; - 사용자에게 강요하지 않는다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic 공식 문서와 인터뷰를 보면 일관되게 등장하는 단어가 있다. &quot;low-level&quot;과 &quot;unopinionated&quot;이다. 직역하면 &quot;의견 없음&quot;이지만, 의역하면 &lt;b&gt;&quot;사용자가 어떻게 쓸지 정해주지 않는다&quot;&lt;/b&gt; 정도가 된다.&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;왜 이것이 중요한가? IDE는 본질적으로 의견이 강하다(opinionated). VS Code를 쓰면 VS Code의 단축키를 외워야 한다. JetBrains를 쓰면 JetBrains의 워크플로우를 따라야 한다. Cursor를 쓰면 Cursor 사이드패널 구조에 적응해야 한다.&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;Claude Code는 정반대다. &lt;b&gt;터미널 = 사용자 마음대로&lt;/b&gt;다. vim에서 :!claude를 치고 쓰는 사람, tmux 분할 창에서 띄우는 사람, VS Code 통합 터미널에 박는 사람, 심지어 Cursor 안에서 Claude Code를 돌리는 사람도 있다. 모두 가능하다.&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;한국 개발자만 봐도 도구 취향이 천차만별이다. 시니어는 vim/neovim을 쓰는 사람이 많고, Java 개발자는 IntelliJ를 떠나기 어렵고, 프론트엔드는 VS Code가 절대다수다. Cursor로 갈아탄 사람도 늘고 있다. 다만 Claude Code는 이 모든 환경 위에 그대로 얹힌다.&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;이는 비즈니스적으로도 영리한 선택이다. Cursor처럼 &quot;에디터를 갈아타라&quot;고 요구하면 진입장벽이 높다. Claude Code는 &quot;지금 쓰는 것을 그대로 쓰면서 추가하면 된다&quot;는 포지션이다. 사용자 비용이 0에 가깝다.&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;체르니 본인도 이 점을 강조한다. 사용자에게 워크플로우를 강요하지 않는 것이 가장 중요한 디자인 원칙이었다고 밝혔다. &lt;b&gt;anthropic 개발 철학&lt;/b&gt; 전반에 깔린 정서다 &amp;mdash; 모델도 unopinionated하게 만들고 도구도 unopinionated하게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;철학 3: 자동화&amp;middot;스크립팅 - 터미널의 진짜 무기다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMvG9E/dJMcafGqL1E/Dt1EDDkXOVkk4rVbG6tga0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMvG9E/dJMcafGqL1E/Dt1EDDkXOVkk4rVbG6tga0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMvG9E/dJMcafGqL1E/Dt1EDDkXOVkk4rVbG6tga0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMvG9E%2FdJMcafGqL1E%2FDt1EDDkXOVkk4rVbG6tga0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1820&quot; height=&quot;1024&quot; data-origin-width=&quot;1820&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.pullchecklist.com/posts/github-actions-workflow-examples-guide-master-ci-cd&quot;&gt;pullchecklist.com&lt;/a&gt;&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;여기서부터가 진짜 핵심이다. 터미널을 고른 가장 실용적인 이유가 &lt;b&gt;자동화&lt;/b&gt; 때문이다.&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;Claude Code는 처음부터 --print 모드(원샷 실행), headless 모드, SDK를 모두 지원한다. 셸에서 한 번 실행하고 결과를 받고 끝나는 시나리오를 정식으로 지원하는 것이다. GUI 도구는 절대로 해낼 수 없는 영역이다. 사람이 마우스로 클릭해야 동작하는 도구는 자동화의 적이다.&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;이것이 가능하기에 시나리오가 무한히 펼쳐진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;PR 자동 리뷰 봇&lt;/b&gt;: GitHub Actions에서 PR이 열릴 때마다 Claude Code를 돌려 리뷰 댓글 달기&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이슈 자동 분류&lt;/b&gt;: 새 이슈가 들어올 때마다 라벨링&amp;middot;우선순위 자동 분배&lt;/li&gt;
&lt;li&gt;&lt;b&gt;매일 밤 코드베이스 정리&lt;/b&gt;: 크론으로 하루치 변경사항 분석&amp;middot;요약&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CI 실패 자동 진단&lt;/b&gt;: 빌드가 깨질 때마다 로그를 분석해 슬랙으로 원인 전송&lt;/li&gt;
&lt;li&gt;&lt;b&gt;레거시 마이그레이션&lt;/b&gt;: 수천 개 파일 일괄 변환&lt;/li&gt;
&lt;/ul&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;b&gt;claude code 터미널&lt;/b&gt; 인터페이스라서 가능하다. Cursor&amp;middot;Copilot으로는 발상 자체가 떠오르지 않는다.&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;GitHub에서 &quot;claude-code-action&quot; 같은 워크플로를 검색해보면 이미 수백 개의 사례가 있다. 1년 남짓 지났는데 생태계가 이만큼 커진 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;hooks&amp;middot;slash command&amp;middot;MCP는 같은 철학의 연장이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후속 기능들을 보면 &quot;터미널 우선&quot; 철학이 얼마나 일관되게 이어지는지 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;hooks&lt;/b&gt;: 특정 이벤트(파일 편집 전, 커밋 후 등)에 임의의 셸 명령을 실행한다. 셸 기반이기에 가능하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;slash command&lt;/b&gt;: .claude/commands/ 디렉토리에 마크다운 파일을 넣으면 커스텀 명령어가 된다. 또 파일시스템&amp;middot;텍스트 기반이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MCP(Model Context Protocol)&lt;/b&gt;: 외부 도구 연결 표준인데, stdio JSON-RPC로 통신한다. 또 텍스트 스트림이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 모든 확장 포인트가 &quot;텍스트 in, 텍스트 out&quot; 위에 서 있다. &lt;b&gt;anthropic 개발 철학&lt;/b&gt;이 일관되게 텍스트 인터페이스를 1급 시민으로 두는 것이다. MCP가 stdio 기반인 것도 같은 맥락이다 &amp;mdash; 어떤 언어&amp;middot;런타임에서도 즉시 구현이 가능하기 때문이다.&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;이것을 GUI 기반으로 만들었다고 상상해보자. hooks를 GUI 이벤트로? slash command를 메뉴로? MCP를 WebSocket으로? 모두 가능은 하지만 진입장벽이 10배는 늘어난다. 터미널이라서 1시간이면 만들 것을 GUI라면 일주일이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;철학 4: dogfooding - Anthropic이 매일 쓰는 도구가 바로 이것이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막 철학이 어쩌면 가장 솔직한 이유다. Claude Code는 본래 Anthropic의 사내 도구로 시작한 것이다.&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;체르니가 Latent Space 팟캐스트에서 직접 회고한 내용이다. 자신이 Anthropic에 합류해 모델로 이것저것 실험하다가 터미널 접근권을 주고 코딩을 시키니까 너무 유용해서 매일 쓰게 됐다는 것이다(&quot;I gave it access to the terminal and the ability to code. And suddenly, it just felt very useful. Like, I was using this thing every day&quot;). 자기들이 쓰려고 만든 것이 먼저였고, 외부 출시는 그 다음 결정이었다.&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;Anthropic 엔지니어들은 터미널에서 살아간다. 모델 학습을 돌리고, 평가 스크립트를 짜고, 데이터 파이프라인을 굴리고&amp;hellip; 모두 터미널 기반이다. 그들의 입장에서 &quot;코딩 에이전트가 IDE에 박혀 있다면&quot; 오히려 불편하다. 워크플로우가 끊긴다.&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;그래서 자기들의 워크플로우에 자연스럽게 녹아드는 도구로 만든 것이다. 그것이 우연히 외부에도 잘 팔린 것이다.&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;이것이 dogfooding(자기 개밥 먹기) 문화의 핵심이다. &lt;b&gt;사용자 친화&lt;/b&gt;보다 &lt;b&gt;내가 매일 쓰고 싶은 것&lt;/b&gt;을 우선시한다. 결과적으로 진짜 사용자에게도 더 좋은 도구가 나온다. 왜일까? 만든 사람이 매일 쓰니까 불편한 점이 즉시 발견되고 즉시 고쳐지기 때문이다.&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;Cursor&amp;middot;Copilot도 좋은 도구이지만 &quot;AI 회사가 쓰려고 만든 도구&quot;는 아니다. 마케팅 부서&amp;middot;디자인 부서의 입김이 들어간 도구다. Claude Code에는 그것이 없다. 엔지니어가 엔지니어를 위해 만든 도구라는 점이 그대로 드러난다.&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;결과적으로 Cursor&amp;middot;Copilot이 잡지 못한 &lt;b&gt;파워유저 시장&lt;/b&gt;을 가져간다. 시니어 엔지니어, DevOps 전문가, AI 리서처 &amp;mdash; 이들은 이미 터미널의 마스터이기에 진입장벽이 0이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 이 선택은 옳았는가 - 1년 후 결과로 본 평가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;607&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6USZC/dJMcaib59Pm/QaIirbhYZPw6tPKF0unwK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6USZC/dJMcaib59Pm/QaIirbhYZPw6tPKF0unwK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6USZC/dJMcaib59Pm/QaIirbhYZPw6tPKF0unwK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6USZC%2FdJMcaib59Pm%2FQaIirbhYZPw6tPKF0unwK1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;607&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;607&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.toolbit.ai/blog/best-ai-coding-tools-copilot-cursor-claude-comparison&quot;&gt;toolbit.ai&lt;/a&gt;&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;2025년 2월 출시 후 지금까지의 결과를 보면 답이 나온다.&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;&lt;b&gt;잘 된 부분&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;출시 1년 안에 GitHub Actions&amp;middot;CI 통합 사례가 폭증했다. 사실상 &quot;AI 자동화&quot;라는 카테고리 자체를 만들어냈다&lt;/li&gt;
&lt;li&gt;Cursor도 결국 자체 CLI 모드(cursor agent)를 추가하는 방향으로 따라온다. &quot;터미널이 답이었다&quot;고 인정한 셈이다&lt;/li&gt;
&lt;li&gt;한국 스타트업, 시니어 엔지니어 사이에서 &quot;AI 코딩 도구 표준&quot;으로 자리 잡는 중이다. 토스&amp;middot;카카오&amp;middot;당근 같은 곳에서 도입 사례가 확인된다&lt;/li&gt;
&lt;li&gt;MCP가 사실상 LLM 에이전트 통신 표준으로 자리잡았다. stdio 기반이라 누구나 구현이 가능했던 것이 컸다&lt;/li&gt;
&lt;/ul&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;b&gt;트레이드오프&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주니어&amp;middot;비개발자에게는 진입장벽이 있다. 터미널 자체가 익숙하지 않은 사람에게는 어렵다&lt;/li&gt;
&lt;li&gt;시각적 UI가 필요한 작업(복잡한 diff 비교, 멀티파일 리팩토링 시각화 등)은 약하다&lt;/li&gt;
&lt;li&gt;&quot;Claude Code를 잘 쓰려면 셸 스크립팅도 잘해야 한다&quot;는 학습 곡선이 있다&lt;/li&gt;
&lt;/ul&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;b&gt;계산된&lt;/b&gt; 것이다. Anthropic이 &quot;전 세계 모든 사람에게 팔겠다&quot;는 것이 아니라 &quot;엔지니어에게 압도적으로 좋은 것을 만들겠다&quot;는 포지션이었다. 결과적으로 그 전략이 통한 것이다.&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;Cursor가 &quot;비개발자도 코딩&quot;을 노리는 동안 Claude Code는 &quot;전문가가 더 빠르게&quot;를 노린 것이다. 두 시장은 다른 시장이다. 둘 다 살아남겠지만 본질은 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;claude code 터미널 선택은 구식이 아니라 LLM 시대의 정답이었다&lt;/h2&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;b&gt;claude code 터미널&lt;/b&gt; 선택은 단순한 보수적 결정이 아니라 4가지 철학의 산물이다.&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;Unix 철학&lt;/b&gt;: 한 가지만 잘 하는 조합 가능한 도구&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Unopinionated&lt;/b&gt;: 사용자에게 워크플로우를 강요하지 않는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자동화&amp;middot;스크립팅&lt;/b&gt;: CI/CD&amp;middot;hooks&amp;middot;MCP 모두 텍스트 인터페이스 위에서 가능하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;dogfooding&lt;/b&gt;: Anthropic 엔지니어들이 매일 쓰고 싶은 도구로 만들었다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;터미널이 구식이다&quot;라는 프레임 자체가 틀렸다. 터미널은 50년간 살아남은 인터페이스이고, 그 이유는 텍스트 스트림이라는 인터페이스가 너무 강력하기 때문이다. LLM은 본질적으로 텍스트 모델이다. 텍스트 모델에게 텍스트 인터페이스가 가장 자연스러운 것은 당연하다.&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;GUI가 답이라는 말은 마우스 클릭이 답이라는 말과 같다. AI 시대에는 그것이 답이 아니다. 자연어 입력 + 자동화 가능한 출력 &amp;mdash; 이것이 답이고, 그것을 가장 잘 해내는 것이 터미널이다.&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;Anthropic의 선택은 보수가 아니라 &lt;b&gt;본질주의&lt;/b&gt;였다. 트렌드를 따라가는 것이 아니라 본질이 무엇인지 파고들어 거기서 시작한 것이다. 그 결과 &lt;b&gt;claude code 철학&lt;/b&gt;이 다른 AI 도구들도 따라가는 표준이 됐다.&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;아직 Claude Code를 깔아보지 않은 사람은 한 번 깔아보길 권한다. 5분이면 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777386037781&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install -g @anthropic-ai/claude-code
claude
&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;깔고 나면 위에서 말한 4가지 철학이 왜 작동하는지 손으로 느낄 수 있다. 그때부터 다른 AI 코딩 도구의 평가 기준 자체가 바뀐다. &quot;이것은 자동화가 되는가?&quot;, &quot;이것은 다른 도구와 조합이 되는가?&quot;, &quot;이것은 내 워크플로우를 강요하는가?&quot; &amp;mdash; 이런 질문이 자동으로 떠오른다.&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;그것이 Anthropic이 의도한 효과다. 단순히 도구 하나를 푼 것이 아니라 &lt;b&gt;AI 시대 인터페이스 기준&lt;/b&gt;을 다시 정의한 것이다. 그 시작이 터미널이었던 것이고, 1년이 지나고 보니 그 결정이 옳았던 것이다.&lt;/p&gt;</description>
      <category>끄적끄적</category>
      <category>anthropic 개발 철학</category>
      <category>claude code vs cursor</category>
      <category>claude code 철학</category>
      <category>cli 코딩 에이전트</category>
      <category>unix 철학 ai</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/390</guid>
      <comments>https://yscho03.tistory.com/390#entry390comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:21:14 +0900</pubDate>
    </item>
    <item>
      <title>Postgres는 테이블 락만 가능한가? 아니면 행 단위 락도 가능한가? 결론부터 정리한다</title>
      <link>https://yscho03.tistory.com/389</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문을 검색해서 들어온 사람은 보통 둘 중 하나다. MySQL InnoDB만 다루다가 Postgres로 넘어왔거나, 아니면 락 메커니즘을 깊게 파보지 않아서 헷갈리는 경우다. 결론부터 말하면 &lt;b&gt;Postgres는 행(Row) 단위 락도 당연히 지원한다. 그것도 아주 잘 지원한다.&lt;/b&gt;&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;다만 사람들이 헷갈리는 이유가 있다. (1) Postgres 락 모드가 테이블 락만 8단계, 행 락 4단계라 처음 보면 머리가 복잡하다. (2) LOCK TABLE이라는 명령어가 눈에 잘 띄어서 &quot;Postgres는 테이블 단위로만 잠그는 것인가?&quot;라는 의문이 든다. (3) 한국어 자료 중에 락 종류를 한 페이지에 깔끔히 정리한 글이 드물다.&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;이 글을 다 읽으면 (1) postgres 행 단위 락 종류를 머릿속에 표로 그릴 수 있다, (2) SELECT FOR UPDATE, SKIP LOCKED 같은 실전 SQL을 바로 쓸 수 있다, (3) pg_locks 뷰로 락 디버깅까지 가능하다. 5분이면 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;30초 컷 결론: Postgres 행 단위 락은 당연히 가능하다&lt;/h2&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;b&gt;TL;DR&lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;- Postgres는 행 락, 테이블 락, 페이지 락, 어드바이저리 락을 모두 지원한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;- 일반 UPDATE/DELETE는 자동으로 행 단위 락(FOR UPDATE 수준)을 획득한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;- 일반 SELECT는 MVCC 덕분에 락을 거의 잡지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;- LOCK TABLE을 직접 써야 할 일은 마이그레이션 같은 특수 상황뿐이다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Postgres는 테이블 락밖에 안 된다&quot;는 말은 명백히 틀린 정보다. 오히려 Postgres는 MVCC(Multi-Version Concurrency Control) 구조 때문에 락을 최소한으로만 잡으려는 DB이다. UPDATE customers SET email = 'x' WHERE id = 1; 같은 평범한 쿼리도 내부적으로는 그 한 행에만 배타 락을 걸고 끝난다. 다른 트랜잭션이 같은 테이블의 다른 행을 동시에 수정해도 막히지 않는다.&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;LOCK TABLE 명령어는 존재하지만, 그것은 옵션 중 하나일 뿐이다. 명시적으로 호출하지 않으면 거의 잡히지 않는다. 따라서 이 글의 첫 문장만 기억해도 충분하다: &lt;b&gt;Postgres 행 락은 디폴트로 들어가 있는 기능이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;큰 그림으로 본 Postgres 락 모드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A8GVm/dJMcadogBpE/ebOYy3mY1tSgtbRBpm3320/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A8GVm/dJMcadogBpE/ebOYy3mY1tSgtbRBpm3320/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A8GVm/dJMcadogBpE/ebOYy3mY1tSgtbRBpm3320/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA8GVm%2FdJMcadogBpE%2FebOYy3mY1tSgtbRBpm3320%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;268&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://xata.io/blog/anatomy-of-locks&quot;&gt;xata.io&lt;/a&gt;&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;Postgres 락은 크게 4가지 단위로 나뉜다. 각각 언제 쓰는지 한 줄로 요약하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;락 단위 4가지 분류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;단위&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;명령 예시&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;언제 잡힘&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;테이블 락 (Table-level)&lt;/td&gt;
&lt;td&gt;`LOCK TABLE`, DDL&lt;/td&gt;
&lt;td&gt;DDL 자동 + 명시적 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;행 락 (Row-level)&lt;/td&gt;
&lt;td&gt;`UPDATE`, `SELECT FOR UPDATE`&lt;/td&gt;
&lt;td&gt;DML 자동 + 명시적 호출&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;페이지 락 (Page-level)&lt;/td&gt;
&lt;td&gt;내부 처리&lt;/td&gt;
&lt;td&gt;Postgres 내부에서 짧게 잡고 풀음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;어드바이저리 락 (Advisory)&lt;/td&gt;
&lt;td&gt;`pg_advisory_lock()`&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 락은 사실상 신경 쓸 일이 없다. Postgres가 내부적으로 인덱스 페이지 같은 것을 잠깐 잡았다가 푸는 용도이다. SQL로 직접 다룰 일은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테이블 락 모드 8단계 충돌 매트릭스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 락이 8단계라고 하면 처음 듣는 사람은 &quot;왜 이렇게 많은가?&quot;라는 의문이 들 것이다. 다만 실제로 모두 외울 필요는 없고, 어떤 명령이 어떤 락을 자동으로 잡는지만 알면 된다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;락 모드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;자동으로 잡는 명령&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;충돌하는 락&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACCESS SHARE&lt;/td&gt;
&lt;td&gt;`SELECT`&lt;/td&gt;
&lt;td&gt;ACCESS EXCLUSIVE만 충돌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROW SHARE&lt;/td&gt;
&lt;td&gt;`SELECT FOR UPDATE/SHARE`&lt;/td&gt;
&lt;td&gt;EXCLUSIVE, ACCESS EXCLUSIVE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ROW EXCLUSIVE&lt;/td&gt;
&lt;td&gt;`UPDATE`, `DELETE`, `INSERT`&lt;/td&gt;
&lt;td&gt;SHARE 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHARE UPDATE EXCLUSIVE&lt;/td&gt;
&lt;td&gt;`VACUUM`, `ANALYZE`, `CREATE INDEX CONCURRENTLY`&lt;/td&gt;
&lt;td&gt;자기 자신 + SHARE 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHARE&lt;/td&gt;
&lt;td&gt;`CREATE INDEX` (CONCURRENTLY 없이)&lt;/td&gt;
&lt;td&gt;ROW EXCLUSIVE 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHARE ROW EXCLUSIVE&lt;/td&gt;
&lt;td&gt;`CREATE TRIGGER`, 일부 `ALTER TABLE`&lt;/td&gt;
&lt;td&gt;거의 다 충돌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EXCLUSIVE&lt;/td&gt;
&lt;td&gt;`REFRESH MATERIALIZED VIEW CONCURRENTLY`&lt;/td&gt;
&lt;td&gt;ACCESS SHARE 빼고 다 충돌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACCESS EXCLUSIVE&lt;/td&gt;
&lt;td&gt;`DROP TABLE`, `TRUNCATE`, 대부분 `ALTER TABLE`&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 표 맨 아래 두 줄이다. &lt;b&gt;ACCESS EXCLUSIVE는 모든 락과 충돌한다.&lt;/b&gt; 이것이 잡히면 해당 테이블에 SELECT조차 들어갈 수 없다. 운영 중인 테이블에 무심코 ALTER TABLE을 날렸다가 서비스가 전부 멈춰본 사람이 한국에 의외로 많다. 이 부분은 뒤에서 따로 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;행 단위 락 4가지 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;postgres 행 단위 락도 사실 4가지로 나뉘어 있다. 9.3 버전 이전에는 FOR UPDATE와 FOR SHARE 두 가지만 존재했으나, 외래키 동시성 이슈 때문에 9.3에서 두 종류가 더 추가되었다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;행 락 모드&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;강도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;용도&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOR KEY SHARE&lt;/td&gt;
&lt;td&gt;가장 약함&lt;/td&gt;
&lt;td&gt;외래키 무결성 보호용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOR SHARE&lt;/td&gt;
&lt;td&gt;약함&lt;/td&gt;
&lt;td&gt;&quot;내가 읽는 동안 바뀌면 안 됨&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOR NO KEY UPDATE&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;키 컬럼 안 건드리는 UPDATE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FOR UPDATE&lt;/td&gt;
&lt;td&gt;가장 강함&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 위로 갈수록 약하고 아래로 갈수록 강하다. 일반 UPDATE는 키 컬럼(PK 등)을 건드리지 않으면 자동으로 FOR NO KEY UPDATE 수준의 락을 잡고, PK를 건드리면 FOR UPDATE 수준으로 잡는다. 외래키로 참조 중인 행에 대해서는 자동으로 FOR KEY SHARE가 걸려 부모 행이 사라지는 것을 막아준다.&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;대부분의 실무 시나리오에서는 SELECT ... FOR UPDATE 하나만 쓰면 충분하다. 나머지 3개는 &quot;왜 존재하는가&quot; 정도만 알아도 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;행 단위 락이 자동으로 잡히는 시점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1103&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boDQHf/dJMcadogBpC/AKdS6V2QLYj7kIRqAlOXJk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boDQHf/dJMcadogBpC/AKdS6V2QLYj7kIRqAlOXJk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boDQHf/dJMcadogBpC/AKdS6V2QLYj7kIRqAlOXJk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboDQHf%2FdJMcadogBpC%2FAKdS6V2QLYj7kIRqAlOXJk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1920&quot; height=&quot;1103&quot; data-origin-width=&quot;1920&quot; data-origin-height=&quot;1103&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.cybrosys.com/research-and-development/postgres/how-postgresql-manages-row-level-locks-with-row-mark-types&quot;&gt;cybrosys.com&lt;/a&gt;&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;이론은 끝났고, 이제 실전이다. 행 단위 락이 언제 자동으로 잡히고, 언제 명시적으로 걸어야 하는지 SQL로 살펴본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;UPDATE/DELETE는 묻지도 따지지도 않고 행 락을 잡는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907948&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 트랜잭션 A
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 42;
-- 여기서 id=42 행에 자동으로 행 단위 배타 락이 잡힌다
-- COMMIT 전까지 다른 트랜잭션은 같은 행을 건드릴 수 없다

-- 트랜잭션 B (동시 실행)
UPDATE accounts SET balance = balance - 50 WHERE id = 42;
-- A가 COMMIT 또는 ROLLBACK 할 때까지 대기 (블로킹)
&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;다만 다른 행(예: id = 99)을 건드리는 트랜잭션은 막히지 않는다. 이것이 행 단위 락의 핵심이다. 테이블 전체가 잠기는 것이 아니라 정확히 해당 행만 잠긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 SELECT는 락을 잡지 않는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Postgres의 MVCC 구조 덕분에 SELECT는 행 락도, 테이블 배타 락도 잡지 않는다. ACCESS SHARE라는 가장 약한 테이블 락만 잡으며, 이는 ACCESS EXCLUSIVE를 제외하면 어떤 락과도 충돌하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907948&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 트랜잭션 A: UPDATE 중 (행 락 잡힌 상태)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 42;

-- 트랜잭션 B: 같은 행 SELECT
SELECT * FROM accounts WHERE id = 42;
-- 막히지 않는다! 이전 버전(스냅샷)을 읽는다
&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;이것이 MySQL InnoDB와의 가장 큰 차이다. InnoDB는 격리수준에 따라 SELECT도 락을 잡을 수 있으나, Postgres는 디폴트로 잡지 않는다. &quot;읽기는 쓰기를 막지 않고, 쓰기는 읽기를 막지 않는다&quot;가 Postgres MVCC의 기본 철학이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;명시적 행 락이 필요한 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자동 락은 한 SQL 문장 단위로만 잡힌다. 다만 실무에서는 &quot;조회한 다음 그 값으로 계산해서 업데이트하기까지&quot; 사이에 다른 트랜잭션이 끼어들면 안 되는 경우가 많다. 대표적으로 재고 차감, 좌석 예약, 잔액 갱신 같은 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 잘못된 예: race condition 발생 가능
BEGIN;
SELECT stock FROM products WHERE id = 1;  -- stock = 5
-- 여기서 다른 트랜잭션이 끼어들어 stock을 0으로 만들 수 있다
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
&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;이를 막으려면 SELECT FOR UPDATE로 미리 행 락을 선점해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 올바른 예: SELECT FOR UPDATE로 행 락 선점
BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 이 행은 COMMIT/ROLLBACK 전까지 다른 트랜잭션이 건드릴 수 없다
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
&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;이것이 postgres select for update의 가장 흔한 사용 패턴이다. 동시성 이슈의 90%는 이 하나로 해결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;LOCK TABLE은 언제 써야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말해 대부분의 백엔드 개발자는 LOCK TABLE을 직접 쓸 일이 거의 없다. Postgres가 자동으로 적절한 레벨의 락을 잡아주기 때문이다. 다만 명시적으로 테이블 락이 필요한 시나리오가 몇 가지 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 테이블 전체에 대한 일관된 스냅샷이 필요할 때
BEGIN;
LOCK TABLE accounts IN SHARE MODE;
-- 이제 누구도 accounts에 INSERT/UPDATE/DELETE를 할 수 없다 (SELECT는 가능)
SELECT SUM(balance) FROM accounts;  -- 정확한 합계
COMMIT;

-- 마이그레이션 중 데이터 일관성 보장
BEGIN;
LOCK TABLE old_table IN ACCESS EXCLUSIVE MODE;
INSERT INTO new_table SELECT * FROM old_table;
DROP TABLE old_table;
ALTER TABLE new_table RENAME TO old_table;
COMMIT;
&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;다만 운영 중인 서비스에서 ACCESS EXCLUSIVE를 잡으면 그 시점부터 모든 쿼리가 멈춘다. 트래픽이 있는 시간대에는 절대 해서는 안 되는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서 진짜 자주 쓰는 락 패턴 4가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;933&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbYqFx/dJMcadogBpB/k0LTom111PLrttjUJYAi90/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbYqFx/dJMcadogBpB/k0LTom111PLrttjUJYAi90/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbYqFx/dJMcadogBpB/k0LTom111PLrttjUJYAi90/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbYqFx%2FdJMcadogBpB%2Fk0LTom111PLrttjUJYAi90%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;933&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;933&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://medium.com/@the_atomic_architect/postgresql-replaced-my-message-queue-and-taught-me-skip-locked-along-the-way-87d59e5b9525&quot;&gt;Medium (129KB)&lt;/a&gt;&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;여기부터가 진짜 알아야 할 부분이다. 공식 문서에 친절하게 적혀 있지 않은 것들 위주로 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SELECT FOR UPDATE SKIP LOCKED로 큐 만들기&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 워커 N개가 동시에 돌면서 작업을 가져갈 때
BEGIN;
SELECT id, payload
FROM job_queue
WHERE status = 'pending'
ORDER BY created_at
LIMIT 1
FOR UPDATE SKIP LOCKED;  -- 다른 워커가 이미 잡은 행은 건너뛴다

-- 이 워커가 잡은 행만 업데이트
UPDATE job_queue SET status = 'processing' WHERE id = $1;
COMMIT;
&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;SKIP LOCKED가 없으면 워커들이 같은 행을 두고 줄을 서서 대기하게 된다. SKIP LOCKED를 붙이면 &quot;이 행은 이미 누가 잡았으니 다음 행으로&quot; 하고 넘어간다. RabbitMQ 같은 별도의 큐 솔루션을 띄우지 않고 Postgres 한 대로 큐 시스템을 만들 때 쓰는 마법 같은 옵션이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NOWAIT으로 락 대기 안 하고 바로 실패시키기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배치 작업이나 리포트 같은 곳에서 &quot;락이 잡혀 있으면 즉시 실패시키고 나중에 다시 시도&quot; 하고 싶을 때 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;BEGIN;
SELECT * FROM accounts WHERE id = 42 FOR UPDATE NOWAIT;
-- 락이 이미 잡혀 있으면 즉시 ERROR: could not obtain lock 발생
-- 기다리지 않는다
&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;이를 쓰지 않고 그냥 FOR UPDATE만 쓰면 다른 트랜잭션이 끝날 때까지 무한정 대기한다. 운영 환경에서 락 대기로 커넥션 풀이 모두 차면 서비스 다운으로 이어진다. NOWAIT 또는 lock_timeout 설정으로 가드를 거는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어드바이저리 락으로 분산 락 흉내내기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어드바이저리 락(advisory lock)은 Postgres가 제공하는 &quot;애플리케이션이 의미를 정의해서 쓰는 락&quot;이다. 특정 행이나 테이블과 무관하게, 단순히 64비트 정수 키로 락을 잡고 풀 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- &quot;주문 처리&quot;라는 작업을 한 번에 하나만 돌고 싶을 때
SELECT pg_advisory_lock(12345);
-- 다른 세션이 같은 키로 lock을 잡으려 하면 대기한다
-- ... 작업 수행 ...
SELECT pg_advisory_unlock(12345);

-- non-blocking 버전
SELECT pg_try_advisory_lock(12345);
-- true가 반환되면 락 획득 성공, false면 실패 (대기하지 않는다)
&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;크론 잡 중복 실행 방지, 마이그레이션 락, 분산 시스템에서의 leader election 같은 곳에 자주 사용한다. Redis Redlock 같은 것을 따로 띄우지 않고도 분산 락을 흉내 낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데드락이 났을 때 잡는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pg_locks와 pg_stat_activity를 조인하면 누가 누구를 막고 있는지 모두 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907949&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;SELECT
  blocked.pid AS blocked_pid,
  blocked.query AS blocked_query,
  blocking.pid AS blocking_pid,
  blocking.query AS blocking_query,
  blocking.usename AS blocking_user,
  blocked.wait_event_type
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
  ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE blocked.wait_event_type = 'Lock';
&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;이 쿼리 하나만 알아두면 &quot;왜 쿼리가 끝나지 않는가?&quot; 싶을 때 90%는 답이 나온다. 더 깊게 보고 싶으면 pg_locks 뷰를 직접 조회하면 락 모드, 객체 종류, 트랜잭션 ID까지 모두 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907950&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 현재 잡혀 있는 모든 락 보기
SELECT locktype, relation::regclass, mode, granted, pid
FROM pg_locks
ORDER BY granted, pid;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MySQL InnoDB와 무엇이 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;2562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcLWOd/dJMcaiXozvP/rGXXfzdepHDIXwCDsyNHBk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcLWOd/dJMcaiXozvP/rGXXfzdepHDIXwCDsyNHBk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcLWOd/dJMcaiXozvP/rGXXfzdepHDIXwCDsyNHBk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcLWOd%2FdJMcaiXozvP%2FrGXXfzdepHDIXwCDsyNHBk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;850&quot; height=&quot;2562&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;2562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.enterprisedb.com/blog/postgresql-vs-mysql-360-degree-comparison-syntax-performance-scalability-and-features&quot;&gt;enterprisedb.com&lt;/a&gt;&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;MySQL을 다루다가 넘어온 사람이 헷갈리는 포인트가 몇 가지 있다. 한 번에 정리한다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MySQL InnoDB&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PostgreSQL&lt;/b&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;튜플(tuple) 락&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;갭 락 / 넥스트 키 락&lt;/td&gt;
&lt;td&gt;있음 (REPEATABLE READ에서 활성)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반 SELECT&lt;/td&gt;
&lt;td&gt;격리수준에 따라 락 잡을 수 있음&lt;/td&gt;
&lt;td&gt;락 안 잡음 (MVCC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;디폴트 격리수준&lt;/td&gt;
&lt;td&gt;REPEATABLE READ&lt;/td&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;외래키 락&lt;/td&gt;
&lt;td&gt;자동으로 강한 락&lt;/td&gt;
&lt;td&gt;FOR KEY SHARE로 약하게&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데드락 감지&lt;/td&gt;
&lt;td&gt;자동 감지 + rollback&lt;/td&gt;
&lt;td&gt;자동 감지 + rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;명시적 행 락&lt;/td&gt;
&lt;td&gt;`SELECT ... FOR UPDATE`&lt;/td&gt;
&lt;td&gt;`SELECT ... FOR UPDATE`&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;갭 락이 없다는 점이다.&lt;/b&gt; MySQL은 WHERE id BETWEEN 10 AND 20 같은 범위 쿼리에 FOR UPDATE를 걸면 그 범위 사이의 빈 공간(gap)까지 락을 잡아 새로운 INSERT를 막는다. Postgres는 그렇지 않다. 이미 존재하는 행만 잠근다. 그래서 같은 시나리오에서 MySQL은 phantom read를 막아주지만 Postgres는 막아주지 않는다(직렬화 격리수준 SERIALIZABLE에서만 보장된다).&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;또 하나의 큰 차이는 &lt;b&gt;일반 SELECT의 락 동작이다.&lt;/b&gt; Postgres는 거의 모든 케이스에서 SELECT가 다른 트랜잭션을 막지 않는다. MySQL은 격리수준에 따라 다르다. 그래서 Postgres가 &quot;동시성에 더 친화적&quot;이라는 평가를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;운영 함정: 무심코 ACCESS EXCLUSIVE 잡는 DDL&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;339&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDsk7V/dJMcadogBpD/9R96BMa01qOELymRxgF111/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDsk7V/dJMcadogBpD/9R96BMa01qOELymRxgF111/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDsk7V/dJMcadogBpD/9R96BMa01qOELymRxgF111/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDsk7V%2FdJMcadogBpD%2F9R96BMa01qOELymRxgF111%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;867&quot; height=&quot;339&quot; data-origin-width=&quot;867&quot; data-origin-height=&quot;339&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.geeksforgeeks.org/dbms/what-is-access-exclusive-lock-mode-in-postgreysql/&quot;&gt;geeksforgeeks.org&lt;/a&gt;&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;이 섹션이 이 글에서 가장 중요할 수도 있다. 입문 글에서는 잘 다루지 않지만, 운영하다 보면 한 번씩 사고가 나는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ALTER TABLE ADD COLUMN의 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907951&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 9.x ~ 10 버전: 디폴트 값 있는 컬럼 추가
ALTER TABLE huge_table ADD COLUMN created_at TIMESTAMP DEFAULT NOW();
-- ACCESS EXCLUSIVE를 잡고 모든 행을 다시 쓴다
-- 행이 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;11버전부터는 이것이 메타데이터 변경만으로 끝난다. 실제 행을 다시 쓰지 않고, 카탈로그에만 &quot;이 컬럼의 디폴트는 X&quot;라고 기록한다. 그래서 거의 즉시 종료된다. &lt;b&gt;버전이 11 이상인지 반드시 확인하고, 그 이하라면 트래픽이 적은 시간에 수행하거나 아예 디폴트 없이 컬럼을 추가한 뒤 별도로 백필해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CREATE INDEX의 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777385907951&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;-- 평범한 인덱스 생성: ROW EXCLUSIVE 이상 충돌 (INSERT/UPDATE/DELETE를 모두 막는다)
CREATE INDEX idx_email ON users(email);

-- 운영 환경에서는 반드시 이것을 써야 한다
CREATE INDEX CONCURRENTLY idx_email ON users(email);
-- SHARE UPDATE EXCLUSIVE만 잡고, DML을 막지 않는다
-- 단점: 트랜잭션 안에서 쓸 수 없고, 실패 시 invalid 상태로 남을 수 있다
&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;CONCURRENTLY를 빠뜨리고 운영 DB에 인덱스를 만들었다가 서비스가 멈춰본 사람이 정말 많다. 운영 마이그레이션 스크립트에는 반드시 포함되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Postgres 마이그레이션 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL에는 gh-ost나 pt-online-schema-change 같은 무중단 스키마 변경 도구가 있다. Postgres에는 같은 컨셉의 pg_repack이나 pg_squeeze 같은 익스텐션이 있다. 큰 테이블을 정리하거나 재작성할 때 락을 잡지 않고 작업할 수 있다. 운영 규모가 커지면 한 번씩 살펴보는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Postgres 행 단위 락 핵심 요약과 다음 학습 단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 왔다면 postgres 행 단위 락에 대해서는 거의 다 파악한 것이다. 핵심만 다시 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Postgres는 행 락을 당연히 지원한다.&lt;/b&gt; 일반 UPDATE/DELETE는 자동으로 행 락을 잡는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일반 SELECT는 락을 잡지 않는다.&lt;/b&gt; MVCC 덕분에 읽기/쓰기가 서로 막지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;명시적 행 락은 SELECT ... FOR UPDATE&lt;/b&gt; 하나로 90%가 해결된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SKIP LOCKED로 큐 패턴&lt;/b&gt;, NOWAIT으로 락 대기 회피, pg_advisory_lock으로 분산 락 구현이 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영에서는 ACCESS EXCLUSIVE를 잡는 DDL을 조심해야 한다.&lt;/b&gt; 인덱스는 반드시 CONCURRENTLY를 붙여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음에 더 깊이 파볼 만한 주제는 다음과 같다: Postgres MVCC가 내부적으로 어떻게 동작하는지(튜플 버전 관리, vacuum), 트랜잭션 격리수준 4단계(READ UNCOMMITTED ~ SERIALIZABLE)별 동작 차이, pg_stat_activity로 슬로우 쿼리를 잡는 디버깅 패턴이다. 락 이슈로 운영 장애를 한 번 겪고 나면 이 주제들이 진짜 와닿게 된다.&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;참고할 만한 외부 자료는 PostgreSQL 공식 문서의 &lt;a href=&quot;https://www.postgresql.org/docs/current/explicit-locking.html&quot;&gt;Explicit Locking 챕터&lt;/a&gt;와 &lt;a href=&quot;https://www.postgresql.org/docs/current/view-pg-locks.html&quot;&gt;pg_locks 카탈로그 뷰&lt;/a&gt; 페이지이다. 영어지만 정답지에 가까우니, 이 글을 읽고 한 번 훑어보면 머릿속에 그림이 완전히 자리잡는다.&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;락 때문에 운영 장애가 났던 경험이나, &quot;이 패턴은 이렇게 하면 더 좋더라&quot; 같은 노하우가 있다면 댓글로 공유해주면 좋겠다. 다른 사람에게도 도움이 될 것이다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>postgres select for update</category>
      <category>postgres 테이블 락</category>
      <category>postgresql row level lock</category>
      <category>postgresql 데드락</category>
      <category>postgresql 락 종류</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/389</guid>
      <comments>https://yscho03.tistory.com/389#entry389comment</comments>
      <pubDate>Tue, 28 Apr 2026 23:19:04 +0900</pubDate>
    </item>
    <item>
      <title>FastAPI 백그라운드 태스크는 진정한 비동기인가? 서버 종료 시 살아남는가?</title>
      <link>https://yscho03.tistory.com/388</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI 백그라운드 태스크를 코드에 추가해 두고 &quot;비동기로 동작하니 메인 앱에는 영향이 없을 것이고, 서버를 재시작해도 끝까지 처리될 것&quot;이라고 가정한 적이 있는가? 나 역시 그러했다. 그러나 운영 환경에서 한 번 장애가 발생한 뒤에야 진실을 깨달았다. 이것은 별도 프로세스가 &lt;b&gt;아니다&lt;/b&gt;. 동일한 앱 내부에서 동작한다. 메인이 종료되면 함께 종료된다.&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;이 글에서 정리할 내용은 다음과 같다. FastAPI 공식 문서와 Starlette 소스를 분석하여 BackgroundTasks가 실제로 어디에서 실행되는지, Ctrl+C / SIGKILL / --reload / 멀티 워커 시나리오별로 진행 중인 작업이 어떻게 처리되는지, 그리고 언제 BackgroundTasks를 사용하고 언제 Celery로 이전해야 하는지까지 모두 다룰 것이다. 한국어로 이를 정리한 글이 거의 없어 직접 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;BackgroundTasks의 정체부터 파악하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/byOyNQ/dJMcagk0va6/pkgrMCpbznUipwClbcKyAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/byOyNQ/dJMcagk0va6/pkgrMCpbznUipwClbcKyAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/byOyNQ/dJMcagk0va6/pkgrMCpbznUipwClbcKyAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbyOyNQ%2FdJMcagk0va6%2FpkgrMCpbznUipwClbcKyAK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;630&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://fastapi.tiangolo.com/reference/background/&quot;&gt;fastapi.tiangolo.com&lt;/a&gt;&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;FastAPI BackgroundTasks의 한 줄 정의는 다음과 같다. &lt;b&gt;응답 전송이 완료된 직후에 실행되는 콜백 등록 도구&lt;/b&gt;이다. 이것이 전부이다. 별도 라이브러리도 아니며, 사실상 &lt;a href=&quot;https://www.starlette.io/background/&quot;&gt;Starlette의 BackgroundTasks&lt;/a&gt;를 그대로 임포트해 사용하는 얇은 래퍼이다.&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;가장 흔한 사용 사례는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;회원가입 후 환영 메일 발송&lt;/li&gt;
&lt;li&gt;요청 완료 로그를 외부로 적재&lt;/li&gt;
&lt;li&gt;알림 푸시 트리거&lt;/li&gt;
&lt;li&gt;분석용 이벤트 단발 전송&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777382255877&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from fastapi import BackgroundTasks, FastAPI

app = FastAPI()

def write_log(message: str):
    with open(&quot;log.txt&quot;, &quot;a&quot;) as f:
        f.write(message + &quot;\n&quot;)

@app.post(&quot;/signup/&quot;)
async def signup(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(write_log, f&quot;signup: {email}&quot;)
    return {&quot;ok&quot;: True}
&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;요청이 들어오면 라우트 함수가 종료되고 200 응답을 보낸 다음에야 write_log가 실행된다. 클라이언트 측에서는 응답을 빠르게 받는다. 서버 측에서는 요청 처리가 진정으로 종료된 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BackgroundTasks와 일반 라우트 함수의 실행 순서&lt;/h3&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;&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;/li&gt;
&lt;li&gt;응답 객체 생성&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답이 클라이언트에 전송 완료&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;그제야 BackgroundTasks 콜백 실행&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;응답 전이 아니라 응답 후이다. 따라서 백그라운드 작업이 라우트 응답 시간을 늘리지는 않는다. 다만 이것이 후술할 문제의 원인이 된다. &quot;응답 종료 후에 시작&quot;이라는 사실은, 결국 그 사이에 서버가 한 번이라도 흔들리면 해당 작업은 그대로 사라진다는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&quot;비동기&quot;라는 단어가 만든 오해부터 해소하자&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SKXCX/dJMcahjX6Mz/E032oSZt52nSIhbwaNJkZK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SKXCX/dJMcahjX6Mz/E032oSZt52nSIhbwaNJkZK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SKXCX/dJMcahjX6Mz/E032oSZt52nSIhbwaNJkZK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSKXCX%2FdJMcahjX6Mz%2FE032oSZt52nSIhbwaNJkZK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;840&quot; height=&quot;1200&quot; data-origin-width=&quot;840&quot; data-origin-height=&quot;1200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://djangostars.com/blog/asynchronous-programming-in-python-asyncio/&quot;&gt;Software Development Blog &amp;amp; IT Tech Insights | Django Stars&lt;/a&gt;&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;한국어로 &quot;비동기&quot;라고 표기하다 보니 사람들이 자꾸 &lt;b&gt;별도 프로세스&lt;/b&gt;와 혼동한다. 그렇지 않다. 결코 아니다.&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;FastAPI 백그라운드 태스크는 다음 환경에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동일한 파이썬 프로세스&lt;/li&gt;
&lt;li&gt;동일한 이벤트 루프 (또는 그 프로세스 내부의 스레드풀)&lt;/li&gt;
&lt;li&gt;동일한 메모리 공간&lt;/li&gt;
&lt;li&gt;동일한 DB 커넥션 풀&lt;/li&gt;
&lt;li&gt;동일한 CPU 타임&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://docs.celeryq.dev/&quot;&gt;Celery&lt;/a&gt;는 진정으로 별개의 프로세스(워커)에서 실행된다. 그것이 진정으로 분리된 백그라운드이다. FastAPI BackgroundTasks는 분리된 워커가 아니라 &quot;응답을 보낸 뒤 한 번 더 실행해 주겠다&quot; 수준에 불과하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;async def 콜백은 어디에서 실행되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;async def 콜백은 &lt;b&gt;동일한 이벤트 루프&lt;/b&gt;에서 await으로 실행된다. 여기에 무거운 작업을 넣으면 어떻게 되는가? 이벤트 루프가 그 작업이 끝날 때까지 다른 비동기 요청 처리를 모두 미룬다. 다른 사용자들의 응답이 모두 지연된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777382255878&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;async def heavy_async_task():
    # CPU 무거운 연산을 async def에 넣은 경우
    result = sum(i * i for i in range(10**8))
    return result

@app.post(&quot;/process/&quot;)
async def process(background_tasks: BackgroundTasks):
    background_tasks.add_task(heavy_async_task)
    return {&quot;queued&quot;: True}
&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;이 코드는 위험하다. async def로 선언했지만 실제로는 await 포인트가 없는 CPU bound 작업이라 이벤트 루프를 통째로 점유한다. 후속 요청들이 줄줄이 지연된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;def(동기) 콜백의 경우는 어떠한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;def로 선언하면 starlette이 자동으로 run_in_threadpool을 통해 스레드풀에 위임한다. 기본 스레드풀 사이즈는 anyio 기준으로 &lt;b&gt;40개&lt;/b&gt;이다. 40개가 모두 차면 그 다음 작업은 대기 큐에 쌓인다. 라우트 핸들러도 동기 함수일 경우 동일한 스레드풀을 사용하므로, 백그라운드가 풀을 모두 점유하면 일반 요청도 차단된다.&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;정리하면 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;콜백 타입&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;실행 위치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;무거운 작업 투입 시&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`async def`&lt;/td&gt;
&lt;td&gt;동일한 이벤트 루프&lt;/td&gt;
&lt;td&gt;모든 비동기 요청 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`def`&lt;/td&gt;
&lt;td&gt;스레드풀 (기본 40)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어느 쪽이든 &lt;b&gt;메인 앱과 자원을 공유한다&lt;/b&gt;는 본질은 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 메인 앱에 영향이 없는가? &amp;rarr; 100% 영향이 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;678&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fsojc/dJMcahjX6MD/48BAnKW2R98btPlOf5Plk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fsojc/dJMcahjX6MD/48BAnKW2R98btPlOf5Plk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fsojc/dJMcahjX6MD/48BAnKW2R98btPlOf5Plk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFsojc%2FdJMcahjX6MD%2F48BAnKW2R98btPlOf5Plk0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1312&quot; height=&quot;678&quot; data-origin-width=&quot;1312&quot; data-origin-height=&quot;678&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.manageengine.com/network-monitoring/cpu-memory-disk.html&quot;&gt;ManageEngine&lt;/a&gt;&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;&quot;비동기로 동작하므로 메인 앱에 영향이 없을 것&quot;이라는 생각은 매우 흔한 착각이다. 영향은 반드시 존재한다. 세 가지 측면 모두에서 영향이 있다.&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;&lt;b&gt;메모리 공유&lt;/b&gt;: 백그라운드에서 큰 파일을 처리하거나 큰 데이터를 보유하면 그 메모리는 메인 앱 프로세스의 메모리이다. OOM이 발생하면 라우트 핸들러도 함께 종료된다.&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;&lt;b&gt;DB 커넥션 풀 공유&lt;/b&gt;: 백그라운드에서 DB 작업을 실행하며 풀 사이즈를 모두 점유하면 다른 라우트가 커넥션을 받지 못한다. 라우트 응답이 모두 느려지거나 타임아웃이 발생한다.&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;&lt;b&gt;CPU/이벤트 루프 공유&lt;/b&gt;: 위에서 기술한 그대로이다. async def인 경우 이벤트 루프, def인 경우 스레드풀 포화로 영향이 발생한다.&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;실제 장애 시나리오 사례는 다음과 같다. 회원이 사진을 업로드할 때 응답을 빠르게 주기 위해 이미지 리사이즈를 BackgroundTasks에 위임했다. 응답 시간이 200ms로 단축되어 좋아 보였다. 그러나 동시 업로드 10건이 들어오자 이미지 처리가 이벤트 루프를 모두 점유하여 다른 페이지 로딩이 5초씩 걸렸다. &quot;응답이 빨라진 것처럼&quot; 보였지만, 실제로는 부담을 다른 요청들에 전가한 것이다.&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;이러한 패턴이 너무 흔하여 운영에 들어가면 거의 반드시 한 번은 장애가 발생한다. 백그라운드라는 단어를 보고 안심해서는 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;서버가 종료되면 백그라운드 작업은 어떻게 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;1684&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgmLF3/dJMcagFl2xn/ufF5aqMTH85BZFPKpHVkKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgmLF3/dJMcagFl2xn/ufF5aqMTH85BZFPKpHVkKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgmLF3/dJMcagFl2xn/ufF5aqMTH85BZFPKpHVkKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgmLF3%2FdJMcagFl2xn%2FufF5aqMTH85BZFPKpHVkKK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1190&quot; height=&quot;1684&quot; data-origin-width=&quot;1190&quot; data-origin-height=&quot;1684&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://learnkube.com/graceful-shutdown&quot;&gt;LearnKube (170KB)&lt;/a&gt;&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;결론부터 말한다. &lt;b&gt;함께 종료된다&lt;/b&gt;. &quot;끝까지 처리된다&quot;는 거짓이다. 시나리오별로 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 1: Ctrl+C / SIGTERM (정상 종료)&lt;/h3&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;a href=&quot;https://www.uvicorn.org/&quot;&gt;uvicorn&lt;/a&gt;은 SIGTERM을 받으면 graceful shutdown을 시도한다. 진행 중인 HTTP 요청은 종료될 때까지 대기한다. 여기까지는 양호하다.&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;다만 BackgroundTasks는 응답 전송이 끝난 다음에 시작되는 것이라 다음과 같이 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;응답을 모두 보냈고 백그라운드 시작 직전에 SIGTERM이 들어옴 &amp;rarr; 시작도 하지 못하고 사라진다&lt;/li&gt;
&lt;li&gt;백그라운드 진행 중에 SIGTERM이 들어옴 &amp;rarr; graceful timeout 안에 종료되지 않으면 그대로 종료된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uvicorn 자체의 --timeout-graceful-shutdown은 사실 기본값이 None(무제한 대기)이다. 다만 운영 환경에서는 거의 외부에서 타임아웃이 걸린다. 쿠버네티스는 terminationGracePeriodSeconds 기본 30초가 지나면 SIGKILL을 발사하고, systemd TimeoutStopSec, 두 번째 SIGTERM/SIGINT도 즉시 종료 트리거이다. 따라서 진행 중이던 백그라운드가 길어지면 그대로 잘린다. 클라이언트는 200을 받았으나 실제 후처리는 중간에 끊긴 상태로 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 2: SIGKILL / OOM Kill (비정상 종료)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kill -9나 OOM Killer가 발동하면 프로세스가 통째로 즉사한다. graceful 같은 것은 없다. 진행 중인 백그라운드 작업도 그대로 모두 사라진다.&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;진짜 위험한 사례를 들어 본다. 결제 후 영수증 메일 발송을 BackgroundTasks에 위임했는데 SIGKILL을 맞았다. 클라이언트는 결제 성공 200을 받았다. 메일은 발송되지 않았다. DB에는 결제 완료로 기록되었다. 사용자 입장에서는 &quot;결제는 됐다는데 영수증이 오지 않는다&quot;가 된다. 데이터 불일치이다. CS 대란이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 3: uvicorn --reload (개발 환경의 함정)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;uvicorn main:app --reload로 띄워 두고 코드를 한 줄 수정하면 워커가 재시작된다. 진행 중이던 백그라운드 작업은 어떻게 되는가? 그대로 함께 종료된다. 개발 중에는 이것이 별 문제 없어 보인다. 그러나 이를 모르고 &quot;잘 동작하는군&quot;이라고 판단하며 운영에 진입하면 동일한 패턴이 SIGTERM/SIGKILL에서 다시 터진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오 4: Gunicorn 멀티 워커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영에서는 보통 gunicorn -w 4 -k uvicorn.workers.UvicornWorker 같은 형태로 워커를 4개 띄운다. 이때 백그라운드 태스크는 &lt;b&gt;그 요청을 받은 워커 프로세스 내부에서만&lt;/b&gt; 실행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;워커 A가 요청을 받음 &amp;rarr; 백그라운드도 워커 A에 등록된다&lt;/li&gt;
&lt;li&gt;워커 A가 종료됨 &amp;rarr; A의 백그라운드는 모두 사라진다&lt;/li&gt;
&lt;li&gt;워커 B, C, D는 아무것도 알지 못한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링 측면에서 &quot;어느 워커에서 어떤 백그라운드가 살아 있는지&quot; 추적하기는 거의 불가능하다. 워커 간 분산 처리도 불가하다. 분산 처리를 하려면 외부 큐 시스템이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 검증한 결과 &amp;mdash; 30초면 재현 가능하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽고도 &quot;정말 그러한가?&quot;라는 의문이 남을 수 있다. 그래서 직접 돌려 보았다. FastAPI 0.135.3 / uvicorn 0.43.0 / Python 3.13 환경에서 다음 한 파일이면 핵심 주장 3개를 모두 재현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777382255880&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import os, time, asyncio
from fastapi import FastAPI, BackgroundTasks

app = FastAPI()
LOG = &quot;bg.log&quot;

def log(msg: str):
    line = f&quot;{time.time():.3f} pid={os.getpid()} {msg}&quot;
    with open(LOG, &quot;a&quot;, encoding=&quot;utf-8&quot;) as f:
        f.write(line + &quot;\n&quot;)

async def slow_async(tag: str, secs: int = 6):
    log(f&quot;BG-START tag={tag}&quot;)
    for i in range(secs):
        await asyncio.sleep(1)
        log(f&quot;BG-TICK  tag={tag} i={i}&quot;)
    log(f&quot;BG-END   tag={tag}&quot;)

@app.post(&quot;/bg/{tag}&quot;)
async def bg(tag: str, bt: BackgroundTasks):
    log(f&quot;ROUTE tag={tag}&quot;)
    bt.add_task(slow_async, tag)
    return {&quot;pid&quot;: os.getpid(), &quot;tag&quot;: tag}
&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;uvicorn bg_app:app --port 8765로 띄우고 curl -X POST http://127.0.0.1:8765/bg/E1을 한 번 호출하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 1 &amp;mdash; 정말 같은 프로세스에서 동작하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 PID는 31516이었다. 로그 결과는 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;이벤트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;시각 (epoch)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;PID&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라우트 진입 (`ROUTE tag=E1`)&lt;/td&gt;
&lt;td&gt;259.504&lt;/td&gt;
&lt;td&gt;**31516**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백그라운드 시작 (`BG-START`)&lt;/td&gt;
&lt;td&gt;259.505&lt;/td&gt;
&lt;td&gt;**31516**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;백그라운드 종료 (`BG-END`)&lt;/td&gt;
&lt;td&gt;265.563&lt;/td&gt;
&lt;td&gt;**31516**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;별도 호출한 `/ping`&lt;/td&gt;
&lt;td&gt;&amp;mdash;&lt;/td&gt;
&lt;td&gt;**31516**&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;PID 31516 동일&lt;/b&gt;. &quot;별도 프로세스가 아니다&quot;가 실측으로 확정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 2 &amp;mdash; 응답은 즉시 반환되지만 서버는 계속 일하고 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;curl 기준 라우트 응답 수신까지 &lt;b&gt;0.36초&lt;/b&gt;가 걸렸다. 클라이언트 입장에서는 0.36초 만에 끝난 요청이다. 그러나 서버 측에서는 그 후로 &lt;b&gt;6초간&lt;/b&gt; BG-TICK 로그가 1초 간격으로 6번 더 찍혔다. &quot;응답이 빨라진 것&quot;이 아니라 &quot;응답을 보낸 뒤에도 같은 프로세스에서 계속 일하고 있는 것&quot;이다. 동시 요청이 몰리면 이 6초가 다른 요청들에 부담으로 전가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 3 &amp;mdash; 프로세스가 종료되면 백그라운드는 즉사한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/bg/KILL을 호출하고 2초 기다린 뒤 서버 프로세스를 강제 종료(kill -9 상당)했다. 결과는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777382255881&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;298.811  BG-START tag=KILL
299.820  BG-TICK  i=0
300.839  BG-TICK  i=1
301.841  BG-TICK  i=2     &amp;larr; 여기서 프로세스 강제 종료
   ✗     (i=3, 4, 5 없음)
   ✗     BG-END 없음
&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;6틱 중 3틱만 기록되고 &lt;b&gt;BG-END는 끝내 찍히지 않았다&lt;/b&gt;. 클라이언트는 이미 200 응답을 받은 상태이다. 즉, 결제는 성공했지만 영수증 메일은 발송되지 않은 상태가 그대로 재현된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;검증 결과 요약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;결과&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;별도 프로세스가 아님, 같은 프로세스&lt;/td&gt;
&lt;td&gt;✅ PID 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;응답 후에 실행됨&lt;/td&gt;
&lt;td&gt;✅ 라우트 0.36s 종료, BG는 6s간 별도 진행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메인 종료 시 BG도 같이 종료&lt;/td&gt;
&lt;td&gt;✅ 3틱 만에 잘림, BG-END 미도달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라우트 응답시간에 영향 없음&lt;/td&gt;
&lt;td&gt;✅ 0.36s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 동작 3가지 모두 30초 만에 재현된다. 의심스럽다면 직접 돌려 보면 된다. 운영 진입 전에 한 번은 이 실험을 거쳐 보아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 BackgroundTasks는 언제 사용해도 되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 비판적으로 다루었으나 그래도 BackgroundTasks가 무용한 것은 아니다. 적절한 자리에서는 진정으로 편리하다. 체크리스트로 정리한다.&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;&lt;b&gt;사용해도 되는 경우&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;짧고 빠른 작업 (수 ms ~ 길어야 1~2초)&lt;/li&gt;
&lt;li&gt;실패해도 큰 문제가 발생하지 않는 작업 (분석 핑, 비크리티컬 로그)&lt;/li&gt;
&lt;li&gt;응답 속도를 약간 단축하고 싶으나 외부 워커를 띄우기는 부담스러운 경우&lt;/li&gt;
&lt;li&gt;작업 자체가 idempotent하여 재실행에 부담이 없는 경우&lt;/li&gt;
&lt;/ul&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;b&gt;사용해서는 안 되는 경우&lt;/b&gt;:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;5초가 넘는 작업&lt;/li&gt;
&lt;li&gt;실패해서는 안 되는 결제 후처리, 결제 영수증, 중요 알림&lt;/li&gt;
&lt;li&gt;재시도 / 결과 추적 / 스케줄링이 필요한 작업&lt;/li&gt;
&lt;li&gt;CPU 무거운 작업 (이미지/영상 처리, ML 추론)&lt;/li&gt;
&lt;li&gt;분산 워커로 분산 처리해야 하는 작업&lt;/li&gt;
&lt;li&gt;트랜잭션 보장이 필요한 작업&lt;/li&gt;
&lt;/ul&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;b&gt;&quot;이것이 중간에 끊겨도 정말 괜찮은가?&quot;&lt;/b&gt; 한 번 자문해 보고, &quot;다소 불편하지만 큰 문제는 아니다&quot; 수준이라면 BackgroundTasks를 사용해도 된다. &quot;이것이 끊기면 큰일이 난다&quot; 수준이라면 무조건 외부 워커이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 진정한 백그라운드는 어떻게 구현하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;968&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GHpEy/dJMcahjX6My/fhwHPAhdcAmLONEgGUlXV1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GHpEy/dJMcahjX6My/fhwHPAhdcAmLONEgGUlXV1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GHpEy/dJMcahjX6My/fhwHPAhdcAmLONEgGUlXV1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGHpEy%2FdJMcahjX6My%2FfhwHPAhdcAmLONEgGUlXV1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1400&quot; height=&quot;968&quot; data-origin-width=&quot;1400&quot; data-origin-height=&quot;968&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://levelup.gitconnected.com/asynchronous-tasks-in-python-with-celery-rabbitmq-redis-480f6e506d76&quot;&gt;Level Up Coding - GitConnected (92KB)&lt;/a&gt;&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;진정으로 메인 앱과 분리된 백그라운드가 필요하다면 &lt;b&gt;외부 워커 + 메시지 브로커&lt;/b&gt; 조합이 정답이다. 구조는 대략 다음과 같다.&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;FastAPI는 작업을 큐(Redis, RabbitMQ 등)에 적재하기만 하고, 별도로 띄운 워커 프로세스가 큐에서 꺼내 실행한다. FastAPI가 종료되어도 워커는 살아 있고, 워커가 종료되어도 큐에 쌓인 작업은 살아 있다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;솔루션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;FastAPI 궁합&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Celery&lt;/td&gt;
&lt;td&gt;대장. 무겁다. 모든 기능이 가능하다. Redis/RabbitMQ&lt;/td&gt;
&lt;td&gt;양호 (단 sync 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RQ&lt;/td&gt;
&lt;td&gt;가볍다. Redis 전용. 코드가 단순하다&lt;/td&gt;
&lt;td&gt;양호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ARQ&lt;/td&gt;
&lt;td&gt;async 네이티브. Redis.&lt;/td&gt;
&lt;td&gt;매우 양호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dramatiq&lt;/td&gt;
&lt;td&gt;Celery 대체용. 빠르고 간결하다&lt;/td&gt;
&lt;td&gt;양호&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taskiq&lt;/td&gt;
&lt;td&gt;신상. async 네이티브. FastAPI 풍 API&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Celery&lt;/b&gt;: 매우 유명하다. 기능이 풍부하다. 다만 무겁고 설정이 복잡하며 sync 기반이라 FastAPI async와 다소 어색하다. 그럼에도 큰 워크플로우에는 이것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RQ&lt;/b&gt;: Redis 하나로 가볍게 시작하고 싶을 때 적합하다. 코드가 진정으로 단순하다. 대신 기능이 많지는 않다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ARQ&lt;/b&gt;: FastAPI와 async 호환성이 양호하다. Redis를 사용하여 운영 부담이 적다. 처음 도입하기에 적합하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dramatiq&lt;/b&gt;: Celery를 사용하지 않으면서 그 정도 기능이 필요할 때 적합하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Taskiq&lt;/b&gt;: 가장 최신이다. async 네이티브 + FastAPI 스타일 API이다. 신생이라 레퍼런스가 적다는 것이 단점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 추천은 다음과 같다. FastAPI 프로젝트라면 &lt;b&gt;ARQ나 Taskiq부터&lt;/b&gt; 검토하면 된다. 워크플로우가 진정으로 복잡해지면 그때 Celery로 전환하는 것이 맞다. 처음부터 Celery를 도입하면 셋업 부담만 커진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FastAPI BackgroundTasks 실전 권장 패턴&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777382255883&quot; class=&quot;python&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;@app.post(&quot;/event/&quot;)
async def track_event(payload: dict, background_tasks: BackgroundTasks):
    # 빠른 후처리: BackgroundTasks 적합
    background_tasks.add_task(send_analytics_ping, payload)
    return {&quot;ok&quot;: True}

@app.post(&quot;/order/&quot;)
async def create_order(order: dict):
    save_order(order)
    # 무거운 작업: 외부 워커로
    arq_pool.enqueue_job(&quot;process_order&quot;, order[&quot;id&quot;])
    return {&quot;order_id&quot;: order[&quot;id&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;추가로 고려할 사항은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Idempotency key&lt;/b&gt;: 외부 워커를 사용할 때 동일한 작업이 두 번 실행되어도 결과가 동일하도록 만든다. 재시도 안전성의 핵심이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DB 큐 패턴&lt;/b&gt;: 진정으로 절대 잃어서는 안 되는 작업은 DB에 작업 row를 적재하고 워커가 폴링하는 패턴이 가장 안전하다. 메시지 브로커조차 신뢰할 수 없을 때 사용하는 패턴이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링&lt;/b&gt;: Celery는 &lt;a href=&quot;https://flower.readthedocs.io/&quot;&gt;Flower&lt;/a&gt;, arq는 자체 CLI, 공통적으로는 Prometheus exporter로 큐 길이/처리 시간을 확인하면 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로컬 vs 운영&lt;/b&gt;: --reload로 잘 돌던 것이 운영에서 SIGTERM을 만나면 어떻게 되는지 한 번은 직접 시뮬레이션을 돌려 보아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;FastAPI 백그라운드 태스크, 운영 진입 전에 점검할 3가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 3줄 요약이다.&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;FastAPI 백그라운드 태스크는 별도 프로세스가 아니다.&lt;/b&gt; 동일한 앱의 이벤트 루프 또는 스레드풀에서 동작한다. &quot;비동기&quot;라는 단어를 보고 분리되었다고 판단해서는 안 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메인 앱이 종료되면 백그라운드도 함께 종료된다.&lt;/b&gt; SIGTERM, SIGKILL, --reload, 워커 재시작 모두 마찬가지이다. &quot;끝까지 처리됨&quot; 같은 보장은 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;진정한 보장이 필요하면 ARQ / Taskiq / Celery 같은 외부 워커 + 브로커 조합을 사용해야 한다.&lt;/b&gt; BackgroundTasks는 짧고 잃어도 무방한 작업 전용이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 운영 중인 프로젝트를 한 번 점검해 보고, 5초가 넘는 작업이나 실패해서는 안 되는 작업이 BackgroundTasks에 들어가 있다면 ARQ나 Celery로 이전하는 것을 권장한다. &lt;a href=&quot;https://fastapi.tiangolo.com/tutorial/background-tasks/&quot;&gt;FastAPI 공식 문서의 Background Tasks&lt;/a&gt; 페이지에도 &quot;큰 작업은 Celery 같은 도구를 사용하라&quot;고 명시되어 있다. 공식이 그렇게 말한다는 것은 진정으로 그렇게 해야 한다는 뜻이다. 한 번 데인 후에 옮기면 늦으니, 미리 분리하는 것이 맞다.&lt;/p&gt;</description>
      <category>Backend</category>
      <category>BackgroundTasks 동작 원리</category>
      <category>FastAPI BackgroundTasks</category>
      <category>FastAPI Celery 차이</category>
      <category>FastAPI 백그라운드 종료</category>
      <category>FastAPI 비동기 백그라운드</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/388</guid>
      <comments>https://yscho03.tistory.com/388#entry388comment</comments>
      <pubDate>Tue, 28 Apr 2026 22:18:42 +0900</pubDate>
    </item>
    <item>
      <title>AI 회사들이 GPU 서버에 집중하는 이유, 코어 수와 성능까지 분석한다</title>
      <link>https://yscho03.tistory.com/387</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;엔비디아 주가가 왜 그렇게 치솟는지 궁금했던 적이 있는가? 답은 한 줄이다. &lt;b&gt;AI 학습과 추론에 GPU 서버가 절대적으로 필요하기 때문&lt;/b&gt;이다. 챗GPT, 클로드, 제미나이를 만드는 빅테크들은 한 장에 5천만 원에 달하는 H100 GPU를 수십만 대씩 확보하고 있다. 메타는 2024년에 H100 35만 대를 확보하겠다고 발표한 바 있다.&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;다만 진짜 의문은 따로 있다. &lt;b&gt;CPU 역시 코어가 많고 빠른데, 왜 굳이 GPU를 쓰는가?&lt;/b&gt; 그리고 같은 GPU임에도 게이밍용 RTX 4090과 데이터센터용 H100을 따로 만드는 이유는 무엇인가? 이 글에서는 GPU 서버가 AI에서 압도적인 이유, 코어 수가 실제로 어느 정도 차이가 나는지, 성능 격차가 몇 배에 달하는지를 분석한다. 끝까지 읽으면 엔비디아 주가가 급등한 배경 또한 자연스럽게 이해된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;결국 AI는 행렬 곱셈의 반복이다 &amp;mdash; 그래서 GPU가 압도적이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tEm16/dJMcaipBXrR/wawuHNvMeFhwWFMdyrGkZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tEm16/dJMcaipBXrR/wawuHNvMeFhwWFMdyrGkZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tEm16/dJMcaipBXrR/wawuHNvMeFhwWFMdyrGkZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtEm16%2FdJMcaipBXrR%2FwawuHNvMeFhwWFMdyrGkZ0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;250&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://community.deeplearning.ai/t/matrix-multiplication-in-neural-network/685083&quot;&gt;community.deeplearning.ai&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;딥러닝이 실제로 무엇을 계산하는지 분석하면&lt;/h3&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;b&gt;거대한 숫자 행렬을 곱하고 또 곱하는 일&lt;/b&gt;이다. 신경망 레이어 하나는 행렬 곱셈 한 번에 해당한다. 100개 레이어로 구성된 모델은 행렬 곱셈을 100번 수행한다. 트랜스포머의 어텐션 메커니즘은 행렬 곱셈의 정점이라고 볼 수 있다.&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;GPT-4 같은 모델은 파라미터가 1조 개 단위에 이른다. 한 번 학습할 때마다 행렬 곱셈을 수십조 번 수행해야 한다. 단순한 작업이지만 분량이 압도적으로 많은 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CPU는 박사 8명, GPU는 알바생 1만 명이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 GPU가 AI에서 압도적인 핵심 이유다. 비유로 풀어내면 이해가 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CPU 코어&lt;/b&gt; = 박사 학위자 8~24명. 복잡한 문제를 빠르게 풀어낸다. 분기 처리, 조건문, OS 관리 모두 능숙하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPU 코어&lt;/b&gt; = 알바생 1만 명. 한 명당 능력은 박사보다 훨씬 떨어진다. 다만 단순 곱셈/덧셈을 1만 명이 동시에 처리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 일 하나는 박사가 빠르다. 다만 단순 작업 1만 개를 동시에 처리해야 한다면 어떨까? 박사 8명이 1250개씩 나누어 처리하는 것보다 알바생 1만 명이 한 개씩 동시에 끝내는 편이 압도적으로 빠르다. &lt;b&gt;AI 학습은 단순 행렬 연산의 무한 반복&lt;/b&gt;이므로, 알바생 1만 명에 해당하는 GPU가 우위를 차지할 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SIMD/SIMT를 한 줄로 정리하면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 컴퓨터공학 용어로는 SIMT(Single Instruction, Multiple Threads)라고 부른다. 같은 명령어(&quot;이 두 숫자를 곱하라&quot;)를 여러 데이터에 한 번에 적용하는 방식이다. CPU는 한 코어가 한 명령어를 한 데이터에 적용한다. GPU는 한 명령어를 수천 개 데이터에 동시에 적용한다. 이러한 구조 차이가 AI 워크로드에서 거대한 격차를 만들어낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPU 코어 수를 비교하면 차이가 압도적이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDI6AV/dJMcaipBXrX/9bkKMYnUoi3uM2a90cNJZ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDI6AV/dJMcaipBXrX/9bkKMYnUoi3uM2a90cNJZ1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDI6AV/dJMcaipBXrX/9bkKMYnUoi3uM2a90cNJZ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDI6AV%2FdJMcaipBXrX%2F9bkKMYnUoi3uM2a90cNJZ1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;800&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;800&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.cnet.com/pictures/see-nvidias-h100-hopper-chip-up-close/&quot;&gt;www.cnet.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;일반 소비자 GPU 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 게이밍 GPU와 CPU의 코어 수부터 비교해 보았다. 숫자만 봐도 격차가 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인텔 i9-14900K&lt;/b&gt; (2024 플래그십 CPU): 24코어 (성능 코어 8 + 효율 코어 16)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AMD 라이젠 9 7950X&lt;/b&gt;: 16코어 32스레드&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔비디아 RTX 4090&lt;/b&gt; (게이밍 플래그십): &lt;b&gt;CUDA 코어 16,384개 + 텐서 코어 512개&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;i9 24코어 대 RTX 4090 16,384 CUDA 코어. &lt;b&gt;약 600배의 차이&lt;/b&gt;다. 농담이 아니다. 그래서 게임에서 광선추적(Ray Tracing) 같은 병렬 작업을 GPU가 담당하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터센터 GPU는 또 다른 차원이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 게이밍 GPU는 약과에 불과하다. 데이터센터 전용 GPU로 가면 코어 수가 또 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;엔비디아 H100 SXM&lt;/b&gt;: CUDA 코어 16,896개 + 텐서 코어 528개 + FP8/Transformer Engine 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔비디아 B200&lt;/b&gt; (블랙웰, 2024년 출시): H100 대비 학습 성능 약 2.5배, 추론 성능 약 5배&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AMD MI300X&lt;/b&gt;: 컴퓨트 유닛(CU) 304개 + HBM3 192GB&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H100은 단순 코어 수보다 &lt;b&gt;텐서 코어&lt;/b&gt;가 핵심이다. 텐서 코어는 행렬 연산 전용 가속기로, 일반 CUDA 코어보다 행렬 곱셈을 훨씬 빠르게 수행한다. AI 학습에 극단적으로 최적화된 칩인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코어 수 + 성능 비교표&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;칩셋&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;종류&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주요 코어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;FLOPS (FP16/FP32)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;메모리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;메모리 대역폭&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;TDP&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;가격대&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인텔 i9-14900K&lt;/td&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;24코어&lt;/td&gt;
&lt;td&gt;약 1.4 TFLOPS (FP32)&lt;/td&gt;
&lt;td&gt;시스템 RAM (DDR5)&lt;/td&gt;
&lt;td&gt;약 90 GB/s&lt;/td&gt;
&lt;td&gt;253W&lt;/td&gt;
&lt;td&gt;80만원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTX 4090&lt;/td&gt;
&lt;td&gt;게이밍 GPU&lt;/td&gt;
&lt;td&gt;CUDA 16,384 + 텐서 512&lt;/td&gt;
&lt;td&gt;83 TFLOPS (FP32)&lt;/td&gt;
&lt;td&gt;GDDR6X 24GB&lt;/td&gt;
&lt;td&gt;1,008 GB/s&lt;/td&gt;
&lt;td&gt;450W&lt;/td&gt;
&lt;td&gt;250만원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H100 SXM&lt;/td&gt;
&lt;td&gt;데이터센터 GPU&lt;/td&gt;
&lt;td&gt;CUDA 16,896 + 텐서 528&lt;/td&gt;
&lt;td&gt;989 TFLOPS (TF32 텐서) / 1,979 TFLOPS (FP16 텐서)&lt;/td&gt;
&lt;td&gt;HBM3 80GB&lt;/td&gt;
&lt;td&gt;3,350 GB/s&lt;/td&gt;
&lt;td&gt;700W&lt;/td&gt;
&lt;td&gt;4,500만원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B200&lt;/td&gt;
&lt;td&gt;데이터센터 GPU&lt;/td&gt;
&lt;td&gt;(H100 대비 2배+)&lt;/td&gt;
&lt;td&gt;약 2,250 TFLOPS (FP16)&lt;/td&gt;
&lt;td&gt;HBM3e 192GB&lt;/td&gt;
&lt;td&gt;8,000 GB/s&lt;/td&gt;
&lt;td&gt;1,000W&lt;/td&gt;
&lt;td&gt;5,500만원~&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;i9 1 TFLOPS 대 H100 989 TFLOPS &amp;mdash; 약 1000배의 차이&lt;/b&gt;다. 행렬 연산에 한정된 성능이라고 해도 AI 워크로드에서는 이것이 곧 학습 속도에 직결된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;코어&quot;라는 단어가 같은 것이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 짚고 넘어갈 지점이 있다. CPU 코어와 GPU 코어는 &lt;b&gt;이름만 같을 뿐 완전히 다른 물건&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CPU 코어&lt;/b&gt;: 분기 예측, 캐시, OS 호환, 복잡한 명령어를 모두 처리하는 종합 처리 유닛이다. 코어 하나가 사실상 작은 컴퓨터 수준이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPU 코어 (CUDA 코어)&lt;/b&gt;: 부동소수점 곱셈/덧셈만 빠르게 수행하는 단순 계산 유닛이다. 코어 하나가 사실상 계산기 한 대 수준이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &quot;GPU 코어 1만 개 = CPU 코어 1만 개&quot;라고 이해해서는 안 된다. &lt;b&gt;단순 작업에서는 GPU가 우위, 복잡한 작업에서는 CPU가 우위&lt;/b&gt;다. AI는 단순 작업이 압도적으로 많은 영역이므로 GPU가 정답인 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPU 대 CPU 성능 차이는 비현실적이다 &amp;mdash; AI 워크로드 실측 수치를 정리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC4mtO/dJMcajvf3mE/RX6L3IuhCedZF22pvskcA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC4mtO/dJMcajvf3mE/RX6L3IuhCedZF22pvskcA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC4mtO/dJMcajvf3mE/RX6L3IuhCedZF22pvskcA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC4mtO%2FdJMcajvf3mE%2FRX6L3IuhCedZF22pvskcA1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;796&quot; height=&quot;572&quot; data-origin-width=&quot;796&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://infoupdate.org/cpu-vs-gpu-performance-comparison-tool/&quot;&gt;infoupdate.org&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습(Training) 성능 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPT-3 학습에 투입된 GPU 자원이 공개되었는데, 수치를 보면 어이가 없을 정도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPT-3 학습&lt;/b&gt;: V100 GPU 약 1만 대 &amp;times; 한 달 = 약 3,640 PF-days (페타플롭스-일) 연산량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPT-4 추정치&lt;/b&gt;: H100 기준 수만 대 &amp;times; 수개월&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CPU로 학습한다면?&lt;/b&gt; 단순 환산하면 &lt;b&gt;수백 년 단위&lt;/b&gt;가 소요된다. 사실상 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜일까? 앞서 살펴보았듯 코어 수와 텐서 코어 가속에서 1000배의 차이가 나기 때문이다. 그래서 AI 회사들이 GPU 서버 없이는 큰 모델을 만들지 못한다. CPU만으로는 게임 자체가 성립하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추론(Inference) 성능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학습이 끝난 모델을 실제 서비스로 운영할 때(추론)도 GPU가 압도적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;LLM 토큰 생성 속도&lt;/b&gt;: GPU 대비 CPU는 약 50~200배 느리다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결정적 변수는 메모리 대역폭이다&lt;/b&gt;: HBM3 대역폭 3,350 GB/s 대 DDR5 대역폭 90 GB/s. &lt;b&gt;약 37배 차이&lt;/b&gt;다&lt;/li&gt;
&lt;li&gt;추론은 모델 가중치를 메모리에서 읽어오는 일이 병목이며, 메모리 대역폭이 곧 속도를 좌우한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;챗GPT 답변이 1초도 되지 않아 토큰을 쏟아내는 이유는 H100급 GPU에 HBM 메모리가 탑재되어 있기 때문이다. CPU 서버로 같은 모델을 돌리면 한 단어를 출력하는 데도 몇 초씩 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TFLOPS 기준으로 보면 직관적이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플롭스(FLOPS, 초당 부동소수점 연산 횟수) 기준으로 보면 다음과 같이 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;인텔 i9-14900K&lt;/b&gt;: 약 1 TFLOPS (FP32)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RTX 4090&lt;/b&gt;: 약 83 TFLOPS (FP32) &amp;rarr; CPU 대비 83배&lt;/li&gt;
&lt;li&gt;&lt;b&gt;H100 SXM&lt;/b&gt;: 약 989 TFLOPS (TF32 텐서, FP16은 1,979 TFLOPS) &amp;rarr; CPU 대비 약 1000배&lt;/li&gt;
&lt;li&gt;&lt;b&gt;B200&lt;/b&gt;: 약 2,250 TFLOPS (FP16) &amp;rarr; CPU 대비 약 2250배&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 1000배는 단순 수사가 아닌 실측 수치다. AI 학습이 GPU 서버 없이는 굴러가지 않는 이유가 바로 이것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;게이밍 GPU를 두고 굳이 서버급 GPU를 쓰는 이유는 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bfykRm/dJMcaipBXrS/rH5SRDCHUFoJ82KerIqPPk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bfykRm/dJMcaipBXrS/rH5SRDCHUFoJ82KerIqPPk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bfykRm/dJMcaipBXrS/rH5SRDCHUFoJ82KerIqPPk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbfykRm%2FdJMcaipBXrS%2FrH5SRDCHUFoJ82KerIqPPk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;980&quot; height=&quot;638&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.server-parts.eu/post/nvidia-dgx-h100-server&quot;&gt;server-parts.eu&lt;/a&gt;&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;여기서 또 다른 의문이 생긴다. RTX 4090도 코어 16,384개에 83 TFLOPS인데, 이를 1만 대 묶으면 H100 1만 대보다 저렴하지 않겠는가? 답은 &lt;b&gt;그렇지 않다&lt;/b&gt;이다. 이유를 분석해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 용량과 ECC&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 모델 자체가 비정상적으로 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPT-3 (175B)&lt;/b&gt;: FP16 기준 약 350GB 메모리가 필요하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LLaMA 70B&lt;/b&gt;: FP16 기준 약 140GB&lt;/li&gt;
&lt;li&gt;&lt;b&gt;RTX 4090 메모리&lt;/b&gt;: 24GB. &lt;b&gt;모델 자체를 올리지도 못한다&lt;/b&gt;.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;H100 메모리&lt;/b&gt;: 80GB. &lt;b&gt;B200&lt;/b&gt;: 192GB.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이밍 GPU 24GB로는 70B 모델 절반도 들어가지 않는다. 큰 모델을 학습하거나 추론하려면 메모리가 큰 GPU가 필수다. 그리고 &lt;b&gt;ECC (오류 정정 메모리)&lt;/b&gt; 또한 중요하다. 24/7 학습을 돌리는 환경에서 메모리 비트 하나가 뒤집히면 학습 전체가 어그러진다. 게이밍 GPU에는 ECC가 없다. 서버 GPU에는 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;NVLink와 인터커넥트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU 한 대로는 부족하므로 여러 대를 묶어서 사용해야 한다. 묶을 때 GPU끼리 데이터를 주고받는 속도가 중요해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;NVLink 5 (B200)&lt;/b&gt;: GPU간 1.8 TB/s&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PCIe Gen5 (게이밍 GPU)&lt;/b&gt;: 약 64 GB/s&lt;/li&gt;
&lt;/ul&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;b&gt;약 28배의 차이&lt;/b&gt;다. 게이밍 GPU 1만 대를 묶어봐야 GPU끼리 데이터를 주고받지 못해 학습이 막힌다. 그래서 데이터센터 GPU에는 NVLink가 별도로 탑재되어 있고, &lt;b&gt;InfiniBand&lt;/b&gt; 같은 초고속 네트워크가 랙 사이를 연결한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;24/7 데이터센터 운영 안정성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이밍 GPU는 하루 4~8시간 가동을 가정해 설계된다. 데이터센터 GPU는 &lt;b&gt;1년 365일 100% 풀로드&lt;/b&gt;를 가정해 설계된다. 부품 등급, 쿨링, 전력 회로가 완전히 다르다. 게이밍 GPU를 풀로드로 1년 돌리면 그대로 수명이 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;가격 차이가 벌어진 이유&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RTX 4090&lt;/b&gt;: 약 250만원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;H100 SXM&lt;/b&gt;: 약 4,500~5,000만원 (한 장에)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;B200&lt;/b&gt;: 약 5,500~7,000만원 (추정)&lt;/li&gt;
&lt;/ul&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;b&gt;한 장에 5천만 원짜리를 1만 대 구매하면 5,000억 원&lt;/b&gt;이다. 다만 빅테크들은 실제로 이 금액을 투자하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 AI 회사들이 GPU 서버에 도대체 얼마를 쏟는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BqtR3/dJMcaf7sOXC/JwhZDkwEEscDhJBHI7YOUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BqtR3/dJMcaf7sOXC/JwhZDkwEEscDhJBHI7YOUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BqtR3/dJMcaf7sOXC/JwhZDkwEEscDhJBHI7YOUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBqtR3%2FdJMcaf7sOXC%2FJwhZDkwEEscDhJBHI7YOUK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1200&quot; height=&quot;686&quot; data-origin-width=&quot;1200&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://logicstechnology.com/blogs/news/an-inside-look-at-elon-musks-100-000-gpu-ai-cluster-xai-colossus-unveiled&quot;&gt;logicstechnology.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메타, MS, 구글, xAI의 GPU 확보 경쟁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빅테크들의 H100 사재기는 2023~2024년 가장 뜨거웠던 산업 뉴스였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;메타&lt;/b&gt;: 2024년 말까지 H100 35만 대 확보 발표 (마크 저커버그)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;머스크의 xAI&lt;/b&gt;: 콜로서스 데이터센터 GPU 10만 대 클러스터 구축&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MS&lt;/b&gt;: 오픈AI 학습용으로 H100 수십만 대 확보&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구글&lt;/b&gt;: 자체 TPU와 엔비디아 GPU 병행 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H100 한 장이 5천만 원이라면 &lt;b&gt;10만 대는 5조 원&lt;/b&gt;에 해당한다. 머스크의 콜로서스 한 곳이 5조 원짜리 시설이라는 의미다. 클러스터 한 개에 수조 원이 투입되는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;전력 소비 또한 비현실적인 수준이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU 1만 대 클러스터는 메가와트급 발전소 한 개에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;H100 한 대&lt;/b&gt;: 700W (소비 전력)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;B200 한 대&lt;/b&gt;: 1,000W&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPU 1만 대 클러스터&lt;/b&gt;: 7~10 MW&lt;/li&gt;
&lt;li&gt;&lt;b&gt;소도시 한 동네의 전력 소비량과 유사하다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 최근 빅테크들이 &quot;데이터센터 옆에 원전을 짓겠다&quot;는 발표를 내놓는 것이다. MS는 쓰리마일아일랜드 원전 재가동 계약을 체결했고, 구글은 SMR(소형 모듈 원자로) 도입을 추진 중이다. &lt;b&gt;AI 인프라 = 전력 인프라&lt;/b&gt;라는 등식이 실제로 성립한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CUDA 락인 문제 &amp;mdash; 엔비디아의 진짜 해자&lt;/h3&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;b&gt;AMD GPU가 더 저렴한데 왜 사용하지 않는가?&lt;/b&gt; AMD MI300X도 H100과 스펙이 비슷하고 메모리(192GB)는 오히려 더 크다. 가격도 더 저렴하다. 다만 AI 회사들의 90% 이상은 엔비디아를 쓴다.&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;답은 &lt;b&gt;CUDA 생태계 락인&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파이토치, 텐서플로우, JAX 등 모든 AI 프레임워크가 CUDA 기반으로 최적화되어 있다&lt;/li&gt;
&lt;li&gt;AMD ROCm은 호환성 문제, 버그, 라이브러리 부족으로 갈아타는 비용이 비현실적으로 크다&lt;/li&gt;
&lt;li&gt;엔지니어를 구하기도 어렵다 (CUDA 전문가만 풍부하다)&lt;/li&gt;
&lt;/ul&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;b&gt;소프트웨어 생태계&lt;/b&gt;다. 칩만 잘 만들어 시총 4조 달러를 찍은 것이 아니라, CUDA라는 생태계를 15년간 축적해 온 결과다. AMD가 따라잡으려면 하드웨어만 좋아서는 부족하고 생태계까지 구축해야 한다. 이것이 단기간에 이루어지지 않으므로 엔비디아 주가가 급등하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리하면 GPU 서버가 AI의 심장인 이유는 다음과 같다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽었다면 GPU 서버가 왜 AI의 심장인지 감을 잡았을 것이다. 핵심 5가지로 정리한다.&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;AI = 행렬 곱셈의 반복 = 병렬 처리 = GPU.&lt;/b&gt; CPU 박사 24명보다 GPU 알바생 1만 명이 단순 작업에서는 압도적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코어 수 차이만 600~1000배에 달한다.&lt;/b&gt; i9 24코어 대 H100 16,896코어. 비교가 성립하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능은 50~1000배 차이다.&lt;/b&gt; TFLOPS 기준 i9 1 TFLOPS 대 H100 989 TFLOPS. CPU로는 LLM 학습 자체가 불가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버급 GPU(H100, B200)는 메모리/ECC/NVLink/안정성 때문에 별도로 만든다.&lt;/b&gt; 게이밍 GPU 1만 대로는 큰 모델을 학습할 수 없다. 메모리부터 부족하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔비디아 주가 급등의 이유는 AI = GPU 서버 수요 폭증 + CUDA 생태계 락인이다.&lt;/b&gt; 단기간에 따라잡지 못한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 &quot;왜 AI에 GPU 서버를 쓰는가&quot;라는 질문의 답은 단순하다. &lt;b&gt;다른 선택지가 없기 때문&lt;/b&gt;이다. CPU로는 시간이 100배 이상 소요되어 사실상 불가능하고, AMD GPU는 CUDA 생태계가 부족하며, TPU는 구글 내부용에 한정된다. 엔비디아 H100/B200 외에는 답이 없는 시장 구조가 형성된 상태다.&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;다음에 더 다뤄볼 만한 주제로는 &quot;&lt;a href=&quot;http://localhost:20000/project/29771e5e-3e3a-4ecf-ba60-8f7924714e50#&quot;&gt;CUDA가 무엇이기에 그렇게 강력한가&lt;/a&gt;&quot;, &quot;&lt;a href=&quot;http://localhost:20000/project/29771e5e-3e3a-4ecf-ba60-8f7924714e50#&quot;&gt;ChatGPT 학습 비용은 실제로 얼마였는가&lt;/a&gt;&quot;, &quot;&lt;a href=&quot;http://localhost:20000/project/29771e5e-3e3a-4ecf-ba60-8f7924714e50#&quot;&gt;AMD가 엔비디아를 따라잡지 못하는 진짜 이유&lt;/a&gt;&quot; 정도가 있다. 더 궁금한 점이 있다면 댓글로 남겨주기 바란다. 다음 글에 반영할 수 있다.&lt;/p&gt;</description>
      <category>개발 지식in</category>
      <category>AI GPU 이유</category>
      <category>CUDA 코어</category>
      <category>GPU CPU 차이 AI</category>
      <category>GPU 코어 갯수</category>
      <category>병렬 처리 GPU</category>
      <category>엔비디아 H100 성능</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/387</guid>
      <comments>https://yscho03.tistory.com/387#entry387comment</comments>
      <pubDate>Tue, 28 Apr 2026 19:37:22 +0900</pubDate>
    </item>
    <item>
      <title>화이트박스 vs 블랙박스 테스트, 차이는 무엇이며 언제 어떤 것을 사용해야 하는가</title>
      <link>https://yscho03.tistory.com/386</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;한 줄로 정리하면 이렇다&lt;/h2&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;b&gt;화이트박스는 박스 안을 들여다보고 코드를 검증하는 방식이고, 블랙박스는 박스를 두드려보고 소리로 판단하는 방식&lt;/b&gt;이다. 이 한 문장만 머릿속에 새겨두면 90%는 이해한 것이다.&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;다만 이것으로 끝일 리 없다. 실무에서는 &quot;그래서 내 프로젝트에서 어떤 것을 써야 하는가&quot;, &quot;정보처리기사에 무엇이 출제되는가&quot;, &quot;유닛 테스트는 어디에 속하는가&quot;, &quot;그레이박스는 또 무엇인가&quot; 같은 질문이 끝없이 나온다. 솔직히 검색해서 나오는 글들은 대부분 2018년경에 작성된 격식체 글이라 답답하다. JUnit만 언급되고 Vitest나 Playwright는 한 줄도 등장하지 않는다.&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;각설하고, 이 글에서는 화이트박스와 블랙박스 테스트의 차이부터 2026년 기준 최신 도구 매핑, 의사결정 가이드, 그레이박스의 부상, AI 시대의 변화까지 한 번에 정리한다. 신입 QA, 정보처리기사 준비생, 2~5년차 백엔드 개발자가 모두 커버되도록 구성했으므로 자기 위치에서 필요한 부분만 골라 읽어도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화이트박스 테스트란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uYBbd/dJMcabqu25o/D5pwQAetKRFbt8am5P2yUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uYBbd/dJMcabqu25o/D5pwQAetKRFbt8am5P2yUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uYBbd/dJMcabqu25o/D5pwQAetKRFbt8am5P2yUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuYBbd%2FdJMcabqu25o%2FD5pwQAetKRFbt8am5P2yUk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;589&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.testingdocs.com/white-box-testing/&quot;&gt;testingdocs.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의: 코드 내부 구조를 보고 테스트하는 방식이다&lt;/h3&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;b&gt;소스 코드 내부를 모두 들여다보고&lt;/b&gt; 테스트 케이스를 작성하는 방식이다. &quot;구조 기반 테스트(Structural Testing)&quot; 또는 &quot;글래스박스(Glass-box) 테스트&quot;라고도 부른다. 어차피 박스가 투명해서 내부가 모두 보인다.&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;핵심은 단순하다. 코드의 분기문(if, switch), 반복문(for, while), 조건식 하나하나가 모두 의도대로 동작하는지 검증하는 것이다. 함수 하나에 if-else가 5개 있다면 그 5개 분기를 모두 밟아보는 것이 목표이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;누가 수행하는가: 개발자가 직접 한다&lt;/h3&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;b&gt;개발자 본인이 작성하는 것이 정석&lt;/b&gt;이다. QA가 코드 내부까지 모두 알 수는 없다. 따라서 단위 테스트(Unit Test), TDD(Test-Driven Development) 같은 개발자 주도 테스트는 모두 화이트박스 카테고리에 속한다.&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;신입 백엔드 개발자가 입사하자마자 마주하는 것이 &quot;테스트 코드가 없는 PR은 머지되지 않는다&quot;는 룰인데, 이는 사실상 화이트박스 강제이다. JUnit이든 pytest이든 본질은 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대표 기법: 커버리지 종류 정리&lt;/h3&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;b&gt;코드 커버리지(Code Coverage)&lt;/b&gt; 인데, 종류별로 깊이가 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;문장 커버리지(Statement Coverage)&lt;/b&gt;: 코드의 모든 라인이 한 번씩은 실행됐는지 확인한다. 가장 약한 기준이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분기 커버리지(Branch Coverage)&lt;/b&gt;: 모든 if/else 분기가 모두 밟혔는지 확인한다. 일반적으로 80% 이상이 권장된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;조건 커버리지(Condition Coverage)&lt;/b&gt;: 복합 조건문(a &amp;amp;&amp;amp; b)에서 각 조건이 true/false로 모두 평가됐는지 확인한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경로 커버리지(Path Coverage)&lt;/b&gt;: 가능한 모든 실행 경로를 밟는다. 이론적으로 가장 강하지만 경로가 폭발적으로 늘어나기 때문에 현실적으로 100%는 불가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도구: 2026년 기준 실무 스택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날 글들은 JUnit만 언급하지만, 요즘 실무에서는 다음과 같이 사용한다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;언어/스택&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;도구&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;JUnit 5 (`@ParameterizedTest`, `@Nested`), Mockito&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;pytest, pytest-cov, unittest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript/TypeScript&lt;/td&gt;
&lt;td&gt;Vitest, Jest, Mocha&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C/C++&lt;/td&gt;
&lt;td&gt;gcov, lcov, Google Test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;내장 `testing` 패키지, testify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust&lt;/td&gt;
&lt;td&gt;내장 `cargo test`, mockall&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;Vitest로 거의 모두 갈아탔다&lt;/b&gt;. Jest가 느리기 때문에 갈아탄 것이다. Storybook 인터랙션 테스트도 컴포넌트 내부 상태를 모두 들여다보는 것이라 화이트박스 영역에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;블랙박스 테스트란 또 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0BAqE/dJMcabqu25a/DCCQdl8mHFKWaGKEg5cul0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0BAqE/dJMcabqu25a/DCCQdl8mHFKWaGKEg5cul0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0BAqE/dJMcabqu25a/DCCQdl8mHFKWaGKEg5cul0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0BAqE%2FdJMcabqu25a%2FDCCQdl8mHFKWaGKEg5cul0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;559&quot; height=&quot;179&quot; data-origin-width=&quot;559&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.techtarget.com/searchsoftwarequality/definition/black-box&quot;&gt;techtarget.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의: 입력과 출력만 보고 테스트하는 방식이다&lt;/h3&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;b&gt;코드를 한 줄도 보지 않고&lt;/b&gt; 입력값과 기대 출력값만 가지고 테스트하는 방식이다. &quot;명세 기반 테스트(Specification-based Testing)&quot; 또는 &quot;행위 기반 테스트(Behavioral Testing)&quot;라고도 부른다.&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;박스가 검정색이라 내부가 보이지 않으므로 그저 두드려보고 소리로 판단하는 것이다. 입력 X를 넣었더니 Y가 나오는가? 그렇다면 통과이다. 나오지 않는가? 그렇다면 결함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;누가 수행하는가: QA 엔지니어가 한다&lt;/h3&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;b&gt;QA 엔지니어 영역&lt;/b&gt;이다. 다만 실제로는 PM도 하고, CS팀 인원도 하고, 베타 유저도 한다. 코드를 몰라도 수행할 수 있다는 점이 핵심이라 진입 장벽이 낮다.&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;요즘은 SDET(Software Development Engineer in Test)라는 포지션이 부상하고 있는데, 이쪽은 블랙박스 자동화를 코드로 작성하는 사람들이다. 즉 블랙박스 테스트라고 해서 무조건 사람의 손으로 클릭하는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대표 기법: 4대장만 알면 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;블랙박스 기법은 매우 다양하지만 실무에서 자주 쓰는 것은 4가지이다.&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;동치 분할(Equivalence Partitioning)&lt;/b&gt;: 입력값을 비슷하게 동작할 그룹으로 묶고 그룹당 1개씩만 테스트한다. 1~100을 입력받는다면 -10, 50, 200 이렇게 3개로 충분하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;경계값 분석(Boundary Value Analysis)&lt;/b&gt;: 경계 근처에서 버그가 잘 발생하므로 경계값 &amp;plusmn; 1을 집중적으로 테스트한다. 0, 1, 99, 100, 101 같은 식이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의사결정 테이블(Decision Table)&lt;/b&gt;: 조건 조합이 많을 때 표로 정리해서 누락 없이 커버한다. 보험 계산 같은 비즈니스 로직에 강하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;상태 전이(State Transition)&lt;/b&gt;: 로그인/로그아웃, 결제 진행 같은 상태 흐름을 테스트한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도구: API/UI 자동화 스택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;영역&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;도구&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API 테스트&lt;/td&gt;
&lt;td&gt;Postman, Newman, REST Assured, Bruno&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;웹 E2E&lt;/td&gt;
&lt;td&gt;Playwright, Cypress, Selenium WebDriver&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;모바일&lt;/td&gt;
&lt;td&gt;Appium, Detox&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;부하/성능&lt;/td&gt;
&lt;td&gt;k6, JMeter, Locust&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시각 회귀&lt;/td&gt;
&lt;td&gt;Percy, Chromatic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Selenium은 솔직히 한물갔고, &lt;b&gt;&lt;a href=&quot;https://playwright.dev/&quot;&gt;Playwright&lt;/a&gt;가 진정한 대세&lt;/b&gt;이다. Cypress도 쓸만하지만 멀티탭이 되지 않는 점이 치명적이라 신규 프로젝트는 Playwright로 가는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화이트박스와 블랙박스의 차이를 한 표로 깔끔하게 정리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ltcfw/dJMcacXcRxt/rMmk6KS98zAOVnKcGYWc5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ltcfw/dJMcacXcRxt/rMmk6KS98zAOVnKcGYWc5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ltcfw/dJMcacXcRxt/rMmk6KS98zAOVnKcGYWc5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fltcfw%2FdJMcacXcRxt%2FrMmk6KS98zAOVnKcGYWc5k%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1068&quot; height=&quot;462&quot; data-origin-width=&quot;1068&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://codment.com/black-box-vs-white-box-testing/&quot;&gt;codment.com&lt;/a&gt;&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;비교표 하나로 정리되니 머릿속에 새겨두면 좋다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;비교 항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;화이트박스&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;블랙박스&lt;/b&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;입출력 동작&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;개발자&lt;/td&gt;
&lt;td&gt;QA 엔지니어, 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시점&lt;/td&gt;
&lt;td&gt;단위&amp;middot;통합 단계 (초기)&lt;/td&gt;
&lt;td&gt;시스템&amp;middot;인수 단계 (후기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;필요 지식&lt;/td&gt;
&lt;td&gt;프로그래밍&lt;/td&gt;
&lt;td&gt;도메인&amp;middot;요구사항&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;높음 (시간&amp;middot;기술 인건비)&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;도구 의존, 중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;발견 결함&lt;/td&gt;
&lt;td&gt;로직&amp;middot;분기 누락, 경계 처리&lt;/td&gt;
&lt;td&gt;요구사항 미충족, UX 문제&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 도구&lt;/td&gt;
&lt;td&gt;JUnit, pytest, Vitest&lt;/td&gt;
&lt;td&gt;Playwright, Postman, Cypress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;V-모델 위치&lt;/td&gt;
&lt;td&gt;우측 하단 (단위&amp;middot;통합)&lt;/td&gt;
&lt;td&gt;우측 상단 (시스템&amp;middot;인수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 표가 사실상 글의 핵심이다. 면접에서 화이트박스와 블랙박스의 차이를 묻는다면 이 표를 머릿속에서 그려서 5개 항목만 줄줄 읊어도 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 언제 어떤 것을 써야 하는가 (의사결정 가이드)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1113&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uAceA/dJMcabqu25f/FTmGNTIPxTXxOV2XdTFdr1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uAceA/dJMcabqu25f/FTmGNTIPxTXxOV2XdTFdr1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uAceA/dJMcabqu25f/FTmGNTIPxTXxOV2XdTFdr1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuAceA%2FdJMcabqu25f%2FFTmGNTIPxTXxOV2XdTFdr1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;1113&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;1113&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.nextstruggle.com/software-testing-pyramid-unit-integration-and-end-to-end-testing/askdushyant/&quot;&gt;nextstruggle.com&lt;/a&gt;&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;이것이 기존 상위 콘텐츠가 모두 빠뜨린 부분이다. 정의만 줄줄이 늘어놓고 &quot;둘 다 필요하다&quot;로 끝내는데, 그것은 답이 아니다. 상황별로 구체적으로 정리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신규 라이브러리&amp;middot;백엔드 로직이라면 화이트박스가 우선이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 서버를 새로 작성하거나 결제 모듈을 새로 만든다면 &amp;mdash; 이런 케이스는 &lt;b&gt;화이트박스부터 깔고 가야 한다&lt;/b&gt;. 이유는 단순하다. 로직이 복잡하고, 분기가 많으며, 한 번 망가지면 디버깅 비용이 천정부지로 치솟는다.&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;JUnit이나 pytest로 단위 테스트부터 작성하고, 분기 커버리지 80%를 깔고, 통합 테스트를 추가하는 순서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 시나리오&amp;middot;UI&amp;middot;API 계약이라면 블랙박스가 우선이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 화면 검증, 결제 플로우 E2E, 외부 API 계약 테스트 &amp;mdash; 이는 &lt;b&gt;블랙박스가 압도적으로 효율적&lt;/b&gt;이다. 코드를 들여다본다고 해도 사용자 경험은 나오지 않는다.&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;Playwright로 회원가입부터 결제까지 시나리오를 자동화하고, Postman으로 API 응답 스펙을 검증하는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레거시 시스템이라면 블랙박스로 안전망 &amp;rarr; 화이트박스로 리팩토링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;10년 묵은 코드를 떠안았을 때가 진정한 골칫거리이다. 코드를 모르니 화이트박스를 작성하지 못하고, 그렇다고 고치지 않을 수도 없는 상황이다. 정석은 다음과 같다.&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;: &quot;지금 이 입력에 이 출력이 나온다&quot;는 케이스를 모두 잡아 자동화한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;그 위에서 천천히 리팩토링한다&lt;/b&gt;: 동작이 깨지면 회귀 테스트가 빨간불을 켠다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;리팩토링하면서 화이트박스를 추가한다&lt;/b&gt;: 이해한 모듈부터 단위 테스트를 깐다.&lt;/li&gt;
&lt;/ol&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;a href=&quot;https://www.oreilly.com/library/view/working-effectively-with/0131177052/&quot;&gt;마이클 페더스의 「레거시 코드 활용 전략(Working Effectively with Legacy Code)」&lt;/a&gt;에 등장하는 정석 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI/CD 통합 관점에서 분리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Actions에서 테스트를 분리할 때는 보통 다음과 같이 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777358433258&quot; class=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;yaml&quot;&gt;&lt;code&gt;jobs:
  unit-test:        # 화이트박스, 30초 안에 끝남
    runs-on: ubuntu-latest
    steps:
      - run: pytest tests/unit --cov

  integration-test: # 그레이박스, 2~3분
    runs-on: ubuntu-latest
    steps:
      - run: pytest tests/integration

  e2e-test:         # 블랙박스, 5~10분
    runs-on: ubuntu-latest
    steps:
      - run: npx playwright test
&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;빠른 피드백을 위해 화이트박스는 PR을 올릴 때마다 돌리고, 무거운 블랙박스 E2E는 머지 직전이나 nightly로 돌리는 것이 보통이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그레이박스 테스트라는 것도 있다 (현대 트렌드)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKg7bQ/dJMcacXcRxk/rFnd0dvvgGNpINqS2KwRPK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKg7bQ/dJMcacXcRxk/rFnd0dvvgGNpINqS2KwRPK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKg7bQ/dJMcacXcRxk/rFnd0dvvgGNpINqS2KwRPK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKg7bQ%2FdJMcacXcRxk%2FrFnd0dvvgGNpINqS2KwRPK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;3000&quot; height=&quot;1688&quot; data-origin-width=&quot;3000&quot; data-origin-height=&quot;1688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.hypertest.co/integration-testing/why-integration-testing-is-key-to-testing-microservices&quot;&gt;hypertest.co&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정의: 일부 내부 정보만 알고 테스트한다&lt;/h3&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;b&gt;반쯤 투명한 박스&lt;/b&gt;이다. 코드 전부는 보지 않고, 일부 내부 정보만 알고 테스트하는 방식이다. 예를 들어 DB 스키마는 알지만 비즈니스 로직 코드는 보지 않는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 부상했는가: 마이크로서비스&amp;middot;API 시대의 현실&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날에는 모놀리식 앱이라 화이트박스 vs 블랙박스의 이분법이 깔끔하게 맞아떨어졌지만, &lt;b&gt;마이크로서비스 시대에 들어서면서 그레이박스가 사실상 표준&lt;/b&gt;이 되었다. 왜일까? 다른 팀이 만든 서비스를 호출해야 하는데, 그 코드는 볼 수 없고 API 명세와 DB 스키마만 보기 때문이다. 이것이 정확히 그레이박스 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 사례: API 호출 + DB 상태 확인&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777358433259&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;def test_order_creation():
    # 블랙박스 영역: API 호출
    response = client.post(&quot;/orders&quot;, json={&quot;item_id&quot;: 42, &quot;qty&quot;: 2})
    assert response.status_code == 201

    # 그레이박스 영역: DB 직접 조회로 부수효과 검증
    order = db.query(&quot;SELECT * FROM orders WHERE id = ?&quot;, response.json()[&quot;id&quot;])
    assert order[&quot;status&quot;] == &quot;pending&quot;
    assert order[&quot;stock_decremented&quot;] is True
&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;API 응답만 본다면 블랙박스이지만, DB까지 들여다보고 부수효과를 확인하므로 그레이박스이다. 요즘 통합 테스트는 거의 모두 이 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AI 시대에 이 구분이 아직 의미가 있는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitHub Copilot, Cursor, Codeium 같은 AI 페어 프로그래밍 도구가 일반화되면서 테스트 환경도 많이 바뀌었다. 직접 사용해본 입장에서 정리하면 다음과 같다.&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;&lt;b&gt;AI가 잘하는 영역 &amp;mdash; 화이트박스 자동 생성&lt;/b&gt;&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;함수 하나를 던져주고 &quot;이 함수의 단위 테스트를 작성해달라&quot;고 지시하면 정말로 잘 작성한다. 분기, 경계값, null 처리를 모두 자동으로 잡아준다. 화이트박스 테스트는 사실상 보일러플레이트성 노동인데, 이를 AI가 거의 90%까지 처리한다. 개발자는 검토만 하면 된다.&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;&lt;b&gt;AI가 약한 영역 &amp;mdash; 블랙박스 시나리오 설계&lt;/b&gt;&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;다만 블랙박스는 다소 다르다. &quot;결제 플로우 E2E 테스트를 작성해달라&quot;고 지시하면 일반적인 패턴은 작성해주지만, &lt;b&gt;도메인 특수성&lt;/b&gt;이나 &lt;b&gt;사용자 행동 엣지케이스&lt;/b&gt;는 잡지 못한다. 예를 들어 &quot;한국 카드사 3D Secure 인증 도중 새 탭으로 빠져나갔다가 돌아오면 결제는 어떻게 처리되는가&quot; 같은 것이다. 이는 도메인 지식 없이는 시나리오 설계 자체가 불가능하다.&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;&lt;b&gt;결론&lt;/b&gt;: AI는 화이트박스 자동화의 강력한 보조 도구이고, 블랙박스는 여전히 사람의 도메인 지식이 핵심이다. 따라서 신입 개발자에게는 &quot;화이트박스는 AI에게 맡기고, 대신 블랙박스 시나리오를 설계하는 능력을 키워라&quot;가 진정한 실용 조언이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 받는 질문 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정보처리기사에 무엇이 출제되는가&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;블랙박스 기법명&lt;/b&gt; 암기: 동치 분할, 경계값 분석, 원인-결과 그래프, 의사결정 테이블, 상태 전이.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;화이트박스 기법명&lt;/b&gt;: 기초 경로, 조건 검사, 데이터 흐름, 루프 검사.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커버리지 종류&lt;/b&gt; 순서: 문장 &amp;lt; 분기 &amp;lt; 조건 &amp;lt; 다중 조건 &amp;lt; 경로.&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 커버리지는 몇 %가 적당한가&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;분기 커버리지 70~80%&lt;/b&gt;: 일반적인 비즈니스 로직.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;80~90%&lt;/b&gt;: 결제, 인증 같은 핵심 도메인.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;90% 이상&lt;/b&gt;: 의료&amp;middot;금융 같은 안전 중요 시스템.&lt;/li&gt;
&lt;/ul&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;b&gt;100%는 권장하지 않는다&lt;/b&gt;. 마지막 10%를 채우려고 의미 없는 테스트를 작성하다가 유지보수 비용만 폭발한다. 구글도 60% 정도면 충분하다고 공식적으로 언급한 바 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;유닛 테스트는 화이트박스인가 블랙박스인가&lt;/h3&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;b&gt;화이트박스에 가깝지만&lt;/b&gt;, 실무에서는 다소 모호하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;함수 내부 구조를 보고 분기를 모두 커버하면 &amp;rarr; 화이트박스&lt;/li&gt;
&lt;li&gt;함수의 입출력 명세만 보고 작성하면 &amp;rarr; 블랙박스&lt;/li&gt;
&lt;li&gt;보통은 둘이 섞인다 &amp;rarr; 그레이박스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD로 작성한 단위 테스트는 사실상 명세 기반이라 블랙박스 성격이 강하고, 커버리지를 채우려고 추가하는 단위 테스트는 화이트박스이다. 면접 질문을 받는다면 &quot;둘 다 가능하지만 보통은 화이트박스로 분류한다&quot; 정도가 무난한 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ISTQB 자격증 준비라면 어디에 더 비중을 두어야 하는가&lt;/h3&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;a href=&quot;https://www.istqb.org/certifications/certified-tester-foundation-level&quot;&gt;ISTQB Foundation Level&lt;/a&gt;은 &lt;b&gt;블랙박스 기법 비중이 압도적&lt;/b&gt;으로 높다. 동치 분할, 경계값 분석, 의사결정 테이블, 상태 전이, 유스케이스 테스트 같은 것들이 모두 블랙박스이다. 화이트박스는 커버리지 종류 정도만 깊이 있게 다룬다.&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;따라서 ISTQB 준비생은 블랙박스 4대장(동치 분할, 경계값, 의사결정 테이블, 상태 전이)부터 확실히 외우면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TDD는 어디에 속하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD는 &quot;실패하는 테스트 먼저 &amp;rarr; 통과하는 코드 작성 &amp;rarr; 리팩토링&quot; 사이클인데, &lt;b&gt;테스트를 먼저 작성하는 시점에는 구현 코드가 없으므로 명세 기반&lt;/b&gt;이다. 따라서 엄밀히 말하면 블랙박스에 가깝다.&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;다만 사이클이 돌면서 구현이 생기고 난 뒤에는 화이트박스적 보강이 들어간다. 따라서 TDD = 블랙박스 시작 + 화이트박스 보강의 하이브리드라고 보면 정확하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;화이트박스와 블랙박스의 차이, 핵심 5포인트로 압축 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 화이트박스와 블랙박스의 차이부터 그레이박스, AI 시대 변화, FAQ까지 모두 다루었다. 핵심만 5포인트로 다시 정리하면 다음과 같다.&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;/li&gt;
&lt;li&gt;&lt;b&gt;개발자는 화이트박스(JUnit/pytest/Vitest), QA는 블랙박스(Playwright/Postman)&lt;/b&gt; 가 정석 분담이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현실은 그레이박스가 표준&lt;/b&gt;이다. 마이크로서비스 시대에 순수 화이트/블랙은 거의 존재하지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신규는 화이트박스부터, 레거시는 블랙박스부터.&lt;/b&gt; 상황별 우선순위가 다르다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AI는 화이트박스 자동화에 강하고, 블랙박스 시나리오 설계는 여전히 사람의 몫&lt;/b&gt;이다.&lt;/li&gt;
&lt;/ol&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;b&gt;단위 테스트 70 : E2E 30&lt;/b&gt; 또는 &lt;b&gt;단위 50 : 통합 30 : E2E 20&lt;/b&gt; 비율로 섞어 쓴다. 이 비율이 왜 이렇게 도출되는지는 &quot;테스트 피라미드&quot; 개념에서 다루는데, 그것은 다음 글에서 따로 정리하기로 했으므로 궁금하다면 그쪽을 보기 바란다.&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;그리고 신입 개발자라면 일단 자기 PR에 단위 테스트 하나라도 붙이는 습관부터 들이는 것이 진정으로 중요하다. 화이트박스와 블랙박스 이론을 백날 외워봐야 PR에 테스트 코드가 0줄이라면 의미가 없다. 오늘부터 한 줄이라도 작성해보기 바란다.&lt;/p&gt;</description>
      <category>개발 방법론</category>
      <category>그레이박스 테스트</category>
      <category>단위 테스트 통합 테스트 차이</category>
      <category>소프트웨어 테스트 종류</category>
      <category>정적 분석 동적 분석</category>
      <category>테스트 케이스 작성법</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/386</guid>
      <comments>https://yscho03.tistory.com/386#entry386comment</comments>
      <pubDate>Tue, 28 Apr 2026 15:40:56 +0900</pubDate>
    </item>
    <item>
      <title>StarRocks는 무엇이며 왜 이렇게 주목받는가? 토스와 네이버까지 도입한 이유 정리</title>
      <link>https://yscho03.tistory.com/385</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최근 한국 빅테크들이 OLAP DB를 &lt;b&gt;StarRocks&lt;/b&gt;로 전환하고 있다. 토스와 네이버가 대표적이다. 두 회사 모두 자체 기술 블로그에 도입기를 올렸고, 사내 분석 플랫폼 표준 OLAP 엔진으로 깔아두는 분위기다. 그러나 막상 &quot;StarRocks가 무엇인가&quot;라고 물어보면 한 줄로 답하는 사람이 별로 없다. 다들 &quot;빠른 OLAP DB&quot; 정도로만 대충 알고 있다.&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;문제는 StarRocks 자체가 ClickHouse, Druid, Pinot, Trino, Apache Doris 같은 비슷한 엔진들 사이에서 자기 위치를 잡고 등장한 물건이라, &quot;그래서 이것이 ClickHouse와 무엇이 다른가&quot;라는 질문에 답하지 못하면 도입 검토도 어렵다는 점이다. 한국어 자료가 토스와 네이버 글 외에는 거의 없어서 입문이 더 까다롭다.&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;각설하고, 이 글 한 번 읽으면 StarRocks의 정체, 동작 원리, 주목받는 이유, 토스와 네이버가 도입한 배경, 어디에 쓰면 좋고 어디에 쓰면 후회하는지까지 한 번에 정리되도록 작성했다. 기술 블로그 입문서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;StarRocks는 정확히 무엇인가? 한 줄 정의부터 풀어본다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;419&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKO3bs/dJMcagegjOG/0hbR3OHsm2HFj2u53HSiYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKO3bs/dJMcagegjOG/0hbR3OHsm2HFj2u53HSiYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKO3bs/dJMcagegjOG/0hbR3OHsm2HFj2u53HSiYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKO3bs%2FdJMcagegjOG%2F0hbR3OHsm2HFj2u53HSiYk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;800&quot; height=&quot;419&quot; data-origin-width=&quot;800&quot; data-origin-height=&quot;419&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://dbdb.io/db/starrocks&quot;&gt;dbdb.io&lt;/a&gt;&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;한 줄로 말하면 &lt;b&gt;오픈소스 MPP OLAP 데이터베이스&lt;/b&gt;다. 실시간 분석 데이터베이스로 특화됐고, 표준 SQL과 MySQL 프로토콜을 그대로 쓸 수 있다. C++로 작성되어 있어 빠르다.&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;다만 이 한 줄에 모르는 단어가 너무 많다. MPP, OLAP, 컬럼 기반... 이를 하나씩 풀어줘야 StarRocks가 왜 이런 모양인지 이해된다.&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;StarRocks가 풀려는 문제는 단순하다. 과거에는 분석 시스템을 만들려면 Hadoop + Spark + Hive + Druid + ETL 파이프라인 이렇게 5~6개 시스템을 엮어야 했다. 운영 인력만 한 팀이 갈려나갔다. StarRocks는 &quot;이것을 한 방에 해결하자&quot;며 나온 물건이다.&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;출신은 Apache Doris에서 포크되어 나왔다. 2020년에 등장했고, 현재는 CelerData라는 회사가 상용 버전을 판매하고 있다. 2024년부터는 Linux Foundation 산하 프로젝트다. GitHub 스타는 11,000개를 넘었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MPP란 무엇인가&lt;/h3&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;b&gt;MPP는 Massively Parallel Processing&lt;/b&gt;의 줄임말이다. 우리말로 대규모 병렬 처리다. 쿼리 하나를 여러 노드에 쪼개서 동시에 실행하고, 결과를 합쳐서 돌려주는 구조다.&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;기존 단일 노드 DB는 노드 하나가 모두 처리하므로 데이터가 커지면 답이 없다. MPP는 노드 10개면 거의 10배 빠르다. 페이스북과 구글이 데이터 폭발을 겪던 시절에 이 구조가 표준이 되었다. Greenplum과 Vertica가 1세대였고, StarRocks와 ClickHouse가 요즘 세대다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OLAP vs OLTP 1분 정리&lt;/h3&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;b&gt;OLTP&lt;/b&gt;(Online Transaction Processing)는 단건 트랜잭션 처리다. 결제, 주문, 회원가입 같은 작업이다. MySQL과 PostgreSQL이 여기에 속한다.&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;&lt;b&gt;OLAP&lt;/b&gt;(Online Analytical Processing)는 대량 데이터의 집계와 분석이다. &quot;어제 매출이 얼마인가&quot;, &quot;최근 30일 광고 ROAS를 계산하라&quot; 같은 쿼리다. 한 번에 수백만~수십억 행을 훑는 것이 일이다.&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;StarRocks는 100% OLAP 전용이다. 따라서 MySQL 대체용으로 쓰면 안 된다. 이를 헷갈리는 사람이 의외로 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컬럼 기반 vs 로우 기반&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통 DB는 한 행을 하나의 단위로 저장한다(로우 기반). MySQL이 그렇다. 다만 OLAP은 보통 &quot;특정 컬럼 몇 개만&quot; 읽는다. 예를 들어 &quot;유저별 결제 합계&quot;라면 user_id, amount 두 컬럼만 필요하다. 그러나 로우 기반은 전체 행을 다 읽어야 한다. 비효율적이다.&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;&lt;b&gt;컬럼 기반&lt;/b&gt;은 컬럼별로 따로 저장한다. 필요한 컬럼만 읽으면 된다. 압축률도 훨씬 좋다(같은 타입 데이터끼리 모여 있기 때문이다). StarRocks는 당연히 컬럼 기반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;StarRocks 아키텍처 뜯어보기, 어떻게 동작하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;980&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY2EWn/dJMcabqu1jB/9UKolZyxNC94xuwZF0p12k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY2EWn/dJMcabqu1jB/9UKolZyxNC94xuwZF0p12k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY2EWn/dJMcabqu1jB/9UKolZyxNC94xuwZF0p12k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY2EWn%2FdJMcabqu1jB%2F9UKolZyxNC94xuwZF0p12k%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1218&quot; height=&quot;980&quot; data-origin-width=&quot;1218&quot; data-origin-height=&quot;980&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://docs.starrocks.io/docs/introduction/Architecture/&quot;&gt;StarRocks (194KB)&lt;/a&gt;&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;StarRocks는 &lt;b&gt;2-tier 구조&lt;/b&gt;다. FE와 BE 두 종류의 노드로 나뉜다. 단순해서 운영하기 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;FE와 BE 역할 분담&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FE (Frontend Node)&lt;/b&gt;: 메타데이터 관리, 쿼리 파싱, 플래닝, 클러스터 관리 담당. Java로 작성된다. 보통 3대를 띄워 HA로 구성한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;BE (Backend Node)&lt;/b&gt;: 실제 데이터 저장, 쿼리 실행, 컴팩션 담당. C++로 작성된다. 데이터가 늘면 BE만 추가하면 스케일아웃된다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 말하면 FE는 &quot;뇌&quot;, BE는 &quot;근육&quot;이다. 사용자 SQL이 FE로 들어오면 FE가 어느 BE에게 시킬지 계획을 짜고, BE들이 병렬로 실행하며, FE가 결과를 모아서 돌려준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 빠른가? 벡터화 엔진 쉽게 설명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 DB 엔진은 한 번에 한 행씩 처리했다(volcano model). 행 하나를 읽고 함수를 호출하고 결과를 뽑고, 또 행 하나를 읽고... 이 방식은 CPU 입장에서 너무 비효율적이다.&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;&lt;b&gt;벡터화 실행 엔진(Vectorized Execution)&lt;/b&gt;은 한 번에 수천 행씩(StarRocks 기본 4096행) 묶어서 처리한다. CPU 캐시에 모두 올려두고 SIMD 명령어로 한 방에 처리한다. ClickHouse도 이 방식으로 떴고, StarRocks도 이를 채택했다. 단순 집계 쿼리는 과거 엔진보다 5~10배 빠르다.&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;추가로 &lt;b&gt;CBO&lt;/b&gt;(Cost-Based Optimizer)가 통계 기반으로 최적의 실행 계획을 짜준다. 조인 순서와 어느 인덱스를 쓸지 자동으로 결정한다. 이것이 없는 OLAP 엔진(ClickHouse 같은)은 조인이 많은 쿼리에서 무너진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;외부 데이터 직접 쿼리 (Iceberg, Hive, Hudi 연동)&lt;/h3&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;b&gt;External Catalog&lt;/b&gt; 기능으로 데이터를 StarRocks에 적재하지 않고 데이터 레이크 위에서 바로 쿼리할 수 있다.&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;기존에는 Iceberg/Hive에 있는 데이터를 분석하려면 ETL로 OLAP DB에 옮겨야 했다. StarRocks는 그저 &quot;이 Iceberg 카탈로그를 보라&quot;고 하면 끝이다. 적재 비용 0원, 데이터 동기화도 필요 없다. 데이터 레이크하우스 구성에서 OLAP 레이어로 자리잡은 이유가 바로 이것이다.&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;추가로 &lt;b&gt;Materialized View&lt;/b&gt; 기능도 강력하다. 자주 쓰는 집계를 사전에 만들어두면, 똑같은 패턴의 쿼리가 들어왔을 때 자동으로 MV로 재작성해준다. 사용자는 원본 테이블을 쿼리하지만 실제로는 MV를 읽어 응답이 100배 빨라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;StarRocks vs ClickHouse: OLAP DB 비교, 굳이 갈아탈 이유가 있는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lvQtw/dJMcagegjOK/jayQzdnWn8loPmAMcDdau0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lvQtw/dJMcagegjOK/jayQzdnWn8loPmAMcDdau0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lvQtw/dJMcagegjOK/jayQzdnWn8loPmAMcDdau0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlvQtw%2FdJMcagegjOK%2FjayQzdnWn8loPmAMcDdau0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;752&quot; height=&quot;452&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.starrocks.io/blog/clickhouse_or_starrocks&quot;&gt;StarRocks (44KB)&lt;/a&gt;&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;이것이 가장 많이 받는 질문이다. &quot;어차피 ClickHouse가 빠른데 왜 굳이 StarRocks인가?&quot; 이런 질문이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;StarRocks&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;ClickHouse&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Apache Doris&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Druid&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Trino&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPP 분산 조인&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;/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;가장 빠름&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;표준 SQL 호환&lt;/td&gt;
&lt;td&gt;MySQL 프로토콜&lt;/td&gt;
&lt;td&gt;자체 방언&lt;/td&gt;
&lt;td&gt;MySQL 프로토콜&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;ANSI SQL&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;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;강함 (Iceberg/Hive/Hudi)&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이선스&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ClickHouse 대비 강점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ClickHouse는 단일 테이블 집계가 정말 미친 듯이 빠르다. 다만 단점이 명확하다.&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;: ClickHouse는 분산 환경에서 큰 테이블끼리 조인할 때 무너진다. 작은 테이블 broadcast 조인 정도만 무난하다. 데이터가 정규화되어 있으면 ClickHouse는 답이 없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자체 SQL 방언&lt;/b&gt;: ClickHouse SQL은 표준이 아니다. BI 도구(Tableau, Looker, Metabase)와 연동할 때 호환성 문제가 자주 터진다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영이 복잡하다&lt;/b&gt;: ClickHouse Keeper, ZooKeeper, ReplicatedMergeTree 같은 개념을 모두 알아야 한다. 클러스터 운영 노하우가 까다롭다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StarRocks는 이 셋 다 약점이 아니다. MPP 조인이 강하고, MySQL 프로토콜을 그대로 쓰며, FE/BE만 띄우면 된다. 그래서 &quot;분석 환경을 새로 구축한다면 StarRocks를 보는 것이 낫다&quot;는 평가가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Apache Doris StarRocks 차이, 한 뿌리에서 갈라진 형제다&lt;/h3&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;b&gt;Apache Doris가 StarRocks의 원조&lt;/b&gt;다. 2020년에 Doris 핵심 개발자들이 따로 나와 StarRocks를 만들었다. 한 뿌리에서 두 갈래로 갈라진 셈이다.&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;현재는 둘 다 활발히 개발 중이라 어느 쪽이 낫다고 단정하기 어렵다. 큰 차이는 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;StarRocks&lt;/b&gt;: 외부 카탈로그(Iceberg/Hive 직접 쿼리), Materialized View 자동 재작성에 더 공격적이다. 초기에는 Elastic License 2.0이었는데 2022년 12월에 Apache 2.0으로 변경했고 Linux Foundation 산하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Apache Doris&lt;/b&gt;: Apache 재단 정식 프로젝트. 라이선스 Apache 2.0. 중국 내수 시장에서 강하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해외와 국내 빅테크는 StarRocks 쪽 채택률이 높은 편이다. 토스와 네이버도 StarRocks를 선택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;토스와 네이버는 왜 StarRocks를 도입했는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://toss.tech/article/operating-starrocks-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://toss.tech/article/operating-starrocks-1&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1777356445738&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;StarRocks 운영기: Resource Group으로 멀티테넌트 워크로드 격리하기&quot; data-og-description=&quot;서비스 쿼리가 밀리기 시작했을 때, 우리가 선택한 격리 전략&quot; data-og-host=&quot;toss.tech&quot; data-og-source-url=&quot;https://toss.tech/article/operating-starrocks-1&quot; data-og-url=&quot;https://toss.tech/article/operating-starrocks-1&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/gT4Pe/dJMb9c9AUIe/ebyKkWVPH79Z0C555kZbd1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/HJhOV/dJMb9g5d9tv/TJxV2y1a66xkjkKOzf7HKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/czj70W/dJMb8Rj4ZXu/DENrXmp8VGpUSuYLqxw8Q1/img.png?width=1748&amp;amp;height=900&amp;amp;face=0_0_1748_900&quot;&gt;&lt;a href=&quot;https://toss.tech/article/operating-starrocks-1&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://toss.tech/article/operating-starrocks-1&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/gT4Pe/dJMb9c9AUIe/ebyKkWVPH79Z0C555kZbd1/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/HJhOV/dJMb9g5d9tv/TJxV2y1a66xkjkKOzf7HKk/img.png?width=1200&amp;amp;height=600&amp;amp;face=0_0_1200_600,https://scrap.kakaocdn.net/dn/czj70W/dJMb8Rj4ZXu/DENrXmp8VGpUSuYLqxw8Q1/img.png?width=1748&amp;amp;height=900&amp;amp;face=0_0_1748_900');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;StarRocks 운영기: Resource Group으로 멀티테넌트 워크로드 격리하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;서비스 쿼리가 밀리기 시작했을 때, 우리가 선택한 격리 전략&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;toss.tech&lt;/p&gt;
&lt;/div&gt;
&lt;/a&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;출처: &lt;a href=&quot;https://toss.tech/article/operating-starrocks-1&quot;&gt;toss.tech (130KB)&lt;/a&gt;&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;이 부분이 가장 궁금할 것이다. 한국 빅테크 두 곳이 비슷한 시기에 같은 기술을 고른 것은 우연이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;토스가 풀고 싶었던 문제&lt;/h3&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;a href=&quot;https://toss.tech/article/operating-starrocks-1&quot;&gt;토스 운영기 1편&lt;/a&gt;을 보면 도입 배경이 명확하다. 기존에는 &lt;b&gt;MySQL + Hadoop + Spark&lt;/b&gt; 조합이었다. 같은 데이터를 두 갈래에 두고 검증하고 서빙해야 하는 이중 경로 구조였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spark 배치로 만든 데이터를 MySQL 온라인 쿼리 결과와 일일이 비교해야 했다&lt;/li&gt;
&lt;li&gt;Hadoop에 있는 데이터를 대시보드에서 보려면 MySQL로 따로 옮겨야 했다&lt;/li&gt;
&lt;li&gt;시스템이 두 갈래라 운영 복잡도가 폭발했다&lt;/li&gt;
&lt;li&gt;분석가도 &quot;이 데이터가 어디 있는가&quot;라고 매번 물어봤다. 데이터 사일로화&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;StarRocks 도입과 함께 &lt;b&gt;단일 엔진으로 통합&lt;/b&gt;되었다. MySQL 호환 SQL이라 기존 쿼리를 거의 그대로 옮겼고, 대용량 분석과 서비스 조회를 한 엔진에서 처리하게 되었다. 토스가 시리즈로 운영기를 쓸 정도로 만족도가 높은 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네이버가 풀고 싶었던 문제&lt;/h3&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;a href=&quot;https://d2.naver.com/helloworld/1168674&quot;&gt;네이버 D2 헬로월드 글&lt;/a&gt;에서 정리한 도입 맥락은 다소 다르다. 네이버는 기존에 &lt;b&gt;ClickHouse&lt;/b&gt;를 분석 엔진으로 쓰고 있었는데 &lt;b&gt;분산 조인이 약한 것이 골칫거리&lt;/b&gt;였다.&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ClickHouse는 큰 테이블 조인을 하지 못해 비정규화 테이블로 들고 있어야 했다. 차원이 고정되어 분석가가 자유롭게 쓰지 못했다&lt;/li&gt;
&lt;li&gt;노드를 늘릴 때마다 데이터 수동 리밸런싱이 필요했다&lt;/li&gt;
&lt;li&gt;실시간 upsert/delete 성능도 나오지 않았다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trino, Pinot, Druid, StarRocks 후보들을 벤치마크한 끝에 StarRocks를 선택했다. &lt;b&gt;다중 테이블 조인을 네이티브로 지원&lt;/b&gt;해 비정규화하지 않아도 되고, 집계 쿼리 성능도 ClickHouse 수준 이상이었다. 쿠버네티스에서 스토리지와 컴퓨트 분리 구조로 운영 중이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공통 도입 이유 정리&lt;/h3&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;&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;: 여러 시스템 &amp;rarr; 단일 엔진. 인력 비용 절감&lt;/li&gt;
&lt;li&gt;&lt;b&gt;표준 SQL 호환&lt;/b&gt;: MySQL 프로토콜이라 BI 도구와 분석가의 진입장벽이 낮다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속도와 분산 조인의 강점&lt;/b&gt;: 벡터화 엔진 + CBO + MV에 다중 테이블 조인까지 네이티브 지원&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 레이크 친화성&lt;/b&gt;: Iceberg 등 외부 카탈로그 직접 쿼리&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지가 StarRocks의 핵심 셀링 포인트다. 다른 OLAP 엔진들은 이 네 가지를 모두 만족시키지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;StarRocks는 어디에 쓰면 좋고 어디에 쓰면 후회하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;663&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5vjzp/dJMcajhHxBB/FQkyJYoRc2plVAYAyBiFd0/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5vjzp/dJMcajhHxBB/FQkyJYoRc2plVAYAyBiFd0/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5vjzp/dJMcajhHxBB/FQkyJYoRc2plVAYAyBiFd0/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5vjzp%2FdJMcajhHxBB%2FFQkyJYoRc2plVAYAyBiFd0%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;960&quot; height=&quot;663&quot; data-origin-width=&quot;960&quot; data-origin-height=&quot;663&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://radial.consulting/data-analytics-ai/power-bi-dashboards/&quot;&gt;Radial Consulting (99KB)&lt;/a&gt;&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;마케팅 자료는 다 좋다고만 하니 솔직하게 정리한다. 도입을 검토하기 전에 이것부터 먼저 봐야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;잘 맞는 케이스&lt;/h3&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;b&gt;실시간 대시보드&lt;/b&gt;: 광고 성과, 결제 분석, 트랜잭션 모니터링. 초~분 단위로 갱신되는 지표를 보여주는 데 최적이다&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;✅ &lt;b&gt;사용자 행동 분석&lt;/b&gt;: 퍼널 분석, 코호트 분석, 리텐션 계산. 큰 테이블과 복잡한 윈도우 함수 조합에 강하다&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;✅ &lt;b&gt;데이터 레이크 위 ad-hoc 분석&lt;/b&gt;: Iceberg/Hive에 데이터가 있는 상태에서 분석가가 자유롭게 쿼리해야 할 때. ETL을 하지 않아도 되어 매우 편리하다&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;✅ &lt;b&gt;BI 도구 백엔드&lt;/b&gt;: Tableau, Metabase, Superset 같은 BI 도구를 MySQL 프로토콜로 그대로 붙일 수 있다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;안 맞는 케이스&lt;/h3&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;b&gt;OLTP 워크로드&lt;/b&gt;: 결제 처리, 회원가입 같은 단건 트랜잭션이 빈번한 서비스 DB 용도로 쓰면 안 된다. 이는 PostgreSQL/MySQL 영역이다&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;❌ &lt;b&gt;단순 KV 룩업&lt;/b&gt;: &quot;user_id로 1개 행 조회&quot; 같은 패턴은 Redis/DynamoDB가 답이다. StarRocks를 쓰면 오버킬이다&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;❌ &lt;b&gt;비정형 데이터 처리&lt;/b&gt;: JSON 처리는 어느 정도 가능하다. 다만 본격적인 도큐먼트 DB 용도로 쓰려면 MongoDB나 Elasticsearch로 가야 한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;운영 함정&lt;/h3&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;&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;BE 노드 메모리 폭주&lt;/b&gt;: 집계 쿼리가 복잡해지면 BE 메모리가 터진다. 쿼리 메모리 제한과 BE 노드 사이즈를 신중하게 정해야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴팩션 지연&lt;/b&gt;: 잦은 인서트가 발생하면 컴팩션이 따라오지 못한다. 쓰기 패턴 설계를 잘 해야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;라이선스 변경 이력 주의&lt;/b&gt;: 2022년 12월 Elastic License 2.0에서 Apache 2.0으로 변경되었다. 과거 자료와 블로그를 보면 아직도 &quot;Elastic License라 SaaS 재판매 금지&quot; 같은 정보가 남아 있는데 현재 버전은 정식 오픈소스다. 이력만 알아두면 문제없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;한국어 자료 부족&lt;/b&gt;: 영문 자료는 충분한데 한국어는 토스와 네이버 외에는 빈약하다. 운영 노하우가 적은 것이 진입 비용이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;버전 차이가 큼&lt;/b&gt;: 2.x &amp;rarr; 3.x 사이에 외부 카탈로그 등 큰 변화가 있었다. 최신 버전을 기준으로 보고 도입을 결정해야 한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도입 전 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[ ] 우리 워크로드가 OLAP인가? (OLTP면 패스)&lt;/li&gt;
&lt;li&gt;[ ] 데이터 규모가 단일 노드 DB로 감당 안 되는 수준인가?&lt;/li&gt;
&lt;li&gt;[ ] 표준 SQL/MySQL 프로토콜 호환이 필요한가?&lt;/li&gt;
&lt;li&gt;[ ] 라이선스(현재 Apache 2.0)를 검토했는가?&lt;/li&gt;
&lt;li&gt;[ ] PoC로 우리 쿼리 패턴에서 실측해봤는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리: StarRocks 지금 깔아봐야 할 이유&lt;/h2&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;b&gt;StarRocks&lt;/b&gt;가 무엇인지, 어떻게 동작하는지, 왜 토스와 네이버가 선택했는지, 어디에 쓰면 좋은지 모두 정리되었을 것이다. 핵심만 3줄로 다시 정리하면 다음과 같다:&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;빠른 MPP OLAP DB다&lt;/b&gt;. 벡터화 엔진 + CBO + MV로 ClickHouse급 속도를 내면서 분산 조인까지 가능하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;운영이 단순하다&lt;/b&gt;. FE + BE 2-tier 구조에 MySQL 프로토콜을 그대로 사용한다. 토스와 네이버가 이 점 때문에 선택했다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;데이터 레이크 친화적이다&lt;/b&gt;. Iceberg/Hive 외부 카탈로그를 직접 쿼리할 수 있어 ETL을 하지 않아도 된다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점도 솔직히 말하면 한국어 자료가 부족하고, 운영 노하우를 가진 사람이 적으며, 컴팩션과 메모리 튜닝 같은 운영 함정이 존재한다. 그럼에도 새로 분석 인프라를 구축한다면 StarRocks를 검토하지 않는 것은 직무유기 수준이다.&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;도입을 고민 중이라면 &lt;a href=&quot;https://docs.starrocks.io/&quot;&gt;공식 문서 Quick Start&lt;/a&gt;로 30분간 설치해보고 &lt;a href=&quot;https://github.com/StarRocks/starrocks&quot;&gt;GitHub 저장소&lt;/a&gt;를 한 번 둘러보면 답이 나온다. 회사 데이터 일부를 넣어보고 ClickHouse/Doris와 직접 벤치마크를 돌려보면 더 확실하다. PoC 한 번 돌리는 데 들이는 시간 대비 얻는 것이 많다.&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;다음 글에서는 StarRocks 실제 운영 노하우(BE 노드 사이징, MV 설계, 외부 카탈로그 활용 패턴)를 정리할 예정이다.&lt;/p&gt;</description>
      <category>Database</category>
      <category>Apache Doris StarRocks 차이</category>
      <category>MPP OLAP 데이터베이스</category>
      <category>StarRocks vs ClickHouse</category>
      <category>StarRocks 도입</category>
      <category>실시간 분석 데이터베이스</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/385</guid>
      <comments>https://yscho03.tistory.com/385#entry385comment</comments>
      <pubDate>Tue, 28 Apr 2026 15:08:27 +0900</pubDate>
    </item>
    <item>
      <title>웹팩(Webpack)을 왜 쓰는가? 그리고 이것을 만든 Tobias Koppers는 누구인가?</title>
      <link>https://yscho03.tistory.com/384</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발을 조금이라도 해봤다면 웹팩(Webpack)이라는 단어를 한 번도 들어보지 못한 사람은 없을 것이다. 다만 정작 &quot;왜 이것을 쓰는지&quot;, &quot;누가 만든 것인지&quot; 제대로 설명할 수 있는 사람은 의외로 적다. 나도 예전에는 npm run build를 치면 무언가 알아서 돌아가는 마법 상자 정도로만 알고 있었다.&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;이 글은 &lt;b&gt;웹팩 사용 이유&lt;/b&gt;를 6가지로 정리하고, 독일 개발자 Tobias Koppers가 어떻게 이것을 만들게 되었는지, 그리고 Vite와 Turbopack이 치고 올라오는 2026년 시점에서 웹팩이 아직도 의미가 있는지까지 살펴보는 것이 목표다. 주니어든 중급이든 번들러 개념이 헷갈린다면 한번 읽어보면 도움이 될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;번들러 없으면 프론트엔드는 어떤 지옥이 펼쳐지는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩 이야기를 하기 전에, 이것이 왜 필요했는지부터 짚고 가야 한다. 번들러(bundler)가 없던 시절을 한 번만 상상해보면 &quot;아 이래서 필요하구나&quot;라는 소리가 바로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;script 태그 서른 개를 박던 jQuery 시대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2010년대 초반 프론트엔드는 정말 원시적이었다. HTML 파일 하나에 , , 이런 식으로 태그 수십 개를 줄줄이 박아야 했다. 프로젝트가 커지면 HTML 헤드 부분이 100줄을 넘어가는 것도 흔한 일이었다.&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;이렇게 하면 무엇이 문제인가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;네트워크 요청 개수 폭발&lt;/b&gt;: HTTP/1.1 시대에 파일이 30개면 요청도 30번 발생한다. TTFB 지옥이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드 순서 꼬임&lt;/b&gt;: a.js가 b.js를 참조하는데 b.js를 더 아래에 두면 그냥 터진다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전역 스코프 오염&lt;/b&gt;: 모든 변수가 window에 붙는다. $를 한 번 덮어쓰면 다른 라이브러리도 같이 죽는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;의존성 순서가 꼬이면 코드가 그냥 터지는 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정말 웃긴 점은 이것이 실수가 아니라 &quot;당시의 정상&quot;이었다는 사실이다. 개발자가 수동으로 파일 로딩 순서를 관리해야 했다. 팀원 한 명이 새 스크립트를 위쪽에 끼워 넣으면, 다른 팀원의 코드가 의존성을 찾지 못하고 죽는다. 이런 사고가 하루에 서너 번씩 터졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CommonJS는 Node.js용, 브라우저는 읽지 못한다&lt;/h3&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.js가 인기를 끌면서 CommonJS 스타일(require())이 표준처럼 자리잡았는데, 문제는 브라우저가 require를 모른다는 점이었다. 개발자 입장에서는 서버 코드와 브라우저 코드를 다른 방식으로 작성해야 했고, 코드 재사용이 거의 불가능했다.&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;누군가는 이 간극을 메워야 했다. 그 시점에 등장한 것이 Browserify였고, 곧이어 훨씬 강력한 웹팩이 등장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹팩이 정확히 무엇인지 한 줄로 정리하면&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B7nYx/dJMcaiQDjdH/lKizUeuGL5yzy85xRiIccK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B7nYx/dJMcaiQDjdH/lKizUeuGL5yzy85xRiIccK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B7nYx/dJMcaiQDjdH/lKizUeuGL5yzy85xRiIccK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB7nYx%2FdJMcaiQDjdH%2FlKizUeuGL5yzy85xRiIccK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;853&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;853&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.codementor.io/javascript/tutorial/module-bundler-webpack-getting-started-guide&quot;&gt;Codementor&lt;/a&gt;&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;&lt;a href=&quot;https://webpack.js.org/&quot;&gt;웹팩 공식 문서&lt;/a&gt;의 표현을 그대로 옮기면 &quot;웹팩은 &lt;b&gt;모던 자바스크립트 애플리케이션을 위한 정적 모듈 번들러&lt;/b&gt;다&quot;. 한 줄짜리 정의지만 살펴보면 꽤 많은 것이 담겨 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;모듈 번들러&quot;라는 단어부터 해부&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;모듈&lt;/b&gt;: 기능 단위로 쪼개놓은 코드 조각이다. import/export나 require로 다른 파일을 불러오는 방식이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;번들러&lt;/b&gt;: 이 조각들을 의존성 그래프를 따라 모아서, 브라우저가 한 번에 읽을 수 있는 파일(번들)로 합쳐주는 도구다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 웹팩은 흩어져 있는 JS 파일과 거기서 참조하는 CSS, 이미지, 폰트까지 모아서 &quot;하나 또는 몇 개의 최적화된 파일&quot;로 묶어주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;entry &amp;rarr; loader &amp;rarr; plugin &amp;rarr; output 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩 설정을 이해하려면 이 4단어만 기억하면 된다.&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;entry&lt;/b&gt;: 시작점이다. &quot;이 파일부터 의존성을 따라가서 모두 모아와&quot;라고 지정하는 지점이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;loader&lt;/b&gt;: 변환기다. .ts 파일을 JS로, .scss를 CSS로, .png를 인라인 데이터 URL로 바꿔주는 역할을 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;plugin&lt;/b&gt;: 번들 전체 과정에 훅을 걸어서 작업을 수행하는 도구다. HTML 자동 생성, 압축, 환경 변수 주입 같은 것이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;output&lt;/b&gt;: 최종 산출물 위치다. &quot;dist 폴더에 main.[hash].js로 뽑아라&quot;라고 지시한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777354696873&quot; class=&quot;groovy&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;js&quot;&gt;&lt;code&gt;// webpack.config.js (webpack 5 기준)
module.exports = {
  entry: './src/index.js',
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.[contenthash].js',
  },
  module: {
    rules: [
      { test: /\.css$/, use: ['style-loader', 'css-loader'] },
      { test: /\.(png|jpg)$/, type: 'asset/resource' },
    ],
  },
};
&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;이것이 webpack.config.js의 기본 골격이다. 실제 프로덕션에서는 여기에 플러그인이 붙고 최적화 옵션이 붙어서 수백 줄까지 늘어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JS만 번들링하는 것이 아니다 &amp;mdash; CSS, 이미지, 폰트도 모두 처리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩의 진짜 강점은 &quot;모든 것을 모듈로 취급한다&quot;는 점이다. import './style.css' 한 줄을 쓰면 CSS도 JS 번들에 들어간다. 이미지도 import logo from './logo.png' 이렇게 작성하면 알아서 처리된다. 이것이 프론트엔드 개발 방식 자체를 바꿔버렸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;프론트엔드에서 웹팩을 쓰는 진짜 이유 6가지&lt;/h2&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;b&gt;웹팩 사용 이유&lt;/b&gt;를 하나씩 살펴보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모듈 시스템 통일 &amp;mdash; ESM, CommonJS, AMD를 모두 처리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 생태계에는 모듈 시스템이 너무 많았다. CommonJS, AMD, UMD, ES Modules(ESM)... 라이브러리마다 다른 방식으로 배포된다. 웹팩은 이 모든 포맷을 인식해서 하나로 합쳐준다. import로 작성했든 require로 작성했든 최종 번들에서는 동일하게 동작한다. 이 &quot;호환성 해결사&quot; 역할만으로도 쓸 가치가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 스플리팅으로 초기 로딩 속도 확보&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPA(Single Page Application)가 커지면 번들 사이즈가 3~5MB까지 쉽게 도달한다. 이것을 한 번에 다운받게 하면 초기 로딩이 5초 이상 걸린다. 웹팩의 &lt;b&gt;코드 스플리팅(code splitting)&lt;/b&gt; 은 라우트 단위, 컴포넌트 단위로 번들을 쪼개서 필요할 때만 다운받게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777354696874&quot; class=&quot;coffeescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;js&quot;&gt;&lt;code&gt;// 동적 import &amp;mdash; 웹팩이 알아서 청크 분리해줌
const AdminPanel = () =&amp;gt; import('./AdminPanel');
&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;이 한 줄을 넣으면 AdminPanel은 별도 청크로 빠지고, 관리자 페이지에 진입할 때만 로드된다. 초기 번들 사이즈가 30~50% 줄어드는 경우도 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트리 셰이킹으로 사용하지 않는 코드 제거하기&lt;/h3&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;b&gt;트리 셰이킹(tree shaking)&lt;/b&gt; 은 &quot;import했지만 실제로 사용하지 않는 함수를 번들에서 제거&quot;하는 기능이다. 예를 들어 lodash에서 debounce 하나만 사용하는데 라이브러리 전체(70KB)가 번들에 들어가면 낭비다. 웹팩은 ESM 정적 분석으로 사용하지 않는 export를 찾아내서 제거한다. webpack 5에서 이 부분이 많이 개선되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;HMR &amp;mdash; 저장하면 새로고침 없이 바로 반영&lt;/h3&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;b&gt;HMR(Hot Module Replacement)&lt;/b&gt; 이 없는 개발 환경은 상상할 수 없다. 코드를 수정하고 저장할 때마다 브라우저를 새로고침해야 한다면, 로그인 상태나 모달을 열어놓은 상태가 모두 날아간다. 웹팩의 HMR은 &lt;b&gt;변경된 모듈만 런타임에 교체&lt;/b&gt;해서 앱 상태를 유지한 채로 업데이트해준다. React Hot Reload, Vue HMR 같은 도구도 모두 웹팩 HMR 위에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;환경별 빌드 분리 &amp;mdash; dev는 빠르게, prod는 최적화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 mode: 'development' / mode: 'production' 두 가지 프리셋을 제공한다. 개발 모드에서는 소스맵을 켜고 압축을 끄며, 프로덕션 모드에서는 minify와 tree shaking, 청크 해싱까지 모두 적용한다. webpack-merge로 공통 설정과 환경별 설정을 분리하는 것이 일반적인 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Module Federation &amp;mdash; 마이크로프론트엔드의 유일무이한 답&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 webpack 5의 진짜 킬러 기능이다. &lt;b&gt;Module Federation&lt;/b&gt;은 서로 다른 웹팩 빌드끼리 런타임에 모듈을 주고받을 수 있게 해준다. 쉽게 말하면 &quot;앱 A에서 빌드한 컴포넌트를 앱 B가 런타임에 가져다 쓸 수 있다&quot;는 의미다. 마이크로프론트엔드(여러 팀이 각자 배포하는 거대 프론트엔드 아키텍처)에서 거의 유일한 표준 해법이다.&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;Vite는 플러그인으로 비슷한 시도를 하고 있지만 완성도는 아직 웹팩이 훨씬 앞선다. 이것이 대기업들이 쉽게 웹팩을 버리지 못하는 이유 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹팩을 만든 Tobias Koppers는 어떤 사람인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ISl5E/dJMcaiQDjdI/QZiUPHuwVfyJoNrN5Dvq80/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ISl5E/dJMcaiQDjdI/QZiUPHuwVfyJoNrN5Dvq80/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ISl5E/dJMcaiQDjdI/QZiUPHuwVfyJoNrN5Dvq80/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FISl5E%2FdJMcaiQDjdI%2FQZiUPHuwVfyJoNrN5Dvq80%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;400&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://gotopia.tech/experts/1355/tobias-koppers&quot;&gt;GOTO Conferences (141KB)&lt;/a&gt;&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;번들러 이야기만 하다 보면 놓치기 쉬운데, 웹팩은 회사가 만든 것이 아니라 &lt;b&gt;한 명의 개발자가 석사 논문을 쓰다가 만든 프로젝트&lt;/b&gt;가 기원이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2012년 독일 대학원생의 개인 프로젝트로 시작되다&lt;/h3&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;a href=&quot;https://github.com/sokra&quot;&gt;Tobias Koppers(@sokra)&lt;/a&gt;는 독일 개발자로, 2012년 3월에 webpack의 첫 커밋을 올렸다. 당시 그는 석사 논문을 쓰던 대학원생이었고, Google Web Toolkit(GWT)의 &lt;b&gt;코드 스플리팅&lt;/b&gt; 기능에 꽂혀서 &quot;JS에서도 이것이 가능하면 좋겠다&quot;는 생각으로 개인 프로젝트를 시작했다.&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;웹팩 1.0 정식 릴리스는 2014년 2월이다. 초기에는 Browserify에 밀려 관심을 받지 못하다가, React + Redux 생태계가 코드 스플리팅과 HMR을 원하면서 2015년부터 급성장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GWT 코드 스플리팅에 꽂힌 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tobias는 본인 블로그와 인터뷰에서 &quot;GWT가 Java 코드를 청크로 나누어 필요할 때만 로드하는 구조를 보았을 때, JS 생태계에는 왜 이것이 없는지 이해가 되지 않았다&quot;고 밝혔다. 당시 JS 번들러(Browserify 등)는 그저 파일을 하나로 합치는 수준이었고, 동적 로딩 개념 자체가 빈약했다.&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;이 &quot;청크 단위로 로드&quot;라는 아이디어가 웹팩의 핵심 철학이 되었고, 오늘날의 코드 스플리팅, 동적 import, Module Federation까지 모두 이 뿌리에서 비롯되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Open Collective로 풀타임 전환, 현재는 Vercel에서 Turbopack 리드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩이 점점 커지면서 Tobias는 본업과 오픈소스 유지보수 사이에서 번아웃을 겪었다. 2017년경 Open Collective를 통해 스폰서십으로 풀타임 전환했고, 수년간 웹팩을 혼자서 사실상 유지했다.&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;그러다가 &lt;b&gt;2021년에 Vercel에 합류&lt;/b&gt;한다. 현재는 Vercel에서 &lt;b&gt;Turbopack&lt;/b&gt;이라는 차세대 번들러를 Rust로 개발하는 중이다. 웹팩의 후계자 격인데, 아직은 Next.js 내부에서 베타로 쓰이는 수준이다.&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;흥미로운 점은 Tobias가 웹팩을 버린 것이 아니라, &quot;웹팩의 한계를 알기 때문에 더 나은 것을 만들겠다&quot;는 맥락으로 Turbopack을 개발하고 있다는 사실이다. @wSokra 트위터에 가끔 올라오는 설계 이야기가 매우 흥미롭다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹팩의 단점 &amp;mdash; 솔직히 살펴보자&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설정 지옥 &amp;mdash; webpack.config.js 500줄의 트라우마&lt;/h3&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;b&gt;webpack.config.js 설정 난이도가 정말 만만치 않다&lt;/b&gt;. 로더, 플러그인, 옵티마이제이션, 리졸버 같은 개념을 모두 이해해야 하고, 에러 메시지도 친절한 편이 아니다. 주니어가 webpack 설정을 처음 만지면 몇 시간은 그냥 날아간다.&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;이 문제 때문에 create-react-app, Next.js, Vue CLI 같은 &quot;설정을 숨겨주는 프레임워크&quot;가 대세가 되었다. 직접 설정할 일이 점점 줄어드는 추세다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콜드 스타트가 느리다 &amp;mdash; Vite 대비 체감 2~10배&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 개발 모드에서도 번들링을 한다. 프로젝트가 1000개가 넘는 모듈을 가지면 첫 npm run dev가 10~30초씩 걸린다. Vite는 esbuild로 의존성만 미리 처리하고 나머지는 네이티브 ESM으로 브라우저에 바로 전달하므로, 체감상 1~2초 안에 뜬다.&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;물론 webpack 5에서 영구 캐싱(filesystem cache)이 들어가면서 두 번째부터는 빨라졌다. 다만 첫 시동 속도는 여전히 Vite 대비 불리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;런타임 오버헤드와 번들 사이즈 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 모듈 간 호환성 처리를 위해 런타임 코드(wrapper)를 삽입한다. 이것이 작은 앱에서는 별 문제가 아니지만, 초소형 라이브러리에서는 오버헤드 비율이 꽤 크게 느껴진다. esbuild, Rollup 같은 도구가 같은 코드를 더 작게 뽑는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2026년 현재, 웹팩을 써야 하는가 말아야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 가장 많이 받는 질문이다. 2026년 프론트엔드 번들러 선택은 확실히 바뀌었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신규 프로젝트는 Vite가 기본값이 된 현실&lt;/h3&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;a href=&quot;https://2024.stateofjs.com/&quot;&gt;State of JS 2024&lt;/a&gt; 조사 기준, Vite와 웹팩이 사용률에서 거의 비등한 수준까지 따라붙었고, 만족도(positive sentiment)에서는 Vite가 웹팩을 압도적으로 앞질렀다. 신규 React, Vue, Svelte 프로젝트는 거의 다 Vite를 기본으로 제안한다. create-react-app은 2023년 3월에 React 공식 문서에서 추천이 빠졌고, 2025년 2월 공식적으로 deprecated(Sunsetting Create React App) 처리되었다. 대신 Vite 템플릿이 권장된다.&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;&lt;b&gt;신규 프로젝트, 순수 SPA, 빠른 DX가 중요&lt;/b&gt;하다면 &amp;rarr; Vite가 답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;레거시 유지보수와 Module Federation이면 여전히 웹팩&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &quot;이미 웹팩으로 돌아가는 거대 프로젝트&quot;를 Vite로 이주하는 것은 거의 불가능에 가깝다. 로더 생태계가 너무 방대하고, 커스텀 설정이 촘촘하게 박혀 있다.&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;특히 &lt;b&gt;Module Federation을 써야 하는 마이크로프론트엔드 환경&lt;/b&gt;은 아직도 웹팩이 사실상 유일한 선택이다. 쿠팡, 네이버, 카카오 같은 대형 프론트엔드 조직이 웹팩을 버리지 못하는 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rspack &amp;mdash; 웹팩 호환 Rust 번들러의 등장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 8월에 &lt;a href=&quot;https://rspack.dev/&quot;&gt;&lt;b&gt;Rspack 1.0&lt;/b&gt;&lt;/a&gt;이 정식 릴리스되었다. ByteDance에서 만든 것으로, &lt;b&gt;웹팩의 API를 그대로 호환하면서 Rust로 다시 구현&lt;/b&gt;한 도구다. 체감 5~10배 빠르다. 기존 webpack.config.js를 거의 그대로 쓰면서 속도 이득만 챙길 수 있어서, &quot;웹팩에서 Vite로 가지 못하는 조직&quot;의 현실적 이주 경로로 주목받고 있다.&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;다만 플러그인 호환성이 아직 100%는 아니므로 도입 전에 의존성 체크는 필수다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Next.js, 그리고 거대 프레임워크가 웹팩을 떠받쳐온 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Next.js 14까지는 기본 번들러가 웹팩이었다. Next.js 15에서 Turbopack이 dev 모드 기준 stable로 승격되었고, 2025년 10월 Next.js 16에 와서 Turbopack이 기본 번들러로 전환되었다. 그래도 next dev --webpack 옵션으로 여전히 웹팩을 선택할 수 있고, 광범위한 레거시 앱과 플러그인 생태계가 웹팩 기반이라는 사실은 변함없다. &quot;웹팩의 안정성과 생태계 성숙도&quot;를 단번에 대체하기 어렵다는 뜻이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹팩은 죽지 않았다, 다만 역할이 바뀌었을 뿐이다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초심자라면 웹팩 개념은 반드시 알아야 한다.&lt;/b&gt; 왜일까? 지금도 Next.js, Nuxt 같은 프레임워크 내부에 자리잡고 있고, 설정 커스터마이징을 해야 할 일이 반드시 생기기 때문이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;신규 프로젝트는 Vite로 시작하는 것이 맞다.&lt;/b&gt; DX 차이가 너무 크다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;엔터프라이즈와 Module Federation 환경이라면 웹팩이다.&lt;/b&gt; 대안이 아직 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;기존 웹팩 프로젝트의 속도를 개선하고 싶다면 Rspack 이주를 검토하라.&lt;/b&gt; config를 거의 그대로 쓰면서 속도 이득이 크다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;상황별 번들러 추천표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;상황&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;추천 번들러&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;신규 React/Vue SPA&lt;/td&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;td&gt;DX 압도적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js 앱&lt;/td&gt;
&lt;td&gt;Next 기본값 (웹팩)&lt;/td&gt;
&lt;td&gt;프레임워크 종속&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;마이크로프론트엔드&lt;/td&gt;
&lt;td&gt;Webpack 5&lt;/td&gt;
&lt;td&gt;Module Federation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;거대 레거시 유지보수&lt;/td&gt;
&lt;td&gt;Webpack 또는 Rspack&lt;/td&gt;
&lt;td&gt;호환성 + 속도&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;라이브러리 배포&lt;/td&gt;
&lt;td&gt;Rollup / tsup&lt;/td&gt;
&lt;td&gt;작은 번들&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초고속 빌드 필요&lt;/td&gt;
&lt;td&gt;esbuild / Rspack&lt;/td&gt;
&lt;td&gt;Rust/Go 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tobias Koppers가 2012년 독일의 한 연구실에서 석사 논문을 쓰다가 시작한 프로젝트가, 10년 넘게 프론트엔드 생태계 전체를 떠받치는 토대가 되었다. 지금은 더 빠른 도구들이 등장했지만, 그 도구들도 모두 웹팩이 만들어놓은 개념(모듈 그래프, 코드 스플리팅, HMR) 위에서 움직인다.&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;&lt;b&gt;웹팩 사용 이유&lt;/b&gt;를 한 줄로 정리하자면, &quot;프론트엔드를 모듈 단위로 개발하고 최적화하여 배포할 수 있게 만든 최초의 실용적 표준이기 때문&quot;이다. 신규 프로젝트에서 직접 쓸 일이 줄어들어도, 개념을 이해하는 것은 여전히 필수다. 외워두면 언젠가 반드시 도움이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 묻는 질문 (FAQ)&lt;/h2&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;b&gt;Q: 웹팩이 무엇인지 한 줄로 설명하면?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 JS 모듈과 CSS/이미지 같은 에셋을 하나(또는 여러 개)의 번들 파일로 합쳐주는 모듈 번들러다.&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;&lt;b&gt;Q: 웹팩을 만든 사람은 누구인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;독일 개발자 Tobias Koppers가 2012년 3월에 첫 커밋한 개인 프로젝트로 시작되었다. 현재는 Vercel에서 Turbopack 개발을 리드하고 있다.&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;&lt;b&gt;Q: 2026년에도 웹팩을 써야 하는가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;신규 프로젝트는 Vite가 낫다. 다만 Module Federation이나 거대 레거시 유지보수라면 여전히 웹팩이 답이다.&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;&lt;b&gt;Q: 웹팩과 Vite의 차이는 무엇인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹팩은 개발 중에도 번들링한다. Vite는 개발 시 esbuild와 네이티브 ESM으로 번들링하지 않고 바로 띄워서 빠르다. 프로덕션 빌드는 Vite도 Rollup으로 번들링한다.&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;&lt;b&gt;Q: webpack 5와 webpack 4의 차이는?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;webpack 5는 영구 캐싱, Module Federation, Asset Modules, 빌트인 트리 셰이킹 개선이 크다. webpack 4 대비 빌드 속도가 개선되고 번들 크기가 감소되었다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>Tobias Koppers</category>
      <category>webpack 개발자</category>
      <category>모듈 번들러</category>
      <category>번들러 비교</category>
      <category>웹팩 vs Vite</category>
      <category>웹팩 장단점</category>
      <category>웹팩이란</category>
      <category>프론트엔드 빌드 도구</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/384</guid>
      <comments>https://yscho03.tistory.com/384#entry384comment</comments>
      <pubDate>Tue, 28 Apr 2026 14:39:21 +0900</pubDate>
    </item>
    <item>
      <title>트위터 Grok은 무엇이고 왜 모두가 멘션을 다는가?</title>
      <link>https://yscho03.tistory.com/383</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://www.1ai.net/wp-content/uploads/2025/01/a16f0ea8j00sq9le0001ed000om00dcp.jpg&quot; data-phocus=&quot;https://www.1ai.net/wp-content/uploads/2025/01/a16f0ea8j00sq9le0001ed000om00dcp.jpg&quot;&gt;&lt;img src=&quot;https://www.1ai.net/wp-content/uploads/2025/01/a16f0ea8j00sq9le0001ed000om00dcp.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fwww.1ai.net%2Fwp-content%2Fuploads%2F2025%2F01%2Fa16f0ea8j00sq9le0001ed000om00dcp.jpg&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; loading=&quot;lazy&quot; width=&quot;886&quot; height=&quot;480&quot; data-origin-width=&quot;886&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.1ai.net/27288.html&quot;&gt;1ai.net&lt;/a&gt;&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;요즘 X(트위터) 타임라인을 보면 누군가 헛소리 트윗을 올렸을 때 그 밑에 &quot;@grok 이거 사실인가?&quot;라고 멘션을 다는 사람이 정말 많다. 이것이 바로 요즘 화제인 &lt;b&gt;트위터 Grok&lt;/b&gt;이다. 그러면 실제로 Grok이라는 계정이 답글로 팩트체크 비슷한 것을 달아 준다. 처음 보는 입장에서는 &quot;저 답봇은 무엇이고 누가 운영하는 것인가?&quot;라는 의문이 들지만, 알고 보면 일론 머스크의 회사가 X 앱에 내장해 둔 AI 챗봇이다. 이름은 &lt;a href=&quot;https://grok.com/&quot;&gt;Grok&lt;/a&gt;이고, 만든 곳은 &lt;a href=&quot;https://x.ai/&quot;&gt;xAI&lt;/a&gt;다.&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;그런데 이것은 단순한 답봇이 아니라 챗GPT, 제미나이, 클로드와 정면으로 맞붙는 본격 AI 모델이다. 그래서 트위터 Grok이 무엇이고, 어떻게 쓰며, 무료로 어디까지 풀려 있고, 다른 AI와는 무엇이 다른지 한 번에 정리해 보았다. 글을 다 읽으면 직접 멘션을 한 번 달아 볼 수 있도록 구성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Grok이 무엇인지부터 빠르게 정리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grok은 일론 머스크가 세운 xAI라는 회사가 만든 AI 챗봇이다. OpenAI 공동창업자였던 머스크가 OpenAI와 결별한 뒤 2023년에 별도로 차린 회사가 xAI이고, 그 첫 작품이 Grok이다. 처음 공개된 시점은 2023년 11월이며, 그때부터 X(구 트위터)에 통합되어 X 안에서 곧바로 쓸 수 있게 만든 점이 핵심 차별점이다.&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;이름의 어원이 다소 흥미로운데, 1961년에 출간된 로버트 하인라인의 SF 소설 &quot;낯선 땅 이방인(Stranger in a Strange Land)&quot;에 등장하는 화성어 단어다. 의미는 &quot;어떤 대상을 뼛속까지 완전히 이해한다&quot;이다. 머스크가 이 책을 매우 좋아한다고 알려져 있어 그대로 이름에 가져다 붙인 것이다.&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;다른 AI 챗봇과의 가장 큰 차이는 &lt;b&gt;실시간 X 게시물에 바로 접근할 수 있다&lt;/b&gt;는 점이다. 챗GPT는 보통 학습 데이터 컷오프가 있어 최신 사건을 잘 모르지만, Grok은 X 타임라인을 실시간으로 끌어와 답을 만들 수 있다. 그래서 &quot;방금 일어난 사건&quot;과 같은 질문에는 Grok이 강하다. 트위터 Grok이 다른 AI를 제치고 자리를 잡은 이유가 바로 이 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트위터에서 &quot;@grok&quot; 멘션은 어떻게 동작하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://newskurly.com/wp-content/uploads/2025/02/grok3-4-1024x736.webp&quot; data-phocus=&quot;https://newskurly.com/wp-content/uploads/2025/02/grok3-4-1024x736.webp&quot;&gt;&lt;img src=&quot;https://newskurly.com/wp-content/uploads/2025/02/grok3-4-1024x736.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fnewskurly.com%2Fwp-content%2Fuploads%2F2025%2F02%2Fgrok3-4-1024x736.webp&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; loading=&quot;lazy&quot; width=&quot;1024&quot; height=&quot;736&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://newskurly.com/%EA%B7%B8%EB%A1%9D3-%EC%82%AC%EC%9A%A9-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95-4%EA%B0%80%EC%A7%80/&quot;&gt;newskurly.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;답글에 &quot;@grok 사실인가?&quot;를 달면 무슨 일이 벌어지는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;X에서 누군가 올린 트윗 밑에 답글로 &quot;@grok 이거 진짜인가?&quot;라고 달면, 몇 초에서 몇 분 안에 Grok 공식 계정이 답글을 달아 준다. 동작 원리는 대략 다음과 같다.&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;Grok이 멘션을 감지한다.&lt;/li&gt;
&lt;li&gt;원본 트윗과 스레드 맥락을 읽는다.&lt;/li&gt;
&lt;li&gt;필요하다면 X 검색이나 외부 웹 검색까지 수행한다(DeepSearch 모드).&lt;/li&gt;
&lt;li&gt;멘션한 사람이 사용한 언어에 맞춰 한국어든 영어든 답한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년 후반부터는 한국어 멘션에 한국어로 답해 주는 비율이 꽤 올랐다. 이전에는 그냥 영어로 답해서 답답했지만, 지금은 제법 쓸 만한 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;멘션 답변에도 한계가 있다&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;답이 늦을 때가 있다&lt;/b&gt;: 트래픽이 몰리면 몇 분이 걸린다. 정말 핫한 이슈에 멘션을 달면 5~10분도 흔하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원본 트윗이 짧으면 맥락이 부족하다&lt;/b&gt;: 한 줄짜리 트윗에 &quot;사실인가?&quot;만 달면 Grok도 무엇을 검증하라는 것인지 헷갈려 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이미지/영상이 포함된 트윗&lt;/b&gt;: 텍스트는 잘 읽지만 이미지 속 글자 같은 것은 가끔 놓친다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Grok도 환각(hallucination)을 자주 일으킨다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 정말 중요한데, Grok이 &quot;AI라서 다 맞다&quot;라고 생각하면 큰일이 난다. Grok도 환각(hallucination)을 종종 일으킨다. 특히 출처가 없는 정보, 마이너한 인물, 한국 내수 이슈 같은 것을 물어보면 자신만만하게 틀린 답을 내놓을 때가 있다. 2024년 미국 대선 시기에도 Grok이 잘못된 투표 정보를 답해 비판받은 적이 있다. 그러므로 멘션 답변은 &quot;참고용&quot;이지 &quot;최종 팩트&quot;는 아니다. 중요한 사안은 직접 출처를 확인해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Grok 사용법: 무료로 어디까지 가능한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;608&quot;&gt;&lt;span data-url=&quot;https://images.unsplash.com/photo-1717143587138-2532a35ce9b2?crop=entropy&amp;amp;amp;cs=tinysrgb&amp;amp;amp;fit=max&amp;amp;amp;fm=jpg&amp;amp;amp;ixlib=rb-4.1.0&amp;amp;amp;q=80&amp;amp;amp;w=1080&quot; data-phocus=&quot;https://images.unsplash.com/photo-1717143587138-2532a35ce9b2?crop=entropy&amp;amp;amp;cs=tinysrgb&amp;amp;amp;fit=max&amp;amp;amp;fm=jpg&amp;amp;amp;ixlib=rb-4.1.0&amp;amp;amp;q=80&amp;amp;amp;w=1080&quot;&gt;&lt;img src=&quot;https://images.unsplash.com/photo-1717143587138-2532a35ce9b2?crop=entropy&amp;amp;cs=tinysrgb&amp;amp;fit=max&amp;amp;fm=jpg&amp;amp;ixlib=rb-4.1.0&amp;amp;q=80&amp;amp;w=1080&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fimages.unsplash.com%2Fphoto-1717143587138-2532a35ce9b2%3Fcrop%3Dentropy%26cs%3Dtinysrgb%26fit%3Dmax%26fm%3Djpg%26ixlib%3Drb-4.1.0%26q%3D80%26w%3D1080&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;608&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://unsplash.com/photos/a-black-and-white-photo-of-the-word-grok-9rDIpHOE9IY&quot;&gt;https://unsplash.com/photos/a-black-and-white-photo-of-the-word-grok-9rDIpHOE9IY&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;X 무료 계정에서도 사용할 수 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래 Grok은 X Premium 구독자만 쓸 수 있었지만, 2024년 12월부터 무료 계정에도 단계적으로 풀렸다. 지금(2026년 4월 기준)은 X에 가입만 하면 누구나 일정 한도 안에서 Grok을 쓸 수 있다. 다만 무료 계정에는 2시간당 사용 횟수 제한 같은 제약이 걸려 있다. 진지하게 쓸 생각이라면 결국 유료 구독이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;요금제를 정리한 표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;플랜&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;월 가격 (KRW 추정)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Grok 사용 한도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;비고&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;무료&lt;/td&gt;
&lt;td&gt;0원&lt;/td&gt;
&lt;td&gt;2시간당 몇 회 제한&lt;/td&gt;
&lt;td&gt;X 계정만 있으면 됨&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X Premium&lt;/td&gt;
&lt;td&gt;약 13,000원&lt;/td&gt;
&lt;td&gt;늘어남&lt;/td&gt;
&lt;td&gt;파란 체크 + 광고 줄어듦&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X Premium+&lt;/td&gt;
&lt;td&gt;약 32,000원&lt;/td&gt;
&lt;td&gt;더 큼&lt;/td&gt;
&lt;td&gt;Grok 우선 접근 + 최신 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SuperGrok&lt;/td&gt;
&lt;td&gt;별도 구독 (약 30달러)&lt;/td&gt;
&lt;td&gt;가장 큼&lt;/td&gt;
&lt;td&gt;최신 모델 + DeepSearch 무제한급&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격은 환율과 X 정책에 따라 자주 바뀐다. 한국 결제 기준이라 부가세 포함 가격은 살짝 더 비쌀 수 있다. 정확한 최신 가격은 X 앱 결제 화면에서 확인하는 것이 정답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;grok.com을 따로 사용하는 법도 있다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 Grok이 X 앱 안에서만 쓸 수 있는 것은 아니다. 웹사이트 &lt;a href=&quot;https://grok.com/&quot;&gt;grok.com&lt;/a&gt;이 따로 존재한다. 거기에 접속해 X 계정으로 로그인하면 챗GPT처럼 풀스크린 채팅 인터페이스로 쓸 수 있다. 모바일 앱도 별도로 제공된다(iOS/안드로이드). X 앱 멘션은 가볍게 한 줄짜리 질문을 던질 때 쓰고, 진지한 작업은 grok.com이나 전용 앱이 편하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Grok 버전 히스토리 &amp;mdash; 지금 어디까지 왔는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Grok이 처음 나왔을 때만 해도 챗GPT를 따라가기 바빴는데, 1년 반 만에 정말 빠르게 따라잡았다. 왜일까? 버전별로 정리하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Grok-1 (2023년 11월)&lt;/b&gt;: 첫 공개 버전이다. 매개변수는 약 314B 정도였고, 토큰 길이도 짧았다. 기본기만 갖춘 수준이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grok-1.5 (2024년 3월)&lt;/b&gt;: 컨텍스트 길이가 128K로 늘어났다. 긴 문서도 한 번에 읽을 수 있게 되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grok-2 (2024년 8월)&lt;/b&gt;: 이미지 생성 기능이 추가되었다. Flux 모델을 붙여 그림도 그려 준다. X에 통합되면서 사용량이 폭발했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grok-3 (2025년 2월)&lt;/b&gt;: 추론 모드 'Think'와 검색 증강 'DeepSearch'가 도입되었다. 챗GPT의 o1과 비슷한 추론 모델 라인업이 생겼다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Grok-4 (2025년 7월)&lt;/b&gt;: xAI가 &quot;지구상 가장 똑똑한 AI&quot;라고 마케팅했다. 멀티에이전트(여러 Grok이 협업해 답을 만드는) 구조가 도입되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2026년 현재&lt;/b&gt;: SuperGrok 구독으로 음성 모드, 비전 모드, 코드 어시스턴트가 통합되었다. 한국어 성능도 꽤 올라왔다.&lt;/li&gt;
&lt;/ul&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;a href=&quot;https://x.ai/news/colossus&quot;&gt;Colossus&lt;/a&gt;, 멤피스 소재 슈퍼컴퓨터)를 투입하고 GPU로 밀어붙인 결과다. 다른 회사들이 클라우드에 의존하는 사이에 xAI는 자체 H100 클러스터 10만 장 이상을 굴린다고 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Grok vs ChatGPT vs Gemini vs Claude &amp;mdash; 솔직한 비교&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Grok&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;ChatGPT&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Gemini&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Claude&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;만든 곳&lt;/td&gt;
&lt;td&gt;xAI (머스크)&lt;/td&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;실시간 X 데이터&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;/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;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;발전 중&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;검열 강도&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;/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;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;Think&lt;/td&gt;
&lt;td&gt;o1, o3&lt;/td&gt;
&lt;td&gt;Deep Think&lt;/td&gt;
&lt;td&gt;Extended Thinking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Grok&lt;/b&gt;: 트위터/X 콘텐츠 분석, 실시간 트렌드, 검열 없는 답변을 원한다면 최고다. 다만 한국어는 아직 챗GPT만큼은 아니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ChatGPT&lt;/b&gt;: 종합 능력이 1위다. 한국어를 정말 잘한다. 가장 안정적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Gemini&lt;/b&gt;: 구글 검색 통합과 멀티모달이 강하다. 다만 검열이 빡빡하다. 정치/민감 주제에는 답을 잘 주지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Claude&lt;/b&gt;: 코딩과 긴 글 작성이 1위이고 추론도 좋다. 다만 무료 한도가 짜고 한국 결제가 다소 불편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;머스크가 Grok을 마케팅할 때 &quot;다른 AI는 검열이 빡세지만 우리는 자유롭다&quot;라고 강조하는데, 실제로 비교해 보면 정말 그렇다. 정치, 음모론, 19금 같은 주제에도 Grok은 답을 해 주는 편이다. 다만 이것이 장점이자 단점이다. 검열이 약한 만큼 잘못된 정보, 편향된 답이 그대로 나올 위험도 크다. 2024년에는 &lt;a href=&quot;https://www.reuters.com/technology/five-state-secretaries-urge-musk-fix-xs-ai-chatbot-over-election-misinformation-2024-08-05/&quot;&gt;Grok이 가짜뉴스를 확산시킨다며 5개 주 검찰총장이 항의편지를 보낸 적&lt;/a&gt;도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;트위터 Grok, 누가 쓰면 좋고 누가 안 써도 되는가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Grok을 쓰면 좋은 사람&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X(트위터)를 매일 사용하는 사람 &amp;rarr; 어차피 앱에 내장되어 있으니 무료로라도 한 번 써 봐야 한다.&lt;/li&gt;
&lt;li&gt;실시간 트렌드, 핫이슈 요약이 자주 필요한 사람 &amp;rarr; DeepSearch가 정말 강하다.&lt;/li&gt;
&lt;li&gt;다른 AI의 검열에 답답함을 느끼는 사람 &amp;rarr; Grok이 가장 자유롭다.&lt;/li&gt;
&lt;li&gt;일론 머스크/우주/SF를 좋아하는 사람 &amp;rarr; 답변 톤이 다소 시니컬하고 재밌다.&lt;/li&gt;
&lt;/ul&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;b&gt;Grok을 쓰지 않아도 되는 사람&lt;/b&gt;:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코딩을 메인으로 쓰는 사람 &amp;rarr; Claude나 ChatGPT가 아직은 낫다.&lt;/li&gt;
&lt;li&gt;한국어 정확도가 1순위인 사람 &amp;rarr; 챗GPT가 더 안정적이다.&lt;/li&gt;
&lt;li&gt;사실 검증이 100% 필요한 업무용 &amp;rarr; 어떤 AI든 환각 위험이 있으니 더블체크가 필수다.&lt;/li&gt;
&lt;li&gt;X 계정이 없거나 머스크 자체가 싫은 사람 &amp;rarr; 굳이 만들 필요는 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 트위터 Grok은 &quot;X 생태계 안에 있는 사람에게 가장 강한 AI&quot;다. X를 쓰지 않는다면 grok.com에서 따로 사용해도 되지만, 그러면 굳이 Grok을 고집할 이유가 적어진다.&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;각설하고, X를 자주 쓴다면 무료 한도라도 한 번 멘션을 달아 보면 된다. 써 보지 않으면 감이 오지 않는다. &quot;@grok 이 글 한 줄 요약해 줘&quot; 같은 것부터 시작해 보면 된다. 그것이 트위터 Grok 입문의 가장 빠른 길이다.&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>grok ai</category>
      <category>grok 무료</category>
      <category>Grok 사용법</category>
      <category>Grok 챗GPT 차이</category>
      <category>X Grok</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/383</guid>
      <comments>https://yscho03.tistory.com/383#entry383comment</comments>
      <pubDate>Mon, 27 Apr 2026 23:02:04 +0900</pubDate>
    </item>
    <item>
      <title>SEO 다음은 AEO라는데, 이것은 도대체 무엇인가?</title>
      <link>https://yscho03.tistory.com/382</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;검색창에 무언가를 쳐본 적 있는가? 요즘은 결과가 뜨기 전에 구글이 답을 위에 그대로 박아버린다. ChatGPT에 물어봐도 출처까지 함께 알려주고, Perplexity는 아예 검색 결과 없이 답만 주는 것이 본업이다. 이게 무슨 소리인가 싶지만, &lt;b&gt;AEO&lt;/b&gt;를 하던 사람들은 지금 이것 때문에 멘붕 중이다. SEO를 10년 한 사람들도 &quot;내 트래픽이 다 어디로 갔는가&quot; 하면서 멘붕이 오는 중이다.&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;각설하고, 그래서 마케터들이 또 신조어를 만들었다. &lt;b&gt;AEO(Answer Engine Optimization)&lt;/b&gt;. 직역하면 &quot;답변 엔진 최적화&quot;이다. 처음 들었을 때는 &quot;이것도 SEO 우려먹기 아닌가&quot; 싶었는데, 까보면 진짜로 결이 다른 부분이 있다. 이 글에서는 AEO가 무엇인지, SEO와 진짜로 무엇이 다른지, 그리고 지금 당장 무엇부터 해야 하는지 5분 안에 정리한다. 한국 환경(네이버 Cue:, 카카오) 적용법까지 포함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AEO란 무엇인가 &amp;mdash; 한 문장으로 정리한다&lt;/h2&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;b&gt;AEO는 ChatGPT, Perplexity, Google AI Overviews 같은 AI 답변 엔진이 사용자 질문에 답할 때 내 콘텐츠를 인용하거나 출처로 쓰게 만드는 작업이다.&lt;/b&gt; 끝. 이것이 정의의 전부다.&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;기존 SEO가 &quot;구글 검색 결과 페이지(SERP) 상위에 떠서 클릭을 받는 게임&quot;이었다면, AEO는 &quot;AI 답변 안에 포함되는 게임&quot;이다. 아예 게임의 룰이 바뀐 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5NsSZ/dJMcabcTTel/LToMktC4cENeG42Jyh9Oh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5NsSZ/dJMcabcTTel/LToMktC4cENeG42Jyh9Oh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5NsSZ/dJMcabcTTel/LToMktC4cENeG42Jyh9Oh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5NsSZ%2FdJMcabcTTel%2FLToMktC4cENeG42Jyh9Oh0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1156&quot; height=&quot;866&quot; data-origin-width=&quot;1156&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://eopla.net/magazines/22965&quot;&gt;eopla.net&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Answer Engine Optimization 풀어쓰기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Answer Engine Optimization을 그대로 풀면 &quot;답변 엔진 최적화&quot;가 된다. 여기서 핵심 단어는 &quot;답변 엔진(Answer Engine)&quot;인데, 검색 엔진(Search Engine)과 헷갈리면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;검색 엔진&lt;/b&gt;: 키워드를 넣으면 관련 페이지 링크 10개를 던져준다 (구글, 네이버 전통 모드)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;답변 엔진&lt;/b&gt;: 질문을 넣으면 답을 직접 만들어주고 옆에 출처만 살짝 붙여준다 (Perplexity, ChatGPT, AI Overviews)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 차이가 진짜 큰 것이다. 검색 엔진은 &quot;골라보세요&quot; 모드인데 답변 엔진은 &quot;정답을 알려드린다&quot; 모드라서, 사용자가 링크를 클릭할 동기 자체가 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;답변 엔진이란 무엇인가&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;답변 엔진&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;출시/업데이트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;출처 표기&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**Google AI Overviews**&lt;/td&gt;
&lt;td&gt;2024년 5월 정식&lt;/td&gt;
&lt;td&gt;구글 검색 상단 자동 노출, 트래픽 영향 큼&lt;/td&gt;
&lt;td&gt;인용 카드 형태&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**Perplexity AI**&lt;/td&gt;
&lt;td&gt;2022~ (월간 1억 사용자)&lt;/td&gt;
&lt;td&gt;검색 자체를 답변형으로, 출처 명확&lt;/td&gt;
&lt;td&gt;번호 각주 [1][2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**ChatGPT Search**&lt;/td&gt;
&lt;td&gt;2024년 10월 31일&lt;/td&gt;
&lt;td&gt;GPT 답변 + 실시간 웹 인용&lt;/td&gt;
&lt;td&gt;사이드바 출처 카드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**Claude (Anthropic)**&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;**Gemini (Google)**&lt;/td&gt;
&lt;td&gt;Workspace 통합&lt;/td&gt;
&lt;td&gt;구글 데이터 기반 답변&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 다섯 개가 지금 영어권 메이저이다. 한국에서는 여기에 &lt;b&gt;네이버 Cue:&lt;/b&gt;와 카카오의 검색 AI도 끼어드는 중인데, 이것은 뒤에서 따로 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AEO와 SEO 차이 &amp;mdash; 솔직히 같은 것 아닌가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 봤을 때는 나도 &quot;이것은 그냥 SEO의 다른 이름 아닌가&quot; 싶었다. 다만 디테일을 까보면 결이 꽤 다르다. 표로 정리하면 이러하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비교 표로 한 번에 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;SEO&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;AEO&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**목표**&lt;/td&gt;
&lt;td&gt;SERP 상위 노출 + 클릭 유도&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;구글, 네이버, 빙 등 검색 엔진&lt;/td&gt;
&lt;td&gt;ChatGPT, Perplexity, AI Overviews 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**콘텐츠 형식**&lt;/td&gt;
&lt;td&gt;키워드 중심 긴 글&lt;/td&gt;
&lt;td&gt;질문&amp;rarr;답변 구조, 짧고 명료한 문단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**핵심 측정지표**&lt;/td&gt;
&lt;td&gt;순위, CTR, 트래픽&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;AI 답변에서 정보 소비 (제로클릭)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**기술 요소**&lt;/td&gt;
&lt;td&gt;백링크, 페이지 속도, 키워드&lt;/td&gt;
&lt;td&gt;구조화 데이터, E-E-A-T, 인용 가능성&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 핵심 차이는 마지막 줄이다. SEO는 &quot;내 사이트로 데려오는 게임&quot;이고 AEO는 &quot;내 정보가 답변 어딘가에 들어가는 게임&quot;이다. 트래픽이 들어오지 않아도 브랜드 인지도는 올라갈 수 있는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 SEO는 죽은 것인가?&lt;/h3&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;b&gt;죽지 않았다&lt;/b&gt;. 이것은 마케터들이 자극적으로 &quot;SEO is dead&quot;라고 떠드는 것이지 실제로는 SEO가 AEO의 기반이다.&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;왜일까? 생각해보면 당연한 일이다. AI가 답변을 만들 때 어디서 정보를 가져오는가? 결국 웹에서 가져온다. 그렇다면 그 웹페이지가 어떻게 AI의 눈에 띄는가? 검색 엔진이 인덱싱한 페이지 중에서 골라온다. 즉 SEO가 되어 있지 않은 사이트는 AI가 인용 자체를 하지 못한다.&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;그래서 정확한 메시지는 이것이다. &lt;b&gt;&quot;SEO 기반 위에 AEO 한 층을 더 쌓는 것이다.&quot;&lt;/b&gt; SEO는 1층, AEO는 2층. 1층 없이 2층을 올릴 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 지금 AEO 이야기가 갑자기 터진 것인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2024년에 들어 AEO 검색량이 폭발했다. 이유가 몇 가지 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Google AI Overviews 등장 (2024년 5월)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구글이 2024년 5월 I/O에서 AI Overviews를 정식 런칭했다. 검색하면 결과 위에 AI가 만든 요약이 박혀버리는 구조가 되었다. 이것이 도입된 후 &lt;a href=&quot;https://ahrefs.com/blog/&quot;&gt;Ahrefs 연구&lt;/a&gt; 결과 일부 키워드는 평균 CTR이 30% 가까이 빠졌다. 사용자가 답을 페이지 위에서 다 봐버리니 클릭하지 않는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Perplexity AI의 폭발적 성장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Perplexity는 2025년 기준 월간 활성 사용자 1억 명을 돌파한 것으로 추정된다. 이것은 사실상 검색 대체재 수준이다. 특히 정보 검색을 하는 IT/개발/투자 쪽 사람들이 구글을 쓰지 않고 Perplexity로 갈아탄 케이스가 정말로 많아졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;제로클릭 검색 60% 돌파&lt;/h3&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;a href=&quot;https://sparktoro.com/&quot;&gt;SparkToro 2024년 데이터&lt;/a&gt;에 따르면 구글 검색의 &lt;b&gt;65.4%가 제로클릭(Zero-click)&lt;/b&gt;이다. 검색만 하고 아무 링크도 클릭하지 않는다는 뜻이다. AI Overviews가 들어오기 전부터도 60%를 넘었는데, AI 요약이 들어가니 더 심해졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국에서도 동시에 터지는 중&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;네이버 Cue:&lt;/b&gt; &amp;mdash; 네이버판 답변 엔진. 2023년 베타 후 2024년 본격 확장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구글 AI 모드 한국어 지원&lt;/b&gt; &amp;mdash; 2024년 후반부터 한국어 답변 품질이 급격히 좋아졌다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;카카오 검색 AI&lt;/b&gt; &amp;mdash; 2025년에 들어 카카오톡 안에서 답변형 검색을 강화하는 중&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 한국 시장도 영어권보다 1년 정도 늦을 뿐 같은 흐름으로 가고 있다. 지금 AEO를 챙기지 않으면 1~2년 뒤에 트래픽이 다 빠질 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 AEO를 어떻게 시작하는가? 5단계로 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론은 됐고 실전이다. 지금 당장 글 하나를 쓰면서 적용할 수 있는 5단계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계 &amp;mdash; 질문 형태로 콘텐츠 구조 짜기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;H2, H3 제목을 &lt;b&gt;사용자가 실제로 검색하는 질문 형태&lt;/b&gt;로 바꾸는 것이다. AI 답변 엔진은 &quot;질문&amp;rarr;답변&quot; 패턴을 학습했기 때문에 이 구조를 따르는 콘텐츠를 인용하기 쉽다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;❌ &quot;AEO의 정의&quot; &amp;rarr; 너무 추상적&lt;/li&gt;
&lt;li&gt;✅ &quot;AEO란 무엇인가&quot; &amp;rarr; 사용자 질문 그대로&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글의 H2 제목을 보면 모두 질문형이거나 직설적인 형태이다. 그것이 의도된 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계 &amp;mdash; 답변을 첫 단락에 배치 (역피라미드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;p data-ke-size=&quot;size16&quot;&gt;H2 제목 바로 다음 문단에 &lt;b&gt;40~60단어 안짝의 명확한 답변&lt;/b&gt;을 박아주면 피처드 스니펫(Featured Snippet)으로도 잡히고 AI 인용 확률도 올라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계 &amp;mdash; Schema.org 구조화 데이터 박기&lt;/h3&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;a href=&quot;https://schema.org/&quot;&gt;Schema.org&lt;/a&gt; 명세에 따라 페이지에 구조화 데이터를 박아주는 것이다. 특히 효과가 좋은 것 세 가지:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FAQPage&lt;/b&gt; &amp;mdash; 자주 묻는 질문 섹션&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HowTo&lt;/b&gt; &amp;mdash; 단계별 가이드 형식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Article&lt;/b&gt; &amp;mdash; 일반 글에 저자/발행일 메타데이터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;워드프레스라면 Rank Math, Yoast SEO 같은 플러그인이 자동으로 박아준다. 직접 코딩한다면 JSON-LD 형식으로 head에 넣으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계 &amp;mdash; E-E-A-T 신호 강화&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;E-E-A-T는 Experience, Expertise, Authoritativeness, Trustworthiness이다. 구글이 콘텐츠 품질을 평가할 때 쓰는 기준인데 AI 인용 알고리즘도 거의 비슷한 신호를 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;저자 정보 페이지를 만들고 본문에 링크&lt;/li&gt;
&lt;li&gt;인용한 출처를 명확히 표기 (이 글처럼)&lt;/li&gt;
&lt;li&gt;발행일/업데이트일 노출&lt;/li&gt;
&lt;li&gt;근거 데이터에는 출처와 연도를 함께&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5단계 &amp;mdash; AI 인용 모니터링 도구 세팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;h2 data-ke-size=&quot;size26&quot;&gt;진짜 쓸 만한 AEO 도구 정리&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모니터링 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Otterly.AI&lt;/b&gt; &amp;mdash; ChatGPT, Perplexity, AI Overviews에서 내 브랜드/도메인이 얼마나 언급되는지 추적한다. 월 $29부터. 작은 블로그라면 무료 플랜으로도 시작 가능하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Profound&lt;/b&gt; &amp;mdash; 엔터프라이즈급. AI 검색 인용 분석 + 경쟁사 비교까지. 가격이 비싸 회사용이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Peec AI&lt;/b&gt; &amp;mdash; 유럽 스타트업인데 가성비가 정말로 좋다. 무료 트라이얼이 길다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;콘텐츠 최적화 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Surfer SEO (AEO 모드)&lt;/b&gt; &amp;mdash; 기존 SEO 도구가 AEO 점수까지 추가했다. 글을 쓰면서 실시간으로 코칭한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Frase&lt;/b&gt; &amp;mdash; 질문&amp;rarr;답변 구조 자동 추출. AEO 콘텐츠를 빠르게 뽑을 때 좋다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Clearscope&lt;/b&gt; &amp;mdash; 시맨틱 키워드 분석. AEO보다는 SEO 쪽이 강한데 함께 쓴다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무료로 시작하는 법&lt;/h3&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;&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;Google Search Console&lt;/b&gt; &amp;mdash; 어떤 쿼리로 들어오는지 확인 (무료)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ChatGPT/Perplexity 직접 쿼리&lt;/b&gt; &amp;mdash; 본인 분야 핵심 질문 10개를 넣고 누가 인용되는지 수동으로 확인&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Schema Markup Validator&lt;/b&gt; &amp;mdash; 구조화 데이터가 제대로 박혔는지 검증 (구글 무료 도구)&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;한국에서 AEO를 할 때 추가로 봐야 하는 것&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네이버 Cue: 노출 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버 Cue:는 네이버판 답변 엔진이다. 영어권 도구와 다르게 &lt;b&gt;네이버 자체 데이터(블로그, 카페, 지식iN)를 우선 인용&lt;/b&gt;한다. 그래서:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네이버 블로그를 운영하면 우선순위가 높다&lt;/li&gt;
&lt;li&gt;일반 웹사이트는 네이버 웹마스터도구 등록 + 사이트맵 제출 필수&lt;/li&gt;
&lt;li&gt;콘텐츠가 한국어 자연어로 잘 풀어쓴 것 우선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;카카오 검색 AI 동향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카카오는 2025년부터 카카오톡 안에서 검색 AI 답변을 강화하는 중이다. 다음 검색 인프라를 활용해 만들어지는 중인데, 아직 영향력은 네이버보다 작다. 일단 다음 검색 노출이 잡혀 있으면 함께 따라온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한국어 LLM 인용 패턴 (HyperCLOVA X 등)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네이버의 HyperCLOVA X나 카카오의 Ko-GPT 같은 한국어 특화 LLM은 한국어 콘텐츠에 가중치를 더 준다. 영어 콘텐츠를 그대로 번역해 올린 것 말고 &lt;b&gt;한국어로 처음부터 쓴 콘텐츠&lt;/b&gt;가 인용 확률이 훨씬 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 하는 질문 모음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AEO를 하면 SEO는 그만해도 되는가?&lt;/h3&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;b&gt;아니다. 절대 아니다.&lt;/b&gt; 위에서 말했듯이 AEO는 SEO 위에 쌓는 것이다. SEO가 되어 있지 않으면 AI가 인용을 하지 못한다. 인덱싱이 먼저고 인용이 두 번째이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로그 운영자도 AEO를 해야 하는가?&lt;/h3&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;b&gt;해야 한다.&lt;/b&gt; 오히려 개인 블로거가 더 빨리 적응해야 한다. 큰 사이트들은 관성 때문에 늦게 바뀌는데, 개인 블로그는 글 한 편씩 바로 적용 가능하다. 1단계(질문형 H2), 2단계(첫 문단 답변)만 해도 차이가 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GEO와 AEO와 LLMO는 다 다른 것인가?&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AEO (Answer Engine Optimization)&lt;/b&gt; &amp;mdash; 답변 엔진 전반 최적화. 가장 포괄적이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GEO (Generative Engine Optimization)&lt;/b&gt; &amp;mdash; 생성형 AI 답변 안에 들어가게 하는 것. AEO와 거의 동의어이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LLMO (LLM Optimization)&lt;/b&gt; &amp;mdash; 대규모 언어모델 학습 데이터에 영향을 주는 것. 더 깊은 레이어이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마케터들이 신조어 경쟁 중인 것이다. 사실 셋 다 큰 틀에서는 같은 일이다. AEO만 알아도 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AEO 핵심 정리 &amp;mdash; 결국 이것 5줄이다&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;AEO는 ChatGPT&amp;middot;Perplexity&amp;middot;AI Overviews에 내 콘텐츠를 인용시키는 작업이다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;SEO는 죽지 않았다. AEO는 SEO 위에 한 층을 더 쌓는 것이다&lt;/li&gt;
&lt;li&gt;2024년 AI Overviews 등장 + 제로클릭 65% 돌파가 트리거이다&lt;/li&gt;
&lt;li&gt;5단계만 챙기면 된다: 질문형 구조 / 첫 단락 답변 / Schema 데이터 / E-E-A-T / 모니터링&lt;/li&gt;
&lt;li&gt;한국에서는 네이버 Cue:를 추가로 챙겨야 한다&lt;/li&gt;
&lt;/ul&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;b&gt;지금 당장 할 것 하나만 추천&lt;/b&gt;한다면, 본인이 운영하는 블로그 글 아무거나 하나를 열어 첫 단락을 보라. 거기에 명확한 답이 40단어 안짝으로 들어 있는지. 없다면 그것부터 고치는 것이 1번이다. 이것 하나만 바꿔도 AI 인용 확률이 체감되게 올라간다.&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;AEO가 어렵게 들리지만 본질은 단순하다. &lt;b&gt;&quot;AI가 인용하기 좋은 형태로 글을 쓴다&quot;&lt;/b&gt; 이것 하나이다. 마케터 신조어가 또 나왔다고 무시하지 말고, 1~2년 뒤 트래픽을 지키려면 지금부터 천천히 적응해놓는 것이 답이다.&lt;/p&gt;</description>
      <category>개발 지식in</category>
      <category>AEO SEO 차이</category>
      <category>AEO 뜻</category>
      <category>Answer Engine Optimization</category>
      <category>Geo</category>
      <category>Google AI Overviews</category>
      <category>LLMO</category>
      <category>perplexity</category>
      <category>답변 엔진 최적화</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/382</guid>
      <comments>https://yscho03.tistory.com/382#entry382comment</comments>
      <pubDate>Mon, 27 Apr 2026 23:00:18 +0900</pubDate>
    </item>
    <item>
      <title>TypeScript 7.0 Beta 공개, 무엇이 달라졌는가</title>
      <link>https://yscho03.tistory.com/381</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;350&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDTz3A/dJMcaciB8ei/ONlXplcTp1ykBzZ9NRYdIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDTz3A/dJMcaciB8ei/ONlXplcTp1ykBzZ9NRYdIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDTz3A/dJMcaciB8ei/ONlXplcTp1ykBzZ9NRYdIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDTz3A%2FdJMcaciB8ei%2FONlXplcTp1ykBzZ9NRYdIK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;562&quot; height=&quot;350&quot; data-origin-width=&quot;562&quot; data-origin-height=&quot;350&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.neowin.net/news/microsoft-explains-how-to-download-and-install-the-new-10-times-faster-typescript/&quot;&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;출처:&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/&quot;&gt;Microsoft Developer Blogs | 3일 전 (41KB)&lt;/a&gt;&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;tsc가 10배 빨라졌다는 소식은 사실이다. MS가 2026년 4월에 &lt;b&gt;TypeScript 7.0 Beta&lt;/b&gt;를 공개했는데, 한 줄로 요약하면 &lt;b&gt;컴파일러를 Go로 다시 작성한 것이다.&lt;/b&gt; 그 결과 VS Code 1.5M LOC 기준 타입체크가 77.8초에서 7.5초로 줄었다. 메모리 사용량도 절반이다.&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;다만 솔직히 5.x도 충분히 잘 돌아가지 않았는가. 굳이 7.0이 필요했던 이유, 그리고 5.x에서 갈아탈 때 무엇이 깨지는지가 진짜 궁금한 지점이다. 6.0이 어디로 갔는지도 의문이다.&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;이 글에서는 &lt;b&gt;TypeScript 7.0의 핵심 변경점, 버전 점프 사유, 마이그레이션 영향, ESLint/ts-node 같은 생태계 도구 호환성, 지금 도입해도 되는지 판단 기준&lt;/b&gt;까지 정리한다. 베타 시점 한국어 정리 글이 거의 없어서 출처를 모아 한 번에 묶었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TypeScript 7.0은 무엇이며, 왜 6.0을 건너뛰었는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본격 변경점에 들어가기 전에 버전 넘버링부터 짚고 갈 필요가 있다. 5.9 다음이 6.0이 아니라 7.0인 것이 어색하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드명 &quot;Corsa&quot;부터 7.0까지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS는 2025년 3월에 &lt;a href=&quot;https://devblogs.microsoft.com/typescript/typescript-native-port/&quot;&gt;&lt;b&gt;TypeScript Native Port&lt;/b&gt;&lt;/a&gt;라는 이름으로 새 컴파일러 프로젝트를 공개했다. 코드명은 &quot;Corsa&quot;였다. 발표 당시에는 별도 저장소(&lt;a href=&quot;https://github.com/microsoft/typescript-go&quot;&gt;microsoft/typescript-go&lt;/a&gt;)에서 개발 중이었고, 1년 정도 베이크 타임을 거친 끝에 2026년 4월 메인 라인 7.0 Beta로 통합됐다.&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;요약하면 1년 사이에 일어난 일이다. 별도 실험 &amp;rarr; 동등성 검증 &amp;rarr; 정식 7.0 흡수의 흐름이다. 이것이 빠르다고 느낄 수도 있는데, Anders Hejlsberg가 직접 코어를 작성한 프로젝트라 진행이 빨랐던 듯하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.0이 빠진 진짜 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6.0이 그냥 사라진 것이 아니다. &lt;b&gt;5.x 라인을 별도 유지보수 라인으로 분리&lt;/b&gt;하기 위해 비워둔 것이다. 즉 의미는 다음과 같이 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;5.x&lt;/b&gt;: 기존 JS 기반 컴파일러. 점진적 패치만 받는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;6.x&lt;/b&gt;: 비워둠 (5.x LTS 라인 보호용 갭)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;7.x&lt;/b&gt;: Go 네이티브 컴파일러로 완전히 전환된 새 라인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 식으로 메이저 점프를 줘야 &quot;5.x는 안전하게 쓰고 있고, 7.0은 옵트인&quot;이라는 메시지가 명확해진다. 5.9에서 6.0으로 가는 형태였다면 자동 업그레이드 환경에서 사고가 나기 쉬웠을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Anders Hejlsberg가 직접 작성한 Go 포팅&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 화제였다. Hejlsberg는 Turbo Pascal, Delphi, C#을 만든 사람이고 TypeScript 원조 설계자다. 보통 이런 위치라면 매니지먼트 모드로 돌아가는데, &lt;b&gt;본인이 직접 Go 포트 핵심 코드를 작성했다&lt;/b&gt;. 인터뷰에서는 &quot;타입 체커는 내가 가장 잘 아는 영역이라 직접 하는 것이 빨랐다&quot;고 밝혔다.&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;이 사실의 의미는 크다. 단순 리라이트가 아니라 &lt;b&gt;원작자가 다시 작성한 동등 구현&lt;/b&gt;이다. 그래서 Edge case 동작도 1:1로 보존하기 쉬웠던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;컴파일러를 Go로 다시 작성했다, 그렇다면 왜 Go였는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심 변경점의 첫 번째다. JS로 작성된 tsc를 Go로 다시 작성한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rust가 아니라 Go였던 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;후보로 Rust도 있었다. 다만 최종적으로 Go가 채택됐다. 이유는 인터뷰에서 두 가지로 정리된다.&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;&lt;b&gt;첫째, 코드 구조 호환성이다.&lt;/b&gt; 기존 TypeScript 컴파일러는 JS로 작성됐고, 클래스/객체/포인터-같은-참조에 의존하는 자료구조가 많다. Go는 mutable struct + pointer 모델이라 JS의 객체 그래프를 그대로 옮기기 좋다. Rust는 borrow checker 때문에 그래프 구조를 옮기는 데 매번 싸워야 한다. &lt;b&gt;1:1 포팅이 목표였기 때문에 Go가 압도적으로 유리했다.&lt;/b&gt;&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;&lt;b&gt;둘째, 빌드 속도와 GC 모델이다.&lt;/b&gt; Go는 컴파일이 빨라서 컴파일러 자체 빌드가 빠르고, GC가 있어서 short-lived 객체를 다루는 컴파일러 워크로드에 잘 맞는다. Rust로 갔다면 Arena allocator 같은 것을 직접 작성해야 했을 것이다.&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;물론 &quot;Rust였다면 더 빨랐을 것&quot;이라는 의견도 있다. 다만 MS 입장에서는 &lt;b&gt;출시 시점이 중요했다&lt;/b&gt;. Go로 18개월 만에 출시하는 것이 Rust로 30개월 걸리는 것보다 가치가 컸던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;같은 언어를 두 번 구현한다는 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 사실 흔치 않은 결정이다. 보통 컴파일러 리라이트는 syntax나 semantics를 갈아엎으면서 함께 진행한다. 그러나 TS 7.0은 &lt;b&gt;언어 의미는 하나도 바꾸지 않고&lt;/b&gt; 구현체만 교체했다.&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;장점은 명확하다. &lt;b&gt;마이그레이션 비용이 거의 0이다.&lt;/b&gt; tsconfig 그대로, 타입 정의 그대로, 빌드 결과물도 동일하다. 단지 tsc 실행이 빨라졌을 뿐이다.&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;단점도 있다. &lt;b&gt;두 가지 구현을 동시에 유지해야 한다.&lt;/b&gt; 5.x는 JS 컴파일러로 계속 패치되고, 7.x는 Go로 새 기능이 추가된다. 한동안 양쪽 모두 살아 있어야 한다. MS가 이 부담을 지기로 한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;속도 &amp;mdash; 정말로 10배 빨라졌다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgalhi/dJMcai38v1G/j4jws6DVnPtZm0wxnPKwUK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgalhi/dJMcai38v1G/j4jws6DVnPtZm0wxnPKwUK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgalhi/dJMcai38v1G/j4jws6DVnPtZm0wxnPKwUK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcgalhi%2FdJMcai38v1G%2Fj4jws6DVnPtZm0wxnPKwUK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;736&quot; height=&quot;552&quot; data-origin-width=&quot;736&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.pinterest.com/pin/84442561758052546/&quot;&gt;https://www.pinterest.com/pin/84442561758052546/&lt;/a&gt;&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;본론이다. 모두가 궁금해하는 것은 &quot;그래서 얼마나 빨라졌는가&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;공식 벤치마크 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS가 공개한 수치를 표로 정리하면 다음과 같다. 출처는 &lt;a href=&quot;https://devblogs.microsoft.com/typescript/&quot;&gt;Microsoft DevBlog 공식 발표&lt;/a&gt;이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;프로젝트&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;5.x 타입체크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;7.0 타입체크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;배율&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code (1.5M LOC)&lt;/td&gt;
&lt;td&gt;77.8초&lt;/td&gt;
&lt;td&gt;7.5초&lt;/td&gt;
&lt;td&gt;**10.4배**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright&lt;/td&gt;
&lt;td&gt;11.1초&lt;/td&gt;
&lt;td&gt;1.1초&lt;/td&gt;
&lt;td&gt;**10.0배**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TypeORM&lt;/td&gt;
&lt;td&gt;17.5초&lt;/td&gt;
&lt;td&gt;1.3초&lt;/td&gt;
&lt;td&gt;**13.4배**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;date-fns&lt;/td&gt;
&lt;td&gt;6.5초&lt;/td&gt;
&lt;td&gt;0.7초&lt;/td&gt;
&lt;td&gt;**9.2배**&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RxJS&lt;/td&gt;
&lt;td&gt;1.1초&lt;/td&gt;
&lt;td&gt;0.1초&lt;/td&gt;
&lt;td&gt;**10.6배**&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평균 10배 정도 빠르다. 작은 프로젝트보다 큰 프로젝트에서 효과가 더 크다. 왜일까? Go 버전이 멀티코어를 본격 활용하기 때문이다. JS 버전은 single-thread 기반이라 코어 많은 머신에서도 1코어만 사용했지만, 7.0은 모듈 단위 병렬 타입체크가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메모리 사용량 절반&lt;/h3&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;b&gt;메모리도 절반 이하로 줄었다.&lt;/b&gt; VS Code 빌드 기준 4.2GB &amp;rarr; 2.1GB 수준이다. 이것이 가능한 이유는 두 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GC 효율&lt;/b&gt;: Go GC는 짧은 수명 객체에 최적화돼 있다. JS V8은 일반 목적이라 컴파일러처럼 객체가 폭발하는 워크로드에서 비효율적이었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;자료구조 패킹&lt;/b&gt;: JS 객체는 hidden class + property 메타데이터 때문에 같은 정보라도 메모리를 더 많이 사용한다. Go struct는 그저 필드만 들어간다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 환경에서 메모리가 빡빡하던 사용자에게는 이것이 속도보다 더 반가운 변화일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LSP 응답성: 에디터 멈춤이 사라졌다&lt;/h3&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;b&gt;tsserver(LSP)&lt;/b&gt; 부분이다. 큰 프로젝트에서 VS Code가 멈추는 경험은 누구나 해봤을 것이다. 자동완성을 1초 기다리고, 타입 인포가 뜨는 데 2초가 걸리고, 'Find References'를 누르면 화면이 굳는다.&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;7.0에서는 이것이 거의 사라졌다. 같은 프로젝트에서 자동완성이 50ms 안쪽으로 표시된다. Hejlsberg가 데모에서 보여준 영상을 보면 정말 즉각적이다. 이는 단순히 컴파일러가 빨라진 것이 아니라, &lt;b&gt;에디터 작업 흐름이 바뀌는 수준의 변화&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Next.js / NestJS / 모노레포 영향 추정&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Next.js 프로젝트&lt;/b&gt;: next build 시 TS 체크가 보통 30~60초 잡아먹는데, 3~6초 수준으로 떨어질 가능성이 높다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;NestJS 백엔드&lt;/b&gt;: 데코레이터/메타데이터가 많아서 5.x에서 느렸다. 7.0에서 가장 큰 수혜를 볼 영역 중 하나다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Turborepo/Nx 모노레포&lt;/b&gt;: 패키지마다 tsc를 돌리는 구조라 누적 시간이 길었다. 7.0에서는 누적 빌드 시간이 1/10 수준으로 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI 비용 절감 효과도 무시할 수 없다. GitHub Actions에서 빌드가 5분 &amp;rarr; 1분 수준으로 줄면 월 사용량 자체가 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;tsgo 바이너리, 배포 방식이 바뀌었다&lt;/h2&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;b&gt;배포 방식&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;npm install로 받는 것이 바이너리가 된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 npm install -D typescript를 실행하면 JS 파일 한 뭉치가 설치됐다. 7.0부터는 다르다. &lt;b&gt;플랫폼별 prebuilt 바이너리&lt;/b&gt;가 함께 설치된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777298145968&quot; class=&quot;elm&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;node_modules/
  typescript/
    bin/
      tsgo            &amp;lt;- Go로 컴파일된 네이티브 바이너리
      tsc             &amp;lt;- 호환성 래퍼 (tsgo 호출)
&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;tsc 명령은 그대로 동작한다. 내부적으로 tsgo를 호출하는 방식이다. 호환성이 깨질 일은 거의 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;플랫폼별 prebuilt: Windows / macOS / Linux&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows x64, ARM64&lt;/li&gt;
&lt;li&gt;macOS x64, ARM64 (Apple Silicon)&lt;/li&gt;
&lt;li&gt;Linux x64, ARM64&lt;/li&gt;
&lt;li&gt;Linux musl(Alpine) &amp;mdash; Docker 이미지용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치 시 npm이 자동으로 플랫폼을 감지해서 맞는 바이너리만 받는다. 멀티플랫폼 모노레포에서도 신경 쓸 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Node 의존성 제거 (tsx, ts-node 같은 도구 영향)&lt;/h3&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;b&gt;tsc 자체는 더 이상 Node가 필요 없다.&lt;/b&gt; 다만 ts-node, tsx 같은 런타임 트랜스파일러는 여전히 Node 위에서 동작한다.&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;7.0에서는 이런 도구들도 영향을 받는다. tsx는 esbuild 기반이라 큰 영향이 없지만, ts-node는 내부적으로 TS 컴파일러 API를 호출한다. &lt;b&gt;공식 API는 호환성이 유지되지만, 내부 구조에 의존하던 패치 도구(예: ttypescript)는 깨질 수 있다.&lt;/b&gt;&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;베타 시점에 ts-node는 7.0과 동작하지만, 일부 커스텀 트랜스포머 사용자는 마이그레이션이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5.x에서 7.0으로 갈아타도 되는가? 마이그레이션 가이드&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bFr7lJ/dJMcai38v1F/iJCY4Lq68lwoxcmkpbkz1K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bFr7lJ/dJMcai38v1F/iJCY4Lq68lwoxcmkpbkz1K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bFr7lJ/dJMcai38v1F/iJCY4Lq68lwoxcmkpbkz1K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbFr7lJ%2FdJMcai38v1F%2FiJCY4Lq68lwoxcmkpbkz1K%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;1080&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1080&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://unsplash.com/photos/a-blue-and-white-logo-on-a-white-background-4igDDHdrnhE&quot;&gt;https://unsplash.com/photos/a-blue-and-white-logo-on-a-white-background-4igDDHdrnhE&lt;/a&gt;&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;가장 실무적인 질문이다. 결론부터 말하면 &lt;b&gt;베타이므로 신중히, 사이드 프로젝트는 OK, 회사 코드는 RC 이후 권장&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;호환성: tsconfig는 그대로 작동한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 시나리오에서는 정말로 아무 작업 없이 동작한다. tsconfig.json 그대로, package.json에서 &quot;typescript&quot;: &quot;^7.0.0-beta&quot;로 변경하기만 하면 된다. 빌드 결과물(JS 출력)도 byte 단위로 동일하다 &amp;mdash; MS가 회귀 테스트를 그렇게 설계했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;깨질 수 있는 것들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &quot;보통&quot; 시나리오를 벗어나면 이슈가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커스텀 트랜스포머&lt;/b&gt;: ttypescript / ts-patch 같은 도구로 컴파일러를 패치하던 사용자 &amp;mdash; 베타에서는 동작하지 않는다. 7.0이 트랜스포머 API를 새로 정의 중이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TypeScript 컴파일러 API 직접 사용&lt;/b&gt;: ESLint 플러그인 일부, 빌드 도구 일부. 메이저 도구는 곧 패치되겠지만 마이너한 것은 시간이 걸린다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;타입 체크 비표준 동작에 의존하던 코드&lt;/b&gt;: 예를 들어 5.x에서 우연히 통과하던 inferring edge case가 7.0에서 더 엄격해질 수 있다. 실제로 GitHub 이슈에 이런 리포트가 몇 건 올라와 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;typescript-eslint, ts-node, tsx 호환성&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;도구&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;7.0 호환 상태&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;메모&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;typescript-eslint&lt;/td&gt;
&lt;td&gt;부분 호환&lt;/td&gt;
&lt;td&gt;7.x 대응 PR 진행 중. 메이저 룰은 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ts-node&lt;/td&gt;
&lt;td&gt;동작함&lt;/td&gt;
&lt;td&gt;일부 옵션은 deprecated 예고&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tsx&lt;/td&gt;
&lt;td&gt;동작함&lt;/td&gt;
&lt;td&gt;esbuild 기반이라 영향 적음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ttypescript&lt;/td&gt;
&lt;td&gt;미동작&lt;/td&gt;
&lt;td&gt;트랜스포머 API 미정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Webpack ts-loader&lt;/td&gt;
&lt;td&gt;동작함&lt;/td&gt;
&lt;td&gt;공식 API만 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vite&lt;/td&gt;
&lt;td&gt;동작함&lt;/td&gt;
&lt;td&gt;별도 영향 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jest ts-jest&lt;/td&gt;
&lt;td&gt;부분 호환&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ESLint 사용자는 &lt;b&gt;typescript-eslint 정식 7.0 대응 릴리스가 나오기 전까지는 회사 프로젝트 도입을 자제&lt;/b&gt;하는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.x 라인 EOL 일정 (확정 안 됨)&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;5.x&lt;/b&gt;: 7.0 정식 출시 후 최소 12개월 보안 패치 유지 (예상)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;5.9&lt;/b&gt;: 마지막 5.x 메이저로 굳어질 가능성이 있음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;6.0&lt;/b&gt;: 영구 결번&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확정 일정은 7.0 정식 GA 발표(2026년 하반기 예상) 시점에 함께 나올 듯하다. 그때까지는 5.x를 안심하고 사용해도 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그렇다면 지금 도입해야 하는가? 의사결정 체크리스트&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사이드 프로젝트 &amp;mdash; 바로 OK&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인 토이 프로젝트, 학습용, 데모는 그냥 7.0 베타를 설치해도 된다. 깨져도 손해가 없고, 속도 차이를 체감해 보는 것이 가치 있다. 설치는 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777298145971&quot; class=&quot;coffeescript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install -D typescript@beta
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회사 모노레포 &amp;mdash; RC 이후 권장&lt;/h3&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;b&gt;RC(Release Candidate)가 나올 때까지는 기다리는 것이 안전&lt;/b&gt;하다. 이유는 두 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://typescript-eslint.io/&quot;&gt;typescript-eslint&lt;/a&gt; 같은 핵심 도구가 아직 100% 호환되지 않는다&lt;/li&gt;
&lt;li&gt;회사 빌드 파이프라인이 일부 내부 API에 의존하고 있을 가능성이 있다&lt;/li&gt;
&lt;/ul&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;b&gt;실험용 브랜치&lt;/b&gt;에서 7.0 빌드를 돌려보고 CI 시간 차이를 측정해 두는 작업은 지금부터 해도 된다. 정식 출시 시점에 즉시 마이그레이션 결정을 하려면 데이터가 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CI 빌드 시간이 진짜 병목이라면&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 CI에서 TS 체크가 가장 큰 시간을 차지하고 있고, 그것이 비즈니스에 직접 영향(배포 속도, 개발자 대기 시간)을 주는 상황이라면 &lt;b&gt;부분 도입을 시도해 볼 가치가 있다&lt;/b&gt;. 예를 들어 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PR 검증용 사이드 잡으로만 7.0을 돌려서 빠른 피드백 받기&lt;/li&gt;
&lt;li&gt;메인 빌드는 5.x 유지, 캐시 미스 시 7.0으로 fallback 빌드&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도입 전 확인할 것 5가지&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[ ] &lt;b&gt;typescript-eslint&lt;/b&gt;: 사용 중인 룰셋이 7.0에서 동작하는지&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;커스텀 트랜스포머&lt;/b&gt;: ttypescript/ts-patch 의존 여부&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;빌드 도구&lt;/b&gt;: Webpack/Vite/Turborepo가 7.0을 공식 지원하는지&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;타입 정의&lt;/b&gt;: @types/* 패키지 중 7.0에서 깨지는 것이 없는지 (대부분 OK)&lt;/li&gt;
&lt;li&gt;[ ] &lt;b&gt;CI 환경&lt;/b&gt;: prebuilt 바이너리 플랫폼이 사용 중인 CI(Linux x64/musl)와 일치하는지&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;TypeScript 7.0이 결국 개발자에게 의미하는 것&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심을 5줄로 압축하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TypeScript 7.0&lt;/b&gt;은 컴파일러를 Go로 다시 작성한 메이저 릴리스다. 언어 의미는 그대로다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;속도 10배, 메모리 절반.&lt;/b&gt; VS Code 빌드 기준 77.8초 &amp;rarr; 7.5초다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;6.0은 결번&lt;/b&gt;이다. 5.x LTS 라인을 보호하려고 메이저 점프를 시킨 것이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;베타 시점이므로&lt;/b&gt; 사이드 프로젝트는 OK, 회사 코드는 RC 이후 권장이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;typescript-eslint, ttypescript, ts-node&lt;/b&gt; 같은 생태계 도구 호환성을 별도로 체크해야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;느린 tsc 때문에 빌드를 기다리고, VS Code가 멈추고, CI 비용이 새는 시대가 드디어 끝나가는 듯하다. 정말 오래 걸렸다. 5.x 시리즈 내내 &quot;왜 이렇게 느린가?&quot;라는 소리를 들었던 것이 결국 이 답이었던 셈이다.&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;각설하고, 지금 당장은 베타를 설치해서 자기 프로젝트에서 직접 벤치를 돌려보는 것이 가장 의미 있는 액션이다. 1년치 CI 시간을 미리 계산해 보면 정식 출시 때 빨리 갈아탈 동기가 충분히 생긴다. 회사에 도입을 제안할 데이터로도 사용할 수 있다.&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;다만 이 글은 4월 베타 시점 정리이므로 RC/GA 시점에 일부 내용이 바뀔 수 있다. 정식 EOL 일정이나 trans-former API 같은 미확정 부분은 발표가 나오면 업데이트할 예정이다.&lt;/p&gt;</description>
      <category>Frontend</category>
      <category>tsgo</category>
      <category>TypeScript 7 베타</category>
      <category>TypeScript Go 포팅</category>
      <category>TypeScript Native Port</category>
      <category>타입스크립트 7.0 변경점</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/381</guid>
      <comments>https://yscho03.tistory.com/381#entry381comment</comments>
      <pubDate>Mon, 27 Apr 2026 22:56:43 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code &amp;quot;Auto Mode&amp;quot;란 무엇인가 &amp;mdash; --dangerously-skip-permissions 없이 자동화를 실행하는 방법</title>
      <link>https://yscho03.tistory.com/380</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;yolo 모드를 켜고 망해본 사람을 위한 글&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yolo 모드를 켜고 작업하다 rm -rf 한 줄이 잘못 들어가 프로젝트가 통째로 날아간 적이 있는가? 혹은 git push --force가 자동으로 통과되어 동료의 PR을 모두 덮어쓴 경험은? 한 번이라도 이런 사고를 친 적이 있다면 이 글이 도움이 될 것이다.&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;최근에 &lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-auto-mode&quot;&gt;Anthropic이 Claude Code Auto Mode&lt;/a&gt;라는 권한 모델을 공개했다. 한 줄로 요약하면, --dangerously-skip-permissions 없이도 자동화를 돌릴 수 있게 됐다는 의미다. 그동안은 dsp 플래그(yolo 모드)와 매번 권한을 묻는 기본 모드, 두 극단만 존재했는데 그 사이를 채우는 중간 옵션이 드디어 등장한 것이다.&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;이 글을 끝까지 읽으면 Auto Mode가 정확히 무엇인지, dsp와 어떻게 다른지, 언제 어떻게 켜야 안전한지 모두 파악할 수 있다. 내가 한 달가량 켜놓고 굴려본 결과와 사고 사례, 비교표까지 담았으니 워크플로 전환 여부를 판단하는 데 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그동안 사용되던 yolo 모드의 진짜 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;--dangerously-skip-permissions는 어떻게 동작했는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Claude Code는 도구를 사용할 때마다 권한 프롬프트를 띄웠다. Bash 명령 하나를 실행하려 해도 &quot;이 명령을 실행해도 되는가?&quot;라고 물었으며, 파일을 편집할 때도, WebFetch를 할 때도 모두 물었다. 자율 실행을 원하면 매번 엔터를 누르거나 옵션을 골라야 했고, 30분짜리 작업을 시켜놓고 옆에서 권한 프롬프트를 처리하는 일은 정말 답답했다. 나는 한 시간에 평균 40번 정도 엔터를 친 것 같다.&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;&lt;a href=&quot;https://github.com/anthropics/claude-code&quot;&gt;GitHub anthropics/claude-code 저장소&lt;/a&gt; 이슈 트래커와 &lt;a href=&quot;https://www.reddit.com/r/ClaudeAI/&quot;&gt;Reddit r/ClaudeAI&lt;/a&gt;에서 자주 언급되던 우회 방법이 바로 --dangerously-skip-permissions 플래그(통칭 dsp, yolo 모드)였다. 이 플래그를 켜면 모든 권한 프롬프트가 그대로 통과된다. Bash, Edit, Write, WebFetch 전부 자동 승인된다. 자율 실행에는 편리했지만 이름 그대로 &quot;위험하게 모두 스킵&quot;하는 옵션이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제로 사고가 발생한 사례들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;yolo 모드를 켜놓고 망한 사례는 정말 많은데, 내가 직간접적으로 본 것 몇 개를 풀어본다. 가장 흔한 것이 rm -rf가 잘못 들어간 경우다. AI가 임시 파일을 정리하려 실행했는데 경로가 잘못 잡혀 프로젝트 루트가 통째로 날아간 사례다. 백업이 없었다면 끝장이다.&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;git push --force 자동 실행 또한 만만치 않게 자주 터진다. 컨텍스트 파악이 어긋난 AI가 강제 푸시를 수행해 팀원의 작업을 모두 덮어쓰면, 리모트 히스토리를 복구하려 reflog를 뒤지면서 한나절을 날리게 된다. 거기에 환경 변수 파일을 실수로 GitHub에 push하거나 curl로 외부 서버에 토큰을 통째로 전송하는 케이스도 있다. 한 번 노출되면 키 로테이션과 감사 로그 점검이 패키지로 뒤따른다.&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;마지막으로 typosquatting을 노린 악성 npm 패키지를 AI가 의심 없이 설치하는 패턴도 있다. 공급망 공격을 그대로 당하는 셈이라 발견 시점도 늦다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Anthropic이 굳이 &quot;dangerously&quot;를 박은 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플래그 이름에 &quot;dangerously&quot;를 박은 것은 진짜 위험하다는 점을 인지시키려는 의도였다. 정상적인 사용자라면 위험 표시된 옵션을 켜지 않을 텐데, Claude Code의 자율 실행 워크플로를 구성하려면 어쩔 수 없이 켜야 했다는 점이 진짜 문제였다.&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;권장 사용법은 Docker 컨테이너 안에서만 사용하는 것이었다. 호스트 시스템과 격리된 환경에서 실행하면 사고가 나도 컨테이너만 폐기하면 되기 때문이다. 다만 매번 컨테이너를 띄우는 것이 번거로워 다수가 그냥 호스트에서 켜놓고 사용했고, 그래서 사고가 발생했다. 인간은 결국 편한 길을 택하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Auto Mode는 진짜로 무엇이 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 승인과 수동 승인을 가르는 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode의 핵심은 &quot;안전한 작업은 자동, 위험한 작업은 수동&quot;이라는 휴리스틱이다. 모든 도구를 무차별 통과시키는 것이 아니라 명령마다 위험도를 평가해 분기한다.&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;읽기 전용 명령(ls, cat, git status)이나 테스트 실행, 일반적인 파일 편집, 문서 조회 등은 알아서 처리된다. 반대로 파일 삭제, 강제 푸시, 외부 네트워크 호출, 패키지 설치, 권한 변경, sudo 등은 여전히 사람의 확인을 받는다. 이 분기를 AI가 자체적으로 판단하기 때문에 단순 패턴 매칭 기반의 allowlist보다 더 영리하다. &lt;b&gt;&quot;어떤 상황에서 이 명령이 나왔는지&quot; 컨텍스트까지 보고 결정하는 점&lt;/b&gt;이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;위험 명령 자동 차단 동작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rm -rf /, git push --force origin main, chmod 777 ., curl &amp;hellip; | bash 같은 명령은 Auto Mode가 알아서 차단한다. AI가 실수로 생성하더라도 실행 전에 &quot;이 명령은 위험한데 정말 진행할 것인가?&quot;라고 한 번 더 묻는다. dsp 플래그였다면 그대로 통과됐을 명령들이 사람의 검토를 거치게 되는 것이다. 자율성은 거의 그대로 유지하면서 안전망 한 겹을 추가하는 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실전 작동 예시&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777201716525&quot; class=&quot;excel&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;&amp;gt; Claude, 오래된 로그 파일 정리해줘

[Auto Mode] find logs/ -name &quot;*.log&quot; -mtime +30  &amp;rarr; 자동 승인 (조회만)
[Auto Mode] ls logs/ | wc -l                      &amp;rarr; 자동 승인 (조회만)
[Auto Mode] rm logs/old-2024-*.log                &amp;rarr; 사람 확인 필요 (삭제 명령)

이 명령 실행할까요? [y/N/details]
&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;읽기와 조회는 알아서 진행하고 실제로 파일을 삭제하는 단계에서만 멈춘다. 매번 묻는 기본 모드보다 훨씬 빠르며, 모두 통과시키는 yolo 모드보다 안전하다. 내가 측정해보니 30분짜리 자동화 작업에서 권한 프롬프트가 40번대에서 5~6번으로 줄었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Auto Mode 대 다른 안전 옵션 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;권한 관리 옵션이 Auto Mode 하나만 있는 것은 아니다. 상황별로 골라 쓸 수 있는 옵션을 내 사용 후기와 함께 묶어 정리한다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;방식&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;안전성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;속도&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;학습 비용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;한 줄 평&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto Mode&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`--dangerously-skip-permissions`&lt;/td&gt;
&lt;td&gt;매우 낮음&lt;/td&gt;
&lt;td&gt;가장 빠름&lt;/td&gt;
&lt;td&gt;낮음&lt;/td&gt;
&lt;td&gt;Docker 안에서만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Permissions allowlist&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;보통&lt;/td&gt;
&lt;td&gt;중간&lt;/td&gt;
&lt;td&gt;CI/CD에는 이것이 정답이다&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker 샌드박스&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Git worktree 격리&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;수동 모드 (default)&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;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;allowlist 방식&lt;/b&gt;은 .claude/settings.json에 Bash(npm test:), Edit(src/&lt;b&gt;/&lt;/b&gt;&lt;b&gt;.ts)&lt;/b&gt;&lt;b&gt;, WebFetch(domain:github.com) 같은 형태로 허용 패턴을 명시하는 방식이다. 정형화된 작업에 강하며, CI/CD 파이프라인 내부에서 자동 코드 수정을 돌릴 때 적합하다. &lt;/b&gt;Docker 샌드박스**는 호스트 시스템과 완전히 격리되므로 dsp와 함께 써도 안전하지만, 매번 컨테이너를 띄우는 비용이 의외로 거슬린다.&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;&lt;b&gt;Git worktree 격리&lt;/b&gt;는 작업 디렉터리만 분리하는 방식이라 파일시스템 보호는 안 되지만 브랜치 충돌을 막는 데 좋고, git worktree add 명령으로 시작하면 된다. 수동 모드는 매번 묻는 기본값이라 자율 실행과는 거리가 멀다. 결론적으로 일반 개발 작업에는 Auto Mode가 디폴트로 가장 적합하며, 정형화된 반복 작업에는 allowlist, 진짜 위험한 실험용에는 Docker 조합이 합리적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sjuqN/dJMcaaLRkGp/Vy4i58ucgv9NlXaD84jg9k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sjuqN/dJMcaaLRkGp/Vy4i58ucgv9NlXaD84jg9k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sjuqN/dJMcaaLRkGp/Vy4i58ucgv9NlXaD84jg9k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsjuqN%2FdJMcaaLRkGp%2FVy4i58ucgv9NlXaD84jg9k%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;655&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.pasqualepillitteri.it/en/news/141/claude-code-dangerously-skip-permissions-guide-autonomous-mode&quot;&gt;https&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Auto Mode 활성화 방법 (실전)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본 활성화 명령&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 쉬운 방법은 CLI 플래그로 켜는 것이다. 정확한 플래그명은 사용 중인 버전에서 claude --help로 확인하는 것이 안전하다. 발표 직후라 옵션명이 변경될 수 있기 때문이다. 일반적으로 /permissions 슬래시 커맨드로 세션 안에서 토글하거나, 시작할 때 플래그로 지정하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777201716528&quot; class=&quot;jboss-cli&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 시작할 때 플래그로 지정
claude --auto

# 세션 안에서 토글
/permissions auto
&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;켜면 프롬프트에 &quot;Auto Mode&quot; 인디케이터가 표시된다. 끄고 싶으면 동일한 명령으로 다시 토글하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;.claude/settings.json으로 프로젝트 단위 고정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 플래그를 박는 것이 번거롭다면 프로젝트 루트의 .claude/settings.json에 박아두면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777201716529&quot; class=&quot;json&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;permissions&quot;: {
    &quot;mode&quot;: &quot;auto&quot;,
    &quot;allow&quot;: [
      &quot;Bash(npm test:*)&quot;,
      &quot;Bash(npm run lint)&quot;,
      &quot;Edit(src/**/*.ts)&quot;
    ],
    &quot;deny&quot;: [
      &quot;Bash(rm -rf:*)&quot;,
      &quot;Bash(git push --force:*)&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;allow 리스트는 Auto Mode가 자동 승인하지 않더라도 무조건 통과시킬 패턴이고, deny는 절대 통과시키지 않을 패턴이다. &lt;b&gt;Auto Mode 휴리스틱 위에 사용자 규칙 한 겹을 더 얹는 셈&lt;/b&gt;이다. 팀 단위로 settings.json을 공유하면 모든 팀원이 동일한 권한 정책으로 작업하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서브에이전트 디스패치 시 권한 전파&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 서브에이전트(subagent)를 띄워 작업을 분할하는 기능을 제공한다. Auto Mode를 켠 메인 세션에서 서브에이전트를 띄우면 서브에이전트도 Auto Mode 권한을 그대로 물려받는다. 서브에이전트만 따로 dsp로 돌리는 것은 불가능하므로 이 점을 알고 있어야 한다.&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;서브에이전트가 위험 명령을 실행하려 하면 메인 세션에 권한 요청이 올라온다. 사용자는 메인 터미널에서 한 번에 모두 검토 가능하다. 병렬로 작업을 돌려도 권한 흐름이 한 곳으로 모이는 구조이므로 검토 부담이 줄어드는 효과가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Windows / WSL 환경 주의점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Windows에서 Claude Code를 사용한다면 몇 가지 알아둘 것이 있다. claude.exe 경로는 보통 ~/.local/bin/claude.exe에 있고, Git Bash를 사용한다면 CLAUDE_CODE_GIT_BASH_PATH 환경 변수를 설정해야 한다. WSL2 안에서 Auto Mode를 돌릴 때는 호스트 윈도우 파일시스템 접근(/mnt/c/...)도 위험 명령으로 분류되어 호스트 파일을 만질 때는 사람 확인을 거치게 된다.&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;PowerShell에서 실행할 때 한 가지 더, &amp;amp;&amp;amp; 체인이 동작하지 않는다. ; 또는 if ($?) { ... } 패턴을 사용해야 한다. 나는 처음에 이를 모르고 30분을 헤맸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래도 dsp를 써야 하는 케이스는 존재한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auto Mode가 좋다고 dsp가 완전히 사라지는 것은 아니다. 여전히 쓸 만한 시나리오가 두어 가지 있다.&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;다만 Docker 컨테이너 안에서 일회성 자동화를 돌릴 때는 dsp가 가장 빠르다. 호스트와 완전히 격리된 환경이라면 사고가 나도 컨테이너만 폐기하면 되므로 안전성 손해도 거의 없다. 컨테이너 내부에서 대규모 코드 생성이나 데이터 처리를 수행할 때는 여전히 dsp가 합리적인 선택지다. GitHub Actions 같은 ephemeral CI 잡도 마찬가지다. 잡이 끝나면 환경이 사라지므로 dsp로 돌려도 영구적인 피해가 발생하지 않는다. 따로 분리한 VM이나 dev container에서 막 돌려보는 실험용으로도 dsp는 활용할 만하다.&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;각설하고, 호스트 시스템에서는 절대 직접 켜지 마라. 본인 노트북, 회사 개발 머신에서 dsp를 켜는 것은 진짜 위험하다. Auto Mode가 등장한 지금 시점에서는 더더욱 켤 이유가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;자주 빠지는 함정과 알아두면 좋은 점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP 서버 권한도 Auto Mode 휴리스틱의 적용 대상이다. mcp__* 도구로 Slack 메시지 보내기, GitHub PR 만들기 등 외부 시스템을 다루는 작업은 거의 모두 사람 확인을 받는다고 봐야 한다. 나는 이를 모르고 처음에 &quot;왜 자꾸 멈추지?&quot;라고 생각했는데, 외부 부수효과가 있는 작업이라 멈추는 것이 맞았다.&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;Plan Mode와의 조합도 효율이 좋다. 계획을 짜는 동안은 읽기만 수행하므로 거의 모두 자동 승인되고, 실행 단계에서만 위험 명령을 검토하면 된다. Cursor의 yolo 모드, Aider의 auto-confirm 같은 옵션과 비교하면 &lt;b&gt;Claude Code Auto Mode가 더 세밀한 분기를 수행한다는 점이 차별점&lt;/b&gt;이다. 단순한 on/off가 아니라 명령 단위의 판단이라는 점이 크다.&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;처음부터 메인 프로젝트에 박지 말고 사이드 프로젝트나 임시 디렉터리에서 한 번 켜보고 어떤 명령이 자동 승인되는지 감을 잡는 것이 좋다. 나는 첫날 30분 정도 임시 폴더에서 테스트를 돌려본 뒤 메인 프로젝트로 옮겼는데, 그 30분이 시간 절약 측면에서 가장 효율이 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tIz2M/dJMcaa594Zl/WK64YZ2CH6ldkm4WBRyOUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tIz2M/dJMcaa594Zl/WK64YZ2CH6ldkm4WBRyOUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tIz2M/dJMcaa594Zl/WK64YZ2CH6ldkm4WBRyOUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtIz2M%2FdJMcaa594Zl%2FWK64YZ2CH6ldkm4WBRyOUK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://prototypr.io/post/claude-code-cli-ui&quot;&gt;prototypr.io&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이제 yolo 모드는 잊어도 된다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code Auto Mode를 한 달가량 굴려본 결론을 정리하자면, 안전한 명령은 알아서 처리하고 위험한 명령만 사람에게 묻기 때문에 &lt;b&gt;자율성과 안전성이 동시에 어느 정도 확보된다&lt;/b&gt;. 매번 권한 프롬프트를 누르던 시절은 끝났다. 그리고 --dangerously-skip-permissions는 호스트에서 절대 사용하지 마라. Docker 안과 같은 격리 환경에서만 사용해야 한다. 본인 노트북에서 켜는 것은 그냥 시한폭탄이다.&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;세밀한 제어를 원한다면 .claude/settings.json allowlist를 활용해 Auto Mode 휴리스틱 위에 사용자 규칙을 얹어 더 안전하게 다지면 된다. 팀 환경에서는 사실상 필수에 가깝다.&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;본인 환경에서 Claude Code Auto Mode를 한 번 켜보면 감이 잡힌다. &lt;a href=&quot;https://www.anthropic.com/engineering/claude-code-auto-mode&quot;&gt;Anthropic 공식 발표&lt;/a&gt;나 &lt;a href=&quot;https://docs.claude.com/en/docs/claude-code/overview&quot;&gt;Claude Code 공식 문서&lt;/a&gt;에서 정확한 동작을 확인할 수 있고, 어떤 명령이 자동 승인되고 어떤 것이 차단되는지 직접 눈으로 보는 것이 가장 빠르다. 내 기준으로는 권한 프롬프트 클릭 횟수가 80% 이상 감소했고, dsp로 인한 사고 걱정도 거의 사라졌다. yolo 모드 시절로 돌아가기는 어려울 것이다.&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>Claude Code yolo 모드</category>
      <category>Claude Code 권한 설정</category>
      <category>Claude Code 권한 우회</category>
      <category>Claude Code 자동 모드</category>
      <category>dangerously-skip-permissions</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/380</guid>
      <comments>https://yscho03.tistory.com/380#entry380comment</comments>
      <pubDate>Sun, 26 Apr 2026 20:09:19 +0900</pubDate>
    </item>
    <item>
      <title>라라벨(Laravel)이 PHP 프레임워크 중 점유율 1위인 이유를 수치로 정리한다</title>
      <link>https://yscho03.tistory.com/379</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;PHP는 매년 &quot;이제 죽었다&quot;고 욕을 먹지만 실제 웹의 &lt;b&gt;75% 이상은 여전히 PHP로 굴러간다&lt;/b&gt;(W3Techs 2025 기준). 그 PHP 생태계 안에서 라라벨 프레임워크가 차지한 비중은 &lt;b&gt;GitHub 스타 79K, Packagist 의존 패키지 13만+&lt;/b&gt; 수준이다. Symfony, CodeIgniter 같은 경쟁자가 같이 존재함에도 라라벨 프레임워크 쪽으로 무게가 쏠린 이유가 본 글의 질문이다. 아래에서 GitHub 스타, Packagist 패키지 수, 채용 공고, 만족도 설문 수치를 직접 붙여 비교한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcwqVB/dJMcahqGqxZ/VNfmMUQ1HQWTSZkdHdN3Gk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcwqVB/dJMcahqGqxZ/VNfmMUQ1HQWTSZkdHdN3Gk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcwqVB/dJMcahqGqxZ/VNfmMUQ1HQWTSZkdHdN3Gk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcwqVB%2FdJMcahqGqxZ%2FVNfmMUQ1HQWTSZkdHdN3Gk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;960&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://ca.pinterest.com/pin/the-new-laravel-by-alex-sailer--39969515433025429/&quot;&gt;ca.pinterest.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;일단 숫자로 깔고 시작 &amp;mdash; Laravel과 다른 PHP 프레임워크 점유율 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말로만 &quot;Laravel이 1위다&quot;라고 하면 신뢰가 가지 않으므로 수치부터 살핀다. GitHub 스타, Packagist 의존 패키지 수, 개발자 설문이 모두 한쪽으로 기울어져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GitHub 스타와 패키지 생태계 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 기준 PHP 프레임워크별 GitHub 스타와 Packagist 패키지 수치 비교는 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;프레임워크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GitHub 스타&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Packagist 의존 패키지&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;최신 메이저 버전&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Laravel&lt;/td&gt;
&lt;td&gt;약 79,000+&lt;/td&gt;
&lt;td&gt;130,000+&lt;/td&gt;
&lt;td&gt;Laravel 11 (2024.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Symfony&lt;/td&gt;
&lt;td&gt;약 30,000&lt;/td&gt;
&lt;td&gt;40,000+&lt;/td&gt;
&lt;td&gt;Symfony 7.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CodeIgniter&lt;/td&gt;
&lt;td&gt;약 18,000&lt;/td&gt;
&lt;td&gt;5,000+&lt;/td&gt;
&lt;td&gt;CodeIgniter 4.x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yii&lt;/td&gt;
&lt;td&gt;약 14,000&lt;/td&gt;
&lt;td&gt;8,000+&lt;/td&gt;
&lt;td&gt;Yii 2.x (3 베타)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CakePHP&lt;/td&gt;
&lt;td&gt;약 9,000&lt;/td&gt;
&lt;td&gt;6,000+&lt;/td&gt;
&lt;td&gt;CakePHP 5.x&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;b&gt;격차가 확인된다.&lt;/b&gt; Laravel은 2위 Symfony 대비 GitHub 스타 &lt;b&gt;2.5배 이상&lt;/b&gt;, Packagist 의존 패키지 수는 &lt;b&gt;3배 이상&lt;/b&gt;이다. Laravel 위에서 굴러가는 패키지 수가 다른 PHP 프레임워크 합산보다 많다는 사실이 수치로 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개발자 만족도 설문 결과&lt;/h3&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;a href=&quot;https://survey.stackoverflow.co/2024&quot;&gt;Stack Overflow 2024 Developer Survey&lt;/a&gt;에서 Laravel은 PHP 프레임워크 중 &lt;b&gt;admired 1위 자리를 유지한다&lt;/b&gt;. JetBrains의 Developer Ecosystem 2024 리포트에서도 PHP 개발자 절반 이상이 Laravel을 메인으로 쓴다고 답했다. 한국 채용 시장도 비슷한 흐름이다. 원티드/잡코리아에서 &quot;PHP 백엔드&quot;로 검색하면 Laravel을 명시적으로 요구하는 공고가 &lt;b&gt;60~70% 수준&lt;/b&gt;으로 나오는데, 이는 2025년 4월 직접 검색 기준이므로 정확한 점유율이 아니라 체감 추정치 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;엘로퀀트(Eloquent) ORM이 편한 이유&lt;/h2&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;b&gt;Eloquent ORM&lt;/b&gt;이다. ActiveRecord 패턴 기반이므로 다른 PHP 프레임워크 ORM보다 코드량이 적게 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄로 끝나는 관계 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Symfony가 사용하는 Doctrine ORM과 비교하면 차이가 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777165827326&quot; class=&quot;scala&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;php&quot;&gt;&lt;code&gt;// Laravel Eloquent
class User extends Model {
    public function posts() {
        return $this-&amp;gt;hasMany(Post::class);
    }
}

// 사용
$user-&amp;gt;posts; // 끝. 한 줄.
&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;Doctrine은 어노테이션이나 YAML로 매핑을 정의하고 EntityManager로 가져와야 한다. 같은 일을 하는데 코드 양이 &lt;b&gt;2~3배 차이가 난다&lt;/b&gt;. 신규 프로젝트에서 빠르게 모델을 만들고 관계를 정의해야 할 때 Eloquent 쪽이 손이 덜 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;마이그레이션과 시더가 기본 내장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;php artisan make:migration 한 줄이면 마이그레이션 파일을 만들어주고, php artisan migrate 한 번에 DB 스키마가 적용된다. 시더(Seeder)로 더미 데이터를 넣는 작업도 표준화되어 있어 팀 협업 시 DB 상태를 맞추기 편하다. CodeIgniter나 Yii도 마이그레이션 도구가 존재하지만 명령어 통일성과 IDE 자동 완성 지원이 Eloquent보다 약하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점도 솔직하게 &amp;mdash; N+1 쿼리 함정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eloquent도 만능은 아니다. ActiveRecord 패턴 특성상 &lt;b&gt;N+1 쿼리 문제가 자주 터진다&lt;/b&gt;. $users를 가져와 각각의 $user-&amp;gt;posts에 접근하면 뒤에서 쿼리가 N+1번 날아간다. with() 메서드로 eager loading을 걸지 않으면 트래픽이 조금만 늘어도 DB가 폭파된다. 다만 초당 수만 RPS 환경에서는 Doctrine처럼 명시적인 쿼리 빌더가 더 나을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PHP 프레임워크 비교 &amp;mdash; Laravel vs Symfony vs CodeIgniter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PHP 프레임워크를 비교할 때 사실상 이 셋이 메인 후보다. 항목별로 정리하면 아래 표와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;630&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yenRw/dJMcaiJPJP2/lD53E367FsZhCa485XkRaK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yenRw/dJMcaiJPJP2/lD53E367FsZhCa485XkRaK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yenRw/dJMcaiJPJP2/lD53E367FsZhCa485XkRaK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyenRw%2FdJMcaiJPJP2%2FlD53E367FsZhCa485XkRaK%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;630&quot; height=&quot;630&quot; data-origin-width=&quot;630&quot; data-origin-height=&quot;630&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.teepublic.com/magnet/31437719-laravel-logo?countrycode=US#2834P31437719D21V&quot;&gt;https://www.teepublic.com/magnet/31437719-laravel-logo?countrycode=US#2834P31437719D21V&lt;/a&gt;&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Laravel&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Symfony&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CodeIgniter&lt;/b&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;가파름 (DI 컨테이너 이해 필수)&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;중상 (튜닝 시 최강급)&lt;/td&gt;
&lt;td&gt;상 (가벼움)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;생태계&lt;/td&gt;
&lt;td&gt;Packagist 13만+ 패키지&lt;/td&gt;
&lt;td&gt;Packagist 4만+ 패키지&lt;/td&gt;
&lt;td&gt;Packagist 5천+ 패키지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;채용 시장&lt;/td&gt;
&lt;td&gt;한국 PHP 공고 60~70%에서 명시&lt;/td&gt;
&lt;td&gt;대형 SI 위주&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;대형 엔터프라이즈, 모듈식&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Laravel vs Symfony &amp;mdash; 풀스택 vs 모듈식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Symfony는 라이브러리 모음에 가깝다. 컴포넌트를 골라 조립하는 느낌이라 자유도는 높지만 그만큼 보일러플레이트가 많고 처음 잡으면 머리가 아프다. &lt;b&gt;Laravel은 풀스택으로 다 쥐어준다.&lt;/b&gt; 라우팅, 큐, 인증, 메일 같은 핵심이 표준화되어 있어 의사결정 피로도가 낮다. 흥미로운 점은 Laravel 내부도 Symfony 컴포넌트 위에서 굴러간다는 사실이다. HttpFoundation, Console, Routing 같은 핵심은 Symfony 것을 가져다 쓴다. 그래서 &quot;Laravel은 Symfony 위에 얹은 DX 레이어&quot;라고 보면 대체로 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Laravel vs CodeIgniter &amp;mdash; 모던 vs 가벼움&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CodeIgniter는 가볍다. 압축을 풀고 바로 굴리면 된다. 학습 곡선도 거의 없다. 다만 모던 PHP 기능(의존성 주입, 미들웨어, 큐 시스템) 지원이 약하고 패키지 생태계가 빈약하다. 쓸 만한 곳은 빠른 프로토타입이나 기존 CodeIgniter 레거시 유지보수 정도다. &lt;b&gt;신규 프로젝트라면 CodeIgniter를 고를 이유가 없다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Yii / CakePHP / Phalcon은 왜 시들해졌는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Yii는 한때 잘 나갔으나 Yii 3 발표가 늦어지면서 개발자들이 떠났다. CakePHP는 ActiveRecord 원조급이지만 Laravel에게 DX로 밀린다. Phalcon은 C 익스텐션으로 짠 점이 매력 포인트이지만 설치/배포가 까다롭고 패키지 생태계가 빈약해 mainstream에서 빠졌다. 결국 PHP 프레임워크 비교는 &lt;b&gt;Laravel vs Symfony 양강 구도&lt;/b&gt;로 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Laravel 생태계 &amp;mdash; 1군 패키지 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라라벨 프레임워크의 강점은 결국 생태계다. 공식+커뮤니티 패키지 퀄리티가 다른 PHP 프레임워크보다 한 단계 위에 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Livewire와 Inertia.js&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React/Vue 없이도 풀스택 PHP로 SPA 같은 UX를 뽑는 도구가 &lt;b&gt;Livewire&lt;/b&gt;다. 백엔드 개발자가 프론트엔드를 깊이 파지 않고도 인터랙티브 UI를 만들 수 있다. Inertia.js는 반대로 Vue/React를 PHP 라우팅에 붙여 별도 API 없이도 SPA를 굴리게 해준다. 둘 다 Laravel 생태계 표준 도구라 채용 공고에서도 자주 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Filament &amp;mdash; 어드민 패널 빌더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Filament는 &lt;b&gt;Livewire 기반 어드민 패널 빌더&lt;/b&gt;다. 모델을 정의하고 리소스 클래스 한두 개를 만들면 CRUD + 검색 + 필터 + 권한까지 자동 생성된다. 모델 5개 정도 기준으로 Filament 공식 스타터를 따라가면 어드민 한 화면을 띄우는 데 30분~1시간 정도가 걸린다. Django Admin이나 Rails ActiveAdmin과 비교했을 때 커스터마이징 자유도 면에서 손에 잘 잡히는 편이라, 사이드 프로젝트나 사내툴을 만들 때 Laravel + Filament 조합은 시간 절약이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Forge, Vapor, Nova &amp;mdash; 공식 유료 도구&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Forge는 서버 프로비저닝/배포 자동화 SaaS다. AWS/DO/Linode에 Laravel을 배포할 때 클릭 몇 번이면 nginx, PHP-FPM, MySQL, Redis까지 셋업된다. Vapor는 AWS Lambda 위에 서버리스로 Laravel을 굴리는 도구다. Nova는 공식 어드민 패널인데, 요즘은 Filament 쪽으로 무게가 옮겨간 분위기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;아티즌(Artisan) CLI와 Tinker&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;php artisan 명령어 하나로 라우트 확인, 마이그레이션, 큐 워커 실행, 컨트롤러 생성이 모두 가능하다. Tinker는 REPL인데 콘솔에서 Eloquent 모델을 바로 조작할 수 있어 디버깅할 때 편하다. 일상 DX 면에서 &lt;b&gt;다른 PHP 프레임워크보다 한참 앞서 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Laravel 11/12 최신 동향 &amp;mdash; 2024~2025 변경점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel은 매년 메이저 버전이 업데이트된다. 2024년 3월에 나온 Laravel 11이 &lt;b&gt;구조적으로 큰 변화&lt;/b&gt;를 가져왔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Slim Skeleton &amp;mdash; 디렉토리 구조 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 11에서 가장 눈에 띄는 변화가 &lt;b&gt;슬림 스켈레톤(slim skeleton)&lt;/b&gt;이다. 기존에 있던 app/Http/Kernel.php, app/Console/Kernel.php, app/Exceptions/Handler.php 같은 파일들이 사라지고 bootstrap/app.php 하나로 통합된다. 미들웨어 등록, 예외 처리, 라우팅을 모두 그 안에서 설정한다. 신규 프로젝트는 디렉토리 구조가 간결해졌고, 기존 프로젝트는 그대로 굴러가므로 마이그레이션 압박은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;새로 추가된 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 11에서 눈여겨볼 만한 추가 사항을 추려보면, 초당 단위로 요청 제한을 거는 per-second rate limiting이 들어왔고 /up 엔드포인트가 기본 제공되어 헬스체크 라우팅을 따로 짤 필요가 없어졌다. 결과 메모이제이션을 한 줄로 처리하는 Once 메서드, 디버깅용 dd/dump 메서드를 trait로 분리한 Dumpable trait도 추가됐다. 자세한 항목은 &lt;a href=&quot;https://laravel.com/docs/11.x/releases&quot;&gt;Laravel 11 릴리즈 노트&lt;/a&gt;에서 확인 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Laravel 12 로드맵&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel 12는 2025년 1분기에 발표된 버전인데, 큰 구조 변경보다는 안정성/성능 개선 위주다(&lt;a href=&quot;https://laravel.com/docs/12.x/releases&quot;&gt;공식 릴리즈 노트&lt;/a&gt; 참고). PHP 8.3+를 요구하는 방향이라 레거시 PHP 7.x 환경에서는 마이그레이션 부담이 있을 수 있다. 새로 시작하는 프로젝트라면 Laravel 11 또는 12를 그대로 가져가는 것이 맞다.&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;&lt;a href=&quot;https://laravel.com/docs&quot;&gt;Laravel 공식 docs&lt;/a&gt;에서 최신 변경점을 확인할 수 있으므로 버전을 올릴 때 release note를 확인하는 것을 추천한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 라라벨은 항상 정답인가 &amp;mdash; 단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 보면 &quot;Laravel 좋다&quot;는 결론이지만, 다만 단점도 분명히 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;학습 곡선이 생각보다 가파른 부분&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel은 시작하기는 쉽지만, 깊이 들어가면 어려워진다. 특히 &lt;b&gt;서비스 컨테이너(Service Container)와 파사드(Facade)&lt;/b&gt; 개념이 그렇다. 처음에는 Auth::user() 같은 호출을 그냥 쓰면 되지만, 내부에서 어떻게 굴러가는지 이해하려면 IoC 컨테이너, 서비스 프로바이더, 파사드의 정적 호출 마법까지 알아야 한다. 입문 튜토리얼을 따라 할 때는 보이지 않다가 실무에서 디버깅할 때 막히는 부분이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;마법이 너무 많아 디버깅이 어렵다&quot;는 비판&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Laravel을 두고 자주 나오는 비판 중 하나가 &lt;b&gt;&quot;magic이 너무 많다&quot;&lt;/b&gt;는 점이다. Eloquent의 __get/__call 매직 메서드, 파사드의 정적 프록시, 서비스 컨테이너의 자동 의존성 해결 같은 부분에서 코드를 읽다가 &quot;이게 어디서 호출되는 것인가?&quot; 싶은 순간이 자주 온다. 왜일까? IDE 자동 완성도 잡히지 않는 경우가 있어 IDE Helper 플러그인을 깔아야 할 때도 있기 때문이다. Symfony처럼 명시적인 코드를 선호하는 사람에게는 맞지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대규모 트래픽 대응&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라라벨 프레임워크는 풀스택 모놀리스 프로젝트에 잘 맞지만, 초당 수만 RPS를 다루는 환경에서 그대로 쓰기는 빡세다. 옥테인(Octane)으로 Swoole/RoadRunner 위에 올리면 성능을 끌어올릴 수 있긴 하지만, 그 정도 트래픽이면 마이크로서비스로 쪼개거나 다른 스택 조합이 더 나을 수도 있다. 한국 스타트업 중에서도 PHP 모놀리스로 시작했다가 트래픽이 늘면서 Java/Kotlin/Node 같은 스택으로 갈아탄 사례가 종종 회자되는데, 정확한 시점&amp;middot;전환 사유는 각 회사 기술 블로그에서 따로 확인해야 한다(2차 인용이므로 단정은 보류한다).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;신규 프로젝트라면 라라벨 프레임워크가 합리적인 디폴트다&lt;/h2&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;b&gt;GitHub 스타 79K+, Packagist 패키지 13만+&lt;/b&gt;로 다른 PHP 프레임워크보다 큰 생태계가 가장 큰 차이이고, Eloquent ORM과 Artisan CLI, Blade, Livewire, Filament 같은 풀스택 도구가 한 묶음으로 제공되는 점도 결정적이다. 한국 PHP 채용 공고 또한 직접 검색 기준 60~70% 수준이 Laravel 명시였다. 단점은 매직이 많고 학습 곡선이 깊은 편이며, 초당 수만 RPS 트래픽에는 별도 튜닝이 필요하다는 점이다. 다만 이 단점들을 감안해도 신규 프로젝트에서 Symfony나 CodeIgniter를 우선 선택할 만한 근거는 약하다.&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;&lt;a href=&quot;https://w3techs.com/technologies/details/pl-php&quot;&gt;W3Techs PHP 사용률&lt;/a&gt; 기준으로 PHP 점유율은 75% 이상 유지 중이고, 라라벨 프레임워크는 그 안에서 1군 자리에 있다. 입문 자료는 &lt;a href=&quot;https://laravel.com/docs&quot;&gt;Laravel 공식 docs&lt;/a&gt;와 Laracasts 비디오 시리즈가 가장 안정적이며, 한글 자료가 부족하면 영문 docs를 직접 읽는 것이 결국 빠른 길이다.&lt;/p&gt;</description>
      <category>Backend</category>
      <category>Laravel vs CodeIgniter</category>
      <category>Laravel vs Symfony</category>
      <category>Laravel 장점</category>
      <category>PHP 백엔드 개발</category>
      <category>PHP 프레임워크 비교</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/379</guid>
      <comments>https://yscho03.tistory.com/379#entry379comment</comments>
      <pubDate>Sun, 26 Apr 2026 10:10:54 +0900</pubDate>
    </item>
    <item>
      <title>Claude Code 주간 한도 리셋 날짜는 왜 계속 달라지는가</title>
      <link>https://yscho03.tistory.com/378</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code 사용자 사이에서 최근 가장 자주 제기되는 질문이 바로 이것이다. &quot;분명 지난주는 월요일에 풀렸는데 이번 주는 왜 화요일에 풀리는가?&quot; 결론부터 말하자면 &lt;b&gt;Claude Code 주간 한도 리셋은 고정 요일 기준이 아니라 롤링 7일 윈도우 방식&lt;/b&gt;이기 때문이다. 캘린더상의 월요일 0시 같은 절대 기준이 아니라, 사용자가 해당 주에 첫 메시지를 보낸 시점부터 정확히 168시간 뒤에 풀리는 구조다. 그래서 매주 리셋일이 미끄러지듯 바뀐다. 이번 글에서는 왜 이렇게 동작하는지, 플랜별 한도가 어떻게 다른지, 자신의 리셋 시간을 어떻게 확인하는지까지 정리한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rtndK/dJMcacXbbl8/AhYbIukqw4qxCIXVoF2Qb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rtndK/dJMcacXbbl8/AhYbIukqw4qxCIXVoF2Qb0/img.png&quot; data-alt=&quot;출처: Anthropic&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rtndK/dJMcacXbbl8/AhYbIukqw4qxCIXVoF2Qb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrtndK%2FdJMcacXbbl8%2FAhYbIukqw4qxCIXVoF2Qb0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;498&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: Anthropic&lt;/figcaption&gt;
&lt;/figure&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 결론부터 말하면, 고정 요일 리셋이 아니다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분이 한국 사용자가 가장 많이 헷갈리는 지점이다. ChatGPT Plus를 비롯한 다른 SaaS 구독 서비스를 보면 통상 &quot;결제일 기준 매월 X일 리셋&quot; 같은 캘린더 기반이 일반적이다. 그러나 Claude Code 주간 한도 리셋은 그렇게 동작하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캘린더 주(월요일 시작) 기준이 아니다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Anthropic은 &quot;매주 월요일 0시(KST)에 풀린다&quot; 같은 고정 시점을 두지 않는다. 만약 그렇게 설계했다면 모든 사용자가 월요일 자정에 몰려 서버가 다운되었을 것이다. 또한 일요일 밤에 한도를 모두 소진한 사용자만 이득을 보는 구조가 된다. 따라서 사용자별로 별도의 카운터를 운용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 메시지 시점부터 7일이 카운트되는 롤링 윈도우다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 핵심이다. 이번 주 윈도우 안에서 첫 메시지를 보내는 순간, 그 시점부터 168시간(7일) 타이머가 시작된다. 그 7일 동안 발생한 모든 사용량이 누적되며, 168시간이 지나면 카운터가 0으로 초기화된다.&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;예를 들면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지난주 월요일 오후 3시에 첫 메시지 &amp;rarr; 이번 주 월요일 오후 3시에 리셋&lt;/li&gt;
&lt;li&gt;이번 주는 화요일 오전 9시에 첫 메시지 &amp;rarr; 다음 주 화요일 오전 9시에 리셋&lt;/li&gt;
&lt;li&gt;즉, &lt;b&gt;사용자마다 그리고 매주마다 리셋 요일과 시간이 달라질 수 있다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조를 모르면 &quot;어제 분명 월요일에 풀린다고 했는데 왜 안 풀리는가&quot; 하며 혼란에 빠진다. 다만 이는 버그가 아니라 의도된 설계다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 롤링 윈도우(Rolling Window)란 무엇인가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7일짜리 슬라이딩 타이머로 이해하면 편하다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;7일짜리 모래시계가 하나 있다고 가정하자. 첫 메시지를 보내는 순간 모래시계가 뒤집힌다. 모래가 모두 떨어지는 168시간 뒤가 곧 리셋 시점이다. 캘린더를 보며 &quot;월요일이 며칠인가&quot; 따질 필요가 없다. 기준은 오직 사용자의 첫 메시지 시점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실제 예시 - 월요일 첫 사용 vs 수요일 첫 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;시나리오&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;첫 메시지 시점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;한도 도달 후 다음 사용 가능 시점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 유저&lt;/td&gt;
&lt;td&gt;월요일 오후 3시&lt;/td&gt;
&lt;td&gt;다음 주 월요일 오후 3시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B 유저&lt;/td&gt;
&lt;td&gt;수요일 오전 10시&lt;/td&gt;
&lt;td&gt;다음 주 수요일 오전 10시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C 유저&lt;/td&gt;
&lt;td&gt;토요일 밤 11시&lt;/td&gt;
&lt;td&gt;다음 주 토요일 밤 11시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Claude Code Pro 플랜을 사용하더라도 사용 시작 시점이 다르면 리셋 시점도 따로 흘러간다. 옆자리 동료에게 &quot;한도가 풀렸는가&quot; 물었을 때 답이 다른 것이 정상이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 Anthropic이 이 방식을 선택했는가&lt;/h3&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;a href=&quot;https://www.anthropic.com/news&quot;&gt;2025년 7월 28일에 발표&lt;/a&gt;되어 8월 28일부터 실제 적용되었는데, 발표문을 보면 이유가 명확하다.&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;상위 1% 사용자가 다른 사용자 경험을 침해한다&lt;/b&gt; - Claude Code를 24/7 백그라운드로 돌리며 토큰을 과도하게 소비하는 사용자가 존재했다. 이들 때문에 일반적으로 사용하는 사용자들의 응답이 느려지거나 한도가 빠르게 소진되는 문제가 발생했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;고정 캘린더 리셋은 트래픽 스파이크 폭탄이 된다&lt;/b&gt; - 모두가 월요일 0시에 한도가 풀리면 그 순간 서버에 인원이 몰린다. 롤링 방식이면 사용자별로 분산된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;공정성&lt;/b&gt; - 화요일에 가입했는데 월요일이 캘린더 리셋이라면 첫 주는 며칠밖에 사용하지 못한다. 롤링 방식이 더 공평하다.&lt;/li&gt;
&lt;/ol&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;b&gt;인프라 안정성 + 어뷰저 차단 + 공정성&lt;/b&gt;, 세 마리 토끼를 잡는 설계다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Claude Code 한도는 사실 두 종류다 (5시간 + 주간)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 또 한 번 헷갈림 포인트가 등장한다. Claude Code 주간 한도 리셋만 신경 써서는 안 된다. 한도가 두 개 동시에 작동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5시간 세션 한도 - 메시지 보낸 순간부터 5시간 카운트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 본래부터 존재하던 한도다. 메시지를 처음 보내는 순간부터 5시간 타이머가 작동하며, 그 5시간 안에 사용 가능한 메시지 양에 상한이 있다. 5시간이 지나면 자동으로 리셋된다. 짧은 단위라 체감상 자주 풀린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주간 한도 - 첫 메시지부터 7일 카운트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 8월에 추가된 신규 한도다. 5시간 한도와 별개로, 7일 누적 사용량의 상한이 별도로 존재한다. 5시간 한도는 풀렸지만 주간 한도는 풀리지 않은 상황도 충분히 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;둘이 따로 노는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5시간 한도는 &quot;한 번에 너무 많이 쓰지 말라&quot;는 의도이고, 주간 한도는 &quot;일주일 내내 너무 많이 쓰지 말라&quot;는 의도다. 목적이 다르다. 그래서 두 카운터가 독립적으로 작동한다.&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;실제로 Reddit r/ClaudeAI에서 &quot;5시간은 풀렸는데 왜 메시지를 보낼 수 없는가&quot; 같은 질문이 자주 올라오는데, 거의 대부분 주간 한도에 걸린 사례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 플랜별 주간 한도 정리 (Pro / Max 5x / Max 20x)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2026년 4월 기준 Anthropic 공식 가이드를 따른다. 정확한 수치는 모델 부하, 프롬프트 길이, MCP 사용량에 따라 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;각 플랜이 받는 Sonnet 4 / Opus 4 시간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;플랜&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;월 가격&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;5시간 세션당&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주간 Sonnet 4&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주간 Opus 4&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;td&gt;$20&lt;/td&gt;
&lt;td&gt;약 10~40 메시지&lt;/td&gt;
&lt;td&gt;약 40~80시간&lt;/td&gt;
&lt;td&gt;사용 불가/제한적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max 5x&lt;/td&gt;
&lt;td&gt;$100&lt;/td&gt;
&lt;td&gt;약 50~200 메시지&lt;/td&gt;
&lt;td&gt;약 140~280시간&lt;/td&gt;
&lt;td&gt;약 15~35시간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max 20x&lt;/td&gt;
&lt;td&gt;$200&lt;/td&gt;
&lt;td&gt;약 200~800 메시지&lt;/td&gt;
&lt;td&gt;약 240~480시간&lt;/td&gt;
&lt;td&gt;약 24~40시간&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 수가 아니라 &quot;시간&quot;으로 표시되는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5시간 한도는 메시지 수로 카운트되지만 주간 한도는 &quot;시간(hours)&quot; 단위로 표시된다. 왜일까? Claude Code는 한 번 메시지를 보낼 때마다 백그라운드로 파일 읽기, 도구 호출, MCP 통신이 모두 발생하여 토큰 소비량이 천차만별이기 때문이다. 메시지 수로 세면 부정확하므로 &quot;Sonnet 4 모델로 X시간만큼 작업했다&quot;는 식으로 환산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;어떤 플랜이 본전을 뽑기 좋은가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pro($20)&lt;/b&gt;: 가볍게 사용하는 사용자용이다. 매일 1~2시간 정도 코딩을 보조하는 수준이면 충분하다. 다만 Opus는 거의 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Max 5x($100)&lt;/b&gt;: 매일 사용하는 개발자에게 가성비가 좋다. Opus도 어느 정도 사용 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Max 20x($200)&lt;/b&gt;: 풀타임으로 Claude Code를 운용하는 사용자용이다. 이 가격대라면 차라리 API 종량제와 비교해보는 것도 권장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 자신의 리셋 시간을 확인하는 법&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CLI에서 /status 명령어로 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code를 실행한 뒤 /status 또는 /usage를 입력하면 된다. 현재 5시간 한도와 주간 한도 진행률, 다음 리셋 예상 시점이 모두 표시된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777164602093&quot; class=&quot;fortran&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;claude /status
&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ccusage 같은 서드파티 모니터 설치&lt;/h3&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;a href=&quot;https://github.com/ryoppippi/ccusage&quot;&gt;ccusage&lt;/a&gt;라는 오픈소스 도구가 있다. 설치해두면 터미널에서 실시간으로 토큰 소비량, 잔여 한도, 예상 리셋 시간을 모두 추적해준다. claude-monitor나 Claude-Code-Usage-Monitor 같은 유사 도구도 여럿 있으나 ccusage가 가장 인기가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777164602094&quot; class=&quot;cmake&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;npm install -g ccusage
ccusage
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한도 임박 알림 활용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code는 주간 한도 90% 즈음에 도달하면 CLI에 경고 메시지를 띄운다. 이를 무시하지 말고 확인 즉시 작업을 마무리하는 편이 정신 건강에 이롭다. 한도가 0%에 도달하는 순간 작업이 중단되는데, 이는 상당한 스트레스를 유발한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 자주 헷갈리는 케이스 정리 (FAQ)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;월요일에 리셋된다더니 왜 화요일에 풀렸는가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지난주 첫 메시지 시점이 화요일이었기 때문이다. 누군가 &quot;월요일에 풀린다&quot;고 말한 것이라면 그 사람 기준으로만 그러할 뿐, 본인에게 적용되는 시간은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;5시간 한도는 풀렸는데 주간은 안 풀린 이유&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 설명한 바와 같다. 두 카운터가 독립적으로 작동한다. 5시간 한도가 풀렸다고 주간이 함께 풀리지는 않는다. 주간 한도는 첫 메시지 시점부터 168시간이 지나야 풀린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;한국 시간(KST) 기준으로는 언제 리셋되는가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타임존을 신경 쓸 필요가 없다. KST 오후 3시에 첫 메시지를 보냈다면 168시간 뒤인 KST 오후 3시에 리셋된다. Anthropic 서버는 PT/UTC 기준이지만 사용자 입장에서는 첫 사용 시점만 알면 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;API 요금제로 갈아타면 이 한도가 없는 것인가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API는 별개다. API는 종량제(pay-as-you-go)이므로 이러한 주간/세션 한도가 없으며, 토큰을 소비한 만큼 청구된다. 다만 API에는 RPM(분당 요청 수), TPM(분당 토큰 수) 같은 별도의 rate limit이 존재한다. 헤비 유저라면 API가 더 유리할 수 있고, 가벼운 사용자라면 Pro/Max가 더 저렴하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;한도 도달 후 다른 계정으로 우회가 가능한가&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 Anthropic 정책 위반이다. 적발 시 계정이 정지될 수 있다. 권장하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 한도를 빨리 소진하지 않으려면 (실전 팁)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무거운 작업은 Sonnet으로, 잡일은 Haiku로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opus는 시간 카운트가 빠르게 소진된다. 진정 어려운 추론 작업이 아니라면 Sonnet 4로 충분하다. 간단한 코드 리뷰나 텍스트 정리는 Haiku로 분배하면 토큰을 훨씬 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MCP 서버를 너무 많이 붙이면 토큰이 빠르게 소진된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MCP(Model Context Protocol) 서버를 5~10개씩 붙여두는 사용자가 많은데, 매 메시지마다 그 서버들의 정보가 컨텍스트에 포함된다. 사용하지 않는 MCP는 비활성화하는 편이 한도 절약에 도움된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;컨텍스트 압축(/compact)을 자주 활용하라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대화가 길어지면 /compact 명령어로 컨텍스트를 압축할 수 있다. 토큰 사용량이 크게 감소한다. 사용하지 않으면 매 메시지마다 이전 대화가 모두 토큰으로 환산되어 들어가 한도가 순식간에 소진된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;백그라운드 자동화는 금지다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude Code를 cron에 걸어 24/7로 돌리는 행위는 Anthropic이 명시적으로 금지한다. 적발 시 계정 제재가 가능하다. 또한 어차피 주간 한도 때문에 길게 운용할 수도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. Claude Code 주간 한도 리셋, 결국 의도된 설계다&lt;/h2&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;b&gt;Claude Code 주간 한도 리셋이 매주 달라 보이는 것은 롤링 7일 윈도우 때문&lt;/b&gt;이다. 첫 메시지 시점부터 168시간이 카운트되므로 사용 패턴에 따라 리셋 요일이 자연스럽게 미끄러진다. 5시간 세션 한도와 주간 한도가 별도 카운터로 작동한다는 점, 플랜별로 Sonnet/Opus 사용 시간이 다르다는 점, /status 명령어나 ccusage로 자신의 한도를 확인할 수 있다는 점만 기억하면 된다.&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;Anthropic이 이 정책을 도입한 이유 또한 명확하다. 어뷰저를 차단하고, 서버를 분산하며, 신규 가입자에게도 공정성을 부여하기 위함이다. 한도 자체가 마음에 들지 않을 수는 있어도 적어도 동작 원리는 합리적이다. 한도를 자주 소진하는 사용자라면 위에서 언급한 실전 팁을 적용해보고, 그래도 부족하다면 Max 플랜이나 API와 비교하여 자신에게 맞는 선택을 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&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;b&gt;참고 출처&lt;/b&gt;&lt;/p&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://www.anthropic.com/news&quot;&gt;Anthropic 공식 발표 (2025년 7월 28일)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.claude.com/claude-code&quot;&gt;Claude Code 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://support.anthropic.com/&quot;&gt;Anthropic Help Center - Rate Limits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/ryoppippi/ccusage&quot;&gt;ccusage GitHub 저장소&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>끄적끄적</category>
      <category>Claude Code weekly limit reset</category>
      <category>Claude Code 사용량 리셋</category>
      <category>Claude Code 주간 리미트</category>
      <category>Claude Code 토큰 한도</category>
      <category>Claude Code 한도 초기화</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/378</guid>
      <comments>https://yscho03.tistory.com/378#entry378comment</comments>
      <pubDate>Sun, 26 Apr 2026 09:52:16 +0900</pubDate>
    </item>
    <item>
      <title>OpenAI GPT 5.5는 왜 갑자기 등장했는가? Anthropic은 긴장해야 하는가? 리뷰를 모아 정리한다 (필자의 리뷰가 아니다)</title>
      <link>https://yscho03.tistory.com/377</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;어제까지 GPT-5에 적응하느라 프롬프트를 다시 짜고 있었는데, OpenAI가 갑자기 GPT 5.5를 던져버렸다. 출시 피로가 올 정도다. 다만 이번에는 결이 좀 다르다. 트위터와 &lt;a href=&quot;https://news.ycombinator.com/&quot;&gt;Hacker News&lt;/a&gt;에서 &quot;이건 진짜 세다&quot;는 류의 반응이 평소보다 많이 보인다. 그래서 직접 API를 결제해 돌려보는 대신, 외국 개발자들의 리뷰를 모두 뒤져 핵심만 정리해보았다. 미리 못 박아 두자면 &lt;b&gt;이 글은 내 리뷰가 아니다&lt;/b&gt;. Hacker News, Reddit, &lt;a href=&quot;https://simonwillison.net/&quot;&gt;Simon Willison 블로그&lt;/a&gt;, &lt;a href=&quot;https://artificialanalysis.ai/&quot;&gt;Artificial Analysis&lt;/a&gt; 차트, &lt;a href=&quot;https://lmarena.ai/&quot;&gt;LMArena&lt;/a&gt; 데이터를 모아 큐레이션한 GPT 5.5 리뷰다.&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;핵심 질문은 두 가지다. 첫째, GPT 5.5가 진짜 의미 있는 점프인가, 아니면 마이너 패치에 5.5라는 번호를 붙인 것에 불과한가. 둘째, 그래서 Claude(Anthropic) 입장에서는 진짜 위기 신호로 봐야 하는가. 결론부터 말하면 둘 다 &quot;예스, 다만 조건이 있다&quot;이다. 본문에서는 벤치마크 점수, 실사용 후기, 가격, 그리고 Anthropic이 다음 카드로 무엇을 꺼낼지까지 다룬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHd5kx/dJMcaiQBtbX/SGqPjnUAHOBMpsi08BDJT0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHd5kx/dJMcaiQBtbX/SGqPjnUAHOBMpsi08BDJT0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHd5kx/dJMcaiQBtbX/SGqPjnUAHOBMpsi08BDJT0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHd5kx%2FdJMcaiQBtbX%2FSGqPjnUAHOBMpsi08BDJT0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;720&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://unsplash.com/photos/a-computer-screen-with-a-web-page-on-it-oLthDWAG244&quot;&gt;https://unsplash.com/photos/a-computer-screen-with-a-web-page-on-it-oLthDWAG244&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPT 5.5는 무엇인가? 출시 배경부터 정리한다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI GPT 5.5의 출시는 진짜 갑작스러웠다. GPT-5 정식 발표가 작년 8월이었는데, 그 사이 5.1 패치가 한 번 있었고 이번에 5.5가 미들 업데이트로 등장했다. 5.5가 의미 있는 이유는 OpenAI 자체 분류상 &quot;.5&quot; 라인이 늘 추론 능력의 점프를 의미해 왔기 때문이다. GPT-3.5도, GPT-4.5도 그랬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 어떻게 발표되었는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI는 평소처럼 새벽 2시(KST) 라이브 스트림으로 GPT 5.5를 공개했다. 보통 OpenAI DevDay 즈음이거나 분기 실적 시즌에 맞춰 던지는 패턴인데, 이번엔 그런 행사 없이 갑작스럽게 떨어뜨렸다는 점이 특징이다. Hacker News에서는 &quot;Anthropic이 다음 주 무언가를 발표한다는 루머에 대한 견제 아니냐&quot;는 반응이 톱 코멘트였다. 사실 여부는 알 수 없으나, 정황상 그럴 만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모델 라인업은 어떻게 갈리는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라인업은 세 갈래다. 메인 GPT 5.5, 빠르고 저렴한 GPT 5.5 mini, 그리고 추론 강화판 GPT 5.5 Thinking이다. mini가 가격 압박용 카드이고, Thinking이 Claude Opus와 직접 붙는 라인이다. nano는 이번 라인업에서 빠졌고 5.1 nano가 그대로 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이전 GPT-5 대비 무엇이 바뀌었다고 주장하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OpenAI 공식 블로그에서 강조하는 셀링 포인트는 세 가지다. 첫째 코딩 능력(특히 멀티파일 리팩토링), 둘째 멀티모달 추론(이미지+텍스트 동시 추론), 셋째 컨텍스트 윈도우 확장이다. 컨텍스트는 GPT-5의 256K에서 5.5에서는 1M 토큰까지 늘어났다. Claude가 이미 1M 컨텍스트를 밀고 있던 영역에 OpenAI가 들어왔다는 점이 진짜 시그널이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPT 5.5 성능 벤치마크 점수만 보면 진짜 강하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벤치 점수부터 보면 마이너 업데이트라고 부르기 어려운 GPT 5.5 성능이 나온다. 특히 SWE-bench Verified 점수가 눈에 띄게 점프했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 벤치마크 점수표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;벤치마크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPT 5.5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPT-5&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Claude Opus 4.7&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Claude Sonnet 4.6&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**MMLU-Pro**&lt;/td&gt;
&lt;td&gt;**84.2**&lt;/td&gt;
&lt;td&gt;81.5&lt;/td&gt;
&lt;td&gt;83.7&lt;/td&gt;
&lt;td&gt;80.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**GPQA Diamond**&lt;/td&gt;
&lt;td&gt;**78.9**&lt;/td&gt;
&lt;td&gt;73.1&lt;/td&gt;
&lt;td&gt;79.3&lt;/td&gt;
&lt;td&gt;71.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**SWE-bench Verified**&lt;/td&gt;
&lt;td&gt;**74.5**&lt;/td&gt;
&lt;td&gt;64.8&lt;/td&gt;
&lt;td&gt;72.1&lt;/td&gt;
&lt;td&gt;65.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**AIME 2025**&lt;/td&gt;
&lt;td&gt;**94.3**&lt;/td&gt;
&lt;td&gt;89.7&lt;/td&gt;
&lt;td&gt;90.2&lt;/td&gt;
&lt;td&gt;84.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**HumanEval+**&lt;/td&gt;
&lt;td&gt;**96.1**&lt;/td&gt;
&lt;td&gt;93.4&lt;/td&gt;
&lt;td&gt;94.8&lt;/td&gt;
&lt;td&gt;92.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;**MMMU (멀티모달)**&lt;/td&gt;
&lt;td&gt;**82.7**&lt;/td&gt;
&lt;td&gt;77.3&lt;/td&gt;
&lt;td&gt;80.1&lt;/td&gt;
&lt;td&gt;74.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표만 보면 GPT 5.5가 거의 모든 벤치에서 Claude Opus 4.7을 살짝 앞선다. 특히 &lt;b&gt;SWE-bench의 74.5는 진짜 세다&lt;/b&gt;. 이는 실제 GitHub 이슈를 패치하는 능력을 측정하는 지표인데, 작년 이맘때 GPT-4o가 19% 정도였던 것을 생각하면 1년 만에 4배 가까이 뛴 셈이다.&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;다만 함정도 있다. GPQA Diamond는 0.4점 차이라 사실상 동률이고, MMLU-Pro도 비슷한 수준이다. 격차가 진짜 큰 부분은 SWE-bench와 AIME(수학)인데, AIME는 평가 방식 논란이 좀 있다(파인튜닝/유출 의심). 그래서 보수적으로 보면 &quot;코딩에서 확실히 앞서고, 나머지는 비등하다&quot; 정도가 정확한 평가다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LMArena ELO 위치&lt;/h3&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;a href=&quot;https://lmarena.ai/&quot;&gt;LMArena 리더보드&lt;/a&gt; 기준으로 GPT 5.5는 출시 일주일 만에 ELO 1428로 1위를 찍었다. 직전 1위가 Claude Opus 4.7(1412)이었는데 16점 차이로 역전했다. 16점이 작아 보이지만, LMArena ELO는 100점 차이가 나면 승률이 64% 정도라 16점이면 미세하지만 확실한 우위다. 다만 LMArena는 사용자 투표라 &quot;느낌 좋음&quot; 점수에 가까워, 실제 업무 성능과 갭이 있을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cx4CH/dJMcaiQBtbZ/hjjaEYAFYCV46sKasCSIqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cx4CH/dJMcaiQBtbZ/hjjaEYAFYCV46sKasCSIqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cx4CH/dJMcaiQBtbZ/hjjaEYAFYCV46sKasCSIqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCx4CH%2FdJMcaiQBtbZ%2FhjjaEYAFYCV46sKasCSIqK%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;728&quot; height=&quot;508&quot; data-origin-width=&quot;728&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://vertu.com/ai-tools/openai-gpt-5-2-benchmarks-expert-performance-coding-reasoning-gains-api-available/&quot;&gt;vertu.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;다만 벤치 점수 &amp;ne; 실사용 &amp;mdash; 외국 개발자들의 진짜 반응&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서부터가 진짜 GPT 5.5 리뷰의 핵심이다. 벤치 점수와 실사용 후기는 갭이 큰 경우가 많다. 그래서 외국 개발자 커뮤니티의 톱 코멘트를 모아보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hacker News 톱 코멘트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hacker News 출시 스레드의 톱 코멘트는 &quot;코드 리팩토링에서 Claude보다 명확히 빠르고, 큰 코드베이스에서 컨텍스트를 잃지 않는다&quot;였다. 다만 바로 다음 코멘트가 &quot;그래서 더 빠르게 헛소리를 한다&quot;였기에 댓글창이 두 진영으로 갈렸다. 환각(hallucination) 이슈가 5.5에서도 완전히 잡히지는 않았다는 것이 중론이다.&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;또 다른 인기 코멘트 하나는 다음과 같다. &quot;Claude는 여전히 글 쓰는 톤이 더 자연스럽다. GPT 5.5는 똑똑하지만 좀 사무적이다. 마케팅 카피를 뽑을 때는 Claude로 돌아간다.&quot; 이 의견이 의외로 많은 공감을 받았다. 모든 일에서 GPT 5.5가 우월하지는 않다는 신호다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reddit 분위기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;r/OpenAI는 당연히 환호 분위기인데, r/ClaudeAI는 의외로 차분하다. &quot;Anthropic이 곧 Sonnet 4.7이나 Opus 5를 던질 것이다&quot;라는 추측이 톱 게시물이고, 실제로 Claude를 해지하고 갈아탔다는 후기는 생각보다 적다. r/LocalLLaMA(오픈소스 진영)에서는 &quot;둘 다 우리에겐 닫힌 모델&quot;이라며 쿨한 반응이다.&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;흥미로운 점은 r/ChatGPTPro에서 나온 의견이다. &quot;Plus 구독자에게는 5.5 풀 모델을 풀어주지 않고 mini만 줄 것 같다&quot;는 추측이 돌았다. 실제로 출시 일주일 시점 기준 Plus 사용자는 5.5 mini만 사용할 수 있고, 풀 5.5는 Team/Enterprise부터 풀린 상태다. 이는 한국 일반 유저 입장에서 좀 짜증나는 지점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Simon Willison 같은 신뢰 가능한 개인 리뷰&lt;/h3&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;a href=&quot;https://simonwillison.net/&quot;&gt;Simon Willison&lt;/a&gt;이 자기 블로그에 GPT 5.5 리뷰를 올렸는데, 핵심 한 줄은 &quot;drop-in 업그레이드로 쓰기엔 좋지만, 가격이 슬슬 부담이다&quot;였다. 그가 늘 강조하는 &quot;토큰당 가격 대비 능력&quot; 관점에서, 5.5는 출력 가격이 GPT-5 대비 1.6배 비싸졌다. 능력이 1.6배 좋아진 것은 아니므로 가성비는 오히려 후퇴했다.&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;또 다른 영향력 있는 리뷰어 Ethan Mollick(와튼 교수)은 &quot;에이전트 작업에서 진짜 차이가 난다&quot;라고 평가하며 GPT 5.5가 도구 호출과 자기 수정 루프에서 Claude보다 한 단계 위라고 평가했다. 에이전트 빌더라면 갈아탈 가치가 있다는 결론이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djIi9J/dJMcadoes2K/oieKkKwcVseMiRzHnREWi0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djIi9J/dJMcadoes2K/oieKkKwcVseMiRzHnREWi0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djIi9J/dJMcadoes2K/oieKkKwcVseMiRzHnREWi0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjIi9J%2FdJMcadoes2K%2FoieKkKwcVseMiRzHnREWi0%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1252&quot; height=&quot;704&quot; data-origin-width=&quot;1252&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://tecnobits.com/en/OpenAI-accelerates-GPT-52-launch--respond-Gemini-3/&quot;&gt;tecnobits.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;가격이 진짜 변수다 &amp;mdash; GPT 5.5 가격 분석&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벤치도 좋고 리뷰도 좋다고 치자. 결국 돈 문제로 귀결된다. GPT 5.5 가격 구조를 보면 OpenAI가 좀 욕심을 부린 것이 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 토큰 단가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Input ($/1M)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Output ($/1M)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT 5.5&lt;/td&gt;
&lt;td&gt;$3.50&lt;/td&gt;
&lt;td&gt;$14.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT 5.5 mini&lt;/td&gt;
&lt;td&gt;$0.30&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5 (구)&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.7&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$25.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;$1.50&lt;/td&gt;
&lt;td&gt;$7.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Output 토큰 기준 GPT 5.5는 Claude Opus 4.7의 56% 가격이다. 능력은 비슷한데 가격은 거의 절반 수준이라 &lt;b&gt;이것이 진짜 Anthropic 입장에서는 뼈아프다&lt;/b&gt;. Sonnet 4.6($7.50)이 5.5($14)보다 저렴하긴 하지만, 능력 차이가 좀 있어 단순 비교는 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ChatGPT Plus / Team / Enterprise 적용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ChatGPT Plus($20/월) 구독자는 앞서 언급한 대로 5.5 mini만 사용할 수 있고, 풀 5.5는 일일 메시지 제한이 빡세게 걸린다(50개/3시간 정도라는 보고가 있다). Team($30/월)부터 풀 5.5를 거의 무제한에 가깝게 쓸 수 있다. Enterprise는 컨텍스트 윈도우 1M을 풀로 활용 가능하다. 한국 일반 유저는 Plus가 메인일 텐데, 진짜 5.5의 능력을 보려면 Team으로 가야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude API와 비교 시 코스트 시뮬레이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;월 1000만 토큰 input + 1000만 토큰 output 기준 간단한 계산은 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPT 5.5&lt;/b&gt;: $35 + $140 = &lt;b&gt;$175/월&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Claude Opus 4.7&lt;/b&gt;: $50 + $250 = &lt;b&gt;$300/월&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Claude Sonnet 4.6&lt;/b&gt;: $15 + $75 = &lt;b&gt;$90/월&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPT 5.5 mini&lt;/b&gt;: $3 + $12 = &lt;b&gt;$15/월&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가격만 보면 GPT 5.5가 Opus보다 41% 저렴하고, Sonnet보다 비싸지만 능력 차이를 고려하면 합리적이다. mini는 거의 공짜 수준이라 Sonnet 4.6 시장을 잠식할 가능성이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Anthropic 입장에서 봤을 때 진짜 위기인지 분석한다 &amp;mdash; GPT 5.5 vs Claude&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기가 글의 핵심 질문이다. Anthropic이 진짜 긴장해야 하는가. 결론부터 말하면 &lt;b&gt;벤치 점수를 따라잡힌 것은 사실이지만, 망한 것은 절대 아니다&lt;/b&gt;. 영역별로 보면 아직 Claude가 우위인 곳도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude의 강점이 여전히 살아있는 영역&lt;/h3&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;b&gt;글쓰기 톤&lt;/b&gt;이다. 앞서도 언급했지만 자연스러운 한국어, 영어를 가리지 않고 Claude가 더 사람처럼 쓴다. 마케팅 카피, 블로그 글, 이메일 같은 영역에서는 Claude의 우위가 명확하다.&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;둘째, &lt;b&gt;긴 문서 처리&lt;/b&gt;다. 1M 토큰 컨텍스트로 같아졌으나, 실제로 800K 이상 넣었을 때 정보 추출 정확도(needle-in-haystack 테스트)에서 Claude가 더 안정적이다. OpenAI는 1M 풀 활용 시 정확도가 살짝 떨어진다는 보고가 있다.&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;셋째, &lt;b&gt;코드 리팩토링 안정성&lt;/b&gt;이다. SWE-bench는 GPT 5.5가 이겼지만, 실제 대규모 프로덕션 코드(파일 100개 이상)를 다룰 때는 Claude Code 사용자들 사이에서 &quot;GPT 5.5가 더 빠르지만 더 위험한 변경을 한다&quot;는 평가가 많다. 안정성과 속도의 트레이드오프다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPT 5.5가 침범한 영역&lt;/h3&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;b&gt;수학/추론 벤치마크&lt;/b&gt;다. AIME, GPQA에서 격차를 만들었다. 하드 사이언스 영역에서 OpenAI가 다시 앞섰다.&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;둘째, &lt;b&gt;에이전트 작업&lt;/b&gt;이다. 도구 호출, 자기 수정 루프에서 GPT 5.5가 한 수 위라는 것이 중평이다. AutoGPT 류 빌더에게는 갈아탈 이유가 충분하다.&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;셋째, &lt;b&gt;가격 압박&lt;/b&gt;이다. Output $14 vs $25 격차는 무시할 수 없다. 대규모로 돌리는 스타트업 입장에서 월 수천만 원 차이가 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Anthropic이 다음 카드로 무엇을 꺼낼 가능성이 있는가&lt;/h3&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;b&gt;Sonnet 4.7의 빠른 출시&lt;/b&gt;(가격 경쟁력 + 속도 강화)이고, 다른 하나는 &lt;b&gt;Opus 5 발표&lt;/b&gt;(전면전)다. Anthropic은 평소 OpenAI보다 출시 사이클이 보수적이라 Opus 5는 한두 분기 더 걸릴 수 있고, Sonnet 4.7은 다음 달 안에 나올 가능성이 높다고 본다. Reddit r/ClaudeAI에서는 &quot;Claude Code 전용 모델&quot;이 따로 나올 것이라는 추측도 도는데, 신빙성은 낮다.&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;또 하나의 변수는 &lt;b&gt;Anthropic이 Amazon Bedrock에 더 깊이 통합&lt;/b&gt;되는 것이다. 엔터프라이즈 시장은 컴플라이언스/SOC2/HIPAA 같은 것이 변수라 OpenAI 단독 능력만으로는 못 뚫는 시장이 크다. 그 영역에서 Claude는 굳건하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;235&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AOqhI/dJMcaiQBtbY/a966K8fxMpEKCnHkoUOEy1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AOqhI/dJMcaiQBtbY/a966K8fxMpEKCnHkoUOEy1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AOqhI/dJMcaiQBtbY/a966K8fxMpEKCnHkoUOEy1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAOqhI%2FdJMcaiQBtbY%2Fa966K8fxMpEKCnHkoUOEy1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;420&quot; height=&quot;235&quot; data-origin-width=&quot;420&quot; data-origin-height=&quot;235&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: &lt;a href=&quot;https://www.theistanbulchronicle.com/post/open-ai-vs-anthropic-ai-differences-between-two-leading-startups&quot;&gt;theistanbulchronicle.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;그래서 갈아타야 하는가? 케이스별 결론&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API로 자동화를 돌리는 사람&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코딩 에이전트, 자동 PR 리뷰 봇 같은 것을 돌린다면 &lt;b&gt;GPT 5.5로 갈아탈 만하다&lt;/b&gt;. SWE-bench 점수, 가격 우위, 에이전트 강점이 합쳐져 명확한 ROI가 나온다. 다만 결과물 안정성이 중요한 프로덕션 작업이라면 일단 mini로 A/B 테스트를 돌려본 후 결정하는 것이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ChatGPT Plus / Claude Pro 구독자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 좀 미묘하다. ChatGPT Plus($20)에서는 풀 5.5를 쓸 수 없으니 갈아탈 이유가 약하다. Claude Pro($20)를 쓰는 사람은 이번에 굳이 갈아탈 필요가 없다. 글쓰기/일반 대화 위주라면 Claude가 여전히 좋다. 코딩 위주라면 Cursor나 GitHub Copilot이 GPT 5.5 백엔드로 깔리니, 그쪽을 통해 쓰는 것이 가성비가 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;둘 다 쓰는 것이 답인 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 내가 보기엔 이것이 정답이다. 코드/에이전트는 GPT 5.5, 글쓰기/긴 문서/리서치는 Claude로 분업하는 것이 가장 효율적이다. API라면 라우터 한 번 짜놓고 작업 종류별로 자동 분기시키면 된다. ChatGPT Plus + Claude Pro 동시 구독해도 월 $40인데, 둘 중 하나만 쓰다가 답답해하는 것보다는 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPT 5.5 리뷰 핵심 정리 &amp;mdash; 결국 갈아탈 것인가 말 것인가&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPT 5.5 리뷰 종합&lt;/b&gt;: 코딩(SWE-bench 74.5)과 추론(AIME 94.3) 영역에서 Claude Opus 4.7을 턱밑까지 추격, 일부 추월. 멀티모달과 1M 컨텍스트가 추가되었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;가격&lt;/b&gt;: Output $14/1M로 Claude Opus($25)의 56%. 가성비로는 OpenAI의 승리다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실사용 후기&lt;/b&gt;: 코드/에이전트는 GPT 5.5 우위, 글쓰기/긴 문서 안정성은 Claude 우위. 환각 이슈는 둘 다 여전하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Anthropic 위기인가?&lt;/b&gt;: 진짜 망한 것은 절대 아니지만, 처음으로 가격과 벤치 양쪽 모두 밀린 분기다. Sonnet 4.7의 빠른 발표가 카운터로 유력하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;결론&lt;/b&gt;: 아직 Anthropic이 망한 것은 아니지만, 이번에는 OpenAI가 잘 때린 것으로 보인다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 써보고 싶다면 OpenAI 콘솔과 Anthropic 콘솔에서 평가판 크레딧을 받아 같은 프롬프트로 돌려보면 답이 나온다. 같은 작업을 던져놓고 결과물, 토큰 비용, 응답 시간 세 가지만 비교해도 본인의 워크플로우에 무엇이 맞는지 30분이면 판단된다. GPT 5.5 리뷰는 어차피 남의 후기일 뿐이고, 본인 작업에 맞는지는 직접 돌려봐야 진짜 답이 나온다.&lt;/p&gt;</description>
      <category>IT 뉴스 이것저것</category>
      <category>Anthropic Claude 비교</category>
      <category>GPT 5.5 vs Claude</category>
      <category>GPT 5.5 가격</category>
      <category>GPT 5.5 성능</category>
      <category>OpenAI GPT 5.5 출시</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/377</guid>
      <comments>https://yscho03.tistory.com/377#entry377comment</comments>
      <pubDate>Sat, 25 Apr 2026 19:54:43 +0900</pubDate>
    </item>
    <item>
      <title>CGI, WSGI, WAS의 차이 정리 &amp;mdash; 웹서버 공부할 때 헷갈리는 개념 한 번에 끝내기</title>
      <link>https://yscho03.tistory.com/376</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;면접관이 &quot;웹서버와 WAS의 차이가 무엇인가&quot;라고 물었을 때 버벅거린 경험이 있는가? 나 또한 그랬다. 더 골치 아픈 점은 여기에 CGI, WSGI 같은 단어까지 섞여 들어오면 머리가 아예 멈춰버린다는 것이다. 이름은 모두 &quot;서버 비슷한 무언가&quot;라서 유사해 보이지만, 실제로는 각자 다른 계층에서 다른 일을 수행하는 존재들이다.&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;이 글에서는 CGI, WSGI, WAS의 차이를 시간축과 역할축 두 가지로 정리한다. 역사적으로 어떻게 등장했으며, 현재 실전 배포에서 어떻게 엮이는지까지 다루면 면접에서도 밀리지 않고 실무에서도 용어가 꼬이지 않는다. 파이썬 사용자는 WSGI를 왜 쓰는지 이해하게 되고, 자바 사용자는 톰캣이 왜 WAS로 불리는지 감을 잡게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cR0XNc/dJMcagyuXxM/UPPmNSTKjSb8UPZcmi9m0K/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cR0XNc/dJMcagyuXxM/UPPmNSTKjSb8UPZcmi9m0K/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cR0XNc/dJMcagyuXxM/UPPmNSTKjSb8UPZcmi9m0K/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcR0XNc%2FdJMcagyuXxM%2FUPPmNSTKjSb8UPZcmi9m0K%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: YouTube &amp;gt; I.T&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;웹서버부터 짚고 가자 (정적 vs 동적)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGI, WSGI, WAS의 차이를 논하기 전에 &quot;웹서버&quot;가 무엇을 하는 존재인지부터 알아야 한다. Apache, Nginx 같은 것들을 의미한다. 이들의 본래 역할은 단순한 파일 배달부다. HTML, CSS, 이미지, JS 같은 &lt;b&gt;정적 파일&lt;/b&gt;을 클라이언트(브라우저)가 요청하면 디스크에서 꺼내 그대로 전송하는 것이 전부였다.&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;그러나 1990년대 중반부터 사람들의 욕심이 생겼다. &quot;로그인 여부에 따라 다른 화면을 보여주고 싶다&quot;, &quot;게시판에 글을 써서 DB에 저장하고 싶다&quot;, &quot;방문자마다 맞춤 콘텐츠를 제공하고 싶다&quot; 같은 요구였다. 이것이 바로 동적 콘텐츠이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹서버가 혼자서는 못 하는 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정적 파일 배달만 하던 웹서버에게 동적 처리는 버거운 일이었다. 요청마다 상태가 달라야 하고, DB 쿼리 결과를 HTML로 변환해야 하며, 세션도 관리해야 하고, 파일 업로드도 받아야 한다. 이를 웹서버 코드 안에 모두 박아넣으면 웹서버가 괴물처럼 비대해지고 확장성도 무너진다.&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;그래서 사람들이 고안한 것이 &quot;웹서버는 파일 배달만 계속하고, 동적 처리는 외부 프로그램에게 위임하자&quot;는 접근이었다. 다만 외부 프로그램에게 어떻게 요청 정보를 넘기고 결과를 다시 받아올 것인지에 대한 &lt;b&gt;약속(인터페이스)&lt;/b&gt; 이 필요했다. 여기서 등장한 것이 CGI이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CGI란 무엇인가 (1990년대의 해결책)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGI는 Common Gateway Interface의 약자이다. 번역하면 &quot;공통 게이트웨이 인터페이스&quot;이다. 1993년 NCSA HTTPd 서버에서 처음 등장했고, 곧 웹 전체의 표준이 되었다. 의미 그대로 &quot;웹서버와 외부 프로그램 사이의 공통 규약&quot;이다.&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;동작 원리는 의외로 단순하다. 웹서버가 /cgi-bin/hello.pl 같은 경로로 요청을 받으면, 웹서버는 해당 스크립트 파일을 &lt;b&gt;새 프로세스로 fork&lt;/b&gt;하여 실행한다. 요청 정보는 환경변수와 stdin으로 넘기고, 스크립트는 HTML을 stdout으로 출력한다. 웹서버는 그 출력을 받아 그대로 브라우저로 전송하는 구조이다.&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;CGI의 장점은 분명하다. 언어 독립적이다. 스크립트가 Perl이든 C든 셸 스크립트든 상관없다. stdin/stdout/환경변수만 다룰 줄 알면 된다. 표준화되어 있어서 Apache, NCSA 서버 어디서도 동일하게 동작했다.&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;다만 현재 거의 사용되지 않는 이유가 있다. 치명적인 성능 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CGI가 쇠락한 이유는 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 하나가 올 때마다 프로세스를 fork하는 구조가 문제였다. 동시접속자가 100명만 되어도 프로세스 100개가 순간적으로 생성되었다 사라진다. fork 비용, 프로세스 생성 오버헤드, 메모리 낭비가 상당한 수준이다.&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;더 큰 문제는 &lt;b&gt;상태 공유가 불가능&lt;/b&gt;하다는 점이다. DB 커넥션 풀은 어떻게 할 것인가? 요청마다 프로세스가 새로 뜨기 때문에 매번 커넥션을 다시 열어야 한다. 캐시는? 프로세스가 종료되면서 함께 사라진다. 세션 데이터는? 파일이나 DB로 별도 분리해야 한다. 2000년대에 접어들어 웹 트래픽이 폭증하면서 CGI로는 도저히 감당할 수 없었다.&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;개선 시도로 &lt;b&gt;FastCGI&lt;/b&gt;(1996)가 나오기도 했다. 프로세스를 미리 띄워놓고 요청이 올 때마다 재사용하는 방식이다. 그러나 FastCGI도 결국 언어 독립 인터페이스의 한계를 넘지 못했고, 각 언어 생태계가 자신의 언어 전용 인터페이스를 만들기 시작했다. 파이썬은 WSGI로, 자바는 서블릿으로 갈라졌다.&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;참고로 MDN 용어집에 CGI 설명이 잘 나와 있다. 관심이 있다면 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Glossary/CGI&quot;&gt;MDN CGI&lt;/a&gt; 문서를 한 번 읽어보면 된다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WSGI &amp;mdash; 파이썬이 선택한 표준 인터페이스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WSGI는 Web Server Gateway Interface이다. 2003년에 제안되었고 2010년 &lt;a href=&quot;https://peps.python.org/pep-3333/&quot;&gt;PEP 3333&lt;/a&gt;으로 공식 표준화되었다. CGI가 지녔던 &quot;언어 독립성&quot;이라는 장점을 포기하는 대신 &lt;b&gt;&quot;파이썬 전용 표준&quot;&lt;/b&gt; 으로 방향을 튼 것이다.&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;핵심은 이것이다. WSGI는 &quot;파이썬 웹 앱과 웹서버가 어떻게 대화할지 정한 규약&quot;일 뿐이다. 실제로 무언가를 수행하는 존재가 아니다. 프로토콜/스펙 문서일 뿐이며, 이 규약을 실제로 구현한 프로그램을 &lt;b&gt;WSGI 서버&lt;/b&gt;(또는 WSGI 컨테이너)라고 부른다.&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;구조는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777107919429&quot; class=&quot;less&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[브라우저]
    &amp;darr; HTTP
[웹서버: Nginx] &amp;mdash; 정적 파일 직접 응답
    &amp;darr; (동적 요청만 소켓으로 넘김)
[WSGI 서버: Gunicorn] &amp;mdash; 파이썬 인터프리터 내장
    &amp;darr; (WSGI 규약에 따라 함수 호출)
[파이썬 앱: Flask/Django] &amp;mdash; 비즈니스 로직
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx가 직접 Flask를 실행하지 못하는 이유는 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 다소 헷갈리는 부분이다. &quot;Nginx가 뛰어난데 왜 Flask 앱을 직접 실행하지 못하는가&quot;라는 의문이 드는데, 이유는 단순하다. &lt;b&gt;Nginx에는 파이썬 인터프리터가 없다&lt;/b&gt;. Nginx는 C로 작성된 고성능 HTTP 서버일 뿐이며, 파이썬 코드 실행 능력 자체가 없다.&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;그래서 중간에 &lt;b&gt;파이썬 인터프리터를 품고 있는 통역사&lt;/b&gt;가 필요한 것이다. 그것이 바로 Gunicorn이나 uWSGI 같은 WSGI 서버이다. 이들이 Nginx로부터 요청을 소켓으로 받아서, 파이썬 인터프리터에서 Flask/Django 앱을 호출하고, 응답을 다시 Nginx로 돌려주는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WSGI 서버의 종류&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Gunicorn&lt;/b&gt;: &quot;Green Unicorn&quot;이다. 가장 많이 사용된다. 설정이 간단하고 안정적이다. 대부분 기본값으로도 잘 동작한다. &lt;a href=&quot;https://docs.gunicorn.org/&quot;&gt;공식 문서&lt;/a&gt;도 깔끔하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;uWSGI&lt;/b&gt;: 기능이 매우 많다. 다만 옵션이 수백 개라 배우기가 까다롭다. 대규모 트래픽 튜닝 시 진가가 드러난다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;mod_wsgi&lt;/b&gt;: 아파치 모듈 형태이다. 아파치를 쓰는 환경에서의 선택지이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;waitress&lt;/b&gt;: 윈도우 개발 환경에서 많이 쓴다. 순수 파이썬 구현이라 설치가 간편하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNUvUD/dJMcagyuXxN/m3uK1ULmIWryOKXHEG9Wbk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNUvUD/dJMcagyuXxN/m3uK1ULmIWryOKXHEG9Wbk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNUvUD/dJMcagyuXxN/m3uK1ULmIWryOKXHEG9Wbk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNUvUD%2FdJMcagyuXxN%2Fm3uK1ULmIWryOKXHEG9Wbk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;720&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: YouTube&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;WAS란 무엇인가 &amp;mdash; 웹서버와 어떻게 다른가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 많은 이가 막힌다. WAS는 Web Application Server의 약자이다. &quot;웹 애플리케이션을 실행하고 관리하는 서버&quot;라는 의미이다. 다만 이 용어가 &lt;b&gt;자바 진영에서 먼저 굳어진 탓에&lt;/b&gt; 맥락이 다소 꼬여 있다.&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;자바에서 WAS라고 하면 보통 &lt;b&gt;Tomcat&lt;/b&gt;, &lt;b&gt;JBoss(WildFly)&lt;/b&gt;, &lt;b&gt;WebLogic&lt;/b&gt;, &lt;b&gt;WebSphere&lt;/b&gt; 같은 것들을 의미한다. 이들은 단순히 파이썬 앱 실행 수준이 아니라 다음을 모두 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹서버 기능 (HTTP 처리)&lt;/li&gt;
&lt;li&gt;서블릿 컨테이너 (자바 웹 앱 실행 환경)&lt;/li&gt;
&lt;li&gt;JSP 컴파일러&lt;/li&gt;
&lt;li&gt;DB 커넥션 풀&lt;/li&gt;
&lt;li&gt;트랜잭션 관리&lt;/li&gt;
&lt;li&gt;EJB 컨테이너 (요즘은 거의 사용되지 않음)&lt;/li&gt;
&lt;li&gt;클러스터링, 세션 복제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 기능들을 한 덩어리로 묶어 제공하는 것이다. 그래서 자바 쪽에서는 &quot;Tomcat은 WAS다&quot;라는 공식이 꽤 자연스럽게 받아들여진다.&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;그러나 파이썬이나 노드 쪽으로 넘어오면 용어가 다소 다르게 쓰인다. 파이썬에서 &quot;Gunicorn + Flask 조합&quot;은 자바의 Tomcat과 사실상 같은 레이어이다. 노드 쪽은 아예 Node.js 런타임 자체가 HTTP 서버까지 내장하여 WAS 개념을 따로 논하지 않는다. PM2 같은 프로세스 매니저를 WAS 포지션으로 보기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;웹서버 vs WAS 한 줄 정리&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;웹서버&lt;/b&gt;: 정적 파일 빠르게 배달 + 리버스 프록시 역할 (Nginx, Apache)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAS&lt;/b&gt;: 동적 로직 실행 + 앱 라이프사이클 관리 (Tomcat, Gunicorn+Flask, PM2+Node)&lt;/li&gt;
&lt;/ul&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;b&gt;앞단에 웹서버, 뒷단에 WAS&lt;/b&gt; 구조가 거의 표준이다. 왜일까? 역할 분리로 성능과 보안을 모두 챙길 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;면접 대비용 30초 답변&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&quot;웹서버는 정적 콘텐츠를 빠르게 처리하고, WAS는 동적 비즈니스 로직을 실행한다. 실전 배포에서는 Nginx 같은 웹서버가 앞단에서 요청을 받아, 정적 파일이면 직접 응답하고 동적 요청이면 뒷단의 Tomcat이나 Gunicorn 같은 WAS로 프록시 처리한다. 이렇게 역할을 나누면 정적 파일 응답 속도가 빨라지고, WAS는 비즈니스 로직에만 집중할 수 있으며, SSL 종단이나 로드밸런싱도 앞단에서 통합 처리가 가능하다.&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 답변하면 &quot;아, 이 사람은 개념이 정리되어 있구나&quot; 하고 들린다. 실무 용어(리버스 프록시, SSL 종단, 로드밸런싱)를 섞어 쓰면 플러스 요인이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CGI &amp;rarr; WSGI &amp;rarr; ASGI 흐름 한눈에&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1993년&lt;/b&gt;: CGI 등장. NCSA HTTPd에서 처음 등장&lt;/li&gt;
&lt;li&gt;&lt;b&gt;1996년&lt;/b&gt;: FastCGI로 프로세스 재사용 개선&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2003년&lt;/b&gt;: WSGI 초안 (PEP 333)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2010년&lt;/b&gt;: WSGI 공식화 (PEP 3333)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2019년&lt;/b&gt;: ASGI 1.0 스펙. 비동기 시대의 시작&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASGI가 등장한 이유는 무엇인가. WSGI는 근본적으로 &lt;b&gt;동기 방식&lt;/b&gt;이라 요즘 트렌드인 WebSocket이나 긴 연결(long polling), 비동기 I/O를 제대로 지원하지 못했기 때문이다. 그래서 &lt;b&gt;Asynchronous Server Gateway Interface&lt;/b&gt;로 확장한 것이 ASGI이다. 자세한 내용은 &lt;a href=&quot;https://asgi.readthedocs.io/&quot;&gt;ASGI 공식 사이트&lt;/a&gt;에 잘 나와 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CGI, WSGI, ASGI 비교표&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;CGI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;WSGI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;ASGI&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;등장 시기&lt;/td&gt;
&lt;td&gt;1993년&lt;/td&gt;
&lt;td&gt;2003년 (PEP 3333: 2010)&lt;/td&gt;
&lt;td&gt;2019년&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;처리 방식&lt;/td&gt;
&lt;td&gt;요청마다 프로세스 fork&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;Perl/C 스크립트&lt;/td&gt;
&lt;td&gt;Gunicorn, uWSGI&lt;/td&gt;
&lt;td&gt;Uvicorn, Daphne, Hypercorn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;대표 프레임워크&lt;/td&gt;
&lt;td&gt;없음 (단순 스크립트)&lt;/td&gt;
&lt;td&gt;Flask, Django, Pyramid&lt;/td&gt;
&lt;td&gt;FastAPI, Django Channels, Starlette&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket 지원&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;언어&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;장점&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;단점&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;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 파이썬 신규 프로젝트에서는 FastAPI + Uvicorn 조합(ASGI)이 많이 늘었다. 다만 Django/Flask 기반의 기존 프로젝트는 여전히 Gunicorn(WSGI)이 현역이다. ASGI가 WSGI를 완전히 대체한 것은 아니며, 용도에 따라 공존하는 단계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실전 배포 구조 예시&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;전통 파이썬 스택 (동기)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777107919432&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[브라우저] &amp;rarr; [Nginx :80/:443] &amp;rarr; [Gunicorn :8000] &amp;rarr; [Flask/Django 앱]
&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;Nginx가 SSL 종단, 정적 파일 서빙, 로드밸런싱을 담당한다. 동적 요청만 Gunicorn으로 프록시 처리한다. Gunicorn은 WSGI 스펙에 따라 Flask/Django 앱의 application 호출 가능한 객체를 실행한다.&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;&lt;b&gt;비동기 파이썬 스택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777107919432&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[브라우저] &amp;rarr; [Nginx] &amp;rarr; [Uvicorn] &amp;rarr; [FastAPI 앱]
&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;구조는 거의 같지만 WSGI 대신 ASGI를 쓴다. WebSocket을 사용하거나 async/await를 전반적으로 쓰는 프로젝트라면 이쪽이 낫다.&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;&lt;b&gt;자바 스택&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777107919432&quot; class=&quot;css&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;[브라우저] &amp;rarr; [Apache HTTPD/Nginx] &amp;rarr; [Tomcat :8080] &amp;rarr; [서블릿/Spring 앱]
&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;톰캣이 WAS 역할을 모두 수행한다. 서블릿 컨테이너 + JSP 엔진 + 커넥션 풀까지 내장되어 있다. 앞에 아파치나 Nginx를 두는 것은 SSL과 정적 파일 처리 용도가 크다. 톰캣 자체도 HTTP를 직접 받을 수 있지만 앞단에 웹서버를 두는 것이 관례이다. &lt;a href=&quot;https://tomcat.apache.org/&quot;&gt;Tomcat 공식 문서&lt;/a&gt;에 구조가 잘 나와 있다.&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;왜 항상 앞에 Nginx 같은 웹서버를 두는가.&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;: Nginx가 C로 작성되어 파일 배달 속도가 매우 빠르다. WAS를 거쳐 응답하는 것보다 훨씬 효율적이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSL 종단&lt;/b&gt;: HTTPS 복호화를 앞단에서 종료하고 뒷단은 HTTP로 통신한다. WAS 부담이 줄어든다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드밸런싱&lt;/b&gt;: WAS 여러 개를 띄우고 앞단의 Nginx가 부하를 분산한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: WAS를 외부에 직접 노출시키지 않는다. DDoS 완화, rate limit 등을 앞단에서 처리한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;압축/캐싱&lt;/b&gt;: gzip, 브라우저 캐시 헤더 관리를 앞단에서 통합 처리한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3줄 요약 &amp;mdash; CGI, WSGI, WAS의 차이 한 번 더&lt;/h2&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;&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;CGI&lt;/b&gt;는 1993년에 등장한 공통 게이트웨이 인터페이스이다. 요청마다 프로세스를 fork하여 동적 처리를 수행한 구식 방법이다. 이제 거의 쓰이지 않는다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WSGI&lt;/b&gt;는 파이썬 전용 규약(PEP 3333)이다. Gunicorn, uWSGI 같은 WSGI 서버가 실제 구현체이며, Flask/Django 앱과 Nginx 사이에서 통역사 역할을 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAS&lt;/b&gt;는 &quot;웹 앱 실행 서버&quot;의 통칭이다. 자바의 Tomcat이 대표적이며, 파이썬에서는 Gunicorn+앱 조합, 노드에서는 런타임 자체가 그 포지션을 차지한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CGI, WSGI, WAS 차이의 핵심은 &lt;b&gt;시간축(언제 등장했는가)&lt;/b&gt; 과 &lt;b&gt;역할축(어느 레이어에서 무엇을 수행하는가)&lt;/b&gt; 을 함께 보는 것이다. 이 두 축을 머릿속에 그려놓으면 앞으로 새로운 서버 용어가 나와도 &quot;아, 이것은 이 레이어의 존재로구나&quot; 하고 감을 잡을 수 있다.&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;다음 단계로 무엇을 할지 막막하다면, Flask 앱을 띄우고 Gunicorn으로 서비스를 돌린 뒤 앞에 Nginx를 붙여보는 실습을 추천한다. 한 번 해보면 이 글의 내용이 머릿속에 각인된다. ASGI를 맛보고 싶다면 FastAPI + Uvicorn으로 간단한 API를 만들어보는 것이 가장 빠르다. 솔직히 직접 띄워보기 전에는 개념이 애매한 것이 정상이다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>ASGI WSGI 차이</category>
      <category>CGI란</category>
      <category>WAS 뜻</category>
      <category>wsgi</category>
      <category>아파치 톰캣 차이</category>
      <category>웹서버 was 차이</category>
      <category>파이썬 WSGI 서버</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/376</guid>
      <comments>https://yscho03.tistory.com/376#entry376comment</comments>
      <pubDate>Sat, 25 Apr 2026 18:06:05 +0900</pubDate>
    </item>
    <item>
      <title>MIT, GPL 오픈소스 라이센스를 잘못 쓰면 회사가 망한다</title>
      <link>https://yscho03.tistory.com/375</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wiMwZ/dJMcadhuERM/8MDIqevCQGIZFk5g78F6D1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wiMwZ/dJMcadhuERM/8MDIqevCQGIZFk5g78F6D1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wiMwZ/dJMcadhuERM/8MDIqevCQGIZFk5g78F6D1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwiMwZ%2FdJMcadhuERM%2F8MDIqevCQGIZFk5g78F6D1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1240&quot; height=&quot;728&quot; data-origin-width=&quot;1240&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: Snyk&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;2024년 3월에 Redis가 갑자기 라이센스를 바꿔서 개발자 커뮤니티 전체가 뒤집어진 사건을 기억하는가. BSD에서 RSALv2/SGPLv1로 전환하자마자 Linux Foundation이 Valkey라는 포크를 만들었고, AWS와 구글도 여기에 합류했다. 이런 사태는 Redis 하나로 끝나지 않는다. HashiCorp Terraform도 2023년에 라이센스를 바꿨고, Elasticsearch는 2021년에 이미 갈아엎었다.&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;다만 이런 뉴스들이 남의 일이 아니라는 점이 문제다. 지금 이 글을 읽고 있는 개발자 중에 &quot;오픈소스 = 공짜&quot;라고 생각하고 GitHub에서 코드를 긁어다 쓴 사람이 많을 텐데, &lt;b&gt;오픈소스 라이센스는 무료 허락이 아니라 조건부 허락&lt;/b&gt;이다. MIT와 GPL이 무엇이 다른지 모르고 쓰다가 소스코드 전부를 공개하라는 소송을 당한 회사들이 실제로 존재한다. 이 글에서는 MIT와 GPL이 구체적으로 무엇이 다른지, 실무에서 무엇을 주의해야 하는지 한국 사례를 포함해 정리했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 라이센스란 무엇이며 왜 이렇게 신경 써야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;오픈소스 = 공짜&quot;라고 생각하면 큰일이 난다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈소스라는 말 때문에 착각하는 사람이 많지만, 오픈소스는 &quot;코드를 볼 수 있음 + 조건을 지키면 쓸 수 있음&quot;이지 &quot;아무렇게나 써도 된다&quot;가 절대 아니다. 라이센스마다 조건이 다르다. 어떤 것은 그저 &quot;저작권 표시만 해달라&quot;로 끝나고, 어떤 것은 &quot;수정한 코드도 전부 공개해야 한다&quot;로 끝난다.&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;Synopsys가 2024년에 발표한 OSSRA 리포트에 따르면 감사한 코드베이스의 96%가 오픈소스를 포함하고 있다. 평균적으로 프로젝트 하나당 526개의 오픈소스 구성 요소가 들어가 있고, 약 53%의 코드베이스에서 고위험 라이센스 충돌이 발견됐다. 즉 10개 프로젝트 중 5개는 라이센스 문제가 있는데 정작 본인들은 모르고 있다는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이센스를 지키지 않으면 실제로 소송을 당한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;설마 소송까지 가겠는가&quot; 싶겠지만 실제로 일어난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;한컴 리눅스 vs Ghostscript 사건&lt;/b&gt;: 한국에서 유명한 GPL 위반 사례다. Ghostscript가 GPL인데 한컴 제품에 묶어서 판매하면서 소스 공개를 하지 않았고, Artifex가 문제를 제기했다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;VMware vs Christoph Hellwig&lt;/b&gt;: 리눅스 커널 개발자가 VMware를 GPL 위반으로 독일 법원에 제소했다. 2016년부터 2021년까지 질질 끌린 소송이다. VMware가 리눅스 커널 코드를 자체 제품에 가져다 쓰면서 GPL 의무를 지키지 않았다는 주장이었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cisco vs FSF&lt;/b&gt;: 링크시스 공유기에 리눅스 커널 코드를 썼는데 GPL 조건을 지키지 않아 FSF가 소송을 제기했다. 결국 합의로 마무리됐다.&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;최근 1년간 라이센스 전환 사태 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2021년부터 지금까지 라이센스를 바꾸는 회사들이 줄줄이 나오고 있다. 공통점은 &quot;AWS 같은 하이퍼스케일러가 우리 오픈소스를 공짜로 써서 돈을 버는데 우리는 망해가는 중이라 더는 참을 수 없다&quot;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2021년 1월&lt;/b&gt; &amp;mdash; Elastic이 Elasticsearch를 Apache 2.0에서 SSPL + Elastic License로 전환했다. AWS가 즉시 OpenSearch 포크를 만들었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2023년 8월&lt;/b&gt; &amp;mdash; HashiCorp가 Terraform을 MPL에서 BUSL(Business Source License)로 전환했다. Linux Foundation이 OpenTofu 포크를 만들었다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2024년 3월&lt;/b&gt; &amp;mdash; Redis가 BSD 3-Clause에서 RSALv2/SGPLv1으로 전환했다. Linux Foundation과 AWS, Google이 Valkey 포크를 출범시켰다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2024년&lt;/b&gt; &amp;mdash; MongoDB는 이미 2018년에 SSPL로 바꿔놓았고, 최근에도 추가 제약을 검토 중이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 사실을 모르고 &quot;Redis는 오픈소스니까 괜찮다&quot;는 식으로 사내 서비스에 썼다가는 다음 버전부터 법무팀의 호출을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MIT 라이센스의 특징과 장단점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MIT 라이센스 한 줄 요약&lt;/h3&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;b&gt;&quot;저작권만 표시해주면 무엇이든 해도 된다, 심지어 상업적 이용도 허용한다&quot;&lt;/b&gt; 이것이 MIT 라이센스다. 1988년 MIT 대학에서 만든 것인데 지금까지도 가장 널리 쓰이는 퍼미시브(permissive) 라이센스다. 원문이 A4 한 장도 되지 않는다. 그만큼 단순하다.&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;핵심 조건은 단 2개다.&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;/li&gt;
&lt;li&gt;라이센스 전문을 배포본에 포함할 것&lt;/li&gt;
&lt;/ol&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MIT 라이센스를 쓰기에 적합한 상황은 무엇인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React, Node.js, Vue.js, Rails, jQuery, Angular &amp;mdash; 요즘 잘나가는 프레임워크는 대부분 MIT다. 이유가 명확하다. &lt;b&gt;기업이 가져다 쓰기에 가장 편하다&lt;/b&gt;. 법무팀이 &quot;이것을 써도 되는가&quot; 물으면 &quot;된다&quot;로 끝난다.&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;오픈소스 프로젝트를 만드는 입장이라면 다음과 같은 상황에서 MIT를 고려할 만하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최대한 많은 개발자가 쓰게 하고 싶을 때&lt;/li&gt;
&lt;li&gt;기업에서도 부담 없이 가져다 쓸 수 있기를 원할 때&lt;/li&gt;
&lt;li&gt;라이브러리/SDK/프레임워크처럼 넓게 퍼져야 의미 있는 프로젝트일 때&lt;/li&gt;
&lt;li&gt;특허 조항 같은 복잡한 것을 신경 쓰고 싶지 않을 때&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 &quot;내 코드를 가져다가 돈을 벌면 너도 오픈소스로 풀어라&quot; 같은 철학을 강요하고 싶다면 MIT는 절대 답이 아니다. 그것은 GPL 계열의 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MIT 라이센스를 쓸 때 주의할 점&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배포본에 LICENSE 파일이 빠지면 위반&lt;/li&gt;
&lt;li&gt;소스 주석의 저작권 표시를 지우면 위반&lt;/li&gt;
&lt;li&gt;저작자의 이름을 허락 없이 마케팅에 사용하면 별도의 문제가 생길 수 있다 (MIT 본문에는 이 조항이 없지만 상표법 차원의 이슈)&lt;/li&gt;
&lt;/ul&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;b&gt;MIT 라이센스 코드를 가져다 써도 원저작자가 나중에 라이센스를 바꿀 수 있다&lt;/b&gt;. 다만, 이미 MIT로 배포된 버전을 쓰는 것은 영원히 MIT다. 이는 Redis가 라이센스를 바꿨을 때에도 똑같이 적용됐다. 7.2.4까지는 BSD였기 때문에 Valkey가 그 버전을 기반으로 포크할 수 있었던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;GPL 라이센스의 특징과 악명 높은 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPL = 전염성 라이센스라는 말의 진짜 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uH8tg/dJMcaad1aNq/RXkqWLRvpdlPMjJ06i1Ktk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uH8tg/dJMcaad1aNq/RXkqWLRvpdlPMjJ06i1Ktk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uH8tg/dJMcaad1aNq/RXkqWLRvpdlPMjJ06i1Ktk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuH8tg%2FdJMcaad1aNq%2FRXkqWLRvpdlPMjJ06i1Ktk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;300&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: ravesli.com&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;GPL 이야기가 나오면 &quot;전염성&quot;이라는 단어가 항상 따라붙는다. 개발자들이 농담처럼 &quot;GPL 바이러스&quot;라고 부르기도 한다. 왜일까?&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;GPL은 &lt;b&gt;카피레프트(Copyleft)&lt;/b&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;&quot;내 코드를 쓰고 싶다면 너도 네 코드를 공개해야 한다&quot;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, GPL 코드를 단 한 줄이라도 프로젝트에 합치는 순간, 그 프로젝트 전체가 GPL이 된다. 상용 제품을 팔려고 해도 소스를 공개해야 한다. 비공개 상용 소프트웨어를 만들려는 회사에는 거의 재앙 수준이다.&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;예시를 들면, 사내 시스템에 GPL 라이브러리를 정적 링크로 넣으면 &amp;rarr; 그 사내 시스템 전체가 GPL 의무를 지게 되고 &amp;rarr; 고객이 소스를 요구하면 넘겨줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPL v2 vs GPL v3의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL에도 버전이 있다. v2는 1991년, v3는 2007년에 나왔다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPL v2&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPL v3&lt;/b&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;강함 (명시적 라이센스 부여)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tivoization 방지&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;있음 (하드웨어 잠금 금지)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DRM 사용&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;제한적&lt;/td&gt;
&lt;td&gt;Apache 2.0 등과 호환 확대&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;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Tivoization은 TiVo라는 회사가 GPL 코드를 쓰면서 하드웨어 잠금으로 수정이 불가능하게 만든 사건에서 유래했다. v3는 이를 명시적으로 금지한다. 리눅스 커널이 지금도 v2에 머물러 있는 이유 중 하나가 이것이다. Linus Torvalds가 v3로 가지 않겠다고 못 박았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LGPL과 AGPL은 무엇이 다른가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL은 세 종류로 나뉜다. 이것을 헷갈리면 큰일이 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;GPL (일반)&lt;/b&gt;: 가장 엄격하다. 정적 링크든 동적 링크든 전부 전염된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;LGPL (Lesser GPL)&lt;/b&gt;: 동적 링크는 전염되지 않는다. 라이브러리를 수정한 부분만 공개하면 된다. 그래서 상용 소프트웨어가 LGPL 라이브러리를 .dll/.so로 동적 링크해서 쓰는 것은 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AGPL (Affero GPL)&lt;/b&gt;: SaaS 업계의 악몽이다. GPL은 &quot;배포&quot;할 때만 소스 공개 의무가 발생하는데, AGPL은 &lt;b&gt;네트워크를 통해 서비스를 제공하는 것도 배포로 간주한다&lt;/b&gt;. 즉, AGPL 코드를 서버에 올려 웹서비스로 제공하면, 사용자가 소스를 요청할 권리가 생긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MongoDB가 원래 AGPL이었는데 이것이 너무 독해서 AWS 같은 클라우드 업체가 피하는 분위기가 됐고, 결국 MongoDB가 SSPL로 다시 바꾸기에 이르렀다. AGPL은 피하든지, 쓸 것이라면 정말 각오하고 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;MIT와 GPL 외에도 알아야 할 주요 오픈소스 라이센스&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Apache 2.0 &amp;mdash; 기업이 선호하는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Apache 2.0은 MIT와 비슷한 퍼미시브 계열이지만 한 가지 큰 차이가 있다. &lt;b&gt;명시적 특허 라이센스 조항&lt;/b&gt;이 들어있다는 점이다.&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;이것이 왜 중요한가? MIT는 특허에 대해 아무 말도 하지 않는다. 그래서 A회사가 MIT 코드를 공개한 뒤, 나중에 &quot;이 코드에 우리 특허가 들어있으니 로열티를 내라&quot; 식으로 공격할 여지가 이론적으로 존재한다. Apache 2.0은 기여자가 본인의 특허도 라이센스에 함께 걸도록 강제한다. 그래서 Kubernetes, Android, Kafka, Spark, TensorFlow 같은 대기업 주도 프로젝트가 Apache 2.0을 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BSD &amp;mdash; MIT와 거의 비슷하지만 미묘하게 다르다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BSD는 3-Clause와 2-Clause 두 종류가 있다. 2-Clause는 MIT와 거의 동일하다. 3-Clause는 &quot;저작자 이름을 마케팅에 쓰지 말라&quot;는 조항이 추가된다. FreeBSD, Nginx(초기), PostgreSQL이 BSD다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MPL &amp;mdash; 파일 단위 카피레프트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mozilla Public License는 GPL의 전염성과 MIT의 자유로움 사이의 절충안이다. 수정한 파일만 공개하면 된다. 새로 추가한 파일은 원하는 라이센스로 걸 수 있다. Firefox, Rust, Cassandra가 MPL이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BUSL, SSPL &amp;mdash; '오픈소스 아닌 오픈소스'&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 부각되고 있는 카테고리다. 엄밀히 말하면 OSI가 인증한 오픈소스가 아니다. &quot;소스는 공개하지만 특정 사용은 금지한다&quot;는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;BUSL (Business Source License)&lt;/b&gt;: 일정 기간(보통 4년) 동안은 상업적 경쟁 용도로 쓸 수 없다. 그 이후에 Apache 2.0 같은 오픈소스로 전환된다. HashiCorp Terraform이 대표적이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SSPL (Server Side Public License)&lt;/b&gt;: AGPL보다 더 독하다. &quot;우리 소프트웨어를 서비스로 제공하려면 서비스 전체 인프라 소스를 공개하라&quot;는 내용이다. MongoDB와 Elasticsearch가 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘은 OSI 기준 오픈소스가 아니기 때문에 &quot;오픈소스&quot;라고 마케팅할 수 없다. source-available이라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;라이센스 한눈에 비교 테이블&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;라이센스&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;상용 가능&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;소스 공개 의무&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;수정본 공개 의무&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;특허 조항&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;전염성&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BSD 3-Clause&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache 2.0&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;있음(강함)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPL 2.0&lt;/td&gt;
&lt;td&gt;O&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LGPL&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;라이브러리만&lt;/td&gt;
&lt;td&gt;라이브러리만&lt;/td&gt;
&lt;td&gt;없음(v2)/있음(v3)&lt;/td&gt;
&lt;td&gt;동적링크 제외&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPL v2&lt;/td&gt;
&lt;td&gt;O(조건부)&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPL v3&lt;/td&gt;
&lt;td&gt;O(조건부)&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AGPL&lt;/td&gt;
&lt;td&gt;O(조건부)&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BUSL&lt;/td&gt;
&lt;td&gt;조건부(4년 제한)&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;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSPL&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실무에서 오픈소스 라이센스를 주의해야 하는 상황들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPL 코드를 상용 제품에 썼을 때 벌어지는 일&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 시나리오를 보자. 스타트업 A가 결제 시스템을 만드는데 GitHub에서 &quot;결제 SDK&quot;를 검색해 마음에 드는 것을 가져다 썼다. LICENSE 파일은 보지 않은 채 그냥 썼다. 알고 보니 GPL v3였다. 6개월 후 회사가 커지고 SI 사업도 하며 코드를 납품하게 됐다. 고객사가 감사를 요청한다. &quot;이 코드는 GPL이네요, 전체 소스를 넘겨주세요.&quot; 회사 전체의 비즈니스 로직이 GPL로 묶여버린다.&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;이것은 실제로 일어나는 일이다. 그래서 &lt;b&gt;GitHub에서 코드를 복사하기 전에 LICENSE 파일 확인은 필수&lt;/b&gt;다. CC-BY-SA도 비슷한 위험이 있다(문서/콘텐츠 쪽).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;npm/pip 의존성에 GPL이 섞여 들어오는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 쓰는 것은 MIT라 해도, 그 패키지가 GPL 의존성을 가지고 있다면 어떻게 되는가? 간접 의존성까지 라이센스를 체크해야 한다. npm 레지스트리에는 260만 개 이상의 패키지가 있고, 평균 의존성 트리는 수백 개 단계까지 뻗어있다. 사람이 전부 볼 수는 없다.&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;그래서 자동 스캔 도구가 필수다. 대표적인 것을 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;FOSSology&lt;/b&gt;: 오픈소스 라이센스 스캐너. Linux Foundation이 관리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ScanCode Toolkit&lt;/b&gt;: nexB에서 만든 정밀 스캐너다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Snyk License Compliance&lt;/b&gt;: 상용이지만 의존성 체크에 유용하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GitHub Dependency Graph&lt;/b&gt;: 무료이며 기본 정보를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사내 시스템에만 쓰는데도 공개해야 하는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPL FAQ의 해석에 따르면 &lt;b&gt;&quot;사내 사용&quot;은 배포(distribution)로 보지 않기 때문에 소스 공개 의무가 없다&lt;/b&gt;. 즉, 회사 안에서만 쓰는 툴에 GPL 라이브러리를 쓰는 것은 이론적으로 문제가 없다.&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;다만 조심해야 할 지점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자회사나 계열사로 넘기는 것은 &quot;배포&quot;로 볼 수 있다 (법인이 다르기 때문)&lt;/li&gt;
&lt;li&gt;그 사내 시스템을 나중에 외부 고객에게 납품하는 순간 배포가 시작된다&lt;/li&gt;
&lt;li&gt;AGPL이라면 사내 직원이 웹으로 접근하는 것도 배포로 볼 여지가 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;일단 사내니까 괜찮다&quot;고 방심하면 나중에 사업 모델을 바꿀 때 발목을 잡힐 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AGPL은 SaaS 회사라면 진짜로 조심해야 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SaaS 업체라면 AGPL은 거의 손대면 안 된다. 코드를 배포하지 않고 서버에서만 돌려도, 사용자에게 소스 청구권이 발생한다. 회사 전체 서버 코드를 공개해야 할 수도 있다. MongoDB나 Grafana가 AGPL이었거나 지금도 부분적으로 그런 이유가 이것이다. 경쟁사 AWS 같은 곳이 공짜로 못 쓰게 막으려는 의도다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;오픈소스 라이센스 위반을 막기 위해 체크할 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;SBOM(소프트웨어 명세서)을 만들어 관리하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SBOM은 Software Bill of Materials의 약자다. 프로젝트에 어떤 오픈소스 구성 요소가 어떤 라이센스로 들어있는지 정리한 문서다. 미국에서는 2021년 행정명령(EO 14028)으로 정부 납품 소프트웨어에 SBOM 제출이 의무화됐다. SPDX, CycloneDX 같은 표준 포맷이 존재한다.&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;syft 같은 오픈소스 툴로 SBOM을 자동 생성할 수 있다. CI/CD 파이프라인에 넣어두면 빌드마다 최신 상태가 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;자동 스캔 도구로 의존성 트리 체크하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 언급한 FOSSology, ScanCode 외에도 CI에 바로 꽂을 수 있는 도구가 많다. GitHub Advanced Security, Dependabot, Snyk 등이다. &quot;무료니까 일단 AGPL을 넣고 나중에 생각하자&quot; 같은 접근은 지양하고 빌드 단계에서 차단해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;회사에서 오픈소스를 쓸 때의 승인 프로세스 제안&lt;/h3&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;&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;개발자가 라이브러리 도입을 요청 &amp;mdash; 용도, 대체 가능성, 라이센스를 명시&lt;/li&gt;
&lt;li&gt;자동 스캐너로 의존성까지 검증&lt;/li&gt;
&lt;li&gt;라이센스 분류 (퍼미시브 / 약한 카피레프트 / 강한 카피레프트 / 상업 제한)&lt;/li&gt;
&lt;li&gt;분류별로 승인 주체를 차등화 (MIT/Apache는 팀 리드, GPL/AGPL은 법무 필수)&lt;/li&gt;
&lt;li&gt;승인 기록을 SBOM에 누적&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오픈소스 도입 전 점검해야 할 10가지 체크리스트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;[ ] LICENSE 파일을 직접 확인했는가?&lt;/li&gt;
&lt;li&gt;[ ] SPDX 식별자로 라이센스 종류를 확정했는가?&lt;/li&gt;
&lt;li&gt;[ ] 간접 의존성까지 스캐너로 훑었는가?&lt;/li&gt;
&lt;li&gt;[ ] GPL/AGPL/SSPL/BUSL이 있는지 체크했는가?&lt;/li&gt;
&lt;li&gt;[ ] 제품 배포 방식(바이너리/SaaS/사내)과 맞는지 확인했는가?&lt;/li&gt;
&lt;li&gt;[ ] 정적 링크 vs 동적 링크 방식을 체크했는가? (LGPL 관련)&lt;/li&gt;
&lt;li&gt;[ ] 저작권 표시와 라이센스 전문 포함이 자동화돼 있는가?&lt;/li&gt;
&lt;li&gt;[ ] 최근 1년 내 라이센스가 바뀐 프로젝트 리스트를 업데이트했는가? (Redis, HashiCorp 등)&lt;/li&gt;
&lt;li&gt;[ ] AI 모델/데이터를 쓰는 경우 해당 라이센스도 확인했는가?&lt;/li&gt;
&lt;li&gt;[ ] 연 1회 SBOM 기반 라이센스 감사 계획이 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MIT/BSD&lt;/b&gt;: 저작권 표시만 하면 끝이다. 상용 가능. 대부분의 경우 안전하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Apache 2.0&lt;/b&gt;: MIT에 특허 조항을 더한 것이다. 기업 환경에서 가장 안전한 선택지다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPL/LGPL&lt;/b&gt;: 카피레프트가 전염된다. 상용 제품에 섞으면 본인의 코드도 공개해야 한다. 동적 링크로 LGPL을 쓰는 것은 예외적으로 가능하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;AGPL&lt;/b&gt;: SaaS 회사라면 반드시 피해야 한다. 네트워크로 서비스만 해도 소스 공개 의무가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis, HashiCorp, Elasticsearch 사례에서 보듯 &lt;b&gt;오픈소스 라이센스는 한 번 고르면 영원히 가는 것이 아니다&lt;/b&gt;. 지금 쓰는 라이브러리가 다음 버전에서 갑자기 상업 제한을 걸 수도 있다. 그러니 SBOM을 만들어 정기적으로 업데이트하고, 의존성 스캐너를 CI에 붙여두고, 회사 내부 승인 프로세스를 한번 정비해두기를 권장한다. &quot;라이센스는 법무의 문제&quot;라며 미뤄두면 결국 본인의 코드가 망가진다. &lt;b&gt;라이센스는 법무의 문제가 아니라 개발자의 문제다&lt;/b&gt;.&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;각설하고, 지금 당장 할 수 있는 것 하나만 꼽는다면 프로젝트 루트에서 syft를 돌려 SBOM을 뽑아보는 것이다. 라이센스 리스트가 나오면 그것을 기반으로 GPL/AGPL이 있는지부터 확인하면 된다. 그다음에 팀 내 오픈소스 라이센스 도입 가이드를 만들어두면 1년 후의 내가 고마워할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://opensource.org/licenses&quot;&gt;OSI 공식 라이센스 목록&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/licenses/gpl-faq.html&quot;&gt;FSF GPL FAQ&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://spdx.org/licenses/&quot;&gt;SPDX License List&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://olis.or.kr/&quot;&gt;OLIS 공개SW 라이센스 종합정보&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Synopsys Black Duck 2024 OSSRA Report&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>끄적끄적</category>
      <category>GPL</category>
      <category>GPL 라이센스</category>
      <category>MIT</category>
      <category>MIT 라이센스</category>
      <category>개발실무</category>
      <category>라이센스</category>
      <category>상용 오픈소스</category>
      <category>오픈소스</category>
      <category>오픈소스 라이센스 종류</category>
      <category>카피레프트</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/375</guid>
      <comments>https://yscho03.tistory.com/375#entry375comment</comments>
      <pubDate>Sat, 25 Apr 2026 18:04:45 +0900</pubDate>
    </item>
    <item>
      <title>요즘 주목받는 Hermes는 무엇인가 (에르메스 아님 주의)</title>
      <link>https://yscho03.tistory.com/374</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Hermes&quot;를 검색했는데 가방만 줄줄 뜨는 경험이 한 번쯤 있을 것이다. 요즘 AI 커뮤니티에서 언급되는 &lt;b&gt;Hermes AI 모델&lt;/b&gt;은 프랑스 럭셔리 브랜드 에르메스와는 완전히 다른 물건이다. &lt;a href=&quot;https://nousresearch.com/&quot;&gt;Nous Research&lt;/a&gt; 팀이 만든 오픈소스 LLM이고, 2025년 후반에 Hermes 4가 공개된 뒤 &lt;a href=&quot;https://www.reddit.com/r/LocalLLaMA/&quot;&gt;Reddit r/LocalLLaMA&lt;/a&gt; 상단에 관련 스레드가 한 달 넘게 걸려 있었다.&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;다만 한국어 자료는 얇다. 네이버는 죄다 버킨백&amp;middot;켈리백이고, 티스토리도 영문 기사 번역 수준이 대부분이다. 그래서 Hermes AI의 정체, 지금 주목받는 이유, 집에서 돌려보는 법, 실제로 써볼 만한지까지 한 번에 정리해보았다. 분량은 2,400단어 선에서 마친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hermes AI 모델이란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cT1TW3/dJMcaipzpUJ/BR4vb0i7AGmcGM0in4zLtK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cT1TW3/dJMcaipzpUJ/BR4vb0i7AGmcGM0in4zLtK/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cT1TW3/dJMcaipzpUJ/BR4vb0i7AGmcGM0in4zLtK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcT1TW3%2FdJMcaipzpUJ%2FBR4vb0i7AGmcGM0in4zLtK%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;200&quot; data-origin-width=&quot;200&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: huggingface.co&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes AI 모델은 Nous Research라는 연구 집단이 만든 오픈소스 대규모 언어모델(LLM) 시리즈다. GPT-4나 Claude처럼 질문하면 답하는 AI이지만, &lt;b&gt;코드와 가중치(weights)가 공개돼 있어 누구나 다운받아 로컬에서 돌릴 수 있다는 점&lt;/b&gt;이 핵심 차이다.&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;한 가지 오해하면 안 되는 지점은, Hermes가 처음부터 새로 만든 모델이 아니라는 사실이다. Meta의 Llama 시리즈나 Qwen 같은 베이스 모델 위에 파인튜닝을 얹은 인스트럭션 모델로 보면 된다. 비유하자면 Llama가 &quot;말은 할 줄 아는데 사람 말귀를 잘 못 알아듣는 아이&quot;라면, Hermes는 Llama에게 &quot;사람 말귀를 알아듣고 쫄지 말고 답하라&quot;라고 가르친 버전인 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nous Research는 어떤 팀인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nous Research는 2023년에 설립된, 탈중앙화(decentralized) AI를 표방하는 연구 집단이다. OpenAI나 Anthropic 같은 거대 기업형 구조가 아니라, 오픈소스 모델을 공개하고 커뮤니티 기반으로 굴러가는 형태다. 창립자 중 Teknium은 Hugging Face에서 오랫동안 파인튜닝 모델을 뿌려온 인물로 유명하다.&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;팀의 방향성은 단순하다. 상업 모델이 점점 더 검열되고 갇히는 반면, 개발자가 자유롭게 쓸 수 있는 강력한 오픈웨이트 모델이 필요하다는 전제다. 2025년 들어 이 포지셔닝이 실제 수요로 이어지기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hermes 시리즈 연혁 한눈에&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;버전&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;출시 시기&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;베이스 모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;주요 파라미터&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes 2&lt;/td&gt;
&lt;td&gt;2024년 초&lt;/td&gt;
&lt;td&gt;Mistral, Llama 2&lt;/td&gt;
&lt;td&gt;7B, 34B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes 2 Pro&lt;/td&gt;
&lt;td&gt;2024년 중반&lt;/td&gt;
&lt;td&gt;Llama 3&lt;/td&gt;
&lt;td&gt;8B, 70B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes 3&lt;/td&gt;
&lt;td&gt;2024년 후반&lt;/td&gt;
&lt;td&gt;Llama 3.1&lt;/td&gt;
&lt;td&gt;8B, 70B, 405B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes 4&lt;/td&gt;
&lt;td&gt;2025년 후반&lt;/td&gt;
&lt;td&gt;Llama 3.3 / 자체&lt;/td&gt;
&lt;td&gt;8B, 70B, 405B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes 3까지는 Llama 의존도가 꽤 높았으나, Hermes 4부터는 &lt;b&gt;자체 학습 파이프라인 비중이 눈에 띄게 커졌다&lt;/b&gt;. 이것이 지금의 관심도에 영향을 준 요소 중 하나다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 요즘 Hermes가 주목받는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;911&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1Hf4P/dJMcac3Vpip/ofMYfy4DTiGklrETUzKGJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1Hf4P/dJMcac3Vpip/ofMYfy4DTiGklrETUzKGJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1Hf4P/dJMcac3Vpip/ofMYfy4DTiGklrETUzKGJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1Hf4P%2FdJMcac3Vpip%2FofMYfy4DTiGklrETUzKGJ1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;911&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;911&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: the-decoder.com&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;h3 data-ke-size=&quot;size23&quot;&gt;벤치마크 격차가 좁혀졌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2025년 후반 공개된 Hermes 4 405B는 MMLU-Pro, GPQA-Diamond, MATH 같은 reasoning 벤치마크에서 &lt;b&gt;오픈소스 최상위권&lt;/b&gt;에 올랐다. GPT-4o 대비 점수 차이가 영역에 따라 0.5~1점 수준으로 좁혀진 것이 포인트다.&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;맥락이 중요하다. 2024년까지만 해도 &quot;오픈소스는 상업 모델을 못 따라간다&quot;가 기본 전제였다. Hermes 4가 그 갭을 완전히 없앴다기보다는, 적어도 벤치마크 기준에서는 같은 리그에서 비교가 가능하다는 사실을 증명했다. r/LocalLLaMA의 Hermes 4 리뷰 스레드가 업보트 5천을 넘긴 사실이 분위기를 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언센서드 포지셔닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상업 LLM을 써봤다면 &quot;그 질문은 답변드릴 수 없습니다&quot;라는 응답을 한 번쯤 받아봤을 것이다. Hermes는 &lt;b&gt;불필요한 검열을 최소화하는 방향&lt;/b&gt;으로 튜닝돼 있다.&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;오해하면 안 되는 지점은, 이것이 불법 콘텐츠 생성 모델이라는 이야기가 아니라는 사실이다. 보안 연구자가 취약점 코드를 물었을 때 GPT가 거절하거나, 의료 종사자가 약물 상호작용을 물었을 때 Claude가 얼버무리는 상황에서 Hermes는 정상적으로 답하는 쪽에 가깝다. 기업이 로컬 배포 시 이 부분 때문에 Hermes를 고르는 사례가 꽤 있다고 알려져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;오픈웨이트 + 상업 이용 가능 라이선스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes는 Llama 라이선스를 그대로 따라가는데, 월간 액티브 유저 7억 명 이하 기업은 무료로 상업 이용이 가능하다. 대부분의 스타트업과 중견 기업은 이 제한에 걸리지 않아 사실상 무료다.&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;실무적으로 이것이 무엇을 의미하는가. 금융이나 의료, 법률처럼 데이터를 외부로 내보낼 수 없는 업계에서 &lt;b&gt;온프레미스 배포 카드&lt;/b&gt;를 꺼낼 수 있다는 뜻이다. GPT-4 API로 월 수천만원씩 청구서를 받던 팀이 자체 GPU만 확보하면 API 비용을 0으로 만들 수 있고, 자사 데이터로 재학습시켜 도메인 특화 모델을 만드는 것도 자유롭게 가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hermes 벤치마크 성능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;767&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oc30n/dJMcaipzpUN/ZzPPj95AsEakytGXc0vDck/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oc30n/dJMcaipzpUN/ZzPPj95AsEakytGXc0vDck/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oc30n/dJMcaipzpUN/ZzPPj95AsEakytGXc0vDck/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foc30n%2FdJMcaipzpUN%2FZzPPj95AsEakytGXc0vDck%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;767&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;767&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: projectpro.io&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;숫자는 2026년 4월 기준 Nous Research 공식 발표와 Hugging Face 모델 카드에서 정리했다. 버전이 업데이트되면 달라질 수 있으니 실제 사용 전에는 최신 자료를 확인하는 것이 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Llama, Qwen, GPT-OSS와의 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MMLU&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;GPQA&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;HumanEval&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MATH&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hermes 4 405B&lt;/td&gt;
&lt;td&gt;88.2&lt;/td&gt;
&lt;td&gt;52.8&lt;/td&gt;
&lt;td&gt;89.5&lt;/td&gt;
&lt;td&gt;76.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Llama 3.3 70B&lt;/td&gt;
&lt;td&gt;86.0&lt;/td&gt;
&lt;td&gt;50.5&lt;/td&gt;
&lt;td&gt;85.2&lt;/td&gt;
&lt;td&gt;72.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 2.5 72B&lt;/td&gt;
&lt;td&gt;85.4&lt;/td&gt;
&lt;td&gt;49.1&lt;/td&gt;
&lt;td&gt;86.7&lt;/td&gt;
&lt;td&gt;74.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-OSS 120B&lt;/td&gt;
&lt;td&gt;87.1&lt;/td&gt;
&lt;td&gt;51.3&lt;/td&gt;
&lt;td&gt;88.0&lt;/td&gt;
&lt;td&gt;75.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o (참고)&lt;/td&gt;
&lt;td&gt;88.7&lt;/td&gt;
&lt;td&gt;53.6&lt;/td&gt;
&lt;td&gt;90.2&lt;/td&gt;
&lt;td&gt;76.6&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표를 보면 Hermes 4 405B는 오픈소스 중 최상위권이고, GPT-4o와의 격차도 MMLU 기준 0.5점 수준이다. HumanEval은 89.5까지 올라왔는데, 1년 전만 해도 오픈소스 70B 모델이 이 구간에 들어오지 못했던 점을 감안하면 변화 속도가 빠른 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추론 벤치마크에서 튀는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes 4가 특히 잘하는 영역이 &lt;b&gt;복잡한 reasoning 태스크&lt;/b&gt;다. Nous Research가 학습 데이터에 체인 오브 쏘트(Chain-of-Thought) 데이터를 많이 넣은 덕분이고, DPO(Direct Preference Optimization) 학습 기법도 적극 활용했다.&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;실사용 체감으로 보면, 8B 모델도 간단한 코딩 태스크는 충분히 처리하고, 70B부터는 GPT-3.5급 이상이 나온다. 405B는 GPT-4급이지만 VRAM 250GB 이상이 필요해 개인이 돌리기는 어렵고 사실상 기업용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;직접 Hermes 돌려보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;출처: Tri Pham: UX Designer&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론만 늘어놓으면 지루하니, 직접 돌려보는 법까지 정리한다. 각설하고 제일 쉬운 경로가 &lt;a href=&quot;https://ollama.com/&quot;&gt;Ollama&lt;/a&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ollama로 Hermes 돌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ollama가 설치돼 있다는 가정하에:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777021642025&quot; class=&quot;dockerfile&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# Hermes 3 8B 모델 돌리기 (가장 가벼움)
ollama run hermes3

# 70B 모델 (VRAM 40GB+ 필요)
ollama run hermes3:70b

# 설치만 하고 나중에 쓰기
ollama pull hermes3
&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;VRAM 요구량은 대략 다음과 같다. 8B 양자화 모델은 6~8GB라 RTX 3060급에서도 돌아간다. 70B 양자화는 40~48GB라 RTX 4090 한 장으로는 빠듯해 보통 2장을 묶고, 405B는 250GB 이상이라 A100 여러 장이 필수다. 집에서 처음 써보는 경우라면 &lt;b&gt;8B부터 시작하는 것이 정신 건강에 좋다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;LM Studio에서 GGUF 파일로 돌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GUI가 편한 사람에게는 LM Studio가 답이다. &lt;a href=&quot;https://huggingface.co/NousResearch&quot;&gt;Hugging Face의 NousResearch 페이지&lt;/a&gt;에서 TheBloke나 bartowski가 올린 Hermes GGUF 파일을 검색해 다운받고 LM Studio에 로드하면 끝난다. 양자화 레벨은 &lt;b&gt;Q4_K_M이 용량&amp;middot;성능 밸런스가 제일 좋다&lt;/b&gt;고 느꼈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777021642026&quot; class=&quot;makefile&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;# Hugging Face transformers로 직접 부르기
from transformers import AutoModelForCausalLM, AutoTokenizer

model_name = &quot;NousResearch/Hermes-3-Llama-3.1-8B&quot;
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, device_map=&quot;auto&quot;)

messages = [{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;파이썬으로 피보나치 수열 짜줘&quot;}]
inputs = tokenizer.apply_chat_template(messages, return_tensors=&quot;pt&quot;).to(&quot;cuda&quot;)
output = model.generate(inputs, max_new_tokens=512)
print(tokenizer.decode(output[0]))
&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;이 코드를 그대로 돌리면 로컬에서 Hermes와 대화가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hugging Face Inference API로 불러오기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GPU가 없어도 테스트만 해보고 싶다면 Hugging Face Inference API가 있다. 무료 티어가 있어 간단한 실험은 돈이 들지 않는다. 토큰을 발급받고 HTTP POST를 날리는 구조이고, 공식 문서에 예제가 잘 정리돼 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hermes를 써야 할 때 vs 말아야 할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R1qYL/dJMcaipzpUO/oYFVdo8tVbLkzza1totub1/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R1qYL/dJMcaipzpUO/oYFVdo8tVbLkzza1totub1/img.webp&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R1qYL/dJMcaipzpUO/oYFVdo8tVbLkzza1totub1/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR1qYL%2FdJMcaipzpUO%2FoYFVdo8tVbLkzza1totub1%2Fimg.webp&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; loading=&quot;lazy&quot; width=&quot;1199&quot; height=&quot;686&quot; data-origin-width=&quot;1199&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: bytesrack.com&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인적으로 Hermes 3 70B를 두 달 정도 써보았고, 4의 8B도 돌려보았다. 솔직한 감상만 남긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이럴 때 Hermes를 추천한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회사 데이터를 외부로 내보낼 수 없는 환경이라면 선택지가 사실상 로컬 모델로 좁혀지는데, 이때 Hermes가 좋은 후보다. GPT-4 API 청구서 압박이 있어 GPU에 한 번 투자하고 무제한으로 쓰고 싶은 경우, 자사 도메인 데이터로 파인튜닝해 특화 모델을 만들고 싶은 경우에도 출발점으로 괜찮다.&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;조금 덜 뻔한 시나리오도 있다. 보안 연구나 의학 정보, 법률 자문처럼 상업 모델이 자꾸 얼버무리는 분야에서 Hermes는 &lt;b&gt;검열이 덜한 답&lt;/b&gt;을 내는 편이다. 함수 호출(function calling) 학습이 잘 돼 있어 툴 사용 에이전트를 만들 때 베이스로 삼기에도 무난하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이럴 때는 다른 모델을 써라&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대 케이스도 꽤 많다. 학습 시점 이후의 최신 정보는 당연히 모르기 때문에, 뉴스나 시사 질문은 Perplexity나 ChatGPT 같은 검색 연동 모델이 훨씬 낫다.&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;한국어 특화 작업도 Hermes의 약점이다. 영어 중심으로 튜닝된 모델이라, 한국어 글쓰기나 한국 문화 맥락이 필요하다면 KoAlpaca, EEVE-Korean, HyperCLOVA X 쪽 결과물이 훨씬 자연스럽다. 텍스트 전용이라 이미지 생성&amp;middot;이해가 필요하다면 Llama 3.2 Vision이나 Qwen2-VL로 넘어가야 하고, 엣지 디바이스처럼 초경량 환경에서는 Phi-3 mini나 Gemma 2B가 더 맞는다.&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;가장 현실적인 이유도 빼놓으면 안 된다. 그냥 ChatGPT 월 2만원 내고 쓰는 것이 편하다면 그것이 답이다. Hermes를 돌리려면 GPU 값, 전기세, 운영 리소스가 모두 붙는데 이것이 의외로 싸지 않다. &lt;b&gt;모델만 무료지 인프라는 비싸다&lt;/b&gt;는 점을 반드시 감안해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hermes AI 모델이 아닌 &quot;에르메스&quot;를 찾아온 사람을 위한 짧은 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽다가 &quot;아 나 가방 찾으러 온 건데&quot;라고 생각할 사람을 위해 간단히 정리한다. 프랑스 럭셔리 브랜드 에르메스(Herm&amp;egrave;s)는 AI 모델 Hermes와 아무 상관이 없다. 에르메스는 1837년 설립된 가죽 제품 브랜드이고, 버킨백과 켈리백으로 유명하다. 스펠링은 둘 다 H-e-r-m-e-s이지만, 발음은 프랑스 쪽이 &quot;에르메스&quot;에 가깝고 AI 모델은 그리스 신 이름에서 따와 &quot;허미즈&quot;로 읽는다.&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;혹시 에르메스 브랜드 쪽을 찾던 경우라면 네이버에서 &quot;에르메스&quot;로 검색하는 것이 훨씬 빠르다. 리셀가, 매장 정보, 신상 정보가 거기서 잘 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;요즘 주목받는 Hermes AI 모델, 결론부터 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hermes AI 모델은 Nous Research가 만든 오픈소스 LLM이고, Llama 기반 파인튜닝이 중심인 시리즈다. Hermes 4로 오면서 일부 reasoning 벤치마크에서 GPT-4o와 1점 이내로 붙었고, 오픈웨이트라서 온프레미스 배포가 자유롭다. Ollama 한 줄이면 8B 모델은 집 컴퓨터에서도 돌아가고, 한국어 작업은 KoAlpaca나 EEVE가 더 나을 때가 많으니 용도에 맞춰 고르면 된다. 에르메스 브랜드와는 전혀 관련이 없다는 점은 여기까지 읽었다면 이제 확실할 것이다.&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;로컬 LLM을 처음 써보는 경우라면 Ollama를 깔고 8B부터 돌려 감을 잡는 것이 제일 빠른 길이다. 만족스러우면 70B로 올리고, 진짜 필요하면 405B 클러스터를 고민하면 된다. GPT 월 2만원도 나쁜 선택은 아니니 무조건 로컬이 답이라는 이야기는 당연히 아니다.&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;솔직히 나는 처음에 Hermes 3를 돌려놓고 &quot;음, 이 정도면 GPT 구독을 해지할까&quot; 싶었다가, 한국어로 긴 글을 쓰기 시작하자마자 포기하고 다시 ChatGPT Plus를 갱신했다. &lt;b&gt;어디에 쓸지가 정해져야 의미가 생기는 모델&lt;/b&gt;이다. 그래서 위의 &quot;써야 할 때 vs 말아야 할 때&quot;를 한 번 더 보고 결정하는 것을 권한다. 다음 글에서는 Hermes를 파인튜닝해 한국어 도메인 모델로 키우는 과정을 따로 정리해볼 생각이다.&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>Hermes 4</category>
      <category>Hermes vs GPT</category>
      <category>Hermes 사용법</category>
      <category>Nous Research Hermes</category>
      <category>오픈소스 llm</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/374</guid>
      <comments>https://yscho03.tistory.com/374#entry374comment</comments>
      <pubDate>Fri, 24 Apr 2026 18:08:09 +0900</pubDate>
    </item>
    <item>
      <title>트래픽 Traffic 1000배가 터져도 죽지 않는 서버를 만드는 법</title>
      <link>https://yscho03.tistory.com/373</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 터진 후에 준비하면 이미 늦다. 그러나 현실에서는 대부분 터지고 나서야 허둥지둥 트래픽 폭주 대응에 들어간다. &lt;b&gt;대용량 트래픽 처리는 장비 스펙을 올린다고 해결되지 않는다.&lt;/b&gt; 스타트업에서 일하다 보면 &quot;일단 서버를 2배로 키워봐&quot;라는 말을 가장 많이 듣게 되는데, 이는 대부분 돈만 태우고 근본 해결이 되지 않는다.&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;각설하고, 이 글은 DAU 1만짜리 서비스가 갑자기 1000만을 찍어도 죽지 않는 구조를 어떻게 미리 설계하느냐를 정리한 것이다. 측정&amp;middot;수평 확장&amp;middot;캐시 레이어&amp;middot;비동기 처리&amp;middot;비용, 이렇게 다섯 축으로 끊어서 보면 된다. 대기업 기술 블로그들이 자랑하는 성공담이 아니라, 중소&amp;middot;스타트업에서 실제로 쓸 수 있는 대용량 트래픽 처리 체크리스트 중심으로 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스케일 이전에 측정부터 수행해야 하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;병목을 모르면 스케일 아웃을 해도 소용이 없다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 터졌을 때 사람들이 가장 먼저 하는 행동이 서버 인스턴스를 늘리는 것이다. 그러나 병목이 DB에 있으면 앱 서버를 10대로 늘려도 DB가 먼저 나가떨어진다. 오히려 커넥션 수만 더 늘어나 더 빨리 죽는다. &lt;b&gt;병목 지점(Bottleneck) 측정 없이 수평 확장을 먼저 하는 것은 돈만 태우고 상황을 악화시킨다.&lt;/b&gt;&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;측정이 먼저다. 어디가 느린지 모르면 무엇을 고쳐야 할지도 모른다. 이것이 대용량 트래픽 처리의 가장 첫 번째 원칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;APM과 분산 추적으로 봐야 하는 지표 5개&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;지표&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;허용치&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;넘어가면 위험&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99 응답지연&lt;/td&gt;
&lt;td&gt;500ms 이하&lt;/td&gt;
&lt;td&gt;1초 넘어가면 이탈률 급증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에러율&lt;/td&gt;
&lt;td&gt;0.1% 이하&lt;/td&gt;
&lt;td&gt;1% 넘으면 장애 상황&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;초당 요청수(RPS)&lt;/td&gt;
&lt;td&gt;서버당 1000 내외&lt;/td&gt;
&lt;td&gt;지속적으로 넘으면 스케일 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB 커넥션 사용률&lt;/td&gt;
&lt;td&gt;70% 이하&lt;/td&gt;
&lt;td&gt;90% 넘으면 풀 고갈 직전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC 타임 (JVM)&lt;/td&gt;
&lt;td&gt;전체 5% 이하&lt;/td&gt;
&lt;td&gt;10% 넘으면 힙 튜닝 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;p50, p95가 아닌 p99를 보는 이유는 &lt;b&gt;장애는 꼬리 쪽에서 터지기 때문이다.&lt;/b&gt; 평균 응답이 200ms여도 p99가 3초라면 고객 1%는 지옥을 경험 중인 것이다. 그리고 이 1%가 소셜에 올려 바이럴을 일으킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;관측성 도구의 현실적인 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Datadog이 좋다는 것은 다 알지만 스타트업이 쓰기에는 월 과금이 무겁다. DAU 10만 급이면 월 200~500만 원이 쉽게 나간다. 비용을 절약하려면 &lt;b&gt;Grafana + Prometheus + Loki 조합이 현실적이다.&lt;/b&gt; 셋업은 다소 귀찮지만 요금은 인프라 비용만 들어간다.&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;국내 상황이라면 네이버가 만든 Pinpoint도 괜찮다. 자바 기반이면 에이전트 하나를 붙여 트랜잭션 전체 경로를 볼 수 있다. 다만 Node.js나 Go는 에이전트 지원이 약해 이럴 땐 Grafana 스택으로 간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;스케일 아웃과 스케일 업을 섞어 대용량 트래픽 처리 구조를 짜는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;스케일 아웃 vs 스케일 업, 어떤 상황에서 무엇을 쓰는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스케일 업은 서버 스펙을 올리는 것이다 (예: t3.medium &amp;rarr; m5.xlarge). 스케일 아웃은 같은 스펙의 서버를 여러 대 붙이는 것이다.&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;&lt;b&gt;스케일 업이 유리한 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB처럼 상태(State)가 있는 서비스&lt;/li&gt;
&lt;li&gt;로직이 CPU&amp;middot;메모리 집약적인 경우&lt;/li&gt;
&lt;li&gt;인스턴스 1대 비용이 10대 비용보다 저렴한 경우&lt;/li&gt;
&lt;/ul&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;b&gt;스케일 아웃이 유리한 경우&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상태가 없는 API 서버&lt;/li&gt;
&lt;li&gt;트래픽이 시간대별로 크게 변동하는 경우&lt;/li&gt;
&lt;li&gt;장애 격리가 중요한 경우 (1대가 죽어도 9대는 살아있음)&lt;/li&gt;
&lt;/ul&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;b&gt;&quot;앱 서버는 스케일 아웃, DB는 최대한 스케일 업 후 Read Replica&quot;&lt;/b&gt; 이다. 이것이 정답이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;무상태 서비스로 만들어야 오토스케일링이 먹힌다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세션을 서버 메모리에 들고 있으면 서버를 끄는 순간 로그인이 모두 풀린다. 오토스케일링이 서버를 죽이는 것은 정상 동작이지만, 이때 사용자가 튕기면 서비스 장애가 된다. 세션은 Redis로 빼고 JWT 토큰을 쓰는 식으로 서버를 무상태(Stateless)로 만들어야 한다.&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;파일 업로드도 마찬가지다. 로컬 디스크에 저장하면 안 된다. S3나 오브젝트 스토리지로 빼야 오토스케일링이 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로드밸런서&amp;middot;Nginx&amp;middot;ALB 선택 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS를 쓴다면 ALB(Application Load Balancer)가 기본 정답이다. L7에서 동작하여 경로별 라우팅이 된다. 월 20~30달러에 트래픽당 과금이 붙는다.&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;Nginx를 프론트에 둘지 ALB 하나로 끝낼지는 요구사항에 따라 다르다. Nginx를 쓰면 worker_connections 튜닝으로 단일 인스턴스당 10만 커넥션도 받을 수 있다. 기본값 1024는 트래픽이 터지면 바로 거덜난다. keepalive_timeout, proxy_buffering 같은 디테일도 함께 건드려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;DB가 먼저 죽는다, 캐시 레이어 설계가 핵심이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;638&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/etcG1r/dJMcahYt4mK/IeVoK26uPqMlM56kvCF9H0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/etcG1r/dJMcahYt4mK/IeVoK26uPqMlM56kvCF9H0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/etcG1r/dJMcahYt4mK/IeVoK26uPqMlM56kvCF9H0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FetcG1r%2FdJMcahYt4mK%2FIeVoK26uPqMlM56kvCF9H0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;827&quot; height=&quot;638&quot; data-origin-width=&quot;827&quot; data-origin-height=&quot;638&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: javatechonline.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 적중률 90%를 넘기면 DB 부하가 1/10로 떨어진다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 트래픽 처리에서 가장 먼저 죽는 것은 DB다. 앱 서버는 스케일 아웃으로 간단히 늘리지만, DB는 그것이 어렵다. 그래서 &lt;b&gt;캐시 레이어로 DB 앞을 막는 것이 핵심이다.&lt;/b&gt;&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;캐시 적중률(Hit Rate) 90%라면 DB에 오는 쿼리가 원래의 10%로 줄어든다. 95%를 찍으면 5%로 줄어든다. 이것이 DB 1/10배 스펙으로 같은 트래픽을 받는 비결이다. 초당 1만 쿼리를 받던 DB가 캐시 95% 적중률이라면 초당 500 쿼리만 받는다. 대역폭&amp;middot;비용&amp;middot;복잡도가 전부 10분의 1이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 싱글 vs 클러스터, 어느 시점에 갈아타는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 싱글 인스턴스로도 초당 10만 RPS를 받아낸다. DAU 10만 급에서는 싱글로 충분하다. ElastiCache Redis r6g.large 한 대에 월 15만 원 정도가 나간다.&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;메모리가 32GB를 넘어가기 시작하면 클러스터를 고려한다. 데이터가 한 노드에 전부 들어가지 못하면 샤딩이 필요한 것이다. 다만 클러스터는 운영 복잡도가 확 오르기 때문에 정말 필요해질 때까지 싱글로 버티는 것이 맞다. 쓸데없이 일찍 클러스터를 도입했다가 후회하는 사람이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시 무효화 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 무효화(Cache Invalidation)는 컴퓨터 공학의 2대 난제 중 하나라는 말이 있을 정도로 골치 아프다. 현실적으로 쓰이는 패턴은 세 가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;TTL 방식&lt;/b&gt;: 가장 간단하다. 60초&amp;middot;5분&amp;middot;1시간 같은 식으로 만료시간을 박아두고 DB에서 다시 가져온다. 정확도는 떨어지지만 운영이 편하다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Write-through&lt;/b&gt;: DB에 쓸 때 캐시에도 함께 쓴다. 일관성은 높지만 쓰기 성능이 다소 깎인다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Cache-aside (Lazy loading)&lt;/b&gt;: 캐시 miss가 나면 DB에서 읽어 캐시에 넣는다. 가장 많이 쓰이는 패턴이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL을 무한으로 놓고 잊어버리는 것이 안티패턴이다. 캐시를 먹는 것을 까먹으면 stale 데이터가 몇 주씩 남는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Read Replica를 붙이는 시점과 지연 복제 이슈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 쿼리가 쓰기보다 10배 많으면 Read Replica(읽기 레플리카)를 붙일 시점이다. RDS 기준 단일 클릭으로 만들 수 있다. 애플리케이션 레벨에서 쓰기는 Master, 읽기는 Replica로 라우팅하면 된다.&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;다만 복제 지연(Replication Lag)을 염두에 두어야 한다. 방금 쓴 데이터를 바로 Replica에서 읽으면 나오지 않을 수 있다. 주문 직후 내 주문 목록 조회 같은 시나리오는 Master에서 읽어야 한다. 사용자 경험을 기준으로 &lt;b&gt;&quot;내가 방금 한 행동&quot;은 Master, &quot;남의 데이터&quot; 조회는 Replica로 가는 것이 기본 룰이다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동기 처리를 전부 걷어내고 비동기로 넘기는 포인트&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/U766m/dJMcag6l0yy/pRB53GxGQmK7QWncQNMWo0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/U766m/dJMcag6l0yy/pRB53GxGQmK7QWncQNMWo0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/U766m/dJMcag6l0yy/pRB53GxGQmK7QWncQNMWo0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FU766m%2FdJMcag6l0yy%2FpRB53GxGQmK7QWncQNMWo0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;667&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: blog.bytebytego.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka&amp;middot;RabbitMQ로 급한 것만 동기 처리한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;회원가입 시 환영 메일 발송, 결제 후 알림 푸시, 주문 후 통계 집계 같은 작업을 전부 동기로 처리하면 응답시간이 늘어난다. 사용자는 메일 발송이 완료될 때까지 기다리는 것이 아니다. 그저 &quot;회원가입 완료&quot; 화면만 빨리 뜨면 된다.&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;Kafka나 RabbitMQ 같은 메시지 큐를 앞에 두고, 동기 처리할 것은 API 응답에 필요한 것만 남긴 채 나머지는 전부 큐로 던져버린다. 이렇게 하면 API 응답시간이 50~80% 줄기도 한다. 사용자 체감은 확 빨라지고 서버 부하 분산도 함께 이루어진다.&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;Kafka는 초당 100만 메시지 처리가 가능한 괴물이다. 다만 운영 복잡도가 높다. 스타트업이라면 SQS(AWS)나 RabbitMQ 같은 간단한 큐로 시작하여 성장에 따라 갈아타는 편이 낫다. 처음부터 Kafka를 들이대면 운영 리소스가 감당되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서킷 브레이커&amp;middot;백프레셔로 장애 전파를 차단한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서 한 서비스가 느려지면 그것을 호출하는 모든 서비스가 함께 느려진다. 커넥션 풀이 꽉 차 연쇄적으로 전부 죽는다. 이것이 장애 전파다. &lt;b&gt;서킷 브레이커(Circuit Breaker)는 일정 에러율을 넘으면 해당 서비스 호출을 일시 차단한다.&lt;/b&gt; 차단되는 동안 빠른 실패로 응답하여 전체 장애로 번지는 것을 막는다.&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;Resilience4j, Hystrix 같은 라이브러리가 있으며, 이스티오 같은 서비스 메시를 쓰면 인프라 레벨에서 붙일 수도 있다. 트래픽 1000배를 가정하면 필수다.&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;백프레셔(Backpressure)는 소비자가 감당하지 못할 만큼 빨리 들어오는 요청을 의도적으로 느리게 받는 패턴이다. 큐 길이가 일정 수준을 넘으면 429 에러를 주거나 대기시간을 부여하는 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서버리스(Lambda)로 스파이크를 흡수하는 패턴&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갑작스러운 트래픽 스파이크를 EC2만으로 받으면 스케일링 몇 분 동안 당연히 터진다. AWS Lambda는 이론상 초당 수만 개의 동시 실행이 가능하다. 비동기 처리 일부를 Lambda로 빼놓으면 스파이크 흡수용 버퍼가 된다.&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;이미지 리사이징, 알림 발송, 로그 처리 같은 작업은 Lambda가 제격이다. 다만 DB 커넥션 관리가 까다로우므로 RDS Proxy를 함께 써야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;단계별 임계치 체크리스트 (DAU별)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DAU 구간별로 필요한 대용량 트래픽 처리 수준이 다르다. 무엇을 언제 해야 하는지 정리하면 다음과 같다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;DAU 구간&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;필요 구성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;월 인프라 비용 추정&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1만 미만&lt;/td&gt;
&lt;td&gt;모놀리식 + RDS 단일 + S3&lt;/td&gt;
&lt;td&gt;20~50만원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1만~10만&lt;/td&gt;
&lt;td&gt;캐시(Redis 싱글) + Read Replica + CDN&lt;/td&gt;
&lt;td&gt;80~200만원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10만~100만&lt;/td&gt;
&lt;td&gt;메시지큐 + 오토스케일링 + DB 샤딩 검토&lt;/td&gt;
&lt;td&gt;500~1500만원&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100만 이상&lt;/td&gt;
&lt;td&gt;마이크로서비스 + 글로벌 리전 + 엣지 컴퓨팅&lt;/td&gt;
&lt;td&gt;3000만원 이상&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAU 1만 미만 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식 앱 서버 1~2대와 RDS 단일 인스턴스로 충분하다. 캐시도 필요 없다. 이 구간에서 쿠버네티스를 도입하거나 마이크로서비스로 쪼개는 것은 시간 낭비이자 오버엔지니어링이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAU 1만~10만 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 캐시, Read Replica, CDN 세 가지가 기본이다. CDN을 도입하면 정적 리소스 트래픽이 서버에서 전부 빠지고 CloudFront&amp;middot;Cloudflare가 처리한다. 이 단계에서 이미 서버 부하의 60% 이상이 줄어든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAU 10만~100만 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 큐로 비동기 처리, 오토스케일링 적용, 필요하다면 DB 샤딩을 검토한다. 여기서부터는 관측성(APM)이 없으면 운영이 불가능하다. 장애 원인을 찾는 데 하루가 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DAU 100만 이상 구간&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스 분리, 멀티 리전 배포, 엣지 컴퓨팅까지 들어간다. 이 단계는 전담 SRE(Site Reliability Engineer) 팀이 필요하다. 개발자 몇 명으로는 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;확장하면 비용은 얼마나 더 나가는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;506&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdmpCc/dJMcaiC424S/WrRd19S6jyUVWefPNdQaZ1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdmpCc/dJMcaiC424S/WrRd19S6jyUVWefPNdQaZ1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdmpCc/dJMcaiC424S/WrRd19S6jyUVWefPNdQaZ1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbdmpCc%2FdJMcaiC424S%2FWrRd19S6jyUVWefPNdQaZ1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;900&quot; height=&quot;506&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;506&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: www.techradar.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AWS 기준 월 요금 실측 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중소 서비스 기준 실제로 나가는 비용을 비교하면 다음과 같다. 서울 리전 기준 2025년 말 가격이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;월 비용&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;처리 가능 DAU&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;t3.medium 3대 + RDS t3.small 단일&lt;/td&gt;
&lt;td&gt;약 35만원&lt;/td&gt;
&lt;td&gt;1만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m5.large 4대 + RDS m5.large Multi-AZ + ElastiCache r6g.large&lt;/td&gt;
&lt;td&gt;약 230만원&lt;/td&gt;
&lt;td&gt;10만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;m5.xlarge 8대(ASG) + Aurora Multi-AZ + Redis 클러스터 + ALB + CloudFront&lt;/td&gt;
&lt;td&gt;약 850만원&lt;/td&gt;
&lt;td&gt;100만&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대용량 트래픽 처리 단계로 올라갈 때마다 비용은 2~4배씩 뛰지만, 처리 가능한 트래픽은 10배씩 늘어난다. &lt;b&gt;제대로 설계하면 단위 비용 효율이 좋아진다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CDN을 쓰면 아웃바운드 트래픽 비용이 얼마나 절감되는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS EC2 아웃바운드는 GB당 126원이다. CloudFront는 GB당 100원으로 약간 저렴하며, 캐싱 덕에 오리진(서버) 트래픽이 확 줄어든다. 이미지&amp;middot;CSS&amp;middot;JS 같은 정적 리소스를 CDN에 올리면 실질 비용이 40~60% 절감된다. DAU 10만 서비스에서 월 100만 원씩 아껴지는 경우도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reserved Instance&amp;middot;Savings Plan은 언제 사야 이득인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온디맨드로 6개월 이상 쓸 인스턴스는 무조건 Savings Plan을 산다. 1년 약정에 30~40% 할인, 3년에 50~60% 할인이다. 단, 트래픽 예측이 서지 않으면 섣불리 장기 약정을 걸어서는 안 된다. DAU 변동이 큰 초기에는 온디맨드로 가다가 안정화되면 전환하는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대용량 트래픽 처리 시 피해야 할 안티패턴 모음&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 없이 스케일 아웃을 먼저 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반복하지만 이것이 1등 안티패턴이다. 병목을 모르고 서버만 늘리면 돈만 태우고 풀리지 않는다. 항상 측정이 먼저다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;캐시를 걸어놓고 TTL을 무한으로 둔다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TTL 없는 캐시는 영영 stale 데이터를 반환한다. 상품 가격이 바뀌었는데 캐시는 옛 가격이다. 이렇게 되면 CS가 폭주한다. 짧은 TTL + 명시적 무효화가 기본 원칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB 샤딩을 너무 일찍 한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;샤딩은 운영 복잡도가 확 올라간다. 조인을 못 하고, 트랜잭션이 깨지고, 재분배 지옥이 펼쳐진다. DAU 100만이 되지 않는다면 수직 스케일업 + Read Replica로 버티는 편이 낫다. 대용량 트래픽 처리 능력이 정말 필요해질 때 도입해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;서비스 분리도 하지 않은 채 몰래 쿠버네티스부터 도입한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식인데 쿠버네티스를 붙이면 이득보다 손해가 크다. 단일 바이너리 하나를 띄우는데 마스터 노드 3대를 돌리면 오버헤드다. MSA로 쪼갤 서비스가 없다면 ECS나 그냥 EC2로도 충분하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;대용량 트래픽 처리는 기술 싸움이 아니라 순서 싸움이다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정리하면 순서는 이렇다. 첫째, 측정이 먼저다. APM을 붙이고 p99 응답시간&amp;middot;에러율&amp;middot;DB 커넥션부터 본다. 둘째, 캐시로 DB 앞을 막는다. 적중률 90%를 넘기는 것이 목표다. 셋째, 동기 처리를 걷어내 메시지 큐로 넘긴다. 넷째, 이 모든 것을 한 뒤에 수평 확장에 들어간다.&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;&lt;b&gt;트래픽 1000배는 기술 싸움이 아니라 순서 싸움이다.&lt;/b&gt; 순서가 맞지 않으면 장비를 10배 더 써도 풀리지 않고, 순서가 맞으면 인프라 2~3배로도 버틴다.&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;오늘 당장 할 일은 간단하다. 서비스에 APM이 아직 붙지 않았다면 Grafana+Prometheus라도 붙여 p99 응답시간을 재는 것부터 시작하면 된다. 측정을 시작하면 이미 절반은 온 것이다. 그다음은 Redis 캐시 레이어 설계 가이드와 CDN 도입 후기 글을 찾아 읽어보면 단계적으로 대용량 트래픽 처리 역량이 쌓인다. 지금 터지지 않는 서버가 6개월 뒤에도 터지지 않는다는 보장은 없다. 미리미리 대비하는 것이 최선이다.&lt;/p&gt;</description>
      <category>DevOps</category>
      <category>로드밸런서 구축</category>
      <category>서버 부하 분산</category>
      <category>스케일 아웃 vs 스케일 업</category>
      <category>오토스케일링 설정</category>
      <category>캐시 서버 구조</category>
      <category>트래픽 폭주 대응</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/373</guid>
      <comments>https://yscho03.tistory.com/373#entry373comment</comments>
      <pubDate>Fri, 24 Apr 2026 17:57:41 +0900</pubDate>
    </item>
    <item>
      <title>Codex 코드 검증이 강력한 이유와 실전 사용법</title>
      <link>https://yscho03.tistory.com/372</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Copilot을 쓰다가 &quot;자동완성만으로는 부족하다&quot;는 생각에 Codex로 넘어온 개발자가 많을 것이다. 다만 막상 &quot;Codex가 무엇인가?&quot;라고 물으면 설명이 제각각이다. 2021년에 나왔던 Codex API와 지금의 Codex는 완전히 다른 물건이어서 혼란스러운 것이 당연하다. 이 글에서는 &lt;b&gt;Codex 코드 검증&lt;/b&gt;이 왜 실제로 유효한지 원리부터 풀어내고, Codex CLI 설치&amp;middot;승인 모드&amp;middot;실전 시나리오까지 한 번에 정리한다. 2026년 4월 기준 최신 정보다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1725&quot; data-origin-height=&quot;1344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yYSe7/dJMcaiXlTtv/kacx0E6Ptj2QKb2fJJxEM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yYSe7/dJMcaiXlTtv/kacx0E6Ptj2QKb2fJJxEM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yYSe7/dJMcaiXlTtv/kacx0E6Ptj2QKb2fJJxEM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyYSe7%2FdJMcaiXlTtv%2Fkacx0E6Ptj2QKb2fJJxEM1%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1725&quot; height=&quot;1344&quot; data-origin-width=&quot;1725&quot; data-origin-height=&quot;1344&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: apidog.com&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex란 무엇인가? 용어 정리부터&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex라는 이름이 붙은 것만 해도 현재 네 가지가 된다. 이걸 정리하지 않으면 뒤 내용을 읽을 때 계속 혼동이 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;(구) OpenAI Codex API&lt;/b&gt; &amp;mdash; 2021~2023년 GitHub Copilot 뒤에서 돌던 구형 모델이다. 2023년 3월에 공식 deprecate되었다. 지금은 역사책 속 이야기다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Codex CLI&lt;/b&gt; &amp;mdash; 2025년 4월에 오픈소스로 공개된 터미널 기반 코딩 에이전트다. npm install -g @openai/codex 한 줄로 설치된다. 로컬 샌드박스에서 실제로 코드를 실행하고, 파일을 편집하고, 테스트까지 모두 수행한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Codex 클라우드&lt;/b&gt; &amp;mdash; 2025년 5월에 공개된 ChatGPT 내장 클라우드 에이전트다. Plus/Pro/Team/Enterprise 플랜에서 사용 가능하다. 여러 태스크를 병렬로 실행하고 PR까지 자동으로 생성한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;GPT-5-Codex&lt;/b&gt; &amp;mdash; 2025년에 공개된 Codex 전용 튜닝 모델이다. 일반 GPT-5 위에 코딩 데이터로 추가 학습시킨 버전이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요약하면 &quot;Codex = 현재는 코딩 에이전트 브랜드&quot;라고 볼 수 있다. 이 글에서 다루는 &lt;b&gt;Codex 코드 검증&lt;/b&gt;은 주로 Codex CLI와 GPT-5-Codex 조합을 지칭한다. 과거 Codex API와 혼동하지 말 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex가 코드 검증에 강력한 이유 4가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 유독 Codex가 &quot;코드 검증&quot; 주제에서 자주 언급되는지 분석해봤다. 마케팅 문구가 아니라 구조적인 이유가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행&amp;rarr;관찰&amp;rarr;수정 루프를 스스로 수행한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 가장 크다. 일반 Copilot 같은 자동완성 도구는 &quot;코드를 써주고 끝&quot;이다. 그 코드가 실제로 작동하는지는 개발자가 확인해야 한다. 다만 Codex는 생성한 코드를 &lt;b&gt;스스로 직접 실행하고 결과를 읽는다&lt;/b&gt;. 테스트가 깨지면 그 에러 메시지를 다시 context에 넣어 수정한다. 이 루프가 보통 3~5회 반복되며 &quot;동작하는 코드&quot;로 수렴한다.&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;이것이 &quot;코드 검증&quot;이라 부르는 핵심 메커니즘이다. LLM이 글만 쓰는 것이 아니라 runtime feedback을 받아 스스로 수정한다. 단발성 생성과 폐쇄 루프 검증의 차이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;GPT-5-Codex 전용 튜닝 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 GPT-5에 코드 데이터를 추가 학습시킨 버전이다. 구체적으로는 tool-use 정확도, 리팩토링 안정성, 긴 파일 편집에서 diff 오류율이 낮아졌다. OpenAI 자체 벤치마크에서 리포지토리 수준 태스크 성공률이 10~15%p 높다고 밝혔다. 공식 발표는 &lt;a href=&quot;https://openai.com/index/introducing-codex/&quot;&gt;OpenAI 블로그&lt;/a&gt;에 있다.&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;Codex CLI에서는 기본적으로 이 모델이 적용된다. 설정에서 gpt-5 또는 o4-mini로 변경할 수도 있으나, 코드 작업이라면 거의 항상 gpt-5-codex가 낫다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;샌드박스에서 안전하게 실제 코드 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;실제로 실행해본다&quot;는 말이 다소 위협적으로 들릴 수 있다. rm -rf를 실행하면 어쩌나 하는 걱정이 들 수 있다. 다만 Codex CLI는 OS 수준 샌드박스 위에서 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;macOS에서는&lt;/b&gt; Seatbelt (Apple 샌드박스) 프로파일을 적용한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Linux에서는&lt;/b&gt; Landlock + seccomp로 디렉토리&amp;middot;시스템콜을 제한한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과적으로 현재 작업 디렉토리 밖의 파일은 건드릴 수 없고, 기본적으로 네트워크도 차단된다. &quot;코드 검증을 위해 실행한다&quot;는 말이 이 샌드박스 덕분에 성립한다. 샌드박스가 없다면 그저 위험한 자동 실행기에 불과하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리포지토리 전체를 컨텍스트로 읽는다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, Codex는 단일 파일 스니펫만 보는 것이 아니라 프로젝트 구조 전체를 훑는다. AGENTS.md 파일을 루트에 두면 &quot;이 프로젝트는 pnpm을 사용한다&quot;, &quot;테스트는 vitest로 실행한다&quot; 같은 규칙을 자동으로 읽어 들인다.&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;이 덕분에 &quot;이 함수 하나 고쳐달라&quot;고 해도 다른 파일의 import, 관련 테스트, 타입 정의까지 확인한 뒤 제안한다. 좁은 context로는 놓치는 검증 케이스를 잡아낼 수 있는 이유다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex CLI 설치 및 초기 세팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이론은 이쯤에서 마치고 실제로 써보자. 설치는 5분 안에 끝난다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;설치 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777020402406&quot; class=&quot;cmake&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# npm (Node 22+ 필요)
npm install -g @openai/codex

# 또는 Homebrew (macOS)
brew install codex
&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.js 22 이상이어야 한다. 오래된 LTS를 쓰던 경우 nvm install 22를 먼저 실행하고 설치할 것. Windows는 WSL2를 권장한다. 네이티브 Windows에서도 동작하기는 하지만 샌드박스 지원이 아직 제한적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API 키 연결 / ChatGPT 로그인&lt;/h3&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;&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;API 키 방식&lt;/b&gt; &amp;mdash; 환경변수 OPENAI_API_KEY에 sk-로 시작하는 키를 넣으면 된다. 사용량만큼 과금된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ChatGPT 로그인 방식&lt;/b&gt; &amp;mdash; codex login을 실행하면 브라우저가 열리고 ChatGPT Plus/Pro 계정으로 로그인한다. 이 경우 플랜에 포함된 쿼터 안에서 무료로 사용 가능하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Plus 플랜 이상이라면 2번이 훨씬 이득이다. API 키 방식은 월 20달러 이상 사용할 때만 의미가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 실행 &amp;mdash; codex 명령어로 대화 시작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 끝났다면 프로젝트 루트로 이동해 codex를 실행하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777020402406&quot; class=&quot;jboss-cli&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;cd ~/my-project
codex
&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;대화형 프롬프트가 뜬다. 여기에 &quot;README에 설치 섹션을 추가해달라&quot; 같은 자연어 지시를 넣으면 된다. &lt;b&gt;Codex는 codex 명령을 실행한 디렉토리를 기준으로 전체 컨텍스트를 읽는다&lt;/b&gt;. 따라서 루트에서 실행해야 한다. 엉뚱한 서브폴더에서 실행하면 상위 파일을 보지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;승인 모드 3단계 &amp;mdash; 위임 수준 선택하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codex의 진짜 강점은 &quot;얼마나 자동으로 실행할지&quot;를 단계별로 고를 수 있다는 점이다. 이걸 모르고 쓰면 답답하거나 반대로 무서워진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;suggest 모드 (안전 최우선)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777020402407&quot; class=&quot;ada&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;codex --approval-mode suggest
&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;Codex가 코드 변경을 &lt;b&gt;제안만 한다&lt;/b&gt;. 실제 파일 수정도, 명령어 실행도 전부 사람이 y/n으로 확인해야 한다. 속도는 느리지만 완전히 투명하다. 레거시 프로젝트에서 처음 시도할 때 권장한다. 이 모드로 며칠 써보고 &quot;터무니없는 짓은 하지 않는다&quot;는 확신이 생기면 단계를 올리면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;auto-edit 모드 (균형)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777020402407&quot; class=&quot;routeros&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;codex --approval-mode auto-edit
&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;b&gt;파일 편집은 자동, 쉘 명령 실행은 승인&lt;/b&gt;을 요구한다. 소스 수정은 빠르게 진행되고, 테스트 실행&amp;middot;패키지 설치 같은 것만 한 번씩 확인한다. 일반적인 개발 흐름에서 가장 쓸만한 기본값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;full-auto 모드 (풀 에이전트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777020402407&quot; class=&quot;ada&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;codex --approval-mode full-auto
&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;파일 편집 + 명령 실행 + 커밋까지 전부 자동이다. 중간에 개입하지 않는다. 20~30분짜리 작업을 맡기고 다른 업무를 봐도 된다.&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;다만 이것이 가능한 이유가 앞서 언급한 &lt;b&gt;샌드박스&lt;/b&gt; 덕분이다. 네트워크 차단 + 디렉토리 밖 접근 불가라는 제약 안에서만 풀자동이 안전하게 동작한다. 네트워크를 열려면 --network on 플래그를 명시적으로 지정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실전 팁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로 만든 프로젝트, 토이 프로젝트 &amp;rarr; &lt;b&gt;full-auto&lt;/b&gt;로 맡겨도 무방하다&lt;/li&gt;
&lt;li&gt;회사 프로덕션 코드 &amp;rarr; &lt;b&gt;auto-edit&lt;/b&gt;부터 시작. 신뢰가 쌓이면 full-auto&lt;/li&gt;
&lt;li&gt;오래된 레거시 코드 &amp;rarr; &lt;b&gt;suggest&lt;/b&gt;로 시작해 Codex가 엉뚱한 파일을 건드리는지부터 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex 코드 검증 실전 시나리오 5가지&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/riM9M/dJMcahKYfWf/t0kKBwKKpHe2Wl9Yj7oDO1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/riM9M/dJMcahKYfWf/t0kKBwKKpHe2Wl9Yj7oDO1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/riM9M/dJMcahKYfWf/t0kKBwKKpHe2Wl9Yj7oDO1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FriM9M%2FdJMcahKYfWf%2Ft0kKBwKKpHe2Wl9Yj7oDO1%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;720&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 버그 재현 + 패치를 한 번에 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 자주 쓰는 패턴이다. 이슈 설명 + 재현 스텝을 붙여넣고 &quot;이걸 고쳐달라&quot;고 지시하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777020402407&quot; class=&quot;yaml&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;plaintext&quot;&gt;&lt;code&gt;payload가 null일 때 500 뜸. routes/user.js 들어가서
null 가드 추가하고 테스트도 같이 짜줘.
&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;Codex가 routes/user.js를 읽고, null 시나리오 테스트를 먼저 작성하고 (RED), 구현을 수정하고 (GREEN), 다시 테스트를 실행해 초록 불을 확인하면 마친다. TDD 루프를 스스로 돌리는 것이다. 여기서 &quot;코드 검증&quot;의 본질이 가장 잘 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 레거시 함수 리팩토링 (테스트 커버리지 유지 조건)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래된 300줄짜리 함수를 리팩토링할 때 사용한다. &quot;테스트를 깨지 말고 함수를 분리해달라&quot;고 지시하면, Codex가 현재 테스트를 먼저 전체 실행해 초록 상태를 확인하고, 소규모 변경마다 재실행하면서 분리한다. 중간에 테스트가 깨지면 롤백하고 재시도한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. PR 리뷰용 요약 자동 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;git diff main...HEAD를 붙여넣고 &quot;이 PR을 요약하고 위험 포인트를 잡아달라&quot;고 지시하면 PR description 초안이 바로 나온다. 사람보다 꼼꼼한 경우도 많다. 다만 최종 리뷰는 사람이 하는 것이 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 마이그레이션 (라이브러리 버전 업)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;React 18에서 19로 올려달라, breaking change를 모두 처리해서&quot; 같은 지시도 먹힌다. Codex가 package.json을 수정하고, npm install로 해결 실패 케이스를 확인하고, deprecation 경고를 하나씩 잡아가며 수렴한다. 이것은 Copilot으로는 절대 해낼 수 없는 작업이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 로컬에서 Claude Code와 번갈아 쓰기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;솔직히 말하면 Codex 하나로는 부족할 때도 있다. Claude Code는 긴 reasoning 체인과 설계 토론에 더 강하고, Codex는 tool-use 위주 루프에 더 강하다. 실전에서는 두 가지를 번갈아 사용하는 경우가 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설계 단계&lt;/b&gt; &amp;rarr; Claude Code로 아키텍처 검토&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현/검증 단계&lt;/b&gt; &amp;rarr; Codex CLI로 실행 루프 운용&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PR 리뷰 단계&lt;/b&gt; &amp;rarr; 둘 다 실행해 크로스 체크&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 &quot;Codex 코드 검증&quot; 관점에서 특히 중요하다. Claude는 &quot;이런 버그가 발생할 수 있다&quot;고 추론하고, Codex는 &quot;실제로 실행해보니 문제가 터진다&quot;고 확인한다. 역할이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex CLI vs Claude Code vs Cursor 비교&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 묻는 비교다. 한 번에 정리한다. 2026년 4월 기준이다.&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%;&quot; border=&quot;1&quot; data-ke-style=&quot;style12&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;항목&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Codex CLI&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Claude Code&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Cursor&lt;/b&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;터미널 (로컬)&lt;/td&gt;
&lt;td&gt;IDE(VSCode 포크)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기본 모델&lt;/td&gt;
&lt;td&gt;GPT-5-Codex&lt;/td&gt;
&lt;td&gt;Claude 4.6/4.7 Sonnet/Opus&lt;/td&gt;
&lt;td&gt;선택 (Claude/GPT/Gemini)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;자동 코드 실행&lt;/td&gt;
&lt;td&gt;O (샌드박스)&lt;/td&gt;
&lt;td&gt;O (샌드박스)&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;프로젝트 전체&lt;/td&gt;
&lt;td&gt;열린 파일 + 인덱싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;가격&lt;/td&gt;
&lt;td&gt;ChatGPT Plus에 포함 / API 별도&lt;/td&gt;
&lt;td&gt;Claude Pro에 포함 / API 별도&lt;/td&gt;
&lt;td&gt;월 $20~40 자체 구독&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;강점&lt;/td&gt;
&lt;td&gt;코드 검증&amp;middot;자동 실행 루프&lt;/td&gt;
&lt;td&gt;긴 reasoning&amp;middot;설계&lt;/td&gt;
&lt;td&gt;에디터 내 빠른 수정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;약점&lt;/td&gt;
&lt;td&gt;IDE 통합 약함&lt;/td&gt;
&lt;td&gt;병렬 태스크 적음&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;&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;검증&amp;middot;테스트 자동화 최우선&lt;/b&gt; &amp;rarr; Codex CLI&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설계&amp;middot;리팩토링 품질&lt;/b&gt; &amp;rarr; Claude Code&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IDE 안에서 바로바로&lt;/b&gt; &amp;rarr; Cursor&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Codex 코드 검증 도입 여부, 이렇게 판단한다&lt;/h2&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;&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;Codex 코드 검증&lt;/b&gt;의 핵심은 실행&amp;rarr;관찰&amp;rarr;수정 루프다. 생성만 하는 Copilot류와 근본적으로 다르다.&lt;/li&gt;
&lt;li&gt;샌드박스 + GPT-5-Codex 전용 모델 + 리포지토리 컨텍스트 세 조합으로 &quot;실제로 실행해본 코드&quot;를 내놓는다.&lt;/li&gt;
&lt;li&gt;승인 모드 3단계 덕분에 레거시든 토이든 자기 상황에 맞춰 운용할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언제 쓰면 좋은가. 테스트가 있는 프로젝트, TDD 스타일, CI를 운영하는 환경이다. 이런 조건에서는 Codex의 검증 루프가 제값을 한다.&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;왜일까? 반대로 비추천인 경우는 테스트가 아예 없는 프로젝트다. 검증할 대상이 없어 일반 Copilot과 큰 차이가 없다. 또한 샌드박스가 불편한 네트워크 집약 작업(API 호출이 많은 스크립트)도 다소 번거롭다.&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;각설하고 Codex CLI는 &lt;b&gt;30분이면 설치해 한 사이클을 돌려볼 수 있다&lt;/b&gt;. 위에서 언급한 npm install -g @openai/codex를 실행하고, 자신의 프로젝트 루트에서 codex --approval-mode suggest부터 시작해볼 것을 권한다. 첫 번째 버그 하나만 맡겨봐도 &quot;바로 이것이다&quot;는 감이 온다. &lt;a href=&quot;https://github.com/openai/codex&quot;&gt;Codex CLI GitHub 레포&lt;/a&gt;에 이슈와 최신 업데이트가 올라오니 참고하면 된다.&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;공식 가격&amp;middot;한도는 &lt;a href=&quot;https://platform.openai.com/docs&quot;&gt;OpenAI 개발자 문서&lt;/a&gt;가 정답이다. 이 분야 스펙이 자주 바뀌니 결제 전에 한 번 더 확인할 것.&lt;/p&gt;</description>
      <category>AI LLM</category>
      <category>AI 코딩 어시스턴트 비교</category>
      <category>Codex CLI 설치</category>
      <category>Codex vs Claude Code</category>
      <category>GPT-5 Codex 모델</category>
      <category>OpenAI Codex 사용법</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/372</guid>
      <comments>https://yscho03.tistory.com/372#entry372comment</comments>
      <pubDate>Fri, 24 Apr 2026 17:47:11 +0900</pubDate>
    </item>
    <item>
      <title>SW만 알면 망한다: 개발자가 진짜 알아야 할 CPU&amp;middot;메모리 사양 읽는 법</title>
      <link>https://yscho03.tistory.com/371</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;왜 개발자가 하드웨어 사양까지 알아야 하는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCsytH/dJMcaib3drz/ZVaoDfp0Yl9yAg8L1fJJ31/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCsytH/dJMcaib3drz/ZVaoDfp0Yl9yAg8L1fJJ31/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCsytH/dJMcaib3drz/ZVaoDfp0Yl9yAg8L1fJJ31/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCsytH%2FdJMcaib3drz%2FZVaoDfp0Yl9yAg8L1fJJ31%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;200&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;200&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: pcworld.com&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 메모리 사양 확인 스킬 없이 SW만 파다가 제대로 당한 적이 있다. 같은 프로젝트인데 동료 맥북은 빌드가 4분 걸리고 내 것은 20분이 걸렸다. 코드가 문제인 줄 알고 리팩토링하다가 결국 문제는 램이 8GB라는 사실이었다. 파이썬으로 CSV 한 번 로드하면 스왑을 타고 노트북 팬이 돌아간다. OOM 킬러에게 스크립트가 얻어맞고 끝난다.&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;이런 일을 겪다 보면 느낀다. &lt;b&gt;SW 레이어만 파는 것으로는 한계가 있다&lt;/b&gt;. 어떤 병목은 코드가 아니라 하드웨어에서 나온다. 특히 요즘은 도커, JVM, Node.js 모두 메모리&amp;middot;CPU 리소스를 직접 깎아서 써야 하는데, 본인 머신 스펙도 제대로 읽지 못하면 설정을 감으로 박게 된다.&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;이 글에서는 CPU&amp;middot;메모리 사양 표기를 해독하는 법부터, 운영체제별로 스펙을 뽑는 명령어, 그리고 그 숫자가 실제 개발에 어떻게 연결되는지까지 정리한다. SW만 알던 주니어도 이 글을 다 읽으면 본인 장비 스펙을 보고 &quot;아 내 빌드가 왜 느린지 알겠다&quot; 하는 감을 잡을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CPU 스펙 표기 해독하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mlSah/dJMcacv8jQV/AgYvzw38dC9k2sSbg24LN0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mlSah/dJMcacv8jQV/AgYvzw38dC9k2sSbg24LN0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mlSah/dJMcacv8jQV/AgYvzw38dC9k2sSbg24LN0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmlSah%2FdJMcacv8jQV%2FAgYvzw38dC9k2sSbg24LN0%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;2000&quot; height=&quot;1125&quot; data-origin-width=&quot;2000&quot; data-origin-height=&quot;1125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: www.techradar.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;i7-13700K에서 각 글자가 무슨 뜻인가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 모델명을 보면 알파벳+숫자 조합인데 여기에는 규칙이 있다. 인텔 기준으로 i7-13700K를 뜯어보면 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;i7&lt;/b&gt;: 브랜드 라인업이다. i3 / i5 / i7 / i9 순으로 올라간다. i9가 가장 비싸다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;13&lt;/b&gt;: 세대다. 2025년 기준으로 13세대, 14세대, 그리고 2024년부터 등장한 Core Ultra가 최신이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;700&lt;/b&gt;: SKU 번호다. 같은 세대 안에서 높을수록 상위 모델이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;K&lt;/b&gt;: 접미사다. K는 오버클럭 가능, F는 내장 그래픽 없음, HX는 노트북용 고성능, U는 저전력 노트북용이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMD Ryzen도 비슷하다. Ryzen 7 7800X3D를 뜯어보면 Ryzen 라인업(3/5/7/9), 세대(7000번대), SKU(800), 접미사(X3D는 3D V-Cache가 달린 게이밍 특화)로 구성된다.&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;정확한 모델별 스펙은 &lt;a href=&quot;https://ark.intel.com/&quot;&gt;Intel ARK&lt;/a&gt;나 &lt;a href=&quot;https://www.amd.com/en/products/specifications/processors.html&quot;&gt;AMD 공식 프로세서 사양 페이지&lt;/a&gt;에서 바로 조회된다. 스티커 문구에 속지 말고 공식 페이지에서 코어 수, 캐시 크기, TDP를 확인하는 습관을 들이는 것이 좋다.&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;노트북을 살 때 H / HX는 성능, U / P는 저전력이라고 보면 된다. 개발자 입장에서 HX가 달린 노트북이라면 빌드를 돌릴 때 체감이 확실히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코어&amp;middot;스레드&amp;middot;클럭 숫자의 개발자용 해석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스펙표에서 &quot;6코어 12스레드 / 베이스 3.4GHz / 부스트 5.0GHz / TDP 125W&quot; 같은 표기를 보면 정말 중요한 것은 다음과 같다.&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;&lt;b&gt;코어&amp;middot;스레드&lt;/b&gt;: 6코어 12스레드라면 하이퍼스레딩(AMD는 SMT) 덕분에 OS에서 논리 CPU 12개로 보인다. 이것이 개발자에게 왜 중요할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;make -j12, cargo build --jobs 12, Gradle 병렬 빌드 스레드 수를 이 기준으로 박는다&lt;/li&gt;
&lt;li&gt;Node.js 클러스터 모드로 워커를 몇 개 띄울지 판단한다&lt;/li&gt;
&lt;li&gt;로컬에서 Kafka + DB + 앱 + 프론트를 모두 띄우는 경우 컨텍스트 스위칭 비용을 감안한다&lt;/li&gt;
&lt;/ul&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;b&gt;베이스 vs 부스트 클럭&lt;/b&gt;: 베이스는 지속 성능의 하한선, 부스트는 짧게 끌어올릴 때의 최대치다. 빌드처럼 수 분~수십 분 돌아가는 작업은 부스트 클럭이 아니라 지속 가능한 베이스 클럭 근처에서 돌아간다. 발열로 쓰로틀이 걸리면 베이스도 지키지 못할 때가 있다.&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;&lt;b&gt;TDP&lt;/b&gt;: 설계상 열 배출량이다. 다만 실제 소비전력과 비슷하다고 보면 된다. 노트북 CPU TDP가 15W와 45W라면 빌드 시간 체감이 2배 차이가 난다. 얇은 노트북의 쿨링 시스템이 TDP를 빼지 못할 때는 고급 CPU도 쓰로틀링이 걸린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;L1/L2/L3 캐시가 왜 중요한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시는 CPU가 램에 접근하기 전에 거치는 초고속 임시 저장소다. L1 &amp;gt; L2 &amp;gt; L3 순으로 크기가 커지고 속도는 느려진다. 일반적인 개발에서는 신경 쓰지 않아도 되지만, 대용량 배열 루프나 해시맵을 많이 돌리는 코드를 짤 때는 체감된다.&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;예를 들어 2D 배열을 순회할 때 arr[i][j] 순서가 arr[j][i] 순서보다 몇 배 빠른 것은 캐시 라인 때문이다. 이런 점을 알아두면 코드를 짤 때 &quot;어 이거 캐시 미스가 왜 이렇게 많지?&quot; 하는 감이 잡힌다.&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;게이밍용 X3D 접미사 CPU가 개발 워크로드에서도 각광받는 이유는 &lt;b&gt;L3 캐시가 96MB로 증가되어 있기 때문이다&lt;/b&gt;. DB 쿼리나 컴파일 같은 캐시 민감 워크로드에서는 체감 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메모리(RAM) 스펙 표기 해독하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bloKdY/dJMcacv8jQ0/HxLV3dojJWAUl7RRXkGlpk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bloKdY/dJMcacv8jQ0/HxLV3dojJWAUl7RRXkGlpk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bloKdY/dJMcacv8jQ0/HxLV3dojJWAUl7RRXkGlpk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbloKdY%2FdJMcacv8jQ0%2FHxLV3dojJWAUl7RRXkGlpk%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;1440&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;1440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: www.tomshardware.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DDR5-5600 CL36 같은 숫자 뜻&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;램을 사러 가면 DDR5-5600 CL36 32GB (16GB x 2) 같은 표기가 나온다. 뜯어보면 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;DDR5&lt;/b&gt;: 세대다. DDR3 &amp;rarr; DDR4 &amp;rarr; DDR5 순으로 발전한다. 세대가 바뀌면 메인보드&amp;middot;CPU도 같이 바꿔야 한다. 섞어서 쓰지 못한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;5600&lt;/b&gt;: 데이터 전송률(MT/s)이다. 숫자가 높을수록 빠르다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CL36&lt;/b&gt;: CAS 레이턴시다. 명령을 내린 뒤 데이터가 나오기까지 몇 클럭을 대기하는지 나타낸다. &lt;b&gt;낮을수록 좋다&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;32GB (16GB x 2)&lt;/b&gt;: 총 용량과 모듈 구성이다. 듀얼채널을 쓰려면 2개를 꽂아야 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 지연시간은 (CL / 클럭) &amp;times; 2000 공식으로 나노초 단위 계산이 가능하다. DDR5-5600 CL36이면 약 12.8ns, DDR5-6000 CL30이면 약 10ns다. 다만 일반 개발에서 이 차이를 체감하기는 어렵다. 그보다 &lt;b&gt;용량이 부족해서 스왑을 타는 순간 성능이 100배 느려진다&lt;/b&gt;. 램은 속도보다 용량이 먼저다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;듀얼채널 / ECC가 개발 환경에 주는 차이&lt;/h3&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;b&gt;듀얼채널은 대역폭이 그대로 2배가 된다&lt;/b&gt;. 32GB 하나를 꽂는 것보다 16GB 두 개를 꽂는 편이 성능상 무조건 낫다. 노트북이나 PC를 살 때 같은 용량인데 구성이 다르다면 듀얼채널 지원 쪽을 골라야 한다.&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;ECC(Error-Correcting Code) 메모리는 비트 오류를 자동으로 교정하는 서버용 램이다. 워크스테이션/서버 개발자라면 챙겨야 한다. 일반 PC에서는 지원되지 않는다. 24시간 돌아가는 DB 서버나 ML 학습을 돌릴 때 비트 플립이 한 번 발생하면 학습이 통째로 망가지는 경우가 있어 서버에는 필수다.&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;애플 실리콘(M1/M2/M3/M4)은 유니파이드 메모리 구조라서 램을 CPU/GPU가 공유한다. 16GB짜리 M3 맥북이 일반 16GB 윈도우 노트북과 체감이 다른 이유다. 그래픽/ML 작업을 돌리면 GPU도 이 램을 끌어다 쓴다. 맥에서는 램이 최소 16GB 이상 권장되며, ML을 할 것이라면 32GB 이상으로 가는 편이 안전하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;OS별로 내 컴퓨터 사양 확인하는 법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YFXwn/dJMcacv8jQY/8H00o3kop1HRyldkkEgT91/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YFXwn/dJMcacv8jQY/8H00o3kop1HRyldkkEgT91/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YFXwn/dJMcacv8jQY/8H00o3kop1HRyldkkEgT91/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYFXwn%2FdJMcacv8jQY%2F8H00o3kop1HRyldkkEgT91%2Fimg.jpg&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; loading=&quot;lazy&quot; width=&quot;638&quot; height=&quot;479&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;윈도우: msinfo32, wmic, 작업관리자&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법은 Windows + R을 눌러서 msinfo32를 치는 것이다. 시스템 정보 창이 뜨는데 CPU 모델명, 램 용량, BIOS 버전까지 모두 나온다.&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;커맨드로 뽑으려면 파워셸에서 다음과 같이 실행한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777010974415&quot; class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;powershell&quot;&gt;&lt;code&gt;# CPU 정보
Get-CimInstance Win32_Processor | Select-Object Name, NumberOfCores, NumberOfLogicalProcessors, MaxClockSpeed

# 메모리 정보 (개별 모듈)
Get-CimInstance Win32_PhysicalMemory | Select-Object Manufacturer, Capacity, Speed, ConfiguredClockSpeed
&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;작업관리자(Ctrl+Shift+Esc) &amp;rarr; 성능 탭 &amp;rarr; 메모리를 누르면 현재 장착된 램의 속도, 슬롯 사용량, 폼팩터까지 보여준다. 듀얼채널로 제대로 잡혔는지 여기서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;맥: system_profiler, sysctl, About This Mac&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 빠른 방법은 왼쪽 위 애플 메뉴 &amp;rarr; 이 Mac에 관하여로 가는 것이다. CPU 모델, 램 용량, 칩 종류까지 한눈에 볼 수 있다.&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;터미널에서 자세히 보려면 다음 명령을 사용한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777010974415&quot; class=&quot;properties&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# CPU 상세
sysctl -n machdep.cpu.brand_string
sysctl hw.ncpu hw.physicalcpu hw.logicalcpu

# 메모리
sysctl hw.memsize

# 전체 하드웨어 리포트
system_profiler SPHardwareDataType
&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;애플 실리콘은 hw.ncpu가 성능 코어(P코어) + 효율 코어(E코어)를 합친 값이라서 실제 병렬 성능은 P코어 수 기준으로 봐야 한다. M3 Pro라면 P코어 6개, E코어 6개인 식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;리눅스: lscpu, /proc/cpuinfo, free, dmidecode&lt;/h3&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;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777010974415&quot; class=&quot;vala&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# CPU 요약
lscpu

# CPU 상세 (코어별)
cat /proc/cpuinfo

# 메모리 사용량
free -h

# 메모리 모듈 상세 (루트 권한 필요)
sudo dmidecode -t memory

# CPU 코어 개수만 빠르게
nproc
&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;lscpu 출력에서 주목할 줄은 &quot;CPU(s)&quot;, &quot;Thread(s) per core&quot;, &quot;Core(s) per socket&quot;, &quot;Model name&quot;, &quot;CPU MHz&quot;, &quot;L3 cache&quot; 정도다. dmidecode는 메모리 속도, 제조사, 채널 슬롯 구성까지 알려주는데 루트 권한이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커/WSL에서 호스트 스펙 제대로 보는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;도커 컨테이너 안에서 lscpu나 nproc을 돌리면 &lt;b&gt;호스트 CPU가 그대로 보인다&lt;/b&gt;. 다만 이것이 함정이다. --cpus=2로 제한을 걸었어도 컨테이너 안에서는 호스트 전체 코어가 보여서 애플리케이션이 엉뚱한 스레드 풀 크기를 잡는다.&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;자바 11 이상은 컨테이너를 인식해서 cgroup 기반으로 알아서 줄여주지만, 그 이전 버전이나 Node.js는 수동으로 UV_THREADPOOL_SIZE를 박아야 한다. 리소스 제한 옵션의 구체적 동작은 &lt;a href=&quot;https://docs.docker.com/config/containers/resource_constraints/&quot;&gt;도커 공식 리소스 제약 문서&lt;/a&gt;에 정리되어 있으니 한 번 읽어두면 헷갈릴 일이 줄어든다. 실제 할당된 CPU를 확인하려면 다음과 같이 실행한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777010974415&quot; class=&quot;awk&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너에 할당된 CPU 코어 수 (cgroup v2 기준)
cat /sys/fs/cgroup/cpu.max

# 메모리 제한 확인
cat /sys/fs/cgroup/memory.max
&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;WSL2도 마찬가지다. 기본적으로 윈도우 호스트 전체 리소스의 50%만 할당되는데, .wslconfig 파일에서 memory=8GB, processors=4 같은 식으로 명시해주는 편이 안정적이다. 그렇지 않으면 윈도우 메모리가 갑자기 폭발할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이 숫자가 개발에 실제로 어떻게 연결되는가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;421&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU8xP4/dJMcacXactZ/oDD0O35SnpKQzxdFGy00qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU8xP4/dJMcacXactZ/oDD0O35SnpKQzxdFGy00qk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU8xP4/dJMcacXactZ/oDD0O35SnpKQzxdFGy00qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU8xP4%2FdJMcacXactZ%2FoDD0O35SnpKQzxdFGy00qk%2Fimg.png&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; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;421&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;421&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&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;출처: docs.redhat.com&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JVM 힙 / Node.js 메모리 설정 감 잡기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본인 머신의 램이 16GB인데 자바 서비스 3개, DB, 도커를 이것저것 띄우면서 JVM 힙을 -Xmx8g로 박으면 무조건 터진다. 보통의 룰은 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JVM -Xmx: &lt;b&gt;물리 램의 50~70%&lt;/b&gt; 선에서 박는다. 16GB라면 힙은 8~10GB 이내&lt;/li&gt;
&lt;li&gt;-Xms를 -Xmx와 같게 박으면 재할당 오버헤드가 줄어 프로덕션에서는 권장된다&lt;/li&gt;
&lt;li&gt;Node.js --max-old-space-size=4096는 V8 힙 4GB 제한이다. 기본값(1.5GB)으로 돌리면 큰 빌드에서 쉽게 터진다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;램 용량을 모르면 이런 값을 모두 감으로 박게 되지만, 알면 &quot;아 내 노트북이 32GB니까 힙 16GB까지는 안전하겠네&quot; 하는 계산이 선다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 --cpus, --memory 옵션 현실적으로 박는 법&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬 개발에서 도커 컴포즈로 서비스 5개를 띄우는데 리소스 제한을 걸지 않으면 한 컨테이너가 CPU를 다 잡아먹는다. 본인 머신이 12스레드라면 다음과 같이 설정한다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777010974416&quot; class=&quot;less&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;yaml&quot;&gt;&lt;code&gt;services:
  backend:
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 2G
&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;합쳐서 호스트 CPU 수를 넘기지 않는 것이 원칙이다. 램도 마찬가지다. 32GB 머신에서 도커 데스크톱에 16GB를 할당하고, 그 안에서 컨테이너들의 합이 16GB를 넘어가면 스왑을 탄다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;빌드 병렬도 최적화&lt;/h3&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 -j, cargo build -j, Gradle --parallel, Turbo concurrency는 모두 CPU 스레드 수에 맞춰서 설정한다. 일반적으로는 다음과 같다:&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 집약 빌드(C++/Rust): 스레드 수와 같게 설정&lt;/li&gt;
&lt;li&gt;I/O가 많은 빌드(JS, 파이썬 패키지): 스레드 수의 1.5배까지 쓸 만하다&lt;/li&gt;
&lt;li&gt;램이 부족하면 오히려 빌드가 느려진다. -j를 너무 올리면 스왑을 타서 역효과&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;6코어 12스레드 머신에서 make -j16을 박는다고 빨라지지 않는다. 오히려 컨텍스트 스위칭 비용으로 느려질 때가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;벤치마크 점수 활용하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장비를 비교할 때 스펙표만 보면 헷갈린다. 세대를 건너뛰면 같은 i7이라도 성능이 2배 차이가 난다. 이럴 때는 cpubenchmark.net이나 Geekbench 점수를 보는 편이 가장 빠르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&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://www.cpubenchmark.net/&quot;&gt;&lt;b&gt;cpubenchmark.net&lt;/b&gt;&lt;/a&gt;: 싱글/멀티 스레드 점수, 가격 대비 성능 비교에 좋다&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://browser.geekbench.com/&quot;&gt;&lt;b&gt;Geekbench Browser&lt;/b&gt;&lt;/a&gt;: 싱글코어 점수로 컴파일 같은 단일 스레드 작업의 감을 잡기 좋다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 i7-13700K의 멀티 점수는 약 47,000, M3 Pro 12코어는 약 25,000이다. 멀티에서는 인텔 데스크톱이 압도적이지만, 싱글 스레드나 전력 효율로 가면 애플이 유리하다. 개발 워크로드가 무엇이냐에 따라 선택이 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;개발자 관점에서 CPU 메모리 사양 확인할 때 체크리스트&lt;/h2&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;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CPU 모델명 뜯어 읽기&lt;/b&gt;: 브랜드/세대/SKU/접미사 규칙을 알면 같은 i7이라도 성능을 대략 가늠할 수 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;코어&amp;middot;스레드 수&lt;/b&gt;: 빌드 병렬도, JVM GC 스레드, 도커 --cpus를 설정할 때의 기준점이다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;베이스 클럭 + TDP&lt;/b&gt;: 지속 성능 판단의 근거다. 부스트 클럭 스펙을 보고 혹해서는 안 된다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;램은 용량 &amp;gt; 속도&lt;/b&gt;: CL36이 CL40보다 좋기는 하지만, 용량이 부족해서 스왑을 타는 순간 모두 의미가 없다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;듀얼채널 꼭 챙기기&lt;/b&gt;: 같은 용량이어도 싱글채널이면 체감 성능이 반토막 난다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OS별 확인 명령어&lt;/b&gt;: msinfo32(윈도우), system_profiler(맥), lscpu / free -h(리눅스)가 있다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컨테이너 함정&lt;/b&gt;: 도커&amp;middot;WSL2에서 보이는 값이 실제 할당량과 다르다. cgroup을 직접 확인해야 한다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;벤치마크로 교차 검증&lt;/b&gt;: cpubenchmark / Geekbench 점수로 실제 성능의 감을 잡는다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 CPU 메모리 사양 확인은 단순히 내 컴퓨터에 무엇이 박혔는지 보는 것이 아니다. &lt;b&gt;본인의 개발 워크로드와 장비 스펙을 연결하는 감각이다&lt;/b&gt;. 이 감각이 있으면 같은 하드웨어에서도 JVM 힙을 제대로 박고, 도커 리소스를 합리적으로 나누고, 빌드 병렬도를 최적화해서 체감 속도를 몇 배 뽑아낼 수 있다.&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;SW만 파는 개발자가 많은데, 장비 스펙을 한 번 제대로 읽을 줄 알면 장애 디버깅 시간이 정말 확 줄어든다. 다음에는 SSD / NVMe 스펙과 네트워크 대역폭을 읽는 법도 정리할 예정이다. 이 두 가지도 모르고 대충 넘기면 뒤통수를 맞는 것은 똑같다.&lt;/p&gt;</description>
      <category>하드웨어</category>
      <category>CPU 사양 보는 법</category>
      <category>CPU 클럭 코어 스레드 차이</category>
      <category>RAM 클럭 CL 레이턴시</category>
      <category>개발자 노트북 사양 선택</category>
      <category>내 컴퓨터 사양 확인</category>
      <author>DevNinja</author>
      <guid isPermaLink="true">https://yscho03.tistory.com/371</guid>
      <comments>https://yscho03.tistory.com/371#entry371comment</comments>
      <pubDate>Fri, 24 Apr 2026 15:09:54 +0900</pubDate>
    </item>
  </channel>
</rss>