<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>look-forest</title>
    <link>https://look-forest.tistory.com/</link>
    <description>학습 내용 정리 목적의 블로그.
세부 내용은 여기에 정리해둔 것을 참고하고,
전체 구조와 흐름을 파악하는데 집중하자.
</description>
    <language>ko</language>
    <pubDate>Sun, 14 Jun 2026 08:36:22 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>studyHub</managingEditor>
    <image>
      <title>look-forest</title>
      <url>https://tistory1.daumcdn.net/tistory/4469534/attach/a687ccfed8a949a987d78fd75de4ea58</url>
      <link>https://look-forest.tistory.com</link>
    </image>
    <item>
      <title>기본적인 마이크로서비스 구축해보기</title>
      <link>https://look-forest.tistory.com/224</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;기본적인 마이크로서비스 구축&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로젝트 아키텍처&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFXVnk/dJMcacCn8gy/CuFTu6Rwg4kiMxYsGMrOB1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFXVnk/dJMcacCn8gy/CuFTu6Rwg4kiMxYsGMrOB1/img.png&quot; data-alt=&quot;마이크로서비스마다 각각 다른 프레임워크(ex. Nest.js, Django),
각각 다른 DB(ex. MongoDB, PostgreSQL 등)를 써도 된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFXVnk/dJMcacCn8gy/CuFTu6Rwg4kiMxYsGMrOB1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFXVnk%2FdJMcacCn8gy%2FCuFTu6Rwg4kiMxYsGMrOB1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;408&quot; height=&quot;599&quot; data-origin-width=&quot;786&quot; data-origin-height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;마이크로서비스마다 각각 다른 프레임워크(ex. Nest.js, Django),
각각 다른 DB(ex. MongoDB, PostgreSQL 등)를 써도 된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 쓸 MSA를 구축하려면 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;각 Service와 DB를 전부 독립적인 서버에 실행시켜야&lt;/span&gt;&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로젝트의 심플함을 위해 로컬 컴퓨터 한 대에서 포트 번호만 다르게 해서 실행시킬 것이다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DB설계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[모놀리식 아키텍처]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpr6RI/dJMcaf6VBsg/r2HEVCfagZ8BCCXnPK2f21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpr6RI/dJMcaf6VBsg/r2HEVCfagZ8BCCXnPK2f21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpr6RI/dJMcaf6VBsg/r2HEVCfagZ8BCCXnPK2f21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdpr6RI%2FdJMcaf6VBsg%2Fr2HEVCfagZ8BCCXnPK2f21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;467&quot; height=&quot;291&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식 아키텍처에서는 위 그림과 같이 user_id를 참조한 채로 DB를 설계하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[마이크로서비스 아키텍처]&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;498&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n8qLy/dJMcaio4ogi/t8sK2O1o0X6ejqlwQRSCk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n8qLy/dJMcaio4ogi/t8sK2O1o0X6ejqlwQRSCk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n8qLy/dJMcaio4ogi/t8sK2O1o0X6ejqlwQRSCk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn8qLy%2FdJMcaio4ogi%2Ft8sK2O1o0X6ejqlwQRSCk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;486&quot; height=&quot;294&quot; data-origin-width=&quot;824&quot; data-origin-height=&quot;498&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 마이크로서비스에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;DB가 독립적으로 분리되어 있기 때문에 user_id를 참조할 수 없다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만&amp;nbsp;특정&amp;nbsp;게시글이&amp;nbsp;어떤&amp;nbsp;사용자의&amp;nbsp;게시글인지&amp;nbsp;파악하기&amp;nbsp;위해&amp;nbsp;user_id에&amp;nbsp;대한&amp;nbsp;정보는&amp;nbsp;있어야&amp;nbsp;한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&amp;nbsp;boards&amp;nbsp;테이블에서&amp;nbsp;user_id는&amp;nbsp;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;FK를&amp;nbsp;설정하지&amp;nbsp;않은&amp;nbsp;채로&amp;nbsp;컬럼을&amp;nbsp;생성&lt;/span&gt;&lt;/b&gt;하면&amp;nbsp;된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt; MSA에서는 DB를 독립적으로 분리시켜야하기 때문에 FK를 설정하지 않는 경우가 많다 &lt;/b&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://biz.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90%EB%8F%84-%EC%9D%B4%ED%95%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-msa-%EC%9E%85/dashboard?cid=338964&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;비전공자도 이해할 수 있는 MSA 입문/실전&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Architecture/MSA</category>
      <category>Microservice</category>
      <category>MSA</category>
      <category>Service</category>
      <category>마이크로서비스</category>
      <category>서비스</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/224</guid>
      <comments>https://look-forest.tistory.com/224#entry224comment</comments>
      <pubDate>Mon, 16 Mar 2026 23:02:59 +0900</pubDate>
    </item>
    <item>
      <title>MSA 기본 개념</title>
      <link>https://look-forest.tistory.com/223</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA(Microservice Architecture)란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 큰 애플리케이션을 여러 개의 작고 독립적인 서비스로 나누어 개발하고 배포하는 소프트웨어 개발 아키텍처.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 하나에 모든 API(결제 관련 API, 인증 관련 API, 상품 관련 API)를 전부 구현하는 &lt;b&gt;모놀리식&amp;nbsp;아키텍처&lt;/b&gt; 방식과 비교해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;897&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G1oqF/dJMcacoO7ap/FZY67TKjmytx0k6aJpYYwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G1oqF/dJMcacoO7ap/FZY67TKjmytx0k6aJpYYwK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G1oqF/dJMcacoO7ap/FZY67TKjmytx0k6aJpYYwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG1oqF%2FdJMcacoO7ap%2FFZY67TKjmytx0k6aJpYYwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;897&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;897&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 반해, &lt;b&gt;마이크로서비스&amp;nbsp;아키텍처(MSA)&lt;/b&gt;는 서비스에 필요한 API들을 하나의 프로젝트가 아닌 여러 개의 프로젝트로 나눠서 구현하는 방식이다. 아래 그림을 보면 결제 관련 API들끼리 모아놓은 프로젝트와, 인증 관련 API들끼리 모아놓은 프로젝트와, 상품 관련 API들끼리 모아놓은 프로젝트를 분리해서 구성했다. 그리고 MSA에서는 &lt;u&gt;독립적으로 분리된 하나의 프로젝트를 서비스&lt;/u&gt;(service)라고 부른다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;761&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boHkfv/dJMcahXXSSl/SFxrSps6q0axZu3WqXAShK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boHkfv/dJMcahXXSSl/SFxrSps6q0axZu3WqXAShK/img.png&quot; data-alt=&quot;보통 DB를 서비스마다 분리해서 쓴다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boHkfv/dJMcahXXSSl/SFxrSps6q0axZu3WqXAShK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboHkfv%2FdJMcahXXSSl%2FSFxrSps6q0axZu3WqXAShK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;761&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;761&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;보통 DB를 서비스마다 분리해서 쓴다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA가 도대체 어떤 특징과 장점을 갖고 있길래 많은 회사에서 MSA를 도입하는 걸까?&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA의 핵심 특징과 장점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA의 핵심적인 특징은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;독립성&lt;/b&gt;&lt;/span&gt;이다.&amp;nbsp;각 기능을 독립적으로 분리해서 프로젝트를 구성함으로써, 다양한 장점이 생기게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 서비스 별 독립적인 배포 &amp;rarr; 배포 사이클이 빨라짐&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 큰 프로젝트를 100명이 작업하는 것보다, 여러 개의 프로젝트로 나눠서 각 프로젝트를 관리하는 팀을 구성해 독립적으로 작업하는 게 훨씬 효율적이다. 그 이유는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 의사소통할 인원이 적기 때문에 프로젝트 진행이 빠르다.&lt;/li&gt;
&lt;li&gt;팀이 독립적으로 개발/배포를 진행할 수 있다.&lt;/li&gt;
&lt;li&gt;코드의 양이 비교적 작기 때문에 코드 작성, 테스트, 빌드 과정의 시간이 훨씬 적게 걸린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 데이터베이스의 독립적인 분리 &amp;rarr;&lt;span&gt; 대규모 트래픽 처리 용이&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;모놀리식 아키텍처에서는 단일 데이터베이스를 활용하는 구조를 많이 채택한다. 하지만 AWS의 단일 데이터베이스의 가장 좋은 성능의 데이터베이스로 셋팅을 한다고 하더라도 트래픽이 감당이 안 되는 순간이 온다.&amp;nbsp;&lt;br /&gt;읽기&amp;nbsp;전용&amp;nbsp;데이터베이스를&amp;nbsp;추가하거나&amp;nbsp;캐시를&amp;nbsp;활용해&amp;nbsp;트래픽을&amp;nbsp;줄이려는&amp;nbsp;시도를&amp;nbsp;하지만,&amp;nbsp;이런&amp;nbsp;셋팅을&amp;nbsp;다&amp;nbsp;해도&amp;nbsp;트래픽을&amp;nbsp;감당&amp;nbsp;못할&amp;nbsp;때가&amp;nbsp;있다.&amp;nbsp;이런&amp;nbsp;경우에&amp;nbsp;MSA&amp;nbsp;구조를&amp;nbsp;활용해&amp;nbsp;각&amp;nbsp;서비스&amp;nbsp;별로&amp;nbsp;데이터베이스를&amp;nbsp;따로따로&amp;nbsp;둠으로써&amp;nbsp;대규모&amp;nbsp;트래픽을&amp;nbsp;감당하기도&amp;nbsp;한다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. &lt;span data-token-index=&quot;0&quot;&gt;서비스를 독립적으로 분리 &amp;rarr; 장애 전파를 최소화&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식 서비스에서는 단일 데이터베이스를 사용하고 있기 때문에, 결제 API로 인해 데이터베이스에 장애가 나면 결제와 관련되지 않은 다른 기능들도 100% 같이 장애가 난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, &lt;u&gt;MSA 구조에서는 각 서비스 별로 데이터베이스도 독립적으로 분리해서 구축하기 때문에,&lt;/u&gt; 결제 서비스가 장애가 나더라도 다른 서비스에는 영향을 덜 끼친다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;언제 MSA를 도입하는게 적절할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;글로벌 기업들의 사례를 보면 크게 3가지 이유 때문에 MSA로 전환을 했다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;신속한 배포&lt;/li&gt;
&lt;li&gt;대규모 트래픽를 안정적으로 처리&lt;/li&gt;
&lt;li&gt;시스템 안정성 확보&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 MSA의 핵심적인 장점 3가지와 완전 일치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 모놀리식 아키텍처로 프로젝트를 관리하고 운영하면서 다음과 같은 문제를 만날 때 MSA 전환을 고민하면 된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;프로젝트 규모가 크거나 또는 개발자 인원이 너무 많아서 배포 사이클이 느려진 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모놀리식 아키텍처에서는 도저히 감당할 수 없는 트래픽이 발생하는 경우&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;모놀리식 아키텍처로 인한 &lt;b&gt;장애 전파의 횟수가 잦거나&lt;/b&gt;, &lt;b&gt;한 번의 장애 전파도 서비스에 치명적인 경우&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA로 전환하는데는 모놀리식 구조보다 훨씬 복잡하고 많은 시간과 비용이 드므로, 득실을 신중히 따져야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA인지 아닌지 판단하는 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA인지 아닌지를 판단할 때 핵심 개념은 &lt;span style=&quot;color: #cf5148;&quot; data-token-index=&quot;1&quot;&gt;&amp;lsquo;독립성&amp;rsquo;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은&amp;nbsp;기업들이&amp;nbsp;MSA로&amp;nbsp;전환하는&amp;nbsp;이유는&amp;nbsp;독립성으로&amp;nbsp;인해&amp;nbsp;생기는&amp;nbsp;장점들&amp;nbsp;때문이다.&amp;nbsp;따라서&amp;nbsp;독립성을&amp;nbsp;적용시켰냐&amp;nbsp;여부가&amp;nbsp;MSA를&amp;nbsp;판단하는&amp;nbsp;데&amp;nbsp;중요한&amp;nbsp;기준점이&amp;nbsp;된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MSA에서&amp;nbsp;얘기하는&amp;nbsp;&amp;lsquo;독립성&amp;rsquo;&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ntd8N/dJMcabcmg8a/NaOxoGrTLyNMk73xkmJmT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ntd8N/dJMcabcmg8a/NaOxoGrTLyNMk73xkmJmT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ntd8N/dJMcabcmg8a/NaOxoGrTLyNMk73xkmJmT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fntd8N%2FdJMcabcmg8a%2FNaOxoGrTLyNMk73xkmJmT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;768&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1. 각 서비스를 &lt;b&gt;독립적으로 개발 및 배포 가능해야&lt;/b&gt; 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스를 &lt;span style=&quot;color: #ee2323;&quot;&gt;독립적으로 개발 및 배포가 가능해야만&lt;/span&gt;, MSA의 핵심적인 장점(&amp;rsquo;&lt;span style=&quot;color: #ee2323;&quot;&gt;배포 사이클이 빠르다&lt;/span&gt;&amp;rsquo;)을 취할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2. 서비스 간&amp;nbsp;&lt;b&gt;API 통신 및 DB 분리&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스는 간에는 API로만 통신해야 한다. 가령, 다른 서비스의 DB에 직접적으로 접근해서 데이터를 가져오면 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래야 API를 제외한 다른 부분에서 의존성이 안 생겨서 &lt;span style=&quot;color: #ee2323;&quot;&gt;독립적인 개발/배포가 원할&lt;/span&gt;하게 가능해지고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB를 별도로 분리해서 써야 &lt;span style=&quot;color: #ee2323;&quot;&gt;DB 장애로 인한 장애 전파를 예방&lt;/span&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://biz.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90%EB%8F%84-%EC%9D%B4%ED%95%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-msa-%EC%9E%85/dashboard?cid=338964&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;비전공자도 이해할 수 있는 MSA 입문/실전&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Architecture/MSA</category>
      <category>Microservice</category>
      <category>MSA</category>
      <category>마이크로서비스</category>
      <category>마이크로서비스 아키텍처</category>
      <category>모놀리식 아키텍처</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/223</guid>
      <comments>https://look-forest.tistory.com/223#entry223comment</comments>
      <pubDate>Fri, 13 Mar 2026 23:27:29 +0900</pubDate>
    </item>
    <item>
      <title>DB 연동 메시지큐의 트랜잭션 처리(작성중)</title>
      <link>https://look-forest.tistory.com/222</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB&amp;nbsp;연동&amp;nbsp;메시지큐의&amp;nbsp;트랜잭션&amp;nbsp;처리&amp;nbsp;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DB 연동 메시지큐의 트랜잭션 처리 실습&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Publisher-confirms을 이용한 트랜잭션 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;트랜잭션 처리의 확장 - TCC의 이해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>Publisher-confirms</category>
      <category>TCC</category>
      <category>트랜잭션</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/222</guid>
      <comments>https://look-forest.tistory.com/222#entry222comment</comments>
      <pubDate>Fri, 13 Mar 2026 22:49:18 +0900</pubDate>
    </item>
    <item>
      <title>Dead Letter Queue 재처리와 Retry</title>
      <link>https://look-forest.tistory.com/221</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dead Letter Queue와 Dead Letter Exchange&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DLQ (Dead Letter Queue)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323; font-size: 16px; letter-spacing: 0px;&quot;&gt;실패한 메시지가 &lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;저장되는 큐&lt;/span&gt;. &lt;/b&gt;메시지가 큐에서 제대로 처리 되지 못할 경우 DLQ로 이동됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DLQ로 이동하는 경우&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NACK 처리나 거부: basic.reject 혹은 basic.nack&lt;/li&gt;
