<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>cookie-meringue.log</title>
        <link>https://velog.io/</link>
        <description>동작보단 원리를 좋아하는 머랭의 블로그입니다.</description>
        <lastBuildDate>Mon, 01 Jun 2026 21:20:32 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>cookie-meringue.log</title>
            <url>https://velog.velcdn.com/images/cookie-meringue/profile/0b15d65f-c6db-4b0a-992c-5646fb405c1b/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. cookie-meringue.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/cookie-meringue" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[자바는 정말 Write Once, Run Anywhere 일까?]]></title>
            <link>https://velog.io/@cookie-meringue/%EC%9E%90%EB%B0%94%EB%8A%94-%EC%A0%95%EB%A7%90-Write-Once-Run-Anywhere-%EC%9D%BC%EA%B9%8C</link>
            <guid>https://velog.io/@cookie-meringue/%EC%9E%90%EB%B0%94%EB%8A%94-%EC%A0%95%EB%A7%90-Write-Once-Run-Anywhere-%EC%9D%BC%EA%B9%8C</guid>
            <pubDate>Mon, 01 Jun 2026 21:20:32 GMT</pubDate>
            <description><![CDATA[<h1 id="write-once-run-anywhere">Write Once, Run Anywhere</h1>
<p>자바에는 &quot;Write Once, Run Anywhere&quot;, 줄여서 WORA라는 잘 알려진 슬로건이 있습니다.</p>
<p>한 번 작성한 코드가 어디서나 똑같이 동작한다는 뜻입니다.
자바 코드는 특정 OS에 맞춰 컴파일되지 않고 바이트코드라는 중간 형태로 컴파일되며, 이 바이트코드를 각 OS에 설치된 JVM이 해석해 실행합니다.
OS마다 다른 부분은 JVM이 대신 처리하므로, 개발자는 코드를 한 번만 작성해도 Windows든 Linux든 macOS든 똑같이 동작할 것이라고 기대합니다.</p>
<p>이번 포스팅에서는 자바가 내세우는 WORA의 진정한 의미를 <code>Thread.sleep()</code> 의 사례로 짚어 보겠습니다.</p>
<blockquote>
<p>이 포스팅은 Hotspot JVM을 기준으로 작성되었습니다.</p>
</blockquote>
<h2 id="1-threadsleep">1. Thread.sleep()</h2>
<p>코드를 작성하다 보면 <code>Thread.sleep()</code>을 사용하는 일이 종종 있습니다.</p>
<ol>
<li>외부 API 호출이 실패했을 때 간격을 두고 재시도</li>
<li>작업이 끝났는지 일정 간격으로 폴링</li>
<li>요청 사이에 간격을 두어 요청 속도를 제한</li>
<li>비동기 결과를 기다리는 테스트를 작성할 때(권장되지는 않습니다)</li>
</ol>
<pre><code class="language-java">// 재시도 사이에 100ms 간격을 둘 때
for (int attempt = 0; attempt &lt; MAX_RETRY; attempt++) {
    if (callExternalApi()) break;
    Thread.sleep(100);
}

// 조건이 만족될 때까지 50ms 간격으로 폴링할 때
while (!job.isDone()) {
    Thread.sleep(50);
}</code></pre>
<p><code>Thread.sleep(100)</code>을 호출하면 우리는 보통 &quot;100ms 뒤에 다시 실행되겠지&quot;라고 생각합니다.</p>
<p>그런데 <strong>정말 우리가 지정한 시간만큼 스레드의 실행이 멈출까요?</strong></p>
<br>

<h2 id="2-같은-코드-다른-결과">2. 같은 코드, 다른 결과</h2>
<p>다음은 10ms씩 1,000번 실행을 멈췄다가 재개하는 동작을 반복하면서, 전체 시간이 얼마나 걸리는지 측정하는 간단한 벤치마크입니다.</p>
<pre><code class="language-java">public class SleepBenchmark {
    public static void main(String[] args) {
        final int iterations = 1_000;

        long start = System.nanoTime();
        for (int i = 0; i &lt; iterations; i++) {
            try {
                Thread.sleep(10); // 10ms씩 멈추면 합계는 10초가 나와야 정상
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        long elapsedMs = (System.nanoTime() - start) / 1_000_000;

        System.out.printf(&quot;총 소요: %d ms%n&quot;, elapsedMs);
        System.out.printf(&quot;sleep 1회 평균: %.2f ms%n&quot;, elapsedMs / (double) iterations);
    }
}</code></pre>
<p>이론대로라면 <code>10ms × 1,000회 = 10,000ms</code>, 즉 10초 정도면 끝나야 합니다.</p>
<p>그런데 실제로 실행해 보면 결과가 다릅니다.</p>
<p><strong>Linux 환경에서 실험한 결과</strong></p>
<pre><code>총 소요: 10125 ms
sleep 1회 평균: 10.13 ms</code></pre><p><strong>Windows 환경에서 실험한 결과</strong></p>
<pre><code>총 소요: 15944 ms
sleep 1회 평균: 15.94 ms</code></pre><p>똑같은 바이트코드인데 OS가 달라졌다는 이유만으로 실행 시간이 달랐습니다. 앞에서 설명한 WORA의 기대와 어긋나는 결과입니다.</p>
<blockquote>
<p><strong>참고 1</strong>: 구체적인 수치는 OS 버전, 하드웨어 설정 등에 따라 달라집니다.
Windows도 설정 변경을 통해 차이를 줄일 수 있습니다.
<strong>중요한 것은 절대적인 수치가 아니라, OS에 따라 결과가 달라진다는 점입니다.</strong></p>
</blockquote>
<blockquote>
<p><strong>참고 2</strong>: Windows 환경에서 실행되는 Thread.sleep()은 매개변수로 받은 ms 가 10의 배수가 아닌 경우에는 OS 타이머 인터럽트 주기를 1ms로 조정한 후 sleep 을 시작합니다.
따라서, ms 가 10의 배수가 아닌 경우에는 Windows 와 Linux가 동일하게 동작합니다.
이번 포스팅에서는 OS에 따라 결과가 다르다는 예시를 보여드리기 위해 10ms 로 설정했습니다.</p>
</blockquote>
<br>

<h2 id="3-원인은-os의-타이머-인터럽트-주기">3. 원인은 OS의 타이머 인터럽트 주기</h2>
<p><code>Thread.sleep()</code>이 얼마나 정확한지는 자바가 정하지 않습니다.
자바는 OS에 &quot;이만큼 멈췄다가 다시 실행해 달라&quot;고 요청할 뿐이고, 실제로 언제 재개되는지는 OS 타이머 인터럽트 주기에 좌우됩니다.</p>
<h3 id="타이머-인터럽트란">타이머 인터럽트란?</h3>
<p>인터럽트는 실행 중인 흐름을 잠시 멈추고 CPU 코어를 미리 정해진 처리 루틴으로 분기시키는 신호입니다.
실행 중인 스레드가 시스템 콜이나 예외로 스스로 커널에 진입하지 않는 한, OS가 그 스레드를 강제로 멈추려면 타이머 인터럽트 같은 신호가 필요합니다.
그래서 OS는 프로그래밍 가능한 하드웨어 타이머가 일정 간격으로 인터럽트를 발생시키도록 설정해 두고, 그 신호가 올 때마다 제어권을 돌려받아 시간 관리와 스케줄링 같은 일을 처리합니다.
이렇게 주기적으로 발생하는 인터럽트를 tick이라고 부릅니다.</p>
<p>tick이 발생하면 다음 작업이 수행됩니다.</p>
<ul>
<li>실행 중이던 스레드가 잠시 멈추고 제어권이 커널로 넘어갑니다.</li>
<li>커널이 시스템 시간을 갱신하고, 만료된 타이머가 있는지 확인합니다.</li>
<li>대기 중인 스레드 중 재개 시점(sleep deadline)이 지난 것이 있으면 실행 대기열(런큐)로 옮겨 다시 실행될 수 있게 합니다.</li>
</ul>
<h3 id="linux와-windows의-타이머-인터럽트-주기는-다르다">Linux와 Windows의 타이머 인터럽트 주기는 다르다</h3>
<p><strong>Linux</strong>
보통 1~4ms 정도입니다.
다만 HotSpot은 sleep을 고해상도 타이머(hrtimer)로 처리하므로, 타이머 인터럽트 주기와 무관하게 요청한 시간과 거의 비슷하게 sleep 합니다.
(실제로 tick이 4ms인 환경에서도 sleep(10)이 ~10ms로 정확했습니다.)</p>
<p><strong>Windows</strong>
보통 15ms입니다.
<code>Thread.sleep(10)</code>을 호출해도 다음 tick은 최대 15ms 뒤에야 옵니다. 그래서 10ms sleep을 호출해도 최대 16ms 정도 중단됩니다.</p>
<p>스레드 관점에서는 동일하게 실행을 멈췄을 뿐인데, 그 스레드를 다시 실행할지 점검하는 주기는 OS마다 다릅니다.
JVM은 OS에 &quot;이 스레드를 10ms 뒤에 다시 실행해 달라&quot;고 요청할 뿐이고, 실제로 언제 재개할지는 OS가 자신의 타이머 인터럽트 주기에 맞춰 결정합니다.</p>
<br>

<h2 id="4-jdk-문서">4. JDK 문서</h2>
<p><code>Thread.sleep()</code>의 명세를 보면 표현이 조심스럽습니다.</p>
<blockquote>
<p>현재 실행 중인 스레드의 실행을 지정된 밀리초 동안 일시 중단한다(temporarily cease execution).
단, 시스템 타이머와 스케줄러의 정밀도 및 정확도에 따른다(subject to the precision and accuracy of system timers and schedulers).</p>
</blockquote>
<p>핵심은 &quot;시스템 타이머와 스케줄러의 정밀도에 따른다&quot;는 조건입니다.</p>
<p>명세가 보장하는 것은 &quot;최소 N ms 이상 멈춘다&quot;는 하한선뿐이고, 그보다 얼마나 더 멈춰 있을지는 OS의 타이머 인터럽트 주기에 달려 있습니다.
그래서 <strong>&quot;정확히 N ms&quot;</strong> 가 아니라 <strong>&quot;적어도 N ms&quot;</strong> 라는 최소한의 보장만 제공하는 것입니다.</p>
<p>Windows 환경에서 15ms 동안 멈춰 있던 것은 버그가 아니라 명세대로 동작한 결과입니다.
&quot;최소 10ms&quot;라는 약속을 어긴 것은 아니기 때문입니다. 다만 우리가 &quot;10ms를 요청하면 정확히 10ms일 것&quot;이라고 기대했을 뿐입니다.</p>
<br>

<h2 id="5-wora가-보장하는-것과-보장하지-않는-것">5. WORA가 보장하는 것과 보장하지 않는 것</h2>
<p>JVM은 OS에 의존하는 영역(스레드 스케줄링, 타이머 정밀도, 파일 시스템 동작, 줄바꿈 문자, 시그널 처리)은 추상화하지 못합니다.</p>
<p><code>Thread.sleep(10)</code>은 그 차이를 단적으로 드러내는 예시입니다.
한 줄의 코드가 OS에 따라 다르게 동작한다는 사실은, &quot;한 번 작성하면 어디서나 실행된다&quot;와 &quot;어디서나 똑같이 동작한다&quot;가 서로 다른 말임을 보여 줍니다.</p>
<p><strong>WORA는 &quot;바이트코드가 어디서나 실행된다&quot;는 약속이지, &quot;모든 실행 동작이 똑같다&quot;는 약속은 아닙니다.</strong>
JVM은 OS 위에서 동작하는 추상화 계층일 뿐 OS 자체를 대체하지는 못합니다. 그래서 OS 영역이 동작하는 지점에서는 차이가 생깁니다.</p>
<h3 id="비슷한-사례">비슷한 사례</h3>
<ul>
<li><p><strong>문자 인코딩</strong></p>
<ul>
<li>Java 18 이전에는 인코딩을 지정하지 않고 파일을 읽거나 쓰면 OS와 로케일에 따라 기본 charset이 달라졌습니다.</li>
<li>이로 인해, 같은 코드라도 Linux(UTF-8)에서는 정상적으로 표시되던 한글이 Windows(MS949)에서는 깨지는 경우가 있었습니다.</li>
<li>이 문제는 Java 18에서 기본 charset을 UTF-8로 통일하면서 해소되었습니다.</li>
</ul>
</li>
<li><p><strong>GUI</strong></p>
<ul>
<li>Swing이나 AWT로 만든 화면은 폰트, DPI 처리, 적용한 룩앤필에 따라 OS마다 조금씩 다르게 보입니다.</li>
<li>특히 플랫폼 네이티브 룩앤필을 사용하면 버튼이나 입력창이 각 OS의 기본 모양을 따르므로, 같은 레이아웃 코드라도 화면이 동일하지는 않습니다.</li>
</ul>
<br>

</li>
</ul>
<h1 id="마무리">마무리</h1>
<p>&quot;Write Once, Run Anywhere&quot;는 대체로 사실이고, 충분히 강력한 약속입니다.
JVM이 책임지는 부분은 어느 OS에서나 동일하게 동작합니다.</p>
<p>문제는 OS나 하드웨어에 크게 종속되는 순간 발생합니다.
Thread.sleep의 정밀도는 OS의 타이머 인터럽트 주기에 달려 있고, 기본 인코딩이나, 타임존, 파일 경로 규칙도 환경마다 다릅니다.</p>
<p>정리하면, <strong>WORA가 보장하는 계층은 바이트코드 실행과 언어 의미론</strong>까지입니다.
OS나 하드웨어의 차이가 드러나는 영역에서는 같은 코드가 다른 결과를 낼 수 있습니다.</p>
<p>**&quot;Write Once, Run Anywhere&quot;는 &quot;한 번 작성하면 어디서나 실행된다&quot;는 뜻이지, &quot;어디서나 똑같이 동작한다&quot;는 뜻은 아닙니다.
**</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프레임워크 코드 뜯어보다 Spring 컨트리뷰터가 되었습니다]]></title>
            <link>https://velog.io/@cookie-meringue/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EB%8B%A4-Spring-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%ED%84%B0%EA%B0%80-%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
            <guid>https://velog.io/@cookie-meringue/%ED%94%84%EB%A0%88%EC%9E%84%EC%9B%8C%ED%81%AC-%EC%BD%94%EB%93%9C-%EB%9C%AF%EC%96%B4%EB%B3%B4%EB%8B%A4-Spring-%EC%BB%A8%ED%8A%B8%EB%A6%AC%EB%B7%B0%ED%84%B0%EA%B0%80-%EB%90%98%EC%97%88%EC%8A%B5%EB%8B%88%EB%8B%A4</guid>
            <pubDate>Wed, 22 Apr 2026 10:32:37 GMT</pubDate>
            <description><![CDATA[<p>최근에 Spring Framework에 기여하게 되었습니다.</p>
<p>이번 글에서는 프레임워크 내부 코드를 탐구하다가 오픈소스 기여까지 이어지게 된 과정을 설명해드리려 합니다.</p>
<h2 id="배경">배경</h2>
<p>블로그 설명에도 나와있지만, 저는 기술을 사용할 때 단순히 주어지는 대로 쓰기보다는, <strong>&quot;이건 내부적으로 어떻게 동작할까?&quot;</strong> 하고 그 원리를 파헤치고 탐구하는 것을 좋아합니다.</p>
<p>이런 성향 덕분에, 저는 Spring MVC의 내부 동작 원리에도 관심이 많았습니다.</p>
<p>Interceptor가 요청을 어떻게 가로채는지, DispatcherServlet이 어떻게 요청을 분배하는지, Controller의 메서드 파라미터로 어떻게 객체들이 바인딩되는지 종종 내부 코드를 열어보며 확인하곤 했습니다.</p>
<p>이러한 호기심으로 인해 우아한테크코스에서 같은 뜻을 가진 크루들과 &quot;Spring 내부 코드 탐험 스터디&quot;를 진행했고, 그 과정에서 <code>HandlerMethodReturnValueHandlerComposite</code> 이라는 클래스를 자세히 살펴보게 되었습니다.</p>
<p>이 클래스는 Controller(HandlerMethod)에서 반환된 객체를 지원하는 적절한 <code>HandlerMethodReturnValueHandler</code> 를 찾아 처리를 위임하는 역할을 합니다. 이때 분석했던 내용이 꽤 흥미로워서, 이를 주제로 테코톡 발표를 진행하기도 했습니다.</p>
<blockquote>
<p> 테코톡 발표 영상 링크 : <a href="https://youtu.be/JOLwv6Btayg?si=N2N019VSeGo3RL08">https://youtu.be/JOLwv6Btayg?si=N2N019VSeGo3RL08</a></p>
</blockquote>
<br>

<h2 id="문제">문제</h2>
<p>몇 개월 후, 프로젝트를 진행하다 커스텀 Handler를 만드는 과정에서 <code>HandlerMethodReturnValueHandlerComposite</code> 내부 코드를 다시 읽다가 흥미로운 점을 발견했습니다.</p>
<p>코드를 다시 살펴본 이유는 <strong>이 클래스에 캐시를 도입해 보면 어떨까?</strong> 하는 생각 때문이었습니다.</p>
<blockquote>
<p>🤔 <strong>배경지식</strong></p>
<p><code>HandlerMethodReturnValueHandler</code> 란
HandlerMethod(컨트롤러의 메서드)가 반환한 객체를 사용해 Http 응답을 구성하기 위해 사용하는 &quot;응답 값 핸들러&quot;입니다.</p>
<p><code>HandlerMethodReturnValueHandlerComposite</code> 란
다양한 <code>HandlerMethodReturnValueHandler</code>를 하나의 묶음으로 구성해서, HandlerMethod(컨트롤러의 메서드)가 반환한 객체를 처리(Handle)할 수 있는 적절한 <code>HandlerMethodReturnValueHandler</code>를 탐색하고 호출합니다.</p>
</blockquote>
<p><code>HandlerMethodReturnValueHandlerComposite</code> 는 현재 반환된 객체를 처리할 수 있는 적절한 <code>HandlerMethodReturnValueHandler</code>를 찾기 위해 내부적으로 반복문을 돕니다.</p>
<pre><code class="language-java">@Nullable
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
    for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
        if (handler.supportsReturnType(returnType)) {
            return handler;
        }
    }
    return null;
}</code></pre>
<p><strong>매번 루프를 도는 대신 캐시를 도입한다면 큰 성능 최적화가 가능하지 않을까?</strong> 하는 호기심이 생겼습니다.</p>
<p>캐싱 로직을 추가해 볼 수 있는지 알아보기 위해 클래스를 자세히 살펴보기 시작했는데, 상단의 JavaDoc을 읽다가 의아한 부분을 발견했습니다.</p>
<pre><code class="language-java">/**
 * Handles method return values by delegating to a list of registered
 * {@link HandlerMethodReturnValueHandler HandlerMethodReturnValueHandlers}.
 * Previously resolved return types are cached for faster lookups.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 */
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {</code></pre>
<p>Javadoc 일부를 보면,  <code>Previously resolved return types are cached for faster lookups.</code> 라는 설명이 존재합니다.</p>
<p>분명 코드 로직 상으로는 매번 루프를 돌며 Handler를 찾고 있었는데, JavaDoc에는 <strong>&quot;이 클래스는 빠른 탐색을 위해 Handler를 캐싱한다&quot;</strong>는 설명이 남아있었습니다.</p>
<br>

<p>코드와 주석의 내용이 일치하지 않는 이유를 찾기 위해 해당 클래스의 Git History를 거슬러 올라가 보았습니다.</p>
<p>그 과정에서, 2012년에 작성된 하나의 커밋을 발견했습니다.</p>
<blockquote>
<p>문제의 커밋 : <a href="https://github.com/spring-projects/spring-framework/commit/cfe2af76906039e42b12dc24cf4fca7b91c9b910">https://github.com/spring-projects/spring-framework/commit/cfe2af76906039e42b12dc24cf4fca7b91c9b910</a></p>
</blockquote>
<p>내용을 살펴보니, 2012년 이전에는 <strong>실제로 캐싱 로직이 존재</strong>했습니다.</p>
<p>하지만 HandlerMethod가 반환한 객체의 내부 상태에 따라 분기 처리를 해야 하는 요구사항이 생기면서, 단순히 반환 타입만으로는 특정 Handler를 고정해서 캐싱할 수 없게 된 것입니다.</p>
<p>결국 캐싱 로직은 해당 커밋으로 인해 제거되었지만, <strong>Javadoc 주석은 함께 지워지지 않고 누락</strong>되어 있었습니다.</p>
<p>그리고 그 상태로 2012년부터 2026년인 지금까지 그대로 남아있었던 것입니다.</p>
<br>

<h2 id="pr-제출">PR 제출</h2>
<p>히스토리를 분석해 보니 그 Javadoc이 잘못되었다는 것을 파악했고, 실제 <code>HandlerMethodReturnValueHandlerComposite</code>의 동작과 일치하도록 수정하여 PR을 제출했습니다.</p>
<blockquote>
<p>PR : <a href="https://github.com/spring-projects/spring-framework/pull/36555">https://github.com/spring-projects/spring-framework/pull/36555</a></p>
</blockquote>
<br>

<h2 id="머지">머지</h2>
<p>단순한 문서 수정이었기에 리뷰 과정은 비교적 짧게 끝났고, 제 PR은 Spring Framework의 7.0.7 마일스톤에 성공적으로 반영되어 머지되었습니다.</p>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/72d56ec3-e74d-4f79-bf20-3f8ebb67c187/image.png" alt=""></p>
<br>

<h2 id="후기">후기</h2>
<p>거창한 코드 기여나 엄청난 성능 개선을 이룬 것은 아닙니다.</p>
<p>하지만 평소에 프레임워크의 내부 코드를 열어보고 원리를 궁금해하던 습관 덕분에, 오래된 문서 오류를 바로잡는 소소한 기여를 할 수 있었습니다.</p>
<p>이번 경험을 통해 멀게만 느껴졌던 오픈소스 생태계와의 심리적 거리가 한결 가까워졌고, 제 목표인 공통 플랫폼 개발자에 한 걸음 더 다가간 것 같아 뿌듯한 마음도 듭니다.</p>
<p>프로젝트를 진행하시다가 &quot;이건 내부적으로 어떻게 돌아갈까?&quot;라는 궁금증이 든다면, 라이브러리 코드를 슬쩍 열어보는건 어떨까요?
혹시 아시나요? 작은 궁금증이 오픈소스 기여로 이어질지도 모릅니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[WAL(Write-Ahead Logging) 알아보기 2편 - Redo Log와 WAL]]></title>
            <link>https://velog.io/@cookie-meringue/WALWrite-Ahead-Logging-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-2%ED%8E%B8-Redo-Log%EC%99%80-WAL</link>
            <guid>https://velog.io/@cookie-meringue/WALWrite-Ahead-Logging-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-2%ED%8E%B8-Redo-Log%EC%99%80-WAL</guid>
            <pubDate>Mon, 19 Jan 2026 16:46:00 GMT</pubDate>
            <description><![CDATA[<h1 id="0-서론">0. 서론</h1>
<p>안녕하세요 머랭입니다.
지난 포스팅에선, InnoDB가 버퍼를 관리하기 위해 <strong>STEAL &amp; No-FORCE 정책</strong>을 사용한다는 것을 알아보았습니다.</p>
<p>이번 포스팅에서는 <strong>No-FORCE 정책을 유지하면서, 동시에 Durability를 보장</strong>하기 위한 <strong>Redo Log와 WAL(Write-Ahead Logging)</strong>에 대해 설명드리겠습니다.</p>
<br>

<h1 id="1-redo-log">1. Redo Log</h1>
<p>InnoDB는 랜덤 I/O를 최소화하기 위해, 트랜잭션이 커밋 이후에도 더티 페이지를 즉시 디스크에 반영하지 않고 버퍼에 보관하는 No-FORCE 정책을 사용합니다.
그러나, <strong>No-FORCE 정책은 Durability를 보장할 수 없다는 단점</strong>이 존재합니다.</p>
<p>InnoDB는 No-FORCE 정책을 사용하면서도 Durability를 보장하기 위해 Redo Log를 사용합니다.
<strong>Redo Log는 페이지 내의 데이터 변경 기록을 기록하고, 추후 다시 수행될 수 있도록 하기 위해 존재하는 로그 파일</strong>입니다.
장애가 발생하더라도, 재부팅 후 Redo Log를 읽어 변경사항을 다시 수행하면 커밋된 트랜잭션에 대한 데이터는 완벽히 복구됩니다.</p>
<br>

<h1 id="2-구조">2. 구조</h1>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/0c923301-e85b-4208-8bc4-e68724614108/image.png" alt=""></p>
<p>Redo Log는 <strong>Redo Log Block</strong>으로 이루어지며, Redo Log Block은 <strong>Redo Log Record</strong>들로 이루어집니다.
Redo Log Record의 구조는 다음과 같습니다.</p>
<ul>
<li><strong>Type:</strong> 변경 작업의 성격(Insert, Update, Delete 등)을 나타내는 타입 코드입니다.</li>
<li><strong>Space ID:</strong> 데이터가 변경된 테이블스페이스의 ID입니다.</li>
<li><strong>Page Number:</strong> 변경이 발생한 페이지의 번호입니다.</li>
<li><strong>Offset:</strong> 페이지 내에서 실제 데이터가 수정된 시작 위치입니다.</li>
<li><strong>Data:</strong> 변경된 실제 데이터 내용입니다.</li>
</ul>
<br>

<h1 id="3-walwrite-ahead-logging과-로그-버퍼">3. WAL(Write-Ahead Logging)과 로그 버퍼</h1>
<p>WAL(Write-Ahead Logging)은 <strong>데이터를 변경하기 전, 변경 로그를 먼저 기록한다는 원칙</strong>입니다.
<strong>트랜잭션이 커밋되기 전에 Redo Log를 먼저 기록</strong>함으로써 <strong>Durability를 보장</strong>할 수 있습니다.
장애가 발생하더라도, 재부팅 후 Redo Log를 읽어 변경사항을 다시 수행하면 커밋된 트랜잭션에 대한 데이터를 복구할 수 있습니다.</p>
<p>WAL의 핵심은, “트랜잭션 커밋 전 Redo Log를 저장해야 한다”는 것입니다.
트랜잭션이 커밋되기 전, Redo Log를 어떤 수준까지 저장할 것인지 이해하기 위해선 로그 버퍼에 대해 알아야 합니다.</p>
<blockquote>
<p>이후 포스팅에서 다룰 예정이지만, 사실 Redo Log Record는 트랜잭션 커밋 시점이 아닌 <strong>MTR(Mini-Transaction)</strong>이라는 최소 단위 트랜잭션 커밋 시마다 생성됩니다.</p>
</blockquote>
<p>만약, 매 트랜잭션이 끝날 때마다 모든 Redo Log Recoord들이 디스크(혹은 OS 페이지 캐시)저장되어야 한다면 계속해서 랜덤 I/O가 발생하기 때문에 매우 비효율적일 것입니다.</p>
<br>

<p>이 문제를 해결하기 위해 도입된 것이 <strong>로그 버퍼</strong>입니다.
로그 버퍼는 <strong>트랜잭션 진행 중 발생하는 Redo Log Record들을 디스크에 쓰기 전 메모리에 임시로 저장하는 버퍼</strong>입니다.
트랜잭션 과정에서 발생한 Redo Log Record들을 모아 <strong>블록 단위로 묶어 I/O함으로써, 디스크 I/O 횟수를 줄일 수 있습니다.</strong></p>
<p>그러나, 로그 버퍼는 메모리 기반 버퍼이기 때문에 <strong>버퍼와 디스크 사이의 데이터 불일치가 발생하는 시점이 존재</strong>합니다.
InnoDB는 데이터 불일치가 발생하는 기간을 조절할 수 있는 <code>innodb_flush_log_at_trx_commit</code> 속성을 제공합니다.
<code>innodb_flush_log_at_trx_commit</code> 속성은 트랜잭션이 성공하기 위해서 <strong>로그 버퍼의 블록을 어느 단계까지 작성해야 하는지 정하는 속성</strong>입니다.</p>
<table>
<thead>
<tr>
<th><strong>값</strong></th>
<th><strong>커밋 시</strong></th>
<th><strong>유실 범위 (장애 시)</strong></th>
<th><strong>성능 오버헤드</strong></th>
</tr>
</thead>
<tbody><tr>
<td><code>1</code> (기본)</td>
<td>write() + fsync() 수행</td>
<td>없음</td>
<td>높음 (디스크 I/O 대기)</td>
</tr>
<tr>
<td><code>2</code></td>
<td>write()만 수행</td>
<td>최대 1초</td>
<td>중간 (I/O 대기 없음)</td>
</tr>
<tr>
<td><code>0</code></td>
<td>아무 일도 하지 않음</td>
<td>최대 1초</td>
<td>낮음 (System Call 없음)</td>
</tr>
<tr>
<td><code>1</code>: 모든 블록이 <strong>디스크까지 작성</strong>되어야 트랜잭션이 성공할 수 있습니다.</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 데이터 유실은 없으나, <strong>디스크 쓰기 속도가 전체 트랜잭션의 병목 지점</strong>이 될 수 있습니다.</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 먼저 완료된 트랜잭션으로 인해 디스크 쓰기 작업이 진행중일 때 다른 트랜잭션이 커밋된다면, 로그 버퍼에 Redo Log Record를 쌓아두었다가 디스크 쓰기 작업이 끝나면 곧바로 디스크에 작성합니다.</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p><code>2</code>: 모든 블록이 <strong>OS의 페이지 캐시에 작성</strong>되어야 트랜잭션이 성공할 수 있습니다.</p>
<ul>
<li>실제 디스크 기록은 OS가 수행하므로 빠르지만, <strong>서버 장애 발생 시 OS 캐시는 유실</strong>됩니다.</li>
<li>백그라운드 스레드가 1초 주기로 fsync()를 호출합니다.</li>
</ul>
<p><code>0</code>: 모든 블록이 <strong>로그 버퍼에 작성</strong>되어야 트랜잭션이 성공할 수 있습니다.</p>
<ul>
<li>백그라운드 스레드가 1초 주기로 로그 버퍼의 모든 블록을 디스크에 작성합니다.</li>
<li>DB 엔진이나 OS 중 하나만 비정상 종료되어도 마지막 1초간의 커밋은 무효화됩니다.</li>
</ul>
<br>

<h1 id="4-로그-그룹">4. 로그 그룹</h1>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/9355f061-0aac-456c-b878-e778374abbef/image.png" alt=""></p>
<p>InnoDB는 Redo Log의 <strong>순차 I/O를 위해 여러 개의 Redo Log 파일을 로그 그룹으로 묶어서 관리</strong>합니다.</p>
<p>로그 그룹은 하나 이상의 물리적인 Redo Log 파일들을 <strong>논리적인 바이트 스트림으로 관리할 수 있게 도와주는 컨테이너</strong>입니다.
로그 그룹은 Redo Log 파일들을 원형 큐 형태로 관리합니다. N번 파일의 마지막 바이트 다음 바이트는 N+1번 파일의 첫 번째 바이트로 이어지도록 구성합니다.</p>
<blockquote>
<p>바이트를 스트림 형태로 다루는 것은 <strong>‘논리적 추상화’</strong> 입니다.
논리적으로 파일 간 데이터 흐름이 이어질 수 있어도, <strong>파일 단편화</strong>로 인해 순차 I/O가 발생하지 않을 수 있습니다.
InnoDB는 <strong>데이터베이스 초기화 시점에 전체 Redo Log 파일의 크기를 미리 할당</strong>받습니다.
이 과정에서, 운영체제는 최대한 연속된 물리 공간을 할당하게 되므로 파편화를 최대한 방지하고, 디스크 헤더 이동 시간을 최소화합니다.</p>
</blockquote>
<p>파일이 가득 차면 다음 파일로 넘어가고, 마지막 파일까지 가득 차면 다시 첫 번째 파일의 처음으로 돌아와서 데이터를 덮어씁니다.</p>
<blockquote>
<p>무조건 덮어쓰는 것은 아닙니다. 덮어쓰려는 Redo Log가 Inactive 상태여야 합니다.
<strong>Active</strong>: 반영되지 않은 Redo Log Record가 존재해 추후 복구에 해당 Redo Log 파일이 필요할 수 있는 상태.
<strong>Inactive</strong>: 모든 Redo Log Record가 반영되어 더 이상 사용되지 않는 상태.</p>
</blockquote>
<p>덮어써야 하는 Redo Log가 Active 상태라면, InnoDB는 새로운 트랜잭션을 잠시 멈추고(Blocking), Redo Log 내 Redo Log Record와 연결된 더티 페이지를 디스크에 쓰기 시작합니다.</p>
<blockquote>
<p>Redo Log의 크기와 갯수 옵션을 조절하여 Blocking 문제를 최대한 방지할 수 있습니다.
참고: <a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_log_file_size">https://dev.mysql.com/doc/refman/8.4/en/innodb-parameters.html#sysvar_innodb_log_file_size</a></p>
</blockquote>
<br>

<h1 id="5-lsnlog-sequence-number">5. LSN(Log Sequence Number)</h1>
<p>Redo Log Record는 더티 페이지가 발생할 때마다 생성됩니다.
이로 인해 장애 복구 시간이 크게 늘어나고, 무한한 Redo Log 저장 공간을 요구하게 됩니다.</p>
<p>이를 해결하기 위해 <strong>LSN(Log Sequence Number)</strong> 이 도입되었습니다.
LSN은 <strong>데이터베이스 생성 시점부터 현재까지 발생한 Redo Log의 총 누적 바이트 합계</strong>를 나타내는 값으로, <strong>Redo Log Record와 페이지에 부여</strong>됩니다.</p>
<p>LSN은 <strong>Global LSN</strong>을 통해 생성되며, <strong>처음엔 Redo Log Record에 부여</strong>됩니다. 이후, <strong>Redo Log Record가 수정하는 페이지 헤더에 똑같은 LSN을 기록</strong>합니다.
이 시점부터 해당 페이지는 더티 페이지가 됩니다.</p>
<br>
LSN이 사용되는 이유는 두 가지입니다. 

<h4 id="1-redo-log-record의-lsn과-페이지의-lsn을-비교하면-redo-log-record가-이미-반영되었는지-확인할-수-있습니다">1. Redo Log Record의 LSN과 페이지의 LSN을 비교하면, Redo Log Record가 이미 반영되었는지 확인할 수 있습니다.</h4>
<p>만약, Redo Log Record LSN이 페이지 LSN 보다 크다면 해당 Redo Log Record는 반영되지 않은 것입니다.</p>
<h4 id="2-redo-log-record-주소-탐색-비용을-최소화할-수-있습니다">2. Redo Log Record 주소 탐색 비용을 최소화할 수 있습니다.</h4>
<p>  Redo Log Record 크기는 DML 종류에 따라 다릅니다. 누적 바이트 단위인 LSN을 사용하면 간단한 산술 연산만으로 Redo Log Record를 작성해야 하는 주소를 도출할 수 있습니다.
  <strong>Redo Log Record를 작성해야 하는 주소 = Redo Log 시작 주소 + (LSN % 로그 그룹의 총 크기)</strong>    </p>
<br>

<h1 id="6-checkpoint-lsn">6. Checkpoint LSN</h1>
<p>InnoDB는 LSN을 통해 복구 시 데이터 비교 없이 특정 Redo Log Record의 변경사항이 특정 페이지에 반영되었는지 확인할 수 있습니다.
그러나, 장애 복구 시 <strong>모든 Redo Log Record들을 읽어 일일히 비교하는 것은 비효율</strong>적입니다.</p>
<p>InnoDB는 장애 복구 시간을 최소화하기 위해 <strong>Checkpoint LSN</strong>을 사용합니다.
Checkpoint LSN은 <strong>Redo Log에 기록된 변경 사항 중, 완전히 반영된 마지막 지점을 나타내는 LSN</strong>입니다.
장애 복구 시, <strong>Checkpoint LSN 이전의 데이터는 이미 디스크에 반영된 것이 확실하므로 복구할 필요가 없습니다.</strong>
<br>
Page Cleaner는 주기적으로 더티 페이지들을 디스크에 쓰고, 더티 페이지들 중 가장 낮은 LSN을 Redo Log 파일 헤더의 Checkpoint LSN으로 기록합니다.</p>
<blockquote>
<p>SPOF를 방지하기 위해 Checkpoint LSN은 첫 번째, 두 번째 Redo Log 파일 헤더에 기록됩니다.
복구 시, 두 Checkpoint LSN을 비교해 높은 값을 사용합니다.</p>
</blockquote>
<p>또한, Checkpoint LSN을 활용하면 Redo Log 파일 순환 시 덮어쓰려는 Redo Log 파일이 Active인지 Inactive인지 판별할 수 있습니다.</p>
<blockquote>
<p>OS 페이지 캐시에 반영된 LSN을 <strong>Wirte LSN</strong>이라 합니다.
<strong>Active인 경우</strong>: Redo Log Group 총 용량 ≤ Write LSN - Checkpoint LSN
<strong>Inactive인 경우</strong>: Redo Log Group 총 용량 &gt; Write LSN - Checkpoint LSN</p>
</blockquote>
<br>

<h1 id="7-doublewrite-buffer">7. DoubleWrite Buffer</h1>
<p>만약, 더티 페이지를 디스크에 쓰는 중 장애가 발생하면 어떻게 될까요?
16KB 페이지 하나를 디스크에 쓰는 도중 전원이 차단되면, 16KB 중 일부만 써지고 뒷부분은 예전 데이터가 남는 <strong>Partial Write</strong> 현상이 발생합니다.</p>
<p>Redo Log는 물리적으로 무결한 페이지의 특정 오프셋에 기록된 바이트 값을 새 값으로 덮어쓰는 방식으로 작동합니다.
<strong>페이지 쓰기 도중 장애로 인해 Torn Page 상태가 되면, Redo Log를 적용할 수 없습니다.</strong></p>
<blockquote>
<p><strong>Torn Page</strong>란?
페이지 일부분만 저장되어 Checksum이 일치하지 않거나 헤더가 파손된 페이지를 말합니다.</p>
<p>Torn Page에 대해 더 궁금하다면?
<a href="https://dev.mysql.com/doc/refman/8.4/en/glossary.html#glos_torn_page">https://dev.mysql.com/doc/refman/8.4/en/glossary.html#glos_torn_page</a></p>
</blockquote>
<p>InnoDB는 이를 해결하기 위해 <strong>Doublewrite Buffer</strong>를 사용합니다.
Doublewrite Buffer는 <strong>더티 페이지들을 디스크에 쓰기 전, 해당 페이지들의 원본 전체를 기록해 두는 디스크 상의 저장 영역</strong>입니다.
장애 복구 과정에서, 특정 페이지가 Torn Page 상태라면, Redo Log를 적용하기 전 <strong>Doublewrite Buffer 파일에서 해당 페이지의 복사본을 찾아 사용</strong>합니다.</p>
<blockquote>
<p>Doublewrite Buffer에 대해 더 궁금하다면?
<a href="https://dev.mysql.com/doc/refman/8.4/en/glossary.html#glos_doublewrite_buffer">https://dev.mysql.com/doc/refman/8.4/en/glossary.html#glos_doublewrite_buffer</a></p>
</blockquote>
<h1 id="마치며">마치며</h1>
<p>이번 포스팅에서는 No-FORCE 정책을 유지하면서, 동시에 Durability를 보장하기 위한 <strong>Redo Log</strong>와 <strong>WAL(Write-Ahead Logging)</strong>에 대해 살펴보았습니다.</p>
<p>다음 포스팅에서는, InnoDB가 <strong>STEAL 정책을 유지하면서 어떻게 Atomicity를 보장할 수 있는지</strong> 알아보도록 하겠습니다.
끝까지 읽어주셔서 감사합니다.</p>
<blockquote>
<p>참고 문서
<a href="https://dev.mysql.com/doc/refman/8.4/en/glossary.html">https://dev.mysql.com/doc/refman/8.4/en/glossary.html</a>
MySQL 8.4 Glossary</p>
<p><a href="https://tech.kakao.com/posts/721">https://tech.kakao.com/posts/721</a>
MySQL InnoDB Log에 대한 이해 - (1) - christy.seo, sun.j</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[WAL(Write-Ahead Logging) 알아보기 1편 - 버퍼 풀 관리 정책]]></title>
            <link>https://velog.io/@cookie-meringue/WALWrite-Ahead-Logging-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-1%ED%8E%B8-%EB%B2%84%ED%8D%BC-%ED%92%80-%EA%B4%80%EB%A6%AC-%EC%A0%95%EC%B1%85</link>
            <guid>https://velog.io/@cookie-meringue/WALWrite-Ahead-Logging-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-1%ED%8E%B8-%EB%B2%84%ED%8D%BC-%ED%92%80-%EA%B4%80%EB%A6%AC-%EC%A0%95%EC%B1%85</guid>
            <pubDate>Wed, 14 Jan 2026 19:11:39 GMT</pubDate>
            <description><![CDATA[<h1 id="0-서론">0. 서론</h1>
<h2 id="글의-목적">글의 목적</h2>
<p>안녕하세요 머랭입니다.
데이터베이스를 사용하는 애플리케이션을 개발하다 보면 수많은 트랜잭션을 커밋하게 됩니다.
그런데, <strong>여러분이 커밋한 트랜잭션이 실제로 디스크에 반영되지 않았을 수 있다는 사실을 알고 계신가요?</strong></p>
<p>디스크에 반영한다는 것은, 랜덤 I/O가 발생한다는 의미입니다.
만약 모든 트랜잭션마다 랜덤 I/O가 발생한다면, DBMS의 처리량은 매우 낮을 것입니다.
MySQL의 InnoDB는 이런 문제를 어떻게 해결했을까요?
몇 개의 포스팅을 통해 InnoDB가 높은 처리량을 보장하면서도 ACID를 지킬 수 있도록 하는 핵심 기술인 <strong>WAL(Write-Ahead Logging)</strong>에 대해 이야기해보고자 합니다.</p>
<blockquote>
<p>이번 포스팅에서는 WAL(Write-Ahead Logging)을 이해하기 위해 필요한 핵심 개념인 <strong>InnoDB의 버퍼 풀 관리 정책</strong>에 대해 설명드리겠습니다.</p>
</blockquote>
<h2 id="대상-독자">대상 독자</h2>
<ol>
<li><strong>Undo Log와 Redo Log에 대해서 들어봤지만, 확실하게 알지 못하는 개발자</strong><ul>
<li>Undo Log와 Redo Log가 무슨 역할을 하는지, 왜 존재하는지 이해할 수 있습니다.</li>
</ul>
</li>
<li><strong>InnoDB가 페이지를 어떻게 관리하는지 궁금한 개발자</strong><ul>
<li>추상적인 DML 뒤에서 InnoDB 스토리지 엔진이 페이지를 어떻게 관리하는지 이해할 수 있습니다.</li>
</ul>
</li>
</ol>
<h2 id="이번-글을-통해-얻어갈-수-있는-것들">이번 글을 통해 얻어갈 수 있는 것들</h2>
<ol>
<li>InnoDB가 효율적인 I/O를 위해 선택한 페이지 관리 방식에 대해 이해할 수 있습니다.</li>
<li>트랜잭션이 커밋된 후 디스크에 반영되기까지의 과정을 이해하고 설명할 수 있게 됩니다.</li>
<li>InnoDB가 택한 버퍼 관리 정책의 심각한 문제점들을 인지하게 됩니다.</li>
</ol>
<br>

<h1 id="1-버퍼버퍼-풀">1. 버퍼(버퍼 풀)</h1>
<p>데이터베이스의 가장 큰 병목은 언제나 디스크 I/O, 특히 랜덤 I/O 입니다.
버퍼는 디스크 I/O를 최소화하기 위해 페이지를 메모리(RAM)에 캐싱해두는 공간입니다.</p>
<p>InnoDB는 변형된 LRU 알고리즘을 통해 버퍼를 관리합니다.
변형된 LRU 알고리즘이 궁금하다면? <a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html">MySQL InnoDB 공식 문서</a></p>
<blockquote>
<ul>
<li><strong>데이터 캐시:</strong> 자주 쓰이는 데이터를 메모리에 올려두어 디스크 I/O 없이 바로 응답합니다.</li>
<li><strong>쓰기 지연:</strong> 데이터를 변경할 때 즉시 디스크에 기록하지 않고, 버퍼에서 먼저 변경한 뒤 Page Ceaner가 백그라운드에서 디스크에 기록합니다.</li>
</ul>
</blockquote>
<h3 id="page-cleaner">Page Cleaner</h3>
<p>Page Cleaner는 InnoDB의 <strong>백그라운드 스레드</strong>로, 버퍼에 있는 <strong>더티 페이지들을 주기적으로 디스크로 플러시(Flush)</strong>하여 데이터 손실을 방지하고 버퍼 풀 공간을 확보하는 역할을 합니다.</p>
<br>

<h1 id="2-버퍼-관리-정책">2 버퍼 관리 정책</h1>
<h2 id="21-steal--no-steal">2.1 STEAL / No-STEAL</h2>
<p>STEAL / No-STEAL 정책은 <strong>트랜잭션이 진행 중</strong>(커밋 전)일 때, <strong>버퍼에 존재하는 수정된 페이지를 디스크에 미리 쓸 수 있는지 여부</strong>를 결정합니다.</p>
<h3 id="steal">STEAL</h3>
<p>트랜잭션의 진행 여부와 관계없이, <strong>수정된 페이지를 언제든지 디스크에 쓸 수 있는 정책</strong>입니다.
버퍼가 부족하면, 버퍼 관리자는 아직 <strong>완료되지 않은 트랜잭션이 수정한 페이지라도 디스크에 기록</strong>하고 버퍼를 비울 수 있습니다.
<strong>아직 커밋되지 않은 데이터가 디스크에 존재</strong>하게 됩니다.</p>
<p>InnoDB를 포함한 대부분의 스토리지 엔진이 이 정책을 사용합니다.</p>
<blockquote>
<p>한정된 버퍼를 효율적으로 사용할 수 있습니다.</p>
</blockquote>
<h3 id="no-steal">No-STEAL</h3>
<p><strong>트랜잭션이 종료(커밋)될 때까지는 수정된 페이지를 절대 디스크에 쓰지 않는 정책</strong>입니다.
변경된 페이지를 계속해서 버퍼에 유지합니다.
<strong>디스크에는 항상 커밋된 데이터만 존재</strong>하게 됩니다.</p>
<blockquote>
<p>트랜잭션이 다루는 데이터가 매우 크면 그만큼 <strong>엄청난 양의 메모리 버퍼가 필요</strong>해집니다.</p>
</blockquote>
<br>

<h2 id="22-force--no-force">2.2 FORCE / No-FORCE</h2>
<p>FORCE / No-FORCE 정책은 <strong>트랜잭션이 커밋되는 시점</strong>에 <strong>수정된 모든 페이지를 반드시 디스크에 반영해야 하는지 여부를 결정</strong>합니다.</p>
<h3 id="force">FORCE</h3>
<p><strong>매 트랜잭션이 커밋되는 시점에 수정했던 모든 페이지를 디스크에 즉시 반영하는 정책</strong>입니다.
커밋 직후 장애가 발생해도, 이미 디스크 쓰기가 완료된 상태이므로 복구가 필요 없습니다.</p>
<blockquote>
<p><strong>매 트랜잭션 커밋 시마다 랜덤 I/O가 발생</strong>하므로, 성능이 매우 떨어집니다.</p>
</blockquote>
<h3 id="no-force">No-FORCE</h3>
<p><strong>트랜잭션이 커밋되어도 수정된 페이지를 디스크에 즉시 반영하지 않는 정책</strong>입니다.
InnoDB를 포함한 대부분의 스토리지 엔진이 이 정책을 사용합니다.</p>
<blockquote>
<p>랜덤 I/O 횟수가 크게 감소합니다.</p>
</blockquote>
<br>

<h1 id="3-버퍼-관리-정책의-문제점">3. 버퍼 관리 정책의 문제점</h1>
<p>InnoDB를 포함한 대부분의 스토리지 엔진은 <strong>STEAL &amp; No-FORCE 정책</strong>을 사용합니다.</p>
<blockquote>
<p><strong>한정된 버퍼를 효율적으로 사용하기 위해 STEAL 정책을 사용합니다.</strong>
<strong>랜덤 I/O를 최소화하기 위해 No-FORCE 정책을 사용합니다.</strong></p>
</blockquote>
<p>그러나, 두 정책을 사용하게 되면 더티 페이지가 실제 DB에 반영되는 시점과 단위가 모호해지기 때문에 <strong>Atomicity와 Durability를 보장할 수 없게 됩니다.</strong></p>
<h2 id="31-steal---atomicity-위반">3.1 STEAL - Atomicity 위반</h2>
<p>STEAL 정책은 버퍼를 효율적으로 사용하기 위해, 아직 커밋되지 않은 페이지를 디스크에 반영합니다.
이로 인해 장애 발생 시 Atomicity를 보장할 수 없습니다.</p>
<h3 id="시나리오">시나리오</h3>
<p><strong>1. 트랜잭션 시작 및 페이지 수정</strong>
디스크에서 페이지를 읽어와 버퍼 풀에서 데이터를 수정합니다.
<strong>수정할 페이지가 많아 수정 중간에 버퍼는 더티 페이지로 가득 찼습니다.</strong></p>
<p><strong>2. STEAL 발생</strong>
아직 데이터 수정이 더 필요합니다.
이때, STEAL 정책에 의해 <strong>Page Cleaner가 동작해 아직 커밋되지 않은 페이지를 디스크에 작성</strong>합니다.</p>
<p><strong>3. 갑작스러운 장애 발생</strong>
나머지 데이터를 수정하던 중, 갑자기 서버가 다운되었습니다.
<strong>트랜잭션은 커밋되지 못하고 비정상 종료</strong>되었습니다.</p>
<p><strong>4. 재부팅 후 Atomicity 위반</strong>
트랜잭션은 실패했으므로 데이터는 수정 전으로 롤백되어야 합니다.
그러나, <strong>Page Cleaner가 디스크에 작성한 페이지까지만 디스크에 반영</strong>되어있습니다.</p>
<br>

<h2 id="32-no-force---durability-위반">3.2 No-FORCE - Durability 위반</h2>
<p>No-FORCE 정책은 랜덤 I/O를 최소화하기 위해, 트랜잭션이 커밋되어도 수정된 페이지를 디스크에 즉시 반영하지 않고 한 번에 Flush합니다.</p>
<h3 id="시나리오-1">시나리오</h3>
<p><strong>1. 트랜잭션 시작 및 커밋</strong>
트랜잭션 커밋 시, 변경된 페이지는 더티 페이지 상태로 버퍼에만 존재합니다.
더티 페이지가 실제로 디스크에 반영되는 것을 기다리지 않습니다.
버퍼에 반영되었으니 트랜잭션 커밋은 성공적으로 이루어집니다.</p>
<p><strong>2. 갑작스러운 장애 발생</strong>
Page Cleaner가 더티 페이지들을 디스크에 작성하기 전, 갑자기 서버가 다운되었습니다.
<strong>버퍼는 RAM에 존재하므로, 버퍼에 존재하던 더티 페이지는 유실</strong>됩니다.</p>
<p><strong>3. 재부팅 후 Durability 위반</strong>
트랜잭션은 성공했었으므로, 재부팅 후에도 성공 상태로 남아있어야 합니다.
그러나, <strong>버퍼 유실로 인해 디스크에는 커밋 전 데이터만 존재</strong>합니다.</p>
<br>

<h1 id="마치며">마치며</h1>
<p>이번 포스팅에서는 랜덤 I/O로 인한 병목을 해결하기 위해 InnoDB가 도입한 <strong>버퍼</strong>, 이를 관리하는 <strong>Page Cleaner</strong>의 역할을 살펴보았습니다.
또한, 버퍼를 관리하기 위한 정책인 <strong>STEAL / No-STEAL</strong> 그리고 <strong>FORCE / No-FORCE</strong>에 대해 알아보았습니다.
InnoDB를 포함한 대부분의 스토리지 엔진은 <strong>STEAL &amp; No-FORCE 정책</strong>을 사용합니다.</p>
<p><strong>STEAL &amp; No-FORCE 정책은 버퍼를 효율적으로 사용하고 랜덤 I/O를 최소화한다는 장점</strong>이 있지만, <strong>Atomocity와 Durability를 보장하지 못한다는 치명적인 단점</strong>이 존재합니다.</p>
<p>다음 포스팅에서는, InnoDB가 이 단점들을 어떻게 해결했는지 알아보도록 하겠습니다.
끝까지 읽어주셔서 감사합니다.</p>
<blockquote>
<p>참고 문서
<a href="https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html">https://dev.mysql.com/doc/refman/8.4/en/innodb-buffer-pool.html</a>
MySQL 8.4 Glossary</p>
<p><a href="https://d2.naver.com/helloworld/407507">https://d2.naver.com/helloworld/407507</a>
DBMS는 어떻게 트랜잭션을 관리할까? - 오이석|NBP 서비스플랫폼개발센터</p>
<p><a href="https://tech.kakao.com/posts/721">https://tech.kakao.com/posts/721</a>
MySQL InnoDB Log에 대한 이해 - (1) - christy.seo, sun.j</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[RabbitMQ보다 중요한 AMQP 알아보기]]></title>
            <link>https://velog.io/@cookie-meringue/RabbitMQ%EB%B3%B4%EB%8B%A4-%EC%A4%91%EC%9A%94%ED%95%9C-AMQP-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@cookie-meringue/RabbitMQ%EB%B3%B4%EB%8B%A4-%EC%A4%91%EC%9A%94%ED%95%9C-AMQP-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sat, 10 Jan 2026 14:03:01 GMT</pubDate>
            <description><![CDATA[<h1 id="0-서론">0. 서론</h1>
<p><strong>&quot;RabbitMQ를 사용해 보셨나요? 그렇다면 AMQP가 무엇인지 아시나요?&quot;</strong></p>
<h3 id="글의-목적">글의 목적</h3>
<p>안녕하세요, 백엔드 개발자(지망생) 머랭입니다.
많은 개발자가 RabbitMQ를 사용하지만, 그 근간이 되는 AMQP의 깊은 곳까지는 들여다보지 못하곤 합니다.</p>
<p>AMQP는 크게 0-9-1 버전과 1.0 버전으로 나뉩니다. 언뜻 보면 1.0이 0-9-1의 상위 호환 버전처럼 느껴지지만, 실제로는 지향점이 완전히 다른 프로토콜입니다.</p>
<blockquote>
<ul>
<li><strong>AMQP 0-9-1</strong>은 Exchange, Queue, Binding 등 브로커 내부의 동작 모델을 구체적으로 명시했습니다.</li>
<li><strong>AMQP 1.0</strong>은 브로커 내부 구현을 각 벤더에게 맡기고, 전송 규약에 집중하는 추상적인 접근을 택했습니다.</li>
</ul>
</blockquote>
<p>RabbitMQ는 현대 개발 시장에서 가장 유명한 AMQP 0-9-1 구현체입니다.</p>
<blockquote>
<p>RabbitMQ 4.0이 출시되며 AMQP 1.0을 플러그인 형태로 지원하기 시작했지만, 여전히 전 세계 수많은 메시징 시스템은 AMQP 0-9-1 위에서 동작하고 있습니다.</p>
</blockquote>
<p><strong>이번 포스팅에서는 현대 개발 시장에서 활발하게 사용되는 AMQP 0-9-1 프로토콜의 공식 문서와 논문을 분석하며 얻은 본질적인 동작 원리를 공유하고자 합니다.</strong></p>
<h3 id="대상-독자">대상 독자</h3>
<ol>
<li><strong>RabbitMQ 등 AMQP 기반 브로커에 대한 기본 흐름을 알고 있는 개발자</strong><ul>
<li>그 중에서도, 내부 동작 방식에 대해 더 깊은 호기심을 가진 개발자.</li>
</ul>
</li>
<li><strong>&quot;왜&quot;를 찾는 멋진 개발자</strong><ul>
<li>AMQP가 왜 그렇게 설계되었는지 그 논리와 내부 동작이 궁금한 개발자.</li>
</ul>
</li>
</ol>
<p>단순히 <strong>&quot;어떤 라이브러리를 써서 어떻게 보낸다&quot;</strong>는 방법론을 넘어, AMQP 0-9-1 공식 문서를 탐구하며 얻은 지식을 바탕으로 &quot;AMQP는 왜 그렇게 동작하는지&quot;에 대한 본질적인 답을 찾아보고자 합니다.</p>
<h3 id="글을-통해-얻어갈-수-있는-것들">글을 통해 얻어갈 수 있는 것들</h3>
<p>라이브러리 메서드 하나로 메시지를 보낼 수 있는 편리한 시대에, 수백 페이지의 명세서를 읽으며 내부 구조를 탐구하는 이유는 명확합니다.
<strong>도구를 사용하는 것을 넘어, 기술의 본질을 이해하고 통제하는 힘을 가질 수 있습니다.</strong></p>
<ol>
<li><strong>AMQP에 대한 깊은 이해</strong><ul>
<li>Exchange, Queue, Binding이 맺는 유기적인 관계와 라우팅 논리를 명확히 이해할 수 있습니다.</li>
</ul>
</li>
<li><strong>물리적 실체에 대한 이해</strong><ul>
<li>데이터(메시지)가 네트워크 위에서 어떻게 Frame 단위로 쪼개지고 조립되는지 파악할 수 있습니다.</li>
</ul>
</li>
<li><strong>코더에서 시스템 설계 능력을 갖춘 개발자로의 진화</strong><ul>
<li><strong>&quot;~ 하니까 되던데?&quot;</strong> 와 같은 비논리적인 사고방식에서, 근본을 이해하고 시스템을 설계하는 개발자로 나아갈 수 있습니다.</li>
</ul>
</li>
</ol>
<br>

<h1 id="1-amqp의-탄생-배경">1. AMQP의 탄생 배경</h1>
<p>AMQP는 월스트리트의 투자 은행 JPMorgan에서 시작되었습니다.</p>
<p>기존의 상용 메시징 미들웨어는 <strong>메시지 형식이 제각각</strong>이었고, <strong>특정 벤더에 종속적</strong>이라는 단점이 있었습니다.</p>
<blockquote>
<p>ex) IBM의 메시징 미들웨어가 전송한 메시지를 Microsoft의 Consumer가 처리할 수 없었습니다.
이를 가능하게 하려면 <strong>메시지 형식 변환 브릿지(어댑터)</strong>를 개발해야 했습니다.</p>
</blockquote>
<p>AMQP 개발자들은 메시징 기술이 TCP/IP처럼 누구나 사용할 수 있는 공용어가 되기를 원했습니다.
특정 벤더의 기술이나 프로토콜에 얽매이지 않고, <strong>이기종 시스템 간에도 메시지를 신뢰성 있게 교환할 수 있는 개방형 표준을 만드는 것</strong>이 AMQP의 목표였습니다.</p>
<br>

<h1 id="2-철학">2. 철학</h1>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/1046f605-bccb-40a9-b928-20f2a2b52f5a/image.png" alt=""></p>
<h2 id="broker-중심-아키텍처">Broker 중심 아키텍처</h2>
<p>Broker는 복잡한 메시지 라우팅, Queue 관리를 비롯해 메시지 전송의 모든 책임을 집니다.</p>
<p>Consumer가 메시지를 잘 받았는지 확인될 때까지 Broker는 메시지를 보관하고 관리하며, 처리가 완료되면 Queue에서 메시지를 제거합니다.</p>
<h2 id="최소-한-번-전달at-least-once">최소 한 번 전달(At-least-once)</h2>
<p>메시지를 최소한 한 번 전달합니다.</p>
<p>메시지 유실을 방지하지만, 두 번 전달되는 것은 막을 수 없어 멱등한 비즈니스 로직을 작성해야 합니다.</p>
<h2 id="pub-sub-기반-push-전송">Pub-Sub 기반 Push 전송</h2>
<p>Consumer가 데이터를 가져오기 위해 대기하거나 주기적으로 확인할 필요가 없습니다.</p>
<p>Broker는 새로운 메시지가 Queue에 도착하면 연결된 Consumer에게 즉시 전달합니다.</p>
<p>이 과정에서, prefetch 메커니즘을 통해 Consumer가 처리 가능한 메시지 양을 Broker에 알림으로써, 무분별한 Push로 인한 시스템 마비를 방지합니다</p>
<br>

<h1 id="3-핵심-구성-요소---공통">3. 핵심 구성 요소 - 공통</h1>
<h2 id="31-frame">3.1 Frame</h2>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/3830a326-6b5d-41af-aa33-95e82ea5ac4b/image.png" alt=""></p>
<h3 id="설명">설명</h3>
<p>Frame은 AMQP 통신에서 네트워크를 타고 흐르는 데이터의 최소 단위입니다.</p>
<p>TCP는 데이터 경계가 없는 스트림 방식이기 때문에, 어디서부터 어디까지가 하나의 데이터 단위인지 구분하기 위해 모든 데이터를 Frame으로 포장해 주고받습니다.</p>
<p>Frame은 네 가지로 분류됩니다.</p>
<ul>
<li>명령을 주고받기 위한 <strong>Method Frame</strong></li>
<li>메시지의 메타데이터를 주고받기 위한 <strong>Content Header Frame</strong></li>
<li>메시지의 실제 데이터를 주고받기 위한 <strong>Content Body Frame</strong></li>
<li>Peer 간 하트비트를 주고받기 위한 <strong>Heartbeat Frame</strong></li>
</ul>
<h3 id="구조">구조</h3>
<ul>
<li><strong>type</strong>: Frame의 유형을 나타냅니다.<ul>
<li>1: Method Frame</li>
<li>2: Content Header Frame</li>
<li>3: Content Body Frame</li>
<li>4: Heartbeat Frame</li>
</ul>
</li>
<li><strong>channel</strong>: 해당 Frame이 속한 Channel 번호입니다.</li>
<li><strong>size</strong>: payload 영역의 총 바이트 수입니다.</li>
<li><strong>payload</strong>: 실제 데이터가 담기는 공간입니다.</li>
<li><strong>frame-end</strong>: Frame의 끝을 알리는 특수한 값입니다.</li>
</ul>
<br>

<h2 id="32-methodmethod-frame">3.2 Method(Method Frame)</h2>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/54d74afd-6057-44a5-8ad0-4b52c329a449/image.png" alt=""></p>
<h3 id="설명-1">설명</h3>
<p>AMQP는 기능들을 Class로 묶고, 그 안의 동작을 Method로 정의합니다.</p>
<p>Method Frame은 Peer간 명령을 주고받기 위해 사용되는 Frame입니다.</p>
<p><strong>“test-queue라는 이름의 큐를 생성해라”</strong> 혹은 <strong>“A Exchange를 제거해라”</strong>와 같은 명령 데이터를 주고받기 위해 사용됩니다.</p>
<p>개인적으로 CPU instruction set architecture와 비슷한 개념이라고 느꼈습니다.</p>
<h3 id="구조-1">구조</h3>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/ecf67cf7-511c-4cb3-8908-51368a75f6f1/image.png" alt=""></p>
<ul>
<li><strong>class-id</strong>: Method가 저장되어있는 Class의 ID입니다.</li>
<li><strong>method-id</strong>: 해당 Class의 명령 번호입니다.<ul>
<li>예: Reject(거절) = 90입니다.</li>
</ul>
</li>
<li><strong>arguments</strong>: 명령을 수행하는 데에 필요한 매개변수들입니다.</li>
</ul>
<br>

<h2 id="33-message">3.3 Message</h2>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/7938fd1d-8200-49ad-8595-2690a509e156/image.png" alt=""></p>
<h3 id="설명-2">설명</h3>
<blockquote>
<p>A message is the atomic unit of processing of the middleware routing and queuing system. Messages carry a content, which consists of a content header, holding a set of properties, and a content body, holding an opaque block of binary data.</p>
</blockquote>
<blockquote>
<p>메시지는 미들웨어 라우팅 및 대기열 시스템의 처리를 위한 원자 단위입니다. 메시지는 콘텐츠 헤더, 속성 세트, 불투명한 이진 데이터 블록을 포함하는 콘텐츠 본문으로 구성된 콘텐츠를 전달합니다.
<em>AMQP 0-9-1 공식 문서 중..</em></p>
</blockquote>
<p>메시지는 AMQP 시스템에서 데이터 이동의 최소 단위입니다.
논리적인 최소 단위이며, 실제로는 Frame이 데이터 이동의 최소 단위입니다.</p>
<ul>
<li><strong>영속성(Persistence)</strong>: 영속화되어 디스크에 저장될 수 있습니다.</li>
<li><strong>우선순위(Priority)</strong>: 우선순위를 가질 수 있습니다. 우선순위가 높은 메시지가 먼저 처리되거나, 우선순위가 낮은 메시지가 우선적으로 폐기됩니다.</li>
<li><strong>불투명성(Opaque)과 불변성(Immutable)</strong>: Broker는 메시지의 본문을 확인하거나 수정해선 안됩니다.</li>
</ul>
<h3 id="구조-2">구조</h3>
<p>메시지는 <strong>Content Header</strong>와 <strong>Content Body</strong>라는 두 계층으로 설계되었습니다.</p>
<p><strong>메시지를 전달하는 주체는 Content Header 영역의 데이터만 사용</strong>해 메시지를 전달합니다.</p>
<p><strong>Content Header:</strong> 메타데이터 영역으로, 메시지의 속성(Properties)이 담겨 있습니다.</p>
<ul>
<li><strong>body-size</strong>: Content Body 영역의 크기 값입니다.</li>
<li><strong>delivery-mode</strong>: 영속화 옵션으로, 메시지를 메모리에만 저장할지 디스크에도 저장할지 설정할 수 있습니다.</li>
<li><strong>priority</strong>: 우선순위(0~9)로, 우선순위가 높은 메시지는 먼저 처리됩니다.</li>
<li><strong>reply-to</strong>: 응답을 받을 Queue의 이름을 명시하여 Request-Response 패턴을 구현할 수 있습니다.</li>
</ul>
<p><strong>Content Body:</strong> 실제로 애플리케이션이 전달하고자 하는 비즈니스 데이터가 담겨 있습니다.</p>
<ul>
<li>바이너리 형태로 저장되며, Producer와 Consumer를 제외한 모든 Peer는 메시지의 본문을 확인하거나 수정해선 안됩니다.<ul>
<li>Zero-Copy를 통해 메시지 처리량을 높이기 위함입니다.</li>
</ul>
</li>
</ul>
<br>

<h2 id="34-connection--channel">3.4 Connection &amp; Channel</h2>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/7bcc59e4-474a-4c17-9128-36f3427b111f/image.png" alt=""></p>
<h3 id="설명-3">설명</h3>
<p>AMQP에서 통신은 물리적 연결인 Connection과 논리적 통로인 Channel의 이중 구조로 이루어집니다.</p>
<p>Channel은 메시지를 Frame 단위로 분해하여 전송하는 실질적인 데이터 스트리밍의 주체입니다.</p>
<ul>
<li><p><strong>Connection:</strong> Peer 간 맺는 물리적 TCP 연결로, Peer들은 Connection을 통해 여러 Frame을 전송합니다.</p>
<ul>
<li><p>Connection으로 전송되는 메시지는 여러 개의 Frame으로 나뉘어집니다. (메시지 분할은 Channel이 수행)</p>
</li>
<li><p>덕분에 멀티스레드 환경에서 여러 메시지를 동시에 전송할 수 있습니다.</p>
</li>
<li><p>단일 TCP 연결은 대역폭 한계가 존재하므로, 여러 개의 Connection을 생성해 Connection 풀을 구성할 수 있습니다.</p>
<blockquote>
<p>만약 메시지가 Frame 단위로 나뉘어지지 않는다면?
한 스레드가 Connection을 독점하고 자신이 보낼 모든 메시지를 보낸 후 Connection을 반납합니다.</p>
</blockquote>
</li>
<li><p><em>그 과정에서 다른 스레드는 Blocking됩니다.*</em></p>
</li>
<li><p><em>결과적으로, 처리량이 낮아집니다*</em></p>
</li>
</ul>
</li>
<li><p><strong>Channel:</strong> Connection 내부에 생성되는 논리적인 가상 통로로, 메시지를 Frame으로 쪼개 Connection으로 전송하는 동작을 추상화해 제공하는 인터페이스입니다.</p>
<ul>
<li>Channel을 여는 행위는 Peer에게 지금부터 이 Channel 번호로 대화하자는 합의를 보내는 과정입니다. <strong>TCP 연결을 맺는 것과는 다릅니다.</strong></li>
<li>Peer에게 Channel.Open Frame을 전송해 Channel 생성을 알립니다.</li>
<li>Peer는 Channel.Open-Ok 메서드 Frame을 응답해 Channel 생성을 합의합니다.</li>
<li>Channel을 여는 행위는 2번의 네트워크 비용을 요구하기 때문에, 여러 Channel을 생성해 Channel 풀을 구성할 수 있습니다.</li>
</ul>
</li>
</ul>
<br>

<h2 id="35-routing-key">3.5 Routing Key</h2>
<h3 id="설명-4">설명</h3>
<p>Routing Key는 Producer가 메시지를 발행할 때, 메시지와 함께 전송하는 문자열입니다.</p>
<p>Exchange는 Routing Key를 사용해 메시지를 어떤 Queue로 전달할지 결정합니다.
Producer는 메시지가 정확히 어떤 Queue로 전달되어야 하는지에 관심가지지 않고 Routing Key만 Exchange에 전달합니다.
덕분에 Broker는 단순한 1:1 전달을 넘어 하나의 메시지를 여러 Queue에 전달하거나 특정 조건에 맞는 곳으로만 보내는 복잡한 라우팅을 로직을 수행할 수 있습니다.
대소문자를 구분하므로, 설계 시 참고해야 합니다.</p>
<p><strong>Point-To-Point 방식</strong>을 사용하려면 Routing Key를 Queue의 이름으로 설정합니다.
<strong>Pub-Sub 방식</strong>을 사용하려면 데이터의 성격을 나타내는 계층적 값을 사용합니다.</p>
<blockquote>
<p>예: <code>order.new</code>, <code>payment.success</code></p>
</blockquote>
<h3 id="구조-3">구조</h3>
<p>255 octet 크기의 문자열 형식으로 이루어집니다.</p>
<blockquote>
<p>octet: 8비트 크기의 데이터 단위로, 과거 1 바이트가 8비트이지 않은 경우가 있어 생긴 단위입니다.</p>
</blockquote>
<br>

<h1 id="4-핵심-구성-요소---producer">4. 핵심 구성 요소 - Producer</h1>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/7646d499-fd47-401b-b68a-fe5d2c156baa/image.png" alt=""></p>
<h3 id="설명-5">설명</h3>
<p>Producer는 메시지를 생성하고 Broker를 향해 발행하는 클라이언트 애플리케이션입니다.
Producer는 Queue에 메시지를 직접 발행하지 않고, <strong>메시지와 Routing Key를 Exchange에게 전송</strong>합니다.
메시지를 Queue에 전달하는 과정은 Broker에서 이루어집니다.</p>
<p>높은 처리량을 위해 Producer → Broker 간 메시지 수신응답(ACK/NACK)은 이루어지지 않습니다.</p>
<blockquote>
<p>RabbitMQ는 확장 기능으로 이 기능을 제공합니다. 신뢰성이 중요한 경우 사용할 수 있습니다.</p>
</blockquote>
<h3 id="동작">동작</h3>
<ol>
<li><p><strong>메시지 전송 시작 Frame 전송</strong></p>
<ul>
<li><p>메시지를 여러 Frame으로 쪼갠 후 모든 Frame 헤더에 Channel 번호를 기입합니다.</p>
</li>
<li><p>Basic.Publish Method Frame을 생성해 Connection으로 전송합니다.</p>
<ul>
<li><p>Basic.Publish Method Frame은 <strong>메시지 생산 시작을 위한 Method Frame 입니다.</strong></p>
</li>
<li><p>Basic.Publish Method Frame에는 다음 Arguments가 포함됩니다.</p>
</li>
<li><p><strong>Exchange 이름</strong></p>
</li>
<li><p><strong>Routing Key</strong></p>
</li>
<li><p><strong>Mandatory(필수 전달) 플래그</strong></p>
<ul>
<li>해당 Routing Key와 매칭되는 Queue가 존재하지 않을 때 Basic.Return Method Frame을 통해 Producer에게 메시지를 반환합니다.</li>
<li>Basic.Return Method Frame은 <strong>메시지 라우팅 실패를 알리기 위한 Method Frame입니다.</strong></li>
</ul>
</li>
<li><p><strong>Immediate(즉시 전달) 플래그</strong></p>
<ul>
<li><p>Queue에서 즉시 메시지를 소비할 수 있는 Consumer가 존재하지 않을 때 Basic.Return Method Frame을 통해 Producer에게 메시지를 반환합니다.</p>
<blockquote>
<p>RabbitMQ 3.0 이상에서는 다중 Consumer 에 대한 메시지 소비 체크 오버헤드로 인해 Immediate 플래그를 지원하지 않습니다.</p>
</blockquote>
</li>
</ul>
</li>
</ul>
</li>
<li><p>Basic.Publish Method Frame을 전달받은 Broker는 이제 해당 Frame을 보낸 Channel에서 메시지 Frame을 보낼 것임을 인지합니다.</p>
</li>
</ul>
</li>
<li><p><strong>메시지 Frame 전송</strong></p>
<ul>
<li>Broker에게 메시지의 Content Header Frame을 전송합니다.<ul>
<li>Broker는 Content Header Frame의 body size를 확인합니다.</li>
</ul>
</li>
<li>Broker에게 Content Body Frame들을 전송합니다.<ul>
<li>Broker는 전달받는 Content Body Frame들의 데이터 합계가 body size와 일치할 때까지 계속해서 Content Body Frame을 수신합니다.</li>
</ul>
</li>
</ul>
</li>
</ol>
<br>

<h1 id="5-핵심-구성-요소---broker">5. 핵심 구성 요소 - Broker</h1>
<p>AMQP의 핵심인 Broker는 메시지의 수신, 라우팅, 보관 및 전달을 총괄하는 시스템입니다.</p>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/7a47c3a0-055d-468f-9255-68372c7e7d5d/image.png" alt=""></p>
<h2 id="51-exchange">5.1 Exchange</h2>
<h3 id="설명-6">설명</h3>
<p>Exchange는 Producer로부터 수신한 메시지를 하나 이상의 Queue 혹은 Exchange로 전달하기 위한 라우팅 엔진입니다.
라우팅 규칙에 따라 하나의 메시지를 여러 Queue 혹은 Exchange에 동시에 전달할 수 있습니다.
메시지의 Content Body에는 관여하지 않으며, 수신한 데이터를 변경 없이 그대로 전달합니다.</p>
<h3 id="구조-4">구조</h3>
<p>가장 중요한 속성 세 가지에 대해 설명하겠습니다.</p>
<ul>
<li><strong>name</strong>: Exchange를 식별하기 위한 고유한 이름입니다.</li>
<li><strong>type</strong>: 메시지 라우팅 알고리즘입니다.<ul>
<li><strong>Direct Exchange</strong>(Unicast): Routing Key와 Binding Key가 정확히 일치하는 바인딩의 Queue 혹은 Exchange로 전달합니다.</li>
<li><strong>Topic Exchange</strong>(Multicast): Binding Key 패턴(예: <code>order.*</code>)이 Routing Key(예: <code>order.new</code>)에 매칭되는 바인딩의 Queue 혹은 Exchange로 메시지를 전달합니다.</li>
<li><strong>Headers Exchange</strong>(Multicast): Routing Key와 Binding Key 대신 메시지의 Content Header 내 headers 테이블의 속성값을 기준으로 전달합니다.</li>
<li><strong>Fanout Exchange</strong>(Broadcast): Routing Key를 무시하고 해당 Exchange에 바인딩된 모든 Queue 혹은 Exchange로 메시지를 전달하는 브로드캐스트 방식을 수행합니다.</li>
</ul>
</li>
<li><strong>durability</strong>: Broker 재시작 시 Exchange 보존 여부입니다<ul>
<li><strong>true: 비휘발성 Exchange</strong><ul>
<li>Exchange 정보를 디스크에 저장합니다.</li>
<li>Broker 재시작 후에도 비휘발성 Exchange들은 유지됩니다.</li>
</ul>
</li>
<li><strong>false: 휘발성 Exchange</strong><ul>
<li>Broker가 종료되면 휘발성 Exchange들은 사라집니다.</li>
<li>재시작 후 다시 사용하려면 새로 생성해야 합니다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="동작-1">동작</h3>
<ol>
<li><strong>서버의 메시지 수신</strong><ul>
<li>Channel을 통해 들어온 Basic.Publish Method Frame을 읽어 Exchange Name과 Routing Key를 확인합니다.</li>
<li>이후 도착한 Content Header와 Content Body Frame을 조립하여 완성된 메시지 객체를 생성합니다.</li>
<li>최종적으로 Exchange는 메시지를 수신하게 됩니다.</li>
</ul>
</li>
<li><strong>Binding 테이블을 통한 메시지 라우팅</strong><ul>
<li>각 Exchange는 Binding Table을 참조하며, 이 Binding Table에는 Binding 목록이 저장되어 있습니다.<ul>
<li>Binding Table은 Broker의 메타데이터 저장소에 존재합니다.</li>
</ul>
</li>
<li>Binding Table을 통해 연결(바인딩)할 수 있는 Queue를 찾아 메시지를 전달합니다.</li>
</ul>
</li>
</ol>
<br>

<h3 id="511-direct-exchange">5.1.1 Direct Exchange</h3>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/9c64cac1-2748-4e6a-8a82-1c3a8f8dc178/image.png" alt=""></p>
<p>Routing Key와 Binding Key가 정확히 일치하는 바인딩의 Queue 혹은 Exchange로 메시지를 전달합니다.</p>
<blockquote>
<p><strong>Routing Key</strong>: <code>order.new</code>, <strong>Binding Key:</strong> <code>order.new</code></p>
<ul>
<li>Binding Key가 정확히 일치하기 때문에 메시지가 전달됩니다.</li>
</ul>
<p><strong>Routing Key</strong>: <code>order.new</code>, <strong>Binding Key:</strong> <code>order.cancel</code></p>
<ul>
<li>Binding Key가 일치하지 않기 때문에 메시지가 전달되지 않습니다.</li>
</ul>
</blockquote>
<br>

<h3 id="512-fanout-exchange">5.1.2 Fanout Exchange</h3>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/ad7ebf3f-4ce8-4663-92ce-dd7f5f7bf808/image.png" alt=""></p>
<p>Binding Key, arguments를 사용하지 않고, 모든 Binding의 목적지로 메시지를 전달합니다(Broadcast).</p>
<br>

<h3 id="513-topic-exchange">5.1.3 Topic Exchange</h3>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/b0b19ff5-7d8a-4fc3-8fd7-1c6dd78a6dd9/image.png" alt=""></p>
<p>Binding Key 패턴이 Routing Key와 매칭되는 Binding의 목적지에만 메시지를 전달합니다.</p>
<blockquote>
<p><strong>Routing Key</strong>: <code>order.new</code>, <strong>Binding Key:</strong> <code>order.*</code></p>
<ul>
<li>Binding Key 패턴이 Routing Key와 매칭되기 때문에 메시지가 전달됩니다.</li>
</ul>
<p><strong>Routing Key</strong>: <code>payment.new</code>, <strong>Binding Key:</strong> <code>order.*</code></p>
<ul>
<li>Binding Key 패턴이 Routing Key와 매칭되지 않기 때문에 메시지가 전달되지 않습니다.</li>
</ul>
</blockquote>
<br>

<h3 id="514-headers-exchange">5.1.4 Headers Exchange</h3>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/97a18c0b-c0bf-448e-a9ff-5f62cf626693/image.png" alt=""></p>
<p>메시지 Content Headers 내의 headers 테이블 내 속성과 arguments 내의 속성을 비교해 메시지를 전달할 지 결정합니다.</p>
<blockquote>
<p><strong>headers 테이블</strong>: <code>{ ”membership”: “VIP”,  “foodType”: “PIZZA” }</code>
<strong>arguments</strong>: <code>{ ”membership”: “VIP”,  “foodType”: “PIZZA”, &quot;x-match&quot;: &quot;all” }</code></p>
<ul>
<li>headers 테이블 내 속성과 arguments의 모든(<code>all</code> 이기 때문) 속성이 일치하기 때문에 메시지가 전달됩니다.</li>
</ul>
<p><strong>headers 테이블</strong>: <code>{ “foodType”: “PIZZA” }</code>
<strong>arguments</strong>: <code>{ ”membership”: “VIP”,  “foodType”: “PIZZA”, &quot;x-match&quot;: &quot;any” }</code></p>
<ul>
<li>headers 테이블 내 속성과 arguments의 하나 이상의(<code>any</code> 이기 때문) 속성이 일치하기 때문에 메시지가 전달됩니다.</li>
</ul>
<p><strong>headers 테이블</strong>: <code>{ ”membership”: “VIP” }</code>
<strong>arguments</strong>: <code>{ ”membership”: “VIP”,  “foodType”: “PIZZA”, &quot;x-match&quot;: &quot;all” }</code></p>
<ul>
<li>headers 테이블 내 속성과 arguments의 모든(<code>all</code> 이기 때문) 속성이 일치하지 않기 때문에 메시지가 전달되지 않습니다.</li>
</ul>
</blockquote>
<br>

<h2 id="52-binding">5.2 Binding</h2>
<p><img src="https://velog.velcdn.com/images/cookie-meringue/post/e45862da-cfab-4889-bb45-18db1af14f3b/image.png" alt=""></p>
<h3 id="설명-7">설명</h3>
<p>Binding은 Exchange가 수신한 메시지를 다음 목적지로 전달하기 위한 논리적인 연결 규칙입니다.
단순히 Exchange-Queue 형태로 연결하는 것을 넘어, Exchange-Exchange 형태로 메시지 라우팅 경로를 체이닝할 수 있습니다.</p>
<h3 id="구조-5">구조</h3>
<ul>
<li><strong>source</strong>: 메시지를 보내는 출발지 Exchange의 이름입니다.</li>
<li><strong>destination</strong>: 메시지를 받는 목적지의 이름입니다.</li>
<li><strong>destination type</strong>: 목적지의 타입입니다.<ul>
<li>목적지는 Queue혹은 Exchange입니다.</li>
</ul>
</li>
<li><strong>binding key</strong>: 라우팅 시 Routing Key와 대조할 기준 패턴 문자열입니다.<ul>
<li>예: <code>order.new</code>, <code>order.*</code></li>
</ul>
</li>
<li><strong>arguments</strong>: Headers: Map을 기반으로 하는 필터 조건으로, Headers Exchange에서 사용합니다.<ul>
<li>arguments 내의 <code>x-match</code> 속성은 사전 정의 속성으로, <code>all</code>과 <code>any</code> 값을 가질 수 있습니다.<ul>
<li><code>all</code>: 메시지 Content Headers 내의 headers 테이블 내 속성이 <strong>전부 일치</strong>해야 메시지를 전달합니다.</li>
<li><code>any</code>: 메시지 Content Headers 내의 headers 테이블 내 속성이 <strong>하나라도 일치</strong>하면 메시지를 전달합니다.</li>
<li>예: <code>{ ”membership”: “VIP”,  “foodType”: “PIZZA”, &quot;x-match&quot;: &quot;all” }</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="동작-2">동작</h3>
<ol>
<li>각 Exchange는 Binding Table을 참조하며, 이 Binding Table에는 Binding 목록이 저장되어 있습니다.</li>
<li>Exchange는 메시지를 전달받으면, Binding Table을 통해 연결(바인딩)할 수 있는 Queue를 찾아 메시지를 전달합니다.</li>
<li>Binding을 어떻게 사용하는지는 Exchange 타입 별로 나뉩니다.</li>
</ol>
<br>

<h2 id="53-queuemessage-queue">5.3 Queue(Message Queue)</h2>
<h3 id="설명-8">설명</h3>
<p>Queue는 메시지가 최송 소비되기 전까지 보관되는 FIFO 버퍼입니다.
기본적으로 FIFO 구조이지만, 다중 소비자 환경이나 메시지 우선순위(Priority) 사용 시 엄격한 순서가 보장되지 않을 수 있습니다.</p>
<ul>
<li>이를 Weak-FIFO라 부릅니다.</li>
</ul>
<p>하나의 Queue에는 동일한 역할을 하는 여러 Consumer가 연결될 수 있습니다.</p>
<p>AMQP 모델에서 Queue는 단순한 저장소가 아니라 영리한 객체(Reasonably clever object)로 설계되었습니다.
메시지가 도착하면, Queue는 연결된 Consumer에게 즉시 메시지를 전달하려고 시도합니다.
다중 Consumer 존재 시, 일반적으로 라운드 로빈(Round-Robin) 방식으로 메시지를 분배합니다.</p>
<h3 id="구조-6">구조</h3>
<ul>
<li><strong>name</strong>: Queue를 식별하기 위한 고유한 이름입니다.</li>
<li><strong>durable</strong>: Broker 재시작 시 Queue 보존 여부입니다<ul>
<li><strong>true: 비휘발성</strong> Queue<ul>
<li>Queue 정보를 디스크에 저장합니다.</li>
<li>Broker 재시작 후에도 비휘발성 Queue들은 유지됩니다.</li>
</ul>
</li>
<li><strong>false: 휘발성</strong> Queue<ul>
<li>Broker가 종료되면 휘발성 Queue들은 사라집니다.</li>
<li>재시작 후 다시 사용하려면 새로 생성해야 합니다.</li>
</ul>
</li>
<li>durable 속성은 메시지의 영속화와는 관련이 없는 Queue 자체의 속성입니다.</li>
<li>메시지를 영속화 여부는 메시지의 Content Header에 delivery-mode를 기반으로 결정됩니다.</li>
<li>Queue가 Durable로 설정되어있더라도, 메시지가 Transient 라면 Broker 재시작 시 Queue는 보존되지만 메시지는 유실됩니다.</li>
</ul>
</li>
<li><strong>exclusive</strong>: 해당 Queue를 생성한 Connection만 접근할 수 있도록 하는 설정입니다.<ul>
<li>true인 경우, 해당 Queue를 생성한 Connection이 닫히면 Queue도 자동으로 삭제됩니다.</li>
</ul>
</li>
<li><strong>auto-delete</strong>: Queue를 사용하던 모든 Consumer가 연결을 끊으면 Queue가 자동으로 삭제됩니다.</li>
</ul>
<h3 id="동작-3">동작</h3>
<ol>
<li>Exchange는 Binding을 사용해 메시지를 Queue에 삽입합니다.</li>
<li>메시지가 도착하면, Queue는 연결된 Consumer에게 즉시 메시지를 전달합니다.</li>
<li>메시지가 Consumer로 전달된 후, 해당 메시지를 <strong>승인 대기</strong> 상태로 전환합니다<ul>
<li>이때, Consumer로부터 성공/실패 응답을 받기 전까지 해당 메시지에 배타적 락을 걸어 다른 Consumer로 이중 전송되는 것을 방지합니다.</li>
</ul>
</li>
<li>Consumer가 처리를 완료하고 승인(ACK) 신호를 보내면, Queue는 메시지를 완전히 제거합니다.</li>
<li>소비자가 승인 전 연결을 끊거나 부정 승인(Reject)을 보내면, Queue는 락을 해제하고 메시지를 다시 대기열에 넣거나 제거합니다.<ul>
<li>거절된 메시지는 Reject Method Frame의 requeue 속성에 따라 처리됩니다.<ul>
<li><strong>requeue = true</strong><ul>
<li>해당 메시지를 Queue의 맨 앞에 넣습니다. 이후 다시 Consumer에게 전달됩니다.</li>
</ul>
</li>
<li><strong>requeue = false</strong><ul>
<li>Queue에서 메시지를 제거합니다.</li>
</ul>
</li>
</ul>
</li>
<li>RabbitMQ는 DLX라는 특수한 Exchange로 메시지를 전송하기도 합니다.</li>
<li>RabbitMQ는 메시지의 재시도 카운트 개념을 통해 일정 횟수만 재시도하도록 하기도 합니다.</li>
</ul>
</li>
</ol>
<br>

<h1 id="6-consumer">6. Consumer</h1>
<h3 id="설명-9">설명</h3>
<p>Broker로부터 메시지를 전달받아 소비하는 주체입니다.
Push받을 수 있는 메시지의 최대 허용량을 설정할 수 있습니다.
이를 통해 자신의 처리 역량에 맞춰 메시지 Push 속도를 조절함으로써, 부하를 방지합니다.</p>
<h3 id="구조-7">구조</h3>
<ul>
<li><strong>consumer tag</strong>: Broker가 발급해주는 식별자로, Consumer를 유일하기 식별하기 위한 이름표입니다.<ul>
<li>Consumer가 Queue를 구독하면, Broker는 consumer tag를 발행합니다.(구독 시 원하는 consumer tag도 명시 가능)</li>
<li>Queue에 메시지가 들어오면, 해당 Queue를 구독하는 consumer tag를 찾아 해당 Channel로 메시지를 전달합니다.</li>
<li>Consumer 입장에서, Channel로부터 전달받은 메시지가 어떤 Queue로부터 온 것인지 구분하기 위해 Broker는 메시지 Frame에 consumer tag를 붙여서 보냅니다.</li>
</ul>
</li>
<li><strong>channel</strong>: Consumer가 메시지를 전달받기 위한 Channel입니다.</li>
<li><strong>acknowledgement mode</strong>: 메시지 수신응답 방식입니다.<ul>
<li><strong>acknowledgement mode: automatic</strong><ul>
<li>메시지를 수신받은 후, 즉시 ACK를 반환합니다.</li>
</ul>
</li>
<li><strong>acknowledgement mode: explicit</strong><ul>
<li>메시지를 수신받은 후, 클라이언트 로직(비즈니스 로직)을 수행한 다음 ACK를 반환합니다.</li>
</ul>
</li>
</ul>
</li>
<li><strong>prefetch-count:</strong> Consumer가 ACK를 보내기 전까지 Broker가 한 번에 보낼 수 있는 메시지의 최대 갯수입니다.<ul>
<li>메시지를 전달받은 Consumer가 메시지를 처리하는 동안, Broker는 ACK를 기다리지 않고, prefetch-count만큼 메시지를 계속해서 전달합니다.</li>
<li>Consumer의 메시지 처리 속도를 고려하기 위해 존재합니다.</li>
</ul>
</li>
</ul>
<h3 id="동작-4">동작</h3>
<ol>
<li>Broker에게 특정 Queue에 대한 Consumer 생성 명령을 전송합니다.<ul>
<li>Basic.Consume Method Frame을 통해 이루어집니다.</li>
<li>이 때, Basic.Consume Method Frame을 보낸 Channel 위에서 구독이 발생합니다.</li>
<li>Broker는 consumer tag를 생성한 후, Channel과 Queue 그리고 consumer tag를 연결하는 레코드를 생성합니다.<ul>
<li>구독 시 원하는 consumer tag를 명시할 수도 있습니다.</li>
</ul>
</li>
</ul>
</li>
<li>Broker가 메시지를 발송합니다.<ul>
<li>Broker는 Queue에 메시지 인입 시, 해당 Queue와 연결된 consumer tag를 찾고, 연결된 Channel로 메시지를 발송합니다.</li>
</ul>
</li>
<li>메시지를 전달받은 Consumer는 acknowledgement mode에 따라 ACK를 응답합니다.</li>
</ol>
<br>

<h1 id="7-마치며">7. 마치며</h1>
<p>여러분들은 그동안 Spring AMQP와 같은 라이브러리를 사용하며 메서드 하나로 메시지를 아주 편리하게 주고받았습니다.
잘 만들어진 도구들이 복잡한 내부 사정을 우아하게 감추어주었기 때문입니다.
이 방대한 명세서를 한 페이지씩 공부하지 않아도 비즈니스 로직을 구현하고 서비스를 배포하는 데에는 별다른 문제가 없습니다.</p>
<p><strong>사실, 저는 RabbitMQ를 사용/운영해 본 경험이 없습니다.</strong>
이 사실에 실망하셨나요? 혹은 <strong>&quot;써보지도 않았으면서 원리를 논하나?&quot;</strong>라는 의구심이 드시나요?</p>
<p><strong>‘사용’</strong>이란 무엇일까요?</p>
<blockquote>
<p><strong>사용하다:</strong> (사람이 사물을) 어떤 목적이나 기능에 맞게 필요로 하거나 소용이 되는 곳에 쓰다.
출처: 네이버 백과사전</p>
</blockquote>
<p>현대 개발 시대에서, 많은 개발자들은 자신이 작성한 메서드 아래에 숨어 있는 블랙박스에는 관심을 가지지 않고, ‘동작’하는 것에 관심을 가집니다.
이것이 나쁘다는 이야기는 아닙니다. 내부의 복잡한 구조를 모르더라도, 문제를 해결하기 위한 다양한 라이브러리 활용법을 아는 것도 훌륭한 능력입니다.</p>
<p>그러나, 저는 코드 한 줄을 작성하는 것보다는 동작 원리를 학습하는 것이 더 재미있어 보였습니다.
 RabbitMQ를 사용해 본 경험이 없더라도, 저는 이제 메시지 브로커를 잘 활용할 자신이 있습니다.</p>
<p>긴 글 읽어주셔서 감사합니다.</p>
<blockquote>
<p>참고 문서
<a href="https://www.amqp.org/specification/0-9-1/amqp-org-download">https://www.amqp.org/specification/0-9-1/amqp-org-download</a>
Advanced Message Queuing Protocol (AMQP) Protocol Specification, Version 0-9-1</p>
<p><a href="https://queue.acm.org/detail.cfm?id=1255424">https://queue.acm.org/detail.cfm?id=1255424</a>
Toward a Commodity Enterprise Middleware - John O&#39;Hara, JPMorgan</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>