<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.1.1">Jekyll</generator><link href="https://youngwon.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://youngwon.io/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-02-12T21:53:49+09:00</updated><id>https://youngwon.io/feed.xml</id><title type="html">Youngwon Seo</title><subtitle>Youngwon Seo</subtitle><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><entry><title type="html">2025 회고 (WIP)</title><link href="https://youngwon.io/review-2025/" rel="alternate" type="text/html" title="2025 회고 (WIP)" /><published>2026-01-01T12:00:00+09:00</published><updated>2026-01-01T12:00:00+09:00</updated><id>https://youngwon.io/review-2025</id><content type="html" xml:base="https://youngwon.io/review-2025/"><![CDATA[<h2 id="서론">서론</h2>

<p>어느덧 2025년이 지나가고 회고를 작성합니다. 좋은일, 안좋은일 여러가지 일이 있었지만 더 나은 한해를 보내기 위해 지나간 시간에 대해 회고해 봅니다.</p>

<h2 id="회사">회사</h2>

<p>이직을 하고 어느덧 1년이 지났습니다. <a href="/review-2024/#이직">작년회고</a>에 작성한것과 같이 새로운 도메인/문화 그리고 새로운 사람들과의 일을 경험하기 위해 이직을 했습니다. 결론 부터 말하면 70%정도는 성공했습니다. 성공의 이유를 작성해보면 다음과 같습니다.</p>

<ol>
  <li>이전 회사의 동료들과 성향이 다른분들과 일을 해봄</li>
  <li>새로운 도메인을 경험함, 앞으로 어떤일을 할지 알수는 없지만 도움이 될거라고 생각됨</li>
  <li>새로운 업무 문화, 조직 문화를 경험함</li>
</ol>

<p>성공에 대한 가장 큰 이유는 새로운 환경을 경험했기때문입니다. 이미 자리를 지키고 있는 사람들이 있는 새로운 환경에 적응하기 위해 노력이 필요했습니다. 이 과정에서 많은 것들을 배울수 있었습니다. 지금와서 생각해보니 정말 적절한 시기에 이직을 했다고 생각이 됩니다.</p>

<p>그러면 실패의 30%는 어떤부분일까를 서술해보면 위와 같은 경험적에서는 많은 것들을 얻었지만 기술적인 부분에서는 크게 성장하지 못했기 때문입니다. 물론 이것은 회사의 몫이 아니라 개인의 몫이고 환경에 적응하느라 스스로 지친게 컸습니다.</p>

<h2 id="개인-프로젝트">개인 프로젝트</h2>

<p>올해 두 개의 모바일 애플리케이션을 출시 했습니다.</p>

<h2 id="잊혀진-개인-목표들">잊혀진 개인 목표들</h2>

<p><a href="/review-2024/#2025년-계획">작년회고</a>에서 여러가지 목표를 새웠습니다. 하지만 이룬건 없었습니다.</p>

<ul>
  <li>블로그: 실패</li>
  <li>개인 프로젝트 &amp; 강의 제작: 개인프로젝트로 앱을 2개 배포함, 그 외 없음</li>
  <li>다독: 반정도 성공</li>
  <li>그 외: 한식조리기능사 자격증이나 일본어 공부 실패</li>
</ul>

<h2 id="그-외">그 외</h2>

<p>작년에 제가 가장 성공적으로 했다고 말할수 있는건 제가 좋아하면 Brad Mehldau 내한 공연을 갔던것입니다. 제 평생 잊지 못할 공연이었고 행복한 시간을 보냈었습니다. 브레드(Brad)라는 이름때문에 한국에서는 ‘빵형’이라고도 불리는데 팀의 베이시스트로 온 Christian McBride는 제 어린시절의 우상이나 다름 없는 사람이었습니다. 아래는 내한때 왔던 셋리스트와 유사한 같은 투어에 속한 공연 영상입니다.</p>

<iframe width="100%" height="400" src="https://www.youtube.com/embed/0YsVMfIMG9c" title="Brad Mehldau, Christian McBride, Marcus Gilmore In Concert at The Gilmore" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>

<h2 id="2026년-계획">2026년 계획</h2>

<h3 id="글-작성">글 작성</h3>

<p>가장 큰 목표는 글을 많이 작성하는 것입니다. 기술적인 글이든, 개인적인 의견에 대한 글이든 머리속에 있는 여러가지 생각들을 글로 표현하고 싶습니다.</p>

<h3 id="wip">WIP</h3>

<h2 id="읽은-책">읽은 책</h2>

<ol>
  <li>한 번 배워서 평생 써먹는 박곰희 투자법, 박곰희 지음, 인플루엔셜</li>
  <li>우리는 어떻게 주식으로 18,000% 수익을 얻었나, 길 모랄레스, 크리스 케쳐 지음, 박준형 옮김, 이레 미디어</li>
  <li>오브젝트, 조영호 지음, 위키북스</li>
  <li>무엇이 1등 팀을 만드는가?, 애디 오스마니 지음, LINE SQE 팀 옮김, 한빛미디어</li>
  <li>기본기가 탄탄한 자바 개발자 (제2판), 벤저민 J. 에번스,제이슨 클라크,마르테인 페르뷔르흐 지음, 김성원 옮김, 제이펍</li>
  <li>실시간 데이터 파이프라인 아키텍처, 엔드류 살티스 지음, 최원영 옮김, 비제이퍼블릭</li>
  <li>스트리밍 시스템, 타일러 아키다우, 슬라바 체르냑, 루벤 락스 지음, 이덕기, 전웅 옮김, 에이콘</li>
  <li>대표 전략으로 입문하는 미국 주식 퀀트 투자, 이용환 지음, 프리렉</li>
  <li>재즈가 나에게 말하는 것들, 최은창 저자(글), 노르웨이숲</li>
  <li>그렇게 나는 다시 삶을 선택했다, 최지은 저자(글), 유선사</li>
  <li>…</li>
</ol>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="회고" /><summary type="html"><![CDATA[서론]]></summary></entry><entry><title type="html">Netflix Spikes Traffic</title><link href="https://youngwon.io/netflix-spikes-traffic/" rel="alternate" type="text/html" title="Netflix Spikes Traffic" /><published>2025-12-29T00:00:00+09:00</published><updated>2025-12-29T00:00:00+09:00</updated><id>https://youngwon.io/netflix-spikes-traffic</id><content type="html" xml:base="https://youngwon.io/netflix-spikes-traffic/"><![CDATA[<p>https://www.youtube.com/watch?app=desktop&amp;v=TkFyZyxFRBM</p>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Bucket4j Rate Limiting 분석</title><link href="https://youngwon.io/bucket4j/" rel="alternate" type="text/html" title="Bucket4j Rate Limiting 분석" /><published>2025-10-09T12:00:00+09:00</published><updated>2025-10-09T12:00:00+09:00</updated><id>https://youngwon.io/bucket4j</id><content type="html" xml:base="https://youngwon.io/bucket4j/"><![CDATA[<p>대규모 마이크로서비스 환경에서 안정적인 Rate Limiting을 구현하기 위해 <a href="https://bucket4j.com">Bucket4j</a>를 도입했습니다. 단순한 토큰 버킷을 넘어 분산 시스템에서의 복잡한 동시성 제어와 성능 최적화까지, 프로덕션 환경에서 겪은 실전 경험을 공유합니다.</p>

<h2 id="1-bucket4j-기반-token-bucket-구현-분석">1. Bucket4j 기반 Token Bucket 구현 분석</h2>

<p><a href="https://bucket4j.com/8.15.0/toc.html#what-is-bucket4j">Bucket4j</a>는 <a href="https://github.com/vladimir-bukhtoyarov">Vladimir Bukhtoyarov</a>이 개발한 Java용 Rate Limiting 라이브러리로, <a href="https://en.wikipedia.org/wiki/Token_bucket">Token Bucket 알고리즘</a>을 <strong>절대적 정밀도(absolutely non-compromise precision)</strong>와 <strong>lock-free 멀티스레딩</strong>으로 구현했습니다.</p>

<h3 id="11-bucket4j의-token-bucket-구현-특징">1.1 Bucket4j의 Token Bucket 구현 특징</h3>

<p><a href="https://bucket4j.com/8.15.0/toc.html#bucket4j-features">공식 문서</a>에 따르면 Bucket4j는 다음과 같은 고유한 특징을 제공합니다:</p>

<ul>
  <li><strong>정수 연산 정밀도</strong>: 부동소수점 연산 오차 없이 정확한 토큰 계산</li>
  <li><strong>멀티 대역폭 지원</strong>: 하나의 버킷에서 여러 제한 정책 동시 적용</li>
  <li><strong>Garbage Collection 최적화</strong>: 원시 타입 사용으로 메모리 할당 최소화</li>
</ul>

<h4 id="111-bucket4j의-핵심-api-구조">1.1.1 Bucket4j의 핵심 API 구조</h4>