&lt;li&gt;TTL 만료&lt;/li&gt;
&lt;li&gt;Overflow: 큐에 설정된 최대 메시지 갯수를 초과하면 가장 오래된 메시지가 삭제되고 DLQ로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DLX (Dead Letter Exchange)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실패한 메시지를 &lt;b&gt;어디로 보낼지 라우팅하는 Exchange. &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; DLQ로 바로 보내는 것이 아니라&lt;/span&gt; RabbitMQ는 &lt;span style=&quot;color: #ee2323;&quot;&gt;Exchange를 통해 라우팅&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 구조&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Producer&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr; &lt;br /&gt;Exchange&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr; &lt;br /&gt;OrderQueue &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr; (reject / nack / ttl) &lt;br /&gt;&lt;b&gt;DeadLetterExchange&lt;/b&gt; &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr; &lt;br /&gt;&lt;b&gt;DeadLetterQueue&lt;/b&gt;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dead Letter Queue를 활용한 실패 메시지 재처리&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. DLQ와 DLX 빈 선언&lt;/h4&gt;
&lt;pre id=&quot;code_1773394459884&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	public static final String ORDER_COMPLETED_QUEUE = &quot;order_completed_queue&quot;;
	public static final String ORDER_EXCHANGE = &quot;order_completed_exchange&quot;;
	public static final String DLQ = &quot;deadLetterQueue&quot;;
	public static final String DLX = &quot;deadLetterExchange&quot;;

	@Bean
	public TopicExchange orderExchange() {
		return new TopicExchange(ORDER_EXCHANGE);
	}

	@Bean
	public TopicExchange deadLetterExchange() {
		return new TopicExchange(DLX);
	}

	// 메시지가 처리되지 못했을 경우 자동으로 Dead Letter Queue로 이동하도록 설정
	@Bean
	public Queue orderqueue() {
		// return new Queue(ORDER_COMPLETED_QUEUE, false);
		return QueueBuilder.durable(ORDER_COMPLETED_QUEUE)
			.withArgument(&quot;x-dead-letter-exchange&quot;, DLX) // Dead Letter Exchange 설정
			.withArgument(&quot;x-dead-letter-routing-key&quot;, DLQ) // Dead Letter Routing Key 설정
			.ttl(5000)
			.build();
	}

	@Bean
	public Queue deadLetterQueue() {
		return new Queue(DLQ);
	}

	@Bean
	public Binding orderComplededBinding() {
		return BindingBuilder.bind(orderqueue()).to(orderExchange()).with(&quot;order.completed.#&quot;); //bindingKey를 넣는건데, Spring API에서는 routingKey 파라미터
	}
	// Binding도 routing key를 기준으로 정의되기 때문에, AMQP에는 &amp;ldquo;binding key&amp;rdquo;라는 독립된 개념이 없다.
	//우리가 흔히 말하는 &amp;ldquo;binding key&amp;rdquo;는 AMQP 공식 용어라기보다는 설명 편의를 위한 표현에 가깝다.


	@Bean
	public Binding deadLetterBinding() {
		return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DLQ);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Ack/Nack 처리를 개발 시점에 직접 핸들링 하도록 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SimpleRabbitListenerContainerFactory를 이용하여 AcknowledgeMode 모드를 Manual로 설정.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AcknowledgeMode.MANUAL을 통해 메시지의 처리 결과를 RabbitMQ에 명시적으로 전달하고 &lt;br /&gt;basicAck , basicNack , basicReject 를 사용하여 메시지 재시도 및 DLQ 처리를 유연하게 구현할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1773394543335&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * RabbitMQ 설정을 수동으로 하는 클래스
 */
@EnableRabbit //RabbitMQ 리스너(@RabbitListener)을 스캔하여 동작하게 만드는 활성화 어노테이션(스프링부트는 자동)
@Configuration
public class RabbitMQManualConfig {
	@Bean
	public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
		SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory);
		factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
		return factory;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Producer 메시지 발행&lt;/h4&gt;
&lt;pre id=&quot;code_1773394600499&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OrderProducer {

	private final RabbitTemplate rabbitTemplate;

	public void sendShipping(String message) {
		rabbitTemplate.convertAndSend(
			RabbitMQConfig.ORDER_EXCHANGE,
			&quot;order.completed.shipping&quot;,
			message);
		System.out.println(&quot;[주문 완료. 배송 지시 메시지 생성 : &quot; + message + &quot;]&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Consumer 에서 메시지 실패 건에 대해 3번의 재시도 후 DLQ 이동&lt;/h4&gt;
&lt;pre id=&quot;code_1773395153759&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderConsumer {

	private static final int MAX_RETRIES = 3; // 최대 재시도 횟수
	private int retryCount = 0; // 현재 재시도 횟수

	// containerFactory: 리스너 컨테이너의 설정(ACK 모드, 재시도, prefetch, concurrency 등)을 바꾸기 위해 지정
	// @Header(&quot;amqp_deliveryTag&quot;): RabbitMQ가 메시지에 붙여주는 전달 식별자(ack/nack/reject에 필요)를 Spring이 꺼내서 주입해주는 것
	// Channel: RabbitMQ Java Client의 현재 연결/채널(프로토콜 세션) 객체로, basicAck/basicNack/basicReject 같은 저수준 AMQP 명령을 보내는 통로
	@RabbitListener(queues = RabbitMQConfig.ORDER_COMPLETED_QUEUE, containerFactory = &quot;rabbitListenerContainerFactory&quot;)
	public void processOrder(String message, Channel channel, @Header(&quot;amqp_deliveryTag&quot;) long tag) {
		try {
			// 실패 유발
			if (&quot;fail&quot;.equalsIgnoreCase(message)) {
				if (retryCount &amp;lt; MAX_RETRIES) {
					System.err.println(&quot;#### Fail &amp;amp; Retry: &quot; + message + &quot; (Retry Count: &quot; + retryCount + &quot;)&quot;);
					retryCount++;
					throw new RuntimeException(message);
				} else {
					System.err.println(&quot;#### 최대 횟수 초과, DLQ로 이동 시킴&quot;);
					retryCount = 0;
					// deliveryTag: 메시지 고유식별 태그, multiple: true면 deliveryTag 이하 메시지 전부, requeue: true면 다시 큐로, false면 DLQ or drop
					channel.basicNack(tag, false, false); // 메시지 거부, 재큐잉하지 않음 (DLQ로 이동)
					return;
				}
			}
			// 성공 처리
			System.out.println(&quot;# 성공: &quot; + message);
			channel.basicAck(tag, false); // 메시지 수동 ACK
			retryCount = 0;
		} catch (Exception e) {
			System.err.println(&quot;# error 발생 : &quot; + e.getMessage());
			try {
				// 실패 시 basicReject를 사용하여 메시지를 재처리 전송
				channel.basicReject(tag, true); //basicReject는 단일 메시지 거부 전용 (간단 버전), basicNack은 확장형 거부 (여러 메시지 가능)
			} catch (IOException ex) {
				System.err.println(&quot;# fail &amp;amp; reject message : &quot; + ex.getMessage());
			}
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. DLQ 컨슈머에서 메시지 수정하여 큐잉&lt;/h4&gt;
&lt;pre id=&quot;code_1773395276697&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OrderDLQConsumer {

	private final RabbitTemplate rabbitTemplate;

	@RabbitListener(queues = RabbitMQConfig.DLQ)
	public void process(String message) {
		System.out.println(&quot;DLQ Message Received: &quot; + message);

		try {
			String fixedMessage = &quot;success&quot;;

			rabbitTemplate.convertAndSend(RabbitMQConfig.ORDER_EXCHANGE,
				&quot;order.completed.shipping&quot;,
				fixedMessage
			);
			System.out.println(&quot;DLQ Message Sent: &quot; + fixedMessage);
		} catch (Exception e) {
			System.err.println(&quot;### [DLQ Consumer Error] &quot; + e.getMessage());
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;channel.basicReject(deliveryTag, requeue);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;channel.basicNack(deliveryTag, multiple, requeue); // DLQ로 메시지 이동&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 설정은 처리가 다소 복잡하고, 메서드 호출도 혼동되기 쉬우므로 이런 처리보다는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AMQP에서 제공하는 &lt;b&gt;RetryTemplate&lt;/b&gt;을 통해 좀더 명확하고 간단하게 기능을 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;[발전1] RetryTemplate을 통한 간편한 재처리 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AMQP는 RetryTemplate 을 통해 재시도 로직을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 Spring AMQP에서 AcknowledgeMode가 AUTO로 기본 세팅되어 있기 때문에 별도의 SimpleRabbitListenerContainerFactory를 통하지 않고 자동으로 Ack/Nack 처리가 가능하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;재시도 중 메시지가 성공적으로 처리되면 Spring AMQP가 자동으로 Ack를 전송&lt;/li&gt;
&lt;li&gt;모든 재시도가 실패하면 Nack를 보내고 RabbitMQ가 메시지를 DLQ로 이동&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ build.gradle 에 retry dependency 추가 필요&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RetryConfig 클래스에서 기본 설정 세팅하여 빈으로 선언&lt;/h4&gt;
&lt;pre id=&quot;code_1773408569113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RetryConfig {

	@Bean
	public RetryTemplate retryTemplate() {
		RetryTemplate retryTemplate = new RetryTemplate();
		// 재시도 정책 설정
		SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
		retryPolicy.setMaxAttempts(3); // 최대 재시도 횟수 설정
		// 백오프 정책 설정
		FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
		backOffPolicy.setBackOffPeriod(1000L); // 재시도 간격 설정 (1초)
		retryTemplate.setRetryPolicy(retryPolicy);
		retryTemplate.setBackOffPolicy(backOffPolicy);

		return retryTemplate;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer에서 retryTemplate으로 재시도 및 DLQ 이관&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;retryTemplate의 retryCount를 확인하여 세번 이하라면 throw e를 던짐으로써 (channel.basicReject(tag, true)) , RetryTemplate이 예외를 감지해서 BackOffPolicy에 따라 1초 후 네번까지 재실행(동일한 로직 실행)&lt;/p&gt;
&lt;pre id=&quot;code_1773408717313&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class OrderConsumer {

	private final RabbitTemplate rabbitTemplate;
	private final RetryTemplate retryTemplate;

	@RabbitListener(queues = RabbitMQConfig.ORDER_COMPLETED_QUEUE)
	public void consume(String message) {
		retryTemplate.execute(context -&amp;gt; {
			try {
				System.out.println(&quot;# 리시브 메시지: &quot; + message + &quot; | 시도 횟수: &quot; + (context.getRetryCount() + 1));

				if (&quot;fail&quot;.equals(message)) {
					throw new RuntimeException(message);
				}
				System.out.println(&quot;# 메시지 처리 성공: &quot; + message);
			} catch (Exception e) {
				if (context.getRetryCount() + 1 &amp;gt;= 3) {
					rabbitTemplate.convertAndSend(
						RabbitMQConfig.ORDER_TOPIC_DLX,
						RabbitMQConfig.DEAD_LETTER_ROUTING_KEY,
						message);
				} else {
					throw e; // RetryTemplate이 재시도를 수행하도록 예외를 다시 던짐
				}
			}
			return null;
		});
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;ACK/NACK를 수동으로 던질 필요가 없어 훨씬 깔끔&lt;/span&gt;하다!&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;[발전2]&lt;span&gt; &lt;/span&gt;Application.yml 설정을 통한 RetryTemplate 속성 정의&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;application.yml 에 rabbitmq listener retry 설정 추가&lt;/h4&gt;
&lt;pre id=&quot;code_1773408796497&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guestuser
    password: guestuser
    listener:
      simple:
        retry:
          enabled: true # 재시도 활성화
          initial-interval: 1000 # 첫 재시도 간격 1초
          max-attempts: 3 # 최대 재시도 횟수
          max-interval: 10000 # 최대 재시도 간격 10초
        default-requeue-rejected: false # 재시도 실패 시 자동으로 DLQ로 이동&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer 로직 변경&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 설정을 xml에 해두었기 때문에, execption만 던지면 retry 한다. 설정 횟수를 넘으면 DLQ로 간다.&lt;/p&gt;
&lt;pre id=&quot;code_1773409613526&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class OrderConsumer {
	private int retryCount;

	@RabbitListener(queues = RabbitMQConfig.ORDER_COMPLETED_QUEUE)
	public void consume(String message) {
		System.out.println(&quot;Received message: &quot; + message + &quot;count: &quot; + retryCount++);
		if (&quot;fail&quot;.equals(message)) {
			throw new RuntimeException(&quot;Processing failed. Retry: &quot; + message);
		}
		System.out.println(&quot;Message processed successfully: &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>Dead Letter Exchange</category>
      <category>Dead Letter Queue</category>
      <category>DLQ</category>
      <category>DLX</category>
      <category>Nack</category>
      <category>Retry</category>
      <category>RetryTemplate</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/221</guid>
      <comments>https://look-forest.tistory.com/221#entry221comment</comments>
      <pubDate>Fri, 13 Mar 2026 22:47:19 +0900</pubDate>
    </item>
    <item>
      <title>Routing Model을 이용한 Log 수집</title>
      <link>https://look-forest.tistory.com/220</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Routing 모델&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 Routing Key에 따라 특정 큐에 전달하는 기능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Direct Exchange와 Topic Exchange 모두에서 사용 가능.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 69.8805%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.5533%;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.7764%;&quot;&gt;&lt;b&gt;Direct Exchange&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 26.3361%;&quot;&gt;&lt;b&gt;Topic Exchange&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.5533%;&quot;&gt;&lt;b&gt;라우팅 키 매칭 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.7764%;&quot;&gt;정확히 일치&lt;/td&gt;
&lt;td style=&quot;width: 26.3361%;&quot;&gt;패턴 기반 (*, # 지원)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.5533%;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.7764%;&quot;&gt;매칭에 제한이 있음&lt;/td&gt;
&lt;td style=&quot;width: 26.3361%;&quot;&gt;매우 유연(다양한 패턴 매칭)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.5533%;&quot;&gt;&lt;b&gt;사용 사례&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.7764%;&quot;&gt;단순 명확한 목적의 라우팅&lt;/td&gt;
&lt;td style=&quot;width: 26.3361%;&quot;&gt;복잡하고 동적인 라우팅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Topic&amp;nbsp;의&amp;nbsp;경우&amp;nbsp;패턴&amp;nbsp;매칭&amp;nbsp;기반&amp;nbsp;라우팅으로&amp;nbsp;binding&amp;nbsp;key와&amp;nbsp;매칭되는&amp;nbsp;메시지만&amp;nbsp;수신
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;* : 한 단어에 해당하는 모든 단어 수신&lt;/li&gt;
&lt;li&gt;# : 0개 이상의 단어 (와일드 카드)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 특징&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;고성능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 필요한 곳에만 전달하기 때문에, 네트워크 부하 감소 효과가 있고 브로드캐스팅보다 자원 효율적 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;라우팅 키 기반의 메시지 분배
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 큐는 &lt;b&gt;하나 이상의 라우팅 키&lt;/b&gt;와 &lt;b&gt;매칭&lt;/b&gt;&lt;br /&gt;(Direct Exchange의 경우 라우팅 키가 정확히 일치해야 해서 1:1 로 동작한다고 알고 있는데, &lt;br /&gt;동일한 라우팅 키로 여러 큐에 바인딩 할 수 있으므로 1:N으로 동작)&lt;/li&gt;
&lt;li&gt;Fanout 방식과 달리, 메시지가 &lt;b&gt;특정 큐로만 전달&lt;/b&gt;됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;바인딩 설정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; Exchange와 큐 사이의 관계를 바인딩 키(binding key)를 통해&lt;/b&gt; 매핑하여 메시지가 전달&lt;/li&gt;
&lt;li&gt;라우팅 키와 바인딩 키가 일치 하지 않을 경우 메시지가 전달이 안됨&lt;/li&gt;
&lt;li&gt;다수의 큐와 라우팅을 관리할 경우 복잡성이 늘어남&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메시지 흐름&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Producer가 메시지와 함께 &lt;b&gt;라우팅 키를 설정&lt;/b&gt;해 Exchange로 전송&lt;/li&gt;
&lt;li&gt;Exchange는 &lt;b&gt;바인딩 키를 확인&lt;/b&gt;하고, 해당 키와 매칭되는 큐로 메시지를 전달&lt;/li&gt;
&lt;li&gt;Consumer는 해당 큐에서 메시지를 소비&lt;/li&gt;
&lt;/ol&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;189&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btz8Fy/dJMcahjaS9k/lvBtuorkeQrKT6TOBNL4Ck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btz8Fy/dJMcahjaS9k/lvBtuorkeQrKT6TOBNL4Ck/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btz8Fy/dJMcahjaS9k/lvBtuorkeQrKT6TOBNL4Ck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbtz8Fy%2FdJMcahjaS9k%2FlvBtuorkeQrKT6TOBNL4Ck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;189&quot; data-origin-width=&quot;669&quot; data-origin-height=&quot;189&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Routing 모델의 활용 사례&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그 수집
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각 log.* 나 *.error으로 끝나는 모든 에러&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주문 상태 전이 (주문 완료시 order.completed 를 키로 배송 지시, 재고 차감 지시, 이메일 마케팅 전송 큐 등에 분배)&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주문의 완료(order.completed) 이후 배송지시(order.completed.shipped), 재고 차감(order.completed.inventory), &lt;br /&gt;마케팅 이메일 발송(order.completed.email) 등의 큐 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;채팅의 방 개설 단위로 분류하여 메시지 전달
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;chat.room1, chat.room2,,&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Routing Model을 이용한 Log 수집&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 기능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exception이 발생했을 때, 해당 메시지를 송/수신하는 로직으로 메시지를 routing&lt;/li&gt;
&lt;li&gt;에러 로그별 매칭되는 큐로 전달도 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;로그를 수집하는 Routing 모델 예제 (using DirectExchange)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; RabbitMQConfig &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3개의 Queue Bean 선언(Error, Warn, Info), 3개의 Binding &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Bean&lt;span&gt; &lt;/span&gt;&lt;/span&gt;선언 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(to DirectExchage)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771767611768&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	public static final String ERROR_QUEUE = &quot;error_queue&quot;;
	public static final String WARN_QUEUE = &quot;warn_queue&quot;;
	public static final String INFO_QUEUE = &quot;info_queue&quot;;

	public static final String DIRECT_EXCHANGE = &quot;direct_exchange&quot;;

	@Bean
	public DirectExchange directExchange() {
		return new DirectExchange(DIRECT_EXCHANGE);
	}

	@Bean
	public Queue errorQueue() {
		return new Queue(ERROR_QUEUE, false);
	}

	@Bean
	public Queue warnQueue() {
		return new Queue(WARN_QUEUE, false);
	}

	@Bean
	public Queue infoQueue() {
		return new Queue(INFO_QUEUE, false);
	}

	@Bean
	public Binding errorBinding() {
		return BindingBuilder.bind(errorQueue()).to(directExchange()).with(&quot;error&quot;);
	}

	@Bean
	public Binding warnBinding() {
		return BindingBuilder.bind(warnQueue()).to(directExchange()).with(&quot;warn&quot;);
	}

	@Bean
	public Binding infoBinding() {
		return BindingBuilder.bind(infoQueue()).to(directExchange()).with(&quot;info&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LogConsumer&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771767656535&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class LogConsumer {

	@RabbitListener(queues = RabbitMQConfig.ERROR_QUEUE)
	public void consumeError(String message) {
		System.out.println(&quot;[ERROR]를 받음 : &quot; + message);
	}

	@RabbitListener(queues = RabbitMQConfig.WARN_QUEUE)
	public void consumeWarn(String message) {
		System.out.println(&quot;[WARN]를 받음 : &quot; + message);
	}

	@RabbitListener(queues = RabbitMQConfig.INFO_QUEUE)
	public void consumeInfo(String message) {
		System.out.println(&quot;[INFO]를 받음 : &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LogPublisher&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchange에 라우팅 키와 함께 메시지 전송&lt;/p&gt;
&lt;pre id=&quot;code_1771767701812&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class LogPublisher {

	private final RabbitTemplate rabbitTemplate;

	public void publish(String routingKey, String message) {
		rabbitTemplate.convertAndSend(RabbitMQConfig.DIRECT_EXCHANGE, routingKey, message);
		System.out.println(&quot;[#] message published: &quot; + routingKey + &quot;:&quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; Exception 처리 로직&lt;/b&gt;(CustomExsceptionHandler &amp;rarr; &lt;b&gt;LogPublisher로 전송&lt;/b&gt;)&lt;/p&gt;
&lt;pre id=&quot;code_1771767731160&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class CustomExceptionHandler {

	private final LogPublisher logPublisher;

	//에러 처리
	public void handleException(Exception e) {
		String routingKey = e instanceof IllegalArgumentException ? &quot;warn&quot; : &quot;error&quot;;
		logPublisher.publish(routingKey, &quot;Exception Log: &quot; + e.getMessage());
	}

	// 메시지 처리
	public void handleMessage(String message) {
		String routingKey = &quot;info&quot;;
		logPublisher.publish(routingKey, &quot;Info Log: &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771767555136&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X GET &quot;http://localhost:8080/api/logs/error&quot; 
curl -X GET &quot;http://localhost:8080/api/logs/warn&quot; 
curl -X POST &quot;http://localhost:8080/api/logs/info&quot; \
 -H &quot;Content-Type: application/json&quot; \
 -d &quot;\&quot;System initialized successfully.\&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;204&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Cz2P/dJMb996ukhV/fOLrkXy9inPWi2z4eQ1j81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Cz2P/dJMb996ukhV/fOLrkXy9inPWi2z4eQ1j81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Cz2P/dJMb996ukhV/fOLrkXy9inPWi2z4eQ1j81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Cz2P%2FdJMb996ukhV%2FfOLrkXy9inPWi2z4eQ1j81%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1260&quot; height=&quot;204&quot; data-origin-width=&quot;1260&quot; data-origin-height=&quot;204&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;패턴별 로그를 수집하는 Routing 모델 예제 (using TopicExchange)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개별 로그와 더불어 모든 로그를 수집하는 큐를 만들어보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;패턴을 만들기 위해, Routing Key에 log.을 추가&lt;/li&gt;
&lt;li&gt;각각의 로그에 대한 개별 큐에 추가하여, 모든 로그를 수집하는 큐 생성(all_log_queue)&lt;/li&gt;
&lt;li&gt;all_log_queue에 대한 &lt;b&gt;바인딩 키를 log.*&lt;/b&gt;로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQConfig 추가/변경 사항&lt;/p&gt;
&lt;pre id=&quot;code_1771768948561&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	...
	public static final String ALL_LOG_QUEUE = &quot;all_log_queue&quot;;

	public static final String TOPIC_EXCHANGE = &quot;topic_exchange&quot;;

	@Bean
	public TopicExchange topicExchange() {
		return new TopicExchange(TOPIC_EXCHANGE);
	}

	...

	@Bean
	public Queue allLogQueue() {
		return new Queue(ALL_LOG_QUEUE, false);
	}

	...

	@Bean
	public Binding allLogBinding() {
		return BindingBuilder.bind(allLogQueue()).to(topicExchange()).with(&quot;log.*&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exceptionhandler 변경 사항: 패턴을 만들기 위해 log. 추가&lt;/p&gt;
&lt;pre id=&quot;code_1771768981044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class CustomExceptionHandler {

	private final LogPublisher logPublisher;

	//에러 처리
	public void handleException(Exception e) {
		String routingKey = e instanceof IllegalArgumentException ? &quot;log.warn&quot; : &quot;log.error&quot;;
		logPublisher.publish(routingKey, &quot;Exception Log: &quot; + e.getMessage());
	}

	// 메시지 처리
	public void handleMessage(String message) {
		String routingKey = &quot;log.info&quot;;
		logPublisher.publish(routingKey, &quot;Info Log: &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer 추가 사항&lt;/p&gt;
&lt;pre id=&quot;code_1771769033986&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class LogConsumer {

	...

	@RabbitListener(queues = RabbitMQConfig.ALL_LOG_QUEUE)
	public void consumeAllLogs(String message) {
		System.out.println(&quot;[All LOGS]를 받음 : &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>binding key</category>
      <category>DirectExchange</category>
      <category>Log 수집</category>
      <category>Routing Key</category>
      <category>Routing Model</category>
      <category>Routing 모델</category>
      <category>TopicExchange</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/220</guid>
      <comments>https://look-forest.tistory.com/220#entry220comment</comments>
      <pubDate>Sun, 22 Feb 2026 21:16:46 +0900</pubDate>
    </item>
    <item>
      <title>Pub/Sub 모델을 이용한 실시간 알림과 뉴스 구독 (WebSocket, STOMP 활용)</title>
      <link>https://look-forest.tistory.com/219</link>
      <description>&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Publish / Subscribe 모델&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pub/Sub은 메시지 &lt;b&gt;발행&lt;/b&gt;(Publish)과 &lt;b&gt;구독&lt;/b&gt;(Subscribe)의 &lt;b&gt;개념을 기반&lt;/b&gt;으로 하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;메시징 패턴(메시지 전달 모델)&lt;/u&gt;으로,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 하나의 메시지가 복제되어 여러&lt;/span&gt; &amp;ldquo;독립적인 소비자 그룹&amp;rdquo;에 각각 전달&lt;/b&gt;되는 모델이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Publisher는 특정 대상이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;주제(이벤트)에 메시지를 발행&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;여러 Subscriber가 동시에&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;메시지를 수신&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;(여러 독립 구독자에게 복제 전달)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKZ4Ds/dJMcadAYkWG/lsFNrWIKuwsjlmeTRTk9f0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKZ4Ds/dJMcadAYkWG/lsFNrWIKuwsjlmeTRTk9f0/img.png&quot; data-alt=&quot;하나의 Exchange에 여러 Queue가 바인딩되어 있으면 Pub/Sub&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKZ4Ds/dJMcadAYkWG/lsFNrWIKuwsjlmeTRTk9f0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKZ4Ds%2FdJMcadAYkWG%2FlsFNrWIKuwsjlmeTRTk9f0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;330&quot; height=&quot;203&quot; data-origin-width=&quot;429&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;하나의 Exchange에 여러 Queue가 바인딩되어 있으면 Pub/Sub&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Fanout Exchange와의 차이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-section-id=&quot;9qvgfs&quot; data-start=&quot;1335&quot; data-end=&quot;1362&quot;&gt;Pub/Sub = &quot;신문을 여러 사람이 구독&quot;&lt;/li&gt;
&lt;li data-section-id=&quot;hml9wn&quot; data-start=&quot;1363&quot; data-end=&quot;1397&quot;&gt;Fanout = &quot;신문을 그냥 무조건 모든 우편함에 배달&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;경쟁 소비자 모델과의 차이&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1257&quot; data-start=&quot;1238&quot; data-section-id=&quot;nvn2re&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;큐가 여러 개&lt;/span&gt;에 각각의 컨슈머 / &lt;span style=&quot;color: #ee2323;&quot;&gt;메시지 복제&lt;/span&gt; 있음, 여러 구독자가 동일 메시지 수신 &amp;rarr; Pub/Sub&lt;/li&gt;
&lt;li data-end=&quot;1274&quot; data-start=&quot;1258&quot; data-section-id=&quot;xvidri&quot;&gt;큐가 하나에 컨슈머 여러개 / 메시지 복제 없음, 여러 소비자가 나눠 처리 &amp;rarr; 경쟁 소비자&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;467&quot; data-origin-height=&quot;226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3mQQC/dJMcahKesay/j6A499oMG0kkOBBEC86oL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3mQQC/dJMcahKesay/j6A499oMG0kkOBBEC86oL1/img.png&quot; data-alt=&quot;Pub/Sub vs 경쟁 소비자는 &amp;amp;ldquo;큐 구조 차이&amp;amp;rdquo;가 있다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3mQQC/dJMcahKesay/j6A499oMG0kkOBBEC86oL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3mQQC%2FdJMcahKesay%2Fj6A499oMG0kkOBBEC86oL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;467&quot; height=&quot;226&quot; data-origin-width=&quot;467&quot; data-origin-height=&quot;226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Pub/Sub vs 경쟁 소비자는 &amp;ldquo;큐 구조 차이&amp;rdquo;가 있다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;Pub/Sub 모델의 주요 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유연성과 확장성이 좋아 여러 subscriber를 쉽게 추가하여도 서로 독립적으로 동작이 가능하다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;다대다 메시징&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 메시지가 여러 Subscriber에게 전달&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메시지 복사가 이뤄지므로 Subscriber는 동일한 메시지를 수신&lt;/span&gt;.&lt;br /&gt;동일한 메시지가 여러 큐에 처리되므로 중복 처리 로직이 필요할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구독자 독립성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Publisher는 메시지가 어떤 Subscriber에게 전달될지 알 필요가 없음&lt;/li&gt;
&lt;li&gt;메시지의 전달은 브로커가 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기 메시지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Publisher와 Subscriber는 서로 독립적으로 동작하며, 동시에 실행될 필요가 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;확장성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 Subscriber를 추가하거나 제거해도 시스템이 영향을 받지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구독 제어
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구독자는 특정 조건(ex.라우팅 키, 토픽)을 기반으로 메시지를 필터링하여 수신할 수도 있음&lt;/li&gt;
&lt;li&gt;Fanout Exchange는 모든 구독자에게 메시지를 브로드캐스트하는 반면(Routing Key는 필요하지 않음),&lt;br /&gt;Topic Exchange나 Direct Exchange는 메시지를 선택적으로 전달 가능&lt;/li&gt;
&lt;li&gt;구독자가 많을수록 복잡도가 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;WebSocket과 STOMP&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 WebSocket과 STOMP에 대해 알아보자.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RabbitMQ/Kafka = 서버 간 메시징&lt;/li&gt;
&lt;li&gt;WebSocket/STOMP = 서버 &amp;rarr; 브라우저 실시간 전송
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;WebSocket = 서버가 먼저 말할 수 있게 만드는 기술&lt;/li&gt;
&lt;li&gt;STOMP = 그 말을 어떤 방에 보낼지 정하는 규칙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;브라우저&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; WebSocket + STOMP&lt;br /&gt;Spring 서버&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;Kafka / Redis / RabbitMQ&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;WebSocket&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;서버와 클라이언트가 한 번 연결하면, 끊기 전까지 서로 실시간으로 데이터를 주고 받을 수 있는 통신 방식&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;(실시간 통신이란,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;이벤트가 발생하는 즉시 상대방에게 전달&lt;/u&gt;되는 통신 방식)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebSocket은 HTTP 위에서 시작하지만,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;연결 후에는 지속적인 양방향 실시간 통신을 제공하는 프로토콜.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※ 소켓: 네트워크에서 두 프로그램이 데이터를 주고받기 위한 통신의 endpoint . IP + Port + Protocol = Socket&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP는 요청/응답 1회성&lt;/li&gt;
&lt;li&gt;WebSocket은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;지속 연결 + 양방향 통신&lt;/b&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebSocket은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;TCP 커넥션을 끊지 않고 계속 유지&lt;/span&gt;한다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;하지만 스레드를 계속 점유하지는 않는다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Spring은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;소수의 워커 스레드가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;여러 소켓 이벤트를 처리한다.&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;u&gt;TCP 소켓 자체가 세션&lt;/u&gt;이다. (Spring은 내부적으로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Map&amp;lt;sessionId, WebSocketSession&amp;gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;형태로 관리)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;클라이언트가 요청하지 않아도 서버가 먼저 보낼 수 있음&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;실시간 채팅 등에 사용
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채팅을 HTTP로 하면 1초마다 서버에 물어봄 (polling) &amp;rarr; 부하/지연 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매번 연결을 새로 만들지 않아도 되기 때문에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;지연(latency)이 거의 없음&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;데이터를 프레임 단위로 전송하며, 오버헤드가 낮음&lt;/li&gt;
&lt;li&gt;ws://호스트:포트/경로 형태로 작성 (ws://example.com/chat)&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 66.0466%;&quot; border=&quot;1&quot; data-ke-style=&quot;style13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.6326%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 23.4548%;&quot;&gt;&lt;b&gt;HTTP&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 23.6301%;&quot;&gt;&lt;b&gt;WebSocket&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.6326%;&quot;&gt;연결 방식&lt;/td&gt;
&lt;td style=&quot;width: 23.4548%;&quot;&gt;요청/응답&lt;/td&gt;
&lt;td style=&quot;width: 23.6301%;&quot;&gt;지속 연결&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.6326%;&quot;&gt;통신 방향&lt;/td&gt;
&lt;td style=&quot;width: 23.4548%;&quot;&gt;단방향&lt;/td&gt;
&lt;td style=&quot;width: 23.6301%;&quot;&gt;양방향&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.6326%;&quot;&gt;실시간성&lt;/td&gt;
&lt;td style=&quot;width: 23.4548%;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;width: 23.6301%;&quot;&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.6326%;&quot;&gt;오버헤드&lt;/td&gt;
&lt;td style=&quot;width: 23.4548%;&quot;&gt;큼 (헤더 반복)&lt;/td&gt;
&lt;td style=&quot;width: 23.6301%;&quot;&gt;적음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;STOMP (Simple Text Oriented Messaging Protocol)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;WebSocket 위에서 사용하는 메시지 프로토콜로, 텍스트 기반의 단순한 메시지 규칙.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebSocket = 통신 터널(네트워크 레벨)&lt;/li&gt;
&lt;li&gt;STOMP = 그 터널 안에서 사용하는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메시지 형식/라우팅 규칙(프로토콜)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;클라이언트&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;WebSocket&amp;nbsp;연결&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;그&amp;nbsp;위에서&amp;nbsp;STOMP&amp;nbsp;프로토콜&amp;nbsp;사용&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;darr;&lt;br /&gt;Spring&amp;nbsp;Message&amp;nbsp;Broker&amp;nbsp;처리&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;STOMP가 제공하는 기능&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 45.9282%; height: 126px;&quot; border=&quot;1&quot; data-ke-style=&quot;style13&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; width: 17.9264%;&quot;&gt;기능&lt;/td&gt;
&lt;td style=&quot;height: 22px; width: 23.721%;&quot;&gt;설명&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; width: 17.9264%;&quot;&gt;SUBSCRIBE&lt;/td&gt;
&lt;td style=&quot;height: 22px; width: 23.721%;&quot;&gt;특정 topic 구독&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; width: 17.9264%;&quot;&gt;SEND&lt;/td&gt;
&lt;td style=&quot;height: 22px; width: 23.721%;&quot;&gt;메시지 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; width: 17.9264%;&quot;&gt;ACK&lt;/td&gt;
&lt;td style=&quot;height: 22px; width: 23.721%;&quot;&gt;메시지 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;height: 22px; width: 17.9264%;&quot;&gt;destination&lt;/td&gt;
&lt;td style=&quot;height: 22px; width: 23.721%;&quot;&gt;주소 개념&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;STOMP 메시지 형식&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771740733519&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;COMMAND
header1:value1
header2:value2
body
---
# 예시
SEND
destination:/topic/chat
{&quot;message&quot;:&quot;hello&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;STOMP를&lt;span&gt;&amp;nbsp;쓰는 이유&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;WebSocket은 그냥 문자열 or byte[] 전송으로, 형식 규칙과 pub/sub 구독 등의 개념이 없다. 그래서 등장한 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;STOMP&lt;/b&gt;&amp;nbsp;이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;약속을 기반으로 API 등이 구현되어 있는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;92&quot; data-start=&quot;12&quot; data-section-id=&quot;7vha8d&quot;&gt;&lt;b&gt;STOMP를 안 쓰면&lt;/b&gt;: &amp;ldquo;WebSocket 통로&amp;rdquo;만 있고, &lt;b&gt;채팅방/구독/라우팅/세션관리/재전송 정책&lt;/b&gt;을 전부 직접 만들어야 한다.&lt;/li&gt;
&lt;li data-end=&quot;249&quot; data-start=&quot;93&quot; data-section-id=&quot;1sbm91p&quot;&gt;&lt;b&gt;STOMP를 쓰면&lt;/b&gt;: WebSocket 위에 &lt;b&gt;목적지(destination) 기반 Pub/Sub 규칙&lt;/b&gt;이 올라가서,&lt;br /&gt;클라이언트는 SUBSCRIBE/SEND만 하면 되고, 서버(Spring)가 &lt;b&gt;구독자 목록 관리 + 라우팅 + 전송&lt;/b&gt;을 대신해준다.&lt;/li&gt;
&lt;li data-end=&quot;249&quot; data-start=&quot;93&quot; data-section-id=&quot;1sbm91p&quot;&gt;&lt;u&gt;Spring WebSocket(STOMP) 스택이 STOMP 프레임을 해석&lt;/u&gt;해서 구독을 &amp;ldquo;등록 테이블&amp;rdquo;에 저장하고, 메시지 전송 시 그 테이블을 조회해 해당 세션으로 push한다. &lt;br /&gt;이를 쓰려면 WebSocket/STOMP 관련 의존성과 설정이 필요한데, Spring Boot 기준으로 spring-boot-starter-websocket만 추가하면 STOMP 메시징까지 같이 들어온다.&lt;/li&gt;
&lt;li data-end=&quot;249&quot; data-start=&quot;93&quot; data-section-id=&quot;1sbm91p&quot;&gt;STOMP를 쓰면 Spring이 &amp;ldquo;&lt;b&gt;destination(목적지) &amp;harr; 구독 세션 목록&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&amp;rdquo;을 &lt;/span&gt;&lt;b&gt;메모리(또는 외부 브로커)&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;에서 관리하고,&lt;br /&gt;&lt;/span&gt;메시지를 보내면 &lt;b&gt;해당 destination을 구독한 세션들에게 자동으로 fan-out&lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li data-end=&quot;249&quot; data-start=&quot;93&quot; data-section-id=&quot;1sbm91p&quot;&gt;STOMP는 destination이라는 개념을 제공하고, 실제 라우팅은 &lt;b&gt;SimpleBroker&lt;/b&gt; 사용&lt;br /&gt;: Spring이 내부 메모리로 간단한 브로커 역할 수행(destination -&amp;gt; 세션 목록)&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Client &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;WebSocket &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;STOMP &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Spring&amp;nbsp;SimpleBroker&amp;nbsp;(메모리&amp;nbsp;Map) &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;다른&amp;nbsp;Client&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;채팅방 room1에 A가 메시지를 보내면 roo1에 있는 모든 사람(B,C,,)에게 즉시 전달되는 프로그램을 만든다고 가정해보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1) &lt;span style=&quot;color: #ee2323;&quot;&gt;클라이언트가 이런 STOMP 프레임을 보낸다&lt;/span&gt;고 치면:&lt;/p&gt;
&lt;pre id=&quot;code_1771741509427&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SUBSCRIBE
destination:/topic/room1
id:sub-1&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2)&lt;span style=&quot;color: #ee2323;&quot;&gt; Spring이 프레임을 파싱&lt;/span&gt;하고 &amp;ldquo;구독 등록&amp;rdquo;을 한다. Spring 내부에서는 대략 아래와 같은 식의 매핑을 유지한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771741554921&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;destination(&quot;/topic/room1&quot;) -&amp;gt; [sessionA, sessionB, ...]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; sessionId&lt;/b&gt;(WebSocket 연결 단위)&lt;b&gt;별로 어떤 destination을 구독했는지 저장 &lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;destination별로 어떤 세션들이 붙어있는지 인덱싱&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;3) 서버가 destination으로 메시지 발행&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;서버에서 아래를 호출하면&lt;/p&gt;
&lt;pre id=&quot;code_1771741962055&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;messagingTemplate.convertAndSend(&quot;/topic/room1&quot;, payload);&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring은 &quot;/topic/room1&quot;을 &lt;span style=&quot;color: #ee2323;&quot;&gt;구독한 세션 목록을 찾고&lt;/span&gt;, 그 세션들에게 WebSocket으로 메시지를 push한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;즉, &amp;ldquo;&lt;span style=&quot;color: #ee2323;&quot;&gt;누가 구독했는지&amp;rdquo;를 Spring이 이미 알고 있기 때문에&lt;/span&gt; 서버 코드는 &amp;ldquo;어디로 보낼지(destination)&amp;rdquo;만 말하면 끝이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에는 javascript와 java 코드로 살펴보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;1. 서버에서 규칙(prefix)을 먼저 정한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Spring 설정에서 보통 아래와 같이 나눈다:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;958&quot; data-start=&quot;864&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;904&quot; data-start=&quot;864&quot; data-section-id=&quot;1vs4ccp&quot;&gt;&lt;b&gt;클라이언트가 서버로 보내는 목적지(prefix)&lt;/b&gt;: /app&lt;/li&gt;
&lt;li data-end=&quot;958&quot; data-start=&quot;905&quot; data-section-id=&quot;vgarp7&quot;&gt;&lt;b&gt;서버가 클라이언트에게 보내는 목적지(prefix)&lt;/b&gt;: /topic, /queue&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771742909540&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry registry) {
    // 서버 &amp;rarr; 클라(구독 대상)
    // SimpleBroker: Spring이 내부 메모리로 간단한 브로커 역할을 하겠다 (RabbitMQ 아님)
    registry.enableSimpleBroker(&quot;/topic&quot;, &quot;/queue&quot;);
    // 클라 &amp;rarr; 서버(@MessageMapping으로 들어옴)
    registry.setApplicationDestinationPrefixes(&quot;/app&quot;);
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1427&quot; data-start=&quot;1367&quot; data-section-id=&quot;1716ils&quot;&gt;클라가 SEND /app/... 하면 &amp;rarr; 서버의 @MessageMapping(&quot;...&quot;)로 라우팅&lt;/li&gt;
&lt;li data-end=&quot;1494&quot; data-start=&quot;1428&quot; data-section-id=&quot;1954wyn&quot;&gt;서버가 convertAndSend(&quot;/topic/...&quot;) 하면 &amp;rarr; 그 destination 구독자에게 push&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;2. &quot;어디로 보낼지&quot;는 결국 2가지 중 하나로 결정한다.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A) 브로드캐스트(채팅방)로 보낼 때&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트(구독)&lt;/p&gt;
&lt;pre id=&quot;code_1771742618777&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stompClient.subscribe(&quot;/topic/room.1&quot;, onMessage);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버(전송)&lt;/p&gt;
&lt;pre id=&quot;code_1771742646069&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;messagingTemplate.convertAndSend(&quot;/topic/room.1&quot;, payload);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘이 destination 문자열이 같으니까 &amp;ldquo;room.1을 구독한 사람 모두&amp;rdquo;가 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;B) 서버로 보내서(핸들러 타서) 처리한 뒤, 다시 뿌릴 때&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트(서버로&amp;nbsp;SEND)&lt;/p&gt;
&lt;pre id=&quot;code_1771742757779&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;stompClient.send(&quot;/app/chat.send&quot;, {}, JSON.stringify({
  roomId: 1,
  text: &quot;안녕&quot;
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버(받는 곳)&lt;/p&gt;
&lt;pre id=&quot;code_1771742834609&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@MessageMapping(&quot;/chat.send&quot;) // 앞의 /app 은 설정에서 떼고 매핑됨
public void handle(ChatMessage msg) {
  messagingTemplate.convertAndSend(&quot;/topic/room.&quot; + msg.roomId(), msg);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2153&quot; data-start=&quot;2126&quot; data-ke-size=&quot;size16&quot;&gt;여기서 &amp;ldquo;어디로 보낼지&amp;rdquo;는 서버 코드가 결정: &quot;/topic/room.&quot; + roomId&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;WebSocket과 Pub/Sub을 통한 실시간 웹 알림 구현(Notification)&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;웹 페이지를 하나 만들고, WebSocket을 이용하여 API를 연동한 뒤,&lt;br /&gt;서버에서 publish 한 메시지를 web 알림 영역에 실시간으로 표시하는 프로그램을 만들어보자.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQConfig&lt;/b&gt; : 3개의 Bean(Queue, Exchange, Binding)을 설정&lt;/p&gt;
&lt;pre id=&quot;code_1771740733520&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	// 큐 네임 설정
	public static final String QUEUE_NAME = &quot;notificationQueue&quot;;
	public static final String FANOUT_EXCHANGE = &quot;notificationExchange&quot;;

	//Spring이 시작될 때 RabbitMQ 서버에 &amp;ldquo;이 큐를 생성하라&amp;rdquo;라고 선언하기 위해
	@Bean
	public Queue queue() {
		//QUEUE_NAME은 메시지가 쌓이고 처리될 큐의 이름을 정의
		return new Queue(QUEUE_NAME, false); //영속화 여부
	}

	@Bean
	public FanoutExchange fanoutExchange() {
		//메시지를 수신하면 모든 큐로 브로드캐스트
		return new FanoutExchange(FANOUT_EXCHANGE);
	}

	@Bean
	public Binding bindingNotification(Queue notificationQueue, FanoutExchange fanoutExchange) {
		//큐와 익스체인지를 연결
		return BindingBuilder.bind(notificationQueue).to(fanoutExchange);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Controller&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지 경로로 알림을 클라이언트에 브로드캐스트하므로 Controller도 두개이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;A)&lt;span&gt; StompController&lt;br /&gt;&lt;/span&gt;STOMP 클라이언트가 /app/send로 보낸 메시지를 @MessageMapping이 받아 SimpMessagingTemplate.convertAndSend(&quot;/topic/notifications&quot;, ...)로 브로드캐스트&lt;/p&gt;
&lt;pre id=&quot;code_1771747873171&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Controller
@RequiredArgsConstructor
public class StompController {

	private final SimpMessagingTemplate simpMessagingTemplate;

	@MessageMapping(&quot;/send&quot;)
	public void sendMessage(NotificationMessage notificationMessage) {
		// 수신된 메시지를 브로드캐스팅
		String message = notificationMessage.message();
		System.out.println(&quot;[#] message = &quot; + message);

		// 클라이언트에 메시지 브로드캐스트
		simpMessagingTemplate.convertAndSend(&quot;/topic/notifications&quot;, message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B) NotificationController&lt;br /&gt;HTTP(REST)로 들어온 알림을 NotificationPublisher가 RabbitMQ Exchange에 publish &lt;br /&gt;&amp;rarr; NotificationSubscriber가 큐에서 수신 &lt;br /&gt;&amp;rarr; SimpMessagingTemplate.convertAndSend(&quot;/topic/notifications&quot;, ...)로 브로드캐스트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; *&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;SimpMessagingTemplate은 Spring에서 WebSocket(STOMP) 클라이언트에게 메시지를 보내는 서버 측 유틸리티 클래스&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771745519702&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;curl -X POST 'http://localhost:8080/notifications' -H 'Content-Type: application/json' -d 'Hello! Subscriber!!'&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1771745301709&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestController
@RequestMapping(&quot;/notifications&quot;)
@RequiredArgsConstructor
public class NotificationController {

	private final NotificationPublisher publisher;

	@PostMapping
	public String sendNotification(@RequestBody String message) {
		publisher.publish(message);
		return &quot;[#] Notification sent: &quot; + message + &quot;\n&quot;;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Publisher&lt;/b&gt;: RabbitTemplate을 이용해 convertAndSend&lt;/p&gt;
&lt;pre id=&quot;code_1771747987266&quot; class=&quot;less&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class NotificationPublisher {

	private final RabbitTemplate rabbitTemplate;

	public void publish(String message) {
		rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE, &quot;&quot;, message);
		System.out.println(&quot;[#] Published Notification: &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Subscriber&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;@RabbitListener로 큐 네임 지정
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring이 메시지 수신을 자동화하고, 비동기적으로 처리. 코드가 간결&lt;/li&gt;
&lt;li&gt;내부적으로 Spring의 &lt;span style=&quot;color: #ee2323;&quot;&gt;MessageListenerContainer를 사용하여 Queue를 지속적으로 모니터링&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;메시지 수신, 메서드 호출과 변환, Ack 전송 등의 역할&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SimpMessageingTemplate을 통해 특정 경로에 메시지 전달
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;WebSocket(STOMP) 메시지를 클라이언트에게 보내는 템플릿 클래스&lt;/b&gt;&lt;br /&gt;쉽게 말해 웹소켓용 RabbitTemplate/KafkaTemplate 같은 것&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;WebSocket 메시지 브로커와 통신하며, 클라이언트가 구독하는 특정 경로로 메시지를 전송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;HTTP 응답이 아니라,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;실시간 push 메시지 전송용&lt;/b&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Server&amp;nbsp;(Spring)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;SimpMessagingTemplate&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;WebSocket&amp;nbsp;Broker&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;darr;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;Client (브라우저)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771743361718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class NotificationSubscriber {

	public static final String CLIENT_URL = &quot;/topic/notifications&quot;;

	// WebSocket으로 메시지를 전달하기 위한 Spring의 템플릿 클래스
	private final SimpMessagingTemplate simpMessagingTemplate;

	// RabbitMQ Queue에서 메시지 수신
	// RabbitListener에 의해 QUEUE_NAME을 바라보다가 exchange에 메시지가 도착하면 Queue로 발행되고 이 Queue가 메시지를 수신
	// String message = (String) rabbitTemplate.receiveAndConvert(RabbitMQConfig.QUEUE_NAME); 과 같은 번거로운 코딩 생략
	@RabbitListener(queues = RabbitMQConfig.QUEUE_NAME)
	public void subscribe(String message) {
		System.out.println(&quot;[#] Received Notification: &quot; + message);
		// WebSocket을 통해 클라이언트로 메시지를 전달
		simpMessagingTemplate.convertAndSend(CLIENT_URL, message); // 클라이언트에 브로드캐스트
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; WebSocketConfig &lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;WebSocketMessageBroker 활성화&lt;/li&gt;
&lt;li&gt;WebSocketMessageBrokerConfigurer를 구현&lt;br /&gt;Spring에서 WebSocket 메시지 브로커를 구성하기 위한 인터페이스. &lt;br /&gt;웹소켓 연결, 메시지 브로커 설정 및 라우팅 등의 웹 소켓 관련 확장 기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771743929650&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		// SimpleBroker: Spring이 내부 메모리로 간단한 브로커 역할을 하겠다 (RabbitMQ 아님)
		// 서버 &amp;rarr; 클라(구독 대상). 서버가 convertAndSend(&quot;/topic/...&quot;) 하면 &amp;rarr; 그 destination 구독자에게 push(세션 뒤져서)
		registry.enableSimpleBroker(&quot;/topic&quot;); // 클라이언트가 구독할 수 있는 경로 설정. SimpleBroker가 /topic/*에 대한 구독을 관리
		// 클라 &amp;rarr; 서버(@MessageMapping으로 들어옴). 클라가 SEND /app/... 하면 &amp;rarr; 서버의 @MessageMapping(&quot;...&quot;)로 라우팅
		registry.setApplicationDestinationPrefixes(&quot;/app&quot;); // 클라이언트가 메시지를 보낼 때 사용할 접두사 설정
	}

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint(&quot;/ws&quot;) // 클라이언트가 WebSocket 연결을 시도할 때 사용할 엔드포인트 설정
			.setAllowedOriginPatterns(&quot;*&quot;) // CORS 설정
			.withSockJS(); // SockJS 지원
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;HTML 작성&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;필요 라이브러리
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sock.js : WebSocket 연결을 지원하지 않는 브라우저에서도 동작하도록 fallback 기능을 제공하는 라이브러리로 다음과 같은 전송 타입을 지원: WebSocket, HTTP Streaming, HTTP Long Polling&lt;/li&gt;
&lt;li&gt;stomp.js: STOMP 프로토콜을 지원하는 JavaScript 클라이언트 라이브러리.&amp;nbsp;&lt;br /&gt;텍스트 기반 프로토콜로, 메시지의 유형, 내용을 정의하여 클라이언트와 서버 간 메시지를 주고 받음.&lt;br /&gt;RabbitMQ와 같은 브로커와 통신하는 데 사용.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771745605598&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;!-- html 생략 --&amp;gt;
&amp;lt;script&amp;gt;
    const socket = new SockJS('/ws'); // 서버의 WebSocket Endpoint 연결
    const stompClient = Stomp.over(socket); // SockJS 객체를 STOMP 클라이언트로 wrapping

    stompClient.connect({}, function () {
        console.log('Connected to WebSocket');
        // STOMP 연결 후 stompClient.subscribe('/topic/notifications', ...) 호출 &amp;rarr; 서버의 SimpleBroker에 구독 등록
        // WebSocketConfig.configureMessageBroker(...)에서 registry.enableSimpleBroker(&quot;/topic&quot;) -&amp;gt; SimpleBroker가 /topic/*에 대한 구독을 관리
        // 서버가 /topic/notifications로 메시지 전송하면(서버 발행)
        // SimpleBroker가 해당 destination의 구독 목록을 찾아 세션별로 메시지를 전송 &amp;rarr; 클라이언트의 subscribe 콜백으로 도달
        stompClient.subscribe('/topic/notifications', function (message) {
            const notificationsDiv = document.getElementById('notifications');
            const newNotification = document.createElement('div');
            newNotification.textContent = message.body;
            notificationsDiv.appendChild(newNotification);
        });
        // 서버로 전송도 가능
        const form = document.getElementById('notificationForm')
        form.addEventListener('submit', function (event) {
            event.preventDefault();
            const messageInput = document.getElementById('notificationMessage');
            const message = messageInput.value;

            stompClient.send('/app/send', {}, JSON.stringify({ message: message }));
            messageInput.value = '';
        })
    });
&amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;여러 큐를 소비하는 Fanout Exchange 예제 (관심사 기반의 뉴스 레터 발행/구독 모델)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뉴스 레터 구독 프로그램을 만들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 사용자는 관심있는 개발 뉴스 레더를 체크하고 구독한다. (Java/Spring/Vue)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 관심사별 Exchange 연결에 따른 메시지 발행과 구독을 구현하면 된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;시퀀스 A: STOMP &amp;rarr; 서버 &amp;rarr; RabbitMQ &amp;rarr; WebSocket 브로드캐스트
시퀀스 B: HTTP REST &amp;rarr; RabbitMQ &amp;rarr; WebSocket 브로드캐스트&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQConfig&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;3개의 Queue Bean, 1개의 Exchange Bean, 3개의 큐 Binding에 대한 bind().to() 설정&lt;br /&gt;(사실 해당 예제는 Topic Exchange가 적합하나, 아직 배우기 전이라 Fanout Exchange로 구현했다.)&lt;/li&gt;
&lt;li&gt;매개변수 이름은 실제로 주입되는 빈 이름과 일치하지 않아도 동작하지만, 타입이 같은 여러개의 빈이 존재하면 @Qualifier를 사용해 이름으로 구분 지어야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771761575871&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	// 큐 네임 설정
	public static final String JAVA_QUEUE = &quot;javaQueue&quot;;
	public static final String SPRING_QUEUE = &quot;springQueue&quot;;
	public static final String VUE_QUEUE = &quot;vueQueue&quot;;

	public static final String FANOUT_EXCHANGE_FOR_NEWS = &quot;newsExchange&quot;;

	//Spring이 시작될 때 RabbitMQ 서버에 &amp;ldquo;이 큐를 생성하라&amp;rdquo;라고 선언하기 위해
	@Bean
	public Queue javaQueue() {
		return new Queue(JAVA_QUEUE, false);
	}

	@Bean
	public Queue springQueue() {
		return new Queue(SPRING_QUEUE, false);
	}

	@Bean
	public Queue vueQueue() {
		return new Queue(VUE_QUEUE, false);
	}

	@Bean
	public FanoutExchange fanoutExchange() {
		// 메시지를 수신하면 연결된 모든 큐로 브로드캐스트
		return new FanoutExchange(FANOUT_EXCHANGE_FOR_NEWS);
	}

	@Bean
	public Binding javaBinding(Queue javaQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(javaQueue).to(fanoutExchange);
	}

	@Bean
	public Binding springBinding(Queue springQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(springQueue).to(fanoutExchange);
	}

	@Bean
	public Binding vueBinding(Queue vueQueue, FanoutExchange fanoutExchange) {
		return BindingBuilder.bind(vueQueue).to(fanoutExchange);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;컨트롤러&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹용(STOMP 기반) 컨트롤러와 API용 컨트롤러 모두 publisher를 호출해 메시지 발행&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지발행(publisher)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;fanout이므로 라우팅 키는 무시되고 모든 큐에 삽입됨&lt;/p&gt;
&lt;div style=&quot;background-color: #191a1c; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class NewsPublisher {

    private final RabbitTemplate rabbitTemplate;

    private String publishMessage(String news, String messageSuffix) {
       String message = news + messageSuffix;
       rabbitTemplate.convertAndSend(RabbitMQConfig.FANOUT_EXCHANGE_FOR_NEWS, news, message);
       System.out.println(&quot;News Published: &quot; + message);
       return message;
    }

    public String publish(String news) {
       return publishMessage(news, &quot; 관련 새 소식이 나왔습니다.&quot;);
    }

    public String publishAPI(String news) {
       return publishMessage(news, &quot; 관련 새 소식이 나왔습니다. (API)&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; 컨슈머(subscriber)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐에서 메시지를 수신하면 웹 소켓으로 메시지를 전달한다. 이때, destination을 보고 구독한 클라이언트만 메시지를 수신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #24292f; text-align: start;&quot;&gt;즉, Fanout으로 모든 큐에 메시지를 브로드캐스트하지만 WebSocket을 이용해 특정 뉴스 타입만 선택적으로 구독하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771761185495&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class NewsSubscriber {

	// WebSocket으로 메시지를 전달하기 위한 Spring의 템플릿 클래스
	private final SimpMessagingTemplate simpMessagingTemplate;

	// RabbitListener에 의해 QUEUE_NAME을 바라보다가 exchange에 메시지가 도착하면 Queue로 발행되고 이 Queue가 메시지를 수신
	@RabbitListener(queues = RabbitMQConfig.JAVA_QUEUE)
	public void javaNews(String message) {
		// 해당 destination으로 구독한 세션들에게 메시지 push
		simpMessagingTemplate.convertAndSend(&quot;/topic/java&quot;, message);
	}

	@RabbitListener(queues = RabbitMQConfig.SPRING_QUEUE)
	public void springNews(String message) {
		simpMessagingTemplate.convertAndSend(&quot;/topic/spring&quot;, message);
	}

	@RabbitListener(queues = RabbitMQConfig.VUE_QUEUE)
	public void vueNews(String message) {
		simpMessagingTemplate.convertAndSend(&quot;/topic/vue&quot;, message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>fanout exchange</category>
      <category>Notification</category>
      <category>pub/sub 모델</category>
      <category>Publish / Subscribe 모델</category>
      <category>Publisher</category>
      <category>Stomp</category>
      <category>subscribe</category>
      <category>Subscriber</category>
      <category>websocket</category>
      <category>구독</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/219</guid>
      <comments>https://look-forest.tistory.com/219#entry219comment</comments>
      <pubDate>Sun, 22 Feb 2026 15:12:40 +0900</pubDate>
    </item>
    <item>
      <title>경쟁 소비자 패턴(Work Queue 모델)과 큐의 메시지 상태</title>
      <link>https://look-forest.tistory.com/218</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Consumer간 작업 분배 - WorkQueue&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt; Work Queues&lt;/b&gt; : Competing Consumers Pattern&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Work Queue 패턴에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;여러 소비자가 하나의 큐에서 메시지를 가져가 경쟁적으로 처리&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 부하를 효율적으로 분산하고, 병렬 처리를 가능하게 만들어 처리량을 향상시키는 효과가 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; Round-Robin&lt;/b&gt; 방식(default)과 Fair Dispatch 방식을 사용하여 메시지를 Consumer 간에 분배&lt;/li&gt;
&lt;li&gt;Fair Dispatch 방식은 개발자가 코드 레벨에서 컨슈머간 분배 조정.&lt;br /&gt;메시지 수동 확인(Manual Acknowledgement) 모드로 설정해야함. (AMQP 기본 값 Auto)&lt;br /&gt;메시지 처리 비중(Prefetch Count) 설정 등을 통해 조정 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 특징&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;경쟁적인 메시지 소비&lt;br /&gt;여러 Consumer가 동일한 메시지 큐에서 메시지를 가져가 처리하나,&lt;br /&gt;특정 메시지는 한 번에 하나의 Consumer에 의해 처리되므로 중복 처리는 되지 않는다.&lt;/li&gt;
&lt;li&gt;작업 분산&lt;br /&gt;메시지가 여러 Consumer 간에 분배되어 병렬로 처리되므로 작업 부하를 효율적으로 분산&lt;/li&gt;
&lt;li&gt;확장성&lt;br /&gt;Consumer를 추가하거나 제거함으로써 작업 처리 능력을 동적으로 확장하거나 축소&lt;/li&gt;
&lt;li&gt;내결함성&lt;br /&gt;Consumer 중 하나가 실패하더라도 다른 Consumer가 작업을 이어받아 처리할 수 있어 중단없이 작동&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Config는 별반 다를 바 없으나, 오류 확인을 위해 큐의 Duration을 true로 설정하여 서버를 내려도 데이터가 유지되도록 했다.&lt;/p&gt;
&lt;pre id=&quot;code_1771684511610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	// 큐 네임 설정
	public static final String QUEUE_NAME = &quot;WorkQueue&quot;;

	//Spring이 시작될 때 RabbitMQ 서버에 &amp;ldquo;이 큐를 생성하라&amp;rdquo;라고 선언하기 위해
	@Bean
	public Queue queue() {
		//QUEUE_NAME은 메시지가 쌓이고 처리될 큐의 이름을 정의
		return new Queue(QUEUE_NAME, true); //영속화 여부. true: 서버 셧다운 시 데이터 보관
	}


	//RabbitTemplate: 래빗엠큐로 메시지를 보내는 데 사용되는 템플릿 클래스
	@Bean
	public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
		return new RabbitTemplate(connectionFactory); //ConnectionFactory: 래빗엠큐의 연결 관리
	}

	//SimpleMessageListenerContainer: 래빗엠큐에서 메시지를 수신하고 처리하는 리스너 컨테이너
	@Bean
	public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
		MessageListenerAdapter listenerAdapter) {
		SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
		container.setConnectionFactory(connectionFactory);
		container.setQueueNames(QUEUE_NAME);
		container.setMessageListener(listenerAdapter);
		container.setAcknowledgeMode(AcknowledgeMode.AUTO); //default이나 명시
		return container;
	}

	//MessageListenerAdapter: 메시지를 처리할 리스너 어댑터
	@Bean
	public MessageListenerAdapter listenerAdapter(WorkQueueConsumer workQueueTask) {
		return new MessageListenerAdapter(workQueueTask, &quot;workQueueTask&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외 Controller, Producer, Consumer는 역할이 별반 다를게 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;사실 Work Queue 전용 설정이 따로 있는 것이 아니고, 하나의 Queue를 여러 Consumer가 같이 소비하면 그게 곧 Work Queue&lt;/span&gt; 패턴이다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;같은 큐를 바라보는 @RabbitListener 인스터스가 여러 개 있으면&lt;/b&gt;&lt;/span&gt; Work Queue 처럼 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 RabbitMQ 서버에 여러 Spring 서버를 띄워서 확인해보자.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실행&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jar로 빌드하여 spring을 두대 띄워본 후 curl로 요청을 3번 보내보니, 아래와 같이 1,3 / 2 요청이 분산됨을 확인할 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1497&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blv26H/dJMcacB8aZp/mCONXCJmoQxqetrx5IMXVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blv26H/dJMcacB8aZp/mCONXCJmoQxqetrx5IMXVK/img.png&quot; data-alt=&quot;8080 포트로 요청을 3개 보낸 후 8080 및 8081 포트의 Consumer에 메시지가 각각 분배되어 처리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blv26H/dJMcacB8aZp/mCONXCJmoQxqetrx5IMXVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fblv26H%2FdJMcacB8aZp%2FmCONXCJmoQxqetrx5IMXVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1497&quot; height=&quot;248&quot; data-origin-width=&quot;1497&quot; data-origin-height=&quot;248&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;8080 포트로 요청을 3개 보낸 후 8080 및 8081 포트의 Consumer에 메시지가 각각 분배되어 처리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;메시지&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;어드민에서 메시지 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의도적으로 Consumer에서 오류를 발생하게 하고, 어드민에서 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;325&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zVZ1b/dJMcahXJDcx/Z9KQEsyvy39VODKgZ9dKk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zVZ1b/dJMcahXJDcx/Z9KQEsyvy39VODKgZ9dKk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zVZ1b/dJMcahXJDcx/Z9KQEsyvy39VODKgZ9dKk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzVZ1b%2FdJMcahXJDcx%2FZ9KQEsyvy39VODKgZ9dKk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1003&quot; height=&quot;325&quot; data-origin-width=&quot;1003&quot; data-origin-height=&quot;325&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;큐의 상태를 보면 메시지가 상태에 따라 분류되는데,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ready&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 큐에 있지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;아직 컨슈머에게 전달되지 않은 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이 상태의 메시지는 대기 중이며, 컨슈머가 연결되면 전달될 준비가 됨&lt;/li&gt;
&lt;li&gt;큐의 적재량이 많이지면 ready 메시지 수가 증가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;unacked&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 &lt;span style=&quot;color: #ee2323;&quot;&gt;컨슈머에게 전달되었으나, 아직 확인되지 않은 상태&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;컨슈머가 메시지를 처리하고 &lt;u&gt;확인(ACK)를 보내면, RabbitMQ는 해당 메시지를 삭제&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;컨슈머가 확인을 보내지 못하거나 연결이 끊어지면, 메시지는 다시 ready 상태로 돌아감(설정에 따라 다름)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 삭제가 필요한 경우, admin에서 purge 버튼을 눌러서 삭제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;purge 명령은 큐에 있는 ready 메시지를 삭제 (unacked 메시지는 purge로 삭제되지 않음)&lt;/li&gt;
&lt;li&gt;삭제된&amp;nbsp;메시지는&amp;nbsp;복구되지&amp;nbsp;않으며,&amp;nbsp;데이터는&amp;nbsp;소실&lt;/li&gt;
&lt;li&gt;unacked 메시지를 삭제하려면 컨슈머가 연결을 끊거나 RabbitMQ 큐를 재시작해야 함 &lt;br /&gt;(컨슈머가&amp;nbsp;연결을&amp;nbsp;끊으면&amp;nbsp;unacked&amp;nbsp;메시지는&amp;nbsp;다시&amp;nbsp;ready&amp;nbsp;상태로&amp;nbsp;돌아가므로)&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일반적인 문제 해결 방법&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ready가 많을 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머 수를 늘리거나 컨슈머의 메시지 처리 속도를 최적화해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;unacked가 많을 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨슈머 코드 수정 (프로그램 에러일 가능성이 높다) &lt;br /&gt;unacked 상태에서 consumer 를 제대로 소비하도록 프로그램을 수정하면 unacked 에서도 처리가 되어 삭제된다.&lt;/li&gt;
&lt;li&gt;컨슈머 연결 상태를 확인 하여 재연결 혹은 재시작 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li style=&quot;list-style-type: none;&quot;&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>Competing Consumers Pattern</category>
      <category>MessageListenerAdapter</category>
      <category>RabbitTemplate</category>
      <category>SimpleMessageListenerContainer</category>
      <category>Stomp</category>
      <category>websocket</category>
      <category>WorkQueue</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/218</guid>
      <comments>https://look-forest.tistory.com/218#entry218comment</comments>
      <pubDate>Tue, 17 Feb 2026 19:53:22 +0900</pubDate>
    </item>
    <item>
      <title>Exchange의 이해와 기본 비동기 메시지 전송</title>
      <link>https://look-forest.tistory.com/217</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exchange 유형에 따른 처리 흐름&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5JrEp/dJMcabXsQGJ/hMLKYruX11Rz8kuQ44QOiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5JrEp/dJMcabXsQGJ/hMLKYruX11Rz8kuQ44QOiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5JrEp/dJMcabXsQGJ/hMLKYruX11Rz8kuQ44QOiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5JrEp%2FdJMcabXsQGJ%2FhMLKYruX11Rz8kuQ44QOiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1206&quot; height=&quot;736&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Direct Exchange&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 발행할 때 사용하는 &lt;u&gt;라우팅 키와 동일한 키&lt;/u&gt;로 익스체인지에 바인딩 된 모든 큐에 메세지를 전달.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당&lt;span style=&quot;color: #ee2323;&quot;&gt; 라우팅 키와 일치하는 큐에만&lt;/span&gt; 메시지가 전달되는 방식이기 때문에 Direct Exchange 라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매핑이 정확하게 되는 한개의 키만 있으니까 1:1로 가능할거 같은데, &lt;u&gt;하나의 라우팅 키에 대해 여러 큐가 바인딩될 수 있기 때문에 1:N 매칭이 가능&lt;/u&gt;하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;메시지가 명확하게 특정 큐로 전달되어야 할 때&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 큐마다 고유한 라우팅 규칙을 적용하여 메시지를 분류해야 할 때 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;예시&amp;nbsp;업무:&amp;nbsp;주문&amp;nbsp;상태&amp;nbsp;처리,&amp;nbsp;결제&amp;nbsp;처리,&amp;nbsp;사용자&amp;nbsp;알림&amp;nbsp;시스템&amp;nbsp;등&lt;br /&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;-&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;주문 상태별로 라우팅 키를 정의하고, 각 상태에 해당하는 큐가 메시지를 받는다 &lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Topic Exchange&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 라우팅 키를 패턴 기반으로 정의&lt;/span&gt;하여 메시지를 여러 큐에 유연하게 전달.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 키에 와일드카드(*, #) 매칭을 사용하여 더 복잡한 라우팅이 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;와일드카드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;* : 1개 단어 대체. log.info, log.warn, log.error -&amp;gt; log.*&lt;/li&gt;
&lt;li&gt;# : 0개 이상의 단어를 대체: app.order.success, app.payment.success -&amp;gt; #.success&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동적이고&amp;nbsp;유연한&amp;nbsp;라우팅이&amp;nbsp;필요할&amp;nbsp;때(로그&amp;nbsp;수집&amp;nbsp;시스템,&amp;nbsp;이벤트&amp;nbsp;기반&amp;nbsp;모니터링&amp;nbsp;등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. Fanout Exchange&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 브로드캐스트&lt;/span&gt; 방식으로 메시지를 모든 바인딩된 큐에 전달.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 번의 메시지 발행으로 모든 큐가 동일한 메시지를 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이벤트가 발생하면 모든 서비스가 동일한 메시지를 받는 서비스에서 유용(시스템 점검 공지 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. Headers Exchange&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 메시지의 속성(헤더)에 기반한 복잡한 라우팅&lt;/span&gt;이 필요할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;활용&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다국어&amp;nbsp;서비스,&amp;nbsp;고객의&amp;nbsp;등급별&amp;nbsp;혜택&amp;nbsp;알림&lt;br /&gt;- 메시지 헤더에 language: &quot;ko&quot;, language: &quot;en&quot; 등의 값을 설정하여 헤더 기반 라우팅을 수행 &lt;br /&gt;&amp;nbsp; &quot;ko&quot;로 설정된 메시지는 한국어 이메일 서비스에서 처리, &quot;en&quot;으로 설정된 메시지는 영어 이메일 서비스에서 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;메시지 전송 단계별 프로세스&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;772&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4qUeL/dJMcaflgAd5/E5FFtxIbFCagCEmtKjGSI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4qUeL/dJMcaflgAd5/E5FFtxIbFCagCEmtKjGSI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4qUeL/dJMcaflgAd5/E5FFtxIbFCagCEmtKjGSI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4qUeL%2FdJMcaflgAd5%2FE5FFtxIbFCagCEmtKjGSI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;522&quot; data-origin-width=&quot;877&quot; data-origin-height=&quot;772&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;메시지 송신 (Producer -&amp;gt; Broker)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer가 RabbitMQ Broker로 메시지를 송신&lt;/li&gt;
&lt;li&gt;이때 메시지는 큐에 저장되며, 익스체인지와 바인딩 설정에 따라 적절한 큐로 라우팅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;메시지 전달 (Broker -&amp;gt; Consumer)&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; Broker는 큐에 있는 메시지를 Consumer에게 전달 &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Consumer는&amp;nbsp;&amp;nbsp;큐에서 메시지를 가져가거나(Polling, 일정 주기로 계속 물어보는 Pull의 한 형태), 메시지를 푸시(Push) 받는 방식으로 수신&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 메시지 확인(ACK) 또는 거절(NACK) &lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;ACK&lt;/b&gt;: Consumer가 메시지를 &lt;b&gt;성공&lt;/b&gt;적으로 처리한 후 Broker에 ACK(Acknowledgment) 전송.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;이 경우 Broker는 해당 메시지를 &lt;u&gt;큐에서 제거하고 Producer에게 Message Acknowledged 응답&lt;/u&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt; NACK&lt;/b&gt;: Consumer가 메시지 처리에 &lt;b&gt;실패&lt;/b&gt;하거나 메시지를 &lt;b&gt;거절&lt;/b&gt;할 경우 NACK(Negative Acknowledgment) 전송.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Consumer가 메시지를 NACK하면 Broker는&lt;u&gt; Producer에게 Message Rejected 응답&lt;/u&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;NACK에는 메시지를&lt;u&gt; 다시 큐로 보내야 할지(requeue) 또는 폐기해야 할지(discard) 설정&lt;/u&gt; 가능&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; Producer에 응답 (Message Acknowledged / Message Rejected)&lt;/span&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Producer가 &lt;u&gt;Publisher Confirms를 활성화한 경우&lt;/u&gt;,&lt;u&gt; Broker는 ACK 또는 NACK 결과를 &lt;/u&gt;&lt;/span&gt;&lt;u&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;Producer에게&amp;nbsp;전송&lt;/span&gt;&lt;/u&gt;&lt;b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;ACK를 받은 경우 메시지가 성공적으로 소비된 것으로 간주되며, &lt;br /&gt;NACK를 받은 경우 Producer는&amp;nbsp;메시지&amp;nbsp;실패를&amp;nbsp;기록하거나&amp;nbsp;재전송&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;단순 메시지 전송 (Producer to Consume)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;아무 설정을 안 해도 동작하도록&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;이미 내부에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;기본 Exchange(Default Exchange) + 자동 바인딩 구조&lt;/b&gt;가 존재한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771761990821&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;rabbitTemplate.convertAndSend(&quot;myQueue&quot;, message);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 메시지를 보냈다면, 실제 내부적으로는 아래와 같이 동작한다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;Producer&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;Default Direct Exchange (&quot;&quot;)&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;darr;&amp;nbsp; (routingKey = myQueue)&lt;br /&gt;Queue (myQueue)&lt;br /&gt;&amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;Consumer&lt;/blockquote&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;구현&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;application.yml 작성&lt;/p&gt;
&lt;pre id=&quot;code_1771761990822&quot; class=&quot;yaml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  rabbitmq:
    host: localhost
    port: 5672 # 기본 통신 포트, 15672는 주로 관리 및 모니터링(admin) 용
    username: guestuser
    password: guestuser
  application:
    name: HelloMessageQueue
server:
  port: 8080&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;설정 Bean 생성&lt;/p&gt;
&lt;pre id=&quot;code_1771761990822&quot; class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-amqp'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 처럼 RabbitMQ를 사용하기 위해 위 의존성을 추가하면 자동으로 RabbitTemplate 등을 제공하지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMQP 구조를 명확히 보기 위해 직접 Bean을 등록해보자.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Queue&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Queue 인스턴스를 생성하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;애플리케이션이 사용할 큐를 정의&lt;/span&gt;, 메시지를 전달하고 처리하는 기본 큐 세팅&lt;/li&gt;
&lt;li&gt;Spring이 시작될 때 RabbitMQ 서버에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;ldquo;이 큐를 생성하라&amp;rdquo;라고 선언하기 위해 빈으로 만든다&lt;/span&gt;.&lt;br /&gt;Queue&amp;nbsp;Bean&amp;nbsp;발견&lt;br /&gt;&amp;darr;&lt;br /&gt;RabbitAdmin이&amp;nbsp;감지&lt;br /&gt;&amp;darr;&lt;br /&gt;애플리케이션&amp;nbsp;시작&amp;nbsp;시&lt;br /&gt;&amp;darr;&lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;RabbitMQ 서버에 queue.declare 실행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333;&quot;&gt;Durable 속성을 True로 설정하면 RabbitMQ 서버가 예기치 않게 종료되거나 재시작될 경우에도 해당 Queue의 정의나 Queue에 있던 메시지가 손실되지 않고 보존. 영구적인 메시지 처리가 필요한 경우 중요.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;.RabbitTemplate&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;RabbitMQ와 통신하기 위한&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;템플릿 인스턴스 생성, 메시지 송수신용&lt;/li&gt;
&lt;li&gt;JdbcTemplate과 비슷하게,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;RabbitMQ와 상호작용하기 위한 간단한 API 제공&lt;/span&gt;. 주로 메시지 전송 담당.&lt;br /&gt;메시지를 전송하는 Sender가 rabbitTemplate.convertAndSend() 메서드를 사용해 큐에 메시지를 넣는데 사용.&lt;br /&gt;(convert는 메시지 변환(직렬화)을 해준다는 의미)&lt;/li&gt;
&lt;li&gt;ConnectionFactory는 RabbitMQ와의 연결을 관리하는 객체.&lt;br /&gt;rabbitTemplate에 주입하여 메시지를 전송할 때 사용할 연결을 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SimpleMessageListenerContainer&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RabbitMQ&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메시지를 비동기적으로 수신하기 위한 리스너&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이 컨테이너가 특정&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;큐를 지속적으로 모니터링&lt;/u&gt;하고 메시지를 수신하면 지정된 리스너(MessageListenerAdapter)를 통해 처리&lt;/li&gt;
&lt;li&gt;ConnectionFactory는 RabbitMQ와 연결을 유지하며, 수신하는 메시지를 이 연결을 통해 가져옴&lt;/li&gt;
&lt;li&gt;setQueueNames(QUEUE_NAME) 메서드는 특정 큐 이름을 설정. 이 컨테이너는 코드에서 설정한 큐에서 수신되는 메시지를 모니터링&lt;/li&gt;
&lt;li&gt;setMessageListener(listenerAdapter)는 listenerAdapter를 설정하여,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;메시지가 수신될 때 호출할 리스너를 지정&lt;/u&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;MessageListenerAdapter&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;수신한 메시지를 특정 클래스의 특정 메서드로 전달하는 어댑터&lt;/span&gt;, 인자로 전달된 메서드를 자동으로 호출&lt;/li&gt;
&lt;li&gt;receiver 객체는 메시지를 처리하는 역할을 하는 빈이며, receiveMessage 메서드를 호출&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre id=&quot;code_1771761990823&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class RabbitMQConfig {
	// 큐 네임 설정
	public static final String QUEUE_NAME = &quot;hello-queue&quot;;

	//Spring이 시작될 때 RabbitMQ 서버에 &amp;ldquo;이 큐를 생성하라&amp;rdquo;라고 선언하기 위해
	@Bean
	public Queue queue() {
		//QUEUE_NAME은 메시지가 쌓이고 처리될 큐의 이름을 정의
		return new Queue(QUEUE_NAME, false); //영속화 여부
	}


	//RabbitTemplate: 래빗엠큐로 메시지를 보내는 데 사용되는 템플릿 클래스
	@Bean
	public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
		return new RabbitTemplate(connectionFactory); //ConnectionFactory: 래빗엠큐의 연결 관리
	}

	//SimpleMessageListenerContainer: 래빗엠큐에서 메시지를 수신하고 처리하는 리스너 컨테이너
	@Bean
	public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
		MessageListenerAdapter listenerAdapter) { 
		SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
		container.setConnectionFactory(connectionFactory);
		container.setQueueNames(QUEUE_NAME);
		container.setMessageListener(listenerAdapter);
		return container;
	}

	//MessageListenerAdapter: 메시지를 처리할 리스너 어댑터
	@Bean
	public MessageListenerAdapter listenerAdapter(Receiver receiver) {
		return new MessageListenerAdapter(receiver, &quot;receiveMessage&quot;);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메시지 Sender와 Receiver 구현&lt;/p&gt;
&lt;pre id=&quot;code_1771761990825&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;//consumer 역할
@Component
public class Receiver {
	public void receiveMessage(String message) {
		System.out.println(&quot;[#] Received: &quot; + message);
	}
}

@Component
@RequiredArgsConstructor
public class Sender {

	private final RabbitTemplate rabbitTemplate;

	public void send(String message) {
		rabbitTemplate.convertAndSend(RabbitMQConfig.QUEUE_NAME, message);
		System.out.println(&quot;[#] Sent: &quot; + message);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>consumer</category>
      <category>Dead Letter Queue</category>
      <category>direct exchange</category>
      <category>DLQ</category>
      <category>durable</category>
      <category>Exchange</category>
      <category>fanout exchange</category>
      <category>Message Acknowledgment</category>
      <category>Routing Key</category>
      <category>topic exchange</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/217</guid>
      <comments>https://look-forest.tistory.com/217#entry217comment</comments>
      <pubDate>Tue, 17 Feb 2026 17:57:43 +0900</pubDate>
    </item>
    <item>
      <title>RabbitMQ 개요와 주요 용어</title>
      <link>https://look-forest.tistory.com/216</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;개요&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 &lt;b&gt;메시지 브로커&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; 애플리케이션 사이에서 메시지를 받아서 적절한 Queue로 라우팅하고 Consumer에게 전달&lt;/span&gt;하는 중간 서버이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;시스템 간 비동기 메시징&lt;/b&gt;을 가능하게 하여 서비스 간 통신을 안정적이고 효율적으로 처리할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 MSA 환경에서 가장 일반적으로 쓰이는 기술 중 하나이며, &lt;span style=&quot;color: #ee2323;&quot;&gt;대량의 데이터 전송 시 발생할 수 있는 과부화를 분산&lt;/span&gt;시키며&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;비동기 처리와 지연이 필요한 작업을 효과적으로 관리&lt;/span&gt;해 줄 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터를 설정할 때 큐를 HA(High Available)로 설정하여 여러 노드에 저정함으로써 메시지 손실을 방지하고, Federation 플러그인을 통해 데이터 동기화와 다중 마스터 복제를 구성할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;메세지큐를 사용하는 이유&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 메시지를 사용하여 다른 응용프로그램 사이에 데이터를 송수신하기 위해&lt;/li&gt;
&lt;li&gt;&lt;u&gt;클라이언트에 대한 동기 처리는 병목의 요인&lt;/u&gt;이므로, &lt;span style=&quot;color: #ee2323;&quot;&gt;비동기로 처리해도 될 영역에 대해서는 큐를 통해 분리해서 처리&lt;/span&gt;한다.&lt;/li&gt;
&lt;li&gt;요청에 대한 응답을 기다릴 필요가 없기 때문에 각 영역의 역할만 신경쓰면 된다&lt;/li&gt;
&lt;li&gt;결국 분산환경에서 응용프로그램들을 분리하고 독립적으로 확장하기 위해서 사용, 기능 별로 모듈 구성이 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;대표적인 도메인 활용 사례&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;EDA(Event-Driven Architecture) : 주문/결제/배송 등의 비즈니스 도메인을 이벤트 기반의 독립적인 도메인 모듈로 구성&lt;/li&gt;
&lt;li&gt;로그 및 모니터링 : 로그 수집 및 준 실시간 처리 모니터링 시스템 구축&lt;/li&gt;
&lt;li&gt;채팅 및 알람 등 Notification 시스템&lt;/li&gt;
&lt;li&gt;비동기 데이터 처리 : 대용량의 이미지나 비디오 처리에 효율적인 분산 시스템 구축 가능&lt;/li&gt;
&lt;li&gt;여러&amp;nbsp;디바이스의&amp;nbsp;요청&amp;nbsp;처리&amp;nbsp;:&amp;nbsp;IoT와&amp;nbsp;같은&amp;nbsp;멀티&amp;nbsp;디바이스&amp;nbsp;환경에서의&amp;nbsp;요청&amp;nbsp;처리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kafka와 RabbitMQ의 차이&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설계 철학 자체가 다르고, 적용 영역이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Kafka는 &amp;ldquo;&lt;b&gt;대량 데이터 스트리밍&lt;/b&gt;&amp;rdquo;에 최적화된 구조 (고속 로그 저장 + 병렬 소비가 목표)&lt;/li&gt;
&lt;li&gt;RabbitMQ는 &amp;ldquo;&lt;b&gt;정교한 라우팅&lt;/b&gt;&amp;rdquo;에 최적화된 구조 (메시지 분기가 목표)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 대규모 이벤트 스트리밍 플랫폼에 적합하고, RabbitMQ는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;정교한 메시지 브로커 역할&lt;/b&gt;&lt;/span&gt;에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 문제가 &amp;ldquo;대용량 이벤트 스트리밍&amp;rdquo;은 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1247&quot; data-start=&quot;1233&quot;&gt;이메일 발송 작업 분배&lt;/li&gt;
&lt;li data-end=&quot;1260&quot; data-start=&quot;1248&quot;&gt;주문 처리 비동기화&lt;/li&gt;
&lt;li data-end=&quot;1274&quot; data-start=&quot;1261&quot;&gt;이미지 변환 작업 큐&lt;/li&gt;
&lt;li data-end=&quot;1292&quot; data-start=&quot;1275&quot;&gt;RPC 기반 요청-응답 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 건 RabbitMQ가 더 직관적이고 단순하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Kafka와의 비교&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 94.3013%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.8837%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 28.1191%;&quot;&gt;RabbitMQ&lt;/td&gt;
&lt;td style=&quot;width: 42.7811%;&quot;&gt;Kafka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.8837%;&quot;&gt;설계 목표&lt;/td&gt;
&lt;td style=&quot;width: 28.1191%;&quot;&gt;메시지 전달&lt;/td&gt;
&lt;td style=&quot;width: 42.7811%;&quot;&gt;로그 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.8837%;&quot;&gt;메시지 모델&lt;/td&gt;
&lt;td style=&quot;width: 28.1191%;&quot;&gt;큐 기반&lt;/td&gt;
&lt;td style=&quot;width: 42.7811%;&quot;&gt;로그 스트림 기반(메시지를 지우지않고 로그에 쌓음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.8837%;&quot;&gt;메시지 삭제&lt;/td&gt;
&lt;td style=&quot;width: 28.1191%;&quot;&gt;소비 후 삭제&lt;/td&gt;
&lt;td style=&quot;width: 42.7811%;&quot;&gt;보관 기간 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.8837%;&quot;&gt;실시간 스트리밍&lt;/td&gt;
&lt;td style=&quot;width: 28.1191%;&quot;&gt;약함 (메시지 단위 관리 오버헤드)&lt;/td&gt;
&lt;td style=&quot;width: 42.7811%;&quot;&gt;강함 (파티션 단위 병렬 확장)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 24.8837%;&quot;&gt;복잡한 라우팅&lt;/td&gt;
&lt;td style=&quot;width: 28.1191%;&quot;&gt;강함 (Exchange 기반 강력)&lt;/td&gt;
&lt;td style=&quot;width: 42.7811%;&quot;&gt;약함 &lt;span style=&quot;background-color: #f9f9f9; color: #333333; text-align: start;&quot;&gt;(Routing key 없음)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AMQP의 이해&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AMQP(Advanced Message Queuing Protocol)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMQP는 RabbitMQ가 대표적으로 사용하는 프로토콜로, (Kafka는 자체 프로토콜 사용)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메시지 브로커와 애플리케이션이 통신&lt;/b&gt;하기 위한 &lt;b&gt;표준&lt;/b&gt; &lt;b&gt;메시징 프로토콜&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;AMQP는 통신 규약이고, RabbitMQ는 그 약속을 구현한 제품&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;AMQP의 목적은 &lt;b&gt;서로 다른 시스템 간에&lt;/b&gt; (비용/기술/시간적인 측면에서) 최대한 효율적인 방법으로 &lt;b&gt;메시지를 교환&lt;/b&gt;하기 위한 MQ 프로토콜이기 때문에 아래와 같은 특징이 있다. &lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 broker들은 똑같은 방식으로 동작할 것&lt;/li&gt;
&lt;li&gt;모든 client들은 똑같은 방식으로 동작할 것&lt;/li&gt;
&lt;li&gt;네트웍상으로 전송되는 명령어들의 표준화&lt;/li&gt;
&lt;li&gt;프로그래밍 언어 중립적&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1467&quot; data-origin-height=&quot;744&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bjPjqg/dJMcafFy6z0/Qcw9jYhUgos9KN5j8VRTP1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bjPjqg/dJMcafFy6z0/Qcw9jYhUgos9KN5j8VRTP1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bjPjqg/dJMcafFy6z0/Qcw9jYhUgos9KN5j8VRTP1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbjPjqg%2FdJMcafFy6z0%2FQcw9jYhUgos9KN5j8VRTP1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1467&quot; height=&quot;744&quot; data-origin-width=&quot;1467&quot; data-origin-height=&quot;744&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Routing Model Components&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMQP의 라우팅 모델은 3개의 중요한 component 들로 구성된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt; Exchange&lt;/b&gt; : Publisher로부터 수신한 메시지를 적절한 큐 또는 다른 exchange로 분배하는 &lt;b&gt;라우터&lt;/b&gt;의 기능을 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Queue&lt;/b&gt; : 일반적으로 알고 있는 큐이다. 메모리나 디스크에 &lt;b&gt;메시지를 저장&lt;/b&gt;하고, 그것을 consumer에게 전달하는 역할을 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt; Binding&lt;/b&gt; : exchange와 큐와의 관계를 정의한 일종의 &lt;b&gt;라우팅 테이블&lt;/b&gt;이다. &lt;br /&gt;같은 큐가 여러 개의 exchange에 bind 될 수도 있고, 하나의 exchange에 여러 개의 큐가 binding 될 수 도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;515&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTF2Vq/dJMcag5ykVN/FhuM68l4rmBFjpHkMdx4v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTF2Vq/dJMcag5ykVN/FhuM68l4rmBFjpHkMdx4v0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTF2Vq/dJMcag5ykVN/FhuM68l4rmBFjpHkMdx4v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTF2Vq%2FdJMcag5ykVN%2FFhuM68l4rmBFjpHkMdx4v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1069&quot; height=&quot;515&quot; data-origin-width=&quot;1069&quot; data-origin-height=&quot;515&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;주요 용어 정리&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;736&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b5JrEp/dJMcabXsQGJ/hMLKYruX11Rz8kuQ44QOiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b5JrEp/dJMcabXsQGJ/hMLKYruX11Rz8kuQ44QOiK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b5JrEp/dJMcabXsQGJ/hMLKYruX11Rz8kuQ44QOiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb5JrEp%2FdJMcabXsQGJ%2FhMLKYruX11Rz8kuQ44QOiK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1206&quot; height=&quot;736&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;736&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;주요 개념&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Producer&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 생성하고 RabbitMQ에 전송하는 애플리케이션&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Producer는 특정 Exchange에 메시지를 전송하고 Exchange는 메시지를 라우팅하여 큐에 배치&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Exchange&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Producer로부터 받은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메시지를 큐에 전달&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;Exchange 유형
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Direct: 특정 라우팅 키와 정확히 일치하는 큐에 메시지를 전송&lt;/li&gt;
&lt;li&gt;Fanout: 모든 큐에 메시지를 브로드캐스트&lt;/li&gt;
&lt;li&gt;Topic: 라우팅 키 패턴을 기반으로 메시지를 특정 큐에 전달&lt;/li&gt;
&lt;li&gt;Headers: 메시지 헤더 속성에 따라 메시지를 라우팅&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Routing Key&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지를 전송할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;u&gt;Producer가 Exchange에 전달하는 키&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Exchange는 이 Routing Key를 참고하여 어떤 큐에 메시지를 전달할지 결정&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. Queue&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메시지를 일시적으로 저장&lt;/span&gt;하는 버퍼 역할&lt;/li&gt;
&lt;li&gt;RabbitMQ의 큐는 FIFO(First In, First Out) 방식으로 동작하며, 메시지가 소비자에게&amp;nbsp;전달될&amp;nbsp;때까지&amp;nbsp;보관&lt;/li&gt;
&lt;li&gt;각 큐는 여러 Consumer가 구독(수신)할 수 있으며, 메시지는 큐에 들어온 순서대로 전달&lt;/li&gt;
&lt;li&gt;비동기적으로 동작하며, 여러 컨슈머가 동시에 메시지를 소비할 수 있다.&lt;/li&gt;
&lt;li&gt;단, 하나의 메시지가 여러 소비자에게 중복으로 전달될수는 없다.&lt;br /&gt;(동일한 메시지를 수신하려면 Fanout Exchange 방식으로 동작해야 함)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;5. Binding&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Exchange와 큐간의 관계를 정의, 일종의 라우팅 테이블&lt;/li&gt;
&lt;li&gt;바인딩은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;메시지를 라우팅할 때&lt;span&gt;&amp;nbsp;&lt;/span&gt;어떤 조건으로 큐에 보낼지 정의하고 이를 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323; text-align: start;&quot;&gt;Binding&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;key가 사용됨&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Binding Key와 Routing Key가 일치하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;해당 큐로 메시지가 전달 (패턴 매칭 가능)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;6. Consumer&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큐에서&amp;nbsp;메시지를&amp;nbsp;가져와&amp;nbsp;처리하는&amp;nbsp;애플리케이션&lt;/li&gt;
&lt;li&gt;RabbitMQ는&amp;nbsp;여러&amp;nbsp;소비자에게&amp;nbsp;메시지를&amp;nbsp;로드&amp;nbsp;밸런싱&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있다&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;Consumer는 큐에서 메시지를 받아 처리하면 메시지에 대한 확인(ACK, acknowledgment)을 브로커에 전송&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;확인을 보내지 않으면, 브로커는 메시지를 재전송하거나 설정한 다른 Consumer에게 전달할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;7. Message Acknowledgment (메시지 확인)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가&amp;nbsp;성공적으로&amp;nbsp;처리되었음을&amp;nbsp;RabbitMQ에&amp;nbsp;알리는&amp;nbsp;과정&lt;/li&gt;
&lt;li&gt;만약 소비자가 메시지를 성공적으로 처리하지 못했다면, 메시지를 다시 큐에 넣어 다른 소비자가&amp;nbsp;처리하도록&amp;nbsp;할&amp;nbsp;수&amp;nbsp;있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;주요 프로세스&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;547&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yVppO/dJMcagR1uNS/HdsgcbL2ZKCD7QuQXAZm30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yVppO/dJMcagR1uNS/HdsgcbL2ZKCD7QuQXAZm30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yVppO/dJMcagR1uNS/HdsgcbL2ZKCD7QuQXAZm30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyVppO%2FdJMcagR1uNS%2FHdsgcbL2ZKCD7QuQXAZm30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;935&quot; height=&quot;547&quot; data-origin-width=&quot;935&quot; data-origin-height=&quot;547&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Producer가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;메시지와 Routing Key&lt;/b&gt;를 Exchange에 전송&lt;/li&gt;
&lt;li&gt;Exchange가&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Routing Key를 이용해 Binding Key가 일치&lt;/b&gt;하는 큐에 메시지 라우팅&lt;/li&gt;
&lt;li&gt;Consumer가 큐에서 메시지를 가져와 처리하고, 성공적으로 처리되었음음&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;acknowledgment로 RabbitMQ에게 알림&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;추가로 알아야 할 용어&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Prefetch Count&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;소비자가 받을 수 있는 최대 메시지 수를 설정&lt;/li&gt;
&lt;li&gt;한 번에 많은 양의 메시지를 처리하지 않도록 하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;소비자의 성능 최적화&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. Virtual Host&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RabbitMQ 서버 내의 논리적인 구획으로, &lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;사용자 권한 관리와 리소스를 격리&lt;/span&gt;하는데 사용.&lt;br /&gt;여러 애플리케이션이 독립적으로 RabbitMQ를 이용할 수 있게 해준다. (멀티태넌시 전략처럼) &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;하나의 RabbitMQ 서버 내에 여러 개의 가상 호스트를 설정하여&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;서로 다른 애플리케이션의 메시지를 격리&lt;/span&gt;할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Dead Letter Queue (DLQ)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메시지가 처리되지 못하거나 유효 기간이 지난 경우 별도의 큐로&amp;nbsp;이동하는&amp;nbsp;구조도&amp;nbsp;설정할&amp;nbsp;수&amp;nbsp;있다&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/rabbitmq-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%ED%95%9C%EB%B0%A9%EC%97%90/dashboard?cid=336022&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/RabbitMQ (메시지 브로커)</category>
      <category>amqp</category>
      <category>binding</category>
      <category>Dead Letter Queue</category>
      <category>DLQ</category>
      <category>Exchange</category>
      <category>MSA</category>
      <category>Queue</category>
      <category>RabbitMQ</category>
      <category>Routing Key</category>
      <category>비동기 아키텍처</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/216</guid>
      <comments>https://look-forest.tistory.com/216#entry216comment</comments>
      <pubDate>Tue, 17 Feb 2026 00:40:59 +0900</pubDate>
    </item>
    <item>
      <title>Kafka 장애 대비하기 (고가용성)</title>
      <link>https://look-forest.tistory.com/214</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;kafka의&amp;nbsp;고가용성(시스템이&amp;nbsp;장애&amp;nbsp;상황에서도&amp;nbsp;멈추지&amp;nbsp;않고&amp;nbsp;정상적으로&amp;nbsp;서비스를&amp;nbsp;제공할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;능력)을&amp;nbsp;확보하는&amp;nbsp;방법을&amp;nbsp;이해하려면&amp;nbsp;아래&amp;nbsp;용어들을&amp;nbsp;먼저&amp;nbsp;알고있어야&amp;nbsp;한다.&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;노드, 브로커, 컨트롤러, 클러스터, 레플리케이션&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;노드(node)와 클러스터(cluster)란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Node는 &lt;b&gt;서버 1대&lt;/b&gt;라는 의미이고, Cluster는 &lt;b&gt;여러&amp;nbsp;대의&amp;nbsp;서버가&amp;nbsp;연결되어&amp;nbsp;하나의&amp;nbsp;시스템처럼&amp;nbsp;동작하는&amp;nbsp;서버들의&amp;nbsp;집합&lt;/b&gt;이라는 뜻으로 널리 쓰인다. kafka에서 Node는 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;b&gt;카프카가 설치되어 있는 서버&lt;/b&gt;를 의미하고, 클러스터는 노드들의 묶음이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;655&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LAzoS/dJMcabiSFau/7IfSbpWZfAmWtaReq8Upgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LAzoS/dJMcabiSFau/7IfSbpWZfAmWtaReq8Upgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LAzoS/dJMcabiSFau/7IfSbpWZfAmWtaReq8Upgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLAzoS%2FdJMcabiSFau%2F7IfSbpWZfAmWtaReq8Upgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;655&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;655&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;0&quot;&gt;노드(node)&lt;/span&gt;가 고장나게 되면 메시지를 전달하는 것 자체가 막히기 때문에, 서비스 장애가 일어나게 된다. 그래서 실무에서는 최소 3대의 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;3&quot;&gt;노드(node)&lt;/span&gt;를 구성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3대의 노드로 클러스터를 구성하면, 3대의 노드들이 서로 유기적으로 작동한다. 서로 들어오는 메시지를 나눠 저장하고, 서로의 복제본을 생성해 유지할 수도 있으며, 장애 시 시스템 전체가 중단없이 작동되게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;브로커(broker),&amp;nbsp;컨트롤러(controller)란?&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;kafka 서버는 크게 &lt;b&gt;컨트롤러(controller)&lt;/b&gt;와 &lt;b&gt;브로커(broker)&lt;/b&gt;로 구성&lt;/span&gt;되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브로커(broker)란, &lt;b&gt;메시지를 저장하고 클라이언트의 요청을 처리하는 역할&lt;/b&gt;을 한다. (직원)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러(controller)란, &lt;b&gt;브로커들간의 연동과 전반적인 클러스터의 상태를 총괄&lt;/b&gt;한다. (총관리자)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로 kafka 노드에서 broker는 9092번 포트에서 실행되고, controller는 9093번 포트에서 별개의 프로세스로 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;633&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dN2nb1/dJMcaaROvU9/9m7rbtXwvKbnG6fAbOn7Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dN2nb1/dJMcaaROvU9/9m7rbtXwvKbnG6fAbOn7Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dN2nb1/dJMcaaROvU9/9m7rbtXwvKbnG6fAbOn7Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdN2nb1%2FdJMcaaROvU9%2F9m7rbtXwvKbnG6fAbOn7Tk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;633&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;633&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;레플리케이션(replication)이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka에서의 replication은, 데이터의 안정성과 가용성을 높이기 위해&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;nbsp;토픽의&amp;nbsp;파티션을&amp;nbsp;여러&amp;nbsp;노드에&amp;nbsp;복제&lt;/b&gt;&lt;/span&gt;하는&amp;nbsp;걸&amp;nbsp;의미한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레플리케이션 개수는 kafka 서버 수만큼 설정할 수 있지만, 실무에서는 레플리케이션 개수를 2나 3으로 설정해서 활용하는 편이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(한 클러스터의 노드가 7개여도 레플리케이션 개수는 2-3대 정도이다)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bLjsYC/dJMcai94Qlc/MuGdEKnSgXWUWOlfS9P240/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bLjsYC/dJMcai94Qlc/MuGdEKnSgXWUWOlfS9P240/img.png&quot; data-alt=&quot;레플리케이션 설정을 하면 email.send 의 파티션 #0 을 다른 노드에도 복사해서 저장해둔다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bLjsYC/dJMcai94Qlc/MuGdEKnSgXWUWOlfS9P240/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbLjsYC%2FdJMcai94Qlc%2FMuGdEKnSgXWUWOlfS9P240%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;667&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;667&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;레플리케이션 설정을 하면 email.send 의 파티션 #0 을 다른 노드에도 복사해서 저장해둔다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복제된 파티션들은 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;1&quot;&gt;리더 파티션(원본)&lt;/span&gt;과 &lt;span style=&quot;color: #000000;&quot; data-token-index=&quot;3&quot;&gt;팔로워 파티션(복제본)&lt;/span&gt;으로 구분된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;b&gt;리더 파티션&lt;/b&gt;은 프로듀서나 컨슈머가&lt;span style=&quot;color: #ee2323;&quot;&gt; 직접적으로 메시지를 쓰고 읽는 파티션&lt;/span&gt;이다.&lt;br /&gt;반면 팔로워 파티션은 프로듀서나 컨슈머가 직접적으로 메시지를 쓰고 읽지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;팔로워 파티션&lt;/b&gt;은 &lt;span style=&quot;color: #ee2323;&quot;&gt;리더 파티션의 메시지를 실시간으로 복제하며 유지&lt;/span&gt;한다.&lt;/li&gt;
&lt;li&gt;리더 파티션에 장애가 발생하면 팔로워 파티션이 리더 역할로 승격한다.&lt;br /&gt;이미 팔로워 파티션은 리더 파티션 내부의 메시지까지 복제해서 가지고 있으므로 정상적으로 이어서 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실습&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3대의 노드로 클러스터 구성하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현업의 구성처럼 구축하려면 각각의 EC2 인스턴스에 kafka 노드를 따로따로 설치해야 하지만, 비용 절감과 실습의 편의를 위해 하나의 EC2 인스턴스에 3개의 kafka 노드를 한꺼번에 셋팅할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Kafka 설정 수정하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server.propertie를 수정하고, server2.properties, server3.properties로 복제하여 설정을 수정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 서버의 포트는 19092, 19093, 3번 서버는 29092, 29093으로 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771244190855&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

# kafka 노드를 식별하는 ID
node.id=1

# 클러스터를 구성할 컨트롤러의 노드 주소 목록을 설정
# (추가적인 노드의 컨트롤러를 19093, 29093번 포트에 실행시킬 예정)
controller.quorum.bootstrap.servers={EC2 Public IP}:9093,{EC2 Public IP}:19093,{EC2 Public IP}:29093

# 브로커, 컨트롤러 프로세스를 실행시킬 포트를 지정
# (브로커를 PLAINTEXT, 컨트롤러를 CONTROLLER라고 지칭)
listeners=PLAINTEXT://:9092,CONTROLLER://:9093

# 외부에서 접근할 수 있는 주소
advertised.listeners=PLAINTEXT://{EC2 Public IP}:9092,CONTROLLER://{EC2 Public IP}:9093

# kafka가 데이터(kafka 설정, 브로커가 받은 메시지, 로그 등)를 저장할 디렉터리 경로 설정
log.dirs=/tmp/kafka-logs-1

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 첫 노드는 클러스터를 초기화하고, 나머지 노드는 해당 클러스터에 연결하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771244404036&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;cd .. # kafka 디렉터리로 이동

# kafka 종료하기
$ bin/kafka-server-stop.sh

# 처음 실행하는 kafka 노드는 아래 명령어로 실행
$ KAFKA_CLUSTER_ID=&quot;$(bin/kafka-storage.sh random-uuid)&quot;
$ KAFKA_CONTROLLER_ID=&quot;$(bin/kafka-storage.sh random-uuid)&quot;
$ bin/kafka-storage.sh format \
  -t $KAFKA_CLUSTER_ID \
	-c config/server.properties \
	--initial-controllers &quot;1@localhost:9093:$KAFKA_CONTROLLER_ID&quot;

# 추가로 연동시킬 kafka 노드는 아래 명령어로 실행 
# 주의 : server.properties가 아니라 server2.properties를 사용해야 한다.
$ bin/kafka-storage.sh format \
  -t $KAFKA_CLUSTER_ID \
	-c config/server2.properties \
	--no-initial-controllers

# 추가로 연동시킬 kafka 노드는 아래 명령어로 실행 
# 주의 : server.properties가 아니라 server3.properties를 사용해야 한다.
$ bin/kafka-storage.sh format \
  -t $KAFKA_CLUSTER_ID \
	-c config/server3.properties \
	--no-initial-controllers&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. &lt;span data-token-index=&quot;0&quot;&gt;kafka 노드 3대 전부 실행하기&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그 확인을 쉽게 하기 위해 EC2 창을 각각 띄워서 포그라운드로 kafka 노드를 실행시키자.&lt;/p&gt;
&lt;pre id=&quot;code_1771244797481&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# kafka 노드 3대 실행하기
$ bin/kafka-server-start.sh config/server.properties 
$ bin/kafka-server-start.sh config/server2.properties 
$ bin/kafka-server-start.sh config/server3.properties&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;kafka 노드 3대가 전부 잘 실행됐는 지 확인하기 위해 EC2 창을 새로 띄워서 아래 명령어를 입력해보자. &lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771244832123&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# kafka 노드들의 브로커 실행 확인
$ lsof -i:9092 # 노드 1의 브로커
$ lsof -i:19092 # 노드 2의 브로커
$ lsof -i:29092 # 노드 3의 브로커

# kafka 노드들의 컨트롤러 실행 확인
$ lsof -i:9093 # 노드 1의 컨트롤러
$ lsof -i:19093 # 노드 2의 컨트롤러
$ lsof -i:29093 # 노드 3의 컨트롤러&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;4. 클러스터에&amp;nbsp;컨트롤러&amp;nbsp;등록하기&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771244851210&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ bin/kafka-metadata-quorum.sh \
	--command-config config/server2.properties \
	--bootstrap-server localhost:9092 \
	add-controller
	
$ bin/kafka-metadata-quorum.sh \
	--command-config config/server3.properties \
	--bootstrap-server localhost:9092 \
	add-controller&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5. &lt;span data-token-index=&quot;0&quot;&gt;컨트롤러끼리 잘 연동됐는 지 확인하기&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1771244884674&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ bin/kafka-metadata-quorum.sh \
	--bootstrap-server localhost:9092 describe \
	--status&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;219&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ojzle/dJMcaajXAj8/6bNrrbV14qJP8yGqcKd7fK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ojzle/dJMcaajXAj8/6bNrrbV14qJP8yGqcKd7fK/img.png&quot; data-alt=&quot;위 명령어를 입력했을 때 CurrentVoters 에 3개의 컨트롤러 정보가 찍히면 클러스터에 3개의 컨트롤러가 정상적으로 잘 등록된 것이다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ojzle/dJMcaajXAj8/6bNrrbV14qJP8yGqcKd7fK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fojzle%2FdJMcaajXAj8%2F6bNrrbV14qJP8yGqcKd7fK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;219&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;219&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;위 명령어를 입력했을 때 CurrentVoters 에 3개의 컨트롤러 정보가 찍히면 클러스터에 3개의 컨트롤러가 정상적으로 잘 등록된 것이다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Kafka 서버 3대가 서로 잘 연동됐는 지 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 서버 3대가 서로 잘 연동됐는 지 확인하는 확실한 방법은, 아래와 같이 Kafka의 서버 개수만큼 토픽의 레플리케이션을 만들어보는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;676&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGzAXu/dJMcad1ZDRd/Q7tS9Ur99WXZNHvcv0AxqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGzAXu/dJMcad1ZDRd/Q7tS9Ur99WXZNHvcv0AxqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGzAXu/dJMcad1ZDRd/Q7tS9Ur99WXZNHvcv0AxqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGzAXu%2FdJMcad1ZDRd%2FQ7tS9Ur99WXZNHvcv0AxqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;676&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;676&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽을 생성하면서 레플리케이션을 생성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1771246244245&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ bin/kafka-topics.sh --bootstrap-server localhost:9092 \
    --create --topic email.send \
    --partitions 1 \
    --replication-factor 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 토픽의 세부 정보를 조회했을 때, Replicas와&amp;nbsp;Isr에&amp;nbsp;3개의&amp;nbsp;숫자(1,&amp;nbsp;2,&amp;nbsp;3)가&amp;nbsp;다&amp;nbsp;있다면&amp;nbsp;3개의&amp;nbsp;Kafka&amp;nbsp;서버가&amp;nbsp;정상적으로&amp;nbsp;잘&amp;nbsp;연동되고&amp;nbsp;있다는&amp;nbsp;뜻이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1771246330323&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 토픽 세부 정보 조회하기
$ bin/kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic email.send&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;107&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JSPby/dJMcafr07xm/rIEIsoLHkSb6lKkeGhLVcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JSPby/dJMcafr07xm/rIEIsoLHkSb6lKkeGhLVcK/img.png&quot; data-alt=&quot;포트 번호를 바꿔 다른 kafka 서버에도 토픽의 파티션이 잘 복제됐는 지 확인해보면 동일한 결과가 출력된다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JSPby/dJMcafr07xm/rIEIsoLHkSb6lKkeGhLVcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJSPby%2FdJMcafr07xm%2FrIEIsoLHkSb6lKkeGhLVcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;107&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;107&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;포트 번호를 바꿔 다른 kafka 서버에도 토픽의 파티션이 잘 복제됐는 지 확인해보면 동일한 결과가 출력된다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PartitionCount : 해당 토픽의 파티션 수&lt;/li&gt;
&lt;li&gt;ReplicationFactor : 해당 토픽의 레플리케이션 수 (원본 포함)&lt;/li&gt;
&lt;li&gt;Partition : 파티션 번호&lt;/li&gt;
&lt;li&gt;Leader : 해당 토픽의 리더 파티션을 가지고 있는 노드 id&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Replicas&lt;/b&gt; : 해당 토픽의 파티션을 &lt;b&gt;복제하기로 설정된&lt;/b&gt; 노드들의 id&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Isr&lt;/b&gt;(In-Sync Replicas) : 리더 파티션과 똑같은 상태로 &lt;b&gt;복제(동기화)가 완료된&lt;/b&gt; 노드들의 id (복제가 안됐으면 여기서 빠진다)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;팔로워 파티션에 메시지를 넣으면 어떻게 될까?&lt;/span&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리더 파티션은 프로듀서나 컨슈머가 직접적으로 메시지를 쓰고 읽는 파티션이다. 반면에 팔로워 파티션은 프로듀서나 컨슈머가 직접적으로 메시지를 쓰고 읽지 않는다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 했는데, 실제로 팔로워 파티션에는 직접 메시지를 넣을 수 없는지 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽 상세 정보를 조회해서 리더 노드의 id를 알아낸후, 팔로워 파티션에 메시지를 넣어봤다.&lt;/p&gt;
&lt;pre id=&quot;code_1771247273565&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;$ bin/kafka-console-producer.sh --bootstrap-server localhost:19092 --topic email.send
  
# 위 명령어 입력 후 넣을 메시지 내용 입력하고 Enter 누르기
follower-message-1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각 노드에서 메시지를 조회해봤는데.. &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;모든 노드에서 삽입한 메시지가 잘 조회된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;메시지가 모든 노드에 잘 복제된 것이다. 왜 그럴까?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;사실은 Kafka &lt;span style=&quot;color: #ee2323;&quot;&gt;프로듀서는 메시지를 보내기 전에 해당 파티션의 리더가 누구인지 확인하고, 자동으로 리더 파티션에 메시지를 전송&lt;/span&gt;해준다. 이게 가능한 이유는 &lt;u&gt;kafka 노드들끼리 서로 연동되어 있어서, 리더 파티션을 가진 Kafka 노드가 누군지&lt;/u&gt;에 대한 정보를 주고 받을 수 있기 때문이다. &lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리더 파티션에 장애가 발생하면 어떻게 될까?&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;리더 파티션에 장애가 발생하면 팔로워 파티션이 리더 역할(프로듀서로부터 메시지를 받고, 컨슈머가 메시지를 처리)을 대신 수행한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라고 했는데, 정말 그런지 확인해보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bgvlp/dJMcahcioKR/krkt4ilypcAJCZDeFCaQk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bgvlp/dJMcahcioKR/krkt4ilypcAJCZDeFCaQk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bgvlp/dJMcahcioKR/krkt4ilypcAJCZDeFCaQk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBgvlp%2FdJMcahcioKR%2Fkrkt4ilypcAJCZDeFCaQk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;108&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1번 노드가 리더 파티션을 가지고 있다. 그럼 1번 노드에 장애가 발생했다는 걸 가정하기 위해 1번 노드를 종료시켜보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포그라운드에서 실행 중이던 1번 노드 서버를 Ctrl + c로 종료시키면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;다른 노드의 주소로 다시 토픽 정보를 조회해보면, Leader의 값이 2로 바뀐 것을 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더 파티션에 장애가 발생해서 팔로워 파티션이 리더 역할을 대신 수행하게끔 &lt;b&gt;리더 파티션으로 승격&lt;/b&gt;된 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;54&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cH2rrS/dJMcaflf5Fq/I5UUr74XOWFky17sWpE211/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cH2rrS/dJMcaflf5Fq/I5UUr74XOWFky17sWpE211/img.png&quot; data-alt=&quot;리더 파티션이 2번 노드로 바뀌었다그&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cH2rrS/dJMcaflf5Fq/I5UUr74XOWFky17sWpE211/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcH2rrS%2FdJMcaflf5Fq%2FI5UUr74XOWFky17sWpE211%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;54&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;54&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;리더 파티션이 2번 노드로 바뀌었다그&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Isr&amp;nbsp;값을&amp;nbsp;보니&amp;nbsp;1번&amp;nbsp;노드가&amp;nbsp;빠져있고&amp;nbsp;2,&amp;nbsp;3번&amp;nbsp;노드만&amp;nbsp;있는&amp;nbsp;걸&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있다.&amp;nbsp;1번&amp;nbsp;노드가&amp;nbsp;ISR에&amp;nbsp;빠졌다는&amp;nbsp;것은&amp;nbsp;1번&amp;nbsp;노드의&amp;nbsp;네트워크가&amp;nbsp;끊겼거나,&amp;nbsp;서버에&amp;nbsp;장애가&amp;nbsp;생겼거나,&amp;nbsp;아직&amp;nbsp;리더&amp;nbsp;파티션의&amp;nbsp;데이터와&amp;nbsp;동기화가&amp;nbsp;되지&amp;nbsp;않았다는&amp;nbsp;뜻이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 입력해보면 정상적으로 push가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 죽인 1번 서버를 재기동하면, ISR에 1번이 다시 추가가 되어있고 새로 추가한 메시지도 &lt;b&gt;동기화&lt;/b&gt;되어 누락없이 조회가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbCkjQ/dJMcaiPLGFE/NtfaKBhpPZUFvA7yK0k2mK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbCkjQ/dJMcaiPLGFE/NtfaKBhpPZUFvA7yK0k2mK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbCkjQ/dJMcaiPLGFE/NtfaKBhpPZUFvA7yK0k2mK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbCkjQ%2FdJMcaiPLGFE%2FNtfaKBhpPZUFvA7yK0k2mK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;111&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 &lt;b&gt;레플리카&lt;/b&gt;로 Kafka 서버 3대를 운용함으로써, 특정 kafka 서버에 장애가 발생하더라도 시스템 전체가 중단되지 않고 지속적으로 서비스를 제공할 수 있다는 점을 알아봤다. 이러한 구성은 Kafka의&lt;b&gt; 고가용성&lt;/b&gt;(시스템이 장애 상황에서도 멈추지 않고 정상적으로 서비스를 제공할 수 있는 능력)&lt;b&gt;을&lt;/b&gt; &lt;b&gt;확보하기 위한 대표적인 방법&lt;/b&gt;으로, &lt;span style=&quot;color: #ee2323;&quot;&gt;브로커 간 복제&lt;/span&gt;를 통해 데이터 손실을 방지하고, &lt;span style=&quot;color: #ee2323;&quot;&gt;리더 파티션 장애 시에도 다른 팔로워 파티션이 자동으로 리더로 승격&lt;/span&gt;되며 kafka 서버가 지속적으로 운영될 수 있게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;※ Kafka 서버는 몇 대를 운용하는 게 좋을까?&lt;br /&gt;kafka 서버를 1대로 운용한다고 해서 서비스를 아예 운영하지 못할 정도의 치명적인 장애가 발생하는 건 아니다.&lt;br /&gt;초기 스타트업, 초기 단계 서비스, 개발/테스트 단계에서는 비용 절감이 중요하므로 1대를 추천한다.&lt;br /&gt;서비스의 안정성이 중요한 중견 기업 또는 대기업은 장애 발생 시 손실이 크므로 최소 3대 이상을 추천한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Spring Boot에 Kafka 서버 3대를 연결해서 사용하는 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer와 Consumer의 &amp;nbsp;`application.yaml` 파일에 &lt;b&gt;모든 Kafka 브로커의 주소를 등록&lt;/b&gt;하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 특정 브로커에 장애가 발생해도, 애플리케이션은 다른 사용 가능한 브로커에 연결하여 연속적으로 메시지를 처리한다.&lt;/p&gt;
&lt;pre id=&quot;code_1771249814781&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 0

spring:
  kafka:
    bootstrap-servers:
      - {Kafka 서버 IP 주소}:9092
      - {Kafka 서버 IP 주소}:19092
      - {Kafka 서버 IP 주소}:29092
    consumer:
      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
      auto-offset-reset: earliest&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;참고 자료 &amp;amp; 이미지 출처&lt;br /&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EC%8B%A4%EC%A0%84%EC%97%90%EC%84%9C-%EB%B0%94%EB%A1%9C-%EC%8D%A8%EB%A8%B9%EB%8A%94-kafka-%EC%9E%85%EB%AC%B8/dashboard?cid=338153&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;실전에서 바로 써먹는 kafka 입문&lt;/a&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>Middleware/Kafka (메시지 브로커)</category>
      <category>isr</category>
      <category>replica</category>
      <category>고가용성</category>
      <category>노드</category>
      <category>레플리케이션</category>
      <category>리더 파티션</category>
      <category>브로커</category>
      <category>컨트롤러</category>
      <category>클러스터</category>
      <category>팔로워 파티션</category>
      <author>studyHub</author>
      <guid isPermaLink="true">https://look-forest.tistory.com/214</guid>
      <comments>https://look-forest.tistory.com/214#entry214comment</comments>
      <pubDate>Mon, 16 Feb 2026 20:58:22 +0900</pubDate>
    </item>
  </channel>
</rss>