<p><a href="https://bucket4j.com/8.15.0/toc.html#quick-start-examples">Bucket4j Builder API</a>는 직관적인 fluent interface를 제공합니다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 기본 로컬 버킷 생성</span>
<span class="nc">Bucket</span> <span class="n">bucket</span> <span class="o">=</span> <span class="nc">Bucket</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="n">limit</span> <span class="o">-&gt;</span> <span class="n">limit</span><span class="o">.</span><span class="na">capacity</span><span class="o">(</span><span class="mi">50</span><span class="o">).</span><span class="na">refillGreedy</span><span class="o">(</span><span class="mi">10</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">)))</span>
    <span class="o">.</span><span class="na">build</span><span class="o">();</span>

<span class="c1">// 다중 대역폭 버킷</span>
<span class="nc">Bucket</span> <span class="n">bucket</span> <span class="o">=</span> <span class="nc">Bucket</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="n">limit</span> <span class="o">-&gt;</span> <span class="n">limit</span><span class="o">.</span><span class="na">capacity</span><span class="o">(</span><span class="mi">50</span><span class="o">).</span><span class="na">refillGreedy</span><span class="o">(</span><span class="mi">10</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">)))</span>  <span class="c1">// 초당 제한</span>
    <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="n">limit</span> <span class="o">-&gt;</span> <span class="n">limit</span><span class="o">.</span><span class="na">capacity</span><span class="o">(</span><span class="mi">1000</span><span class="o">).</span><span class="na">refillGreedy</span><span class="o">(</span><span class="mi">100</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">)))</span> <span class="c1">// 분당 제한</span>
    <span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<h4 id="112-수학적-모델">1.1.2 수학적 모델</h4>

<p>토큰 충전 공식:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>new_tokens = min(capacity, current_tokens + (current_time - last_refill_time) × refill_rate)
</code></pre></div></div>

<p>요청 허용 조건:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>request_allowed = (required_tokens ≤ current_tokens)
</code></pre></div></div>

<h4 id="113-알고리즘의-특징">1.1.3 알고리즘의 특징</h4>

<ol>
  <li><strong>Burst Handling</strong>: 짧은 시간 내 트래픽 급증을 버킷 용량 범위에서 허용</li>
  <li><strong>Smooth Rate Control</strong>: 장기적으로는 설정된 속도로 트래픽 제한</li>
  <li><strong>Memory Efficiency</strong>: 각 사용자당 단일 상태만 유지</li>
</ol>

<h3 id="12-bucket4j-refill-전략-분석">1.2 Bucket4j Refill 전략 분석</h3>

<p><a href="https://bucket4j.com/8.15.0/toc.html#refill-types">공식 문서</a>에서 제공하는 세 가지 토큰 리필 전략:</p>

<h4 id="121-greedy-refill-기본값">1.2.1 Greedy Refill (기본값)</h4>

<p>가장 일반적인 방식으로, 토큰이 소비되는 순간 즉시 리필을 계산합니다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Bucket</span> <span class="n">bucket</span> <span class="o">=</span> <span class="nc">Bucket</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="n">limit</span> <span class="o">-&gt;</span> <span class="n">limit</span>
        <span class="o">.</span><span class="na">capacity</span><span class="o">(</span><span class="mi">100</span><span class="o">)</span>
        <span class="o">.</span><span class="na">refillGreedy</span><span class="o">(</span><span class="mi">10</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>  <span class="c1">// 초당 10개 토큰 즉시 충전</span>
    <span class="o">)</span>
    <span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<h4 id="122-intervally-refill">1.2.2 Intervally Refill</h4>

<p>고정된 시간 간격으로 배치 단위로 토큰을 충전합니다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Bucket</span> <span class="n">bucket</span> <span class="o">=</span> <span class="nc">Bucket</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
    <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="n">limit</span> <span class="o">-&gt;</span> <span class="n">limit</span>
        <span class="o">.</span><span class="na">capacity</span><span class="o">(</span><span class="mi">100</span><span class="o">)</span>
        <span class="o">.</span><span class="na">refillIntervally</span><span class="o">(</span><span class="mi">10</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofSeconds</span><span class="o">(</span><span class="mi">1</span><span class="o">))</span>  <span class="c1">// 정확히 1초마다 10개 토큰 충전</span>
    <span class="o">)</span>
    <span class="o">.</span><span class="na">build</span><span class="o">();</span>
</code></pre></div></div>

<h4 id="123-bucket4j의-고급-기능들">1.2.3 Bucket4j의 고급 기능들</h4>

<p><a href="https://bucket4j.com/8.15.0/toc.html#advanced-features">Bucket4j 고급 API</a>:</p>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>설명</th>
      <th>사용 사례</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Verbose API</strong></td>
      <td>상세한 진단 정보 제공</td>
      <td>디버깅, 모니터링</td>
    </tr>
    <tr>
      <td><strong>Listener API</strong></td>
      <td>토큰 소비 이벤트 감지</td>
      <td>로깅, 메트릭 수집</td>
    </tr>
    <tr>
      <td><strong>Async API</strong></td>
      <td>비동기 토큰 소비</td>
      <td>논블로킹 애플리케이션</td>
    </tr>
    <tr>
      <td><strong>Configuration Replacement</strong></td>
      <td>런타임 설정 변경</td>
      <td>동적 Rate Limit 조정</td>
    </tr>
  </tbody>
</table>

<h3 id="13-bucket4j의-내부-구현-세부사항">1.3 Bucket4j의 내부 구현 세부사항</h3>

<p><a href="https://github.com/bucket4j/bucket4j">GitHub 저장소</a>에서 확인할 수 있는 Bucket4j의 고성능 구현 특징:</p>

<h4 id="131-정수-연산-기반-정밀도">1.3.1 정수 연산 기반 정밀도</h4>

<p>Bucket4j는 “<strong>absolutely non-compromise precision</strong>“을 위해 모든 계산을 64비트 정수로 수행:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// io.github.bucket4j.local.LocalBucketState 클래스에서</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">refill</span><span class="o">(</span><span class="kt">int</span> <span class="n">bandwidthIndex</span><span class="o">,</span> <span class="nc">Bandwidth</span> <span class="n">bandwidth</span><span class="o">,</span> <span class="kt">long</span> <span class="n">currentTimeNanos</span><span class="o">)</span> <span class="o">{</span>
    <span class="c1">// 나노초 정밀도로 시간 계산</span>
    <span class="kt">long</span> <span class="n">durationSinceLastRefillNanos</span> <span class="o">=</span> <span class="n">currentTimeNanos</span> <span class="o">-</span> <span class="n">previousRefillNanos</span><span class="o">;</span>
    
    <span class="c1">// 정수 연산으로 토큰 수 계산 (부동소수점 오차 없음)</span>
    <span class="kt">long</span> <span class="n">tokensToAdd</span> <span class="o">=</span> <span class="nc">Math</span><span class="o">.</span><span class="na">multiplyExact</span><span class="o">(</span><span class="n">durationSinceLastRefillNanos</span><span class="o">,</span> <span class="n">refillTokens</span><span class="o">)</span> <span class="o">/</span> <span class="n">refillPeriodNanos</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="132-lock-free-멀티스레딩-최적화">1.3.2 Lock-free 멀티스레딩 최적화</h4>

<p>로컬 버킷의 경우 CAS(Compare-And-Swap) 연산을 사용하여 lock-free 동시성 제어:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 원자적 상태 업데이트</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">tryConsumeAsMuchAsPossible</span><span class="o">(</span><span class="kt">long</span> <span class="n">limit</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">while</span> <span class="o">(</span><span class="kc">true</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">currentState</span> <span class="o">=</span> <span class="n">stateRef</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
        <span class="c1">// ... 상태 계산 로직</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">stateRef</span><span class="o">.</span><span class="na">compareAndSet</span><span class="o">(</span><span class="n">currentState</span><span class="o">,</span> <span class="n">newState</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="c1">// CAS 실패 시 재시도</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="133-garbage-collection-최적화">1.3.3 Garbage Collection 최적화</h4>

<p><a href="https://github.com/bucket4j/bucket4j#why-bucket4j">README</a>에서 명시된 바와 같이 원시 타입 사용으로 GC 오버헤드 최소화:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 객체 할당 대신 long 배열로 상태 관리</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="nc">AtomicLongArray</span> <span class="n">stateArray</span><span class="o">;</span>

<span class="c1">// 메모리 사용량: 8 bytes × 상태 필드 수</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">BANDWIDTH_SIZE</span> <span class="o">=</span> <span class="mi">4</span><span class="o">;</span> <span class="c1">// capacity, availableTokens, lastRefillTime, roundingError</span>
</code></pre></div></div>

<h2 id="2-bucket4j-핵심-구현-분석">2. Bucket4j 핵심 구현 분석</h2>

<h3 id="21-토큰-소비-메커니즘-token-consumption">2.1 토큰 소비 메커니즘 (Token Consumption)</h3>

<p>토큰 소비 요청 시 <a href="https://github.com/bucket4j/bucket4j">Bucket4j</a>는 원자적 연산을 통해 다음 단계를 수행합니다:</p>
<ol>
  <li>나노초 정밀도로 현재 시간 측정</li>
  <li>마지막 리필 시점부터 경과된 시간을 기반으로 토큰 리필</li>
  <li>요청된 토큰 수와 사용 가능한 토큰 수 비교</li>
  <li>토큰 소비 또는 거부 결정</li>
</ol>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">boolean</span> <span class="nf">tryConsumeImpl</span><span class="o">(</span><span class="kt">long</span> <span class="n">tokensToConsume</span><span class="o">)</span> <span class="o">{</span>
  <span class="kt">long</span> <span class="n">currentTimeNanos</span> <span class="o">=</span> <span class="n">timeMeter</span><span class="o">.</span><span class="na">currentTimeNanos</span><span class="o">();</span>
  <span class="n">lock</span><span class="o">.</span><span class="na">lock</span><span class="o">();</span>
  <span class="k">try</span> <span class="o">{</span>
      <span class="n">state</span><span class="o">.</span><span class="na">refillAllBandwidth</span><span class="o">(</span><span class="n">currentTimeNanos</span><span class="o">);</span>
      <span class="kt">long</span> <span class="n">availableToConsume</span> <span class="o">=</span> <span class="n">state</span><span class="o">.</span><span class="na">getAvailableTokens</span><span class="o">();</span>
      <span class="k">if</span> <span class="o">(</span><span class="n">tokensToConsume</span> <span class="o">&gt;</span> <span class="n">availableToConsume</span><span class="o">)</span> <span class="o">{</span>
          <span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
      <span class="o">}</span>
      <span class="n">state</span><span class="o">.</span><span class="na">consume</span><span class="o">(</span><span class="n">tokensToConsume</span><span class="o">);</span>
      <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
  <span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
      <span class="n">lock</span><span class="o">.</span><span class="na">unlock</span><span class="o">();</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="22-토큰-리필-전략-refill-strategies">2.2 토큰 리필 전략 (Refill Strategies)</h3>

<p><a href="https://bucket4j.com/8.15.0/toc.html#refill-types">Bucket4j</a>는 다양한 비즈니스 요구사항에 맞는 토큰 리필 전략을 제공합니다:</p>

<ul>
  <li><strong>Greedy Refill</strong>: 즉시 토큰 충전 (기본값)</li>
  <li><strong>Intervally Refill</strong>: 고정 간격으로 배치 충전</li>
  <li><strong>Smooth Refill</strong>: 균등한 속도로 연속 충전</li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">refillAllBandwidth</span><span class="o">(</span><span class="kt">long</span> <span class="n">currentTimeNanos</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Bandwidth</span><span class="o">[]</span> <span class="n">bandwidths</span> <span class="o">=</span> <span class="n">configuration</span><span class="o">.</span><span class="na">getBandwidths</span><span class="o">();</span>
    <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">bandwidths</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
        <span class="n">refill</span><span class="o">(</span><span class="n">i</span><span class="o">,</span> <span class="n">bandwidths</span><span class="o">[</span><span class="n">i</span><span class="o">],</span> <span class="n">currentTimeNanos</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">refill</span><span class="o">(</span><span class="kt">int</span> <span class="n">bandwidthIndex</span><span class="o">,</span> <span class="nc">Bandwidth</span> <span class="n">bandwidth</span><span class="o">,</span> <span class="kt">long</span> <span class="n">currentTimeNanos</span><span class="o">)</span> <span class="o">{</span>
    
    <span class="kt">long</span> <span class="n">previousRefillNanos</span> <span class="o">=</span> <span class="n">getLastRefillTimeNanos</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">);</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">currentTimeNanos</span> <span class="o">&lt;=</span> <span class="n">previousRefillNanos</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="c1">// 주기적 리필 방식인경우 </span>
    <span class="k">if</span> <span class="o">(</span><span class="n">bandwidth</span><span class="o">.</span><span class="na">isRefillIntervally</span><span class="o">())</span> <span class="o">{</span>
        <span class="c1">// currentTimeNanos에 리필이 되어야하는 마지막 나노초까지의 타임스탬프를 지정</span>
        <span class="kt">long</span> <span class="n">incompleteIntervalCorrection</span> <span class="o">=</span> <span class="o">(</span><span class="n">currentTimeNanos</span> <span class="o">-</span> <span class="n">previousRefillNanos</span><span class="o">)</span> <span class="o">%</span> <span class="n">bandwidth</span><span class="o">.</span><span class="na">getRefillPeriodNanos</span><span class="o">();</span>
        <span class="n">currentTimeNanos</span> <span class="o">-=</span> <span class="n">incompleteIntervalCorrection</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">currentTimeNanos</span> <span class="o">&lt;=</span> <span class="n">previousRefillNanos</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
        <span class="n">setLastRefillTimeNanos</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">currentTimeNanos</span><span class="o">);</span>
    <span class="o">}</span>

    <span class="kd">final</span> <span class="kt">long</span> <span class="n">capacity</span> <span class="o">=</span> <span class="n">bandwidth</span><span class="o">.</span><span class="na">getCapacity</span><span class="o">();</span>
    <span class="kd">final</span> <span class="kt">long</span> <span class="n">refillPeriodNanos</span> <span class="o">=</span> <span class="n">bandwidth</span><span class="o">.</span><span class="na">getRefillPeriodNanos</span><span class="o">();</span>
    <span class="kd">final</span> <span class="kt">long</span> <span class="n">refillTokens</span> <span class="o">=</span> <span class="n">bandwidth</span><span class="o">.</span><span class="na">getRefillTokens</span><span class="o">();</span>
    <span class="kd">final</span> <span class="kt">long</span> <span class="n">currentSize</span> <span class="o">=</span> <span class="n">getCurrentSize</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">);</span>

    <span class="c1">// 버킷이 가득찼는지 검사</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">currentSize</span> <span class="o">&gt;=</span> <span class="n">capacity</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// can come here if forceAddTokens has been used</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kt">long</span> <span class="n">durationSinceLastRefillNanos</span> <span class="o">=</span> <span class="n">currentTimeNanos</span> <span class="o">-</span> <span class="n">previousRefillNanos</span><span class="o">;</span>
    <span class="kt">long</span> <span class="n">newSize</span> <span class="o">=</span> <span class="n">currentSize</span><span class="o">;</span>

    <span class="c1">// 마지막 리필시간으로부터 현재 시간까지의 기간이 리필되어야하는 기간보다 큰경우, 즉 리필이 수행되어야하는경우</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">durationSinceLastRefillNanos</span> <span class="o">&gt;</span> <span class="n">refillPeriodNanos</span><span class="o">)</span> <span class="o">{</span>
        <span class="kt">long</span> <span class="n">elapsedPeriods</span> <span class="o">=</span> <span class="n">durationSinceLastRefillNanos</span> <span class="o">/</span> <span class="n">refillPeriodNanos</span><span class="o">;</span>
        <span class="kt">long</span> <span class="n">calculatedRefill</span> <span class="o">=</span> <span class="n">elapsedPeriods</span> <span class="o">*</span> <span class="n">refillTokens</span><span class="o">;</span>
        <span class="c1">// 기존 버킷의 토큰수 + 리필되어야하는 토큰수</span>
        <span class="n">newSize</span> <span class="o">+=</span> <span class="n">calculatedRefill</span><span class="o">;</span>
        <span class="c1">// 리필후 토큰사이즈가 버킷의 토큰 용량보다 큰경우 용량 사이즈로 리필하고 종료</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">newSize</span> <span class="o">&gt;</span> <span class="n">capacity</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">resetBandwidth</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">capacity</span><span class="o">);</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="c1">// 리필후 토큰사이즈가 현재 사이즈보다 작은경우 오버플로가 발생한것으로 판단하고 버킷 용량 사이즈로 리필하고 종료</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">newSize</span> <span class="o">&lt;</span> <span class="n">currentSize</span><span class="o">)</span> <span class="o">{</span>
            <span class="c1">// arithmetic overflow happens. This mean that tokens reached Long.MAX_VALUE tokens.</span>
            <span class="c1">// just reset bandwidth state</span>
            <span class="n">resetBandwidth</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">capacity</span><span class="o">);</span>
            <span class="k">return</span><span class="o">;</span>
        <span class="o">}</span>
        <span class="n">durationSinceLastRefillNanos</span> <span class="o">%=</span> <span class="n">refillPeriodNanos</span><span class="o">;</span>
    <span class="o">}</span>


    <span class="kt">long</span> <span class="n">roundingError</span> <span class="o">=</span> <span class="n">getRoundingError</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">);</span>
    <span class="kt">long</span> <span class="n">dividedWithoutError</span> <span class="o">=</span> <span class="n">multiplyExactOrReturnMaxValue</span><span class="o">(</span><span class="n">refillTokens</span><span class="o">,</span> <span class="n">durationSinceLastRefillNanos</span><span class="o">);</span>
    <span class="kt">long</span> <span class="n">divided</span> <span class="o">=</span> <span class="n">dividedWithoutError</span> <span class="o">+</span> <span class="n">roundingError</span><span class="o">;</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">divided</span> <span class="o">&lt;</span> <span class="mi">0</span> <span class="o">||</span> <span class="n">dividedWithoutError</span> <span class="o">==</span> <span class="nc">Long</span><span class="o">.</span><span class="na">MAX_VALUE</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// arithmetic overflow happens.</span>
        <span class="c1">// there is no sense to stay in integer arithmetic when having deal with so big numbers</span>
        <span class="kt">long</span> <span class="n">calculatedRefill</span> <span class="o">=</span> <span class="o">(</span><span class="kt">long</span><span class="o">)</span> <span class="o">((</span><span class="kt">double</span><span class="o">)</span> <span class="n">durationSinceLastRefillNanos</span> <span class="o">/</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">refillPeriodNanos</span> <span class="o">*</span> <span class="o">(</span><span class="kt">double</span><span class="o">)</span> <span class="n">refillTokens</span><span class="o">);</span>
        <span class="n">newSize</span> <span class="o">+=</span> <span class="n">calculatedRefill</span><span class="o">;</span>
        <span class="n">roundingError</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
    <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
        <span class="c1">// 리필되어야하는 토큰수</span>
        <span class="kt">long</span> <span class="n">calculatedRefill</span> <span class="o">=</span> <span class="n">divided</span> <span class="o">/</span> <span class="n">refillPeriodNanos</span><span class="o">;</span>
        <span class="k">if</span> <span class="o">(</span><span class="n">calculatedRefill</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">roundingError</span> <span class="o">=</span> <span class="n">divided</span><span class="o">;</span>
        <span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
            <span class="n">newSize</span> <span class="o">+=</span> <span class="n">calculatedRefill</span><span class="o">;</span>
            <span class="n">roundingError</span> <span class="o">=</span> <span class="n">divided</span> <span class="o">%</span> <span class="n">refillPeriodNanos</span><span class="o">;</span>
        <span class="o">}</span>
    <span class="o">}</span>

    <span class="k">if</span> <span class="o">(</span><span class="n">newSize</span> <span class="o">&gt;=</span> <span class="n">capacity</span><span class="o">)</span> <span class="o">{</span>
        <span class="n">resetBandwidth</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">capacity</span><span class="o">);</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>
    <span class="k">if</span> <span class="o">(</span><span class="n">newSize</span> <span class="o">&lt;</span> <span class="n">currentSize</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// arithmetic overflow happens. This mean that bucket reached Long.MAX_VALUE tokens.</span>
        <span class="c1">// just reset bandwidth state</span>
        <span class="n">resetBandwidth</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">capacity</span><span class="o">);</span>
        <span class="k">return</span><span class="o">;</span>
    <span class="o">}</span>
    
    <span class="c1">// 새로계산된 토큰수로 버킷 토큰수 설정</span>
    <span class="n">setCurrentSize</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">newSize</span><span class="o">);</span>
    <span class="n">setRoundingError</span><span class="o">(</span><span class="n">bandwidthIndex</span><span class="o">,</span> <span class="n">roundingError</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>

<h2 id="3-분산-환경에서의-bucket4j-아키텍처">3. 분산 환경에서의 Bucket4j 아키텍처</h2>

<h3 id="31-분산-캐시-통합-전략">3.1 분산 캐시 통합 전략</h3>

<p>대규모 마이크로서비스 환경에서 <a href="https://bucket4j.com/8.15.0/toc.html#distributed-bucket4j">Bucket4j</a>는 단일 인스턴스의 메모리 기반 제한을 넘어 <a href="https://redis.io">Redis</a>, <a href="https://hazelcast.com">Hazelcast</a>, <a href="https://coherence.community">Coherence</a> 등의 분산 캐시와 통합하여 글로벌 Rate Limiting을 구현합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Configuration</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">DistributedRateLimitConfig</span> <span class="o">{</span>
    
    <span class="nd">@Bean</span>
    <span class="kd">public</span> <span class="nc">ProxyManager</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="nf">bucketProxyManager</span><span class="o">(</span><span class="nc">RedissonClient</span> <span class="n">redissonClient</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="nc">Bucket4j</span><span class="o">.</span><span class="na">extension</span><span class="o">(</span><span class="nc">RedissonProxyManager</span><span class="o">.</span><span class="na">class</span><span class="o">)</span>
            <span class="o">.</span><span class="na">builder</span><span class="o">()</span>
            <span class="o">.</span><span class="na">withExpirationAfterWriteStrategy</span><span class="o">(</span><span class="nc">ExpirationAfterWriteStrategy</span><span class="o">.</span><span class="na">basedOnTimeForRefillingBucketUpToMax</span><span class="o">(</span>
                <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">10</span><span class="o">)))</span> <span class="c1">// TTL 최적화</span>
            <span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="n">redissonClient</span><span class="o">);</span>
    <span class="o">}</span>
    
    <span class="nd">@Component</span>
    <span class="kd">public</span> <span class="kd">class</span> <span class="nc">DistributedRateLimiter</span> <span class="o">{</span>
        <span class="kd">private</span> <span class="kd">final</span> <span class="nc">ProxyManager</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">&gt;</span> <span class="n">buckets</span><span class="o">;</span>
        
        <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAllowed</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="kt">long</span> <span class="n">tokens</span><span class="o">)</span> <span class="o">{</span>
            <span class="nc">Bucket</span> <span class="n">bucket</span> <span class="o">=</span> <span class="n">buckets</span><span class="o">.</span><span class="na">builder</span><span class="o">()</span>
                <span class="o">.</span><span class="na">addLimit</span><span class="o">(</span><span class="nc">Bandwidth</span><span class="o">.</span><span class="na">classic</span><span class="o">(</span><span class="mi">100</span><span class="o">,</span> <span class="nc">Refill</span><span class="o">.</span><span class="na">intervally</span><span class="o">(</span><span class="mi">100</span><span class="o">,</span> <span class="nc">Duration</span><span class="o">.</span><span class="na">ofMinutes</span><span class="o">(</span><span class="mi">1</span><span class="o">))))</span>
                <span class="o">.</span><span class="na">build</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="o">()</span> <span class="o">-&gt;</span> <span class="n">createNewBucket</span><span class="o">());</span>
                
            <span class="k">return</span> <span class="n">bucket</span><span class="o">.</span><span class="na">tryConsume</span><span class="o">(</span><span class="n">tokens</span><span class="o">);</span>
        <span class="o">}</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="32-분산-동기화-메커니즘">3.2 분산 동기화 메커니즘</h3>

<p>분산 환경에서 rate limit 상태 동기화는 복잡한 동시성 제어 문제를 야기합니다. <a href="https://bucket4j.com/8.15.0/toc.html#synchronization-strategies">Bucket4j의 분산 구현</a>은 다음과 같은 전략을 사용합니다:</p>

<h4 id="321-compare-and-swap-cas-기반-낙관적-잠금">3.2.1 Compare-and-Swap (CAS) 기반 낙관적 잠금</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">RedisBasedBucketState</span> <span class="o">{</span>
    
    <span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">updateStateWithCAS</span><span class="o">(</span><span class="nc">String</span> <span class="n">key</span><span class="o">,</span> <span class="nc">BucketState</span> <span class="n">expectedState</span><span class="o">,</span> <span class="nc">BucketState</span> <span class="n">newState</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">String</span> <span class="n">script</span> <span class="o">=</span> <span class="s">"""
            local current = redis.call('GET', KEYS[1])
            if current == ARGV[1] then
                redis.call('SET', KEYS[1], ARGV[2])
                return 1
            else
                return 0
            end
            """</span><span class="o">;</span>
            
        <span class="nc">Long</span> <span class="n">result</span> <span class="o">=</span> <span class="n">jedis</span><span class="o">.</span><span class="na">eval</span><span class="o">(</span><span class="n">script</span><span class="o">,</span> 
            <span class="nc">Collections</span><span class="o">.</span><span class="na">singletonList</span><span class="o">(</span><span class="n">key</span><span class="o">),</span>
            <span class="nc">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">serialize</span><span class="o">(</span><span class="n">expectedState</span><span class="o">),</span> <span class="n">serialize</span><span class="o">(</span><span class="n">newState</span><span class="o">)));</span>
            
        <span class="k">return</span> <span class="n">result</span> <span class="o">==</span> <span class="mi">1</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="322-수학적-정확성을-위한-시간-동기화">3.2.2 수학적 정확성을 위한 시간 동기화</h4>

<p>분산 환경에서 가장 중요한 것은 시간 동기화입니다. 각 노드의 시계 차이 δ가 있을 때, refill 계산의 정확도는 다음과 같이 영향받습니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>실제 토큰 수 = min(capacity, current_tokens + (time_diff - δ) × refill_rate)
</code></pre></div></div>

<p>네트워크 지연 λ와 시계 차이 δ를 고려한 최악의 경우 오차는:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>최대 오차 = (λ + 2δ) × refill_rate
</code></pre></div></div>

<h3 id="33-분산-처리-성능-분석">3.3 분산 처리 성능 분석</h3>

<h4 id="331-네트워크-오버헤드-계산">3.3.1 네트워크 오버헤드 계산</h4>

<p>분산 Rate Limiting의 성능은 네트워크 RTT(Round Trip Time)에 크게 의존합니다. <a href="https://redis.io/docs/management/optimization/">Redis의 성능 특성</a>과 <a href="https://redis.io/docs/manual/clients-guide/">네트워크 지연 최적화</a>를 고려해야 합니다.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">PerformanceMetrics</span> <span class="o">{</span>
    
    <span class="c1">// 단일 요청의 총 레이턴시</span>
    <span class="c1">// Total Latency = Network RTT + Redis Processing + Serialization</span>
    <span class="kd">public</span> <span class="kt">double</span> <span class="nf">calculateTotalLatency</span><span class="o">(</span><span class="kt">double</span> <span class="n">networkRtt</span><span class="o">,</span> <span class="kt">double</span> <span class="n">redisProcessing</span><span class="o">,</span> <span class="kt">double</span> <span class="n">serialization</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">networkRtt</span> <span class="o">+</span> <span class="n">redisProcessing</span> <span class="o">+</span> <span class="n">serialization</span><span class="o">;</span>
    <span class="o">}</span>
    
    <span class="c1">// 초당 처리 가능한 최대 요청 수</span>
    <span class="kd">public</span> <span class="kt">double</span> <span class="nf">maxThroughput</span><span class="o">(</span><span class="kt">double</span> <span class="n">totalLatency</span><span class="o">,</span> <span class="kt">int</span> <span class="n">connectionPoolSize</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">connectionPoolSize</span> <span class="o">/</span> <span class="n">totalLatency</span><span class="o">;</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>일반적인 환경에서의 성능 벤치마크:</p>
<ul>
  <li>Local Cache: ~100,000 ops/sec</li>
  <li>Redis (same DC): ~10,000 ops/sec</li>
  <li>Redis (cross-region): ~1,000 ops/sec</li>
</ul>

<h4 id="332-메모리-사용량-최적화">3.3.2 메모리 사용량 최적화</h4>

<p>분산 환경에서 각 bucket의 메모리 사용량을 최적화하는 것이 중요합니다:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">OptimizedBucketState</span> <span class="o">{</span>
    <span class="c1">// 기본 상태: 96 bytes (8개 long 필드)</span>
    <span class="kd">private</span> <span class="kt">long</span><span class="o">[]</span> <span class="n">bandwidthStates</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">long</span><span class="o">[</span><span class="mi">8</span><span class="o">];</span>
    
    <span class="c1">// 압축된 상태: ~30% 메모리 절약</span>
    <span class="kd">public</span> <span class="kt">byte</span><span class="o">[]</span> <span class="nf">compressState</span><span class="o">()</span> <span class="o">{</span>
        <span class="nc">ByteBuffer</span> <span class="n">buffer</span> <span class="o">=</span> <span class="nc">ByteBuffer</span><span class="o">.</span><span class="na">allocate</span><span class="o">(</span><span class="mi">64</span><span class="o">);</span>
        <span class="k">for</span> <span class="o">(</span><span class="kt">long</span> <span class="n">state</span> <span class="o">:</span> <span class="n">bandwidthStates</span><span class="o">)</span> <span class="o">{</span>
            <span class="n">buffer</span><span class="o">.</span><span class="na">putLong</span><span class="o">(</span><span class="n">state</span><span class="o">);</span>
        <span class="o">}</span>
        <span class="k">return</span> <span class="nf">compress</span><span class="o">(</span><span class="n">buffer</span><span class="o">.</span><span class="na">array</span><span class="o">());</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="34-실전-분산-환경-설계-고려사항">3.4 실전 분산 환경 설계 고려사항</h3>

<h4 id="341-hot-partition-문제-해결">3.4.1 Hot Partition 문제 해결</h4>

<p>특정 사용자나 API에 트래픽이 집중될 때 발생하는 <a href="https://redis.io/docs/management/optimization/memory-optimization/">hot partition 문제</a>를 해결하기 위한 <a href="https://en.wikipedia.org/wiki/Consistent_hashing">Consistent Hashing</a> 기반 샤딩 전략:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ShardedRateLimiter</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">SHARD_COUNT</span> <span class="o">=</span> <span class="mi">10</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAllowed</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="kt">long</span> <span class="n">tokens</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 사용자별로 여러 샤드에 분산하여 부하 분산</span>
        <span class="nc">List</span><span class="o">&lt;</span><span class="nc">CompletableFuture</span><span class="o">&lt;</span><span class="nc">Boolean</span><span class="o">&gt;&gt;</span> <span class="n">futures</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ArrayList</span><span class="o">&lt;&gt;();</span>
        <span class="kt">long</span> <span class="n">tokensPerShard</span> <span class="o">=</span> <span class="n">tokens</span> <span class="o">/</span> <span class="no">SHARD_COUNT</span><span class="o">;</span>
        <span class="kt">long</span> <span class="n">remainingTokens</span> <span class="o">=</span> <span class="n">tokens</span> <span class="o">%</span> <span class="no">SHARD_COUNT</span><span class="o">;</span>
        
        <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="no">SHARD_COUNT</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
            <span class="nc">String</span> <span class="n">shardKey</span> <span class="o">=</span> <span class="n">userId</span> <span class="o">+</span> <span class="s">":shard:"</span> <span class="o">+</span> <span class="n">i</span><span class="o">;</span>
            <span class="kt">long</span> <span class="n">shardTokens</span> <span class="o">=</span> <span class="n">tokensPerShard</span> <span class="o">+</span> <span class="o">(</span><span class="n">i</span> <span class="o">&lt;</span> <span class="n">remainingTokens</span> <span class="o">?</span> <span class="mi">1</span> <span class="o">:</span> <span class="mi">0</span><span class="o">);</span>
            
            <span class="n">futures</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="nc">CompletableFuture</span><span class="o">.</span><span class="na">supplyAsync</span><span class="o">(()</span> <span class="o">-&gt;</span> 
                <span class="n">consumeFromShard</span><span class="o">(</span><span class="n">shardKey</span><span class="o">,</span> <span class="n">shardTokens</span><span class="o">)));</span>
        <span class="o">}</span>
        
        <span class="k">return</span> <span class="n">futures</span><span class="o">.</span><span class="na">stream</span><span class="o">()</span>
            <span class="o">.</span><span class="na">allMatch</span><span class="o">(</span><span class="n">future</span> <span class="o">-&gt;</span> <span class="o">{</span>
                <span class="k">try</span> <span class="o">{</span>
                    <span class="k">return</span> <span class="n">future</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="mi">100</span><span class="o">,</span> <span class="nc">TimeUnit</span><span class="o">.</span><span class="na">MILLISECONDS</span><span class="o">);</span>
                <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
                    <span class="k">return</span> <span class="kc">false</span><span class="o">;</span> <span class="c1">// Fail-safe</span>
                <span class="o">}</span>
            <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="342-circuit-breaker-패턴-적용">3.4.2 Circuit Breaker 패턴 적용</h4>

<p>분산 캐시 장애 시 <a href="https://martinfowler.com/bliki/CircuitBreaker.html">Circuit Breaker 패턴</a>을 활용한 resilient fallback 전략:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">ResilientRateLimiter</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">CircuitBreaker</span> <span class="n">circuitBreaker</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">LocalRateLimiter</span> <span class="n">fallbackLimiter</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAllowed</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="kt">long</span> <span class="n">tokens</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">circuitBreaker</span><span class="o">.</span><span class="na">executeSupplier</span><span class="o">(()</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="c1">// 분산 rate limiter 시도</span>
            <span class="k">return</span> <span class="n">distributedLimiter</span><span class="o">.</span><span class="na">isAllowed</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">tokens</span><span class="o">);</span>
        <span class="o">}).</span><span class="na">recover</span><span class="o">(</span><span class="n">throwable</span> <span class="o">-&gt;</span> <span class="o">{</span>
            <span class="c1">// 장애 시 로컬 rate limiter로 fallback</span>
            <span class="n">log</span><span class="o">.</span><span class="na">warn</span><span class="o">(</span><span class="s">"Distributed rate limiter failed, using local fallback"</span><span class="o">,</span> <span class="n">throwable</span><span class="o">);</span>
            <span class="k">return</span> <span class="n">fallbackLimiter</span><span class="o">.</span><span class="na">isAllowed</span><span class="o">(</span><span class="n">userId</span><span class="o">,</span> <span class="n">tokens</span><span class="o">);</span>
        <span class="o">});</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="343-멀티-리전-배포-전략">3.4.3 멀티 리전 배포 전략</h4>

<p>글로벌 서비스를 위한 <a href="https://redis.io/docs/management/replication/">지리적 분산</a> 기반 멀티 리전 Rate Limiting:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">GlobalRateLimiter</span> <span class="o">{</span>
    
    <span class="c1">// 지역별 제한: 전체 제한의 60%, 글로벌 예비: 40%</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">double</span> <span class="no">REGIONAL_QUOTA_RATIO</span> <span class="o">=</span> <span class="mf">0.6</span><span class="o">;</span>
    <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">double</span> <span class="no">GLOBAL_RESERVE_RATIO</span> <span class="o">=</span> <span class="mf">0.4</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">isAllowed</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="nc">String</span> <span class="n">region</span><span class="o">,</span> <span class="kt">long</span> <span class="n">tokens</span><span class="o">)</span> <span class="o">{</span>
        <span class="c1">// 1. 지역별 quota 먼저 확인</span>
        <span class="nc">String</span> <span class="n">regionalKey</span> <span class="o">=</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"%s:region:%s"</span><span class="o">,</span> <span class="n">userId</span><span class="o">,</span> <span class="n">region</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">regionalLimit</span> <span class="o">=</span> <span class="o">(</span><span class="kt">long</span><span class="o">)</span> <span class="o">(</span><span class="n">totalLimit</span> <span class="o">*</span> <span class="no">REGIONAL_QUOTA_RATIO</span><span class="o">);</span>
        
        <span class="k">if</span> <span class="o">(</span><span class="n">consumeFromBucket</span><span class="o">(</span><span class="n">regionalKey</span><span class="o">,</span> <span class="n">tokens</span><span class="o">,</span> <span class="n">regionalLimit</span><span class="o">))</span> <span class="o">{</span>
            <span class="k">return</span> <span class="kc">true</span><span class="o">;</span>
        <span class="o">}</span>
        
        <span class="c1">// 2. 글로벌 예비 quota 확인</span>
        <span class="nc">String</span> <span class="n">globalKey</span> <span class="o">=</span> <span class="nc">String</span><span class="o">.</span><span class="na">format</span><span class="o">(</span><span class="s">"%s:global"</span><span class="o">,</span> <span class="n">userId</span><span class="o">);</span>
        <span class="kt">long</span> <span class="n">globalReserve</span> <span class="o">=</span> <span class="o">(</span><span class="kt">long</span><span class="o">)</span> <span class="o">(</span><span class="n">totalLimit</span> <span class="o">*</span> <span class="no">GLOBAL_RESERVE_RATIO</span><span class="o">);</span>
        
        <span class="k">return</span> <span class="nf">consumeFromBucket</span><span class="o">(</span><span class="n">globalKey</span><span class="o">,</span> <span class="n">tokens</span><span class="o">,</span> <span class="n">globalReserve</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="35-성능-모니터링-및-알러팅">3.5 성능 모니터링 및 알러팅</h3>

<p>분산 Rate Limiting 시스템의 건강성을 모니터링하기 위한 <a href="https://micrometer.io/">Micrometer</a> 기반 메트릭 수집:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">RateLimitMetrics</span> <span class="o">{</span>
    <span class="kd">private</span> <span class="kd">final</span> <span class="nc">MeterRegistry</span> <span class="n">meterRegistry</span><span class="o">;</span>
    
    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">recordRateLimitDecision</span><span class="o">(</span><span class="nc">String</span> <span class="n">userId</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">allowed</span><span class="o">,</span> <span class="kt">double</span> <span class="n">latency</span><span class="o">)</span> <span class="o">{</span>
        <span class="nc">Timer</span><span class="o">.</span><span class="na">Sample</span> <span class="n">sample</span> <span class="o">=</span> <span class="nc">Timer</span><span class="o">.</span><span class="na">start</span><span class="o">(</span><span class="n">meterRegistry</span><span class="o">);</span>
        <span class="n">sample</span><span class="o">.</span><span class="na">stop</span><span class="o">(</span><span class="nc">Timer</span><span class="o">.</span><span class="na">builder</span><span class="o">(</span><span class="s">"rate_limit_check"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"user"</span><span class="o">,</span> <span class="n">userId</span><span class="o">)</span>
            <span class="o">.</span><span class="na">tag</span><span class="o">(</span><span class="s">"allowed"</span><span class="o">,</span> <span class="nc">String</span><span class="o">.</span><span class="na">valueOf</span><span class="o">(</span><span class="n">allowed</span><span class="o">))</span>
            <span class="o">.</span><span class="na">register</span><span class="o">(</span><span class="n">meterRegistry</span><span class="o">));</span>
            
        <span class="n">meterRegistry</span><span class="o">.</span><span class="na">counter</span><span class="o">(</span><span class="s">"rate_limit_decisions"</span><span class="o">,</span>
            <span class="s">"result"</span><span class="o">,</span> <span class="n">allowed</span> <span class="o">?</span> <span class="s">"allowed"</span> <span class="o">:</span> <span class="s">"denied"</span><span class="o">)</span>
            <span class="o">.</span><span class="na">increment</span><span class="o">();</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<p>분산 환경에서의 Bucket4j는 단순한 토큰 버킷을 넘어 복잡한 분산 시스템의 동시성 제어, 네트워크 최적화, 장애 복구 등 다양한 엔지니어링 과제를 해결해야 합니다. 이러한 고려사항들을 통해 대규모 프로덕션 환경에서도 안정적이고 정확한 rate limiting을 구현할 수 있습니다.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://bucket4j.com">Bucket4j 공식 문서</a></li>
  <li><a href="https://redis.io/learn/howtos/ratelimiting">Redis Rate Limiting 가이드</a></li>
  <li><a href="https://github.com/bucket4j/bucket4j">Bucket4j GitHub 저장소</a></li>
  <li><a href="https://www.mimul.com/blog/about-rate-limit-algorithm/">Rate Limiting 알고리즘 분석</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Token_bucket">Token Bucket Algorithm - Wikipedia</a></li>
  <li><a href="https://smudge.ai/blog/ratelimit-algorithms">Rate Limiting 알고리즘 비교</a></li>
  <li><a href="https://redis.io/docs/clients/jedis/">Jedis Redis 클라이언트</a></li>
  <li><a href="https://github.com/resilience4j/resilience4j">Resilience4j Circuit Breaker</a></li>
  <li><a href="https://martinfowler.com/bliki/CircuitBreaker.html">Martin Fowler’s Circuit Breaker</a></li>
  <li><a href="https://en.wikipedia.org/wiki/Consistent_hashing">Consistent Hashing 구현</a></li>
  <li><a href="https://redis.io/docs/management/optimization/">Redis 성능 최적화</a></li>
  <li><a href="https://micrometer.io/">Micrometer 메트릭 수집</a></li>
</ul>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="Rate Limiter" /><category term="Distributed Systems" /><category term="Performance Engineering" /><summary type="html"><![CDATA[대규모 마이크로서비스 환경에서 안정적인 Rate Limiting을 구현하기 위해 Bucket4j를 도입했습니다. 단순한 토큰 버킷을 넘어 분산 시스템에서의 복잡한 동시성 제어와 성능 최적화까지, 프로덕션 환경에서 겪은 실전 경험을 공유합니다.]]></summary></entry><entry><title type="html">NHN FORWARD 22 - 분산 시스템에서 데이터를 전달하는 효율적인 방법</title><link href="https://youngwon.io/efficient-way-to-pass-data-in-distributed-systems/" rel="alternate" type="text/html" title="NHN FORWARD 22 - 분산 시스템에서 데이터를 전달하는 효율적인 방법" /><published>2025-01-07T12:00:00+09:00</published><updated>2025-01-07T12:00:00+09:00</updated><id>https://youngwon.io/efficient-way-to-pass-data-in-distributed-systems</id><content type="html" xml:base="https://youngwon.io/efficient-way-to-pass-data-in-distributed-systems/"><![CDATA[<blockquote>
  <p>본 글은 <a href="https://www.youtube.com/watch?v=uk5fRLUsBfk">[NHN FORWARD 22] 분산 시스템에서 데이터를 전달하는 효율적인 방법</a>을 정리한 글입니다.</p>
</blockquote>

<h3 id="다룰-내용">다룰 내용</h3>

<ul>
  <li>데이터 전달 보장 방법론</li>
  <li>RDB를 사용하는 애플리케이션에서 전달 방법</li>
  <li>RabbitMQ를 사용한 전달 방법</li>
  <li>Kafka를 사용하는 애플리케이션의 전달 방법</li>
</ul>

<h3 id="분산-시스템">분산 시스템</h3>

<ul>
  <li>여러개의 컴퓨터 리소스를 사용하는 시스템</li>
  <li>시스템은 두 개 이상의 컴포넌트로 구성
    <ul>
      <li>마이크로서비스</li>
    </ul>
  </li>
  <li>네트워크를 통해 데이터를 전달
    <ul>
      <li>Remote API
        <ul>
          <li>서버/클라이언트 방식</li>
          <li>요청시 즉각 응답하는 동기식 방식</li>
          <li>비교적 간단</li>
        </ul>
      </li>
      <li>Message Queue
        <ul>
          <li>Publisher/Consumer 구조
            <ul>
              <li>Consumer가 데이터 조작</li>
            </ul>
          </li>
          <li>배치/비동기 작업</li>
          <li>비교적 복잡</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="분산-시스템에서-데이터를-전달하는-효율적인-방법">분산 시스템에서 데이터를 전달하는 효율적인 방법</h3>

<ul>
  <li>분산 시스템은 네트워크로 연결
    <ul>
      <li>유실가능</li>
    </ul>
  </li>
  <li>데이터 전달 보장 방법
    <ul>
      <li>At most once (최대 한번)
        <ul>
          <li>Producer는 한번만 전송</li>
          <li>Consumer는 한번만 수신</li>
          <li>간단하개 개발가능하나 Producer/Consumer 둘중 하나가 실패하면 메시지 유실 발생</li>
        </ul>
      </li>
      <li>At least once (최소 한번)
        <ul>
          <li>Producer는 최소 한번 이상 발송</li>
          <li>Consumer는 최소 한번 이상 수신</li>
          <li>Producer는 메시지발송 보장</li>
          <li>Consumer는 메시지 처리시 멱등성(idempotent)을 보장해야함</li>
        </ul>
      </li>
      <li>Exactly once (정확히 한번)
        <ul>
          <li>메시지는 정확하게 한번 전송
            <ul>
              <li>누락과 중복이 없음</li>
            </ul>
          </li>
          <li>가장 어려운 난이도</li>
          <li>Producer, Consumer에서 모든 상태 관리</li>
          <li>MessageQueue 기능에 의존한 개발
            <ul>
              <li>MessageQueue 추가로 인한 시스템 복잡도 증가</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>우리는 개발시 <strong>최소 한번</strong>은 전달하고 있는가?</li>
</ul>

<h3 id="rdb를-사용하는-애플리케이션에서-전달-방법">RDB를 사용하는 애플리케이션에서 전달 방법</h3>

<ul>
  <li>서비스별 독립된 데이터베이스를 가지고 있는 경우
    <ul>
      <li><code class="language-plaintext highlighter-rouge">@TransactionalEventlistener</code>,
        <ul>
          <li>네크워트 실패를 방어하지 못함
            <ul>
              <li><code class="language-plaintext highlighter-rouge">@Retryable</code>을 사용하면?
                <ul>
                  <li>1번 재시도?</li>
                </ul>
              </li>
              <li><code class="language-plaintext highlighter-rouge">maxAttempts</code>, <code class="language-plaintext highlighter-rouge">backoff</code> 설정</li>
              <li>그러나 네트워크는 계속 실패할 수 있음</li>
            </ul>
          </li>
        </ul>
      </li>
      <li><code class="language-plaintext highlighter-rouge">TransactionSynchronizionManager</code>, <code class="language-plaintext highlighter-rouge">TransactionSynchronization</code></li>
      <li>다음과 같은 마이크로서비스 아키텍처 패턴 사용가능
        <ul>
          <li>Transactional Outbox Pattern
            <ul>
              <li>RDB를 Message Queue로 사용</li>
              <li>OLTP에 Event Message를 포함하는 패턴</li>
            </ul>
          </li>
          <li>Polling Publisher Pattern
            <ul>
              <li>RDB Message Queue Polling &amp; Publishing</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>두개 혼합해서 사용하면 안전하게 메시지 전달 가능
        <ul>
          <li>이벤트를 RDB에 넣음
            <ul>
              <li>하나의 트랜잭션으로 서비스의 로직과 이벤트를 처리 가능</li>
            </ul>
          </li>
          <li>데몬/스케줄러로 DB에 저장된 이벤트를 주기적으로 발행</li>
        </ul>
      </li>
      <li>이벤트 저장시 중요한 값
        <ul>
          <li>event_id
            <ul>
              <li>pk로 사용하면 이벤트 순서 보장 가능</li>
            </ul>
          </li>
          <li>created_at</li>
          <li>status
            <ul>
              <li>ready / done</li>
            </ul>
          </li>
          <li>payload</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h4 id="transactional-outbox-pattern-예시">Transactional Outbox Pattern 예시</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">CreateTaskService</span> <span class="kd">implements</span> <span class="nc">CreateTaskUserCase</span> <span class="o">{</span>

  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="nc">CreateTaskResponse</span> <span class="nf">createTask</span><span class="o">(</span><span class="nc">CreateTaskCommand</span> <span class="n">createTaskCommand</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">Task</span> <span class="n">task</span> <span class="o">=</span> <span class="n">createTaskCommand</span><span class="o">.</span><span class="na">toTask</span><span class="o">();</span>

    <span class="n">taskRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="n">task</span><span class="o">);</span>
    <span class="n">eventRepository</span><span class="o">.</span><span class="na">save</span><span class="o">(</span><span class="nc">CreateTaskEvent</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">task</span><span class="o">));</span>

    <span class="k">return</span> <span class="nc">CreateTaskResponse</span><span class="o">.</span><span class="na">of</span><span class="o">(</span><span class="n">task</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="polling-publisher-pattern-예시">Polling Publisher Pattern 예시</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Service</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">MessagePublisher</span> <span class="o">{</span>

  <span class="nd">@Scheduled</span><span class="o">(</span><span class="n">cron</span> <span class="o">=</span> <span class="s">"0/5 * * * * *"</span><span class="o">)</span>
  <span class="nd">@Transactional</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">publish</span><span class="o">()</span> <span class="o">{</span>
    <span class="nc">LocalDateTime</span> <span class="n">now</span> <span class="o">=</span> <span class="nc">LocalDateTime</span><span class="o">.</span><span class="na">now</span><span class="o">();</span>
    <span class="n">eventRepository</span><span class="o">.</span><span class="na">findByCreatedAtBefore</span><span class="o">(</span><span class="n">now</span><span class="o">,</span> <span class="nc">EventStatus</span><span class="o">.</span><span class="na">READY</span><span class="o">)</span>
                    <span class="o">.</span><span class="na">stream</span><span class="o">()</span>
                    <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">event</span> <span class="o">-&gt;</span> <span class="n">restTemplate</span><span class="o">.</span><span class="na">execute</span><span class="o">(</span><span class="n">event</span><span class="o">))</span>
                    <span class="o">.</span><span class="na">map</span><span class="o">(</span><span class="n">event</span> <span class="o">-&gt;</span> <span class="n">event</span><span class="o">.</span><span class="na">done</span><span class="o">())</span>
                    <span class="o">.</span><span class="na">forEach</span><span class="o">(</span><span class="nl">eventRepository:</span><span class="o">:</span><span class="n">save</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li>장점
    <ul>
      <li>REST-API 환경에서 At-least-once 가능</li>
    </ul>
  </li>
  <li>단점
    <ul>
      <li>Polling, Publisher 과정으로 지연 처리</li>
      <li>데이터베이스 부하
        <ul>
          <li>처리속도가 데이터베이스 속도에 의존됨</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="rabbitmq를-사용한-전달-방법">RabbitMQ를 사용한 전달 방법</h3>

<ul>
  <li>AMQP(Advanced Message Queuing Protocol)을 구현한 메시지 브로커</li>
  <li>Publish/Subscribe 방식 지원</li>
  <li>ACK(Acknowledgement) 기반 메시지 전송/수신 방식 지원
    <ul>
      <li>Ack, Nack 방식</li>
      <li>Producer Confirm</li>
      <li>Consumer Ack</li>
    </ul>
  </li>
  <li>메시지 전달방법
    <ul>
      <li>Producer가 메시지 발생</li>
      <li>Exchange가 받아서 적절한 Queue에 입력 (Routing)</li>
      <li>Queue</li>
      <li>Consumer가 Queue 메시지 처리</li>
    </ul>
  </li>
  <li>Routing 실패하는경우
    <ul>
      <li>Producer Confirm으로 전달
        <ul>
          <li>org.springframework.amqp.rabbit.connection.CorrelationData.java
            <ul>
              <li>Producer Confirm을 확인할 수 있는 기본 클래스</li>
              <li>RabbitTemplate와 사용</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h4 id="producer-confirm">Producer Confirm</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">RabbitTemplate</span> <span class="nf">rabbitTemplate</span><span class="o">(</span><span class="nc">ConnectionFactory</span> <span class="n">rabbitConnectionFactory</span><span class="o">)</span> <span class="o">{</span>
  <span class="nc">RabbitTemplate</span> <span class="n">rabbitTemplate</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">RabbitTemplate</span><span class="o">(</span><span class="n">rabbitConnectionFactory</span><span class="o">);</span>
  <span class="c1">//...</span>
  <span class="n">rabbitTemplate</span><span class="o">.</span><span class="na">setConfirmCallback</span><span class="o">((</span><span class="n">correlationData</span><span class="o">,</span> <span class="n">ack</span><span class="o">,</span> <span class="n">cause</span><span class="o">)</span> <span class="o">-&gt;</span> <span class="o">{</span>
    <span class="k">if</span> <span class="o">(!</span><span class="n">ack</span><span class="o">)</span> <span class="o">{</span>
      <span class="nc">Message</span> <span class="n">message</span> <span class="o">=</span> <span class="n">correlationData</span><span class="o">.</span><span class="na">getReturned</span><span class="o">().</span><span class="na">getMessage</span><span class="o">();</span>
      <span class="kt">byte</span><span class="o">[]</span> <span class="n">body</span> <span class="o">=</span> <span class="n">message</span><span class="o">.</span><span class="na">getBody</span><span class="o">();</span>
      <span class="n">log</span><span class="o">.</span><span class="na">error</span> <span class="o">(</span><span class="s">"Fail to produce. ID: {}"</span><span class="o">,</span> <span class="n">correlationData</span><span class="o">.</span><span class="na">getId</span><span class="o">());</span>
    <span class="o">}</span>
  <span class="o">});</span>
  <span class="k">return</span> <span class="n">rabbitTemplate</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">spring.rabbitmq.publisher-confirm-type=correlated</span>
</code></pre></div></div>

<h4 id="consumer-ack">Consumer Ack</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Component</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Messagelistener</span> <span class="o">{</span>
  <span class="nd">@RabbitListener</span><span class="o">(</span><span class="n">queues</span> <span class="o">=</span> <span class="s">"dooray.tast"</span><span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">receiveMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">message</span><span class="o">,</span> <span class="nc">Channel</span> <span class="n">channel</span><span class="o">)</span> <span class="o">{</span>
    
    <span class="c1">// 수동 ACK 전송</span>
    <span class="n">channel</span><span class="o">.</span><span class="na">basicAck</span><span class="o">(</span><span class="n">message</span><span class="o">.</span><span class="na">getMessageProperties</span><span class="o">().</span><span class="na">getDeliveryTag</span><span class="o">(),</span> <span class="kc">false</span><span class="o">);</span>

    <span class="c1">// 수동 NACK 전송</span>
    <span class="n">channel</span><span class="o">.</span><span class="na">basicNack</span><span class="o">(</span><span class="n">message</span><span class="o">.</span><span class="na">getMessageProperties</span><span class="o">().</span><span class="na">getDeliveryTag</span><span class="o">(),</span> <span class="kc">false</span><span class="o">,</span> <span class="kc">true</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<ul>
  <li>ChannelAware가 Prefix인 Listener 클래스를 사용하면 Ack 구현가능
    <ul>
      <li>ChannelAwareBatchMessageListener.java</li>
      <li>ChannelAwareMessageListener.java</li>
    </ul>
  </li>
  <li>컨슈머에 버그가 있어서 계속 NACK만 발생시키면?
    <ul>
      <li>이런경우 Dead Letter를 설정
        <ul>
          <li>Dead Latter Exchange -&gt; Dead Letter Queue</li>
          <li>RabbitMQ DLQ 조건
            <ul>
              <li>retry false, basic reject basic nack</li>
              <li>queue의 메시지가 ttl이 넘은경우</li>
              <li>queue가 가득차서 더이상 처리 불가능한경우</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Bean</span>
<span class="kd">public</span> <span class="nc">SimpleRabbitListenerContainerFactory</span> <span class="nf">retryContainerFactory</span> <span class="o">(</span><span class="nc">ConnectionFactory</span> <span class="n">connFactory</span><span class="o">)</span> <span class="o">{</span>
  <span class="c1">//...</span>
  <span class="kt">var</span> <span class="n">containerFactory</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">SimpleRabbitListenerContainerFactory</span><span class="o">();</span>
  <span class="n">containerFactory</span><span class="o">.</span><span class="na">setConnectionFactory</span><span class="o">(</span><span class="n">connFactory</span><span class="o">);</span>
  <span class="n">containerFactory</span><span class="o">.</span><span class="na">setAdviceChain</span><span class="o">(</span>
    <span class="nc">RetryInterceptorBuilder</span><span class="o">.</span><span class="na">stateless</span><span class="o">()</span>
                           <span class="o">.</span><span class="na">maxAttempts</span> <span class="o">(</span><span class="mi">3</span><span class="o">)</span>
                           <span class="o">.</span><span class="na">backOffOptions</span> <span class="o">(</span><span class="mi">1000</span><span class="o">,</span> <span class="mi">2</span><span class="o">,</span> <span class="mi">2000</span><span class="o">)</span>
                           <span class="o">.</span><span class="na">recoverer</span><span class="o">(</span><span class="k">new</span> <span class="nc">RejectAndDontRequeueRecoverer</span><span class="o">())</span> <span class="o">.</span><span class="na">build</span><span class="o">());</span>
  <span class="k">return</span> <span class="n">containerFactory</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="kafka를-사용한-전달-방법">Kafka를 사용한 전달 방법</h3>

<ul>
  <li>Producer Confirm와 Consumer Ack 구현가능</li>
</ul>

<h4 id="producer-confirm-1">Producer Confirm</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Slf4j</span>
<span class="nd">@Component</span>
<span class="nd">@RequiredArgsConstructor</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Producer</span> <span class="o">{</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">sendEvent</span><span class="o">(</span><span class="nc">CreateTaskEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
    <span class="nc">ListenableFuture</span><span class="o">&lt;</span><span class="nc">SendResult</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">CreateTaskEvent</span><span class="o">&gt;&gt;</span> <span class="n">future</span> <span class="o">=</span> 
      <span class="n">kafkaTemplate</span><span class="o">.</span><span class="na">send</span> <span class="o">(</span><span class="no">TOPIC_TASK</span><span class="o">,</span> <span class="n">event</span><span class="o">);</span>
    <span class="n">future</span><span class="o">.</span><span class="na">addCallback</span><span class="o">(</span>
      <span class="n">result</span> <span class="o">-&gt;</span> <span class="n">log</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"offset : {}"</span><span class="o">,</span> <span class="n">result</span><span class="o">.</span><span class="na">getRecordMetadata</span><span class="o">.</span><span class="na">offset</span><span class="o">),</span>
      <span class="n">throwable</span> <span class="o">&gt;</span> <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"fail to publish"</span><span class="o">,</span> <span class="n">throwable</span><span class="o">)</span>
    <span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h4 id="consumer-ack-1">Consumer Ack</h4>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@FunctionalInterface</span>
<span class="kd">public</span> <span class="kd">interface</span> <span class="nc">AcknowledgingMessageListener</span><span class="o">&lt;</span><span class="no">K</span><span class="o">,</span> <span class="no">V</span><span class="err">›</span> <span class="kd">extends</span> <span class="nc">MessageListener</span><span class="o">&lt;</span><span class="no">K</span><span class="o">,</span> <span class="no">V</span><span class="err">›</span> <span class="o">{</span>
  <span class="k">default</span> <span class="kt">void</span> <span class="nf">onMessage</span><span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="no">K</span><span class="o">,</span> <span class="no">V</span><span class="o">&gt;</span> <span class="n">data</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nf">UnsupportedOperationException</span> <span class="o">(</span><span class="s">"Container should never call this"</span><span class="o">);</span>
  <span class="o">}</span>
  <span class="kt">void</span> <span class="nf">onMessage</span><span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="no">K</span><span class="o">,</span> <span class="no">V</span><span class="o">&gt;</span> <span class="n">var1</span><span class="o">,</span> <span class="nc">Acknowledgment</span> <span class="n">var2</span><span class="o">);</span>
<span class="o">}</span>

<span class="c1">// consumer class</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Consumer</span> <span class="o">{</span>
  <span class="nd">@Override</span>
  <span class="nd">@KafkaListener</span><span class="o">(</span>
    <span class="c1">// ...</span>
    <span class="n">containerFactory</span> <span class="o">=</span> <span class="s">"kafkaListenerContainerFactory"</span>
  <span class="o">)</span>
  <span class="kd">public</span> <span class="kt">void</span> <span class="nf">onMessage</span> <span class="o">(</span><span class="nc">ConsumerRecord</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="n">consumerRecord</span><span class="o">,</span> <span class="nc">Acknowledgement</span> <span class="n">acknowledgement</span><span class="o">)</span> <span class="o">{</span>
    <span class="k">try</span> <span class="o">{</span>
      <span class="c1">// Do something...</span>
      <span class="n">acknowledgement</span><span class="o">.</span><span class="na">acknowledge</span><span class="o">();</span>
    <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
      <span class="n">log</span><span class="o">.</span><span class="na">error</span><span class="o">(</span><span class="s">"Error to receive messages."</span><span class="o">,</span> <span class="n">e</span><span class="o">);</span>
    <span class="o">}</span>
  <span class="o">}</span>
<span class="o">}</span>

<span class="c1">// configuration class</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">Configuration</span> <span class="o">{</span>
  <span class="nd">@Bean</span>
  <span class="kd">public</span> <span class="nc">ConsumerFactory</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">String</span><span class="o">&gt;</span> <span class="nf">consumerFactory</span><span class="o">()</span> <span class="o">{</span>
      <span class="nc">Map</span><span class="o">&lt;</span><span class="nc">String</span><span class="o">,</span> <span class="nc">Object</span><span class="o">&gt;</span> <span class="n">props</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">HashMap</span><span class="o">&lt;&gt;();</span>
      <span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">BOOTSTRAP_SERVERS_CONFIS</span><span class="o">,</span> <span class="n">bootstrapServer</span><span class="o">);</span>
      <span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">KEY_DESERIALIZER_CLASS_CONFIG</span><span class="o">,</span> <span class="nc">StringDeserializer</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
      <span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">VALUE_DESERIALIZER_CLASS_CONFIG</span><span class="o">,</span> <span class="nc">StringDeserializer</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
      <span class="c1">// 메뉴얼로 acknowledgement를 전송할때는 ENABLE_AUTO_COMMIT_CONFIG false 처리</span>
      <span class="n">props</span><span class="o">.</span><span class="na">put</span><span class="o">(</span><span class="nc">ConsumerConfig</span><span class="o">.</span><span class="na">ENABLE_AUTO_COMMIT_CONFIG</span><span class="o">,</span> <span class="s">"false"</span><span class="o">);</span>
  <span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>

<h3 id="마무리">마무리</h3>

<ul>
  <li>Event driven Achitecture의 기본은 데이터 전달</li>
  <li>At Least Once 설정</li>
  <li>Producer Confirm, Consumer Ack 고려</li>
</ul>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="NHN FORWARD 22" /><category term="Distributied System" /><category term="Transactional Outbox Pattern" /><category term="Polling Publisher Pattern" /><category term="RabbitMQ" /><category term="Kafka" /><summary type="html"><![CDATA[본 글은 [NHN FORWARD 22] 분산 시스템에서 데이터를 전달하는 효율적인 방법을 정리한 글입니다.]]></summary></entry><entry><title type="html">(TBD) Learnings From Netflix To Effective Testing With Spring Boot</title><link href="https://youngwon.io/testing-spring-boot-like-netflix/" rel="alternate" type="text/html" title="(TBD) Learnings From Netflix To Effective Testing With Spring Boot" /><published>2025-01-05T00:00:00+09:00</published><updated>2025-01-05T00:00:00+09:00</updated><id>https://youngwon.io/testing-spring-boot-like-netflix</id><content type="html" xml:base="https://youngwon.io/testing-spring-boot-like-netflix/"><![CDATA[<blockquote>
  <p>https://www.youtube.com/watch?v=2bTAb-2vhBk</p>
</blockquote>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="Spring Boot" /><category term="Test" /><summary type="html"><![CDATA[https://www.youtube.com/watch?v=2bTAb-2vhBk]]></summary></entry><entry><title type="html">2024 회고</title><link href="https://youngwon.io/review-2024/" rel="alternate" type="text/html" title="2024 회고" /><published>2025-01-01T12:00:00+09:00</published><updated>2025-01-01T12:00:00+09:00</updated><id>https://youngwon.io/review-2024</id><content type="html" xml:base="https://youngwon.io/review-2024/"><![CDATA[<p>2024년은 꽤 많은 일이 있던 한 해였습니다. 2년간 살던 집을 떠나 새로운 곳으로 이사를 했으며 3년간 다니던 직장에서 퇴사하고 새로운 직장으로 이직했습니다. 또한 큰 병 없이 한 해를 보내서 다행이라고 생각합니다.</p>

<h2 id="이사">이사</h2>

<p>개인적인 경험으로 볼 때 좋았던 싫었던 살던 곳을 떠날 때는 항상 아쉬움을 남깁니다. 집 근처의 풍경, 이웃의 얼굴들도 기억에 남지만 아쉬움을 남기는 건 그곳에 살면서 겪은 많은 일들과 경험을 함께한 건 항상 집이기 때문입니다. (특히나 혼자 살기 때문에 이런 감정을 많이 느끼는 것 같습니다)</p>

<p>지금 집에서도 언젠가는 이사를 해야 하는데 그때까지 좋은 추억을 많이 쌓아야겠습니다.</p>

<blockquote>
  <p>낙성대(2021) -&gt; 가양(2022) -&gt; 낙성대(2024)</p>
</blockquote>

<h2 id="이직">이직</h2>

<p>3년간 다닌 회사에서 퇴사하고 새로운 직장으로 이직했습니다. 이직을 한 가장 큰 이유는 새로운 도메인/사람들과 일을 함으로써 더 많은 경험을 쌓고 싶어서였습니다. 큰 회사라면 회사 내에서 그게 가능할 수 있겠지만 그게 힘들다고 판단해서 이직을 결정하였습니다. 결과적으로 만족한 결정이었고 이직 과정 중에도 많은 것을 배울 수 있었습니다. 다만 시기가 시기인 만큼 시장 상황이 좋지 않아 조금 더 조심스러웠어 했지 않나 생각해 봅니다.</p>

<h2 id="2025년-계획">2025년 계획</h2>

<p>여러 가지 계획이 있지만 큰 항목에 대해서만 작성해 봅니다.</p>

<h2 id="시니어-개발자-또는-그-이상의-스태프-엔지니어">시니어 개발자 (또는 그 이상의 스태프 엔지니어)</h2>

<p>조직, 팀에 따라 시니어 개발자의 기준이 다르겠지만 현재 조직에서는 시니어 개발자 역할로 일을 하고 있습니다. 다음 같은 사항들을 지켜나가며 본격적으로 시니어 개발자의 역할을 조직 내에서 해볼 예정입니다.</p>

<ul>
  <li>주도적 문제 정의</li>
  <li>문제 해결
    <ul>
      <li>큰 그림을 기준으로 작은 문제로 나눠서 해결하기</li>
    </ul>
  </li>
  <li>고품질 코드 작성
    <ul>
      <li>사용하는 기술/라이브러리 잘 이해하기</li>
    </ul>
  </li>
  <li>중요한 의사결정 참여
    <ul>
      <li>제품 전략
        <ul>
          <li>도메인 파악 잘하기</li>
        </ul>
      </li>
      <li>아키텍처
        <ul>
          <li>도메인/사용 기술 전반적으로 잘 이해하기</li>
          <li>끊임없는 학습</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h3 id="블로그">블로그</h3>

<p>지금은 블로그를 메모장처럼 사용하고 있습니다. 영상이나 책봤던 내용을 단순히 정리만 하고 있는데 보다 품질높은, 즉 기술적 가치가 있는 글들과 다채로운 일상에 대한 글들을 작성해볼 예정입니다.</p>

<h3 id="개인-프로젝트--강의-제작">개인 프로젝트 &amp; 강의 제작</h3>

<p>여러 가지 개인 프로젝트들을 진행해 볼 예정입니다. 특히 LLM &amp; RAG을 사용한 프로젝트를 진행할 예정입니다. 프로젝트 결과로 꼭 서비스로 만들어야 한다기보다 필요에 따라 강의로 제작을 해보고 싶습니다.</p>

<h3 id="다독">다독</h3>

<p>인생을 살아감에 있어서는 한 가지 분야보다는 문학, 철학, 재테크 등 여러 가지 분야의 책을 골고루 읽는 것이 도움이 됨을 느낍니다. 더욱 깊이있게 많이 읽을 예정입니다.</p>

<p>기술은 서적보다는 공식 문서를 보고 학습하는게 가장 좋은 방법이라고 생각합니다. 다만 필요에 따라서 작성된 책은 저자의 경험까지 학습할 수 있어서 좋은 학습 방법이라고 생각합니다.</p>

<h3 id="그외">그외</h3>

<ul>
  <li>한식조리기능사 자격증 취득</li>
  <li>일본어 공부</li>
</ul>

<h2 id="마무리">마무리</h2>

<p>좋아하는 말 중에 무소식이 희소식이라는 말이 있습니다. 사람들과 연락을 잘 안 하다 보니 항상 연락할 일이 생기면 무슨 일이 있었기 때문입니다. 2024년은 개인적으로도 그렇지만 경제적으로나 정치적으로나 많은 일이 있던 한 해였습니다. 2025년은 아무 일 없는 무소식인 한 해가 되었으면 합니다.</p>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="회고" /><summary type="html"><![CDATA[2024년은 꽤 많은 일이 있던 한 해였습니다. 2년간 살던 집을 떠나 새로운 곳으로 이사를 했으며 3년간 다니던 직장에서 퇴사하고 새로운 직장으로 이직했습니다. 또한 큰 병 없이 한 해를 보내서 다행이라고 생각합니다.]]></summary></entry><entry><title type="html">토스 SLASH 24 정리</title><link href="https://youngwon.io/toss-slash-24/" rel="alternate" type="text/html" title="토스 SLASH 24 정리" /><published>2024-10-07T12:00:00+09:00</published><updated>2024-10-07T12:00:00+09:00</updated><id>https://youngwon.io/toss-slash-24</id><content type="html" xml:base="https://youngwon.io/toss-slash-24/"><![CDATA[<blockquote>
  <p>토스 SLASH24 중 일부 영상을 정리한 글입니다.</p>
</blockquote>

<h2 id="리플레이-검증으로-새로운-금융-시스템-안전하게-도입하기"><a href="https://youtu.be/D3p-V5Sr7gk" target="_blank">리플레이 검증으로 새로운 금융 시스템 안전하게 도입하기</a></h2>

<h3 id="verifier-개발">Verifier 개발</h3>

<ul>
  <li>토스에서는 국내원장 차세대 프로젝트를 진행
    <ul>
      <li>운영중인 기존 C언어, Monolithic으로 구현된 원장 시스템을 Kotlin, MSA로 전환</li>
      <li>다음과 같은 제약사항을 두고 전환
        <ul>
          <li>API 스펙유지</li>
          <li>DB 스키마 유지</li>
          <li>주요 비즈니스 로직 유지</li>
        </ul>
      </li>
      <li>기존 원장 시스템을 사용하</li>
      <li>필요시 기존 원장으로 롤백 가능</li>
      <li>어려움
        <ul>
          <li>기존 시스템 변경</li>
          <li>테스트 코드 작성에 대한 리소스 부족</li>
          <li>한번 통합 테스트를 위해 많은 선행 작업 필요</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Read Verifier
    <ul>
      <li>Read API 검증 가동하ㅗ</li>
      <li>기존 원장에 대한 요청와 응답을 비동기로 신규 원장 호출,
<img src="/toss-slash-24/read-verifier.png" alt="" /></li>
      <li>위 그림의 Verifier가 Kotlin기반 MSA의 요청, 응답 비교 결과를 저장</li>
      <li>어드민에서 API별 verifier 사용여부, 사용시간, 트래픽 비율, 재시도 여부</li>
      <li>일별 리포팅</li>
    </ul>
  </li>
  <li>Write Verifier
    <ul>
      <li>그림</li>
      <li>기존 원장에 보내기 전에 차세대 원장에서 처리</li>
      <li>트랜잭션을 마치기 전에 트랜잭션 내에서 실행된 CUD 쿼리를 조회 쿼리로 변경, 조회 결과를 저장
        <ul>
          <li>조회 쿼리, 결과를 레디스에 저장</li>
        </ul>
      </li>
      <li>트랜잭션 롤백</li>
      <li>CUD 쿼리 -&gt; 조회 쿼리
        <ul>
          <li><a href="https://github.com/p6spy/p6spy">P6Spy</a>
            <ul>
              <li>쿼리 로깅</li>
              <li>실행되는 쿼리를 수집하는데 활용</li>
            </ul>
          </li>
          <li><a href="https://github.com/antlr/antlr4">ANTLR</a>
            <ul>
              <li>수집된 CUD쿼리를 분석해 조회 쿼리로 변환하는데 활용</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>기존 원장의 요청 처리</li>
      <li>요청과 응답을 Verifier로 전송
        <ul>
          <li>레디스에 저장된 쿼리, 처리결과와 비교</li>
        </ul>
      </li>
      <li>정리
        <ul>
          <li>요청을 미리 처리해보고 롤백하는 방식</li>
          <li>동기식으로 동작하기 때문에 성능에 영향을 주어 개발 환경에서만 사용</li>
          <li>검증을 위한 기능을 라이브러리로 제공</li>
        </ul>
      </li>
      <li>결과 리포팅</li>
    </ul>
  </li>
</ul>

<h3 id="생애-주기가-긴-대출-상품">생애 주기가 긴 대출 상품</h3>

<ul>
  <li>대출 기간이 10년인 상품이 존재</li>
  <li>
    <p>약정서에 명시된 약정을 향후 10년동안 정확하게 무리없이 수행하는 시스템</p>
  </li>
  <li>발생가능한 이벤트
    <ul>
      <li>기준금리 변경
        <ul>
          <li>기준금리만 변경되고 가산금리는 유지</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>개발단계에서 작성된 로직이 향후 10년동안 문제없이 동작하는것을 테스트하는 방법
    <ul>
      <li></li>
    </ul>
  </li>
  <li>종단 테스트에서는?
    <ul>
      <li>대출 실행, 해지, 상환 등 은 테스트가능</li>
      <li>장기적인 기간에 대한 테스트</li>
    </ul>
  </li>
  <li></li>
  <li>미래에 실행될 <strong>로직</strong>을 지금 테스트 하는 방법
    <ul>
      <li>대출 시스템이 모놀리식이라는 것을 활용
        <ul>
          <li>하나의 시스템, 하나의 데이터베이스</li>
          <li>복잡하다는 단점이 있지만 한번 설정으로 대출거래 시스템 세팅 가능</li>
          <li>모든 시간관련 로직이 한곳에서 제어</li>
        </ul>
      </li>
      <li>비즈니스
        <ul>
          <li>실시간 거래</li>
          <li>배치 거래</li>
          <li>테스트하려는 거래를 테스트환경에서 순차적으로 수행하면 동일한</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>독립된 테스트 환경에서 시간을 바꿔가며, 거래실행(실시간, 배치), 검증</li>
</ul>

<h3 id="구현-레벨">구현 레벨</h3>

<p>대출 시스템, 데이터베이스
대출 시스템과 제어하는 에이전트는 하나의 pod에서 실행
 각 pod별 독립된 테스트환경 구축 가능</p>

<p>시뮬레이션 마다 독립된 데이터베이스 사용</p>
<ul>
  <li>프로시저를 통한 복제</li>
  <li>3분이내</li>
</ul>

<h2 id="next-코어뱅킹-msa와-mysql로-여는-평생-무료-환전-시대"><a href="https://youtu.be/uWnAVYgCd0k" target="_blank">Next 코어뱅킹, MSA와 MySQL로 여는 평생 무료 환전 시대</a></h2>

<ol>
  <li>토스뱅크의 기술 전환 배경</li>
</ol>

<p>일반적인 은행시스템은 고객 -&gt; 채널계 -&gt; 코어뱅킹(계정계)</p>

<ul>
  <li>채널계: 고객의 트래픽을 받아 코어뱅킹에 전달하는 시스템</li>
  <li>코어뱅킹 (계정계): 금원과 관련된 메인 비즈니스 로직을 처리하는 시스템 (모놀리식, Oracle)</li>
</ul>

<p>[초창기 아키텍처]</p>

<p>코어뱅킹 자체는 레거시(모놀리식)</p>
<ul>
  <li>장점
    <ul>
      <li>트랜잭션 처리 용이</li>
    </ul>
  </li>
  <li>단점
    <ul>
      <li>한개의 서버에서 모든 트래픽 처리
        <ul>
          <li>단일 장애 지점</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>모놀리식 -&gt; 마이크로서비스</li>
  <li>각 서비스들은 마이크로서비스로 분리되었지만 DB는 여전히 Oracle을 공유해서 사용
    <ul>
      <li>Oracle이 단일 장애지점</li>
    </ul>
  </li>
  <li>Oracle을 분리
    <ul>
      <li>비용문제
Oracle 장점</li>
    </ul>
  </li>
  <li>신뢰성과 안정성</li>
  <li>확장성</li>
  <li>성능</li>
  <li>오랜기간 축적된 (금융)노하우
단점</li>
  <li>단일장애지점</li>
  <li>비싼 비용</li>
  <li>제한적인 Scale-out</li>
</ul>

<ol>
  <li>
    <p>기술 비교: 원화예금 VS 외화예금 시스템 구조</p>
  </li>
  <li>
    <p>외화예금 트랜잭션 아키텍처</p>
  </li>
</ol>

<p><img src="/toss-slash-24/foreign-currency-deposit-architecture.png" alt="" />
동기 비동기로 분리</p>

<ol>
  <li>외화예금 API 뜯어보기</li>
</ol>

<p>입금</p>
<ul>
  <li>잔액 변동을 막는 동시성 제어가 필수</li>
  <li></li>
</ul>

<ol>
  <li>24/365 무중단 환전 서비스 구현 방법</li>
</ol>

<h2 id="토스뱅크가-차세대를-하지-않는-이유-지속-가능한-마이그레이션-전략"><a href="https://www.youtube.com/watch?v=LwH9h8dG3PQ" target="_blank">토스뱅크가 차세대를 하지 않는 이유: 지속 가능한 마이그레이션 전략</a></h2>

<ul>
  <li>Strangler Fig Pattern</li>
</ul>

<p><img src="/toss-slash-24/migration-cycle.png" alt="" /></p>

<ul>
  <li>대상선정</li>
  <li>분석</li>
  <li>설계</li>
  <li>구현</li>
  <li>검증</li>
</ul>

<h2 id="대규모-사용자-기반의-마이데이터-서비스-안정적으로-운영하기"><a href="https://www.youtube.com/watch?v=5I7ehDTvnWA" target="_blank">대규모 사용자 기반의 마이데이터 서비스 안정적으로 운영하기</a></h2>

<ul>
  <li>70,000TPS를 처리중인 마이데이터 서비스를 다음 관점에서 소개
    <ul>
      <li>장애 대응</li>
      <li>토스 시스템 부하 개선</li>
      <li>피크 트래픽 제어</li>
    </ul>
  </li>
</ul>

<p><img src="mydata-architecture.png" alt="" /></p>

<h2 id="보상-트랜잭션으로-분산-환경에서도-안전하게-환전하기"><a href="https://www.youtube.com/watch?v=xpwRTu47fqY" target="_blank">보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기</a></h2>

<ul>
  <li>토스뱅크는 모놀리식의 뱅킹 시스템을 마이크로서비스로 분리</li>
  <li>팀, 도메인 등의 특성에 따라 <strong>원화계좌와 외화계좌가 분리</strong>되어 개발됨
    <ul>
      <li>데이터베이스도 분리되어있는 마이크로서비스</li>
    </ul>
  </li>
  <li>환전은 위와같이 서버와 데이터베이스가 분리된 환경에서 구현되어야 했음
    <ul>
      <li>즉 데이터베이스 레벨에서 원자성을 유지할수 없음</li>
      <li>이를 위해 토스는 <strong>분산 트랜잭션</strong>을 구현해야했음</li>
    </ul>
  </li>
  <li>분산 트랜잭션으로는 대표적으로 두 가지가 존재
    <ul>
      <li>Two Phase Commit (2PC): <strong>두 단계로 구분해서 커밋 수행</strong>
        <ul>
          <li>Voting
            <ul>
              <li>Coordinator가 트랜잭션 참여자에게 commit 가능여부를 질의</li>
              <li>트랜잭션 참여자들은 응답
                <ul>
                  <li>모든 서비스가 트랜잭션 가능인경우 아래의 Commit단계 진행</li>
                  <li>하나의 서비스라도 커밋이 불가능한 경우 모든 서비스에 Rollback 요청</li>
                </ul>
              </li>
            </ul>
          </li>
          <li>Commit
            <ul>
              <li>Coordinator가 트랜잭션 참여자에게 commit 요청</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>Saga 패턴
        <ul>
          <li>각 서비스들이 작은 로컬트랜잭션을 실행하면서 진행</li>
          <li>특정 단계에서 실패하면 <strong>보상 트랜잭션</strong> 실행</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>두 가지 방식 비교</li>
</ul>

<p><img src="/toss-slash-24/2pcandsaga.png" alt="" /></p>

<ul>
  <li>토스는 환전서비스를 위해 Saga 패턴 선택
    <ul>
      <li>높은 트래픽 처리</li>
      <li>다양한 트랜잭션 참여자들이 추가될 가능성이 존재</li>
    </ul>
  </li>
  <li>Saga는 크게 두 가지 방식으로 구분
    <ul>
      <li>[두 가지 방식의 사가 그림]</li>
      <li>Choreography Saga
        <ul>
          <li>중앙제어자 없이 메시지 브로커를 사용해서 이벤트 교환하며 진행</li>
          <li>장점
            <ul>
              <li>단일 장애지점 없음</li>
              <li>느슨한 결합</li>
            </ul>
          </li>
          <li>단점
            <ul>
              <li>현재 진행중인 트랜잭션 상태 추적/디버깅 어려움</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>Orchestration Saga
        <ul>
          <li>Orchestrator가 각 서비스들에게 트랜잭션과 보상트랜잭션을 명령하며 진행하는 방식</li>
          <li>장점
            <ul>
              <li>현재 진행중인 트랜잭션 상태 추적 쉬움</li>
            </ul>
          </li>
          <li>단점
            <ul>
              <li>단일 장애지점이 존재</li>
              <li>모든서비스가 결합됨</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>토스는 Orchestration Saga를 선택
    <ul>
      <li>클라이언트의 요청을 받는 환전 서버(Orchestrator 역할 가능)가 필요</li>
      <li>진행중인 환전들의 상태를 관리/추척</li>
    </ul>
  </li>
  <li>환전 성공과 실패
    <ul>
      <li>정상적인 상황에서는 환전 서버가 원화계좌, 외화계좌에 요청하고 각 서비스들이 성공하는 케이스</li>
      <li>그 외 실패 가능 케이스들이 존재
        <ul>
          <li>정상적인 실패</li>
          <li>비정상적인 실패
<img src="/toss-slash-24/fail-type.png" alt="" /></li>
        </ul>
      </li>
      <li>정상적인 실패 처리
        <ul>
          <li>보상트랜잭션으로 성공한 서비스의 상태를 롤백</li>
          <li><strong>출금부터 처리</strong>
            <ul>
              <li>Saga Pattern의 특징은 중간상태가 노출되는것</li>
              <li>입금된 돈이 사용되면 취소가 어려움 (보상트랜잭션 수행의 어려움)</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>비정상적인 실패 처리</li>
    </ul>
  </li>
  <li>
    <p>서버간 통신 방식
<img src="/toss-slash-24/http-messaging.png" alt="" /></p>

    <ul>
      <li>메시지방식은 메시지브로커 레벨에서 에러를 핸들링 및 재시도를 지원하기 때문에 대부분의 Saga는 메시징방식으로 구현됨</li>
      <li>입금, 출금은 HTTP
        <ul>
          <li>입출금 결과를 알고넘어가야함 (동기)</li>
          <li>유저는 환전이 즉시 완료되기를 기대함 (HTTP는 타임아웃 구현이 쉬움)</li>
        </ul>
      </li>
      <li>출금 취소는 Messaging (메시지 브로커에 에러핸들링을 위임)
        <ul>
          <li>출금 취소는 마지막 과정</li>
          <li>유저가 기다릴 필요 없음(비동기)</li>
          <li>출금 취소에 에러 핸들링을 하기 싫음, 결과적 정합성 보장만 되면 됨</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>에러 핸들링
    <ul>
      <li>출금/입금 요청에서 타임하웃 또는 실패를 응답받는경우
        <ul>
          <li>출금/입금 결과를 확인, 그 후속 (보상 트랜잭션) 처리 수행</li>
        </ul>
      </li>
      <li>결과확인에서 실패하는 경우
        <ul>
          <li>메지시를 지연해서 발행할 수 있는 Kafka Message Scheduler가 존재</li>
          <li>지연을 통해 실패한 서비스가 회복할 시간을 줌</li>
          <li>환전 서비스는 메지시를 컨슘해 보상트랜잭션 처리</li>
        </ul>
      </li>
      <li>지연 메시지 발행이 실패하는경우
        <ul>
          <li>Batch를 통해 재처리
            <ul>
              <li>Orchestration가 마지막 환전의 상태를 저장하고 있음</li>
              <li>이후 프로세스를 배치를 통해 처리</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>원화계좌가 출금취소 메시지를 받는것에 실패하는 경우
        <ul>
          <li>Eventually Consistency: Consumer Dead Letter (CDL) 사용
            <ul>
              <li>원화 계좌는 메지시를 DL 서버로 전송</li>
              <li>DL 서버는 서비스 메시지 브로커로 출금 취소 메시지 발생</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li>Transactional Messaging
    <ul>
      <li>Saga에서는 Local Transactional commit과 Message발행이 원자적으로 이루어져야함
        <ul>
          <li>입금 실패로 인한 환전 실패 처리와 출금 취소 메시지 발행은 항상 같이 이루어져야함</li>
        </ul>
      </li>
      <li>이를 위해 토스는 Producer Dead Letter를 사용
        <ul>
          <li>그 외 Outbox Pattern 등이 존재</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p><img src="/toss-slash-24/pdl.png" alt="" /></p>

<ul>
  <li>모니터링</li>
  <li>결론 및 성과
    <ul>
      <li>손쉬운 확장 가능
        <ul>
          <li>회계</li>
        </ul>
      </li>
      <li>또 다른 Saga로 부족한돈 자동환전 결제</li>
      <li>Trade Off
        <ul>
          <li>pros
            <ul>
              <li>MSA 로의 전환, 기존 계정계 시스템이 가지는 문제 해결</li>
              <li>계정계 시스템들과의 느슨한 결합, 확장 가능한 구조</li>
            </ul>
          </li>
          <li>cons
            <ul>
              <li>트랜잭션 구현의 복잡도 증가</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://www.youtube.com/@toss_official" target="_blank">토스 유튜브 채널</a></li>
</ul>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="Toss slash 24" /><category term="소프트웨어 아키텍처" /><category term="소프트웨어 테스트" /><summary type="html"><![CDATA[토스 SLASH24 중 일부 영상을 정리한 글입니다.]]></summary></entry><entry><title type="html">(TBD) 팀워크의 부활 (책 리뷰)</title><link href="https://youngwon.io/the-five-dysfunctions-of-a-team/" rel="alternate" type="text/html" title="(TBD) 팀워크의 부활 (책 리뷰)" /><published>2024-09-25T00:00:00+09:00</published><updated>2024-09-25T00:00:00+09:00</updated><id>https://youngwon.io/the-five-dysfunctions-of-a-team</id><content type="html" xml:base="https://youngwon.io/the-five-dysfunctions-of-a-team/"><![CDATA[<p><img src="https://contents.kyobobook.co.kr/sih/fit-in/458x0/pdt/9791168120112.jpg" alt="" /></p>

<ul>
  <li>패트릭 렌시오니 지름</li>
  <li>서진영 옮김</li>
</ul>

<h2 id="후기">후기</h2>

<ul>
  <li>추천: ★★★★★</li>
  <li>변역: ★★★★☆</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://product.kyobobook.co.kr/detail/S000001852223">교보문고 - 팀워크의 부활</a></li>
</ul>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="팀워크의 부활" /><category term="The Five Dysfunctions of a Team" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">대기열 시스템 (Bufferable)</title><link href="https://youngwon.io/bufferable/" rel="alternate" type="text/html" title="대기열 시스템 (Bufferable)" /><published>2024-08-22T12:00:00+09:00</published><updated>2024-08-22T12:00:00+09:00</updated><id>https://youngwon.io/bufferable</id><content type="html" xml:base="https://youngwon.io/bufferable/"><![CDATA[<blockquote>
  <p>모든 코드는 <a href="https://github.com/youngwonseo/bufferable" target="_blank">github</a> 에서 확인할 수 있습니다.</p>
</blockquote>

<h2 id="개요">개요</h2>

<p>특정 시점에 순간적으로 많은 사용자 요청이 발생하는 이벤트, 예를 들어 티켓팅, 수강신청, 또는 상품 프로모션의 경우, 요청을 순차적으로 처리하는 것이 매우 중요합니다. 이 글에서는 이러한 상황에서 <strong>요청을 대기시키고 순차적으로 처리</strong>하는 대기열  처리를 위한 라이브러리(<strong>Bufferable</strong>)를 설계/구현하고 사용예시를 소개합니다. 이를 통해 사용자 경험을 향상시키고, 시스템의 안정성을 확보할 수 있습니다.</p>

<h2 id="대기열-시스템">대기열 시스템</h2>

<h2 id="설계">설계</h2>

<p>대기열정보를 관리하기 위해 레디스를 사용합니다. 레디스를 선택한 이유는 다음과 같습니다.</p>

<ul>
  <li>싱글 스레드로 동작하여 동시성 이슈를 쉽게 처리가능</li>
  <li>인메모리 기반의 Key-Value 저장소로 보다 높은 성능으로 대기열을 관리</li>
  <li>FIFO을 위한 다양한 데이터구조 지원</li>
</ul>

<p>대기열을 관련 로직을 처리하기 위해 레디스의 List(FIFO 오퍼레이션을 사용해서 큐처럼 사용; lpush, rpop)와 Sorted Set을 고려할 수 있습니다. 이중 다음과 같은 이유로 Sorted Set을 사용했습니다.</p>

<ul>
  <li>대기열에서 중요한 접근 시간이라는 스코어를 기준으로 FIFO 동작이 보장되어야함
    <ul>
      <li>Timestamp를 Score로 Sorted Set을 사용하면 요청 발생 순서로 처리가능</li>
      <li>게이트웨이의 인스턴스가 여러대인경우 List는 올바른 순서로 동작하지 않을 수 있음</li>
    </ul>
  </li>
  <li>List가 조금더 빠르나 Redis 자체가 높은 성능을 보장
    <ul>
      <li>레디스의 List는 링크드리스트이므로 삽입, 제거시 O(1)</li>
      <li>Sorted Set는 삽입, 제거시 O(log(n))</li>
    </ul>
  </li>
</ul>

<p>대기중인 사용자 관리와 함께 처리중인 요청 관리도 필요합니다. 대기열과 다르게 우선순위와 상관없이 서비스에 진입가능한 요청을 관리하므로 Set을 사용합니다. 레디스를 사용해서 처리큐와 대기큐를 관리하면 두개의 데이터 구조 관리(두 개 이상의 오퍼레이션)에 대한 원자성을 보장할 수 있습니다.</p>

<ul>
  <li>처리큐: 현재 접근 가능한 요청 정보 관리</li>
  <li>대기큐: 대기중인 요청 정보 관리</li>
  <li>두개 이상의 오퍼레이션에 대한 원사정을 보장하기 위해 <a href="https://redis.io/docs/latest/develop/interact/programmability/eval-intro/" target="_blank">Lua Script</a> 사용
    <ul>
      <li>여러개의 오퍼레이션을 기반으로 원자성이 보장되는 동작을 구성할때는 해당 동작의 성능이 매우 중요</li>
      <li>싱글스레드 기반으로 동작하는 레디스의 경우 원자성 동작 안에서 데드락등 대기가 발생하면 전체 시스템 문제 발생</li>
    </ul>
  </li>
</ul>

<p>처리큐에 여유가 발생하는 경우 대기큐에 존재하는 요청을 처리큐로 옮겨와야합니다. 이를 위해 배치작업을 수행합니다. 위에서 레디스의 여러 오퍼레이션에 대한 원자성을 보장할 수 있다고 했습니다. 이는 대기큐에서 처리큐로 요청을 옮기는 작업에 원자성도 보장한다는 의미로 싱글 스레드 기반으로 동작하는 레디스 사용시 대기큐에서 처리큐로 요청을 옮기는 사이에 다른 동작이 수행될수 없다는 의미가 됩니다. 즉 동시성 이슈가 발생하지 않습니다.</p>

<h3 id="동작방식">동작방식</h3>

<p><img src="/bufferable/flowchart.png" alt="flowchart" /></p>

<p>요청이 발생했는데 토큰이 없거나 잘못된 토큰인 경우 토큰을 생성합니다. 이후 토큰이 처리큐 또는 대기큐에 존재하는지 확인하며 적절한 동작을 수행합니다.</p>

<h2 id="구현">구현</h2>

<blockquote>
  <p>작성중입니다.</p>
</blockquote>

<h2 id="사용-예시">사용 예시</h2>

<h3 id="아키텍처">아키텍처</h3>

<p><img src="/bufferable/architecture.png" alt="architecture" /></p>

<h3 id="클라이언트">클라이언트</h3>

<p>요청의 주체를 의미합니다. 일반적으로 웹 애플리케이션 또는 모바일 애플리케이션을 의미합니다.</p>

<h3 id="게이트웨이">게이트웨이</h3>

<p>클라이언트 요청의 진입점으로서 인증, 인가 등의 공통요소를 처리합니다. 이후 처리해야할 백엔드 서비스로 라우팅합니다. 본 글에서는 대기열 처리를 위한 토큰 발급 및 토큰 유효성 검사 등을 수행하고 처리가능여부, 대기 여부등을 판단 후 사용자에게 적절한 응답을 반환합니다.</p>

<ul>
  <li>사용자 요청에 대한 토큰 관리
    <ul>
      <li>토큰을 기반으로 진입가능여부 판단</li>
      <li>토큰이 없으면 생성</li>
      <li>Sanitize를 통한 잘못된(또는 악의적으로 변경된) 토큰 처리 방어</li>
    </ul>
  </li>
</ul>

<h3 id="애플리케이션">애플리케이션</h3>

<p>게이트웨이에서 라우팅된 요청을 처리하는 애플리케이션입니다. 비즈니스 로직을 포함하고 있습니다.</p>

<h2 id="테스트">테스트</h2>

<blockquote>
  <p>작성중입니다.</p>
</blockquote>

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

<blockquote>
  <p>작성중입니다.</p>
</blockquote>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="Bufferable" /><category term="Queueing System" /><category term="Redis" /><summary type="html"><![CDATA[모든 코드는 github 에서 확인할 수 있습니다.]]></summary></entry><entry><title type="html">트랜잭셔널 아웃박스 패턴</title><link href="https://youngwon.io/transactional-outbox-pattern/" rel="alternate" type="text/html" title="트랜잭셔널 아웃박스 패턴" /><published>2024-08-06T12:00:00+09:00</published><updated>2024-08-06T12:00:00+09:00</updated><id>https://youngwon.io/transactional-outbox-pattern</id><content type="html" xml:base="https://youngwon.io/transactional-outbox-pattern/"><![CDATA[<p>카프카 같은 메시지 브로커를 사용할때 로직 수행과 이벤트 발행의 원자성을 보장, 메시발송 순서 보장을 위한 트랜잭셔널 아웃박스 패턴(Transactional Outfox Pattern)을 소개합니다.</p>

<h2 id="트랜잭셔널-아웃박스-패턴이란">트랜잭셔널 아웃박스 패턴이란?</h2>

<p>이벤트 드리븐 아키텍처(EDA)는 마이크로서비스 아키텍처(MSA)처럼 여러 서비스가 운영되는 환경에서 서비스 간 통신을 위해 이벤트를 사용하는 아키텍처로, 한 서비스가 이벤트를 발행하고 다른 서비스가 이벤트를 구독하면서 동작합니다. 이러한 방식은 카프카 같은 메시지 브로커를 통해 이벤트를 발행/구독함으로써 서비스간의 직접적인 참조를 제거, 서비스간의 의존성과 결합도를 최소화하도록 지원하는 아키텍처입니다.</p>

<p>클라이언트 요청을 받은 서비스는 로직을 수행하고 이벤트를 발행, 이벤트를 구독하고 있는 서비스들은 발행된 이벤트를 확인하고 서비스가 맡은 역할을 수행합니다. 예로들면 1) 주문 서비스가 주문정보를 데이터베이스에 저장하고 주문이벤트로 발행, 2) 재고 서비스는 주문이벤트를 받아 주문이 발생한 상품의 재고를 처리하면 동작하는것입니다.</p>

<p>위 예에서 주문 서비스가 주문정보를 처리했는데 주문 이벤트 발행에 실패한다면 어떻게 될까요? 주문 서비스와 재고 서비스간의 정합성이 깨지고 재고보다 더 많은 주문을 받거나하는 원하지 않는 상황이 발생할것입니다.</p>

<p>트랜잭셔널 아웃박스 패턴은 이벤트 발행을 테이블(outbox 테이블)에 먼저 기록함으로써 서비스 로직과 이벤트 발행 동작의 트랜잭션을 데이터베이스 레벨에서 처리할 수 있도록 하는 방법입니다. 데이터베이스 레벨의 트랜잭션은 원자성을 보장하므로 로직수행이 실패하면 메시지 발행도 하지 않는것 또한 보장합니다.</p>

<h2 id="트랜잭셔널-아웃박스-패턴-예시">트랜잭셔널 아웃박스 패턴 예시</h2>

<p><img src="https://microservices.io/i/patterns/data/ReliablePublication.png" alt="" /></p>

<ol>
  <li>도메인 로직 처리, outbox 테이블에 발행할 메시지 저장, 이때 두 동작은 하나의 데이터베이스 트랜잭션 단위</li>
  <li>별도 프로세스가 outbox 테이블에 존재하는 메시지를 조회</li>
  <li>2번에서 읽은 메시지를 메시지브로커로 발행</li>
  <li>메시지 발행에 성공하면 outbox 테이블에서 제거</li>
</ol>

<p>이벤트 발행에 실패하면 outbox 테이블에 이벤트가 여전히 존재하기 때문에 이벤트발행을 계속해서 재시도할수 있기때문에 적어도 한번은 메시지가 발행되는것을 보장할 수 있습니다. 또한 outbox 테이블에 기록된 순서로 이벤트발행을 시도하기 때문에 이벤트 생성 순서대로 이벤트 발행을 보장합니다.</p>

<h2 id="트랜잭셔널-아웃박스-패턴-구현">트랜잭셔널 아웃박스 패턴 구현</h2>

<blockquote>
  <p><a href="https://github.com/youngwonseo/transactional-outbox-pattern" target="_blank">구현중</a>입니다.</p>
</blockquote>

<p><img src="/transactional-outbox-pattern/implementation.png" alt="" /></p>

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

<ul>
  <li>트랙잭셔널 아웃박스 패턴은 outbox에 대한 별도의 테이블을 사용해 서비스의 로직과 이벤트 발행의 원자성을 보장하는 방법</li>
</ul>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://microservices.io/patterns/data/transactional-outbox.html" target="_blank">Microservice.io - Pattern: Transactional outbox</a></li>
  <li><a href="https://spring.io/blog/2023/10/24/a-use-case-for-transactions-adapting-to-transactional-outbox-pattern" target="_blank">Spring.io - A Use Case for Transactions: Outbox Pattern Strategies in Spring Cloud Stream Kafka Binder</a></li>
  <li><a href="https://medium.com/@greg.shiny82/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%94%EB%84%90-%EC%95%84%EC%9B%83%EB%B0%95%EC%8A%A4-%ED%8C%A8%ED%84%B4%EC%9D%98-%EC%8B%A4%EC%A0%9C-%EA%B5%AC%ED%98%84-%EC%82%AC%EB%A1%80-29cm-0f822fc23edb" target="_blank">Greg Lee - 트랜잭셔널 아웃박스 패턴의 실제 구현 사례 (29CM)</a></li>
  <li><a href="https://ridicorp.com/story/transactional-outbox-pattern-ridi/" target="_blank">RIDI - Transactional Outbox 패턴으로 메시지 발행 보장하기</a></li>
</ul>]]></content><author><name>Youngwon Seo</name><email>jazz9008@gmail.com</email></author><category term="Transactional Outbox Pattern" /><category term="Event Driven Architecture" /><summary type="html"><![CDATA[카프카 같은 메시지 브로커를 사용할때 로직 수행과 이벤트 발행의 원자성을 보장, 메시발송 순서 보장을 위한 트랜잭셔널 아웃박스 패턴(Transactional Outfox Pattern)을 소개합니다.]]></summary></entry></feed>